fauxnix-cli 0.9.1 → 0.9.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 CHANGED
@@ -154,10 +154,13 @@ development:
154
154
  translate time (unsupported constructs throw named errors, never silently misbehave)
155
155
  - **text I/O**: `echo printf cat head tail wc tee nl tac md5sum sha1sum sha256sum base64 seq yes xargs`
156
156
 
157
- `cp` / `mv` / `rm` / `touch` / `tee` / `grep` / `head` / `du` carry a `CommandSpec`: unknown
158
- options fail with a GNU-style usage error instead of being ignored. Implemented GNU holes:
159
- `cp -n` / `mv -n` / `touch -c` / `tee --append` / `grep -m` / `head --lines` / `du --max-depth`.
160
- `fauxnix list --json` and `docs/command-specs.md` dump the same metadata.
157
+ `cp` / `mv` / `rm` / `touch` / `du` / `ls` / `ll` / `mkdir` / `rmdir` / `mktemp` / `ln` /
158
+ `readlink` / `realpath` / `basename` / `dirname` / `stat` / `file` / `df` / `chmod` / `chown` /
159
+ `diff` / `tee` / `grep` / `head` carry a `CommandSpec`: unknown options fail with a GNU-style
160
+ usage error instead of being ignored (`find` stays unspec'd so predicates like `-name` still
161
+ compile). Implemented GNU holes: `cp -n` / `mv -n` / `touch -c` / `tee --append` / `grep -m` /
162
+ `head --lines` / `du --max-depth`. `fauxnix list --json` and `docs/command-specs.md` dump the
163
+ same metadata.
161
164
  - **shell/system**: `cd pwd export unset env printenv ps kill pkill pgrep sleep which type whoami
162
165
  id groups date uname hostname uptime free nproc clear true false test [ [[ : pushd popd dirs sudo
163
166
  timeout man history less more source . eval exit alias set`
package/dist/cli.js CHANGED
@@ -87,6 +87,10 @@ async function runCheck() {
87
87
  stdio: ['ignore', 'pipe', 'pipe'],
88
88
  windowsHide: true,
89
89
  });
90
+ probe.on('error', (e) => {
91
+ console.error('status : FAILED to run powershell.exe: ' + e.message);
92
+ process.exit(1);
93
+ });
90
94
  let out = '';
91
95
  probe.stdout.on('data', (d) => (out += d.toString('utf8')));
92
96
  const code = await new Promise((resolve) => probe.on('close', (c) => resolve(c ?? 1)));
