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.
@@ -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('--format=long') || longs.has('--long');
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 (' + (sum ? '$true' : '$false') + ') {',
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
- /* find */
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 namePat = extractValue(preds, ['-name']);
487
- const inamePat = extractValue(preds, ['-iname']);
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 ' + (minDepthS ?? '0') + ') { continue }',
537
- maxDepthS !== null ? ' if ($fx_depth -gt ' + maxDepthS + ') { continue }' : '',
538
- ' if (-not (' + cond + ')) { continue }',
539
- sizeCond ? ' $fx_sz = 0; if (-not $fx_i.PSIsContainer) { try { $fx_sz = $fx_i.Length } catch {} }' : '',
540
- sizeCond ? ' if (-not (' + sizeCond + ')) { continue }' : '',
541
- mtimeCond ? ' if (-not (' + mtimeCond + ')) { continue }' : '',
542
- ' if (' + (wantDelete ? '$true' : '$false') + ') {',
543
- ' try { Remove-Item -LiteralPath $fx_i.FullName -Recurse -Force -ErrorAction SilentlyContinue } catch {}',
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') || flags.has('brief');
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();
@@ -209,8 +209,9 @@ const env = (args, ctx) => {
209
209
  break;
210
210
  }
211
211
  if (t === '-i' || t === '--ignore-environment') {
212
- i++; // best-effort: the flag is accepted and ignored
213
- continue;
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)
@@ -1,2 +1,3 @@
1
- import { Handler } from '../registry.js';
1
+ import { CommandSpec, Handler } from '../registry.js';
2
+ export declare const specs: CommandSpec[];
2
3
  export declare const handlers: Record<string, Handler>;