@cplieger/deadset-ts 8.0.0-dev.12 → 8.0.0-dev.14

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