@cplieger/deadset-ts 8.0.0-dev.11 → 8.0.0-dev.13

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 (73) hide show
  1. package/README.md +2 -0
  2. package/THIRD_PARTY_NOTICES.md +27 -0
  3. package/dist/src/build-scripts.js +6 -13
  4. package/dist/src/commands/read.js +156 -0
  5. package/dist/src/commands/shell.js +75 -0
  6. package/dist/src/commands/tools.js +460 -0
  7. package/dist/src/configuration-files.js +11 -25
  8. package/dist/src/conformance-record.js +1 -1
  9. package/dist/src/dependencies.js +67 -115
  10. package/dist/src/load/synthesize.js +12 -5
  11. package/dist/src/manifest.js +27 -14
  12. package/dist/src/roots.js +18 -11
  13. package/dist/src/text/named-files.js +24 -3
  14. package/dist/src/text/yaml.js +464 -173
  15. package/dist/src/vendor/unbash/ansi-c.js +132 -0
  16. package/dist/src/vendor/unbash/arithmetic-parts.js +37 -0
  17. package/dist/src/vendor/unbash/arithmetic.js +591 -0
  18. package/dist/src/vendor/unbash/assignment.js +131 -0
  19. package/dist/src/vendor/unbash/chars.js +48 -0
  20. package/dist/src/vendor/unbash/command.js +41 -0
  21. package/dist/src/vendor/unbash/expansion.js +80 -0
  22. package/dist/src/vendor/unbash/glob.js +268 -0
  23. package/dist/src/vendor/unbash/heredoc.js +27 -0
  24. package/dist/src/vendor/unbash/internal-types.js +1 -0
  25. package/dist/src/vendor/unbash/lexer.js +4274 -0
  26. package/dist/src/vendor/unbash/limits.js +3 -0
  27. package/dist/src/vendor/unbash/parser.js +1366 -0
  28. package/dist/src/vendor/unbash/parts.js +57 -0
  29. package/dist/src/vendor/unbash/substitution-context.js +8 -0
  30. package/dist/src/vendor/unbash/traversal.js +212 -0
  31. package/dist/src/vendor/unbash/types.js +1 -0
  32. package/dist/src/vendor/unbash/word-value.js +61 -0
  33. package/dist/src/vendor/unbash/word.js +40 -0
  34. package/dist/src/workflow-steps.js +111 -133
  35. package/jsr.json +2 -1
  36. package/package.json +9 -7
  37. package/src/build-scripts.ts +6 -13
  38. package/src/commands/read.ts +230 -0
  39. package/src/commands/shell.ts +126 -0
  40. package/src/commands/tools.ts +618 -0
  41. package/src/configuration-files.ts +10 -26
  42. package/src/conformance-record.ts +1 -1
  43. package/src/dependencies.ts +99 -123
  44. package/src/load/synthesize.ts +18 -5
  45. package/src/manifest.ts +43 -25
  46. package/src/roots.ts +20 -13
  47. package/src/text/named-files.ts +26 -3
  48. package/src/text/yaml.ts +503 -179
  49. package/src/vendor/unbash/LICENSE +15 -0
  50. package/src/vendor/unbash/VENDORED.md +16 -0
  51. package/src/vendor/unbash/ansi-c.ts +132 -0
  52. package/src/vendor/unbash/arithmetic-parts.ts +53 -0
  53. package/src/vendor/unbash/arithmetic.ts +641 -0
  54. package/src/vendor/unbash/assignment.ts +163 -0
  55. package/src/vendor/unbash/chars.ts +48 -0
  56. package/src/vendor/unbash/command.ts +47 -0
  57. package/src/vendor/unbash/expansion.ts +144 -0
  58. package/src/vendor/unbash/glob.ts +284 -0
  59. package/src/vendor/unbash/heredoc.ts +34 -0
  60. package/src/vendor/unbash/internal-types.ts +94 -0
  61. package/src/vendor/unbash/lexer.ts +4352 -0
  62. package/src/vendor/unbash/limits.ts +3 -0
  63. package/src/vendor/unbash/parser.ts +1612 -0
  64. package/src/vendor/unbash/parts.ts +96 -0
  65. package/src/vendor/unbash/substitution-context.ts +20 -0
  66. package/src/vendor/unbash/traversal.ts +182 -0
  67. package/src/vendor/unbash/types.ts +660 -0
  68. package/src/vendor/unbash/word-value.ts +64 -0
  69. package/src/vendor/unbash/word.ts +50 -0
  70. package/src/workflow-steps.ts +153 -154
  71. package/dist/src/text/shell.js +0 -159
  72. package/src/text/named-files.inputs.json +0 -1522
  73. package/src/text/shell.ts +0 -161
