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
@@ -40,13 +40,17 @@ var __export = (target, all) => {
40
40
  var exports_interpreter = {};
41
41
  __export(exports_interpreter, {
42
42
  Interpreter: () => Interpreter,
43
+ ExitException: () => ExitException,
43
44
  ContinueException: () => ContinueException,
44
45
  BreakException: () => BreakException
45
46
  });
46
47
  module.exports = __toCommonJS(exports_interpreter);
47
48
  var import_context = require("./context.cjs");
49
+ var import_lexer = require("../lexer/lexer.cjs");
50
+ var import_parser = require("../parser/parser.cjs");
48
51
  var import_stdin = require("../io/stdin.cjs");
49
52
  var import_stdout = require("../io/stdout.cjs");
53
+ var import_async_queue = require("../io/async-queue.cjs");
50
54
  var import_special_files = require("../fs/special-files.cjs");
51
55
  var DEFAULT_IFS = `
52
56
  `;
@@ -68,6 +72,14 @@ class ContinueException extends Error {
68
72
  }
69
73
  }
70
74
 
75
+ class ExitException extends Error {
76
+ exitCode;
77
+ constructor(exitCode) {
78
+ super("exit");
79
+ this.exitCode = exitCode;
80
+ }
81
+ }
82
+
71
83
  class Interpreter {
72
84
  fs;
73
85
  cwd;
@@ -76,54 +88,154 @@ class Interpreter {
76
88
  redirectObjects;
77
89
  loopDepth = 0;
78
90
  isTTY;
91
+ terminal;
92
+ activeSignal;
93
+ externalCommand;
94
+ argv0;
95
+ positionalParameters;
96
+ lastExitCode;
79
97
  constructor(options) {
80
98
  this.fs = options.fs;
81
99
  this.cwd = options.cwd;
82
100
  this.env = { ...options.env };
83
101
  this.commands = options.commands;
84
102
  this.redirectObjects = options.redirectObjects ?? {};
85
- this.isTTY = options.isTTY ?? false;
103
+ this.terminal = options.terminal ?? { isTTY: options.isTTY ?? false };
104
+ this.isTTY = this.terminal.isTTY;
105
+ this.activeSignal = options.signal ?? new AbortController().signal;
106
+ this.externalCommand = options.externalCommand;
107
+ this.argv0 = options.argv0 ?? "sh";
108
+ this.positionalParameters = [...options.positionalParameters ?? []];
109
+ this.lastExitCode = options.lastExitCode ?? 0;
86
110
  }
87
111
  getLoopDepth() {
88
112
  return this.loopDepth;
89
113
  }
90
114
  async execute(ast) {
91
- const stdout = import_stdout.createStdout(this.isTTY);
92
- const stderr = import_stdout.createStderr(this.isTTY);
93
- const exitCode = await this.executeNode(ast, null, stdout, stderr);
94
- stdout.close();
95
- stderr.close();
115
+ return this.executeStreaming(ast).exit;
116
+ }
117
+ executeStreaming(ast, options = {}) {
118
+ const terminal = options.terminal ?? this.terminal;
119
+ const controller = new AbortController;
120
+ const eventQueue = new import_async_queue.AsyncQueue;
121
+ if (options.signal) {
122
+ if (options.signal.aborted) {
123
+ controller.abort(options.signal.reason);
124
+ } else {
125
+ options.signal.addEventListener("abort", () => controller.abort(options.signal?.reason), { once: true });
126
+ }
127
+ }
128
+ const stdout = import_stdout.createStdout(terminal.isTTY, async (chunk) => {
129
+ eventQueue.push({ fd: 1, chunk });
130
+ await options.stdout?.write(chunk);
131
+ });
132
+ const stderr = import_stdout.createStderr(terminal.isTTY, async (chunk) => {
133
+ eventQueue.push({ fd: 2, chunk });
134
+ await options.stderr?.write(chunk);
135
+ });
136
+ const previousSignal = this.activeSignal;
137
+ const previousTerminal = this.terminal;
138
+ const previousIsTTY = this.isTTY;
139
+ const stdinSource = this.normalizeInputSource(options.stdin);
140
+ const exit = (async () => {
141
+ this.activeSignal = controller.signal;
142
+ this.terminal = terminal;
143
+ this.isTTY = terminal.isTTY;
144
+ let exitCode;
145
+ try {
146
+ this.throwIfAborted();
147
+ exitCode = await this.executeNode(ast, stdinSource, stdout, stderr);
148
+ } catch (err) {
149
+ if (err instanceof ExitException) {
150
+ exitCode = err.exitCode;
151
+ } else if (controller.signal.aborted) {
152
+ exitCode = 130;
153
+ } else {
154
+ throw err;
155
+ }
156
+ } finally {
157
+ stdout.close();
158
+ stderr.close();
159
+ eventQueue.close();
160
+ this.activeSignal = previousSignal;
161
+ this.terminal = previousTerminal;
162
+ this.isTTY = previousIsTTY;
163
+ }
164
+ this.lastExitCode = exitCode;
165
+ return {
166
+ stdout: await stdout.collect(),
167
+ stderr: await stderr.collect(),
168
+ exitCode
169
+ };
170
+ })();
96
171
  return {
97
- stdout: await stdout.collect(),
98
- stderr: await stderr.collect(),
99
- exitCode
172
+ stdout: stdout.getReadableStream(),
173
+ stderr: stderr.getReadableStream(),
174
+ output: eventQueue,
175
+ exit,
176
+ kill: (reason) => controller.abort(reason ?? new Error("Shell execution killed"))
100
177
  };
101
178
  }
179
+ normalizeInputSource(source) {
180
+ if (source === undefined || source === null) {
181
+ return null;
182
+ }
183
+ if (typeof source === "string") {
184
+ return async function* () {
185
+ yield new TextEncoder().encode(source);
186
+ }();
187
+ }
188
+ if (Buffer.isBuffer(source)) {
189
+ return async function* () {
190
+ yield new Uint8Array(source);
191
+ }();
192
+ }
193
+ return source;
194
+ }
102
195
  async executeNode(node, stdinSource, stdout, stderr) {
196
+ this.throwIfAborted();
197
+ let exitCode;
103
198
  switch (node.type) {
104
199
  case "command":
105
- return this.executeCommand(node, stdinSource, stdout, stderr);
200
+ exitCode = await this.executeCommand(node, stdinSource, stdout, stderr);
201
+ break;
106
202
  case "pipeline":
107
- return this.executePipeline(node.commands, stdinSource, stdout, stderr);
203
+ exitCode = await this.executePipeline(node.commands, stdinSource, stdout, stderr);
204
+ break;
108
205
  case "sequence":
109
- return this.executeSequence(node.commands, stdinSource, stdout, stderr);
206
+ exitCode = await this.executeSequence(node.commands, stdinSource, stdout, stderr);
207
+ break;
110
208
  case "and":
111
- return this.executeAnd(node.left, node.right, stdinSource, stdout, stderr);
209
+ exitCode = await this.executeAnd(node.left, node.right, stdinSource, stdout, stderr);
210
+ break;
112
211
  case "or":
113
- return this.executeOr(node.left, node.right, stdinSource, stdout, stderr);
212
+ exitCode = await this.executeOr(node.left, node.right, stdinSource, stdout, stderr);
213
+ break;
114
214
  case "if":
115
- return this.executeIf(node, stdinSource, stdout, stderr);
215
+ exitCode = await this.executeIf(node, stdinSource, stdout, stderr);
216
+ break;
116
217
  case "for":
117
- return this.executeFor(node, stdinSource, stdout, stderr);
218
+ exitCode = await this.executeFor(node, stdinSource, stdout, stderr);
219
+ break;
118
220
  case "while":
119
- return this.executeWhile(node, stdinSource, stdout, stderr);
221
+ exitCode = await this.executeWhile(node, stdinSource, stdout, stderr);
222
+ break;
120
223
  case "until":
121
- return this.executeUntil(node, stdinSource, stdout, stderr);
224
+ exitCode = await this.executeUntil(node, stdinSource, stdout, stderr);
225
+ break;
122
226
  case "case":
123
- return this.executeCase(node, stdinSource, stdout, stderr);
227
+ exitCode = await this.executeCase(node, stdinSource, stdout, stderr);
228
+ break;
124
229
  default:
125
230
  throw new Error("Cannot execute unknown node type");
126
231
  }
232
+ this.lastExitCode = exitCode;
233
+ return exitCode;
234
+ }
235
+ throwIfAborted() {
236
+ if (this.activeSignal.aborted) {
237
+ throw this.activeSignal.reason ?? new Error("Shell execution aborted");
238
+ }
127
239
  }
128
240
  async executeCommand(node, stdinSource, stdout, stderr) {
129
241
  const assignmentEnv = { ...this.env };
@@ -171,95 +283,288 @@ class Interpreter {
171
283
  if (stdoutToStderr) {
172
284
  actualStdout = actualStderr;
173
285
  }
286
+ let exitCode = await this.invokeCommand(name, args, actualStdin, actualStdout, actualStderr, assignmentEnv);
287
+ if (actualStdout !== stdout) {
288
+ actualStdout.close();
289
+ }
290
+ if (actualStderr !== stderr && actualStderr !== actualStdout) {
291
+ actualStderr.close();
292
+ }
293
+ try {
294
+ await Promise.all(fileWritePromises);
295
+ } catch (err) {
296
+ const message = err instanceof Error ? err.message : String(err);
297
+ const writeRedirects = node.redirects.filter((r) => r.mode !== "<" && r.mode !== "2>&1" && r.mode !== "1>&2");
298
+ const target = writeRedirects.length > 0 ? await this.expandWordScalar(writeRedirects[writeRedirects.length - 1].target, this.env) : "unknown";
299
+ await stderr.writeText(`sh: ${target}: ${message}
300
+ `);
301
+ exitCode = 1;
302
+ }
303
+ return exitCode;
304
+ }
305
+ async invokeCommand(name, args, stdinSource, stdout, stderr, env) {
174
306
  const command = this.commands[name];
175
- if (!command) {
176
- await stderr.writeText(`${name}: command not found
307
+ if (command) {
308
+ return this.invokeRegisteredCommand(name, command, args, stdinSource, stdout, stderr, env);
309
+ }
310
+ if (name.includes("/")) {
311
+ const resolved = this.fs.resolve(this.cwd, name);
312
+ if (await this.fs.exists(resolved)) {
313
+ return this.executeExecutableFile(name, args, stdinSource, stdout, stderr, env);
314
+ }
315
+ if (this.externalCommand) {
316
+ return this.invokeExternalCommand(name, args, stdinSource, stdout, stderr, env);
317
+ }
318
+ return this.executeExecutableFile(name, args, stdinSource, stdout, stderr, env);
319
+ }
320
+ if (this.externalCommand) {
321
+ return this.invokeExternalCommand(name, args, stdinSource, stdout, stderr, env);
322
+ }
323
+ await stderr.writeText(`${name}: command not found
177
324
  `);
325
+ return 127;
326
+ }
327
+ async invokeExternalCommand(name, args, stdinSource, stdout, stderr, env) {
328
+ if (!this.externalCommand) {
178
329
  return 127;
179
330
  }
180
- const exec = async (cmdName, cmdArgs) => {
181
- const cmd = this.commands[cmdName];
182
- if (!cmd) {
183
- return {
184
- stdout: Buffer.alloc(0),
185
- stderr: Buffer.from(`${cmdName}: command not found
186
- `),
187
- exitCode: 127
188
- };
331
+ const ctx = import_context.createCommandContext({
332
+ args,
333
+ stdin: import_stdin.createStdin(stdinSource),
334
+ stdout,
335
+ stderr,
336
+ fs: this.fs,
337
+ cwd: this.cwd,
338
+ env,
339
+ terminal: this.terminal,
340
+ signal: this.activeSignal,
341
+ setCwd: (path) => this.setCwd(path),
342
+ exec: this.createExec(env),
343
+ shell: this.createShellApi(stdinSource, stdout, stderr, env)
344
+ });
345
+ try {
346
+ return await this.externalCommand({ ...ctx, name });
347
+ } catch (err) {
348
+ if (err instanceof BreakException || err instanceof ContinueException || err instanceof ExitException) {
349
+ throw err;
189
350
  }
190
- const subStdout = import_stdout.createStdout();
191
- const subStderr = import_stdout.createStderr();
192
- const subCtx = import_context.createCommandContext({
193
- args: cmdArgs,
194
- stdin: import_stdin.createStdin(null),
195
- stdout: subStdout,
196
- stderr: subStderr,
197
- fs: this.fs,
198
- cwd: this.cwd,
199
- env: { ...assignmentEnv },
200
- setCwd: (path) => this.setCwd(path),
201
- exec
202
- });
203
- let exitCode2;
204
- try {
205
- exitCode2 = await cmd(subCtx);
206
- } catch (err) {
207
- if (err instanceof BreakException || err instanceof ContinueException) {
208
- throw err;
209
- }
210
- const message = err instanceof Error ? err.message : String(err);
211
- await subStderr.writeText(`${cmdName}: ${message}
351
+ if (this.activeSignal.aborted) {
352
+ throw err;
353
+ }
354
+ const message = err instanceof Error ? err.message : String(err);
355
+ await stderr.writeText(`${name}: ${message}
212
356
  `);
213
- exitCode2 = 1;
357
+ return 1;
358
+ }
359
+ }
360
+ async invokeRegisteredCommand(name, command, args, stdinSource, stdout, stderr, env) {
361
+ const exec = this.createExec(env);
362
+ const shell = this.createShellApi(stdinSource, stdout, stderr, env);
363
+ const ctx = import_context.createCommandContext({
364
+ args,
365
+ stdin: import_stdin.createStdin(stdinSource),
366
+ stdout,
367
+ stderr,
368
+ fs: this.fs,
369
+ cwd: this.cwd,
370
+ env,
371
+ terminal: this.terminal,
372
+ signal: this.activeSignal,
373
+ setCwd: (path) => this.setCwd(path),
374
+ exec,
375
+ shell
376
+ });
377
+ try {
378
+ return await command(ctx);
379
+ } catch (err) {
380
+ if (err instanceof BreakException || err instanceof ContinueException || err instanceof ExitException) {
381
+ throw err;
214
382
  }
383
+ const message = err instanceof Error ? err.message : String(err);
384
+ await stderr.writeText(`${name}: ${message}
385
+ `);
386
+ return 1;
387
+ }
388
+ }
389
+ createExec(env) {
390
+ return async (name, args) => {
391
+ const subStdout = import_stdout.createStdout();
392
+ const subStderr = import_stdout.createStderr();
393
+ const exitCode = await this.invokeCommand(name, args, null, subStdout, subStderr, { ...env });
215
394
  subStdout.close();
216
395
  subStderr.close();
217
396
  return {
218
397
  stdout: await subStdout.collect(),
219
398
  stderr: await subStderr.collect(),
220
- exitCode: exitCode2
399
+ exitCode
221
400
  };
222
401
  };
223
- const ctx = import_context.createCommandContext({
224
- args,
225
- stdin: import_stdin.createStdin(actualStdin),
226
- stdout: actualStdout,
227
- stderr: actualStderr,
402
+ }
403
+ createShellApi(stdinSource, stdout, stderr, env) {
404
+ return {
405
+ eval: (source) => this.executeSourceInCurrentFrame(source, stdinSource, stdout, stderr, "eval"),
406
+ source: (path, args = []) => this.sourceFile(path, args, stdinSource, stdout, stderr),
407
+ runScript: (path, args = []) => this.executeExecutableFile(path, args, stdinSource, stdout, stderr, { ...env }),
408
+ runShell: (source, options = {}) => this.executeIsolatedShellSource(source, options.argv0 ?? "sh", options.args ?? [], stdinSource, stdout, stderr, env),
409
+ getLastExitCode: () => this.lastExitCode,
410
+ exit: (exitCode = this.lastExitCode) => {
411
+ throw new ExitException(this.normalizeExitCode(exitCode));
412
+ }
413
+ };
414
+ }
415
+ async executeExecutableFile(pathName, args, stdinSource, stdout, stderr, env) {
416
+ const loaded = await this.loadScriptSource(pathName, stderr, 127);
417
+ if (!loaded.ok) {
418
+ return loaded.exitCode;
419
+ }
420
+ const shebang = this.parseShebang(loaded.source);
421
+ if (!shebang || shebang.command === "sh") {
422
+ return this.executeIsolatedShellSource(loaded.source, pathName, args, stdinSource, stdout, stderr, env);
423
+ }
424
+ const command = this.commands[shebang.command];
425
+ if (!command) {
426
+ await stderr.writeText(`${pathName}: unsupported interpreter: ${shebang.display}
427
+ `);
428
+ return 126;
429
+ }
430
+ const child = new Interpreter({
228
431
  fs: this.fs,
229
432
  cwd: this.cwd,
230
- env: assignmentEnv,
231
- setCwd: (path) => this.setCwd(path),
232
- exec
433
+ env: { ...env },
434
+ commands: this.commands,
435
+ redirectObjects: this.redirectObjects,
436
+ terminal: this.terminal,
437
+ externalCommand: this.externalCommand,
438
+ signal: this.activeSignal,
439
+ argv0: shebang.command,
440
+ positionalParameters: []
233
441
  });
234
- let exitCode;
442
+ return child.invokeRegisteredCommand(shebang.command, command, [...shebang.args, pathName, ...args], stdinSource, stdout, stderr, { ...env });
443
+ }
444
+ async sourceFile(pathName, args, stdinSource, stdout, stderr) {
445
+ const loaded = await this.loadScriptSource(pathName, stderr, 1);
446
+ if (!loaded.ok) {
447
+ return loaded.exitCode;
448
+ }
449
+ return this.executeSourceInCurrentFrame(loaded.source, stdinSource, stdout, stderr, pathName, args.length > 0 ? { args } : undefined);
450
+ }
451
+ async executeIsolatedShellSource(source, argv0, args, stdinSource, stdout, stderr, env) {
452
+ const interpreter = new Interpreter({
453
+ fs: this.fs,
454
+ cwd: this.cwd,
455
+ env: { ...env },
456
+ commands: this.commands,
457
+ redirectObjects: this.redirectObjects,
458
+ terminal: this.terminal,
459
+ externalCommand: this.externalCommand,
460
+ signal: this.activeSignal,
461
+ argv0,
462
+ positionalParameters: args
463
+ });
464
+ try {
465
+ return await interpreter.executeSourceInCurrentFrame(source, stdinSource, stdout, stderr, argv0);
466
+ } catch (err) {
467
+ if (err instanceof ExitException) {
468
+ return err.exitCode;
469
+ }
470
+ throw err;
471
+ }
472
+ }
473
+ async executeSourceInCurrentFrame(source, stdinSource, stdout, stderr, errorName, positionalOverride) {
474
+ const previousArgv0 = this.argv0;
475
+ const previousPositionals = this.positionalParameters;
476
+ if (positionalOverride?.argv0 !== undefined) {
477
+ this.argv0 = positionalOverride.argv0;
478
+ }
479
+ if (positionalOverride?.args !== undefined) {
480
+ this.positionalParameters = [...positionalOverride.args];
481
+ }
235
482
  try {
236
- exitCode = await command(ctx);
483
+ const ast = this.parseSource(source);
484
+ if (!ast) {
485
+ return 0;
486
+ }
487
+ return await this.executeNode(ast, stdinSource, stdout, stderr);
237
488
  } catch (err) {
238
- if (err instanceof BreakException || err instanceof ContinueException) {
489
+ if (err instanceof BreakException || err instanceof ContinueException || err instanceof ExitException) {
239
490
  throw err;
240
491
  }
241
492
  const message = err instanceof Error ? err.message : String(err);
242
- await stderr.writeText(`${name}: ${message}
493
+ await stderr.writeText(`${errorName}: ${message}
243
494
  `);
244
- exitCode = 1;
495
+ return 2;
496
+ } finally {
497
+ if (positionalOverride?.argv0 !== undefined) {
498
+ this.argv0 = previousArgv0;
499
+ }
500
+ if (positionalOverride?.args !== undefined) {
501
+ this.positionalParameters = previousPositionals;
502
+ }
245
503
  }
246
- if (actualStdout !== stdout) {
247
- actualStdout.close();
504
+ }
505
+ parseSource(source) {
506
+ const tokens = new import_lexer.Lexer(source, { preserveNewlines: true }).tokenize();
507
+ if (tokens.every((token) => token.type === "newline" || token.type === "eof")) {
508
+ return null;
248
509
  }
249
- if (actualStderr !== stderr && actualStderr !== actualStdout) {
250
- actualStderr.close();
510
+ return new import_parser.Parser(tokens).parse();
511
+ }
512
+ async loadScriptSource(pathName, stderr, missingExitCode) {
513
+ const path = this.fs.resolve(this.cwd, pathName);
514
+ if (!await this.fs.exists(path)) {
515
+ await stderr.writeText(`${pathName}: No such file or directory
516
+ `);
517
+ return { ok: false, exitCode: missingExitCode };
518
+ }
519
+ const stat = await this.fs.stat(path);
520
+ if (stat.isDirectory()) {
521
+ await stderr.writeText(`${pathName}: is a directory
522
+ `);
523
+ return { ok: false, exitCode: 126 };
524
+ }
525
+ if (!stat.isFile()) {
526
+ await stderr.writeText(`${pathName}: not a file
527
+ `);
528
+ return { ok: false, exitCode: 126 };
251
529
  }
252
530
  try {
253
- await Promise.all(fileWritePromises);
531
+ return { ok: true, path, source: await this.fs.readFile(path, "utf-8") };
254
532
  } catch (err) {
255
533
  const message = err instanceof Error ? err.message : String(err);
256
- const writeRedirects = node.redirects.filter((r) => r.mode !== "<" && r.mode !== "2>&1" && r.mode !== "1>&2");
257
- const target = writeRedirects.length > 0 ? await this.expandWordScalar(writeRedirects[writeRedirects.length - 1].target, this.env) : "unknown";
258
- await stderr.writeText(`sh: ${target}: ${message}
534
+ await stderr.writeText(`${pathName}: ${message}
259
535
  `);
260
- exitCode = 1;
536
+ return { ok: false, exitCode: 126 };
261
537
  }
262
- return exitCode;
538
+ }
539
+ parseShebang(source) {
540
+ if (!source.startsWith("#!")) {
541
+ return null;
542
+ }
543
+ const lineEnd = source.indexOf(`
544
+ `);
545
+ const line = source.slice(2, lineEnd === -1 ? undefined : lineEnd).trim();
546
+ if (line === "") {
547
+ return { command: "sh", args: [], display: "sh" };
548
+ }
549
+ const parts = line.split(/\s+/);
550
+ const executable = parts[0];
551
+ const executableName = this.fs.basename(executable);
552
+ if (executableName === "env") {
553
+ const envCommand = parts[1];
554
+ if (!envCommand) {
555
+ return { command: "env", args: [], display: line };
556
+ }
557
+ return {
558
+ command: this.fs.basename(envCommand),
559
+ args: parts.slice(2),
560
+ display: line
561
+ };
562
+ }
563
+ return {
564
+ command: executableName,
565
+ args: parts.slice(1),
566
+ display: line
567
+ };
263
568
  }
264
569
  async handleRedirect(redirect, stdin, stdout, stderr, env) {
265
570
  const target = await this.expandWordScalar(redirect.target, env);
@@ -672,6 +977,10 @@ class Interpreter {
672
977
  this.appendSegment(fields[fields.length - 1], part.value, part.quoted);
673
978
  continue;
674
979
  }
980
+ if (part.type === "variable" && part.name === "@" && part.quoted) {
981
+ this.appendQuotedPositionalParameters(fields);
982
+ continue;
983
+ }
675
984
  const value = await this.expandWordPart(part, env);
676
985
  if (part.quoted) {
677
986
  this.appendSegment(fields[fields.length - 1], value, true);
@@ -695,7 +1004,7 @@ class Interpreter {
695
1004
  case "text":
696
1005
  return part.value;
697
1006
  case "variable":
698
- return env[part.name] ?? "";
1007
+ return this.getVariableValue(part.name, env);
699
1008
  case "substitution":
700
1009
  return this.executeSubstitution(part.command, env);
701
1010
  case "arithmetic":
@@ -704,6 +1013,35 @@ class Interpreter {
704
1013
  throw new Error("Cannot expand unknown word part");
705
1014
  }
706
1015
  }
1016
+ getVariableValue(name, env) {
1017
+ if (name === "0") {
1018
+ return this.argv0;
1019
+ }
1020
+ if (name === "#") {
1021
+ return String(this.positionalParameters.length);
1022
+ }
1023
+ if (name === "?") {
1024
+ return String(this.lastExitCode);
1025
+ }
1026
+ if (name === "*" || name === "@") {
1027
+ return this.positionalParameters.join(" ");
1028
+ }
1029
+ if (/^[1-9]$/.test(name)) {
1030
+ return this.positionalParameters[Number(name) - 1] ?? "";
1031
+ }
1032
+ return env[name] ?? "";
1033
+ }
1034
+ appendQuotedPositionalParameters(fields) {
1035
+ if (this.positionalParameters.length === 0) {
1036
+ return;
1037
+ }
1038
+ this.appendSegment(fields[fields.length - 1], this.positionalParameters[0], true);
1039
+ for (let i = 1;i < this.positionalParameters.length; i++) {
1040
+ const field = this.createExpandedField();
1041
+ this.appendSegment(field, this.positionalParameters[i], true);
1042
+ fields.push(field);
1043
+ }
1044
+ }
707
1045
  async executeSubstitution(command, env) {
708
1046
  const interpreter = new Interpreter({
709
1047
  fs: this.fs,
@@ -711,7 +1049,12 @@ class Interpreter {
711
1049
  env,
712
1050
  commands: this.commands,
713
1051
  redirectObjects: this.redirectObjects,
714
- isTTY: false
1052
+ isTTY: false,
1053
+ externalCommand: this.externalCommand,
1054
+ signal: this.activeSignal,
1055
+ argv0: this.argv0,
1056
+ positionalParameters: this.positionalParameters,
1057
+ lastExitCode: this.lastExitCode
715
1058
  });
716
1059
  const result = await interpreter.execute(command);
717
1060
  return result.stdout.toString("utf-8").replace(/\n+$/, "");
@@ -812,8 +1155,8 @@ class Interpreter {
812
1155
  expandedExpr = expandedExpr.replace(/\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g, (_, name) => {
813
1156
  return env[name] ?? "0";
814
1157
  });
815
- expandedExpr = expandedExpr.replace(/\$([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, name) => {
816
- return env[name] ?? "0";
1158
+ expandedExpr = expandedExpr.replace(/\$([a-zA-Z_][a-zA-Z0-9_]*|[0-9#*@?])/g, (_, name) => {
1159
+ return this.getVariableValue(name, env) || "0";
817
1160
  });
818
1161
  expandedExpr = expandedExpr.replace(/\b([a-zA-Z_][a-zA-Z0-9_]*)\b/g, (match) => {
819
1162
  if (/^\d+$/.test(match))
@@ -970,6 +1313,12 @@ class Interpreter {
970
1313
  };
971
1314
  return parseOr();
972
1315
  }
1316
+ normalizeExitCode(exitCode) {
1317
+ if (!Number.isFinite(exitCode)) {
1318
+ return 2;
1319
+ }
1320
+ return (Math.trunc(exitCode) % 256 + 256) % 256;
1321
+ }
973
1322
  setCwd(cwd) {
974
1323
  this.env.OLDPWD = this.cwd;
975
1324
  this.cwd = cwd;
@@ -983,6 +1332,9 @@ class Interpreter {
983
1332
  getEnv() {
984
1333
  return { ...this.env };
985
1334
  }
1335
+ getLastExitCode() {
1336
+ return this.lastExitCode;
1337
+ }
986
1338
  }
987
1339
 
988
- //# debugId=1238E19B5ED5404064756E2164756E21
1340
+ //# debugId=F9FD7440AC6CB03664756E2164756E21