fauxnix-cli 0.2.1 → 0.3.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 +7 -1
- package/dist/ast.js +10 -0
- package/dist/cli.js +13 -13
- package/dist/commands/sysinfo.js +1300 -48
- 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 +420 -15
- package/dist/registry.js +10 -0
- package/dist/translator.d.ts +16 -1
- package/dist/translator.js +271 -11
- 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
|
}
|
|
@@ -226,6 +245,228 @@ function readDollar(input, i) {
|
|
|
226
245
|
}
|
|
227
246
|
return null;
|
|
228
247
|
}
|
|
248
|
+
function hasBareAmp(w) {
|
|
249
|
+
let depth = 0;
|
|
250
|
+
for (const p of w) {
|
|
251
|
+
if (p.kind !== 'Text' || p.escaped)
|
|
252
|
+
continue;
|
|
253
|
+
for (const c of p.text) {
|
|
254
|
+
if (c === '(')
|
|
255
|
+
depth++;
|
|
256
|
+
else if (c === ')' && depth > 0)
|
|
257
|
+
depth--;
|
|
258
|
+
else if (c === '&' && depth === 0)
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
function looksLikeArithWord(w) {
|
|
265
|
+
const s = wordToString(w);
|
|
266
|
+
return (/^[0-9a-fA-FxX#+\-*/%()<>&=!~|^?,: \t]+$/.test(s) && /[+\-*/%<>&=!~|^?]/.test(s));
|
|
267
|
+
}
|
|
268
|
+
function hasBareNonExtglobParen(w) {
|
|
269
|
+
const chars = [];
|
|
270
|
+
for (const p of w) {
|
|
271
|
+
if (p.kind === 'Text' && !p.escaped) {
|
|
272
|
+
for (const c of p.text)
|
|
273
|
+
chars.push(c);
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
chars.push('\0');
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
for (let i = 0; i < chars.length; i++) {
|
|
280
|
+
if (chars[i] !== '(')
|
|
281
|
+
continue;
|
|
282
|
+
const prev = i > 0 ? chars[i - 1] : '';
|
|
283
|
+
if (prev !== '@' && prev !== '*' && prev !== '?' && prev !== '+' && prev !== '!')
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
function regexHasExtraClose(w) {
|
|
289
|
+
let depth = 0;
|
|
290
|
+
for (const p of w) {
|
|
291
|
+
if (p.kind !== 'Text' || p.escaped)
|
|
292
|
+
continue;
|
|
293
|
+
for (const c of p.text) {
|
|
294
|
+
if (c === '(')
|
|
295
|
+
depth++;
|
|
296
|
+
else if (c === ')') {
|
|
297
|
+
if (depth === 0)
|
|
298
|
+
return true;
|
|
299
|
+
depth--;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
function unmatchedOpenParen(w) {
|
|
306
|
+
let depth = 0;
|
|
307
|
+
for (const p of w) {
|
|
308
|
+
if (p.kind !== 'Text' || p.escaped)
|
|
309
|
+
continue;
|
|
310
|
+
for (const c of p.text) {
|
|
311
|
+
if (c === '(')
|
|
312
|
+
depth++;
|
|
313
|
+
else if (c === ')' && depth > 0)
|
|
314
|
+
depth--;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return depth > 0;
|
|
318
|
+
}
|
|
319
|
+
/** True when `w` has an unclosed unquoted `@(…)`, `+(…)`, `*(…)`, `?(…)`, or `!(…)`. */
|
|
320
|
+
function unmatchedExtglob(w) {
|
|
321
|
+
const chars = [];
|
|
322
|
+
for (const p of w) {
|
|
323
|
+
if (p.kind === 'Text' && !p.escaped) {
|
|
324
|
+
for (const c of p.text)
|
|
325
|
+
chars.push(c);
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
chars.push('\0');
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
let depth = 0;
|
|
332
|
+
for (let i = 0; i < chars.length; i++) {
|
|
333
|
+
const c = chars[i];
|
|
334
|
+
if ((c === '@' || c === '*' || c === '?' || c === '+' || c === '!') &&
|
|
335
|
+
chars[i + 1] === '(') {
|
|
336
|
+
depth++;
|
|
337
|
+
i++;
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (c === ')' && depth > 0)
|
|
341
|
+
depth--;
|
|
342
|
+
}
|
|
343
|
+
return depth > 0;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Peel grouping `(` / `)` that bash tokenizes even without spaces,
|
|
347
|
+
* but leave extglob `@(…)` / `!(foo)bar` intact.
|
|
348
|
+
*/
|
|
349
|
+
function splitCondParens(w) {
|
|
350
|
+
const leading = [];
|
|
351
|
+
const trailing = [];
|
|
352
|
+
let rest = w.map((p) => (p.kind === 'Text' ? { ...p } : p));
|
|
353
|
+
for (;;) {
|
|
354
|
+
if (rest.length === 0)
|
|
355
|
+
break;
|
|
356
|
+
const p = rest[0];
|
|
357
|
+
if (p.kind !== 'Text' || p.escaped || !p.text.startsWith('('))
|
|
358
|
+
break;
|
|
359
|
+
leading.push([{ kind: 'Text', text: '(' }]);
|
|
360
|
+
rest =
|
|
361
|
+
p.text.length === 1
|
|
362
|
+
? rest.slice(1)
|
|
363
|
+
: [{ kind: 'Text', text: p.text.slice(1), escaped: p.escaped }, ...rest.slice(1)];
|
|
364
|
+
}
|
|
365
|
+
for (;;) {
|
|
366
|
+
if (rest.length === 0)
|
|
367
|
+
break;
|
|
368
|
+
const last = rest[rest.length - 1];
|
|
369
|
+
if (last.kind !== 'Text' || last.escaped || !last.text.endsWith(')'))
|
|
370
|
+
break;
|
|
371
|
+
const without = last.text.length === 1
|
|
372
|
+
? rest.slice(0, -1)
|
|
373
|
+
: [
|
|
374
|
+
...rest.slice(0, -1),
|
|
375
|
+
{ kind: 'Text', text: last.text.slice(0, -1), escaped: last.escaped },
|
|
376
|
+
];
|
|
377
|
+
if (unmatchedExtglob(without) || unmatchedOpenParen(without))
|
|
378
|
+
break;
|
|
379
|
+
trailing.unshift([{ kind: 'Text', text: ')' }]);
|
|
380
|
+
rest = without;
|
|
381
|
+
}
|
|
382
|
+
const mid = rest.length > 0 ? [rest] : [];
|
|
383
|
+
return [...leading, ...mid, ...trailing];
|
|
384
|
+
}
|
|
385
|
+
function groupingDepth(args) {
|
|
386
|
+
let d = 0;
|
|
387
|
+
for (const w of args) {
|
|
388
|
+
if (isUnquotedLiteral(w, '('))
|
|
389
|
+
d++;
|
|
390
|
+
else if (isUnquotedLiteral(w, ')'))
|
|
391
|
+
d--;
|
|
392
|
+
}
|
|
393
|
+
return d;
|
|
394
|
+
}
|
|
395
|
+
/** Peel trailing grouping `)` off a `=~` operand when a `(` is still open. */
|
|
396
|
+
function peelTrailingGroupCloses(w, max) {
|
|
397
|
+
if (max <= 0)
|
|
398
|
+
return w.length ? [w] : [];
|
|
399
|
+
const { rest, trailing } = (() => {
|
|
400
|
+
const trailing = [];
|
|
401
|
+
let rest = w.map((p) => (p.kind === 'Text' ? { ...p } : p));
|
|
402
|
+
while (trailing.length < max) {
|
|
403
|
+
if (rest.length === 0)
|
|
404
|
+
break;
|
|
405
|
+
const last = rest[rest.length - 1];
|
|
406
|
+
if (last.kind !== 'Text' || last.escaped || !last.text.endsWith(')'))
|
|
407
|
+
break;
|
|
408
|
+
const without = last.text.length === 1
|
|
409
|
+
? rest.slice(0, -1)
|
|
410
|
+
: [
|
|
411
|
+
...rest.slice(0, -1),
|
|
412
|
+
{ kind: 'Text', text: last.text.slice(0, -1), escaped: last.escaped },
|
|
413
|
+
];
|
|
414
|
+
if (unmatchedOpenParen(without))
|
|
415
|
+
break;
|
|
416
|
+
rest = without;
|
|
417
|
+
trailing.unshift([{ kind: 'Text', text: ')' }]);
|
|
418
|
+
}
|
|
419
|
+
return { rest, trailing };
|
|
420
|
+
})();
|
|
421
|
+
const mid = rest.length > 0 ? [rest] : [];
|
|
422
|
+
return [...mid, ...trailing];
|
|
423
|
+
}
|
|
424
|
+
function canNlInsideDblBracket(args) {
|
|
425
|
+
if (args.length === 0)
|
|
426
|
+
return true;
|
|
427
|
+
const last = args[args.length - 1];
|
|
428
|
+
if (isUnquotedLiteral(last, '&&') ||
|
|
429
|
+
isUnquotedLiteral(last, '||') ||
|
|
430
|
+
isUnquotedLiteral(last, '!') ||
|
|
431
|
+
isUnquotedLiteral(last, '('))
|
|
432
|
+
return true;
|
|
433
|
+
if (isUnquotedLiteral(last, '=~') ||
|
|
434
|
+
isUnquotedLiteral(last, '==') ||
|
|
435
|
+
isUnquotedLiteral(last, '=') ||
|
|
436
|
+
isUnquotedLiteral(last, '!=') ||
|
|
437
|
+
isUnquotedLiteral(last, '-e') ||
|
|
438
|
+
isUnquotedLiteral(last, '-a') ||
|
|
439
|
+
isUnquotedLiteral(last, '-f') ||
|
|
440
|
+
isUnquotedLiteral(last, '-d') ||
|
|
441
|
+
isUnquotedLiteral(last, '-r') ||
|
|
442
|
+
isUnquotedLiteral(last, '-w') ||
|
|
443
|
+
isUnquotedLiteral(last, '-x') ||
|
|
444
|
+
isUnquotedLiteral(last, '-s') ||
|
|
445
|
+
isUnquotedLiteral(last, '-z') ||
|
|
446
|
+
isUnquotedLiteral(last, '-n') ||
|
|
447
|
+
isUnquotedLiteral(last, '-L') ||
|
|
448
|
+
isUnquotedLiteral(last, '-h') ||
|
|
449
|
+
isUnquotedLiteral(last, '-v'))
|
|
450
|
+
return false;
|
|
451
|
+
if (args.length === 1)
|
|
452
|
+
return false;
|
|
453
|
+
return true;
|
|
454
|
+
}
|
|
455
|
+
/** Most recent unquoted `=~` / `==` / `=` / `!=` still open in this `[[`. */
|
|
456
|
+
function pendingPatternOp(args) {
|
|
457
|
+
for (let i = args.length - 1; i >= 0; i--) {
|
|
458
|
+
const w = args[i];
|
|
459
|
+
if (isUnquotedLiteral(w, '=~'))
|
|
460
|
+
return '=~';
|
|
461
|
+
if (isUnquotedLiteral(w, '==') ||
|
|
462
|
+
isUnquotedLiteral(w, '=') ||
|
|
463
|
+
isUnquotedLiteral(w, '!='))
|
|
464
|
+
return '==';
|
|
465
|
+
if (isUnquotedLiteral(w, '&&') || isUnquotedLiteral(w, '||'))
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
229
470
|
/* ------------------------------------------------------------------ */
|
|
230
471
|
/* Parser */
|
|
231
472
|
/* ------------------------------------------------------------------ */
|
|
@@ -237,17 +478,18 @@ export function parseCommand(input) {
|
|
|
237
478
|
const parseList = () => {
|
|
238
479
|
const segments = [];
|
|
239
480
|
let op = ';';
|
|
240
|
-
|
|
481
|
+
const isListSep = (o) => o === ';' || o === '\n';
|
|
482
|
+
while (peek().type === 'OP' && isListSep(peek().op))
|
|
241
483
|
next();
|
|
242
484
|
while (peek().type !== 'EOF') {
|
|
243
485
|
const pipeline = parsePipeline();
|
|
244
486
|
segments.push({ pipeline, op });
|
|
245
487
|
const t = peek();
|
|
246
|
-
if (t.type === 'OP' && (t.op === '&&' || t.op === '||' || t.op
|
|
247
|
-
op = t.op;
|
|
488
|
+
if (t.type === 'OP' && (t.op === '&&' || t.op === '||' || isListSep(t.op))) {
|
|
489
|
+
op = t.op === '\n' ? ';' : t.op;
|
|
248
490
|
next();
|
|
249
|
-
while (peek().type === 'OP' && peek().op
|
|
250
|
-
next();
|
|
491
|
+
while (peek().type === 'OP' && isListSep(peek().op))
|
|
492
|
+
next();
|
|
251
493
|
}
|
|
252
494
|
else if (t.type === 'EOF') {
|
|
253
495
|
break;
|
|
@@ -283,6 +525,95 @@ export function parseCommand(input) {
|
|
|
283
525
|
if (t.type === 'EOF')
|
|
284
526
|
break;
|
|
285
527
|
// possible redirect operator
|
|
528
|
+
// Inside `[[ ... ]]`, && || < > and other redirect-shaped tokens are
|
|
529
|
+
// conditional operators (or just words), not shell redirects/lists.
|
|
530
|
+
// Stop this special case at the first *unquoted* ]].
|
|
531
|
+
if (t.type === 'OP' &&
|
|
532
|
+
name !== null &&
|
|
533
|
+
isUnquotedLiteral(name, '[[') &&
|
|
534
|
+
!args.some((w) => isUnquotedLiteral(w, ']]')) &&
|
|
535
|
+
(t.op === '&&' ||
|
|
536
|
+
t.op === '||' ||
|
|
537
|
+
t.op === '|' ||
|
|
538
|
+
t.op === '>' ||
|
|
539
|
+
t.op === '<' ||
|
|
540
|
+
t.op === '>>' ||
|
|
541
|
+
t.op === '2>' ||
|
|
542
|
+
t.op === '2>>' ||
|
|
543
|
+
t.op === '&>' ||
|
|
544
|
+
t.op === '&>>' ||
|
|
545
|
+
t.op === '2>&1' ||
|
|
546
|
+
t.op === '1>&2')) {
|
|
547
|
+
next();
|
|
548
|
+
// Glue `|` onto the surrounding words so `=~ ^a|z$`,
|
|
549
|
+
// `=~ (a | b)c`, and `== @(x | y)` stay one operand. A
|
|
550
|
+
// spaced `|` outside an open regex / extglob group is a
|
|
551
|
+
// syntax error (bash).
|
|
552
|
+
const last = args.length ? args[args.length - 1] : null;
|
|
553
|
+
const lastIsEqTilde = last !== null && isUnquotedLiteral(last, '=~');
|
|
554
|
+
const prevIsEqTilde = args.length >= 2 && isUnquotedLiteral(args[args.length - 2], '=~');
|
|
555
|
+
const openRe = last !== null &&
|
|
556
|
+
pendingPatternOp(args) === '=~' &&
|
|
557
|
+
unmatchedOpenParen(last);
|
|
558
|
+
const openExt = last !== null &&
|
|
559
|
+
pendingPatternOp(args) === '==' &&
|
|
560
|
+
unmatchedExtglob(last);
|
|
561
|
+
const inRe = lastIsEqTilde || prevIsEqTilde || openRe;
|
|
562
|
+
if (t.op === '|' || ((inRe || openExt) && t.tightLeft && t.op === '||')) {
|
|
563
|
+
if (t.op === '|' &&
|
|
564
|
+
!lastIsEqTilde &&
|
|
565
|
+
!(prevIsEqTilde && t.tightLeft) &&
|
|
566
|
+
!openRe &&
|
|
567
|
+
!openExt) {
|
|
568
|
+
throw new FauxnixParseError("fauxnix: [[: syntax error near unexpected token `|'");
|
|
569
|
+
}
|
|
570
|
+
const piece = t.tightLeft || lastIsEqTilde
|
|
571
|
+
? [{ kind: 'Text', text: t.op }]
|
|
572
|
+
: [{ kind: 'Text', text: ' ' }, { kind: 'Text', text: t.op }];
|
|
573
|
+
if (lastIsEqTilde)
|
|
574
|
+
args.push(piece);
|
|
575
|
+
else
|
|
576
|
+
args[args.length - 1] = [...args[args.length - 1], ...piece];
|
|
577
|
+
const n = peek();
|
|
578
|
+
if (n.type === 'WORD' && n.tightLeft && n.parts) {
|
|
579
|
+
next();
|
|
580
|
+
args[args.length - 1] = [...args[args.length - 1], ...n.parts];
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
else if (t.op === '&&' || t.op === '||' || t.op === '>' || t.op === '<') {
|
|
584
|
+
if (openRe && last) {
|
|
585
|
+
const piece = t.tightLeft
|
|
586
|
+
? [{ kind: 'Text', text: t.op }]
|
|
587
|
+
: [{ kind: 'Text', text: ' ' }, { kind: 'Text', text: t.op }];
|
|
588
|
+
args[args.length - 1] = [...last, ...piece];
|
|
589
|
+
const n = peek();
|
|
590
|
+
if (n.type === 'WORD' && n.tightLeft && n.parts) {
|
|
591
|
+
next();
|
|
592
|
+
args[args.length - 1] = [...args[args.length - 1], ...n.parts];
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
args.push([{ kind: 'Text', text: t.op }]);
|
|
597
|
+
if (t.op === '&&' || t.op === '||') {
|
|
598
|
+
while (peek().type === 'OP' && peek().op === '\n')
|
|
599
|
+
next();
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
throw new FauxnixParseError("fauxnix: [[: syntax error near unexpected token `" + t.op + "'");
|
|
605
|
+
}
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
if (t.type === 'OP' &&
|
|
609
|
+
t.op === '\n' &&
|
|
610
|
+
name !== null &&
|
|
611
|
+
isUnquotedLiteral(name, '[[') &&
|
|
612
|
+
!args.some((w) => isUnquotedLiteral(w, ']]')) &&
|
|
613
|
+
canNlInsideDblBracket(args)) {
|
|
614
|
+
next();
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
286
617
|
if (t.type === 'OP' && isRedirectOp(t.op)) {
|
|
287
618
|
const op = t.op;
|
|
288
619
|
next();
|
|
@@ -308,12 +639,86 @@ export function parseCommand(input) {
|
|
|
308
639
|
next();
|
|
309
640
|
}
|
|
310
641
|
else {
|
|
311
|
-
args.
|
|
642
|
+
if (isUnquotedLiteral(name, '[[') && !args.some((a) => isUnquotedLiteral(a, ']]'))) {
|
|
643
|
+
const pending = pendingPatternOp(args);
|
|
644
|
+
const last = args.length ? args[args.length - 1] : null;
|
|
645
|
+
// bash keeps `( x )` / `@(foo|bar baz)` as one =~ / extglob operand
|
|
646
|
+
if (last &&
|
|
647
|
+
!isUnquotedLiteral(word, ']]') &&
|
|
648
|
+
!isUnquotedLiteral(word, '&&') &&
|
|
649
|
+
!isUnquotedLiteral(word, '||') &&
|
|
650
|
+
((pending === '=~' && unmatchedOpenParen(last)) ||
|
|
651
|
+
(pending === '==' && unmatchedExtglob(last)))) {
|
|
652
|
+
const glued = [...last, { kind: 'Text', text: ' ' }, ...word];
|
|
653
|
+
const pieces = pending === '=~'
|
|
654
|
+
? peelTrailingGroupCloses(glued, groupingDepth(args.slice(0, -1)))
|
|
655
|
+
: splitCondParens(glued);
|
|
656
|
+
args.pop();
|
|
657
|
+
for (const sw of pieces) {
|
|
658
|
+
if (sw.length > 0)
|
|
659
|
+
args.push(sw);
|
|
660
|
+
}
|
|
661
|
+
next();
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
if (hasBareAmp(word) && !(pending === '=~' && last && unmatchedOpenParen(last))) {
|
|
665
|
+
throw new FauxnixParseError("fauxnix: [[: syntax error near unexpected token `&'");
|
|
666
|
+
}
|
|
667
|
+
if (pending === '==' && hasBareNonExtglobParen(word)) {
|
|
668
|
+
throw new FauxnixParseError("fauxnix: [[: syntax error near unexpected token `('");
|
|
669
|
+
}
|
|
670
|
+
const pieces = pending === '=~'
|
|
671
|
+
? peelTrailingGroupCloses(word, groupingDepth(args))
|
|
672
|
+
: splitCondParens(word);
|
|
673
|
+
if (pending !== '=~') {
|
|
674
|
+
for (const sw of pieces) {
|
|
675
|
+
if (!isUnquotedLiteral(sw, '(') &&
|
|
676
|
+
!isUnquotedLiteral(sw, ')') &&
|
|
677
|
+
hasBareNonExtglobParen(sw) &&
|
|
678
|
+
!looksLikeArithWord(sw)) {
|
|
679
|
+
throw new FauxnixParseError("fauxnix: [[: syntax error near unexpected token `('");
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
for (const sw of pieces) {
|
|
684
|
+
if (sw.length > 0)
|
|
685
|
+
args.push(sw);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
else {
|
|
689
|
+
args.push(word);
|
|
690
|
+
}
|
|
312
691
|
next();
|
|
313
692
|
}
|
|
314
693
|
}
|
|
315
|
-
if (!name)
|
|
694
|
+
if (!name) {
|
|
695
|
+
// assignment-only segment (`X=1; cmd`) — no command word
|
|
696
|
+
if (assignments.length > 0) {
|
|
697
|
+
return { kind: 'SimpleCommand', assignments, name: null, args: [], redirects };
|
|
698
|
+
}
|
|
316
699
|
throw new FauxnixParseError('fauxnix: expected a command');
|
|
700
|
+
}
|
|
701
|
+
if (isUnquotedLiteral(name, '[[')) {
|
|
702
|
+
const close = args.findIndex((w) => isUnquotedLiteral(w, ']]'));
|
|
703
|
+
if (close < 0)
|
|
704
|
+
throw new FauxnixParseError("fauxnix: [[: missing `]]'");
|
|
705
|
+
if (close !== args.length - 1) {
|
|
706
|
+
throw new FauxnixParseError("fauxnix: [[: syntax error near unexpected token after `]]'");
|
|
707
|
+
}
|
|
708
|
+
if (args.slice(0, close).some((w) => unmatchedExtglob(w))) {
|
|
709
|
+
throw new FauxnixParseError('fauxnix: [[: syntax error in conditional expression');
|
|
710
|
+
}
|
|
711
|
+
for (let i = 0; i < close; i++) {
|
|
712
|
+
if (!isUnquotedLiteral(args[i], '=~') || i + 1 >= close)
|
|
713
|
+
continue;
|
|
714
|
+
if (regexHasExtraClose(args[i + 1])) {
|
|
715
|
+
throw new FauxnixParseError("fauxnix: [[: syntax error near unexpected token `)'");
|
|
716
|
+
}
|
|
717
|
+
if (unmatchedOpenParen(args[i + 1])) {
|
|
718
|
+
throw new FauxnixParseError('fauxnix: [[: syntax error in conditional expression: unexpected end of file');
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
317
722
|
return { kind: 'SimpleCommand', assignments, name, args, redirects };
|
|
318
723
|
};
|
|
319
724
|
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
4
|
export declare function varExpr(name: 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
|
/**
|
|
@@ -26,6 +28,19 @@ export declare function operandExpr(w: Word): string;
|
|
|
26
28
|
/** Translate the inside of $(...) — pipelines only, no wrappers. */
|
|
27
29
|
export declare function translateCmdSub(cmdText: string): string;
|
|
28
30
|
export declare function translateSimple(cmd: SimpleCommand, position: PipelineCtx['position'], hasStdin: boolean): string;
|
|
31
|
+
/** PS expr: encode a string so SETVALS records can stay newline-delimited. */
|
|
32
|
+
export declare function encodeSetValExpr(srcExpr: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* Apply env assignments (and optional unsets) only for `body`, then restore.
|
|
35
|
+
* All assignment *values* are evaluated before any name is mutated.
|
|
36
|
+
* `persistWords` are evaluated after the prefix is applied (so
|
|
37
|
+
* `export "$NAME"` sees the current env) and those names are not restored.
|
|
38
|
+
*/
|
|
39
|
+
export declare function wrapTempEnv(sets: Assignment[], body: string, extra?: {
|
|
40
|
+
unsets?: string[];
|
|
41
|
+
persistNames?: Set<string>;
|
|
42
|
+
persistWords?: Word[];
|
|
43
|
+
}): string;
|
|
29
44
|
export interface PipelineParts {
|
|
30
45
|
/** Generated function definitions (empty for single commands). */
|
|
31
46
|
defs: string;
|