shell-dsl 0.0.38 → 0.0.40

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 (60) hide show
  1. package/README.md +118 -3
  2. package/dist/cjs/index.cjs +2 -1
  3. package/dist/cjs/index.cjs.map +3 -3
  4. package/dist/cjs/package.json +1 -1
  5. package/dist/cjs/src/commands/exit/exit.cjs +84 -0
  6. package/dist/cjs/src/commands/exit/exit.cjs.map +10 -0
  7. package/dist/cjs/src/commands/index.cjs +22 -2
  8. package/dist/cjs/src/commands/index.cjs.map +3 -3
  9. package/dist/cjs/src/commands/printf/printf.cjs +416 -0
  10. package/dist/cjs/src/commands/printf/printf.cjs.map +10 -0
  11. package/dist/cjs/src/commands/sh/sh.cjs +134 -0
  12. package/dist/cjs/src/commands/sh/sh.cjs.map +10 -0
  13. package/dist/cjs/src/index.cjs +2 -1
  14. package/dist/cjs/src/index.cjs.map +3 -3
  15. package/dist/cjs/src/interpreter/context.cjs +4 -1
  16. package/dist/cjs/src/interpreter/context.cjs.map +3 -3
  17. package/dist/cjs/src/interpreter/index.cjs +2 -1
  18. package/dist/cjs/src/interpreter/index.cjs.map +3 -3
  19. package/dist/cjs/src/interpreter/interpreter.cjs +301 -76
  20. package/dist/cjs/src/interpreter/interpreter.cjs.map +3 -3
  21. package/dist/cjs/src/lexer/lexer.cjs +13 -1
  22. package/dist/cjs/src/lexer/lexer.cjs.map +3 -3
  23. package/dist/cjs/src/parser/parser.cjs +11 -1
  24. package/dist/cjs/src/parser/parser.cjs.map +3 -3
  25. package/dist/cjs/src/types.cjs.map +2 -2
  26. package/dist/mjs/index.mjs +3 -1
  27. package/dist/mjs/index.mjs.map +3 -3
  28. package/dist/mjs/package.json +1 -1
  29. package/dist/mjs/src/commands/exit/exit.mjs +44 -0
  30. package/dist/mjs/src/commands/exit/exit.mjs.map +10 -0
  31. package/dist/mjs/src/commands/index.mjs +22 -2
  32. package/dist/mjs/src/commands/index.mjs.map +3 -3
  33. package/dist/mjs/src/commands/printf/printf.mjs +376 -0
  34. package/dist/mjs/src/commands/printf/printf.mjs.map +10 -0
  35. package/dist/mjs/src/commands/sh/sh.mjs +94 -0
  36. package/dist/mjs/src/commands/sh/sh.mjs.map +10 -0
  37. package/dist/mjs/src/index.mjs +3 -2
  38. package/dist/mjs/src/index.mjs.map +3 -3
  39. package/dist/mjs/src/interpreter/context.mjs +4 -1
  40. package/dist/mjs/src/interpreter/context.mjs.map +3 -3
  41. package/dist/mjs/src/interpreter/index.mjs +3 -2
  42. package/dist/mjs/src/interpreter/index.mjs.map +2 -2
  43. package/dist/mjs/src/interpreter/interpreter.mjs +301 -76
  44. package/dist/mjs/src/interpreter/interpreter.mjs.map +3 -3
  45. package/dist/mjs/src/lexer/lexer.mjs +13 -1
  46. package/dist/mjs/src/lexer/lexer.mjs.map +3 -3
  47. package/dist/mjs/src/parser/parser.mjs +11 -1
  48. package/dist/mjs/src/parser/parser.mjs.map +3 -3
  49. package/dist/mjs/src/types.mjs.map +2 -2
  50. package/dist/types/index.d.ts +1 -1
  51. package/dist/types/src/commands/exit/exit.d.ts +2 -0
  52. package/dist/types/src/commands/index.d.ts +3 -0
  53. package/dist/types/src/commands/printf/printf.d.ts +2 -0
  54. package/dist/types/src/commands/sh/sh.d.ts +5 -0
  55. package/dist/types/src/index.d.ts +2 -2
  56. package/dist/types/src/interpreter/context.d.ts +2 -1
  57. package/dist/types/src/interpreter/index.d.ts +1 -1
  58. package/dist/types/src/interpreter/interpreter.d.ts +24 -0
  59. package/dist/types/src/types.d.ts +13 -0
  60. package/package.json +1 -1
