@pipelex/sdk 0.24.0 → 0.25.0

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.
@@ -0,0 +1,1304 @@
1
+ /**
2
+ * A reading of a POSIX shell script that tells, for an offset in it, which
3
+ * directory the patch command reading that offset runs in.
4
+ *
5
+ * Codex reports a patch run through the shell as a `Bash` call carrying the
6
+ * whole script, and a relative path in that patch is relative to wherever the
7
+ * script had moved when `apply_patch` ran. This module lexes the script far
8
+ * enough to follow `cd`: commands, quotes, heredocs, substitutions, scopes,
9
+ * branches and loops. It is not a shell parser. Past what it reads, it
10
+ * answers "unknown" rather than guessing, and a script it cannot lex at all
11
+ * is "unparsed".
12
+ *
13
+ * - Text is placed only where a patch command reads it: the words and
14
+ * heredoc bodies of an `apply_patch` command, and the text of a command
15
+ * feeding one through a pipeline or a substitution in its words. Text that
16
+ * any other command holds, such as a variable, a file, `bash -c` or `eval`,
17
+ * is unknown, since this reading does not follow whatever reads it later.
18
+ * - `cd DIR` with one literal operand (optionally after `-L`, `-P` or `--`)
19
+ * moves the directory, and is assumed to succeed; any other `cd`, and
20
+ * `pushd`, `popd`, `eval`, `source`, `.` or a function definition, makes
21
+ * it unknown. An absolute `cd` makes an unknown directory known again.
22
+ * - The reading follows the path on which each command succeeds, so a
23
+ * negated `cd` fails on it. What runs only on the other path (after `||`,
24
+ * in `elif` and `else`, after a negated `cd` and `&&`, and on the path a
25
+ * loop's condition takes to end or skip it) starts where the shell stood
26
+ * when nothing had moved it, and from an unknown directory otherwise.
27
+ * - `( … )`, `$( … )`, `<( … )` and `>( … )` are scopes: a `cd` inside one
28
+ * ends at its closing parenthesis. A list sent to the background, and every
29
+ * member of a pipeline but the last, run in subshells in bash and zsh
30
+ * alike. zsh runs a pipeline's last member in the current shell and bash
31
+ * does not, so a directory change there makes the directory after it
32
+ * unknown.
33
+ * - `{ … }` and the separators `;`, newline and `&&` are followed in order.
34
+ * Where only one of several paths runs, after `||`, between the branches of
35
+ * `if` and `case`, and across the passes of a loop, the directory is kept
36
+ * when every path leaves it the same, and is unknown otherwise.
37
+ * - Heredoc bodies, quoted strings, comments and the expression of a `[[ … ]]`
38
+ * are never read as commands. A heredoc body belongs to the command that
39
+ * opened it.
40
+ * - The text a command reads is the script's after the shell's quoting, which
41
+ * the reading also tells: a patch in double quotes or in an unquoted heredoc
42
+ * has its backslash escapes removed and its `$` and backtick expansions run.
43
+ */
44
+ import { basename, isAbsolute, resolve } from "node:path";
45
+ /**
46
+ * Read `script` as a POSIX shell script run from `sessionDir`, or return
47
+ * `"unparsed"` when it holds an unterminated quote, substitution or backtick,
48
+ * a heredoc inside backticks, a parenthesis that closes nothing, or any other
49
+ * syntax error this reading can see.
50
+ */
51
+ export function readShellScript(script, sessionDir) {
52
+ let extents;
53
+ let quoted;
54
+ try {
55
+ const parser = new ScriptParser(script);
56
+ const walk = new DirectoryWalk(WALK_BUDGET_FLOOR + WALK_BUDGET_PER_CHARACTER * script.length);
57
+ walk.list(parser.parse(), sessionDir);
58
+ extents = walk.extents;
59
+ quoted = parser.quoted;
60
+ }
61
+ catch {
62
+ // An Unparsed signal, or a script nested past the stack: either way the
63
+ // script cannot be read, and "unparsed" is the answer that fails safe.
64
+ return "unparsed";
65
+ }
66
+ return {
67
+ directoryAt(offset) {
68
+ const holder = innermost(extents, offset);
69
+ if (!holder) {
70
+ return { kind: "outside" };
71
+ }
72
+ if (!holder.read) {
73
+ return { kind: "unread" };
74
+ }
75
+ return holder.directory === null
76
+ ? { kind: "unknown" }
77
+ : { kind: "directory", path: holder.directory };
78
+ },
79
+ quotingAt(offset) {
80
+ return innermost(quoted, offset)?.quoting ?? "verbatim";
81
+ },
82
+ };
83
+ }
84
+ /** The shortest of `spans` holding `offset`, which is the innermost when they nest. */
85
+ function innermost(spans, offset) {
86
+ let holder = null;
87
+ for (const span of spans) {
88
+ if (span.start <= offset && offset < span.end) {
89
+ if (!holder || span.end - span.start < holder.end - holder.start) {
90
+ holder = span;
91
+ }
92
+ }
93
+ }
94
+ return holder;
95
+ }
96
+ /**
97
+ * The lines of a patch section as the patch command reads them, given the
98
+ * quoting its header is in, less any line an expansion makes unknown. A line
99
+ * in quotes may close them and open others, as `'\''` writes an apostrophe in
100
+ * single quotes, and is read through each. When a line ends in other quoting
101
+ * than it began in, or in a backslash that joins it to the next, the lines
102
+ * after it are read in quoting this reading does not follow, so no line of
103
+ * the section is known and none is given.
104
+ */
105
+ export function linesAsRead(lines, quoting) {
106
+ const read = [];
107
+ for (const line of lines) {
108
+ const asRead = lineAsRead(line, quoting);
109
+ if (asRead === LOST) {
110
+ return [];
111
+ }
112
+ if (asRead !== null) {
113
+ read.push(asRead);
114
+ }
115
+ }
116
+ return read;
117
+ }
118
+ /** What `lineAsRead` gives when the quoting after the line is not the one it began in. */
119
+ const LOST = Symbol("lost");
120
+ /** A character after `$` that makes an expansion of it. */
121
+ const EXPANSION_START = /[\w{(@*#?$!-]/;
122
+ /** A character that ends an unquoted word, or may expand it. */
123
+ const UNQUOTED_BREAK = /[\s;&|<>()*?[\]{}$]/;
124
+ /**
125
+ * One line as read, starting in `quoting`: null when an expansion makes it
126
+ * unknown, and `LOST` when it does not end in the quoting it began in.
127
+ */
128
+ function lineAsRead(line, quoting) {
129
+ if (quoting === "verbatim") {
130
+ return line;
131
+ }
132
+ const initial = quoting === "single-quoted" ? "single" : quoting === "double-quoted" ? "double" : "heredoc";
133
+ let state = initial;
134
+ let read = "";
135
+ let known = true;
136
+ for (let index = 0; index < line.length; index++) {
137
+ const char = line[index];
138
+ const next = line[index + 1];
139
+ if (state === "single") {
140
+ if (char === "'") {
141
+ state = "unquoted";
142
+ }
143
+ else {
144
+ read += char;
145
+ }
146
+ }
147
+ else if (char === "\\") {
148
+ if (next === undefined) {
149
+ return LOST; // the shell removes it with the newline, joining the next line to this one
150
+ }
151
+ const escapable = state === "double" ? '$`"\\' : state === "heredoc" ? "$`\\" : next;
152
+ if (escapable.includes(next)) {
153
+ read += next;
154
+ index++;
155
+ }
156
+ else {
157
+ read += char;
158
+ }
159
+ }
160
+ else if (char === "`" || (char === "$" && next !== undefined && EXPANSION_START.test(next))) {
161
+ known = false;
162
+ }
163
+ else if (state === "heredoc") {
164
+ read += char;
165
+ }
166
+ else if (state === "double") {
167
+ if (char === '"') {
168
+ state = "unquoted";
169
+ }
170
+ else {
171
+ read += char;
172
+ }
173
+ }
174
+ else if (char === "'") {
175
+ state = "single";
176
+ }
177
+ else if (char === '"') {
178
+ state = "double";
179
+ }
180
+ else if (UNQUOTED_BREAK.test(char)) {
181
+ return LOST;
182
+ }
183
+ else {
184
+ read += char;
185
+ }
186
+ }
187
+ if (state !== initial) {
188
+ return LOST;
189
+ }
190
+ return known ? read : null;
191
+ }
192
+ /** The script cannot be read: a syntax error, or a walk past its budget. */
193
+ class Unparsed extends Error {
194
+ }
195
+ // ---------------------------------------------------------------------------
196
+ // Lexer and parser
197
+ /** Reserved words another command follows on the same line. */
198
+ const COMMAND_PREFIXES = new Set(["!", "time"]);
199
+ /** Reserved words that end or divide a compound command, and never name a command. */
200
+ const CLOSING_WORDS = new Set(["then", "elif", "else", "fi", "do", "done", "esac", "}"]);
201
+ /** Characters that end an unquoted word. */
202
+ const WORD_BREAKS = new Set([" ", "\t", "\n", ";", "&", "|", "<", ">", "(", ")"]);
203
+ const REDIRECTION_OPERATORS = ["<<<", "<<-", "<<", "<&", "<>", "<", ">>", ">&", ">|", ">"];
204
+ const ASSIGNMENT_PREFIX = /^[A-Za-z_][A-Za-z0-9_]*\+?=/;
205
+ class ScriptParser {
206
+ src;
207
+ /** The quoted strings and heredoc bodies read so far. */
208
+ quoted = [];
209
+ pos = 0;
210
+ pending = [];
211
+ constructor(src) {
212
+ this.src = src;
213
+ }
214
+ parse() {
215
+ return this.parseList([]);
216
+ }
217
+ /**
218
+ * Commands up to one of `terminators`, which is left unread: a reserved
219
+ * word where a command would start, `)`, or `;;` for a case branch, which
220
+ * `;&` and `;;&` end too. With no terminator, the list runs to the end.
221
+ */
222
+ parseList(terminators) {
223
+ const items = [];
224
+ for (;;) {
225
+ this.skipLinebreaks();
226
+ if (this.atEnd()) {
227
+ if (terminators.length > 0) {
228
+ throw new Unparsed();
229
+ }
230
+ break;
231
+ }
232
+ if (this.atTerminator(terminators)) {
233
+ break;
234
+ }
235
+ const item = this.parseAndOr();
236
+ items.push(item);
237
+ this.skipBlanks();
238
+ if (this.startsWith("&") && !this.startsWith("&&")) {
239
+ item.background = true;
240
+ this.pos++;
241
+ }
242
+ else if (this.startsWith(";;") || this.startsWith(";&")) {
243
+ if (!terminators.includes(";;")) {
244
+ throw new Unparsed();
245
+ }
246
+ }
247
+ else if (this.startsWith(";")) {
248
+ this.pos++;
249
+ }
250
+ else if (!this.atEnd() &&
251
+ !["\n", "#"].includes(this.src[this.pos]) &&
252
+ !this.atTerminator(terminators)) {
253
+ throw new Unparsed();
254
+ }
255
+ }
256
+ return { items };
257
+ }
258
+ atTerminator(terminators) {
259
+ return terminators.some((terminator) => {
260
+ if (terminator === ")") {
261
+ return this.startsWith(")");
262
+ }
263
+ if (terminator === ";;") {
264
+ return this.startsWith(";;") || this.startsWith(";&");
265
+ }
266
+ return this.atReservedWord(terminator);
267
+ });
268
+ }
269
+ parseAndOr() {
270
+ const item = { pipelines: [this.parsePipeline()], operators: [], background: false };
271
+ for (;;) {
272
+ this.skipBlanks();
273
+ const operator = this.startsWith("&&") ? "&&" : this.startsWith("||") ? "||" : null;
274
+ if (operator === null) {
275
+ return item;
276
+ }
277
+ this.pos += 2;
278
+ this.skipLinebreaks();
279
+ item.operators.push(operator);
280
+ item.pipelines.push(this.parsePipeline());
281
+ }
282
+ }
283
+ parsePipeline() {
284
+ this.skipBlanks();
285
+ const negated = this.atReservedWord("!");
286
+ if (negated) {
287
+ this.pos++;
288
+ }
289
+ const commands = [this.parseCommand()];
290
+ for (;;) {
291
+ this.skipBlanks();
292
+ if (!this.startsWith("|") || this.startsWith("||")) {
293
+ break;
294
+ }
295
+ this.pos += this.startsWith("|&") ? 2 : 1;
296
+ this.skipLinebreaks();
297
+ commands.push(this.parseCommand());
298
+ }
299
+ return { commands, negated };
300
+ }
301
+ parseCommand() {
302
+ this.skipBlanks();
303
+ if (this.startsWith("((")) {
304
+ const command = this.emptyCommand();
305
+ this.pos += 2;
306
+ this.skipArithmetic();
307
+ command.end = this.pos;
308
+ return command;
309
+ }
310
+ if (this.startsWith("(")) {
311
+ this.pos++;
312
+ const body = this.parseList([")"]);
313
+ this.pos++;
314
+ return { kind: "subshell", body, tail: this.parseTail() };
315
+ }
316
+ if (this.atReservedWord("{")) {
317
+ this.pos++;
318
+ const body = this.parseList(["}"]);
319
+ this.pos++;
320
+ return { kind: "group", body, tail: this.parseTail() };
321
+ }
322
+ if (!this.atWordStart() || this.atRedirection()) {
323
+ const command = this.parseSimple(null);
324
+ if (command.kind === "simple" && command.end === command.start) {
325
+ throw new Unparsed(); // an operator where a command should be
326
+ }
327
+ return command;
328
+ }
329
+ const first = this.readWord();
330
+ const reserved = first.quoted || first.expands ? null : first.value;
331
+ if (reserved !== null && COMMAND_PREFIXES.has(reserved)) {
332
+ if (reserved === "time") {
333
+ this.skipBlanks();
334
+ if (/^-p(?![^\s;&|<>()])/.test(this.src.slice(this.pos, this.pos + 3))) {
335
+ this.pos += 2;
336
+ }
337
+ }
338
+ return this.parseCommand();
339
+ }
340
+ if (reserved !== null && CLOSING_WORDS.has(reserved)) {
341
+ throw new Unparsed();
342
+ }
343
+ switch (reserved) {
344
+ case "if":
345
+ return this.parseIf();
346
+ case "while":
347
+ case "until":
348
+ return this.parseWhile(reserved === "until");
349
+ case "for":
350
+ case "select":
351
+ return this.parseFor();
352
+ case "case":
353
+ return this.parseCase();
354
+ case "[[":
355
+ return this.parseConditional(first);
356
+ case "function":
357
+ this.skipBlanks();
358
+ if (!this.atWordStart()) {
359
+ throw new Unparsed();
360
+ }
361
+ this.readWord();
362
+ return this.parseFunctionBody();
363
+ }
364
+ return this.parseSimple(first);
365
+ }
366
+ parseIf() {
367
+ const clauses = [];
368
+ for (;;) {
369
+ const condition = this.parseList(["then"]);
370
+ this.readReserved("then");
371
+ clauses.push({ condition, body: this.parseList(["elif", "else", "fi"]) });
372
+ if (this.atReservedWord("elif")) {
373
+ this.readReserved("elif");
374
+ continue;
375
+ }
376
+ let otherwise = null;
377
+ if (this.atReservedWord("else")) {
378
+ this.readReserved("else");
379
+ otherwise = this.parseList(["fi"]);
380
+ }
381
+ this.readReserved("fi");
382
+ return { kind: "if", clauses, otherwise, tail: this.parseTail() };
383
+ }
384
+ }
385
+ parseWhile(until) {
386
+ const condition = this.parseList(["do"]);
387
+ this.readReserved("do");
388
+ const body = this.parseList(["done"]);
389
+ this.readReserved("done");
390
+ return { kind: "loop", head: null, condition, until, body, tail: this.parseTail() };
391
+ }
392
+ /** `for NAME [in WORDS]; do … done`, `for (( … )); do … done`, and `select` alike. */
393
+ parseFor() {
394
+ this.skipBlanks();
395
+ const head = this.emptyCommand();
396
+ if (this.startsWith("((")) {
397
+ this.pos += 2;
398
+ this.skipArithmetic();
399
+ head.end = this.pos;
400
+ }
401
+ else {
402
+ if (!this.atWordStart()) {
403
+ throw new Unparsed();
404
+ }
405
+ const name = this.readWord();
406
+ head.start = name.start;
407
+ head.end = name.end;
408
+ this.skipLinebreaks();
409
+ if (this.atReservedWord("in")) {
410
+ this.pos += 2;
411
+ for (;;) {
412
+ this.skipBlanks();
413
+ if (!this.atWordStart()) {
414
+ break;
415
+ }
416
+ addWord(head, this.readWord());
417
+ }
418
+ }
419
+ }
420
+ this.skipBlanks();
421
+ if (this.startsWith(";") && !this.startsWith(";;")) {
422
+ this.pos++;
423
+ }
424
+ this.skipLinebreaks();
425
+ this.readReserved("do");
426
+ const body = this.parseList(["done"]);
427
+ this.readReserved("done");
428
+ return { kind: "loop", head, condition: null, until: false, body, tail: this.parseTail() };
429
+ }
430
+ parseCase() {
431
+ this.skipBlanks();
432
+ if (!this.atWordStart()) {
433
+ throw new Unparsed();
434
+ }
435
+ const subject = this.emptyCommand();
436
+ addWord(subject, this.readWord());
437
+ this.skipLinebreaks();
438
+ this.readReserved("in");
439
+ const branches = [];
440
+ for (;;) {
441
+ this.skipLinebreaks();
442
+ if (this.atReservedWord("esac")) {
443
+ this.readReserved("esac");
444
+ return { kind: "case", subject, branches, tail: this.parseTail() };
445
+ }
446
+ const patterns = this.emptyCommand();
447
+ if (this.startsWith("(")) {
448
+ this.pos++;
449
+ }
450
+ for (;;) {
451
+ this.skipBlanks();
452
+ if (!this.atWordStart()) {
453
+ throw new Unparsed();
454
+ }
455
+ addWord(patterns, this.readWord());
456
+ this.skipBlanks();
457
+ if (this.startsWith(")")) {
458
+ this.pos++;
459
+ break;
460
+ }
461
+ if (!this.startsWith("|")) {
462
+ throw new Unparsed();
463
+ }
464
+ this.pos++;
465
+ }
466
+ const body = this.parseList([";;", "esac"]);
467
+ const terminator = [";;&", ";;", ";&"].find((candidate) => this.startsWith(candidate));
468
+ this.pos += terminator?.length ?? 0;
469
+ branches.push({ patterns, body, fallsThrough: terminator === ";;&" || terminator === ";&" });
470
+ }
471
+ }
472
+ /** The words and redirections of a simple command, `first` already read. */
473
+ parseSimple(first) {
474
+ const command = this.emptyCommand();
475
+ if (first) {
476
+ command.start = first.start;
477
+ addWord(command, first);
478
+ }
479
+ for (;;) {
480
+ this.skipBlanks();
481
+ if (this.atEnd()) {
482
+ break;
483
+ }
484
+ const char = this.src[this.pos];
485
+ if (char === "#") {
486
+ this.skipComment();
487
+ break;
488
+ }
489
+ if (this.startsWith("<(") || this.startsWith(">(")) {
490
+ const start = this.pos;
491
+ this.pos += 2;
492
+ const substitution = this.parseList([")"]);
493
+ this.pos++;
494
+ addWord(command, {
495
+ ...bareWord(start, this.pos),
496
+ expands: true,
497
+ substitutions: [substitution],
498
+ });
499
+ continue;
500
+ }
501
+ if (this.atRedirection()) {
502
+ this.readRedirection(command);
503
+ continue;
504
+ }
505
+ if (char === "(") {
506
+ if (command.words.length === 1 &&
507
+ /^\(\s*\)/.test(this.src.slice(this.pos, this.pos + 64))) {
508
+ return this.parseFunctionBody(); // NAME ( ) BODY
509
+ }
510
+ throw new Unparsed();
511
+ }
512
+ if (WORD_BREAKS.has(char)) {
513
+ break;
514
+ }
515
+ addWord(command, this.readWord());
516
+ }
517
+ return command;
518
+ }
519
+ /**
520
+ * A `[[ … ]]` conditional, `[[` already read, as one command. Inside it,
521
+ * parentheses, `<`, `>`, `&&`, `||` and `|` belong to the expression (a
522
+ * regex after `=~` among them), and a newline does not end it.
523
+ */
524
+ parseConditional(first) {
525
+ const command = this.emptyCommand();
526
+ command.start = first.start;
527
+ addWord(command, first);
528
+ for (;;) {
529
+ this.skipLinebreaks();
530
+ if (this.atEnd()) {
531
+ throw new Unparsed();
532
+ }
533
+ if (this.atReservedWord("]]")) {
534
+ addWord(command, this.readWord());
535
+ break;
536
+ }
537
+ const char = this.src[this.pos];
538
+ if (char === ";") {
539
+ throw new Unparsed();
540
+ }
541
+ if (WORD_BREAKS.has(char)) {
542
+ this.pos++;
543
+ addWord(command, bareWord(this.pos - 1, this.pos));
544
+ }
545
+ else {
546
+ addWord(command, this.readWord());
547
+ }
548
+ }
549
+ for (;;) {
550
+ this.skipBlanks();
551
+ if (!this.atRedirection()) {
552
+ return command;
553
+ }
554
+ this.readRedirection(command);
555
+ }
556
+ }
557
+ /** The redirections after a compound command's closing word, and nothing else. */
558
+ parseTail() {
559
+ const tail = this.emptyCommand();
560
+ for (;;) {
561
+ this.skipBlanks();
562
+ if (this.startsWith("<(") || this.startsWith(">(") || !this.atRedirection()) {
563
+ return tail;
564
+ }
565
+ this.readRedirection(tail);
566
+ }
567
+ }
568
+ /** The body of a function whose name was read, with its optional `()`. */
569
+ parseFunctionBody() {
570
+ this.skipBlanks();
571
+ const parentheses = /^\(\s*\)/.exec(this.src.slice(this.pos, this.pos + 64));
572
+ if (parentheses) {
573
+ this.pos += parentheses[0].length;
574
+ }
575
+ this.skipLinebreaks();
576
+ return { kind: "function", body: this.parseCommand() };
577
+ }
578
+ emptyCommand() {
579
+ return {
580
+ kind: "simple",
581
+ words: [],
582
+ start: this.pos,
583
+ end: this.pos,
584
+ heredocs: [],
585
+ substitutions: [],
586
+ };
587
+ }
588
+ readReserved(word) {
589
+ if (!this.atReservedWord(word)) {
590
+ throw new Unparsed();
591
+ }
592
+ this.pos += word.length;
593
+ }
594
+ atRedirection() {
595
+ return /^(?:\d*[<>]|&>)/.test(this.src.slice(this.pos, this.pos + 24));
596
+ }
597
+ readRedirection(command) {
598
+ while (/\d/.test(this.src[this.pos] ?? "")) {
599
+ this.pos++;
600
+ }
601
+ let operator;
602
+ if (this.startsWith("&>")) {
603
+ operator = this.startsWith("&>>") ? "&>>" : "&>";
604
+ }
605
+ else {
606
+ operator = REDIRECTION_OPERATORS.find((candidate) => this.startsWith(candidate));
607
+ }
608
+ this.pos += operator.length;
609
+ this.skipBlanks();
610
+ const heredoc = operator === "<<" || operator === "<<-";
611
+ if (!heredoc && (this.startsWith("<(") || this.startsWith(">("))) {
612
+ // A process substitution as the target, as in `done < <(find …)`.
613
+ this.pos += 2;
614
+ command.substitutions.push(this.parseList([")"]));
615
+ this.pos++;
616
+ command.end = this.pos;
617
+ return;
618
+ }
619
+ if (!this.atWordStart()) {
620
+ throw new Unparsed();
621
+ }
622
+ const target = this.readWord();
623
+ command.end = target.end;
624
+ command.substitutions.push(...target.substitutions);
625
+ if (heredoc) {
626
+ this.pending.push({
627
+ delimiter: target.value,
628
+ stripTabs: operator === "<<-",
629
+ quoted: target.quoted,
630
+ owner: command,
631
+ });
632
+ }
633
+ }
634
+ /** Past a newline, the bodies of the heredocs opened on the line it ends. */
635
+ consumeNewline() {
636
+ this.pos++;
637
+ for (const heredoc of this.pending.splice(0)) {
638
+ const bodyStart = this.pos;
639
+ let bodyEnd = this.src.length;
640
+ while (this.pos < this.src.length) {
641
+ const newline = this.src.indexOf("\n", this.pos);
642
+ const lineEnd = newline === -1 ? this.src.length : newline;
643
+ const next = newline === -1 ? this.src.length : newline + 1;
644
+ let line = this.src.slice(this.pos, lineEnd);
645
+ if (heredoc.stripTabs) {
646
+ line = line.replace(/^\t+/, "");
647
+ }
648
+ if (line === heredoc.delimiter) {
649
+ bodyEnd = this.pos;
650
+ this.pos = next;
651
+ break;
652
+ }
653
+ this.pos = next;
654
+ }
655
+ heredoc.owner.heredocs.push([bodyStart, bodyEnd]);
656
+ this.quoted.push({
657
+ start: bodyStart,
658
+ end: bodyEnd,
659
+ quoting: heredoc.quoted ? "verbatim" : "heredoc",
660
+ });
661
+ }
662
+ }
663
+ readWord() {
664
+ const word = bareWord(this.pos, this.pos);
665
+ while (!this.atEnd()) {
666
+ const char = this.src[this.pos];
667
+ if (WORD_BREAKS.has(char)) {
668
+ if (char === "(" &&
669
+ /^[A-Za-z_][A-Za-z0-9_]*\+?=$/.test(this.src.slice(word.start, this.pos))) {
670
+ this.skipArrayValue(); // NAME=( … )
671
+ word.expands = true;
672
+ continue;
673
+ }
674
+ break;
675
+ }
676
+ if (char === "\\") {
677
+ const escaped = this.src[this.pos + 1];
678
+ this.pos = Math.min(this.pos + 2, this.src.length);
679
+ if (escaped !== "\n") {
680
+ word.quoted = true;
681
+ word.value += escaped ?? "";
682
+ }
683
+ }
684
+ else if (char === "'") {
685
+ const close = this.src.indexOf("'", this.pos + 1);
686
+ if (close === -1) {
687
+ throw new Unparsed();
688
+ }
689
+ word.value += this.src.slice(this.pos + 1, close);
690
+ word.quoted = true;
691
+ this.quoted.push({ start: this.pos, end: close + 1, quoting: "single-quoted" });
692
+ this.pos = close + 1;
693
+ }
694
+ else if (char === '"') {
695
+ this.readDoubleQuoted(word);
696
+ }
697
+ else if (char === "$") {
698
+ this.readDollar(word, false);
699
+ }
700
+ else if (char === "`") {
701
+ this.skipBackticks();
702
+ word.expands = true;
703
+ }
704
+ else {
705
+ if ("*?[{}".includes(char)) {
706
+ word.pattern = true;
707
+ }
708
+ if ((char === "~" || char === "=") && this.pos === word.start) {
709
+ word.expandsHome = true;
710
+ }
711
+ word.value += char;
712
+ this.pos++;
713
+ }
714
+ }
715
+ word.end = this.pos;
716
+ word.assignment = ASSIGNMENT_PREFIX.test(this.src.slice(word.start, word.end));
717
+ return word;
718
+ }
719
+ readDoubleQuoted(word) {
720
+ word.quoted = true;
721
+ const start = this.pos;
722
+ this.pos++;
723
+ for (;;) {
724
+ if (this.atEnd()) {
725
+ throw new Unparsed();
726
+ }
727
+ const char = this.src[this.pos];
728
+ if (char === '"') {
729
+ this.pos++;
730
+ this.quoted.push({ start, end: this.pos, quoting: "double-quoted" });
731
+ return;
732
+ }
733
+ if (char === "\\") {
734
+ const escaped = this.src[this.pos + 1];
735
+ if (escaped === "\n") {
736
+ this.pos += 2;
737
+ }
738
+ else if (escaped !== undefined && '$`"\\'.includes(escaped)) {
739
+ word.value += escaped;
740
+ this.pos += 2;
741
+ }
742
+ else {
743
+ word.value += char;
744
+ this.pos++;
745
+ }
746
+ }
747
+ else if (char === "$") {
748
+ this.readDollar(word, true);
749
+ }
750
+ else if (char === "`") {
751
+ this.skipBackticks();
752
+ word.expands = true;
753
+ }
754
+ else {
755
+ word.value += char;
756
+ this.pos++;
757
+ }
758
+ }
759
+ }
760
+ /** A `$` construct: `$'…'`, `$"…"`, `$(( … ))`, `$( … )`, `${ … }`, or a parameter. */
761
+ readDollar(word, inDoubleQuotes) {
762
+ const next = this.src[this.pos + 1];
763
+ word.expands = true;
764
+ if (next === "'" && !inDoubleQuotes) {
765
+ this.pos += 2;
766
+ for (;;) {
767
+ if (this.atEnd()) {
768
+ throw new Unparsed();
769
+ }
770
+ const char = this.src[this.pos];
771
+ this.pos += char === "\\" ? 2 : 1;
772
+ if (char === "'") {
773
+ break;
774
+ }
775
+ word.value += char;
776
+ }
777
+ word.quoted = true;
778
+ }
779
+ else if (next === '"' && !inDoubleQuotes) {
780
+ this.pos++;
781
+ this.readDoubleQuoted(word);
782
+ }
783
+ else if (next === "(" && this.src[this.pos + 2] === "(") {
784
+ this.pos += 3;
785
+ this.skipArithmetic();
786
+ }
787
+ else if (next === "(") {
788
+ this.pos += 2;
789
+ word.substitutions.push(this.parseList([")"]));
790
+ this.pos++;
791
+ }
792
+ else if (next === "{") {
793
+ this.pos += 2;
794
+ this.skipParameterExpansion(word);
795
+ }
796
+ else {
797
+ word.value += "$";
798
+ this.pos++;
799
+ }
800
+ }
801
+ /** Past the closing `}` of a `${ … }`, collecting the substitutions inside it. */
802
+ skipParameterExpansion(word) {
803
+ for (;;) {
804
+ if (this.atEnd()) {
805
+ throw new Unparsed();
806
+ }
807
+ const char = this.src[this.pos];
808
+ if (char === "}") {
809
+ this.pos++;
810
+ return;
811
+ }
812
+ if (char === "\\") {
813
+ this.pos += 2;
814
+ }
815
+ else if (char === "'") {
816
+ const close = this.src.indexOf("'", this.pos + 1);
817
+ if (close === -1) {
818
+ throw new Unparsed();
819
+ }
820
+ this.pos = close + 1;
821
+ }
822
+ else if (char === '"') {
823
+ const inner = bareWord(this.pos, this.pos);
824
+ this.readDoubleQuoted(inner);
825
+ word.substitutions.push(...inner.substitutions);
826
+ }
827
+ else if (char === "$") {
828
+ const inner = bareWord(this.pos, this.pos);
829
+ this.readDollar(inner, true);
830
+ word.substitutions.push(...inner.substitutions);
831
+ }
832
+ else if (char === "`") {
833
+ this.skipBackticks();
834
+ }
835
+ else {
836
+ this.pos++;
837
+ }
838
+ }
839
+ }
840
+ /** Past the `))` closing an arithmetic expansion or command whose `((` was read. */
841
+ skipArithmetic() {
842
+ let depth = 2;
843
+ while (depth > 0) {
844
+ if (this.atEnd()) {
845
+ throw new Unparsed();
846
+ }
847
+ const char = this.src[this.pos];
848
+ if (char === "\\") {
849
+ this.pos++;
850
+ }
851
+ else if (char === "(") {
852
+ depth++;
853
+ }
854
+ else if (char === ")") {
855
+ depth--;
856
+ }
857
+ this.pos++;
858
+ }
859
+ }
860
+ /** Past the `)` closing an array assignment's value list. */
861
+ skipArrayValue() {
862
+ this.pos++;
863
+ for (;;) {
864
+ this.skipLinebreaks();
865
+ if (this.atEnd()) {
866
+ throw new Unparsed();
867
+ }
868
+ if (this.src[this.pos] === ")") {
869
+ this.pos++;
870
+ return;
871
+ }
872
+ if (!this.atWordStart()) {
873
+ throw new Unparsed();
874
+ }
875
+ this.readWord();
876
+ }
877
+ }
878
+ /**
879
+ * Past a backtick substitution. Its commands run in a subshell, so a `cd`
880
+ * inside cannot move the script, and it is not read; a heredoc inside it
881
+ * would need that reading, so the script is unparsed.
882
+ */
883
+ skipBackticks() {
884
+ const start = this.pos + 1;
885
+ this.pos++;
886
+ for (;;) {
887
+ if (this.atEnd()) {
888
+ throw new Unparsed();
889
+ }
890
+ const char = this.src[this.pos];
891
+ if (char === "`") {
892
+ break;
893
+ }
894
+ this.pos += char === "\\" ? 2 : 1;
895
+ }
896
+ const body = this.src.slice(start, this.pos);
897
+ this.pos++;
898
+ if (/(?<!<)<<(?!<)/.test(body)) {
899
+ throw new Unparsed();
900
+ }
901
+ }
902
+ skipBlanks() {
903
+ for (;;) {
904
+ const char = this.src[this.pos];
905
+ if (char === " " || char === "\t") {
906
+ this.pos++;
907
+ }
908
+ else if (char === "\\" && this.src[this.pos + 1] === "\n") {
909
+ this.pos += 2;
910
+ }
911
+ else {
912
+ return;
913
+ }
914
+ }
915
+ }
916
+ skipComment() {
917
+ const newline = this.src.indexOf("\n", this.pos);
918
+ this.pos = newline === -1 ? this.src.length : newline;
919
+ }
920
+ /** Blanks, comments and newlines, reading heredoc bodies at each newline. */
921
+ skipLinebreaks() {
922
+ for (;;) {
923
+ this.skipBlanks();
924
+ const char = this.src[this.pos];
925
+ if (char === "#") {
926
+ this.skipComment();
927
+ }
928
+ else if (char === "\n") {
929
+ this.consumeNewline();
930
+ }
931
+ else {
932
+ return;
933
+ }
934
+ }
935
+ }
936
+ atWordStart() {
937
+ const char = this.src[this.pos];
938
+ return char !== undefined && char !== "#" && !WORD_BREAKS.has(char);
939
+ }
940
+ /** A reserved word (`{`, `fi`, `done`, …) standing alone at the current position. */
941
+ atReservedWord(reserved) {
942
+ if (!this.startsWith(reserved)) {
943
+ return false;
944
+ }
945
+ const after = this.src[this.pos + reserved.length];
946
+ return after === undefined || WORD_BREAKS.has(after);
947
+ }
948
+ startsWith(text) {
949
+ return this.src.startsWith(text, this.pos);
950
+ }
951
+ atEnd() {
952
+ return this.pos >= this.src.length;
953
+ }
954
+ }
955
+ function bareWord(start, end) {
956
+ return {
957
+ value: "",
958
+ start,
959
+ end,
960
+ quoted: false,
961
+ expands: false,
962
+ pattern: false,
963
+ expandsHome: false,
964
+ assignment: false,
965
+ substitutions: [],
966
+ };
967
+ }
968
+ function isReservedWord(word, reserved) {
969
+ return !word.quoted && !word.expands && reserved.has(word.value);
970
+ }
971
+ function addWord(command, word) {
972
+ command.words.push(word);
973
+ command.substitutions.push(...word.substitutions);
974
+ command.end = word.end;
975
+ }
976
+ // ---------------------------------------------------------------------------
977
+ // The directory walk
978
+ /**
979
+ * A directory the reading cannot tell. Each one differs from every other, so
980
+ * the walk can see that a command moved the shell even when it knows neither
981
+ * where from nor where to.
982
+ */
983
+ class UnknownDirectory {
984
+ }
985
+ const UNKNOWN_EFFECT_COMMANDS = new Set(["pushd", "popd", "eval", "source", "."]);
986
+ /**
987
+ * The longest directory the walk follows. `cd` itself refuses a longer path
988
+ * on every system, and the bound keeps a script of chained relative `cd`s
989
+ * linear to read.
990
+ */
991
+ const PATH_MAX = 4096;
992
+ /** The names `apply_patch` is run by, as Codex puts it on the `PATH`. */
993
+ const PATCH_COMMANDS = new Set(["apply_patch", "applypatch"]);
994
+ /**
995
+ * How many commands a walk may visit: a floor, plus a number per character of
996
+ * the script. A loop whose passes start in different directories is walked
997
+ * twice, so loops nested deep enough could otherwise cost time exponential in
998
+ * their depth; a script past the budget is unparsed.
999
+ */
1000
+ const WALK_BUDGET_FLOOR = 100_000;
1001
+ const WALK_BUDGET_PER_CHARACTER = 16;
1002
+ class DirectoryWalk {
1003
+ budget;
1004
+ extents = [];
1005
+ /** The directory of the patch command reading the text being walked, if one does. */
1006
+ reader;
1007
+ steps = 0;
1008
+ constructor(budget) {
1009
+ this.budget = budget;
1010
+ }
1011
+ list(list, directory) {
1012
+ return this.run(list, directory).after;
1013
+ }
1014
+ /** A list, with the status its last and-or list exits with. */
1015
+ run(list, directory) {
1016
+ let outcome = { after: directory, status: "success", moved: false };
1017
+ for (const item of list.items) {
1018
+ const next = this.andOr(item, outcome.after);
1019
+ // A list sent to the background runs in a subshell, in bash and zsh
1020
+ // alike, and the shell goes on at once, with success.
1021
+ outcome = item.background ? { ...outcome, status: "success", moved: false } : next;
1022
+ }
1023
+ return outcome;
1024
+ }
1025
+ andOr(item, directory) {
1026
+ let outcome = this.pipeline(item.pipelines[0], directory);
1027
+ item.operators.forEach((operator, index) => {
1028
+ // `&&` runs the pipeline after it when what came before succeeded, `||` when it failed.
1029
+ const on = operator === "&&" ? "success" : "failure";
1030
+ const next = this.pipeline(item.pipelines[index + 1], branchStart(outcome, on));
1031
+ if (outcome.status === on) {
1032
+ outcome = { ...next, moved: outcome.moved || next.moved };
1033
+ }
1034
+ else if (next.moved) {
1035
+ // It runs off the followed path. When it does not move the shell
1036
+ // (`cd sub || exit 1`), the directory is the one the followed path
1037
+ // reached; when it does, either may be the one in effect after it.
1038
+ outcome = { ...outcome, after: new UnknownDirectory() };
1039
+ }
1040
+ });
1041
+ return outcome;
1042
+ }
1043
+ /**
1044
+ * Every member of a pipeline starts in the directory the pipeline starts
1045
+ * in. All but the last run in subshells; zsh runs the last in the current
1046
+ * shell and bash does not, so a directory change there is unknown after it.
1047
+ * When a member is a patch command, the other members feed it.
1048
+ */
1049
+ pipeline(pipeline, directory) {
1050
+ const after = this.members(pipeline.commands, directory);
1051
+ return {
1052
+ after,
1053
+ status: pipeline.negated ? "failure" : "success",
1054
+ moved: after !== directory,
1055
+ };
1056
+ }
1057
+ members(commands, directory) {
1058
+ if (commands.length === 1) {
1059
+ return this.command(commands[0], directory);
1060
+ }
1061
+ const reader = this.reader;
1062
+ if (commands.some((command) => command.kind === "simple" && isPatchCommand(command.words))) {
1063
+ this.reader = directory;
1064
+ }
1065
+ let last = directory;
1066
+ for (const command of commands) {
1067
+ last = this.command(command, directory);
1068
+ }
1069
+ this.reader = reader;
1070
+ return last === directory ? directory : new UnknownDirectory();
1071
+ }
1072
+ command(command, directory) {
1073
+ switch (command.kind) {
1074
+ case "simple":
1075
+ return this.simple(command, directory);
1076
+ case "subshell":
1077
+ this.text(command.tail, directory, false);
1078
+ this.list(command.body, directory);
1079
+ return directory;
1080
+ case "group":
1081
+ this.text(command.tail, directory, false);
1082
+ return this.list(command.body, directory);
1083
+ case "if":
1084
+ return this.ifCommand(command, directory);
1085
+ case "case":
1086
+ return this.caseCommand(command, directory);
1087
+ case "loop":
1088
+ return this.loop(command, directory);
1089
+ case "function":
1090
+ // The body runs whenever the function is called, from wherever.
1091
+ this.command(command.body, new UnknownDirectory());
1092
+ return new UnknownDirectory();
1093
+ }
1094
+ }
1095
+ /** Each condition runs after the ones before it failed; one body, or none, runs. */
1096
+ ifCommand(command, directory) {
1097
+ this.text(command.tail, directory, false);
1098
+ return this.clauses(command.clauses, command.otherwise, directory);
1099
+ }
1100
+ /**
1101
+ * The first clause, and the rest (the `elif` clauses after it and the `else`
1102
+ * body), which runs when the first clause's condition fails, as in `if C;
1103
+ * then BODY; else REST; fi`. When the condition does not move the shell,
1104
+ * both paths start where it ends. When it does, the one it takes off the
1105
+ * followed path starts from an unknown directory and, as after `||`, keeps
1106
+ * the directory the other reached unless it moves the shell itself.
1107
+ */
1108
+ clauses(clauses, otherwise, directory) {
1109
+ const [clause, ...rest] = clauses;
1110
+ if (!clause) {
1111
+ return otherwise ? this.list(otherwise, directory) : directory;
1112
+ }
1113
+ const checked = this.run(clause.condition, directory);
1114
+ const body = (start) => this.list(clause.body, start);
1115
+ const remainder = (start) => this.clauses(rest, otherwise, start);
1116
+ if (!checked.moved) {
1117
+ return common([body(checked.after), remainder(checked.after)]);
1118
+ }
1119
+ const followed = checked.status === "success" ? body : remainder;
1120
+ const other = checked.status === "success" ? remainder : body;
1121
+ const end = followed(checked.after);
1122
+ const start = new UnknownDirectory();
1123
+ return other(start) === start ? end : new UnknownDirectory();
1124
+ }
1125
+ /** One branch runs, or none; a branch fallen into may start where the one before ended. */
1126
+ caseCommand(command, directory) {
1127
+ this.text(command.tail, directory, false);
1128
+ this.text(command.subject, directory, false);
1129
+ const ends = [directory];
1130
+ let fallenFrom = null;
1131
+ for (const branch of command.branches) {
1132
+ this.text(branch.patterns, directory, false);
1133
+ const start = fallenFrom === null ? directory : common([directory, fallenFrom]);
1134
+ const end = this.list(branch.body, start);
1135
+ ends.push(end);
1136
+ fallenFrom = branch.fallsThrough ? end : null;
1137
+ }
1138
+ return common(ends);
1139
+ }
1140
+ /**
1141
+ * A loop runs its body any number of times. When a pass ends where it
1142
+ * started, every pass does. Otherwise the passes after the first start
1143
+ * elsewhere, so the loop is walked again from an unknown directory, whose
1144
+ * placements then stand for every pass.
1145
+ */
1146
+ loop(command, directory) {
1147
+ this.text(command.tail, directory, false);
1148
+ if (command.head) {
1149
+ this.text(command.head, directory, false);
1150
+ }
1151
+ const mark = this.extents.length;
1152
+ const first = this.pass(command, directory);
1153
+ if (first.next === directory) {
1154
+ return first.exit;
1155
+ }
1156
+ if (directory instanceof UnknownDirectory) {
1157
+ // From one unknown directory, the walk places text as it would from any other.
1158
+ return new UnknownDirectory();
1159
+ }
1160
+ this.extents.length = mark;
1161
+ const later = this.pass(command, new UnknownDirectory());
1162
+ return common([first.exit, later.exit]);
1163
+ }
1164
+ /**
1165
+ * One pass of a loop from `start`: where the next pass starts, and where
1166
+ * the loop ends if it ends instead, once a `for` loop's words run out, or
1167
+ * on the status of its condition that ends it: failure for `while`,
1168
+ * success for `until`.
1169
+ */
1170
+ pass(command, start) {
1171
+ if (!command.condition) {
1172
+ return { next: this.list(command.body, start), exit: start };
1173
+ }
1174
+ const checked = this.run(command.condition, start);
1175
+ const [runs, ends] = command.until
1176
+ ? ["failure", "success"]
1177
+ : ["success", "failure"];
1178
+ return {
1179
+ next: this.list(command.body, branchStart(checked, runs)),
1180
+ exit: branchStart(checked, ends),
1181
+ };
1182
+ }
1183
+ simple(command, directory) {
1184
+ this.text(command, directory, isPatchCommand(command.words));
1185
+ const effect = directoryEffect(command.words);
1186
+ switch (effect.kind) {
1187
+ case "none":
1188
+ return directory;
1189
+ case "unknown":
1190
+ return new UnknownDirectory();
1191
+ case "cd": {
1192
+ let moved;
1193
+ if (isAbsolute(effect.operand)) {
1194
+ moved = resolve(effect.operand);
1195
+ }
1196
+ else if (typeof directory === "string") {
1197
+ moved = resolve(directory, effect.operand);
1198
+ }
1199
+ else {
1200
+ return new UnknownDirectory();
1201
+ }
1202
+ return moved.length > PATH_MAX ? new UnknownDirectory() : moved;
1203
+ }
1204
+ }
1205
+ }
1206
+ /**
1207
+ * Place a command's words and heredoc bodies, after walking the
1208
+ * substitutions in them, which run first and in the same directory. A patch
1209
+ * command reads its own text and what its substitutions print; any other
1210
+ * command's text is read by the patch command it feeds, if there is one.
1211
+ */
1212
+ text(command, directory, patchCommand) {
1213
+ if (++this.steps > this.budget) {
1214
+ throw new Unparsed();
1215
+ }
1216
+ const reader = this.reader;
1217
+ if (patchCommand) {
1218
+ this.reader = directory;
1219
+ }
1220
+ for (const substitution of command.substitutions) {
1221
+ this.list(substitution, directory);
1222
+ }
1223
+ const read = this.reader !== undefined;
1224
+ const placed = typeof this.reader === "string" ? this.reader : null;
1225
+ this.reader = reader;
1226
+ if (command.end > command.start) {
1227
+ this.extents.push({ start: command.start, end: command.end, read, directory: placed });
1228
+ }
1229
+ for (const [start, end] of command.heredocs) {
1230
+ this.extents.push({ start, end, read, directory: placed });
1231
+ }
1232
+ }
1233
+ }
1234
+ /**
1235
+ * Where a path taken when a list exits with `on` starts: where the list left
1236
+ * the shell when that is the followed path's status or the list never moved
1237
+ * the shell, and an unknown directory otherwise, since the list may have
1238
+ * failed anywhere after it moved.
1239
+ */
1240
+ function branchStart(outcome, on) {
1241
+ return outcome.status === on || !outcome.moved ? outcome.after : new UnknownDirectory();
1242
+ }
1243
+ /** The directory every path leaves the shell in, or an unknown one when they differ. */
1244
+ function common(ends) {
1245
+ return ends.every((end) => end === ends[0]) ? ends[0] : new UnknownDirectory();
1246
+ }
1247
+ /** The index of the word naming the command run, past assignments and `builtin` or `command`. */
1248
+ function commandIndex(words) {
1249
+ let index = 0;
1250
+ while (words[index]?.assignment) {
1251
+ index++;
1252
+ }
1253
+ while (words[index] &&
1254
+ isReservedWord(words[index], new Set(["builtin", "command"])) &&
1255
+ !words[index + 1]?.value.startsWith("-")) {
1256
+ index++;
1257
+ }
1258
+ return index;
1259
+ }
1260
+ function isPatchCommand(words) {
1261
+ const name = words[commandIndex(words)];
1262
+ return name !== undefined && !name.expands && PATCH_COMMANDS.has(basename(name.value));
1263
+ }
1264
+ /** What running a simple command does to the shell's working directory. */
1265
+ function directoryEffect(words) {
1266
+ const index = commandIndex(words);
1267
+ const name = words[index];
1268
+ if (!name || name.expands) {
1269
+ return { kind: "none" };
1270
+ }
1271
+ if (name.value === "cd") {
1272
+ return cdEffect(words.slice(index + 1));
1273
+ }
1274
+ if (UNKNOWN_EFFECT_COMMANDS.has(name.value)) {
1275
+ return { kind: "unknown" };
1276
+ }
1277
+ return { kind: "none" };
1278
+ }
1279
+ function cdEffect(args) {
1280
+ let index = 0;
1281
+ while (args[index] && !args[index].expands && ["-L", "-P"].includes(args[index].value)) {
1282
+ index++;
1283
+ }
1284
+ let endOfOptions = false;
1285
+ if (args[index] && !args[index].expands && args[index].value === "--") {
1286
+ endOfOptions = true;
1287
+ index++;
1288
+ }
1289
+ const operands = args.slice(index);
1290
+ if (operands.length !== 1) {
1291
+ return { kind: "unknown" }; // `cd` alone goes home; zsh's `cd OLD NEW` substitutes
1292
+ }
1293
+ const operand = operands[0];
1294
+ if (operand.expands ||
1295
+ operand.pattern ||
1296
+ operand.expandsHome ||
1297
+ operand.value === "" ||
1298
+ (operand.value.startsWith("-") && !endOfOptions) ||
1299
+ /^\+\d+$/.test(operand.value)) {
1300
+ return { kind: "unknown" };
1301
+ }
1302
+ return { kind: "cd", operand: operand.value };
1303
+ }
1304
+ //# sourceMappingURL=shell-script.js.map