shell-dsl 0.0.39 → 0.0.41

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 (85) hide show
  1. package/README.md +183 -3
  2. package/dist/cjs/package.json +1 -1
  3. package/dist/cjs/src/commands/exit/exit.cjs +84 -0
  4. package/dist/cjs/src/commands/exit/exit.cjs.map +10 -0
  5. package/dist/cjs/src/commands/index.cjs +18 -2
  6. package/dist/cjs/src/commands/index.cjs.map +3 -3
  7. package/dist/cjs/src/commands/sh/sh.cjs +134 -0
  8. package/dist/cjs/src/commands/sh/sh.cjs.map +10 -0
  9. package/dist/cjs/src/index.cjs +9 -1
  10. package/dist/cjs/src/index.cjs.map +3 -3
  11. package/dist/cjs/src/input-analysis.cjs +154 -0
  12. package/dist/cjs/src/input-analysis.cjs.map +10 -0
  13. package/dist/cjs/src/interpreter/context.cjs +6 -1
  14. package/dist/cjs/src/interpreter/context.cjs.map +3 -3
  15. package/dist/cjs/src/interpreter/index.cjs +2 -1
  16. package/dist/cjs/src/interpreter/index.cjs.map +3 -3
  17. package/dist/cjs/src/interpreter/interpreter.cjs +434 -82
  18. package/dist/cjs/src/interpreter/interpreter.cjs.map +3 -3
  19. package/dist/cjs/src/io/async-queue.cjs +105 -0
  20. package/dist/cjs/src/io/async-queue.cjs.map +10 -0
  21. package/dist/cjs/src/io/index.cjs +4 -1
  22. package/dist/cjs/src/io/index.cjs.map +3 -3
  23. package/dist/cjs/src/io/input-controller.cjs +113 -0
  24. package/dist/cjs/src/io/input-controller.cjs.map +10 -0
  25. package/dist/cjs/src/io/stdout.cjs +9 -6
  26. package/dist/cjs/src/io/stdout.cjs.map +3 -3
  27. package/dist/cjs/src/lexer/lexer.cjs +13 -1
  28. package/dist/cjs/src/lexer/lexer.cjs.map +3 -3
  29. package/dist/cjs/src/parser/parser.cjs +11 -1
  30. package/dist/cjs/src/parser/parser.cjs.map +3 -3
  31. package/dist/cjs/src/shell-dsl.cjs +13 -5
  32. package/dist/cjs/src/shell-dsl.cjs.map +3 -3
  33. package/dist/cjs/src/shell-session.cjs +128 -0
  34. package/dist/cjs/src/shell-session.cjs.map +10 -0
  35. package/dist/cjs/src/types.cjs.map +2 -2
  36. package/dist/mjs/package.json +1 -1
  37. package/dist/mjs/src/commands/exit/exit.mjs +44 -0
  38. package/dist/mjs/src/commands/exit/exit.mjs.map +10 -0
  39. package/dist/mjs/src/commands/index.mjs +18 -2
  40. package/dist/mjs/src/commands/index.mjs.map +3 -3
  41. package/dist/mjs/src/commands/sh/sh.mjs +94 -0
  42. package/dist/mjs/src/commands/sh/sh.mjs.map +10 -0
  43. package/dist/mjs/src/index.mjs +19 -3
  44. package/dist/mjs/src/index.mjs.map +3 -3
  45. package/dist/mjs/src/input-analysis.mjs +114 -0
  46. package/dist/mjs/src/input-analysis.mjs.map +10 -0
  47. package/dist/mjs/src/interpreter/context.mjs +6 -1
  48. package/dist/mjs/src/interpreter/context.mjs.map +3 -3
  49. package/dist/mjs/src/interpreter/index.mjs +3 -2
  50. package/dist/mjs/src/interpreter/index.mjs.map +2 -2
  51. package/dist/mjs/src/interpreter/interpreter.mjs +434 -82
  52. package/dist/mjs/src/interpreter/interpreter.mjs.map +3 -3
  53. package/dist/mjs/src/io/async-queue.mjs +64 -0
  54. package/dist/mjs/src/io/async-queue.mjs.map +10 -0
  55. package/dist/mjs/src/io/index.mjs +4 -1
  56. package/dist/mjs/src/io/index.mjs.map +3 -3
  57. package/dist/mjs/src/io/input-controller.mjs +72 -0
  58. package/dist/mjs/src/io/input-controller.mjs.map +10 -0
  59. package/dist/mjs/src/io/stdout.mjs +9 -6
  60. package/dist/mjs/src/io/stdout.mjs.map +3 -3
  61. package/dist/mjs/src/lexer/lexer.mjs +13 -1
  62. package/dist/mjs/src/lexer/lexer.mjs.map +3 -3
  63. package/dist/mjs/src/parser/parser.mjs +11 -1
  64. package/dist/mjs/src/parser/parser.mjs.map +3 -3
  65. package/dist/mjs/src/shell-dsl.mjs +13 -5
  66. package/dist/mjs/src/shell-dsl.mjs.map +3 -3
  67. package/dist/mjs/src/shell-session.mjs +88 -0
  68. package/dist/mjs/src/shell-session.mjs.map +10 -0
  69. package/dist/mjs/src/types.mjs.map +2 -2
  70. package/dist/types/src/commands/exit/exit.d.ts +2 -0
  71. package/dist/types/src/commands/index.d.ts +2 -0
  72. package/dist/types/src/commands/sh/sh.d.ts +5 -0
  73. package/dist/types/src/index.d.ts +6 -3
  74. package/dist/types/src/input-analysis.d.ts +14 -0
  75. package/dist/types/src/interpreter/context.d.ts +4 -1
  76. package/dist/types/src/interpreter/index.d.ts +1 -1
  77. package/dist/types/src/interpreter/interpreter.d.ts +36 -1
  78. package/dist/types/src/io/async-queue.d.ts +12 -0
  79. package/dist/types/src/io/index.d.ts +1 -0
  80. package/dist/types/src/io/input-controller.d.ts +15 -0
  81. package/dist/types/src/io/stdout.d.ts +4 -3
  82. package/dist/types/src/shell-dsl.d.ts +2 -0
  83. package/dist/types/src/shell-session.d.ts +23 -0
  84. package/dist/types/src/types.d.ts +52 -0
  85. package/package.json +1 -1
