@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
@@ -1,117 +1,368 @@
1
1
  import { setParserName } from './parser.js';
2
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';
3
7
  import { promiseCompose } from './promiseCompose.js';
4
8
  import { createTupleParser } from './tupleParser.js';
5
9
  import { createDisjunctionParser } from './disjunctionParser.js';
6
10
  import { createArrayParser } from './arrayParser.js';
7
11
  import { createParserAccessorParser } from './parserAccessorParser.js';
8
12
  import { createOptionalParser } from './optionalParser.js';
9
- import { createRegExpParser } from './regexpParser.js';
10
13
  import { createNonEmptyArrayParser } from './nonEmptyArrayParser.js';
11
14
  import { createSeparatedNonEmptyArrayParser } from './separatedNonEmptyArrayParser.js';
12
- // Whitespace (spaces and tabs, not newlines - those are significant)
13
- const bashInlineWhitespaceParser = promiseCompose(createRegExpParser(/[ \t]+/), match => match[0]);
14
- const bashOptionalInlineWhitespaceParser = promiseCompose(createRegExpParser(/[ \t]*/), match => match[0]);
15
- // Newline
16
- const bashNewlineParser = promiseCompose(createRegExpParser(/\n/), match => match[0]);
15
+ import { createObjectParser } from './objectParser.js';
16
+ // Character predicates
17
+ function isDigit(ch) {
18
+ return ch >= '0' && ch <= '9';
19
+ }
20
+ function isLetter(ch) {
21
+ return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z');
22
+ }
23
+ function isIdentStart(ch) {
24
+ return isLetter(ch) || ch === '_';
25
+ }
26
+ function isIdentChar(ch) {
27
+ return isIdentStart(ch) || isDigit(ch);
28
+ }
29
+ // Whitespace (spaces, tabs, and line continuations - not bare newlines which are significant)
30
+ const bashInlineWhitespaceUnitParser = createDisjunctionParser([
31
+ promiseCompose(createExactSequenceParser(' '), () => ' '),
32
+ promiseCompose(createExactSequenceParser('\t'), () => '\t'),
33
+ promiseCompose(createExactSequenceParser('\\\n'), () => '\\\n'),
34
+ ]);
35
+ const bashInlineWhitespaceParser = promiseCompose(createNonEmptyArrayParser(bashInlineWhitespaceUnitParser), parts => parts.join(''));
36
+ const bashOptionalInlineWhitespaceParser = promiseCompose(createArrayParser(bashInlineWhitespaceUnitParser), parts => parts.join(''));
17
37
  // Word characters (unquoted, no special chars)
