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