@@ -56,7 +56,10 @@ function createCommandContext(options) {
56
56
  if (options.exec) {
57
57
  ctx.exec = options.exec;
58
58
  }
59
+ if (options.shell) {
60
+ ctx.shell = options.shell;
61
+ }
59
62
  return ctx;
60
63
  }
61
64
 
62
- //# debugId=F914897FD3ECFE8664756E2164756E21
65
+ //# debugId=A1213B6F49E1441764756E2164756E21
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/interpreter/context.ts"],
4
4
  "sourcesContent": [
5
- "import type { CommandContext, VirtualFS, Stdin, Stdout, Stderr, ExecResult } from \"../types.cjs\";\n\nexport interface ContextOptions {\n args: string[];\n stdin: Stdin;\n stdout: Stdout;\n stderr: Stderr;\n fs: VirtualFS;\n cwd: string;\n env: Record<string, string>;\n setCwd: (path: string) => void;\n exec?: (name: string, args: string[]) => Promise<ExecResult>;\n}\n\nexport function createCommandContext(options: ContextOptions): CommandContext {\n const ctx: CommandContext = {\n args: options.args,\n stdin: options.stdin,\n stdout: options.stdout,\n stderr: options.stderr,\n fs: options.fs,\n cwd: options.cwd,\n env: options.env,\n setCwd: options.setCwd,\n };\n if (options.exec) {\n ctx.exec = options.exec;\n }\n return ctx;\n}\n"
5
+ "import type { CommandContext, VirtualFS, Stdin, Stdout, Stderr, ExecResult, ShellCommandApi } from \"../types.cjs\";\n\nexport interface ContextOptions {\n args: string[];\n stdin: Stdin;\n stdout: Stdout;\n stderr: Stderr;\n fs: VirtualFS;\n cwd: string;\n env: Record<string, string>;\n setCwd: (path: string) => void;\n exec?: (name: string, args: string[]) => Promise<ExecResult>;\n shell?: ShellCommandApi;\n}\n\nexport function createCommandContext(options: ContextOptions): CommandContext {\n const ctx: CommandContext = {\n args: options.args,\n stdin: options.stdin,\n stdout: options.stdout,\n stderr: options.stderr,\n fs: options.fs,\n cwd: options.cwd,\n env: options.env,\n setCwd: options.setCwd,\n };\n if (options.exec) {\n ctx.exec = options.exec;\n }\n if (options.shell) {\n ctx.shell = options.shell;\n }\n return ctx;\n}\n"
6
6
  ],
