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.
- package/README.md +117 -3
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/src/commands/exit/exit.cjs +84 -0
- package/dist/cjs/src/commands/exit/exit.cjs.map +10 -0
- package/dist/cjs/src/commands/index.cjs +18 -2
- package/dist/cjs/src/commands/index.cjs.map +3 -3
- package/dist/cjs/src/commands/sh/sh.cjs +134 -0
- package/dist/cjs/src/commands/sh/sh.cjs.map +10 -0
- package/dist/cjs/src/index.cjs +2 -1
- package/dist/cjs/src/index.cjs.map +3 -3
- package/dist/cjs/src/interpreter/context.cjs +4 -1
- package/dist/cjs/src/interpreter/context.cjs.map +3 -3
- package/dist/cjs/src/interpreter/index.cjs +2 -1
- package/dist/cjs/src/interpreter/index.cjs.map +3 -3
- package/dist/cjs/src/interpreter/interpreter.cjs +301 -76
- package/dist/cjs/src/interpreter/interpreter.cjs.map +3 -3
- package/dist/cjs/src/lexer/lexer.cjs +13 -1
- package/dist/cjs/src/lexer/lexer.cjs.map +3 -3
- package/dist/cjs/src/parser/parser.cjs +11 -1
- package/dist/cjs/src/parser/parser.cjs.map +3 -3
- package/dist/cjs/src/types.cjs.map +2 -2
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/src/commands/exit/exit.mjs +44 -0
- package/dist/mjs/src/commands/exit/exit.mjs.map +10 -0
- package/dist/mjs/src/commands/index.mjs +18 -2
- package/dist/mjs/src/commands/index.mjs.map +3 -3
- package/dist/mjs/src/commands/sh/sh.mjs +94 -0
- package/dist/mjs/src/commands/sh/sh.mjs.map +10 -0
- package/dist/mjs/src/index.mjs +3 -2
- package/dist/mjs/src/index.mjs.map +3 -3
- package/dist/mjs/src/interpreter/context.mjs +4 -1
- package/dist/mjs/src/interpreter/context.mjs.map +3 -3
- package/dist/mjs/src/interpreter/index.mjs +3 -2
- package/dist/mjs/src/interpreter/index.mjs.map +2 -2
- package/dist/mjs/src/interpreter/interpreter.mjs +301 -76
- package/dist/mjs/src/interpreter/interpreter.mjs.map +3 -3
- package/dist/mjs/src/lexer/lexer.mjs +13 -1
- package/dist/mjs/src/lexer/lexer.mjs.map +3 -3
- package/dist/mjs/src/parser/parser.mjs +11 -1
- package/dist/mjs/src/parser/parser.mjs.map +3 -3
- package/dist/mjs/src/types.mjs.map +2 -2
- package/dist/types/src/commands/exit/exit.d.ts +2 -0
- package/dist/types/src/commands/index.d.ts +2 -0
- package/dist/types/src/commands/sh/sh.d.ts +5 -0
- package/dist/types/src/index.d.ts +2 -2
- package/dist/types/src/interpreter/context.d.ts +2 -1
- package/dist/types/src/interpreter/index.d.ts +1 -1
- package/dist/types/src/interpreter/interpreter.d.ts +24 -0
- package/dist/types/src/types.d.ts +13 -0
- 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
|
-
|
|
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
|
-
|
|
86
|
+
exitCode = await this.executeCommand(node, stdinSource, stdout, stderr);
|
|
87
|
+
break;
|
|
61
88
|
case "pipeline":
|
|
62
|
-
|
|
89
|
+
exitCode = await this.executePipeline(node.commands, stdinSource, stdout, stderr);
|
|
90
|
+
break;
|
|
63
91
|
case "sequence":
|
|
64
|
-
|
|
92
|
+
exitCode = await this.executeSequence(node.commands, stdinSource, stdout, stderr);
|
|
93
|
+
break;
|
|
65
94
|
case "and":
|
|
66
|
-
|
|
95
|
+
exitCode = await this.executeAnd(node.left, node.right, stdinSource, stdout, stderr);
|
|
96
|
+
break;
|
|
67
97
|
case "or":
|
|
68
|
-
|
|
98
|
+
exitCode = await this.executeOr(node.left, node.right, stdinSource, stdout, stderr);
|
|
99
|
+
break;
|
|
69
100
|
case "if":
|
|
70
|
-
|
|
101
|
+
exitCode = await this.executeIf(node, stdinSource, stdout, stderr);
|
|
102
|
+
break;
|
|
71
103
|
case "for":
|
|
72
|
-
|
|
104
|
+
exitCode = await this.executeFor(node, stdinSource, stdout, stderr);
|
|
105
|
+
break;
|
|
73
106
|
case "while":
|
|
74
|
-
|
|
107
|
+
exitCode = await this.executeWhile(node, stdinSource, stdout, stderr);
|
|
108
|
+
break;
|
|
75
109
|
case "until":
|
|
76
|
-
|
|
110
|
+
exitCode = await this.executeUntil(node, stdinSource, stdout, stderr);
|
|
111
|
+
break;
|
|
77
112
|
case "case":
|
|
78
|
-
|
|
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
|
-
|
|
130
|
-
if (
|
|
131
|
-
|
|
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
|
-
|
|
182
|
+
exitCode = 1;
|
|
134
183
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
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
|
|
235
|
+
exitCode
|
|
176
236
|
};
|
|
177
237
|
};
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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:
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
297
|
+
return await interpreter.executeSourceInCurrentFrame(source, stdinSource, stdout, stderr, argv0);
|
|
192
298
|
} catch (err) {
|
|
193
|
-
if (err instanceof
|
|
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(`${
|
|
325
|
+
await stderr.writeText(`${errorName}: ${message}
|
|
198
326
|
`);
|
|
199
|
-
|
|
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
|
-
|
|
202
|
-
|
|
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
|
-
|
|
205
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
368
|
+
return { ok: false, exitCode: 126 };
|
|
216
369
|
}
|
|
217
|
-
|
|
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
|
|
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_]
|
|
771
|
-
return
|
|
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=
|
|
1173
|
+
//# debugId=37348867B8905B5F64756E2164756E21
|