fauxnix-cli 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/dist/cli.js +8 -2
- package/dist/commands/files.d.ts +4 -1
- package/dist/commands/files.js +277 -54
- package/dist/commands/install-all.js +5 -3
- package/dist/commands/sysinfo.js +3 -2
- package/dist/commands/text-filters.js +93 -13
- package/dist/commands/text-io.d.ts +2 -1
- package/dist/commands/text-io.js +14 -3
- package/dist/executor.d.ts +13 -1
- package/dist/executor.js +114 -14
- package/dist/mcp.d.ts +19 -0
- package/dist/mcp.js +81 -24
- package/dist/parser.js +55 -24
- package/dist/ps-host.d.ts +7 -1
- package/dist/ps-host.js +75 -8
- package/dist/registry.d.ts +50 -0
- package/dist/registry.js +142 -0
- package/dist/translator.js +38 -3
- package/dist/version.d.ts +1 -0
- package/dist/version.js +8 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -67,7 +67,9 @@ npm install -g fauxnix-cli
|
|
|
67
67
|
Or from source:
|
|
68
68
|
|
|
69
69
|
```bash
|
|
70
|
-
git clone https://github.com/20000419/fauxnix && cd fauxnix
|
|
70
|
+
git clone https://github.com/20000419/fauxnix && cd fauxnix
|
|
71
|
+
npm ci
|
|
72
|
+
npm install -g .
|
|
71
73
|
```
|
|
72
74
|
|
|
73
75
|
> npm package name is `fauxnix-cli` (the `fauxnix` name on npm belongs to an
|
|
@@ -151,6 +153,10 @@ development:
|
|
|
151
153
|
- **text filters**: `grep egrep sed awk sort uniq cut tr` — sed/awk scripts are parsed at
|
|
152
154
|
translate time (unsupported constructs throw named errors, never silently misbehave)
|
|
153
155
|
- **text I/O**: `echo printf cat head tail wc tee nl tac md5sum sha1sum sha256sum base64 seq yes xargs`
|
|
156
|
+
|
|
157
|
+
`cp` / `mv` / `rm` / `touch` / `tee` carry a `CommandSpec`: unknown options fail with a GNU-style
|
|
158
|
+
usage error instead of being ignored. `cp -n` / `mv -n` / `touch -c` / `tee --append` match GNU
|
|
159
|
+
(no clobber / no-create / append). `fauxnix list --json` dumps the same capability metadata.
|
|
154
160
|
- **shell/system**: `cd pwd export unset env printenv ps kill pkill pgrep sleep which type whoami
|
|
155
161
|
id groups date uname hostname uptime free nproc clear true false test [ [[ : pushd popd dirs sudo
|
|
156
162
|
timeout man history less more source . eval exit alias set`
|
|
@@ -194,6 +200,7 @@ fauxnix optimizes for the commands agents actually run. Documented deviations:
|
|
|
194
200
|
- `yes` is capped at 65,536 lines — PS 5.1 pipelines cannot signal upstream producers to stop, so
|
|
195
201
|
an unbounded `yes | head` would hang.
|
|
196
202
|
- `tail -f`, `eval`, `alias`, heredocs, `while`/`until`/`case`,
|
|
203
|
+
`env -i`/`--ignore-environment`,
|
|
197
204
|
and background `&` are rejected with actionable error messages instead of misbehaving.
|
|
198
205
|
(`if/then/elif/else/fi`, `for x in ...`, backtick substitution, `command -v`, pipeline `read`,
|
|
199
206
|
dotenv-style `source`, and word-level `$((...))` arithmetic expansion are supported.)
|
package/dist/cli.js
CHANGED
|
@@ -2,9 +2,10 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { FauxnixSession } from './executor.js';
|
|
3
3
|
import { parseCommand } from './parser.js';
|
|
4
4
|
import { translateCommandList } from './translator.js';
|
|
5
|
-
import { registeredNames } from './registry.js';
|
|
5
|
+
import { listCommandsJson, registeredNames } from './registry.js';
|
|
6
6
|
import { encodeCommand } from './encoding.js';
|
|
7
7
|
import { startMcpServer } from './mcp.js';
|
|
8
|
+
import { packageVersion } from './version.js';
|
|
8
9
|
import './commands/install-all.js';
|
|
9
10
|
const USAGE = `fauxnix — run Linux-style commands on Windows via PowerShell translation
|
|
10
11
|
|
|
@@ -14,6 +15,7 @@ Usage:
|
|
|
14
15
|
fauxnix translate "cmd" show the PowerShell translation only
|
|
15
16
|
fauxnix mcp start the MCP stdio server (for agent harnesses)
|
|
16
17
|
fauxnix list list translated commands
|
|
18
|
+
fauxnix list --json same list as machine-readable capability metadata
|
|
17
19
|
fauxnix check verify the local PowerShell environment
|
|
18
20
|
fauxnix --version
|
|
19
21
|
|
|
@@ -26,10 +28,14 @@ export async function runCli(argv) {
|
|
|
26
28
|
}
|
|
27
29
|
const [verb, ...rest] = argv;
|
|
28
30
|
if (verb === '--version' || verb === '-v') {
|
|
29
|
-
console.log(
|
|
31
|
+
console.log(`fauxnix ${packageVersion}`);
|
|
30
32
|
return;
|
|
31
33
|
}
|
|
32
34
|
if (verb === 'list') {
|
|
35
|
+
if (rest[0] === '--json') {
|
|
36
|
+
console.log(JSON.stringify(listCommandsJson(), null, 2));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
33
39
|
const names = registeredNames();
|
|
34
40
|
console.log(names.length + ' translated commands:');
|
|
35
41
|
for (const n of names)
|
package/dist/commands/files.d.ts
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
|
-
import { Handler } from '../registry.js';
|
|
1
|
+
import { CommandSpec, Handler } from '../registry.js';
|
|
2
|
+
/** Destructive file commands migrated first (#130). Unspec'd handlers stay unchecked.
|
|
3
|
+
* cp/mv `-f`/`--force` is always-on overwrite (Copy-Item/Move-Item -Force); listed so `cp -rf` stays valid. */
|
|
4
|
+
export declare const specs: CommandSpec[];
|
|
2
5
|
export declare const handlers: Record<string, Handler>;
|
package/dist/commands/files.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { wordToString } from '../ast.js';
|
|
2
|
-
import { parseWords, psStr } from '../registry.js';
|
|
1
|
+
import { FauxnixParseError, wordToString } from '../ast.js';
|
|
2
|
+
import { parseWords, psStr, } from '../registry.js';
|
|
3
3
|
import { argListExpr, exprOfWord, operandExpr } from '../translator.js';
|
|
4
4
|
/* ------------------------------------------------------------------ */
|
|
5
5
|
/* Shared PS snippets */
|
|
@@ -124,6 +124,7 @@ const cp = (args) => {
|
|
|
124
124
|
const { flags, longs, operandWords } = parseWords(args);
|
|
125
125
|
const recurse = flags.has('r') || flags.has('R') || longs.has('--recursive');
|
|
126
126
|
const verbose = flags.has('v') || longs.has('--verbose');
|
|
127
|
+
const noclobber = flags.has('n') || longs.has('--no-clobber');
|
|
127
128
|
return [
|
|
128
129
|
PS_GLOB_FN,
|
|
129
130
|
'$fx_all = ' + argListExpr(operandWords),
|
|
@@ -138,6 +139,7 @@ const cp = (args) => {
|
|
|
138
139
|
' if ($fx_isdir -and ' + (recurse ? '$false' : '$true') + ') { [Console]::Error.WriteLine("cp: -r not specified; omitting directory \'" + $fx_g + "\'"); $script:fx_exit = 1; continue }',
|
|
139
140
|
' $fx_target = $fx_dst',
|
|
140
141
|
' if (Test-Path -LiteralPath $fx_dst -PathType Container) { $fx_target = Join-Path $fx_dst (Split-Path $fx_g -Leaf) }',
|
|
142
|
+
' if (' + (noclobber ? '$true' : '$false') + ' -and (Test-Path -LiteralPath $fx_target)) { continue }',
|
|
141
143
|
' try {',
|
|
142
144
|
' Copy-Item -LiteralPath $fx_g -Destination $fx_target -Recurse:' + (recurse ? '$true' : '$false') + ' -Force',
|
|
143
145
|
' if (' + (verbose ? '$true' : '$false') + ') { [Console]::Error.WriteLine("\'" + $fx_g + "\' -> \'" + $fx_target + "\'") }',
|
|
@@ -150,6 +152,7 @@ const cp = (args) => {
|
|
|
150
152
|
const mv = (args) => {
|
|
151
153
|
const { flags, longs, operandWords } = parseWords(args);
|
|
152
154
|
const verbose = flags.has('v') || longs.has('--verbose');
|
|
155
|
+
const noclobber = flags.has('n') || longs.has('--no-clobber');
|
|
153
156
|
return [
|
|
154
157
|
PS_GLOB_FN,
|
|
155
158
|
'$fx_all = ' + argListExpr(operandWords),
|
|
@@ -163,6 +166,7 @@ const mv = (args) => {
|
|
|
163
166
|
' if (-not (Test-Path -LiteralPath $fx_g)) { [Console]::Error.WriteLine("mv: cannot stat \'" + $fx_g + "\': No such file or directory"); $script:fx_exit = 1; continue }',
|
|
164
167
|
' $fx_target = $fx_dst',
|
|
165
168
|
' if (Test-Path -LiteralPath $fx_dst -PathType Container) { $fx_target = Join-Path $fx_dst (Split-Path $fx_g -Leaf) }',
|
|
169
|
+
' if (' + (noclobber ? '$true' : '$false') + ' -and (Test-Path -LiteralPath $fx_target)) { continue }',
|
|
166
170
|
' try {',
|
|
167
171
|
' if (Test-Path -LiteralPath $fx_target) { Remove-Item -LiteralPath $fx_target -Recurse -Force }',
|
|
168
172
|
' Move-Item -LiteralPath $fx_g -Destination $fx_target -Force',
|
|
@@ -177,7 +181,7 @@ const rm = (args) => {
|
|
|
177
181
|
const { flags, longs, operandWords } = parseWords(args);
|
|
178
182
|
const recurse = flags.has('r') || flags.has('R') || longs.has('--recursive');
|
|
179
183
|
const force = flags.has('f') || longs.has('--force');
|
|
180
|
-
const verbose = flags.has('v');
|
|
184
|
+
const verbose = flags.has('v') || longs.has('--verbose');
|
|
181
185
|
return [
|
|
182
186
|
PS_GLOB_FN,
|
|
183
187
|
'$fx_files = ' + psArray(operandWords),
|
|
@@ -233,7 +237,8 @@ const rmdir = (args) => {
|
|
|
233
237
|
].join('\n');
|
|
234
238
|
};
|
|
235
239
|
const touch = (args) => {
|
|
236
|
-
const { operandWords } = parseWords(args);
|
|
240
|
+
const { flags, longs, operandWords } = parseWords(args);
|
|
241
|
+
const noCreate = flags.has('c') || longs.has('--no-create');
|
|
237
242
|
return [
|
|
238
243
|
'$fx_files = ' + psArray(operandWords),
|
|
239
244
|
"if ($fx_files.Count -eq 0) { [Console]::Error.WriteLine('touch: missing file operand'); $script:fx_exit = 1 }",
|
|
@@ -241,6 +246,7 @@ const touch = (args) => {
|
|
|
241
246
|
' if (Test-Path -LiteralPath $fx_f) {',
|
|
242
247
|
' try { (Get-Item -LiteralPath $fx_f).LastWriteTime = Get-Date } catch { [Console]::Error.WriteLine("touch: cannot touch \'" + $fx_f + "\': Permission denied"); $script:fx_exit = 1 }',
|
|
243
248
|
' } else {',
|
|
249
|
+
' if (' + (noCreate ? '$true' : '$false') + ') { continue }',
|
|
244
250
|
' try { New-Item -ItemType File -Path $fx_f | Out-Null }',
|
|
245
251
|
' catch { [Console]::Error.WriteLine("touch: cannot touch \'" + $fx_f + "\': No such file or directory"); $script:fx_exit = 1 }',
|
|
246
252
|
' }',
|
|
@@ -465,15 +471,215 @@ const df = (args) => {
|
|
|
465
471
|
'}',
|
|
466
472
|
].join('\n');
|
|
467
473
|
};
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
474
|
+
function findFail(msg) {
|
|
475
|
+
throw new FauxnixParseError(msg);
|
|
476
|
+
}
|
|
477
|
+
function canStartFindFactor(t) {
|
|
478
|
+
return (t === '!' ||
|
|
479
|
+
t === '-not' ||
|
|
480
|
+
t === '(' ||
|
|
481
|
+
t === '-name' ||
|
|
482
|
+
t === '-iname' ||
|
|
483
|
+
t === '-type' ||
|
|
484
|
+
t === '-size' ||
|
|
485
|
+
t === '-mtime' ||
|
|
486
|
+
t === '-delete' ||
|
|
487
|
+
t === '-print' ||
|
|
488
|
+
t === '-maxdepth' ||
|
|
489
|
+
t === '-mindepth');
|
|
490
|
+
}
|
|
491
|
+
/** GNU find expression: `!` tightest, juxtaposition/`-a` next, `-o` loosest. */
|
|
492
|
+
function parseFindPreds(preds) {
|
|
493
|
+
const plan = {
|
|
494
|
+
ast: { kind: 'true' },
|
|
495
|
+
maxDepth: null,
|
|
496
|
+
minDepth: null,
|
|
497
|
+
hasDelete: false,
|
|
498
|
+
hasPrint: false,
|
|
499
|
+
hasSize: false,
|
|
500
|
+
};
|
|
501
|
+
let i = 0;
|
|
502
|
+
const peek = () => (i < preds.length ? preds[i] : null);
|
|
503
|
+
const take = () => preds[i++];
|
|
504
|
+
const needArg = (opt) => {
|
|
505
|
+
if (i >= preds.length)
|
|
506
|
+
findFail("find: missing argument to '" + opt + "'");
|
|
507
|
+
return take();
|
|
508
|
+
};
|
|
509
|
+
const parsePrimary = () => {
|
|
510
|
+
const t = take();
|
|
511
|
+
if (t === '-name' || t === '-iname') {
|
|
512
|
+
return { kind: 'name', pat: needArg(t), ci: t === '-iname' };
|
|
513
|
+
}
|
|
514
|
+
if (t === '-type') {
|
|
515
|
+
const v = needArg(t);
|
|
516
|
+
if (v !== 'f' && v !== 'd' && v !== 'l')
|
|
517
|
+
findFail('find: Unknown argument to -type: ' + v);
|
|
518
|
+
return { kind: 'type', t: v };
|
|
519
|
+
}
|
|
520
|
+
if (t === '-size') {
|
|
521
|
+
const v = needArg(t);
|
|
522
|
+
const ps = sizeOf(v);
|
|
523
|
+
if (!ps)
|
|
524
|
+
findFail("find: Invalid argument '" + v + "' to -size");
|
|
525
|
+
plan.hasSize = true;
|
|
526
|
+
return { kind: 'size', ps };
|
|
527
|
+
}
|
|
528
|
+
if (t === '-mtime') {
|
|
529
|
+
const v = needArg(t);
|
|
530
|
+
const ps = mtimeOf(v);
|
|
531
|
+
if (!ps)
|
|
532
|
+
findFail("find: Invalid argument '" + v + "' to -mtime");
|
|
533
|
+
return { kind: 'mtime', ps };
|
|
534
|
+
}
|
|
535
|
+
if (t === '-delete') {
|
|
536
|
+
plan.hasDelete = true;
|
|
537
|
+
return { kind: 'delete' };
|
|
538
|
+
}
|
|
539
|
+
if (t === '-print') {
|
|
540
|
+
plan.hasPrint = true;
|
|
541
|
+
return { kind: 'print' };
|
|
542
|
+
}
|
|
543
|
+
if (t === '-maxdepth' || t === '-mindepth') {
|
|
544
|
+
const v = needArg(t);
|
|
545
|
+
if (!/^\d+$/.test(v)) {
|
|
546
|
+
findFail('find: expected a non-negative decimal integer argument to ' +
|
|
547
|
+
t +
|
|
548
|
+
", but got '" +
|
|
549
|
+
v +
|
|
550
|
+
"'");
|
|
551
|
+
}
|
|
552
|
+
if (t === '-maxdepth' && plan.maxDepth === null)
|
|
553
|
+
plan.maxDepth = v;
|
|
554
|
+
if (t === '-mindepth' && plan.minDepth === null)
|
|
555
|
+
plan.minDepth = v;
|
|
556
|
+
return { kind: 'true' };
|
|
557
|
+
}
|
|
558
|
+
if (t.startsWith('-'))
|
|
559
|
+
findFail("find: unknown predicate '" + t + "'");
|
|
560
|
+
findFail("find: paths must precede expression: '" + t + "'");
|
|
561
|
+
};
|
|
562
|
+
const parseFactor = () => {
|
|
563
|
+
const t = peek();
|
|
564
|
+
if (t === null)
|
|
565
|
+
findFail('find: expected an expression');
|
|
566
|
+
if (t === '-o' || t === '-or' || t === '-a' || t === '-and') {
|
|
567
|
+
findFail("find: invalid expression; you have used a binary operator '" +
|
|
568
|
+
t +
|
|
569
|
+
"' with nothing before it.");
|
|
570
|
+
}
|
|
571
|
+
if (t === '!' || t === '-not') {
|
|
572
|
+
take();
|
|
573
|
+
if (peek() === null)
|
|
574
|
+
findFail("find: expected an expression after '" + t + "'");
|
|
575
|
+
if (peek() === ')')
|
|
576
|
+
findFail("find: expected an expression between '" + t + "' and ')'");
|
|
577
|
+
return { kind: 'not', inner: parseFactor() };
|
|
578
|
+
}
|
|
579
|
+
if (t === '(') {
|
|
580
|
+
take();
|
|
581
|
+
if (peek() === ')')
|
|
582
|
+
findFail('find: invalid expression; empty parentheses are not allowed.');
|
|
583
|
+
if (peek() === null) {
|
|
584
|
+
findFail("find: invalid expression; expected to find a ')' but didn't see one.");
|
|
585
|
+
}
|
|
586
|
+
const inner = parseOr();
|
|
587
|
+
if (peek() !== ')') {
|
|
588
|
+
findFail("find: invalid expression; expected to find a ')' but didn't see one.");
|
|
589
|
+
}
|
|
590
|
+
take();
|
|
591
|
+
return inner;
|
|
592
|
+
}
|
|
593
|
+
if (t === ')')
|
|
594
|
+
findFail("find: invalid expression; you have too many ')'");
|
|
595
|
+
return parsePrimary();
|
|
596
|
+
};
|
|
597
|
+
const parseAnd = () => {
|
|
598
|
+
let left = parseFactor();
|
|
599
|
+
while (true) {
|
|
600
|
+
const t = peek();
|
|
601
|
+
if (t === null || t === ')' || t === '-o' || t === '-or')
|
|
602
|
+
break;
|
|
603
|
+
if (t === '-a' || t === '-and') {
|
|
604
|
+
take();
|
|
605
|
+
if (peek() === null || peek() === ')') {
|
|
606
|
+
findFail("find: expected an expression after '" + t + "'");
|
|
607
|
+
}
|
|
608
|
+
left = { kind: 'and', left, right: parseFactor() };
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
if (canStartFindFactor(t)) {
|
|
612
|
+
left = { kind: 'and', left, right: parseFactor() };
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
findFail("find: paths must precede expression: '" + t + "'");
|
|
616
|
+
}
|
|
617
|
+
return left;
|
|
618
|
+
};
|
|
619
|
+
const parseOr = () => {
|
|
620
|
+
let left = parseAnd();
|
|
621
|
+
while (peek() === '-o' || peek() === '-or') {
|
|
622
|
+
const op = take();
|
|
623
|
+
if (peek() === null || peek() === ')') {
|
|
624
|
+
findFail("find: expected an expression after '" + op + "'");
|
|
625
|
+
}
|
|
626
|
+
left = { kind: 'or', left, right: parseAnd() };
|
|
627
|
+
}
|
|
628
|
+
return left;
|
|
629
|
+
};
|
|
630
|
+
if (preds.length === 0) {
|
|
631
|
+
plan.ast = { kind: 'print' };
|
|
632
|
+
plan.hasPrint = true;
|
|
633
|
+
return plan;
|
|
634
|
+
}
|
|
635
|
+
plan.ast = parseOr();
|
|
636
|
+
if (i < preds.length) {
|
|
637
|
+
if (preds[i] === ')')
|
|
638
|
+
findFail("find: invalid expression; you have too many ')'");
|
|
639
|
+
findFail("find: paths must precede expression: '" + preds[i] + "'");
|
|
640
|
+
}
|
|
641
|
+
if (!plan.hasDelete && !plan.hasPrint) {
|
|
642
|
+
plan.ast = { kind: 'and', left: plan.ast, right: { kind: 'print' } };
|
|
643
|
+
plan.hasPrint = true;
|
|
644
|
+
}
|
|
645
|
+
return plan;
|
|
646
|
+
}
|
|
647
|
+
function emitFind(n) {
|
|
648
|
+
switch (n.kind) {
|
|
649
|
+
case 'true':
|
|
650
|
+
return '$true';
|
|
651
|
+
case 'and':
|
|
652
|
+
return '(' + emitFind(n.left) + ' -and ' + emitFind(n.right) + ')';
|
|
653
|
+
case 'or':
|
|
654
|
+
return '(' + emitFind(n.left) + ' -or ' + emitFind(n.right) + ')';
|
|
655
|
+
case 'not':
|
|
656
|
+
return '(-not ' + emitFind(n.inner) + ')';
|
|
657
|
+
case 'name':
|
|
658
|
+
if (n.ci)
|
|
659
|
+
return "($fx_i.Name.ToLower() -like '" + likeOf(n.pat).toLowerCase() + "')";
|
|
660
|
+
return "($fx_i.Name -clike '" + likeOf(n.pat) + "')";
|
|
661
|
+
case 'type':
|
|
662
|
+
if (n.t === 'f')
|
|
663
|
+
return '(-not $fx_i.PSIsContainer)';
|
|
664
|
+
if (n.t === 'd')
|
|
665
|
+
return '($fx_i.PSIsContainer)';
|
|
666
|
+
return '([bool]$fx_i.LinkType)';
|
|
667
|
+
case 'size':
|
|
668
|
+
return '(' + n.ps + ')';
|
|
669
|
+
case 'mtime':
|
|
670
|
+
return '(' + n.ps + ')';
|
|
671
|
+
case 'delete':
|
|
672
|
+
return '(fx-find-delete)';
|
|
673
|
+
case 'print':
|
|
674
|
+
return '(fx-find-print)';
|
|
675
|
+
}
|
|
676
|
+
}
|
|
471
677
|
const find = (args) => {
|
|
472
678
|
const raw = args.map((w) => wordToString(w));
|
|
473
679
|
let pathEnd = 0;
|
|
474
680
|
while (pathEnd < raw.length &&
|
|
475
681
|
!raw[pathEnd].startsWith('-') &&
|
|
476
|
-
!['(', ')', '!', '-a', '-o'].includes(raw[pathEnd])) {
|
|
682
|
+
!['(', ')', '!', '-a', '-o', '-not', '-and', '-or'].includes(raw[pathEnd])) {
|
|
477
683
|
pathEnd++;
|
|
478
684
|
}
|
|
479
685
|
const pathWords = args.slice(0, pathEnd);
|
|
@@ -483,65 +689,41 @@ const find = (args) => {
|
|
|
483
689
|
psStr('find: -exec is not supported by fauxnix; pipe into the command instead (e.g. `find . -name "*.log" | xargs rm`)') +
|
|
484
690
|
'); $script:fx_exit = 1');
|
|
485
691
|
}
|
|
486
|
-
const
|
|
487
|
-
const
|
|
488
|
-
const typeV = extractValue(preds, ['-type']);
|
|
489
|
-
const maxDepthS = extractValue(preds, ['-maxdepth']);
|
|
490
|
-
const minDepthS = extractValue(preds, ['-mindepth']);
|
|
491
|
-
const sizeExpr = extractValue(preds, ['-size']);
|
|
492
|
-
const mtimeExpr = extractValue(preds, ['-mtime']);
|
|
493
|
-
const wantDelete = preds.includes('-delete');
|
|
692
|
+
const plan = parseFindPreds(preds);
|
|
693
|
+
const expr = emitFind(plan.ast);
|
|
494
694
|
const paths = pathWords.length ? argListExpr(pathWords) : "@('.')";
|
|
495
|
-
const conditions = [];
|
|
496
|
-
if (namePat !== null)
|
|
497
|
-
conditions.push("($fx_i.Name -like '" + likeOf(namePat) + "')");
|
|
498
|
-
if (inamePat !== null)
|
|
499
|
-
conditions.push("($fx_i.Name.ToLower() -like '" + likeOf(inamePat).toLowerCase() + "')");
|
|
500
|
-
if (typeV === 'f')
|
|
501
|
-
conditions.push('(-not $fx_i.PSIsContainer)');
|
|
502
|
-
if (typeV === 'd')
|
|
503
|
-
conditions.push('($fx_i.PSIsContainer)');
|
|
504
|
-
if (typeV === 'l')
|
|
505
|
-
conditions.push('([bool]$fx_i.LinkType)');
|
|
506
|
-
const cond = conditions.length ? conditions.join(' -and ') : '$true';
|
|
507
|
-
const sizeCond = sizeOf(sizeExpr);
|
|
508
|
-
const mtimeCond = mtimeOf(mtimeExpr);
|
|
509
695
|
return [
|
|
696
|
+
plan.hasPrint
|
|
697
|
+
? 'function fx-find-print { $script:fx_find_print = $true; $true }'
|
|
698
|
+
: '',
|
|
699
|
+
plan.hasDelete
|
|
700
|
+
? 'function fx-find-delete { try { Remove-Item -LiteralPath $fx_i.FullName -Force -ErrorAction SilentlyContinue | Out-Null } catch {}; $true }'
|
|
701
|
+
: '',
|
|
510
702
|
'$fx_paths = ' + paths,
|
|
511
703
|
'foreach ($fx_p in $fx_paths) {',
|
|
512
704
|
' if (-not (Test-Path -LiteralPath $fx_p)) { [Console]::Error.WriteLine("find: \'" + $fx_p + "\': No such file or directory"); $script:fx_exit = 1; continue }',
|
|
513
705
|
' $fx_root = (Get-Item -LiteralPath $fx_p -Force).FullName',
|
|
514
706
|
' $fx_all = @(Get-Item -LiteralPath $fx_p -Force)',
|
|
515
707
|
' $fx_all += @(Get-ChildItem -LiteralPath $fx_p -Recurse -Force -ErrorAction SilentlyContinue)',
|
|
708
|
+
plan.hasDelete ? ' [array]::Reverse($fx_all)' : '',
|
|
516
709
|
' foreach ($fx_i in $fx_all) {',
|
|
517
710
|
" $fx_rel = $fx_i.FullName.Substring($fx_root.Length).TrimStart('\\').Replace('\\', '/')",
|
|
518
711
|
" if ($fx_rel -eq '') { $fx_disp = $fx_p } else { $fx_disp = ($fx_p.TrimEnd('/') + '/' + $fx_rel) }",
|
|
519
|
-
' $fx_depth = 0; foreach ($fx_c in $fx_rel.ToCharArray()) { if ($fx_c -eq \'/\') { $fx_depth++ } }',
|
|
520
|
-
' if ($fx_depth -lt ' + (
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
' if (' +
|
|
527
|
-
|
|
528
|
-
' } else {',
|
|
529
|
-
' $fx_disp',
|
|
530
|
-
' }',
|
|
712
|
+
' $fx_depth = 0; if ($fx_rel -ne \'\') { $fx_depth = 1; foreach ($fx_c in $fx_rel.ToCharArray()) { if ($fx_c -eq \'/\') { $fx_depth++ } } }',
|
|
713
|
+
' if ($fx_depth -lt ' + (plan.minDepth ?? '0') + ') { continue }',
|
|
714
|
+
plan.maxDepth !== null ? ' if ($fx_depth -gt ' + plan.maxDepth + ') { continue }' : '',
|
|
715
|
+
plan.hasSize
|
|
716
|
+
? ' $fx_sz = 0; if (-not $fx_i.PSIsContainer) { try { $fx_sz = $fx_i.Length } catch {} }'
|
|
717
|
+
: '',
|
|
718
|
+
plan.hasPrint ? ' $script:fx_find_print = $false' : '',
|
|
719
|
+
' if (' + expr + ') { }',
|
|
720
|
+
plan.hasPrint ? ' if ($script:fx_find_print) { $fx_disp }' : '',
|
|
531
721
|
' }',
|
|
532
722
|
'}',
|
|
533
723
|
]
|
|
534
724
|
.filter((l) => l !== '')
|
|
535
725
|
.join('\n');
|
|
536
726
|
};
|
|
537
|
-
function extractValue(preds, names) {
|
|
538
|
-
for (const n of names) {
|
|
539
|
-
const i = preds.indexOf(n);
|
|
540
|
-
if (i >= 0 && i + 1 < preds.length)
|
|
541
|
-
return preds[i + 1];
|
|
542
|
-
}
|
|
543
|
-
return null;
|
|
544
|
-
}
|
|
545
727
|
/** fnmatch glob → PowerShell -like pattern (same semantics for * and ?). */
|
|
546
728
|
function likeOf(glob) {
|
|
547
729
|
return glob.replace(/'/g, "''");
|
|
@@ -684,15 +866,56 @@ const diff = (args) => {
|
|
|
684
866
|
'}',
|
|
685
867
|
].join('\n');
|
|
686
868
|
};
|
|
869
|
+
function opt(short, long, support = 'implemented', extra = {}) {
|
|
870
|
+
const o = { support };
|
|
871
|
+
if (short)
|
|
872
|
+
o.short = short;
|
|
873
|
+
if (long)
|
|
874
|
+
o.long = long;
|
|
875
|
+
return extra.takesValue || extra.reason ? { ...o, ...extra } : o;
|
|
876
|
+
}
|
|
877
|
+
function fileSpec(names, effects, options, handler) {
|
|
878
|
+
return {
|
|
879
|
+
names,
|
|
880
|
+
options,
|
|
881
|
+
effects,
|
|
882
|
+
platform: 'windows-ps51',
|
|
883
|
+
dispatch: 'translated',
|
|
884
|
+
handler,
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
const INTERACTIVE = { reason: 'interactive prompt' };
|
|
888
|
+
/** Destructive file commands migrated first (#130). Unspec'd handlers stay unchecked.
|
|
889
|
+
* cp/mv `-f`/`--force` is always-on overwrite (Copy-Item/Move-Item -Force); listed so `cp -rf` stays valid. */
|
|
890
|
+
export const specs = [
|
|
891
|
+
fileSpec(['cp'], ['read', 'write'], [
|
|
892
|
+
opt('r', undefined),
|
|
893
|
+
opt('R', '--recursive'),
|
|
894
|
+
opt('v', '--verbose'),
|
|
895
|
+
opt('n', '--no-clobber'),
|
|
896
|
+
opt('f', '--force'),
|
|
897
|
+
opt('i', '--interactive', 'unsupported', INTERACTIVE),
|
|
898
|
+
], cp),
|
|
899
|
+
fileSpec(['mv'], ['read', 'write', 'delete'], [
|
|
900
|
+
opt('v', '--verbose'),
|
|
901
|
+
opt('n', '--no-clobber'),
|
|
902
|
+
opt('f', '--force'),
|
|
903
|
+
opt('i', '--interactive', 'unsupported', INTERACTIVE),
|
|
904
|
+
], mv),
|
|
905
|
+
fileSpec(['rm'], ['delete'], [
|
|
906
|
+
opt('r', undefined),
|
|
907
|
+
opt('R', '--recursive'),
|
|
908
|
+
opt('f', '--force'),
|
|
909
|
+
opt('v', '--verbose'),
|
|
910
|
+
opt('i', '--interactive', 'unsupported', INTERACTIVE),
|
|
911
|
+
], rm),
|
|
912
|
+
fileSpec(['touch'], ['write'], [opt('c', '--no-create')], touch),
|
|
913
|
+
];
|
|
687
914
|
export const handlers = {
|
|
688
915
|
ls,
|
|
689
916
|
ll: ls, // common alias
|
|
690
|
-
cp,
|
|
691
|
-
mv,
|
|
692
|
-
rm,
|
|
693
917
|
mkdir,
|
|
694
918
|
rmdir,
|
|
695
|
-
touch,
|
|
696
919
|
mktemp,
|
|
697
920
|
ln,
|
|
698
921
|
readlink,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { registerAll } from '../registry.js';
|
|
2
|
-
import { handlers as files } from './files.js';
|
|
1
|
+
import { registerAll, registerSpecs } from '../registry.js';
|
|
2
|
+
import { handlers as files, specs as fileSpecs } from './files.js';
|
|
3
3
|
import { handlers as textFilters } from './text-filters.js';
|
|
4
|
-
import { handlers as textIo } from './text-io.js';
|
|
4
|
+
import { handlers as textIo, specs as textIoSpecs } from './text-io.js';
|
|
5
5
|
import { handlers as sysinfo } from './sysinfo.js';
|
|
6
6
|
import { handlers as net } from './net.js';
|
|
7
7
|
import { handlers as archive } from './archive.js';
|
|
@@ -13,5 +13,7 @@ export function installAll() {
|
|
|
13
13
|
registerAll(sysinfo);
|
|
14
14
|
registerAll(net);
|
|
15
15
|
registerAll(archive);
|
|
16
|
+
registerSpecs(fileSpecs);
|
|
17
|
+
registerSpecs(textIoSpecs);
|
|
16
18
|
}
|
|
17
19
|
installAll();
|
package/dist/commands/sysinfo.js
CHANGED
|
@@ -209,8 +209,9 @@ const env = (args, ctx) => {
|
|
|
209
209
|
break;
|
|
210
210
|
}
|
|
211
211
|
if (t === '-i' || t === '--ignore-environment') {
|
|
212
|
-
|
|
213
|
-
|
|
212
|
+
return ('[Console]::Error.WriteLine(' +
|
|
213
|
+
psStr('fauxnix: env -i/--ignore-environment is not supported (would silently keep inherited secrets)') +
|
|
214
|
+
'); $script:fx_exit = 2');
|
|
214
215
|
}
|
|
215
216
|
if (t === '-u' || t === '--unset') {
|
|
216
217
|
if (i + 1 < raw.length)
|