@@ -1,7 +1,10 @@
1
1
  // src/interpreter/interpreter.ts
2
2
  import { createCommandContext } from "./context.mjs";
3
+ import { Lexer } from "../lexer/lexer.mjs";
4
+ import { Parser } from "../parser/parser.mjs";
3
5
  import { createStdin } from "../io/stdin.mjs";
4
6
  import { createStdout, createStderr, createPipe, createBufferTargetCollector } from "../io/stdout.mjs";
7
+ import { AsyncQueue } from "../io/async-queue.mjs";
5
8
  import { isDevNullPath } from "../fs/special-files.mjs";
6
9
  var DEFAULT_IFS = `
7
10
  `;
@@ -23,6 +26,14 @@ class ContinueException extends Error {
23
26
  }
24
27
  }
25
28
 
29
+ class ExitException extends Error {
30
+ exitCode;
31
+ constructor(exitCode) {
32
+ super("exit");
33
+ this.exitCode = exitCode;
34
+ }
35
+ }
36
+
26
37
  class Interpreter {
27
38
  fs;
28
39
  cwd;
@@ -31,54 +42,154 @@ class Interpreter {
31
42
  redirectObjects;
32
43
  loopDepth = 0;
33
44
  isTTY;
45
+ terminal;
46
+ activeSignal;
47
+ externalCommand;
48
+ argv0;
49
+ positionalParameters;
50
+ lastExitCode;
34
51
  constructor(options) {
35
52
  this.fs = options.fs;
36
53
  this.cwd = options.cwd;
37
54
  this.env = { ...options.env };
38
55
  this.commands = options.commands;
39
56
  this.redirectObjects = options.redirectObjects ?? {};
40
- this.isTTY = options.isTTY ?? false;
57
+ this.terminal = options.terminal ?? { isTTY: options.isTTY ?? false };
58
+ this.isTTY = this.terminal.isTTY;
59
+ this.activeSignal = options.signal ?? new AbortController().signal;
60
+ this.externalCommand = options.externalCommand;
61
+ this.argv0 = options.argv0 ?? "sh";
62
+ this.positionalParameters = [...options.positionalParameters ?? []];
63
+ this.lastExitCode = options.lastExitCode ?? 0;
41
64
  }
42
65
  getLoopDepth() {
43
66
  return this.loopDepth;
44
67
  }
45
68
  async execute(ast) {
46
- const stdout = createStdout(this.isTTY);
47
- const stderr = createStderr(this.isTTY);
48
- const exitCode = await this.executeNode(ast, null, stdout, stderr);
49
- stdout.close();
50
- stderr.close();
69
+ return this.executeStreaming(ast).exit;
70
+ }
71
+ executeStreaming(ast, options = {}) {
72
+ const terminal = options.terminal ?? this.terminal;
73
+ const controller = new AbortController;
74
+ const eventQueue = new AsyncQueue;
75
+ if (options.signal) {
76
+ if (options.signal.aborted) {
77
+ controller.abort(options.signal.reason);
78
+ } else {
79
+ options.signal.addEventListener("abort", () => controller.abort(options.signal?.reason), { once: true });
80
+ }
81
+ }
82
+ const stdout = createStdout(terminal.isTTY, async (chunk) => {
83
+ eventQueue.push({ fd: 1, chunk });
84
+ await options.stdout?.write(chunk);
85
+ });
86
+ const stderr = createStderr(terminal.isTTY, async (chunk) => {
87
+ eventQueue.push({ fd: 2, chunk });
88
+ await options.stderr?.write(chunk);
89
+ });
90
+ const previousSignal = this.activeSignal;
91
+ const previousTerminal = this.terminal;
92
+ const previousIsTTY = this.isTTY;
93
+ const stdinSource = this.normalizeInputSource(options.stdin);
94
+ const exit = (async () => {
95
+ this.activeSignal = controller.signal;
96
+ this.terminal = terminal;
97
+ this.isTTY = terminal.isTTY;
98
+ let exitCode;
99
+ try {
100
+ this.throwIfAborted();
101
+ exitCode = await this.executeNode(ast, stdinSource, stdout, stderr);
102
+ } catch (err) {
103
+ if (err instanceof ExitException) {
104
+ exitCode = err.exitCode;
105
+ } else if (controller.signal.aborted) {
106
+ exitCode = 130;
107
+ } else {
108
+ throw err;
109
+ }
110
+ } finally {
111
+ stdout.close();
112
+ stderr.close();
113
+ eventQueue.close();
114
+ this.activeSignal = previousSignal;
115
+ this.terminal = previousTerminal;
116
+ this.isTTY = previousIsTTY;
117
+ }
118
+ this.lastExitCode = exitCode;
119
+ return {
120
+ stdout: await stdout.collect(),
121
+ stderr: await stderr.collect(),
122
+ exitCode
123
+ };
124
+ })();
51
125
  return {
52
- stdout: await stdout.collect(),
53
- stderr: await stderr.collect(),
54
- exitCode
126
+ stdout: stdout.getReadableStream(),
127
+ stderr: stderr.getReadableStream(),
128
+ output: eventQueue,
129
+ exit,
130
+ kill: (reason) => controller.abort(reason ?? new Error("Shell execution killed"))
55
131
  };
56
132
  }
133
+ normalizeInputSource(source) {
134
+ if (source === undefined || source === null) {
135
+ return null;
136
+ }
137
+ if (typeof source === "string") {
138
+ return async function* () {
139
+ yield new TextEncoder().encode(source);
140
+ }();
141
+ }
142
+ if (Buffer.isBuffer(source)) {
143
+ return async function* () {
144
+ yield new Uint8Array(source);
145
+ }();
146
+ }
147
+ return source;
148
+ }
57
149
  async executeNode(node, stdinSource, stdout, stderr) {
150
+ this.throwIfAborted();
151
+ let exitCode;
58
152
  switch (node.type) {
59
153
  case "command":
60
- return this.executeCommand(node, stdinSource, stdout, stderr);
154
+ exitCode = await this.executeCommand(node, stdinSource, stdout, stderr);
155
+ break;
61
156
  case "pipeline":
62
- return this.executePipeline(node.commands, stdinSource, stdout, stderr);
157
+ exitCode = await this.executePipeline(node.commands, stdinSource, stdout, stderr);
158
+ break;
63
159
  case "sequence":
64
- return this.executeSequence(node.commands, stdinSource, stdout, stderr);
160
+ exitCode = await this.executeSequence(node.commands, stdinSource, stdout, stderr);
161
+ break;
65
162
  case "and":
66
- return this.executeAnd(node.left, node.right, stdinSource, stdout, stderr);
163
+ exitCode = await this.executeAnd(node.left, node.right, stdinSource, stdout, stderr);
164
+ break;
67
165
  case "or":
68
- return this.executeOr(node.left, node.right, stdinSource, stdout, stderr);
166
+ exitCode = await this.executeOr(node.left, node.right, stdinSource, stdout, stderr);
167
+ break;
69
168
  case "if":
70
- return this.executeIf(node, stdinSource, stdout, stderr);
169
+ exitCode = await this.executeIf(node, stdinSource, stdout, stderr);
170
+ break;
71
171
  case "for":
72
- return this.executeFor(node, stdinSource, stdout, stderr);
172
+ exitCode = await this.executeFor(node, stdinSource, stdout, stderr);
173
+ break;
73
174
  case "while":
74
- return this.executeWhile(node, stdinSource, stdout, stderr);
175
+ exitCode = await this.executeWhile(node, stdinSource, stdout, stderr);
176
+ break;
75
177
  case "until":
76
- return this.executeUntil(node, stdinSource, stdout, stderr);
178
+ exitCode = await this.executeUntil(node, stdinSource, stdout, stderr);
179
+ break;
77
180
  case "case":
78
- return this.executeCase(node, stdinSource, stdout, stderr);
181
+ exitCode = await this.executeCase(node, stdinSource, stdout, stderr);
182
+ break;
79
183
  default:
80
184
  throw new Error("Cannot execute unknown node type");
81
185
  }
186
+ this.lastExitCode = exitCode;
187
+ return exitCode;
188
+ }
189
+ throwIfAborted() {
190
+ if (this.activeSignal.aborted) {
191
+ throw this.activeSignal.reason ?? new Error("Shell execution aborted");
192
+ }
82
193
  }
83
194
  async executeCommand(node, stdinSource, stdout, stderr) {
84
195
  const assignmentEnv = { ...this.env };
@@ -126,95 +237,288 @@ class Interpreter {
126
237
  if (stdoutToStderr) {
127
238
  actualStdout = actualStderr;
128
239
  }
240
+ let exitCode = await this.invokeCommand(name, args, actualStdin, actualStdout, actualStderr, assignmentEnv);
241
+ if (actualStdout !== stdout) {
242
+ actualStdout.close();
243
+ }
244
+ if (actualStderr !== stderr && actualStderr !== actualStdout) {
245
+ actualStderr.close();
246
+ }
247
+ try {
248
+ await Promise.all(fileWritePromises);
249
+ } catch (err) {
250
+ const message = err instanceof Error ? err.message : String(err);
251
+ const writeRedirects = node.redirects.filter((r) => r.mode !== "<" && r.mode !== "2>&1" && r.mode !== "1>&2");
252
+ const target = writeRedirects.length > 0 ? await this.expandWordScalar(writeRedirects[writeRedirects.length - 1].target, this.env) : "unknown";
253
+ await stderr.writeText(`sh: ${target}: ${message}
254
+ `);
255
+ exitCode = 1;
256
+ }
257
+ return exitCode;
258
+ }
259
+ async invokeCommand(name, args, stdinSource, stdout, stderr, env) {
129
260
  const command = this.commands[name];
130
- if (!command) {
131
- await stderr.writeText(`${name}: command not found
261
+ if (command) {
262
+ return this.invokeRegisteredCommand(name, command, args, stdinSource, stdout, stderr, env);
263
+ }
264
+ if (name.includes("/")) {
265
+ const resolved = this.fs.resolve(this.cwd, name);
266
+ if (await this.fs.exists(resolved)) {
267
+ return this.executeExecutableFile(name, args, stdinSource, stdout, stderr, env);
268
+ }
269
+ if (this.externalCommand) {
270
+ return this.invokeExternalCommand(name, args, stdinSource, stdout, stderr, env);
271
+ }
272
+ return this.executeExecutableFile(name, args, stdinSource, stdout, stderr, env);
273
+ }
274
+ if (this.externalCommand) {
275
+ return this.invokeExternalCommand(name, args, stdinSource, stdout, stderr, env);
276
+ }
277
+ await stderr.writeText(`${name}: command not found
132
278
  `);
279
+ return 127;
280
+ }
281
+ async invokeExternalCommand(name, args, stdinSource, stdout, stderr, env) {
282
+ if (!this.externalCommand) {
133
283
  return 127;
134
284
  }
135
- const exec = async (cmdName, cmdArgs) => {
136
- const cmd = this.commands[cmdName];
137
- if (!cmd) {
138
- return {
139
- stdout: Buffer.alloc(0),
140
- stderr: Buffer.from(`${cmdName}: command not found
141
- `),
142
- exitCode: 127
143
- };
285
+ const ctx = createCommandContext({
286
+ args,
287
+ stdin: createStdin(stdinSource),
288
+ stdout,
289
+ stderr,
290
+ fs: this.fs,
291
+ cwd: this.cwd,
292
+ env,
293
+ terminal: this.terminal,
294
+ signal: this.activeSignal,
295
+ setCwd: (path) => this.setCwd(path),
296
+ exec: this.createExec(env),
297
+ shell: this.createShellApi(stdinSource, stdout, stderr, env)
298
+ });
299
+ try {
300
+ return await this.externalCommand({ ...ctx, name });
301
+ } catch (err) {
302
+ if (err instanceof BreakException || err instanceof ContinueException || err instanceof ExitException) {
303
+ throw err;
144
304
  }
145
- const subStdout = createStdout();
146
- const subStderr = createStderr();
147
- const subCtx = createCommandContext({
148
- args: cmdArgs,
149
- stdin: createStdin(null),
150
- stdout: subStdout,
151
- stderr: subStderr,
152
- fs: this.fs,
153
- cwd: this.cwd,
154
- env: { ...assignmentEnv },
155
- setCwd: (path) => this.setCwd(path),
156
- exec
157
- });
158
- let exitCode2;
159
- try {
160
- exitCode2 = await cmd(subCtx);
161
- } catch (err) {
162
- if (err instanceof BreakException || err instanceof ContinueException) {
163
- throw err;
164
- }
165
- const message = err instanceof Error ? err.message : String(err);
166
- await subStderr.writeText(`${cmdName}: ${message}
305
+ if (this.activeSignal.aborted) {
306
+ throw err;
307
+ }
308
+ const message = err instanceof Error ? err.message : String(err);
309
+ await stderr.writeText(`${name}: ${message}
167
310
  `);
168
- exitCode2 = 1;
311
+ return 1;
312
+ }
313
+ }
314
+ async invokeRegisteredCommand(name, command, args, stdinSource, stdout, stderr, env) {
315
+ const exec = this.createExec(env);
316
+ const shell = this.createShellApi(stdinSource, stdout, stderr, env);
317
+ const ctx = createCommandContext({
318
+ args,
319
+ stdin: createStdin(stdinSource),
320
+ stdout,
321
+ stderr,
322
+ fs: this.fs,
323
+ cwd: this.cwd,
324
+ env,
325
+ terminal: this.terminal,
326
+ signal: this.activeSignal,
327
+ setCwd: (path) => this.setCwd(path),
328
+ exec,
329
+ shell
330
+ });
331
+ try {
332
+ return await command(ctx);
333
+ } catch (err) {
334
+ if (err instanceof BreakException || err instanceof ContinueException || err instanceof ExitException) {
335
+ throw err;
169
336
  }
337
+ const message = err instanceof Error ? err.message : String(err);
338
+ await stderr.writeText(`${name}: ${message}
339
+ `);
340
+ return 1;
341
+ }
342
+ }
343
+ createExec(env) {
344
+ return async (name, args) => {
345
+ const subStdout = createStdout();
346
+ const subStderr = createStderr();
347
+ const exitCode = await this.invokeCommand(name, args, null, subStdout, subStderr, { ...env });
170
348
  subStdout.close();
171
349
  subStderr.close();
172
350
  return {
173
351
  stdout: await subStdout.collect(),
174
352
  stderr: await subStderr.collect(),
175
- exitCode: exitCode2
353
+ exitCode
176
354
  };
177
355
  };
178
- const ctx = createCommandContext({
179
- args,
180
- stdin: createStdin(actualStdin),
181
- stdout: actualStdout,
182
- stderr: actualStderr,
356
+ }
357
+ createShellApi(stdinSource, stdout, stderr, env) {
358
+ return {
359
+ eval: (source) => this.executeSourceInCurrentFrame(source, stdinSource, stdout, stderr, "eval"),
360
+ source: (path, args = []) => this.sourceFile(path, args, stdinSource, stdout, stderr),
361
+ runScript: (path, args = []) => this.executeExecutableFile(path, args, stdinSource, stdout, stderr, { ...env }),
362
+ runShell: (source, options = {}) => this.executeIsolatedShellSource(source, options.argv0 ?? "sh", options.args ?? [], stdinSource, stdout, stderr, env),
363
+ getLastExitCode: () => this.lastExitCode,
364
+ exit: (exitCode = this.lastExitCode) => {
365
+ throw new ExitException(this.normalizeExitCode(exitCode));
366
+ }
367
+ };
368
+ }
369
+ async executeExecutableFile(pathName, args, stdinSource, stdout, stderr, env) {
370
+ const loaded = await this.loadScriptSource(pathName, stderr, 127);
371
+ if (!loaded.ok) {
372
+ return loaded.exitCode;
373
+ }
374
+ const shebang = this.parseShebang(loaded.source);
375
+ if (!shebang || shebang.command === "sh") {
376
+ return this.executeIsolatedShellSource(loaded.source, pathName, args, stdinSource, stdout, stderr, env);
377
+ }
378
+ const command = this.commands[shebang.command];
379
+ if (!command) {
380
+ await stderr.writeText(`${pathName}: unsupported interpreter: ${shebang.display}
381
+ `);
382
+ return 126;
383
+ }
384
+ const child = new Interpreter({
183
385
  fs: this.fs,
184
386
  cwd: this.cwd,
185
- env: assignmentEnv,
186
- setCwd: (path) => this.setCwd(path),
187
- exec
387
+ env: { ...env },
388
+ commands: this.commands,
389
+ redirectObjects: this.redirectObjects,
390
+ terminal: this.terminal,
391
+ externalCommand: this.externalCommand,
392
+ signal: this.activeSignal,
393
+ argv0: shebang.command,
394
+ positionalParameters: []
188
395
  });
189
- let exitCode;
396
+ return child.invokeRegisteredCommand(shebang.command, command, [...shebang.args, pathName, ...args], stdinSource, stdout, stderr, { ...env });
397
+ }
398
+ async sourceFile(pathName, args, stdinSource, stdout, stderr) {
399
+ const loaded = await this.loadScriptSource(pathName, stderr, 1);
400
+ if (!loaded.ok) {
401
+ return loaded.exitCode;
402
+ }
403
+ return this.executeSourceInCurrentFrame(loaded.source, stdinSource, stdout, stderr, pathName, args.length > 0 ? { args } : undefined);
404
+ }
405
+ async executeIsolatedShellSource(source, argv0, args, stdinSource, stdout, stderr, env) {
406
+ const interpreter = new Interpreter({
407
+ fs: this.fs,
408
+ cwd: this.cwd,
409
+ env: { ...env },
410
+ commands: this.commands,
411
+ redirectObjects: this.redirectObjects,
412
+ terminal: this.terminal,
413
+ externalCommand: this.externalCommand,
414
+ signal: this.activeSignal,
415
+ argv0,
416
+ positionalParameters: args
417
+ });
418
+ try {
419
+ return await interpreter.executeSourceInCurrentFrame(source, stdinSource, stdout, stderr, argv0);
420
+ } catch (err) {
421
+ if (err instanceof ExitException) {
422
+ return err.exitCode;
423
+ }
424
+ throw err;
425
+ }
426
+ }
427
+ async executeSourceInCurrentFrame(source, stdinSource, stdout, stderr, errorName, positionalOverride) {
428
+ const previousArgv0 = this.argv0;
429
+ const previousPositionals = this.positionalParameters;
430
+ if (positionalOverride?.argv0 !== undefined) {
431
+ this.argv0 = positionalOverride.argv0;
432
+ }
433
+ if (positionalOverride?.args !== undefined) {
434
+ this.positionalParameters = [...positionalOverride.args];
435
+ }
190
436
  try {
191
- exitCode = await command(ctx);
437
+ const ast = this.parseSource(source);
438
+ if (!ast) {
439
+ return 0;
440
+ }
441
+ return await this.executeNode(ast, stdinSource, stdout, stderr);
192
442
  } catch (err) {
193
- if (err instanceof BreakException || err instanceof ContinueException) {
443
+ if (err instanceof BreakException || err instanceof ContinueException || err instanceof ExitException) {
194
444
  throw err;
195
445
  }
196
446
  const message = err instanceof Error ? err.message : String(err);
197
- await stderr.writeText(`${name}: ${message}
447
+ await stderr.writeText(`${errorName}: ${message}
198
448
  `);
199
- exitCode = 1;
449
+ return 2;
450
+ } finally {
451
+ if (positionalOverride?.argv0 !== undefined) {
452
+ this.argv0 = previousArgv0;
453
+ }
454
+ if (positionalOverride?.args !== undefined) {
455
+ this.positionalParameters = previousPositionals;
456
+ }
200
457
  }
201
- if (actualStdout !== stdout) {
202
- actualStdout.close();
458
+ }
459
+ parseSource(source) {
460
+ const tokens = new Lexer(source, { preserveNewlines: true }).tokenize();
461
+ if (tokens.every((token) => token.type === "newline" || token.type === "eof")) {
462
+ return null;
203
463
  }
204
- if (actualStderr !== stderr && actualStderr !== actualStdout) {
205
- actualStderr.close();
464
+ return new Parser(tokens).parse();
465
+ }
466
+ async loadScriptSource(pathName, stderr, missingExitCode) {
467
+ const path = this.fs.resolve(this.cwd, pathName);
468
+ if (!await this.fs.exists(path)) {
469
+ await stderr.writeText(`${pathName}: No such file or directory
470
+ `);
471
+ return { ok: false, exitCode: missingExitCode };
472
+ }
473
+ const stat = await this.fs.stat(path);
474
+ if (stat.isDirectory()) {
475
+ await stderr.writeText(`${pathName}: is a directory
476
+ `);
477
+ return { ok: false, exitCode: 126 };
478
+ }
479
+ if (!stat.isFile()) {
480
+ await stderr.writeText(`${pathName}: not a file
481
+ `);
482
+ return { ok: false, exitCode: 126 };
206
483
  }
207
484
  try {
208
- await Promise.all(fileWritePromises);
485
+ return { ok: true, path, source: await this.fs.readFile(path, "utf-8") };
209
486
  } catch (err) {
210
487
  const message = err instanceof Error ? err.message : String(err);
211
- const writeRedirects = node.redirects.filter((r) => r.mode !== "<" && r.mode !== "2>&1" && r.mode !== "1>&2");
212
- const target = writeRedirects.length > 0 ? await this.expandWordScalar(writeRedirects[writeRedirects.length - 1].target, this.env) : "unknown";
213
- await stderr.writeText(`sh: ${target}: ${message}
488
+ await stderr.writeText(`${pathName}: ${message}
214
489
  `);
215
- exitCode = 1;
490
+ return { ok: false, exitCode: 126 };
216
491
  }
217
- return exitCode;
492
+ }
493
+ parseShebang(source) {
494
+ if (!source.startsWith("#!")) {
495
+ return null;
496
+ }
497
+ const lineEnd = source.indexOf(`
498
+ `);
499
+ const line = source.slice(2, lineEnd === -1 ? undefined : lineEnd).trim();
500
+ if (line === "") {
501
+ return { command: "sh", args: [], display: "sh" };
502
+ }
503
+ const parts = line.split(/\s+/);
504
+ const executable = parts[0];
505
+ const executableName = this.fs.basename(executable);
506
+ if (executableName === "env") {
507
+ const envCommand = parts[1];
508
+ if (!envCommand) {
509
+ return { command: "env", args: [], display: line };
510
+ }
511
+ return {
512
+ command: this.fs.basename(envCommand),
513
+ args: parts.slice(2),
514
+ display: line
515
+ };
516
+ }
517
+ return {
518
+ command: executableName,
519
+ args: parts.slice(1),
520
+ display: line
521
+ };
218
522
  }
219
523
  async handleRedirect(redirect, stdin, stdout, stderr, env) {
220
524
  const target = await this.expandWordScalar(redirect.target, env);
@@ -627,6 +931,10 @@ class Interpreter {
627
931
  this.appendSegment(fields[fields.length - 1], part.value, part.quoted);
628
932
  continue;
629
933
  }
934
+ if (part.type === "variable" && part.name === "@" && part.quoted) {
935
+ this.appendQuotedPositionalParameters(fields);
936
+ continue;
937
+ }
630
938
  const value = await this.expandWordPart(part, env);
631
939
  if (part.quoted) {
632
940
  this.appendSegment(fields[fields.length - 1], value, true);
@@ -650,7 +958,7 @@ class Interpreter {
650
958
  case "text":
651
959
  return part.value;
652
960
  case "variable":
653
- return env[part.name] ?? "";
961
+ return this.getVariableValue(part.name, env);
654
962
  case "substitution":
655
963
  return this.executeSubstitution(part.command, env);
656
964
  case "arithmetic":
@@ -659,6 +967,35 @@ class Interpreter {
659
967
  throw new Error("Cannot expand unknown word part");
660
968
  }
661
969
  }
970
+ getVariableValue(name, env) {
971
+ if (name === "0") {
972
+ return this.argv0;
973
+ }
974
+ if (name === "#") {
975
+ return String(this.positionalParameters.length);
976
+ }
977
+ if (name === "?") {
978
+ return String(this.lastExitCode);
979
+ }
980
+ if (name === "*" || name === "@") {
981
+ return this.positionalParameters.join(" ");
982
+ }
983
+ if (/^[1-9]$/.test(name)) {
984
+ return this.positionalParameters[Number(name) - 1] ?? "";
985
+ }
986
+ return env[name] ?? "";
987
+ }
988
+ appendQuotedPositionalParameters(fields) {
989
+ if (this.positionalParameters.length === 0) {
990
+ return;
991
+ }
992
+ this.appendSegment(fields[fields.length - 1], this.positionalParameters[0], true);
993
+ for (let i = 1;i < this.positionalParameters.length; i++) {
994
+ const field = this.createExpandedField();
995
+ this.appendSegment(field, this.positionalParameters[i], true);
996
+ fields.push(field);
997
+ }
998
+ }
662
999
  async executeSubstitution(command, env) {
663
1000
  const interpreter = new Interpreter({
664
1001
  fs: this.fs,
@@ -666,7 +1003,12 @@ class Interpreter {
666
1003
  env,
667
1004
  commands: this.commands,
668
1005
  redirectObjects: this.redirectObjects,
669
- isTTY: false
1006
+ isTTY: false,
1007
+ externalCommand: this.externalCommand,
1008
+ signal: this.activeSignal,
1009
+ argv0: this.argv0,
1010
+ positionalParameters: this.positionalParameters,
1011
+ lastExitCode: this.lastExitCode
670
1012
  });
671
1013
  const result = await interpreter.execute(command);
672
1014
  return result.stdout.toString("utf-8").replace(/\n+$/, "");
@@ -767,8 +1109,8 @@ class Interpreter {
767
1109
  expandedExpr = expandedExpr.replace(/\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g, (_, name) => {
768
1110
  return env[name] ?? "0";
769
1111
  });
770
- expandedExpr = expandedExpr.replace(/\$([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, name) => {
771
- return env[name] ?? "0";
1112
+ expandedExpr = expandedExpr.replace(/\$([a-zA-Z_][a-zA-Z0-9_]*|[0-9#*@?])/g, (_, name) => {
1113
+ return this.getVariableValue(name, env) || "0";
772
1114
  });
773
1115
  expandedExpr = expandedExpr.replace(/\b([a-zA-Z_][a-zA-Z0-9_]*)\b/g, (match) => {
774
1116
  if (/^\d+$/.test(match))
@@ -925,6 +1267,12 @@ class Interpreter {
925
1267
  };
926
1268
  return parseOr();
927
1269
  }
1270
+ normalizeExitCode(exitCode) {
1271
+ if (!Number.isFinite(exitCode)) {
1272
+ return 2;
1273
+ }
1274
+ return (Math.trunc(exitCode) % 256 + 256) % 256;
1275
+ }
928
1276
  setCwd(cwd) {
929
1277
  this.env.OLDPWD = this.cwd;
930
1278
  this.cwd = cwd;
@@ -938,11 +1286,15 @@ class Interpreter {
938
1286
  getEnv() {
939
1287
  return { ...this.env };
940
1288
  }
1289
+ getLastExitCode() {
1290
+ return this.lastExitCode;
1291
+ }
941
1292
  }
942
1293
  export {
943
1294
  Interpreter,
1295
+ ExitException,
944
1296
  ContinueException,
945
1297
  BreakException
946
1298
  };
947
1299
 
948
- //# debugId=96916342C8FA80E364756E2164756E21
1300
+ //# debugId=1C8DFE25269FD4A764756E2164756E21