shell-dsl 0.0.39 → 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 (50) hide show
  1. package/README.md +117 -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 +2 -1
  10. package/dist/cjs/src/index.cjs.map +3 -3
  11. package/dist/cjs/src/interpreter/context.cjs +4 -1
  12. package/dist/cjs/src/interpreter/context.cjs.map +3 -3
  13. package/dist/cjs/src/interpreter/index.cjs +2 -1
  14. package/dist/cjs/src/interpreter/index.cjs.map +3 -3
  15. package/dist/cjs/src/interpreter/interpreter.cjs +301 -76
  16. package/dist/cjs/src/interpreter/interpreter.cjs.map +3 -3
  17. package/dist/cjs/src/lexer/lexer.cjs +13 -1
  18. package/dist/cjs/src/lexer/lexer.cjs.map +3 -3
  19. package/dist/cjs/src/parser/parser.cjs +11 -1
  20. package/dist/cjs/src/parser/parser.cjs.map +3 -3
  21. package/dist/cjs/src/types.cjs.map +2 -2
  22. package/dist/mjs/package.json +1 -1
  23. package/dist/mjs/src/commands/exit/exit.mjs +44 -0
  24. package/dist/mjs/src/commands/exit/exit.mjs.map +10 -0
  25. package/dist/mjs/src/commands/index.mjs +18 -2
  26. package/dist/mjs/src/commands/index.mjs.map +3 -3
  27. package/dist/mjs/src/commands/sh/sh.mjs +94 -0
  28. package/dist/mjs/src/commands/sh/sh.mjs.map +10 -0
  29. package/dist/mjs/src/index.mjs +3 -2
  30. package/dist/mjs/src/index.mjs.map +3 -3
  31. package/dist/mjs/src/interpreter/context.mjs +4 -1
  32. package/dist/mjs/src/interpreter/context.mjs.map +3 -3
  33. package/dist/mjs/src/interpreter/index.mjs +3 -2
  34. package/dist/mjs/src/interpreter/index.mjs.map +2 -2
  35. package/dist/mjs/src/interpreter/interpreter.mjs +301 -76
  36. package/dist/mjs/src/interpreter/interpreter.mjs.map +3 -3
  37. package/dist/mjs/src/lexer/lexer.mjs +13 -1
  38. package/dist/mjs/src/lexer/lexer.mjs.map +3 -3
  39. package/dist/mjs/src/parser/parser.mjs +11 -1
  40. package/dist/mjs/src/parser/parser.mjs.map +3 -3
  41. package/dist/mjs/src/types.mjs.map +2 -2
  42. package/dist/types/src/commands/exit/exit.d.ts +2 -0
  43. package/dist/types/src/commands/index.d.ts +2 -0
  44. package/dist/types/src/commands/sh/sh.d.ts +5 -0
  45. package/dist/types/src/index.d.ts +2 -2
  46. package/dist/types/src/interpreter/context.d.ts +2 -1
  47. package/dist/types/src/interpreter/index.d.ts +1 -1
  48. package/dist/types/src/interpreter/interpreter.d.ts +24 -0
  49. package/dist/types/src/types.d.ts +13 -0
  50. package/package.json +1 -1
@@ -1,5 +1,7 @@
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";
5
7
  import { isDevNullPath } from "../fs/special-files.mjs";
@@ -23,6 +25,14 @@ class ContinueException extends Error {
23
25
  }
24
26
  }
25
27
 
