fauxnix-cli 0.4.2 → 0.5.1
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 +6 -1
- package/dist/ast.d.ts +27 -4
- package/dist/ast.js +4 -3
- package/dist/commands/sysinfo.js +184 -9
- package/dist/mcp.js +1 -1
- package/dist/parser.js +172 -3
- package/dist/translator.d.ts +10 -8
- package/dist/translator.js +398 -185
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -193,8 +193,13 @@ fauxnix optimizes for the commands agents actually run. Documented deviations:
|
|
|
193
193
|
word expansion precedes the temporary environment).
|
|
194
194
|
- `yes` is capped at 65,536 lines — PS 5.1 pipelines cannot signal upstream producers to stop, so
|
|
195
195
|
an unbounded `yes | head` would hang.
|
|
196
|
-
- `tail -f`, `
|
|
196
|
+
- `tail -f`, `eval`, `alias`, heredocs, `while`/`until`/`case`, word-level
|
|
197
|
+
`$((...))` arithmetic expansion
|
|
197
198
|
and background `&` are rejected with actionable error messages instead of misbehaving.
|
|
199
|
+
(`if/then/else/fi`, `for x in ...`, backtick substitution, `command -v`, pipeline `read`,
|
|
200
|
+
and dotenv-style `source` are supported.)
|
|
201
|
+
- `command -v <builtin>` prints `/usr/bin/<name>` where bash prints the bare builtin name;
|
|
202
|
+
exit codes and empty-result semantics match.
|
|
198
203
|
- `chmod` maps only the read-only bit; exec bits are no-ops on Windows. `chown` is a silent no-op
|
|
199
204
|
(as in Git Bash).
|
|
200
205
|
- `ps aux` columns are approximations (no per-process CPU% accounting, USER shows `?`).
|
package/dist/ast.d.ts
CHANGED
|
@@ -7,12 +7,13 @@
|
|
|
7
7
|
* - redirections: > >> 2> 2>> &> 2>&1 1>&2 <
|
|
8
8
|
* - quoting: 'literal' "interp $VAR $(cmd)"
|
|
9
9
|
* - variables: $VAR ${VAR} ${VAR[n]} plus special cases ($HOME $USER $PATH ...)
|
|
10
|
-
* - command substitution: $(...) (recursively translated)
|
|
10
|
+
* - command substitution: $(...) and `...` (recursively translated)
|
|
11
11
|
* - env assignment prefix: VAR=value cmd
|
|
12
12
|
*
|
|
13
13
|
* Explicitly unsupported (parser throws a helpful FauxnixError):
|
|
14
|
-
* heredocs,
|
|
15
|
-
* (
|
|
14
|
+
* heredocs, subshells (...), background &, while/until/case,
|
|
15
|
+
* word-level $((...)) arithmetic expansion, globs inside quotes,
|
|
16
|
+
* process substitution <(...).
|
|
16
17
|
*/
|
|
17
18
|
export interface CommandList {
|
|
18
19
|
kind: 'CommandList';
|
|
@@ -24,9 +25,24 @@ export interface ListSegment {
|
|
|
24
25
|
/** ';' for the first segment, otherwise the operator seen before this one. */
|
|
25
26
|
op: ';' | '&&' | '||';
|
|
26
27
|
}
|
|
28
|
+
export type ShellCommand = SimpleCommand | IfCommand | ForCommand;
|
|
27
29
|
export interface Pipeline {
|
|
28
30
|
kind: 'Pipeline';
|
|
29
|
-
commands:
|
|
31
|
+
commands: ShellCommand[];
|
|
32
|
+
}
|
|
33
|
+
export interface IfCommand {
|
|
34
|
+
kind: 'If';
|
|
35
|
+
test: CommandList;
|
|
36
|
+
then: CommandList;
|
|
37
|
+
else?: CommandList;
|
|
38
|
+
redirects: Redirect[];
|
|
39
|
+
}
|
|
40
|
+
export interface ForCommand {
|
|
41
|
+
kind: 'For';
|
|
42
|
+
name: string;
|
|
43
|
+
words: Word[];
|
|
44
|
+
body: CommandList;
|
|
45
|
+
redirects: Redirect[];
|
|
30
46
|
}
|
|
31
47
|
export interface SimpleCommand {
|
|
32
48
|
kind: 'SimpleCommand';
|
|
@@ -63,6 +79,13 @@ export type WordPart = {
|
|
|
63
79
|
kind: 'Var';
|
|
64
80
|
name: string;
|
|
65
81
|
index?: string;
|
|
82
|
+
/** `${name:-word}` / `${name:+word}` / `${name:?word}` (and non-colon). */
|
|
83
|
+
param?: {
|
|
84
|
+
op: ':-' | ':=' | ':+' | ':?' | '-' | '+' | '?';
|
|
85
|
+
word: string;
|
|
86
|
+
};
|
|
87
|
+
/** `${#name}` / `${#name[@]}` — string/array length expansion. */
|
|
88
|
+
length?: boolean;
|
|
66
89
|
} | {
|
|
67
90
|
kind: 'CmdSub';
|
|
68
91
|
cmd: string;
|
package/dist/ast.js
CHANGED
|
@@ -7,12 +7,13 @@
|
|
|
7
7
|
* - redirections: > >> 2> 2>> &> 2>&1 1>&2 <
|
|
8
8
|
* - quoting: 'literal' "interp $VAR $(cmd)"
|
|
9
9
|
* - variables: $VAR ${VAR} ${VAR[n]} plus special cases ($HOME $USER $PATH ...)
|
|
10
|
-
* - command substitution: $(...) (recursively translated)
|
|
10
|
+
* - command substitution: $(...) and `...` (recursively translated)
|
|
11
11
|
* - env assignment prefix: VAR=value cmd
|
|
12
12
|
*
|
|
13
13
|
* Explicitly unsupported (parser throws a helpful FauxnixError):
|
|
14
|
-
* heredocs,
|
|
15
|
-
* (
|
|
14
|
+
* heredocs, subshells (...), background &, while/until/case,
|
|
15
|
+
* word-level $((...)) arithmetic expansion, globs inside quotes,
|
|
16
|
+
* process substitution <(...).
|
|
16
17
|
*/
|
|
17
18
|
export function wordToString(w) {
|
|
18
19
|
return w.map(partToString).join('');
|
package/dist/commands/sysinfo.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { FauxnixParseError, isUnquotedLiteral, wordToString } from '../ast.js';
|
|
2
2
|
import { lookup, parseWords, psStr, registeredNames } from '../registry.js';
|
|
3
|
-
import { argListExpr, exprOfWord, operandExpr, translateSimple, wrapTempEnv, encodeSetValExpr, escapeDq, translateCmdSub, normalizeLiteralPath, pathExpr, } from '../translator.js';
|
|
3
|
+
import { argListExpr, exprOfWord, operandExpr, translateSimple, wrapTempEnv, encodeSetValExpr, escapeDq, translateCmdSub, normalizeLiteralPath, pathExpr, paramExpr, varExpr, } from '../translator.js';
|
|
4
4
|
import { handlers as textIoHandlers } from './text-io.js';
|
|
5
5
|
/* ------------------------------------------------------------------ */
|
|
6
6
|
/* Shared TS helpers */
|
|
@@ -545,6 +545,55 @@ const type = (args) => {
|
|
|
545
545
|
'}',
|
|
546
546
|
].join('\n');
|
|
547
547
|
};
|
|
548
|
+
/** `command -v name` (and bare `command name args` as a no-alias run). */
|
|
549
|
+
const commandCmd = (args, ctx) => {
|
|
550
|
+
let i = 0;
|
|
551
|
+
let identify = false;
|
|
552
|
+
while (i < args.length) {
|
|
553
|
+
const t = wordToString(args[i]);
|
|
554
|
+
if (t === '-v' || t === '-V') {
|
|
555
|
+
identify = true;
|
|
556
|
+
i++;
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
if (t === '--') {
|
|
560
|
+
i++;
|
|
561
|
+
break;
|
|
562
|
+
}
|
|
563
|
+
if (t.startsWith('-')) {
|
|
564
|
+
i++;
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
break;
|
|
568
|
+
}
|
|
569
|
+
const rest = args.slice(i);
|
|
570
|
+
if (identify) {
|
|
571
|
+
if (rest.length === 0)
|
|
572
|
+
return '';
|
|
573
|
+
return [
|
|
574
|
+
PS_WHICH_FN,
|
|
575
|
+
'$fx_b = @(' + builtinNames().map(psStr).join(', ') + ')',
|
|
576
|
+
'$fx_ns = ' + textArgs(rest),
|
|
577
|
+
'foreach ($fx_n in $fx_ns) {',
|
|
578
|
+
" if ($fx_b -contains $fx_n) { '/usr/bin/' + $fx_n }",
|
|
579
|
+
' else {',
|
|
580
|
+
' $fx_w = fx-which $fx_n',
|
|
581
|
+
" if ($fx_w -eq '') { $script:fx_exit = 1 }",
|
|
582
|
+
" else { $fx_w.Replace('\\', '/') }",
|
|
583
|
+
' }',
|
|
584
|
+
'}',
|
|
585
|
+
].join('\n');
|
|
586
|
+
}
|
|
587
|
+
if (rest.length === 0)
|
|
588
|
+
return '';
|
|
589
|
+
return translateSimple({
|
|
590
|
+
kind: 'SimpleCommand',
|
|
591
|
+
assignments: [],
|
|
592
|
+
name: rest[0],
|
|
593
|
+
args: rest.slice(1),
|
|
594
|
+
redirects: [],
|
|
595
|
+
}, ctx.position, ctx.hasStdin);
|
|
596
|
+
};
|
|
548
597
|
/* ------------------------------------------------------------------ */
|
|
549
598
|
/* whoami / id / groups */
|
|
550
599
|
/* ------------------------------------------------------------------ */
|
|
@@ -1344,6 +1393,12 @@ function kshExprOfWord(w) {
|
|
|
1344
1393
|
if (tilde && expanded.length === 0)
|
|
1345
1394
|
return '(fx-home)';
|
|
1346
1395
|
if (!tilde && expanded.length === 1 && expanded[0].kind === 'Var') {
|
|
1396
|
+
if (expanded[0].param) {
|
|
1397
|
+
return paramExpr(expanded[0].name, expanded[0].param.op, expanded[0].param.word);
|
|
1398
|
+
}
|
|
1399
|
+
if (expanded[0].length) {
|
|
1400
|
+
return varExpr(expanded[0].name, expanded[0].index, undefined, true);
|
|
1401
|
+
}
|
|
1347
1402
|
if (expanded[0].index !== undefined) {
|
|
1348
1403
|
return '(fx-subget ' + psStr(expanded[0].name) + ' ' + psStr(expanded[0].index) + ')';
|
|
1349
1404
|
}
|
|
@@ -1369,9 +1424,11 @@ function kshExprOfWord(w) {
|
|
|
1369
1424
|
break;
|
|
1370
1425
|
case 'Var':
|
|
1371
1426
|
out +=
|
|
1372
|
-
p.
|
|
1373
|
-
? '$(
|
|
1374
|
-
:
|
|
1427
|
+
p.param
|
|
1428
|
+
? '$(' + paramExpr(p.name, p.param.op, p.param.word) + ')'
|
|
1429
|
+
: p.index !== undefined
|
|
1430
|
+
? '$(fx-subget ' + psStr(p.name) + ' ' + psStr(p.index) + ')'
|
|
1431
|
+
: '$(fx-envget ' + psStr(p.name) + ')';
|
|
1375
1432
|
break;
|
|
1376
1433
|
case 'CmdSub':
|
|
1377
1434
|
// [[ ]] does not IFS-split, so keep the newline contract.
|
|
@@ -2310,12 +2367,107 @@ const less = (args, ctx) => {
|
|
|
2310
2367
|
return cat(files, ctx);
|
|
2311
2368
|
};
|
|
2312
2369
|
/* ------------------------------------------------------------------ */
|
|
2370
|
+
/* read */
|
|
2371
|
+
/* ------------------------------------------------------------------ */
|
|
2372
|
+
const readCmd = (args, ctx) => {
|
|
2373
|
+
const names = [];
|
|
2374
|
+
for (const w of args) {
|
|
2375
|
+
const t = wordToString(w);
|
|
2376
|
+
if (t === '-r' || t === '-a' || t.startsWith('-'))
|
|
2377
|
+
continue;
|
|
2378
|
+
if (!NAME_RE.test(t)) {
|
|
2379
|
+
return ('[Console]::Error.WriteLine(' +
|
|
2380
|
+
psStr('bash: read: `' + t + "': not a valid identifier") +
|
|
2381
|
+
'); $script:fx_exit = 2');
|
|
2382
|
+
}
|
|
2383
|
+
names.push(t);
|
|
2384
|
+
}
|
|
2385
|
+
if (names.length === 0)
|
|
2386
|
+
names.push('REPLY');
|
|
2387
|
+
const first = names[0];
|
|
2388
|
+
return [
|
|
2389
|
+
ctx.hasStdin
|
|
2390
|
+
? "$fx_line = [string]((@($input) | Select-Object -First 1))"
|
|
2391
|
+
: "$fx_line = ''",
|
|
2392
|
+
ctx.hasStdin ? '' : '$script:fx_exit = 1',
|
|
2393
|
+
names.length === 1
|
|
2394
|
+
? [
|
|
2395
|
+
"Set-Item -LiteralPath ('Env:\\' + " + psStr(first) + ') -Value $fx_line',
|
|
2396
|
+
" $n = " + psStr(first),
|
|
2397
|
+
" $env:FAUXNIX_SETVARS = ((@($env:FAUXNIX_SETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $n }) + $n) -join ';')",
|
|
2398
|
+
" $env:FAUXNIX_UNSETVARS = (@($env:FAUXNIX_UNSETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $n }) -join ';')",
|
|
2399
|
+
' fx-arrdrop $n',
|
|
2400
|
+
' $fx_sv = @(); foreach ($fx_pair in @($env:FAUXNIX_SETVALS -split [string][char]10)) { $fx_eq = $fx_pair.IndexOf([char]61); if ($fx_eq -lt 1) { continue }; if ($fx_pair.Substring(0, $fx_eq) -cne $n) { $fx_sv += $fx_pair } }',
|
|
2401
|
+
' $fx_sv += ($n + [string][char]61 + (fx-svenc $fx_line)); $env:FAUXNIX_SETVALS = ($fx_sv -join [string][char]10)',
|
|
2402
|
+
].join('\n')
|
|
2403
|
+
: [
|
|
2404
|
+
"$fx_fs = @($fx_line -split '\\s+', " + names.length + ')',
|
|
2405
|
+
'foreach ($fx_i in 0..' + (names.length - 1) + ') {',
|
|
2406
|
+
' $n = @(' + names.map(psStr).join(',') + ')[$fx_i]',
|
|
2407
|
+
" $fx_v = $(if ($fx_i -lt $fx_fs.Count) { [string]$fx_fs[$fx_i] } else { '' })",
|
|
2408
|
+
" Set-Item -LiteralPath ('Env:\\' + $n) -Value $fx_v",
|
|
2409
|
+
" $env:FAUXNIX_SETVARS = ((@($env:FAUXNIX_SETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $n }) + $n) -join ';')",
|
|
2410
|
+
" $env:FAUXNIX_UNSETVARS = (@($env:FAUXNIX_UNSETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $n }) -join ';')",
|
|
2411
|
+
' fx-arrdrop $n',
|
|
2412
|
+
' $fx_sv = @(); foreach ($fx_pair in @($env:FAUXNIX_SETVALS -split [string][char]10)) { $fx_eq = $fx_pair.IndexOf([char]61); if ($fx_eq -lt 1) { continue }; if ($fx_pair.Substring(0, $fx_eq) -cne $n) { $fx_sv += $fx_pair } }',
|
|
2413
|
+
' $fx_sv += ($n + [string][char]61 + (fx-svenc $fx_v)); $env:FAUXNIX_SETVALS = ($fx_sv -join [string][char]10)',
|
|
2414
|
+
'}',
|
|
2415
|
+
].join('\n'),
|
|
2416
|
+
]
|
|
2417
|
+
.filter((l) => l !== '')
|
|
2418
|
+
.join('\n');
|
|
2419
|
+
};
|
|
2420
|
+
/* ------------------------------------------------------------------ */
|
|
2313
2421
|
/* source / . / eval / exit / alias / set */
|
|
2314
2422
|
/* ------------------------------------------------------------------ */
|
|
2315
|
-
const source = () => {
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
'
|
|
2423
|
+
const source = (args) => {
|
|
2424
|
+
const files = stripFlags(args);
|
|
2425
|
+
if (files.length === 0) {
|
|
2426
|
+
return ('[Console]::Error.WriteLine(' +
|
|
2427
|
+
psStr('bash: source: filename argument required') +
|
|
2428
|
+
'); $script:fx_exit = 2');
|
|
2429
|
+
}
|
|
2430
|
+
const q = (c) => '[char]' + c.charCodeAt(0);
|
|
2431
|
+
return [
|
|
2432
|
+
'$fx_files = ' + argListExpr(files, operandExpr),
|
|
2433
|
+
'foreach ($fx_f in $fx_files) {',
|
|
2434
|
+
' if (-not (Test-Path -LiteralPath $fx_f -PathType Leaf)) {',
|
|
2435
|
+
" [Console]::Error.WriteLine('bash: source: ' + $fx_f + ': No such file or directory'); $script:fx_exit = 1; continue",
|
|
2436
|
+
' }',
|
|
2437
|
+
' $fx_raw = [IO.File]::ReadAllText($fx_f) -replace ([string][char]13 + [string][char]10), [string][char]10',
|
|
2438
|
+
' foreach ($fx_line in @($fx_raw -split [string][char]10)) {',
|
|
2439
|
+
' $fx_t = $fx_line.Trim()',
|
|
2440
|
+
" if ($fx_t -eq '' -or $fx_t.StartsWith([string][char]35)) { continue }",
|
|
2441
|
+
' if ($fx_t.StartsWith(' +
|
|
2442
|
+
psStr('export ') +
|
|
2443
|
+
') -or $fx_t.StartsWith(' +
|
|
2444
|
+
psStr('export\t') +
|
|
2445
|
+
')) { $fx_t = $fx_t.Substring(7).Trim() }',
|
|
2446
|
+
' $fx_eq = $fx_t.IndexOf([char]61)',
|
|
2447
|
+
' if ($fx_eq -lt 1) { [Console]::Error.WriteLine(' +
|
|
2448
|
+
psStr('fauxnix: source: only NAME=VALUE lines are supported') +
|
|
2449
|
+
'); $script:fx_exit = 1; continue }',
|
|
2450
|
+
' $fx_n = $fx_t.Substring(0, $fx_eq)',
|
|
2451
|
+
" if ($fx_n -notmatch '^[A-Za-z_][A-Za-z0-9_]*$') { [Console]::Error.WriteLine('bash: source: ' + $fx_n + ': not a valid identifier'); $script:fx_exit = 1; continue }",
|
|
2452
|
+
' $fx_v = $fx_t.Substring($fx_eq + 1)',
|
|
2453
|
+
' if ($fx_v.Length -ge 2 -and (($fx_v[0] -eq ' +
|
|
2454
|
+
q('"') +
|
|
2455
|
+
' -and $fx_v[$fx_v.Length-1] -eq ' +
|
|
2456
|
+
q('"') +
|
|
2457
|
+
') -or ($fx_v[0] -eq ' +
|
|
2458
|
+
q("'") +
|
|
2459
|
+
' -and $fx_v[$fx_v.Length-1] -eq ' +
|
|
2460
|
+
q("'") +
|
|
2461
|
+
'))) { $fx_v = $fx_v.Substring(1, $fx_v.Length - 2) }',
|
|
2462
|
+
" Set-Item -LiteralPath ('Env:\\' + $fx_n) -Value $fx_v",
|
|
2463
|
+
" $env:FAUXNIX_SETVARS = ((@($env:FAUXNIX_SETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $fx_n }) + $fx_n) -join ';')",
|
|
2464
|
+
" $env:FAUXNIX_UNSETVARS = (@($env:FAUXNIX_UNSETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $fx_n }) -join ';')",
|
|
2465
|
+
' fx-arrdrop $fx_n',
|
|
2466
|
+
' $fx_sv = @(); foreach ($fx_pair in @($env:FAUXNIX_SETVALS -split [string][char]10)) { $fx_eq2 = $fx_pair.IndexOf([char]61); if ($fx_eq2 -lt 1) { continue }; if ($fx_pair.Substring(0, $fx_eq2) -cne $fx_n) { $fx_sv += $fx_pair } }',
|
|
2467
|
+
' $fx_sv += ($fx_n + [string][char]61 + (fx-svenc $fx_v)); $env:FAUXNIX_SETVALS = ($fx_sv -join [string][char]10)',
|
|
2468
|
+
' }',
|
|
2469
|
+
'}',
|
|
2470
|
+
].join('\n');
|
|
2319
2471
|
};
|
|
2320
2472
|
const evalCmd = () => {
|
|
2321
2473
|
return ("[Console]::Error.WriteLine('fauxnix: eval is not supported; pass the command itself'); $script:fx_exit = 1");
|
|
@@ -2352,7 +2504,28 @@ const alias = (args) => {
|
|
|
2352
2504
|
return '';
|
|
2353
2505
|
return "[Console]::Error.WriteLine('fauxnix: alias is not supported'); $script:fx_exit = 1";
|
|
2354
2506
|
};
|
|
2355
|
-
const set = () =>
|
|
2507
|
+
const set = (args) => {
|
|
2508
|
+
const raw = args.map(wordToString);
|
|
2509
|
+
const unsupported = raw.filter((t) => t === '-e' ||
|
|
2510
|
+
t === '-u' ||
|
|
2511
|
+
t === '-x' ||
|
|
2512
|
+
t === '-o' ||
|
|
2513
|
+
t === '+e' ||
|
|
2514
|
+
t === '+u' ||
|
|
2515
|
+
t === '+x' ||
|
|
2516
|
+
t.startsWith('-o') ||
|
|
2517
|
+
t.startsWith('+o') ||
|
|
2518
|
+
t === '-eu' ||
|
|
2519
|
+
t === '-ue' ||
|
|
2520
|
+
t === '-eux' ||
|
|
2521
|
+
/^-.*[eux]/.test(t));
|
|
2522
|
+
if (unsupported.length > 0) {
|
|
2523
|
+
return ('[Console]::Error.WriteLine(' +
|
|
2524
|
+
psStr('fauxnix: set -e/-u/-x is not supported (would silently lie); use explicit || exit') +
|
|
2525
|
+
'); $script:fx_exit = 2');
|
|
2526
|
+
}
|
|
2527
|
+
return '';
|
|
2528
|
+
};
|
|
2356
2529
|
/* ------------------------------------------------------------------ */
|
|
2357
2530
|
export const handlers = {
|
|
2358
2531
|
cd,
|
|
@@ -2368,6 +2541,7 @@ export const handlers = {
|
|
|
2368
2541
|
sleep,
|
|
2369
2542
|
which,
|
|
2370
2543
|
type,
|
|
2544
|
+
command: commandCmd,
|
|
2371
2545
|
whoami,
|
|
2372
2546
|
id,
|
|
2373
2547
|
groups,
|
|
@@ -2393,6 +2567,7 @@ export const handlers = {
|
|
|
2393
2567
|
history,
|
|
2394
2568
|
less,
|
|
2395
2569
|
more: less,
|
|
2570
|
+
read: readCmd,
|
|
2396
2571
|
source,
|
|
2397
2572
|
'.': source,
|
|
2398
2573
|
eval: evalCmd,
|
package/dist/mcp.js
CHANGED
|
@@ -37,7 +37,7 @@ Output is formatted to look like GNU/Linux tooling (ls -l, ps aux, df -h ...), e
|
|
|
37
37
|
|
|
38
38
|
Supported: pipes (|), && / || / ;, redirections (> >> 2> 2>&1 < /dev/null), variables ($VAR $HOME ~), command substitution $(...), and ${registeredNames().length}+ coreutils-style commands (${registeredNames().slice(0, 18).join(', ')}...).
|
|
39
39
|
Unknown commands (git, node, npm, python, cargo...) are passed through and executed natively with argv-style quoting.
|
|
40
|
-
Not supported: heredocs,
|
|
40
|
+
Not supported: heredocs, while/until/case, word-level \$((...)) arithmetic expansion, background jobs. if/then/else/fi and for-in loops are supported.
|
|
41
41
|
|
|
42
42
|
CWD, environment variables, export/unset and cd persist across calls within this session — but prefer COMBINING related commands in one call with ; or && (e.g. 'cd src && ls | wc -l'); each call is a fresh translation+process, so batching is faster than many tiny calls.
|
|
43
43
|
Exit codes follow bash conventions (0 ok, 1 fail, 2 usage/serious, 127 command not found, 124 timeout).
|
package/dist/parser.js
CHANGED
|
@@ -127,6 +127,16 @@ export function tokenize(input) {
|
|
|
127
127
|
continue;
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
|
+
if (c === '`') {
|
|
131
|
+
const v = readBacktick(input, i);
|
|
132
|
+
if (buf) {
|
|
133
|
+
parts.push({ kind: 'Text', text: buf });
|
|
134
|
+
buf = '';
|
|
135
|
+
}
|
|
136
|
+
parts.push(v.part);
|
|
137
|
+
i = v.next;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
130
140
|
buf += c;
|
|
131
141
|
i++;
|
|
132
142
|
}
|
|
@@ -153,7 +163,11 @@ export function tokenize(input) {
|
|
|
153
163
|
continue;
|
|
154
164
|
}
|
|
155
165
|
if (ch === '`') {
|
|
156
|
-
|
|
166
|
+
const v = readBacktick(input, i);
|
|
167
|
+
beginWordPart();
|
|
168
|
+
cur.push(v.part);
|
|
169
|
+
i = v.next;
|
|
170
|
+
continue;
|
|
157
171
|
}
|
|
158
172
|
// escape outside quotes — keep the escape so [[ =~ ]] / == can
|
|
159
173
|
// treat `\*` as a literal rather than a metacharacter
|
|
@@ -186,6 +200,23 @@ function isNameStart(c) {
|
|
|
186
200
|
function isNameChar(c) {
|
|
187
201
|
return /[A-Za-z0-9_]/.test(c);
|
|
188
202
|
}
|
|
203
|
+
/** Parse `cmd` as command substitution (same AST as $(cmd)). */
|
|
204
|
+
function readBacktick(input, i) {
|
|
205
|
+
if (input[i] !== '`')
|
|
206
|
+
throw new FauxnixParseError('fauxnix: expected backtick');
|
|
207
|
+
let k = i + 1;
|
|
208
|
+
while (k < input.length) {
|
|
209
|
+
if (input[k] === '\\' && k + 1 < input.length) {
|
|
210
|
+
k += 2;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (input[k] === '`') {
|
|
214
|
+
return { part: { kind: 'CmdSub', cmd: input.slice(i + 1, k) }, next: k + 1 };
|
|
215
|
+
}
|
|
216
|
+
k++;
|
|
217
|
+
}
|
|
218
|
+
throw new FauxnixParseError('fauxnix: unclosed backtick');
|
|
219
|
+
}
|
|
189
220
|
/** Parse $VAR, ${VAR}, $(cmd substitution). Returns null when not a valid dollar construct. */
|
|
190
221
|
function readDollar(input, i) {
|
|
191
222
|
const n = input.length;
|
|
@@ -204,12 +235,33 @@ function readDollar(input, i) {
|
|
|
204
235
|
if (sub) {
|
|
205
236
|
return { part: { kind: 'Var', name: sub[1], index: sub[2] }, next: end + 1 };
|
|
206
237
|
}
|
|
238
|
+
const pm = name.match(/^([A-Za-z_][A-Za-z0-9_]*)(:?[-+?])(.*)$/);
|
|
239
|
+
if (pm) {
|
|
240
|
+
const op = pm[2];
|
|
241
|
+
return {
|
|
242
|
+
part: { kind: 'Var', name: pm[1], param: { op, word: pm[3] } },
|
|
243
|
+
next: end + 1,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
const hash = name.match(/^#([A-Za-z_][A-Za-z0-9_]*)(\[([0-9]+|@|\*)\])?$/);
|
|
247
|
+
if (hash) {
|
|
248
|
+
return {
|
|
249
|
+
part: { kind: 'Var', name: hash[1], index: hash[3], length: true },
|
|
250
|
+
next: end + 1,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
207
253
|
if (!name || !isNameStart(name[0]) || !name.split('').every(isNameChar)) {
|
|
208
|
-
// ${VAR
|
|
254
|
+
// ${VAR:=default} etc. still raw text
|
|
209
255
|
return { part: { kind: 'Text', text: input.slice(i, end + 1) }, next: end + 1 };
|
|
210
256
|
}
|
|
211
257
|
return { part: { kind: 'Var', name }, next: end + 1 };
|
|
212
258
|
}
|
|
259
|
+
// $((...)) is arithmetic expansion, not command substitution of a
|
|
260
|
+
// parenthesized body. Today it was parsed as $( (expr) ) and became an
|
|
261
|
+
// empty/confusing command. Reject loudly until word-level arith lands.
|
|
262
|
+
if (input[j] === '(' && j + 1 < n && input[j + 1] === '(') {
|
|
263
|
+
throw new FauxnixParseError('fauxnix: $((...)) arithmetic expansion is not supported; compute the value in the agent or compare with [[ $n -eq m ]]');
|
|
264
|
+
}
|
|
213
265
|
// $(cmd substitution) — captured with balanced parens; the translator
|
|
214
266
|
// recursively translates this text before embedding it.
|
|
215
267
|
if (input[j] === '(') {
|
|
@@ -509,7 +561,22 @@ export function parseCommand(input) {
|
|
|
509
561
|
const parsePipeline = () => {
|
|
510
562
|
const commands = [];
|
|
511
563
|
for (;;) {
|
|
512
|
-
|
|
564
|
+
const kw = peekKw();
|
|
565
|
+
if (kw === 'if') {
|
|
566
|
+
if (commands.length > 0) {
|
|
567
|
+
throw new FauxnixParseError('fauxnix: if in a pipeline is not supported');
|
|
568
|
+
}
|
|
569
|
+
commands.push(parseIf());
|
|
570
|
+
}
|
|
571
|
+
else if (kw === 'for') {
|
|
572
|
+
if (commands.length > 0) {
|
|
573
|
+
throw new FauxnixParseError('fauxnix: for in a pipeline is not supported');
|
|
574
|
+
}
|
|
575
|
+
commands.push(parseFor());
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
commands.push(parseSimple());
|
|
579
|
+
}
|
|
513
580
|
const t = peek();
|
|
514
581
|
if (t.type === 'OP' && t.op === '|') {
|
|
515
582
|
next();
|
|
@@ -519,6 +586,108 @@ export function parseCommand(input) {
|
|
|
519
586
|
}
|
|
520
587
|
return { kind: 'Pipeline', commands };
|
|
521
588
|
};
|
|
589
|
+
const peekKw = () => {
|
|
590
|
+
const t = peek();
|
|
591
|
+
if (t.type !== 'WORD' || !t.parts)
|
|
592
|
+
return null;
|
|
593
|
+
const s = wordToString(t.parts);
|
|
594
|
+
if (!isUnquotedLiteral(t.parts, s))
|
|
595
|
+
return null;
|
|
596
|
+
if (s === 'if' ||
|
|
597
|
+
s === 'then' ||
|
|
598
|
+
s === 'else' ||
|
|
599
|
+
s === 'elif' ||
|
|
600
|
+
s === 'fi' ||
|
|
601
|
+
s === 'for' ||
|
|
602
|
+
s === 'in' ||
|
|
603
|
+
s === 'do' ||
|
|
604
|
+
s === 'done' ||
|
|
605
|
+
s === 'while') {
|
|
606
|
+
return s;
|
|
607
|
+
}
|
|
608
|
+
return null;
|
|
609
|
+
};
|
|
610
|
+
const expectKw = (k) => {
|
|
611
|
+
if (peekKw() !== k) {
|
|
612
|
+
throw new FauxnixParseError('fauxnix: expected `' + k + "'");
|
|
613
|
+
}
|
|
614
|
+
next();
|
|
615
|
+
};
|
|
616
|
+
const parseListUntil = (stops) => {
|
|
617
|
+
const stop = new Set(stops);
|
|
618
|
+
const segments = [];
|
|
619
|
+
let op = ';';
|
|
620
|
+
const isListSep = (o) => o === ';' || o === '\n';
|
|
621
|
+
while (peek().type === 'OP' && isListSep(peek().op))
|
|
622
|
+
next();
|
|
623
|
+
while (peek().type !== 'EOF') {
|
|
624
|
+
const kw = peekKw();
|
|
625
|
+
if (kw && stop.has(kw))
|
|
626
|
+
break;
|
|
627
|
+
const pipeline = parsePipeline();
|
|
628
|
+
segments.push({ pipeline, op });
|
|
629
|
+
const t = peek();
|
|
630
|
+
if (t.type === 'OP' && (t.op === '&&' || t.op === '||' || isListSep(t.op))) {
|
|
631
|
+
op = t.op === '\n' ? ';' : t.op;
|
|
632
|
+
next();
|
|
633
|
+
while (peek().type === 'OP' && isListSep(peek().op))
|
|
634
|
+
next();
|
|
635
|
+
}
|
|
636
|
+
else {
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
if (segments.length === 0) {
|
|
641
|
+
throw new FauxnixParseError('fauxnix: empty command');
|
|
642
|
+
}
|
|
643
|
+
return { kind: 'CommandList', segments };
|
|
644
|
+
};
|
|
645
|
+
const parseIf = () => {
|
|
646
|
+
expectKw('if');
|
|
647
|
+
const test = parseListUntil(['then']);
|
|
648
|
+
expectKw('then');
|
|
649
|
+
const thenL = parseListUntil(['else', 'elif', 'fi']);
|
|
650
|
+
let elseL;
|
|
651
|
+
if (peekKw() === 'elif') {
|
|
652
|
+
throw new FauxnixParseError('fauxnix: elif is not supported yet; use else + if');
|
|
653
|
+
}
|
|
654
|
+
if (peekKw() === 'else') {
|
|
655
|
+
next();
|
|
656
|
+
elseL = parseListUntil(['fi']);
|
|
657
|
+
}
|
|
658
|
+
expectKw('fi');
|
|
659
|
+
return { kind: 'If', test, then: thenL, else: elseL, redirects: [] };
|
|
660
|
+
};
|
|
661
|
+
const parseFor = () => {
|
|
662
|
+
expectKw('for');
|
|
663
|
+
const nt = peek();
|
|
664
|
+
if (nt.type !== 'WORD' || !nt.parts) {
|
|
665
|
+
throw new FauxnixParseError('fauxnix: `for` expected a name');
|
|
666
|
+
}
|
|
667
|
+
const name = wordToString(nt.parts);
|
|
668
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name) || !isUnquotedLiteral(nt.parts, name)) {
|
|
669
|
+
throw new FauxnixParseError('fauxnix: `for` name must be an identifier');
|
|
670
|
+
}
|
|
671
|
+
next();
|
|
672
|
+
expectKw('in');
|
|
673
|
+
const words = [];
|
|
674
|
+
for (;;) {
|
|
675
|
+
while (peek().type === 'OP' && (peek().op === ';' || peek().op === '\n'))
|
|
676
|
+
next();
|
|
677
|
+
if (peekKw() === 'do')
|
|
678
|
+
break;
|
|
679
|
+
const t = peek();
|
|
680
|
+
if (t.type !== 'WORD' || !t.parts) {
|
|
681
|
+
throw new FauxnixParseError("fauxnix: `for` expected `do`");
|
|
682
|
+
}
|
|
683
|
+
words.push(t.parts);
|
|
684
|
+
next();
|
|
685
|
+
}
|
|
686
|
+
expectKw('do');
|
|
687
|
+
const body = parseListUntil(['done']);
|
|
688
|
+
expectKw('done');
|
|
689
|
+
return { kind: 'For', name, words, body, redirects: [] };
|
|
690
|
+
};
|
|
522
691
|
const parseSimple = () => {
|
|
523
692
|
const assignments = [];
|
|
524
693
|
let redirects = [];
|
package/dist/translator.d.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import { Assignment, CommandList, Redirect, SimpleCommand, Word } from './ast.js';
|
|
1
|
+
import { Assignment, CommandList, Redirect, SimpleCommand, IfCommand, ForCommand, Word } from './ast.js';
|
|
2
2
|
import { PipelineCtx } from './registry.js';
|
|
3
|
+
/** `${name:-word}` and friends using case-exact fx-scalar0. */
|
|
4
|
+
export declare function paramExpr(name: string, op: ':-' | ':=' | ':+' | ':?' | '-' | '+' | '?', word: string): string;
|
|
3
5
|
/** Map a bash $VAR name to a PowerShell expression (usable inside $(...)). */
|
|
4
|
-
export declare function varExpr(name: string, index?: string
|
|
6
|
+
export declare function varExpr(name: string, index?: string, param?: {
|
|
7
|
+
op: ':-' | ':=' | ':+' | ':?' | '-' | '+' | '?';
|
|
8
|
+
word: string;
|
|
9
|
+
}, length?: boolean): string;
|
|
5
10
|
/** Escape text destined for the inside of a PS double-quoted string. */
|
|
6
11
|
export declare function escapeDq(s: string): string;
|
|
7
12
|
/** Normalize a literal POSIX-ish path to its Windows equivalent. */
|
|
@@ -66,13 +71,8 @@ export interface PipelineParts {
|
|
|
66
71
|
/** The pipeline invocation itself. */
|
|
67
72
|
call: string;
|
|
68
73
|
}
|
|
69
|
-
/**
|
|
70
|
-
* Pipeline body. A lone command runs as a plain script-block expression;
|
|
71
|
-
* multi-command pipelines become generated functions chained with `|`
|
|
72
|
-
* (PS 5.1 forbids parenthesized expressions as non-first pipeline elements).
|
|
73
|
-
*/
|
|
74
74
|
export declare function translatePipelineBody(p: {
|
|
75
|
-
commands: SimpleCommand
|
|
75
|
+
commands: Array<SimpleCommand | IfCommand | ForCommand>;
|
|
76
76
|
}): PipelineParts;
|
|
77
77
|
export interface SegmentPlan {
|
|
78
78
|
op: ';' | '&&' | '||';
|
|
@@ -85,5 +85,7 @@ export declare function translateCommandList(list: CommandList): SegmentPlan[];
|
|
|
85
85
|
/**
|
|
86
86
|
* Wrap a pipeline body with the Fauxnix executor contract:
|
|
87
87
|
* UTF-8 everywhere, bash-style exit codes, cwd/env persistence channels.
|
|
88
|
+
* Only the fx- helpers the body actually calls are emitted — the full
|
|
89
|
+
* catalog is ~170 lines and was paid on every `echo hi`.
|
|
88
90
|
*/
|
|
89
91
|
export declare function wrapScript(body: string): string;
|
package/dist/translator.js
CHANGED
|
@@ -4,8 +4,55 @@ import { lookup, psStr } from './registry.js';
|
|
|
4
4
|
/* ------------------------------------------------------------------ */
|
|
5
5
|
/* Variable mapping */
|
|
6
6
|
/* ------------------------------------------------------------------ */
|
|
7
|
+
function paramWordExpr(word) {
|
|
8
|
+
if (word.startsWith('$') && /^\$[A-Za-z_][A-Za-z0-9_]*$/.test(word)) {
|
|
9
|
+
return varExpr(word.slice(1));
|
|
10
|
+
}
|
|
11
|
+
return psStr(word);
|
|
12
|
+
}
|
|
13
|
+
/** `${name:-word}` and friends using case-exact fx-scalar0. */
|
|
14
|
+
export function paramExpr(name, op, word) {
|
|
15
|
+
const alt = paramWordExpr(word);
|
|
16
|
+
const get = '(fx-scalar0 ' + psStr(name) + ')';
|
|
17
|
+
const empty = '($null -eq $fx_pv -or [string]$fx_pv -eq \'\')';
|
|
18
|
+
const unset = '($null -eq $fx_pv)';
|
|
19
|
+
if (op === ':-') {
|
|
20
|
+
return '$( $fx_pv = ' + get + '; if (' + empty + ') { ' + alt + ' } else { $fx_pv } )';
|
|
21
|
+
}
|
|
22
|
+
if (op === '-') {
|
|
23
|
+
return '$( $fx_pv = ' + get + '; if (' + unset + ') { ' + alt + ' } else { $fx_pv } )';
|
|
24
|
+
}
|
|
25
|
+
if (op === ':+') {
|
|
26
|
+
return '$( $fx_pv = ' + get + '; if (' + empty + ') { \'\' } else { ' + alt + ' } )';
|
|
27
|
+
}
|
|
28
|
+
if (op === '+') {
|
|
29
|
+
return '$( $fx_pv = ' + get + '; if (' + unset + ') { \'\' } else { ' + alt + ' } )';
|
|
30
|
+
}
|
|
31
|
+
const msg = word === '' ? name + ': parameter null or not set' : name + ': ' + word;
|
|
32
|
+
const cond = op === ':?' ? empty : unset;
|
|
33
|
+
return ('$( $fx_pv = ' +
|
|
34
|
+
get +
|
|
35
|
+
'; if (' +
|
|
36
|
+
cond +
|
|
37
|
+
') { [Console]::Error.WriteLine(' +
|
|
38
|
+
psStr('bash: ' + msg) +
|
|
39
|
+
'); $script:fx_exit = 1; \'\' } else { $fx_pv } )');
|
|
40
|
+
}
|
|
7
41
|
/** Map a bash $VAR name to a PowerShell expression (usable inside $(...)). */
|
|
8
|
-
export function varExpr(name, index) {
|
|
42
|
+
export function varExpr(name, index, param, length = false) {
|
|
43
|
+
if (param)
|
|
44
|
+
return paramExpr(name, param.op, param.word);
|
|
45
|
+
if (length) {
|
|
46
|
+
if (index === '@' || index === '*') {
|
|
47
|
+
return '@(fx-arrload ' + psStr(name) + ').Count';
|
|
48
|
+
}
|
|
49
|
+
if (index !== undefined) {
|
|
50
|
+
return '([string](fx-subget ' + psStr(name) + ' ' + psStr(index) + ')).Length';
|
|
51
|
+
}
|
|
52
|
+
return ('([string]$(if ($null -eq ($fx_pv = fx-scalar0 ' +
|
|
53
|
+
psStr(name) +
|
|
54
|
+
')) { \'\' } else { $fx_pv })).Length');
|
|
55
|
+
}
|
|
9
56
|
// Indexed reads always go through fx-subget → fx-arrload → fx-scalar0 so
|
|
10
57
|
// ${PWD[0]} keeps the special mapping and ${bash_rematch[0]} stays
|
|
11
58
|
// case-exact (a `$env:name` fallback would alias BASH_REMATCH on Windows).
|
|
@@ -106,7 +153,8 @@ export function exprOfWord(w, opts) {
|
|
|
106
153
|
}
|
|
107
154
|
// single bare variable → bare expression
|
|
108
155
|
if (expanded.length === 1 && expanded[0].kind === 'Var') {
|
|
109
|
-
|
|
156
|
+
const v = expanded[0];
|
|
157
|
+
return varExpr(v.name, v.index, v.param, v.length === true);
|
|
110
158
|
}
|
|
111
159
|
// Bare `$(...)` must not sit inside a PS expandable string: the
|
|
112
160
|
// substitution body contains `"` / `$_` that would break interpolation.
|
|
@@ -133,7 +181,7 @@ export function exprOfWord(w, opts) {
|
|
|
133
181
|
emitPart(q, true);
|
|
134
182
|
break;
|
|
135
183
|
case 'Var':
|
|
136
|
-
out += '$(' + varExpr(p.name, p.index) + ')';
|
|
184
|
+
out += '$(' + varExpr(p.name, p.index, p.param, p.length === true) + ')';
|
|
137
185
|
break;
|
|
138
186
|
case 'CmdSub':
|
|
139
187
|
out += '$(' + translateCmdSub(p.cmd, quoted || opts?.preserveCmdSub === true) + ')';
|
|
@@ -189,7 +237,9 @@ export function splatSpec(w) {
|
|
|
189
237
|
let suffix = '';
|
|
190
238
|
let seen = false;
|
|
191
239
|
for (const { part: p, quoted } of parts) {
|
|
192
|
-
const splat = p.kind === 'Var' &&
|
|
240
|
+
const splat = p.kind === 'Var' &&
|
|
241
|
+
!p.length &&
|
|
242
|
+
(p.index === '@' || (p.index === '*' && !quoted));
|
|
193
243
|
if (splat) {
|
|
194
244
|
if (seen)
|
|
195
245
|
return null;
|
|
@@ -258,6 +308,12 @@ export function translateCmdSub(cmdText, keepNl = false) {
|
|
|
258
308
|
/* ------------------------------------------------------------------ */
|
|
259
309
|
/* Simple command translation */
|
|
260
310
|
/* ------------------------------------------------------------------ */
|
|
311
|
+
function indentBlock(s) {
|
|
312
|
+
return s
|
|
313
|
+
.split('\n')
|
|
314
|
+
.map((l) => (l ? ' ' + l : l))
|
|
315
|
+
.join('\n');
|
|
316
|
+
}
|
|
261
317
|
export function translateSimple(cmd, position, hasStdin) {
|
|
262
318
|
// assignment-only segment (`X=1; cmd`): bash semantics are "set for the
|
|
263
319
|
// rest of the shell". Reuse the export code path — persist + env shadow —
|
|
@@ -276,20 +332,35 @@ export function translateSimple(cmd, position, hasStdin) {
|
|
|
276
332
|
let body;
|
|
277
333
|
if (nameSplat) {
|
|
278
334
|
const invoke = '& $fx_cmd @fx_na';
|
|
279
|
-
|
|
335
|
+
const hasAffix = !!(nameSplat.prefix || nameSplat.suffix);
|
|
336
|
+
const promoted = cmd.args.length === 0
|
|
337
|
+
? ''
|
|
338
|
+
: translateSimple({
|
|
339
|
+
kind: 'SimpleCommand',
|
|
340
|
+
assignments: [],
|
|
341
|
+
name: cmd.args[0],
|
|
342
|
+
args: cmd.args.slice(1),
|
|
343
|
+
redirects: cmd.redirects,
|
|
344
|
+
}, position, hasStdin);
|
|
345
|
+
const emptyCmdLines = [
|
|
280
346
|
'$fx_cw = @(fx-arrload ' + psStr(nameSplat.name) + ')',
|
|
281
|
-
|
|
282
|
-
|
|
347
|
+
];
|
|
348
|
+
if (hasAffix) {
|
|
349
|
+
emptyCmdLines.push('if ($fx_cw.Count -eq 0) { $fx_cw = @(' +
|
|
350
|
+
psStr(nameSplat.prefix + nameSplat.suffix) +
|
|
351
|
+
') } else { $fx_cw[0] = ' +
|
|
283
352
|
psStr(nameSplat.prefix) +
|
|
284
353
|
' + $fx_cw[0]; $fx_cw[$fx_cw.Count-1] = $fx_cw[$fx_cw.Count-1] + ' +
|
|
285
354
|
psStr(nameSplat.suffix) +
|
|
286
|
-
' }'
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
355
|
+
' }');
|
|
356
|
+
}
|
|
357
|
+
emptyCmdLines.push('if ($fx_cw.Count -eq 0) {',
|
|
358
|
+
// No words left → bash null command (exit 0). Remaining words are
|
|
359
|
+
// known at compile time, so reuse translateSimple (handlers, not `&`).
|
|
360
|
+
promoted ? indentBlock(promoted) : ' ', '} else {', ' $fx_na = [object[]]@(' + argListExpr(cmd.args) + ')', ' $fx_cmd = [string]$fx_cw[0]', ' if ($fx_cw.Count -gt 1) { $fx_na = @($fx_cw[1..($fx_cw.Count - 1)]) + $fx_na }', ' ' +
|
|
361
|
+
(hasStdin ? '($input | ' + invoke + ')' : invoke) +
|
|
362
|
+
' | ForEach-Object { [string]$_ }', ' if ($LASTEXITCODE -gt 0) { $script:fx_exit = $LASTEXITCODE } elseif ($LASTEXITCODE -lt 0) { $script:fx_exit = 1 }', '}');
|
|
363
|
+
body = emptyCmdLines.join('\n');
|
|
293
364
|
}
|
|
294
365
|
else if (nameLit !== null) {
|
|
295
366
|
const handler = lookup(nameLit);
|
|
@@ -591,12 +662,77 @@ let stageSeq = 0;
|
|
|
591
662
|
* multi-command pipelines become generated functions chained with `|`
|
|
592
663
|
* (PS 5.1 forbids parenthesized expressions as non-first pipeline elements).
|
|
593
664
|
*/
|
|
665
|
+
function translateListInline(list) {
|
|
666
|
+
const chunks = [];
|
|
667
|
+
for (const seg of list.segments) {
|
|
668
|
+
const { defs, call } = translatePipelineBody(seg.pipeline);
|
|
669
|
+
const body = (defs ? defs + '\n' : '') + call;
|
|
670
|
+
if (seg.op === '&&') {
|
|
671
|
+
chunks.push('if ($script:fx_exit -eq 0) {\n' + body + '\n}');
|
|
672
|
+
}
|
|
673
|
+
else if (seg.op === '||') {
|
|
674
|
+
chunks.push('if ($script:fx_exit -ne 0) {\n' + body + '\n}');
|
|
675
|
+
}
|
|
676
|
+
else {
|
|
677
|
+
chunks.push(body);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return chunks.join('\n');
|
|
681
|
+
}
|
|
682
|
+
function translateIf(cmd) {
|
|
683
|
+
// Branch bodies reset fx_exit first: the compound's exit status must come
|
|
684
|
+
// from the taken branch's last command (bash semantics), not leak the test's
|
|
685
|
+
// failure — `if false; then A; else B; fi` exits 0 in bash.
|
|
686
|
+
const lines = [translateListInline(cmd.test), 'if ($script:fx_exit -eq 0) {', ' $script:fx_exit = 0'];
|
|
687
|
+
for (const l of translateListInline(cmd.then).split('\n'))
|
|
688
|
+
lines.push(l ? ' ' + l : l);
|
|
689
|
+
lines.push('} else {', ' $script:fx_exit = 0');
|
|
690
|
+
if (cmd.else) {
|
|
691
|
+
for (const l of translateListInline(cmd.else).split('\n'))
|
|
692
|
+
lines.push(l ? ' ' + l : l);
|
|
693
|
+
}
|
|
694
|
+
lines.push('}');
|
|
695
|
+
return lines.join('\n');
|
|
696
|
+
}
|
|
697
|
+
function translateFor(cmd) {
|
|
698
|
+
const n = cmd.name.replace(/'/g, "''");
|
|
699
|
+
const lines = [
|
|
700
|
+
'$fx_for = ' + argListExpr(cmd.words),
|
|
701
|
+
'foreach ($fx_it in @($fx_for)) {',
|
|
702
|
+
" Set-Item -LiteralPath ('Env:\\' + '" + n + "') -Value ([string]$fx_it)",
|
|
703
|
+
" $env:FAUXNIX_SETVARS = ((@($env:FAUXNIX_SETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne '" +
|
|
704
|
+
n +
|
|
705
|
+
"' }) + '" +
|
|
706
|
+
n +
|
|
707
|
+
"') -join ';')",
|
|
708
|
+
" $env:FAUXNIX_UNSETVARS = (@($env:FAUXNIX_UNSETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne '" +
|
|
709
|
+
n +
|
|
710
|
+
"' }) -join ';')",
|
|
711
|
+
' fx-arrdrop ' + psStr(cmd.name),
|
|
712
|
+
" $fx_sv = @(); foreach ($fx_pair in @($env:FAUXNIX_SETVALS -split [string][char]10)) { $fx_eq = $fx_pair.IndexOf([char]61); if ($fx_eq -lt 1) { continue }; if ($fx_pair.Substring(0, $fx_eq) -cne '" +
|
|
713
|
+
n +
|
|
714
|
+
"') { $fx_sv += $fx_pair } }",
|
|
715
|
+
" $fx_sv += ('" +
|
|
716
|
+
n +
|
|
717
|
+
"' + [string][char]61 + (fx-svenc ([string]$fx_it))); $env:FAUXNIX_SETVALS = ($fx_sv -join [string][char]10)",
|
|
718
|
+
];
|
|
719
|
+
for (const l of translateListInline(cmd.body).split('\n'))
|
|
720
|
+
lines.push(l ? ' ' + l : l);
|
|
721
|
+
lines.push('}');
|
|
722
|
+
return lines.join('\n');
|
|
723
|
+
}
|
|
594
724
|
export function translatePipelineBody(p) {
|
|
595
725
|
const bodies = [];
|
|
596
726
|
for (let i = 0; i < p.commands.length; i++) {
|
|
597
|
-
const
|
|
727
|
+
const c = p.commands[i];
|
|
728
|
+
const hasStdin = i > 0 || c.redirects.some((r) => r.op === '<');
|
|
598
729
|
const position = i === 0 ? 'first' : i === p.commands.length - 1 ? 'last' : 'middle';
|
|
599
|
-
|
|
730
|
+
if (c.kind === 'If')
|
|
731
|
+
bodies.push(translateIf(c));
|
|
732
|
+
else if (c.kind === 'For')
|
|
733
|
+
bodies.push(translateFor(c));
|
|
734
|
+
else
|
|
735
|
+
bodies.push(translateSimple(c, position, hasStdin));
|
|
600
736
|
}
|
|
601
737
|
if (bodies.length === 1) {
|
|
602
738
|
return { defs: '', call: '(& {\n' + bodies[0] + '\n})' };
|
|
@@ -639,11 +775,72 @@ export function translateCommandList(list) {
|
|
|
639
775
|
}
|
|
640
776
|
return plans;
|
|
641
777
|
}
|
|
778
|
+
const WRAP_HELPER_ORDER = [
|
|
779
|
+
'fx-readlines',
|
|
780
|
+
'fx-csub',
|
|
781
|
+
'fx-svenc',
|
|
782
|
+
'fx-svdec',
|
|
783
|
+
'fx-arrload',
|
|
784
|
+
'fx-scalar0',
|
|
785
|
+
'fx-ifs1',
|
|
786
|
+
'fx-arrdrop',
|
|
787
|
+
'fx-arrhas',
|
|
788
|
+
'fx-arrpackget',
|
|
789
|
+
'fx-arrpackset',
|
|
790
|
+
'fx-arrput',
|
|
791
|
+
'fx-arrclr',
|
|
792
|
+
'fx-subget',
|
|
793
|
+
];
|
|
794
|
+
const WRAP_HELPER_DEPS = {
|
|
795
|
+
'fx-readlines': [],
|
|
796
|
+
'fx-csub': [],
|
|
797
|
+
'fx-svenc': [],
|
|
798
|
+
'fx-svdec': [],
|
|
799
|
+
'fx-arrload': ['fx-scalar0', 'fx-svdec'],
|
|
800
|
+
'fx-scalar0': ['fx-svdec'],
|
|
801
|
+
'fx-ifs1': ['fx-scalar0'],
|
|
802
|
+
'fx-arrdrop': [],
|
|
803
|
+
'fx-arrhas': [],
|
|
804
|
+
'fx-arrpackget': [],
|
|
805
|
+
'fx-arrpackset': ['fx-arrdrop'],
|
|
806
|
+
'fx-arrput': ['fx-arrdrop', 'fx-svenc'],
|
|
807
|
+
'fx-arrclr': ['fx-arrdrop'],
|
|
808
|
+
'fx-subget': ['fx-arrload', 'fx-ifs1'],
|
|
809
|
+
};
|
|
810
|
+
/** Helpers the body calls that wrapScript still has to emit (not already defined there). */
|
|
811
|
+
function wrapHelpersNeeded(body) {
|
|
812
|
+
const defined = new Set();
|
|
813
|
+
const defRe = /function\s+(fx-[A-Za-z0-9]+)/g;
|
|
814
|
+
let m;
|
|
815
|
+
while ((m = defRe.exec(body)))
|
|
816
|
+
defined.add(m[1]);
|
|
817
|
+
const seeds = [];
|
|
818
|
+
const callRe = /\b(fx-[A-Za-z0-9]+)\b/g;
|
|
819
|
+
while ((m = callRe.exec(body))) {
|
|
820
|
+
const n = m[1];
|
|
821
|
+
if (!defined.has(n) && n in WRAP_HELPER_DEPS)
|
|
822
|
+
seeds.push(n);
|
|
823
|
+
}
|
|
824
|
+
const needed = new Set();
|
|
825
|
+
const stack = seeds.slice();
|
|
826
|
+
while (stack.length) {
|
|
827
|
+
const n = stack.pop();
|
|
828
|
+
if (needed.has(n) || defined.has(n))
|
|
829
|
+
continue;
|
|
830
|
+
needed.add(n);
|
|
831
|
+
for (const d of WRAP_HELPER_DEPS[n])
|
|
832
|
+
stack.push(d);
|
|
833
|
+
}
|
|
834
|
+
return needed;
|
|
835
|
+
}
|
|
642
836
|
/**
|
|
643
837
|
* Wrap a pipeline body with the Fauxnix executor contract:
|
|
644
838
|
* UTF-8 everywhere, bash-style exit codes, cwd/env persistence channels.
|
|
839
|
+
* Only the fx- helpers the body actually calls are emitted — the full
|
|
840
|
+
* catalog is ~170 lines and was paid on every `echo hi`.
|
|
645
841
|
*/
|
|
646
842
|
export function wrapScript(body) {
|
|
843
|
+
const needed = wrapHelpersNeeded(body);
|
|
647
844
|
const lines = [
|
|
648
845
|
'$ErrorActionPreference = "Continue"',
|
|
649
846
|
"$ProgressPreference = 'SilentlyContinue'",
|
|
@@ -667,175 +864,191 @@ export function wrapScript(body) {
|
|
|
667
864
|
// .NET APIs (ReadAllBytes & friends) resolve relative paths against the
|
|
668
865
|
// process working directory, NOT the PS location — keep them in sync.
|
|
669
866
|
'try { [Environment]::CurrentDirectory = (Get-Location).ProviderPath } catch {}',
|
|
670
|
-
// byte-sniffing line reader for `< file` stdin redirects (UTF-8 → GBK)
|
|
671
|
-
'function fx-readlines($p) {',
|
|
672
|
-
' $b = [IO.File]::ReadAllBytes($p)',
|
|
673
|
-
' $t = $null',
|
|
674
|
-
' try { $t = (New-Object System.Text.UTF8Encoding($false, $true)).GetString($b) } catch {}',
|
|
675
|
-
" if ($null -eq $t) { try { $t = [System.Text.Encoding]::GetEncoding(936).GetString($b) } catch { $t = [System.Text.Encoding]::ASCII.GetString($b) } }",
|
|
676
|
-
' $t = $t -replace "`r`n", "`n"',
|
|
677
|
-
' $t = $t -replace "`r", "`n"',
|
|
678
|
-
' $parts = @($t.Split("`n"))',
|
|
679
|
-
" if ($parts.Count -eq 1 -and $parts[0] -eq '') { return @() }",
|
|
680
|
-
" if ($parts[$parts.Count - 1] -eq '') { $parts = $parts[0..($parts.Count - 2)] }",
|
|
681
|
-
' return $parts',
|
|
682
|
-
'}',
|
|
683
|
-
'function fx-csub([scriptblock]$b) {',
|
|
684
|
-
' $fx_prevcs = $script:fx_csub',
|
|
685
|
-
' $script:fx_csub = $true',
|
|
686
|
-
' try { $fx_o = @(& $b | ForEach-Object { [string]$_ }) }',
|
|
687
|
-
' finally { $script:fx_csub = $fx_prevcs }',
|
|
688
|
-
' $fx_s = ($fx_o -join [string][char]10)',
|
|
689
|
-
' while ($fx_s.Length -gt 0 -and $fx_s[$fx_s.Length - 1] -eq [char]10) {',
|
|
690
|
-
' $fx_s = $fx_s.Substring(0, $fx_s.Length - 1)',
|
|
691
|
-
' }',
|
|
692
|
-
' return $fx_s',
|
|
693
|
-
'}',
|
|
694
|
-
'function fx-svenc($s) {',
|
|
695
|
-
' return ([string]$s).Replace([string][char]92, ([string][char]92 + [string][char]92)).Replace([string][char]13, ([string][char]92 + [char]114)).Replace([string][char]10, ([string][char]92 + [char]110))',
|
|
696
|
-
'}',
|
|
697
|
-
'function fx-svdec($s) {',
|
|
698
|
-
' $s = [string]$s',
|
|
699
|
-
' $sb = New-Object System.Text.StringBuilder',
|
|
700
|
-
' $i = 0',
|
|
701
|
-
' while ($i -lt $s.Length) {',
|
|
702
|
-
' $c = $s[$i]',
|
|
703
|
-
' if ($c -eq [char]92 -and ($i + 1) -lt $s.Length) {',
|
|
704
|
-
' $n2 = $s[$i + 1]',
|
|
705
|
-
' if ($n2 -eq [char]110) { [void]$sb.Append([char]10); $i += 2; continue }',
|
|
706
|
-
' if ($n2 -eq [char]114) { [void]$sb.Append([char]13); $i += 2; continue }',
|
|
707
|
-
' if ($n2 -eq [char]92) { [void]$sb.Append([char]92); $i += 2; continue }',
|
|
708
|
-
' }',
|
|
709
|
-
' [void]$sb.Append($c)',
|
|
710
|
-
' $i++',
|
|
711
|
-
' }',
|
|
712
|
-
' return [string]$sb',
|
|
713
|
-
'}',
|
|
714
|
-
'function fx-arrload($n) {',
|
|
715
|
-
' $n = [string]$n',
|
|
716
|
-
' foreach ($fx_pair in @($env:FAUXNIX_ARRS -split [string][char]10)) {',
|
|
717
|
-
' $fx_eq = $fx_pair.IndexOf([char]61)',
|
|
718
|
-
' if ($fx_eq -lt 1) { continue }',
|
|
719
|
-
' if ($fx_pair.Substring(0, $fx_eq) -cne $n) { continue }',
|
|
720
|
-
' $out = @()',
|
|
721
|
-
' foreach ($el in @($fx_pair.Substring($fx_eq + 1) -split [string][char]30)) { $out += ,(fx-svdec $el) }',
|
|
722
|
-
' return $out',
|
|
723
|
-
' }',
|
|
724
|
-
' $s0 = fx-scalar0 $n',
|
|
725
|
-
' if ($null -eq $s0) { return @() }',
|
|
726
|
-
' return @([string]$s0)',
|
|
727
|
-
'}',
|
|
728
|
-
'function fx-scalar0($n) {',
|
|
729
|
-
' $n = [string]$n',
|
|
730
|
-
" if (@($env:FAUXNIX_UNSETVARS -split ';' | Where-Object { $_ -ceq $n }).Count -gt 0) { return $null }",
|
|
731
|
-
' foreach ($fx_pair in @($env:FAUXNIX_SETVALS -split [string][char]10)) {',
|
|
732
|
-
' $fx_eq = $fx_pair.IndexOf([char]61)',
|
|
733
|
-
' if ($fx_eq -lt 1) { continue }',
|
|
734
|
-
' if ($fx_pair.Substring(0, $fx_eq) -ceq $n) { return (fx-svdec $fx_pair.Substring($fx_eq + 1)) }',
|
|
735
|
-
' }',
|
|
736
|
-
" if ($n -ceq 'HOME') { return [string]$HOME }",
|
|
737
|
-
" if ($n -ceq 'PWD') { return [string]$PWD.Path }",
|
|
738
|
-
" if ($n -ceq 'USER' -or $n -ceq 'LOGNAME') { return [string]$env:USERNAME }",
|
|
739
|
-
" if ($n -ceq 'PATH') { return [string]$env:PATH }",
|
|
740
|
-
" if ($n -ceq 'SHELL') { return 'powershell' }",
|
|
741
|
-
" if ($n -ceq 'TERM') { return 'xterm-256color' }",
|
|
742
|
-
" if ($n -ceq 'OLDPWD') { return $(if ($env:FAUXNIX_OLDPWD) { [string]$env:FAUXNIX_OLDPWD } else { $null }) }",
|
|
743
|
-
" if ($n -ceq 'HOSTNAME') { return [string]$env:COMPUTERNAME }",
|
|
744
|
-
' $ev = Get-ChildItem Env: | Where-Object { $_.Name -ceq $n } | Select-Object -First 1',
|
|
745
|
-
' if ($ev) { return [string]$ev.Value }',
|
|
746
|
-
' return $null',
|
|
747
|
-
'}',
|
|
748
|
-
'function fx-ifs1 {',
|
|
749
|
-
" $s = fx-scalar0 'IFS'",
|
|
750
|
-
" if ($null -eq $s) { return ' ' }",
|
|
751
|
-
" if ([string]$s -eq '') { return '' }",
|
|
752
|
-
' return [string]$s[0]',
|
|
753
|
-
'}',
|
|
754
|
-
'function fx-arrdrop($n) {',
|
|
755
|
-
' $n = [string]$n',
|
|
756
|
-
' $fx_sm = @()',
|
|
757
|
-
' foreach ($fx_pair in @($env:FAUXNIX_ARRS -split [string][char]10)) {',
|
|
758
|
-
' $fx_eq = $fx_pair.IndexOf([char]61)',
|
|
759
|
-
' if ($fx_eq -lt 1) { continue }',
|
|
760
|
-
' if ($fx_pair.Substring(0, $fx_eq) -cne $n) { $fx_sm += $fx_pair }',
|
|
761
|
-
' }',
|
|
762
|
-
' $env:FAUXNIX_ARRS = ($fx_sm -join [string][char]10)',
|
|
763
|
-
'}',
|
|
764
|
-
'function fx-arrhas($n) {',
|
|
765
|
-
' $n = [string]$n',
|
|
766
|
-
' foreach ($fx_pair in @($env:FAUXNIX_ARRS -split [string][char]10)) {',
|
|
767
|
-
' $fx_eq = $fx_pair.IndexOf([char]61)',
|
|
768
|
-
' if ($fx_eq -lt 1) { continue }',
|
|
769
|
-
' if ($fx_pair.Substring(0, $fx_eq) -ceq $n) { return $true }',
|
|
770
|
-
' }',
|
|
771
|
-
' return $false',
|
|
772
|
-
'}',
|
|
773
|
-
'function fx-arrpackget($n) {',
|
|
774
|
-
' $n = [string]$n',
|
|
775
|
-
' foreach ($fx_pair in @($env:FAUXNIX_ARRS -split [string][char]10)) {',
|
|
776
|
-
' $fx_eq = $fx_pair.IndexOf([char]61)',
|
|
777
|
-
' if ($fx_eq -lt 1) { continue }',
|
|
778
|
-
' if ($fx_pair.Substring(0, $fx_eq) -ceq $n) { return $fx_pair.Substring($fx_eq + 1) }',
|
|
779
|
-
' }',
|
|
780
|
-
' return $null',
|
|
781
|
-
'}',
|
|
782
|
-
'function fx-arrpackset($n, $pay) {',
|
|
783
|
-
' fx-arrdrop $n',
|
|
784
|
-
' if ($null -eq $pay) { return }',
|
|
785
|
-
" $env:FAUXNIX_ARRS = ((@($env:FAUXNIX_ARRS -split [string][char]10 | Where-Object { $_ -ne '' }) + ([string]$n + [string][char]61 + [string]$pay)) -join [string][char]10)",
|
|
786
|
-
'}',
|
|
787
|
-
'function fx-arrput($n, $vals) {',
|
|
788
|
-
' $n = [string]$n',
|
|
789
|
-
' $vals = @($vals)',
|
|
790
|
-
' fx-arrdrop $n',
|
|
791
|
-
' if ($vals.Count -eq 0) { } else {',
|
|
792
|
-
' $encs = @(); foreach ($v in $vals) { $encs += (fx-svenc $v) }',
|
|
793
|
-
" $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)",
|
|
794
|
-
' }',
|
|
795
|
-
" $fx_0 = $(if ($vals.Count -gt 0) { [string]$vals[0] } else { '' })",
|
|
796
|
-
' Set-Item -LiteralPath (\'Env:\\\' + $n) -Value $fx_0',
|
|
797
|
-
" $env:FAUXNIX_SETVARS = ((@($env:FAUXNIX_SETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $n }) + $n) -join ';')",
|
|
798
|
-
" $env:FAUXNIX_UNSETVARS = (@($env:FAUXNIX_UNSETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $n }) -join ';')",
|
|
799
|
-
' $fx_sv = @(); foreach ($fx_pair in @($env:FAUXNIX_SETVALS -split [string][char]10)) { $fx_eq = $fx_pair.IndexOf([char]61); if ($fx_eq -lt 1) { continue }; if ($fx_pair.Substring(0, $fx_eq) -cne $n) { $fx_sv += $fx_pair } }',
|
|
800
|
-
" $fx_sv += ($n + [string][char]61 + (fx-svenc $fx_0)); $env:FAUXNIX_SETVALS = ($fx_sv -join [string][char]10)",
|
|
801
|
-
'}',
|
|
802
|
-
'function fx-arrclr($n) {',
|
|
803
|
-
' $n = [string]$n',
|
|
804
|
-
' fx-arrdrop $n',
|
|
805
|
-
" Remove-Item -LiteralPath ('Env:\\' + $n) -ErrorAction SilentlyContinue",
|
|
806
|
-
" $env:FAUXNIX_SETVARS = (@($env:FAUXNIX_SETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $n }) -join ';')",
|
|
807
|
-
" $env:FAUXNIX_UNSETVARS = ((@($env:FAUXNIX_UNSETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $n }) + $n) -join ';')",
|
|
808
|
-
' $fx_sv = @(); foreach ($fx_pair in @($env:FAUXNIX_SETVALS -split [string][char]10)) { $fx_eq = $fx_pair.IndexOf([char]61); if ($fx_eq -lt 1) { continue }; if ($fx_pair.Substring(0, $fx_eq) -cne $n) { $fx_sv += $fx_pair } }; $env:FAUXNIX_SETVALS = ($fx_sv -join [string][char]10)',
|
|
809
|
-
'}',
|
|
810
|
-
'function fx-subget($n, $ix) {',
|
|
811
|
-
' $arr = @(fx-arrload $n)',
|
|
812
|
-
' $ix = [string]$ix',
|
|
813
|
-
// argv-level `@` is expanded by argListExpr; this is the scalar/quoted-* join.
|
|
814
|
-
" if ($ix -eq '*') { return ($arr -join (fx-ifs1)) }",
|
|
815
|
-
" if ($ix -eq '@') { return ($arr -join (fx-ifs1)) }",
|
|
816
|
-
' $i = 0',
|
|
817
|
-
' if (-not [int]::TryParse($ix, [ref]$i)) { return \'\' }',
|
|
818
|
-
" if ($i -lt 0 -or $i -ge $arr.Count) { return '' }",
|
|
819
|
-
' return [string]$arr[$i]',
|
|
820
|
-
'}',
|
|
821
|
-
'try {',
|
|
822
|
-
...body.split('\n').map((l) => ' ' + l),
|
|
823
|
-
'} catch [System.Management.Automation.CommandNotFoundException] {',
|
|
824
|
-
" [Console]::Error.WriteLine('bash: ' + $_.Exception.TargetName + ': command not found')",
|
|
825
|
-
' $script:fx_exit = 127',
|
|
826
|
-
'} catch {',
|
|
827
|
-
' [Console]::Error.WriteLine(($_.Exception.Message).Split("`n")[0])',
|
|
828
|
-
' $script:fx_exit = 1',
|
|
829
|
-
'}',
|
|
830
|
-
'# persist session cwd and environment for the next segment',
|
|
831
|
-
'try { [IO.File]::WriteAllText($env:FAUXNIX_CWD_FILE, (Get-Location).Path) } catch {}',
|
|
832
|
-
'if ((Get-Location).Path -ne $fx_oldcwd) { $env:FAUXNIX_OLDPWD = $fx_oldcwd }',
|
|
833
|
-
'try {',
|
|
834
|
-
' $envObj = @{}',
|
|
835
|
-
' Get-ChildItem Env: | ForEach-Object { $envObj[$_.Name] = $_.Value }',
|
|
836
|
-
' [IO.File]::WriteAllText($env:FAUXNIX_ENV_FILE, (ConvertTo-Json $envObj -Compress))',
|
|
837
|
-
'} catch {}',
|
|
838
|
-
'exit $script:fx_exit',
|
|
839
867
|
];
|
|
868
|
+
const helpers = {
|
|
869
|
+
'fx-readlines': [
|
|
870
|
+
'function fx-readlines($p) {',
|
|
871
|
+
' $b = [IO.File]::ReadAllBytes($p)',
|
|
872
|
+
' $t = $null',
|
|
873
|
+
' try { $t = (New-Object System.Text.UTF8Encoding($false, $true)).GetString($b) } catch {}',
|
|
874
|
+
" if ($null -eq $t) { try { $t = [System.Text.Encoding]::GetEncoding(936).GetString($b) } catch { $t = [System.Text.Encoding]::ASCII.GetString($b) } }",
|
|
875
|
+
' $t = $t -replace "`r`n", "`n"',
|
|
876
|
+
' $t = $t -replace "`r", "`n"',
|
|
877
|
+
' $parts = @($t.Split("`n"))',
|
|
878
|
+
" if ($parts.Count -eq 1 -and $parts[0] -eq '') { return @() }",
|
|
879
|
+
" if ($parts[$parts.Count - 1] -eq '') { $parts = $parts[0..($parts.Count - 2)] }",
|
|
880
|
+
' return $parts',
|
|
881
|
+
'}',
|
|
882
|
+
],
|
|
883
|
+
'fx-csub': [
|
|
884
|
+
'function fx-csub([scriptblock]$b) {',
|
|
885
|
+
' $fx_prevcs = $script:fx_csub',
|
|
886
|
+
' $script:fx_csub = $true',
|
|
887
|
+
' try { $fx_o = @(& $b | ForEach-Object { [string]$_ }) }',
|
|
888
|
+
' finally { $script:fx_csub = $fx_prevcs }',
|
|
889
|
+
' $fx_s = ($fx_o -join [string][char]10)',
|
|
890
|
+
' while ($fx_s.Length -gt 0 -and $fx_s[$fx_s.Length - 1] -eq [char]10) {',
|
|
891
|
+
' $fx_s = $fx_s.Substring(0, $fx_s.Length - 1)',
|
|
892
|
+
' }',
|
|
893
|
+
' return $fx_s',
|
|
894
|
+
'}',
|
|
895
|
+
],
|
|
896
|
+
'fx-svenc': [
|
|
897
|
+
'function fx-svenc($s) {',
|
|
898
|
+
' return ([string]$s).Replace([string][char]92, ([string][char]92 + [string][char]92)).Replace([string][char]13, ([string][char]92 + [char]114)).Replace([string][char]10, ([string][char]92 + [char]110))',
|
|
899
|
+
'}',
|
|
900
|
+
],
|
|
901
|
+
'fx-svdec': [
|
|
902
|
+
'function fx-svdec($s) {',
|
|
903
|
+
' $s = [string]$s',
|
|
904
|
+
' $sb = New-Object System.Text.StringBuilder',
|
|
905
|
+
' $i = 0',
|
|
906
|
+
' while ($i -lt $s.Length) {',
|
|
907
|
+
' $c = $s[$i]',
|
|
908
|
+
' if ($c -eq [char]92 -and ($i + 1) -lt $s.Length) {',
|
|
909
|
+
' $n2 = $s[$i + 1]',
|
|
910
|
+
' if ($n2 -eq [char]110) { [void]$sb.Append([char]10); $i += 2; continue }',
|
|
911
|
+
' if ($n2 -eq [char]114) { [void]$sb.Append([char]13); $i += 2; continue }',
|
|
912
|
+
' if ($n2 -eq [char]92) { [void]$sb.Append([char]92); $i += 2; continue }',
|
|
913
|
+
' }',
|
|
914
|
+
' [void]$sb.Append($c)',
|
|
915
|
+
' $i++',
|
|
916
|
+
' }',
|
|
917
|
+
' return [string]$sb',
|
|
918
|
+
'}',
|
|
919
|
+
],
|
|
920
|
+
'fx-arrload': [
|
|
921
|
+
'function fx-arrload($n) {',
|
|
922
|
+
' $n = [string]$n',
|
|
923
|
+
' foreach ($fx_pair in @($env:FAUXNIX_ARRS -split [string][char]10)) {',
|
|
924
|
+
' $fx_eq = $fx_pair.IndexOf([char]61)',
|
|
925
|
+
' if ($fx_eq -lt 1) { continue }',
|
|
926
|
+
' if ($fx_pair.Substring(0, $fx_eq) -cne $n) { continue }',
|
|
927
|
+
' $out = @()',
|
|
928
|
+
' foreach ($el in @($fx_pair.Substring($fx_eq + 1) -split [string][char]30)) { $out += ,(fx-svdec $el) }',
|
|
929
|
+
' return $out',
|
|
930
|
+
' }',
|
|
931
|
+
' $s0 = fx-scalar0 $n',
|
|
932
|
+
' if ($null -eq $s0) { return @() }',
|
|
933
|
+
' return @([string]$s0)',
|
|
934
|
+
'}',
|
|
935
|
+
],
|
|
936
|
+
'fx-scalar0': [
|
|
937
|
+
'function fx-scalar0($n) {',
|
|
938
|
+
' $n = [string]$n',
|
|
939
|
+
" if (@($env:FAUXNIX_UNSETVARS -split ';' | Where-Object { $_ -ceq $n }).Count -gt 0) { return $null }",
|
|
940
|
+
' foreach ($fx_pair in @($env:FAUXNIX_SETVALS -split [string][char]10)) {',
|
|
941
|
+
' $fx_eq = $fx_pair.IndexOf([char]61)',
|
|
942
|
+
' if ($fx_eq -lt 1) { continue }',
|
|
943
|
+
' if ($fx_pair.Substring(0, $fx_eq) -ceq $n) { return (fx-svdec $fx_pair.Substring($fx_eq + 1)) }',
|
|
944
|
+
' }',
|
|
945
|
+
" if ($n -ceq 'HOME') { return [string]$HOME }",
|
|
946
|
+
" if ($n -ceq 'PWD') { return [string]$PWD.Path }",
|
|
947
|
+
" if ($n -ceq 'USER' -or $n -ceq 'LOGNAME') { return [string]$env:USERNAME }",
|
|
948
|
+
" if ($n -ceq 'PATH') { return [string]$env:PATH }",
|
|
949
|
+
" if ($n -ceq 'SHELL') { return 'powershell' }",
|
|
950
|
+
" if ($n -ceq 'TERM') { return 'xterm-256color' }",
|
|
951
|
+
" if ($n -ceq 'OLDPWD') { return $(if ($env:FAUXNIX_OLDPWD) { [string]$env:FAUXNIX_OLDPWD } else { $null }) }",
|
|
952
|
+
" if ($n -ceq 'HOSTNAME') { return [string]$env:COMPUTERNAME }",
|
|
953
|
+
' $ev = Get-ChildItem Env: | Where-Object { $_.Name -ceq $n } | Select-Object -First 1',
|
|
954
|
+
' if ($ev) { return [string]$ev.Value }',
|
|
955
|
+
' return $null',
|
|
956
|
+
'}',
|
|
957
|
+
],
|
|
958
|
+
'fx-ifs1': [
|
|
959
|
+
'function fx-ifs1 {',
|
|
960
|
+
" $s = fx-scalar0 'IFS'",
|
|
961
|
+
" if ($null -eq $s) { return ' ' }",
|
|
962
|
+
" if ([string]$s -eq '') { return '' }",
|
|
963
|
+
' return [string]$s[0]',
|
|
964
|
+
'}',
|
|
965
|
+
],
|
|
966
|
+
'fx-arrdrop': [
|
|
967
|
+
'function fx-arrdrop($n) {',
|
|
968
|
+
' $n = [string]$n',
|
|
969
|
+
' $fx_sm = @()',
|
|
970
|
+
' foreach ($fx_pair in @($env:FAUXNIX_ARRS -split [string][char]10)) {',
|
|
971
|
+
' $fx_eq = $fx_pair.IndexOf([char]61)',
|
|
972
|
+
' if ($fx_eq -lt 1) { continue }',
|
|
973
|
+
' if ($fx_pair.Substring(0, $fx_eq) -cne $n) { $fx_sm += $fx_pair }',
|
|
974
|
+
' }',
|
|
975
|
+
' $env:FAUXNIX_ARRS = ($fx_sm -join [string][char]10)',
|
|
976
|
+
'}',
|
|
977
|
+
],
|
|
978
|
+
'fx-arrhas': [
|
|
979
|
+
'function fx-arrhas($n) {',
|
|
980
|
+
' $n = [string]$n',
|
|
981
|
+
' foreach ($fx_pair in @($env:FAUXNIX_ARRS -split [string][char]10)) {',
|
|
982
|
+
' $fx_eq = $fx_pair.IndexOf([char]61)',
|
|
983
|
+
' if ($fx_eq -lt 1) { continue }',
|
|
984
|
+
' if ($fx_pair.Substring(0, $fx_eq) -ceq $n) { return $true }',
|
|
985
|
+
' }',
|
|
986
|
+
' return $false',
|
|
987
|
+
'}',
|
|
988
|
+
],
|
|
989
|
+
'fx-arrpackget': [
|
|
990
|
+
'function fx-arrpackget($n) {',
|
|
991
|
+
' $n = [string]$n',
|
|
992
|
+
' foreach ($fx_pair in @($env:FAUXNIX_ARRS -split [string][char]10)) {',
|
|
993
|
+
' $fx_eq = $fx_pair.IndexOf([char]61)',
|
|
994
|
+
' if ($fx_eq -lt 1) { continue }',
|
|
995
|
+
' if ($fx_pair.Substring(0, $fx_eq) -ceq $n) { return $fx_pair.Substring($fx_eq + 1) }',
|
|
996
|
+
' }',
|
|
997
|
+
' return $null',
|
|
998
|
+
'}',
|
|
999
|
+
],
|
|
1000
|
+
'fx-arrpackset': [
|
|
1001
|
+
'function fx-arrpackset($n, $pay) {',
|
|
1002
|
+
' fx-arrdrop $n',
|
|
1003
|
+
' if ($null -eq $pay) { return }',
|
|
1004
|
+
" $env:FAUXNIX_ARRS = ((@($env:FAUXNIX_ARRS -split [string][char]10 | Where-Object { $_ -ne '' }) + ([string]$n + [string][char]61 + [string]$pay)) -join [string][char]10)",
|
|
1005
|
+
'}',
|
|
1006
|
+
],
|
|
1007
|
+
'fx-arrput': [
|
|
1008
|
+
'function fx-arrput($n, $vals) {',
|
|
1009
|
+
' $n = [string]$n',
|
|
1010
|
+
' $vals = @($vals)',
|
|
1011
|
+
' fx-arrdrop $n',
|
|
1012
|
+
' if ($vals.Count -eq 0) { } else {',
|
|
1013
|
+
' $encs = @(); foreach ($v in $vals) { $encs += (fx-svenc $v) }',
|
|
1014
|
+
" $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)",
|
|
1015
|
+
' }',
|
|
1016
|
+
" $fx_0 = $(if ($vals.Count -gt 0) { [string]$vals[0] } else { '' })",
|
|
1017
|
+
' Set-Item -LiteralPath (\'Env:\\\' + $n) -Value $fx_0',
|
|
1018
|
+
" $env:FAUXNIX_SETVARS = ((@($env:FAUXNIX_SETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $n }) + $n) -join ';')",
|
|
1019
|
+
" $env:FAUXNIX_UNSETVARS = (@($env:FAUXNIX_UNSETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $n }) -join ';')",
|
|
1020
|
+
' $fx_sv = @(); foreach ($fx_pair in @($env:FAUXNIX_SETVALS -split [string][char]10)) { $fx_eq = $fx_pair.IndexOf([char]61); if ($fx_eq -lt 1) { continue }; if ($fx_pair.Substring(0, $fx_eq) -cne $n) { $fx_sv += $fx_pair } }',
|
|
1021
|
+
" $fx_sv += ($n + [string][char]61 + (fx-svenc $fx_0)); $env:FAUXNIX_SETVALS = ($fx_sv -join [string][char]10)",
|
|
1022
|
+
'}',
|
|
1023
|
+
],
|
|
1024
|
+
'fx-arrclr': [
|
|
1025
|
+
'function fx-arrclr($n) {',
|
|
1026
|
+
' $n = [string]$n',
|
|
1027
|
+
' fx-arrdrop $n',
|
|
1028
|
+
" Remove-Item -LiteralPath ('Env:\\' + $n) -ErrorAction SilentlyContinue",
|
|
1029
|
+
" $env:FAUXNIX_SETVARS = (@($env:FAUXNIX_SETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $n }) -join ';')",
|
|
1030
|
+
" $env:FAUXNIX_UNSETVARS = ((@($env:FAUXNIX_UNSETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $n }) + $n) -join ';')",
|
|
1031
|
+
' $fx_sv = @(); foreach ($fx_pair in @($env:FAUXNIX_SETVALS -split [string][char]10)) { $fx_eq = $fx_pair.IndexOf([char]61); if ($fx_eq -lt 1) { continue }; if ($fx_pair.Substring(0, $fx_eq) -cne $n) { $fx_sv += $fx_pair } }; $env:FAUXNIX_SETVALS = ($fx_sv -join [string][char]10)',
|
|
1032
|
+
'}',
|
|
1033
|
+
],
|
|
1034
|
+
'fx-subget': [
|
|
1035
|
+
'function fx-subget($n, $ix) {',
|
|
1036
|
+
' $arr = @(fx-arrload $n)',
|
|
1037
|
+
' $ix = [string]$ix',
|
|
1038
|
+
// argv-level `@` is expanded by argListExpr; this is the scalar/quoted-* join.
|
|
1039
|
+
" if ($ix -eq '*') { return ($arr -join (fx-ifs1)) }",
|
|
1040
|
+
" if ($ix -eq '@') { return ($arr -join (fx-ifs1)) }",
|
|
1041
|
+
' $i = 0',
|
|
1042
|
+
' if (-not [int]::TryParse($ix, [ref]$i)) { return \'\' }',
|
|
1043
|
+
" if ($i -lt 0 -or $i -ge $arr.Count) { return '' }",
|
|
1044
|
+
' return [string]$arr[$i]',
|
|
1045
|
+
'}',
|
|
1046
|
+
],
|
|
1047
|
+
};
|
|
1048
|
+
for (const name of WRAP_HELPER_ORDER) {
|
|
1049
|
+
if (needed.has(name))
|
|
1050
|
+
lines.push(...helpers[name]);
|
|
1051
|
+
}
|
|
1052
|
+
lines.push('try {', ...body.split('\n').map((l) => ' ' + l), '} catch [System.Management.Automation.CommandNotFoundException] {', " [Console]::Error.WriteLine('bash: ' + $_.Exception.TargetName + ': command not found')", ' $script:fx_exit = 127', '} catch {', ' [Console]::Error.WriteLine(($_.Exception.Message).Split("`n")[0])', ' $script:fx_exit = 1', '}', '# persist session cwd and environment for the next segment', 'try { [IO.File]::WriteAllText($env:FAUXNIX_CWD_FILE, (Get-Location).Path) } catch {}', 'if ((Get-Location).Path -ne $fx_oldcwd) { $env:FAUXNIX_OLDPWD = $fx_oldcwd }', 'try {', ' $envObj = @{}', ' Get-ChildItem Env: | ForEach-Object { $envObj[$_.Name] = $_.Value }', ' [IO.File]::WriteAllText($env:FAUXNIX_ENV_FILE, (ConvertTo-Json $envObj -Compress))', '} catch {}', 'exit $script:fx_exit');
|
|
840
1053
|
return lines.join('\n');
|
|
841
1054
|
}
|
package/package.json
CHANGED