7
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAcO,SAAS,oBAAoB,CAAC,SAAyC;AAAA,EAC5E,MAAM,MAAsB;AAAA,IAC1B,MAAM,QAAQ;AAAA,IACd,OAAO,QAAQ;AAAA,IACf,QAAQ,QAAQ;AAAA,IAChB,QAAQ,QAAQ;AAAA,IAChB,IAAI,QAAQ;AAAA,IACZ,KAAK,QAAQ;AAAA,IACb,KAAK,QAAQ;AAAA,IACb,QAAQ,QAAQ;AAAA,EAClB;AAAA,EACA,IAAI,QAAQ,MAAM;AAAA,IAChB,IAAI,OAAO,QAAQ;AAAA,EACrB;AAAA,EACA,OAAO;AAAA;",
8
- "debugId": "F914897FD3ECFE8664756E2164756E21",
7
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAeO,SAAS,oBAAoB,CAAC,SAAyC;AAAA,EAC5E,MAAM,MAAsB;AAAA,IAC1B,MAAM,QAAQ;AAAA,IACd,OAAO,QAAQ;AAAA,IACf,QAAQ,QAAQ;AAAA,IAChB,QAAQ,QAAQ;AAAA,IAChB,IAAI,QAAQ;AAAA,IACZ,KAAK,QAAQ;AAAA,IACb,KAAK,QAAQ;AAAA,IACb,QAAQ,QAAQ;AAAA,EAClB;AAAA,EACA,IAAI,QAAQ,MAAM;AAAA,IAChB,IAAI,OAAO,QAAQ;AAAA,EACrB;AAAA,EACA,IAAI,QAAQ,OAAO;AAAA,IACjB,IAAI,QAAQ,QAAQ;AAAA,EACtB;AAAA,EACA,OAAO;AAAA;",
8
+ "debugId": "A1213B6F49E1441764756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -41,6 +41,7 @@ var exports_interpreter = {};
41
41
  __export(exports_interpreter, {
42
42
  createCommandContext: () => import_context.createCommandContext,
43
43
  Interpreter: () => import_interpreter.Interpreter,
44
+ ExitException: () => import_interpreter.ExitException,
44
45
  ContinueException: () => import_interpreter.ContinueException,
45
46
  BreakException: () => import_interpreter.BreakException
46
47
  });
@@ -48,4 +49,4 @@ module.exports = __toCommonJS(exports_interpreter);
48
49
  var import_interpreter = require("./interpreter.cjs");
49
50
  var import_context = require("./context.cjs");
50
51
 
51
- //# debugId=D80D067B497FC53D64756E2164756E21
52
+ //# debugId=478C5E50B11BB65664756E2164756E21
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/interpreter/index.ts"],
4
4
  "sourcesContent": [
5
- "export { Interpreter, type InterpreterOptions, BreakException, ContinueException } from \"./interpreter.cjs\";\nexport { createCommandContext, type ContextOptions } from \"./context.cjs\";\n"
5
+ "export { Interpreter, type InterpreterOptions, BreakException, ContinueException, ExitException } from \"./interpreter.cjs\";\nexport { createCommandContext, type ContextOptions } from \"./context.cjs\";\n"
6
6
  ],
7
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAwF,IAAxF;AAC0D,IAA1D;",
8
- "debugId": "D80D067B497FC53D64756E2164756E21",
7
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAuG,IAAvG;AAC0D,IAA1D;",
8
+ "debugId": "478C5E50B11BB65664756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -40,11 +40,14 @@ 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");
50
53
  var import_special_files = require("../fs/special-files.cjs");
@@ -68,6 +71,14 @@ class ContinueException extends Error {
68
71
  }
69
72
  }
70
73
 
