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