18
- // Note: {} are excluded so brace groups are parsed correctly
19
- const bashUnquotedWordCharsParser = promiseCompose(createRegExpParser(/[^\s\n|&;<>(){}$`"'\\#]+/), match => match[0]);
38
+ // Note: {} and # are excluded from the first character so brace groups and comments are parsed correctly,
39
+ // but allowed as continuation characters for mid-word braces (e.g., file.{c,h}, foo}bar) and hash (foo#bar)
40
+ const bashSpecialCharParser = createDisjunctionParser([...' \t\n|&;<>()$`"\'\\'].map(ch => createExactSequenceParser(ch)));
41
+ const bashWordStartExcludeParser = createDisjunctionParser([
42
+ bashSpecialCharParser,
43
+ createExactSequenceParser('{'),
44
+ createExactSequenceParser('}'),
45
+ createExactSequenceParser('#'),
46
+ ]);
47
+ const bashUnquotedWordStartCharParser = promiseCompose(createTupleParser([
48
+ createNegativeLookaheadParser(bashWordStartExcludeParser),
49
+ createElementParser(),
50
+ ]), ([, ch]) => ch);
51
+ const bashUnquotedWordContinueCharParser = promiseCompose(createTupleParser([
52
+ createNegativeLookaheadParser(bashSpecialCharParser),
53
+ createElementParser(),
54
+ ]), ([, ch]) => ch);
55
+ const bashUnquotedWordCharsParser = promiseCompose(createTupleParser([
56
+ bashUnquotedWordStartCharParser,
57
+ createArrayParser(bashUnquotedWordContinueCharParser),
58
+ ]), ([first, rest]) => first + rest.join(''));
59
+ // Consume characters until a given terminator, returning the accumulated string
60
+ function createUntilCharParser(terminator) {
61
+ return promiseCompose(createArrayParser(promiseCompose(createTupleParser([
62
+ createNegativeLookaheadParser(createExactSequenceParser(terminator)),
63
+ createElementParser(),
64
+ ]), ([, ch]) => ch)), chars => chars.join(''));
65
+ }
20
66
  // Single quoted string: '...'
21
- const bashSingleQuotedParser = promiseCompose(createTupleParser([
22
- createExactSequenceParser("'"),
23
- promiseCompose(createRegExpParser(/[^']*/), match => match[0]),
24
- createExactSequenceParser("'"),
25
- ]), ([, value]) => ({
67
+ const bashSingleQuotedParser = createObjectParser({
26
68
  type: 'singleQuoted',
27
- value,
28
- }));
29
- // Variable name
30
- const bashVariableNameParser = promiseCompose(createRegExpParser(/[a-zA-Z_][a-zA-Z0-9_]*|[0-9]+|[@*#?$!-]/), match => match[0]);
69
+ _open: createExactSequenceParser("'"),
70
+ value: createUntilCharParser("'"),
71
+ _close: createExactSequenceParser("'"),
72
+ });
73
+ // Variable name: identifiers, positional params ($0, $1...), or special params ($@, $*, $#, $?, $$, $!, $-)
74
+ const bashSpecialParams = new Set(['@', '*', '#', '?', '$', '!', '-']);
75
+ const bashIdentifierParser = promiseCompose(createTupleParser([
76
+ createPredicateElementParser(isIdentStart),
77
+ createArrayParser(createPredicateElementParser(isIdentChar)),
78
+ ]), ([first, rest]) => first + rest.join(''));
79
+ const bashDigitsParser = promiseCompose(createNonEmptyArrayParser(createPredicateElementParser(isDigit)), chars => chars.join(''));
80
+ const bashSpecialParamParser = createPredicateElementParser(ch => bashSpecialParams.has(ch));
81
+ const bashVariableNameParser = createDisjunctionParser([
82
+ bashIdentifierParser,
83
+ bashDigitsParser,
84
+ bashSpecialParamParser,
85
+ ]);
31
86
  // Simple variable: $var
32
- const bashSimpleVariableParser = promiseCompose(createTupleParser([
33
- createExactSequenceParser('$'),
34
- bashVariableNameParser,
35
- ]), ([, name]) => ({
87
+ const bashSimpleVariableParser = createObjectParser({
36
88
  type: 'variable',
37
- name,
38
- }));
89
+ _dollar: createExactSequenceParser('$'),
90
+ name: bashVariableNameParser,
91
+ });
39
92
  // Command substitution: $(...)
40
- const bashCommandSubstitutionParser = promiseCompose(createTupleParser([
41
- createExactSequenceParser('$('),
42
- bashOptionalInlineWhitespaceParser,
43
- createParserAccessorParser(() => bashCommandParser),
44
- bashOptionalInlineWhitespaceParser,
45
- createExactSequenceParser(')'),
46
- ]), ([, , command]) => ({
93
+ const bashCommandSubstitutionParser = createObjectParser({
47
94
  type: 'commandSubstitution',
48
- command,
49
- }));
95
+ _open: createExactSequenceParser('$('),
96
+ _ws1: bashOptionalInlineWhitespaceParser,
97
+ command: createParserAccessorParser(() => bashCommandParser),
98
+ _ws2: bashOptionalInlineWhitespaceParser,
99
+ _close: createExactSequenceParser(')'),
100
+ });
50
101
  // Backtick substitution: `...`
51
- const bashBacktickSubstitutionParser = promiseCompose(createTupleParser([
52
- createExactSequenceParser('`'),
53
- createParserAccessorParser(() => bashCommandParser),
54
- createExactSequenceParser('`'),
55
- ]), ([, command]) => ({
102
+ const bashBacktickSubstitutionParser = createObjectParser({
56
103
  type: 'backtickSubstitution',
57
- command,
58
- }));
104
+ _open: createExactSequenceParser('`'),
105
+ command: createParserAccessorParser(() => bashCommandParser),
106
+ _close: createExactSequenceParser('`'),
107
+ });
108
+ // Word characters for use inside ${...} operands (} excluded from continuation to not consume the closing brace)
109
+ const bashBracedVarContinueExcludeParser = createDisjunctionParser([
110
+ bashSpecialCharParser,
111
+ createExactSequenceParser('{'),
112
+ createExactSequenceParser('}'),
113
+ ]);
114
+ const bashBracedVarUnquotedWordCharsParser = promiseCompose(createTupleParser([
115
+ bashUnquotedWordStartCharParser,
116
+ createArrayParser(promiseCompose(createTupleParser([
117
+ createNegativeLookaheadParser(bashBracedVarContinueExcludeParser),
118
+ createElementParser(),
119
+ ]), ([, ch]) => ch)),
120
+ ]), ([first, rest]) => first + rest.join(''));
121
+ const bashBracedVarLiteralWordPartParser = createObjectParser({
122
+ type: 'literal',
123
+ value: bashBracedVarUnquotedWordCharsParser,
124
+ });
125
+ // Braced variable expansion: ${VAR} or ${VAR:-default}
126
+ const bashBracedVariableParser = createObjectParser({
127
+ type: 'variableBraced',
128
+ _open: createExactSequenceParser('${'),
129
+ name: bashVariableNameParser,
130
+ operator: createOptionalParser(createDisjunctionParser([
131
+ promiseCompose(createExactSequenceParser(':-'), () => ':-'),
132
+ promiseCompose(createExactSequenceParser(':='), () => ':='),
133
+ promiseCompose(createExactSequenceParser(':+'), () => ':+'),
134
+ promiseCompose(createExactSequenceParser(':?'), () => ':?'),
135
+ promiseCompose(createExactSequenceParser('##'), () => '##'),
136
+ promiseCompose(createExactSequenceParser('%%'), () => '%%'),
137
+ promiseCompose(createExactSequenceParser('-'), () => '-'),
138
+ promiseCompose(createExactSequenceParser('='), () => '='),
139
+ promiseCompose(createExactSequenceParser('+'), () => '+'),
140
+ promiseCompose(createExactSequenceParser('?'), () => '?'),
141
+ promiseCompose(createExactSequenceParser('#'), () => '#'),
142
+ promiseCompose(createExactSequenceParser('%'), () => '%'),
143
+ ])),
144
+ operand: createOptionalParser(createParserAccessorParser(() => bashBracedVarWordParser)),
145
+ _close: createExactSequenceParser('}'),
146
+ });
147
+ // Arithmetic expansion: $((expression)) - handles nested parentheses
148
+ const bashArithmeticExpressionParser = async (parserContext) => {
149
+ let result = '';
150
+ let depth = 0;
151
+ for (;;) {
152
+ const ch = await parserContext.peek(0);
153
+ if (ch === undefined) {
154
+ break;
155
+ }
156
+ if (ch === '(') {
157
+ depth++;
158
+ result += ch;
159
+ parserContext.skip(1);
160
+ continue;
161
+ }
162
+ if (ch === ')') {
163
+ if (depth > 0) {
164
+ depth--;
165
+ result += ch;
166
+ parserContext.skip(1);
167
+ continue;
168
+ }
169
+ // At depth 0, a ')' means we've hit the closing '))' of $((
170
+ break;
171
+ }
172
+ result += ch;
173
+ parserContext.skip(1);
174
+ }
175
+ return result;
176
+ };
177
+ const bashArithmeticExpansionParser = createObjectParser({
178
+ type: 'arithmeticExpansion',
179
+ _open: createExactSequenceParser('$(('),
180
+ expression: bashArithmeticExpressionParser,
181
+ _close: createExactSequenceParser('))'),
182
+ });
183
+ // ANSI-C quoting: $'...' - content can include \' escapes
184
+ // Each unit is either a backslash-escape pair or a non-quote character
185
+ const bashAnsiCContentUnitParser = createDisjunctionParser([
186
+ // Backslash escape: \x (any char after backslash)
187
+ promiseCompose(createTupleParser([
188
+ createExactSequenceParser('\\'),
189
+ createElementParser(),
190
+ ]), ([bs, ch]) => bs + ch),
191
+ // Any character that isn't ' (and isn't \ which is handled above)
192
+ promiseCompose(createTupleParser([
193
+ createNegativeLookaheadParser(createExactSequenceParser("'")),
194
+ createElementParser(),
195
+ ]), ([, ch]) => ch),
196
+ ]);
197
+ const bashAnsiCContentParser = promiseCompose(createArrayParser(bashAnsiCContentUnitParser), parts => parts.join(''));
198
+ const bashAnsiCQuotedParser = createObjectParser({
199
+ type: 'singleQuoted',
200
+ _prefix: createExactSequenceParser('$'),
201
+ _open: createExactSequenceParser("'"),
202
+ value: bashAnsiCContentParser,
203
+ _close: createExactSequenceParser("'"),
204
+ });
205
+ // Process substitution: <(cmd) or >(cmd)
206
+ const bashProcessSubstitutionDirectionParser = promiseCompose(createTupleParser([
207
+ createDisjunctionParser([
208
+ createExactSequenceParser('<'),
209
+ createExactSequenceParser('>'),
210
+ ]),
211
+ createLookaheadParser(createExactSequenceParser('(')),
212
+ ]), ([dir]) => dir);
213
+ const bashProcessSubstitutionParser = createObjectParser({
214
+ type: 'processSubstitution',
215
+ direction: bashProcessSubstitutionDirectionParser,
216
+ _open: createExactSequenceParser('('),
217
+ _ws1: bashOptionalInlineWhitespaceParser,
218
+ command: createParserAccessorParser(() => bashCommandParser),
219
+ _ws2: bashOptionalInlineWhitespaceParser,
220
+ _close: createExactSequenceParser(')'),
221
+ });
222
+ // Escape sequences in double quotes: \\ \$ \` \" \! \newline
223
+ const bashDoubleQuotedEscapeCharParser = createDisjunctionParser([
224
+ createExactSequenceParser('\\'),
225
+ createExactSequenceParser('$'),
226
+ createExactSequenceParser('`'),
227
+ createExactSequenceParser('"'),
228
+ createExactSequenceParser('!'),
229
+ createExactSequenceParser('\n'),
230
+ ]);
231
+ const bashDoubleQuotedEscapeParser = promiseCompose(createTupleParser([
232
+ createExactSequenceParser('\\'),
233
+ bashDoubleQuotedEscapeCharParser,
234
+ ]), ([, ch]) => ({ type: 'literal', value: ch }));
235
+ // Literal text inside double quotes (no special chars)
236
+ const bashDoubleQuotedLiteralCharParser = promiseCompose(createTupleParser([
237
+ createNegativeLookaheadParser(createDisjunctionParser([
238
+ createExactSequenceParser('$'),
239
+ createExactSequenceParser('`'),
240
+ createExactSequenceParser('"'),
241
+ createExactSequenceParser('\\'),
242
+ ])),
243
+ createElementParser(),
244
+ ]), ([, ch]) => ch);
245
+ const bashDoubleQuotedLiteralParser = promiseCompose(createNonEmptyArrayParser(bashDoubleQuotedLiteralCharParser), chars => ({ type: 'literal', value: chars.join('') }));
246
+ // Bare $ not followed by a valid expansion start
247
+ const bashBareDollarParser = promiseCompose(createExactSequenceParser('$'), () => ({ type: 'literal', value: '$' }));
248
+ // Bare \ not followed by a recognized escape character
249
+ const bashBareBackslashParser = promiseCompose(createExactSequenceParser('\\'), () => ({ type: 'literal', value: '\\' }));
59
250
  // Double quoted string parts (inside "...")
60
251
  const bashDoubleQuotedPartParser = createDisjunctionParser([
252
+ bashBracedVariableParser,
253
+ bashArithmeticExpansionParser,
61
254
  bashSimpleVariableParser,
62
255
  bashCommandSubstitutionParser,
63
256
  bashBacktickSubstitutionParser,
64
- // Escape sequences in double quotes
65
- promiseCompose(createRegExpParser(/\\[\\$`"!\n]/), match => ({
66
- type: 'literal',
67
- value: match[0].slice(1),
68
- })),
69
- // Literal text (no special chars)
70
- promiseCompose(createRegExpParser(/[^$`"\\]+/), match => ({
71
- type: 'literal',
72
- value: match[0],
73
- })),
257
+ bashDoubleQuotedEscapeParser,
258
+ bashDoubleQuotedLiteralParser,
259
+ bashBareDollarParser,
260
+ bashBareBackslashParser,
74
261
  ]);
75
262
  // Double quoted string: "..."
76
- const bashDoubleQuotedParser = promiseCompose(createTupleParser([
77
- createExactSequenceParser('"'),
78
- createArrayParser(bashDoubleQuotedPartParser),
79
- createExactSequenceParser('"'),
80
- ]), ([, parts]) => ({
263
+ const bashDoubleQuotedParser = createObjectParser({
81
264
  type: 'doubleQuoted',
82
- parts,
83
- }));
265
+ _open: createExactSequenceParser('"'),
266
+ parts: createArrayParser(bashDoubleQuotedPartParser),
267
+ _close: createExactSequenceParser('"'),
268
+ });
84
269
  // Literal word part (unquoted)
85
- const bashLiteralWordPartParser = promiseCompose(bashUnquotedWordCharsParser, value => ({
270
+ const bashLiteralWordPartParser = createObjectParser({
271
+ type: 'literal',
272
+ value: bashUnquotedWordCharsParser,
273
+ });
274
+ // Bare {} treated as a literal word (e.g., find -exec cmd {} \;)
275
+ const bashBraceWordPartParser = promiseCompose(createExactSequenceParser('{}'), () => ({
86
276
  type: 'literal',
87
- value,
277
+ value: '{}',
88
278
  }));
89
- // Escape sequence outside quotes
90
- const bashEscapeParser = promiseCompose(createRegExpParser(/\\./), match => ({
279
+ // Bare { treated as a literal word part (e.g., echo {, echo {.})
280
+ // Note: } is NOT included here because it would break brace group closing
281
+ const bashOpenBraceWordPartParser = promiseCompose(createExactSequenceParser('{'), () => ({
91
282
  type: 'literal',
92
- value: match[0].slice(1),
283
+ value: '{',
93
284
  }));
94
- // Word part (any part of a word)
285
+ // Bare } treated as a literal word part (e.g., echo }, echo }hello)
286
+ const bashCloseBraceWordPartParser = promiseCompose(createExactSequenceParser('}'), () => ({
287
+ type: 'literal',
288
+ value: '}',
289
+ }));
290
+ // Escape sequence outside quotes: backslash followed by any character
291
+ const bashEscapeParser = promiseCompose(createTupleParser([
292
+ createExactSequenceParser('\\'),
293
+ createElementParser(),
294
+ ]), ([, ch]) => ({ type: 'literal', value: ch }));
295
+ // Word part for use inside ${...} operands (uses literal parser that excludes } from continuation)
296
+ const bashBracedVarWordPartParser = createDisjunctionParser([
297
+ bashAnsiCQuotedParser,
298
+ bashSingleQuotedParser,
299
+ bashDoubleQuotedParser,
300
+ bashBracedVariableParser,
301
+ bashArithmeticExpansionParser,
302
+ bashCommandSubstitutionParser,
303
+ bashBacktickSubstitutionParser,
304
+ bashSimpleVariableParser,
305
+ bashEscapeParser,
306
+ bashBracedVarLiteralWordPartParser,
307
+ bashBareDollarParser,
308
+ ]);
309
+ const bashBracedVarWordParser = createObjectParser({
310
+ parts: createNonEmptyArrayParser(bashBracedVarWordPartParser),
311
+ });
312
+ // Word part (any part of a word, } excluded from first position so brace groups work)
95
313
  const bashWordPartParser = createDisjunctionParser([
314
+ bashAnsiCQuotedParser,
315
+ bashSingleQuotedParser,
316
+ bashDoubleQuotedParser,
317
+ bashBracedVariableParser,
318
+ bashArithmeticExpansionParser,
319
+ bashCommandSubstitutionParser,
320
+ bashBacktickSubstitutionParser,
321
+ bashSimpleVariableParser,
322
+ bashProcessSubstitutionParser,
323
+ bashEscapeParser,
324
+ bashBraceWordPartParser,
325
+ bashOpenBraceWordPartParser,
326
+ bashLiteralWordPartParser,
327
+ bashBareDollarParser,
328
+ ]);
329
+ // Word part including } as a starter (for argument positions where } is not reserved)
330
+ const bashArgWordPartParser = createDisjunctionParser([
331
+ bashAnsiCQuotedParser,
96
332
  bashSingleQuotedParser,
97
333
  bashDoubleQuotedParser,
334
+ bashBracedVariableParser,
335
+ bashArithmeticExpansionParser,
98
336
  bashCommandSubstitutionParser,
99
337
  bashBacktickSubstitutionParser,
100
338
  bashSimpleVariableParser,
339
+ bashProcessSubstitutionParser,
101
340
  bashEscapeParser,
341
+ bashBraceWordPartParser,
342
+ bashOpenBraceWordPartParser,
343
+ bashCloseBraceWordPartParser,
102
344
  bashLiteralWordPartParser,
345
+ bashBareDollarParser,
103
346
  ]);
104
347
  // Word (sequence of word parts)
105
- export const bashWordParser = promiseCompose(createNonEmptyArrayParser(bashWordPartParser), parts => ({ parts }));
348
+ export const bashWordParser = createObjectParser({
349
+ parts: createNonEmptyArrayParser(bashWordPartParser),
350
+ });
351
+ // Argument word (allows } as first character)
352
+ const bashArgWordParser = createObjectParser({
353
+ parts: createNonEmptyArrayParser(bashArgWordPartParser),
354
+ });
106
355
  setParserName(bashWordParser, 'bashWordParser');
356
+ // Assignment name: identifier followed by =
357
+ const bashAssignmentNameParser = promiseCompose(createTupleParser([
358
+ bashIdentifierParser,
359
+ createExactSequenceParser('='),
360
+ ]), ([name]) => name);
107
361
  // Assignment: NAME=value or NAME=
108
- const bashAssignmentParser = promiseCompose(createTupleParser([
109
- promiseCompose(createRegExpParser(/[a-zA-Z_][a-zA-Z0-9_]*=/), match => match[0].slice(0, -1)),
110
- createOptionalParser(bashWordParser),
111
- ]), ([name, value]) => ({
112
- name,
113
- value: value ?? undefined,
114
- }));
362
+ const bashAssignmentParser = createObjectParser({
363
+ name: bashAssignmentNameParser,
364
+ value: createOptionalParser(bashWordParser),
365
+ });
115
366
  // Redirect operators
116
367
  const bashRedirectOperatorParser = createDisjunctionParser([
117
368
  promiseCompose(createExactSequenceParser('>>'), () => '>>'),
@@ -123,53 +374,62 @@ const bashRedirectOperatorParser = createDisjunctionParser([
123
374
  promiseCompose(createExactSequenceParser('<&'), () => '<&'),
124
375
  promiseCompose(createExactSequenceParser('<'), () => '<'),
125
376
  ]);
377
+ // File descriptor number
378
+ const bashFdParser = promiseCompose(bashDigitsParser, digits => Number.parseInt(digits, 10));
126
379
  // Redirect: [n]op word
127
- const bashRedirectParser = promiseCompose(createTupleParser([
128
- createOptionalParser(promiseCompose(createRegExpParser(/[0-9]+/), match => Number.parseInt(match[0], 10))),
129
- bashRedirectOperatorParser,
130
- bashOptionalInlineWhitespaceParser,
131
- bashWordParser,
132
- ]), ([fd, operator, , target]) => ({
133
- fd: fd ?? undefined,
134
- operator,
135
- target,
136
- }));
380
+ const bashRedirectParser = createObjectParser({
381
+ fd: createOptionalParser(bashFdParser),
382
+ operator: bashRedirectOperatorParser,
383
+ _ws: bashOptionalInlineWhitespaceParser,
384
+ target: bashWordParser,
385
+ });
137
386
  // Word with optional trailing whitespace - for use in arrays
138
387
  const bashWordWithWhitespaceParser = promiseCompose(createTupleParser([
139
388
  bashWordParser,
140
389
  bashOptionalInlineWhitespaceParser,
141
390
  ]), ([word]) => word);
391
+ // Arg word (allows }) with optional trailing whitespace
392
+ const bashArgWordWithWhitespaceParser = promiseCompose(createTupleParser([
393
+ bashArgWordParser,
394
+ bashOptionalInlineWhitespaceParser,
395
+ ]), ([word]) => word);
142
396
  // Redirect with optional trailing whitespace
143
397
  const bashRedirectWithWhitespaceParser = promiseCompose(createTupleParser([
144
398
  bashRedirectParser,
145
399
  bashOptionalInlineWhitespaceParser,
146
400
  ]), ([redirect]) => redirect);
147
- // Word or redirect - for interleaved parsing in simple commands
148
- const bashWordOrRedirectParser = createDisjunctionParser([
149
- promiseCompose(bashRedirectWithWhitespaceParser, redirect => ({ type: 'redirect', redirect })),
150
- promiseCompose(bashWordWithWhitespaceParser, word => ({ type: 'word', word })),
401
+ // Word or redirect for argument position (} allowed)
402
+ const bashArgWordOrRedirectParser = createDisjunctionParser([
403
+ createObjectParser({ type: 'redirect', redirect: bashRedirectWithWhitespaceParser }),
404
+ createObjectParser({ type: 'word', word: bashArgWordWithWhitespaceParser }),
151
405
  ]);
152
406
  // Simple command: [assignments] [name] [args] [redirects]
153
- export const bashSimpleCommandParser = promiseCompose(createTupleParser([
154
- // Assignments at the start
155
- createArrayParser(promiseCompose(createTupleParser([
407
+ export const bashSimpleCommandParser = async (parserContext) => {
408
+ // Parse assignments at the start
409
+ const assignmentsParser = createArrayParser(promiseCompose(createTupleParser([
156
410
  bashAssignmentParser,
157
411
  bashOptionalInlineWhitespaceParser,
158
- ]), ([assignment]) => assignment)),
159
- // Command name, args, and redirects (interleaved)
160
- createArrayParser(bashWordOrRedirectParser),
161
- ]), ([assignments, items]) => {
162
- const words = [];
163
- const redirects = [];
164
- for (const item of items) {
165
- if (item.type === 'word') {
166
- words.push(item.word);
167
- }
168
- else {
169
- redirects.push(item.redirect);
412
+ ]), ([assignment]) => assignment));
413
+ const assignments = await assignmentsParser(parserContext);
414
+ // Parse leading redirects before command name
415
+ const leadingRedirectsParser = createArrayParser(bashRedirectWithWhitespaceParser);
416
+ const leadingRedirects = await leadingRedirectsParser(parserContext);
417
+ // Parse command name (} not allowed here, so brace group closing works)
418
+ const name = await createOptionalParser(bashWordWithWhitespaceParser)(parserContext);
419
+ // Only parse args if we have a command name
420
+ const args = [];
421
+ const redirects = [...leadingRedirects];
422
+ if (name !== undefined) {
423
+ const argItems = await createArrayParser(bashArgWordOrRedirectParser)(parserContext);
424
+ for (const item of argItems) {
425
+ if (item.type === 'word') {
426
+ args.push(item.word);
427
+ }
428
+ else {
429
+ redirects.push(item.redirect);
430
+ }
170
431
  }
171
432
  }
172
- const [name, ...args] = words;
173
433
  return {
174
434
  type: 'simple',
175
435
  name,
@@ -177,33 +437,29 @@ export const bashSimpleCommandParser = promiseCompose(createTupleParser([
177
437
  redirects,
178
438
  assignments,
179
439
  };
180
- });
440
+ };
181
441
  setParserName(bashSimpleCommandParser, 'bashSimpleCommandParser');
182
442
  // Subshell: ( command )
183
- const bashSubshellParser = promiseCompose(createTupleParser([
184
- createExactSequenceParser('('),
185
- bashOptionalInlineWhitespaceParser,
186
- createParserAccessorParser(() => bashCommandParser),
187
- bashOptionalInlineWhitespaceParser,
188
- createExactSequenceParser(')'),
189
- ]), ([, , body]) => ({
443
+ const bashSubshellParser = createObjectParser({
190
444
  type: 'subshell',
191
- body,
192
- }));
445
+ _open: createExactSequenceParser('('),
446
+ _ws1: bashOptionalInlineWhitespaceParser,
447
+ body: createParserAccessorParser(() => bashCommandParser),
448
+ _ws2: bashOptionalInlineWhitespaceParser,
449
+ _close: createExactSequenceParser(')'),
450
+ });
193
451
  setParserName(bashSubshellParser, 'bashSubshellParser');
194
452
  // Brace group: { command; }
195
- const bashBraceGroupParser = promiseCompose(createTupleParser([
196
- createExactSequenceParser('{'),
197
- bashInlineWhitespaceParser,
198
- createParserAccessorParser(() => bashCommandParser),
199
- bashOptionalInlineWhitespaceParser,
200
- createOptionalParser(createExactSequenceParser(';')),
201
- bashOptionalInlineWhitespaceParser,
202
- createExactSequenceParser('}'),
203
- ]), ([, , body]) => ({
453
+ const bashBraceGroupParser = createObjectParser({
204
454
  type: 'braceGroup',
205
- body,
206
- }));
455
+ _open: createExactSequenceParser('{'),
456
+ _ws1: bashInlineWhitespaceParser,
457
+ body: createParserAccessorParser(() => bashCommandParser),
458
+ _ws2: bashOptionalInlineWhitespaceParser,
459
+ _semi: createOptionalParser(createExactSequenceParser(';')),
460
+ _ws3: bashOptionalInlineWhitespaceParser,
461
+ _close: createExactSequenceParser('}'),
462
+ });
207
463
  setParserName(bashBraceGroupParser, 'bashBraceGroupParser');
208
464
  // Command unit: simple command, subshell, or brace group
209
465
  const bashCommandUnitParser = createDisjunctionParser([
@@ -213,7 +469,10 @@ const bashCommandUnitParser = createDisjunctionParser([
213
469
  ]);
214
470
  setParserName(bashCommandUnitParser, 'bashCommandUnitParser');
215
471
  // Single pipe (not ||) - matches | only when not followed by another |
216
- const bashSinglePipeParser = promiseCompose(createRegExpParser(/\|(?!\|)/), match => match[0]);
472
+ const bashSinglePipeParser = promiseCompose(createTupleParser([
473
+ createExactSequenceParser('|'),
474
+ createNegativeLookaheadParser(createExactSequenceParser('|')),
475
+ ]), () => '|');
217
476
  // Pipeline: [!] cmd [| cmd]...
218
477
  const bashPipelineParser = promiseCompose(createTupleParser([
219
478
  createOptionalParser(promiseCompose(createTupleParser([
@@ -231,23 +490,45 @@ const bashPipelineParser = promiseCompose(createTupleParser([
231
490
  commands,
232
491
  }));
233
492
  setParserName(bashPipelineParser, 'bashPipelineParser');
493
+ // Non-newline character
494
+ const bashNonNewlineCharParser = promiseCompose(createTupleParser([
495
+ createNegativeLookaheadParser(createExactSequenceParser('\n')),
496
+ createElementParser(),
497
+ ]), ([, ch]) => ch);
498
+ // Comment: # through end of line (not consuming the newline)
499
+ const bashCommentParser = promiseCompose(createTupleParser([
500
+ createExactSequenceParser('#'),
501
+ createArrayParser(bashNonNewlineCharParser),
502
+ ]), ([hash, chars]) => hash + chars.join(''));
503
+ // Blank line filler: whitespace, newlines, and comments
504
+ const bashBlankLineFillerParser = promiseCompose(createArrayParser(createDisjunctionParser([
505
+ bashInlineWhitespaceUnitParser,
506
+ promiseCompose(createExactSequenceParser('\n'), () => '\n'),
507
+ bashCommentParser,
508
+ ])), () => { });
509
+ // Newline separator: consumes a newline plus any following blank lines, comments, and whitespace
510
+ // This allows multi-line scripts with blank lines and mid-script comments
511
+ const bashNewlineSeparatorParser = promiseCompose(createTupleParser([
512
+ createExactSequenceParser('\n'),
513
+ bashBlankLineFillerParser,
514
+ ]), () => '\n');
234
515
  // Command list separator
235
516
  const bashListSeparatorParser = createDisjunctionParser([
236
517
  promiseCompose(createExactSequenceParser('&&'), () => '&&'),
237
518
  promiseCompose(createExactSequenceParser('||'), () => '||'),
238
519
  promiseCompose(createExactSequenceParser(';'), () => ';'),
239
520
  promiseCompose(createExactSequenceParser('&'), () => '&'),
240
- promiseCompose(bashNewlineParser, () => '\n'),
521
+ bashNewlineSeparatorParser,
241
522
  ]);
242
523
  // Command list: pipeline [sep pipeline]...
243
524
  const bashCommandListParser = promiseCompose(createTupleParser([
244
525
  bashPipelineParser,
245
- createArrayParser(promiseCompose(createTupleParser([
246
- bashOptionalInlineWhitespaceParser,
247
- bashListSeparatorParser,
248
- bashOptionalInlineWhitespaceParser,
249
- bashPipelineParser,
250
- ]), ([, separator, , pipeline]) => ({ separator, pipeline }))),
526
+ createArrayParser(createObjectParser({
527
+ _ws1: bashOptionalInlineWhitespaceParser,
528
+ separator: bashListSeparatorParser,
529
+ _ws2: bashOptionalInlineWhitespaceParser,
530
+ pipeline: bashPipelineParser,
531
+ })),
251
532
  createOptionalParser(promiseCompose(createTupleParser([
252
533
  bashOptionalInlineWhitespaceParser,
253
534
  bashListSeparatorParser,
@@ -285,10 +566,12 @@ setParserName(bashCommandListParser, 'bashCommandListParser');
285
566
  // Top-level command parser
286
567
  export const bashCommandParser = bashCommandListParser;
287
568
  setParserName(bashCommandParser, 'bashCommandParser');
288
- // Script parser (handles leading/trailing whitespace)
569
+ // Trailing whitespace/comments/blank lines at end of script
570
+ const bashTrailingWhitespaceAndCommentsParser = promiseCompose(bashBlankLineFillerParser, () => undefined);
571
+ // Script parser (handles leading/trailing whitespace and comments)
289
572
  export const bashScriptParser = promiseCompose(createTupleParser([
290
573
  bashOptionalInlineWhitespaceParser,
291
574
  bashCommandParser,
292
- bashOptionalInlineWhitespaceParser,
575
+ bashTrailingWhitespaceAndCommentsParser,
293
576
  ]), ([, command]) => command);
294
577
  setParserName(bashScriptParser, 'bashScriptParser');