@warlock.js/ai-workspace 5.30.0 → 5.31.0
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/CHANGELOG.md +46 -2
- package/README.md +34 -21
- package/cjs/index.cjs +3116 -309
- package/cjs/index.cjs.map +1 -1
- package/esm/backends/local.d.mts.map +1 -1
- package/esm/backends/local.mjs +165 -27
- package/esm/backends/local.mjs.map +1 -1
- package/esm/backends/mock.d.mts +11 -5
- package/esm/backends/mock.d.mts.map +1 -1
- package/esm/backends/mock.mjs +34 -0
- package/esm/backends/mock.mjs.map +1 -1
- package/esm/contracts/index.d.mts +3 -3
- package/esm/contracts/tool-io.type.d.mts +202 -4
- package/esm/contracts/tool-io.type.d.mts.map +1 -1
- package/esm/contracts/workspace-backend.contract.d.mts +61 -1
- package/esm/contracts/workspace-backend.contract.d.mts.map +1 -1
- package/esm/contracts/workspace-ops.contract.d.mts +32 -3
- package/esm/contracts/workspace-ops.contract.d.mts.map +1 -1
- package/esm/contracts/workspace-policy.type.d.mts +133 -12
- package/esm/contracts/workspace-policy.type.d.mts.map +1 -1
- package/esm/contracts/workspace.contract.d.mts +63 -9
- package/esm/contracts/workspace.contract.d.mts.map +1 -1
- package/esm/errors.d.mts +79 -5
- package/esm/errors.d.mts.map +1 -1
- package/esm/errors.mjs +22 -1
- package/esm/errors.mjs.map +1 -1
- package/esm/index.d.mts +13 -5
- package/esm/index.mjs +10 -2
- package/esm/ops.d.mts +21 -2
- package/esm/ops.d.mts.map +1 -1
- package/esm/ops.mjs +230 -64
- package/esm/ops.mjs.map +1 -1
- package/esm/policy/jail.mjs +73 -0
- package/esm/policy/jail.mjs.map +1 -0
- package/esm/policy/policy.d.mts +25 -15
- package/esm/policy/policy.d.mts.map +1 -1
- package/esm/policy/policy.mjs +61 -85
- package/esm/policy/policy.mjs.map +1 -1
- package/esm/policy/shell-check.d.mts +64 -0
- package/esm/policy/shell-check.d.mts.map +1 -0
- package/esm/policy/shell-check.mjs +627 -0
- package/esm/policy/shell-check.mjs.map +1 -0
- package/esm/policy/shell-parser.mjs +802 -0
- package/esm/policy/shell-parser.mjs.map +1 -0
- package/esm/policy/tokenize-command.d.mts +63 -0
- package/esm/policy/tokenize-command.d.mts.map +1 -0
- package/esm/policy/tokenize-command.mjs +75 -23
- package/esm/policy/tokenize-command.mjs.map +1 -1
- package/esm/shell-jobs.d.mts +45 -0
- package/esm/shell-jobs.d.mts.map +1 -0
- package/esm/shell-jobs.mjs +247 -0
- package/esm/shell-jobs.mjs.map +1 -0
- package/esm/todos.d.mts +22 -0
- package/esm/todos.d.mts.map +1 -0
- package/esm/todos.mjs +73 -0
- package/esm/todos.mjs.map +1 -0
- package/esm/tools/edit-file.d.mts.map +1 -1
- package/esm/tools/edit-file.mjs +2 -1
- package/esm/tools/edit-file.mjs.map +1 -1
- package/esm/tools/glob.d.mts.map +1 -1
- package/esm/tools/glob.mjs +2 -1
- package/esm/tools/glob.mjs.map +1 -1
- package/esm/tools/grep.d.mts.map +1 -1
- package/esm/tools/grep.mjs +2 -1
- package/esm/tools/grep.mjs.map +1 -1
- package/esm/tools/json-schemas.mjs +175 -0
- package/esm/tools/json-schemas.mjs.map +1 -0
- package/esm/tools/list-dir.d.mts +32 -0
- package/esm/tools/list-dir.d.mts.map +1 -0
- package/esm/tools/list-dir.mjs +50 -0
- package/esm/tools/list-dir.mjs.map +1 -0
- package/esm/tools/multi-edit.d.mts +38 -0
- package/esm/tools/multi-edit.d.mts.map +1 -0
- package/esm/tools/multi-edit.mjs +121 -0
- package/esm/tools/multi-edit.mjs.map +1 -0
- package/esm/tools/read-file.d.mts.map +1 -1
- package/esm/tools/read-file.mjs +2 -1
- package/esm/tools/read-file.mjs.map +1 -1
- package/esm/tools/run-shell.d.mts +34 -12
- package/esm/tools/run-shell.d.mts.map +1 -1
- package/esm/tools/run-shell.mjs +59 -53
- package/esm/tools/run-shell.mjs.map +1 -1
- package/esm/tools/run-tests.d.mts +8 -0
- package/esm/tools/run-tests.d.mts.map +1 -1
- package/esm/tools/run-tests.mjs +10 -3
- package/esm/tools/run-tests.mjs.map +1 -1
- package/esm/tools/schema.mjs +23 -18
- package/esm/tools/schema.mjs.map +1 -1
- package/esm/tools/shell-kill.d.mts +25 -0
- package/esm/tools/shell-kill.d.mts.map +1 -0
- package/esm/tools/shell-kill.mjs +36 -0
- package/esm/tools/shell-kill.mjs.map +1 -0
- package/esm/tools/shell-output.d.mts +30 -0
- package/esm/tools/shell-output.d.mts.map +1 -0
- package/esm/tools/shell-output.mjs +49 -0
- package/esm/tools/shell-output.mjs.map +1 -0
- package/esm/tools/todo.d.mts +43 -0
- package/esm/tools/todo.d.mts.map +1 -0
- package/esm/tools/todo.mjs +126 -0
- package/esm/tools/todo.mjs.map +1 -0
- package/esm/tools/write-file.d.mts.map +1 -1
- package/esm/tools/write-file.mjs +2 -1
- package/esm/tools/write-file.mjs.map +1 -1
- package/esm/workspace.d.mts +11 -8
- package/esm/workspace.d.mts.map +1 -1
- package/esm/workspace.mjs +108 -27
- package/esm/workspace.mjs.map +1 -1
- package/llms-full.txt +155 -34
- package/llms.txt +1 -1
- package/package.json +3 -3
- package/skills/README.md +2 -2
- package/skills/build-loop-agent/SKILL.md +7 -7
- package/skills/index.md +6 -1
- package/skills/use-a-workspace/SKILL.md +148 -27
|
@@ -0,0 +1,802 @@
|
|
|
1
|
+
//#region ../../packages/ai-workspace/src/policy/shell-parser.ts
|
|
2
|
+
/** Deepest nesting of subshells and substitutions the parser accepts. */
|
|
3
|
+
const MAX_NESTING = 16;
|
|
4
|
+
/**
|
|
5
|
+
* Words that start a compound command (or negate a pipeline) when they
|
|
6
|
+
* appear unquoted in command position. The parser does not read compound
|
|
7
|
+
* commands, so a line using one is refused.
|
|
8
|
+
*/
|
|
9
|
+
const RESERVED_WORDS = new Set([
|
|
10
|
+
"!",
|
|
11
|
+
"{",
|
|
12
|
+
"}",
|
|
13
|
+
"[[",
|
|
14
|
+
"]]",
|
|
15
|
+
"case",
|
|
16
|
+
"coproc",
|
|
17
|
+
"do",
|
|
18
|
+
"done",
|
|
19
|
+
"elif",
|
|
20
|
+
"else",
|
|
21
|
+
"esac",
|
|
22
|
+
"fi",
|
|
23
|
+
"for",
|
|
24
|
+
"function",
|
|
25
|
+
"if",
|
|
26
|
+
"in",
|
|
27
|
+
"select",
|
|
28
|
+
"then",
|
|
29
|
+
"until",
|
|
30
|
+
"while"
|
|
31
|
+
]);
|
|
32
|
+
/** cmd.exe keywords the `"cmd"` dialect refuses in command position. */
|
|
33
|
+
const CMD_RESERVED_WORDS = new Set([
|
|
34
|
+
"if",
|
|
35
|
+
"for",
|
|
36
|
+
"goto",
|
|
37
|
+
"setlocal",
|
|
38
|
+
"endlocal"
|
|
39
|
+
]);
|
|
40
|
+
/** A parameter name inside `${…}`: a variable, a positional or a special parameter. */
|
|
41
|
+
const PARAMETER_NAME = String.raw`(?:[A-Za-z_][A-Za-z0-9_]*|[0-9]+|[@*#?$!-])`;
|
|
42
|
+
/**
|
|
43
|
+
* Allowed `${…}` forms: `${name}`, `${#name}`, `${name-word}`,
|
|
44
|
+
* `${name:-word}` (also `=`, `+`, `?`), `${name%word}`, `${name%%word}`,
|
|
45
|
+
* `${name#word}`, `${name##word}`. Everything else — substrings and
|
|
46
|
+
* subscripts (arithmetic in bash), `${!name}`, `${name@op}`, pattern
|
|
47
|
+
* substitution — is refused.
|
|
48
|
+
*/
|
|
49
|
+
const PARAMETER_EXPANSION = new RegExp(`^(?:#${PARAMETER_NAME}|${PARAMETER_NAME}(?:(?::?[-=+?]|%%?|##?)[^\`\$'"\\\\{}()@\\n]*)?)$`);
|
|
50
|
+
/** A refusal raised while parsing; caught by {@link parseShellLine}. */
|
|
51
|
+
var ShellSyntaxRefusal = class extends Error {
|
|
52
|
+
constructor(reason, message) {
|
|
53
|
+
super(message);
|
|
54
|
+
this.reason = reason;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
/** Refuse a construct the parser does not read with confidence. */
|
|
58
|
+
function unsupported(message) {
|
|
59
|
+
return new ShellSyntaxRefusal("unsupported-syntax", message);
|
|
60
|
+
}
|
|
61
|
+
/** Refuse an unterminated quote / substitution. */
|
|
62
|
+
function unbalanced(message) {
|
|
63
|
+
return new ShellSyntaxRefusal("unbalanced-quotes", message);
|
|
64
|
+
}
|
|
65
|
+
/** Parser for the POSIX `sh` dialect. */
|
|
66
|
+
var PosixParser = class PosixParser {
|
|
67
|
+
constructor(src, state) {
|
|
68
|
+
this.src = src;
|
|
69
|
+
this.state = state;
|
|
70
|
+
this.pos = 0;
|
|
71
|
+
this.pendingHeredocs = [];
|
|
72
|
+
}
|
|
73
|
+
/** Parse the whole line as a command list. */
|
|
74
|
+
parseProgram(segments) {
|
|
75
|
+
this.parseList(null, segments);
|
|
76
|
+
this.assertNoPendingHeredocs();
|
|
77
|
+
}
|
|
78
|
+
/** Parse the contents of a backtick substitution (a full list until EOF). */
|
|
79
|
+
parseNestedProgram() {
|
|
80
|
+
this.parseList(null, void 0);
|
|
81
|
+
this.assertNoPendingHeredocs();
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Scan an unquoted-delimiter here-document body: `$…`, `${…}`, `$(…)` and
|
|
85
|
+
* backticks expand there exactly as inside double quotes (but `"` is an
|
|
86
|
+
* ordinary character), so any command substitution in it runs.
|
|
87
|
+
*/
|
|
88
|
+
parseExpandingText() {
|
|
89
|
+
while (!this.atEnd()) {
|
|
90
|
+
const char = this.peek();
|
|
91
|
+
if (char === "\\") {
|
|
92
|
+
this.pos += 2;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (char === "$") {
|
|
96
|
+
this.parseDollar("heredoc");
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (char === "`") {
|
|
100
|
+
this.parseBackticks(false);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
this.pos++;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
atEnd() {
|
|
107
|
+
return this.pos >= this.src.length;
|
|
108
|
+
}
|
|
109
|
+
peek(offset = 0) {
|
|
110
|
+
return this.src.charAt(this.pos + offset);
|
|
111
|
+
}
|
|
112
|
+
assertNoPendingHeredocs() {
|
|
113
|
+
if (this.pendingHeredocs.length > 0) throw unsupported(`The here-document delimited by \`${this.pendingHeredocs[0]?.delimiter ?? ""}\` is never terminated.`);
|
|
114
|
+
}
|
|
115
|
+
enterNesting() {
|
|
116
|
+
this.state.nesting++;
|
|
117
|
+
if (this.state.nesting > MAX_NESTING) throw unsupported("The line nests subshells or substitutions too deeply.");
|
|
118
|
+
}
|
|
119
|
+
exitNesting() {
|
|
120
|
+
this.state.nesting--;
|
|
121
|
+
}
|
|
122
|
+
/** Skip spaces, tabs and backslash-newline continuations. */
|
|
123
|
+
skipBlanks() {
|
|
124
|
+
while (!this.atEnd()) {
|
|
125
|
+
const char = this.peek();
|
|
126
|
+
if (char === " " || char === " ") {
|
|
127
|
+
this.pos++;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (char === "\\" && this.peek(1) === "\n") {
|
|
131
|
+
this.pos += 2;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** Skip a `#` comment up to (not including) the newline. */
|
|
138
|
+
skipComment() {
|
|
139
|
+
const newline = this.src.indexOf("\n", this.pos);
|
|
140
|
+
this.pos = newline === -1 ? this.src.length : newline;
|
|
141
|
+
}
|
|
142
|
+
/** Consume one newline and read any here-document bodies queued before it. */
|
|
143
|
+
consumeNewline() {
|
|
144
|
+
this.pos++;
|
|
145
|
+
if (this.pendingHeredocs.length === 0) return;
|
|
146
|
+
if (this.state.nesting > 0) throw unsupported("Here-documents inside subshells or substitutions are not supported.");
|
|
147
|
+
const pending = this.pendingHeredocs;
|
|
148
|
+
this.pendingHeredocs = [];
|
|
149
|
+
for (const heredoc of pending) this.readHeredocBody(heredoc);
|
|
150
|
+
}
|
|
151
|
+
/** Skip blanks, comments and newlines (separators between commands). */
|
|
152
|
+
skipBlanksAndNewlines() {
|
|
153
|
+
while (!this.atEnd()) {
|
|
154
|
+
this.skipBlanks();
|
|
155
|
+
if (this.peek() === "#") {
|
|
156
|
+
this.skipComment();
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (this.peek() === "\n") {
|
|
160
|
+
this.consumeNewline();
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
readHeredocBody(heredoc) {
|
|
167
|
+
const bodyLines = [];
|
|
168
|
+
while (true) {
|
|
169
|
+
if (this.atEnd()) throw unsupported(`The here-document delimited by \`${heredoc.delimiter}\` is never terminated.`);
|
|
170
|
+
const lineEnd = this.src.indexOf("\n", this.pos);
|
|
171
|
+
const line = lineEnd === -1 ? this.src.slice(this.pos) : this.src.slice(this.pos, lineEnd);
|
|
172
|
+
const compared = heredoc.stripTabs ? line.replace(/^\t+/, "") : line;
|
|
173
|
+
this.pos = lineEnd === -1 ? this.src.length : lineEnd + 1;
|
|
174
|
+
if (compared === heredoc.delimiter) break;
|
|
175
|
+
bodyLines.push(compared);
|
|
176
|
+
}
|
|
177
|
+
if (heredoc.expand) new PosixParser(bodyLines.join("\n"), this.state).parseExpandingText();
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Parse a command list until `closing` (`")"`) or the end of the input.
|
|
181
|
+
* `segments`, when given, collects the top-level pipelines.
|
|
182
|
+
*/
|
|
183
|
+
parseList(closing, segments) {
|
|
184
|
+
let sawCommand = false;
|
|
185
|
+
while (true) {
|
|
186
|
+
this.skipBlanksAndNewlines();
|
|
187
|
+
if (this.atEnd()) {
|
|
188
|
+
if (closing !== null) throw unbalanced(`A \`(\` or \`$(\` is never closed with \`${closing}\`.`);
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
if (closing !== null && this.peek() === closing) break;
|
|
192
|
+
const start = this.pos;
|
|
193
|
+
this.parsePipeline(closing);
|
|
194
|
+
const end = this.pos;
|
|
195
|
+
sawCommand = true;
|
|
196
|
+
this.skipBlanks();
|
|
197
|
+
if (this.peek() === "#") this.skipComment();
|
|
198
|
+
const operator = this.readListOperator();
|
|
199
|
+
segments?.push({
|
|
200
|
+
source: this.src.slice(start, end).trim(),
|
|
201
|
+
...operator !== null ? { operator } : {}
|
|
202
|
+
});
|
|
203
|
+
if (operator === "&&" || operator === "||") {
|
|
204
|
+
this.skipBlanksAndNewlines();
|
|
205
|
+
if (this.atEnd() || closing !== null && this.peek() === closing) throw unsupported(`The line ends with \`${operator}\` but no command follows it.`);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (operator === "&") this.state.background = true;
|
|
209
|
+
if (operator === null) {
|
|
210
|
+
if (this.atEnd() || closing !== null && this.peek() === closing) break;
|
|
211
|
+
throw unsupported(`Unexpected \`${this.peek()}\`.`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (!sawCommand) throw unsupported(closing === null ? "The command is empty." : "A `( )` or `$( )` is empty.");
|
|
215
|
+
}
|
|
216
|
+
/** Read `&&`, `||`, `;`, `&` or a newline after a pipeline. */
|
|
217
|
+
readListOperator() {
|
|
218
|
+
const char = this.peek();
|
|
219
|
+
const next = this.peek(1);
|
|
220
|
+
if (char === "&") {
|
|
221
|
+
if (next === "&") {
|
|
222
|
+
this.pos += 2;
|
|
223
|
+
return "&&";
|
|
224
|
+
}
|
|
225
|
+
if (next === ">") throw unsupported("`&>` (redirect both streams) is not portable; write `> file 2>&1` instead.");
|
|
226
|
+
this.pos++;
|
|
227
|
+
return "&";
|
|
228
|
+
}
|
|
229
|
+
if (char === "|" && next === "|") {
|
|
230
|
+
this.pos += 2;
|
|
231
|
+
return "||";
|
|
232
|
+
}
|
|
233
|
+
if (char === ";") {
|
|
234
|
+
if (next === ";") throw unsupported("`;;` (a `case` clause) is not supported.");
|
|
235
|
+
this.pos++;
|
|
236
|
+
return ";";
|
|
237
|
+
}
|
|
238
|
+
if (char === "\n") {
|
|
239
|
+
this.consumeNewline();
|
|
240
|
+
return "\n";
|
|
241
|
+
}
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
parsePipeline(closing) {
|
|
245
|
+
this.parseCommand(closing);
|
|
246
|
+
while (true) {
|
|
247
|
+
this.skipBlanks();
|
|
248
|
+
if (this.peek() !== "|" || this.peek(1) === "|") return;
|
|
249
|
+
if (this.peek(1) === "&") throw unsupported("`|&` is not portable; write `2>&1 |` instead.");
|
|
250
|
+
this.pos++;
|
|
251
|
+
this.skipBlanksAndNewlines();
|
|
252
|
+
if (this.atEnd() || closing !== null && this.peek() === closing) throw unsupported("The line ends with `|` but no command follows it.");
|
|
253
|
+
this.parseCommand(closing);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
parseCommand(closing) {
|
|
257
|
+
this.skipBlanks();
|
|
258
|
+
if (this.peek() === "(") {
|
|
259
|
+
if (this.peek(1) === "(") throw unsupported("`((…))` (arithmetic) is not supported.");
|
|
260
|
+
this.parseSubshell();
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (this.peek() === ")") throw unsupported(closing === null ? "Unexpected `)`." : "A `( )` or `$( )` is empty.");
|
|
264
|
+
this.parseSimpleCommand();
|
|
265
|
+
}
|
|
266
|
+
parseSubshell() {
|
|
267
|
+
const start = this.pos;
|
|
268
|
+
this.pos++;
|
|
269
|
+
this.enterNesting();
|
|
270
|
+
this.parseList(")", void 0);
|
|
271
|
+
if (this.peek() !== ")") throw unbalanced("A `(` is never closed with `)`.");
|
|
272
|
+
this.pos++;
|
|
273
|
+
this.exitNesting();
|
|
274
|
+
const redirects = [];
|
|
275
|
+
while (true) {
|
|
276
|
+
this.skipBlanks();
|
|
277
|
+
if (!this.atRedirectStart()) break;
|
|
278
|
+
redirects.push(this.parseRedirect());
|
|
279
|
+
}
|
|
280
|
+
if (redirects.length > 0) this.state.commands.push({
|
|
281
|
+
assignments: [],
|
|
282
|
+
words: [],
|
|
283
|
+
redirects,
|
|
284
|
+
source: this.src.slice(start, this.pos).trim()
|
|
285
|
+
});
|
|
286
|
+
if (!this.atEnd() && !this.atOperatorBoundary()) throw unsupported("Only an operator or a redirection may follow `)`.");
|
|
287
|
+
}
|
|
288
|
+
/** Whether the next character ends a simple command. */
|
|
289
|
+
atOperatorBoundary() {
|
|
290
|
+
const char = this.peek();
|
|
291
|
+
return char === "\n" || char === ";" || char === "&" || char === "|" || char === ")" || char === "#" || char === " " || char === " ";
|
|
292
|
+
}
|
|
293
|
+
/** Whether a redirection starts here (`>`, `<`, or digits glued to one). */
|
|
294
|
+
atRedirectStart() {
|
|
295
|
+
let index = this.pos;
|
|
296
|
+
while (index < this.src.length && /[0-9]/.test(this.src.charAt(index))) index++;
|
|
297
|
+
const char = this.src.charAt(index);
|
|
298
|
+
return char === "<" || char === ">";
|
|
299
|
+
}
|
|
300
|
+
parseSimpleCommand() {
|
|
301
|
+
const start = this.pos;
|
|
302
|
+
const command = {
|
|
303
|
+
assignments: [],
|
|
304
|
+
words: [],
|
|
305
|
+
redirects: [],
|
|
306
|
+
source: ""
|
|
307
|
+
};
|
|
308
|
+
while (true) {
|
|
309
|
+
this.skipBlanks();
|
|
310
|
+
if (this.atEnd()) break;
|
|
311
|
+
const char = this.peek();
|
|
312
|
+
if (char === "\n" || char === ";" || char === "&" || char === "|" || char === ")") break;
|
|
313
|
+
if (char === "(") throw unsupported("`(` is only supported at the start of a command (a subshell); quote it to pass it as text.");
|
|
314
|
+
if (char === "#") {
|
|
315
|
+
this.skipComment();
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
if (this.atRedirectStart()) {
|
|
319
|
+
command.redirects.push(this.parseRedirect());
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const word = this.parseWord();
|
|
323
|
+
if (command.words.length === 0 && word.assignmentName !== void 0) {
|
|
324
|
+
command.assignments.push(word);
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
command.words.push(word);
|
|
328
|
+
if (command.words.length === 1 && !word.quoted && RESERVED_WORDS.has(word.value)) throw unsupported(`\`${word.value}\` starts a shell compound command, which is not supported; run the commands one at a time.`);
|
|
329
|
+
}
|
|
330
|
+
if (command.words.length === 0 && command.assignments.length === 0 && command.redirects.length === 0) throw unsupported("The line has an empty command (two operators in a row?).");
|
|
331
|
+
command.source = this.src.slice(start, this.pos).trim();
|
|
332
|
+
this.state.commands.push(command);
|
|
333
|
+
}
|
|
334
|
+
parseRedirect() {
|
|
335
|
+
let digits = "";
|
|
336
|
+
while (/[0-9]/.test(this.peek())) {
|
|
337
|
+
digits += this.peek();
|
|
338
|
+
this.pos++;
|
|
339
|
+
}
|
|
340
|
+
const rest = this.src.slice(this.pos, this.pos + 3);
|
|
341
|
+
let op;
|
|
342
|
+
if (rest.startsWith("<<<")) throw unsupported("`<<<` (a here-string) is not portable; use a here-document.");
|
|
343
|
+
else if (rest.startsWith("<<-")) op = "<<-";
|
|
344
|
+
else if (rest.startsWith("<<")) op = "<<";
|
|
345
|
+
else if (rest.startsWith("<>")) op = "<>";
|
|
346
|
+
else if (rest.startsWith("<&")) op = "<&";
|
|
347
|
+
else if (rest.startsWith("<(") || rest.startsWith(">(")) throw unsupported("Process substitution (`<(…)`, `>(…)`) is not supported.");
|
|
348
|
+
else if (rest.startsWith(">>")) op = ">>";
|
|
349
|
+
else if (rest.startsWith(">|")) op = ">|";
|
|
350
|
+
else if (rest.startsWith(">&")) op = ">&";
|
|
351
|
+
else op = this.peek();
|
|
352
|
+
this.pos += op.length;
|
|
353
|
+
const fd = digits === "" ? void 0 : Number(digits);
|
|
354
|
+
this.skipBlanks();
|
|
355
|
+
if (this.atEnd() || /[\n;&|<>()]/.test(this.peek())) throw unsupported(`The redirection \`${op}\` has no target.`);
|
|
356
|
+
const target = this.parseWord();
|
|
357
|
+
if (op === "<<" || op === "<<-") {
|
|
358
|
+
if (this.state.nesting > 0) throw unsupported("Here-documents inside subshells or substitutions are not supported.");
|
|
359
|
+
if (target.dynamic) throw unsupported("A here-document delimiter must be literal text.");
|
|
360
|
+
this.pendingHeredocs.push({
|
|
361
|
+
delimiter: target.value,
|
|
362
|
+
stripTabs: op === "<<-",
|
|
363
|
+
expand: !target.quoted
|
|
364
|
+
});
|
|
365
|
+
return {
|
|
366
|
+
op,
|
|
367
|
+
...fd !== void 0 ? { fd } : {},
|
|
368
|
+
target,
|
|
369
|
+
type: "heredoc"
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
if (op === ">&" || op === "<&") {
|
|
373
|
+
if (target.dynamic || !/^(?:[0-9]+|-)$/.test(target.value)) throw unsupported(`\`${op}${target.raw}\` is not portable; write \`> file 2>&1\` to send both streams to a file.`);
|
|
374
|
+
return {
|
|
375
|
+
op,
|
|
376
|
+
...fd !== void 0 ? { fd } : {},
|
|
377
|
+
target,
|
|
378
|
+
type: "dup"
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
return {
|
|
382
|
+
op,
|
|
383
|
+
...fd !== void 0 ? { fd } : {},
|
|
384
|
+
target,
|
|
385
|
+
type: op === "<" ? "read" : "write"
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
/** Parse one unquoted-context word. */
|
|
389
|
+
parseWord() {
|
|
390
|
+
const start = this.pos;
|
|
391
|
+
let value = "";
|
|
392
|
+
let dynamic = false;
|
|
393
|
+
let quoted = false;
|
|
394
|
+
let braces = false;
|
|
395
|
+
let nameCandidate = true;
|
|
396
|
+
let assignmentName;
|
|
397
|
+
while (!this.atEnd()) {
|
|
398
|
+
const char = this.peek();
|
|
399
|
+
if (char === " " || char === " " || char === "\n" || char === ";" || char === "&" || char === "|" || char === "<" || char === ">" || char === "(" || char === ")") break;
|
|
400
|
+
if (char === "\r" || char === "\0") throw unsupported("Carriage returns and NUL bytes are not supported in a command line.");
|
|
401
|
+
if (char === "\\") {
|
|
402
|
+
const next = this.peek(1);
|
|
403
|
+
if (next === "") throw unsupported("The line ends with a backslash.");
|
|
404
|
+
this.pos += 2;
|
|
405
|
+
if (next !== "\n") {
|
|
406
|
+
value += next;
|
|
407
|
+
quoted = true;
|
|
408
|
+
nameCandidate = false;
|
|
409
|
+
}
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
if (char === "'") {
|
|
413
|
+
const closing = this.src.indexOf("'", this.pos + 1);
|
|
414
|
+
if (closing === -1) throw unbalanced("A `'` quote is never closed.");
|
|
415
|
+
value += this.src.slice(this.pos + 1, closing);
|
|
416
|
+
quoted = true;
|
|
417
|
+
nameCandidate = false;
|
|
418
|
+
this.pos = closing + 1;
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
if (char === "\"") {
|
|
422
|
+
const part = this.parseDoubleQuoted();
|
|
423
|
+
value += part.value;
|
|
424
|
+
dynamic = dynamic || part.dynamic;
|
|
425
|
+
quoted = true;
|
|
426
|
+
nameCandidate = false;
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
if (char === "$") {
|
|
430
|
+
const part = this.parseDollar("unquoted");
|
|
431
|
+
value += part.text;
|
|
432
|
+
dynamic = dynamic || part.dynamic;
|
|
433
|
+
nameCandidate = false;
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
if (char === "`") {
|
|
437
|
+
value += this.parseBackticks(false);
|
|
438
|
+
dynamic = true;
|
|
439
|
+
nameCandidate = false;
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (char === "=" && nameCandidate && assignmentName === void 0) {
|
|
443
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) assignmentName = value;
|
|
444
|
+
nameCandidate = false;
|
|
445
|
+
} else if (!/[A-Za-z0-9_]/.test(char)) nameCandidate = false;
|
|
446
|
+
if (char === "*" || char === "?" || char === "[") dynamic = true;
|
|
447
|
+
if (char === "~") {
|
|
448
|
+
const previous = this.src.charAt(this.pos - 1);
|
|
449
|
+
if (this.pos === start || assignmentName !== void 0 && (previous === "=" || previous === ":")) dynamic = true;
|
|
450
|
+
}
|
|
451
|
+
if (char === "{" || char === "}") braces = true;
|
|
452
|
+
value += char;
|
|
453
|
+
this.pos++;
|
|
454
|
+
}
|
|
455
|
+
const raw = this.src.slice(start, this.pos);
|
|
456
|
+
if (braces && raw !== "{}") throw unsupported(`Unquoted \`{\` or \`}\` in \`${raw}\` (brace expansion or a command group) is not supported; quote it.`);
|
|
457
|
+
if (raw === "[" || raw === "]") dynamic = false;
|
|
458
|
+
return {
|
|
459
|
+
raw,
|
|
460
|
+
value,
|
|
461
|
+
dynamic,
|
|
462
|
+
quoted,
|
|
463
|
+
...assignmentName !== void 0 ? { assignmentName } : {}
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
/** Parse a `"…"` span (cursor on the opening quote). */
|
|
467
|
+
parseDoubleQuoted() {
|
|
468
|
+
this.pos++;
|
|
469
|
+
let value = "";
|
|
470
|
+
let dynamic = false;
|
|
471
|
+
while (true) {
|
|
472
|
+
if (this.atEnd()) throw unbalanced("A `\"` quote is never closed.");
|
|
473
|
+
const char = this.peek();
|
|
474
|
+
if (char === "\"") {
|
|
475
|
+
this.pos++;
|
|
476
|
+
return {
|
|
477
|
+
value,
|
|
478
|
+
dynamic
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
if (char === "\0") throw unsupported("NUL bytes are not supported in a command line.");
|
|
482
|
+
if (char === "\\") {
|
|
483
|
+
const next = this.peek(1);
|
|
484
|
+
if (next === "") throw unbalanced("A `\"` quote is never closed.");
|
|
485
|
+
if (next === "$" || next === "`" || next === "\"" || next === "\\") {
|
|
486
|
+
value += next;
|
|
487
|
+
this.pos += 2;
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
if (next === "\n") {
|
|
491
|
+
this.pos += 2;
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
value += char;
|
|
495
|
+
this.pos++;
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
if (char === "$") {
|
|
499
|
+
const part = this.parseDollar("dquote");
|
|
500
|
+
value += part.text;
|
|
501
|
+
dynamic = dynamic || part.dynamic;
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
if (char === "`") {
|
|
505
|
+
value += this.parseBackticks(true);
|
|
506
|
+
dynamic = true;
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
value += char;
|
|
510
|
+
this.pos++;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
/** Parse a `$…` expansion (cursor on the `$`). */
|
|
514
|
+
parseDollar(context) {
|
|
515
|
+
const start = this.pos;
|
|
516
|
+
const next = this.peek(1);
|
|
517
|
+
if (next === "(") {
|
|
518
|
+
if (this.peek(2) === "(") throw unsupported("`$((…))` (arithmetic expansion) is not supported.");
|
|
519
|
+
this.pos += 2;
|
|
520
|
+
this.enterNesting();
|
|
521
|
+
this.parseList(")", void 0);
|
|
522
|
+
if (this.peek() !== ")") throw unbalanced("A `$(` is never closed with `)`.");
|
|
523
|
+
this.pos++;
|
|
524
|
+
this.exitNesting();
|
|
525
|
+
return {
|
|
526
|
+
text: this.src.slice(start, this.pos),
|
|
527
|
+
dynamic: true
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
if (next === "{") {
|
|
531
|
+
const closing = this.src.indexOf("}", this.pos + 2);
|
|
532
|
+
if (closing === -1) throw unbalanced("A `${` is never closed with `}`.");
|
|
533
|
+
const body = this.src.slice(this.pos + 2, closing);
|
|
534
|
+
if (!PARAMETER_EXPANSION.test(body)) throw unsupported(`\`\${${body}}\` is not a supported parameter expansion; use \`$name\`, \`\${name}\` or \`\${name:-default}\`.`);
|
|
535
|
+
this.pos = closing + 1;
|
|
536
|
+
return {
|
|
537
|
+
text: this.src.slice(start, this.pos),
|
|
538
|
+
dynamic: true
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
if (next === "[") throw unsupported("`$[…]` (arithmetic expansion) is not supported.");
|
|
542
|
+
if (context === "unquoted" && (next === "'" || next === "\"")) throw unsupported(`\`$${next}…${next}\` quoting is not portable; use plain quotes.`);
|
|
543
|
+
if (/[A-Za-z_]/.test(next)) {
|
|
544
|
+
this.pos += 2;
|
|
545
|
+
while (/[A-Za-z0-9_]/.test(this.peek())) this.pos++;
|
|
546
|
+
return {
|
|
547
|
+
text: this.src.slice(start, this.pos),
|
|
548
|
+
dynamic: true
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
if (/[0-9@*#?$!-]/.test(next) && next !== "") {
|
|
552
|
+
this.pos += 2;
|
|
553
|
+
return {
|
|
554
|
+
text: this.src.slice(start, this.pos),
|
|
555
|
+
dynamic: true
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
this.pos++;
|
|
559
|
+
return {
|
|
560
|
+
text: "$",
|
|
561
|
+
dynamic: false
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
/** Parse a backtick substitution (cursor on the opening backtick); returns its source. */
|
|
565
|
+
parseBackticks(insideDoubleQuotes) {
|
|
566
|
+
const start = this.pos;
|
|
567
|
+
let content = "";
|
|
568
|
+
this.pos++;
|
|
569
|
+
while (true) {
|
|
570
|
+
if (this.atEnd()) throw unbalanced("A backtick substitution is never closed.");
|
|
571
|
+
const char = this.peek();
|
|
572
|
+
if (char === "`") {
|
|
573
|
+
this.pos++;
|
|
574
|
+
break;
|
|
575
|
+
}
|
|
576
|
+
if (char === "\\") {
|
|
577
|
+
const next = this.peek(1);
|
|
578
|
+
if (next === "") throw unbalanced("A backtick substitution is never closed.");
|
|
579
|
+
if (next === "`" || next === "\\" || next === "$") {
|
|
580
|
+
content += next;
|
|
581
|
+
this.pos += 2;
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
if (next === "\"" && insideDoubleQuotes) throw unsupported("An escaped `\\\"` inside backticks inside double quotes is not portable; use `$(…)`.");
|
|
585
|
+
content += char + next;
|
|
586
|
+
this.pos += 2;
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
content += char;
|
|
590
|
+
this.pos++;
|
|
591
|
+
}
|
|
592
|
+
this.enterNesting();
|
|
593
|
+
new PosixParser(content, this.state).parseNestedProgram();
|
|
594
|
+
this.exitNesting();
|
|
595
|
+
return this.src.slice(start, this.pos);
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
/** Parser for the restricted `cmd.exe` dialect. */
|
|
599
|
+
var CmdParser = class {
|
|
600
|
+
constructor(src, commands) {
|
|
601
|
+
this.src = src;
|
|
602
|
+
this.commands = commands;
|
|
603
|
+
this.pos = 0;
|
|
604
|
+
}
|
|
605
|
+
parseProgram(segments) {
|
|
606
|
+
if (/[\r\n\0]/.test(this.src)) throw unsupported("Line breaks and NUL bytes are not supported in a cmd.exe command line.");
|
|
607
|
+
while (true) {
|
|
608
|
+
this.skipBlanks();
|
|
609
|
+
if (this.atEnd()) throw unsupported(segments.length === 0 ? "The command is empty." : "The line ends with an operator but no command follows it.");
|
|
610
|
+
const start = this.pos;
|
|
611
|
+
this.parsePipeline();
|
|
612
|
+
const end = this.pos;
|
|
613
|
+
this.skipBlanks();
|
|
614
|
+
const operator = this.readListOperator();
|
|
615
|
+
segments.push({
|
|
616
|
+
source: this.src.slice(start, end).trim(),
|
|
617
|
+
...operator !== null ? { operator } : {}
|
|
618
|
+
});
|
|
619
|
+
if (operator === null) {
|
|
620
|
+
if (!this.atEnd()) throw unsupported(`Unexpected \`${this.peek()}\`.`);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
atEnd() {
|
|
626
|
+
return this.pos >= this.src.length;
|
|
627
|
+
}
|
|
628
|
+
peek(offset = 0) {
|
|
629
|
+
return this.src.charAt(this.pos + offset);
|
|
630
|
+
}
|
|
631
|
+
skipBlanks() {
|
|
632
|
+
while (this.peek() === " " || this.peek() === " ") this.pos++;
|
|
633
|
+
}
|
|
634
|
+
readListOperator() {
|
|
635
|
+
if (this.peek() === "&") {
|
|
636
|
+
if (this.peek(1) === "&") {
|
|
637
|
+
this.pos += 2;
|
|
638
|
+
return "&&";
|
|
639
|
+
}
|
|
640
|
+
this.pos++;
|
|
641
|
+
return "&";
|
|
642
|
+
}
|
|
643
|
+
if (this.peek() === "|" && this.peek(1) === "|") {
|
|
644
|
+
this.pos += 2;
|
|
645
|
+
return "||";
|
|
646
|
+
}
|
|
647
|
+
return null;
|
|
648
|
+
}
|
|
649
|
+
parsePipeline() {
|
|
650
|
+
this.parseSimpleCommand();
|
|
651
|
+
while (this.peek() === "|" && this.peek(1) !== "|") {
|
|
652
|
+
this.pos++;
|
|
653
|
+
this.skipBlanks();
|
|
654
|
+
if (this.atEnd()) throw unsupported("The line ends with `|` but no command follows it.");
|
|
655
|
+
this.parseSimpleCommand();
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
atRedirectStart() {
|
|
659
|
+
const char = this.peek();
|
|
660
|
+
if (char === "<" || char === ">") return true;
|
|
661
|
+
return /[0-9]/.test(char) && (this.peek(1) === ">" || this.peek(1) === "<");
|
|
662
|
+
}
|
|
663
|
+
parseSimpleCommand() {
|
|
664
|
+
const start = this.pos;
|
|
665
|
+
const command = {
|
|
666
|
+
assignments: [],
|
|
667
|
+
words: [],
|
|
668
|
+
redirects: [],
|
|
669
|
+
source: ""
|
|
670
|
+
};
|
|
671
|
+
while (true) {
|
|
672
|
+
this.skipBlanks();
|
|
673
|
+
if (this.atEnd() || this.peek() === "&" || this.peek() === "|") break;
|
|
674
|
+
if (this.atRedirectStart()) {
|
|
675
|
+
command.redirects.push(this.parseRedirect());
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
const word = this.parseWord();
|
|
679
|
+
command.words.push(word);
|
|
680
|
+
if (command.words.length === 1) this.checkCommandWord(word);
|
|
681
|
+
}
|
|
682
|
+
if (command.words.length === 0 && command.redirects.length === 0) throw unsupported("The line has an empty command (two operators in a row?).");
|
|
683
|
+
command.source = this.src.slice(start, this.pos).trim();
|
|
684
|
+
this.commands.push(command);
|
|
685
|
+
}
|
|
686
|
+
checkCommandWord(word) {
|
|
687
|
+
if (word.raw.startsWith("@") || word.raw.startsWith(":")) throw unsupported(`\`${word.raw}\` is not supported as a cmd.exe command name.`);
|
|
688
|
+
if (/[/=]/.test(word.value)) throw unsupported(`A cmd.exe command name may not contain \`/\` or \`=\` (\`${word.value}\`); use \`\\\` in paths.`);
|
|
689
|
+
if (!word.quoted && CMD_RESERVED_WORDS.has(word.value.toLowerCase())) throw unsupported(`\`${word.value}\` starts a cmd.exe compound command, which is not supported.`);
|
|
690
|
+
}
|
|
691
|
+
parseRedirect() {
|
|
692
|
+
let fd;
|
|
693
|
+
if (/[0-9]/.test(this.peek())) {
|
|
694
|
+
fd = Number(this.peek());
|
|
695
|
+
this.pos++;
|
|
696
|
+
}
|
|
697
|
+
let op;
|
|
698
|
+
if (this.src.startsWith(">>", this.pos)) op = ">>";
|
|
699
|
+
else if (this.src.startsWith(">&", this.pos)) op = ">&";
|
|
700
|
+
else if (this.src.startsWith("<&", this.pos) || this.src.startsWith("<<", this.pos)) throw unsupported(`\`${this.src.slice(this.pos, this.pos + 2)}\` is not supported by cmd.exe.`);
|
|
701
|
+
else op = this.peek();
|
|
702
|
+
this.pos += op.length;
|
|
703
|
+
this.skipBlanks();
|
|
704
|
+
if (this.atEnd() || /[&|<>]/.test(this.peek())) throw unsupported(`The redirection \`${op}\` has no target.`);
|
|
705
|
+
const target = this.parseWord();
|
|
706
|
+
if (op === ">&") {
|
|
707
|
+
if (!/^[0-9]$/.test(target.value)) throw unsupported(`\`>&${target.raw}\` is not supported; write \`> file 2>&1\`.`);
|
|
708
|
+
return {
|
|
709
|
+
op,
|
|
710
|
+
...fd !== void 0 ? { fd } : {},
|
|
711
|
+
target,
|
|
712
|
+
type: "dup"
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
return {
|
|
716
|
+
op,
|
|
717
|
+
...fd !== void 0 ? { fd } : {},
|
|
718
|
+
target,
|
|
719
|
+
type: op === "<" ? "read" : "write"
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
parseWord() {
|
|
723
|
+
const start = this.pos;
|
|
724
|
+
let value = "";
|
|
725
|
+
let dynamic = false;
|
|
726
|
+
let quoted = false;
|
|
727
|
+
while (!this.atEnd()) {
|
|
728
|
+
const char = this.peek();
|
|
729
|
+
if (char === " " || char === " " || char === "&" || char === "|" || char === "<" || char === ">") break;
|
|
730
|
+
if (char === "\"") {
|
|
731
|
+
const closing = this.src.indexOf("\"", this.pos + 1);
|
|
732
|
+
if (closing === -1) throw unbalanced("A `\"` quote is never closed.");
|
|
733
|
+
const inner = this.src.slice(this.pos + 1, closing);
|
|
734
|
+
if (/[%!]/.test(inner)) throw unsupported("`%` and `!` expand even inside quotes in cmd.exe; they are not supported.");
|
|
735
|
+
if (value.endsWith("\\") || inner.endsWith("\\")) throw unsupported("A backslash before a `\"` is read differently by cmd.exe and programs.");
|
|
736
|
+
value += inner;
|
|
737
|
+
quoted = true;
|
|
738
|
+
this.pos = closing + 1;
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
if (char === "%" || char === "!" || char === "^" || char === "(" || char === ")" || char === ";" || char === ",") throw unsupported(`Unquoted \`${char}\` has a special meaning in cmd.exe and is not supported.`);
|
|
742
|
+
if (char === "\\" && this.peek(1) === "\"") throw unsupported("A backslash before a `\"` is read differently by cmd.exe and programs.");
|
|
743
|
+
if (char === "*" || char === "?") dynamic = true;
|
|
744
|
+
value += char;
|
|
745
|
+
this.pos++;
|
|
746
|
+
}
|
|
747
|
+
return {
|
|
748
|
+
raw: this.src.slice(start, this.pos),
|
|
749
|
+
value,
|
|
750
|
+
dynamic,
|
|
751
|
+
quoted
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
/**
|
|
756
|
+
* Parse a command line for the shell policy. Never throws: a construct the
|
|
757
|
+
* parser does not read with confidence comes back as `{ ok: false }` with a
|
|
758
|
+
* message naming it.
|
|
759
|
+
*
|
|
760
|
+
* @example
|
|
761
|
+
* const parsed = parseShellLine("git add -A && git commit -m 'wip'", "posix");
|
|
762
|
+
* // parsed.ok === true; parsed.commands.map((c) => c.words.map((w) => w.value))
|
|
763
|
+
* // → [["git", "add", "-A"], ["git", "commit", "-m", "wip"]]
|
|
764
|
+
*/
|
|
765
|
+
function parseShellLine(line, dialect) {
|
|
766
|
+
const segments = [];
|
|
767
|
+
try {
|
|
768
|
+
if (dialect === "cmd") {
|
|
769
|
+
const commands = [];
|
|
770
|
+
new CmdParser(line, commands).parseProgram(segments);
|
|
771
|
+
return {
|
|
772
|
+
ok: true,
|
|
773
|
+
commands,
|
|
774
|
+
segments,
|
|
775
|
+
background: false
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
const state = {
|
|
779
|
+
commands: [],
|
|
780
|
+
background: false,
|
|
781
|
+
nesting: 0
|
|
782
|
+
};
|
|
783
|
+
new PosixParser(line, state).parseProgram(segments);
|
|
784
|
+
return {
|
|
785
|
+
ok: true,
|
|
786
|
+
commands: state.commands,
|
|
787
|
+
segments,
|
|
788
|
+
background: state.background
|
|
789
|
+
};
|
|
790
|
+
} catch (error) {
|
|
791
|
+
if (error instanceof ShellSyntaxRefusal) return {
|
|
792
|
+
ok: false,
|
|
793
|
+
reason: error.reason,
|
|
794
|
+
message: error.message
|
|
795
|
+
};
|
|
796
|
+
throw error;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
//#endregion
|
|
801
|
+
export { parseShellLine };
|
|
802
|
+
//# sourceMappingURL=shell-parser.mjs.map
|