74
+ class ExitException extends Error {
75
+ exitCode;
76
+ constructor(exitCode) {
77
+ super("exit");
78
+ this.exitCode = exitCode;
79
+ }
80
+ }
81
+
71
82
  class Interpreter {
72
83
  fs;
73
84
  cwd;
@@ -76,6 +87,9 @@ class Interpreter {
76
87
  redirectObjects;
77
88
  loopDepth = 0;
78
89
  isTTY;
90
+ argv0;
91
+ positionalParameters;
92
+ lastExitCode;
79
93
  constructor(options) {
80
94
  this.fs = options.fs;
81
95
  this.cwd = options.cwd;
@@ -83,6 +97,9 @@ class Interpreter {
83
97
  this.commands = options.commands;
84
98
  this.redirectObjects = options.redirectObjects ?? {};
85
99
  this.isTTY = options.isTTY ?? false;
100
+ this.argv0 = options.argv0 ?? "sh";
101
+ this.positionalParameters = [...options.positionalParameters ?? []];
102
+ this.lastExitCode = options.lastExitCode ?? 0;
86
103
  }
87
104
  getLoopDepth() {
88
105
  return this.loopDepth;
@@ -90,7 +107,16 @@ class Interpreter {
90
107
  async execute(ast) {
91
108
  const stdout = import_stdout.createStdout(this.isTTY);
92
109
  const stderr = import_stdout.createStderr(this.isTTY);
93
- const exitCode = await this.executeNode(ast, null, stdout, stderr);
110
+ let exitCode;
111
+ try {
112
+ exitCode = await this.executeNode(ast, null, stdout, stderr);
113
+ } catch (err) {
114
+ if (!(err instanceof ExitException)) {
115
+ throw err;
116
+ }
117
+ exitCode = err.exitCode;
118
+ this.lastExitCode = exitCode;
119
+ }
94
120
  stdout.close();
95
121
  stderr.close();
96
122
  return {
@@ -100,30 +126,43 @@ class Interpreter {
100
126
  };
101
127
  }
102
128
  async executeNode(node, stdinSource, stdout, stderr) {
129
+ let exitCode;
103
130
  switch (node.type) {
104
131
  case "command":
105
- return this.executeCommand(node, stdinSource, stdout, stderr);
132
+ exitCode = await this.executeCommand(node, stdinSource, stdout, stderr);
133
+ break;
106
134
  case "pipeline":
107
- return this.executePipeline(node.commands, stdinSource, stdout, stderr);
135
+ exitCode = await this.executePipeline(node.commands, stdinSource, stdout, stderr);
136
+ break;
108
137
  case "sequence":
109
- return this.executeSequence(node.commands, stdinSource, stdout, stderr);
138
+ exitCode = await this.executeSequence(node.commands, stdinSource, stdout, stderr);
139
+ break;
110
140
  case "and":
111
- return this.executeAnd(node.left, node.right, stdinSource, stdout, stderr);
141
+ exitCode = await this.executeAnd(node.left, node.right, stdinSource, stdout, stderr);
142
+ break;
112
143
  case "or":
113
- return this.executeOr(node.left, node.right, stdinSource, stdout, stderr);
144
+ exitCode = await this.executeOr(node.left, node.right, stdinSource, stdout, stderr);
145
+ break;
114
146
  case "if":
115
- return this.executeIf(node, stdinSource, stdout, stderr);
147
+ exitCode = await this.executeIf(node, stdinSource, stdout, stderr);
148
+ break;
116
149
  case "for":
117
- return this.executeFor(node, stdinSource, stdout, stderr);
150
+ exitCode = await this.executeFor(node, stdinSource, stdout, stderr);
151
+ break;
118
152
  case "while":
119
- return this.executeWhile(node, stdinSource, stdout, stderr);
153
+ exitCode = await this.executeWhile(node, stdinSource, stdout, stderr);
154
+ break;
120
155
  case "until":
121
- return this.executeUntil(node, stdinSource, stdout, stderr);
156
+ exitCode = await this.executeUntil(node, stdinSource, stdout, stderr);
157
+ break;
122
158
  case "case":
123
- return this.executeCase(node, stdinSource, stdout, stderr);
159
+ exitCode = await this.executeCase(node, stdinSource, stdout, stderr);
160
+ break;
124
161
  default:
125
162
  throw new Error("Cannot execute unknown node type");
126
163
  }
164
+ this.lastExitCode = exitCode;
165
+ return exitCode;
127
166
  }
128
167
  async executeCommand(node, stdinSource, stdout, stderr) {
129
168
  const assignmentEnv = { ...this.env };
@@ -171,95 +210,239 @@ class Interpreter {
171
210
  if (stdoutToStderr) {
172
211
  actualStdout = actualStderr;
173
212
  }
174
- const command = this.commands[name];
175
- if (!command) {
176
- await stderr.writeText(`${name}: command not found
213
+ let exitCode = await this.invokeCommand(name, args, actualStdin, actualStdout, actualStderr, assignmentEnv);
214
+ if (actualStdout !== stdout) {
215
+ actualStdout.close();
216
+ }
217
+ if (actualStderr !== stderr && actualStderr !== actualStdout) {
218
+ actualStderr.close();
219
+ }
220
+ try {
221
+ await Promise.all(fileWritePromises);
222
+ } catch (err) {
223
+ const message = err instanceof Error ? err.message : String(err);
224
+ const writeRedirects = node.redirects.filter((r) => r.mode !== "<" && r.mode !== "2>&1" && r.mode !== "1>&2");
225
+ const target = writeRedirects.length > 0 ? await this.expandWordScalar(writeRedirects[writeRedirects.length - 1].target, this.env) : "unknown";
226
+ await stderr.writeText(`sh: ${target}: ${message}
177
227
  `);
178
- return 127;
228
+ exitCode = 1;
179
229
  }
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
- };
230
+ return exitCode;
231
+ }
232
+ async invokeCommand(name, args, stdinSource, stdout, stderr, env) {
233
+ const command = this.commands[name];
234
+ if (command) {
235
+ return this.invokeRegisteredCommand(name, command, args, stdinSource, stdout, stderr, env);
236
+ }
237
+ if (name.includes("/")) {
238
+ return this.executeExecutableFile(name, args, stdinSource, stdout, stderr, env);
239
+ }
240
+ await stderr.writeText(`${name}: command not found
241
+ `);
242
+ return 127;
243
+ }
244
+ async invokeRegisteredCommand(name, command, args, stdinSource, stdout, stderr, env) {
245
+ const exec = this.createExec(env);
246
+ const shell = this.createShellApi(stdinSource, stdout, stderr, env);
247
+ const ctx = import_context.createCommandContext({
248
+ args,
249
+ stdin: import_stdin.createStdin(stdinSource),
250
+ stdout,
251
+ stderr,
252
+ fs: this.fs,
253
+ cwd: this.cwd,
254
+ env,
255
+ setCwd: (path) => this.setCwd(path),
256
+ exec,
257
+ shell
258
+ });
259
+ try {
260
+ return await command(ctx);
261
+ } catch (err) {
262
+ if (err instanceof BreakException || err instanceof ContinueException || err instanceof ExitException) {
263
+ throw err;
189
264
  }
265
+ const message = err instanceof Error ? err.message : String(err);
266
+ await stderr.writeText(`${name}: ${message}
267
+ `);
268
+ return 1;
269
+ }
270
+ }
271
+ createExec(env) {
272
+ return async (name, args) => {
190
273
  const subStdout = import_stdout.createStdout();
191
274
  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}
212
- `);
213
- exitCode2 = 1;
214
- }
275
+ const exitCode = await this.invokeCommand(name, args, null, subStdout, subStderr, { ...env });
215
276
  subStdout.close();
216
277
  subStderr.close();
217
278
  return {
218
279
  stdout: await subStdout.collect(),
219
280
  stderr: await subStderr.collect(),
220
- exitCode: exitCode2
281
+ exitCode
221
282
  };
222
283
  };
223
- const ctx = import_context.createCommandContext({
224
- args,
225
- stdin: import_stdin.createStdin(actualStdin),
226
- stdout: actualStdout,
227
- stderr: actualStderr,
284
+ }
285
+ createShellApi(stdinSource, stdout, stderr, env) {
286
+ return {
287
+ eval: (source) => this.executeSourceInCurrentFrame(source, stdinSource, stdout, stderr, "eval"),
288
+ source: (path, args = []) => this.sourceFile(path, args, stdinSource, stdout, stderr),
289
+ runScript: (path, args = []) => this.executeExecutableFile(path, args, stdinSource, stdout, stderr, { ...env }),
290
+ runShell: (source, options = {}) => this.executeIsolatedShellSource(source, options.argv0 ?? "sh", options.args ?? [], stdinSource, stdout, stderr, env),
291
+ getLastExitCode: () => this.lastExitCode,
292
+ exit: (exitCode = this.lastExitCode) => {
293
+ throw new ExitException(this.normalizeExitCode(exitCode));
294
+ }
295
+ };
296
+ }
297
+ async executeExecutableFile(pathName, args, stdinSource, stdout, stderr, env) {
298
+ const loaded = await this.loadScriptSource(pathName, stderr, 127);
299
+ if (!loaded.ok) {
300
+ return loaded.exitCode;
301
+ }
302
+ const shebang = this.parseShebang(loaded.source);
303
+ if (!shebang || shebang.command === "sh") {
304
+ return this.executeIsolatedShellSource(loaded.source, pathName, args, stdinSource, stdout, stderr, env);
305
+ }
306
+ const command = this.commands[shebang.command];
307
+ if (!command) {
308
+ await stderr.writeText(`${pathName}: unsupported interpreter: ${shebang.display}
309
+ `);
310
+ return 126;
311
+ }
312
+ const child = new Interpreter({
228
313
  fs: this.fs,
229
314
  cwd: this.cwd,
230
- env: assignmentEnv,
231
- setCwd: (path) => this.setCwd(path),
232
- exec
315
+ env: { ...env },
316
+ commands: this.commands,
317
+ redirectObjects: this.redirectObjects,
318
+ isTTY: this.isTTY,
319
+ argv0: shebang.command,
320
+ positionalParameters: []
321
+ });
322
+ return child.invokeRegisteredCommand(shebang.command, command, [...shebang.args, pathName, ...args], stdinSource, stdout, stderr, { ...env });
323
+ }
324
+ async sourceFile(pathName, args, stdinSource, stdout, stderr) {
325
+ const loaded = await this.loadScriptSource(pathName, stderr, 1);
326
+ if (!loaded.ok) {
327
+ return loaded.exitCode;
328
+ }
329
+ return this.executeSourceInCurrentFrame(loaded.source, stdinSource, stdout, stderr, pathName, args.length > 0 ? { args } : undefined);
330
+ }
331
+ async executeIsolatedShellSource(source, argv0, args, stdinSource, stdout, stderr, env) {
332
+ const interpreter = new Interpreter({
333
+ fs: this.fs,
334
+ cwd: this.cwd,
335
+ env: { ...env },
336
+ commands: this.commands,
337
+ redirectObjects: this.redirectObjects,
338
+ isTTY: this.isTTY,
339
+ argv0,
340
+ positionalParameters: args
233
341
  });
234
- let exitCode;
235
342
  try {
236
- exitCode = await command(ctx);
343
+ return await interpreter.executeSourceInCurrentFrame(source, stdinSource, stdout, stderr, argv0);
237
344
  } catch (err) {
238
- if (err instanceof BreakException || err instanceof ContinueException) {
345
+ if (err instanceof ExitException) {
346
+ return err.exitCode;
347
+ }
348
+ throw err;
349
+ }
350
+ }
351
+ async executeSourceInCurrentFrame(source, stdinSource, stdout, stderr, errorName, positionalOverride) {
352
+ const previousArgv0 = this.argv0;
353
+ const previousPositionals = this.positionalParameters;
354
+ if (positionalOverride?.argv0 !== undefined) {
355
+ this.argv0 = positionalOverride.argv0;
356
+ }
357
+ if (positionalOverride?.args !== undefined) {
358
+ this.positionalParameters = [...positionalOverride.args];
359
+ }
360
+ try {
361
+ const ast = this.parseSource(source);
362
+ if (!ast) {
363
+ return 0;
364
+ }
365
+ return await this.executeNode(ast, stdinSource, stdout, stderr);
366
+ } catch (err) {
367
+ if (err instanceof BreakException || err instanceof ContinueException || err instanceof ExitException) {
239
368
  throw err;
240
369
  }
241
370
  const message = err instanceof Error ? err.message : String(err);
242
- await stderr.writeText(`${name}: ${message}
371
+ await stderr.writeText(`${errorName}: ${message}
243
372
  `);
244
- exitCode = 1;
373
+ return 2;
374
+ } finally {
375
+ if (positionalOverride?.argv0 !== undefined) {
376
+ this.argv0 = previousArgv0;
377
+ }
378
+ if (positionalOverride?.args !== undefined) {
379
+ this.positionalParameters = previousPositionals;
380
+ }
245
381
  }
246
- if (actualStdout !== stdout) {
247
- actualStdout.close();
382
+ }
383
+ parseSource(source) {
384
+ const tokens = new import_lexer.Lexer(source, { preserveNewlines: true }).tokenize();
385
+ if (tokens.every((token) => token.type === "newline" || token.type === "eof")) {
386
+ return null;
248
387
  }
249
- if (actualStderr !== stderr && actualStderr !== actualStdout) {
250
- actualStderr.close();
388
+ return new import_parser.Parser(tokens).parse();
389
+ }
390
+ async loadScriptSource(pathName, stderr, missingExitCode) {
391
+ const path = this.fs.resolve(this.cwd, pathName);
392
+ if (!await this.fs.exists(path)) {
393
+ await stderr.writeText(`${pathName}: No such file or directory
394
+ `);
395
+ return { ok: false, exitCode: missingExitCode };
396
+ }
397
+ const stat = await this.fs.stat(path);
398
+ if (stat.isDirectory()) {
399
+ await stderr.writeText(`${pathName}: is a directory
400
+ `);
401
+ return { ok: false, exitCode: 126 };
402
+ }
403
+ if (!stat.isFile()) {
404
+ await stderr.writeText(`${pathName}: not a file
405
+ `);
406
+ return { ok: false, exitCode: 126 };
251
407
  }
252
408
  try {
253
- await Promise.all(fileWritePromises);
409
+ return { ok: true, path, source: await this.fs.readFile(path, "utf-8") };
254
410
  } catch (err) {
255
411
  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}
412
+ await stderr.writeText(`${pathName}: ${message}
259
413
  `);
260
- exitCode = 1;
414
+ return { ok: false, exitCode: 126 };
261
415
  }
262
- return exitCode;
416
+ }
417
+ parseShebang(source) {
418
+ if (!source.startsWith("#!")) {
419
+ return null;
420
+ }
421
+ const lineEnd = source.indexOf(`
422
+ `);
423
+ const line = source.slice(2, lineEnd === -1 ? undefined : lineEnd).trim();
424
+ if (line === "") {
425
+ return { command: "sh", args: [], display: "sh" };
426
+ }
427
+ const parts = line.split(/\s+/);
428
+ const executable = parts[0];
429
+ const executableName = this.fs.basename(executable);
430
+ if (executableName === "env") {
431
+ const envCommand = parts[1];
432
+ if (!envCommand) {
433
+ return { command: "env", args: [], display: line };
434
+ }
435
+ return {
436
+ command: this.fs.basename(envCommand),
437
+ args: parts.slice(2),
438
+ display: line
439
+ };
440
+ }
441
+ return {
442
+ command: executableName,
443
+ args: parts.slice(1),
444
+ display: line
445
+ };
263
446
  }
264
447
  async handleRedirect(redirect, stdin, stdout, stderr, env) {
265
448
  const target = await this.expandWordScalar(redirect.target, env);
@@ -672,6 +855,10 @@ class Interpreter {
672
855
  this.appendSegment(fields[fields.length - 1], part.value, part.quoted);
673
856
  continue;
674
857
  }
858
+ if (part.type === "variable" && part.name === "@" && part.quoted) {
859
+ this.appendQuotedPositionalParameters(fields);
860
+ continue;
861
+ }
675
862
  const value = await this.expandWordPart(part, env);
676
863
  if (part.quoted) {
677
864
  this.appendSegment(fields[fields.length - 1], value, true);
@@ -695,7 +882,7 @@ class Interpreter {
695
882
  case "text":
696
883
  return part.value;
697
884
  case "variable":
698
- return env[part.name] ?? "";
885
+ return this.getVariableValue(part.name, env);
699
886
  case "substitution":
700
887
  return this.executeSubstitution(part.command, env);
701
888
  case "arithmetic":
@@ -704,6 +891,35 @@ class Interpreter {
704
891
  throw new Error("Cannot expand unknown word part");
705
892
  }
706
893
  }
894
+ getVariableValue(name, env) {
895
+ if (name === "0") {
896
+ return this.argv0;
897
+ }
898
+ if (name === "#") {
899
+ return String(this.positionalParameters.length);
900
+ }
901
+ if (name === "?") {
902
+ return String(this.lastExitCode);
903
+ }
904
+ if (name === "*" || name === "@") {
905
+ return this.positionalParameters.join(" ");
906
+ }
907
+ if (/^[1-9]$/.test(name)) {
908
+ return this.positionalParameters[Number(name) - 1] ?? "";
909
+ }
910
+ return env[name] ?? "";
911
+ }
912
+ appendQuotedPositionalParameters(fields) {
913
+ if (this.positionalParameters.length === 0) {
914
+ return;
915
+ }
916
+ this.appendSegment(fields[fields.length - 1], this.positionalParameters[0], true);
917
+ for (let i = 1;i < this.positionalParameters.length; i++) {
918
+ const field = this.createExpandedField();
919
+ this.appendSegment(field, this.positionalParameters[i], true);
920
+ fields.push(field);
921
+ }
922
+ }
707
923
  async executeSubstitution(command, env) {
708
924
  const interpreter = new Interpreter({
709
925
  fs: this.fs,
@@ -711,7 +927,10 @@ class Interpreter {
711
927
  env,
712
928
  commands: this.commands,
713
929
  redirectObjects: this.redirectObjects,
714
- isTTY: false
930
+ isTTY: false,
931
+ argv0: this.argv0,
932
+ positionalParameters: this.positionalParameters,
933
+ lastExitCode: this.lastExitCode
715
934
  });
716
935
  const result = await interpreter.execute(command);
717
936
  return result.stdout.toString("utf-8").replace(/\n+$/, "");
@@ -812,8 +1031,8 @@ class Interpreter {
812
1031
  expandedExpr = expandedExpr.replace(/\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g, (_, name) => {
813
1032
  return env[name] ?? "0";
814
1033
  });
815
- expandedExpr = expandedExpr.replace(/\$([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, name) => {
816
- return env[name] ?? "0";
1034
+ expandedExpr = expandedExpr.replace(/\$([a-zA-Z_][a-zA-Z0-9_]*|[0-9#*@?])/g, (_, name) => {
1035
+ return this.getVariableValue(name, env) || "0";
817
1036
  });
818
1037
  expandedExpr = expandedExpr.replace(/\b([a-zA-Z_][a-zA-Z0-9_]*)\b/g, (match) => {
819
1038
  if (/^\d+$/.test(match))
@@ -970,6 +1189,12 @@ class Interpreter {
970
1189
  };
971
1190
  return parseOr();
972
1191
  }
1192
+ normalizeExitCode(exitCode) {
1193
+ if (!Number.isFinite(exitCode)) {
1194
+ return 2;
1195
+ }
1196
+ return (Math.trunc(exitCode) % 256 + 256) % 256;
1197
+ }
973
1198
  setCwd(cwd) {
974
1199
  this.env.OLDPWD = this.cwd;
975
1200
  this.cwd = cwd;
@@ -985,4 +1210,4 @@ class Interpreter {
985
1210
  }
986
1211
  }
987
1212
 
988
- //# debugId=1238E19B5ED5404064756E2164756E21
1213
+ //# debugId=48778DC29A4E811364756E2164756E21