fauxnix-cli 0.7.1 → 0.9.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 +6 -0
- package/dist/cli.js +11 -1
- package/dist/commands/files.d.ts +4 -1
- package/dist/commands/files.js +356 -89
- package/dist/commands/install-all.js +7 -4
- package/dist/commands/sysinfo.js +3 -2
- package/dist/commands/text-filters.d.ts +2 -1
- package/dist/commands/text-filters.js +67 -7
- package/dist/commands/text-io.d.ts +2 -1
- package/dist/commands/text-io.js +95 -4
- package/dist/executor.d.ts +13 -1
- package/dist/executor.js +83 -13
- package/dist/mcp.d.ts +19 -0
- package/dist/mcp.js +79 -18
- package/dist/parser.js +55 -24
- package/dist/ps-host.d.ts +46 -2
- package/dist/ps-host.js +181 -17
- package/dist/registry.d.ts +54 -0
- package/dist/registry.js +182 -0
- package/dist/translator.js +65 -7
- package/package.json +1 -1
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 */
|
|
@@ -51,8 +51,8 @@ function psArray(words, fn = operandExpr) {
|
|
|
51
51
|
/* ls */
|
|
52
52
|
/* ------------------------------------------------------------------ */
|
|
53
53
|
const ls = (args) => {
|
|
54
|
-
const { flags, longs, operandWords } = parseWords(args);
|
|
55
|
-
const long = flags.has('l') || longs.has('--
|
|
54
|
+
const { flags, longs, values, operandWords } = parseWords(args, [], ['--format']);
|
|
55
|
+
const long = flags.has('l') || longs.has('--long') || values.get('--format') === 'long';
|
|
56
56
|
const all = flags.has('a') || longs.has('--all');
|
|
57
57
|
const almost = flags.has('A') || longs.has('--almost-all');
|
|
58
58
|
const dirOnly = flags.has('d') || longs.has('--directory');
|
|
@@ -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),
|
|
@@ -204,7 +208,7 @@ const rm = (args) => {
|
|
|
204
208
|
const mkdir = (args) => {
|
|
205
209
|
const { flags, longs, operandWords } = parseWords(args);
|
|
206
210
|
const parents = flags.has('p') || longs.has('--parents');
|
|
207
|
-
const verbose = flags.has('v');
|
|
211
|
+
const verbose = flags.has('v') || longs.has('--verbose');
|
|
208
212
|
return [
|
|
209
213
|
'$fx_dirs = ' + psArray(operandWords),
|
|
210
214
|
"if ($fx_dirs.Count -eq 0) { [Console]::Error.WriteLine('mkdir: missing operand'); $script:fx_exit = 1 }",
|
|
@@ -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
|
' }',
|
|
@@ -248,8 +254,8 @@ const touch = (args) => {
|
|
|
248
254
|
].join('\n');
|
|
249
255
|
};
|
|
250
256
|
const mktemp = (args) => {
|
|
251
|
-
const { flags } = parseWords(args);
|
|
252
|
-
const dir = flags.has('d');
|
|
257
|
+
const { flags, longs } = parseWords(args);
|
|
258
|
+
const dir = flags.has('d') || longs.has('--directory');
|
|
253
259
|
return [
|
|
254
260
|
'try {',
|
|
255
261
|
' if (' + (dir ? '$true' : '$false') + ') {',
|
|
@@ -266,8 +272,8 @@ const mktemp = (args) => {
|
|
|
266
272
|
/* ln / readlink / realpath */
|
|
267
273
|
/* ------------------------------------------------------------------ */
|
|
268
274
|
const ln = (args) => {
|
|
269
|
-
const { flags, operandWords } = parseWords(args);
|
|
270
|
-
const sym = flags.has('s');
|
|
275
|
+
const { flags, longs, operandWords } = parseWords(args);
|
|
276
|
+
const sym = flags.has('s') || longs.has('--symbolic');
|
|
271
277
|
const kind = sym ? 'SymbolicLink' : 'HardLink';
|
|
272
278
|
const label = sym ? 'symbolic link' : 'hard link';
|
|
273
279
|
return [
|
|
@@ -282,8 +288,8 @@ const ln = (args) => {
|
|
|
282
288
|
].join('\n');
|
|
283
289
|
};
|
|
284
290
|
const readlink = (args) => {
|
|
285
|
-
const { flags, operandWords } = parseWords(args);
|
|
286
|
-
const canon = flags.has('f');
|
|
291
|
+
const { flags, longs, operandWords } = parseWords(args);
|
|
292
|
+
const canon = flags.has('f') || longs.has('--canonicalize');
|
|
287
293
|
return [
|
|
288
294
|
'$fx_p = ' + (operandWords.length ? operandExpr(operandWords[0]) : "''"),
|
|
289
295
|
"if ($fx_p -eq '') { [Console]::Error.WriteLine('readlink: missing operand'); $script:fx_exit = 1 }",
|
|
@@ -416,10 +422,31 @@ const file = (args) => {
|
|
|
416
422
|
/* du / df */
|
|
417
423
|
/* ------------------------------------------------------------------ */
|
|
418
424
|
const du = (args) => {
|
|
419
|
-
const { flags, longs, operandWords } = parseWords(args, [], ['--max-depth']);
|
|
425
|
+
const { flags, longs, values, missingValue, operandWords } = parseWords(args, ['d'], ['--max-depth']);
|
|
420
426
|
const sum = flags.has('s') || longs.has('--summarize');
|
|
421
427
|
const human = flags.has('h') || longs.has('--human-readable');
|
|
428
|
+
if (missingValue.includes('-d') || missingValue.includes('--max-depth')) {
|
|
429
|
+
return ('[Console]::Error.WriteLine(' +
|
|
430
|
+
psStr("du: option requires an argument -- 'max-depth'") +
|
|
431
|
+
'); $script:fx_exit = 1');
|
|
432
|
+
}
|
|
433
|
+
const maxDepthRaw = values.get('-d') ?? values.get('--max-depth');
|
|
434
|
+
let maxDepth = null;
|
|
435
|
+
if (maxDepthRaw !== undefined) {
|
|
436
|
+
if (!/^\d+$/.test(maxDepthRaw)) {
|
|
437
|
+
return ('[Console]::Error.WriteLine(' +
|
|
438
|
+
psStr("du: invalid maximum depth '" + maxDepthRaw + "'") +
|
|
439
|
+
'); $script:fx_exit = 1');
|
|
440
|
+
}
|
|
441
|
+
maxDepth = parseInt(maxDepthRaw, 10);
|
|
442
|
+
}
|
|
443
|
+
if (sum && maxDepth !== null && maxDepth !== 0) {
|
|
444
|
+
return ('[Console]::Error.WriteLine(' +
|
|
445
|
+
psStr('du: summarizing conflicts with --max-depth=' + String(maxDepth)) +
|
|
446
|
+
'); $script:fx_exit = 1');
|
|
447
|
+
}
|
|
422
448
|
const targets = operandWords.length ? argListExpr(operandWords) : "@('.')";
|
|
449
|
+
const onlyRoot = sum || maxDepth === 0;
|
|
423
450
|
return [
|
|
424
451
|
PS_HSIZE_FN,
|
|
425
452
|
'function fx-size($p) {',
|
|
@@ -430,7 +457,7 @@ const du = (args) => {
|
|
|
430
457
|
'$fx_ts = ' + targets,
|
|
431
458
|
'foreach ($fx_t in $fx_ts) {',
|
|
432
459
|
' 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 }',
|
|
433
|
-
' if (' + (
|
|
460
|
+
' if (' + (onlyRoot ? '$true' : '$false') + ') {',
|
|
434
461
|
' $fx_kb = fx-size $fx_t',
|
|
435
462
|
' if (' + (human ? '$true' : '$false') + ') { "{0}`t{1}" -f (fx-hsize ($fx_kb * 1KB)), $fx_t } else { "{0}`t{1}" -f $fx_kb, $fx_t }',
|
|
436
463
|
' } else {',
|
|
@@ -438,6 +465,11 @@ const du = (args) => {
|
|
|
438
465
|
' foreach ($fx_d in @(Get-ChildItem -LiteralPath $fx_t -Recurse -Force -Directory -ErrorAction SilentlyContinue)) {',
|
|
439
466
|
' $fx_kb = fx-size $fx_d.FullName',
|
|
440
467
|
" $fx_rel = './' + $fx_d.FullName.Substring($fx_root.Length).TrimStart('\\').Replace('\\', '/')",
|
|
468
|
+
maxDepth !== null && maxDepth > 0
|
|
469
|
+
? " $fx_ddepth = @($fx_rel.ToCharArray() | Where-Object { $_ -eq '/' }).Count; if ($fx_ddepth -gt " +
|
|
470
|
+
maxDepth +
|
|
471
|
+
') { continue }'
|
|
472
|
+
: '',
|
|
441
473
|
' if (' + (human ? '$true' : '$false') + ') { "{0}`t{1}" -f (fx-hsize ($fx_kb * 1KB)), $fx_rel } else { "{0}`t{1}" -f $fx_kb, $fx_rel }',
|
|
442
474
|
' }',
|
|
443
475
|
' $fx_kb = fx-size $fx_t',
|
|
@@ -447,8 +479,8 @@ const du = (args) => {
|
|
|
447
479
|
].join('\n');
|
|
448
480
|
};
|
|
449
481
|
const df = (args) => {
|
|
450
|
-
const { flags } = parseWords(args);
|
|
451
|
-
const human = flags.has('h') || flags.has('H');
|
|
482
|
+
const { flags, longs } = parseWords(args);
|
|
483
|
+
const human = flags.has('h') || flags.has('H') || longs.has('--human-readable');
|
|
452
484
|
return [
|
|
453
485
|
PS_HSIZE_FN,
|
|
454
486
|
'"Filesystem Size Used Avail Use% Mounted on"',
|
|
@@ -465,15 +497,215 @@ const df = (args) => {
|
|
|
465
497
|
'}',
|
|
466
498
|
].join('\n');
|
|
467
499
|
};
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
500
|
+
function findFail(msg) {
|
|
501
|
+
throw new FauxnixParseError(msg);
|
|
502
|
+
}
|
|
503
|
+
function canStartFindFactor(t) {
|
|
504
|
+
return (t === '!' ||
|
|
505
|
+
t === '-not' ||
|
|
506
|
+
t === '(' ||
|
|
507
|
+
t === '-name' ||
|
|
508
|
+
t === '-iname' ||
|
|
509
|
+
t === '-type' ||
|
|
510
|
+
t === '-size' ||
|
|
511
|
+
t === '-mtime' ||
|
|
512
|
+
t === '-delete' ||
|
|
513
|
+
t === '-print' ||
|
|
514
|
+
t === '-maxdepth' ||
|
|
515
|
+
t === '-mindepth');
|
|
516
|
+
}
|
|
517
|
+
/** GNU find expression: `!` tightest, juxtaposition/`-a` next, `-o` loosest. */
|
|
518
|
+
function parseFindPreds(preds) {
|
|
519
|
+
const plan = {
|
|
520
|
+
ast: { kind: 'true' },
|
|
521
|
+
maxDepth: null,
|
|
522
|
+
minDepth: null,
|
|
523
|
+
hasDelete: false,
|
|
524
|
+
hasPrint: false,
|
|
525
|
+
hasSize: false,
|
|
526
|
+
};
|
|
527
|
+
let i = 0;
|
|
528
|
+
const peek = () => (i < preds.length ? preds[i] : null);
|
|
529
|
+
const take = () => preds[i++];
|
|
530
|
+
const needArg = (opt) => {
|
|
531
|
+
if (i >= preds.length)
|
|
532
|
+
findFail("find: missing argument to '" + opt + "'");
|
|
533
|
+
return take();
|
|
534
|
+
};
|
|
535
|
+
const parsePrimary = () => {
|
|
536
|
+
const t = take();
|
|
537
|
+
if (t === '-name' || t === '-iname') {
|
|
538
|
+
return { kind: 'name', pat: needArg(t), ci: t === '-iname' };
|
|
539
|
+
}
|
|
540
|
+
if (t === '-type') {
|
|
541
|
+
const v = needArg(t);
|
|
542
|
+
if (v !== 'f' && v !== 'd' && v !== 'l')
|
|
543
|
+
findFail('find: Unknown argument to -type: ' + v);
|
|
544
|
+
return { kind: 'type', t: v };
|
|
545
|
+
}
|
|
546
|
+
if (t === '-size') {
|
|
547
|
+
const v = needArg(t);
|
|
548
|
+
const ps = sizeOf(v);
|
|
549
|
+
if (!ps)
|
|
550
|
+
findFail("find: Invalid argument '" + v + "' to -size");
|
|
551
|
+
plan.hasSize = true;
|
|
552
|
+
return { kind: 'size', ps };
|
|
553
|
+
}
|
|
554
|
+
if (t === '-mtime') {
|
|
555
|
+
const v = needArg(t);
|
|
556
|
+
const ps = mtimeOf(v);
|
|
557
|
+
if (!ps)
|
|
558
|
+
findFail("find: Invalid argument '" + v + "' to -mtime");
|
|
559
|
+
return { kind: 'mtime', ps };
|
|
560
|
+
}
|
|
561
|
+
if (t === '-delete') {
|
|
562
|
+
plan.hasDelete = true;
|
|
563
|
+
return { kind: 'delete' };
|
|
564
|
+
}
|
|
565
|
+
if (t === '-print') {
|
|
566
|
+
plan.hasPrint = true;
|
|
567
|
+
return { kind: 'print' };
|
|
568
|
+
}
|
|
569
|
+
if (t === '-maxdepth' || t === '-mindepth') {
|
|
570
|
+
const v = needArg(t);
|
|
571
|
+
if (!/^\d+$/.test(v)) {
|
|
572
|
+
findFail('find: expected a non-negative decimal integer argument to ' +
|
|
573
|
+
t +
|
|
574
|
+
", but got '" +
|
|
575
|
+
v +
|
|
576
|
+
"'");
|
|
577
|
+
}
|
|
578
|
+
if (t === '-maxdepth' && plan.maxDepth === null)
|
|
579
|
+
plan.maxDepth = v;
|
|
580
|
+
if (t === '-mindepth' && plan.minDepth === null)
|
|
581
|
+
plan.minDepth = v;
|
|
582
|
+
return { kind: 'true' };
|
|
583
|
+
}
|
|
584
|
+
if (t.startsWith('-'))
|
|
585
|
+
findFail("find: unknown predicate '" + t + "'");
|
|
586
|
+
findFail("find: paths must precede expression: '" + t + "'");
|
|
587
|
+
};
|
|
588
|
+
const parseFactor = () => {
|
|
589
|
+
const t = peek();
|
|
590
|
+
if (t === null)
|
|
591
|
+
findFail('find: expected an expression');
|
|
592
|
+
if (t === '-o' || t === '-or' || t === '-a' || t === '-and') {
|
|
593
|
+
findFail("find: invalid expression; you have used a binary operator '" +
|
|
594
|
+
t +
|
|
595
|
+
"' with nothing before it.");
|
|
596
|
+
}
|
|
597
|
+
if (t === '!' || t === '-not') {
|
|
598
|
+
take();
|
|
599
|
+
if (peek() === null)
|
|
600
|
+
findFail("find: expected an expression after '" + t + "'");
|
|
601
|
+
if (peek() === ')')
|
|
602
|
+
findFail("find: expected an expression between '" + t + "' and ')'");
|
|
603
|
+
return { kind: 'not', inner: parseFactor() };
|
|
604
|
+
}
|
|
605
|
+
if (t === '(') {
|
|
606
|
+
take();
|
|
607
|
+
if (peek() === ')')
|
|
608
|
+
findFail('find: invalid expression; empty parentheses are not allowed.');
|
|
609
|
+
if (peek() === null) {
|
|
610
|
+
findFail("find: invalid expression; expected to find a ')' but didn't see one.");
|
|
611
|
+
}
|
|
612
|
+
const inner = parseOr();
|
|
613
|
+
if (peek() !== ')') {
|
|
614
|
+
findFail("find: invalid expression; expected to find a ')' but didn't see one.");
|
|
615
|
+
}
|
|
616
|
+
take();
|
|
617
|
+
return inner;
|
|
618
|
+
}
|
|
619
|
+
if (t === ')')
|
|
620
|
+
findFail("find: invalid expression; you have too many ')'");
|
|
621
|
+
return parsePrimary();
|
|
622
|
+
};
|
|
623
|
+
const parseAnd = () => {
|
|
624
|
+
let left = parseFactor();
|
|
625
|
+
while (true) {
|
|
626
|
+
const t = peek();
|
|
627
|
+
if (t === null || t === ')' || t === '-o' || t === '-or')
|
|
628
|
+
break;
|
|
629
|
+
if (t === '-a' || t === '-and') {
|
|
630
|
+
take();
|
|
631
|
+
if (peek() === null || peek() === ')') {
|
|
632
|
+
findFail("find: expected an expression after '" + t + "'");
|
|
633
|
+
}
|
|
634
|
+
left = { kind: 'and', left, right: parseFactor() };
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
if (canStartFindFactor(t)) {
|
|
638
|
+
left = { kind: 'and', left, right: parseFactor() };
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
findFail("find: paths must precede expression: '" + t + "'");
|
|
642
|
+
}
|
|
643
|
+
return left;
|
|
644
|
+
};
|
|
645
|
+
const parseOr = () => {
|
|
646
|
+
let left = parseAnd();
|
|
647
|
+
while (peek() === '-o' || peek() === '-or') {
|
|
648
|
+
const op = take();
|
|
649
|
+
if (peek() === null || peek() === ')') {
|
|
650
|
+
findFail("find: expected an expression after '" + op + "'");
|
|
651
|
+
}
|
|
652
|
+
left = { kind: 'or', left, right: parseAnd() };
|
|
653
|
+
}
|
|
654
|
+
return left;
|
|
655
|
+
};
|
|
656
|
+
if (preds.length === 0) {
|
|
657
|
+
plan.ast = { kind: 'print' };
|
|
658
|
+
plan.hasPrint = true;
|
|
659
|
+
return plan;
|
|
660
|
+
}
|
|
661
|
+
plan.ast = parseOr();
|
|
662
|
+
if (i < preds.length) {
|
|
663
|
+
if (preds[i] === ')')
|
|
664
|
+
findFail("find: invalid expression; you have too many ')'");
|
|
665
|
+
findFail("find: paths must precede expression: '" + preds[i] + "'");
|
|
666
|
+
}
|
|
667
|
+
if (!plan.hasDelete && !plan.hasPrint) {
|
|
668
|
+
plan.ast = { kind: 'and', left: plan.ast, right: { kind: 'print' } };
|
|
669
|
+
plan.hasPrint = true;
|
|
670
|
+
}
|
|
671
|
+
return plan;
|
|
672
|
+
}
|
|
673
|
+
function emitFind(n) {
|
|
674
|
+
switch (n.kind) {
|
|
675
|
+
case 'true':
|
|
676
|
+
return '$true';
|
|
677
|
+
case 'and':
|
|
678
|
+
return '(' + emitFind(n.left) + ' -and ' + emitFind(n.right) + ')';
|
|
679
|
+
case 'or':
|
|
680
|
+
return '(' + emitFind(n.left) + ' -or ' + emitFind(n.right) + ')';
|
|
681
|
+
case 'not':
|
|
682
|
+
return '(-not ' + emitFind(n.inner) + ')';
|
|
683
|
+
case 'name':
|
|
684
|
+
if (n.ci)
|
|
685
|
+
return "($fx_i.Name.ToLower() -like '" + likeOf(n.pat).toLowerCase() + "')";
|
|
686
|
+
return "($fx_i.Name -clike '" + likeOf(n.pat) + "')";
|
|
687
|
+
case 'type':
|
|
688
|
+
if (n.t === 'f')
|
|
689
|
+
return '(-not $fx_i.PSIsContainer)';
|
|
690
|
+
if (n.t === 'd')
|
|
691
|
+
return '($fx_i.PSIsContainer)';
|
|
692
|
+
return '([bool]$fx_i.LinkType)';
|
|
693
|
+
case 'size':
|
|
694
|
+
return '(' + n.ps + ')';
|
|
695
|
+
case 'mtime':
|
|
696
|
+
return '(' + n.ps + ')';
|
|
697
|
+
case 'delete':
|
|
698
|
+
return '(fx-find-delete)';
|
|
699
|
+
case 'print':
|
|
700
|
+
return '(fx-find-print)';
|
|
701
|
+
}
|
|
702
|
+
}
|
|
471
703
|
const find = (args) => {
|
|
472
704
|
const raw = args.map((w) => wordToString(w));
|
|
473
705
|
let pathEnd = 0;
|
|
474
706
|
while (pathEnd < raw.length &&
|
|
475
707
|
!raw[pathEnd].startsWith('-') &&
|
|
476
|
-
!['(', ')', '!', '-a', '-o'].includes(raw[pathEnd])) {
|
|
708
|
+
!['(', ')', '!', '-a', '-o', '-not', '-and', '-or'].includes(raw[pathEnd])) {
|
|
477
709
|
pathEnd++;
|
|
478
710
|
}
|
|
479
711
|
const pathWords = args.slice(0, pathEnd);
|
|
@@ -483,81 +715,41 @@ const find = (args) => {
|
|
|
483
715
|
psStr('find: -exec is not supported by fauxnix; pipe into the command instead (e.g. `find . -name "*.log" | xargs rm`)') +
|
|
484
716
|
'); $script:fx_exit = 1');
|
|
485
717
|
}
|
|
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');
|
|
718
|
+
const plan = parseFindPreds(preds);
|
|
719
|
+
const expr = emitFind(plan.ast);
|
|
494
720
|
const paths = pathWords.length ? argListExpr(pathWords) : "@('.')";
|
|
495
|
-
for (const option of ['-maxdepth', '-mindepth']) {
|
|
496
|
-
const optionIndex = preds.indexOf(option);
|
|
497
|
-
if (optionIndex < 0)
|
|
498
|
-
continue;
|
|
499
|
-
if (optionIndex + 1 >= preds.length) {
|
|
500
|
-
return ('[Console]::Error.WriteLine(' +
|
|
501
|
-
psStr(`find: missing argument to '${option}'`) +
|
|
502
|
-
'); $script:fx_exit = 1');
|
|
503
|
-
}
|
|
504
|
-
const value = preds[optionIndex + 1];
|
|
505
|
-
if (!/^\d+$/.test(value)) {
|
|
506
|
-
return ('[Console]::Error.WriteLine(' +
|
|
507
|
-
psStr(`find: expected a non-negative decimal integer argument to ${option}, but got '${value}'`) +
|
|
508
|
-
'); $script:fx_exit = 1');
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
const conditions = [];
|
|
512
|
-
if (namePat !== null)
|
|
513
|
-
conditions.push("($fx_i.Name -like '" + likeOf(namePat) + "')");
|
|
514
|
-
if (inamePat !== null)
|
|
515
|
-
conditions.push("($fx_i.Name.ToLower() -like '" + likeOf(inamePat).toLowerCase() + "')");
|
|
516
|
-
if (typeV === 'f')
|
|
517
|
-
conditions.push('(-not $fx_i.PSIsContainer)');
|
|
518
|
-
if (typeV === 'd')
|
|
519
|
-
conditions.push('($fx_i.PSIsContainer)');
|
|
520
|
-
if (typeV === 'l')
|
|
521
|
-
conditions.push('([bool]$fx_i.LinkType)');
|
|
522
|
-
const cond = conditions.length ? conditions.join(' -and ') : '$true';
|
|
523
|
-
const sizeCond = sizeOf(sizeExpr);
|
|
524
|
-
const mtimeCond = mtimeOf(mtimeExpr);
|
|
525
721
|
return [
|
|
722
|
+
plan.hasPrint
|
|
723
|
+
? 'function fx-find-print { $script:fx_find_print = $true; $true }'
|
|
724
|
+
: '',
|
|
725
|
+
plan.hasDelete
|
|
726
|
+
? 'function fx-find-delete { try { Remove-Item -LiteralPath $fx_i.FullName -Force -ErrorAction SilentlyContinue | Out-Null } catch {}; $true }'
|
|
727
|
+
: '',
|
|
526
728
|
'$fx_paths = ' + paths,
|
|
527
729
|
'foreach ($fx_p in $fx_paths) {',
|
|
528
730
|
' if (-not (Test-Path -LiteralPath $fx_p)) { [Console]::Error.WriteLine("find: \'" + $fx_p + "\': No such file or directory"); $script:fx_exit = 1; continue }',
|
|
529
731
|
' $fx_root = (Get-Item -LiteralPath $fx_p -Force).FullName',
|
|
530
732
|
' $fx_all = @(Get-Item -LiteralPath $fx_p -Force)',
|
|
531
733
|
' $fx_all += @(Get-ChildItem -LiteralPath $fx_p -Recurse -Force -ErrorAction SilentlyContinue)',
|
|
734
|
+
plan.hasDelete ? ' [array]::Reverse($fx_all)' : '',
|
|
532
735
|
' foreach ($fx_i in $fx_all) {',
|
|
533
736
|
" $fx_rel = $fx_i.FullName.Substring($fx_root.Length).TrimStart('\\').Replace('\\', '/')",
|
|
534
737
|
" if ($fx_rel -eq '') { $fx_disp = $fx_p } else { $fx_disp = ($fx_p.TrimEnd('/') + '/' + $fx_rel) }",
|
|
535
738
|
' $fx_depth = 0; if ($fx_rel -ne \'\') { $fx_depth = 1; foreach ($fx_c in $fx_rel.ToCharArray()) { if ($fx_c -eq \'/\') { $fx_depth++ } } }',
|
|
536
|
-
' if ($fx_depth -lt ' + (
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
' if (' +
|
|
543
|
-
|
|
544
|
-
' } else {',
|
|
545
|
-
' $fx_disp',
|
|
546
|
-
' }',
|
|
739
|
+
' if ($fx_depth -lt ' + (plan.minDepth ?? '0') + ') { continue }',
|
|
740
|
+
plan.maxDepth !== null ? ' if ($fx_depth -gt ' + plan.maxDepth + ') { continue }' : '',
|
|
741
|
+
plan.hasSize
|
|
742
|
+
? ' $fx_sz = 0; if (-not $fx_i.PSIsContainer) { try { $fx_sz = $fx_i.Length } catch {} }'
|
|
743
|
+
: '',
|
|
744
|
+
plan.hasPrint ? ' $script:fx_find_print = $false' : '',
|
|
745
|
+
' if (' + expr + ') { }',
|
|
746
|
+
plan.hasPrint ? ' if ($script:fx_find_print) { $fx_disp }' : '',
|
|
547
747
|
' }',
|
|
548
748
|
'}',
|
|
549
749
|
]
|
|
550
750
|
.filter((l) => l !== '')
|
|
551
751
|
.join('\n');
|
|
552
752
|
};
|
|
553
|
-
function extractValue(preds, names) {
|
|
554
|
-
for (const n of names) {
|
|
555
|
-
const i = preds.indexOf(n);
|
|
556
|
-
if (i >= 0 && i + 1 < preds.length)
|
|
557
|
-
return preds[i + 1];
|
|
558
|
-
}
|
|
559
|
-
return null;
|
|
560
|
-
}
|
|
561
753
|
/** fnmatch glob → PowerShell -like pattern (same semantics for * and ?). */
|
|
562
754
|
function likeOf(glob) {
|
|
563
755
|
return glob.replace(/'/g, "''");
|
|
@@ -635,9 +827,9 @@ const chown = () => {
|
|
|
635
827
|
/* diff — LCS-based, GNU normal format (+ -q, -u) */
|
|
636
828
|
/* ------------------------------------------------------------------ */
|
|
637
829
|
const diff = (args) => {
|
|
638
|
-
const { flags, operandWords } = parseWords(args);
|
|
639
|
-
const unified = flags.has('u') || flags.has('U');
|
|
640
|
-
const brief = flags.has('q') ||
|
|
830
|
+
const { flags, longs, operandWords } = parseWords(args);
|
|
831
|
+
const unified = flags.has('u') || flags.has('U') || longs.has('--unified');
|
|
832
|
+
const brief = flags.has('q') || longs.has('--brief');
|
|
641
833
|
void unified;
|
|
642
834
|
return [
|
|
643
835
|
PS_READTEXT_FN,
|
|
@@ -700,15 +892,93 @@ const diff = (args) => {
|
|
|
700
892
|
'}',
|
|
701
893
|
].join('\n');
|
|
702
894
|
};
|
|
895
|
+
function opt(short, long, support = 'implemented', extra = {}) {
|
|
896
|
+
const o = { support };
|
|
897
|
+
if (short)
|
|
898
|
+
o.short = short;
|
|
899
|
+
if (long)
|
|
900
|
+
o.long = long;
|
|
901
|
+
return extra.takesValue || extra.reason ? { ...o, ...extra } : o;
|
|
902
|
+
}
|
|
903
|
+
function fileSpec(names, effects, options, handler) {
|
|
904
|
+
return {
|
|
905
|
+
names,
|
|
906
|
+
options,
|
|
907
|
+
effects,
|
|
908
|
+
platform: 'windows-ps51',
|
|
909
|
+
dispatch: 'translated',
|
|
910
|
+
handler,
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
const INTERACTIVE = { reason: 'interactive prompt' };
|
|
914
|
+
/** Destructive file commands migrated first (#130). Unspec'd handlers stay unchecked.
|
|
915
|
+
* cp/mv `-f`/`--force` is always-on overwrite (Copy-Item/Move-Item -Force); listed so `cp -rf` stays valid. */
|
|
916
|
+
export const specs = [
|
|
917
|
+
fileSpec(['cp'], ['read', 'write'], [
|
|
918
|
+
opt('r', undefined),
|
|
919
|
+
opt('R', '--recursive'),
|
|
920
|
+
opt('v', '--verbose'),
|
|
921
|
+
opt('n', '--no-clobber'),
|
|
922
|
+
opt('f', '--force'),
|
|
923
|
+
opt('i', '--interactive', 'unsupported', INTERACTIVE),
|
|
924
|
+
], cp),
|
|
925
|
+
fileSpec(['mv'], ['read', 'write', 'delete'], [
|
|
926
|
+
opt('v', '--verbose'),
|
|
927
|
+
opt('n', '--no-clobber'),
|
|
928
|
+
opt('f', '--force'),
|
|
929
|
+
opt('i', '--interactive', 'unsupported', INTERACTIVE),
|
|
930
|
+
], mv),
|
|
931
|
+
fileSpec(['rm'], ['delete'], [
|
|
932
|
+
opt('r', undefined),
|
|
933
|
+
opt('R', '--recursive'),
|
|
934
|
+
opt('f', '--force'),
|
|
935
|
+
opt('v', '--verbose'),
|
|
936
|
+
opt('i', '--interactive', 'unsupported', INTERACTIVE),
|
|
937
|
+
], rm),
|
|
938
|
+
fileSpec(['touch'], ['write'], [opt('c', '--no-create')], touch),
|
|
939
|
+
fileSpec(['du'], ['read'], [
|
|
940
|
+
opt('s', '--summarize'),
|
|
941
|
+
opt('h', '--human-readable'),
|
|
942
|
+
opt('d', '--max-depth', 'implemented', { takesValue: true }),
|
|
943
|
+
], du),
|
|
944
|
+
fileSpec(['ls', 'll'], ['read'], [
|
|
945
|
+
opt('l', '--long'),
|
|
946
|
+
opt(undefined, '--format', 'implemented', { takesValue: true }),
|
|
947
|
+
opt('a', '--all'),
|
|
948
|
+
opt('A', '--almost-all'),
|
|
949
|
+
opt('d', '--directory'),
|
|
950
|
+
opt('h', '--human-readable'),
|
|
951
|
+
opt('F', '--classify'),
|
|
952
|
+
opt('p', undefined),
|
|
953
|
+
opt('t', undefined),
|
|
954
|
+
opt('S', undefined),
|
|
955
|
+
opt('r', undefined),
|
|
956
|
+
opt('R', '--recursive', 'unsupported', { reason: 'recursive listing' }),
|
|
957
|
+
], ls),
|
|
958
|
+
fileSpec(['mkdir'], ['write'], [opt('p', '--parents'), opt('v', '--verbose')], mkdir),
|
|
959
|
+
fileSpec(['rmdir'], ['delete'], [], rmdir),
|
|
960
|
+
fileSpec(['mktemp'], ['write'], [opt('d', '--directory')], mktemp),
|
|
961
|
+
fileSpec(['ln'], ['read', 'write'], [opt('s', '--symbolic')], ln),
|
|
962
|
+
fileSpec(['readlink'], ['read'], [opt('f', '--canonicalize')], readlink),
|
|
963
|
+
fileSpec(['realpath'], ['read'], [], realpath),
|
|
964
|
+
fileSpec(['basename'], ['read'], [], basename),
|
|
965
|
+
fileSpec(['dirname'], ['read'], [], dirname),
|
|
966
|
+
fileSpec(['stat'], ['read'], [
|
|
967
|
+
opt('c', undefined, 'implemented', { takesValue: true }),
|
|
968
|
+
opt(undefined, '--format', 'implemented', { takesValue: true }),
|
|
969
|
+
opt(undefined, '--printf', 'implemented', { takesValue: true }),
|
|
970
|
+
], stat),
|
|
971
|
+
fileSpec(['file'], ['read'], [], file),
|
|
972
|
+
fileSpec(['df'], ['read'], [opt('h', '--human-readable'), opt('H', undefined)], df),
|
|
973
|
+
fileSpec(['chmod'], ['write'], [opt('R', '--recursive', 'unsupported', { reason: 'recursive chmod' })], chmod),
|
|
974
|
+
fileSpec(['chown'], ['write'], [], chown),
|
|
975
|
+
fileSpec(['diff'], ['read'], [opt('q', '--brief'), opt('u', '--unified'), opt('U', undefined)], diff),
|
|
976
|
+
];
|
|
703
977
|
export const handlers = {
|
|
704
978
|
ls,
|
|
705
979
|
ll: ls, // common alias
|
|
706
|
-
cp,
|
|
707
|
-
mv,
|
|
708
|
-
rm,
|
|
709
980
|
mkdir,
|
|
710
981
|
rmdir,
|
|
711
|
-
touch,
|
|
712
982
|
mktemp,
|
|
713
983
|
ln,
|
|
714
984
|
readlink,
|
|
@@ -717,10 +987,7 @@ export const handlers = {
|
|
|
717
987
|
dirname,
|
|
718
988
|
stat,
|
|
719
989
|
file,
|
|
720
|
-
du,
|
|
721
990
|
df,
|
|
991
|
+
du,
|
|
722
992
|
find,
|
|
723
|
-
chmod,
|
|
724
|
-
chown,
|
|
725
|
-
diff,
|
|
726
993
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { registerAll } from '../registry.js';
|
|
2
|
-
import { handlers as files } from './files.js';
|
|
3
|
-
import { handlers as textFilters } from './text-filters.js';
|
|
4
|
-
import { handlers as textIo } from './text-io.js';
|
|
1
|
+
import { registerAll, registerSpecs } from '../registry.js';
|
|
2
|
+
import { handlers as files, specs as fileSpecs } from './files.js';
|
|
3
|
+
import { handlers as textFilters, specs as textFilterSpecs } from './text-filters.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,8 @@ export function installAll() {
|
|
|
13
13
|
registerAll(sysinfo);
|
|
14
14
|
registerAll(net);
|
|
15
15
|
registerAll(archive);
|
|
16
|
+
registerSpecs(fileSpecs);
|
|
17
|
+
registerSpecs(textIoSpecs);
|
|
18
|
+
registerSpecs(textFilterSpecs);
|
|
16
19
|
}
|
|
17
20
|
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)
|