@@ -47,6 +47,10 @@ const PS_HSIZE_FN = [
47
47
  function psArray(words, fn = operandExpr) {
48
48
  return argListExpr(words, fn);
49
49
  }
50
+ /** GNU-style link: symlink or Windows junction. HardLink is a regular file. */
51
+ function psIsLink(it) {
52
+ return `(${it}.LinkType -eq 'SymbolicLink' -or ${it}.LinkType -eq 'Junction')`;
53
+ }
50
54
  /* ------------------------------------------------------------------ */
51
55
  /* ls */
52
56
  /* ------------------------------------------------------------------ */
@@ -66,7 +70,7 @@ const ls = (args) => {
66
70
  PS_GLOB_FN,
67
71
  PS_FTIME_FN,
68
72
  'function fx-mode($it) {',
69
- " $t = '-'; if ($it.PSIsContainer) { $t = 'd' } elseif ($it.LinkType) { $t = 'l' }",
73
+ " $t = '-'; if ($it.PSIsContainer) { $t = 'd' } elseif " + psIsLink('$it') + " { $t = 'l' }",
70
74
  " if ($it.PSIsContainer) { return ($t + 'rwxr-xr-x') }",
71
75
  " $ro = $it.Attributes.ToString().Contains('ReadOnly')",
72
76
  ' $ex = $false',
@@ -78,7 +82,9 @@ const ls = (args) => {
78
82
  'function fx-name($it) {',
79
83
  ' $n = $it.Name',
80
84
  ' if (' + (classify ? '$true' : '$false') + ') {',
81
- " if ($it.PSIsContainer) { $n = $n + '/' } elseif (@('.exe','.bat','.cmd','.com','.msi','.ps1') -contains $it.Extension.ToLower()) { $n = $n + '*' } elseif ($it.LinkType) { $n = $n + '@' }",
85
+ " if ($it.PSIsContainer) { $n = $n + '/' } elseif (@('.exe','.bat','.cmd','.com','.msi','.ps1') -contains $it.Extension.ToLower()) { $n = $n + '*' } elseif " +
86
+ psIsLink('$it') +
87
+ " { $n = $n + '@' }",
82
88
  ' }',
83
89
  ' return $n',
84
90
  '}',
@@ -298,7 +304,9 @@ const readlink = (args) => {
298
304
  ' if (' + (canon ? '$true' : '$false') + ') { (Resolve-Path -LiteralPath $fx_p).ProviderPath }',
299
305
  ' else {',
300
306
  ' $fx_it = Get-Item -LiteralPath $fx_p -Force',
301
- ' if ($fx_it.LinkType) { $fx_it.Target }',
307
+ ' if ' +
308
+ psIsLink('$fx_it') +
309
+ ' { $fx_tgt = \'\'; try { $fx_tgt = [string](@($fx_it.Target)[0]) } catch {}; $fx_tgt }',
302
310
  ' else { $script:fx_exit = 1 }',
303
311
  ' }',
304
312
  '}',
@@ -367,7 +375,9 @@ const stat = (args) => {
367
375
  ' if (-not (Test-Path -LiteralPath $fx_g)) { [Console]::Error.WriteLine("stat: cannot statx \'" + $fx_g + "\': No such file or directory"); $script:fx_exit = 1; continue }',
368
376
  ' $fx_it = Get-Item -LiteralPath $fx_g -Force',
369
377
  ' $fx_size = 0; if (-not $fx_it.PSIsContainer) { try { $fx_size = $fx_it.Length } catch {} }',
370
- " $fx_ft = 'regular file'; if ($fx_it.PSIsContainer) { $fx_ft = 'directory' } elseif ($fx_it.LinkType) { $fx_ft = 'symbolic link' }",
378
+ " $fx_ft = 'regular file'; if ($fx_it.PSIsContainer) { $fx_ft = 'directory' } elseif " +
379
+ psIsLink('$fx_it') +
380
+ " { $fx_ft = 'symbolic link' }",
371
381
  " $fx_ro = $fx_it.Attributes.ToString().Contains('ReadOnly')",
372
382
  " $fx_mode = '0664'; if ($fx_it.PSIsContainer) { $fx_mode = '0775' } elseif ($fx_ro) { $fx_mode = '0444' }",
373
383
  " $fx_epoch = [int](($fx_it.LastWriteTime.ToUniversalTime() - [datetime]'1970-01-01').TotalSeconds)",
@@ -397,22 +407,25 @@ const file = (args) => {
397
407
  ' foreach ($fx_g in (fx-glob $fx_f)) {',
398
408
  ' if (-not (Test-Path -LiteralPath $fx_g)) { [Console]::Error.WriteLine($fx_g + ": cannot open (No such file or directory)"); $script:fx_exit = 1; continue }',
399
409
  ' $fx_it = Get-Item -LiteralPath $fx_g -Force',
400
- ' if ($fx_it.PSIsContainer) { "$fx_g: directory"; continue }',
401
- ' if ($fx_it.LinkType) { "$fx_g: symbolic link to " + $fx_it.Target; continue }',
410
+ // `"$fx_g: "` is a PS scope (`$fx_g:symbolic`), not "name: text".
411
+ ' if ($fx_it.PSIsContainer) { $fx_g + \': directory\'; continue }',
412
+ ' if ' +
413
+ psIsLink('$fx_it') +
414
+ ' { $fx_tgt = \'\'; try { $fx_tgt = [string](@($fx_it.Target)[0]) } catch {}; $fx_g + \': symbolic link to \' + $fx_tgt; continue }',
402
415
  ' $fx_ext = $fx_it.Extension.ToLower()',
403
- ' if (@(\'.exe\', \'.dll\', \'.sys\') -contains $fx_ext) { "$fx_g: PE32+ executable (console) Intel 80386, for MS Windows"; continue }',
416
+ ' if (@(\'.exe\', \'.dll\', \'.sys\') -contains $fx_ext) { $fx_g + \': PE32+ executable (console) Intel 80386, for MS Windows\'; continue }',
404
417
  ' $fx_bytes = [IO.File]::ReadAllBytes($fx_g)',
405
- ' if ($fx_bytes.Length -eq 0) { "$fx_g: empty"; continue }',
418
+ ' if ($fx_bytes.Length -eq 0) { $fx_g + \': empty\'; continue }',
406
419
  ' $fx_nul = $false',
407
420
  ' $fx_lim = [math]::Min(8192, $fx_bytes.Length)',
408
421
  ' for ($fx_i = 0; $fx_i -lt $fx_lim; $fx_i++) { if ($fx_bytes[$fx_i] -eq 0) { $fx_nul = $true; break } }',
409
- ' if ($fx_nul) { "$fx_g: data"; continue }',
422
+ ' if ($fx_nul) { $fx_g + \': data\'; continue }',
410
423
  ' $fx_txt = fx-read $fx_g',
411
- ' if ($fx_txt.StartsWith(\'#!/\')) { "$fx_g: " + $fx_txt.Split("`n")[0].Trim() + " a /bin/sh script text executable" }',
424
+ ' if ($fx_txt.StartsWith(\'#!/\')) { $fx_g + \': \' + $fx_txt.Split("`n")[0].Trim() + \' a /bin/sh script text executable\' }',
412
425
  ' else {',
413
426
  ' $fx_nonascii = $false',
414
427
  ' foreach ($fx_c in $fx_txt.ToCharArray()) { if ([int]$fx_c -gt 127) { $fx_nonascii = $true; break } }',
415
- ' if ($fx_nonascii) { "$fx_g: UTF-8 Unicode text" } else { "$fx_g: ASCII text" }',
428
+ ' if ($fx_nonascii) { $fx_g + \': UTF-8 Unicode text\' } else { $fx_g + \': ASCII text\' }',
416
429
  ' }',
417
430
  ' }',
418
431
  '}',
@@ -689,7 +702,7 @@ function emitFind(n) {
689
702
  return '(-not $fx_i.PSIsContainer)';
690
703
  if (n.t === 'd')
691
704
  return '($fx_i.PSIsContainer)';
692
- return '([bool]$fx_i.LinkType)';
705
+ return psIsLink('$fx_i');
693
706
  case 'size':
694
707
  return '(' + n.ps + ')';
695
708
  case 'mtime':
@@ -31,7 +31,12 @@ const PS_SPLITLINES_FN = [
31
31
  ' return @($t.Split([char]10))',
32
32
  '}',
33
33
  ].join('\n');
34
- const STDIN_LINES = '@($input | ForEach-Object { [string]$_ })';
34
+ /** stdin flat line array (multi-line items from printf-style stages split). */
35
+ const STDIN_LINES = [
36
+ '$fx_in = New-Object System.Collections.Generic.List[string]',
37
+ 'foreach ($fx_it in @($input | ForEach-Object { [string]$_ })) { $fx_in.AddRange([string[]]@(fx-splitlines $fx_it)) }',
38
+ '$fx_in = @($fx_in)',
39
+ ].join('\n');
35
40
  /** Operand Words → PS array expression of string exprs. */
36
41
  function psArray(words, fn = operandExpr) {
37
42
  return argListExpr(words, fn);
@@ -126,6 +131,55 @@ function collectLongValues(args, names) {
126
131
  }
127
132
  return out;
128
133
  }
134
+ /**
135
+ * Collect EVERY value of a short option and its long aliases, in argv order.
136
+ * parseWords keeps only the last; grep -e/--regexp must OR-accumulate.
137
+ * Handles -e PAT, -ePAT, -ie PAT (bundled), --regexp PAT, --regexp=PAT.
138
+ */
139
+ function collectRepeatOptionValues(args, short, longs) {
140
+ const out = [];
141
+ let onlyOps = false;
142
+ for (let i = 0; i < args.length; i++) {
143
+ const t = wordToString(args[i]);
144
+ if (t === '--') {
145
+ onlyOps = true;
146
+ continue;
147
+ }
148
+ if (onlyOps)
149
+ continue;
150
+ if (t.startsWith('--')) {
151
+ const eq = t.indexOf('=');
152
+ const name = eq >= 0 ? t.slice(0, eq) : t;
153
+ if (!longs.includes(name))
154
+ continue;
155
+ if (eq >= 0) {
156
+ out.push(t.slice(eq + 1));
157
+ }
158
+ else if (i + 1 < args.length) {
159
+ out.push(wordToString(args[i + 1]));
160
+ i++;
161
+ }
162
+ continue;
163
+ }
164
+ if (!(t.startsWith('-') && t.length > 1 && !/^-?\d/.test(t.slice(1, 2))))
165
+ continue;
166
+ const body = t.slice(1);
167
+ for (let c = 0; c < body.length; c++) {
168
+ if (body[c] !== short)
169
+ continue;
170
+ const rest = body.slice(c + 1);
171
+ if (rest) {
172
+ out.push(rest);
173
+ }
174
+ else if (i + 1 < args.length) {
175
+ out.push(wordToString(args[i + 1]));
176
+ i++;
177
+ }
178
+ break;
179
+ }
180
+ }
181
+ return out;
182
+ }
129
183
  /** Build the "collect file operands through fx-glob" PS prologue. */
130
184
  function psCollectSources(filesExpr, cmdErr, leafOnly) {
131
185
  const test = leafOnly
@@ -304,26 +358,43 @@ const grep = (args) => {
304
358
  };
305
359
  const ctxA = Math.max(toInt(values.get('-A')), toInt(values.get('-C')));
306
360
  const ctxB = Math.max(toInt(values.get('-B')), toInt(values.get('-C')));
307
- const ePat = values.get('-e') ?? values.get('--regexp');
308
- if (ePat === undefined && operandWords.length === 0) {
361
+ const regexpPats = collectRepeatOptionValues(args, 'e', ['--regexp']);
362
+ if (regexpPats.length === 0 && operandWords.length === 0) {
309
363
  return ("[Console]::Error.WriteLine('usage: grep [OPTION]... PATTERN [FILE]...'); $script:fx_exit = 2");
310
364
  }
311
- const patternWord = ePat !== undefined ? [{ kind: 'Text', text: ePat }] : operandWords[0];
312
- const fileWords = ePat !== undefined ? operandWords : operandWords.slice(1);
313
- const patLit = literalOfWord(patternWord);
314
- let patExpr;
315
- if (fixed || patLit === null) {
316
- patExpr = textExpr(patternWord);
317
- }
318
- else {
319
- patExpr = psStr(ere ? ereToDotNet(patLit) : breToDotNet(patLit));
365
+ const fileWords = regexpPats.length > 0 ? operandWords : operandWords.slice(1);
366
+ const multiFixed = fixed && regexpPats.length > 1;
367
+ let patExpr = "''";
368
+ if (!multiFixed) {
369
+ if (regexpPats.length > 1) {
370
+ patExpr = psStr(regexpPats.map((p) => '(?:' + (ere ? ereToDotNet(p) : breToDotNet(p)) + ')').join('|'));
371
+ }
372
+ else {
373
+ const patternWord = regexpPats.length === 1 ? [{ kind: 'Text', text: regexpPats[0] }] : operandWords[0];
374
+ const patLit = literalOfWord(patternWord);
375
+ if (fixed || patLit === null) {
376
+ patExpr = textExpr(patternWord);
377
+ }
378
+ else {
379
+ patExpr = psStr(ere ? ereToDotNet(patLit) : breToDotNet(patLit));
380
+ }
381
+ }
320
382
  }
321
383
  const lines = [PS_READTEXT_FN, PS_SPLITLINES_FN];
322
384
  // --- pattern objects -------------------------------------------------
323
385
  if (fixed) {
324
- lines.push('$fx_needle = ' + patExpr);
325
- if (ci)
326
- lines.push('$fx_needle_ll = $fx_needle.ToLower()');
386
+ if (multiFixed) {
387
+ lines.push('$fx_needles = @(' +
388
+ regexpPats.map((p) => textExpr([{ kind: 'Text', text: p }])).join(', ') +
389
+ ')');
390
+ if (ci)
391
+ lines.push('$fx_needles_ll = @($fx_needles | ForEach-Object { $_.ToLower() })');
392
+ }
393
+ else {
394
+ lines.push('$fx_needle = ' + patExpr);
395
+ if (ci)
396
+ lines.push('$fx_needle_ll = $fx_needle.ToLower()');
397
+ }
327
398
  }
328
399
  else {
329
400
  lines.push('$fx_pat = ' + patExpr);
@@ -333,21 +404,45 @@ const grep = (args) => {
333
404
  ? '$fx_re = New-Object System.Text.RegularExpressions.Regex($fx_pat, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)'
334
405
  : '$fx_re = New-Object System.Text.RegularExpressions.Regex($fx_pat)');
335
406
  }
336
- // --- fx-gmatch: line test (-v applied) -------------------------------
407
+ // --- fx-gmatch: line test (-v applied once to the combined OR) --------
337
408
  lines.push('function fx-gmatch($l) {');
338
409
  if (fixed) {
339
410
  if (word) {
340
411
  if (ci)
341
412
  lines.push(' $lx = $l.ToLower()');
342
413
  const hay = ci ? '$lx' : '$l';
343
- const needle = ci ? '$fx_needle_ll' : '$fx_needle';
344
- lines.push(' $p = ' + hay + '.IndexOf(' + needle + ')');
345
- lines.push(' while ($p -ge 0) {');
346
- lines.push(' $ok = $true');
347
- lines.push(" if ($p -gt 0) { $c = " + hay + "[$p - 1]; $ok = -not ([char]::IsLetterOrDigit($c) -or $c -eq '_') }");
348
- lines.push(' if ($ok) { $e = $p + ' + needle + '.Length; if ($e -lt ' + hay + '.Length) { $c = ' + hay + "[$e]; $ok = -not ([char]::IsLetterOrDigit($c) -or $c -eq '_') } }");
349
- lines.push(' if ($ok) { return ' + pb(!inv) + ' }');
350
- lines.push(' $p = ' + hay + '.IndexOf(' + needle + ', $p + 1)');
414
+ if (multiFixed) {
415
+ const arr = ci ? '$fx_needles_ll' : '$fx_needles';
416
+ lines.push(' foreach ($fx_needle in ' + arr + ') {');
417
+ lines.push(' $p = ' + hay + '.IndexOf($fx_needle)');
418
+ lines.push(' while ($p -ge 0) {');
419
+ lines.push(' $ok = $true');
420
+ lines.push(" if ($p -gt 0) { $c = " + hay + "[$p - 1]; $ok = -not ([char]::IsLetterOrDigit($c) -or $c -eq '_') }");
421
+ lines.push(' if ($ok) { $e = $p + $fx_needle.Length; if ($e -lt ' + hay + '.Length) { $c = ' + hay + "[$e]; $ok = -not ([char]::IsLetterOrDigit($c) -or $c -eq '_') } }");
422
+ lines.push(' if ($ok) { return ' + pb(!inv) + ' }');
423
+ lines.push(' $p = ' + hay + '.IndexOf($fx_needle, $p + 1)');
424
+ lines.push(' }');
425
+ lines.push(' }');
426
+ lines.push(' return ' + pb(inv));
427
+ }
428
+ else {
429
+ const needle = ci ? '$fx_needle_ll' : '$fx_needle';
430
+ lines.push(' $p = ' + hay + '.IndexOf(' + needle + ')');
431
+ lines.push(' while ($p -ge 0) {');
432
+ lines.push(' $ok = $true');
433
+ lines.push(" if ($p -gt 0) { $c = " + hay + "[$p - 1]; $ok = -not ([char]::IsLetterOrDigit($c) -or $c -eq '_') }");
434
+ lines.push(' if ($ok) { $e = $p + ' + needle + '.Length; if ($e -lt ' + hay + '.Length) { $c = ' + hay + "[$e]; $ok = -not ([char]::IsLetterOrDigit($c) -or $c -eq '_') } }");
435
+ lines.push(' if ($ok) { return ' + pb(!inv) + ' }');
436
+ lines.push(' $p = ' + hay + '.IndexOf(' + needle + ', $p + 1)');
437
+ lines.push(' }');
438
+ lines.push(' return ' + pb(inv));
439
+ }
440
+ }
441
+ else if (multiFixed) {
442
+ const hay = ci ? '$l.ToLower()' : '$l';
443
+ const arr = ci ? '$fx_needles_ll' : '$fx_needles';
444
+ lines.push(' foreach ($fx_needle in ' + arr + ') {');
445
+ lines.push(' if (' + hay + '.Contains($fx_needle)) { return ' + pb(!inv) + ' }');
351
446
  lines.push(' }');
352
447
  lines.push(' return ' + pb(inv));
353
448
  }
@@ -488,27 +583,48 @@ const grep = (args) => {
488
583
  scan.push(' $fx_mleft--');
489
584
  if (onlyMatch && !inv) {
490
585
  if (fixed) {
491
- if (ci) {
586
+ if (ci)
492
587
  scan.push(' $lx = $fx_l.ToLower()');
493
- scan.push(' $p = $lx.IndexOf($fx_needle_ll)');
494
- }
495
- else {
496
- scan.push(' $p = $fx_l.IndexOf($fx_needle)');
497
- }
498
588
  const hay = ci ? '$lx' : '$fx_l';
499
- const needle = ci ? '$fx_needle_ll' : '$fx_needle';
500
- scan.push(' while ($p -ge 0) {');
501
- scan.push(' $ok = $true');
502
- if (word) {
503
- scan.push(" if ($p -gt 0) { $c = " + hay + "[$p - 1]; $ok = -not ([char]::IsLetterOrDigit($c) -or $c -eq '_') }");
504
- scan.push(' if ($ok) { $e = $p + ' + needle + '.Length; if ($e -lt ' + hay + '.Length) { $c = ' + hay + "[$e]; $ok = -not ([char]::IsLetterOrDigit($c) -or $c -eq '_') } }");
505
- scan.push(' if ($ok) { fx-emitline $fx_i (' + hay + '.Substring($p, ' + needle + '.Length)) }');
589
+ const emitFixedHits = (needle, indent) => {
590
+ scan.push(indent + '$p = ' + hay + '.IndexOf(' + needle + ')');
591
+ scan.push(indent + 'while ($p -ge 0) {');
592
+ scan.push(indent + ' $ok = $true');
593
+ if (word) {
594
+ scan.push(indent +
595
+ " if ($p -gt 0) { $c = " +
596
+ hay +
597
+ "[$p - 1]; $ok = -not ([char]::IsLetterOrDigit($c) -or $c -eq '_') }");
598
+ scan.push(indent +
599
+ ' if ($ok) { $e = $p + ' +
600
+ needle +
601
+ '.Length; if ($e -lt ' +
602
+ hay +
603
+ '.Length) { $c = ' +
604
+ hay +
605
+ "[$e]; $ok = -not ([char]::IsLetterOrDigit($c) -or $c -eq '_') } }");
606
+ scan.push(indent +
607
+ ' if ($ok) { fx-emitline $fx_i (' +
608
+ hay +
609
+ '.Substring($p, ' +
610
+ needle +
611
+ '.Length)) }');
612
+ }
613
+ else {
614
+ scan.push(indent + ' fx-emitline $fx_i (' + hay + '.Substring($p, ' + needle + '.Length))');
615
+ }
616
+ scan.push(indent + ' $p = ' + hay + '.IndexOf(' + needle + ', $p + 1)');
617
+ scan.push(indent + '}');
618
+ };
619
+ if (multiFixed) {
620
+ const arr = ci ? '$fx_needles_ll' : '$fx_needles';
621
+ scan.push(' foreach ($fx_needle in ' + arr + ') {');
622
+ emitFixedHits('$fx_needle', ' ');
623
+ scan.push(' }');
506
624
  }
507
625
  else {
508
- scan.push(' fx-emitline $fx_i (' + hay + '.Substring($p, ' + needle + '.Length))');
626
+ emitFixedHits(ci ? '$fx_needle_ll' : '$fx_needle', ' ');
509
627
  }
510
- scan.push(' $p = ' + hay + '.IndexOf(' + needle + ', $p + 1)');
511
- scan.push(' }');
512
628
  }
513
629
  else {
514
630
  scan.push(' foreach ($fx_m in $fx_re.Matches($fx_l)) { fx-emitline $fx_i $fx_m.Value }');
@@ -558,7 +674,8 @@ const grep = (args) => {
558
674
  else {
559
675
  lines.push('$fx_pre = $false');
560
676
  lines.push("$fx_disp = '(standard input)'");
561
- lines.push('$fx_ls = ' + STDIN_LINES);
677
+ lines.push(STDIN_LINES);
678
+ lines.push('$fx_ls = $fx_in');
562
679
  for (const l of scan)
563
680
  lines.push(l);
564
681
  }
@@ -1130,7 +1247,8 @@ const sed = (args) => {
1130
1247
  }
1131
1248
  else {
1132
1249
  lines.push('$fx_err = $false');
1133
- lines.push('$fx_lines = ' + STDIN_LINES);
1250
+ lines.push(STDIN_LINES);
1251
+ lines.push('$fx_lines = $fx_in');
1134
1252
  lines.push('$fx_n = $fx_lines.Count');
1135
1253
  lines.push('$fx_out = New-Object System.Collections.Generic.List[string]');
1136
1254
  lines.push('$fx_stop = $false');
@@ -1881,7 +1999,8 @@ const awk = (args) => {
1881
1999
  }
1882
2000
  else {
1883
2001
  lines.push('$fx_err = $false');
1884
- lines.push('$fx_lines = ' + STDIN_LINES);
2002
+ lines.push(STDIN_LINES);
2003
+ lines.push('$fx_lines = $fx_in');
1885
2004
  }
1886
2005
  const mainLoop = [];
1887
2006
  if (fsMode === 'ws') {
@@ -1982,7 +2101,8 @@ const sort = (args) => {
1982
2101
  lines.push('foreach ($fx_f in $fx_srcs) { $fx_lines += fx-splitlines (fx-read $fx_f) }');
1983
2102
  }
1984
2103
  else {
1985
- lines.push('$fx_lines = ' + STDIN_LINES);
2104
+ lines.push(STDIN_LINES);
2105
+ lines.push('$fx_lines = $fx_in');
1986
2106
  }
1987
2107
  const fastPath = specs.length === 0 && !globalN && !globalB;
1988
2108
  if (fastPath) {
@@ -2102,7 +2222,8 @@ const uniq = (args) => {
2102
2222
  }
2103
2223
  else {
2104
2224
  lines.push('$fx_err = $false');
2105
- lines.push('$fx_lines = ' + STDIN_LINES);
2225
+ lines.push(STDIN_LINES);
2226
+ lines.push('$fx_lines = $fx_in');
2106
2227
  }
2107
2228
  lines.push('function fx-ueq($a, $b) {');
2108
2229
  if (ignoreCase) {
@@ -2208,7 +2329,8 @@ const cut = (args) => {
2208
2329
  lines.push('foreach ($fx_f in $fx_srcs) { $fx_lines += fx-splitlines (fx-read $fx_f) }');
2209
2330
  }
2210
2331
  else {
2211
- lines.push('$fx_lines = ' + STDIN_LINES);
2332
+ lines.push(STDIN_LINES);
2333
+ lines.push('$fx_lines = $fx_in');
2212
2334
  }
2213
2335
  if (charsMode) {
2214
2336
  lines.push('foreach ($fx_l in $fx_lines) {');
@@ -2339,7 +2461,7 @@ const tr = (args) => {
2339
2461
  mapPairs.push([set1[i], set2[Math.min(i, set2.length - 1)]]);
2340
2462
  }
2341
2463
  }
2342
- const lines = [];
2464
+ const lines = [PS_SPLITLINES_FN];
2343
2465
  lines.push('$fx_dl = @{}');
2344
2466
  if (del) {
2345
2467
  lines.push('foreach ($c in [char[]](' + set1.join(', ') + ')) { $fx_dl[[int]$c] = $true }');
@@ -2352,7 +2474,8 @@ const tr = (args) => {
2352
2474
  if (squeeze) {
2353
2475
  lines.push('foreach ($c in [char[]](' + sqSet.join(', ') + ')) { $fx_sq[[int]$c] = $true }');
2354
2476
  }
2355
- lines.push('foreach ($fx_line in ' + STDIN_LINES + ') {');
2477
+ lines.push(STDIN_LINES);
2478
+ lines.push('foreach ($fx_line in $fx_in) {');
2356
2479
  lines.push(' $fx_sb = New-Object System.Text.StringBuilder');
2357
2480
  lines.push(' $fx_prev = -1');
2358
2481
  lines.push(' foreach ($fx_ch in $fx_line.ToCharArray()) {');
@@ -511,7 +511,9 @@ const head = (args, ctx) => {
511
511
  lines.push(STDIN_INLINES);
512
512
  lines.push(...psCollectFiles(operandWords, (g) => qErr('head', g, 'No such file or directory'), (g) => qErr('head', g, 'Is a directory', 'error reading ')), '$fx_count = [int](' + countLit + ')', '$fx_hdr = ((($fx_srcs.Count -gt 1) -and ' + pb(!quiet) + ') -or ' + pb(verbose) + ')', '$fx_first = $true');
513
513
  if (bytesMode) {
514
- lines.push('$fx_out = New-Object System.Text.StringBuilder', 'for ($fx_k = 0; $fx_k -lt $fx_srcs.Count; $fx_k++) {', ' $fx_g = $fx_srcs[$fx_k]', " if ($fx_g -eq '-') { $fx_txt = (fx-stdinraw $fx_items); $fx_disp = 'standard input' }", ' else { $fx_txt = fx-read $fx_g; $fx_disp = $fx_names[$fx_k] }', ' if ($fx_hdr) {', ' if (-not $fx_first) { [void]$fx_out.Append([string][char]10) }', " [void]$fx_out.Append('==> ' + $fx_disp + ' <==' + [string][char]10)", ' }', ' $fx_first = $false', ' $fx_len = [math]::Min($fx_count, $fx_txt.Length)', ' if ($fx_len -lt 0) { $fx_len = 0 }', ' if ($fx_len -gt 0) { [void]$fx_out.Append($fx_txt.Substring(0, $fx_len)) }', '}', 'fx-write $fx_out.ToString() $fx_term');
514
+ lines.push('$fx_out = New-Object System.Text.StringBuilder', 'for ($fx_k = 0; $fx_k -lt $fx_srcs.Count; $fx_k++) {', ' $fx_g = $fx_srcs[$fx_k]', " if ($fx_g -eq '-') { $fx_txt = (fx-stdinraw $fx_items); $fx_disp = 'standard input' }", ' else { $fx_txt = fx-read $fx_g; $fx_disp = $fx_names[$fx_k] }', ' if ($fx_hdr) {', ' if (-not $fx_first) { [void]$fx_out.Append([string][char]10) }', " [void]$fx_out.Append('==> ' + $fx_disp + ' <==' + [string][char]10)", ' }', ' $fx_first = $false',
515
+ // GNU: --bytes=-N prints all but last N; -0 / N≥size → empty
516
+ ' if ($fx_count -lt 0) { $fx_len = [math]::Max(0, $fx_txt.Length + $fx_count) }', ' else { $fx_len = [math]::Min($fx_count, $fx_txt.Length) }', ' if ($fx_len -gt 0) { [void]$fx_out.Append($fx_txt.Substring(0, $fx_len)) }', '}', 'fx-write $fx_out.ToString() $fx_term');
515
517
  }
516
518
  else {
517
519
  lines.push('for ($fx_k = 0; $fx_k -lt $fx_srcs.Count; $fx_k++) {', ' $fx_g = $fx_srcs[$fx_k]', " if ($fx_g -eq '-') { $fx_ls = @($fx_in); $fx_disp = 'standard input' }", ' else { $fx_ls = @(fx-splitlines (fx-read $fx_g)); $fx_disp = $fx_names[$fx_k] }', ' if ($fx_hdr) {', " if (-not $fx_first) { '' }", " '==> ' + $fx_disp + ' <=='", ' }', ' $fx_first = $false', ' $fx_lim = $fx_ls.Count', ' if ($fx_count -lt 0) { $fx_lim = $fx_ls.Count + $fx_count }', ' elseif ($fx_lim -gt $fx_count) { $fx_lim = $fx_count }', ' if ($fx_lim -lt 0) { $fx_lim = 0 }', ' for ($fx_i = 0; $fx_i -lt $fx_lim; $fx_i++) { $fx_ls[$fx_i] }', '}');
package/dist/executor.js CHANGED
@@ -21,6 +21,11 @@ function winTarget(target) {
21
21
  return path.join(os.tmpdir(), p.slice('$env:TEMP\\'.length));
22
22
  return p;
23
23
  }
24
+ /** Windows NUL device — `NUL`, `\\.\NUL`, and `cwd\NUL` after path.resolve. */
25
+ function isNulPath(p) {
26
+ const base = p.split(/[/\\]/).pop() ?? p;
27
+ return /^NUL$/i.test(base);
28
+ }
24
29
  function emitToPrepDest(dest, msg, fds, fallback) {
25
30
  if (dest.kind === 'nul')
26
31
  return;
@@ -91,53 +96,76 @@ function planRedirects(redirects) {
91
96
  appendStderr: false,
92
97
  mergeStderr: false,
93
98
  devNull: false,
99
+ swallowStderr: false,
94
100
  };
95
101
  for (const red of redirects) {
96
102
  const target = winTarget(red.target);
97
103
  switch (red.op) {
98
104
  case '<':
99
- r.stdinFile = target;
105
+ r.stdinFile = isNulPath(target) ? null : target;
100
106
  break;
101
107
  case '>':
102
108
  case '&>':
103
- if (target === 'NUL')
109
+ if (isNulPath(target)) {
104
110
  r.devNull = true;
111
+ r.stdoutFile = null;
112
+ if (red.op === '&>') {
113
+ r.swallowStderr = true;
114
+ r.stderrFile = null;
115
+ }
116
+ }
105
117
  else {
118
+ r.devNull = false;
106
119
  r.stdoutFile = target;
107
120
  r.appendStdout = false;
108
- if (red.op === '&>')
121
+ if (red.op === '&>') {
109
122
  r.stderrFile = target;
123
+ r.swallowStderr = false;
124
+ }
110
125
  }
111
126
  break;
112
127
  case '>>':
113
128
  case '&>>':
114
- if (target === 'NUL')
129
+ if (isNulPath(target)) {
115
130
  r.devNull = true;
131
+ r.stdoutFile = null;
132
+ if (red.op === '&>>') {
133
+ r.swallowStderr = true;
134
+ r.stderrFile = null;
135
+ }
136
+ }
116
137
  else {
138
+ r.devNull = false;
117
139
  r.stdoutFile = target;
118
140
  r.appendStdout = true;
119
141
  if (red.op === '&>>') {
120
142
  r.stderrFile = target;
121
143
  r.appendStderr = true;
144
+ r.swallowStderr = false;
122
145
  }
123
146
  }
124
147
  break;
125
148
  case '2>':
126
- if (target === 'NUL') {
127
- // 2>/dev/null swallows stderr only
149
+ if (isNulPath(target)) {
150
+ // stderr only — must not undo a prior >/dev/null
128
151
  r.stderrFile = null;
129
- r.devNull = false;
130
152
  r.swallowStderr = true;
131
153
  }
132
154
  else {
133
155
  r.stderrFile = target;
134
156
  r.appendStderr = false;
157
+ r.swallowStderr = false;
135
158
  }
136
159
  break;
137
160
  case '2>>':
138
- if (target !== 'NUL') {
161
+ if (isNulPath(target)) {
162
+ r.stderrFile = null;
163
+ r.swallowStderr = true;
164
+ }
165
+ else {
139
166
  r.stderrFile = target;
140
167
  r.appendStderr = true;
168
+ r.swallowStderr = false;
141
169
  }
142
170
  break;
143
171
  case '2>&1':
@@ -319,7 +347,9 @@ async function runPlans(plans, session, opts, afterSegment, ensureHost) {
319
347
  session.prevExit = exitCode;
320
348
  break;
321
349
  }
322
- const red = planRedirects(plan.redirects);
350
+ const red = planRedirects(plan.outputRedirects);
351
+ const inRed = planRedirects(plan.stdinRedirects);
352
+ red.stdinFile = inRed.stdinFile;
323
353
  red.stdinFile = red.stdinFile ? resolveTarget(red.stdinFile) : null;
324
354
  red.stdoutFile = red.stdoutFile ? resolveTarget(red.stdoutFile) : null;
325
355
  red.stderrFile = red.stderrFile ? resolveTarget(red.stderrFile) : null;
@@ -350,6 +380,8 @@ async function runPlans(plans, session, opts, afterSegment, ensureHost) {
350
380
  }
351
381
  const target = resolveTarget(winTarget(r.target));
352
382
  if (r.op === '<') {
383
+ if (isNulPath(target))
384
+ continue;
353
385
  if (!existsSync(target)) {
354
386
  emitPrepError('bash: ' + target + ': No such file or directory\n');
355
387
  redirectPrepFailed = true;
@@ -357,7 +389,7 @@ async function runPlans(plans, session, opts, afterSegment, ensureHost) {
357
389
  }
358
390
  continue;
359
391
  }
360
- if (target === 'NUL') {
392
+ if (isNulPath(target)) {
361
393
  if (r.op === '>' || r.op === '>>')
362
394
  prepStdout = { kind: 'nul' };
363
395
  else if (r.op === '2>' || r.op === '2>>')
@@ -452,9 +484,10 @@ async function runPlans(plans, session, opts, afterSegment, ensureHost) {
452
484
  segErr += segOut;
453
485
  segOut = '';
454
486
  }
455
- const swallowStderr = red.swallowStderr;
456
- if (swallowStderr)
487
+ if (red.swallowStderr)
457
488
  segErr = '';
489
+ if (red.devNull)
490
+ segOut = '';
458
491
  // Write captured streams through the fds opened during preflight
459
492
  // (bash: the redirect refers to the open file, not the path). Reopening
460
493
  // the path would recreate a file the command just unlinked
@@ -84,8 +84,12 @@ export interface SegmentPlan {
84
84
  script: string;
85
85
  /** Pipeline body before wrapScript — executor host mode re-wraps this. */
86
86
  body: string;
87
- /** All redirects collected from this segment (executor handles them). */
87
+ /** Every stage's redirects, in source order executor prep (open/fail). */
88
88
  redirects: Redirect[];
89
+ /** Last-stage redirects — captured stdout/stderr apply / >/dev/null. */
90
+ outputRedirects: Redirect[];
91
+ /** First-stage `<` only — FAUXNIX_STDIN_FILE feed. */
92
+ stdinRedirects: Redirect[];
89
93
  }
90
94
  export declare function translateCommandList(list: CommandList): SegmentPlan[];
91
95
  export type WrapMode = 'spawn' | 'host';
@@ -393,7 +393,6 @@ export function translateSimple(cmd, position, hasStdin) {
393
393
  const nameSplat = splatSpec(cmd.name);
394
394
  let body;
395
395
  if (nameSplat) {
396
- const invoke = '& $fx_cmd @fx_na';
397
396
  const hasAffix = !!(nameSplat.prefix || nameSplat.suffix);
398
397
  const promoted = cmd.args.length === 0
399
398
  ? ''
@@ -419,9 +418,7 @@ export function translateSimple(cmd, position, hasStdin) {
419
418
  emptyCmdLines.push('if ($fx_cw.Count -eq 0) {',
420
419
  // No words left → bash null command (exit 0). Remaining words are
421
420
  // known at compile time, so reuse translateSimple (handlers, not `&`).
422
- promoted ? indentBlock(promoted) : ' ', '} else {', ' $fx_na = [object[]]@(' + argListExpr(cmd.args) + ')', ' $fx_cmd = [string]$fx_cw[0]', ' if ($fx_cw.Count -gt 1) { $fx_na = @($fx_cw[1..($fx_cw.Count - 1)]) + $fx_na }', ' ' +
423
- (hasStdin ? '($input | ' + invoke + ')' : invoke) +
424
- ' | ForEach-Object { [string]$_ }', ' if ($LASTEXITCODE -gt 0) { $script:fx_exit = $LASTEXITCODE } elseif ($LASTEXITCODE -lt 0) { $script:fx_exit = 1 }', '}');
421
+ promoted ? indentBlock(promoted) : ' ', '} else {', ' $fx_na = [object[]](' + argListExpr(cmd.args) + ')', ' $fx_cmd = [string]$fx_cw[0]', ' if ($fx_cw.Count -gt 1) { $fx_na = [object[]](@($fx_cw[1..($fx_cw.Count - 1)]) + $fx_na) }', ' ' + (hasStdin ? '($input | fx-native $fx_cmd $fx_na)' : 'fx-native $fx_cmd $fx_na'), '}');
425
422
  body = emptyCmdLines.join('\n');
426
423
  }
427
424
  else if (nameLit !== null) {
@@ -431,26 +428,23 @@ export function translateSimple(cmd, position, hasStdin) {
431
428
  }
432
429
  else {
433
430
  // passthrough: native command (git, node, npm, python, cargo, ...)
434
- // invoked with the call operator and an argv-style argument array
435
- // no string re-parsing of user text.
431
+ // via fx-native (Win32 command line + Process). `& name @array` on
432
+ // PS 5.1 drops empty argv entries and eats embedded quotes.
436
433
  const nameExpr = psStr(nameLit);
437
- const invoke = '& ' + nameExpr + ' @fx_na';
434
+ const invoke = 'fx-native ' + nameExpr + ' $fx_na';
438
435
  body = [
439
- '$fx_na = ' + argListExpr(cmd.args),
440
- // feed pipeline stdin into the native process when we are a non-first stage
441
- (hasStdin ? '($input | ' + invoke + ')' : invoke) + ' | ForEach-Object { [string]$_ }',
442
- 'if ($LASTEXITCODE -gt 0) { $script:fx_exit = $LASTEXITCODE } elseif ($LASTEXITCODE -lt 0) { $script:fx_exit = 1 }',
436
+ '$fx_na = [object[]](' + argListExpr(cmd.args) + ')',
437
+ (hasStdin ? '($input | ' + invoke + ')' : invoke),
443
438
  ].join('\n');
444
439
  }
445
440
  }
446
441
  else {
447
442
  // dynamic command name — evaluate it
448
443
  const nameExpr = exprOfWord(cmd.name);
449
- const invoke = '& (' + nameExpr + ') @fx_na';
444
+ const invoke = 'fx-native (' + nameExpr + ') $fx_na';
450
445
  body = [
451
- '$fx_na = ' + argListExpr(cmd.args),
452
- (hasStdin ? '($input | ' + invoke + ')' : invoke) + ' | ForEach-Object { [string]$_ }',
453
- 'if ($LASTEXITCODE -gt 0) { $script:fx_exit = $LASTEXITCODE } elseif ($LASTEXITCODE -lt 0) { $script:fx_exit = 1 }',
446
+ '$fx_na = [object[]](' + argListExpr(cmd.args) + ')',
447
+ (hasStdin ? '($input | ' + invoke + ')' : invoke),
454
448
  ].join('\n');
455
449
  }
456
450
  // `VAR=value cmd` is command-scoped. Values are captured in the
@@ -785,6 +779,20 @@ function translateFor(cmd) {
785
779
  lines.push('}');
786
780
  return lines.join('\n');
787
781
  }
782
+ function stdinTarget(c) {
783
+ let t = null;
784
+ for (const r of c.redirects) {
785
+ if (r.op === '<')
786
+ t = r.target;
787
+ }
788
+ return t;
789
+ }
790
+ function stdinReadExpr(target) {
791
+ const n = normalizeLiteralPath(target);
792
+ if (n === 'NUL')
793
+ return '@()';
794
+ return '@(fx-readlines ' + pathExpr(n) + ')';
795
+ }
788
796
  export function translatePipelineBody(p) {
789
797
  // Every pipeline stage needs its own status slot. Handlers deliberately use
790
798
  // `$script:fx_exit` because their helper functions run in child scopes; in a
@@ -822,7 +830,28 @@ export function translatePipelineBody(p) {
822
830
  }
823
831
  const pipelineName = '__fx_p' + pipelineId;
824
832
  const statuses = bodies.map(() => '0').join(', ');
825
- const pipelineCall = names.join(' | ');
833
+ const stageStdin = p.commands.map((c) => stdinTarget(c));
834
+ const middleStdin = stageStdin.some((t, i) => i > 0 && t !== null);
835
+ let pipelineInner;
836
+ if (middleStdin) {
837
+ // A non-first `< file` replaces the pipe as that stage's stdin. Run
838
+ // earlier stages anyway (they may have side effects) but do not feed
839
+ // their stream into the redirected stage.
840
+ const seq = [' $fx_cur = @($input)'];
841
+ for (let i = 0; i < names.length; i++) {
842
+ if (stageStdin[i] && i > 0)
843
+ seq.push(' $fx_cur = ' + stdinReadExpr(stageStdin[i]));
844
+ if (i === names.length - 1)
845
+ seq.push(' $fx_cur | ' + names[i]);
846
+ else
847
+ seq.push(' $fx_cur = @($fx_cur | ' + names[i] + ')');
848
+ }
849
+ pipelineInner = seq.join('\n');
850
+ }
851
+ else {
852
+ pipelineInner =
853
+ ' $input | ' + names.join(' | ');
854
+ }
826
855
  defs.push([
827
856
  'function ' + pipelineName + ' {',
828
857
  ' ' + statusVar + ' = @(' + statuses + ')',
@@ -830,7 +859,7 @@ export function translatePipelineBody(p) {
830
859
  // The wrapper forwards redirect input to stage zero. With no input,
831
860
  // PowerShell still invokes a regular function once, which preserves the
832
861
  // existing no-stdin pipeline behavior on Windows PowerShell 5.1.
833
- ' $input | ' + pipelineCall,
862
+ pipelineInner,
834
863
  ' } finally {',
835
864
  // Bash defaults to pipefail off: only the last stage controls the list
836
865
  // status used by a following && / || segment.
@@ -843,13 +872,19 @@ export function translatePipelineBody(p) {
843
872
  export function translateCommandList(list) {
844
873
  const plans = [];
845
874
  for (const seg of list.segments) {
875
+ const cmds = seg.pipeline.commands;
846
876
  const redirects = [];
847
- for (const c of seg.pipeline.commands)
877
+ for (const c of cmds)
848
878
  redirects.push(...c.redirects);
879
+ const outputRedirects = cmds.length ? cmds[cmds.length - 1].redirects.slice() : [];
880
+ const stdinRedirects = cmds.length
881
+ ? cmds[0].redirects.filter((r) => r.op === '<')
882
+ : [];
849
883
  const { defs, call } = translatePipelineBody(seg.pipeline);
850
884
  let body = defs ? defs + '\n' + call : call;
851
- // `< file` redirects feed the pipeline via the FAUXNIX_STDIN_FILE channel
852
- if (redirects.some((r) => r.op === '<')) {
885
+ // First-stage `< file` feeds stage zero via FAUXNIX_STDIN_FILE.
886
+ // Later-stage `<` is owned inside the pipeline body, not this wrapper.
887
+ if (stdinRedirects.length) {
853
888
  // `& { ... }` (no parens) so the scriptblock can be a non-first
854
889
  // pipeline element receiving the fed lines.
855
890
  const pipeCall = call.startsWith('(& {') ? call.slice(1, -1) : call;
@@ -861,7 +896,14 @@ export function translateCommandList(list) {
861
896
  call +
862
897
  ' }';
863
898
  }
864
- plans.push({ op: seg.op, script: wrapScript(body), body, redirects });
899
+ plans.push({
900
+ op: seg.op,
901
+ script: wrapScript(body),
902
+ body,
903
+ redirects,
904
+ outputRedirects,
905
+ stdinRedirects,
906
+ });
865
907
  }
866
908
  return plans;
867
909
  }
@@ -880,6 +922,8 @@ const WRAP_HELPER_ORDER = [
880
922
  'fx-arrput',
881
923
  'fx-arrclr',
882
924
  'fx-subget',
925
+ 'fx-winargv',
926
+ 'fx-native',
883
927
  ];
884
928
  const WRAP_HELPER_DEPS = {
885
929
  'fx-readlines': [],
@@ -896,6 +940,8 @@ const WRAP_HELPER_DEPS = {
896
940
  'fx-arrput': ['fx-arrdrop', 'fx-svenc'],
897
941
  'fx-arrclr': ['fx-arrdrop'],
898
942
  'fx-subget': ['fx-arrload', 'fx-ifs1'],
943
+ 'fx-winargv': [],
944
+ 'fx-native': ['fx-winargv'],
899
945
  };
900
946
  /** Helpers the body calls that wrapScript still has to emit (not already defined there). */
901
947
  function wrapHelpersNeeded(body) {
@@ -1169,6 +1215,114 @@ export function wrapScript(body, opts = {}) {
1169
1215
  ' return [string]$arr[$i]',
1170
1216
  '}',
1171
1217
  ],
1218
+ 'fx-winargv': [
1219
+ 'function fx-winargv($argv) {',
1220
+ // Empty [object[]] unwraps to $null on PS 5.1; @($null) is one empty arg.
1221
+ ' if ($null -eq $argv) { $argv = @() }',
1222
+ ' $parts = New-Object System.Collections.Generic.List[string]',
1223
+ ' foreach ($a in @($argv)) {',
1224
+ ' $s = [string]$a',
1225
+ ' if ($s.Length -eq 0) { $parts.Add(\'""\'); continue }',
1226
+ ' $need = $false',
1227
+ ' foreach ($ch in $s.ToCharArray()) {',
1228
+ " if ($ch -eq ' ' -or $ch -eq ([char]9) -or $ch -eq [char]34) { $need = $true; break }",
1229
+ ' }',
1230
+ ' if (-not $need) { $parts.Add($s); continue }',
1231
+ ' $sb = New-Object System.Text.StringBuilder',
1232
+ ' [void]$sb.Append([char]34)',
1233
+ ' $bs = 0',
1234
+ ' foreach ($ch in $s.ToCharArray()) {',
1235
+ ' if ($ch -eq [char]92) { $bs++ }',
1236
+ ' elseif ($ch -eq [char]34) {',
1237
+ ' [void]$sb.Append(([string][char]92) * (2 * $bs + 1))',
1238
+ ' [void]$sb.Append([char]34)',
1239
+ ' $bs = 0',
1240
+ ' } else {',
1241
+ ' if ($bs -gt 0) { [void]$sb.Append(([string][char]92) * $bs); $bs = 0 }',
1242
+ ' [void]$sb.Append($ch)',
1243
+ ' }',
1244
+ ' }',
1245
+ ' if ($bs -gt 0) { [void]$sb.Append(([string][char]92) * (2 * $bs)) }',
1246
+ ' [void]$sb.Append([char]34)',
1247
+ ' $parts.Add($sb.ToString())',
1248
+ ' }',
1249
+ " return (($parts.ToArray()) -join ' ')",
1250
+ '}',
1251
+ ],
1252
+ 'fx-native': [
1253
+ 'function fx-native($name, $argv) {',
1254
+ ' if ($null -eq $argv) { $argv = @() } else { $argv = [object[]]@($argv) }',
1255
+ ' $app = Get-Command -Name $name -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1',
1256
+ ' if ($null -eq $app) {',
1257
+ // Dynamic/splat names can resolve to PS echo/cat aliases, not an .exe.
1258
+ // Application-first keeps node/git on the Win32 argv path; the call
1259
+ // operator is only for names that are not executables.
1260
+ ' $cmd = Get-Command -Name $name -ErrorAction SilentlyContinue | Select-Object -First 1',
1261
+ ' if ($null -eq $cmd) {',
1262
+ " [Console]::Error.WriteLine('bash: ' + $name + ': command not found')",
1263
+ ' $script:fx_exit = 127',
1264
+ ' return',
1265
+ ' }',
1266
+ ' $ins = @($input)',
1267
+ ' $global:LASTEXITCODE = 0',
1268
+ ' if ($ins.Count -gt 0) { $ins | & $name @argv } else { & $name @argv }',
1269
+ ' if ($LASTEXITCODE -gt 0) { $script:fx_exit = $LASTEXITCODE }',
1270
+ ' return',
1271
+ ' }',
1272
+ ' $psi = New-Object System.Diagnostics.ProcessStartInfo',
1273
+ ' $ext = [IO.Path]::GetExtension([string]$app.Source)',
1274
+ // CreateProcess cannot launch .cmd/.bat with UseShellExecute=false (npm.cmd).
1275
+ " if ($ext -eq '.cmd' -or $ext -eq '.bat') {",
1276
+ ' $comspec = Get-Command -Name cmd -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1',
1277
+ ' if ($null -eq $comspec) {',
1278
+ " [Console]::Error.WriteLine('bash: cmd.exe: command not found')",
1279
+ ' $script:fx_exit = 127',
1280
+ ' return',
1281
+ ' }',
1282
+ ' $psi.FileName = $comspec.Source',
1283
+ ' $fx_rest = fx-winargv $argv',
1284
+ " if ($fx_rest.Length -gt 0) { $psi.Arguments = '/d /s /c ' + (fx-winargv $app.Source) + ' ' + $fx_rest } else { $psi.Arguments = '/d /s /c ' + (fx-winargv $app.Source) }",
1285
+ ' } else {',
1286
+ ' $psi.FileName = $app.Source',
1287
+ ' $psi.Arguments = fx-winargv $argv',
1288
+ ' }',
1289
+ ' $psi.UseShellExecute = $false',
1290
+ ' $psi.RedirectStandardInput = $true',
1291
+ ' $psi.RedirectStandardOutput = $true',
1292
+ ' $psi.RedirectStandardError = $true',
1293
+ ' $psi.CreateNoWindow = $true',
1294
+ ' $psi.WorkingDirectory = [Environment]::CurrentDirectory',
1295
+ // StreamReader.ReadToEndAsync is .NET 4.5 (PS 5.1). Start readers
1296
+ // before writing stdin so a chatty child cannot fill the 64KB pipe.
1297
+ " if ($env:FAUXNIX_NATIVE_ENCODING -eq 'ansi') { $enc = [System.Text.Encoding]::GetEncoding(936) } else { $enc = New-Object System.Text.UTF8Encoding $false }",
1298
+ ' $psi.StandardOutputEncoding = $enc',
1299
+ ' $psi.StandardErrorEncoding = $enc',
1300
+ ' $p = New-Object System.Diagnostics.Process',
1301
+ ' $p.StartInfo = $psi',
1302
+ ' [void]$p.Start()',
1303
+ ' $outTask = $p.StandardOutput.ReadToEndAsync()',
1304
+ ' $errTask = $p.StandardError.ReadToEndAsync()',
1305
+ ' $ins = @($input)',
1306
+ ' if ($ins.Count -gt 0) {',
1307
+ ' foreach ($fx_ln in $ins) { $p.StandardInput.WriteLine([string]$fx_ln) }',
1308
+ ' }',
1309
+ ' $p.StandardInput.Close()',
1310
+ ' [void][System.Threading.Tasks.Task]::WaitAll(@($outTask, $errTask))',
1311
+ ' [void]$p.WaitForExit()',
1312
+ ' $errt = [string]$errTask.Result',
1313
+ ' if ($errt.Length -gt 0) { [Console]::Error.Write($errt) }',
1314
+ ' $t = [string]$outTask.Result',
1315
+ " $t = $t.Replace(([string][char]13 + [string][char]10), [string][char]10).Replace([string][char]13, [string][char]10)",
1316
+ " if ($t -ne '') {",
1317
+ ' $parts = @($t.Split([char]10))',
1318
+ " if ($parts.Count -gt 0 -and $parts[$parts.Count - 1] -eq '') { $parts = $parts[0..($parts.Count - 2)] }",
1319
+ ' foreach ($fx_ol in $parts) { $fx_ol }',
1320
+ ' }',
1321
+ ' $code = [int]$p.ExitCode',
1322
+ ' if ($code -gt 0) { $script:fx_exit = $code } elseif ($code -lt 0) { $script:fx_exit = 1 }',
1323
+ ' try { $p.Close() } catch {}',
1324
+ '}',
1325
+ ],
1172
1326
  };
1173
1327
  cachedWrapHelpers = helpers;
1174
1328
  for (const name of WRAP_HELPER_ORDER) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fauxnix-cli",
3
- "version": "0.9.1",
3
+ "version": "0.9.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": {