@futpib/parser 1.0.4 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/.claude/settings.local.json +24 -0
  2. package/.github/workflows/main.yml +1 -0
  3. package/build/androidPackageParser.js +30 -32
  4. package/build/arbitraryDalvikBytecode.d.ts +3 -3
  5. package/build/arbitraryDalvikBytecode.js +33 -27
  6. package/build/arbitraryDalvikExecutable.js +55 -17
  7. package/build/arbitraryJava.d.ts +31 -0
  8. package/build/arbitraryJava.js +532 -0
  9. package/build/arbitraryJavaScript.d.ts +3 -0
  10. package/build/arbitraryJavaScript.js +263 -0
  11. package/build/arbitraryJavascript.d.ts +3 -0
  12. package/build/arbitraryJavascript.js +263 -0
  13. package/build/arbitraryZig.d.ts +3 -0
  14. package/build/arbitraryZig.js +240 -0
  15. package/build/arbitraryZipStream.d.ts +1 -1
  16. package/build/arrayParser.js +72 -13
  17. package/build/backsmali.d.ts +4 -3
  18. package/build/backsmali.js +26 -6
  19. package/build/bash.d.ts +6 -1
  20. package/build/bashParser.js +414 -131
  21. package/build/bashParser.test.js +233 -0
  22. package/build/bashParserEdgeCases.test.d.ts +1 -0
  23. package/build/bashParserEdgeCases.test.js +117 -0
  24. package/build/dalvikBytecodeParser/addressConversion.d.ts +110 -0
  25. package/build/dalvikBytecodeParser/addressConversion.js +334 -0
  26. package/build/dalvikBytecodeParser/formatParsers.d.ts +7 -6
  27. package/build/dalvikBytecodeParser/formatParsers.js +13 -14
  28. package/build/dalvikBytecodeParser.d.ts +60 -31
  29. package/build/dalvikBytecodeParser.js +92 -35
  30. package/build/dalvikBytecodeParser.test-d.d.ts +1 -0
  31. package/build/dalvikBytecodeParser.test-d.js +268 -0
  32. package/build/dalvikBytecodeUnparser/formatUnparsers.d.ts +9 -8
  33. package/build/dalvikBytecodeUnparser/formatUnparsers.js +13 -12
  34. package/build/dalvikBytecodeUnparser.d.ts +2 -2
  35. package/build/dalvikBytecodeUnparser.js +23 -23
  36. package/build/dalvikBytecodeUnparser.test.js +7 -7
  37. package/build/dalvikExecutable.d.ts +3 -3
  38. package/build/dalvikExecutable.test-d.d.ts +1 -0
  39. package/build/dalvikExecutable.test-d.js +59 -0
  40. package/build/dalvikExecutableParser/typedNumbers.d.ts +18 -0
  41. package/build/dalvikExecutableParser/typedNumbers.js +3 -0
  42. package/build/dalvikExecutableParser.d.ts +2 -1
  43. package/build/dalvikExecutableParser.js +96 -77
  44. package/build/dalvikExecutableParser.test.js +24 -3
  45. package/build/dalvikExecutableParserAgainstSmaliParser.test.js +3 -0
  46. package/build/dalvikExecutableUnparser/poolScanners.d.ts +2 -2
  47. package/build/dalvikExecutableUnparser/sectionUnparsers.d.ts +3 -3
  48. package/build/dalvikExecutableUnparser/sectionUnparsers.js +26 -11
  49. package/build/dalvikExecutableUnparser.d.ts +2 -2
  50. package/build/dalvikExecutableUnparser.test.js +2 -1
  51. package/build/disjunctionParser.d.ts +5 -3
  52. package/build/disjunctionParser.js +79 -17
  53. package/build/disjunctionParser.test-d.d.ts +1 -0
  54. package/build/disjunctionParser.test-d.js +72 -0
  55. package/build/elementSwitchParser.d.ts +4 -0
  56. package/build/{exactElementSwitchParser.js → elementSwitchParser.js} +3 -4
  57. package/build/elementSwitchParser.test-d.d.ts +1 -0
  58. package/build/elementSwitchParser.test-d.js +44 -0
  59. package/build/exactSequenceParser.d.ts +4 -2
  60. package/build/exactSequenceParser.test-d.d.ts +1 -0
  61. package/build/exactSequenceParser.test-d.js +36 -0
  62. package/build/fetchCid.js +2 -66
  63. package/build/index.d.ts +4 -2
  64. package/build/index.js +3 -1
  65. package/build/index.test.js +16 -1
  66. package/build/inputReader.d.ts +10 -0
  67. package/build/inputReader.js +36 -0
  68. package/build/java.d.ts +502 -0
  69. package/build/java.js +2 -0
  70. package/build/javaKeyStoreParser.js +14 -17
  71. package/build/javaParser.d.ts +51 -0
  72. package/build/javaParser.js +1538 -0
  73. package/build/javaParser.test.d.ts +1 -0
  74. package/build/javaParser.test.js +1287 -0
  75. package/build/javaScript.d.ts +35 -0
  76. package/build/javaScript.js +1 -0
  77. package/build/javaScriptParser.d.ts +9 -0
  78. package/build/javaScriptParser.js +34 -0
  79. package/build/javaScriptUnparser.d.ts +3 -0
  80. package/build/javaScriptUnparser.js +4 -0
  81. package/build/javaScriptUnparser.test.d.ts +1 -0
  82. package/build/javaScriptUnparser.test.js +24 -0
  83. package/build/javaUnparser.d.ts +2 -0
  84. package/build/javaUnparser.js +519 -0
  85. package/build/javaUnparser.test.d.ts +1 -0
  86. package/build/javaUnparser.test.js +24 -0
  87. package/build/javascript.d.ts +35 -0
  88. package/build/javascript.js +1 -0
  89. package/build/javascriptParser.d.ts +9 -0
  90. package/build/javascriptParser.js +34 -0
  91. package/build/javascriptUnparser.d.ts +3 -0
  92. package/build/javascriptUnparser.js +4 -0
  93. package/build/javascriptUnparser.test.d.ts +1 -0
  94. package/build/javascriptUnparser.test.js +24 -0
  95. package/build/jsonParser.js +2 -12
  96. package/build/lazyMessageError.d.ts +3 -0
  97. package/build/lookaheadParser.js +60 -3
  98. package/build/negativeLookaheadParser.js +70 -11
  99. package/build/nonEmptyArrayParser.js +72 -13
  100. package/build/objectParser.d.ts +12 -0
  101. package/build/objectParser.js +31 -0
  102. package/build/objectParser.test-d.d.ts +1 -0
  103. package/build/objectParser.test-d.js +112 -0
  104. package/build/objectParser.test.d.ts +1 -0
  105. package/build/objectParser.test.js +55 -0
  106. package/build/optionalParser.js +69 -10
  107. package/build/parser.d.ts +4 -0
  108. package/build/parser.js +3 -1
  109. package/build/parser.test.js +114 -1
  110. package/build/parserConsumedSequenceParser.js +66 -7
  111. package/build/parserContext.d.ts +6 -0
  112. package/build/parserContext.js +20 -11
  113. package/build/parserError.d.ts +119 -27
  114. package/build/parserError.js +16 -8
  115. package/build/predicateElementParser.d.ts +3 -0
  116. package/build/predicateElementParser.js +10 -0
  117. package/build/regexpParser.js +33 -3
  118. package/build/regexpParser.test.js +31 -0
  119. package/build/regularExpressionParser.js +35 -15
  120. package/build/separatedArrayParser.js +73 -14
  121. package/build/separatedNonEmptyArrayParser.js +73 -14
  122. package/build/sliceBoundedParser.js +62 -5
  123. package/build/smaliParser.d.ts +7 -7
  124. package/build/smaliParser.js +185 -268
  125. package/build/smaliParser.test.js +58 -0
  126. package/build/stringEscapes.d.ts +5 -0
  127. package/build/stringEscapes.js +244 -0
  128. package/build/symbolicExpression.d.ts +29 -0
  129. package/build/symbolicExpression.js +1 -0
  130. package/build/symbolicExpressionParser.d.ts +4 -0
  131. package/build/symbolicExpressionParser.js +123 -0
  132. package/build/symbolicExpressionParser.test.d.ts +1 -0
  133. package/build/symbolicExpressionParser.test.js +289 -0
  134. package/build/terminatedArrayParser.js +113 -38
  135. package/build/terminatedArrayParser.test.js +4 -2
  136. package/build/tupleParser.d.ts +7 -15
  137. package/build/tupleParser.js +1 -0
  138. package/build/unionParser.d.ts +5 -3
  139. package/build/unionParser.js +7 -2
  140. package/build/unionParser.test-d.d.ts +1 -0
  141. package/build/unionParser.test-d.js +72 -0
  142. package/build/unionParser.test.js +10 -11
  143. package/build/zig.d.ts +280 -0
  144. package/build/zig.js +2 -0
  145. package/build/zigParser.d.ts +3 -0
  146. package/build/zigParser.js +1119 -0
  147. package/build/zigParser.test.d.ts +1 -0
  148. package/build/zigParser.test.js +1590 -0
  149. package/build/zigUnparser.d.ts +2 -0
  150. package/build/zigUnparser.js +460 -0
  151. package/build/zigUnparser.test.d.ts +1 -0
  152. package/build/zigUnparser.test.js +24 -0
  153. package/build/zipParser.js +19 -32
  154. package/build/zipUnparser.js +19 -7
  155. package/build/zipUnparser.test.js +1 -1
  156. package/node_modules-@types/s-expression/index.d.ts +5 -0
  157. package/package.json +24 -6
  158. package/src/androidPackageParser.ts +33 -60
  159. package/src/arbitraryDalvikBytecode.ts +39 -31
  160. package/src/arbitraryDalvikExecutable.ts +65 -20
  161. package/src/arbitraryJava.ts +804 -0
  162. package/src/arbitraryJavaScript.ts +410 -0
  163. package/src/arbitraryZig.ts +380 -0
  164. package/src/arrayParser.ts +1 -3
  165. package/src/backsmali.ts +35 -4
  166. package/src/bash.ts +8 -1
  167. package/src/bashParser.test.ts +396 -0
  168. package/src/bashParser.ts +564 -199
  169. package/src/dalvikBytecodeParser/addressConversion.ts +496 -0
  170. package/src/dalvikBytecodeParser/formatParsers.ts +19 -29
  171. package/src/dalvikBytecodeParser.test-d.ts +310 -0
  172. package/src/dalvikBytecodeParser.ts +194 -69
  173. package/src/dalvikBytecodeUnparser/formatUnparsers.ts +27 -26
  174. package/src/dalvikBytecodeUnparser.test.ts +7 -7
  175. package/src/dalvikBytecodeUnparser.ts +31 -30
  176. package/src/dalvikExecutable.test-d.ts +132 -0
  177. package/src/dalvikExecutable.ts +3 -3
  178. package/src/dalvikExecutableParser/typedNumbers.ts +11 -0
  179. package/src/dalvikExecutableParser.test.ts +37 -3
  180. package/src/dalvikExecutableParser.test.ts.md +163 -2
  181. package/src/dalvikExecutableParser.test.ts.snap +0 -0
  182. package/src/dalvikExecutableParser.ts +121 -139
  183. package/src/dalvikExecutableParserAgainstSmaliParser.test.ts +4 -0
  184. package/src/dalvikExecutableUnparser/poolScanners.ts +6 -6
  185. package/src/dalvikExecutableUnparser/sectionUnparsers.ts +38 -14
  186. package/src/dalvikExecutableUnparser.test.ts +3 -2
  187. package/src/dalvikExecutableUnparser.ts +4 -4
  188. package/src/disjunctionParser.test-d.ts +105 -0
  189. package/src/disjunctionParser.ts +18 -15
  190. package/src/elementSwitchParser.test-d.ts +74 -0
  191. package/src/elementSwitchParser.ts +51 -0
  192. package/src/exactSequenceParser.test-d.ts +43 -0
  193. package/src/exactSequenceParser.ts +13 -8
  194. package/src/fetchCid.ts +2 -76
  195. package/src/index.test.ts +22 -1
  196. package/src/index.ts +11 -1
  197. package/src/inputReader.ts +53 -0
  198. package/src/java.ts +708 -0
  199. package/src/javaKeyStoreParser.ts +18 -32
  200. package/src/javaParser.test.ts +1592 -0
  201. package/src/javaParser.ts +2640 -0
  202. package/src/javaScript.ts +36 -0
  203. package/src/javaScriptParser.ts +57 -0
  204. package/src/javaScriptUnparser.test.ts +37 -0
  205. package/src/javaScriptUnparser.ts +7 -0
  206. package/src/javaUnparser.test.ts +37 -0
  207. package/src/javaUnparser.ts +640 -0
  208. package/src/jsonParser.ts +6 -27
  209. package/src/lookaheadParser.ts +2 -6
  210. package/src/negativeLookaheadParser.ts +1 -3
  211. package/src/nonEmptyArrayParser.ts +1 -3
  212. package/src/objectParser.test-d.ts +152 -0
  213. package/src/objectParser.test.ts +71 -0
  214. package/src/objectParser.ts +69 -0
  215. package/src/optionalParser.ts +1 -3
  216. package/src/parser.test.ts +151 -4
  217. package/src/parser.ts +11 -1
  218. package/src/parserConsumedSequenceParser.ts +2 -4
  219. package/src/parserContext.ts +26 -11
  220. package/src/parserError.ts +17 -3
  221. package/src/predicateElementParser.ts +22 -0
  222. package/src/regexpParser.test.ts +78 -0
  223. package/src/regexpParser.ts +35 -3
  224. package/src/regularExpressionParser.ts +36 -37
  225. package/src/separatedArrayParser.ts +1 -3
  226. package/src/separatedNonEmptyArrayParser.ts +1 -3
  227. package/src/sliceBoundedParser.test.ts +2 -2
  228. package/src/sliceBoundedParser.ts +15 -19
  229. package/src/smaliParser.test.ts +64 -0
  230. package/src/smaliParser.test.ts.md +12 -12
  231. package/src/smaliParser.test.ts.snap +0 -0
  232. package/src/smaliParser.ts +246 -534
  233. package/src/stringEscapes.ts +253 -0
  234. package/src/symbolicExpression.ts +17 -0
  235. package/src/symbolicExpressionParser.test.ts +466 -0
  236. package/src/symbolicExpressionParser.ts +190 -0
  237. package/src/terminatedArrayParser.test.ts +9 -6
  238. package/src/terminatedArrayParser.ts +25 -29
  239. package/src/tupleParser.ts +21 -18
  240. package/src/unionParser.test-d.ts +105 -0
  241. package/src/unionParser.test.ts +18 -17
  242. package/src/unionParser.ts +28 -16
  243. package/src/zig.ts +411 -0
  244. package/src/zigParser.test.ts +1693 -0
  245. package/src/zigParser.ts +1745 -0
  246. package/src/zigUnparser.test.ts +37 -0
  247. package/src/zigUnparser.ts +615 -0
  248. package/src/zipParser.ts +20 -56
  249. package/src/zipUnparser.test.ts +1 -1
  250. package/src/zipUnparser.ts +22 -7
  251. package/tsconfig.json +2 -2
  252. package/build/exactElementSwitchParser.d.ts +0 -3
  253. package/src/exactElementSwitchParser.ts +0 -41
