fauxnix-cli 0.2.1 → 0.4.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/README.md +203 -199
- package/dist/ast.d.ts +9 -2
- package/dist/ast.js +12 -2
- package/dist/cli.js +13 -13
- package/dist/commands/archive.js +5 -5
- package/dist/commands/files.js +28 -39
- package/dist/commands/net.js +8 -5
- package/dist/commands/sysinfo.js +1317 -53
- package/dist/commands/text-filters.js +2 -4
- package/dist/commands/text-io.js +22 -24
- package/dist/encoding.d.ts +9 -0
- package/dist/encoding.js +19 -0
- package/dist/executor.js +230 -108
- package/dist/mcp.js +1 -1
- package/dist/parser.d.ts +2 -0
- package/dist/parser.js +425 -16
- package/dist/registry.js +10 -0
- package/dist/translator.d.ts +39 -5
- package/dist/translator.js +547 -31
- package/package.json +1 -1
package/dist/parser.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FauxnixParseError, } from './ast.js';
|
|
1
|
+
import { FauxnixParseError, isUnquotedLiteral, wordToString, } from './ast.js';
|
|
2
2
|
const OPERATORS = [
|
|
3
3
|
'&&', '||', '>>', '<<', '2>&1', '1>&2', '2>', '&>>', '&>', '>', '<', '|', ';',
|
|
4
4
|
];
|
|
@@ -6,28 +6,37 @@ export function tokenize(input) {
|
|
|
6
6
|
const tokens = [];
|
|
7
7
|
let i = 0;
|
|
8
8
|
const n = input.length;
|
|
9
|
-
const pushWord = (parts) => {
|
|
9
|
+
const pushWord = (parts, tightLeft) => {
|
|
10
10
|
if (parts.length > 0)
|
|
11
|
-
tokens.push({ type: 'WORD', parts });
|
|
11
|
+
tokens.push({ type: 'WORD', parts, tightLeft });
|
|
12
12
|
};
|
|
13
13
|
let cur = [];
|
|
14
14
|
let fdDigits = '';
|
|
15
|
+
let lastWasWs = true;
|
|
16
|
+
let wordTightLeft = false;
|
|
15
17
|
const flush = () => {
|
|
16
|
-
pushWord(cur);
|
|
18
|
+
pushWord(cur, wordTightLeft);
|
|
17
19
|
cur = [];
|
|
18
20
|
fdDigits = '';
|
|
19
21
|
};
|
|
22
|
+
const beginWordPart = () => {
|
|
23
|
+
if (cur.length === 0)
|
|
24
|
+
wordTightLeft = !lastWasWs;
|
|
25
|
+
lastWasWs = false;
|
|
26
|
+
};
|
|
20
27
|
while (i < n) {
|
|
21
28
|
const ch = input[i];
|
|
22
29
|
// whitespace separates words; newline acts as ';'
|
|
23
30
|
if (ch === ' ' || ch === '\t' || ch === '\r') {
|
|
24
31
|
flush();
|
|
32
|
+
lastWasWs = true;
|
|
25
33
|
i++;
|
|
26
34
|
continue;
|
|
27
35
|
}
|
|
28
36
|
if (ch === '\n') {
|
|
29
37
|
flush();
|
|
30
|
-
tokens.push({ type: 'OP', op: '
|
|
38
|
+
tokens.push({ type: 'OP', op: '\n', tightLeft: false });
|
|
39
|
+
lastWasWs = true;
|
|
31
40
|
i++;
|
|
32
41
|
continue;
|
|
33
42
|
}
|
|
@@ -76,8 +85,10 @@ export function tokenize(input) {
|
|
|
76
85
|
if (matched === '<<') {
|
|
77
86
|
throw new FauxnixParseError('fauxnix: heredocs (<<) are not supported yet. Pass the text via echo pipe or a temp file instead.');
|
|
78
87
|
}
|
|
88
|
+
const tightLeft = !lastWasWs;
|
|
79
89
|
flush();
|
|
80
|
-
tokens.push({ type: 'OP', op: matched });
|
|
90
|
+
tokens.push({ type: 'OP', op: matched, tightLeft });
|
|
91
|
+
lastWasWs = false;
|
|
81
92
|
i += advance;
|
|
82
93
|
continue;
|
|
83
94
|
}
|
|
@@ -86,12 +97,14 @@ export function tokenize(input) {
|
|
|
86
97
|
const end = input.indexOf("'", i + 1);
|
|
87
98
|
if (end === -1)
|
|
88
99
|
throw new FauxnixParseError('fauxnix: unclosed single quote');
|
|
100
|
+
beginWordPart();
|
|
89
101
|
cur.push({ kind: 'SingleQuoted', text: input.slice(i + 1, end) });
|
|
90
102
|
i = end + 1;
|
|
91
103
|
continue;
|
|
92
104
|
}
|
|
93
105
|
// double quotes — interpolated
|
|
94
106
|
if (ch === '"') {
|
|
107
|
+
beginWordPart();
|
|
95
108
|
i++;
|
|
96
109
|
const parts = [];
|
|
97
110
|
let buf = '';
|
|
@@ -129,10 +142,12 @@ export function tokenize(input) {
|
|
|
129
142
|
if (ch === '$') {
|
|
130
143
|
const v = readDollar(input, i);
|
|
131
144
|
if (v) {
|
|
145
|
+
beginWordPart();
|
|
132
146
|
cur.push(v.part);
|
|
133
147
|
i = v.next;
|
|
134
148
|
continue;
|
|
135
149
|
}
|
|
150
|
+
beginWordPart();
|
|
136
151
|
cur.push({ kind: 'Text', text: '$' });
|
|
137
152
|
i++;
|
|
138
153
|
continue;
|
|
@@ -140,20 +155,24 @@ export function tokenize(input) {
|
|
|
140
155
|
if (ch === '`') {
|
|
141
156
|
throw new FauxnixParseError('fauxnix: backticks are not supported. Use $(...) command substitution instead.');
|
|
142
157
|
}
|
|
143
|
-
// escape outside quotes
|
|
158
|
+
// escape outside quotes — keep the escape so [[ =~ ]] / == can
|
|
159
|
+
// treat `\*` as a literal rather than a metacharacter
|
|
144
160
|
if (ch === '\\' && i + 1 < n) {
|
|
145
|
-
|
|
161
|
+
beginWordPart();
|
|
162
|
+
cur.push({ kind: 'Text', text: input[i + 1], escaped: true });
|
|
146
163
|
i += 2;
|
|
147
164
|
continue;
|
|
148
165
|
}
|
|
149
166
|
// track leading digits (potential fd number for redirects)
|
|
150
167
|
if (/[0-9]/.test(ch) && cur.length === 0 && fdDigits.length < 2) {
|
|
168
|
+
beginWordPart();
|
|
151
169
|
fdDigits += ch;
|
|
152
170
|
cur.push({ kind: 'Text', text: ch });
|
|
153
171
|
i++;
|
|
154
172
|
continue;
|
|
155
173
|
}
|
|
156
174
|
fdDigits = '';
|
|
175
|
+
beginWordPart();
|
|
157
176
|
cur.push({ kind: 'Text', text: ch });
|
|
158
177
|
i++;
|
|
159
178
|
}
|
|
@@ -181,7 +200,11 @@ function readDollar(input, i) {
|
|
|
181
200
|
if (end === -1)
|
|
182
201
|
throw new FauxnixParseError('fauxnix: unclosed ${');
|
|
183
202
|
const name = input.slice(j + 1, end);
|
|
184
|
-
|
|
203
|
+
const sub = name.match(/^([A-Za-z_][A-Za-z0-9_]*)\[([0-9]+|@|\*)\]$/);
|
|
204
|
+
if (sub) {
|
|
205
|
+
return { part: { kind: 'Var', name: sub[1], index: sub[2] }, next: end + 1 };
|
|
206
|
+
}
|
|
207
|
+
if (!name || !isNameStart(name[0]) || !name.split('').every(isNameChar)) {
|
|
185
208
|
// ${VAR:-default} etc. — unsupported, kept as raw text
|
|
186
209
|
return { part: { kind: 'Text', text: input.slice(i, end + 1) }, next: end + 1 };
|
|
187
210
|
}
|
|
@@ -226,6 +249,228 @@ function readDollar(input, i) {
|
|
|
226
249
|
}
|
|
227
250
|
return null;
|
|
228
251
|
}
|
|
252
|
+
function hasBareAmp(w) {
|
|
253
|
+
let depth = 0;
|
|
254
|
+
for (const p of w) {
|
|
255
|
+
if (p.kind !== 'Text' || p.escaped)
|
|
256
|
+
continue;
|
|
257
|
+
for (const c of p.text) {
|
|
258
|
+
if (c === '(')
|
|
259
|
+
depth++;
|
|
260
|
+
else if (c === ')' && depth > 0)
|
|
261
|
+
depth--;
|
|
262
|
+
else if (c === '&' && depth === 0)
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
function looksLikeArithWord(w) {
|
|
269
|
+
const s = wordToString(w);
|
|
270
|
+
return (/^[0-9a-fA-FxX#+\-*/%()<>&=!~|^?,: \t]+$/.test(s) && /[+\-*/%<>&=!~|^?]/.test(s));
|
|
271
|
+
}
|
|
272
|
+
function hasBareNonExtglobParen(w) {
|
|
273
|
+
const chars = [];
|
|
274
|
+
for (const p of w) {
|
|
275
|
+
if (p.kind === 'Text' && !p.escaped) {
|
|
276
|
+
for (const c of p.text)
|
|
277
|
+
chars.push(c);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
chars.push('\0');
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
for (let i = 0; i < chars.length; i++) {
|
|
284
|
+
if (chars[i] !== '(')
|
|
285
|
+
continue;
|
|
286
|
+
const prev = i > 0 ? chars[i - 1] : '';
|
|
287
|
+
if (prev !== '@' && prev !== '*' && prev !== '?' && prev !== '+' && prev !== '!')
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
function regexHasExtraClose(w) {
|
|
293
|
+
let depth = 0;
|
|
294
|
+
for (const p of w) {
|
|
295
|
+
if (p.kind !== 'Text' || p.escaped)
|
|
296
|
+
continue;
|
|
297
|
+
for (const c of p.text) {
|
|
298
|
+
if (c === '(')
|
|
299
|
+
depth++;
|
|
300
|
+
else if (c === ')') {
|
|
301
|
+
if (depth === 0)
|
|
302
|
+
return true;
|
|
303
|
+
depth--;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
function unmatchedOpenParen(w) {
|
|
310
|
+
let depth = 0;
|
|
311
|
+
for (const p of w) {
|
|
312
|
+
if (p.kind !== 'Text' || p.escaped)
|
|
313
|
+
continue;
|
|
314
|
+
for (const c of p.text) {
|
|
315
|
+
if (c === '(')
|
|
316
|
+
depth++;
|
|
317
|
+
else if (c === ')' && depth > 0)
|
|
318
|
+
depth--;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return depth > 0;
|
|
322
|
+
}
|
|
323
|
+
/** True when `w` has an unclosed unquoted `@(…)`, `+(…)`, `*(…)`, `?(…)`, or `!(…)`. */
|
|
324
|
+
function unmatchedExtglob(w) {
|
|
325
|
+
const chars = [];
|
|
326
|
+
for (const p of w) {
|
|
327
|
+
if (p.kind === 'Text' && !p.escaped) {
|
|
328
|
+
for (const c of p.text)
|
|
329
|
+
chars.push(c);
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
chars.push('\0');
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
let depth = 0;
|
|
336
|
+
for (let i = 0; i < chars.length; i++) {
|
|
337
|
+
const c = chars[i];
|
|
338
|
+
if ((c === '@' || c === '*' || c === '?' || c === '+' || c === '!') &&
|
|
339
|
+
chars[i + 1] === '(') {
|
|
340
|
+
depth++;
|
|
341
|
+
i++;
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (c === ')' && depth > 0)
|
|
345
|
+
depth--;
|
|
346
|
+
}
|
|
347
|
+
return depth > 0;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Peel grouping `(` / `)` that bash tokenizes even without spaces,
|
|
351
|
+
* but leave extglob `@(…)` / `!(foo)bar` intact.
|
|
352
|
+
*/
|
|
353
|
+
function splitCondParens(w) {
|
|
354
|
+
const leading = [];
|
|
355
|
+
const trailing = [];
|
|
356
|
+
let rest = w.map((p) => (p.kind === 'Text' ? { ...p } : p));
|
|
357
|
+
for (;;) {
|
|
358
|
+
if (rest.length === 0)
|
|
359
|
+
break;
|
|
360
|
+
const p = rest[0];
|
|
361
|
+
if (p.kind !== 'Text' || p.escaped || !p.text.startsWith('('))
|
|
362
|
+
break;
|
|
363
|
+
leading.push([{ kind: 'Text', text: '(' }]);
|
|
364
|
+
rest =
|
|
365
|
+
p.text.length === 1
|
|
366
|
+
? rest.slice(1)
|
|
367
|
+
: [{ kind: 'Text', text: p.text.slice(1), escaped: p.escaped }, ...rest.slice(1)];
|
|
368
|
+
}
|
|
369
|
+
for (;;) {
|
|
370
|
+
if (rest.length === 0)
|
|
371
|
+
break;
|
|
372
|
+
const last = rest[rest.length - 1];
|
|
373
|
+
if (last.kind !== 'Text' || last.escaped || !last.text.endsWith(')'))
|
|
374
|
+
break;
|
|
375
|
+
const without = last.text.length === 1
|
|
376
|
+
? rest.slice(0, -1)
|
|
377
|
+
: [
|
|
378
|
+
...rest.slice(0, -1),
|
|
379
|
+
{ kind: 'Text', text: last.text.slice(0, -1), escaped: last.escaped },
|
|
380
|
+
];
|
|
381
|
+
if (unmatchedExtglob(without) || unmatchedOpenParen(without))
|
|
382
|
+
break;
|
|
383
|
+
trailing.unshift([{ kind: 'Text', text: ')' }]);
|
|
384
|
+
rest = without;
|
|
385
|
+
}
|
|
386
|
+
const mid = rest.length > 0 ? [rest] : [];
|
|
387
|
+
return [...leading, ...mid, ...trailing];
|
|
388
|
+
}
|
|
389
|
+
function groupingDepth(args) {
|
|
390
|
+
let d = 0;
|
|
391
|
+
for (const w of args) {
|
|
392
|
+
if (isUnquotedLiteral(w, '('))
|
|
393
|
+
d++;
|
|
394
|
+
else if (isUnquotedLiteral(w, ')'))
|
|
395
|
+
d--;
|
|
396
|
+
}
|
|
397
|
+
return d;
|
|
398
|
+
}
|
|
399
|
+
/** Peel trailing grouping `)` off a `=~` operand when a `(` is still open. */
|
|
400
|
+
function peelTrailingGroupCloses(w, max) {
|
|
401
|
+
if (max <= 0)
|
|
402
|
+
return w.length ? [w] : [];
|
|
403
|
+
const { rest, trailing } = (() => {
|
|
404
|
+
const trailing = [];
|
|
405
|
+
let rest = w.map((p) => (p.kind === 'Text' ? { ...p } : p));
|
|
406
|
+
while (trailing.length < max) {
|
|
407
|
+
if (rest.length === 0)
|
|
408
|
+
break;
|
|
409
|
+
const last = rest[rest.length - 1];
|
|
410
|
+
if (last.kind !== 'Text' || last.escaped || !last.text.endsWith(')'))
|
|
411
|
+
break;
|
|
412
|
+
const without = last.text.length === 1
|
|
413
|
+
? rest.slice(0, -1)
|
|
414
|
+
: [
|
|
415
|
+
...rest.slice(0, -1),
|
|
416
|
+
{ kind: 'Text', text: last.text.slice(0, -1), escaped: last.escaped },
|
|
417
|
+
];
|
|
418
|
+
if (unmatchedOpenParen(without))
|
|
419
|
+
break;
|
|
420
|
+
rest = without;
|
|
421
|
+
trailing.unshift([{ kind: 'Text', text: ')' }]);
|
|
422
|
+
}
|
|
423
|
+
return { rest, trailing };
|
|
424
|
+
})();
|
|
425
|
+
const mid = rest.length > 0 ? [rest] : [];
|
|
426
|
+
return [...mid, ...trailing];
|
|
427
|
+
}
|
|
428
|
+
function canNlInsideDblBracket(args) {
|
|
429
|
+
if (args.length === 0)
|
|
430
|
+
return true;
|
|
431
|
+
const last = args[args.length - 1];
|
|
432
|
+
if (isUnquotedLiteral(last, '&&') ||
|
|
433
|
+
isUnquotedLiteral(last, '||') ||
|
|
434
|
+
isUnquotedLiteral(last, '!') ||
|
|
435
|
+
isUnquotedLiteral(last, '('))
|
|
436
|
+
return true;
|
|
437
|
+
if (isUnquotedLiteral(last, '=~') ||
|
|
438
|
+
isUnquotedLiteral(last, '==') ||
|
|
439
|
+
isUnquotedLiteral(last, '=') ||
|
|
440
|
+
isUnquotedLiteral(last, '!=') ||
|
|
441
|
+
isUnquotedLiteral(last, '-e') ||
|
|
442
|
+
isUnquotedLiteral(last, '-a') ||
|
|
443
|
+
isUnquotedLiteral(last, '-f') ||
|
|
444
|
+
isUnquotedLiteral(last, '-d') ||
|
|
445
|
+
isUnquotedLiteral(last, '-r') ||
|
|
446
|
+
isUnquotedLiteral(last, '-w') ||
|
|
447
|
+
isUnquotedLiteral(last, '-x') ||
|
|
448
|
+
isUnquotedLiteral(last, '-s') ||
|
|
449
|
+
isUnquotedLiteral(last, '-z') ||
|
|
450
|
+
isUnquotedLiteral(last, '-n') ||
|
|
451
|
+
isUnquotedLiteral(last, '-L') ||
|
|
452
|
+
isUnquotedLiteral(last, '-h') ||
|
|
453
|
+
isUnquotedLiteral(last, '-v'))
|
|
454
|
+
return false;
|
|
455
|
+
if (args.length === 1)
|
|
456
|
+
return false;
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
/** Most recent unquoted `=~` / `==` / `=` / `!=` still open in this `[[`. */
|
|
460
|
+
function pendingPatternOp(args) {
|
|
461
|
+
for (let i = args.length - 1; i >= 0; i--) {
|
|
462
|
+
const w = args[i];
|
|
463
|
+
if (isUnquotedLiteral(w, '=~'))
|
|
464
|
+
return '=~';
|
|
465
|
+
if (isUnquotedLiteral(w, '==') ||
|
|
466
|
+
isUnquotedLiteral(w, '=') ||
|
|
467
|
+
isUnquotedLiteral(w, '!='))
|
|
468
|
+
return '==';
|
|
469
|
+
if (isUnquotedLiteral(w, '&&') || isUnquotedLiteral(w, '||'))
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
229
474
|
/* ------------------------------------------------------------------ */
|
|
230
475
|
/* Parser */
|
|
231
476
|
/* ------------------------------------------------------------------ */
|
|
@@ -237,17 +482,18 @@ export function parseCommand(input) {
|
|
|
237
482
|
const parseList = () => {
|
|
238
483
|
const segments = [];
|
|
239
484
|
let op = ';';
|
|
240
|
-
|
|
485
|
+
const isListSep = (o) => o === ';' || o === '\n';
|
|
486
|
+
while (peek().type === 'OP' && isListSep(peek().op))
|
|
241
487
|
next();
|
|
242
488
|
while (peek().type !== 'EOF') {
|
|
243
489
|
const pipeline = parsePipeline();
|
|
244
490
|
segments.push({ pipeline, op });
|
|
245
491
|
const t = peek();
|
|
246
|
-
if (t.type === 'OP' && (t.op === '&&' || t.op === '||' || t.op
|
|
247
|
-
op = t.op;
|
|
492
|
+
if (t.type === 'OP' && (t.op === '&&' || t.op === '||' || isListSep(t.op))) {
|
|
493
|
+
op = t.op === '\n' ? ';' : t.op;
|
|
248
494
|
next();
|
|
249
|
-
while (peek().type === 'OP' && peek().op
|
|
250
|
-
next();
|
|
495
|
+
while (peek().type === 'OP' && isListSep(peek().op))
|
|
496
|
+
next();
|
|
251
497
|
}
|
|
252
498
|
else if (t.type === 'EOF') {
|
|
253
499
|
break;
|
|
@@ -283,6 +529,95 @@ export function parseCommand(input) {
|
|
|
283
529
|
if (t.type === 'EOF')
|
|
284
530
|
break;
|
|
285
531
|
// possible redirect operator
|
|
532
|
+
// Inside `[[ ... ]]`, && || < > and other redirect-shaped tokens are
|
|
533
|
+
// conditional operators (or just words), not shell redirects/lists.
|
|
534
|
+
// Stop this special case at the first *unquoted* ]].
|
|
535
|
+
if (t.type === 'OP' &&
|
|
536
|
+
name !== null &&
|
|
537
|
+
isUnquotedLiteral(name, '[[') &&
|
|
538
|
+
!args.some((w) => isUnquotedLiteral(w, ']]')) &&
|
|
539
|
+
(t.op === '&&' ||
|
|
540
|
+
t.op === '||' ||
|
|
541
|
+
t.op === '|' ||
|
|
542
|
+
t.op === '>' ||
|
|
543
|
+
t.op === '<' ||
|
|
544
|
+
t.op === '>>' ||
|
|
545
|
+
t.op === '2>' ||
|
|
546
|
+
t.op === '2>>' ||
|
|
547
|
+
t.op === '&>' ||
|
|
548
|
+
t.op === '&>>' ||
|
|
549
|
+
t.op === '2>&1' ||
|
|
550
|
+
t.op === '1>&2')) {
|
|
551
|
+
next();
|
|
552
|
+
// Glue `|` onto the surrounding words so `=~ ^a|z$`,
|
|
553
|
+
// `=~ (a | b)c`, and `== @(x | y)` stay one operand. A
|
|
554
|
+
// spaced `|` outside an open regex / extglob group is a
|
|
555
|
+
// syntax error (bash).
|
|
556
|
+
const last = args.length ? args[args.length - 1] : null;
|
|
557
|
+
const lastIsEqTilde = last !== null && isUnquotedLiteral(last, '=~');
|
|
558
|
+
const prevIsEqTilde = args.length >= 2 && isUnquotedLiteral(args[args.length - 2], '=~');
|
|
559
|
+
const openRe = last !== null &&
|
|
560
|
+
pendingPatternOp(args) === '=~' &&
|
|
561
|
+
unmatchedOpenParen(last);
|
|
562
|
+
const openExt = last !== null &&
|
|
563
|
+
pendingPatternOp(args) === '==' &&
|
|
564
|
+
unmatchedExtglob(last);
|
|
565
|
+
const inRe = lastIsEqTilde || prevIsEqTilde || openRe;
|
|
566
|
+
if (t.op === '|' || ((inRe || openExt) && t.tightLeft && t.op === '||')) {
|
|
567
|
+
if (t.op === '|' &&
|
|
568
|
+
!lastIsEqTilde &&
|
|
569
|
+
!(prevIsEqTilde && t.tightLeft) &&
|
|
570
|
+
!openRe &&
|
|
571
|
+
!openExt) {
|
|
572
|
+
throw new FauxnixParseError("fauxnix: [[: syntax error near unexpected token `|'");
|
|
573
|
+
}
|
|
574
|
+
const piece = t.tightLeft || lastIsEqTilde
|
|
575
|
+
? [{ kind: 'Text', text: t.op }]
|
|
576
|
+
: [{ kind: 'Text', text: ' ' }, { kind: 'Text', text: t.op }];
|
|
577
|
+
if (lastIsEqTilde)
|
|
578
|
+
args.push(piece);
|
|
579
|
+
else
|
|
580
|
+
args[args.length - 1] = [...args[args.length - 1], ...piece];
|
|
581
|
+
const n = peek();
|
|
582
|
+
if (n.type === 'WORD' && n.tightLeft && n.parts) {
|
|
583
|
+
next();
|
|
584
|
+
args[args.length - 1] = [...args[args.length - 1], ...n.parts];
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
else if (t.op === '&&' || t.op === '||' || t.op === '>' || t.op === '<') {
|
|
588
|
+
if (openRe && last) {
|
|
589
|
+
const piece = t.tightLeft
|
|
590
|
+
? [{ kind: 'Text', text: t.op }]
|
|
591
|
+
: [{ kind: 'Text', text: ' ' }, { kind: 'Text', text: t.op }];
|
|
592
|
+
args[args.length - 1] = [...last, ...piece];
|
|
593
|
+
const n = peek();
|
|
594
|
+
if (n.type === 'WORD' && n.tightLeft && n.parts) {
|
|
595
|
+
next();
|
|
596
|
+
args[args.length - 1] = [...args[args.length - 1], ...n.parts];
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
args.push([{ kind: 'Text', text: t.op }]);
|
|
601
|
+
if (t.op === '&&' || t.op === '||') {
|
|
602
|
+
while (peek().type === 'OP' && peek().op === '\n')
|
|
603
|
+
next();
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
else {
|
|
608
|
+
throw new FauxnixParseError("fauxnix: [[: syntax error near unexpected token `" + t.op + "'");
|
|
609
|
+
}
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
if (t.type === 'OP' &&
|
|
613
|
+
t.op === '\n' &&
|
|
614
|
+
name !== null &&
|
|
615
|
+
isUnquotedLiteral(name, '[[') &&
|
|
616
|
+
!args.some((w) => isUnquotedLiteral(w, ']]')) &&
|
|
617
|
+
canNlInsideDblBracket(args)) {
|
|
618
|
+
next();
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
286
621
|
if (t.type === 'OP' && isRedirectOp(t.op)) {
|
|
287
622
|
const op = t.op;
|
|
288
623
|
next();
|
|
@@ -308,12 +643,86 @@ export function parseCommand(input) {
|
|
|
308
643
|
next();
|
|
309
644
|
}
|
|
310
645
|
else {
|
|
311
|
-
args.
|
|
646
|
+
if (isUnquotedLiteral(name, '[[') && !args.some((a) => isUnquotedLiteral(a, ']]'))) {
|
|
647
|
+
const pending = pendingPatternOp(args);
|
|
648
|
+
const last = args.length ? args[args.length - 1] : null;
|
|
649
|
+
// bash keeps `( x )` / `@(foo|bar baz)` as one =~ / extglob operand
|
|
650
|
+
if (last &&
|
|
651
|
+
!isUnquotedLiteral(word, ']]') &&
|
|
652
|
+
!isUnquotedLiteral(word, '&&') &&
|
|
653
|
+
!isUnquotedLiteral(word, '||') &&
|
|
654
|
+
((pending === '=~' && unmatchedOpenParen(last)) ||
|
|
655
|
+
(pending === '==' && unmatchedExtglob(last)))) {
|
|
656
|
+
const glued = [...last, { kind: 'Text', text: ' ' }, ...word];
|
|
657
|
+
const pieces = pending === '=~'
|
|
658
|
+
? peelTrailingGroupCloses(glued, groupingDepth(args.slice(0, -1)))
|
|
659
|
+
: splitCondParens(glued);
|
|
660
|
+
args.pop();
|
|
661
|
+
for (const sw of pieces) {
|
|
662
|
+
if (sw.length > 0)
|
|
663
|
+
args.push(sw);
|
|
664
|
+
}
|
|
665
|
+
next();
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
if (hasBareAmp(word) && !(pending === '=~' && last && unmatchedOpenParen(last))) {
|
|
669
|
+
throw new FauxnixParseError("fauxnix: [[: syntax error near unexpected token `&'");
|
|
670
|
+
}
|
|
671
|
+
if (pending === '==' && hasBareNonExtglobParen(word)) {
|
|
672
|
+
throw new FauxnixParseError("fauxnix: [[: syntax error near unexpected token `('");
|
|
673
|
+
}
|
|
674
|
+
const pieces = pending === '=~'
|
|
675
|
+
? peelTrailingGroupCloses(word, groupingDepth(args))
|
|
676
|
+
: splitCondParens(word);
|
|
677
|
+
if (pending !== '=~') {
|
|
678
|
+
for (const sw of pieces) {
|
|
679
|
+
if (!isUnquotedLiteral(sw, '(') &&
|
|
680
|
+
!isUnquotedLiteral(sw, ')') &&
|
|
681
|
+
hasBareNonExtglobParen(sw) &&
|
|
682
|
+
!looksLikeArithWord(sw)) {
|
|
683
|
+
throw new FauxnixParseError("fauxnix: [[: syntax error near unexpected token `('");
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
for (const sw of pieces) {
|
|
688
|
+
if (sw.length > 0)
|
|
689
|
+
args.push(sw);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
else {
|
|
693
|
+
args.push(word);
|
|
694
|
+
}
|
|
312
695
|
next();
|
|
313
696
|
}
|
|
314
697
|
}
|
|
315
|
-
if (!name)
|
|
698
|
+
if (!name) {
|
|
699
|
+
// assignment-only segment (`X=1; cmd`) — no command word
|
|
700
|
+
if (assignments.length > 0) {
|
|
701
|
+
return { kind: 'SimpleCommand', assignments, name: null, args: [], redirects };
|
|
702
|
+
}
|
|
316
703
|
throw new FauxnixParseError('fauxnix: expected a command');
|
|
704
|
+
}
|
|
705
|
+
if (isUnquotedLiteral(name, '[[')) {
|
|
706
|
+
const close = args.findIndex((w) => isUnquotedLiteral(w, ']]'));
|
|
707
|
+
if (close < 0)
|
|
708
|
+
throw new FauxnixParseError("fauxnix: [[: missing `]]'");
|
|
709
|
+
if (close !== args.length - 1) {
|
|
710
|
+
throw new FauxnixParseError("fauxnix: [[: syntax error near unexpected token after `]]'");
|
|
711
|
+
}
|
|
712
|
+
if (args.slice(0, close).some((w) => unmatchedExtglob(w))) {
|
|
713
|
+
throw new FauxnixParseError('fauxnix: [[: syntax error in conditional expression');
|
|
714
|
+
}
|
|
715
|
+
for (let i = 0; i < close; i++) {
|
|
716
|
+
if (!isUnquotedLiteral(args[i], '=~') || i + 1 >= close)
|
|
717
|
+
continue;
|
|
718
|
+
if (regexHasExtraClose(args[i + 1])) {
|
|
719
|
+
throw new FauxnixParseError("fauxnix: [[: syntax error near unexpected token `)'");
|
|
720
|
+
}
|
|
721
|
+
if (unmatchedOpenParen(args[i + 1])) {
|
|
722
|
+
throw new FauxnixParseError('fauxnix: [[: syntax error in conditional expression: unexpected end of file');
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
317
726
|
return { kind: 'SimpleCommand', assignments, name, args, redirects };
|
|
318
727
|
};
|
|
319
728
|
const readRedirectTarget = () => {
|
package/dist/registry.js
CHANGED
|
@@ -18,6 +18,16 @@ export function registeredNames() {
|
|
|
18
18
|
/* ------------------------------------------------------------------ */
|
|
19
19
|
/** Escape a JS string into a single-quoted PowerShell string literal. */
|
|
20
20
|
export function psStr(s) {
|
|
21
|
+
if (/[\r\n]/.test(s)) {
|
|
22
|
+
return ('"' +
|
|
23
|
+
s
|
|
24
|
+
.replace(/`/g, '``')
|
|
25
|
+
.replace(/"/g, '`"')
|
|
26
|
+
.replace(/\$/g, '`$')
|
|
27
|
+
.replace(/\r/g, '`r')
|
|
28
|
+
.replace(/\n/g, '`n') +
|
|
29
|
+
'"');
|
|
30
|
+
}
|
|
21
31
|
return "'" + s.replace(/'/g, "''") + "'";
|
|
22
32
|
}
|
|
23
33
|
/** bash-style stderr line + exit-flag, as PS statements. */
|
package/dist/translator.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { CommandList, Redirect, SimpleCommand, Word } from './ast.js';
|
|
1
|
+
import { Assignment, CommandList, Redirect, SimpleCommand, Word } from './ast.js';
|
|
2
2
|
import { PipelineCtx } from './registry.js';
|
|
3
3
|
/** Map a bash $VAR name to a PowerShell expression (usable inside $(...)). */
|
|
4
|
-
export declare function varExpr(name: string): string;
|
|
4
|
+
export declare function varExpr(name: string, index?: string): string;
|
|
5
|
+
/** Escape text destined for the inside of a PS double-quoted string. */
|
|
6
|
+
export declare function escapeDq(s: string): string;
|
|
5
7
|
/** Normalize a literal POSIX-ish path to its Windows equivalent. */
|
|
6
8
|
export declare function normalizeLiteralPath(s: string): string;
|
|
7
9
|
/**
|
|
@@ -15,7 +17,9 @@ export declare function pathExpr(s: string): string;
|
|
|
15
17
|
* Literal words become single-quoted strings; dynamic ones become
|
|
16
18
|
* double-quoted strings with $(...) interpolation.
|
|
17
19
|
*/
|
|
18
|
-
export declare function exprOfWord(w: Word
|
|
20
|
+
export declare function exprOfWord(w: Word, opts?: {
|
|
21
|
+
preserveCmdSub?: boolean;
|
|
22
|
+
}): string;
|
|
19
23
|
/** Literal text of a word when it contains no interpolation, else null. */
|
|
20
24
|
export declare function literalOfWord(w: Word): string | null;
|
|
21
25
|
/**
|
|
@@ -23,9 +27,39 @@ export declare function literalOfWord(w: Word): string | null;
|
|
|
23
27
|
* Literal paths get POSIX-ish normalization (/dev/null, /tmp, /d/...).
|
|
24
28
|
*/
|
|
25
29
|
export declare function operandExpr(w: Word): string;
|
|
26
|
-
/**
|
|
27
|
-
|
|
30
|
+
/**
|
|
31
|
+
* `${name[@]}` / `"pre${name[@]}post"` — one argv per element.
|
|
32
|
+
* Unquoted `${name[*]}` also splats (bash); quoted `"${name[*]}"` stays one join.
|
|
33
|
+
*/
|
|
34
|
+
export declare function splatSpec(w: Word): {
|
|
35
|
+
name: string;
|
|
36
|
+
prefix: string;
|
|
37
|
+
suffix: string;
|
|
38
|
+
} | null;
|
|
39
|
+
/** PS expression of a string[]: `@` words splat, others stay one element. */
|
|
40
|
+
export declare function argListExpr(words: Word[], fn?: (w: Word) => string): string;
|
|
41
|
+
/**
|
|
42
|
+
* Translate the inside of $(...).
|
|
43
|
+
* `keepNl`: quoted words and assignments keep interior newlines (bash).
|
|
44
|
+
* Unquoted command words join non-empty lines with a space (IFS
|
|
45
|
+
* word-split approximation). Handlers often emit one string object, so
|
|
46
|
+
* a bare `$(…)` interpolation would keep those newlines.
|
|
47
|
+
*/
|
|
48
|
+
export declare function translateCmdSub(cmdText: string, keepNl?: boolean): string;
|
|
28
49
|
export declare function translateSimple(cmd: SimpleCommand, position: PipelineCtx['position'], hasStdin: boolean): string;
|
|
50
|
+
/** PS expr: encode a string so SETVALS records can stay newline-delimited. */
|
|
51
|
+
export declare function encodeSetValExpr(srcExpr: string): string;
|
|
52
|
+
/**
|
|
53
|
+
* Apply env assignments (and optional unsets) only for `body`, then restore.
|
|
54
|
+
* All assignment *values* are evaluated before any name is mutated.
|
|
55
|
+
* `persistWords` are evaluated after the prefix is applied (so
|
|
56
|
+
* `export "$NAME"` sees the current env) and those names are not restored.
|
|
57
|
+
*/
|
|
58
|
+
export declare function wrapTempEnv(sets: Assignment[], body: string, extra?: {
|
|
59
|
+
unsets?: string[];
|
|
60
|
+
persistNames?: Set<string>;
|
|
61
|
+
persistWords?: Word[];
|
|
62
|
+
}): string;
|
|
29
63
|
export interface PipelineParts {
|
|
30
64
|
/** Generated function definitions (empty for single commands). */
|
|
31
65
|
defs: string;
|