@@ -0,0 +1,1366 @@
1
+ export { forEachChild } from "./traversal.js";
2
+ import { LexContext, Token, Lexer, TokenValue } from "./lexer.js";
3
+ import { MAX_SYNTAX_NESTING } from "./limits.js";
4
+ import { parseArithmeticWithParts } from "./arithmetic-parts.js";
5
+ import { computeBareWordParts, wordPartsResolver, embeddedPartsResolver } from "./parts.js";
6
+ import { createSubstitutionContext, substitutionBoundaries } from "./substitution-context.js";
7
+ import { WordImpl } from "./word.js";
8
+ import { AssignmentImpl } from "./assignment.js";
9
+ import { CommandImpl } from "./command.js";
10
+ import { HereDocBodyImpl } from "./heredoc.js";
11
+ import { ArithmeticExpansionImpl, ScriptExpansionImpl } from "./expansion.js";
12
+ Lexer._findSubstitutionEnd = findSubstitutionEnd;
13
+ ScriptExpansionImpl._resolveScript = parseRegion;
14
+ ArithmeticExpansionImpl._resolveExpression = parseArithmeticWithParts;
15
+ function isDeclarationCommand(name) {
16
+ switch (name.length) {
17
+ case 5:
18
+ return name === "local" || name === "alias";
19
+ case 6:
20
+ return name === "export";
21
+ case 7:
22
+ return name === "declare" || name === "typeset";
23
+ case 8:
24
+ return name === "readonly";
25
+ default:
26
+ return false;
27
+ }
28
+ }
29
+ class ArithmeticCommandImpl {
30
+ type = "ArithmeticCommand";
31
+ pos;
32
+ end;
33
+ body;
34
+ #source;
35
+ #depth;
36
+ #context;
37
+ #expression = null;
38
+ constructor(pos, end, body, source, depth, context) {
39
+ this.pos = pos;
40
+ this.end = end;
41
+ this.body = body;
42
+ this.#source = source;
43
+ this.#depth = depth;
44
+ this.#context = context;
45
+ }
46
+ get expression() {
47
+ if (this.#expression === null) {
48
+ this.#expression = parseArithmeticWithParts(this.body, this.pos + 2, this.#source, this.#depth, this.#context);
49
+ }
50
+ return this.#expression;
51
+ }
52
+ set expression(v) {
53
+ this.#expression = v ?? undefined;
54
+ }
55
+ toJSON() {
56
+ return {
57
+ type: this.type,
58
+ pos: this.pos,
59
+ end: this.end,
60
+ expression: this.expression,
61
+ body: this.body,
62
+ };
63
+ }
64
+ }
65
+ class ArithmeticForImpl {
66
+ type = "ArithmeticFor";
67
+ pos;
68
+ end;
69
+ body;
70
+ #initStr;
71
+ #testStr;
72
+ #updateStr;
73
+ #initPos;
74
+ #testPos;
75
+ #updatePos;
76
+ #source;
77
+ #depth;
78
+ #context;
79
+ #initialize = null;
80
+ #test = null;
81
+ #update = null;
82
+ constructor(pos, end, body, initStr, testStr, updateStr, initPos, testPos, updatePos, source, depth, context) {
83
+ this.pos = pos;
84
+ this.end = end;
85
+ this.body = body;
86
+ this.#initStr = initStr;
87
+ this.#testStr = testStr;
88
+ this.#updateStr = updateStr;
89
+ this.#initPos = initPos;
90
+ this.#testPos = testPos;
91
+ this.#updatePos = updatePos;
92
+ this.#source = source;
93
+ this.#depth = depth;
94
+ this.#context = context;
95
+ }
96
+ get initialize() {
97
+ if (this.#initialize === null) {
98
+ if (this.#initStr) {
99
+ this.#initialize = parseArithmeticWithParts(this.#initStr, this.#initPos, this.#source, this.#depth, this.#context);
100
+ }
101
+ else {
102
+ this.#initialize = undefined;
103
+ }
104
+ }
105
+ return this.#initialize;
106
+ }
107
+ set initialize(v) {
108
+ this.#initialize = v ?? undefined;
109
+ }
110
+ get test() {
111
+ if (this.#test === null) {
112
+ if (this.#testStr) {
113
+ this.#test = parseArithmeticWithParts(this.#testStr, this.#testPos, this.#source, this.#depth, this.#context);
114
+ }
115
+ else {
116
+ this.#test = undefined;
117
+ }
118
+ }
119
+ return this.#test;
120
+ }
121
+ set test(v) {
122
+ this.#test = v ?? undefined;
123
+ }
124
+ get update() {
125
+ if (this.#update === null) {
126
+ if (this.#updateStr) {
127
+ this.#update = parseArithmeticWithParts(this.#updateStr, this.#updatePos, this.#source, this.#depth, this.#context);
128
+ }
129
+ else {
130
+ this.#update = undefined;
131
+ }
132
+ }
133
+ return this.#update;
134
+ }
135
+ set update(v) {
136
+ this.#update = v ?? undefined;
137
+ }
138
+ toJSON() {
139
+ return {
140
+ type: this.type,
141
+ pos: this.pos,
142
+ end: this.end,
143
+ initialize: this.initialize,
144
+ test: this.test,
145
+ update: this.update,
146
+ body: this.body,
147
+ };
148
+ }
149
+ }
150
+ const CASE_TERMINATORS = {
151
+ [Token.DoubleSemi]: ";;",
152
+ [Token.SemiAmp]: ";&",
153
+ [Token.DoubleSemiAmp]: ";;&",
154
+ };
155
+ const REDIRECT_OPS = {
156
+ ">": ">",
157
+ ">>": ">>",
158
+ "<": "<",
159
+ "<>": "<>",
160
+ "<&": "<&",
161
+ ">&": ">&",
162
+ ">|": ">|",
163
+ "&>": "&>",
164
+ "&>>": "&>>",
165
+ };
166
+ // Lookup tables for O(1) token classification (replaces sequential comparisons)
167
+ const listTerminators = new Uint8Array(37);
168
+ listTerminators[Token.EOF] = 1;
169
+ listTerminators[Token.RParen] = 1;
170
+ listTerminators[Token.RBrace] = 1;
171
+ listTerminators[Token.Then] = 1;
172
+ listTerminators[Token.Else] = 1;
173
+ listTerminators[Token.Elif] = 1;
174
+ listTerminators[Token.Fi] = 1;
175
+ listTerminators[Token.Do] = 1;
176
+ listTerminators[Token.Done] = 1;
177
+ listTerminators[Token.Esac] = 1;
178
+ listTerminators[Token.DoubleSemi] = 1;
179
+ listTerminators[Token.SemiAmp] = 1;
180
+ listTerminators[Token.DoubleSemiAmp] = 1;
181
+ // After one of these Bash is at a command-start position, the only place a reserved-word
182
+ // terminator may follow with no separator.
183
+ const compoundClosers = new Uint8Array(37);
184
+ compoundClosers[Token.RParen] = 1;
185
+ compoundClosers[Token.RBrace] = 1;
186
+ compoundClosers[Token.DblRBracket] = 1;
187
+ compoundClosers[Token.Fi] = 1;
188
+ compoundClosers[Token.Done] = 1;
189
+ compoundClosers[Token.Esac] = 1;
190
+ compoundClosers[Token.ArithCmd] = 1;
191
+ // Inside `[[ ]]` only an unquoted `!` negates; `'!'` and `\!` are ordinary operands.
192
+ function isTestNegation(t) {
193
+ return t.token === Token.Word && t.keywordEligible && t.value === "!";
194
+ }
195
+ const commandStarts = new Uint8Array(37);
196
+ commandStarts[Token.Word] = 1;
197
+ commandStarts[Token.Assignment] = 1;
198
+ commandStarts[Token.Bang] = 1;
199
+ commandStarts[Token.LParen] = 1;
200
+ commandStarts[Token.LBrace] = 1;
201
+ commandStarts[Token.DblLBracket] = 1;
202
+ commandStarts[Token.If] = 1;
203
+ commandStarts[Token.For] = 1;
204
+ commandStarts[Token.While] = 1;
205
+ commandStarts[Token.Until] = 1;
206
+ commandStarts[Token.Case] = 1;
207
+ commandStarts[Token.Function] = 1;
208
+ commandStarts[Token.Select] = 1;
209
+ commandStarts[Token.ArithCmd] = 1;
210
+ commandStarts[Token.Coproc] = 1;
211
+ commandStarts[Token.Redirect] = 1;
212
+ const UNARY_TEST_OPS = {
213
+ "-a": 1,
214
+ "-b": 1,
215
+ "-c": 1,
216
+ "-d": 1,
217
+ "-e": 1,
218
+ "-f": 1,
219
+ "-g": 1,
220
+ "-h": 1,
221
+ "-k": 1,
222
+ "-p": 1,
223
+ "-r": 1,
224
+ "-s": 1,
225
+ "-t": 1,
226
+ "-u": 1,
227
+ "-v": 1,
228
+ "-w": 1,
229
+ "-x": 1,
230
+ "-z": 1,
231
+ "-n": 1,
232
+ "-o": 1,
233
+ "-N": 1,
234
+ "-S": 1,
235
+ "-L": 1,
236
+ "-G": 1,
237
+ "-O": 1,
238
+ "-R": 1,
239
+ };
240
+ const BINARY_TEST_OPS = {
241
+ "==": 1,
242
+ "!=": 1,
243
+ "=~": 1,
244
+ "=": 1,
245
+ "-eq": 1,
246
+ "-ne": 1,
247
+ "-lt": 1,
248
+ "-le": 1,
249
+ "-gt": 1,
250
+ "-ge": 1,
251
+ "-nt": 1,
252
+ "-ot": 1,
253
+ "-ef": 1,
254
+ "<": 1,
255
+ ">": 1,
256
+ };
257
+ const EMPTY_REDIRECTS = [];
258
+ const EMPTY_PREFIXES = [];
259
+ const EMPTY_ELIFS = [];
260
+ export function parse(source) {
261
+ return new Parser(source, 0, source.length).run();
262
+ }
263
+ // Parse a [start, end) window of `source` in place, so the resulting nodes index the original
264
+ // source directly. Used to resolve substitution scripts with absolute offsets; not public API.
265
+ export function parseRegion(source, start, end, depth = 0, parenBoundary = false, context) {
266
+ return new Parser(source, start, end, depth, parenBoundary, undefined, context).run();
267
+ }
268
+ class SubstitutionLexer extends Lexer {
269
+ boundaries;
270
+ pending;
271
+ context;
272
+ constructor(source, boundary, boundaries, pending, context, parenBoundary = true) {
273
+ super(source, boundary.start, boundary.end, parenBoundary, context);
274
+ this._nestingDepth = boundary.depth;
275
+ this.boundaries = boundaries;
276
+ this.pending = pending;
277
+ this.context = context;
278
+ }
279
+ // Keep child boundary requests on this worklist so nested scans stay iterative.
280
+ fork(start, end, parenBoundary = false) {
281
+ return new SubstitutionLexer(this.getSource(), { start, end, depth: this._nestingDepth }, this.boundaries, this.pending, this.context, parenBoundary);
282
+ }
283
+ findSubstitutionEnd(start, end, depth = this._nestingDepth + 1) {
284
+ const known = this.context.boundaries?.get(start);
285
+ if (known !== undefined)
286
+ return known <= end ? known : -1;
287
+ const boundary = this.boundaries.get(start);
288
+ if (boundary !== undefined)
289
+ return boundary <= end ? boundary : -1;
290
+ if (!this.boundaries.has(start)) {
291
+ this.boundaries.set(start, undefined);
292
+ this.pending.push({ start, end, depth });
293
+ }
294
+ return -1;
295
+ }
296
+ }
297
+ function findSubstitutionEnd(source, start, end, depth, context) {
298
+ const boundaries = new Map([[start, undefined]]);
299
+ const pending = [{ start, end, depth }];
300
+ // Resolve nested requests before retrying their enclosing boundary, without growing the JavaScript stack.
301
+ while (pending.length > 0) {
302
+ const count = pending.length;
303
+ const boundary = pending[count - 1];
304
+ const lexer = new SubstitutionLexer(source, boundary, boundaries, pending, context);
305
+ const found = new Parser(source, boundary.start, boundary.end, boundary.depth, true, lexer).findSubstitutionEnd();
306
+ if (pending.length === count) {
307
+ boundaries.set(boundary.start, found);
308
+ if (found !== -1)
309
+ substitutionBoundaries(context).set(boundary.start, found);
310
+ pending.pop();
311
+ }
312
+ }
313
+ return boundaries.get(start) ?? -1;
314
+ }
315
+ class Parser {
316
+ tok;
317
+ source;
318
+ start;
319
+ end;
320
+ depth;
321
+ errors = null;
322
+ syntaxDepth = 0;
323
+ // `depth` counts the substitution scripts (and sub-fields) enclosing this region; it
324
+ // shares the MAX_SYNTAX_NESTING budget with the lexer's lazy word-part materialization.
325
+ constructor(source, start, end, depth = 0, parenBoundary = false, lexer, context) {
326
+ this.tok = lexer ?? new Lexer(source, start, end, parenBoundary, createSubstitutionContext(context));
327
+ this.tok._nestingDepth = depth;
328
+ this.source = source;
329
+ this.start = start;
330
+ this.end = end;
331
+ this.depth = depth;
332
+ }
333
+ run() {
334
+ const start = this.start;
335
+ if (this.depth > MAX_SYNTAX_NESTING)
336
+ this.error("maximum substitution nesting depth exceeded", start);
337
+ let shebang;
338
+ if (start === 0 && this.source.charCodeAt(0) === 35 && this.source.charCodeAt(1) === 33) {
339
+ const nl = this.source.indexOf("\n");
340
+ shebang = nl === -1 ? this.source : this.source.slice(0, nl);
341
+ }
342
+ const commands = this.list();
343
+ for (;;) {
344
+ const unexpected = this.tok.peek(LexContext.CommandStart);
345
+ if (unexpected.token === Token.EOF)
346
+ break;
347
+ this.unexpectedToken(unexpected);
348
+ // `In` cannot join `listTerminators`: `list()` shares it, and `in` must not terminate a
349
+ // list inside `for`/`case`.
350
+ if (!listTerminators[unexpected.token] && unexpected.token !== Token.In)
351
+ break;
352
+ this.tok.next(LexContext.CommandStart);
353
+ let separator = this.tok.peek(LexContext.CommandStart).token;
354
+ if (separator !== Token.Semi && separator !== Token.Newline && separator !== Token.Amp)
355
+ break;
356
+ while (separator === Token.Semi || separator === Token.Newline || separator === Token.Amp) {
357
+ this.tok.next(LexContext.CommandStart);
358
+ separator = this.tok.peek(LexContext.CommandStart).token;
359
+ }
360
+ const recovered = this.list();
361
+ for (let i = 0; i < recovered.length; i++)
362
+ commands.push(recovered[i]);
363
+ }
364
+ const lexerErrors = this.tok._errors;
365
+ if (lexerErrors !== null && lexerErrors.length > 0) {
366
+ const errors = this.errors ?? (this.errors = []);
367
+ for (let i = 0; i < lexerErrors.length; i++)
368
+ errors.push(lexerErrors[i]);
369
+ }
370
+ if (this.errors !== null && this.errors.length > 1)
371
+ this.errors.sort((a, b) => a.pos - b.pos);
372
+ return { type: "Script", pos: start, end: this.end, shebang, commands, errors: this.errors ?? undefined };
373
+ }
374
+ error(message, pos) {
375
+ (this.errors ?? (this.errors = [])).push({ message, pos });
376
+ }
377
+ unexpectedToken(t) {
378
+ this.error(`unexpected token '${t.token === Token.Newline ? "newline" : t.value}'`, t.pos);
379
+ }
380
+ findSubstitutionEnd() {
381
+ this.list();
382
+ return this.acceptEnd(Token.RParen);
383
+ }
384
+ skipSemi() {
385
+ if (this.tok.peek(LexContext.Normal).token === Token.Semi)
386
+ this.tok.next(LexContext.Normal);
387
+ }
388
+ accept(token, ctx = LexContext.Normal) {
389
+ if (this.tok.peek(ctx).token === token)
390
+ return this.tok.next(ctx);
391
+ return null;
392
+ }
393
+ acceptEnd(token, ctx = LexContext.Normal) {
394
+ if (this.tok.peek(ctx).token === token)
395
+ return this.tok.next(ctx).end;
396
+ return -1;
397
+ }
398
+ skipNewlines(ctx = LexContext.Normal) {
399
+ while (this.tok.peek(ctx).token === Token.Newline)
400
+ this.tok.next(ctx);
401
+ }
402
+ doBody(hasDo) {
403
+ const body = this.list();
404
+ const emptyPos = hasDo ? this.emptyListPos(body) : -1;
405
+ if (emptyPos >= 0)
406
+ this.error("expected command after 'do'", emptyPos);
407
+ return body;
408
+ }
409
+ // Where an empty compound list stopped, or -1. An empty condition is reported only
410
+ // once `then` or `do` follows, so a missing keyword is not reported twice.
411
+ emptyListPos(commands) {
412
+ return commands.length === 0 ? this.tok.peek(LexContext.CommandStart).pos : -1;
413
+ }
414
+ makeStatement(command) {
415
+ return {
416
+ type: "Statement",
417
+ pos: command.pos,
418
+ end: command.end,
419
+ command,
420
+ background: undefined,
421
+ };
422
+ }
423
+ // list := and_or ((';' | '&' | NEWLINE) and_or)* [';' | '&' | NEWLINE]
424
+ list() {
425
+ const commands = [];
426
+ this.skipNewlines(LexContext.CommandStart);
427
+ let t = this.tok.peek(LexContext.CommandStart).token;
428
+ if (listTerminators[t] || !commandStarts[t])
429
+ return commands;
430
+ const first = this.andOr();
431
+ if (first)
432
+ commands.push(this.makeStatement(first));
433
+ for (;;) {
434
+ t = this.tok.peekFollow(compoundClosers).token;
435
+ if (t !== Token.Semi && t !== Token.Newline && t !== Token.Amp)
436
+ break;
437
+ const isBackground = t === Token.Amp;
438
+ const sepEnd = this.tok.next(LexContext.Normal).end;
439
+ if (isBackground) {
440
+ const stmt = commands[commands.length - 1];
441
+ stmt.background = true;
442
+ stmt.end = sepEnd;
443
+ }
444
+ this.skipNewlines(LexContext.CommandStart);
445
+ t = this.tok.peek(LexContext.CommandStart).token;
446
+ if (listTerminators[t] || !commandStarts[t])
447
+ break;
448
+ const node = this.andOr();
449
+ if (node)
450
+ commands.push(this.makeStatement(node));
451
+ }
452
+ return commands;
453
+ }
454
+ // and_or := pipeline (('&&' | '||') newlines pipeline)*
455
+ andOr() {
456
+ const first = this.pipeline();
457
+ if (!first)
458
+ return null;
459
+ let t = this.tok.peek(LexContext.Normal).token;
460
+ if (t !== Token.And && t !== Token.Or)
461
+ return first;
462
+ const commands = [first];
463
+ const operators = [];
464
+ do {
465
+ const operatorToken = this.tok.next(LexContext.Normal);
466
+ const operator = operatorToken.token === Token.And ? "&&" : "||";
467
+ this.skipNewlines(LexContext.CommandStart);
468
+ const next = this.pipeline();
469
+ if (!next) {
470
+ this.error(`expected command after '${operator}'`, operatorToken.end);
471
+ break;
472
+ }
473
+ operators.push(operator);
474
+ commands.push(next);
475
+ t = this.tok.peek(LexContext.Normal).token;
476
+ } while (t === Token.And || t === Token.Or);
477
+ return {
478
+ type: "AndOr",
479
+ pos: first.pos,
480
+ end: commands[commands.length - 1].end,
481
+ commands,
482
+ operators,
483
+ };
484
+ }
485
+ withRedirects(command) {
486
+ const redirects = this.collectTrailingRedirects();
487
+ if (redirects.length === 0)
488
+ return command;
489
+ return { type: "Redirected", pos: command.pos, end: redirects[redirects.length - 1].end, command, redirects };
490
+ }
491
+ pipeline() {
492
+ let prefixes;
493
+ for (;;) {
494
+ const token = this.tok.peek(LexContext.CommandStart);
495
+ const isTime = token.token === Token.Word && token.keywordEligible && token.value === "time";
496
+ if (token.token !== Token.Bang && !isTime)
497
+ break;
498
+ const prefix = this.tok.next(LexContext.CommandStart);
499
+ const pos = prefix.pos;
500
+ const keywordEnd = prefix.end;
501
+ let posix;
502
+ let endOfOptions;
503
+ if (isTime) {
504
+ let flag = this.tok.peek(LexContext.CommandStart);
505
+ if (flag.token === Token.Word && flag.keywordEligible && flag.value === "-p") {
506
+ this.tok.next(LexContext.CommandStart);
507
+ posix = { pos: flag.pos, end: flag.end };
508
+ flag = this.tok.peek(LexContext.CommandStart);
509
+ }
510
+ if (flag.token === Token.Word && flag.keywordEligible && flag.value === "--") {
511
+ this.tok.next(LexContext.CommandStart);
512
+ endOfOptions = { pos: flag.pos, end: flag.end };
513
+ }
514
+ }
515
+ if (prefixes === undefined)
516
+ prefixes = [];
517
+ prefixes.push(isTime
518
+ ? { type: "Time", pos, end: endOfOptions?.end ?? posix?.end ?? keywordEnd, keywordEnd, posix, endOfOptions }
519
+ : { type: "Negation", pos, end: keywordEnd });
520
+ }
521
+ const command = this.pipelineCommands();
522
+ if (!prefixes)
523
+ return command;
524
+ const pos = prefixes[0].pos;
525
+ if (command?.type === "Pipeline") {
526
+ command.prefixes = prefixes;
527
+ command.pos = pos;
528
+ return command;
529
+ }
530
+ return {
531
+ type: "Pipeline",
532
+ pos,
533
+ end: command ? command.end : prefixes[prefixes.length - 1].end,
534
+ prefixes,
535
+ commands: command ? [command] : [],
536
+ operators: [],
537
+ };
538
+ }
539
+ pipelineCommands() {
540
+ const first = this.command();
541
+ if (!first)
542
+ return null;
543
+ if (this.tok.peek(LexContext.Normal).token !== Token.Pipe)
544
+ return first;
545
+ const commands = [first];
546
+ const operators = [];
547
+ while (this.tok.peek(LexContext.Normal).token === Token.Pipe) {
548
+ const pipeToken = this.tok.next(LexContext.Normal);
549
+ const operator = pipeToken.value === "|&" ? "|&" : "|";
550
+ this.skipNewlines(LexContext.CommandStart);
551
+ const cmd = this.command();
552
+ if (!cmd) {
553
+ this.error(`expected command after '${operator}'`, pipeToken.end);
554
+ break;
555
+ }
556
+ operators.push(operator);
557
+ commands.push(cmd);
558
+ }
559
+ if (commands.length === 1) {
560
+ return commands[0];
561
+ }
562
+ const pipeline = {
563
+ type: "Pipeline",
564
+ pos: first.pos,
565
+ end: commands[commands.length - 1].end,
566
+ prefixes: EMPTY_PREFIXES,
567
+ commands,
568
+ operators,
569
+ };
570
+ return pipeline;
571
+ }
572
+ // command := compound_command | function_def | simple_command
573
+ command() {
574
+ let compound;
575
+ switch (this.tok.peek(LexContext.CommandStart).token) {
576
+ case Token.LParen:
577
+ compound = this.subshell();
578
+ break;
579
+ case Token.LBrace:
580
+ compound = this.braceGroup();
581
+ break;
582
+ case Token.If:
583
+ compound = this.ifClause();
584
+ break;
585
+ case Token.For:
586
+ compound = this.forClause();
587
+ break;
588
+ case Token.While:
589
+ compound = this.whileClause();
590
+ break;
591
+ case Token.Until:
592
+ compound = this.untilClause();
593
+ break;
594
+ case Token.Case:
595
+ compound = this.caseClause();
596
+ break;
597
+ case Token.Function:
598
+ return this.functionDef();
599
+ case Token.Select:
600
+ compound = this.selectClause();
601
+ break;
602
+ case Token.DblLBracket:
603
+ compound = this.testCommand();
604
+ break;
605
+ case Token.ArithCmd:
606
+ compound = this.arithCommand();
607
+ break;
608
+ case Token.Coproc:
609
+ return this.coprocCommand();
610
+ case Token.Word:
611
+ case Token.Assignment:
612
+ case Token.Redirect:
613
+ return this.simpleCommandOrFunction(false);
614
+ default:
615
+ return null;
616
+ }
617
+ return this.withRedirects(compound);
618
+ }
619
+ collectTrailingRedirects() {
620
+ let redirects = EMPTY_REDIRECTS;
621
+ while (this.tok.peekFollow(compoundClosers).token === Token.Redirect) {
622
+ if (redirects === EMPTY_REDIRECTS)
623
+ redirects = [];
624
+ redirects.push(this.readRedirect(LexContext.Normal));
625
+ }
626
+ return redirects;
627
+ }
628
+ // arith_command := (( expr ))
629
+ arithCommand() {
630
+ const tok = this.tok.next(LexContext.CommandStart);
631
+ return new ArithmeticCommandImpl(tok.pos, tok.end, tok.value, this.source, this.depth, this.tok.getSubstitutionContext());
632
+ }
633
+ // coproc := COPROC ([name] compound_command [redirections] | simple_command)
634
+ coprocCommand() {
635
+ const start = this.tok.next(LexContext.CommandStart);
636
+ const pos = start.pos;
637
+ const first = this.tok.peek(LexContext.CommandStart);
638
+ let name;
639
+ // Only a compound command can follow a coprocess name. Decide before parsing
640
+ // a simple command so its assignments and reserved words use the actual name.
641
+ if (first.token === Token.Word) {
642
+ const lookahead = this.tok.fork(first.end, this.end);
643
+ switch (lookahead.peek(LexContext.CommandStart).token) {
644
+ case Token.LParen:
645
+ // `name ( )` opens a function definition, not a subshell body after a name.
646
+ lookahead.next(LexContext.CommandStart);
647
+ if (lookahead.peek(LexContext.Normal).token !== Token.RParen)
648
+ name = this.readWord(LexContext.CommandStart);
649
+ break;
650
+ case Token.LBrace:
651
+ case Token.If:
652
+ case Token.For:
653
+ case Token.While:
654
+ case Token.Until:
655
+ case Token.Case:
656
+ case Token.Select:
657
+ case Token.DblLBracket:
658
+ case Token.ArithCmd:
659
+ name = this.readWord(LexContext.CommandStart);
660
+ }
661
+ }
662
+ const cmd = first.token === Token.Word && name === undefined ? this.simpleCommandOrFunction(true) : this.command();
663
+ if (cmd && cmd.type !== "Function" && cmd.type !== "Coproc") {
664
+ return { type: "Coproc", pos, end: cmd.end, name, body: cmd };
665
+ }
666
+ this.error("expected command after 'coproc'", cmd?.pos ?? start.end);
667
+ const body = cmd
668
+ ? this.makeCompoundList([this.makeStatement(cmd)])
669
+ : { type: "CompoundList", pos: start.end, end: start.end, commands: [] };
670
+ return { type: "Coproc", pos, end: body.end, name, body };
671
+ }
672
+ // subshell := '(' list ')'
673
+ subshell() {
674
+ return this.subshellBody(this.tok.next(LexContext.CommandStart).pos);
675
+ }
676
+ // Continues a subshell whose '(' the caller already consumed.
677
+ subshellBody(pos) {
678
+ if (this.syntaxDepth >= MAX_SYNTAX_NESTING) {
679
+ this.error("maximum subshell nesting depth exceeded", pos);
680
+ const closeEnd = this.tok.skipSubshellBody();
681
+ if (closeEnd < 0)
682
+ this.error("expected ')' to close subshell", this.tok.getPos());
683
+ const end = closeEnd >= 0 ? closeEnd : pos;
684
+ return { type: "Subshell", pos, end, body: this.makeCompoundList([]) };
685
+ }
686
+ this.syntaxDepth++;
687
+ const commands = this.list();
688
+ this.syntaxDepth--;
689
+ if (commands.length === 0)
690
+ this.error("expected command in subshell", pos);
691
+ const closeEnd = this.acceptEnd(Token.RParen, LexContext.Normal);
692
+ if (closeEnd < 0)
693
+ this.error("expected ')' to close subshell", this.tok.getPos());
694
+ const end = closeEnd >= 0 ? closeEnd : pos;
695
+ return { type: "Subshell", pos, end, body: this.makeCompoundList(commands) };
696
+ }
697
+ // brace_group := '{' list '}'
698
+ braceGroup() {
699
+ const pos = this.tok.next(LexContext.CommandStart).pos;
700
+ if (this.syntaxDepth >= MAX_SYNTAX_NESTING) {
701
+ this.error("maximum brace group nesting depth exceeded", pos);
702
+ const closeEnd = this.tok.skipCompoundBody(Token.RBrace);
703
+ if (closeEnd < 0)
704
+ this.error("expected '}' to close brace group", this.tok.getPos());
705
+ const end = closeEnd >= 0 ? closeEnd : pos;
706
+ return { type: "BraceGroup", pos, end, body: this.makeCompoundList([]) };
707
+ }
708
+ this.syntaxDepth++;
709
+ const commands = this.list();
710
+ this.syntaxDepth--;
711
+ if (commands.length === 0)
712
+ this.error("expected command in brace group", pos);
713
+ const closeEnd = this.acceptEnd(Token.RBrace, LexContext.Normal);
714
+ if (closeEnd < 0)
715
+ this.error("expected '}' to close brace group", this.tok.getPos());
716
+ const end = closeEnd >= 0 ? closeEnd : pos;
717
+ return { type: "BraceGroup", pos, end, body: this.makeCompoundList(commands) };
718
+ }
719
+ // if_clause := IF list THEN list (ELIF list THEN list)* [ELSE list] FI
720
+ ifClause() {
721
+ const pos = this.tok.next(LexContext.CommandStart).pos;
722
+ if (this.syntaxDepth >= MAX_SYNTAX_NESTING) {
723
+ this.error("maximum if nesting depth exceeded", pos);
724
+ const closeEnd = this.tok.skipCompoundBody(Token.Fi);
725
+ if (closeEnd < 0)
726
+ this.error("expected 'fi' to close 'if'", this.tok.getPos());
727
+ const end = closeEnd >= 0 ? closeEnd : pos;
728
+ return {
729
+ type: "If",
730
+ pos,
731
+ end,
732
+ clause: this.makeCompoundList([]),
733
+ then: this.makeCompoundList([]),
734
+ elif: EMPTY_ELIFS,
735
+ else: undefined,
736
+ };
737
+ }
738
+ this.syntaxDepth++;
739
+ let command;
740
+ let branchPos = pos;
741
+ for (;;) {
742
+ const clauseCommands = this.list();
743
+ const emptyClausePos = this.emptyListPos(clauseCommands);
744
+ const clause = this.makeCompoundList(clauseCommands);
745
+ this.skipSemi();
746
+ const thenToken = this.accept(Token.Then, LexContext.CommandStart);
747
+ if (!thenToken)
748
+ this.error("expected 'then'", this.tok.getPos());
749
+ else if (emptyClausePos >= 0)
750
+ this.error(`expected command after '${branchPos === pos ? "if" : "elif"}'`, emptyClausePos);
751
+ const thenCommands = this.list();
752
+ const then_ = this.makeCompoundList(thenCommands);
753
+ if (thenToken && thenCommands.length === 0) {
754
+ this.error("expected command after 'then'", this.tok.peek(LexContext.CommandStart).pos);
755
+ then_.pos = thenToken.end;
756
+ then_.end = thenToken.end;
757
+ }
758
+ if (command) {
759
+ if (command.elif === EMPTY_ELIFS)
760
+ command.elif = [];
761
+ command.elif.push({
762
+ type: "Elif",
763
+ pos: branchPos,
764
+ end: then_.end,
765
+ clause,
766
+ then: then_,
767
+ });
768
+ }
769
+ else {
770
+ command = { type: "If", pos, end: pos, clause, then: then_, elif: EMPTY_ELIFS, else: undefined };
771
+ }
772
+ this.skipSemi();
773
+ const elif = this.accept(Token.Elif, LexContext.CommandStart);
774
+ if (!elif)
775
+ break;
776
+ branchPos = elif.pos;
777
+ }
778
+ if (this.accept(Token.Else, LexContext.CommandStart)) {
779
+ const elseCommands = this.list();
780
+ const emptyElsePos = this.emptyListPos(elseCommands);
781
+ if (emptyElsePos >= 0)
782
+ this.error("expected command after 'else'", emptyElsePos);
783
+ command.else = this.makeCompoundList(elseCommands);
784
+ this.skipSemi();
785
+ }
786
+ const closeEnd = this.acceptEnd(Token.Fi, LexContext.CommandStart);
787
+ if (closeEnd < 0)
788
+ this.error("expected 'fi' to close 'if'", this.tok.getPos());
789
+ command.end = closeEnd >= 0 ? closeEnd : branchPos;
790
+ this.syntaxDepth--;
791
+ return command;
792
+ }
793
+ // for_clause := FOR word [IN word* (';'|NL)] DO list DONE
794
+ // | FOR '((' expr '))' [';'|NL] DO list DONE
795
+ forClause() {
796
+ const pos = this.tok.next(LexContext.CommandStart).pos;
797
+ if (this.tok.peek(LexContext.Normal).token === Token.LParen) {
798
+ return this.cStyleFor(pos);
799
+ }
800
+ const name = this.readRequiredWord("for");
801
+ let wordlist;
802
+ this.skipNewlines(LexContext.CommandStart);
803
+ if (this.tok.peek(LexContext.CommandStart).token === Token.In) {
804
+ this.tok.next(LexContext.CommandStart);
805
+ wordlist = [];
806
+ while (this.tok.peek(LexContext.Normal).token === Token.Word) {
807
+ wordlist.push(this.readWord(LexContext.Normal));
808
+ }
809
+ }
810
+ this.skipSemi();
811
+ this.skipNewlines(LexContext.CommandStart);
812
+ if (this.tok.peek(LexContext.CommandStart).token === Token.LBrace) {
813
+ const bg = this.braceGroup();
814
+ return { type: "For", pos, end: bg.end, name, wordlist, body: bg.body };
815
+ }
816
+ const hasDo = this.accept(Token.Do, LexContext.CommandStart) !== null;
817
+ if (!hasDo)
818
+ this.error("expected 'do'", this.tok.getPos());
819
+ if (this.syntaxDepth >= MAX_SYNTAX_NESTING) {
820
+ this.error("maximum for nesting depth exceeded", pos);
821
+ const closeEnd = this.tok.skipCompoundBody(Token.Done);
822
+ if (closeEnd < 0)
823
+ this.error("expected 'done' to close 'for'", this.tok.getPos());
824
+ const end = closeEnd >= 0 ? closeEnd : pos;
825
+ return { type: "For", pos, end, name, wordlist, body: this.makeCompoundList([]) };
826
+ }
827
+ this.syntaxDepth++;
828
+ const body = this.doBody(hasDo);
829
+ this.syntaxDepth--;
830
+ this.skipSemi();
831
+ const closeEnd = this.acceptEnd(Token.Done, LexContext.CommandStart);
832
+ if (closeEnd < 0)
833
+ this.error("expected 'done' to close 'for'", this.tok.getPos());
834
+ const end = closeEnd >= 0 ? closeEnd : pos;
835
+ return { type: "For", pos, end, name, wordlist, body: this.makeCompoundList(body) };
836
+ }
837
+ // C-style for: (( expr; expr; expr )) [;|NL] do list done | { list }
838
+ cStyleFor(pos) {
839
+ const [initStr, testStr, updateStr, initPos, testPos, updatePos] = this.tok.readCStyleForExprs();
840
+ if (this.tok.peek(LexContext.CommandStart).token === Token.Semi)
841
+ this.tok.next(LexContext.CommandStart);
842
+ this.skipNewlines(LexContext.CommandStart);
843
+ if (this.tok.peek(LexContext.CommandStart).token === Token.LBrace) {
844
+ const bg = this.braceGroup();
845
+ return new ArithmeticForImpl(pos, bg.end, bg.body, initStr, testStr, updateStr, initPos, testPos, updatePos, this.source, this.depth, this.tok.getSubstitutionContext());
846
+ }
847
+ const hasDo = this.accept(Token.Do, LexContext.CommandStart) !== null;
848
+ if (!hasDo)
849
+ this.error("expected 'do'", this.tok.getPos());
850
+ if (this.syntaxDepth >= MAX_SYNTAX_NESTING) {
851
+ this.error("maximum for nesting depth exceeded", pos);
852
+ const closeEnd = this.tok.skipCompoundBody(Token.Done);
853
+ if (closeEnd < 0)
854
+ this.error("expected 'done' to close 'for'", this.tok.getPos());
855
+ const end = closeEnd >= 0 ? closeEnd : pos;
856
+ return new ArithmeticForImpl(pos, end, this.makeCompoundList([]), initStr, testStr, updateStr, initPos, testPos, updatePos, this.source, this.depth, this.tok.getSubstitutionContext());
857
+ }
858
+ this.syntaxDepth++;
859
+ const body = this.doBody(hasDo);
860
+ this.syntaxDepth--;
861
+ const closeEnd = this.acceptEnd(Token.Done, LexContext.CommandStart);
862
+ if (closeEnd < 0)
863
+ this.error("expected 'done' to close 'for'", this.tok.getPos());
864
+ const end = closeEnd >= 0 ? closeEnd : pos;
865
+ return new ArithmeticForImpl(pos, end, this.makeCompoundList(body), initStr, testStr, updateStr, initPos, testPos, updatePos, this.source, this.depth, this.tok.getSubstitutionContext());
866
+ }
867
+ whileClause() {
868
+ return this.whileOrUntil("while");
869
+ }
870
+ untilClause() {
871
+ return this.whileOrUntil("until");
872
+ }
873
+ whileOrUntil(kind) {
874
+ const pos = this.tok.next(LexContext.CommandStart).pos;
875
+ if (this.syntaxDepth >= MAX_SYNTAX_NESTING) {
876
+ this.error(`maximum ${kind} nesting depth exceeded`, pos);
877
+ const closeEnd = this.tok.skipCompoundBody(Token.Done);
878
+ if (closeEnd < 0)
879
+ this.error(`expected 'done' to close '${kind}'`, this.tok.getPos());
880
+ const end = closeEnd >= 0 ? closeEnd : pos;
881
+ return {
882
+ type: "While",
883
+ pos,
884
+ end,
885
+ kind,
886
+ clause: this.makeCompoundList([]),
887
+ body: this.makeCompoundList([]),
888
+ };
889
+ }
890
+ this.syntaxDepth++;
891
+ const clauseCommands = this.list();
892
+ const emptyClausePos = this.emptyListPos(clauseCommands);
893
+ const clause = this.makeCompoundList(clauseCommands);
894
+ this.skipSemi();
895
+ const hasDo = this.accept(Token.Do, LexContext.CommandStart) !== null;
896
+ if (!hasDo)
897
+ this.error("expected 'do'", this.tok.getPos());
898
+ else if (emptyClausePos >= 0)
899
+ this.error(`expected command after '${kind}'`, emptyClausePos);
900
+ const body = this.doBody(hasDo);
901
+ this.skipSemi();
902
+ const closeEnd = this.acceptEnd(Token.Done, LexContext.CommandStart);
903
+ if (closeEnd < 0)
904
+ this.error(`expected 'done' to close '${kind}'`, this.tok.getPos());
905
+ const end = closeEnd >= 0 ? closeEnd : pos;
906
+ this.syntaxDepth--;
907
+ return { type: "While", pos, end, kind, clause, body: this.makeCompoundList(body) };
908
+ }
909
+ // case_clause := CASE word IN (pattern) list (;; | ;& | ;;&) ... ESAC
910
+ caseClause() {
911
+ const pos = this.tok.next(LexContext.CommandStart).pos;
912
+ const word = this.readRequiredWord("case");
913
+ this.skipNewlines(LexContext.CommandStart);
914
+ if (!this.accept(Token.In, LexContext.CommandStart))
915
+ this.error("expected 'in' after 'case' word", this.tok.getPos());
916
+ this.skipNewlines(LexContext.CasePattern);
917
+ if (this.syntaxDepth >= MAX_SYNTAX_NESTING) {
918
+ this.error("maximum case nesting depth exceeded", pos);
919
+ const closeEnd = this.tok.skipCompoundBody(Token.Esac);
920
+ if (closeEnd < 0)
921
+ this.error("expected 'esac' to close 'case'", this.tok.getPos());
922
+ const end = closeEnd >= 0 ? closeEnd : pos;
923
+ return { type: "Case", pos, end, word, items: [] };
924
+ }
925
+ this.syntaxDepth++;
926
+ const items = [];
927
+ // Report only the first grammar error: recovery below would repeat it.
928
+ let invalid = false;
929
+ let t = this.tok.peek(LexContext.CasePattern).token;
930
+ while (t !== Token.Esac && t !== Token.EOF) {
931
+ const itemPos = this.tok.peek(LexContext.Normal).pos;
932
+ this.accept(Token.LParen, LexContext.Normal);
933
+ // pattern := word ('|' word)*
934
+ const pattern = [];
935
+ let expectWord = true;
936
+ let next = this.tok.peek(LexContext.Normal);
937
+ while (next.token !== Token.RParen && next.token !== Token.EOF) {
938
+ const valid = next.token === (expectWord ? Token.Word : Token.Pipe);
939
+ if (valid)
940
+ expectWord = !expectWord;
941
+ else if (!invalid) {
942
+ this.unexpectedToken(next);
943
+ invalid = true;
944
+ }
945
+ const tok = this.tok.next(LexContext.Normal);
946
+ if (tok.token === Token.Word)
947
+ pattern.push(this.toWord(tok));
948
+ next = this.tok.peek(LexContext.Normal);
949
+ }
950
+ if (expectWord && next.token === Token.RParen && !invalid) {
951
+ this.unexpectedToken(next);
952
+ invalid = true;
953
+ }
954
+ const rparenEnd = this.acceptEnd(Token.RParen, LexContext.Normal);
955
+ const cmds = this.list();
956
+ // An empty body belongs where commands would start, not wherever the lexer stopped.
957
+ const bodyPos = rparenEnd >= 0 ? rparenEnd : pattern.length > 0 ? pattern[pattern.length - 1].end : itemPos;
958
+ const itemEnd = cmds.length > 0 ? cmds[cmds.length - 1].end : bodyPos;
959
+ const item = {
960
+ type: "CaseItem",
961
+ pos: itemPos,
962
+ end: itemEnd,
963
+ pattern,
964
+ body: cmds.length > 0
965
+ ? this.makeCompoundList(cmds)
966
+ : { type: "CompoundList", pos: bodyPos, end: bodyPos, commands: [] },
967
+ terminator: undefined,
968
+ };
969
+ const after = this.tok.peek(LexContext.CommandStart);
970
+ t = after.token;
971
+ if (t === Token.DoubleSemi || t === Token.SemiAmp || t === Token.DoubleSemiAmp) {
972
+ const termTok = this.tok.next(LexContext.CommandStart);
973
+ item.terminator = CASE_TERMINATORS[termTok.token];
974
+ item.end = termTok.end;
975
+ }
976
+ else if (t !== Token.Esac && t !== Token.EOF && !invalid) {
977
+ this.unexpectedToken(after);
978
+ invalid = true;
979
+ }
980
+ items.push(item);
981
+ this.skipNewlines(LexContext.CasePattern);
982
+ t = this.tok.peek(LexContext.CasePattern).token;
983
+ }
984
+ const closeEnd = this.acceptEnd(Token.Esac, LexContext.CommandStart);
985
+ if (closeEnd < 0)
986
+ this.error("expected 'esac' to close 'case'", this.tok.getPos());
987
+ const end = closeEnd >= 0 ? closeEnd : pos;
988
+ this.syntaxDepth--;
989
+ return { type: "Case", pos, end, word, items };
990
+ }
991
+ // select_clause := SELECT word [IN word* (';'|NL)] DO list DONE
992
+ selectClause() {
993
+ const pos = this.tok.next(LexContext.CommandStart).pos;
994
+ const name = this.readRequiredWord("select");
995
+ let wordlist;
996
+ this.skipNewlines(LexContext.CommandStart);
997
+ if (this.tok.peek(LexContext.CommandStart).token === Token.In) {
998
+ this.tok.next(LexContext.CommandStart);
999
+ wordlist = [];
1000
+ while (this.tok.peek(LexContext.Normal).token === Token.Word) {
1001
+ wordlist.push(this.readWord(LexContext.Normal));
1002
+ }
1003
+ }
1004
+ this.skipSemi();
1005
+ this.skipNewlines(LexContext.CommandStart);
1006
+ if (this.tok.peek(LexContext.CommandStart).token === Token.LBrace) {
1007
+ const bg = this.braceGroup();
1008
+ return { type: "Select", pos, end: bg.end, name, wordlist, body: bg.body };
1009
+ }
1010
+ const hasDo = this.accept(Token.Do, LexContext.CommandStart) !== null;
1011
+ if (!hasDo)
1012
+ this.error("expected 'do'", this.tok.getPos());
1013
+ if (this.syntaxDepth >= MAX_SYNTAX_NESTING) {
1014
+ this.error("maximum select nesting depth exceeded", pos);
1015
+ const closeEnd = this.tok.skipCompoundBody(Token.Done);
1016
+ if (closeEnd < 0)
1017
+ this.error("expected 'done' to close 'select'", this.tok.getPos());
1018
+ const end = closeEnd >= 0 ? closeEnd : pos;
1019
+ return { type: "Select", pos, end, name, wordlist, body: this.makeCompoundList([]) };
1020
+ }
1021
+ this.syntaxDepth++;
1022
+ const body = this.doBody(hasDo);
1023
+ this.syntaxDepth--;
1024
+ this.skipSemi();
1025
+ const closeEnd = this.acceptEnd(Token.Done, LexContext.CommandStart);
1026
+ if (closeEnd < 0)
1027
+ this.error("expected 'done' to close 'select'", this.tok.getPos());
1028
+ const end = closeEnd >= 0 ? closeEnd : pos;
1029
+ return { type: "Select", pos, end, name, wordlist, body: this.makeCompoundList(body) };
1030
+ }
1031
+ // test_command := [[ test_expr ]]
1032
+ testCommand() {
1033
+ const pos = this.tok.next(LexContext.CommandStart).pos; // consume [[
1034
+ const expr = this.parseTestOr();
1035
+ const closeEnd = this.acceptEnd(Token.DblRBracket, LexContext.TestMode);
1036
+ if (closeEnd < 0)
1037
+ this.error("expected ']]' to close '[['", this.tok.getPos());
1038
+ const end = closeEnd >= 0 ? closeEnd : pos;
1039
+ return { type: "TestCommand", pos, end, expression: expr };
1040
+ }
1041
+ // test_or := test_and ('||' test_and)*
1042
+ parseTestOr() {
1043
+ const first = this.parseTestAnd();
1044
+ if (this.tok.peek(LexContext.TestMode).token !== Token.Or)
1045
+ return first;
1046
+ const operands = [first];
1047
+ do {
1048
+ this.tok.next(LexContext.TestMode);
1049
+ operands.push(this.parseTestAnd());
1050
+ } while (this.tok.peek(LexContext.TestMode).token === Token.Or);
1051
+ return {
1052
+ type: "TestLogical",
1053
+ pos: first.pos,
1054
+ end: operands[operands.length - 1].end,
1055
+ operator: "||",
1056
+ operands,
1057
+ };
1058
+ }
1059
+ // test_and := test_not ('&&' test_not)*
1060
+ parseTestAnd() {
1061
+ const first = this.parseTestNot();
1062
+ if (this.tok.peek(LexContext.TestMode).token !== Token.And)
1063
+ return first;
1064
+ const operands = [first];
1065
+ do {
1066
+ this.tok.next(LexContext.TestMode);
1067
+ operands.push(this.parseTestNot());
1068
+ } while (this.tok.peek(LexContext.TestMode).token === Token.And);
1069
+ return {
1070
+ type: "TestLogical",
1071
+ pos: first.pos,
1072
+ end: operands[operands.length - 1].end,
1073
+ operator: "&&",
1074
+ operands,
1075
+ };
1076
+ }
1077
+ // test_not := '!' test_not | test_primary
1078
+ parseTestNot() {
1079
+ let t = this.tok.peek(LexContext.TestMode);
1080
+ if (!isTestNegation(t))
1081
+ return this.parseTestPrimary();
1082
+ const negations = [];
1083
+ while (isTestNegation(t)) {
1084
+ const bang = this.tok.next(LexContext.TestMode);
1085
+ negations.push({ type: "Negation", pos: bang.pos, end: bang.end });
1086
+ t = this.tok.peek(LexContext.TestMode);
1087
+ }
1088
+ const operand = this.parseTestPrimary();
1089
+ return { type: "TestNot", pos: negations[0].pos, end: operand.end, negations, operand };
1090
+ }
1091
+ // test_primary := '(' test_or ')' | unary_op word | word binary_op word | word
1092
+ parseTestPrimary() {
1093
+ // Grouped: ( expr )
1094
+ if (this.tok.peek(LexContext.TestMode).token === Token.LParen) {
1095
+ const openPos = this.tok.next(LexContext.TestMode).pos;
1096
+ if (this.syntaxDepth >= MAX_SYNTAX_NESTING) {
1097
+ this.error("maximum test group nesting depth exceeded", openPos);
1098
+ const closeEnd = this.tok.skipTestGroup();
1099
+ if (closeEnd < 0)
1100
+ this.error("expected ')' to close test group", this.tok.getPos());
1101
+ const end = closeEnd >= 0 ? closeEnd : openPos;
1102
+ const operand = new WordImpl("", openPos, openPos);
1103
+ const expression = {
1104
+ type: "TestUnary",
1105
+ pos: openPos,
1106
+ end: openPos,
1107
+ operator: "-n",
1108
+ operand,
1109
+ };
1110
+ return { type: "TestGroup", pos: openPos, end, expression };
1111
+ }
1112
+ this.syntaxDepth++;
1113
+ const expr = this.parseTestOr();
1114
+ this.syntaxDepth--;
1115
+ const closeEnd = this.acceptEnd(Token.RParen, LexContext.TestMode);
1116
+ if (closeEnd < 0)
1117
+ this.error("expected ')' to close test group", this.tok.getPos());
1118
+ const end = closeEnd >= 0 ? closeEnd : openPos;
1119
+ return { type: "TestGroup", pos: openPos, end, expression: expr };
1120
+ }
1121
+ const first = this.tok.next(LexContext.TestMode);
1122
+ if (first.token === Token.And || first.token === Token.Or)
1123
+ this.unexpectedToken(first);
1124
+ const val = first.value;
1125
+ const firstPos = first.pos;
1126
+ const firstEnd = first.end;
1127
+ // Unary test: -op word, recognized only as written. Bash rejects the operator without an
1128
+ // operand, so keep the operator and report the missing word instead of demoting it to a string.
1129
+ if (first.keywordEligible && UNARY_TEST_OPS[val] === 1) {
1130
+ if (this.tok.peek(LexContext.TestMode).token === Token.Word) {
1131
+ const operand = this.readTestOperand();
1132
+ return {
1133
+ type: "TestUnary",
1134
+ pos: firstPos,
1135
+ end: operand.end,
1136
+ operator: val,
1137
+ operand,
1138
+ };
1139
+ }
1140
+ this.error("expected operand after unary test operator", firstEnd);
1141
+ const operand = new WordImpl("", firstEnd, firstEnd);
1142
+ return { type: "TestUnary", pos: firstPos, end: firstEnd, operator: val, operand };
1143
+ }
1144
+ // Check for binary op
1145
+ const nt = this.tok.peek(LexContext.TestMode);
1146
+ if (nt.token === Token.Word && nt.keywordEligible && BINARY_TEST_OPS[nt.value] === 1) {
1147
+ const op = this.tok.next(LexContext.TestMode).value;
1148
+ let right;
1149
+ if (op === "=~") {
1150
+ const token = this.tok.readTestRegexWord();
1151
+ right = new WordImpl(this.source.slice(token.pos, token.end), token.pos, token.end, this.source, embeddedPartsResolver(this.tok.getSubstitutionContext()), this.depth);
1152
+ }
1153
+ else {
1154
+ right = this.readTestOperand();
1155
+ }
1156
+ const left = this.toWordFromPosEnd(first, firstPos, firstEnd);
1157
+ return {
1158
+ type: "TestBinary",
1159
+ pos: firstPos,
1160
+ end: right.end,
1161
+ operator: op,
1162
+ left,
1163
+ right,
1164
+ };
1165
+ }
1166
+ // Standalone word (implicit -n test)
1167
+ const w = this.toWordFromPosEnd(first, firstPos, firstEnd);
1168
+ return { type: "TestUnary", pos: firstPos, end: w.end, operator: "-n", operand: w };
1169
+ }
1170
+ // Unquoted `<` and `>` are operators in `[[ ]]`, never operands.
1171
+ readTestOperand() {
1172
+ const t = this.tok.next(LexContext.TestMode);
1173
+ if (t.keywordEligible && (t.value === "<" || t.value === ">"))
1174
+ this.error(`unexpected argument '${t.value}' to conditional operator`, t.pos);
1175
+ return this.toWord(t);
1176
+ }
1177
+ // function_def with 'function' keyword
1178
+ functionDef() {
1179
+ const pos = this.tok.next(LexContext.CommandStart).pos;
1180
+ const name = this.readWord(LexContext.FunctionName);
1181
+ let body;
1182
+ if (this.tok.peek(LexContext.CommandStart).token === Token.LParen) {
1183
+ const openPos = this.tok.next(LexContext.CommandStart).pos;
1184
+ // `(` is the optional empty parameter list only when `)` follows immediately;
1185
+ // anything else opens a subshell body, as in `f() ( ... )`.
1186
+ if (this.tok.peek(LexContext.CommandStart).token === Token.RParen) {
1187
+ this.tok.next(LexContext.CommandStart);
1188
+ this.skipNewlines(LexContext.CommandStart);
1189
+ body = this.commandAsBody();
1190
+ }
1191
+ else {
1192
+ body = this.withRedirects(this.subshellBody(openPos));
1193
+ }
1194
+ }
1195
+ else {
1196
+ this.skipNewlines(LexContext.CommandStart);
1197
+ body = this.commandAsBody();
1198
+ }
1199
+ return { type: "Function", pos, end: body.end, name, body };
1200
+ }
1201
+ // simple_command or function_def (word '(' ')' body)
1202
+ simpleCommandOrFunction(coproc) {
1203
+ const prefix = [];
1204
+ const cmdPos = this.tok.peek(LexContext.CommandStart).pos;
1205
+ let lastEnd = cmdPos;
1206
+ // Assignments and redirects interleave freely; after the first element CommandPrefix
1207
+ // keeps the following command name from being read as a reserved word.
1208
+ let ctx = LexContext.CommandStart;
1209
+ for (;;) {
1210
+ const t = this.tok.peek(ctx).token;
1211
+ if (t === Token.Assignment) {
1212
+ const assignment = this.tok.next(ctx);
1213
+ lastEnd = assignment.end;
1214
+ prefix.push(this.parseAssignment(assignment));
1215
+ }
1216
+ else if (t === Token.Redirect) {
1217
+ const redirect = this.readRedirect(ctx);
1218
+ prefix.push(redirect);
1219
+ lastEnd = redirect.end;
1220
+ }
1221
+ else {
1222
+ break;
1223
+ }
1224
+ ctx = LexContext.CommandPrefix;
1225
+ }
1226
+ if (this.tok.peek(LexContext.Normal).token !== Token.Word) {
1227
+ return new CommandImpl(cmdPos, lastEnd, undefined, prefix, []);
1228
+ }
1229
+ const nameToken = this.tok.next(LexContext.Normal);
1230
+ const keyword = nameToken.keywordEligible ? nameToken.value : "";
1231
+ const name = this.toWord(nameToken);
1232
+ ctx = isDeclarationCommand(keyword)
1233
+ ? LexContext.Declaration
1234
+ : keyword === "eval" || keyword === "let"
1235
+ ? LexContext.EvalArgument
1236
+ : LexContext.Normal;
1237
+ lastEnd = name.end;
1238
+ const suffix = [];
1239
+ if (coproc) {
1240
+ while (this.tok.peek(LexContext.CommandPrefix).token === Token.Assignment) {
1241
+ const token = this.tok.next(LexContext.CommandPrefix);
1242
+ const word = ctx === LexContext.Declaration ? this.parseAssignment(token) : this.toWord(token);
1243
+ suffix.push(word);
1244
+ lastEnd = word.end;
1245
+ }
1246
+ }
1247
+ // Check for function definition: word '(' ')' body
1248
+ if (this.tok.peek(ctx).token === Token.LParen && suffix.length === 0) {
1249
+ this.tok.next(LexContext.Normal);
1250
+ if (this.tok.peek(LexContext.Normal).token === Token.RParen) {
1251
+ this.tok.next(LexContext.Normal);
1252
+ this.skipNewlines(LexContext.CommandStart);
1253
+ const body = this.commandAsBody();
1254
+ return {
1255
+ type: "Function",
1256
+ pos: name.pos,
1257
+ end: body.end,
1258
+ name,
1259
+ body,
1260
+ };
1261
+ }
1262
+ }
1263
+ // Collect suffix words and redirects
1264
+ for (;;) {
1265
+ const st = this.tok.peek(ctx).token;
1266
+ if (st === Token.Word || st === Token.Assignment) {
1267
+ const token = this.tok.next(ctx);
1268
+ const w = st === Token.Assignment ? this.parseAssignment(token) : this.toWord(token);
1269
+ suffix.push(w);
1270
+ lastEnd = w.end;
1271
+ }
1272
+ else if (st === Token.Redirect) {
1273
+ const redirect = this.readRedirect(ctx);
1274
+ suffix.push(redirect);
1275
+ lastEnd = redirect.end;
1276
+ }
1277
+ else {
1278
+ break;
1279
+ }
1280
+ }
1281
+ return new CommandImpl(cmdPos, lastEnd, name, prefix, suffix);
1282
+ }
1283
+ readRedirect(ctx) {
1284
+ const t = this.tok.next(ctx);
1285
+ const tPos = t.pos;
1286
+ const tEnd = t.end;
1287
+ const descriptor = t.fileDescriptor !== undefined
1288
+ ? { type: "FileDescriptor", pos: tPos, end: t.descriptorEnd, value: t.fileDescriptor }
1289
+ : t.variableName !== undefined
1290
+ ? { type: "FileDescriptorVariable", pos: tPos, end: t.descriptorEnd, name: t.variableName }
1291
+ : undefined;
1292
+ const hasTarget = t.targetEnd > t.targetPos;
1293
+ if (!hasTarget)
1294
+ this.error("expected redirect target", t.targetPos);
1295
+ if (t.value === "<<" || t.value === "<<-") {
1296
+ const r = {
1297
+ type: "HereDoc",
1298
+ pos: tPos,
1299
+ end: tEnd,
1300
+ operator: t.value,
1301
+ descriptor,
1302
+ delimiter: hasTarget
1303
+ ? {
1304
+ type: "HereDocDelimiter",
1305
+ pos: t.targetPos,
1306
+ end: t.targetEnd,
1307
+ text: this.source.slice(t.targetPos, t.targetEnd),
1308
+ value: t.content ?? "",
1309
+ quoted: t.delimiterQuoted,
1310
+ }
1311
+ : undefined,
1312
+ body: new HereDocBodyImpl(this.source, tEnd, t.delimiterQuoted, this.depth, this.tok.getSubstitutionContext()),
1313
+ closing: undefined,
1314
+ };
1315
+ if (hasTarget)
1316
+ this.tok.registerHereDocTarget(r);
1317
+ return r;
1318
+ }
1319
+ const target = hasTarget
1320
+ ? new WordImpl(this.source.slice(t.targetPos, t.targetEnd), t.targetPos, t.targetEnd, this.source, wordPartsResolver(this.tok.getSubstitutionContext()), this.depth)
1321
+ : undefined;
1322
+ return t.value === "<<<"
1323
+ ? { type: "HereString", pos: tPos, end: tEnd, operator: "<<<", descriptor, target }
1324
+ : { type: "Redirect", pos: tPos, end: tEnd, operator: REDIRECT_OPS[t.value] ?? ">", descriptor, target };
1325
+ }
1326
+ commandAsBody() {
1327
+ const cmd = this.command();
1328
+ if (cmd && cmd.type !== "Command" && cmd.type !== "Function" && cmd.type !== "Coproc")
1329
+ return cmd;
1330
+ this.error("expected compound command as function body", cmd?.pos ?? this.tok.getPos());
1331
+ return this.makeCompoundList(cmd ? [this.makeStatement(cmd)] : []);
1332
+ }
1333
+ readWord(ctx) {
1334
+ return this.toWord(this.tok.next(ctx));
1335
+ }
1336
+ // A missing word stays empty and leaves its token to the rest of the clause.
1337
+ readRequiredWord(keyword) {
1338
+ const t = this.tok.peek(LexContext.Normal);
1339
+ if (t.token === Token.Word)
1340
+ return this.toWord(this.tok.next(LexContext.Normal));
1341
+ if (t.token !== Token.EOF)
1342
+ this.error(`expected word after '${keyword}'`, t.pos);
1343
+ return new WordImpl("", t.pos, t.pos);
1344
+ }
1345
+ toWord(tok) {
1346
+ const text = tok.raw ? tok.value : this.source.slice(tok.pos, tok.end);
1347
+ // Raw keyword-eligible words have no quotes or expansions to resolve.
1348
+ const source = tok.raw && tok.keywordEligible ? undefined : this.source;
1349
+ return new WordImpl(text, tok.pos, tok.end, source, source === undefined ? computeBareWordParts : wordPartsResolver(this.tok.getSubstitutionContext()), this.depth);
1350
+ }
1351
+ toWordFromPosEnd(tok, pos, end) {
1352
+ if (tok.pos === pos && tok.end === end)
1353
+ return this.toWord(tok);
1354
+ return new WordImpl(this.source.slice(pos, end), pos, end, this.source, wordPartsResolver(this.tok.getSubstitutionContext()), this.depth);
1355
+ }
1356
+ parseAssignment(token) {
1357
+ const text = token.raw ? token.value : this.source.slice(token.pos, token.end);
1358
+ return new AssignmentImpl(text, token.pos, token.end, this.source, token.assignmentOperatorPos, this.depth, this.tok.getSubstitutionContext());
1359
+ }
1360
+ makeCompoundList(commands) {
1361
+ const p = this.tok.getPos();
1362
+ const pos = commands.length > 0 ? commands[0].pos : p;
1363
+ const end = commands.length > 0 ? commands[commands.length - 1].end : p;
1364
+ return { type: "CompoundList", pos, end, commands };
1365
+ }
1366
+ }