package/src/bashParser.ts CHANGED
@@ -1,15 +1,18 @@
1
1
  import { type Parser, setParserName } from './parser.js';
2
- import { createUnionParser } from './unionParser.js';
3
2
  import { createExactSequenceParser } from './exactSequenceParser.js';
3
+ import { createElementParser } from './elementParser.js';
4
+ import { createPredicateElementParser } from './predicateElementParser.js';
5
+ import { createNegativeLookaheadParser } from './negativeLookaheadParser.js';
6
+ import { createLookaheadParser } from './lookaheadParser.js';
4
7
  import { promiseCompose } from './promiseCompose.js';
5
8
  import { createTupleParser } from './tupleParser.js';
6
9
  import { createDisjunctionParser } from './disjunctionParser.js';
7
10
  import { createArrayParser } from './arrayParser.js';
8
11
  import { createParserAccessorParser } from './parserAccessorParser.js';
9
12
  import { createOptionalParser } from './optionalParser.js';
10
- import { createRegExpParser } from './regexpParser.js';
11
13
  import { createNonEmptyArrayParser } from './nonEmptyArrayParser.js';
12
14
  import { createSeparatedNonEmptyArrayParser } from './separatedNonEmptyArrayParser.js';
15
+ import { createObjectParser } from './objectParser.js';
13
16
  import {
14
17
  type BashWord,
15
18
  type BashWordPart,
@@ -17,8 +20,11 @@ import {
17
20
  type BashWordPartSingleQuoted,
18
21
  type BashWordPartDoubleQuoted,
19
22
  type BashWordPartVariable,
23
+ type BashWordPartVariableBraced,
20
24
  type BashWordPartCommandSubstitution,
21
25
  type BashWordPartBacktickSubstitution,
26
+ type BashWordPartArithmeticExpansion,
27
+ type BashWordPartProcessSubstitution,
22
28
  type BashSimpleCommand,
23
29
  type BashSubshell,
24
30
  type BashBraceGroup,
@@ -30,180 +36,495 @@ import {
30
36
  type BashCommand,
31
37
  } from './bash.js';
32
38
 
33
- // Whitespace (spaces and tabs, not newlines - those are significant)
39
+ // Character predicates
40
+ function isDigit(ch: string): boolean {
41
+ return ch >= '0' && ch <= '9';
42
+ }
43
+
44
+ function isLetter(ch: string): boolean {
45
+ return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z');
46
+ }
47
+
48
+ function isIdentStart(ch: string): boolean {
49
+ return isLetter(ch) || ch === '_';
50
+ }
51
+
52
+ function isIdentChar(ch: string): boolean {
53
+ return isIdentStart(ch) || isDigit(ch);
54
+ }
55
+
56
+ // Whitespace (spaces, tabs, and line continuations - not bare newlines which are significant)
57
+ const bashInlineWhitespaceUnitParser: Parser<string, string> = createDisjunctionParser([
58
+ promiseCompose(createExactSequenceParser(' '), () => ' '),
59
+ promiseCompose(createExactSequenceParser('\t'), () => '\t'),
60
+ promiseCompose(createExactSequenceParser('\\\n'), () => '\\\n'),
61
+ ]);
62
+
34
63
  const bashInlineWhitespaceParser: Parser<string, string> = promiseCompose(
35
- createRegExpParser(/[ \t]+/),
36
- match => match[0],
64
+ createNonEmptyArrayParser(bashInlineWhitespaceUnitParser),
65
+ parts => parts.join(''),
37
66
  );
38
67
 
39
68
  const bashOptionalInlineWhitespaceParser: Parser<string, string> = promiseCompose(
40
- createRegExpParser(/[ \t]*/),
41
- match => match[0],
69
+ createArrayParser(bashInlineWhitespaceUnitParser),
70
+ parts => parts.join(''),
42
71
  );
43
72
 
44
- // Newline
45
- const bashNewlineParser: Parser<string, string> = promiseCompose(
46
- createRegExpParser(/\n/),
47
- match => match[0],
73
+ // Word characters (unquoted, no special chars)
74
+ // Note: {} and # are excluded from the first character so brace groups and comments are parsed correctly,
75
+ // but allowed as continuation characters for mid-word braces (e.g., file.{c,h}, foo}bar) and hash (foo#bar)
76
+ const bashSpecialCharParser: Parser<unknown, string> = createDisjunctionParser(
77
+ [...' \t\n|&;<>()$`"\'\\'].map(ch => createExactSequenceParser(ch)),
78
+ );
79
+
80
+ const bashWordStartExcludeParser: Parser<unknown, string> = createDisjunctionParser([
81
+ bashSpecialCharParser,
82
+ createExactSequenceParser('{'),
83
+ createExactSequenceParser('}'),
84
+ createExactSequenceParser('#'),
85
+ ]);
86
+
87
+ const bashUnquotedWordStartCharParser: Parser<string, string> = promiseCompose(
88
+ createTupleParser([
89
+ createNegativeLookaheadParser(bashWordStartExcludeParser),
90
+ createElementParser<string>(),
91
+ ]),
92
+ ([, ch]) => ch,
93
+ );
94
+
95
+ const bashUnquotedWordContinueCharParser: Parser<string, string> = promiseCompose(
96
+ createTupleParser([
97
+ createNegativeLookaheadParser(bashSpecialCharParser),
98
+ createElementParser<string>(),
99
+ ]),
100
+ ([, ch]) => ch,
48
101
  );
49
102
 
50
- // Word characters (unquoted, no special chars)
51
- // Note: {} are excluded so brace groups are parsed correctly
52
103
  const bashUnquotedWordCharsParser: Parser<string, string> = promiseCompose(
53
- createRegExpParser(/[^\s\n|&;<>(){}$`"'\\#]+/),
54
- match => match[0],
104
+ createTupleParser([
105
+ bashUnquotedWordStartCharParser,
106
+ createArrayParser(bashUnquotedWordContinueCharParser),
107
+ ]),
108
+ ([first, rest]) => first + rest.join(''),
55
109
  );
56
110
 
111
+ // Consume characters until a given terminator, returning the accumulated string
112
+ function createUntilCharParser(terminator: string): Parser<string, string> {
113
+ return promiseCompose(
114
+ createArrayParser(promiseCompose(
115
+ createTupleParser([
116
+ createNegativeLookaheadParser(createExactSequenceParser(terminator)),
117
+ createElementParser<string>(),
118
+ ]),
119
+ ([, ch]) => ch,
120
+ )),
121
+ chars => chars.join(''),
122
+ );
123
+ }
124
+
57
125
  // Single quoted string: '...'
58
- const bashSingleQuotedParser: Parser<BashWordPartSingleQuoted, string> = promiseCompose(
126
+ const bashSingleQuotedParser: Parser<BashWordPartSingleQuoted, string> = createObjectParser({
127
+ type: 'singleQuoted' as const,
128
+ _open: createExactSequenceParser("'"),
129
+ value: createUntilCharParser("'"),
130
+ _close: createExactSequenceParser("'"),
131
+ });
132
+
133
+ // Variable name: identifiers, positional params ($0, $1...), or special params ($@, $*, $#, $?, $$, $!, $-)
134
+ const bashSpecialParams = new Set(['@', '*', '#', '?', '$', '!', '-']);
135
+
136
+ const bashIdentifierParser: Parser<string, string> = promiseCompose(
59
137
  createTupleParser([
60
- createExactSequenceParser("'"),
61
- promiseCompose(
62
- createRegExpParser(/[^']*/),
63
- match => match[0],
64
- ),
65
- createExactSequenceParser("'"),
138
+ createPredicateElementParser<string>(isIdentStart),
139
+ createArrayParser(createPredicateElementParser<string>(isIdentChar)),
66
140
  ]),
67
- ([, value]) => ({
68
- type: 'singleQuoted' as const,
69
- value,
70
- }),
141
+ ([first, rest]) => first + rest.join(''),
142
+ );
143
+
144
+ const bashDigitsParser: Parser<string, string> = promiseCompose(
145
+ createNonEmptyArrayParser(createPredicateElementParser<string>(isDigit)),
146
+ chars => chars.join(''),
71
147
  );
72
148
 
73
- // Variable name
74
- const bashVariableNameParser: Parser<string, string> = promiseCompose(
75
- createRegExpParser(/[a-zA-Z_][a-zA-Z0-9_]*|[0-9]+|[@*#?$!-]/),
76
- match => match[0],
149
+ const bashSpecialParamParser: Parser<string, string> = createPredicateElementParser<string>(
150
+ ch => bashSpecialParams.has(ch),
77
151
  );
78
152
 
153
+ const bashVariableNameParser: Parser<string, string> = createDisjunctionParser([
154
+ bashIdentifierParser,
155
+ bashDigitsParser,
156
+ bashSpecialParamParser,
157
+ ]);
158
+
79
159
  // Simple variable: $var
80
- const bashSimpleVariableParser: Parser<BashWordPartVariable, string> = promiseCompose(
160
+ const bashSimpleVariableParser: Parser<BashWordPartVariable, string> = createObjectParser({
161
+ type: 'variable' as const,
162
+ _dollar: createExactSequenceParser('$'),
163
+ name: bashVariableNameParser,
164
+ });
165
+
166
+ // Command substitution: $(...)
167
+ const bashCommandSubstitutionParser: Parser<BashWordPartCommandSubstitution, string> = createObjectParser({
168
+ type: 'commandSubstitution' as const,
169
+ _open: createExactSequenceParser('$('),
170
+ _ws1: bashOptionalInlineWhitespaceParser,
171
+ command: createParserAccessorParser(() => bashCommandParser),
172
+ _ws2: bashOptionalInlineWhitespaceParser,
173
+ _close: createExactSequenceParser(')'),
174
+ });
175
+
176
+ // Backtick substitution: `...`
177
+ const bashBacktickSubstitutionParser: Parser<BashWordPartBacktickSubstitution, string> = createObjectParser({
178
+ type: 'backtickSubstitution' as const,
179
+ _open: createExactSequenceParser('`'),
180
+ command: createParserAccessorParser(() => bashCommandParser),
181
+ _close: createExactSequenceParser('`'),
182
+ });
183
+
184
+ // Word characters for use inside ${...} operands (} excluded from continuation to not consume the closing brace)
185
+ const bashBracedVarContinueExcludeParser: Parser<unknown, string> = createDisjunctionParser([
186
+ bashSpecialCharParser,
187
+ createExactSequenceParser('{'),
188
+ createExactSequenceParser('}'),
189
+ ]);
190
+
191
+ const bashBracedVarUnquotedWordCharsParser: Parser<string, string> = promiseCompose(
81
192
  createTupleParser([
82
- createExactSequenceParser('$'),
83
- bashVariableNameParser,
193
+ bashUnquotedWordStartCharParser,
194
+ createArrayParser(promiseCompose(
195
+ createTupleParser([
196
+ createNegativeLookaheadParser(bashBracedVarContinueExcludeParser),
197
+ createElementParser<string>(),
198
+ ]),
199
+ ([, ch]) => ch,
200
+ )),
84
201
  ]),
85
- ([, name]) => ({
86
- type: 'variable' as const,
87
- name,
88
- }),
202
+ ([first, rest]) => first + rest.join(''),
89
203
  );
90
204
 
91
- // Command substitution: $(...)
92
- const bashCommandSubstitutionParser: Parser<BashWordPartCommandSubstitution, string> = promiseCompose(
205
+ const bashBracedVarLiteralWordPartParser: Parser<BashWordPartLiteral, string> = createObjectParser({
206
+ type: 'literal' as const,
207
+ value: bashBracedVarUnquotedWordCharsParser,
208
+ });
209
+
210
+ // Braced variable expansion: ${VAR} or ${VAR:-default}
211
+ const bashBracedVariableParser: Parser<BashWordPartVariableBraced, string> = createObjectParser({
212
+ type: 'variableBraced' as const,
213
+ _open: createExactSequenceParser('${'),
214
+ name: bashVariableNameParser,
215
+ operator: createOptionalParser(createDisjunctionParser([
216
+ promiseCompose(createExactSequenceParser(':-'), () => ':-'),
217
+ promiseCompose(createExactSequenceParser(':='), () => ':='),
218
+ promiseCompose(createExactSequenceParser(':+'), () => ':+'),
219
+ promiseCompose(createExactSequenceParser(':?'), () => ':?'),
220
+ promiseCompose(createExactSequenceParser('##'), () => '##'),
221
+ promiseCompose(createExactSequenceParser('%%'), () => '%%'),
222
+ promiseCompose(createExactSequenceParser('-'), () => '-'),
223
+ promiseCompose(createExactSequenceParser('='), () => '='),
224
+ promiseCompose(createExactSequenceParser('+'), () => '+'),
225
+ promiseCompose(createExactSequenceParser('?'), () => '?'),
226
+ promiseCompose(createExactSequenceParser('#'), () => '#'),
227
+ promiseCompose(createExactSequenceParser('%'), () => '%'),
228
+ ])),
229
+ operand: createOptionalParser(createParserAccessorParser(() => bashBracedVarWordParser)),
230
+ _close: createExactSequenceParser('}'),
231
+ });
232
+
233
+ // Arithmetic expansion: $((expression)) - handles nested parentheses
234
+ const bashArithmeticExpressionParser: Parser<string, string> = async (parserContext) => {
235
+ let result = '';
236
+ let depth = 0;
237
+ for (;;) {
238
+ const ch = await parserContext.peek(0);
239
+ if (ch === undefined) {
240
+ break;
241
+ }
242
+
243
+ if (ch === '(') {
244
+ depth++;
245
+ result += ch;
246
+ parserContext.skip(1);
247
+ continue;
248
+ }
249
+
250
+ if (ch === ')') {
251
+ if (depth > 0) {
252
+ depth--;
253
+ result += ch;
254
+ parserContext.skip(1);
255
+ continue;
256
+ }
257
+
258
+ // At depth 0, a ')' means we've hit the closing '))' of $((
259
+ break;
260
+ }
261
+
262
+ result += ch;
263
+ parserContext.skip(1);
264
+ }
265
+
266
+ return result;
267
+ };
268
+
269
+ const bashArithmeticExpansionParser: Parser<BashWordPartArithmeticExpansion, string> = createObjectParser({
270
+ type: 'arithmeticExpansion' as const,
271
+ _open: createExactSequenceParser('$(('),
272
+ expression: bashArithmeticExpressionParser,
273
+ _close: createExactSequenceParser('))'),
274
+ });
275
+
276
+ // ANSI-C quoting: $'...' - content can include \' escapes
277
+ // Each unit is either a backslash-escape pair or a non-quote character
278
+ const bashAnsiCContentUnitParser: Parser<string, string> = createDisjunctionParser([
279
+ // Backslash escape: \x (any char after backslash)
280
+ promiseCompose(
281
+ createTupleParser([
282
+ createExactSequenceParser('\\'),
283
+ createElementParser<string>(),
284
+ ]),
285
+ ([bs, ch]) => bs + ch,
286
+ ),
287
+ // Any character that isn't ' (and isn't \ which is handled above)
288
+ promiseCompose(
289
+ createTupleParser([
290
+ createNegativeLookaheadParser(createExactSequenceParser("'")),
291
+ createElementParser<string>(),
292
+ ]),
293
+ ([, ch]) => ch,
294
+ ),
295
+ ]);
296
+
297
+ const bashAnsiCContentParser: Parser<string, string> = promiseCompose(
298
+ createArrayParser(bashAnsiCContentUnitParser),
299
+ parts => parts.join(''),
300
+ );
301
+
302
+ const bashAnsiCQuotedParser: Parser<BashWordPartSingleQuoted, string> = createObjectParser({
303
+ type: 'singleQuoted' as const,
304
+ _prefix: createExactSequenceParser('$'),
305
+ _open: createExactSequenceParser("'"),
306
+ value: bashAnsiCContentParser,
307
+ _close: createExactSequenceParser("'"),
308
+ });
309
+
310
+ // Process substitution: <(cmd) or >(cmd)
311
+ const bashProcessSubstitutionDirectionParser: Parser<'<' | '>', string> = promiseCompose(
93
312
  createTupleParser([
94
- createExactSequenceParser('$('),
95
- bashOptionalInlineWhitespaceParser,
96
- createParserAccessorParser(() => bashCommandParser),
97
- bashOptionalInlineWhitespaceParser,
98
- createExactSequenceParser(')'),
313
+ createDisjunctionParser([
314
+ createExactSequenceParser('<' as const),
315
+ createExactSequenceParser('>' as const),
316
+ ]),
317
+ createLookaheadParser(createExactSequenceParser('(')),
99
318
  ]),
100
- ([, , command]) => ({
101
- type: 'commandSubstitution' as const,
102
- command,
103
- }),
319
+ ([dir]) => dir as '<' | '>',
104
320
  );
105
321
 
106
- // Backtick substitution: `...`
107
- const bashBacktickSubstitutionParser: Parser<BashWordPartBacktickSubstitution, string> = promiseCompose(
322
+ const bashProcessSubstitutionParser: Parser<BashWordPartProcessSubstitution, string> = createObjectParser({
323
+ type: 'processSubstitution' as const,
324
+ direction: bashProcessSubstitutionDirectionParser,
325
+ _open: createExactSequenceParser('('),
326
+ _ws1: bashOptionalInlineWhitespaceParser,
327
+ command: createParserAccessorParser(() => bashCommandParser),
328
+ _ws2: bashOptionalInlineWhitespaceParser,
329
+ _close: createExactSequenceParser(')'),
330
+ });
331
+
332
+ // Escape sequences in double quotes: \\ \$ \` \" \! \newline
333
+ const bashDoubleQuotedEscapeCharParser: Parser<string, string> = createDisjunctionParser([
334
+ createExactSequenceParser('\\'),
335
+ createExactSequenceParser('$'),
336
+ createExactSequenceParser('`'),
337
+ createExactSequenceParser('"'),
338
+ createExactSequenceParser('!'),
339
+ createExactSequenceParser('\n'),
340
+ ]);
341
+
342
+ const bashDoubleQuotedEscapeParser: Parser<BashWordPartLiteral, string> = promiseCompose(
108
343
  createTupleParser([
109
- createExactSequenceParser('`'),
110
- createParserAccessorParser(() => bashCommandParser),
111
- createExactSequenceParser('`'),
344
+ createExactSequenceParser('\\'),
345
+ bashDoubleQuotedEscapeCharParser,
112
346
  ]),
113
- ([, command]) => ({
114
- type: 'backtickSubstitution' as const,
115
- command,
116
- }),
347
+ ([, ch]) => ({ type: 'literal' as const, value: ch }),
348
+ );
349
+
350
+ // Literal text inside double quotes (no special chars)
351
+ const bashDoubleQuotedLiteralCharParser: Parser<string, string> = promiseCompose(
352
+ createTupleParser([
353
+ createNegativeLookaheadParser(createDisjunctionParser([
354
+ createExactSequenceParser('$'),
355
+ createExactSequenceParser('`'),
356
+ createExactSequenceParser('"'),
357
+ createExactSequenceParser('\\'),
358
+ ])),
359
+ createElementParser<string>(),
360
+ ]),
361
+ ([, ch]) => ch,
362
+ );
363
+
364
+ const bashDoubleQuotedLiteralParser: Parser<BashWordPartLiteral, string> = promiseCompose(
365
+ createNonEmptyArrayParser(bashDoubleQuotedLiteralCharParser),
366
+ chars => ({ type: 'literal' as const, value: chars.join('') }),
367
+ );
368
+
369
+ // Bare $ not followed by a valid expansion start
370
+ const bashBareDollarParser: Parser<BashWordPartLiteral, string> = promiseCompose(
371
+ createExactSequenceParser('$'),
372
+ () => ({ type: 'literal' as const, value: '$' }),
373
+ );
374
+
375
+ // Bare \ not followed by a recognized escape character
376
+ const bashBareBackslashParser: Parser<BashWordPartLiteral, string> = promiseCompose(
377
+ createExactSequenceParser('\\'),
378
+ () => ({ type: 'literal' as const, value: '\\' }),
117
379
  );
118
380
 
119
381
  // Double quoted string parts (inside "...")
120
382
  const bashDoubleQuotedPartParser: Parser<BashWordPart, string> = createDisjunctionParser([
383
+ bashBracedVariableParser,
384
+ bashArithmeticExpansionParser,
121
385
  bashSimpleVariableParser,
122
386
  bashCommandSubstitutionParser,
123
387
  bashBacktickSubstitutionParser,
124
- // Escape sequences in double quotes
125
- promiseCompose(
126
- createRegExpParser(/\\[\\$`"!\n]/),
127
- match => ({
128
- type: 'literal' as const,
129
- value: match[0].slice(1),
130
- }),
131
- ),
132
- // Literal text (no special chars)
133
- promiseCompose(
134
- createRegExpParser(/[^$`"\\]+/),
135
- match => ({
136
- type: 'literal' as const,
137
- value: match[0],
138
- }),
139
- ),
388
+ bashDoubleQuotedEscapeParser,
389
+ bashDoubleQuotedLiteralParser,
390
+ bashBareDollarParser,
391
+ bashBareBackslashParser,
140
392
  ]);
141
393
 
142
394
  // Double quoted string: "..."
143
- const bashDoubleQuotedParser: Parser<BashWordPartDoubleQuoted, string> = promiseCompose(
144
- createTupleParser([
145
- createExactSequenceParser('"'),
146
- createArrayParser(bashDoubleQuotedPartParser),
147
- createExactSequenceParser('"'),
148
- ]),
149
- ([, parts]) => ({
150
- type: 'doubleQuoted' as const,
151
- parts,
395
+ const bashDoubleQuotedParser: Parser<BashWordPartDoubleQuoted, string> = createObjectParser({
396
+ type: 'doubleQuoted' as const,
397
+ _open: createExactSequenceParser('"'),
398
+ parts: createArrayParser(bashDoubleQuotedPartParser),
399
+ _close: createExactSequenceParser('"'),
400
+ });
401
+
402
+ // Literal word part (unquoted)
403
+ const bashLiteralWordPartParser: Parser<BashWordPartLiteral, string> = createObjectParser({
404
+ type: 'literal' as const,
405
+ value: bashUnquotedWordCharsParser,
406
+ });
407
+
408
+ // Bare {} treated as a literal word (e.g., find -exec cmd {} \;)
409
+ const bashBraceWordPartParser: Parser<BashWordPartLiteral, string> = promiseCompose(
410
+ createExactSequenceParser('{}'),
411
+ () => ({
412
+ type: 'literal' as const,
413
+ value: '{}',
152
414
  }),
153
415
  );
154
416
 
155
- // Literal word part (unquoted)
156
- const bashLiteralWordPartParser: Parser<BashWordPartLiteral, string> = promiseCompose(
157
- bashUnquotedWordCharsParser,
158
- value => ({
417
+ // Bare { treated as a literal word part (e.g., echo {, echo {.})
418
+ // Note: } is NOT included here because it would break brace group closing
419
+ const bashOpenBraceWordPartParser: Parser<BashWordPartLiteral, string> = promiseCompose(
420
+ createExactSequenceParser('{'),
421
+ () => ({
159
422
  type: 'literal' as const,
160
- value,
423
+ value: '{',
161
424
  }),
162
425
  );
163
426
 
164
- // Escape sequence outside quotes
165
- const bashEscapeParser: Parser<BashWordPartLiteral, string> = promiseCompose(
166
- createRegExpParser(/\\./),
167
- match => ({
427
+ // Bare } treated as a literal word part (e.g., echo }, echo }hello)
428
+ const bashCloseBraceWordPartParser: Parser<BashWordPartLiteral, string> = promiseCompose(
429
+ createExactSequenceParser('}'),
430
+ () => ({
168
431
  type: 'literal' as const,
169
- value: match[0].slice(1),
432
+ value: '}',
170
433
  }),
171
434
  );
172
435
 
173
- // Word part (any part of a word)
436
+ // Escape sequence outside quotes: backslash followed by any character
437
+ const bashEscapeParser: Parser<BashWordPartLiteral, string> = promiseCompose(
438
+ createTupleParser([
439
+ createExactSequenceParser('\\'),
440
+ createElementParser<string>(),
441
+ ]),
442
+ ([, ch]) => ({ type: 'literal' as const, value: ch }),
443
+ );
444
+
445
+ // Word part for use inside ${...} operands (uses literal parser that excludes } from continuation)
446
+ const bashBracedVarWordPartParser: Parser<BashWordPart, string> = createDisjunctionParser([
447
+ bashAnsiCQuotedParser,
448
+ bashSingleQuotedParser,
449
+ bashDoubleQuotedParser,
450
+ bashBracedVariableParser,
451
+ bashArithmeticExpansionParser,
452
+ bashCommandSubstitutionParser,
453
+ bashBacktickSubstitutionParser,
454
+ bashSimpleVariableParser,
455
+ bashEscapeParser,
456
+ bashBracedVarLiteralWordPartParser,
457
+ bashBareDollarParser,
458
+ ]);
459
+
460
+ const bashBracedVarWordParser: Parser<BashWord, string> = createObjectParser({
461
+ parts: createNonEmptyArrayParser(bashBracedVarWordPartParser),
462
+ });
463
+
464
+ // Word part (any part of a word, } excluded from first position so brace groups work)
174
465
  const bashWordPartParser: Parser<BashWordPart, string> = createDisjunctionParser([
466
+ bashAnsiCQuotedParser,
175
467
  bashSingleQuotedParser,
176
468
  bashDoubleQuotedParser,
469
+ bashBracedVariableParser,
470
+ bashArithmeticExpansionParser,
177
471
  bashCommandSubstitutionParser,
178
472
  bashBacktickSubstitutionParser,
179
473
  bashSimpleVariableParser,
474
+ bashProcessSubstitutionParser,
180
475
  bashEscapeParser,
476
+ bashBraceWordPartParser,
477
+ bashOpenBraceWordPartParser,
181
478
  bashLiteralWordPartParser,
479
+ bashBareDollarParser,
480
+ ]);
481
+
482
+ // Word part including } as a starter (for argument positions where } is not reserved)
483
+ const bashArgWordPartParser: Parser<BashWordPart, string> = createDisjunctionParser([
484
+ bashAnsiCQuotedParser,
485
+ bashSingleQuotedParser,
486
+ bashDoubleQuotedParser,
487
+ bashBracedVariableParser,
488
+ bashArithmeticExpansionParser,
489
+ bashCommandSubstitutionParser,
490
+ bashBacktickSubstitutionParser,
491
+ bashSimpleVariableParser,
492
+ bashProcessSubstitutionParser,
493
+ bashEscapeParser,
494
+ bashBraceWordPartParser,
495
+ bashOpenBraceWordPartParser,
496
+ bashCloseBraceWordPartParser,
497
+ bashLiteralWordPartParser,
498
+ bashBareDollarParser,
182
499
  ]);
183
500
 
184
501
  // Word (sequence of word parts)
185
- export const bashWordParser: Parser<BashWord, string> = promiseCompose(
186
- createNonEmptyArrayParser(bashWordPartParser),
187
- parts => ({ parts }),
188
- );
502
+ export const bashWordParser: Parser<BashWord, string> = createObjectParser({
503
+ parts: createNonEmptyArrayParser(bashWordPartParser),
504
+ });
505
+
506
+ // Argument word (allows } as first character)
507
+ const bashArgWordParser: Parser<BashWord, string> = createObjectParser({
508
+ parts: createNonEmptyArrayParser(bashArgWordPartParser),
509
+ });
189
510
 
190
511
  setParserName(bashWordParser, 'bashWordParser');
191
512
 
192
- // Assignment: NAME=value or NAME=
193
- const bashAssignmentParser: Parser<BashAssignment, string> = promiseCompose(
513
+ // Assignment name: identifier followed by =
514
+ const bashAssignmentNameParser: Parser<string, string> = promiseCompose(
194
515
  createTupleParser([
195
- promiseCompose(
196
- createRegExpParser(/[a-zA-Z_][a-zA-Z0-9_]*=/),
197
- match => match[0].slice(0, -1),
198
- ),
199
- createOptionalParser(bashWordParser),
516
+ bashIdentifierParser,
517
+ createExactSequenceParser('='),
200
518
  ]),
201
- ([name, value]) => ({
202
- name,
203
- value: value ?? undefined,
204
- }),
519
+ ([name]) => name,
205
520
  );
206
521
 
522
+ // Assignment: NAME=value or NAME=
523
+ const bashAssignmentParser: Parser<BashAssignment, string> = createObjectParser({
524
+ name: bashAssignmentNameParser,
525
+ value: createOptionalParser(bashWordParser),
526
+ });
527
+
207
528
  // Redirect operators
208
529
  const bashRedirectOperatorParser: Parser<BashRedirect['operator'], string> = createDisjunctionParser([
209
530
  promiseCompose(createExactSequenceParser('>>'), () => '>>' as const),
@@ -216,28 +537,33 @@ const bashRedirectOperatorParser: Parser<BashRedirect['operator'], string> = cre
216
537
  promiseCompose(createExactSequenceParser('<'), () => '<' as const),
217
538
  ]);
218
539
 
540
+ // File descriptor number
541
+ const bashFdParser: Parser<number, string> = promiseCompose(
542
+ bashDigitsParser,
543
+ digits => Number.parseInt(digits, 10),
544
+ );
545
+
219
546
  // Redirect: [n]op word
220
- const bashRedirectParser: Parser<BashRedirect, string> = promiseCompose(
547
+ const bashRedirectParser: Parser<BashRedirect, string> = createObjectParser({
548
+ fd: createOptionalParser(bashFdParser),
549
+ operator: bashRedirectOperatorParser,
550
+ _ws: bashOptionalInlineWhitespaceParser,
551
+ target: bashWordParser,
552
+ });
553
+
554
+ // Word with optional trailing whitespace - for use in arrays
555
+ const bashWordWithWhitespaceParser: Parser<BashWord, string> = promiseCompose(
221
556
  createTupleParser([
222
- createOptionalParser(promiseCompose(
223
- createRegExpParser(/[0-9]+/),
224
- match => Number.parseInt(match[0], 10),
225
- )),
226
- bashRedirectOperatorParser,
227
- bashOptionalInlineWhitespaceParser,
228
557
  bashWordParser,
558
+ bashOptionalInlineWhitespaceParser,
229
559
  ]),
230
- ([fd, operator, , target]) => ({
231
- fd: fd ?? undefined,
232
- operator,
233
- target,
234
- }),
560
+ ([word]) => word,
235
561
  );
236
562
 
237
- // Word with optional trailing whitespace - for use in arrays
238
- const bashWordWithWhitespaceParser: Parser<BashWord, string> = promiseCompose(
563
+ // Arg word (allows }) with optional trailing whitespace
564
+ const bashArgWordWithWhitespaceParser: Parser<BashWord, string> = promiseCompose(
239
565
  createTupleParser([
240
- bashWordParser,
566
+ bashArgWordParser,
241
567
  bashOptionalInlineWhitespaceParser,
242
568
  ]),
243
569
  ([word]) => word,
@@ -252,85 +578,80 @@ const bashRedirectWithWhitespaceParser: Parser<BashRedirect, string> = promiseCo
252
578
  ([redirect]) => redirect,
253
579
  );
254
580
 
255
- // Word or redirect - for interleaved parsing in simple commands
256
- const bashWordOrRedirectParser: Parser<{ type: 'word'; word: BashWord } | { type: 'redirect'; redirect: BashRedirect }, string> = createDisjunctionParser([
257
- promiseCompose(bashRedirectWithWhitespaceParser, redirect => ({ type: 'redirect' as const, redirect })),
258
- promiseCompose(bashWordWithWhitespaceParser, word => ({ type: 'word' as const, word })),
581
+ // Word or redirect for argument position (} allowed)
582
+ const bashArgWordOrRedirectParser: Parser<{ type: 'word'; word: BashWord } | { type: 'redirect'; redirect: BashRedirect }, string> = createDisjunctionParser([
583
+ createObjectParser({ type: 'redirect' as const, redirect: bashRedirectWithWhitespaceParser }),
584
+ createObjectParser({ type: 'word' as const, word: bashArgWordWithWhitespaceParser }),
259
585
  ]);
260
586
 
261
587
  // Simple command: [assignments] [name] [args] [redirects]
262
- export const bashSimpleCommandParser: Parser<BashSimpleCommand, string> = promiseCompose(
263
- createTupleParser([
264
- // Assignments at the start
265
- createArrayParser(promiseCompose(
266
- createTupleParser([
267
- bashAssignmentParser,
268
- bashOptionalInlineWhitespaceParser,
269
- ]),
270
- ([assignment]) => assignment,
271
- )),
272
- // Command name, args, and redirects (interleaved)
273
- createArrayParser(bashWordOrRedirectParser),
274
- ]),
275
- ([assignments, items]) => {
276
- const words: BashWord[] = [];
277
- const redirects: BashRedirect[] = [];
278
-
279
- for (const item of items) {
588
+ export const bashSimpleCommandParser: Parser<BashSimpleCommand, string> = async (parserContext) => {
589
+ // Parse assignments at the start
590
+ const assignmentsParser = createArrayParser(promiseCompose(
591
+ createTupleParser([
592
+ bashAssignmentParser,
593
+ bashOptionalInlineWhitespaceParser,
594
+ ]),
595
+ ([assignment]) => assignment,
596
+ ));
597
+ const assignments = await assignmentsParser(parserContext);
598
+
599
+ // Parse leading redirects before command name
600
+ const leadingRedirectsParser = createArrayParser(bashRedirectWithWhitespaceParser);
601
+ const leadingRedirects = await leadingRedirectsParser(parserContext);
602
+
603
+ // Parse command name (} not allowed here, so brace group closing works)
604
+ const name = await createOptionalParser(bashWordWithWhitespaceParser)(parserContext);
605
+
606
+ // Only parse args if we have a command name
607
+ const args: BashWord[] = [];
608
+ const redirects: BashRedirect[] = [...leadingRedirects];
609
+
610
+ if (name !== undefined) {
611
+ const argItems = await createArrayParser(bashArgWordOrRedirectParser)(parserContext);
612
+ for (const item of argItems) {
280
613
  if (item.type === 'word') {
281
- words.push(item.word);
614
+ args.push(item.word);
282
615
  } else {
283
616
  redirects.push(item.redirect);
284
617
  }
285
618
  }
619
+ }
286
620
 
287
- const [name, ...args] = words;
288
-
289
- return {
290
- type: 'simple' as const,
291
- name,
292
- args,
293
- redirects,
294
- assignments,
295
- };
296
- },
297
- );
621
+ return {
622
+ type: 'simple' as const,
623
+ name,
624
+ args,
625
+ redirects,
626
+ assignments,
627
+ };
628
+ };
298
629
 
299
630
  setParserName(bashSimpleCommandParser, 'bashSimpleCommandParser');
300
631
 
301
632
  // Subshell: ( command )
302
- const bashSubshellParser: Parser<BashSubshell, string> = promiseCompose(
303
- createTupleParser([
304
- createExactSequenceParser('('),
305
- bashOptionalInlineWhitespaceParser,
306
- createParserAccessorParser(() => bashCommandParser),
307
- bashOptionalInlineWhitespaceParser,
308
- createExactSequenceParser(')'),
309
- ]),
310
- ([, , body]) => ({
311
- type: 'subshell' as const,
312
- body,
313
- }),
314
- );
633
+ const bashSubshellParser: Parser<BashSubshell, string> = createObjectParser({
634
+ type: 'subshell' as const,
635
+ _open: createExactSequenceParser('('),
636
+ _ws1: bashOptionalInlineWhitespaceParser,
637
+ body: createParserAccessorParser(() => bashCommandParser),
638
+ _ws2: bashOptionalInlineWhitespaceParser,
639
+ _close: createExactSequenceParser(')'),
640
+ });
315
641
 
316
642
  setParserName(bashSubshellParser, 'bashSubshellParser');
317
643
 
318
644
  // Brace group: { command; }
319
- const bashBraceGroupParser: Parser<BashBraceGroup, string> = promiseCompose(
320
- createTupleParser([
321
- createExactSequenceParser('{'),
322
- bashInlineWhitespaceParser,
323
- createParserAccessorParser(() => bashCommandParser),
324
- bashOptionalInlineWhitespaceParser,
325
- createOptionalParser(createExactSequenceParser(';')),
326
- bashOptionalInlineWhitespaceParser,
327
- createExactSequenceParser('}'),
328
- ]),
329
- ([, , body]) => ({
330
- type: 'braceGroup' as const,
331
- body,
332
- }),
333
- );
645
+ const bashBraceGroupParser: Parser<BashBraceGroup, string> = createObjectParser({
646
+ type: 'braceGroup' as const,
647
+ _open: createExactSequenceParser('{'),
648
+ _ws1: bashInlineWhitespaceParser,
649
+ body: createParserAccessorParser(() => bashCommandParser),
650
+ _ws2: bashOptionalInlineWhitespaceParser,
651
+ _semi: createOptionalParser(createExactSequenceParser(';')),
652
+ _ws3: bashOptionalInlineWhitespaceParser,
653
+ _close: createExactSequenceParser('}'),
654
+ });
334
655
 
335
656
  setParserName(bashBraceGroupParser, 'bashBraceGroupParser');
336
657
 
@@ -345,8 +666,11 @@ setParserName(bashCommandUnitParser, 'bashCommandUnitParser');
345
666
 
346
667
  // Single pipe (not ||) - matches | only when not followed by another |
347
668
  const bashSinglePipeParser: Parser<string, string> = promiseCompose(
348
- createRegExpParser(/\|(?!\|)/),
349
- match => match[0],
669
+ createTupleParser([
670
+ createExactSequenceParser('|'),
671
+ createNegativeLookaheadParser(createExactSequenceParser('|')),
672
+ ]),
673
+ () => '|',
350
674
  );
351
675
 
352
676
  // Pipeline: [!] cmd [| cmd]...
@@ -377,28 +701,63 @@ const bashPipelineParser: Parser<BashPipeline, string> = promiseCompose(
377
701
 
378
702
  setParserName(bashPipelineParser, 'bashPipelineParser');
379
703
 
704
+ // Non-newline character
705
+ const bashNonNewlineCharParser: Parser<string, string> = promiseCompose(
706
+ createTupleParser([
707
+ createNegativeLookaheadParser(createExactSequenceParser('\n')),
708
+ createElementParser<string>(),
709
+ ]),
710
+ ([, ch]) => ch,
711
+ );
712
+
713
+ // Comment: # through end of line (not consuming the newline)
714
+ const bashCommentParser: Parser<string, string> = promiseCompose(
715
+ createTupleParser([
716
+ createExactSequenceParser('#'),
717
+ createArrayParser(bashNonNewlineCharParser),
718
+ ]),
719
+ ([hash, chars]) => hash + chars.join(''),
720
+ );
721
+
722
+ // Blank line filler: whitespace, newlines, and comments
723
+ const bashBlankLineFillerParser: Parser<void, string> = promiseCompose(
724
+ createArrayParser(createDisjunctionParser([
725
+ bashInlineWhitespaceUnitParser,
726
+ promiseCompose(createExactSequenceParser('\n'), () => '\n'),
727
+ bashCommentParser,
728
+ ])),
729
+ () => {},
730
+ );
731
+
732
+ // Newline separator: consumes a newline plus any following blank lines, comments, and whitespace
733
+ // This allows multi-line scripts with blank lines and mid-script comments
734
+ const bashNewlineSeparatorParser: Parser<'\n', string> = promiseCompose(
735
+ createTupleParser([
736
+ createExactSequenceParser('\n'),
737
+ bashBlankLineFillerParser,
738
+ ]),
739
+ () => '\n' as const,
740
+ );
741
+
380
742
  // Command list separator
381
743
  const bashListSeparatorParser: Parser<'&&' | '||' | ';' | '&' | '\n', string> = createDisjunctionParser([
382
744
  promiseCompose(createExactSequenceParser('&&'), () => '&&' as const),
383
745
  promiseCompose(createExactSequenceParser('||'), () => '||' as const),
384
746
  promiseCompose(createExactSequenceParser(';'), () => ';' as const),
385
747
  promiseCompose(createExactSequenceParser('&'), () => '&' as const),
386
- promiseCompose(bashNewlineParser, () => '\n' as const),
748
+ bashNewlineSeparatorParser,
387
749
  ]);
388
750
 
389
751
  // Command list: pipeline [sep pipeline]...
390
752
  const bashCommandListParser: Parser<BashCommandList, string> = promiseCompose(
391
753
  createTupleParser([
392
754
  bashPipelineParser,
393
- createArrayParser(promiseCompose(
394
- createTupleParser([
395
- bashOptionalInlineWhitespaceParser,
396
- bashListSeparatorParser,
397
- bashOptionalInlineWhitespaceParser,
398
- bashPipelineParser,
399
- ]),
400
- ([, separator, , pipeline]) => ({ separator, pipeline }),
401
- )),
755
+ createArrayParser(createObjectParser({
756
+ _ws1: bashOptionalInlineWhitespaceParser,
757
+ separator: bashListSeparatorParser,
758
+ _ws2: bashOptionalInlineWhitespaceParser,
759
+ pipeline: bashPipelineParser,
760
+ })),
402
761
  createOptionalParser(promiseCompose(
403
762
  createTupleParser([
404
763
  bashOptionalInlineWhitespaceParser,
@@ -448,12 +807,18 @@ export const bashCommandParser: Parser<BashCommand, string> = bashCommandListPar
448
807
 
449
808
  setParserName(bashCommandParser, 'bashCommandParser');
450
809
 
451
- // Script parser (handles leading/trailing whitespace)
810
+ // Trailing whitespace/comments/blank lines at end of script
811
+ const bashTrailingWhitespaceAndCommentsParser: Parser<undefined, string> = promiseCompose(
812
+ bashBlankLineFillerParser,
813
+ () => undefined,
814
+ );
815
+
816
+ // Script parser (handles leading/trailing whitespace and comments)
452
817
  export const bashScriptParser: Parser<BashCommand, string> = promiseCompose(
453
818
  createTupleParser([
454
819
  bashOptionalInlineWhitespaceParser,
455
820
  bashCommandParser,
456
- bashOptionalInlineWhitespaceParser,
821
+ bashTrailingWhitespaceAndCommentsParser,
457
822
  ]),
458
823
  ([, command]) => command,
459
824
  );