fauxnix-cli 0.9.2 → 0.11.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 +54 -12
- package/dist/ast.d.ts +38 -3
- package/dist/ast.js +22 -2
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +25 -1
- package/dist/commands/archive.d.ts +2 -1
- package/dist/commands/archive.js +85 -3
- package/dist/commands/files.js +4 -2
- package/dist/commands/install-all.js +2 -1
- package/dist/commands/net.js +3 -4
- package/dist/commands/sysinfo.js +48 -12
- package/dist/commands/text-filters.d.ts +1 -0
- package/dist/commands/text-filters.js +96 -38
- package/dist/commands/text-io.js +89 -10
- package/dist/doctor.d.ts +20 -0
- package/dist/doctor.js +251 -0
- package/dist/errors.d.ts +4 -0
- package/dist/errors.js +18 -5
- package/dist/executor.js +92 -102
- package/dist/install.d.ts +15 -0
- package/dist/install.js +206 -0
- package/dist/mcp.d.ts +6 -0
- package/dist/mcp.js +34 -12
- package/dist/parser.js +422 -33
- package/dist/registry.d.ts +2 -0
- package/dist/registry.js +4 -1
- package/dist/translator.d.ts +29 -3
- package/dist/translator.js +320 -29
- package/package.json +1 -1
package/dist/translator.d.ts
CHANGED
|
@@ -1,12 +1,35 @@
|
|
|
1
|
-
import { Assignment, CommandList, Redirect, SimpleCommand, IfCommand, ForCommand, Word, WordPart } from './ast.js';
|
|
1
|
+
import { Assignment, CommandList, Redirect, SimpleCommand, IfCommand, ForCommand, WhileCommand, CaseCommand, Word, WordPart } from './ast.js';
|
|
2
2
|
import { PipelineCtx } from './registry.js';
|
|
3
3
|
/** `${name:-word}` and friends using case-exact fx-scalar0. */
|
|
4
4
|
export declare function paramExpr(name: string, op: ':-' | ':=' | ':+' | ':?' | '-' | '+' | '?', word: string): string;
|
|
5
|
+
export declare function varExtraOf(p: WordPart): {
|
|
6
|
+
replace?: {
|
|
7
|
+
global: boolean;
|
|
8
|
+
pat: string;
|
|
9
|
+
repl: string;
|
|
10
|
+
};
|
|
11
|
+
slice?: {
|
|
12
|
+
offset: string;
|
|
13
|
+
length?: string;
|
|
14
|
+
};
|
|
15
|
+
} | undefined;
|
|
5
16
|
/** Map a bash $VAR name to a PowerShell expression (usable inside $(...)). */
|
|
6
17
|
export declare function varExpr(name: string, index?: string, param?: {
|
|
7
18
|
op: ':-' | ':=' | ':+' | ':?' | '-' | '+' | '?';
|
|
8
19
|
word: string;
|
|
9
|
-
}, length?: boolean
|
|
20
|
+
}, length?: boolean, extra?: {
|
|
21
|
+
replace?: {
|
|
22
|
+
global: boolean;
|
|
23
|
+
pat: string;
|
|
24
|
+
repl: string;
|
|
25
|
+
};
|
|
26
|
+
slice?: {
|
|
27
|
+
offset: string;
|
|
28
|
+
length?: string;
|
|
29
|
+
};
|
|
30
|
+
}): string;
|
|
31
|
+
/** `$?` `$$` `$0`–`$n` `$#` `$@` `$*` — not ordinary `$env:` names. */
|
|
32
|
+
export declare function isSpecialShellVar(name: string): boolean;
|
|
10
33
|
/** Escape text destined for the inside of a PS double-quoted string. */
|
|
11
34
|
export declare function escapeDq(s: string): string;
|
|
12
35
|
/** Normalize a literal POSIX-ish path to its Windows equivalent. */
|
|
@@ -39,6 +62,7 @@ export declare function operandExpr(w: Word): string;
|
|
|
39
62
|
/**
|
|
40
63
|
* `${name[@]}` / `"pre${name[@]}post"` — one argv per element.
|
|
41
64
|
* Unquoted `${name[*]}` also splats (bash); quoted `"${name[*]}"` stays one join.
|
|
65
|
+
* `$@` / unquoted `$*` splat like `${arr[@]}`; quoted `"$@"` still splats.
|
|
42
66
|
*/
|
|
43
67
|
export declare function splatSpec(w: Word): {
|
|
44
68
|
name: string;
|
|
@@ -53,6 +77,8 @@ export declare function argListExpr(words: Word[], fn?: (w: Word) => string): st
|
|
|
53
77
|
* Unquoted command words join non-empty lines with a space (IFS
|
|
54
78
|
* word-split approximation). Handlers often emit one string object, so
|
|
55
79
|
* a bare `$(…)` interpolation would keep those newlines.
|
|
80
|
+
* Lists (`;` `&&` `||`) reuse translateListInline inside the fx-csub
|
|
81
|
+
* scriptblock so the newline contract is unchanged.
|
|
56
82
|
*/
|
|
57
83
|
export declare function translateCmdSub(cmdText: string, keepNl?: boolean): string;
|
|
58
84
|
export declare function translateSimple(cmd: SimpleCommand, position: PipelineCtx['position'], hasStdin: boolean): string;
|
|
@@ -76,7 +102,7 @@ export interface PipelineParts {
|
|
|
76
102
|
call: string;
|
|
77
103
|
}
|
|
78
104
|
export declare function translatePipelineBody(p: {
|
|
79
|
-
commands: Array<SimpleCommand | IfCommand | ForCommand>;
|
|
105
|
+
commands: Array<SimpleCommand | IfCommand | ForCommand | WhileCommand | CaseCommand>;
|
|
80
106
|
}): PipelineParts;
|
|
81
107
|
export interface SegmentPlan {
|
|
82
108
|
op: ';' | '&&' | '||';
|
package/dist/translator.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { FauxnixParseError, isUnquotedLiteral, } from './ast.js';
|
|
2
2
|
import { parseCommand } from './parser.js';
|
|
3
3
|
import { lookup, psStr } from './registry.js';
|
|
4
|
+
import { PYTHON3_WINDOWS_HINT, SH_SCRIPT_WINDOWS_HINT } from './errors.js';
|
|
4
5
|
/* ------------------------------------------------------------------ */
|
|
5
6
|
/* Variable mapping */
|
|
6
7
|
/* ------------------------------------------------------------------ */
|
|
@@ -38,8 +39,43 @@ export function paramExpr(name, op, word) {
|
|
|
38
39
|
psStr('bash: ' + msg) +
|
|
39
40
|
'); $script:fx_exit = 1; \'\' } else { $fx_pv } )');
|
|
40
41
|
}
|
|
42
|
+
function sliceArgExpr(s) {
|
|
43
|
+
if (s.length > 0 && s[0] === '$') {
|
|
44
|
+
return '(fx-scalar0 ' + psStr(s.slice(1)) + ')';
|
|
45
|
+
}
|
|
46
|
+
return psStr(s);
|
|
47
|
+
}
|
|
48
|
+
export function varExtraOf(p) {
|
|
49
|
+
if (p.kind !== 'Var')
|
|
50
|
+
return undefined;
|
|
51
|
+
if (!p.replace && !p.slice)
|
|
52
|
+
return undefined;
|
|
53
|
+
const extra = {};
|
|
54
|
+
if (p.replace)
|
|
55
|
+
extra.replace = p.replace;
|
|
56
|
+
if (p.slice)
|
|
57
|
+
extra.slice = p.slice;
|
|
58
|
+
return extra;
|
|
59
|
+
}
|
|
41
60
|
/** Map a bash $VAR name to a PowerShell expression (usable inside $(...)). */
|
|
42
|
-
export function varExpr(name, index, param, length = false) {
|
|
61
|
+
export function varExpr(name, index, param, length = false, extra) {
|
|
62
|
+
if (extra && extra.replace) {
|
|
63
|
+
const r = extra.replace;
|
|
64
|
+
return ('(fx-subst (fx-scalar0 ' +
|
|
65
|
+
psStr(name) +
|
|
66
|
+
') ' +
|
|
67
|
+
psStr(r.pat) +
|
|
68
|
+
' ' +
|
|
69
|
+
psStr(r.repl) +
|
|
70
|
+
' ' +
|
|
71
|
+
(r.global ? '$true' : '$false') +
|
|
72
|
+
')');
|
|
73
|
+
}
|
|
74
|
+
if (extra && extra.slice) {
|
|
75
|
+
const off = sliceArgExpr(extra.slice.offset);
|
|
76
|
+
const len = extra.slice.length !== undefined ? sliceArgExpr(extra.slice.length) : '$null';
|
|
77
|
+
return '(fx-slice (fx-scalar0 ' + psStr(name) + ') ' + off + ' ' + len + ')';
|
|
78
|
+
}
|
|
43
79
|
if (param)
|
|
44
80
|
return paramExpr(name, param.op, param.word);
|
|
45
81
|
if (length) {
|
|
@@ -59,6 +95,12 @@ export function varExpr(name, index, param, length = false) {
|
|
|
59
95
|
if (index !== undefined) {
|
|
60
96
|
return '(fx-subget ' + psStr(name) + ' ' + psStr(index) + ')';
|
|
61
97
|
}
|
|
98
|
+
if (/^[0-9]+$/.test(name)) {
|
|
99
|
+
if (name === '0') {
|
|
100
|
+
return "$(if ($env:FAUXNIX_ARG0) { [string]$env:FAUXNIX_ARG0 } else { 'fauxnix' })";
|
|
101
|
+
}
|
|
102
|
+
return '(fx-posget ' + name + ')';
|
|
103
|
+
}
|
|
62
104
|
switch (name) {
|
|
63
105
|
case 'HOME':
|
|
64
106
|
return '$HOME';
|
|
@@ -81,10 +123,24 @@ export function varExpr(name, index, param, length = false) {
|
|
|
81
123
|
return '[string]$PID';
|
|
82
124
|
case 'HOSTNAME':
|
|
83
125
|
return '$env:COMPUTERNAME';
|
|
126
|
+
case '#':
|
|
127
|
+
return '@(fx-posload).Count';
|
|
128
|
+
case '@':
|
|
129
|
+
case '*':
|
|
130
|
+
return '((@(fx-posload) -join (fx-ifs1)))';
|
|
84
131
|
default:
|
|
85
132
|
return '$env:' + name;
|
|
86
133
|
}
|
|
87
134
|
}
|
|
135
|
+
/** `$?` `$$` `$0`–`$n` `$#` `$@` `$*` — not ordinary `$env:` names. */
|
|
136
|
+
export function isSpecialShellVar(name) {
|
|
137
|
+
return (name === '?' ||
|
|
138
|
+
name === '$' ||
|
|
139
|
+
name === '#' ||
|
|
140
|
+
name === '@' ||
|
|
141
|
+
name === '*' ||
|
|
142
|
+
/^[0-9]+$/.test(name));
|
|
143
|
+
}
|
|
88
144
|
/* ------------------------------------------------------------------ */
|
|
89
145
|
/* Word → PowerShell expression */
|
|
90
146
|
/* ------------------------------------------------------------------ */
|
|
@@ -154,7 +210,7 @@ export function exprOfWord(w, opts) {
|
|
|
154
210
|
// single bare variable → bare expression
|
|
155
211
|
if (expanded.length === 1 && expanded[0].kind === 'Var') {
|
|
156
212
|
const v = expanded[0];
|
|
157
|
-
return varExpr(v.name, v.index, v.param, v.length === true);
|
|
213
|
+
return varExpr(v.name, v.index, v.param, v.length === true, varExtraOf(v));
|
|
158
214
|
}
|
|
159
215
|
// Bare `$(...)` must not sit inside a PS expandable string: the
|
|
160
216
|
// substitution body contains `"` / `$_` that would break interpolation.
|
|
@@ -184,7 +240,7 @@ export function exprOfWord(w, opts) {
|
|
|
184
240
|
emitPart(q, true);
|
|
185
241
|
break;
|
|
186
242
|
case 'Var':
|
|
187
|
-
out += '$(' + varExpr(p.name, p.index, p.param, p.length === true) + ')';
|
|
243
|
+
out += '$(' + varExpr(p.name, p.index, p.param, p.length === true, varExtraOf(p)) + ')';
|
|
188
244
|
break;
|
|
189
245
|
case 'CmdSub':
|
|
190
246
|
out += '$(' + translateCmdSub(p.cmd, quoted || opts?.preserveCmdSub === true) + ')';
|
|
@@ -240,7 +296,7 @@ function arithSourceExpr(parts) {
|
|
|
240
296
|
emit(q);
|
|
241
297
|
break;
|
|
242
298
|
case 'Var':
|
|
243
|
-
out += '$(' + varExpr(p.name, p.index, p.param, p.length === true) + ')';
|
|
299
|
+
out += '$(' + varExpr(p.name, p.index, p.param, p.length === true, varExtraOf(p)) + ')';
|
|
244
300
|
break;
|
|
245
301
|
case 'CmdSub':
|
|
246
302
|
out += '$(' + translateCmdSub(p.cmd, true) + ')';
|
|
@@ -291,6 +347,7 @@ function wordPartsForSplat(w) {
|
|
|
291
347
|
/**
|
|
292
348
|
* `${name[@]}` / `"pre${name[@]}post"` — one argv per element.
|
|
293
349
|
* Unquoted `${name[*]}` also splats (bash); quoted `"${name[*]}"` stays one join.
|
|
350
|
+
* `$@` / unquoted `$*` splat like `${arr[@]}`; quoted `"$@"` still splats.
|
|
294
351
|
*/
|
|
295
352
|
export function splatSpec(w) {
|
|
296
353
|
const parts = wordPartsForSplat(w);
|
|
@@ -301,7 +358,10 @@ export function splatSpec(w) {
|
|
|
301
358
|
for (const { part: p, quoted } of parts) {
|
|
302
359
|
const splat = p.kind === 'Var' &&
|
|
303
360
|
!p.length &&
|
|
304
|
-
(p.
|
|
361
|
+
(p.name === '@' ||
|
|
362
|
+
(p.name === '*' && !quoted) ||
|
|
363
|
+
p.index === '@' ||
|
|
364
|
+
(p.index === '*' && !quoted));
|
|
305
365
|
if (splat) {
|
|
306
366
|
if (seen)
|
|
307
367
|
return null;
|
|
@@ -318,6 +378,12 @@ export function splatSpec(w) {
|
|
|
318
378
|
}
|
|
319
379
|
return name ? { name, prefix, suffix } : null;
|
|
320
380
|
}
|
|
381
|
+
/** Load the splat source: positionals (`@`/`*`) vs named arrays. */
|
|
382
|
+
function splatLoadCall(name) {
|
|
383
|
+
if (name === '@' || name === '*')
|
|
384
|
+
return 'fx-posload';
|
|
385
|
+
return 'fx-arrload ' + psStr(name);
|
|
386
|
+
}
|
|
321
387
|
/** PS expression of a string[]: `@` words splat, others stay one element. */
|
|
322
388
|
export function argListExpr(words, fn = exprOfWord) {
|
|
323
389
|
if (words.length === 0)
|
|
@@ -329,9 +395,9 @@ export function argListExpr(words, fn = exprOfWord) {
|
|
|
329
395
|
if (!s)
|
|
330
396
|
return '@(' + fn(w) + ')';
|
|
331
397
|
if (!s.prefix && !s.suffix)
|
|
332
|
-
return '@(
|
|
333
|
-
return ('@($( $fx_sp = @(
|
|
334
|
-
|
|
398
|
+
return '@(' + splatLoadCall(s.name) + ')';
|
|
399
|
+
return ('@($( $fx_sp = @(' +
|
|
400
|
+
splatLoadCall(s.name) +
|
|
335
401
|
'); if ($fx_sp.Count -eq 0) { $fx_sp = @(' +
|
|
336
402
|
psStr(s.prefix + s.suffix) +
|
|
337
403
|
') } else { $fx_sp[0] = ' +
|
|
@@ -352,14 +418,11 @@ export function argListExpr(words, fn = exprOfWord) {
|
|
|
352
418
|
* Unquoted command words join non-empty lines with a space (IFS
|
|
353
419
|
* word-split approximation). Handlers often emit one string object, so
|
|
354
420
|
* a bare `$(…)` interpolation would keep those newlines.
|
|
421
|
+
* Lists (`;` `&&` `||`) reuse translateListInline inside the fx-csub
|
|
422
|
+
* scriptblock so the newline contract is unchanged.
|
|
355
423
|
*/
|
|
356
424
|
export function translateCmdSub(cmdText, keepNl = false) {
|
|
357
|
-
const
|
|
358
|
-
if (list.segments.length !== 1) {
|
|
359
|
-
throw new FauxnixParseError('fauxnix: command substitution with ; && || is not supported yet');
|
|
360
|
-
}
|
|
361
|
-
const { defs, call } = translatePipelineBody(list.segments[0].pipeline);
|
|
362
|
-
const inner = defs ? defs + '\n' + call : call;
|
|
425
|
+
const inner = translateListInline(parseCommand(cmdText));
|
|
363
426
|
const collected = '(fx-csub { ' + inner + ' })';
|
|
364
427
|
if (keepNl)
|
|
365
428
|
return collected;
|
|
@@ -383,11 +446,21 @@ export function translateSimple(cmd, position, hasStdin) {
|
|
|
383
446
|
// deviation: shell var vs exported var are indistinguishable here).
|
|
384
447
|
if (cmd.name === null) {
|
|
385
448
|
const exportHandler = lookup('export');
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
449
|
+
const chunks = [];
|
|
450
|
+
for (const a of cmd.assignments) {
|
|
451
|
+
if (a.values) {
|
|
452
|
+
chunks.push('fx-arrput ' +
|
|
453
|
+
psStr(a.name) +
|
|
454
|
+
' ' +
|
|
455
|
+
argListExpr(a.values, (w) => exprOfWord(w, { preserveCmdSub: true })));
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
const words = [[{ kind: 'Text', text: a.name + '=' }, ...a.value]];
|
|
459
|
+
if (exportHandler)
|
|
460
|
+
chunks.push(exportHandler(words, { position, hasStdin }));
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return chunks.join('\n');
|
|
391
464
|
}
|
|
392
465
|
const nameLit = literalOfWord(cmd.name);
|
|
393
466
|
const nameSplat = splatSpec(cmd.name);
|
|
@@ -404,7 +477,7 @@ export function translateSimple(cmd, position, hasStdin) {
|
|
|
404
477
|
redirects: cmd.redirects,
|
|
405
478
|
}, position, hasStdin);
|
|
406
479
|
const emptyCmdLines = [
|
|
407
|
-
'$fx_cw = @(
|
|
480
|
+
'$fx_cw = @(' + splatLoadCall(nameSplat.name) + ')',
|
|
408
481
|
];
|
|
409
482
|
if (hasAffix) {
|
|
410
483
|
emptyCmdLines.push('if ($fx_cw.Count -eq 0) { $fx_cw = @(' +
|
|
@@ -546,10 +619,20 @@ export function wrapTempEnv(sets, body, extra) {
|
|
|
546
619
|
lines.push(arrSave + '[' + psStr(n) + '] = (fx-arrpackget ' + psStr(n) + ')');
|
|
547
620
|
}
|
|
548
621
|
const valVars = [];
|
|
622
|
+
const valIsArr = [];
|
|
549
623
|
for (let i = 0; i < sets.length; i++) {
|
|
550
624
|
const vn = '$fx_ev' + id + '_' + i;
|
|
551
625
|
valVars.push(vn);
|
|
552
|
-
|
|
626
|
+
if (sets[i].values) {
|
|
627
|
+
valIsArr.push(true);
|
|
628
|
+
lines.push(vn +
|
|
629
|
+
' = ' +
|
|
630
|
+
argListExpr(sets[i].values, (w) => exprOfWord(w, { preserveCmdSub: true })));
|
|
631
|
+
}
|
|
632
|
+
else {
|
|
633
|
+
valIsArr.push(false);
|
|
634
|
+
lines.push(vn + ' = ' + exprOfWord(sets[i].value, { preserveCmdSub: true }));
|
|
635
|
+
}
|
|
553
636
|
}
|
|
554
637
|
lines.push('try {');
|
|
555
638
|
for (const u of unsets) {
|
|
@@ -575,6 +658,10 @@ export function wrapTempEnv(sets, body, extra) {
|
|
|
575
658
|
for (let i = 0; i < sets.length; i++) {
|
|
576
659
|
const n = sets[i].name;
|
|
577
660
|
const nq = n.replace(/'/g, "''");
|
|
661
|
+
if (valIsArr[i]) {
|
|
662
|
+
lines.push(' fx-arrput ' + psStr(n) + ' ' + valVars[i]);
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
578
665
|
lines.push(' $env:' + n + ' = ' + valVars[i]);
|
|
579
666
|
lines.push(' fx-arrdrop ' + psStr(n));
|
|
580
667
|
lines.push(" $env:FAUXNIX_SETVARS = ((@($env:FAUXNIX_SETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne '" +
|
|
@@ -752,6 +839,22 @@ function translateIf(cmd) {
|
|
|
752
839
|
lines.push('}');
|
|
753
840
|
return lines.join('\n');
|
|
754
841
|
}
|
|
842
|
+
function translateCase(cmd) {
|
|
843
|
+
const lines = ['$script:fx_exit = 0', '$fx_cw = ' + exprOfWord(cmd.word)];
|
|
844
|
+
for (let i = 0; i < cmd.arms.length; i++) {
|
|
845
|
+
const arm = cmd.arms[i];
|
|
846
|
+
const pats = arm.patterns.map((w) => exprOfWord(w)).join(',');
|
|
847
|
+
const head = i === 0 ? 'if' : 'elseif';
|
|
848
|
+
lines.push(head + ' (fx-casematch $fx_cw @(' + pats + ')) {');
|
|
849
|
+
const body = translateListInline(arm.body);
|
|
850
|
+
if (body) {
|
|
851
|
+
for (const l of body.split('\n'))
|
|
852
|
+
lines.push(l ? ' ' + l : l);
|
|
853
|
+
}
|
|
854
|
+
lines.push('}');
|
|
855
|
+
}
|
|
856
|
+
return lines.join('\n');
|
|
857
|
+
}
|
|
755
858
|
function translateFor(cmd) {
|
|
756
859
|
const n = cmd.name.replace(/'/g, "''");
|
|
757
860
|
const lines = [
|
|
@@ -779,6 +882,22 @@ function translateFor(cmd) {
|
|
|
779
882
|
lines.push('}');
|
|
780
883
|
return lines.join('\n');
|
|
781
884
|
}
|
|
885
|
+
function translateWhile(cmd) {
|
|
886
|
+
// Bash: last executed body owns status; a test that ends the loop does not.
|
|
887
|
+
// Never-entered loops (`while false; do …; done`, `until true; do …; done`)
|
|
888
|
+
// exit 0. Save the body status and restore it on the failing test.
|
|
889
|
+
const fail = cmd.until ? '$script:fx_exit -eq 0' : '$script:fx_exit -ne 0';
|
|
890
|
+
const lines = ['$fx_wst = 0', 'do {'];
|
|
891
|
+
for (const l of translateListInline(cmd.test).split('\n'))
|
|
892
|
+
lines.push(l ? ' ' + l : l);
|
|
893
|
+
lines.push(' if (' + fail + ') { $script:fx_exit = $fx_wst; break }');
|
|
894
|
+
lines.push(' $script:fx_exit = 0');
|
|
895
|
+
for (const l of translateListInline(cmd.body).split('\n'))
|
|
896
|
+
lines.push(l ? ' ' + l : l);
|
|
897
|
+
lines.push(' $fx_wst = $script:fx_exit');
|
|
898
|
+
lines.push('} while ($true)');
|
|
899
|
+
return lines.join('\n');
|
|
900
|
+
}
|
|
782
901
|
function stdinTarget(c) {
|
|
783
902
|
let t = null;
|
|
784
903
|
for (const r of c.redirects) {
|
|
@@ -793,7 +912,24 @@ function stdinReadExpr(target) {
|
|
|
793
912
|
return '@()';
|
|
794
913
|
return '@(fx-readlines ' + pathExpr(n) + ')';
|
|
795
914
|
}
|
|
915
|
+
/** `>` `>>` `&>` `&>>` — Node last-stage apply cannot honor these on earlier stages. */
|
|
916
|
+
function isStdoutFileRedirect(op) {
|
|
917
|
+
return op === '>' || op === '>>' || op === '&>' || op === '&>>';
|
|
918
|
+
}
|
|
919
|
+
const NONLAST_STDOUT_REDIRECT_MSG = 'fauxnix: stdout redirect on a non-last pipeline stage is not supported yet; write the file in a previous list segment (cmd >f; cat f) or wait for per-stage fds (#157)';
|
|
920
|
+
function rejectNonLastStdoutRedirects(commands) {
|
|
921
|
+
if (commands.length < 2)
|
|
922
|
+
return;
|
|
923
|
+
for (let i = 0; i < commands.length - 1; i++) {
|
|
924
|
+
if (commands[i].redirects.some((r) => isStdoutFileRedirect(r.op))) {
|
|
925
|
+
throw new FauxnixParseError(NONLAST_STDOUT_REDIRECT_MSG);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
796
929
|
export function translatePipelineBody(p) {
|
|
930
|
+
// Last-stage `>` is Node apply; a non-last `>` would truncate the file and
|
|
931
|
+
// still feed the pipe. Fail loud until in-stage writes (#157).
|
|
932
|
+
rejectNonLastStdoutRedirects(p.commands);
|
|
797
933
|
// Every pipeline stage needs its own status slot. Handlers deliberately use
|
|
798
934
|
// `$script:fx_exit` because their helper functions run in child scopes; in a
|
|
799
935
|
// pipeline that shared flag lets an earlier failure leak into a successful
|
|
@@ -809,6 +945,10 @@ export function translatePipelineBody(p) {
|
|
|
809
945
|
bodies.push(translateIf(c));
|
|
810
946
|
else if (c.kind === 'For')
|
|
811
947
|
bodies.push(translateFor(c));
|
|
948
|
+
else if (c.kind === 'While')
|
|
949
|
+
bodies.push(translateWhile(c));
|
|
950
|
+
else if (c.kind === 'Case')
|
|
951
|
+
bodies.push(translateCase(c));
|
|
812
952
|
else
|
|
813
953
|
bodies.push(translateSimple(c, position, hasStdin));
|
|
814
954
|
}
|
|
@@ -912,6 +1052,10 @@ const WRAP_HELPER_ORDER = [
|
|
|
912
1052
|
'fx-csub',
|
|
913
1053
|
'fx-svenc',
|
|
914
1054
|
'fx-svdec',
|
|
1055
|
+
'fx-posload',
|
|
1056
|
+
'fx-posset',
|
|
1057
|
+
'fx-posget',
|
|
1058
|
+
'fx-posshift',
|
|
915
1059
|
'fx-arrload',
|
|
916
1060
|
'fx-scalar0',
|
|
917
1061
|
'fx-ifs1',
|
|
@@ -922,6 +1066,9 @@ const WRAP_HELPER_ORDER = [
|
|
|
922
1066
|
'fx-arrput',
|
|
923
1067
|
'fx-arrclr',
|
|
924
1068
|
'fx-subget',
|
|
1069
|
+
'fx-casematch',
|
|
1070
|
+
'fx-subst',
|
|
1071
|
+
'fx-slice',
|
|
925
1072
|
'fx-winargv',
|
|
926
1073
|
'fx-native',
|
|
927
1074
|
];
|
|
@@ -930,6 +1077,10 @@ const WRAP_HELPER_DEPS = {
|
|
|
930
1077
|
'fx-csub': [],
|
|
931
1078
|
'fx-svenc': [],
|
|
932
1079
|
'fx-svdec': [],
|
|
1080
|
+
'fx-posload': ['fx-svdec'],
|
|
1081
|
+
'fx-posset': ['fx-svenc'],
|
|
1082
|
+
'fx-posget': ['fx-posload'],
|
|
1083
|
+
'fx-posshift': ['fx-posload', 'fx-posset'],
|
|
933
1084
|
'fx-arrload': ['fx-scalar0', 'fx-svdec'],
|
|
934
1085
|
'fx-scalar0': ['fx-svdec'],
|
|
935
1086
|
'fx-ifs1': ['fx-scalar0'],
|
|
@@ -940,6 +1091,9 @@ const WRAP_HELPER_DEPS = {
|
|
|
940
1091
|
'fx-arrput': ['fx-arrdrop', 'fx-svenc'],
|
|
941
1092
|
'fx-arrclr': ['fx-arrdrop'],
|
|
942
1093
|
'fx-subget': ['fx-arrload', 'fx-ifs1'],
|
|
1094
|
+
'fx-casematch': [],
|
|
1095
|
+
'fx-subst': [],
|
|
1096
|
+
'fx-slice': [],
|
|
943
1097
|
'fx-winargv': [],
|
|
944
1098
|
'fx-native': ['fx-winargv'],
|
|
945
1099
|
};
|
|
@@ -1057,7 +1211,16 @@ export function wrapScript(body, opts = {}) {
|
|
|
1057
1211
|
' $script:fx_csub = $true',
|
|
1058
1212
|
' try { $fx_o = @(& $b | ForEach-Object { [string]$_ }) }',
|
|
1059
1213
|
' finally { $script:fx_csub = $fx_prevcs }',
|
|
1060
|
-
|
|
1214
|
+
// Line items (no trailing NL) get a NL between them. Chunks that
|
|
1215
|
+
// already end in NL concatenate, so $(echo a; echo b) is a\nb.
|
|
1216
|
+
" $fx_s = ''",
|
|
1217
|
+
' foreach ($fx_x in $fx_o) {',
|
|
1218
|
+
' $fx_t = [string]$fx_x',
|
|
1219
|
+
' if ($fx_s.Length -gt 0 -and $fx_s[$fx_s.Length - 1] -ne [char]10) {',
|
|
1220
|
+
' $fx_s += [string][char]10',
|
|
1221
|
+
' }',
|
|
1222
|
+
' $fx_s += $fx_t',
|
|
1223
|
+
' }',
|
|
1061
1224
|
' while ($fx_s.Length -gt 0 -and $fx_s[$fx_s.Length - 1] -eq [char]10) {',
|
|
1062
1225
|
' $fx_s = $fx_s.Substring(0, $fx_s.Length - 1)',
|
|
1063
1226
|
' }',
|
|
@@ -1088,6 +1251,52 @@ export function wrapScript(body, opts = {}) {
|
|
|
1088
1251
|
' return [string]$sb',
|
|
1089
1252
|
'}',
|
|
1090
1253
|
],
|
|
1254
|
+
'fx-posload': [
|
|
1255
|
+
'function fx-posload {',
|
|
1256
|
+
' if ($null -eq $env:FAUXNIX_POS -or [string]$env:FAUXNIX_POS -eq \'\') { return @() }',
|
|
1257
|
+
' $out = @()',
|
|
1258
|
+
' foreach ($el in @($env:FAUXNIX_POS -split [string][char]30)) { $out += ,(fx-svdec $el) }',
|
|
1259
|
+
' return $out',
|
|
1260
|
+
'}',
|
|
1261
|
+
],
|
|
1262
|
+
'fx-posset': [
|
|
1263
|
+
'function fx-posset($vals) {',
|
|
1264
|
+
' if ($null -eq $vals) { $vals = @() }',
|
|
1265
|
+
' $vals = @($vals)',
|
|
1266
|
+
' if ($vals.Count -eq 0) { $env:FAUXNIX_POS = \'\'; return }',
|
|
1267
|
+
' $encs = @(); foreach ($v in $vals) { $encs += (fx-svenc $v) }',
|
|
1268
|
+
' $env:FAUXNIX_POS = ($encs -join [string][char]30)',
|
|
1269
|
+
'}',
|
|
1270
|
+
],
|
|
1271
|
+
'fx-posget': [
|
|
1272
|
+
'function fx-posget($i) {',
|
|
1273
|
+
' $arr = @(fx-posload)',
|
|
1274
|
+
' $n = 0',
|
|
1275
|
+
' if (-not [int]::TryParse([string]$i, [ref]$n)) { return \'\' }',
|
|
1276
|
+
' if ($n -lt 1 -or $n -gt $arr.Count) { return \'\' }',
|
|
1277
|
+
' return [string]$arr[$n - 1]',
|
|
1278
|
+
'}',
|
|
1279
|
+
],
|
|
1280
|
+
'fx-posshift': [
|
|
1281
|
+
'function fx-posshift($n) {',
|
|
1282
|
+
' $arr = @(fx-posload)',
|
|
1283
|
+
' $i = 1',
|
|
1284
|
+
' if ($null -ne $n -and [string]$n -ne \'\') {',
|
|
1285
|
+
' $parsed = 0',
|
|
1286
|
+
' if (-not [int]::TryParse([string]$n, [ref]$parsed)) {',
|
|
1287
|
+
' [Console]::Error.WriteLine(\'bash: shift: \' + [string]$n + \': numeric argument required\')',
|
|
1288
|
+
' $script:fx_exit = 1',
|
|
1289
|
+
' return',
|
|
1290
|
+
' }',
|
|
1291
|
+
' $i = $parsed',
|
|
1292
|
+
' }',
|
|
1293
|
+
' if ($i -lt 0 -or $i -gt $arr.Count) { $script:fx_exit = 1; return }',
|
|
1294
|
+
' if ($i -eq 0) { return }',
|
|
1295
|
+
' if ($i -eq $arr.Count) { fx-posset @(); return }',
|
|
1296
|
+
' $new = @($arr[$i..($arr.Count - 1)])',
|
|
1297
|
+
' fx-posset $new',
|
|
1298
|
+
'}',
|
|
1299
|
+
],
|
|
1091
1300
|
'fx-arrload': [
|
|
1092
1301
|
'function fx-arrload($n) {',
|
|
1093
1302
|
' $n = [string]$n',
|
|
@@ -1095,8 +1304,10 @@ export function wrapScript(body, opts = {}) {
|
|
|
1095
1304
|
' $fx_eq = $fx_pair.IndexOf([char]61)',
|
|
1096
1305
|
' if ($fx_eq -lt 1) { continue }',
|
|
1097
1306
|
' if ($fx_pair.Substring(0, $fx_eq) -cne $n) { continue }',
|
|
1307
|
+
' $pay = $fx_pair.Substring($fx_eq + 1)',
|
|
1308
|
+
' if ($pay -eq [string][char]1) { return @() }',
|
|
1098
1309
|
' $out = @()',
|
|
1099
|
-
' foreach ($el in @($
|
|
1310
|
+
' foreach ($el in @($pay -split [string][char]30)) { $out += ,(fx-svdec $el) }',
|
|
1100
1311
|
' return $out',
|
|
1101
1312
|
' }',
|
|
1102
1313
|
' $s0 = fx-scalar0 $n',
|
|
@@ -1178,9 +1389,12 @@ export function wrapScript(body, opts = {}) {
|
|
|
1178
1389
|
'fx-arrput': [
|
|
1179
1390
|
'function fx-arrput($n, $vals) {',
|
|
1180
1391
|
' $n = [string]$n',
|
|
1181
|
-
' $vals = @($vals)',
|
|
1392
|
+
' if ($null -eq $vals) { $vals = @() } else { $vals = @($vals) }',
|
|
1182
1393
|
' fx-arrdrop $n',
|
|
1183
|
-
' if ($vals.Count -eq 0) {
|
|
1394
|
+
' if ($vals.Count -eq 0) {',
|
|
1395
|
+
// SOH payload: empty array, distinct from scalar '' and from A=('')
|
|
1396
|
+
" $env:FAUXNIX_ARRS = ((@($env:FAUXNIX_ARRS -split [string][char]10 | Where-Object { $_ -ne '' }) + ($n + [string][char]61 + [string][char]1)) -join [string][char]10)",
|
|
1397
|
+
' } else {',
|
|
1184
1398
|
' $encs = @(); foreach ($v in $vals) { $encs += (fx-svenc $v) }',
|
|
1185
1399
|
" $env:FAUXNIX_ARRS = ((@($env:FAUXNIX_ARRS -split [string][char]10 | Where-Object { $_ -ne '' }) + ($n + [string][char]61 + ($encs -join [string][char]30))) -join [string][char]10)",
|
|
1186
1400
|
' }',
|
|
@@ -1215,9 +1429,74 @@ export function wrapScript(body, opts = {}) {
|
|
|
1215
1429
|
' return [string]$arr[$i]',
|
|
1216
1430
|
'}',
|
|
1217
1431
|
],
|
|
1432
|
+
'fx-casematch': [
|
|
1433
|
+
'function fx-casematch($w, $pats) {',
|
|
1434
|
+
' $w = [string]$w',
|
|
1435
|
+
' foreach ($p in @($pats)) {',
|
|
1436
|
+
' $pat = [string]$p',
|
|
1437
|
+
' try {',
|
|
1438
|
+
' $wp = [WildcardPattern]::new($pat, [System.Management.Automation.WildcardOptions]::None)',
|
|
1439
|
+
' if ($wp.IsMatch($w)) { return $true }',
|
|
1440
|
+
' } catch {}',
|
|
1441
|
+
' }',
|
|
1442
|
+
' return $false',
|
|
1443
|
+
'}',
|
|
1444
|
+
],
|
|
1445
|
+
'fx-subst': [
|
|
1446
|
+
'function fx-subst($s, $pat, $repl, $all) {',
|
|
1447
|
+
' if ($null -eq $s) { return \'\' }',
|
|
1448
|
+
' $s = [string]$s',
|
|
1449
|
+
' $pat = [string]$pat',
|
|
1450
|
+
' $repl = [string]$repl',
|
|
1451
|
+
' if ($pat -eq \'\') { return $s }',
|
|
1452
|
+
' $glob = $false',
|
|
1453
|
+
' foreach ($fx_c in $pat.ToCharArray()) {',
|
|
1454
|
+
' if ($fx_c -eq [char]42 -or $fx_c -eq [char]63) { $glob = $true; break }',
|
|
1455
|
+
' }',
|
|
1456
|
+
' if (-not $glob) {',
|
|
1457
|
+
' if ($all) { return $s.Replace($pat, $repl) }',
|
|
1458
|
+
' $i = $s.IndexOf($pat)',
|
|
1459
|
+
' if ($i -lt 0) { return $s }',
|
|
1460
|
+
' return $s.Substring(0, $i) + $repl + $s.Substring($i + $pat.Length)',
|
|
1461
|
+
' }',
|
|
1462
|
+
' $sb = New-Object System.Text.StringBuilder',
|
|
1463
|
+
' foreach ($fx_c in $pat.ToCharArray()) {',
|
|
1464
|
+
' if ($fx_c -eq [char]42) { [void]$sb.Append(\'.*\'); continue }',
|
|
1465
|
+
' if ($fx_c -eq [char]63) { [void]$sb.Append(\'.\'); continue }',
|
|
1466
|
+
' [void]$sb.Append([regex]::Escape([string]$fx_c))',
|
|
1467
|
+
' }',
|
|
1468
|
+
' $rx = New-Object System.Text.RegularExpressions.Regex($sb.ToString(), [System.Text.RegularExpressions.RegexOptions]::Singleline)',
|
|
1469
|
+
' $fx_rep = $repl.Replace([string][char]36, ([string][char]36 + [string][char]36))',
|
|
1470
|
+
' if ($all) { return $rx.Replace($s, $fx_rep) }',
|
|
1471
|
+
' return $rx.Replace($s, $fx_rep, 1)',
|
|
1472
|
+
'}',
|
|
1473
|
+
],
|
|
1474
|
+
'fx-slice': [
|
|
1475
|
+
'function fx-slice($s, $off, $len) {',
|
|
1476
|
+
' if ($null -eq $s) { return \'\' }',
|
|
1477
|
+
' $s = [string]$s',
|
|
1478
|
+
' $n = $s.Length',
|
|
1479
|
+
' $o = 0',
|
|
1480
|
+
' if (-not [int]::TryParse([string]$off, [ref]$o)) { return \'\' }',
|
|
1481
|
+
' if ($o -lt 0) { $o = $n + $o }',
|
|
1482
|
+
' if ($o -lt 0 -or $o -ge $n) { return \'\' }',
|
|
1483
|
+
' if ($null -eq $len -or [string]$len -eq \'\') { return $s.Substring($o) }',
|
|
1484
|
+
' $l = 0',
|
|
1485
|
+
' if (-not [int]::TryParse([string]$len, [ref]$l)) { return \'\' }',
|
|
1486
|
+
' if ($l -lt 0) {',
|
|
1487
|
+
' $fx_end = $n + $l',
|
|
1488
|
+
' if ($fx_end -lt $o) { return \'\' }',
|
|
1489
|
+
' $l = $fx_end - $o',
|
|
1490
|
+
' }',
|
|
1491
|
+
' if ($l -le 0) { return \'\' }',
|
|
1492
|
+
' if (($o + $l) -gt $n) { $l = $n - $o }',
|
|
1493
|
+
' return $s.Substring($o, $l)',
|
|
1494
|
+
'}',
|
|
1495
|
+
],
|
|
1218
1496
|
'fx-winargv': [
|
|
1219
|
-
'function fx-winargv($argv) {',
|
|
1497
|
+
'function fx-winargv($argv, $cmdmeta) {',
|
|
1220
1498
|
// Empty [object[]] unwraps to $null on PS 5.1; @($null) is one empty arg.
|
|
1499
|
+
// $cmdmeta: also quote & | () <> ^ so cmd.exe /c does not split the tail.
|
|
1221
1500
|
' if ($null -eq $argv) { $argv = @() }',
|
|
1222
1501
|
' $parts = New-Object System.Collections.Generic.List[string]',
|
|
1223
1502
|
' foreach ($a in @($argv)) {',
|
|
@@ -1226,6 +1505,7 @@ export function wrapScript(body, opts = {}) {
|
|
|
1226
1505
|
' $need = $false',
|
|
1227
1506
|
' foreach ($ch in $s.ToCharArray()) {',
|
|
1228
1507
|
" if ($ch -eq ' ' -or $ch -eq ([char]9) -or $ch -eq [char]34) { $need = $true; break }",
|
|
1508
|
+
" if ($cmdmeta -and ($ch -eq '&' -or $ch -eq '|' -or $ch -eq '(' -or $ch -eq ')' -or $ch -eq '<' -or $ch -eq '>' -or $ch -eq '^')) { $need = $true; break }",
|
|
1229
1509
|
' }',
|
|
1230
1510
|
' if (-not $need) { $parts.Add($s); continue }',
|
|
1231
1511
|
' $sb = New-Object System.Text.StringBuilder',
|
|
@@ -1259,7 +1539,14 @@ export function wrapScript(body, opts = {}) {
|
|
|
1259
1539
|
// operator is only for names that are not executables.
|
|
1260
1540
|
' $cmd = Get-Command -Name $name -ErrorAction SilentlyContinue | Select-Object -First 1',
|
|
1261
1541
|
' if ($null -eq $cmd) {',
|
|
1262
|
-
"
|
|
1542
|
+
" $fx_nf = 'bash: ' + $name + ': command not found'",
|
|
1543
|
+
" $fx_n = [string]$name",
|
|
1544
|
+
// Hint only — never alias python3→python (wrong interpreter).
|
|
1545
|
+
" if ($fx_n -eq 'python3' -or $fx_n -eq 'python3.exe') { $fx_nf += '" +
|
|
1546
|
+
PYTHON3_WINDOWS_HINT +
|
|
1547
|
+
"' }",
|
|
1548
|
+
" elseif ($fx_n -like '*.sh') { $fx_nf += '" + SH_SCRIPT_WINDOWS_HINT + "' }",
|
|
1549
|
+
' [Console]::Error.WriteLine($fx_nf)',
|
|
1263
1550
|
' $script:fx_exit = 127',
|
|
1264
1551
|
' return',
|
|
1265
1552
|
' }',
|
|
@@ -1272,6 +1559,8 @@ export function wrapScript(body, opts = {}) {
|
|
|
1272
1559
|
' $psi = New-Object System.Diagnostics.ProcessStartInfo',
|
|
1273
1560
|
' $ext = [IO.Path]::GetExtension([string]$app.Source)',
|
|
1274
1561
|
// CreateProcess cannot launch .cmd/.bat with UseShellExecute=false (npm.cmd).
|
|
1562
|
+
// /s strips one outer quote pair from the /c tail; CRT-quote cmd
|
|
1563
|
+
// metacharacters so `&`/`|`/`()` do not start a second command.
|
|
1275
1564
|
" if ($ext -eq '.cmd' -or $ext -eq '.bat') {",
|
|
1276
1565
|
' $comspec = Get-Command -Name cmd -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1',
|
|
1277
1566
|
' if ($null -eq $comspec) {',
|
|
@@ -1280,8 +1569,10 @@ export function wrapScript(body, opts = {}) {
|
|
|
1280
1569
|
' return',
|
|
1281
1570
|
' }',
|
|
1282
1571
|
' $psi.FileName = $comspec.Source',
|
|
1283
|
-
' $
|
|
1284
|
-
|
|
1572
|
+
' $fx_app = fx-winargv $app.Source $true',
|
|
1573
|
+
' $fx_rest = fx-winargv $argv $true',
|
|
1574
|
+
" if ($fx_rest.Length -gt 0) { $fx_tail = $fx_app + ' ' + $fx_rest } else { $fx_tail = $fx_app }",
|
|
1575
|
+
' $psi.Arguments = \'/d /s /c "\' + $fx_tail + \'"\'',
|
|
1285
1576
|
' } else {',
|
|
1286
1577
|
' $psi.FileName = $app.Source',
|
|
1287
1578
|
' $psi.Arguments = fx-winargv $argv',
|
package/package.json
CHANGED