@seanhogg/builderforce-webcontainers-core 2026.10.7 → 2026.10.8
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/dist/archive.d.ts +2 -0
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +6 -4
- package/dist/archive.js.map +1 -1
- package/dist/html.d.ts +2 -0
- package/dist/html.d.ts.map +1 -1
- package/dist/html.js +5 -3
- package/dist/html.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/node/http/client.d.ts +3 -1
- package/dist/node/http/client.d.ts.map +1 -1
- package/dist/node/http/client.js +74 -5
- package/dist/node/http/client.js.map +1 -1
- package/dist/node/http/index.d.ts.map +1 -1
- package/dist/node/http/index.js +2 -1
- package/dist/node/http/index.js.map +1 -1
- package/dist/node/http/messages.d.ts +87 -0
- package/dist/node/http/messages.d.ts.map +1 -0
- package/dist/node/http/messages.js +146 -0
- package/dist/node/http/messages.js.map +1 -0
- package/dist/node/http/server.d.ts +13 -98
- package/dist/node/http/server.d.ts.map +1 -1
- package/dist/node/http/server.js +34 -202
- package/dist/node/http/server.js.map +1 -1
- package/dist/node/http/serverResponse.d.ts +42 -0
- package/dist/node/http/serverResponse.d.ts.map +1 -0
- package/dist/node/http/serverResponse.js +89 -0
- package/dist/node/http/serverResponse.js.map +1 -0
- package/dist/node/http/upgradeSocket.d.ts +34 -0
- package/dist/node/http/upgradeSocket.d.ts.map +1 -0
- package/dist/node/http/upgradeSocket.js +73 -0
- package/dist/node/http/upgradeSocket.js.map +1 -0
- package/dist/node/runtime.js +2 -2
- package/dist/node/runtime.js.map +1 -1
- package/dist/previewBridge.d.ts +42 -0
- package/dist/previewBridge.d.ts.map +1 -0
- package/dist/previewBridge.js +192 -0
- package/dist/previewBridge.js.map +1 -0
- package/dist/shell/ast.d.ts +101 -0
- package/dist/shell/ast.d.ts.map +1 -0
- package/dist/shell/ast.js +7 -0
- package/dist/shell/ast.js.map +1 -0
- package/dist/shell/builtins.d.ts +14 -0
- package/dist/shell/builtins.d.ts.map +1 -1
- package/dist/shell/builtins.js +87 -7
- package/dist/shell/builtins.js.map +1 -1
- package/dist/shell/compound.d.ts +30 -0
- package/dist/shell/compound.d.ts.map +1 -0
- package/dist/shell/compound.js +73 -0
- package/dist/shell/compound.js.map +1 -0
- package/dist/shell/exec.d.ts +27 -6
- package/dist/shell/exec.d.ts.map +1 -1
- package/dist/shell/exec.js +127 -41
- package/dist/shell/exec.js.map +1 -1
- package/dist/shell/expand.d.ts +5 -1
- package/dist/shell/expand.d.ts.map +1 -1
- package/dist/shell/expand.js +7 -5
- package/dist/shell/expand.js.map +1 -1
- package/dist/shell/lineReader.d.ts +9 -0
- package/dist/shell/lineReader.d.ts.map +1 -0
- package/dist/shell/lineReader.js +42 -0
- package/dist/shell/lineReader.js.map +1 -0
- package/dist/shell/packageManagers.d.ts +20 -0
- package/dist/shell/packageManagers.d.ts.map +1 -0
- package/dist/shell/packageManagers.js +74 -0
- package/dist/shell/packageManagers.js.map +1 -0
- package/dist/shell/parse.d.ts +16 -66
- package/dist/shell/parse.d.ts.map +1 -1
- package/dist/shell/parse.js +244 -243
- package/dist/shell/parse.js.map +1 -1
- package/dist/shell/program.d.ts.map +1 -1
- package/dist/shell/program.js +2 -1
- package/dist/shell/program.js.map +1 -1
- package/dist/shell/testCommand.d.ts +9 -0
- package/dist/shell/testCommand.d.ts.map +1 -0
- package/dist/shell/testCommand.js +105 -0
- package/dist/shell/testCommand.js.map +1 -0
- package/dist/shell/tokenize.d.ts +25 -0
- package/dist/shell/tokenize.d.ts.map +1 -0
- package/dist/shell/tokenize.js +279 -0
- package/dist/shell/tokenize.js.map +1 -0
- package/dist/system/cookieJar.d.ts +59 -0
- package/dist/system/cookieJar.d.ts.map +1 -0
- package/dist/system/cookieJar.js +187 -0
- package/dist/system/cookieJar.js.map +1 -0
- package/dist/system/httpStream.d.ts +40 -0
- package/dist/system/httpStream.d.ts.map +1 -0
- package/dist/system/httpStream.js +156 -0
- package/dist/system/httpStream.js.map +1 -0
- package/dist/system/index.d.ts +9 -1
- package/dist/system/index.d.ts.map +1 -1
- package/dist/system/index.js +4 -0
- package/dist/system/index.js.map +1 -1
- package/dist/system/kernel.d.ts +10 -2
- package/dist/system/kernel.d.ts.map +1 -1
- package/dist/system/kernel.js +12 -3
- package/dist/system/kernel.js.map +1 -1
- package/dist/system/ports.d.ts +9 -4
- package/dist/system/ports.d.ts.map +1 -1
- package/dist/system/ports.js +21 -2
- package/dist/system/ports.js.map +1 -1
- package/dist/system/programs.d.ts +1 -1
- package/dist/system/programs.d.ts.map +1 -1
- package/dist/system/programs.js +2 -0
- package/dist/system/programs.js.map +1 -1
- package/dist/system/types.d.ts +36 -6
- package/dist/system/types.d.ts.map +1 -1
- package/dist/system/webSocketClient.d.ts +37 -0
- package/dist/system/webSocketClient.d.ts.map +1 -0
- package/dist/system/webSocketClient.js +152 -0
- package/dist/system/webSocketClient.js.map +1 -0
- package/dist/system/webSocketFrames.d.ts +39 -0
- package/dist/system/webSocketFrames.d.ts.map +1 -0
- package/dist/system/webSocketFrames.js +117 -0
- package/dist/system/webSocketFrames.js.map +1 -0
- package/package.json +1 -1
- package/src/archive.ts +8 -4
- package/src/html.ts +5 -3
- package/src/index.ts +2 -1
- package/src/node/http/client.ts +69 -6
- package/src/node/http/index.ts +2 -1
- package/src/node/http/messages.ts +150 -0
- package/src/node/http/server.ts +37 -202
- package/src/node/http/serverResponse.ts +98 -0
- package/src/node/http/upgradeSocket.ts +79 -0
- package/src/node/runtime.ts +2 -2
- package/src/previewBridge.ts +194 -0
- package/src/shell/ast.ts +85 -0
- package/src/shell/builtins.ts +97 -7
- package/src/shell/compound.ts +89 -0
- package/src/shell/exec.ts +128 -40
- package/src/shell/expand.ts +8 -6
- package/src/shell/lineReader.ts +46 -0
- package/src/shell/packageManagers.ts +75 -0
- package/src/shell/parse.ts +233 -236
- package/src/shell/program.ts +2 -1
- package/src/shell/testCommand.ts +105 -0
- package/src/shell/tokenize.ts +257 -0
- package/src/system/cookieJar.ts +187 -0
- package/src/system/httpStream.ts +168 -0
- package/src/system/index.ts +24 -1
- package/src/system/kernel.ts +16 -4
- package/src/system/ports.ts +23 -4
- package/src/system/programs.ts +3 -1
- package/src/system/types.ts +40 -6
- package/src/system/webSocketClient.ts +177 -0
- package/src/system/webSocketFrames.ts +118 -0
package/src/shell/parse.ts
CHANGED
|
@@ -1,243 +1,89 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The shell grammar, a practical subset of POSIX sh:
|
|
3
3
|
*
|
|
4
|
-
* script
|
|
5
|
-
* list
|
|
6
|
-
* pipeline
|
|
7
|
-
* command
|
|
8
|
-
*
|
|
4
|
+
* script := list (('\n' | ';' | '&') list)*
|
|
5
|
+
* list := pipeline (('&&' | '||') pipeline)*
|
|
6
|
+
* pipeline := ['!'] command ('|' command)*
|
|
7
|
+
* command := simple | compound redirect* | function
|
|
8
|
+
* simple := (NAME=word)* word* redirect*
|
|
9
|
+
* compound := if … then … [elif … then …]* [else …] fi
|
|
10
|
+
* | while|until … do … done | for NAME [in word*] do … done
|
|
11
|
+
* | case word in (pattern [| pattern]*) … ;; … esac
|
|
12
|
+
* | '{' script '}' | '(' script ')'
|
|
13
|
+
* function := NAME '(' ')' compound | 'function' NAME ['(' ')'] compound
|
|
14
|
+
* redirect := ('>' | '>>' | '<' | '2>' | '2>>' | '&>') word | '2>&1' | '>&2' | ('<<' | '<<-') DELIM
|
|
9
15
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
16
|
+
* Reserved words count only in command position and unquoted, as in sh, so
|
|
17
|
+
* `echo done` prints "done".
|
|
12
18
|
*/
|
|
19
|
+
import type { AndOrList, CaseCommand, Command, CompoundCommand, ForCommand, IfCommand, LoopCommand, Pipeline, Redirect, RedirectOp, Script, Segment, SimpleCommand, Word } from './ast.js';
|
|
20
|
+
import { ShellSyntaxError, tokenize, type Token } from './tokenize.js';
|
|
13
21
|
|
|
14
|
-
export type
|
|
15
|
-
|
|
16
|
-
| { kind: 'var'; name: string; quoted: boolean; op?: ':-' | '-' | ':=' | ':+'; arg?: Word }
|
|
17
|
-
| { kind: 'cmd'; source: string; quoted: boolean }
|
|
18
|
-
| { kind: 'tilde' };
|
|
19
|
-
export type Word = Segment[];
|
|
20
|
-
|
|
21
|
-
export type RedirectOp = '>' | '>>' | '<' | '2>' | '2>>' | '&>' | '2>&1' | '>&2';
|
|
22
|
-
export interface Redirect {
|
|
23
|
-
op: RedirectOp;
|
|
24
|
-
target?: Word;
|
|
25
|
-
}
|
|
26
|
-
export interface Command {
|
|
27
|
-
assignments: Array<{ name: string; value: Word }>;
|
|
28
|
-
words: Word[];
|
|
29
|
-
redirects: Redirect[];
|
|
30
|
-
}
|
|
31
|
-
export interface Pipeline {
|
|
32
|
-
negate: boolean;
|
|
33
|
-
commands: Command[];
|
|
34
|
-
}
|
|
35
|
-
export interface AndOrList {
|
|
36
|
-
first: Pipeline;
|
|
37
|
-
rest: Array<{ op: '&&' | '||'; pipeline: Pipeline }>;
|
|
38
|
-
background: boolean;
|
|
39
|
-
}
|
|
40
|
-
export type Script = AndOrList[];
|
|
41
|
-
|
|
42
|
-
type Token = { type: 'word'; word: Word; raw: string } | { type: 'op'; op: string };
|
|
43
|
-
|
|
44
|
-
export class ShellSyntaxError extends Error {
|
|
45
|
-
/** The input ended inside a quote or after `|`/`&&`: an interactive shell asks for more. */
|
|
46
|
-
constructor(message: string, readonly incomplete = false) {
|
|
47
|
-
super(message);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const OPERATORS = ['2>&1', '>&2', '2>>', '&&', '||', '>>', '2>', '&>', '|', '&', ';', '>', '<', '\n'];
|
|
52
|
-
const SPECIAL_VARS = /^[?$#@*!0-9]/;
|
|
53
|
-
|
|
54
|
-
/** Find the `)` closing a `$(` at `start` (just after the paren), honouring quotes and nesting. */
|
|
55
|
-
function matchParen(input: string, start: number): number {
|
|
56
|
-
let depth = 1;
|
|
57
|
-
for (let i = start; i < input.length; i++) {
|
|
58
|
-
const ch = input[i];
|
|
59
|
-
if (ch === '\\') i++;
|
|
60
|
-
else if (ch === "'") i = input.indexOf("'", i + 1) < 0 ? input.length : input.indexOf("'", i + 1);
|
|
61
|
-
else if (ch === '"') {
|
|
62
|
-
for (i++; i < input.length && input[i] !== '"'; i++) if (input[i] === '\\') i++;
|
|
63
|
-
} else if (ch === '(') depth++;
|
|
64
|
-
else if (ch === ')' && --depth === 0) return i;
|
|
65
|
-
}
|
|
66
|
-
throw new ShellSyntaxError('unexpected end of input: missing )', true);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Parse `$…` at `i` (pointing at `$`); returns the segment and the index after it. */
|
|
70
|
-
function readDollar(input: string, i: number, quoted: boolean): [Segment, number] {
|
|
71
|
-
const next = input[i + 1];
|
|
72
|
-
if (next === '(') {
|
|
73
|
-
const end = matchParen(input, i + 2);
|
|
74
|
-
return [{ kind: 'cmd', source: input.slice(i + 2, end), quoted }, end + 1];
|
|
75
|
-
}
|
|
76
|
-
if (next === '{') {
|
|
77
|
-
const end = input.indexOf('}', i + 2);
|
|
78
|
-
if (end < 0) throw new ShellSyntaxError('unexpected end of input: missing }', true);
|
|
79
|
-
const body = input.slice(i + 2, end);
|
|
80
|
-
const m = /^([A-Za-z_][\w]*|[?$#@*!0-9])(?:(:-|-|:=|:\+)(.*))?$/s.exec(body);
|
|
81
|
-
if (!m) throw new ShellSyntaxError(`bad substitution: \${${body}}`);
|
|
82
|
-
const seg: Segment = { kind: 'var', name: m[1]!, quoted };
|
|
83
|
-
if (m[2]) {
|
|
84
|
-
seg.op = m[2] as ':-';
|
|
85
|
-
seg.arg = tokenizeWord(m[3] ?? '', quoted);
|
|
86
|
-
}
|
|
87
|
-
return [seg, end + 1];
|
|
88
|
-
}
|
|
89
|
-
if (next && SPECIAL_VARS.test(next)) return [{ kind: 'var', name: next, quoted }, i + 2];
|
|
90
|
-
const m = /^[A-Za-z_]\w*/.exec(input.slice(i + 1));
|
|
91
|
-
if (!m) return [{ kind: 'text', value: '$', quoted }, i + 1];
|
|
92
|
-
return [{ kind: 'var', name: m[0], quoted }, i + 1 + m[0].length];
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** Segments of a word body (used for `${x:-default}` arguments). */
|
|
96
|
-
function tokenizeWord(text: string, quoted: boolean): Word {
|
|
97
|
-
const tokens = tokenize(text, true);
|
|
98
|
-
const word: Word = [];
|
|
99
|
-
for (const t of tokens) if (t.type === 'word') word.push(...t.word.map((s) => (s.kind === 'tilde' ? s : { ...s, quoted: quoted || ('quoted' in s && s.quoted) } as Segment)));
|
|
100
|
-
return word;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function tokenize(input: string, wordOnly = false): Token[] {
|
|
104
|
-
const tokens: Token[] = [];
|
|
105
|
-
let i = 0;
|
|
106
|
-
while (i < input.length) {
|
|
107
|
-
const ch = input[i]!;
|
|
108
|
-
if (ch === ' ' || ch === '\t' || ch === '\r') {
|
|
109
|
-
i++;
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
if (ch === '\\' && input[i + 1] === '\n') {
|
|
113
|
-
i += 2;
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
if (ch === '#') {
|
|
117
|
-
while (i < input.length && input[i] !== '\n') i++;
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
if (!wordOnly) {
|
|
121
|
-
const op = OPERATORS.find((o) => input.startsWith(o, i));
|
|
122
|
-
if (op) {
|
|
123
|
-
tokens.push({ type: 'op', op });
|
|
124
|
-
i += op.length;
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
// A word: runs until unquoted whitespace or an operator character.
|
|
129
|
-
const word: Word = [];
|
|
130
|
-
const start = i;
|
|
131
|
-
let text = '';
|
|
132
|
-
const flush = () => {
|
|
133
|
-
if (text) word.push({ kind: 'text', value: text, quoted: false });
|
|
134
|
-
text = '';
|
|
135
|
-
};
|
|
136
|
-
if (input[i] === '~' && (i + 1 >= input.length || /[\s/;|&<>]/.test(input[i + 1]!))) {
|
|
137
|
-
word.push({ kind: 'tilde' });
|
|
138
|
-
i++;
|
|
139
|
-
}
|
|
140
|
-
while (i < input.length) {
|
|
141
|
-
const c = input[i]!;
|
|
142
|
-
if (/[\s]/.test(c) && !wordOnly) break;
|
|
143
|
-
if (!wordOnly && /[;|&<>]/.test(c)) break;
|
|
144
|
-
if (c === '\\') {
|
|
145
|
-
text += input[i + 1] ?? '';
|
|
146
|
-
i += 2;
|
|
147
|
-
} else if (c === "'") {
|
|
148
|
-
const end = input.indexOf("'", i + 1);
|
|
149
|
-
if (end < 0) throw new ShellSyntaxError('unexpected end of input: unterminated quote', true);
|
|
150
|
-
flush();
|
|
151
|
-
word.push({ kind: 'text', value: input.slice(i + 1, end), quoted: true });
|
|
152
|
-
i = end + 1;
|
|
153
|
-
} else if (c === '"') {
|
|
154
|
-
flush();
|
|
155
|
-
i++;
|
|
156
|
-
let inner = '';
|
|
157
|
-
const pushInner = () => {
|
|
158
|
-
if (inner) word.push({ kind: 'text', value: inner, quoted: true });
|
|
159
|
-
inner = '';
|
|
160
|
-
};
|
|
161
|
-
let closed = false;
|
|
162
|
-
while (i < input.length) {
|
|
163
|
-
const d = input[i]!;
|
|
164
|
-
if (d === '"') {
|
|
165
|
-
closed = true;
|
|
166
|
-
i++;
|
|
167
|
-
break;
|
|
168
|
-
}
|
|
169
|
-
if (d === '\\' && /["\\$`\n]/.test(input[i + 1] ?? '')) {
|
|
170
|
-
inner += input[i + 1];
|
|
171
|
-
i += 2;
|
|
172
|
-
} else if (d === '$') {
|
|
173
|
-
pushInner();
|
|
174
|
-
const [seg, next] = readDollar(input, i, true);
|
|
175
|
-
word.push(seg);
|
|
176
|
-
i = next;
|
|
177
|
-
} else if (d === '`') {
|
|
178
|
-
pushInner();
|
|
179
|
-
const end = input.indexOf('`', i + 1);
|
|
180
|
-
if (end < 0) throw new ShellSyntaxError('unexpected end of input: unterminated `', true);
|
|
181
|
-
word.push({ kind: 'cmd', source: input.slice(i + 1, end), quoted: true });
|
|
182
|
-
i = end + 1;
|
|
183
|
-
} else {
|
|
184
|
-
inner += d;
|
|
185
|
-
i++;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
if (!closed) throw new ShellSyntaxError('unexpected end of input: unterminated quote', true);
|
|
189
|
-
pushInner();
|
|
190
|
-
if (!word.length || word[word.length - 1]!.kind !== 'text') word.push({ kind: 'text', value: '', quoted: true });
|
|
191
|
-
} else if (c === '$') {
|
|
192
|
-
flush();
|
|
193
|
-
const [seg, next] = readDollar(input, i, false);
|
|
194
|
-
word.push(seg);
|
|
195
|
-
i = next;
|
|
196
|
-
} else if (c === '`') {
|
|
197
|
-
flush();
|
|
198
|
-
const end = input.indexOf('`', i + 1);
|
|
199
|
-
if (end < 0) throw new ShellSyntaxError('unexpected end of input: unterminated `', true);
|
|
200
|
-
word.push({ kind: 'cmd', source: input.slice(i + 1, end), quoted: false });
|
|
201
|
-
i = end + 1;
|
|
202
|
-
} else {
|
|
203
|
-
text += c;
|
|
204
|
-
i++;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
flush();
|
|
208
|
-
tokens.push({ type: 'word', word, raw: input.slice(start, i) });
|
|
209
|
-
}
|
|
210
|
-
return tokens;
|
|
211
|
-
}
|
|
22
|
+
export type * from './ast.js';
|
|
23
|
+
export { ShellSyntaxError, tokenize } from './tokenize.js';
|
|
212
24
|
|
|
213
25
|
const ASSIGNMENT = /^([A-Za-z_]\w*)=/;
|
|
214
|
-
const
|
|
26
|
+
const NAME = /^[A-Za-z_][\w-]*$/;
|
|
27
|
+
const REDIRECTS = new Set(['>', '>>', '<', '2>', '2>>', '&>', '2>&1', '>&2', '<<', '<<-']);
|
|
28
|
+
/** Words that end a nested list; never the start of a command. */
|
|
29
|
+
const CLOSERS = new Set(['then', 'elif', 'else', 'fi', 'do', 'done', 'esac', '}']);
|
|
215
30
|
|
|
216
31
|
export function parse(input: string): Script {
|
|
217
32
|
const tokens = tokenize(input);
|
|
218
33
|
let pos = 0;
|
|
219
|
-
const peek = () => tokens[pos];
|
|
34
|
+
const peek = (): Token | undefined => tokens[pos];
|
|
220
35
|
const isOp = (op: string) => {
|
|
221
36
|
const t = tokens[pos];
|
|
222
37
|
return t?.type === 'op' && t.op === op;
|
|
223
38
|
};
|
|
39
|
+
const isWord = (raw: string, at = pos) => {
|
|
40
|
+
const t = tokens[at];
|
|
41
|
+
return t?.type === 'word' && t.raw === raw;
|
|
42
|
+
};
|
|
43
|
+
const describe = (t: Token | undefined) => (!t ? 'end of input' : t.type === 'op' ? `\`${t.op.replace('\n', 'newline')}'` : t.type === 'word' ? `\`${t.raw}'` : 'here-document');
|
|
44
|
+
const fail = (t: Token | undefined): never => {
|
|
45
|
+
throw new ShellSyntaxError(t ? `syntax error near unexpected token ${describe(t)}` : 'unexpected end of input', !t);
|
|
46
|
+
};
|
|
47
|
+
const skipNewlines = () => {
|
|
48
|
+
while (isOp('\n')) pos++;
|
|
49
|
+
};
|
|
50
|
+
const skipSeparators = () => {
|
|
51
|
+
while (isOp('\n') || isOp(';')) pos++;
|
|
52
|
+
};
|
|
53
|
+
const expectWord = (raw: string) => {
|
|
54
|
+
skipSeparators();
|
|
55
|
+
if (!isWord(raw)) fail(peek());
|
|
56
|
+
pos++;
|
|
57
|
+
};
|
|
224
58
|
|
|
225
|
-
const
|
|
226
|
-
const
|
|
59
|
+
const redirect = (cmd: { redirects: Redirect[] }): boolean => {
|
|
60
|
+
const t = peek();
|
|
61
|
+
if (t?.type !== 'op' || !REDIRECTS.has(t.op)) return false;
|
|
62
|
+
pos++;
|
|
63
|
+
if (t.op === '2>&1' || t.op === '>&2') {
|
|
64
|
+
cmd.redirects.push({ op: t.op });
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
const target = peek();
|
|
68
|
+
if (t.op === '<<' || t.op === '<<-') {
|
|
69
|
+
if (target?.type !== 'heredoc' || !target.body) fail(target);
|
|
70
|
+
pos++;
|
|
71
|
+
cmd.redirects.push({ op: t.op, heredoc: { body: (target as { body: Word }).body } });
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
if (target?.type !== 'word') fail(target);
|
|
75
|
+
pos++;
|
|
76
|
+
cmd.redirects.push({ op: t.op as RedirectOp, target: (target as { word: Word }).word });
|
|
77
|
+
return true;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const simple = (): SimpleCommand => {
|
|
81
|
+
const cmd: SimpleCommand = { type: 'simple', assignments: [], words: [], redirects: [] };
|
|
227
82
|
for (;;) {
|
|
228
83
|
const t = peek();
|
|
229
|
-
if (!t) break;
|
|
84
|
+
if (!t || t.type === 'heredoc') break;
|
|
230
85
|
if (t.type === 'op') {
|
|
231
|
-
if (!
|
|
232
|
-
pos++;
|
|
233
|
-
if (t.op === '2>&1' || t.op === '>&2') {
|
|
234
|
-
cmd.redirects.push({ op: t.op });
|
|
235
|
-
continue;
|
|
236
|
-
}
|
|
237
|
-
const target = peek();
|
|
238
|
-
if (target?.type !== 'word') throw new ShellSyntaxError(`syntax error near unexpected token \`${target?.type === 'op' ? target.op.replace('\n', 'newline') : 'newline'}'`);
|
|
239
|
-
pos++;
|
|
240
|
-
cmd.redirects.push({ op: t.op as RedirectOp, target: target.word });
|
|
86
|
+
if (!redirect(cmd)) break;
|
|
241
87
|
continue;
|
|
242
88
|
}
|
|
243
89
|
pos++;
|
|
@@ -251,45 +97,196 @@ export function parse(input: string): Script {
|
|
|
251
97
|
return cmd;
|
|
252
98
|
};
|
|
253
99
|
|
|
100
|
+
/** A nested list, up to (not including) one of `stops`. */
|
|
101
|
+
const list = (stops: ReadonlySet<string>, stopOps: ReadonlySet<string> = new Set()): Script => {
|
|
102
|
+
const script: Script = [];
|
|
103
|
+
for (;;) {
|
|
104
|
+
skipSeparators();
|
|
105
|
+
const t = peek();
|
|
106
|
+
if (!t) fail(t);
|
|
107
|
+
if (t!.type === 'word' && stops.has(t!.raw)) return script;
|
|
108
|
+
if (t!.type === 'op' && stopOps.has(t!.op)) return script;
|
|
109
|
+
script.push(andOr());
|
|
110
|
+
if (isOp('&')) {
|
|
111
|
+
script[script.length - 1]!.background = true;
|
|
112
|
+
pos++;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const ifCommand = (): IfCommand => {
|
|
118
|
+
pos++; // if
|
|
119
|
+
const clauses: IfCommand['clauses'] = [];
|
|
120
|
+
let otherwise: Script | undefined;
|
|
121
|
+
const condition = list(new Set(['then']));
|
|
122
|
+
expectWord('then');
|
|
123
|
+
clauses.push({ condition, body: list(new Set(['elif', 'else', 'fi'])) });
|
|
124
|
+
for (;;) {
|
|
125
|
+
if (isWord('elif')) {
|
|
126
|
+
pos++;
|
|
127
|
+
const next = list(new Set(['then']));
|
|
128
|
+
expectWord('then');
|
|
129
|
+
clauses.push({ condition: next, body: list(new Set(['elif', 'else', 'fi'])) });
|
|
130
|
+
} else if (isWord('else')) {
|
|
131
|
+
pos++;
|
|
132
|
+
otherwise = list(new Set(['fi']));
|
|
133
|
+
} else break;
|
|
134
|
+
}
|
|
135
|
+
expectWord('fi');
|
|
136
|
+
return { type: 'if', clauses, otherwise, redirects: [] };
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const doBody = (): Script => {
|
|
140
|
+
expectWord('do');
|
|
141
|
+
const body = list(new Set(['done']));
|
|
142
|
+
expectWord('done');
|
|
143
|
+
return body;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const loop = (type: 'while' | 'until'): LoopCommand => {
|
|
147
|
+
pos++;
|
|
148
|
+
const condition = list(new Set(['do']));
|
|
149
|
+
return { type, condition, body: doBody(), redirects: [] };
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const forCommand = (): ForCommand => {
|
|
153
|
+
pos++; // for
|
|
154
|
+
const t = peek();
|
|
155
|
+
if (t?.type !== 'word' || !NAME.test(t.raw)) fail(t);
|
|
156
|
+
const name = (t as { raw: string }).raw;
|
|
157
|
+
pos++;
|
|
158
|
+
skipNewlines();
|
|
159
|
+
let items: Word[] | undefined;
|
|
160
|
+
if (isWord('in')) {
|
|
161
|
+
pos++;
|
|
162
|
+
items = [];
|
|
163
|
+
while (peek()?.type === 'word') items.push((tokens[pos++] as { word: Word }).word);
|
|
164
|
+
}
|
|
165
|
+
return { type: 'for', name, items, body: doBody(), redirects: [] };
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const caseCommand = (): CaseCommand => {
|
|
169
|
+
pos++; // case
|
|
170
|
+
const subject = peek();
|
|
171
|
+
if (subject?.type !== 'word') fail(subject);
|
|
172
|
+
pos++;
|
|
173
|
+
skipNewlines();
|
|
174
|
+
if (!isWord('in')) fail(peek());
|
|
175
|
+
pos++;
|
|
176
|
+
const arms: CaseCommand['arms'] = [];
|
|
177
|
+
for (;;) {
|
|
178
|
+
skipSeparators();
|
|
179
|
+
if (isWord('esac')) break;
|
|
180
|
+
if (isOp('(')) pos++;
|
|
181
|
+
const patterns: Word[] = [];
|
|
182
|
+
for (;;) {
|
|
183
|
+
const p = peek();
|
|
184
|
+
if (p?.type !== 'word') fail(p);
|
|
185
|
+
patterns.push((p as { word: Word }).word);
|
|
186
|
+
pos++;
|
|
187
|
+
if (isOp('|')) pos++;
|
|
188
|
+
else break;
|
|
189
|
+
}
|
|
190
|
+
if (!isOp(')')) fail(peek());
|
|
191
|
+
pos++;
|
|
192
|
+
const body = list(new Set(['esac']), new Set([';;']));
|
|
193
|
+
arms.push({ patterns, body });
|
|
194
|
+
if (isOp(';;')) pos++;
|
|
195
|
+
}
|
|
196
|
+
pos++; // esac
|
|
197
|
+
return { type: 'case', subject: (subject as { word: Word }).word, arms, redirects: [] };
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const compound = (): CompoundCommand | undefined => {
|
|
201
|
+
let node: CompoundCommand | undefined;
|
|
202
|
+
if (isWord('if')) node = ifCommand();
|
|
203
|
+
else if (isWord('while')) node = loop('while');
|
|
204
|
+
else if (isWord('until')) node = loop('until');
|
|
205
|
+
else if (isWord('for')) node = forCommand();
|
|
206
|
+
else if (isWord('case')) node = caseCommand();
|
|
207
|
+
else if (isWord('{')) {
|
|
208
|
+
pos++;
|
|
209
|
+
const body = list(new Set(['}']));
|
|
210
|
+
pos++;
|
|
211
|
+
node = { type: 'group', body, redirects: [] };
|
|
212
|
+
} else if (isOp('(')) {
|
|
213
|
+
pos++;
|
|
214
|
+
const body = list(new Set(), new Set([')']));
|
|
215
|
+
pos++;
|
|
216
|
+
node = { type: 'subshell', body, redirects: [] };
|
|
217
|
+
}
|
|
218
|
+
if (node) while (redirect(node));
|
|
219
|
+
return node;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const command = (): Command => {
|
|
223
|
+
const t = peek();
|
|
224
|
+
if (t?.type === 'word' && CLOSERS.has(t.raw)) fail(t);
|
|
225
|
+
const block = compound();
|
|
226
|
+
if (block) return block;
|
|
227
|
+
// function NAME [()] body | NAME () body
|
|
228
|
+
const named = isWord('function') ? 1 : 0;
|
|
229
|
+
const nameToken = tokens[pos + named];
|
|
230
|
+
if (nameToken?.type === 'word' && NAME.test(nameToken.raw)) {
|
|
231
|
+
const parens = tokens[pos + named + 1]?.type === 'op' && (tokens[pos + named + 1] as { op: string }).op === '(';
|
|
232
|
+
if (named || parens) {
|
|
233
|
+
pos += named + 1;
|
|
234
|
+
if (parens) {
|
|
235
|
+
pos++;
|
|
236
|
+
if (!isOp(')')) fail(peek());
|
|
237
|
+
pos++;
|
|
238
|
+
}
|
|
239
|
+
skipNewlines();
|
|
240
|
+
const body = compound();
|
|
241
|
+
if (!body) fail(peek());
|
|
242
|
+
return { type: 'function', name: nameToken.raw, body: body!, redirects: [] };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return simple();
|
|
246
|
+
};
|
|
247
|
+
|
|
254
248
|
const pipeline = (): Pipeline => {
|
|
255
249
|
let negate = false;
|
|
256
|
-
|
|
257
|
-
if (t?.type === 'word' && t.raw === '!') {
|
|
250
|
+
if (isWord('!')) {
|
|
258
251
|
negate = true;
|
|
259
252
|
pos++;
|
|
260
253
|
}
|
|
261
254
|
const commands = [command()];
|
|
262
255
|
while (isOp('|')) {
|
|
263
256
|
pos++;
|
|
264
|
-
|
|
257
|
+
skipNewlines();
|
|
265
258
|
if (!peek()) throw new ShellSyntaxError('unexpected end of input after |', true);
|
|
266
259
|
commands.push(command());
|
|
267
260
|
}
|
|
268
|
-
for (const c of commands) if (!c.words.length && !c.assignments.length && !c.redirects.length)
|
|
261
|
+
for (const c of commands) if (c.type === 'simple' && !c.words.length && !c.assignments.length && !c.redirects.length) fail(peek() ?? { type: 'op', op: '|' });
|
|
269
262
|
return { negate, commands };
|
|
270
263
|
};
|
|
271
264
|
|
|
272
|
-
const
|
|
273
|
-
while (pos < tokens.length) {
|
|
274
|
-
if (isOp(';') || isOp('\n')) {
|
|
275
|
-
pos++;
|
|
276
|
-
continue;
|
|
277
|
-
}
|
|
265
|
+
const andOr = (): AndOrList => {
|
|
278
266
|
const t = peek()!;
|
|
279
|
-
if (t.type === 'op' && !REDIRECTS.has(t.op)
|
|
280
|
-
const
|
|
267
|
+
if (t.type === 'op' && !REDIRECTS.has(t.op) && t.op !== '(') fail(t);
|
|
268
|
+
const result: AndOrList = { first: pipeline(), rest: [], background: false };
|
|
281
269
|
while (isOp('&&') || isOp('||')) {
|
|
282
270
|
const op = (tokens[pos] as { op: '&&' | '||' }).op;
|
|
283
271
|
pos++;
|
|
284
|
-
|
|
272
|
+
skipNewlines();
|
|
285
273
|
if (!peek()) throw new ShellSyntaxError(`unexpected end of input after ${op}`, true);
|
|
286
|
-
|
|
274
|
+
result.rest.push({ op, pipeline: pipeline() });
|
|
275
|
+
}
|
|
276
|
+
return result;
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const script: Script = [];
|
|
280
|
+
while (pos < tokens.length) {
|
|
281
|
+
if (isOp(';') || isOp('\n')) {
|
|
282
|
+
pos++;
|
|
283
|
+
continue;
|
|
287
284
|
}
|
|
285
|
+
script.push(andOr());
|
|
288
286
|
if (isOp('&')) {
|
|
289
|
-
|
|
287
|
+
script[script.length - 1]!.background = true;
|
|
290
288
|
pos++;
|
|
291
289
|
}
|
|
292
|
-
script.push(list);
|
|
293
290
|
}
|
|
294
291
|
return script;
|
|
295
292
|
}
|
package/src/shell/program.ts
CHANGED
|
@@ -14,9 +14,10 @@ import { Shell, type JobInput, type ShellCommand, type ShellIO } from './exec.js
|
|
|
14
14
|
import { LineEditor } from './lineEditor.js';
|
|
15
15
|
import { BUILTINS, COREUTILS, SYSTEM_PROGRAMS } from './builtins.js';
|
|
16
16
|
import { npmCommand, npxCommand } from './npm.js';
|
|
17
|
+
import { pnpmCommand, yarnCommand } from './packageManagers.js';
|
|
17
18
|
import { glob } from './expand.js';
|
|
18
19
|
|
|
19
|
-
export const SHELL_COMMANDS: Record<string, ShellCommand> = { npm: npmCommand, npx: npxCommand };
|
|
20
|
+
export const SHELL_COMMANDS: Record<string, ShellCommand> = { npm: npmCommand, npx: npxCommand, pnpm: pnpmCommand, yarn: yarnCommand };
|
|
20
21
|
|
|
21
22
|
function createShell(ctx: ProgramContext, positional: string[] = []): Shell {
|
|
22
23
|
return new Shell({
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `test` and `[ … ]`: file tests (-e -f -d -s -r -w -x), string tests (-z -n,
|
|
3
|
+
* = == !=), integer comparisons (-eq -ne -lt -le -gt -ge), `!`, `-a`/`-o` and
|
|
4
|
+
* parentheses. Exit 0 for true, 1 for false, 2 for a malformed expression.
|
|
5
|
+
*/
|
|
6
|
+
import type { Command, CommandContext } from './context.js';
|
|
7
|
+
import { resolvePath } from './context.js';
|
|
8
|
+
|
|
9
|
+
class TestSyntaxError extends Error {}
|
|
10
|
+
|
|
11
|
+
const FILE_TESTS = new Set(['-e', '-f', '-d', '-s', '-r', '-w', '-x', '-L', '-h']);
|
|
12
|
+
const INT_OPS = new Set(['-eq', '-ne', '-lt', '-le', '-gt', '-ge']);
|
|
13
|
+
|
|
14
|
+
function evaluate(ctx: CommandContext, args: string[]): boolean {
|
|
15
|
+
let pos = 0;
|
|
16
|
+
const peek = () => args[pos];
|
|
17
|
+
const next = () => {
|
|
18
|
+
if (pos >= args.length) throw new TestSyntaxError('argument expected');
|
|
19
|
+
return args[pos++]!;
|
|
20
|
+
};
|
|
21
|
+
const integer = (value: string) => {
|
|
22
|
+
if (!/^\s*-?\d+\s*$/.test(value)) throw new TestSyntaxError(`${value}: integer expression expected`);
|
|
23
|
+
return Number(value);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const primary = (): boolean => {
|
|
27
|
+
const token = next();
|
|
28
|
+
if (token === '!') return !primary();
|
|
29
|
+
if (token === '(') {
|
|
30
|
+
const value = or();
|
|
31
|
+
if (next() !== ')') throw new TestSyntaxError("')' expected");
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
const op = peek();
|
|
35
|
+
if (op !== undefined && (op === '=' || op === '==' || op === '!=' || INT_OPS.has(op)) && args[pos + 1] !== undefined) {
|
|
36
|
+
pos++;
|
|
37
|
+
const right = next();
|
|
38
|
+
if (op === '=' || op === '==') return token === right;
|
|
39
|
+
if (op === '!=') return token !== right;
|
|
40
|
+
const [a, b] = [integer(token), integer(right)];
|
|
41
|
+
return op === '-eq' ? a === b : op === '-ne' ? a !== b : op === '-lt' ? a < b : op === '-le' ? a <= b : op === '-gt' ? a > b : a >= b;
|
|
42
|
+
}
|
|
43
|
+
if (token === '-z' && peek() !== undefined) return next().length === 0;
|
|
44
|
+
if (token === '-n' && peek() !== undefined) return next().length > 0;
|
|
45
|
+
if (FILE_TESTS.has(token) && peek() !== undefined) {
|
|
46
|
+
const path = resolvePath(ctx, next());
|
|
47
|
+
const fs = ctx.fs;
|
|
48
|
+
switch (token) {
|
|
49
|
+
case '-f': return fs.isFile(path);
|
|
50
|
+
case '-d': return fs.isDirectory(path);
|
|
51
|
+
case '-s': {
|
|
52
|
+
const contents = fs.readFile(path);
|
|
53
|
+
return contents !== undefined && contents.length > 0;
|
|
54
|
+
}
|
|
55
|
+
case '-L':
|
|
56
|
+
case '-h': return false;
|
|
57
|
+
default: return fs.exists(path);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return token.length > 0;
|
|
61
|
+
};
|
|
62
|
+
const and = (): boolean => {
|
|
63
|
+
let value = primary();
|
|
64
|
+
while (peek() === '-a') {
|
|
65
|
+
pos++;
|
|
66
|
+
const right = primary();
|
|
67
|
+
value = value && right;
|
|
68
|
+
}
|
|
69
|
+
return value;
|
|
70
|
+
};
|
|
71
|
+
const or = (): boolean => {
|
|
72
|
+
let value = and();
|
|
73
|
+
while (peek() === '-o') {
|
|
74
|
+
pos++;
|
|
75
|
+
const right = and();
|
|
76
|
+
value = value || right;
|
|
77
|
+
}
|
|
78
|
+
return value;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
if (!args.length) return false;
|
|
82
|
+
const result = or();
|
|
83
|
+
if (pos < args.length) throw new TestSyntaxError(`${args[pos]}: unexpected argument`);
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function run(ctx: CommandContext, args: string[]): number {
|
|
88
|
+
try {
|
|
89
|
+
return evaluate(ctx, args) ? 0 : 1;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (!(error instanceof TestSyntaxError)) throw error;
|
|
92
|
+
ctx.io.stderr(`${ctx.name}: ${error.message}\n`);
|
|
93
|
+
return 2;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const test: Command = async (ctx) => run(ctx, ctx.args);
|
|
98
|
+
|
|
99
|
+
export const bracket: Command = async (ctx) => {
|
|
100
|
+
if (ctx.args[ctx.args.length - 1] !== ']') {
|
|
101
|
+
ctx.io.stderr("[: missing ']'\n");
|
|
102
|
+
return 2;
|
|
103
|
+
}
|
|
104
|
+
return run(ctx, ctx.args.slice(0, -1));
|
|
105
|
+
};
|