shell-dsl 0.0.40 → 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 +66 -0
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/src/index.cjs +8 -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 +3 -1
- package/dist/cjs/src/interpreter/context.cjs.map +3 -3
- package/dist/cjs/src/interpreter/interpreter.cjs +146 -19
- 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/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/index.mjs +17 -2
- 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 +3 -1
- package/dist/mjs/src/interpreter/context.mjs.map +3 -3
- package/dist/mjs/src/interpreter/interpreter.mjs +146 -19
- 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/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/index.d.ts +5 -2
- package/dist/types/src/input-analysis.d.ts +14 -0
- package/dist/types/src/interpreter/context.d.ts +3 -1
- package/dist/types/src/interpreter/interpreter.d.ts +12 -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 +39 -0
- package/package.json +1 -1
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// src/input-analysis.ts
|
|
2
|
+
import { Lexer } from "./lexer/lexer.mjs";
|
|
3
|
+
import { Parser } from "./parser/parser.mjs";
|
|
4
|
+
import { LexError, ParseError } from "./errors.mjs";
|
|
5
|
+
function analyzeInput(source) {
|
|
6
|
+
const heredoc = findIncompleteHeredoc(source);
|
|
7
|
+
if (heredoc) {
|
|
8
|
+
return { kind: "incomplete", reason: "heredoc" };
|
|
9
|
+
}
|
|
10
|
+
let tokens;
|
|
11
|
+
try {
|
|
12
|
+
tokens = new Lexer(source, { preserveNewlines: true }).tokenize();
|
|
13
|
+
} catch (err) {
|
|
14
|
+
if (err instanceof LexError && err.message.toLowerCase().includes("unterminated")) {
|
|
15
|
+
return { kind: "incomplete", reason: "quote" };
|
|
16
|
+
}
|
|
17
|
+
return { kind: "invalid", error: err instanceof Error ? err : new Error(String(err)) };
|
|
18
|
+
}
|
|
19
|
+
if (tokens.every((token) => token.type === "newline" || token.type === "eof")) {
|
|
20
|
+
return { kind: "complete", ast: emptyCommandAst() };
|
|
21
|
+
}
|
|
22
|
+
if (hasTrailingPipeline(tokens)) {
|
|
23
|
+
return { kind: "incomplete", reason: "pipeline" };
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const ast = new Parser(tokens).parse();
|
|
27
|
+
return { kind: "complete", ast };
|
|
28
|
+
} catch (err) {
|
|
29
|
+
if (hasOpenCompound(tokens)) {
|
|
30
|
+
return { kind: "incomplete", reason: "compound" };
|
|
31
|
+
}
|
|
32
|
+
if (err instanceof ParseError && isIncompleteParseMessage(err.message)) {
|
|
33
|
+
return { kind: "incomplete", reason: "compound" };
|
|
34
|
+
}
|
|
35
|
+
return { kind: "invalid", error: err instanceof Error ? err : new Error(String(err)) };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function emptyCommandAst() {
|
|
39
|
+
return {
|
|
40
|
+
type: "command",
|
|
41
|
+
name: { type: "word", parts: [] },
|
|
42
|
+
args: [],
|
|
43
|
+
redirects: [],
|
|
44
|
+
assignments: []
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function hasTrailingPipeline(tokens) {
|
|
48
|
+
let i = tokens.length - 1;
|
|
49
|
+
while (i >= 0 && (tokens[i].type === "eof" || tokens[i].type === "newline")) {
|
|
50
|
+
i--;
|
|
51
|
+
}
|
|
52
|
+
return i >= 0 && tokens[i].type === "pipe";
|
|
53
|
+
}
|
|
54
|
+
function hasOpenCompound(tokens) {
|
|
55
|
+
const stack = [];
|
|
56
|
+
for (const token of tokens) {
|
|
57
|
+
if (token.type !== "keyword") {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
switch (token.value) {
|
|
61
|
+
case "if":
|
|
62
|
+
stack.push("fi");
|
|
63
|
+
break;
|
|
64
|
+
case "for":
|
|
65
|
+
case "while":
|
|
66
|
+
case "until":
|
|
67
|
+
stack.push("done");
|
|
68
|
+
break;
|
|
69
|
+
case "case":
|
|
70
|
+
stack.push("esac");
|
|
71
|
+
break;
|
|
72
|
+
case "fi":
|
|
73
|
+
case "done":
|
|
74
|
+
case "esac":
|
|
75
|
+
if (stack[stack.length - 1] === token.value) {
|
|
76
|
+
stack.pop();
|
|
77
|
+
}
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return stack.length > 0;
|
|
82
|
+
}
|
|
83
|
+
function isIncompleteParseMessage(message) {
|
|
84
|
+
return message.includes("Expected 'fi'") || message.includes("Expected 'done'") || message.includes("Expected 'esac'") || message.includes("Expected 'then'") || message.includes("Expected 'do'");
|
|
85
|
+
}
|
|
86
|
+
function findIncompleteHeredoc(source) {
|
|
87
|
+
const lines = source.split(/\n/);
|
|
88
|
+
for (let i = 0;i < lines.length; i++) {
|
|
89
|
+
const match = lines[i].match(/<<-?\s*(?:"([^"]+)"|'([^']+)'|([A-Za-z_][A-Za-z0-9_]*|\S+))/);
|
|
90
|
+
if (!match) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const delimiter = match[1] ?? match[2] ?? match[3];
|
|
94
|
+
if (!delimiter) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
let found = false;
|
|
98
|
+
for (let j = i + 1;j < lines.length; j++) {
|
|
99
|
+
if (lines[j] === delimiter || lines[j].replace(/^\t+/, "") === delimiter) {
|
|
100
|
+
found = true;
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (!found) {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
export {
|
|
111
|
+
analyzeInput
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
//# debugId=840AA9CEDC5E108D64756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/input-analysis.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import type { ASTNode } from \"./parser/ast.mjs\";\nimport type { Token, KeywordValue } from \"./lexer/tokens.mjs\";\nimport { Lexer } from \"./lexer/lexer.mjs\";\nimport { Parser } from \"./parser/parser.mjs\";\nimport { LexError, ParseError } from \"./errors.mjs\";\n\nexport type InputIncompleteReason = \"quote\" | \"heredoc\" | \"compound\" | \"pipeline\";\n\nexport type InputAnalysis =\n | { kind: \"complete\"; ast: ASTNode }\n | { kind: \"incomplete\"; reason: InputIncompleteReason }\n | { kind: \"invalid\"; error: LexError | ParseError | Error };\n\nexport function analyzeInput(source: string): InputAnalysis {\n const heredoc = findIncompleteHeredoc(source);\n if (heredoc) {\n return { kind: \"incomplete\", reason: \"heredoc\" };\n }\n\n let tokens: Token[];\n try {\n tokens = new Lexer(source, { preserveNewlines: true }).tokenize();\n } catch (err) {\n if (err instanceof LexError && err.message.toLowerCase().includes(\"unterminated\")) {\n return { kind: \"incomplete\", reason: \"quote\" };\n }\n return { kind: \"invalid\", error: err instanceof Error ? err : new Error(String(err)) };\n }\n\n if (tokens.every((token) => token.type === \"newline\" || token.type === \"eof\")) {\n return { kind: \"complete\", ast: emptyCommandAst() };\n }\n\n if (hasTrailingPipeline(tokens)) {\n return { kind: \"incomplete\", reason: \"pipeline\" };\n }\n\n try {\n const ast = new Parser(tokens).parse();\n return { kind: \"complete\", ast };\n } catch (err) {\n if (hasOpenCompound(tokens)) {\n return { kind: \"incomplete\", reason: \"compound\" };\n }\n if (err instanceof ParseError && isIncompleteParseMessage(err.message)) {\n return { kind: \"incomplete\", reason: \"compound\" };\n }\n return { kind: \"invalid\", error: err instanceof Error ? err : new Error(String(err)) };\n }\n}\n\nfunction emptyCommandAst(): ASTNode {\n return {\n type: \"command\",\n name: { type: \"word\", parts: [] },\n args: [],\n redirects: [],\n assignments: [],\n };\n}\n\nfunction hasTrailingPipeline(tokens: Token[]): boolean {\n let i = tokens.length - 1;\n while (i >= 0 && (tokens[i]!.type === \"eof\" || tokens[i]!.type === \"newline\")) {\n i--;\n }\n return i >= 0 && tokens[i]!.type === \"pipe\";\n}\n\nfunction hasOpenCompound(tokens: Token[]): boolean {\n const stack: KeywordValue[] = [];\n\n for (const token of tokens) {\n if (token.type !== \"keyword\") {\n continue;\n }\n\n switch (token.value) {\n case \"if\":\n stack.push(\"fi\");\n break;\n case \"for\":\n case \"while\":\n case \"until\":\n stack.push(\"done\");\n break;\n case \"case\":\n stack.push(\"esac\");\n break;\n case \"fi\":\n case \"done\":\n case \"esac\":\n if (stack[stack.length - 1] === token.value) {\n stack.pop();\n }\n break;\n }\n }\n\n return stack.length > 0;\n}\n\nfunction isIncompleteParseMessage(message: string): boolean {\n return (\n message.includes(\"Expected 'fi'\") ||\n message.includes(\"Expected 'done'\") ||\n message.includes(\"Expected 'esac'\") ||\n message.includes(\"Expected 'then'\") ||\n message.includes(\"Expected 'do'\")\n );\n}\n\nfunction findIncompleteHeredoc(source: string): boolean {\n const lines = source.split(/\\n/);\n\n for (let i = 0; i < lines.length; i++) {\n const match = lines[i]!.match(/<<-?\\s*(?:\"([^\"]+)\"|'([^']+)'|([A-Za-z_][A-Za-z0-9_]*|\\S+))/);\n if (!match) {\n continue;\n }\n\n const delimiter = match[1] ?? match[2] ?? match[3];\n if (!delimiter) {\n continue;\n }\n\n let found = false;\n for (let j = i + 1; j < lines.length; j++) {\n if (lines[j] === delimiter || lines[j]!.replace(/^\\t+/, \"\") === delimiter) {\n found = true;\n break;\n }\n }\n if (!found) {\n return true;\n }\n }\n\n return false;\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "840AA9CEDC5E108D64756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -8,6 +8,8 @@ function createCommandContext(options) {
|
|
|
8
8
|
fs: options.fs,
|
|
9
9
|
cwd: options.cwd,
|
|
10
10
|
env: options.env,
|
|
11
|
+
terminal: options.terminal,
|
|
12
|
+
signal: options.signal,
|
|
11
13
|
setCwd: options.setCwd
|
|
12
14
|
};
|
|
13
15
|
if (options.exec) {
|
|
@@ -22,4 +24,4 @@ export {
|
|
|
22
24
|
createCommandContext
|
|
23
25
|
};
|
|
24
26
|
|
|
25
|
-
//# debugId=
|
|
27
|
+
//# debugId=648BE3914FE77F1D64756E2164756E21
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/interpreter/context.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"import type {
|
|
5
|
+
"import type {\n CommandContext,\n VirtualFS,\n Stdin,\n Stdout,\n Stderr,\n ExecResult,\n ShellCommandApi,\n TerminalInfo,\n} from \"../types.mjs\";\n\nexport interface ContextOptions {\n args: string[];\n stdin: Stdin;\n stdout: Stdout;\n stderr: Stderr;\n fs: VirtualFS;\n cwd: string;\n env: Record<string, string>;\n terminal: TerminalInfo;\n signal: AbortSignal;\n setCwd: (path: string) => void;\n exec?: (name: string, args: string[]) => Promise<ExecResult>;\n shell?: ShellCommandApi;\n}\n\nexport function createCommandContext(options: ContextOptions): CommandContext {\n const ctx: CommandContext = {\n args: options.args,\n stdin: options.stdin,\n stdout: options.stdout,\n stderr: options.stderr,\n fs: options.fs,\n cwd: options.cwd,\n env: options.env,\n terminal: options.terminal,\n signal: options.signal,\n setCwd: options.setCwd,\n };\n if (options.exec) {\n ctx.exec = options.exec;\n }\n if (options.shell) {\n ctx.shell = options.shell;\n }\n return ctx;\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": ";
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": ";AA0BO,SAAS,oBAAoB,CAAC,SAAyC;AAAA,EAC5E,MAAM,MAAsB;AAAA,IAC1B,MAAM,QAAQ;AAAA,IACd,OAAO,QAAQ;AAAA,IACf,QAAQ,QAAQ;AAAA,IAChB,QAAQ,QAAQ;AAAA,IAChB,IAAI,QAAQ;AAAA,IACZ,KAAK,QAAQ;AAAA,IACb,KAAK,QAAQ;AAAA,IACb,UAAU,QAAQ;AAAA,IAClB,QAAQ,QAAQ;AAAA,IAChB,QAAQ,QAAQ;AAAA,EAClB;AAAA,EACA,IAAI,QAAQ,MAAM;AAAA,IAChB,IAAI,OAAO,QAAQ;AAAA,EACrB;AAAA,EACA,IAAI,QAAQ,OAAO;AAAA,IACjB,IAAI,QAAQ,QAAQ;AAAA,EACtB;AAAA,EACA,OAAO;AAAA;",
|
|
8
|
+
"debugId": "648BE3914FE77F1D64756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -4,6 +4,7 @@ import { Lexer } from "../lexer/lexer.mjs";
|
|
|
4
4
|
import { Parser } from "../parser/parser.mjs";
|
|
5
5
|
import { createStdin } from "../io/stdin.mjs";
|
|
6
6
|
import { createStdout, createStderr, createPipe, createBufferTargetCollector } from "../io/stdout.mjs";
|
|
7
|
+
import { AsyncQueue } from "../io/async-queue.mjs";
|
|
7
8
|
import { isDevNullPath } from "../fs/special-files.mjs";
|
|
8
9
|
var DEFAULT_IFS = `
|
|
9
10
|
`;
|
|
@@ -41,6 +42,9 @@ class Interpreter {
|
|
|
41
42
|
redirectObjects;
|
|
42
43
|
loopDepth = 0;
|
|
43
44
|
isTTY;
|
|
45
|
+
terminal;
|
|
46
|
+
activeSignal;
|
|
47
|
+
externalCommand;
|
|
44
48
|
argv0;
|
|
45
49
|
positionalParameters;
|
|
46
50
|
lastExitCode;
|
|
@@ -50,7 +54,10 @@ class Interpreter {
|
|
|
50
54
|
this.env = { ...options.env };
|
|
51
55
|
this.commands = options.commands;
|
|
52
56
|
this.redirectObjects = options.redirectObjects ?? {};
|
|
53
|
-
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;
|
|
54
61
|
this.argv0 = options.argv0 ?? "sh";
|
|
55
62
|
this.positionalParameters = [...options.positionalParameters ?? []];
|
|
56
63
|
this.lastExitCode = options.lastExitCode ?? 0;
|
|
@@ -59,27 +66,88 @@ class Interpreter {
|
|
|
59
66
|
return this.loopDepth;
|
|
60
67
|
}
|
|
61
68
|
async execute(ast) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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 });
|
|
70
80
|
}
|
|
71
|
-
exitCode = err.exitCode;
|
|
72
|
-
this.lastExitCode = exitCode;
|
|
73
81
|
}
|
|
74
|
-
stdout.
|
|
75
|
-
|
|
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
|
+
})();
|
|
76
125
|
return {
|
|
77
|
-
stdout:
|
|
78
|
-
stderr:
|
|
79
|
-
|
|
126
|
+
stdout: stdout.getReadableStream(),
|
|
127
|
+
stderr: stderr.getReadableStream(),
|
|
128
|
+
output: eventQueue,
|
|
129
|
+
exit,
|
|
130
|
+
kill: (reason) => controller.abort(reason ?? new Error("Shell execution killed"))
|
|
80
131
|
};
|
|
81
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
|
+
}
|
|
82
149
|
async executeNode(node, stdinSource, stdout, stderr) {
|
|
150
|
+
this.throwIfAborted();
|
|
83
151
|
let exitCode;
|
|
84
152
|
switch (node.type) {
|
|
85
153
|
case "command":
|
|
@@ -118,6 +186,11 @@ class Interpreter {
|
|
|
118
186
|
this.lastExitCode = exitCode;
|
|
119
187
|
return exitCode;
|
|
120
188
|
}
|
|
189
|
+
throwIfAborted() {
|
|
190
|
+
if (this.activeSignal.aborted) {
|
|
191
|
+
throw this.activeSignal.reason ?? new Error("Shell execution aborted");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
121
194
|
async executeCommand(node, stdinSource, stdout, stderr) {
|
|
122
195
|
const assignmentEnv = { ...this.env };
|
|
123
196
|
for (const assignment of node.assignments) {
|
|
@@ -189,12 +262,55 @@ class Interpreter {
|
|
|
189
262
|
return this.invokeRegisteredCommand(name, command, args, stdinSource, stdout, stderr, env);
|
|
190
263
|
}
|
|
191
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
|
+
}
|
|
192
272
|
return this.executeExecutableFile(name, args, stdinSource, stdout, stderr, env);
|
|
193
273
|
}
|
|
274
|
+
if (this.externalCommand) {
|
|
275
|
+
return this.invokeExternalCommand(name, args, stdinSource, stdout, stderr, env);
|
|
276
|
+
}
|
|
194
277
|
await stderr.writeText(`${name}: command not found
|
|
195
278
|
`);
|
|
196
279
|
return 127;
|
|
197
280
|
}
|
|
281
|
+
async invokeExternalCommand(name, args, stdinSource, stdout, stderr, env) {
|
|
282
|
+
if (!this.externalCommand) {
|
|
283
|
+
return 127;
|
|
284
|
+
}
|
|
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;
|
|
304
|
+
}
|
|
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}
|
|
310
|
+
`);
|
|
311
|
+
return 1;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
198
314
|
async invokeRegisteredCommand(name, command, args, stdinSource, stdout, stderr, env) {
|
|
199
315
|
const exec = this.createExec(env);
|
|
200
316
|
const shell = this.createShellApi(stdinSource, stdout, stderr, env);
|
|
@@ -206,6 +322,8 @@ class Interpreter {
|
|
|
206
322
|
fs: this.fs,
|
|
207
323
|
cwd: this.cwd,
|
|
208
324
|
env,
|
|
325
|
+
terminal: this.terminal,
|
|
326
|
+
signal: this.activeSignal,
|
|
209
327
|
setCwd: (path) => this.setCwd(path),
|
|
210
328
|
exec,
|
|
211
329
|
shell
|
|
@@ -269,7 +387,9 @@ class Interpreter {
|
|
|
269
387
|
env: { ...env },
|
|
270
388
|
commands: this.commands,
|
|
271
389
|
redirectObjects: this.redirectObjects,
|
|
272
|
-
|
|
390
|
+
terminal: this.terminal,
|
|
391
|
+
externalCommand: this.externalCommand,
|
|
392
|
+
signal: this.activeSignal,
|
|
273
393
|
argv0: shebang.command,
|
|
274
394
|
positionalParameters: []
|
|
275
395
|
});
|
|
@@ -289,7 +409,9 @@ class Interpreter {
|
|
|
289
409
|
env: { ...env },
|
|
290
410
|
commands: this.commands,
|
|
291
411
|
redirectObjects: this.redirectObjects,
|
|
292
|
-
|
|
412
|
+
terminal: this.terminal,
|
|
413
|
+
externalCommand: this.externalCommand,
|
|
414
|
+
signal: this.activeSignal,
|
|
293
415
|
argv0,
|
|
294
416
|
positionalParameters: args
|
|
295
417
|
});
|
|
@@ -882,6 +1004,8 @@ class Interpreter {
|
|
|
882
1004
|
commands: this.commands,
|
|
883
1005
|
redirectObjects: this.redirectObjects,
|
|
884
1006
|
isTTY: false,
|
|
1007
|
+
externalCommand: this.externalCommand,
|
|
1008
|
+
signal: this.activeSignal,
|
|
885
1009
|
argv0: this.argv0,
|
|
886
1010
|
positionalParameters: this.positionalParameters,
|
|
887
1011
|
lastExitCode: this.lastExitCode
|
|
@@ -1162,6 +1286,9 @@ class Interpreter {
|
|
|
1162
1286
|
getEnv() {
|
|
1163
1287
|
return { ...this.env };
|
|
1164
1288
|
}
|
|
1289
|
+
getLastExitCode() {
|
|
1290
|
+
return this.lastExitCode;
|
|
1291
|
+
}
|
|
1165
1292
|
}
|
|
1166
1293
|
export {
|
|
1167
1294
|
Interpreter,
|
|
@@ -1170,4 +1297,4 @@ export {
|
|
|
1170
1297
|
BreakException
|
|
1171
1298
|
};
|
|
1172
1299
|
|
|
1173
|
-
//# debugId=
|
|
1300
|
+
//# debugId=1C8DFE25269FD4A764756E2164756E21
|