fauxnix-cli 0.3.0 → 0.4.2
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 +29 -1
- package/dist/ast.d.ts +2 -1
- package/dist/ast.js +2 -2
- package/dist/cli.js +1 -1
- package/dist/commands/archive.js +5 -5
- package/dist/commands/files.js +28 -39
- package/dist/commands/net.js +8 -5
- package/dist/commands/sysinfo.js +32 -20
- package/dist/commands/text-filters.js +2 -4
- package/dist/commands/text-io.js +22 -24
- package/dist/executor.js +9 -1
- package/dist/mcp.js +35 -5
- package/dist/parser.js +5 -1
- package/dist/translator.d.ts +23 -4
- package/dist/translator.js +278 -22
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# fauxnix
|
|
2
2
|
|
|
3
|
+
[](https://github.com/20000419/fauxnix/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/fauxnix-cli)
|
|
5
|
+
[](https://www.npmjs.com/package/fauxnix-cli)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://glama.ai/mcp/servers/20000419/fauxnix)
|
|
8
|
+
|
|
3
9
|
**Run Linux-style commands on Windows — natively, deterministically, with no VM and no WSL.**
|
|
4
10
|
|
|
5
11
|
fauxnix is a bash→PowerShell translation layer built for AI agents. Your agent keeps writing the
|
|
@@ -8,15 +14,37 @@ fauxnix deterministically translates each command into PowerShell, executes it n
|
|
|
8
14
|
back output that looks like GNU/Linux: `ls -l` columns, bash-style error messages, coreutils exit
|
|
9
15
|
codes, UTF-8/GBK handled automatically.
|
|
10
16
|
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g fauxnix-cli # then point any MCP harness at `fauxnix mcp`
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+

|
|
22
|
+
|
|
11
23
|
```
|
|
12
24
|
$ fauxnix "ls -la src | head -2"
|
|
13
25
|
-rw-r--r-- 1 me me 1204 Aug 16 09:12 ast.ts
|
|
14
|
-
-rw-r--r-- 1 me me
|
|
26
|
+
-rw-r--r-- 1 me me 8192 Aug 16 09:12 cli.ts
|
|
15
27
|
|
|
16
28
|
$ fauxnix "cat nope.txt"
|
|
17
29
|
cat: nope.txt: No such file or directory # not a PowerShell stack trace
|
|
18
30
|
```
|
|
19
31
|
|
|
32
|
+
## Measured: your model is probably worse at PowerShell than you think
|
|
33
|
+
|
|
34
|
+
Same model (DeepSeek-V4-Pro), same 5 tasks, three execution modes on one Windows machine —
|
|
35
|
+
full data in [`docs/benchmark-deepseek-v4-pro.md`](docs/benchmark-deepseek-v4-pro.md) and
|
|
36
|
+
[`docs/benchmark-ark-models.md](docs/benchmark-ark-models.md):
|
|
37
|
+
|
|
38
|
+
| | PowerShell | **fauxnix** | Git Bash |
|
|
39
|
+
|---|---|---|---|
|
|
40
|
+
| tool calls / unexpected errors | 14 / 9 | **7 / 0** | 4 / 0 |
|
|
41
|
+
| time (T1–T4) | 163s | **66s** | 57s |
|
|
42
|
+
|
|
43
|
+
Across 7 models on the Volcano Ark Coding Plan, the PowerShell-vs-fauxnix gap held for every
|
|
44
|
+
model tested — worst case (kimi-k2-thinking): **3.1× slower with 24 error events** writing
|
|
45
|
+
PowerShell vs zero errors through fauxnix. fauxnix lands within ~15% of the real-bash ceiling
|
|
46
|
+
with no bash toolchain installed.
|
|
47
|
+
|
|
20
48
|
## Why
|
|
21
49
|
|
|
22
50
|
LLM agents are dramatically better at bash than at PowerShell — bash dominates training data, so
|
package/dist/ast.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - lists: cmd1 ; cmd2 && cmd3 || cmd4 (newlines act as ';')
|
|
7
7
|
* - redirections: > >> 2> 2>> &> 2>&1 1>&2 <
|
|
8
8
|
* - quoting: 'literal' "interp $VAR $(cmd)"
|
|
9
|
-
* - variables: $VAR ${VAR} plus special cases ($HOME $USER $PATH ...)
|
|
9
|
+
* - variables: $VAR ${VAR} ${VAR[n]} plus special cases ($HOME $USER $PATH ...)
|
|
10
10
|
* - command substitution: $(...) (recursively translated)
|
|
11
11
|
* - env assignment prefix: VAR=value cmd
|
|
12
12
|
*
|
|
@@ -62,6 +62,7 @@ export type WordPart = {
|
|
|
62
62
|
} | {
|
|
63
63
|
kind: 'Var';
|
|
64
64
|
name: string;
|
|
65
|
+
index?: string;
|
|
65
66
|
} | {
|
|
66
67
|
kind: 'CmdSub';
|
|
67
68
|
cmd: string;
|
package/dist/ast.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - lists: cmd1 ; cmd2 && cmd3 || cmd4 (newlines act as ';')
|
|
7
7
|
* - redirections: > >> 2> 2>> &> 2>&1 1>&2 <
|
|
8
8
|
* - quoting: 'literal' "interp $VAR $(cmd)"
|
|
9
|
-
* - variables: $VAR ${VAR} plus special cases ($HOME $USER $PATH ...)
|
|
9
|
+
* - variables: $VAR ${VAR} ${VAR[n]} plus special cases ($HOME $USER $PATH ...)
|
|
10
10
|
* - command substitution: $(...) (recursively translated)
|
|
11
11
|
* - env assignment prefix: VAR=value cmd
|
|
12
12
|
*
|
|
@@ -36,7 +36,7 @@ function partToString(p) {
|
|
|
36
36
|
case 'DoubleQuoted':
|
|
37
37
|
return p.parts.map(partToString).join('');
|
|
38
38
|
case 'Var':
|
|
39
|
-
return `$${p.name}`;
|
|
39
|
+
return p.index !== undefined ? `\${${p.name}[${p.index}]}` : `$${p.name}`;
|
|
40
40
|
case 'CmdSub':
|
|
41
41
|
return '$(' + p.cmd + ')';
|
|
42
42
|
}
|
package/dist/cli.js
CHANGED
package/dist/commands/archive.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { wordToString } from '../ast.js';
|
|
2
2
|
import { psStr } from '../registry.js';
|
|
3
|
-
import { exprOfWord, operandExpr } from '../translator.js';
|
|
3
|
+
import { argListExpr, exprOfWord, operandExpr } from '../translator.js';
|
|
4
4
|
/* ------------------------------------------------------------------ */
|
|
5
5
|
/* gzip family — .NET GZipStream helpers */
|
|
6
6
|
/* ------------------------------------------------------------------ */
|
|
@@ -188,7 +188,7 @@ function gzBlock(args, ctx, forced) {
|
|
|
188
188
|
fileLoop.push(' }');
|
|
189
189
|
return [
|
|
190
190
|
PS_GZ_FNS,
|
|
191
|
-
'$fx_files =
|
|
191
|
+
'$fx_files = ' + (p.files.length ? argListExpr(p.files, operandExpr) : '@()'),
|
|
192
192
|
'if ($fx_files.Count -eq 0) {',
|
|
193
193
|
stdinBranch,
|
|
194
194
|
'} else {',
|
|
@@ -204,7 +204,7 @@ const zcat = (args, ctx) => gzBlock(args, ctx, { decompress: true, stdout: true
|
|
|
204
204
|
/* ------------------------------------------------------------------ */
|
|
205
205
|
const tar = (args) => {
|
|
206
206
|
return [
|
|
207
|
-
|
|
207
|
+
'$fx_args = ' + argListExpr(args, exprOfWord),
|
|
208
208
|
// Prefer the Windows-shipped bsdtar (System32): it accepts both path
|
|
209
209
|
// styles. A PATH lookup could resolve to Git Bash's GNU tar, which
|
|
210
210
|
// misreads `C:\...` argv as a remote-host spec (host:path syntax).
|
|
@@ -255,10 +255,10 @@ const zip = (args) => {
|
|
|
255
255
|
"[Console]::Error.WriteLine('zip error: Nothing to do! (fauxnix: usage: zip [-r] ARCHIVE FILES...)'); $script:fx_exit = 12");
|
|
256
256
|
}
|
|
257
257
|
const arc = operandExpr(rest[0]);
|
|
258
|
-
const inputs = rest.slice(1)
|
|
258
|
+
const inputs = argListExpr(rest.slice(1), operandExpr);
|
|
259
259
|
return [
|
|
260
260
|
note + '$fx_arc = ' + arc,
|
|
261
|
-
'$fx_inputs =
|
|
261
|
+
'$fx_inputs = ' + inputs,
|
|
262
262
|
'$fx_valid = @()',
|
|
263
263
|
'foreach ($fx_p in $fx_inputs) {',
|
|
264
264
|
' if (Test-Path -LiteralPath $fx_p) { $fx_valid += $fx_p }',
|
package/dist/commands/files.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { wordToString } from '../ast.js';
|
|
2
2
|
import { parseWords, psStr } from '../registry.js';
|
|
3
|
-
import { exprOfWord, operandExpr } from '../translator.js';
|
|
3
|
+
import { argListExpr, exprOfWord, operandExpr } from '../translator.js';
|
|
4
4
|
/* ------------------------------------------------------------------ */
|
|
5
5
|
/* Shared PS snippets */
|
|
6
6
|
/* ------------------------------------------------------------------ */
|
|
@@ -45,9 +45,7 @@ const PS_HSIZE_FN = [
|
|
|
45
45
|
].join('\n');
|
|
46
46
|
/** Operand Words → PS array expression of string exprs. */
|
|
47
47
|
function psArray(words, fn = operandExpr) {
|
|
48
|
-
|
|
49
|
-
return '@()';
|
|
50
|
-
return '@(' + words.map(fn).join(', ') + ')';
|
|
48
|
+
return argListExpr(words, fn);
|
|
51
49
|
}
|
|
52
50
|
/* ------------------------------------------------------------------ */
|
|
53
51
|
/* ls */
|
|
@@ -63,7 +61,7 @@ const ls = (args) => {
|
|
|
63
61
|
const sortByTime = flags.has('t');
|
|
64
62
|
const sortBySize = flags.has('S');
|
|
65
63
|
const reverse = flags.has('r');
|
|
66
|
-
const targets = operandWords.length ? operandWords
|
|
64
|
+
const targets = operandWords.length ? argListExpr(operandWords) : "@('.')";
|
|
67
65
|
return [
|
|
68
66
|
PS_GLOB_FN,
|
|
69
67
|
PS_FTIME_FN,
|
|
@@ -84,7 +82,7 @@ const ls = (args) => {
|
|
|
84
82
|
' }',
|
|
85
83
|
' return $n',
|
|
86
84
|
'}',
|
|
87
|
-
'$fx_targets =
|
|
85
|
+
'$fx_targets = ' + targets,
|
|
88
86
|
'$fx_all = @()',
|
|
89
87
|
'foreach ($fx_t in $fx_targets) {',
|
|
90
88
|
' foreach ($fx_g in (fx-glob $fx_t)) {',
|
|
@@ -126,12 +124,10 @@ const cp = (args) => {
|
|
|
126
124
|
const { flags, longs, operandWords } = parseWords(args);
|
|
127
125
|
const recurse = flags.has('r') || flags.has('R') || longs.has('--recursive');
|
|
128
126
|
const verbose = flags.has('v') || longs.has('--verbose');
|
|
129
|
-
const srcs = psArray(operandWords.slice(0, -1));
|
|
130
|
-
const dst = operandWords.length >= 2 ? operandExpr(operandWords[operandWords.length - 1]) : "''";
|
|
131
127
|
return [
|
|
132
128
|
PS_GLOB_FN,
|
|
133
|
-
'$
|
|
134
|
-
|
|
129
|
+
'$fx_all = ' + argListExpr(operandWords),
|
|
130
|
+
"if ($fx_all.Count -lt 2) { $fx_srcs = @(); $fx_dst = '' } else { $fx_dst = [string]$fx_all[$fx_all.Count - 1]; $fx_srcs = @($fx_all[0..($fx_all.Count - 2)]) }",
|
|
135
131
|
"if ($fx_srcs.Count -eq 0) { [Console]::Error.WriteLine('cp: missing file operand'); $script:fx_exit = 1 }",
|
|
136
132
|
"elseif ($fx_dst -eq '') { [Console]::Error.WriteLine('cp: missing destination file operand'); $script:fx_exit = 1 }",
|
|
137
133
|
'else {',
|
|
@@ -154,12 +150,10 @@ const cp = (args) => {
|
|
|
154
150
|
const mv = (args) => {
|
|
155
151
|
const { flags, longs, operandWords } = parseWords(args);
|
|
156
152
|
const verbose = flags.has('v') || longs.has('--verbose');
|
|
157
|
-
const srcs = psArray(operandWords.slice(0, -1));
|
|
158
|
-
const dst = operandWords.length >= 2 ? operandExpr(operandWords[operandWords.length - 1]) : "''";
|
|
159
153
|
return [
|
|
160
154
|
PS_GLOB_FN,
|
|
161
|
-
'$
|
|
162
|
-
|
|
155
|
+
'$fx_all = ' + argListExpr(operandWords),
|
|
156
|
+
"if ($fx_all.Count -lt 2) { $fx_srcs = @(); $fx_dst = '' } else { $fx_dst = [string]$fx_all[$fx_all.Count - 1]; $fx_srcs = @($fx_all[0..($fx_all.Count - 2)]) }",
|
|
163
157
|
"if ($fx_srcs.Count -eq 0) { [Console]::Error.WriteLine('mv: missing file operand'); $script:fx_exit = 1 }",
|
|
164
158
|
"elseif ($fx_dst -eq '') { [Console]::Error.WriteLine('mv: missing destination file operand'); $script:fx_exit = 1 }",
|
|
165
159
|
'elseif ($fx_srcs.Count -gt 1 -and -not (Test-Path -LiteralPath $fx_dst -PathType Container)) { [Console]::Error.WriteLine("mv: target \'" + $fx_dst + "\' is not a directory"); $script:fx_exit = 1 }',
|
|
@@ -274,13 +268,11 @@ const mktemp = (args) => {
|
|
|
274
268
|
const ln = (args) => {
|
|
275
269
|
const { flags, operandWords } = parseWords(args);
|
|
276
270
|
const sym = flags.has('s');
|
|
277
|
-
const src = operandWords.length >= 2 ? operandExpr(operandWords[0]) : "''";
|
|
278
|
-
const dst = operandWords.length >= 2 ? operandExpr(operandWords[1]) : "''";
|
|
279
271
|
const kind = sym ? 'SymbolicLink' : 'HardLink';
|
|
280
272
|
const label = sym ? 'symbolic link' : 'hard link';
|
|
281
273
|
return [
|
|
282
|
-
'$
|
|
283
|
-
'$fx_dst = '
|
|
274
|
+
'$fx_ops = ' + argListExpr(operandWords),
|
|
275
|
+
"if ($fx_ops.Count -lt 2) { $fx_src = ''; $fx_dst = '' } else { $fx_src = [string]$fx_ops[0]; $fx_dst = [string]$fx_ops[1] }",
|
|
284
276
|
"if ($fx_src -eq '' -or $fx_dst -eq '') { [Console]::Error.WriteLine('ln: missing file operand'); $script:fx_exit = 1 }",
|
|
285
277
|
'else {',
|
|
286
278
|
' if (Test-Path -LiteralPath $fx_dst -PathType Container) { $fx_dst = Join-Path $fx_dst (Split-Path $fx_src -Leaf) }',
|
|
@@ -321,29 +313,28 @@ const realpath = (args) => {
|
|
|
321
313
|
/* basename / dirname */
|
|
322
314
|
/* ------------------------------------------------------------------ */
|
|
323
315
|
const basename = (args) => {
|
|
324
|
-
if (args.length === 2) {
|
|
325
|
-
return [
|
|
326
|
-
'$fx_p = ' + exprOfWord(args[0]),
|
|
327
|
-
'$fx_sfx = ' + exprOfWord(args[1]),
|
|
328
|
-
"$fx_n = [IO.Path]::GetFileName(($fx_p.TrimEnd('/')).TrimEnd('\\'))",
|
|
329
|
-
"if ($fx_n -eq '') { $fx_n = '/' }",
|
|
330
|
-
"if ($fx_sfx -ne '' -and $fx_n.EndsWith($fx_sfx) -and ($fx_n.Length -gt $fx_sfx.Length)) { $fx_n = $fx_n.Substring(0, $fx_n.Length - $fx_sfx.Length) }",
|
|
331
|
-
'$fx_n',
|
|
332
|
-
].join('\n');
|
|
333
|
-
}
|
|
334
316
|
return [
|
|
335
|
-
'$fx_ps =
|
|
317
|
+
'$fx_ps = ' + argListExpr(args, exprOfWord),
|
|
336
318
|
"if ($fx_ps.Count -eq 0) { [Console]::Error.WriteLine('basename: missing operand'); $script:fx_exit = 1 }",
|
|
337
|
-
'
|
|
319
|
+
'elseif ($fx_ps.Count -eq 2) {',
|
|
320
|
+
' $fx_p = [string]$fx_ps[0]',
|
|
321
|
+
' $fx_sfx = [string]$fx_ps[1]',
|
|
338
322
|
" $fx_n = [IO.Path]::GetFileName(($fx_p.TrimEnd('/')).TrimEnd('\\'))",
|
|
339
323
|
" if ($fx_n -eq '') { $fx_n = '/' }",
|
|
324
|
+
" if ($fx_sfx -ne '' -and $fx_n.EndsWith($fx_sfx) -and ($fx_n.Length -gt $fx_sfx.Length)) { $fx_n = $fx_n.Substring(0, $fx_n.Length - $fx_sfx.Length) }",
|
|
340
325
|
' $fx_n',
|
|
326
|
+
'} else {',
|
|
327
|
+
' foreach ($fx_p in $fx_ps) {',
|
|
328
|
+
" $fx_n = [IO.Path]::GetFileName(($fx_p.TrimEnd('/')).TrimEnd('\\'))",
|
|
329
|
+
" if ($fx_n -eq '') { $fx_n = '/' }",
|
|
330
|
+
' $fx_n',
|
|
331
|
+
' }',
|
|
341
332
|
'}',
|
|
342
333
|
].join('\n');
|
|
343
334
|
};
|
|
344
335
|
const dirname = (args) => {
|
|
345
336
|
return [
|
|
346
|
-
'$fx_ps =
|
|
337
|
+
'$fx_ps = ' + argListExpr(args, exprOfWord),
|
|
347
338
|
"if ($fx_ps.Count -eq 0) { [Console]::Error.WriteLine('dirname: missing operand'); $script:fx_exit = 1 }",
|
|
348
339
|
'foreach ($fx_p in $fx_ps) {',
|
|
349
340
|
" $fx_n = ($fx_p.TrimEnd('/')).TrimEnd('\\')",
|
|
@@ -428,7 +419,7 @@ const du = (args) => {
|
|
|
428
419
|
const { flags, longs, operandWords } = parseWords(args, [], ['--max-depth']);
|
|
429
420
|
const sum = flags.has('s') || longs.has('--summarize');
|
|
430
421
|
const human = flags.has('h') || longs.has('--human-readable');
|
|
431
|
-
const targets = operandWords.length ? operandWords
|
|
422
|
+
const targets = operandWords.length ? argListExpr(operandWords) : "@('.')";
|
|
432
423
|
return [
|
|
433
424
|
PS_HSIZE_FN,
|
|
434
425
|
'function fx-size($p) {',
|
|
@@ -436,7 +427,7 @@ const du = (args) => {
|
|
|
436
427
|
' Get-ChildItem -LiteralPath $p -Recurse -Force -File -ErrorAction SilentlyContinue | ForEach-Object { $t += $_.Length }',
|
|
437
428
|
' return [math]::Ceiling($t / 1KB)',
|
|
438
429
|
'}',
|
|
439
|
-
'$fx_ts =
|
|
430
|
+
'$fx_ts = ' + targets,
|
|
440
431
|
'foreach ($fx_t in $fx_ts) {',
|
|
441
432
|
' if (-not (Test-Path -LiteralPath $fx_t)) { [Console]::Error.WriteLine("du: cannot access \'" + $fx_t + "\': No such file or directory"); $script:fx_exit = 1; continue }',
|
|
442
433
|
' if (' + (sum ? '$true' : '$false') + ') {',
|
|
@@ -500,7 +491,7 @@ const find = (args) => {
|
|
|
500
491
|
const sizeExpr = extractValue(preds, ['-size']);
|
|
501
492
|
const mtimeExpr = extractValue(preds, ['-mtime']);
|
|
502
493
|
const wantDelete = preds.includes('-delete');
|
|
503
|
-
const paths = pathWords.length ? pathWords
|
|
494
|
+
const paths = pathWords.length ? argListExpr(pathWords) : "@('.')";
|
|
504
495
|
const conditions = [];
|
|
505
496
|
if (namePat !== null)
|
|
506
497
|
conditions.push("($fx_i.Name -like '" + likeOf(namePat) + "')");
|
|
@@ -516,7 +507,7 @@ const find = (args) => {
|
|
|
516
507
|
const sizeCond = sizeOf(sizeExpr);
|
|
517
508
|
const mtimeCond = mtimeOf(mtimeExpr);
|
|
518
509
|
return [
|
|
519
|
-
'$fx_paths =
|
|
510
|
+
'$fx_paths = ' + paths,
|
|
520
511
|
'foreach ($fx_p in $fx_paths) {',
|
|
521
512
|
' if (-not (Test-Path -LiteralPath $fx_p)) { [Console]::Error.WriteLine("find: \'" + $fx_p + "\': No such file or directory"); $script:fx_exit = 1; continue }',
|
|
522
513
|
' $fx_root = (Get-Item -LiteralPath $fx_p -Force).FullName',
|
|
@@ -632,13 +623,11 @@ const diff = (args) => {
|
|
|
632
623
|
const unified = flags.has('u') || flags.has('U');
|
|
633
624
|
const brief = flags.has('q') || flags.has('brief');
|
|
634
625
|
void unified;
|
|
635
|
-
const a = operandWords.length > 0 ? operandExpr(operandWords[0]) : "''";
|
|
636
|
-
const b = operandWords.length > 1 ? operandExpr(operandWords[1]) : "''";
|
|
637
626
|
return [
|
|
638
627
|
PS_READTEXT_FN,
|
|
639
628
|
'function fx-dr($x, $y) { if ($x -eq $y) { return [string]$x } else { return ([string]$x) + \',\' + ([string]$y) } }',
|
|
640
|
-
'$
|
|
641
|
-
'$fx_b = '
|
|
629
|
+
'$fx_ops = ' + argListExpr(operandWords),
|
|
630
|
+
"if ($fx_ops.Count -lt 2) { $fx_a = ''; $fx_b = '' } else { $fx_a = [string]$fx_ops[0]; $fx_b = [string]$fx_ops[1] }",
|
|
642
631
|
"if ($fx_a -eq '' -or $fx_b -eq '') { [Console]::Error.WriteLine('diff: missing operand'); $script:fx_exit = 2 }",
|
|
643
632
|
'elseif (-not (Test-Path -LiteralPath $fx_a)) { [Console]::Error.WriteLine("diff: " + $fx_a + ": No such file or directory"); $script:fx_exit = 2 }',
|
|
644
633
|
'elseif (-not (Test-Path -LiteralPath $fx_b)) { [Console]::Error.WriteLine("diff: " + $fx_b + ": No such file or directory"); $script:fx_exit = 2 }',
|
package/dist/commands/net.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { wordToString } from '../ast.js';
|
|
2
2
|
import { parseWords, psErr, psStr } from '../registry.js';
|
|
3
|
-
import { exprOfWord, literalOfWord, operandExpr } from '../translator.js';
|
|
3
|
+
import { argListExpr, exprOfWord, literalOfWord, operandExpr } from '../translator.js';
|
|
4
4
|
/* ------------------------------------------------------------------ */
|
|
5
5
|
/* Shared PS snippets */
|
|
6
6
|
/* ------------------------------------------------------------------ */
|
|
@@ -94,7 +94,7 @@ const curl = (args) => {
|
|
|
94
94
|
// refuses private/loopback URLs before the process is even started.
|
|
95
95
|
return [
|
|
96
96
|
PS_NETGUARD_FNS,
|
|
97
|
-
|
|
97
|
+
'$fx_args = ' + argListExpr(args, exprOfWord),
|
|
98
98
|
'$fx_bad = $false',
|
|
99
99
|
"foreach ($fx_a in $fx_args) { if (fx-netguard 'curl' $fx_a) { $fx_bad = $true } }",
|
|
100
100
|
'if ($fx_bad) { $script:fx_exit = 1 }',
|
|
@@ -212,11 +212,11 @@ function mapWgetArgs(args) {
|
|
|
212
212
|
return { margs, sawOutput, urls };
|
|
213
213
|
}
|
|
214
214
|
const wget = (args) => {
|
|
215
|
-
const orig = args.map(exprOfWord);
|
|
215
|
+
const orig = args.map((w) => exprOfWord(w));
|
|
216
216
|
const mapped = mapWgetArgs(args);
|
|
217
217
|
return [
|
|
218
218
|
PS_NETGUARD_FNS,
|
|
219
|
-
'$fx_args =
|
|
219
|
+
'$fx_args = ' + argListExpr(args, exprOfWord),
|
|
220
220
|
'$fx_margs = @(' + mapped.margs.join(', ') + ')',
|
|
221
221
|
'$fx_bad = $false',
|
|
222
222
|
"foreach ($fx_a in $fx_args) { if (fx-netguard 'wget' $fx_a) { $fx_bad = $true } }",
|
|
@@ -456,7 +456,10 @@ const ifconfig = (args) => {
|
|
|
456
456
|
/* ------------------------------------------------------------------ */
|
|
457
457
|
/* nslookup / dig / host */
|
|
458
458
|
/* ------------------------------------------------------------------ */
|
|
459
|
-
const nslookup = (args) =>
|
|
459
|
+
const nslookup = (args) => [
|
|
460
|
+
'$fx_args = ' + argListExpr(args, exprOfWord),
|
|
461
|
+
nativeCall('nslookup.exe', '$fx_args'),
|
|
462
|
+
].join('\n');
|
|
460
463
|
const dig = (args) => {
|
|
461
464
|
const raw = args.map(wordToString);
|
|
462
465
|
let short = false;
|
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 { exprOfWord, operandExpr, translateSimple, wrapTempEnv, encodeSetValExpr, escapeDq, translateCmdSub, normalizeLiteralPath, pathExpr, } from '../translator.js';
|
|
3
|
+
import { argListExpr, exprOfWord, operandExpr, translateSimple, wrapTempEnv, encodeSetValExpr, escapeDq, translateCmdSub, normalizeLiteralPath, pathExpr, } from '../translator.js';
|
|
4
4
|
import { handlers as textIoHandlers } from './text-io.js';
|
|
5
5
|
/* ------------------------------------------------------------------ */
|
|
6
6
|
/* Shared TS helpers */
|
|
@@ -10,6 +10,7 @@ const NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
|
10
10
|
function emitSetValPut(nameLit, valueExpr) {
|
|
11
11
|
const n = nameLit.replace(/'/g, "''");
|
|
12
12
|
return [
|
|
13
|
+
"fx-arrdrop '" + n + "'",
|
|
13
14
|
'$fx_sv = @()',
|
|
14
15
|
'foreach ($fx_pair in @($env:FAUXNIX_SETVALS -split [string][char]10)) {',
|
|
15
16
|
' $fx_eq = $fx_pair.IndexOf([char]61)',
|
|
@@ -22,6 +23,7 @@ function emitSetValPut(nameLit, valueExpr) {
|
|
|
22
23
|
}
|
|
23
24
|
function emitSetValDelRuntime(nameVar) {
|
|
24
25
|
return [
|
|
26
|
+
'fx-arrdrop ' + nameVar,
|
|
25
27
|
'$fx_sv = @()',
|
|
26
28
|
'foreach ($fx_pair in @($env:FAUXNIX_SETVALS -split [string][char]10)) {',
|
|
27
29
|
' $fx_eq = $fx_pair.IndexOf([char]61)',
|
|
@@ -62,9 +64,7 @@ function splitAssignWord(w) {
|
|
|
62
64
|
}
|
|
63
65
|
/** Text Words -> PS array expression. */
|
|
64
66
|
function textArgs(words) {
|
|
65
|
-
|
|
66
|
-
return '@()';
|
|
67
|
-
return '@(' + words.map(exprOfWord).join(', ') + ')';
|
|
67
|
+
return argListExpr(words, exprOfWord);
|
|
68
68
|
}
|
|
69
69
|
/** Drop dash-arguments, return operand words. */
|
|
70
70
|
function stripFlags(args) {
|
|
@@ -128,10 +128,12 @@ const cd = (args) => {
|
|
|
128
128
|
].join('\n');
|
|
129
129
|
}
|
|
130
130
|
return [
|
|
131
|
-
'$
|
|
131
|
+
'$fx_ds = ' + argListExpr([args[0]]),
|
|
132
|
+
"if ($fx_ds.Count -ne 1) { [Console]::Error.WriteLine('bash: cd: too many arguments'); $script:fx_exit = 1 }",
|
|
133
|
+
'else { $fx_d = [string]$fx_ds[0]',
|
|
132
134
|
'if (-not (Test-Path -LiteralPath $fx_d)) { [Console]::Error.WriteLine("bash: cd: " + $fx_d + ": No such file or directory"); $script:fx_exit = 1 }',
|
|
133
135
|
'elseif (-not (Test-Path -LiteralPath $fx_d -PathType Container)) { [Console]::Error.WriteLine("bash: cd: " + $fx_d + ": Not a directory"); $script:fx_exit = 1 }',
|
|
134
|
-
'else { try { Set-Location -LiteralPath $fx_d } catch { [Console]::Error.WriteLine("bash: cd: " + $fx_d + ": No such file or directory"); $script:fx_exit = 1 } }',
|
|
136
|
+
'else { try { Set-Location -LiteralPath $fx_d } catch { [Console]::Error.WriteLine("bash: cd: " + $fx_d + ": No such file or directory"); $script:fx_exit = 1 } } }',
|
|
135
137
|
].join('\n');
|
|
136
138
|
};
|
|
137
139
|
const pwd = (args) => {
|
|
@@ -162,7 +164,7 @@ const exportCmd = (args) => {
|
|
|
162
164
|
}
|
|
163
165
|
return sets
|
|
164
166
|
.map((s) => {
|
|
165
|
-
const val = exprOfWord(s.value);
|
|
167
|
+
const val = exprOfWord(s.value, { preserveCmdSub: true });
|
|
166
168
|
return ('$fx_exv = ' +
|
|
167
169
|
val +
|
|
168
170
|
'; $env:' +
|
|
@@ -886,6 +888,7 @@ const FX_TNK_FN = [
|
|
|
886
888
|
' $fx_ev = Get-ChildItem Env: | Where-Object { $_.Name -ceq $n } | Select-Object -First 1',
|
|
887
889
|
' $nm = if ($fx_ev) { $fx_ev.Name } else { $n }',
|
|
888
890
|
" Set-Item -LiteralPath ('Env:' + $nm) -Value ([string][long]$v)",
|
|
891
|
+
' fx-arrdrop $n',
|
|
889
892
|
" $env:FAUXNIX_SETVARS = ((@($env:FAUXNIX_SETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $n }) + $n) -join ';')",
|
|
890
893
|
" $env:FAUXNIX_UNSETVARS = (@($env:FAUXNIX_UNSETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $n }) -join ';')",
|
|
891
894
|
' $fx_sv = @()',
|
|
@@ -1341,6 +1344,9 @@ function kshExprOfWord(w) {
|
|
|
1341
1344
|
if (tilde && expanded.length === 0)
|
|
1342
1345
|
return '(fx-home)';
|
|
1343
1346
|
if (!tilde && expanded.length === 1 && expanded[0].kind === 'Var') {
|
|
1347
|
+
if (expanded[0].index !== undefined) {
|
|
1348
|
+
return '(fx-subget ' + psStr(expanded[0].name) + ' ' + psStr(expanded[0].index) + ')';
|
|
1349
|
+
}
|
|
1344
1350
|
return '(fx-envget ' + psStr(expanded[0].name) + ')';
|
|
1345
1351
|
}
|
|
1346
1352
|
const literal = !tilde && expanded.every((p) => p.kind === 'Text' || p.kind === 'SingleQuoted');
|
|
@@ -1362,10 +1368,14 @@ function kshExprOfWord(w) {
|
|
|
1362
1368
|
emitPart(q);
|
|
1363
1369
|
break;
|
|
1364
1370
|
case 'Var':
|
|
1365
|
-
out +=
|
|
1371
|
+
out +=
|
|
1372
|
+
p.index !== undefined
|
|
1373
|
+
? '$(fx-subget ' + psStr(p.name) + ' ' + psStr(p.index) + ')'
|
|
1374
|
+
: '$(fx-envget ' + psStr(p.name) + ')';
|
|
1366
1375
|
break;
|
|
1367
1376
|
case 'CmdSub':
|
|
1368
|
-
|
|
1377
|
+
// [[ ]] does not IFS-split, so keep the newline contract.
|
|
1378
|
+
out += '$(' + translateCmdSub(p.cmd, true) + ')';
|
|
1369
1379
|
break;
|
|
1370
1380
|
}
|
|
1371
1381
|
};
|
|
@@ -1378,7 +1388,16 @@ const FX_ISSET_FN = [
|
|
|
1378
1388
|
'function fx-isset($n) {',
|
|
1379
1389
|
' $n = [string]$n',
|
|
1380
1390
|
" if ($n -eq '') { return $false }",
|
|
1381
|
-
" if ($n -match '^([A-Za-z_][A-Za-z0-9_]*)\\[(0
|
|
1391
|
+
" if ($n -match '^([A-Za-z_][A-Za-z0-9_]*)\\[([0-9]+|@|\\*)\\]$') {",
|
|
1392
|
+
' $fx_ib = $Matches[1]; $fx_ix = [string]$Matches[2]',
|
|
1393
|
+
" if ($fx_ix -eq '@' -or $fx_ix -eq '*' -or $fx_ix -eq '0') { $n = $fx_ib }",
|
|
1394
|
+
' else {',
|
|
1395
|
+
' $fx_ia = @(fx-arrload $fx_ib)',
|
|
1396
|
+
' $fx_ii = 0',
|
|
1397
|
+
' if (-not [int]::TryParse($fx_ix, [ref]$fx_ii)) { return $false }',
|
|
1398
|
+
' return ($fx_ii -ge 0 -and $fx_ii -lt $fx_ia.Count)',
|
|
1399
|
+
' }',
|
|
1400
|
+
' }',
|
|
1382
1401
|
" elseif ($n -match '^[A-Za-z_][A-Za-z0-9_]*\\[') { return $false }",
|
|
1383
1402
|
" if (@($env:FAUXNIX_UNSETVARS -split ';' | Where-Object { $_ -ceq $n }).Count -gt 0) { return $false }",
|
|
1384
1403
|
" if (@($env:FAUXNIX_SETVARS -split ';' | Where-Object { $_ -ceq $n }).Count -gt 0) { return $true }",
|
|
@@ -1425,18 +1444,11 @@ const FX_RE_FN = [
|
|
|
1425
1444
|
' try {',
|
|
1426
1445
|
' $fx_rm = [regex]::Match([string]$a, (fx-posixre ([string]$b)), [Text.RegularExpressions.RegexOptions]::Singleline)',
|
|
1427
1446
|
' if ($fx_rm.Success) {',
|
|
1428
|
-
' $
|
|
1429
|
-
"
|
|
1430
|
-
" $env:FAUXNIX_UNSETVARS = (@($env:FAUXNIX_UNSETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne 'BASH_REMATCH' }) -join ';')",
|
|
1431
|
-
' $fx_enc = ([string]$fx_rm.Value).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))',
|
|
1432
|
-
" $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 'BASH_REMATCH') { $fx_sv += $fx_pair } }",
|
|
1433
|
-
" $fx_sv += ('BASH_REMATCH' + [string][char]61 + $fx_enc); $env:FAUXNIX_SETVALS = ($fx_sv -join [string][char]10)",
|
|
1447
|
+
' $fx_gs = @(); foreach ($fx_g in $fx_rm.Groups) { $fx_gs += ,[string]$fx_g.Value }',
|
|
1448
|
+
" fx-arrput 'BASH_REMATCH' $fx_gs",
|
|
1434
1449
|
' return $true',
|
|
1435
1450
|
' }',
|
|
1436
|
-
"
|
|
1437
|
-
" $env:FAUXNIX_SETVARS = (@($env:FAUXNIX_SETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne 'BASH_REMATCH' }) -join ';')",
|
|
1438
|
-
" $env:FAUXNIX_UNSETVARS = ((@($env:FAUXNIX_UNSETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne 'BASH_REMATCH' }) + 'BASH_REMATCH') -join ';')",
|
|
1439
|
-
" $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 'BASH_REMATCH') { $fx_sv += $fx_pair } }; $env:FAUXNIX_SETVALS = ($fx_sv -join [string][char]10)",
|
|
1451
|
+
" fx-arrclr 'BASH_REMATCH'",
|
|
1440
1452
|
' return $false',
|
|
1441
1453
|
' }',
|
|
1442
1454
|
" catch { [Console]::Error.WriteLine('bash: [[: invalid regular expression'); $script:fx_exit = 2; return $false }",
|
|
@@ -3,7 +3,7 @@ import os from 'node:os';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { FauxnixParseError, wordToString } from '../ast.js';
|
|
5
5
|
import { parseWords, psStr } from '../registry.js';
|
|
6
|
-
import { exprOfWord, literalOfWord, operandExpr } from '../translator.js';
|
|
6
|
+
import { argListExpr, exprOfWord, literalOfWord, operandExpr } from '../translator.js';
|
|
7
7
|
/* ------------------------------------------------------------------ */
|
|
8
8
|
/* Shared PS snippets (same shape as files.ts) */
|
|
9
9
|
/* ------------------------------------------------------------------ */
|
|
@@ -34,9 +34,7 @@ const PS_SPLITLINES_FN = [
|
|
|
34
34
|
const STDIN_LINES = '@($input | ForEach-Object { [string]$_ })';
|
|
35
35
|
/** Operand Words → PS array expression of string exprs. */
|
|
36
36
|
function psArray(words, fn = operandExpr) {
|
|
37
|
-
|
|
38
|
-
return '@()';
|
|
39
|
-
return '@(' + words.map(fn).join(', ') + ')';
|
|
37
|
+
return argListExpr(words, fn);
|
|
40
38
|
}
|
|
41
39
|
/** PS boolean literal. */
|
|
42
40
|
function pb(v) {
|
package/dist/commands/text-io.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { wordToString } from '../ast.js';
|
|
2
2
|
import { lookup, parseWords, psStr } from '../registry.js';
|
|
3
|
-
import { exprOfWord, operandExpr } from '../translator.js';
|
|
3
|
+
import { argListExpr, exprOfWord, operandExpr } from '../translator.js';
|
|
4
4
|
/* ------------------------------------------------------------------ */
|
|
5
5
|
/* Shared PS snippets (same shape as files.ts / text-filters.ts) */
|
|
6
6
|
/* ------------------------------------------------------------------ */
|
|
@@ -80,6 +80,9 @@ const PS_UNESQ_FN = [
|
|
|
80
80
|
const PS_WRITE_FN = [
|
|
81
81
|
'function fx-write($s, $term) {',
|
|
82
82
|
" if ($s -eq '') { return }",
|
|
83
|
+
// Inside quoted/assignment $(...) the collector wants one string object
|
|
84
|
+
// so interior newlines survive (PS would otherwise join lines with spaces).
|
|
85
|
+
' if ($script:fx_csub) { $s; return }',
|
|
83
86
|
' if (-not $term) { $s; return }',
|
|
84
87
|
' if (-not $s.EndsWith([string][char]10)) { [Console]::Out.Write($s); return }',
|
|
85
88
|
' $t = $s.Substring(0, $s.Length - 1)',
|
|
@@ -110,9 +113,7 @@ function qErr(cmd, g, msg, lead = 'cannot open ') {
|
|
|
110
113
|
}
|
|
111
114
|
/** Operand Words → PS array expression of string exprs. */
|
|
112
115
|
function psArray(words, fn = operandExpr) {
|
|
113
|
-
|
|
114
|
-
return '@()';
|
|
115
|
-
return '@(' + words.map(fn).join(', ') + ')';
|
|
116
|
+
return argListExpr(words, fn);
|
|
116
117
|
}
|
|
117
118
|
/**
|
|
118
119
|
* Collect file operands through fx-glob into `$fx_srcs`. A literal `-`
|
|
@@ -136,8 +137,8 @@ function psCollectFiles(operandWords, missErr, dirErr, stdinDefault = true) {
|
|
|
136
137
|
const lit = w.every((p) => p.kind === 'Text' || p.kind === 'SingleQuoted')
|
|
137
138
|
? w.map((p) => p.text).join('')
|
|
138
139
|
: null;
|
|
139
|
-
out.push('$fx_d
|
|
140
|
-
out.push('foreach ($fx_g in (fx-glob
|
|
140
|
+
out.push('foreach ($fx_d in ' + argListExpr([w], lit !== null ? operandExpr : exprOfWord) + ') {');
|
|
141
|
+
out.push('foreach ($fx_g in (fx-glob $fx_d)) {');
|
|
141
142
|
if (dirErr) {
|
|
142
143
|
const dis = lit !== null && /[*?]/.test(lit) ? '$fx_g' : '$fx_d';
|
|
143
144
|
out.push(' if (Test-Path -LiteralPath $fx_g -PathType Container) { ' +
|
|
@@ -153,7 +154,7 @@ function psCollectFiles(operandWords, missErr, dirErr, stdinDefault = true) {
|
|
|
153
154
|
(lit !== null && /[*?]/.test(lit)
|
|
154
155
|
? '$fx_names += $fx_g'
|
|
155
156
|
: '$fx_names += $fx_d'));
|
|
156
|
-
out.push('}');
|
|
157
|
+
out.push('}', '}');
|
|
157
158
|
}
|
|
158
159
|
return out;
|
|
159
160
|
}
|
|
@@ -915,16 +916,6 @@ const base64 = (args, ctx) => {
|
|
|
915
916
|
const seq = (args, ctx) => {
|
|
916
917
|
const { flags, values, operandWords } = parseWords(args, ['s']);
|
|
917
918
|
const eq = flags.has('w');
|
|
918
|
-
const nums = operandWords.map((w) => exprOfWord(w));
|
|
919
|
-
if (nums.length === 0) {
|
|
920
|
-
return psErrExpr(psStr('seq: missing operand'));
|
|
921
|
-
}
|
|
922
|
-
if (nums.length > 3) {
|
|
923
|
-
return psErrExpr(psStr('seq: extra operand ') + ' + ' + nums[3]);
|
|
924
|
-
}
|
|
925
|
-
const first = nums.length >= 2 ? nums[0] : '1';
|
|
926
|
-
const inc = nums.length === 3 ? nums[1] : '1';
|
|
927
|
-
const last = nums.length === 3 ? nums[2] : nums[nums.length - 1];
|
|
928
919
|
const sepExpr = values.has('-s') ? psStr(values.get('-s')) : '[string][char]10';
|
|
929
920
|
return [
|
|
930
921
|
PS_WRITE_FN,
|
|
@@ -937,9 +928,17 @@ const seq = (args, ctx) => {
|
|
|
937
928
|
" if ([string]$s -match '^[+-]?[0-9]*\\.([0-9]+)') { return $Matches[1].Length }",
|
|
938
929
|
' return 0',
|
|
939
930
|
'}',
|
|
940
|
-
'$
|
|
941
|
-
|
|
942
|
-
|
|
931
|
+
'$fx_nums = ' + argListExpr(operandWords),
|
|
932
|
+
"if ($fx_nums.Count -eq 0) { " +
|
|
933
|
+
psErrExpr(psStr('seq: missing operand')) +
|
|
934
|
+
' }',
|
|
935
|
+
"elseif ($fx_nums.Count -gt 3) { " +
|
|
936
|
+
psErrExpr(psStr('seq: extra operand ') + ' + $fx_nums[3]') +
|
|
937
|
+
' }',
|
|
938
|
+
'else {',
|
|
939
|
+
" $fx_a = [string]$(if ($fx_nums.Count -ge 2) { $fx_nums[0] } else { '1' })",
|
|
940
|
+
" $fx_b = [string]$(if ($fx_nums.Count -eq 3) { $fx_nums[1] } else { '1' })",
|
|
941
|
+
' $fx_c = [string]$(if ($fx_nums.Count -eq 3) { $fx_nums[2] } else { $fx_nums[$fx_nums.Count - 1] })',
|
|
943
942
|
'$fx_first = fx-tod $fx_a',
|
|
944
943
|
'$fx_inc = fx-tod $fx_b',
|
|
945
944
|
'$fx_last = fx-tod $fx_c',
|
|
@@ -968,6 +967,7 @@ const seq = (args, ctx) => {
|
|
|
968
967
|
' }',
|
|
969
968
|
' if ($fx_strs.Count -eq 0) { }',
|
|
970
969
|
' else { fx-write (($fx_strs -join (' + sepExpr + ')) + [string][char]10) $fx_term }',
|
|
970
|
+
' }',
|
|
971
971
|
'}',
|
|
972
972
|
].join('\n');
|
|
973
973
|
};
|
|
@@ -1069,8 +1069,6 @@ const xargs = (args) => {
|
|
|
1069
1069
|
if (firstLit !== null && lookup(firstLit) !== undefined) {
|
|
1070
1070
|
return psErrExpr(psStr(XARGS_BUILTIN_MSG));
|
|
1071
1071
|
}
|
|
1072
|
-
const cmdExpr = exprOfWord(target[0]);
|
|
1073
|
-
const baseArgs = psArray(target.slice(1), exprOfWord);
|
|
1074
1072
|
const n = chunkN !== null && Number.isFinite(chunkN) && chunkN > 0 ? chunkN : 0;
|
|
1075
1073
|
const replExpr = repl !== null ? psStr(repl) : null;
|
|
1076
1074
|
const invoke = [
|
|
@@ -1137,8 +1135,8 @@ const xargs = (args) => {
|
|
|
1137
1135
|
return [
|
|
1138
1136
|
PS_SPLITLINES_FN,
|
|
1139
1137
|
STDIN_INLINES,
|
|
1140
|
-
'$
|
|
1141
|
-
'$fx_base =
|
|
1138
|
+
'$fx_tg = ' + argListExpr(target, exprOfWord),
|
|
1139
|
+
"if ($fx_tg.Count -eq 0) { $fx_cmd = ''; $fx_base = @() } else { $fx_cmd = [string]$fx_tg[0]; $fx_base = $(if ($fx_tg.Count -gt 1) { @($fx_tg[1..($fx_tg.Count - 1)]) } else { @() }) }",
|
|
1142
1140
|
"$fx_args = @($fx_in | Where-Object { $_ -ne '' })",
|
|
1143
1141
|
...dispatch,
|
|
1144
1142
|
].join('\n');
|
package/dist/executor.js
CHANGED
|
@@ -350,7 +350,15 @@ async function runPlans(plans, session, opts, afterSegment, scriptFile) {
|
|
|
350
350
|
}, timeoutMs);
|
|
351
351
|
const code = await new Promise((resolve) => {
|
|
352
352
|
child.on('error', (e) => {
|
|
353
|
-
|
|
353
|
+
if (e.code === 'ENOENT') {
|
|
354
|
+
stderr +=
|
|
355
|
+
'fauxnix: powershell.exe not found — fauxnix executes bash via native Windows PowerShell 5.1+.\n' +
|
|
356
|
+
'This host has no PowerShell on PATH (typical for Linux containers/sandboxes).\n' +
|
|
357
|
+
'Run fauxnix on Windows, or install PowerShell and make powershell.exe reachable on PATH.\n';
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
stderr += 'fauxnix: failed to start powershell.exe: ' + e.message + '\n';
|
|
361
|
+
}
|
|
354
362
|
resolve(127);
|
|
355
363
|
});
|
|
356
364
|
child.on('close', (c) => resolve(running.killed ? 124 : (c ?? 0)));
|
package/dist/mcp.js
CHANGED
|
@@ -1,12 +1,35 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
3
5
|
import { z } from 'zod';
|
|
4
6
|
import { FauxnixSession } from './executor.js';
|
|
5
7
|
import { parseCommand } from './parser.js';
|
|
6
8
|
import { translateCommandList, wrapScript, translatePipelineBody } from './translator.js';
|
|
7
9
|
import { registeredNames } from './registry.js';
|
|
8
10
|
import './commands/install-all.js';
|
|
11
|
+
// single source of truth: the npm package version in package.json
|
|
12
|
+
// (src/ and dist/ sit one level below the root, so the relative path holds in both)
|
|
13
|
+
const pkgVersion = JSON.parse(readFileSync(fileURLToPath(new URL('../package.json', import.meta.url)), 'utf8')).version;
|
|
9
14
|
const TOOL_NAME = process.env.FAUXNIX_TOOL_NAME || 'bash';
|
|
15
|
+
const EXEC_ANNOTATIONS = {
|
|
16
|
+
readOnlyHint: false,
|
|
17
|
+
destructiveHint: true,
|
|
18
|
+
idempotentHint: false,
|
|
19
|
+
openWorldHint: true,
|
|
20
|
+
};
|
|
21
|
+
const TRANSLATE_ANNOTATIONS = {
|
|
22
|
+
readOnlyHint: true,
|
|
23
|
+
destructiveHint: false,
|
|
24
|
+
idempotentHint: true,
|
|
25
|
+
openWorldHint: false,
|
|
26
|
+
};
|
|
27
|
+
const SESSION_ANNOTATIONS = {
|
|
28
|
+
readOnlyHint: false,
|
|
29
|
+
destructiveHint: false,
|
|
30
|
+
idempotentHint: true,
|
|
31
|
+
openWorldHint: false,
|
|
32
|
+
};
|
|
10
33
|
const TOOL_DESCRIPTION = `Execute a Linux/bash-style command on this Windows machine.
|
|
11
34
|
|
|
12
35
|
Commands are deterministically translated to PowerShell and executed natively — no WSL or VM.
|
|
@@ -17,9 +40,11 @@ Unknown commands (git, node, npm, python, cargo...) are passed through and execu
|
|
|
17
40
|
Not supported: heredocs, backticks, control flow (if/for/while), background jobs.
|
|
18
41
|
|
|
19
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.
|
|
20
|
-
Exit codes follow bash conventions (0 ok, 1 fail, 2 usage/serious, 127 command not found, 124 timeout)
|
|
43
|
+
Exit codes follow bash conventions (0 ok, 1 fail, 2 usage/serious, 127 command not found, 124 timeout).
|
|
44
|
+
|
|
45
|
+
Platform requirement: the execution backend is native Windows PowerShell 5.1+. On hosts without PowerShell on PATH (e.g. Linux containers/sandboxes), the bash tool returns exit code 127 with an actionable error instead of running the command.`;
|
|
21
46
|
export async function startMcpServer() {
|
|
22
|
-
const server = new McpServer({ name: 'fauxnix', version:
|
|
47
|
+
const server = new McpServer({ name: 'fauxnix', version: pkgVersion }, { capabilities: { tools: {} } });
|
|
23
48
|
const session = new FauxnixSession();
|
|
24
49
|
server.tool(TOOL_NAME, TOOL_DESCRIPTION, {
|
|
25
50
|
command: z.string().describe('The bash-style command line to run'),
|
|
@@ -30,7 +55,7 @@ export async function startMcpServer() {
|
|
|
30
55
|
.max(600_000)
|
|
31
56
|
.optional()
|
|
32
57
|
.describe('Timeout in milliseconds (default 120000)'),
|
|
33
|
-
}, async ({ command, timeout_ms }) => {
|
|
58
|
+
}, EXEC_ANNOTATIONS, async ({ command, timeout_ms }) => {
|
|
34
59
|
try {
|
|
35
60
|
const plans = translateCommandList(parseCommand(command));
|
|
36
61
|
const result = await session.run(plans, { timeoutMs: timeout_ms });
|
|
@@ -49,7 +74,7 @@ export async function startMcpServer() {
|
|
|
49
74
|
return { content: [{ type: 'text', text: msg }], isError: true };
|
|
50
75
|
}
|
|
51
76
|
});
|
|
52
|
-
server.tool('fauxnix_translate', 'Translate a bash-style command into the equivalent PowerShell script WITHOUT executing it. Useful for learning/debugging what fauxnix does under the hood.', { command: z.string() }, async ({ command }) => {
|
|
77
|
+
server.tool('fauxnix_translate', 'Translate a bash-style command into the equivalent PowerShell script WITHOUT executing it. Useful for learning/debugging what fauxnix does under the hood.', { command: z.string().describe('The bash-style command line to translate (never executed)') }, TRANSLATE_ANNOTATIONS, async ({ command }) => {
|
|
53
78
|
try {
|
|
54
79
|
const list = parseCommand(command);
|
|
55
80
|
const plans = translateCommandList(list);
|
|
@@ -61,7 +86,12 @@ export async function startMcpServer() {
|
|
|
61
86
|
return { content: [{ type: 'text', text: msg }], isError: true };
|
|
62
87
|
}
|
|
63
88
|
});
|
|
64
|
-
server.tool('fauxnix_session', 'Inspect or reset the persistent fauxnix shell session (current directory, environment, session id). Actions: "status" (default) or "reset".', {
|
|
89
|
+
server.tool('fauxnix_session', 'Inspect or reset the persistent fauxnix shell session (current directory, environment, session id). Actions: "status" (default) or "reset".', {
|
|
90
|
+
action: z
|
|
91
|
+
.enum(['status', 'reset'])
|
|
92
|
+
.default('status')
|
|
93
|
+
.describe('"status" shows the session state (cwd, tracked env keys); "reset" clears it back to a fresh shell'),
|
|
94
|
+
}, SESSION_ANNOTATIONS, async ({ action }) => {
|
|
65
95
|
if (action === 'reset') {
|
|
66
96
|
await session.dispose();
|
|
67
97
|
return { content: [{ type: 'text', text: 'fauxnix: session reset' }] };
|
package/dist/parser.js
CHANGED
|
@@ -200,7 +200,11 @@ function readDollar(input, i) {
|
|
|
200
200
|
if (end === -1)
|
|
201
201
|
throw new FauxnixParseError('fauxnix: unclosed ${');
|
|
202
202
|
const name = input.slice(j + 1, end);
|
|
203
|
-
|
|
203
|
+
const sub = name.match(/^([A-Za-z_][A-Za-z0-9_]*)\[([0-9]+|@|\*)\]$/);
|
|
204
|
+
if (sub) {
|
|
205
|
+
return { part: { kind: 'Var', name: sub[1], index: sub[2] }, next: end + 1 };
|
|
206
|
+
}
|
|
207
|
+
if (!name || !isNameStart(name[0]) || !name.split('').every(isNameChar)) {
|
|
204
208
|
// ${VAR:-default} etc. — unsupported, kept as raw text
|
|
205
209
|
return { part: { kind: 'Text', text: input.slice(i, end + 1) }, next: end + 1 };
|
|
206
210
|
}
|
package/dist/translator.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Assignment, CommandList, Redirect, SimpleCommand, Word } from './ast.js';
|
|
2
2
|
import { PipelineCtx } from './registry.js';
|
|
3
3
|
/** Map a bash $VAR name to a PowerShell expression (usable inside $(...)). */
|
|
4
|
-
export declare function varExpr(name: string): string;
|
|
4
|
+
export declare function varExpr(name: string, index?: string): string;
|
|
5
5
|
/** Escape text destined for the inside of a PS double-quoted string. */
|
|
6
6
|
export declare function escapeDq(s: string): string;
|
|
7
7
|
/** Normalize a literal POSIX-ish path to its Windows equivalent. */
|
|
@@ -17,7 +17,9 @@ export declare function pathExpr(s: string): string;
|
|
|
17
17
|
* Literal words become single-quoted strings; dynamic ones become
|
|
18
18
|
* double-quoted strings with $(...) interpolation.
|
|
19
19
|
*/
|
|
20
|
-
export declare function exprOfWord(w: Word
|
|
20
|
+
export declare function exprOfWord(w: Word, opts?: {
|
|
21
|
+
preserveCmdSub?: boolean;
|
|
22
|
+
}): string;
|
|
21
23
|
/** Literal text of a word when it contains no interpolation, else null. */
|
|
22
24
|
export declare function literalOfWord(w: Word): string | null;
|
|
23
25
|
/**
|
|
@@ -25,8 +27,25 @@ export declare function literalOfWord(w: Word): string | null;
|
|
|
25
27
|
* Literal paths get POSIX-ish normalization (/dev/null, /tmp, /d/...).
|
|
26
28
|
*/
|
|
27
29
|
export declare function operandExpr(w: Word): string;
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
+
/**
|
|
31
|
+
* `${name[@]}` / `"pre${name[@]}post"` — one argv per element.
|
|
32
|
+
* Unquoted `${name[*]}` also splats (bash); quoted `"${name[*]}"` stays one join.
|
|
33
|
+
*/
|
|
34
|
+
export declare function splatSpec(w: Word): {
|
|
35
|
+
name: string;
|
|
36
|
+
prefix: string;
|
|
37
|
+
suffix: string;
|
|
38
|
+
} | null;
|
|
39
|
+
/** PS expression of a string[]: `@` words splat, others stay one element. */
|
|
40
|
+
export declare function argListExpr(words: Word[], fn?: (w: Word) => string): string;
|
|
41
|
+
/**
|
|
42
|
+
* Translate the inside of $(...).
|
|
43
|
+
* `keepNl`: quoted words and assignments keep interior newlines (bash).
|
|
44
|
+
* Unquoted command words join non-empty lines with a space (IFS
|
|
45
|
+
* word-split approximation). Handlers often emit one string object, so
|
|
46
|
+
* a bare `$(…)` interpolation would keep those newlines.
|
|
47
|
+
*/
|
|
48
|
+
export declare function translateCmdSub(cmdText: string, keepNl?: boolean): string;
|
|
30
49
|
export declare function translateSimple(cmd: SimpleCommand, position: PipelineCtx['position'], hasStdin: boolean): string;
|
|
31
50
|
/** PS expr: encode a string so SETVALS records can stay newline-delimited. */
|
|
32
51
|
export declare function encodeSetValExpr(srcExpr: string): string;
|
package/dist/translator.js
CHANGED
|
@@ -5,7 +5,13 @@ import { lookup, psStr } from './registry.js';
|
|
|
5
5
|
/* Variable mapping */
|
|
6
6
|
/* ------------------------------------------------------------------ */
|
|
7
7
|
/** Map a bash $VAR name to a PowerShell expression (usable inside $(...)). */
|
|
8
|
-
export function varExpr(name) {
|
|
8
|
+
export function varExpr(name, index) {
|
|
9
|
+
// Indexed reads always go through fx-subget → fx-arrload → fx-scalar0 so
|
|
10
|
+
// ${PWD[0]} keeps the special mapping and ${bash_rematch[0]} stays
|
|
11
|
+
// case-exact (a `$env:name` fallback would alias BASH_REMATCH on Windows).
|
|
12
|
+
if (index !== undefined) {
|
|
13
|
+
return '(fx-subget ' + psStr(name) + ' ' + psStr(index) + ')';
|
|
14
|
+
}
|
|
9
15
|
switch (name) {
|
|
10
16
|
case 'HOME':
|
|
11
17
|
return '$HOME';
|
|
@@ -85,7 +91,7 @@ export function pathExpr(s) {
|
|
|
85
91
|
* Literal words become single-quoted strings; dynamic ones become
|
|
86
92
|
* double-quoted strings with $(...) interpolation.
|
|
87
93
|
*/
|
|
88
|
-
export function exprOfWord(w) {
|
|
94
|
+
export function exprOfWord(w, opts) {
|
|
89
95
|
// tilde expansion (unquoted leading ~)
|
|
90
96
|
const expanded = [];
|
|
91
97
|
if (w.length > 0 && w[0].kind === 'Text' && w[0].text.startsWith('~')) {
|
|
@@ -100,7 +106,12 @@ export function exprOfWord(w) {
|
|
|
100
106
|
}
|
|
101
107
|
// single bare variable → bare expression
|
|
102
108
|
if (expanded.length === 1 && expanded[0].kind === 'Var') {
|
|
103
|
-
return varExpr(expanded[0].name);
|
|
109
|
+
return varExpr(expanded[0].name, expanded[0].index);
|
|
110
|
+
}
|
|
111
|
+
// Bare `$(...)` must not sit inside a PS expandable string: the
|
|
112
|
+
// substitution body contains `"` / `$_` that would break interpolation.
|
|
113
|
+
if (expanded.length === 1 && expanded[0].kind === 'CmdSub') {
|
|
114
|
+
return '$(' + translateCmdSub(expanded[0].cmd, opts?.preserveCmdSub === true) + ')';
|
|
104
115
|
}
|
|
105
116
|
const literal = expanded.every((p) => p.kind === 'Text' || p.kind === 'SingleQuoted');
|
|
106
117
|
if (literal) {
|
|
@@ -109,7 +120,7 @@ export function exprOfWord(w) {
|
|
|
109
120
|
}
|
|
110
121
|
// dynamic — build a PS double-quoted string with interpolation
|
|
111
122
|
let out = '"';
|
|
112
|
-
const emitPart = (p) => {
|
|
123
|
+
const emitPart = (p, quoted) => {
|
|
113
124
|
switch (p.kind) {
|
|
114
125
|
case 'Text':
|
|
115
126
|
out += escapeDq(p.text);
|
|
@@ -119,18 +130,18 @@ export function exprOfWord(w) {
|
|
|
119
130
|
break;
|
|
120
131
|
case 'DoubleQuoted':
|
|
121
132
|
for (const q of p.parts)
|
|
122
|
-
emitPart(q);
|
|
133
|
+
emitPart(q, true);
|
|
123
134
|
break;
|
|
124
135
|
case 'Var':
|
|
125
|
-
out += '$(' + varExpr(p.name) + ')';
|
|
136
|
+
out += '$(' + varExpr(p.name, p.index) + ')';
|
|
126
137
|
break;
|
|
127
138
|
case 'CmdSub':
|
|
128
|
-
out += '$(' + translateCmdSub(p.cmd) + ')';
|
|
139
|
+
out += '$(' + translateCmdSub(p.cmd, quoted || opts?.preserveCmdSub === true) + ')';
|
|
129
140
|
break;
|
|
130
141
|
}
|
|
131
142
|
};
|
|
132
143
|
for (const p of expanded)
|
|
133
|
-
emitPart(p);
|
|
144
|
+
emitPart(p, false);
|
|
134
145
|
out += '"';
|
|
135
146
|
return out;
|
|
136
147
|
}
|
|
@@ -153,17 +164,96 @@ export function operandExpr(w) {
|
|
|
153
164
|
return pathExpr(normalizeLiteralPath(lit));
|
|
154
165
|
return exprOfWord(w);
|
|
155
166
|
}
|
|
167
|
+
/** Flatten quotes but remember whether a part sat inside `"..."`. */
|
|
168
|
+
function wordPartsForSplat(w) {
|
|
169
|
+
const out = [];
|
|
170
|
+
const walk = (parts, quoted) => {
|
|
171
|
+
for (const p of parts) {
|
|
172
|
+
if (p.kind === 'DoubleQuoted')
|
|
173
|
+
walk(p.parts, true);
|
|
174
|
+
else
|
|
175
|
+
out.push({ part: p, quoted });
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
walk(w, false);
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* `${name[@]}` / `"pre${name[@]}post"` — one argv per element.
|
|
183
|
+
* Unquoted `${name[*]}` also splats (bash); quoted `"${name[*]}"` stays one join.
|
|
184
|
+
*/
|
|
185
|
+
export function splatSpec(w) {
|
|
186
|
+
const parts = wordPartsForSplat(w);
|
|
187
|
+
let name = null;
|
|
188
|
+
let prefix = '';
|
|
189
|
+
let suffix = '';
|
|
190
|
+
let seen = false;
|
|
191
|
+
for (const { part: p, quoted } of parts) {
|
|
192
|
+
const splat = p.kind === 'Var' && (p.index === '@' || (p.index === '*' && !quoted));
|
|
193
|
+
if (splat) {
|
|
194
|
+
if (seen)
|
|
195
|
+
return null;
|
|
196
|
+
seen = true;
|
|
197
|
+
name = p.name;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (p.kind !== 'Text' && p.kind !== 'SingleQuoted')
|
|
201
|
+
return null;
|
|
202
|
+
if (seen)
|
|
203
|
+
suffix += p.text;
|
|
204
|
+
else
|
|
205
|
+
prefix += p.text;
|
|
206
|
+
}
|
|
207
|
+
return name ? { name, prefix, suffix } : null;
|
|
208
|
+
}
|
|
209
|
+
/** PS expression of a string[]: `@` words splat, others stay one element. */
|
|
210
|
+
export function argListExpr(words, fn = exprOfWord) {
|
|
211
|
+
if (words.length === 0)
|
|
212
|
+
return '@()';
|
|
213
|
+
return ('(' +
|
|
214
|
+
words
|
|
215
|
+
.map((w) => {
|
|
216
|
+
const s = splatSpec(w);
|
|
217
|
+
if (!s)
|
|
218
|
+
return '@(' + fn(w) + ')';
|
|
219
|
+
if (!s.prefix && !s.suffix)
|
|
220
|
+
return '@(fx-arrload ' + psStr(s.name) + ')';
|
|
221
|
+
return ('@($( $fx_sp = @(fx-arrload ' +
|
|
222
|
+
psStr(s.name) +
|
|
223
|
+
'); if ($fx_sp.Count -eq 0) { $fx_sp = @(' +
|
|
224
|
+
psStr(s.prefix + s.suffix) +
|
|
225
|
+
') } else { $fx_sp[0] = ' +
|
|
226
|
+
psStr(s.prefix) +
|
|
227
|
+
' + $fx_sp[0]; $fx_sp[$fx_sp.Count-1] = $fx_sp[$fx_sp.Count-1] + ' +
|
|
228
|
+
psStr(s.suffix) +
|
|
229
|
+
' }; $fx_sp ))');
|
|
230
|
+
})
|
|
231
|
+
.join(' + ') +
|
|
232
|
+
')');
|
|
233
|
+
}
|
|
156
234
|
/* ------------------------------------------------------------------ */
|
|
157
235
|
/* Command substitution */
|
|
158
236
|
/* ------------------------------------------------------------------ */
|
|
159
|
-
/**
|
|
160
|
-
|
|
237
|
+
/**
|
|
238
|
+
* Translate the inside of $(...).
|
|
239
|
+
* `keepNl`: quoted words and assignments keep interior newlines (bash).
|
|
240
|
+
* Unquoted command words join non-empty lines with a space (IFS
|
|
241
|
+
* word-split approximation). Handlers often emit one string object, so
|
|
242
|
+
* a bare `$(…)` interpolation would keep those newlines.
|
|
243
|
+
*/
|
|
244
|
+
export function translateCmdSub(cmdText, keepNl = false) {
|
|
161
245
|
const list = parseCommand(cmdText);
|
|
162
246
|
if (list.segments.length !== 1) {
|
|
163
247
|
throw new FauxnixParseError('fauxnix: command substitution with ; && || is not supported yet');
|
|
164
248
|
}
|
|
165
249
|
const { defs, call } = translatePipelineBody(list.segments[0].pipeline);
|
|
166
|
-
|
|
250
|
+
const inner = defs ? defs + '\n' + call : call;
|
|
251
|
+
const collected = '(fx-csub { ' + inner + ' })';
|
|
252
|
+
if (keepNl)
|
|
253
|
+
return collected;
|
|
254
|
+
return ('((' +
|
|
255
|
+
collected +
|
|
256
|
+
" -split [string][char]10 | Where-Object { $_ -ne '' }) -join ' ')");
|
|
167
257
|
}
|
|
168
258
|
/* ------------------------------------------------------------------ */
|
|
169
259
|
/* Simple command translation */
|
|
@@ -182,8 +272,26 @@ export function translateSimple(cmd, position, hasStdin) {
|
|
|
182
272
|
return exportHandler ? exportHandler(words, { position, hasStdin }) : '';
|
|
183
273
|
}
|
|
184
274
|
const nameLit = literalOfWord(cmd.name);
|
|
275
|
+
const nameSplat = splatSpec(cmd.name);
|
|
185
276
|
let body;
|
|
186
|
-
if (
|
|
277
|
+
if (nameSplat) {
|
|
278
|
+
const invoke = '& $fx_cmd @fx_na';
|
|
279
|
+
body = [
|
|
280
|
+
'$fx_cw = @(fx-arrload ' + psStr(nameSplat.name) + ')',
|
|
281
|
+
'if ($fx_cw.Count -eq 0) { $fx_cw = @(' + psStr(nameSplat.prefix + nameSplat.suffix) + ') }',
|
|
282
|
+
'else { $fx_cw[0] = ' +
|
|
283
|
+
psStr(nameSplat.prefix) +
|
|
284
|
+
' + $fx_cw[0]; $fx_cw[$fx_cw.Count-1] = $fx_cw[$fx_cw.Count-1] + ' +
|
|
285
|
+
psStr(nameSplat.suffix) +
|
|
286
|
+
' }',
|
|
287
|
+
'$fx_cmd = [string]$fx_cw[0]',
|
|
288
|
+
'$fx_na = ' + argListExpr(cmd.args),
|
|
289
|
+
'if ($fx_cw.Count -gt 1) { $fx_na = @($fx_cw[1..($fx_cw.Count - 1)]) + $fx_na }',
|
|
290
|
+
(hasStdin ? '($input | ' + invoke + ')' : invoke) + ' | ForEach-Object { [string]$_ }',
|
|
291
|
+
'if ($LASTEXITCODE -gt 0) { $script:fx_exit = $LASTEXITCODE } elseif ($LASTEXITCODE -lt 0) { $script:fx_exit = 1 }',
|
|
292
|
+
].join('\n');
|
|
293
|
+
}
|
|
294
|
+
else if (nameLit !== null) {
|
|
187
295
|
const handler = lookup(nameLit);
|
|
188
296
|
if (handler && !(nameLit === '[[' && !isUnquotedLiteral(cmd.name, '[['))) {
|
|
189
297
|
body = handler(cmd.args, { position, hasStdin });
|
|
@@ -193,12 +301,11 @@ export function translateSimple(cmd, position, hasStdin) {
|
|
|
193
301
|
// invoked with the call operator and an argv-style argument array —
|
|
194
302
|
// no string re-parsing of user text.
|
|
195
303
|
const nameExpr = psStr(nameLit);
|
|
196
|
-
const
|
|
197
|
-
const args = argExprs.length ? ' @(' + argExprs.join(', ') + ')' : '';
|
|
198
|
-
const call = '& ' + nameExpr + args;
|
|
304
|
+
const invoke = '& ' + nameExpr + ' @fx_na';
|
|
199
305
|
body = [
|
|
306
|
+
'$fx_na = ' + argListExpr(cmd.args),
|
|
200
307
|
// feed pipeline stdin into the native process when we are a non-first stage
|
|
201
|
-
(hasStdin ? '($input | ' +
|
|
308
|
+
(hasStdin ? '($input | ' + invoke + ')' : invoke) + ' | ForEach-Object { [string]$_ }',
|
|
202
309
|
'if ($LASTEXITCODE -gt 0) { $script:fx_exit = $LASTEXITCODE } elseif ($LASTEXITCODE -lt 0) { $script:fx_exit = 1 }',
|
|
203
310
|
].join('\n');
|
|
204
311
|
}
|
|
@@ -206,11 +313,10 @@ export function translateSimple(cmd, position, hasStdin) {
|
|
|
206
313
|
else {
|
|
207
314
|
// dynamic command name — evaluate it
|
|
208
315
|
const nameExpr = exprOfWord(cmd.name);
|
|
209
|
-
const
|
|
210
|
-
const args = argExprs.length ? ' @(' + argExprs.join(', ') + ')' : '';
|
|
211
|
-
const call = '& (' + nameExpr + ')' + args;
|
|
316
|
+
const invoke = '& (' + nameExpr + ') @fx_na';
|
|
212
317
|
body = [
|
|
213
|
-
|
|
318
|
+
'$fx_na = ' + argListExpr(cmd.args),
|
|
319
|
+
(hasStdin ? '($input | ' + invoke + ')' : invoke) + ' | ForEach-Object { [string]$_ }',
|
|
214
320
|
'if ($LASTEXITCODE -gt 0) { $script:fx_exit = $LASTEXITCODE } elseif ($LASTEXITCODE -lt 0) { $script:fx_exit = 1 }',
|
|
215
321
|
].join('\n');
|
|
216
322
|
}
|
|
@@ -288,11 +394,14 @@ export function wrapTempEnv(sets, body, extra) {
|
|
|
288
394
|
}
|
|
289
395
|
if (names.length === 0)
|
|
290
396
|
return body;
|
|
397
|
+
const assigned = new Set(sets.map((s) => s.name));
|
|
291
398
|
const id = tempEnvSeq++;
|
|
292
399
|
const save = '$fx_es' + id;
|
|
400
|
+
const arrSave = '$fx_ar' + id;
|
|
293
401
|
const keep = persistWords && persistWords.length > 0 ? '$fx_ek' + id : '';
|
|
294
402
|
const lines = [
|
|
295
403
|
save + ' = @{}',
|
|
404
|
+
arrSave + ' = @{}',
|
|
296
405
|
'$fx_sv0' + id + ' = $env:FAUXNIX_SETVARS',
|
|
297
406
|
'$fx_uv0' + id + ' = $env:FAUXNIX_UNSETVARS',
|
|
298
407
|
'$fx_xv0' + id + ' = $env:FAUXNIX_SETVALS',
|
|
@@ -307,17 +416,19 @@ export function wrapTempEnv(sets, body, extra) {
|
|
|
307
416
|
') { [string](Get-Item -LiteralPath ' +
|
|
308
417
|
p +
|
|
309
418
|
').Value } else { $null })');
|
|
419
|
+
lines.push(arrSave + '[' + psStr(n) + '] = (fx-arrpackget ' + psStr(n) + ')');
|
|
310
420
|
}
|
|
311
421
|
const valVars = [];
|
|
312
422
|
for (let i = 0; i < sets.length; i++) {
|
|
313
423
|
const vn = '$fx_ev' + id + '_' + i;
|
|
314
424
|
valVars.push(vn);
|
|
315
|
-
lines.push(vn + ' = ' + exprOfWord(sets[i].value));
|
|
425
|
+
lines.push(vn + ' = ' + exprOfWord(sets[i].value, { preserveCmdSub: true }));
|
|
316
426
|
}
|
|
317
427
|
lines.push('try {');
|
|
318
428
|
for (const u of unsets) {
|
|
319
429
|
const uq = u.replace(/'/g, "''");
|
|
320
430
|
lines.push(' Remove-Item -LiteralPath ' + psStr('Env:\\' + u) + ' -ErrorAction SilentlyContinue');
|
|
431
|
+
lines.push(' fx-arrdrop ' + psStr(u));
|
|
321
432
|
// `env -u NAME` must hide NAME from fx-envget / fx-isset for the
|
|
322
433
|
// wrapped body. Removing Env:\NAME is not enough: an earlier
|
|
323
434
|
// `export NAME=x` still lives in SETVARS/SETVALS, and special
|
|
@@ -338,6 +449,7 @@ export function wrapTempEnv(sets, body, extra) {
|
|
|
338
449
|
const n = sets[i].name;
|
|
339
450
|
const nq = n.replace(/'/g, "''");
|
|
340
451
|
lines.push(' $env:' + n + ' = ' + valVars[i]);
|
|
452
|
+
lines.push(' fx-arrdrop ' + psStr(n));
|
|
341
453
|
lines.push(" $env:FAUXNIX_SETVARS = ((@($env:FAUXNIX_SETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne '" +
|
|
342
454
|
nq +
|
|
343
455
|
"' }) + '" +
|
|
@@ -415,8 +527,14 @@ export function wrapTempEnv(sets, body, extra) {
|
|
|
415
527
|
psStr(n) +
|
|
416
528
|
'] }');
|
|
417
529
|
}
|
|
530
|
+
const restoreArr = 'fx-arrpackset ' + psStr(n) + ' ' + arrSave + '[' + psStr(n) + ']';
|
|
531
|
+
if (keep) {
|
|
532
|
+
lines.push(' if (-not $fx_skip) { ' + restoreArr + ' }');
|
|
533
|
+
}
|
|
534
|
+
else {
|
|
535
|
+
lines.push(' ' + restoreArr);
|
|
536
|
+
}
|
|
418
537
|
}
|
|
419
|
-
const assigned = new Set(sets.map((s) => s.name));
|
|
420
538
|
for (const n of persistNames) {
|
|
421
539
|
const nq = n.replace(/'/g, "''");
|
|
422
540
|
const ep = psStr('Env:\\' + n);
|
|
@@ -562,6 +680,144 @@ export function wrapScript(body) {
|
|
|
562
680
|
" if ($parts[$parts.Count - 1] -eq '') { $parts = $parts[0..($parts.Count - 2)] }",
|
|
563
681
|
' return $parts',
|
|
564
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
|
+
'}',
|
|
565
821
|
'try {',
|
|
566
822
|
...body.split('\n').map((l) => ' ' + l),
|
|
567
823
|
'} catch [System.Management.Automation.CommandNotFoundException] {',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fauxnix-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Fauxnix — run Linux-style commands on Windows via deterministic PowerShell translation. No VM, no WSL. MCP server + CLI for AI agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -53,6 +53,6 @@
|
|
|
53
53
|
"@types/node": "^20.14.0",
|
|
54
54
|
"tsx": "^4.19.0",
|
|
55
55
|
"typescript": "^5.5.0",
|
|
56
|
-
"vitest": "
|
|
56
|
+
"vitest": "3.2.6"
|
|
57
57
|
}
|
|
58
58
|
}
|