28
+ class ExitException extends Error {
29
+ exitCode;
30
+ constructor(exitCode) {
31
+ super("exit");
32
+ this.exitCode = exitCode;
33
+ }
34
+ }
35
+
26
36
  class Interpreter {
27
37
  fs;
28
38
  cwd;
@@ -31,6 +41,9 @@ class Interpreter {
31
41
  redirectObjects;
32
42
  loopDepth = 0;
33
43
  isTTY;
44
+ argv0;
45
+ positionalParameters;
46
+ lastExitCode;
34
47
  constructor(options) {
35
48
  this.fs = options.fs;
36
49
  this.cwd = options.cwd;
@@ -38,6 +51,9 @@ class Interpreter {
38
51
  this.commands = options.commands;
39
52
  this.redirectObjects = options.redirectObjects ?? {};
40
53
  this.isTTY = options.isTTY ?? false;
54
+ this.argv0 = options.argv0 ?? "sh";
55
+ this.positionalParameters = [...options.positionalParameters ?? []];
56
+ this.lastExitCode = options.lastExitCode ?? 0;
41
57
  }
42
58
  getLoopDepth() {
43
59
  return this.loopDepth;
@@ -45,7 +61,16 @@ class Interpreter {
45
61
  async execute(ast) {
46
62
  const stdout = createStdout(this.isTTY);
47
63
  const stderr = createStderr(this.isTTY);
48
- const exitCode = await this.executeNode(ast, null, stdout, stderr);
64
+ let exitCode;
65
+ try {
66
+ exitCode = await this.executeNode(ast, null, stdout, stderr);
67
+ } catch (err) {
68
+ if (!(err instanceof ExitException)) {
69
+ throw err;
70
+ }
71
+ exitCode = err.exitCode;
72
+ this.lastExitCode = exitCode;
73
+ }
49
74
  stdout.close();
50
75
  stderr.close();
51
76
  return {
@@ -55,30 +80,43 @@ class Interpreter {
55
80
  };
56
81
  }
57
82
  async executeNode(node, stdinSource, stdout, stderr) {
83
+ let exitCode;
58
84
  switch (node.type) {
59
85
  case "command":
60
- return this.executeCommand(node, stdinSource, stdout, stderr);
86
+ exitCode = await this.executeCommand(node, stdinSource, stdout, stderr);
87
+ break;
61
88
  case "pipeline":
62
- return this.executePipeline(node.commands, stdinSource, stdout, stderr);
89
+ exitCode = await this.executePipeline(node.commands, stdinSource, stdout, stderr);
90
+ break;
63
91
  case "sequence":
64
- return this.executeSequence(node.commands, stdinSource, stdout, stderr);
92
+ exitCode = await this.executeSequence(node.commands, stdinSource, stdout, stderr);
93
+ break;
65
94
  case "and":
66
- return this.executeAnd(node.left, node.right, stdinSource, stdout, stderr);
95
+ exitCode = await this.executeAnd(node.left, node.right, stdinSource, stdout, stderr);
96
+ break;
67
97
  case "or":
68
- return this.executeOr(node.left, node.right, stdinSource, stdout, stderr);
98
+ exitCode = await this.executeOr(node.left, node.right, stdinSource, stdout, stderr);
99
+ break;
69
100
  case "if":
70
- return this.executeIf(node, stdinSource, stdout, stderr);
101
+ exitCode = await this.executeIf(node, stdinSource, stdout, stderr);
102
+ break;
71
103
  case "for":
72
- return this.executeFor(node, stdinSource, stdout, stderr);
104
+ exitCode = await this.executeFor(node, stdinSource, stdout, stderr);
105
+ break;
73
106
  case "while":
74
- return this.executeWhile(node, stdinSource, stdout, stderr);
107
+ exitCode = await this.executeWhile(node, stdinSource, stdout, stderr);
108
+ break;
75
109
  case "until":
76
- return this.executeUntil(node, stdinSource, stdout, stderr);
110
+ exitCode = await this.executeUntil(node, stdinSource, stdout, stderr);
111
+ break;
77
112
  case "case":
78
- return this.executeCase(node, stdinSource, stdout, stderr);
113
+ exitCode = await this.executeCase(node, stdinSource, stdout, stderr);
114
+ break;
79
115
  default:
80
116
  throw new Error("Cannot execute unknown node type");
81
117
  }
118
+ this.lastExitCode = exitCode;
119
+ return exitCode;
82
120
  }
83
121
  async executeCommand(node, stdinSource, stdout, stderr) {
84
122
  const assignmentEnv = { ...this.env };
@@ -126,95 +164,239 @@ class Interpreter {
126
164
  if (stdoutToStderr) {
127
165
  actualStdout = actualStderr;
128
166
  }
129
- const command = this.commands[name];
130
- if (!command) {
131
- await stderr.writeText(`${name}: command not found
167
+ let exitCode = await this.invokeCommand(name, args, actualStdin, actualStdout, actualStderr, assignmentEnv);
168
+ if (actualStdout !== stdout) {
169
+ actualStdout.close();
170
+ }
171
+ if (actualStderr !== stderr && actualStderr !== actualStdout) {
172
+ actualStderr.close();
173
+ }
174
+ try {
175
+ await Promise.all(fileWritePromises);
176
+ } catch (err) {
177
+ const message = err instanceof Error ? err.message : String(err);
178
+ const writeRedirects = node.redirects.filter((r) => r.mode !== "<" && r.mode !== "2>&1" && r.mode !== "1>&2");
179
+ const target = writeRedirects.length > 0 ? await this.expandWordScalar(writeRedirects[writeRedirects.length - 1].target, this.env) : "unknown";
180
+ await stderr.writeText(`sh: ${target}: ${message}
132
181
  `);
133
- return 127;
182
+ exitCode = 1;
134
183
  }
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
- };
184
+ return exitCode;
185
+ }
186
+ async invokeCommand(name, args, stdinSource, stdout, stderr, env) {
187
+ const command = this.commands[name];
188
+ if (command) {
189
+ return this.invokeRegisteredCommand(name, command, args, stdinSource, stdout, stderr, env);
190
+ }
191
+ if (name.includes("/")) {
192
+ return this.executeExecutableFile(name, args, stdinSource, stdout, stderr, env);
193
+ }
194
+ await stderr.writeText(`${name}: command not found
195
+ `);
196
+ return 127;
197
+ }
198
+ async invokeRegisteredCommand(name, command, args, stdinSource, stdout, stderr, env) {
199
+ const exec = this.createExec(env);
200
+ const shell = this.createShellApi(stdinSource, stdout, stderr, env);
201
+ const ctx = createCommandContext({
202
+ args,
203
+ stdin: createStdin(stdinSource),
204
+ stdout,
205
+ stderr,
206
+ fs: this.fs,
207
+ cwd: this.cwd,
208
+ env,
209
+ setCwd: (path) => this.setCwd(path),
210
+ exec,
211
+ shell
212
+ });
213
+ try {
214
+ return await command(ctx);
215
+ } catch (err) {
216
+ if (err instanceof BreakException || err instanceof ContinueException || err instanceof ExitException) {
217
+ throw err;
144
218
  }
219
+ const message = err instanceof Error ? err.message : String(err);
220
+ await stderr.writeText(`${name}: ${message}
221
+ `);
222
+ return 1;
223
+ }
224
+ }
225
+ createExec(env) {
226
+ return async (name, args) => {
145
227
  const subStdout = createStdout();
146
228
  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}
167
- `);
168
- exitCode2 = 1;
169
- }
229
+ const exitCode = await this.invokeCommand(name, args, null, subStdout, subStderr, { ...env });
170
230
  subStdout.close();
171
231
  subStderr.close();
172
232
  return {
173
233
  stdout: await subStdout.collect(),
174
234
  stderr: await subStderr.collect(),
175
- exitCode: exitCode2
235
+ exitCode
176
236
  };
177
237
  };
178
- const ctx = createCommandContext({
179
- args,
180
- stdin: createStdin(actualStdin),
181
- stdout: actualStdout,
182
- stderr: actualStderr,
238
+ }
239
+ createShellApi(stdinSource, stdout, stderr, env) {
240
+ return {
241
+ eval: (source) => this.executeSourceInCurrentFrame(source, stdinSource, stdout, stderr, "eval"),
242
+ source: (path, args = []) => this.sourceFile(path, args, stdinSource, stdout, stderr),
243
+ runScript: (path, args = []) => this.executeExecutableFile(path, args, stdinSource, stdout, stderr, { ...env }),
244
+ runShell: (source, options = {}) => this.executeIsolatedShellSource(source, options.argv0 ?? "sh", options.args ?? [], stdinSource, stdout, stderr, env),
245
+ getLastExitCode: () => this.lastExitCode,
246
+ exit: (exitCode = this.lastExitCode) => {
247
+ throw new ExitException(this.normalizeExitCode(exitCode));
248
+ }
249
+ };
250
+ }
251
+ async executeExecutableFile(pathName, args, stdinSource, stdout, stderr, env) {
252
+ const loaded = await this.loadScriptSource(pathName, stderr, 127);
253
+ if (!loaded.ok) {
254
+ return loaded.exitCode;
255
+ }
256
+ const shebang = this.parseShebang(loaded.source);
257
+ if (!shebang || shebang.command === "sh") {
258
+ return this.executeIsolatedShellSource(loaded.source, pathName, args, stdinSource, stdout, stderr, env);
259
+ }
260
+ const command = this.commands[shebang.command];
261
+ if (!command) {
262
+ await stderr.writeText(`${pathName}: unsupported interpreter: ${shebang.display}
263
+ `);
264
+ return 126;
265
+ }
266
+ const child = new Interpreter({
183
267
  fs: this.fs,
184
268
  cwd: this.cwd,
185
- env: assignmentEnv,
186
- setCwd: (path) => this.setCwd(path),
187
- exec
269
+ env: { ...env },
270
+ commands: this.commands,
271
+ redirectObjects: this.redirectObjects,
272
+ isTTY: this.isTTY,
273
+ argv0: shebang.command,
274
+ positionalParameters: []
275
+ });
276
+ return child.invokeRegisteredCommand(shebang.command, command, [...shebang.args, pathName, ...args], stdinSource, stdout, stderr, { ...env });
277
+ }
278
+ async sourceFile(pathName, args, stdinSource, stdout, stderr) {
279
+ const loaded = await this.loadScriptSource(pathName, stderr, 1);
280
+ if (!loaded.ok) {
281
+ return loaded.exitCode;
282
+ }
283
+ return this.executeSourceInCurrentFrame(loaded.source, stdinSource, stdout, stderr, pathName, args.length > 0 ? { args } : undefined);
284
+ }
285
+ async executeIsolatedShellSource(source, argv0, args, stdinSource, stdout, stderr, env) {
286
+ const interpreter = new Interpreter({
287
+ fs: this.fs,
288
+ cwd: this.cwd,
289
+ env: { ...env },
290
+ commands: this.commands,
291
+ redirectObjects: this.redirectObjects,
292
+ isTTY: this.isTTY,
293
+ argv0,
294
+ positionalParameters: args
188
295
  });
189
- let exitCode;
190
296
  try {
191
- exitCode = await command(ctx);
297
+ return await interpreter.executeSourceInCurrentFrame(source, stdinSource, stdout, stderr, argv0);
192
298
  } catch (err) {
193
- if (err instanceof BreakException || err instanceof ContinueException) {
299
+ if (err instanceof ExitException) {
300
+ return err.exitCode;
301
+ }
302
+ throw err;
303
+ }
304
+ }
305
+ async executeSourceInCurrentFrame(source, stdinSource, stdout, stderr, errorName, positionalOverride) {
306
+ const previousArgv0 = this.argv0;
307
+ const previousPositionals = this.positionalParameters;
308
+ if (positionalOverride?.argv0 !== undefined) {
309
+ this.argv0 = positionalOverride.argv0;
310
+ }
311
+ if (positionalOverride?.args !== undefined) {
312
+ this.positionalParameters = [...positionalOverride.args];
313
+ }
314
+ try {
315
+ const ast = this.parseSource(source);
316
+ if (!ast) {
317
+ return 0;
318
+ }
319
+ return await this.executeNode(ast, stdinSource, stdout, stderr);
320
+ } catch (err) {
321
+ if (err instanceof BreakException || err instanceof ContinueException || err instanceof ExitException) {
194
322
  throw err;
195
323
  }
196
324
  const message = err instanceof Error ? err.message : String(err);
197
- await stderr.writeText(`${name}: ${message}
325
+ await stderr.writeText(`${errorName}: ${message}
198
326
  `);
199
- exitCode = 1;
327
+ return 2;
328
+ } finally {
329
+ if (positionalOverride?.argv0 !== undefined) {
330
+ this.argv0 = previousArgv0;
331
+ }
332
+ if (positionalOverride?.args !== undefined) {
333
+ this.positionalParameters = previousPositionals;
334
+ }
200
335
  }
201
- if (actualStdout !== stdout) {
202
- actualStdout.close();
336
+ }
337
+ parseSource(source) {
338
+ const tokens = new Lexer(source, { preserveNewlines: true }).tokenize();
339
+ if (tokens.every((token) => token.type === "newline" || token.type === "eof")) {
340
+ return null;
203
341
  }
204
- if (actualStderr !== stderr && actualStderr !== actualStdout) {
205
- actualStderr.close();
342
+ return new Parser(tokens).parse();
343
+ }
344
+ async loadScriptSource(pathName, stderr, missingExitCode) {
345
+ const path = this.fs.resolve(this.cwd, pathName);
346
+ if (!await this.fs.exists(path)) {
347
+ await stderr.writeText(`${pathName}: No such file or directory
348
+ `);
349
+ return { ok: false, exitCode: missingExitCode };
350
+ }
351
+ const stat = await this.fs.stat(path);
352
+ if (stat.isDirectory()) {
353
+ await stderr.writeText(`${pathName}: is a directory
354
+ `);
355
+ return { ok: false, exitCode: 126 };
356
+ }
357
+ if (!stat.isFile()) {
358
+ await stderr.writeText(`${pathName}: not a file
359
+ `);
360
+ return { ok: false, exitCode: 126 };
206
361
  }
207
362
  try {
208
- await Promise.all(fileWritePromises);
363
+ return { ok: true, path, source: await this.fs.readFile(path, "utf-8") };
209
364
  } catch (err) {
210
365
  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}
366
+ await stderr.writeText(`${pathName}: ${message}
214
367
  `);
215
- exitCode = 1;
368
+ return { ok: false, exitCode: 126 };
216
369
  }
217
- return exitCode;
370
+ }
371
+ parseShebang(source) {
372
+ if (!source.startsWith("#!")) {
373
+ return null;
374
+ }
375
+ const lineEnd = source.indexOf(`
376
+ `);
377
+ const line = source.slice(2, lineEnd === -1 ? undefined : lineEnd).trim();
378
+ if (line === "") {
379
+ return { command: "sh", args: [], display: "sh" };
380
+ }
381
+ const parts = line.split(/\s+/);
382
+ const executable = parts[0];
383
+ const executableName = this.fs.basename(executable);
384
+ if (executableName === "env") {
385
+ const envCommand = parts[1];
386
+ if (!envCommand) {
387
+ return { command: "env", args: [], display: line };
388
+ }
389
+ return {
390
+ command: this.fs.basename(envCommand),
391
+ args: parts.slice(2),
392
+ display: line
393
+ };
394
+ }
395
+ return {
396
+ command: executableName,
397
+ args: parts.slice(1),
398
+ display: line
399
+ };
218
400
  }
219
401
  async handleRedirect(redirect, stdin, stdout, stderr, env) {
220
402
  const target = await this.expandWordScalar(redirect.target, env);
@@ -627,6 +809,10 @@ class Interpreter {
627
809
  this.appendSegment(fields[fields.length - 1], part.value, part.quoted);
628
810
  continue;
629
811
  }
812
+ if (part.type === "variable" && part.name === "@" && part.quoted) {
813
+ this.appendQuotedPositionalParameters(fields);
814
+ continue;
815
+ }
630
816
  const value = await this.expandWordPart(part, env);
631
817
  if (part.quoted) {
632
818
  this.appendSegment(fields[fields.length - 1], value, true);
@@ -650,7 +836,7 @@ class Interpreter {
650
836
  case "text":
651
837
  return part.value;
652
838
  case "variable":
653
- return env[part.name] ?? "";
839
+ return this.getVariableValue(part.name, env);
654
840
  case "substitution":
655
841
  return this.executeSubstitution(part.command, env);
656
842
  case "arithmetic":
@@ -659,6 +845,35 @@ class Interpreter {
659
845
  throw new Error("Cannot expand unknown word part");
660
846
  }
661
847
  }
848
+ getVariableValue(name, env) {
849
+ if (name === "0") {
850
+ return this.argv0;
851
+ }
852
+ if (name === "#") {
853
+ return String(this.positionalParameters.length);
854
+ }
855
+ if (name === "?") {
856
+ return String(this.lastExitCode);
857
+ }
858
+ if (name === "*" || name === "@") {
859
+ return this.positionalParameters.join(" ");
860
+ }
861
+ if (/^[1-9]$/.test(name)) {
862
+ return this.positionalParameters[Number(name) - 1] ?? "";
863
+ }
864
+ return env[name] ?? "";
865
+ }
866
+ appendQuotedPositionalParameters(fields) {
867
+ if (this.positionalParameters.length === 0) {
868
+ return;
869
+ }
870
+ this.appendSegment(fields[fields.length - 1], this.positionalParameters[0], true);
871
+ for (let i = 1;i < this.positionalParameters.length; i++) {
872
+ const field = this.createExpandedField();
873
+ this.appendSegment(field, this.positionalParameters[i], true);
874
+ fields.push(field);
875
+ }
876
+ }
662
877
  async executeSubstitution(command, env) {
663
878
  const interpreter = new Interpreter({
664
879
  fs: this.fs,
@@ -666,7 +881,10 @@ class Interpreter {
666
881
  env,
667
882
  commands: this.commands,
668
883
  redirectObjects: this.redirectObjects,
669
- isTTY: false
884
+ isTTY: false,
885
+ argv0: this.argv0,
886
+ positionalParameters: this.positionalParameters,
887
+ lastExitCode: this.lastExitCode
670
888
  });
671
889
  const result = await interpreter.execute(command);
672
890
  return result.stdout.toString("utf-8").replace(/\n+$/, "");
@@ -767,8 +985,8 @@ class Interpreter {
767
985
  expandedExpr = expandedExpr.replace(/\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g, (_, name) => {
768
986
  return env[name] ?? "0";
769
987
  });
770
- expandedExpr = expandedExpr.replace(/\$([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, name) => {
771
- return env[name] ?? "0";
988
+ expandedExpr = expandedExpr.replace(/\$([a-zA-Z_][a-zA-Z0-9_]*|[0-9#*@?])/g, (_, name) => {
989
+ return this.getVariableValue(name, env) || "0";
772
990
  });
773
991
  expandedExpr = expandedExpr.replace(/\b([a-zA-Z_][a-zA-Z0-9_]*)\b/g, (match) => {
774
992
  if (/^\d+$/.test(match))
@@ -925,6 +1143,12 @@ class Interpreter {
925
1143
  };
926
1144
  return parseOr();
927
1145
  }
1146
+ normalizeExitCode(exitCode) {
1147
+ if (!Number.isFinite(exitCode)) {
1148
+ return 2;
1149
+ }
1150
+ return (Math.trunc(exitCode) % 256 + 256) % 256;
1151
+ }
928
1152
  setCwd(cwd) {
929
1153
  this.env.OLDPWD = this.cwd;
930
1154
  this.cwd = cwd;
@@ -941,8 +1165,9 @@ class Interpreter {
941
1165
  }
942
1166
  export {
943
1167
  Interpreter,
1168
+ ExitException,
944
1169
  ContinueException,
945
1170
  BreakException
946
1171
  };
947
1172
 
948
- //# debugId=96916342C8FA80E364756E2164756E21
1173
+ //# debugId=37348867B8905B5F64756E2164756E21