@ssweens/pi-leash 0.12.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 +17 -0
- package/LICENSE +21 -0
- package/README.md +221 -0
- package/package.json +83 -0
- package/src/config.ts +285 -0
- package/src/hooks/index.ts +9 -0
- package/src/hooks/permission-gate.ts +925 -0
- package/src/hooks/policies.ts +315 -0
- package/src/index.ts +38 -0
- package/src/lib/executor.ts +280 -0
- package/src/lib/index.ts +16 -0
- package/src/lib/model-resolver.ts +47 -0
- package/src/lib/timing.ts +42 -0
- package/src/lib/types.ts +115 -0
- package/src/utils/events.ts +32 -0
- package/src/utils/glob-expander.ts +128 -0
- package/src/utils/matching.ts +111 -0
- package/src/utils/shell-utils.ts +139 -0
- package/src/vendor/aliou-sh/NOTICE.md +13 -0
- package/src/vendor/aliou-sh/ast.d.ts +186 -0
- package/src/vendor/aliou-sh/index.d.ts +3 -0
- package/src/vendor/aliou-sh/index.js +1397 -0
- package/src/vendor/aliou-sh/parse.d.ts +3 -0
- package/src/vendor/aliou-sh/upstream.package.json +55 -0
|
@@ -0,0 +1,1397 @@
|
|
|
1
|
+
//#region src/tokenizer/charsets.ts
|
|
2
|
+
const operatorChars = new Set([
|
|
3
|
+
";",
|
|
4
|
+
"|",
|
|
5
|
+
"&"
|
|
6
|
+
]);
|
|
7
|
+
const redirChars = new Set([">", "<"]);
|
|
8
|
+
const symbolChars = new Set([
|
|
9
|
+
"(",
|
|
10
|
+
")",
|
|
11
|
+
"{",
|
|
12
|
+
"}"
|
|
13
|
+
]);
|
|
14
|
+
const isDigit = (value) => value >= "0" && value <= "9";
|
|
15
|
+
const isNameChar = (c) => c >= "a" && c <= "z" || c >= "A" && c <= "Z" || c >= "0" && c <= "9" || c === "_";
|
|
16
|
+
const isNameStart = (c) => c >= "a" && c <= "z" || c >= "A" && c <= "Z" || c === "_";
|
|
17
|
+
const specialParams = new Set([
|
|
18
|
+
"@",
|
|
19
|
+
"*",
|
|
20
|
+
"#",
|
|
21
|
+
"?",
|
|
22
|
+
"-",
|
|
23
|
+
"$",
|
|
24
|
+
"!"
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/tokenizer/scan-backtick.ts
|
|
29
|
+
function scanBacktick(source, pos) {
|
|
30
|
+
let j = pos + 1;
|
|
31
|
+
while (j < source.length && source.charAt(j) !== "`") {
|
|
32
|
+
if (source.charAt(j) === "\\") j++;
|
|
33
|
+
j++;
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
part: {
|
|
37
|
+
type: "backtick",
|
|
38
|
+
raw: source.slice(pos + 1, j)
|
|
39
|
+
},
|
|
40
|
+
end: j + 1
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region src/tokenizer/scan-expansion.ts
|
|
46
|
+
function scanExpansion(source, pos) {
|
|
47
|
+
if (source.charAt(pos) !== "$") return null;
|
|
48
|
+
const next = source.charAt(pos + 1);
|
|
49
|
+
if (next === "(" && source.charAt(pos + 2) === "(") {
|
|
50
|
+
let j = pos + 3;
|
|
51
|
+
let depth = 0;
|
|
52
|
+
while (j < source.length) {
|
|
53
|
+
if (source.charAt(j) === ")" && source.charAt(j + 1) === ")" && depth === 0) break;
|
|
54
|
+
if (source.charAt(j) === "(") depth++;
|
|
55
|
+
if (source.charAt(j) === ")") depth--;
|
|
56
|
+
j++;
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
part: {
|
|
60
|
+
type: "arith-exp",
|
|
61
|
+
raw: source.slice(pos + 3, j).trim()
|
|
62
|
+
},
|
|
63
|
+
end: j + 2
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (next === "(") {
|
|
67
|
+
let j = pos + 2;
|
|
68
|
+
let depth = 1;
|
|
69
|
+
while (j < source.length && depth > 0) {
|
|
70
|
+
if (source.charAt(j) === "(") depth++;
|
|
71
|
+
if (source.charAt(j) === ")") depth--;
|
|
72
|
+
j++;
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
part: {
|
|
76
|
+
type: "cmd-subst",
|
|
77
|
+
raw: source.slice(pos + 2, j - 1)
|
|
78
|
+
},
|
|
79
|
+
end: j
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (next === "{") {
|
|
83
|
+
let j = pos + 2;
|
|
84
|
+
let depth = 1;
|
|
85
|
+
while (j < source.length && depth > 0) {
|
|
86
|
+
if (source.charAt(j) === "{") depth++;
|
|
87
|
+
if (source.charAt(j) === "}") depth--;
|
|
88
|
+
j++;
|
|
89
|
+
}
|
|
90
|
+
const inner = source.slice(pos + 2, j - 1);
|
|
91
|
+
let nameEnd = 0;
|
|
92
|
+
let prefix = "";
|
|
93
|
+
if (inner.charAt(0) === "!" || inner.charAt(0) === "#") {
|
|
94
|
+
prefix = inner.charAt(0);
|
|
95
|
+
nameEnd = 1;
|
|
96
|
+
}
|
|
97
|
+
while (nameEnd < inner.length && isNameChar(inner.charAt(nameEnd))) nameEnd++;
|
|
98
|
+
const name = inner.slice(prefix ? 1 : 0, nameEnd);
|
|
99
|
+
const rest = inner.slice(nameEnd);
|
|
100
|
+
if (rest.length > 0) {
|
|
101
|
+
const opMatch = rest.match(/^(:-|:=|:\+|:\?|-|\+|=|\?|##|%%|#|%|\/\/|\/)/);
|
|
102
|
+
if (opMatch) {
|
|
103
|
+
const op = opMatch[0];
|
|
104
|
+
return {
|
|
105
|
+
part: {
|
|
106
|
+
type: "param",
|
|
107
|
+
name,
|
|
108
|
+
braced: true,
|
|
109
|
+
op,
|
|
110
|
+
value: rest.slice(op.length)
|
|
111
|
+
},
|
|
112
|
+
end: j
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
part: {
|
|
117
|
+
type: "param",
|
|
118
|
+
name: inner,
|
|
119
|
+
braced: true
|
|
120
|
+
},
|
|
121
|
+
end: j
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
part: {
|
|
126
|
+
type: "param",
|
|
127
|
+
name,
|
|
128
|
+
braced: true
|
|
129
|
+
},
|
|
130
|
+
end: j
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (isNameStart(next)) {
|
|
134
|
+
let j = pos + 2;
|
|
135
|
+
while (j < source.length && isNameChar(source.charAt(j))) j++;
|
|
136
|
+
return {
|
|
137
|
+
part: {
|
|
138
|
+
type: "param",
|
|
139
|
+
name: source.slice(pos + 1, j),
|
|
140
|
+
braced: false
|
|
141
|
+
},
|
|
142
|
+
end: j
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
if (isDigit(next)) return {
|
|
146
|
+
part: {
|
|
147
|
+
type: "param",
|
|
148
|
+
name: next,
|
|
149
|
+
braced: false
|
|
150
|
+
},
|
|
151
|
+
end: pos + 2
|
|
152
|
+
};
|
|
153
|
+
if (specialParams.has(next)) return {
|
|
154
|
+
part: {
|
|
155
|
+
type: "param",
|
|
156
|
+
name: next,
|
|
157
|
+
braced: false
|
|
158
|
+
},
|
|
159
|
+
end: pos + 2
|
|
160
|
+
};
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
//#endregion
|
|
165
|
+
//#region src/tokenizer/scan-redir.ts
|
|
166
|
+
function tryRedirOp(source, pos) {
|
|
167
|
+
if (source.startsWith("<<<", pos)) return {
|
|
168
|
+
op: "<<<",
|
|
169
|
+
len: 3
|
|
170
|
+
};
|
|
171
|
+
if (source.startsWith("&>>", pos)) return {
|
|
172
|
+
op: "&>>",
|
|
173
|
+
len: 3
|
|
174
|
+
};
|
|
175
|
+
if (source.startsWith("<<-", pos)) return {
|
|
176
|
+
op: "<<-",
|
|
177
|
+
len: 3
|
|
178
|
+
};
|
|
179
|
+
if (source.startsWith(">>", pos)) return {
|
|
180
|
+
op: ">>",
|
|
181
|
+
len: 2
|
|
182
|
+
};
|
|
183
|
+
if (source.startsWith(">&", pos)) return {
|
|
184
|
+
op: ">&",
|
|
185
|
+
len: 2
|
|
186
|
+
};
|
|
187
|
+
if (source.startsWith(">|", pos)) return {
|
|
188
|
+
op: ">|",
|
|
189
|
+
len: 2
|
|
190
|
+
};
|
|
191
|
+
if (source.startsWith("<>", pos)) return {
|
|
192
|
+
op: "<>",
|
|
193
|
+
len: 2
|
|
194
|
+
};
|
|
195
|
+
if (source.startsWith("<&", pos)) return {
|
|
196
|
+
op: "<&",
|
|
197
|
+
len: 2
|
|
198
|
+
};
|
|
199
|
+
if (source.startsWith("&>", pos)) return {
|
|
200
|
+
op: "&>",
|
|
201
|
+
len: 2
|
|
202
|
+
};
|
|
203
|
+
if (source.startsWith("<<", pos)) return {
|
|
204
|
+
op: "<<",
|
|
205
|
+
len: 2
|
|
206
|
+
};
|
|
207
|
+
if (source.charAt(pos) === ">") return {
|
|
208
|
+
op: ">",
|
|
209
|
+
len: 1
|
|
210
|
+
};
|
|
211
|
+
if (source.charAt(pos) === "<") return {
|
|
212
|
+
op: "<",
|
|
213
|
+
len: 1
|
|
214
|
+
};
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
//#endregion
|
|
219
|
+
//#region src/tokenizer/utils.ts
|
|
220
|
+
function tokenPartsText(parts) {
|
|
221
|
+
return parts.map((p) => {
|
|
222
|
+
if (p.type === "lit") return p.value;
|
|
223
|
+
if (p.type === "sgl") return p.value;
|
|
224
|
+
if (p.type === "dbl") return p.parts.map((dp) => dp.type === "lit" ? dp.value : "").join("");
|
|
225
|
+
return "";
|
|
226
|
+
}).join("");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
//#endregion
|
|
230
|
+
//#region src/tokenizer/tokenize.ts
|
|
231
|
+
function tokenize(source, options = {}) {
|
|
232
|
+
const tokens = [];
|
|
233
|
+
let i = 0;
|
|
234
|
+
let atBoundary = true;
|
|
235
|
+
while (i < source.length) {
|
|
236
|
+
const ch = source.charAt(i);
|
|
237
|
+
if (ch === " " || ch === " " || ch === "\r") {
|
|
238
|
+
atBoundary = true;
|
|
239
|
+
i += 1;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (ch === "\\" && source.charAt(i + 1) === "\n") {
|
|
243
|
+
atBoundary = true;
|
|
244
|
+
i += 2;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (ch === "\\" && source.charAt(i + 1) === "\r") {
|
|
248
|
+
if (source.charAt(i + 2) === "\n") {
|
|
249
|
+
atBoundary = true;
|
|
250
|
+
i += 3;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (ch === "\n") {
|
|
255
|
+
tokens.push({
|
|
256
|
+
type: "op",
|
|
257
|
+
value: ";"
|
|
258
|
+
});
|
|
259
|
+
atBoundary = true;
|
|
260
|
+
i += 1;
|
|
261
|
+
const pendingHeredocs = [];
|
|
262
|
+
for (let ti = 0; ti < tokens.length; ti++) {
|
|
263
|
+
const t = tokens[ti];
|
|
264
|
+
if (t && t.type === "redir" && (t.op === "<<" || t.op === "<<-") && !Object.hasOwn(t, "_collected")) {
|
|
265
|
+
const delimTok = tokens[ti + 1];
|
|
266
|
+
if (delimTok && delimTok.type === "word") {
|
|
267
|
+
pendingHeredocs.push({
|
|
268
|
+
strip: t.op === "<<-",
|
|
269
|
+
delimiter: tokenPartsText(delimTok.parts)
|
|
270
|
+
});
|
|
271
|
+
t._collected = true;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
for (const hd of pendingHeredocs) {
|
|
276
|
+
let body = "";
|
|
277
|
+
while (i < source.length) {
|
|
278
|
+
let lineEnd = source.indexOf("\n", i);
|
|
279
|
+
if (lineEnd === -1) lineEnd = source.length;
|
|
280
|
+
const line = source.slice(i, lineEnd);
|
|
281
|
+
const checkLine = hd.strip ? line.replace(/^\t+/, "") : line;
|
|
282
|
+
i = lineEnd < source.length ? lineEnd + 1 : lineEnd;
|
|
283
|
+
if (checkLine === hd.delimiter) break;
|
|
284
|
+
const processedLine = hd.strip ? line.replace(/^\t+/, "") : line;
|
|
285
|
+
body += `${processedLine}\n`;
|
|
286
|
+
}
|
|
287
|
+
tokens.push({
|
|
288
|
+
type: "heredoc-body",
|
|
289
|
+
content: body
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (ch === "#" && atBoundary) {
|
|
295
|
+
const start = i + 1;
|
|
296
|
+
i += 1;
|
|
297
|
+
while (i < source.length && source.charAt(i) !== "\n") i += 1;
|
|
298
|
+
if (options.keepComments) tokens.push({
|
|
299
|
+
type: "comment",
|
|
300
|
+
text: source.slice(start, i)
|
|
301
|
+
});
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (ch === "!" && atBoundary) {
|
|
305
|
+
tokens.push({
|
|
306
|
+
type: "op",
|
|
307
|
+
value: "!"
|
|
308
|
+
});
|
|
309
|
+
atBoundary = true;
|
|
310
|
+
i += 1;
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (isDigit(ch)) {
|
|
314
|
+
let j = i;
|
|
315
|
+
while (j < source.length && isDigit(source.charAt(j))) j += 1;
|
|
316
|
+
const redir = tryRedirOp(source, j);
|
|
317
|
+
if (redir) {
|
|
318
|
+
tokens.push({
|
|
319
|
+
type: "redir",
|
|
320
|
+
op: redir.op,
|
|
321
|
+
fd: source.slice(i, j)
|
|
322
|
+
});
|
|
323
|
+
i = j + redir.len;
|
|
324
|
+
atBoundary = true;
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (ch === "(" && source.charAt(i + 1) === "(" && atBoundary) {
|
|
329
|
+
let j = i + 2;
|
|
330
|
+
let depth = 0;
|
|
331
|
+
while (j < source.length) {
|
|
332
|
+
const c = source.charAt(j);
|
|
333
|
+
if (c === ")" && source.charAt(j + 1) === ")" && depth === 0) break;
|
|
334
|
+
if (c === "(") depth++;
|
|
335
|
+
if (c === ")") depth--;
|
|
336
|
+
j++;
|
|
337
|
+
}
|
|
338
|
+
tokens.push({
|
|
339
|
+
type: "arith-cmd",
|
|
340
|
+
expr: source.slice(i + 2, j).trim()
|
|
341
|
+
});
|
|
342
|
+
i = j + 2;
|
|
343
|
+
atBoundary = true;
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if ((ch === "<" || ch === ">") && source.charAt(i + 1) === "(" && atBoundary) {
|
|
347
|
+
const op = ch;
|
|
348
|
+
let j = i + 2;
|
|
349
|
+
let depth = 1;
|
|
350
|
+
while (j < source.length && depth > 0) {
|
|
351
|
+
if (source.charAt(j) === "(") depth++;
|
|
352
|
+
if (source.charAt(j) === ")") depth--;
|
|
353
|
+
j++;
|
|
354
|
+
}
|
|
355
|
+
const raw = source.slice(i + 2, j - 1);
|
|
356
|
+
tokens.push({
|
|
357
|
+
type: "word",
|
|
358
|
+
parts: [{
|
|
359
|
+
type: "proc-subst",
|
|
360
|
+
op,
|
|
361
|
+
raw
|
|
362
|
+
}]
|
|
363
|
+
});
|
|
364
|
+
i = j;
|
|
365
|
+
atBoundary = false;
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
{
|
|
369
|
+
const redir = tryRedirOp(source, i);
|
|
370
|
+
if (redir) {
|
|
371
|
+
tokens.push({
|
|
372
|
+
type: "redir",
|
|
373
|
+
op: redir.op
|
|
374
|
+
});
|
|
375
|
+
i += redir.len;
|
|
376
|
+
atBoundary = true;
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (symbolChars.has(ch)) {
|
|
381
|
+
tokens.push({
|
|
382
|
+
type: "symbol",
|
|
383
|
+
value: ch
|
|
384
|
+
});
|
|
385
|
+
atBoundary = true;
|
|
386
|
+
i += 1;
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
if (source.startsWith("&&", i)) {
|
|
390
|
+
tokens.push({
|
|
391
|
+
type: "op",
|
|
392
|
+
value: "&&"
|
|
393
|
+
});
|
|
394
|
+
atBoundary = true;
|
|
395
|
+
i += 2;
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (source.startsWith("||", i)) {
|
|
399
|
+
tokens.push({
|
|
400
|
+
type: "op",
|
|
401
|
+
value: "||"
|
|
402
|
+
});
|
|
403
|
+
atBoundary = true;
|
|
404
|
+
i += 2;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
if (operatorChars.has(ch)) {
|
|
408
|
+
tokens.push({
|
|
409
|
+
type: "op",
|
|
410
|
+
value: ch
|
|
411
|
+
});
|
|
412
|
+
atBoundary = true;
|
|
413
|
+
i += 1;
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
const parts = [];
|
|
417
|
+
let current = "";
|
|
418
|
+
const flushLit = () => {
|
|
419
|
+
if (current.length > 0) {
|
|
420
|
+
parts.push({
|
|
421
|
+
type: "lit",
|
|
422
|
+
value: current
|
|
423
|
+
});
|
|
424
|
+
current = "";
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
while (i < source.length) {
|
|
428
|
+
const currentChar = source.charAt(i);
|
|
429
|
+
if (currentChar === "\\" && source.charAt(i + 1) === "\n") {
|
|
430
|
+
i += 2;
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (currentChar === "\\" && source.charAt(i + 1) === "\r") {
|
|
434
|
+
if (source.charAt(i + 2) === "\n") {
|
|
435
|
+
i += 3;
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (currentChar === " " || currentChar === " " || currentChar === "\r" || currentChar === "\n" || operatorChars.has(currentChar) || redirChars.has(currentChar) || symbolChars.has(currentChar)) break;
|
|
440
|
+
if (currentChar === "'") {
|
|
441
|
+
flushLit();
|
|
442
|
+
i += 1;
|
|
443
|
+
const start = i;
|
|
444
|
+
while (i < source.length && source.charAt(i) !== "'") i += 1;
|
|
445
|
+
if (i >= source.length) throw new Error("Unclosed single quote");
|
|
446
|
+
parts.push({
|
|
447
|
+
type: "sgl",
|
|
448
|
+
value: source.slice(start, i)
|
|
449
|
+
});
|
|
450
|
+
i += 1;
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
if (currentChar === "\"") {
|
|
454
|
+
flushLit();
|
|
455
|
+
i += 1;
|
|
456
|
+
const dblParts = [];
|
|
457
|
+
let dblBuf = "";
|
|
458
|
+
const flushDblLit = () => {
|
|
459
|
+
if (dblBuf.length > 0) {
|
|
460
|
+
dblParts.push({
|
|
461
|
+
type: "lit",
|
|
462
|
+
value: dblBuf
|
|
463
|
+
});
|
|
464
|
+
dblBuf = "";
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
let closed = false;
|
|
468
|
+
while (i < source.length) {
|
|
469
|
+
const dqChar = source.charAt(i);
|
|
470
|
+
if (dqChar === "\\" && source.charAt(i + 1) === "\n") {
|
|
471
|
+
i += 2;
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
if (dqChar === "\\" && source.charAt(i + 1) === "\r") {
|
|
475
|
+
if (source.charAt(i + 2) === "\n") {
|
|
476
|
+
i += 3;
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (dqChar === "\\" && i + 1 < source.length) {
|
|
481
|
+
dblBuf += dqChar + source.charAt(i + 1);
|
|
482
|
+
i += 2;
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
if (dqChar === "$") {
|
|
486
|
+
flushDblLit();
|
|
487
|
+
const exp = scanExpansion(source, i);
|
|
488
|
+
if (exp) {
|
|
489
|
+
dblParts.push(exp.part);
|
|
490
|
+
i = exp.end;
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
dblBuf += dqChar;
|
|
494
|
+
i += 1;
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
if (dqChar === "`") {
|
|
498
|
+
flushDblLit();
|
|
499
|
+
const bt = scanBacktick(source, i);
|
|
500
|
+
dblParts.push(bt.part);
|
|
501
|
+
i = bt.end;
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
if (dqChar === "\"") {
|
|
505
|
+
i += 1;
|
|
506
|
+
closed = true;
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
dblBuf += dqChar;
|
|
510
|
+
i += 1;
|
|
511
|
+
}
|
|
512
|
+
if (!closed) throw new Error("Unclosed double quote");
|
|
513
|
+
flushDblLit();
|
|
514
|
+
parts.push({
|
|
515
|
+
type: "dbl",
|
|
516
|
+
parts: dblParts
|
|
517
|
+
});
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
if (currentChar === "$") {
|
|
521
|
+
flushLit();
|
|
522
|
+
const exp = scanExpansion(source, i);
|
|
523
|
+
if (exp) {
|
|
524
|
+
parts.push(exp.part);
|
|
525
|
+
i = exp.end;
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
current += currentChar;
|
|
529
|
+
i += 1;
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
if (currentChar === "`") {
|
|
533
|
+
flushLit();
|
|
534
|
+
const bt = scanBacktick(source, i);
|
|
535
|
+
parts.push(bt.part);
|
|
536
|
+
i = bt.end;
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
current += currentChar;
|
|
540
|
+
i += 1;
|
|
541
|
+
}
|
|
542
|
+
flushLit();
|
|
543
|
+
if (parts.length === 0) throw new Error("Unexpected character");
|
|
544
|
+
tokens.push({
|
|
545
|
+
type: "word",
|
|
546
|
+
parts
|
|
547
|
+
});
|
|
548
|
+
atBoundary = false;
|
|
549
|
+
}
|
|
550
|
+
return tokens;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
//#endregion
|
|
554
|
+
//#region src/parser/constants.ts
|
|
555
|
+
const DECL_KEYWORDS = new Set([
|
|
556
|
+
"declare",
|
|
557
|
+
"local",
|
|
558
|
+
"export",
|
|
559
|
+
"readonly",
|
|
560
|
+
"typeset",
|
|
561
|
+
"nameref"
|
|
562
|
+
]);
|
|
563
|
+
|
|
564
|
+
//#endregion
|
|
565
|
+
//#region src/parser/parser.ts
|
|
566
|
+
var Parser = class Parser {
|
|
567
|
+
index = 0;
|
|
568
|
+
comments = [];
|
|
569
|
+
constructor(tokens, options = {}) {
|
|
570
|
+
this.tokens = tokens;
|
|
571
|
+
this.options = options;
|
|
572
|
+
}
|
|
573
|
+
parseProgram() {
|
|
574
|
+
const body = [];
|
|
575
|
+
this.skipSeparators();
|
|
576
|
+
while (!this.isEof()) {
|
|
577
|
+
body.push(this.parseStatement());
|
|
578
|
+
this.skipSeparators();
|
|
579
|
+
}
|
|
580
|
+
const program = {
|
|
581
|
+
type: "Program",
|
|
582
|
+
body
|
|
583
|
+
};
|
|
584
|
+
if (this.options.keepComments && this.comments.length > 0) program.comments = this.comments;
|
|
585
|
+
return program;
|
|
586
|
+
}
|
|
587
|
+
assertEof() {
|
|
588
|
+
if (!this.isEof()) {
|
|
589
|
+
const token = this.peek();
|
|
590
|
+
const display = token ? token.type === "op" ? token.value : token.type === "redir" ? token.op : token.type === "symbol" ? token.value : token.type === "arith-cmd" ? "(( ... ))" : token.type === "heredoc-body" ? "<<heredoc>>" : token.type === "comment" ? `#${token.text}` : tokenPartsText(token.parts) : "";
|
|
591
|
+
throw new Error(`Unexpected token: ${display}`);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
parseStatement() {
|
|
595
|
+
let negated = false;
|
|
596
|
+
if (this.matchOp("!")) {
|
|
597
|
+
this.consume();
|
|
598
|
+
negated = true;
|
|
599
|
+
}
|
|
600
|
+
const command = this.parseLogical();
|
|
601
|
+
let background = false;
|
|
602
|
+
if (this.matchOp("&")) {
|
|
603
|
+
this.consume();
|
|
604
|
+
background = true;
|
|
605
|
+
}
|
|
606
|
+
const statement = {
|
|
607
|
+
type: "Statement",
|
|
608
|
+
command
|
|
609
|
+
};
|
|
610
|
+
if (background) statement.background = true;
|
|
611
|
+
if (negated) statement.negated = true;
|
|
612
|
+
return statement;
|
|
613
|
+
}
|
|
614
|
+
parseLogical() {
|
|
615
|
+
let leftCommand = this.parsePipeline();
|
|
616
|
+
while (this.matchOp("&&") || this.matchOp("||")) {
|
|
617
|
+
const opToken = this.consume();
|
|
618
|
+
if (opToken.type !== "op") throw new Error("Expected logical operator");
|
|
619
|
+
const rightCommand = this.parsePipeline();
|
|
620
|
+
leftCommand = {
|
|
621
|
+
type: "Logical",
|
|
622
|
+
op: opToken.value === "&&" ? "and" : "or",
|
|
623
|
+
left: {
|
|
624
|
+
type: "Statement",
|
|
625
|
+
command: leftCommand
|
|
626
|
+
},
|
|
627
|
+
right: {
|
|
628
|
+
type: "Statement",
|
|
629
|
+
command: rightCommand
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
return leftCommand;
|
|
634
|
+
}
|
|
635
|
+
parsePipeline() {
|
|
636
|
+
const first = this.parseCommandAtom();
|
|
637
|
+
if (!this.matchOp("|")) return first;
|
|
638
|
+
const commands = [{
|
|
639
|
+
type: "Statement",
|
|
640
|
+
command: first
|
|
641
|
+
}];
|
|
642
|
+
while (this.matchOp("|")) {
|
|
643
|
+
this.consume();
|
|
644
|
+
const next = this.parseCommandAtom();
|
|
645
|
+
commands.push({
|
|
646
|
+
type: "Statement",
|
|
647
|
+
command: next
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
return {
|
|
651
|
+
type: "Pipeline",
|
|
652
|
+
commands
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
parseCommandAtom() {
|
|
656
|
+
if (this.matchKeyword("if")) return this.parseIfClause();
|
|
657
|
+
if (this.matchKeyword("while")) return this.parseWhileClause(false);
|
|
658
|
+
if (this.matchKeyword("until")) return this.parseWhileClause(true);
|
|
659
|
+
if (this.matchKeyword("for")) return this.parseForOrCStyleLoop();
|
|
660
|
+
if (this.matchKeyword("select")) return this.parseSelectClause();
|
|
661
|
+
if (this.matchKeyword("case")) return this.parseCaseClause();
|
|
662
|
+
if (this.matchKeyword("time")) return this.parseTimeClause();
|
|
663
|
+
if (this.matchKeyword("coproc")) return this.parseCoprocClause();
|
|
664
|
+
if (this.matchKeyword("[[")) return this.parseTestClause();
|
|
665
|
+
if (this.matchKeyword("function") || this.looksLikeFuncDecl()) return this.parseFunctionDecl();
|
|
666
|
+
if (this.matchArithCmd()) return this.consumeArithCmd();
|
|
667
|
+
if (this.matchSymbol("(")) return this.parseSubshell();
|
|
668
|
+
if (this.matchSymbol("{")) return this.parseBlock();
|
|
669
|
+
if (this.matchDeclKeyword()) return this.parseDeclClause();
|
|
670
|
+
if (this.matchKeyword("let")) return this.parseLetClause();
|
|
671
|
+
return this.parseSimpleCommand();
|
|
672
|
+
}
|
|
673
|
+
parseSubshell() {
|
|
674
|
+
this.consumeSymbol("(");
|
|
675
|
+
const body = this.parseStatementList(")");
|
|
676
|
+
this.consumeSymbol(")");
|
|
677
|
+
return {
|
|
678
|
+
type: "Subshell",
|
|
679
|
+
body
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
parseBlock() {
|
|
683
|
+
this.consumeSymbol("{");
|
|
684
|
+
const body = this.parseStatementList("}");
|
|
685
|
+
this.consumeSymbol("}");
|
|
686
|
+
return {
|
|
687
|
+
type: "Block",
|
|
688
|
+
body
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
parseStatementList(endSymbol) {
|
|
692
|
+
const body = [];
|
|
693
|
+
this.skipSeparators();
|
|
694
|
+
while (!this.matchSymbol(endSymbol)) {
|
|
695
|
+
if (this.isEof()) throw new Error(`Unexpected end of input while looking for ${endSymbol}`);
|
|
696
|
+
body.push(this.parseStatement());
|
|
697
|
+
this.skipSeparators();
|
|
698
|
+
}
|
|
699
|
+
return body;
|
|
700
|
+
}
|
|
701
|
+
parseIfClause() {
|
|
702
|
+
this.consumeKeyword("if");
|
|
703
|
+
const cond = this.parseStatementsUntilKeyword(["then"]);
|
|
704
|
+
this.consumeKeyword("then");
|
|
705
|
+
const thenBranch = this.parseStatementsUntilKeyword([
|
|
706
|
+
"else",
|
|
707
|
+
"elif",
|
|
708
|
+
"fi"
|
|
709
|
+
]);
|
|
710
|
+
let elseBranch;
|
|
711
|
+
if (this.matchKeyword("elif")) elseBranch = [{
|
|
712
|
+
type: "Statement",
|
|
713
|
+
command: this.parseElifClause()
|
|
714
|
+
}];
|
|
715
|
+
else if (this.matchKeyword("else")) {
|
|
716
|
+
this.consumeKeyword("else");
|
|
717
|
+
elseBranch = this.parseStatementsUntilKeyword(["fi"]);
|
|
718
|
+
}
|
|
719
|
+
this.consumeKeyword("fi");
|
|
720
|
+
return elseBranch ? {
|
|
721
|
+
type: "IfClause",
|
|
722
|
+
cond,
|
|
723
|
+
then: thenBranch,
|
|
724
|
+
else: elseBranch
|
|
725
|
+
} : {
|
|
726
|
+
type: "IfClause",
|
|
727
|
+
cond,
|
|
728
|
+
then: thenBranch
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
parseElifClause() {
|
|
732
|
+
this.consumeKeyword("elif");
|
|
733
|
+
const cond = this.parseStatementsUntilKeyword(["then"]);
|
|
734
|
+
this.consumeKeyword("then");
|
|
735
|
+
const thenBranch = this.parseStatementsUntilKeyword([
|
|
736
|
+
"else",
|
|
737
|
+
"elif",
|
|
738
|
+
"fi"
|
|
739
|
+
]);
|
|
740
|
+
let elseBranch;
|
|
741
|
+
if (this.matchKeyword("elif")) elseBranch = [{
|
|
742
|
+
type: "Statement",
|
|
743
|
+
command: this.parseElifClause()
|
|
744
|
+
}];
|
|
745
|
+
else if (this.matchKeyword("else")) {
|
|
746
|
+
this.consumeKeyword("else");
|
|
747
|
+
elseBranch = this.parseStatementsUntilKeyword(["fi"]);
|
|
748
|
+
}
|
|
749
|
+
return elseBranch ? {
|
|
750
|
+
type: "IfClause",
|
|
751
|
+
cond,
|
|
752
|
+
then: thenBranch,
|
|
753
|
+
else: elseBranch
|
|
754
|
+
} : {
|
|
755
|
+
type: "IfClause",
|
|
756
|
+
cond,
|
|
757
|
+
then: thenBranch
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
parseWhileClause(until) {
|
|
761
|
+
this.consumeKeyword(until ? "until" : "while");
|
|
762
|
+
const cond = this.parseStatementsUntilKeyword(["do"]);
|
|
763
|
+
this.consumeKeyword("do");
|
|
764
|
+
const body = this.parseStatementsUntilKeyword(["done"]);
|
|
765
|
+
this.consumeKeyword("done");
|
|
766
|
+
return until ? {
|
|
767
|
+
type: "WhileClause",
|
|
768
|
+
cond,
|
|
769
|
+
body,
|
|
770
|
+
until: true
|
|
771
|
+
} : {
|
|
772
|
+
type: "WhileClause",
|
|
773
|
+
cond,
|
|
774
|
+
body
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
parseForOrCStyleLoop() {
|
|
778
|
+
this.consumeKeyword("for");
|
|
779
|
+
if (this.matchArithCmd()) return this.parseCStyleLoop();
|
|
780
|
+
const nameToken = this.consume();
|
|
781
|
+
if (nameToken.type !== "word") throw new Error("Expected loop variable name");
|
|
782
|
+
const name = tokenPartsText(nameToken.parts);
|
|
783
|
+
let items;
|
|
784
|
+
if (this.matchKeyword("in")) {
|
|
785
|
+
this.consumeKeyword("in");
|
|
786
|
+
const collected = [];
|
|
787
|
+
while (this.matchWord() && !this.matchKeyword("do")) {
|
|
788
|
+
const itemToken = this.consume();
|
|
789
|
+
if (itemToken.type !== "word") throw new Error("Expected loop item word");
|
|
790
|
+
collected.push(this.wordFromParts(itemToken.parts));
|
|
791
|
+
}
|
|
792
|
+
if (collected.length > 0) items = collected;
|
|
793
|
+
}
|
|
794
|
+
if (this.matchOp(";")) this.consume();
|
|
795
|
+
this.skipSeparators();
|
|
796
|
+
this.consumeKeyword("do");
|
|
797
|
+
const body = this.parseStatementsUntilKeyword(["done"]);
|
|
798
|
+
this.consumeKeyword("done");
|
|
799
|
+
return items ? {
|
|
800
|
+
type: "ForClause",
|
|
801
|
+
name,
|
|
802
|
+
items,
|
|
803
|
+
body
|
|
804
|
+
} : {
|
|
805
|
+
type: "ForClause",
|
|
806
|
+
name,
|
|
807
|
+
body
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
parseCStyleLoop() {
|
|
811
|
+
const token = this.consume();
|
|
812
|
+
if (token.type !== "arith-cmd") throw new Error("Expected (( )) in c-style for");
|
|
813
|
+
const parts = token.expr.split(";").map((s) => s.trim());
|
|
814
|
+
const init = parts[0] || void 0;
|
|
815
|
+
const cond = parts[1] || void 0;
|
|
816
|
+
const post = parts[2] || void 0;
|
|
817
|
+
if (this.matchOp(";")) this.consume();
|
|
818
|
+
this.skipSeparators();
|
|
819
|
+
this.consumeKeyword("do");
|
|
820
|
+
const body = this.parseStatementsUntilKeyword(["done"]);
|
|
821
|
+
this.consumeKeyword("done");
|
|
822
|
+
const loop = {
|
|
823
|
+
type: "CStyleLoop",
|
|
824
|
+
body
|
|
825
|
+
};
|
|
826
|
+
if (init !== void 0) loop.init = init;
|
|
827
|
+
if (cond !== void 0) loop.cond = cond;
|
|
828
|
+
if (post !== void 0) loop.post = post;
|
|
829
|
+
return loop;
|
|
830
|
+
}
|
|
831
|
+
parseSelectClause() {
|
|
832
|
+
this.consumeKeyword("select");
|
|
833
|
+
const nameToken = this.consume();
|
|
834
|
+
if (nameToken.type !== "word") throw new Error("Expected select variable name");
|
|
835
|
+
const name = tokenPartsText(nameToken.parts);
|
|
836
|
+
let items;
|
|
837
|
+
if (this.matchKeyword("in")) {
|
|
838
|
+
this.consumeKeyword("in");
|
|
839
|
+
const collected = [];
|
|
840
|
+
while (this.matchWord() && !this.matchKeyword("do")) {
|
|
841
|
+
const itemToken = this.consume();
|
|
842
|
+
if (itemToken.type !== "word") throw new Error("Expected select item word");
|
|
843
|
+
collected.push(this.wordFromParts(itemToken.parts));
|
|
844
|
+
}
|
|
845
|
+
if (collected.length > 0) items = collected;
|
|
846
|
+
}
|
|
847
|
+
if (this.matchOp(";")) this.consume();
|
|
848
|
+
this.skipSeparators();
|
|
849
|
+
this.consumeKeyword("do");
|
|
850
|
+
const body = this.parseStatementsUntilKeyword(["done"]);
|
|
851
|
+
this.consumeKeyword("done");
|
|
852
|
+
return items ? {
|
|
853
|
+
type: "SelectClause",
|
|
854
|
+
name,
|
|
855
|
+
items,
|
|
856
|
+
body
|
|
857
|
+
} : {
|
|
858
|
+
type: "SelectClause",
|
|
859
|
+
name,
|
|
860
|
+
body
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
parseFunctionDecl() {
|
|
864
|
+
if (this.matchKeyword("function")) this.consumeKeyword("function");
|
|
865
|
+
const nameToken = this.consume();
|
|
866
|
+
if (nameToken.type !== "word") throw new Error("Expected function name");
|
|
867
|
+
const name = tokenPartsText(nameToken.parts);
|
|
868
|
+
if (this.matchSymbol("(")) {
|
|
869
|
+
this.consumeSymbol("(");
|
|
870
|
+
this.consumeSymbol(")");
|
|
871
|
+
}
|
|
872
|
+
if (this.matchSymbol("{")) return {
|
|
873
|
+
type: "FunctionDecl",
|
|
874
|
+
name,
|
|
875
|
+
body: this.parseBlock().body
|
|
876
|
+
};
|
|
877
|
+
throw new Error("Expected function body block");
|
|
878
|
+
}
|
|
879
|
+
parseCaseClause() {
|
|
880
|
+
this.consumeKeyword("case");
|
|
881
|
+
const wordToken = this.consume();
|
|
882
|
+
if (wordToken.type !== "word") throw new Error("Expected case word");
|
|
883
|
+
const word = this.wordFromParts(wordToken.parts);
|
|
884
|
+
this.consumeKeyword("in");
|
|
885
|
+
const items = [];
|
|
886
|
+
this.skipSeparators();
|
|
887
|
+
while (!this.matchKeyword("esac")) {
|
|
888
|
+
const patterns = [];
|
|
889
|
+
while (!this.matchSymbol(")")) {
|
|
890
|
+
if (this.matchWord()) {
|
|
891
|
+
const patternToken = this.consume();
|
|
892
|
+
if (patternToken.type !== "word") throw new Error("Expected case pattern");
|
|
893
|
+
patterns.push(this.wordFromParts(patternToken.parts));
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
if (this.matchOp("|")) {
|
|
897
|
+
this.consume();
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
throw new Error("Expected case pattern or )");
|
|
901
|
+
}
|
|
902
|
+
this.consumeSymbol(")");
|
|
903
|
+
const body = this.parseCaseItemBody();
|
|
904
|
+
items.push({
|
|
905
|
+
type: "CaseItem",
|
|
906
|
+
patterns,
|
|
907
|
+
body
|
|
908
|
+
});
|
|
909
|
+
if (this.matchOp(";") && this.peekOp(";")) {
|
|
910
|
+
this.consume();
|
|
911
|
+
this.consume();
|
|
912
|
+
}
|
|
913
|
+
this.skipSeparators();
|
|
914
|
+
}
|
|
915
|
+
this.consumeKeyword("esac");
|
|
916
|
+
return {
|
|
917
|
+
type: "CaseClause",
|
|
918
|
+
word,
|
|
919
|
+
items
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
parseTimeClause() {
|
|
923
|
+
this.consumeKeyword("time");
|
|
924
|
+
return {
|
|
925
|
+
type: "TimeClause",
|
|
926
|
+
command: this.parseStatement()
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
parseTestClause() {
|
|
930
|
+
this.consumeKeyword("[[");
|
|
931
|
+
const words = [];
|
|
932
|
+
while (!this.matchKeyword("]]")) {
|
|
933
|
+
if (this.isEof()) throw new Error("Unclosed [[");
|
|
934
|
+
const token = this.consume();
|
|
935
|
+
if (token.type !== "word") throw new Error("Expected word in [[ ]]");
|
|
936
|
+
words.push(this.wordFromParts(token.parts));
|
|
937
|
+
}
|
|
938
|
+
this.consumeKeyword("]]");
|
|
939
|
+
return {
|
|
940
|
+
type: "TestClause",
|
|
941
|
+
expr: words
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
matchArithCmd() {
|
|
945
|
+
return this.peek()?.type === "arith-cmd";
|
|
946
|
+
}
|
|
947
|
+
consumeArithCmd() {
|
|
948
|
+
const token = this.consume();
|
|
949
|
+
if (token.type !== "arith-cmd") throw new Error("Expected arithmetic command");
|
|
950
|
+
return {
|
|
951
|
+
type: "ArithCmd",
|
|
952
|
+
expr: token.expr
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
parseCoprocClause() {
|
|
956
|
+
this.consumeKeyword("coproc");
|
|
957
|
+
if (this.matchWord() && this.peekToken(1)?.type === "symbol") {
|
|
958
|
+
const nameToken = this.peek();
|
|
959
|
+
if (nameToken?.type === "word" && this.peekToken(1)?.type === "symbol" && this.peekToken(1).value === "{") {
|
|
960
|
+
const name = tokenPartsText(nameToken.parts);
|
|
961
|
+
this.consume();
|
|
962
|
+
return {
|
|
963
|
+
type: "CoprocClause",
|
|
964
|
+
name,
|
|
965
|
+
body: this.parseStatement()
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
return {
|
|
970
|
+
type: "CoprocClause",
|
|
971
|
+
body: this.parseStatement()
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
parseCaseItemBody() {
|
|
975
|
+
const body = [];
|
|
976
|
+
this.skipCaseSeparators();
|
|
977
|
+
while (!this.matchKeyword("esac") && !this.isCaseItemEnd()) {
|
|
978
|
+
body.push(this.parseStatement());
|
|
979
|
+
if (this.isCaseItemEnd()) break;
|
|
980
|
+
this.skipCaseSeparators();
|
|
981
|
+
}
|
|
982
|
+
return body;
|
|
983
|
+
}
|
|
984
|
+
isCaseItemEnd() {
|
|
985
|
+
return this.matchOp(";") && this.peekOp(";");
|
|
986
|
+
}
|
|
987
|
+
parseStatementsUntilKeyword(endKeywords) {
|
|
988
|
+
const body = [];
|
|
989
|
+
this.skipSeparators();
|
|
990
|
+
while (!this.matchKeywordIn(endKeywords)) {
|
|
991
|
+
if (this.isEof()) throw new Error(`Unexpected end of input while looking for ${endKeywords.join(", ")}`);
|
|
992
|
+
body.push(this.parseStatement());
|
|
993
|
+
this.skipSeparators();
|
|
994
|
+
}
|
|
995
|
+
return body;
|
|
996
|
+
}
|
|
997
|
+
matchDeclKeyword() {
|
|
998
|
+
const token = this.peek();
|
|
999
|
+
if (token?.type !== "word" || token.parts.length !== 1) return false;
|
|
1000
|
+
const part = token.parts[0];
|
|
1001
|
+
return part?.type === "lit" && DECL_KEYWORDS.has(part.value);
|
|
1002
|
+
}
|
|
1003
|
+
parseDeclClause() {
|
|
1004
|
+
const variantToken = this.consume();
|
|
1005
|
+
if (variantToken.type !== "word") throw new Error("Expected decl keyword");
|
|
1006
|
+
const variant = tokenPartsText(variantToken.parts);
|
|
1007
|
+
const args = [];
|
|
1008
|
+
const assigns = [];
|
|
1009
|
+
const redirects = [];
|
|
1010
|
+
while (true) {
|
|
1011
|
+
if (this.matchRedir()) {
|
|
1012
|
+
const token = this.consume();
|
|
1013
|
+
if (token.type !== "redir") throw new Error("Expected redirect token");
|
|
1014
|
+
const targetToken = this.consume();
|
|
1015
|
+
if (targetToken.type !== "word") throw new Error("Redirect must be followed by a word");
|
|
1016
|
+
const target = this.wordFromParts(targetToken.parts);
|
|
1017
|
+
const redir = token.fd ? {
|
|
1018
|
+
type: "Redirect",
|
|
1019
|
+
op: token.op,
|
|
1020
|
+
fd: token.fd,
|
|
1021
|
+
target
|
|
1022
|
+
} : {
|
|
1023
|
+
type: "Redirect",
|
|
1024
|
+
op: token.op,
|
|
1025
|
+
target
|
|
1026
|
+
};
|
|
1027
|
+
redirects.push(redir);
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
if (this.matchWord()) {
|
|
1031
|
+
const token = this.peek();
|
|
1032
|
+
if (!token || token.type !== "word") break;
|
|
1033
|
+
const assignment = this.tryParseAssignment(token.parts);
|
|
1034
|
+
if (assignment) {
|
|
1035
|
+
assigns.push(assignment);
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
this.consume();
|
|
1039
|
+
args.push(this.wordFromParts(token.parts));
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
break;
|
|
1043
|
+
}
|
|
1044
|
+
const decl = {
|
|
1045
|
+
type: "DeclClause",
|
|
1046
|
+
variant
|
|
1047
|
+
};
|
|
1048
|
+
if (args.length > 0) decl.args = args;
|
|
1049
|
+
if (assigns.length > 0) decl.assigns = assigns;
|
|
1050
|
+
if (redirects.length > 0) decl.redirects = redirects;
|
|
1051
|
+
return decl;
|
|
1052
|
+
}
|
|
1053
|
+
parseLetClause() {
|
|
1054
|
+
this.consumeKeyword("let");
|
|
1055
|
+
const exprs = [];
|
|
1056
|
+
const redirects = [];
|
|
1057
|
+
while (true) {
|
|
1058
|
+
if (this.matchRedir()) {
|
|
1059
|
+
const token = this.consume();
|
|
1060
|
+
if (token.type !== "redir") throw new Error("Expected redirect token");
|
|
1061
|
+
const targetToken = this.consume();
|
|
1062
|
+
if (targetToken.type !== "word") throw new Error("Redirect must be followed by a word");
|
|
1063
|
+
const target = this.wordFromParts(targetToken.parts);
|
|
1064
|
+
const redir = token.fd ? {
|
|
1065
|
+
type: "Redirect",
|
|
1066
|
+
op: token.op,
|
|
1067
|
+
fd: token.fd,
|
|
1068
|
+
target
|
|
1069
|
+
} : {
|
|
1070
|
+
type: "Redirect",
|
|
1071
|
+
op: token.op,
|
|
1072
|
+
target
|
|
1073
|
+
};
|
|
1074
|
+
redirects.push(redir);
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1077
|
+
if (this.matchWord()) {
|
|
1078
|
+
const token = this.consume();
|
|
1079
|
+
if (token.type !== "word") break;
|
|
1080
|
+
exprs.push(this.wordFromParts(token.parts));
|
|
1081
|
+
continue;
|
|
1082
|
+
}
|
|
1083
|
+
break;
|
|
1084
|
+
}
|
|
1085
|
+
if (exprs.length === 0) throw new Error("let requires at least one expression");
|
|
1086
|
+
const clause = {
|
|
1087
|
+
type: "LetClause",
|
|
1088
|
+
exprs
|
|
1089
|
+
};
|
|
1090
|
+
if (redirects.length > 0) clause.redirects = redirects;
|
|
1091
|
+
return clause;
|
|
1092
|
+
}
|
|
1093
|
+
parseSimpleCommand() {
|
|
1094
|
+
const words = [];
|
|
1095
|
+
const assignments = [];
|
|
1096
|
+
const redirects = [];
|
|
1097
|
+
let sawWord = false;
|
|
1098
|
+
while (true) {
|
|
1099
|
+
if (this.matchWord()) {
|
|
1100
|
+
const token = this.peek();
|
|
1101
|
+
if (!token || token.type !== "word") throw new Error("Expected word token");
|
|
1102
|
+
if (!sawWord) {
|
|
1103
|
+
const assignment = this.tryParseAssignment(token.parts);
|
|
1104
|
+
if (assignment) {
|
|
1105
|
+
assignments.push(assignment);
|
|
1106
|
+
continue;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
this.consume();
|
|
1110
|
+
sawWord = true;
|
|
1111
|
+
words.push(this.wordFromParts(token.parts));
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
1114
|
+
if (this.matchRedir()) {
|
|
1115
|
+
const token = this.consume();
|
|
1116
|
+
if (token.type !== "redir") throw new Error("Expected redirect token");
|
|
1117
|
+
const targetToken = this.consume();
|
|
1118
|
+
if (targetToken.type !== "word") throw new Error("Redirect must be followed by a word");
|
|
1119
|
+
const target = this.wordFromParts(targetToken.parts);
|
|
1120
|
+
const redirect = token.fd ? {
|
|
1121
|
+
type: "Redirect",
|
|
1122
|
+
op: token.op,
|
|
1123
|
+
fd: token.fd,
|
|
1124
|
+
target
|
|
1125
|
+
} : {
|
|
1126
|
+
type: "Redirect",
|
|
1127
|
+
op: token.op,
|
|
1128
|
+
target
|
|
1129
|
+
};
|
|
1130
|
+
if (token.op === "<<" || token.op === "<<-") {
|
|
1131
|
+
this.skipSeparators();
|
|
1132
|
+
if (this.peek()?.type === "heredoc-body") {
|
|
1133
|
+
const bodyToken = this.consume();
|
|
1134
|
+
if (bodyToken.type === "heredoc-body") redirect.heredoc = {
|
|
1135
|
+
type: "Word",
|
|
1136
|
+
parts: [{
|
|
1137
|
+
type: "Literal",
|
|
1138
|
+
value: bodyToken.content
|
|
1139
|
+
}]
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
redirects.push(redirect);
|
|
1144
|
+
continue;
|
|
1145
|
+
}
|
|
1146
|
+
break;
|
|
1147
|
+
}
|
|
1148
|
+
if (words.length === 0 && assignments.length === 0 && redirects.length === 0) throw new Error("Expected a command word");
|
|
1149
|
+
const command = { type: "SimpleCommand" };
|
|
1150
|
+
if (words.length > 0) command.words = words;
|
|
1151
|
+
if (assignments.length > 0) command.assignments = assignments;
|
|
1152
|
+
if (redirects.length > 0) command.redirects = redirects;
|
|
1153
|
+
return command;
|
|
1154
|
+
}
|
|
1155
|
+
convertWordPart(part) {
|
|
1156
|
+
switch (part.type) {
|
|
1157
|
+
case "lit": return {
|
|
1158
|
+
type: "Literal",
|
|
1159
|
+
value: part.value
|
|
1160
|
+
};
|
|
1161
|
+
case "sgl": return {
|
|
1162
|
+
type: "SglQuoted",
|
|
1163
|
+
value: part.value
|
|
1164
|
+
};
|
|
1165
|
+
case "dbl": return {
|
|
1166
|
+
type: "DblQuoted",
|
|
1167
|
+
parts: part.parts.map((p) => this.convertWordPart(p))
|
|
1168
|
+
};
|
|
1169
|
+
case "param": {
|
|
1170
|
+
const paramExp = {
|
|
1171
|
+
type: "ParamExp",
|
|
1172
|
+
short: !part.braced,
|
|
1173
|
+
param: {
|
|
1174
|
+
type: "Literal",
|
|
1175
|
+
value: part.name
|
|
1176
|
+
}
|
|
1177
|
+
};
|
|
1178
|
+
if (part.op) paramExp.op = part.op;
|
|
1179
|
+
if (part.value !== void 0) paramExp.value = {
|
|
1180
|
+
type: "Word",
|
|
1181
|
+
parts: [{
|
|
1182
|
+
type: "Literal",
|
|
1183
|
+
value: part.value
|
|
1184
|
+
}]
|
|
1185
|
+
};
|
|
1186
|
+
return paramExp;
|
|
1187
|
+
}
|
|
1188
|
+
case "cmd-subst": return {
|
|
1189
|
+
type: "CmdSubst",
|
|
1190
|
+
stmts: new Parser(tokenize(part.raw)).parseProgram().body
|
|
1191
|
+
};
|
|
1192
|
+
case "arith-exp": return {
|
|
1193
|
+
type: "ArithExp",
|
|
1194
|
+
expr: part.raw
|
|
1195
|
+
};
|
|
1196
|
+
case "proc-subst": {
|
|
1197
|
+
const prog = new Parser(tokenize(part.raw)).parseProgram();
|
|
1198
|
+
return {
|
|
1199
|
+
type: "ProcSubst",
|
|
1200
|
+
op: part.op,
|
|
1201
|
+
stmts: prog.body
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
case "backtick": return {
|
|
1205
|
+
type: "CmdSubst",
|
|
1206
|
+
stmts: new Parser(tokenize(part.raw)).parseProgram().body
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
wordFromParts(parts) {
|
|
1211
|
+
return {
|
|
1212
|
+
type: "Word",
|
|
1213
|
+
parts: parts.map((part) => this.convertWordPart(part))
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* Try to parse an assignment from the current token's parts.
|
|
1218
|
+
* If it returns an assignment, it has already consumed all relevant tokens
|
|
1219
|
+
* (the word, and optionally the array `(...)` symbols).
|
|
1220
|
+
* If it returns undefined, nothing was consumed.
|
|
1221
|
+
*/
|
|
1222
|
+
tryParseAssignment(parts) {
|
|
1223
|
+
if (parts.length !== 1) return void 0;
|
|
1224
|
+
const part = parts[0];
|
|
1225
|
+
if (!part || part.type !== "lit") return void 0;
|
|
1226
|
+
const raw = part.value;
|
|
1227
|
+
let append = false;
|
|
1228
|
+
let eqIndex = raw.indexOf("+=");
|
|
1229
|
+
if (eqIndex > 0) append = true;
|
|
1230
|
+
else eqIndex = raw.indexOf("=");
|
|
1231
|
+
if (eqIndex <= 0) return void 0;
|
|
1232
|
+
const name = raw.slice(0, eqIndex);
|
|
1233
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) return void 0;
|
|
1234
|
+
const afterEq = raw.slice(eqIndex + (append ? 2 : 1));
|
|
1235
|
+
const nextToken = this.peekToken(1);
|
|
1236
|
+
if (afterEq === "" && nextToken?.type === "symbol" && nextToken.value === "(") {
|
|
1237
|
+
this.consume();
|
|
1238
|
+
return this.parseArrayAssignment(name, append);
|
|
1239
|
+
}
|
|
1240
|
+
this.consume();
|
|
1241
|
+
const assignment = {
|
|
1242
|
+
type: "Assignment",
|
|
1243
|
+
name
|
|
1244
|
+
};
|
|
1245
|
+
if (append) assignment.append = true;
|
|
1246
|
+
if (afterEq.length > 0) assignment.value = {
|
|
1247
|
+
type: "Word",
|
|
1248
|
+
parts: [{
|
|
1249
|
+
type: "Literal",
|
|
1250
|
+
value: afterEq
|
|
1251
|
+
}]
|
|
1252
|
+
};
|
|
1253
|
+
return assignment;
|
|
1254
|
+
}
|
|
1255
|
+
parseArrayAssignment(name, append) {
|
|
1256
|
+
this.consumeSymbol("(");
|
|
1257
|
+
const elems = [];
|
|
1258
|
+
while (!this.matchSymbol(")")) {
|
|
1259
|
+
if (this.isEof()) throw new Error("Unclosed array expression");
|
|
1260
|
+
if (this.matchOp(";")) {
|
|
1261
|
+
this.consume();
|
|
1262
|
+
continue;
|
|
1263
|
+
}
|
|
1264
|
+
if (this.matchComment()) {
|
|
1265
|
+
this.consumeComment();
|
|
1266
|
+
continue;
|
|
1267
|
+
}
|
|
1268
|
+
const token = this.consume();
|
|
1269
|
+
if (token.type !== "word") throw new Error("Expected word in array expression");
|
|
1270
|
+
const indexMatch = tokenPartsText(token.parts).match(/^\[([^\]]+)\]=(.*)$/);
|
|
1271
|
+
if (indexMatch) {
|
|
1272
|
+
const indexStr = indexMatch[1];
|
|
1273
|
+
const valStr = indexMatch[2];
|
|
1274
|
+
const elem = {
|
|
1275
|
+
type: "ArrayElem",
|
|
1276
|
+
index: {
|
|
1277
|
+
type: "Word",
|
|
1278
|
+
parts: [{
|
|
1279
|
+
type: "Literal",
|
|
1280
|
+
value: indexStr
|
|
1281
|
+
}]
|
|
1282
|
+
}
|
|
1283
|
+
};
|
|
1284
|
+
if (valStr.length > 0) elem.value = {
|
|
1285
|
+
type: "Word",
|
|
1286
|
+
parts: [{
|
|
1287
|
+
type: "Literal",
|
|
1288
|
+
value: valStr
|
|
1289
|
+
}]
|
|
1290
|
+
};
|
|
1291
|
+
elems.push(elem);
|
|
1292
|
+
} else elems.push({
|
|
1293
|
+
type: "ArrayElem",
|
|
1294
|
+
value: this.wordFromParts(token.parts)
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
this.consumeSymbol(")");
|
|
1298
|
+
const assignment = {
|
|
1299
|
+
type: "Assignment",
|
|
1300
|
+
name,
|
|
1301
|
+
array: {
|
|
1302
|
+
type: "ArrayExpr",
|
|
1303
|
+
elems
|
|
1304
|
+
}
|
|
1305
|
+
};
|
|
1306
|
+
if (append) assignment.append = true;
|
|
1307
|
+
return assignment;
|
|
1308
|
+
}
|
|
1309
|
+
skipSeparators() {
|
|
1310
|
+
while (this.matchOp(";") || this.matchComment()) if (this.matchComment()) this.consumeComment();
|
|
1311
|
+
else this.consume();
|
|
1312
|
+
}
|
|
1313
|
+
skipCaseSeparators() {
|
|
1314
|
+
while (this.matchOp(";") && !this.peekOp(";")) this.consume();
|
|
1315
|
+
}
|
|
1316
|
+
matchOp(value) {
|
|
1317
|
+
const token = this.peek();
|
|
1318
|
+
return token?.type === "op" && token.value === value;
|
|
1319
|
+
}
|
|
1320
|
+
matchWord() {
|
|
1321
|
+
return this.peek()?.type === "word";
|
|
1322
|
+
}
|
|
1323
|
+
matchRedir() {
|
|
1324
|
+
return this.peek()?.type === "redir";
|
|
1325
|
+
}
|
|
1326
|
+
matchKeyword(value) {
|
|
1327
|
+
const token = this.peek();
|
|
1328
|
+
if (token?.type !== "word" || token.parts.length !== 1) return false;
|
|
1329
|
+
const part = token.parts[0];
|
|
1330
|
+
return part?.type === "lit" && part.value === value;
|
|
1331
|
+
}
|
|
1332
|
+
matchKeywordIn(values) {
|
|
1333
|
+
return values.some((value) => this.matchKeyword(value));
|
|
1334
|
+
}
|
|
1335
|
+
looksLikeFuncDecl() {
|
|
1336
|
+
const name = this.peek();
|
|
1337
|
+
const next = this.peekToken(1);
|
|
1338
|
+
const nextNext = this.peekToken(2);
|
|
1339
|
+
const after = this.peekToken(3);
|
|
1340
|
+
return name?.type === "word" && next?.type === "symbol" && next.value === "(" && nextNext?.type === "symbol" && nextNext.value === ")" && after?.type === "symbol" && after.value === "{";
|
|
1341
|
+
}
|
|
1342
|
+
matchSymbol(value) {
|
|
1343
|
+
const token = this.peek();
|
|
1344
|
+
return token?.type === "symbol" && token.value === value;
|
|
1345
|
+
}
|
|
1346
|
+
consumeSymbol(value) {
|
|
1347
|
+
const token = this.consume();
|
|
1348
|
+
if (token.type !== "symbol" || token.value !== value) throw new Error(`Expected symbol ${value}`);
|
|
1349
|
+
}
|
|
1350
|
+
consumeKeyword(value) {
|
|
1351
|
+
const token = this.consume();
|
|
1352
|
+
if (token.type !== "word" || token.parts.length !== 1 || token.parts[0]?.type !== "lit" || token.parts[0].value !== value) throw new Error(`Expected keyword ${value}`);
|
|
1353
|
+
}
|
|
1354
|
+
consume() {
|
|
1355
|
+
if (this.isEof()) throw new Error("Unexpected end of input");
|
|
1356
|
+
const token = this.tokens[this.index];
|
|
1357
|
+
if (!token) throw new Error("Unexpected end of input");
|
|
1358
|
+
this.index += 1;
|
|
1359
|
+
return token;
|
|
1360
|
+
}
|
|
1361
|
+
peek() {
|
|
1362
|
+
return this.tokens[this.index];
|
|
1363
|
+
}
|
|
1364
|
+
peekToken(offset) {
|
|
1365
|
+
return this.tokens[this.index + offset];
|
|
1366
|
+
}
|
|
1367
|
+
peekOp(value) {
|
|
1368
|
+
const token = this.peekToken(1);
|
|
1369
|
+
return token?.type === "op" && token.value === value;
|
|
1370
|
+
}
|
|
1371
|
+
matchComment() {
|
|
1372
|
+
return this.peek()?.type === "comment";
|
|
1373
|
+
}
|
|
1374
|
+
consumeComment() {
|
|
1375
|
+
const token = this.consume();
|
|
1376
|
+
if (token.type === "comment") this.comments.push({
|
|
1377
|
+
type: "Comment",
|
|
1378
|
+
text: token.text
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
isEof() {
|
|
1382
|
+
return this.index >= this.tokens.length;
|
|
1383
|
+
}
|
|
1384
|
+
};
|
|
1385
|
+
|
|
1386
|
+
//#endregion
|
|
1387
|
+
//#region src/parse.ts
|
|
1388
|
+
function parse(source, options = {}) {
|
|
1389
|
+
const parser = new Parser(tokenize(source, options), options);
|
|
1390
|
+
const ast = parser.parseProgram();
|
|
1391
|
+
parser.assertEof();
|
|
1392
|
+
return { ast };
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
//#endregion
|
|
1396
|
+
export { parse };
|
|
1397
|
+
//# sourceMappingURL=index.js.map
|