fauxnix-cli 0.9.3 → 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/README.md +84 -26
- package/dist/ast.d.ts +38 -3
- package/dist/ast.js +22 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +84 -20
- package/dist/commands/archive.d.ts +2 -1
- package/dist/commands/archive.js +169 -25
- package/dist/commands/install-all.js +4 -2
- package/dist/commands/net.d.ts +9 -0
- package/dist/commands/net.js +51 -15
- package/dist/commands/sysinfo.d.ts +2 -1
- package/dist/commands/sysinfo.js +610 -82
- package/dist/commands/text-filters.d.ts +1 -0
- package/dist/commands/text-filters.js +99 -13
- package/dist/commands/text-io.js +72 -43
- package/dist/doctor.d.ts +21 -0
- package/dist/doctor.js +292 -0
- package/dist/errors.js +14 -4
- package/dist/executor.d.ts +4 -1
- package/dist/executor.js +194 -51
- package/dist/install.d.ts +15 -0
- package/dist/install.js +247 -0
- package/dist/mcp.d.ts +19 -0
- package/dist/mcp.js +49 -26
- package/dist/parser.js +432 -35
- package/dist/powershell.d.ts +22 -0
- package/dist/powershell.js +129 -0
- package/dist/ps-host.d.ts +41 -13
- package/dist/ps-host.js +302 -40
- package/dist/qwen-launch.d.ts +12 -0
- package/dist/qwen-launch.js +29 -0
- package/dist/registry.d.ts +22 -0
- package/dist/registry.js +109 -1
- package/dist/translator.d.ts +42 -9
- package/dist/translator.js +697 -95
- package/package.json +3 -1
package/dist/parser.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { FauxnixParseError, isUnquotedLiteral, wordToString, } from './ast.js';
|
|
2
2
|
const OPERATORS = [
|
|
3
|
-
'&&', '||', '>>', '<<', '2>&1', '1>&2', '2>', '&>>', '&>', '>', '<', '|', ';',
|
|
3
|
+
'&&', '||', '2>>', '>>', '<<', '2>&1', '1>&2', '2>', '&>>', '&>', '>', '<', '|', ';;', ';', '&',
|
|
4
4
|
];
|
|
5
|
+
const BACKGROUND_MSG = 'fauxnix: background & is not supported yet. Run the command in the foreground instead.';
|
|
6
|
+
const WHILE_UNTIL_MSG = 'fauxnix: while/until loops are not supported yet. Use `for x in ...; do ...; done` over a known list instead.';
|
|
7
|
+
const CASE_MSG = 'fauxnix: case is not supported yet. Use if/elif/else instead.';
|
|
8
|
+
const FUNCTION_MSG = 'fauxnix: functions are not supported yet. Inline the body or repeat the command instead.';
|
|
9
|
+
const IF_IN_PIPELINE_MSG = 'fauxnix: if in a pipeline is not supported. Run the if as its own list segment instead of piping into it.';
|
|
10
|
+
const FOR_IN_PIPELINE_MSG = 'fauxnix: for in a pipeline is not supported. Run the for as its own list segment instead of piping into it.';
|
|
11
|
+
const CSTYLE_FOR_MSG = 'fauxnix: C-style for ((...)) is not supported yet. Use `for x in ...; do ...; done` instead.';
|
|
5
12
|
export function tokenize(input) {
|
|
6
13
|
const tokens = [];
|
|
7
14
|
let i = 0;
|
|
@@ -54,6 +61,12 @@ export function tokenize(input) {
|
|
|
54
61
|
// try operators (longest first — list above is ordered)
|
|
55
62
|
let matched;
|
|
56
63
|
for (const op of OPERATORS) {
|
|
64
|
+
// A digit-leading fd operator is only a direct token at a word boundary.
|
|
65
|
+
// Otherwise consume the digit normally: `file2>>out` is word `file2`
|
|
66
|
+
// plus stdout `>>out`, while `12>>out` stays one unsupported fd token
|
|
67
|
+
// and fails loud instead of being stolen by `2>>`.
|
|
68
|
+
if (cur.length > 0 && /^\d/.test(op))
|
|
69
|
+
continue;
|
|
57
70
|
if (input.startsWith(op, i)) {
|
|
58
71
|
matched = op;
|
|
59
72
|
break;
|
|
@@ -177,8 +190,10 @@ export function tokenize(input) {
|
|
|
177
190
|
i += 2;
|
|
178
191
|
continue;
|
|
179
192
|
}
|
|
180
|
-
//
|
|
181
|
-
|
|
193
|
+
// Track an all-digit word as a potential fd number. Keep consuming every
|
|
194
|
+
// leading digit so unsupported multi-digit fds stay intact and fail loud
|
|
195
|
+
// instead of becoming argv plus a different redirect.
|
|
196
|
+
if (/[0-9]/.test(ch) && (cur.length === 0 || fdDigits.length === cur.length)) {
|
|
182
197
|
beginWordPart();
|
|
183
198
|
fdDigits += ch;
|
|
184
199
|
cur.push({ kind: 'Text', text: ch });
|
|
@@ -242,6 +257,64 @@ function readArithParts(input) {
|
|
|
242
257
|
parts.push({ kind: 'Text', text: buf });
|
|
243
258
|
return parts;
|
|
244
259
|
}
|
|
260
|
+
/** Integer or `$name` operand of `${name:offset:length}`. */
|
|
261
|
+
function readSliceNum(s, i) {
|
|
262
|
+
while (i < s.length && (s[i] === ' ' || s[i] === '\t'))
|
|
263
|
+
i++;
|
|
264
|
+
if (i >= s.length)
|
|
265
|
+
return null;
|
|
266
|
+
if (s[i] === '$') {
|
|
267
|
+
const j = i + 1;
|
|
268
|
+
if (j < s.length && isNameStart(s[j])) {
|
|
269
|
+
let k = j + 1;
|
|
270
|
+
while (k < s.length && isNameChar(s[k]))
|
|
271
|
+
k++;
|
|
272
|
+
return { val: s.slice(i, k), next: k };
|
|
273
|
+
}
|
|
274
|
+
if (j < s.length && (/[0-9]/.test(s[j]) || s[j] === '?' || s[j] === '$')) {
|
|
275
|
+
return { val: s.slice(i, j + 1), next: j + 1 };
|
|
276
|
+
}
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
let sign = '';
|
|
280
|
+
if (s[i] === '-' || s[i] === '+') {
|
|
281
|
+
sign = s[i];
|
|
282
|
+
i++;
|
|
283
|
+
}
|
|
284
|
+
if (i >= s.length || !/[0-9]/.test(s[i]))
|
|
285
|
+
return null;
|
|
286
|
+
let k = i;
|
|
287
|
+
while (k < s.length && /[0-9]/.test(s[k]))
|
|
288
|
+
k++;
|
|
289
|
+
return { val: sign + s.slice(i, k), next: k };
|
|
290
|
+
}
|
|
291
|
+
/** Parse `offset` / `offset:length` after `${name:`. Null if not a slice. */
|
|
292
|
+
function parseSliceSpec(after) {
|
|
293
|
+
const off = readSliceNum(after, 0);
|
|
294
|
+
if (!off)
|
|
295
|
+
return null;
|
|
296
|
+
let i = off.next;
|
|
297
|
+
while (i < after.length && (after[i] === ' ' || after[i] === '\t'))
|
|
298
|
+
i++;
|
|
299
|
+
if (i >= after.length)
|
|
300
|
+
return { offset: off.val };
|
|
301
|
+
if (after[i] !== ':')
|
|
302
|
+
return null;
|
|
303
|
+
i++;
|
|
304
|
+
while (i < after.length && (after[i] === ' ' || after[i] === '\t'))
|
|
305
|
+
i++;
|
|
306
|
+
if (i >= after.length)
|
|
307
|
+
return { offset: off.val, length: '0' };
|
|
308
|
+
const len = readSliceNum(after, i);
|
|
309
|
+
if (!len)
|
|
310
|
+
return null;
|
|
311
|
+
i = len.next;
|
|
312
|
+
while (i < after.length && (after[i] === ' ' || after[i] === '\t'))
|
|
313
|
+
i++;
|
|
314
|
+
if (i !== after.length)
|
|
315
|
+
return null;
|
|
316
|
+
return { offset: off.val, length: len.val };
|
|
317
|
+
}
|
|
245
318
|
/** Parse $VAR, ${VAR}, $(cmd substitution), $((arith)). Returns null when not a valid dollar construct. */
|
|
246
319
|
function readDollar(input, i) {
|
|
247
320
|
const n = input.length;
|
|
@@ -255,31 +328,76 @@ function readDollar(input, i) {
|
|
|
255
328
|
const end = input.indexOf('}', j);
|
|
256
329
|
if (end === -1)
|
|
257
330
|
throw new FauxnixParseError('fauxnix: unclosed ${');
|
|
258
|
-
const
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
return { part: { kind: 'Var', name: sub[1], index: sub[2] }, next: end + 1 };
|
|
262
|
-
}
|
|
263
|
-
const pm = name.match(/^([A-Za-z_][A-Za-z0-9_]*)(:?[-+?])(.*)$/);
|
|
264
|
-
if (pm) {
|
|
265
|
-
const op = pm[2];
|
|
266
|
-
return {
|
|
267
|
-
part: { kind: 'Var', name: pm[1], param: { op, word: pm[3] } },
|
|
268
|
-
next: end + 1,
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
const hash = name.match(/^#([A-Za-z_][A-Za-z0-9_]*)(\[([0-9]+|@|\*)\])?$/);
|
|
331
|
+
const inner = input.slice(j + 1, end);
|
|
332
|
+
const nextPos = end + 1;
|
|
333
|
+
const hash = inner.match(/^#([A-Za-z_][A-Za-z0-9_]*)(\[([0-9]+|@|\*)\])?$/);
|
|
272
334
|
if (hash) {
|
|
273
335
|
return {
|
|
274
336
|
part: { kind: 'Var', name: hash[1], index: hash[3], length: true },
|
|
275
|
-
next:
|
|
337
|
+
next: nextPos,
|
|
276
338
|
};
|
|
277
339
|
}
|
|
278
|
-
|
|
340
|
+
// ${1} ${#} ${@} ${*} — positional / special params (not ${#name} length)
|
|
341
|
+
if (inner === '#' || inner === '@' || inner === '*' || /^[0-9]+$/.test(inner)) {
|
|
342
|
+
return { part: { kind: 'Var', name: inner }, next: nextPos };
|
|
343
|
+
}
|
|
344
|
+
const sub = inner.match(/^([A-Za-z_][A-Za-z0-9_]*)\[([0-9]+|@|\*)\]$/);
|
|
345
|
+
if (sub) {
|
|
346
|
+
return { part: { kind: 'Var', name: sub[1], index: sub[2] }, next: nextPos };
|
|
347
|
+
}
|
|
348
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*\[([0-9]+|@|\*)\]/.test(inner)) {
|
|
349
|
+
throw new FauxnixParseError('fauxnix: ${name[@]:offset:length} subarray slice is not supported; use ${name:offset:length} on a scalar or ${name[i]} per element');
|
|
350
|
+
}
|
|
351
|
+
const ident = inner.match(/^([A-Za-z_][A-Za-z0-9_]*)/);
|
|
352
|
+
if (ident) {
|
|
353
|
+
const nm = ident[1];
|
|
354
|
+
const rest = inner.slice(nm.length);
|
|
355
|
+
if (rest.startsWith('/#') || rest.startsWith('/%')) {
|
|
356
|
+
throw new FauxnixParseError('fauxnix: ${name/#pat/str} and ${name/%pat/str} are not supported; use ${name//pat/str} instead');
|
|
357
|
+
}
|
|
358
|
+
if (rest.startsWith('/')) {
|
|
359
|
+
const global = rest.startsWith('//');
|
|
360
|
+
const body = rest.slice(global ? 2 : 1);
|
|
361
|
+
const slash = body.indexOf('/');
|
|
362
|
+
const pat = slash === -1 ? body : body.slice(0, slash);
|
|
363
|
+
const repl = slash === -1 ? '' : body.slice(slash + 1);
|
|
364
|
+
return {
|
|
365
|
+
part: { kind: 'Var', name: nm, replace: { global, pat, repl } },
|
|
366
|
+
next: nextPos,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
if (rest.startsWith(':')) {
|
|
370
|
+
const after = rest.slice(1);
|
|
371
|
+
let t = 0;
|
|
372
|
+
while (t < after.length && (after[t] === ' ' || after[t] === '\t'))
|
|
373
|
+
t++;
|
|
374
|
+
const lead = t < after.length ? after[t] : '';
|
|
375
|
+
const hadSpace = t > 0;
|
|
376
|
+
const paramLead = lead === '+' || lead === '?' || lead === '=' || (lead === '-' && !hadSpace);
|
|
377
|
+
if (!paramLead && lead !== '') {
|
|
378
|
+
const sl = parseSliceSpec(after);
|
|
379
|
+
if (sl) {
|
|
380
|
+
return { part: { kind: 'Var', name: nm, slice: sl }, next: nextPos };
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const pm = inner.match(/^([A-Za-z_][A-Za-z0-9_]*)(:?[-+?])(.*)$/);
|
|
385
|
+
if (pm) {
|
|
386
|
+
const op = pm[2];
|
|
387
|
+
return {
|
|
388
|
+
part: { kind: 'Var', name: pm[1], param: { op, word: pm[3] } },
|
|
389
|
+
next: nextPos,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
if (rest === '') {
|
|
393
|
+
return { part: { kind: 'Var', name: nm }, next: nextPos };
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (!inner || !isNameStart(inner[0]) || !inner.split('').every(isNameChar)) {
|
|
279
397
|
// ${VAR:=default} etc. still raw text
|
|
280
|
-
return { part: { kind: 'Text', text: input.slice(i, end + 1) }, next:
|
|
398
|
+
return { part: { kind: 'Text', text: input.slice(i, end + 1) }, next: nextPos };
|
|
281
399
|
}
|
|
282
|
-
return { part: { kind: 'Var', name }, next:
|
|
400
|
+
return { part: { kind: 'Var', name: inner }, next: nextPos };
|
|
283
401
|
}
|
|
284
402
|
// $((...)) arithmetic expansion — distinct from `$( (cmd) )` (space after
|
|
285
403
|
// the first paren is command substitution of a grouped body).
|
|
@@ -350,8 +468,8 @@ function readDollar(input, i) {
|
|
|
350
468
|
len++;
|
|
351
469
|
return { part: { kind: 'Var', name: input.slice(j, j + len) }, next: j + len };
|
|
352
470
|
}
|
|
353
|
-
// special: $? $$ $0-$9 — kept as symbolic Var; the translator maps them
|
|
354
|
-
if ('?$_'.includes(input[j]) || /[0-9]/.test(input[j])) {
|
|
471
|
+
// special: $? $$ $0-$9 $# $@ $* — kept as symbolic Var; the translator maps them
|
|
472
|
+
if ('?$_#@*'.includes(input[j]) || /[0-9]/.test(input[j])) {
|
|
355
473
|
return { part: { kind: 'Var', name: input[j] }, next: j + 1 };
|
|
356
474
|
}
|
|
357
475
|
return null;
|
|
@@ -578,6 +696,76 @@ function pendingPatternOp(args) {
|
|
|
578
696
|
}
|
|
579
697
|
return null;
|
|
580
698
|
}
|
|
699
|
+
/** Unquoted `(` / `)` net depth in a word (quoted / escaped parens ignored). */
|
|
700
|
+
function unquotedParenDelta(w) {
|
|
701
|
+
let d = 0;
|
|
702
|
+
for (const p of w) {
|
|
703
|
+
if (p.kind !== 'Text' || p.escaped)
|
|
704
|
+
continue;
|
|
705
|
+
for (const c of p.text) {
|
|
706
|
+
if (c === '(')
|
|
707
|
+
d++;
|
|
708
|
+
else if (c === ')')
|
|
709
|
+
d--;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return d;
|
|
713
|
+
}
|
|
714
|
+
function startsWithUnquotedOpenParen(w) {
|
|
715
|
+
if (w.length === 0)
|
|
716
|
+
return false;
|
|
717
|
+
const p = w[0];
|
|
718
|
+
return p.kind === 'Text' && !p.escaped && p.text.startsWith('(');
|
|
719
|
+
}
|
|
720
|
+
/** `NAME+=` (scalar or array append) — out of scope for C-2. */
|
|
721
|
+
function isAppendAssignment(w) {
|
|
722
|
+
let s = '';
|
|
723
|
+
for (const p of w) {
|
|
724
|
+
if (p.kind === 'Text' && !p.escaped)
|
|
725
|
+
s += p.text;
|
|
726
|
+
else
|
|
727
|
+
break;
|
|
728
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*\+=/.test(s))
|
|
729
|
+
return true;
|
|
730
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(s))
|
|
731
|
+
return false;
|
|
732
|
+
}
|
|
733
|
+
return /^[A-Za-z_][A-Za-z0-9_]*\+=/.test(s);
|
|
734
|
+
}
|
|
735
|
+
function cloneWord(w) {
|
|
736
|
+
return w.map((p) => {
|
|
737
|
+
if (p.kind === 'Text' || p.kind === 'SingleQuoted')
|
|
738
|
+
return { ...p };
|
|
739
|
+
if (p.kind === 'DoubleQuoted')
|
|
740
|
+
return { kind: 'DoubleQuoted', parts: p.parts.slice() };
|
|
741
|
+
return p;
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
/** Strip the opening `(` and closing `)` that wrap an array assignment value. */
|
|
745
|
+
function stripArrayParens(words) {
|
|
746
|
+
if (words.length === 0)
|
|
747
|
+
return [];
|
|
748
|
+
const ws = words.map(cloneWord);
|
|
749
|
+
const first = ws[0];
|
|
750
|
+
if (first.length > 0 && first[0].kind === 'Text' && first[0].text.startsWith('(')) {
|
|
751
|
+
first[0] = { kind: 'Text', text: first[0].text.slice(1), escaped: first[0].escaped };
|
|
752
|
+
if (first[0].text === '')
|
|
753
|
+
first.shift();
|
|
754
|
+
}
|
|
755
|
+
const last = ws[ws.length - 1];
|
|
756
|
+
for (let i = last.length - 1; i >= 0; i--) {
|
|
757
|
+
const p = last[i];
|
|
758
|
+
if (p.kind === 'Text' && !p.escaped && p.text.endsWith(')')) {
|
|
759
|
+
const trimmed = p.text.slice(0, -1);
|
|
760
|
+
if (trimmed === '')
|
|
761
|
+
last.splice(i, 1);
|
|
762
|
+
else
|
|
763
|
+
last[i] = { kind: 'Text', text: trimmed, escaped: p.escaped };
|
|
764
|
+
break;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
return ws.filter((w) => w.length > 0);
|
|
768
|
+
}
|
|
581
769
|
/* ------------------------------------------------------------------ */
|
|
582
770
|
/* Parser */
|
|
583
771
|
/* ------------------------------------------------------------------ */
|
|
@@ -587,17 +775,44 @@ export function parseCommand(input) {
|
|
|
587
775
|
const peek = () => tokens[pos];
|
|
588
776
|
const next = () => tokens[pos++];
|
|
589
777
|
const isListSep = (o) => o === ';' || o === '\n';
|
|
778
|
+
const isAmpWord = (t) => (!!t && t.type === 'OP' && t.op === '&') ||
|
|
779
|
+
(!!t && t.type === 'WORD' && !!t.parts && isUnquotedLiteral(t.parts, '&'));
|
|
780
|
+
const isCaseFallthrough = () => {
|
|
781
|
+
const t = peek();
|
|
782
|
+
if (t.type !== 'OP')
|
|
783
|
+
return false;
|
|
784
|
+
return (t.op === ';' || t.op === ';;') && isAmpWord(tokens[pos + 1]);
|
|
785
|
+
};
|
|
786
|
+
const throwUnexpectedDsemi = () => {
|
|
787
|
+
throw new FauxnixParseError("fauxnix: syntax error near unexpected token `;;'");
|
|
788
|
+
};
|
|
789
|
+
const throwCaseFallthrough = () => {
|
|
790
|
+
throw new FauxnixParseError('fauxnix: case fallthrough (;& / ;;&) is not supported; use ;; (no fallthrough) or duplicate the body');
|
|
791
|
+
};
|
|
590
792
|
/** Consume `;` / newline / `&&` / `||`. Trailing `&&`/`||` and `;;` fail loud (bash). */
|
|
591
793
|
const consumeListOp = (stops) => {
|
|
592
794
|
const t = peek();
|
|
593
795
|
if (t.type !== 'OP')
|
|
594
796
|
return null;
|
|
797
|
+
if (t.op === '&') {
|
|
798
|
+
// inside a case arm, `;&` / `;;&` is fallthrough — report that, not background
|
|
799
|
+
if (stops && stops.has(';;'))
|
|
800
|
+
throwCaseFallthrough();
|
|
801
|
+
throw new FauxnixParseError(BACKGROUND_MSG);
|
|
802
|
+
}
|
|
803
|
+
if (t.op === ';;') {
|
|
804
|
+
if (stops && stops.has(';;'))
|
|
805
|
+
return null;
|
|
806
|
+
throwUnexpectedDsemi();
|
|
807
|
+
}
|
|
595
808
|
if (!(t.op === '&&' || t.op === '||' || isListSep(t.op)))
|
|
596
809
|
return null;
|
|
597
810
|
if (t.op === ';') {
|
|
811
|
+
if (stops && stops.has(';;') && isAmpWord(tokens[pos + 1]))
|
|
812
|
+
return null;
|
|
598
813
|
next();
|
|
599
|
-
if (peek().type === 'OP' && peek().op === ';') {
|
|
600
|
-
|
|
814
|
+
if (peek().type === 'OP' && (peek().op === ';' || peek().op === ';;')) {
|
|
815
|
+
throwUnexpectedDsemi();
|
|
601
816
|
}
|
|
602
817
|
return ';';
|
|
603
818
|
}
|
|
@@ -627,16 +842,20 @@ export function parseCommand(input) {
|
|
|
627
842
|
const segments = [];
|
|
628
843
|
let op = ';';
|
|
629
844
|
while (peek().type === 'OP' && isListSep(peek().op)) {
|
|
630
|
-
if (peek().op === ';' && tokens[pos + 1]?.type === 'OP' && tokens[pos + 1]?.op === ';') {
|
|
631
|
-
|
|
845
|
+
if (peek().op === ';' && tokens[pos + 1]?.type === 'OP' && (tokens[pos + 1]?.op === ';' || tokens[pos + 1]?.op === ';;')) {
|
|
846
|
+
throwUnexpectedDsemi();
|
|
632
847
|
}
|
|
633
848
|
next();
|
|
634
849
|
}
|
|
635
850
|
while (peek().type !== 'EOF') {
|
|
851
|
+
if (peek().type === 'OP' && peek().op === ';;')
|
|
852
|
+
throwUnexpectedDsemi();
|
|
636
853
|
const pipeline = parsePipeline();
|
|
637
854
|
segments.push({ pipeline, op });
|
|
638
855
|
const nextOp = consumeListOp();
|
|
639
856
|
if (nextOp === null) {
|
|
857
|
+
if (peek().type === 'OP' && peek().op === ';;')
|
|
858
|
+
throwUnexpectedDsemi();
|
|
640
859
|
if (peek().type === 'EOF')
|
|
641
860
|
break;
|
|
642
861
|
throw new FauxnixParseError('fauxnix: unexpected token after pipeline');
|
|
@@ -653,18 +872,43 @@ export function parseCommand(input) {
|
|
|
653
872
|
const kw = peekKw();
|
|
654
873
|
if (kw === 'if') {
|
|
655
874
|
if (commands.length > 0) {
|
|
656
|
-
throw new FauxnixParseError(
|
|
875
|
+
throw new FauxnixParseError(IF_IN_PIPELINE_MSG);
|
|
657
876
|
}
|
|
658
877
|
commands.push(parseIf());
|
|
659
878
|
}
|
|
660
879
|
else if (kw === 'for') {
|
|
661
880
|
if (commands.length > 0) {
|
|
662
|
-
throw new FauxnixParseError(
|
|
881
|
+
throw new FauxnixParseError(FOR_IN_PIPELINE_MSG);
|
|
663
882
|
}
|
|
664
883
|
commands.push(parseFor());
|
|
665
884
|
}
|
|
885
|
+
else if (kw === 'while') {
|
|
886
|
+
if (commands.length > 0) {
|
|
887
|
+
throw new FauxnixParseError('fauxnix: while in a pipeline is not supported');
|
|
888
|
+
}
|
|
889
|
+
commands.push(parseWhile());
|
|
890
|
+
}
|
|
891
|
+
else if (kw === 'until') {
|
|
892
|
+
if (commands.length > 0) {
|
|
893
|
+
throw new FauxnixParseError('fauxnix: until in a pipeline is not supported');
|
|
894
|
+
}
|
|
895
|
+
commands.push(parseUntil());
|
|
896
|
+
}
|
|
897
|
+
else if (kw === 'case') {
|
|
898
|
+
if (commands.length > 0) {
|
|
899
|
+
throw new FauxnixParseError('fauxnix: case in a pipeline is not supported');
|
|
900
|
+
}
|
|
901
|
+
commands.push(parseCase());
|
|
902
|
+
}
|
|
903
|
+
else if (kw === 'function') {
|
|
904
|
+
throw new FauxnixParseError(FUNCTION_MSG);
|
|
905
|
+
}
|
|
666
906
|
else {
|
|
667
|
-
|
|
907
|
+
const cmd = parseSimple();
|
|
908
|
+
if (cmd.kind === 'SimpleCommand' && cmd.name && isFunctionDef(cmd)) {
|
|
909
|
+
throw new FauxnixParseError(FUNCTION_MSG);
|
|
910
|
+
}
|
|
911
|
+
commands.push(cmd);
|
|
668
912
|
}
|
|
669
913
|
const t = peek();
|
|
670
914
|
if (t.type === 'OP' && t.op === '|') {
|
|
@@ -691,7 +935,11 @@ export function parseCommand(input) {
|
|
|
691
935
|
s === 'in' ||
|
|
692
936
|
s === 'do' ||
|
|
693
937
|
s === 'done' ||
|
|
694
|
-
s === 'while'
|
|
938
|
+
s === 'while' ||
|
|
939
|
+
s === 'until' ||
|
|
940
|
+
s === 'case' ||
|
|
941
|
+
s === 'esac' ||
|
|
942
|
+
s === 'function') {
|
|
695
943
|
return s;
|
|
696
944
|
}
|
|
697
945
|
return null;
|
|
@@ -707,23 +955,34 @@ export function parseCommand(input) {
|
|
|
707
955
|
const segments = [];
|
|
708
956
|
let op = ';';
|
|
709
957
|
while (peek().type === 'OP' && isListSep(peek().op)) {
|
|
710
|
-
if (peek().op === ';' && tokens[pos + 1]?.type === 'OP' && tokens[pos + 1]?.op === ';') {
|
|
711
|
-
|
|
958
|
+
if (peek().op === ';' && tokens[pos + 1]?.type === 'OP' && (tokens[pos + 1]?.op === ';' || tokens[pos + 1]?.op === ';;')) {
|
|
959
|
+
throwUnexpectedDsemi();
|
|
712
960
|
}
|
|
713
961
|
next();
|
|
714
962
|
}
|
|
715
963
|
while (peek().type !== 'EOF') {
|
|
964
|
+
if (stop.has(';;') && isCaseFallthrough())
|
|
965
|
+
break;
|
|
716
966
|
const kw = peekKw();
|
|
717
967
|
if (kw && stop.has(kw))
|
|
718
968
|
break;
|
|
969
|
+
const stopOp = peek();
|
|
970
|
+
if (stopOp.type === 'OP' && stopOp.op && stop.has(stopOp.op))
|
|
971
|
+
break;
|
|
972
|
+
if (stopOp.type === 'OP' && stopOp.op === ';;')
|
|
973
|
+
throwUnexpectedDsemi();
|
|
719
974
|
const pipeline = parsePipeline();
|
|
720
975
|
segments.push({ pipeline, op });
|
|
976
|
+
if (stop.has(';;') && isCaseFallthrough())
|
|
977
|
+
break;
|
|
721
978
|
const nextOp = consumeListOp(stop);
|
|
722
979
|
if (nextOp === null)
|
|
723
980
|
break;
|
|
724
981
|
op = nextOp;
|
|
725
982
|
}
|
|
726
983
|
if (segments.length === 0) {
|
|
984
|
+
if (stop.has(';;'))
|
|
985
|
+
return { kind: 'CommandList', segments: [] };
|
|
727
986
|
throw new FauxnixParseError('fauxnix: empty command');
|
|
728
987
|
}
|
|
729
988
|
return { kind: 'CommandList', segments };
|
|
@@ -761,6 +1020,9 @@ export function parseCommand(input) {
|
|
|
761
1020
|
throw new FauxnixParseError('fauxnix: `for` expected a name');
|
|
762
1021
|
}
|
|
763
1022
|
const name = wordToString(nt.parts);
|
|
1023
|
+
if (name.startsWith('((')) {
|
|
1024
|
+
throw new FauxnixParseError(CSTYLE_FOR_MSG);
|
|
1025
|
+
}
|
|
764
1026
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name) || !isUnquotedLiteral(nt.parts, name)) {
|
|
765
1027
|
throw new FauxnixParseError('fauxnix: `for` name must be an identifier');
|
|
766
1028
|
}
|
|
@@ -784,6 +1046,92 @@ export function parseCommand(input) {
|
|
|
784
1046
|
expectKw('done');
|
|
785
1047
|
return { kind: 'For', name, words, body, redirects: [] };
|
|
786
1048
|
};
|
|
1049
|
+
const parseWhile = () => parseWhileLoop(false);
|
|
1050
|
+
const parseUntil = () => parseWhileLoop(true);
|
|
1051
|
+
const parseWhileLoop = (until) => {
|
|
1052
|
+
expectKw(until ? 'until' : 'while');
|
|
1053
|
+
const test = parseListUntil(['do']);
|
|
1054
|
+
expectKw('do');
|
|
1055
|
+
const body = parseListUntil(['done']);
|
|
1056
|
+
expectKw('done');
|
|
1057
|
+
return { kind: 'While', until, test, body, redirects: [] };
|
|
1058
|
+
};
|
|
1059
|
+
const skipCaseSeps = () => {
|
|
1060
|
+
while (peek().type === 'OP' && isListSep(peek().op)) {
|
|
1061
|
+
if (peek().op === ';' && isAmpWord(tokens[pos + 1]))
|
|
1062
|
+
break;
|
|
1063
|
+
next();
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
/** Strip a trailing unquoted `)` that closes a case pattern list. */
|
|
1067
|
+
const stripTrailingUnquotedParen = (w) => {
|
|
1068
|
+
if (w.length === 0)
|
|
1069
|
+
return null;
|
|
1070
|
+
const last = w[w.length - 1];
|
|
1071
|
+
if (last.kind !== 'Text' || last.escaped || !last.text.endsWith(')'))
|
|
1072
|
+
return null;
|
|
1073
|
+
const rest = last.text.slice(0, -1);
|
|
1074
|
+
if (rest.length === 0)
|
|
1075
|
+
return w.slice(0, -1);
|
|
1076
|
+
return [...w.slice(0, -1), { kind: 'Text', text: rest, escaped: last.escaped }];
|
|
1077
|
+
};
|
|
1078
|
+
const parseCasePatterns = () => {
|
|
1079
|
+
const patterns = [];
|
|
1080
|
+
for (;;) {
|
|
1081
|
+
while (peek().type === 'OP' && (peek().op === '|' || peek().op === '\n'))
|
|
1082
|
+
next();
|
|
1083
|
+
if (peekKw() === 'esac') {
|
|
1084
|
+
throw new FauxnixParseError("fauxnix: expected `)'");
|
|
1085
|
+
}
|
|
1086
|
+
const t = peek();
|
|
1087
|
+
if (t.type !== 'WORD' || !t.parts) {
|
|
1088
|
+
throw new FauxnixParseError('fauxnix: `case` expected a pattern');
|
|
1089
|
+
}
|
|
1090
|
+
const stripped = stripTrailingUnquotedParen(t.parts);
|
|
1091
|
+
if (stripped !== null) {
|
|
1092
|
+
next();
|
|
1093
|
+
if (stripped.length > 0)
|
|
1094
|
+
patterns.push(stripped);
|
|
1095
|
+
if (patterns.length === 0) {
|
|
1096
|
+
throw new FauxnixParseError('fauxnix: `case` expected a pattern');
|
|
1097
|
+
}
|
|
1098
|
+
return patterns;
|
|
1099
|
+
}
|
|
1100
|
+
patterns.push(t.parts);
|
|
1101
|
+
next();
|
|
1102
|
+
}
|
|
1103
|
+
};
|
|
1104
|
+
const parseCase = () => {
|
|
1105
|
+
expectKw('case');
|
|
1106
|
+
skipCaseSeps();
|
|
1107
|
+
const wt = peek();
|
|
1108
|
+
if (wt.type !== 'WORD' || !wt.parts) {
|
|
1109
|
+
throw new FauxnixParseError('fauxnix: `case` expected a word');
|
|
1110
|
+
}
|
|
1111
|
+
const word = wt.parts;
|
|
1112
|
+
next();
|
|
1113
|
+
skipCaseSeps();
|
|
1114
|
+
expectKw('in');
|
|
1115
|
+
const arms = [];
|
|
1116
|
+
skipCaseSeps();
|
|
1117
|
+
while (peek().type !== 'EOF' && peekKw() !== 'esac') {
|
|
1118
|
+
if (isCaseFallthrough())
|
|
1119
|
+
throwCaseFallthrough();
|
|
1120
|
+
const patterns = parseCasePatterns();
|
|
1121
|
+
const body = parseListUntil(['esac', ';;']);
|
|
1122
|
+
if (isCaseFallthrough())
|
|
1123
|
+
throwCaseFallthrough();
|
|
1124
|
+
if (peek().type === 'OP' && peek().op === ';;') {
|
|
1125
|
+
next();
|
|
1126
|
+
if (isAmpWord(peek()))
|
|
1127
|
+
throwCaseFallthrough();
|
|
1128
|
+
}
|
|
1129
|
+
arms.push({ patterns, body });
|
|
1130
|
+
skipCaseSeps();
|
|
1131
|
+
}
|
|
1132
|
+
expectKw('esac');
|
|
1133
|
+
return { kind: 'Case', word, arms, redirects: [] };
|
|
1134
|
+
};
|
|
787
1135
|
const parseSimple = () => {
|
|
788
1136
|
const assignments = [];
|
|
789
1137
|
let redirects = [];
|
|
@@ -804,6 +1152,7 @@ export function parseCommand(input) {
|
|
|
804
1152
|
(t.op === '&&' ||
|
|
805
1153
|
t.op === '||' ||
|
|
806
1154
|
t.op === '|' ||
|
|
1155
|
+
t.op === '&' ||
|
|
807
1156
|
t.op === '>' ||
|
|
808
1157
|
t.op === '<' ||
|
|
809
1158
|
t.op === '>>' ||
|
|
@@ -817,7 +1166,8 @@ export function parseCommand(input) {
|
|
|
817
1166
|
// Glue `|` onto the surrounding words so `=~ ^a|z$`,
|
|
818
1167
|
// `=~ (a | b)c`, and `== @(x | y)` stay one operand. A
|
|
819
1168
|
// spaced `|` outside an open regex / extglob group is a
|
|
820
|
-
// syntax error (bash).
|
|
1169
|
+
// syntax error (bash). Tight `&` inside an open `=~ (…)`
|
|
1170
|
+
// group stays in the regex, not a background job.
|
|
821
1171
|
const last = args.length ? args[args.length - 1] : null;
|
|
822
1172
|
const lastIsEqTilde = last !== null && isUnquotedLiteral(last, '=~');
|
|
823
1173
|
const prevIsEqTilde = args.length >= 2 && isUnquotedLiteral(args[args.length - 2], '=~');
|
|
@@ -828,7 +1178,18 @@ export function parseCommand(input) {
|
|
|
828
1178
|
pendingPatternOp(args) === '==' &&
|
|
829
1179
|
unmatchedExtglob(last);
|
|
830
1180
|
const inRe = lastIsEqTilde || prevIsEqTilde || openRe;
|
|
831
|
-
if (t.op === '
|
|
1181
|
+
if (t.op === '&') {
|
|
1182
|
+
if (!(openRe && last && t.tightLeft)) {
|
|
1183
|
+
throw new FauxnixParseError("fauxnix: [[: syntax error near unexpected token `&'");
|
|
1184
|
+
}
|
|
1185
|
+
args[args.length - 1] = [...last, { kind: 'Text', text: '&' }];
|
|
1186
|
+
const n = peek();
|
|
1187
|
+
if (n.type === 'WORD' && n.tightLeft && n.parts) {
|
|
1188
|
+
next();
|
|
1189
|
+
args[args.length - 1] = [...args[args.length - 1], ...n.parts];
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
else if (t.op === '|' || ((inRe || openExt) && t.tightLeft && t.op === '||')) {
|
|
832
1193
|
if (t.op === '|' &&
|
|
833
1194
|
!lastIsEqTilde &&
|
|
834
1195
|
!(prevIsEqTilde && t.tightLeft) &&
|
|
@@ -895,10 +1256,33 @@ export function parseCommand(input) {
|
|
|
895
1256
|
break;
|
|
896
1257
|
const word = t.parts;
|
|
897
1258
|
// assignment prefix before command name?
|
|
1259
|
+
if (name === null && isAppendAssignment(word)) {
|
|
1260
|
+
throw new FauxnixParseError('fauxnix: `+=` append is not supported; use `A=(${A[@]} x)` or `A=(x y)` instead');
|
|
1261
|
+
}
|
|
898
1262
|
if (name === null && isAssignment(word)) {
|
|
899
1263
|
next();
|
|
900
1264
|
const split = splitAssignment(word);
|
|
901
1265
|
if (split) {
|
|
1266
|
+
if (startsWithUnquotedOpenParen(split.value)) {
|
|
1267
|
+
const elems = [split.value];
|
|
1268
|
+
let depth = unquotedParenDelta(split.value);
|
|
1269
|
+
while (depth > 0 && peek().type === 'WORD' && peek().parts) {
|
|
1270
|
+
const nxt = peek().parts;
|
|
1271
|
+
elems.push(nxt);
|
|
1272
|
+
depth += unquotedParenDelta(nxt);
|
|
1273
|
+
next();
|
|
1274
|
+
}
|
|
1275
|
+
if (depth > 0) {
|
|
1276
|
+
throw new FauxnixParseError('fauxnix: unclosed array assignment; close with `)` or use A=value for a scalar');
|
|
1277
|
+
}
|
|
1278
|
+
const values = stripArrayParens(elems);
|
|
1279
|
+
assignments.push({
|
|
1280
|
+
name: split.name,
|
|
1281
|
+
value: values.length > 0 ? values[0] : [],
|
|
1282
|
+
values,
|
|
1283
|
+
});
|
|
1284
|
+
continue;
|
|
1285
|
+
}
|
|
902
1286
|
assignments.push(split);
|
|
903
1287
|
continue;
|
|
904
1288
|
}
|
|
@@ -965,6 +1349,9 @@ export function parseCommand(input) {
|
|
|
965
1349
|
if (assignments.length > 0) {
|
|
966
1350
|
return { kind: 'SimpleCommand', assignments, name: null, args: [], redirects };
|
|
967
1351
|
}
|
|
1352
|
+
if (peek().type === 'OP' && peek().op === '&') {
|
|
1353
|
+
throw new FauxnixParseError(BACKGROUND_MSG);
|
|
1354
|
+
}
|
|
968
1355
|
throw new FauxnixParseError('fauxnix: expected a command');
|
|
969
1356
|
}
|
|
970
1357
|
if (isUnquotedLiteral(name, '[[')) {
|
|
@@ -1061,6 +1448,16 @@ export function parseCommand(input) {
|
|
|
1061
1448
|
}
|
|
1062
1449
|
return parseList();
|
|
1063
1450
|
}
|
|
1451
|
+
function isFunctionDef(cmd) {
|
|
1452
|
+
if (!cmd.name)
|
|
1453
|
+
return false;
|
|
1454
|
+
const n = wordToString(cmd.name);
|
|
1455
|
+
if (!isUnquotedLiteral(cmd.name, n))
|
|
1456
|
+
return false;
|
|
1457
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*\(\)$/.test(n))
|
|
1458
|
+
return true;
|
|
1459
|
+
return cmd.args.length > 0 && isUnquotedLiteral(cmd.args[0], '()');
|
|
1460
|
+
}
|
|
1064
1461
|
function isRedirectOp(op) {
|
|
1065
1462
|
if (!op)
|
|
1066
1463
|
return false;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare const POWERSHELL_ARGS: readonly ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass"];
|
|
2
|
+
export type PowerShellEdition = 'Desktop' | 'Core';
|
|
3
|
+
export interface PowerShellSelection {
|
|
4
|
+
/** Absolute executable path resolved once for this check/session. */
|
|
5
|
+
readonly executable: string;
|
|
6
|
+
readonly expectedEdition: PowerShellEdition;
|
|
7
|
+
readonly configured: boolean;
|
|
8
|
+
readonly requested?: string;
|
|
9
|
+
readonly error?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface PowerShellResolveOptions {
|
|
12
|
+
cwd?: string;
|
|
13
|
+
exists?: (candidate: string) => boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Select the process-wide PowerShell host. FAUXNIX_PS is intentionally a
|
|
17
|
+
* small enum, not a command line: spawn() receives one executable and the
|
|
18
|
+
* fixed fauxnix arguments separately.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolvePowerShell(env?: NodeJS.ProcessEnv, options?: PowerShellResolveOptions): PowerShellSelection;
|
|
21
|
+
export declare function powerShellDisplay(selection: PowerShellSelection): string;
|
|
22
|
+
export declare function powerShellMissingMessage(selection: PowerShellSelection): string;
|