fauxnix-cli 0.9.1 → 0.9.3

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
@@ -58,6 +58,14 @@ Linux command line — file ops, text processing, process management, archives,
58
58
  maps cleanly onto PowerShell + .NET. fauxnix implements that subset faithfully and *fails loudly
59
59
  and helpfully* on what it can't translate, so the agent never gets silently-wrong results.
60
60
 
61
+ Labs now train computer-use agents on fleets of real desktops. Reporting in 2026 (*The
62
+ Information*, widely repeated) has OpenAI buying tens of thousands of Mac mini / Mac Studio
63
+ boxes — no screen, no keyboard — to reinforcement-learn agents that click, edit, test, and
64
+ run bash workflows, and Anthropic renting Mac minis through AWS for the same class of work.
65
+ That scoring environment is macOS. Windows users should not have to install a guest Unix to
66
+ keep up: the agent keeps writing bash; fauxnix makes the Windows box answer like the box the
67
+ agent was trained on. See [`docs/rfc-computer-use-windows.md`](docs/rfc-computer-use-windows.md).
68
+
61
69
  ## Install
62
70
 
63
71
  ```bash
@@ -154,10 +162,13 @@ development:
154
162
  translate time (unsupported constructs throw named errors, never silently misbehave)
155
163
  - **text I/O**: `echo printf cat head tail wc tee nl tac md5sum sha1sum sha256sum base64 seq yes xargs`
156
164
 
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.
165
+ `cp` / `mv` / `rm` / `touch` / `du` / `ls` / `ll` / `mkdir` / `rmdir` / `mktemp` / `ln` /
166
+ `readlink` / `realpath` / `basename` / `dirname` / `stat` / `file` / `df` / `chmod` / `chown` /
167
+ `diff` / `tee` / `grep` / `head` / `echo` / `printf` / `cat` / `tail` / `wc` carry a `CommandSpec`: unknown options fail with a GNU-style
168
+ usage error instead of being ignored (`find` stays unspec'd so predicates like `-name` still
169
+ compile). Implemented GNU holes: `cp -n` / `mv -n` / `touch -c` / `tee --append` / `grep -m` /
170
+ `head --lines` / `du --max-depth`. `fauxnix list --json` and `docs/command-specs.md` dump the
171
+ same metadata.
161
172
  - **shell/system**: `cd pwd export unset env printenv ps kill pkill pgrep sleep which type whoami
162
173
  id groups date uname hostname uptime free nproc clear true false test [ [[ : pushd popd dirs sudo
163
174
  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)));
@@ -214,8 +214,9 @@ const tar = (args) => {
214
214
  ' if ($fx_c) { $fx_tar = $fx_c.Source } else { $fx_tar = $null }',
215
215
  '}',
216
216
  'if ($fx_tar) {',
217
- ' & $fx_tar @($fx_args) | ForEach-Object { [string]$_ }',
218
- ' if ($LASTEXITCODE -gt 0) { $script:fx_exit = $LASTEXITCODE }',
217
+ // fx-native captures stdout as pipeline strings and sets fx_exit.
218
+ // [object[]]@(...) keeps an empty argv from unwrapping to $null on PS 5.1.
219
+ ' fx-native $fx_tar ([object[]]@($fx_args))',
219
220
  '} else {',
220
221
  " [Console]::Error.WriteLine('tar: fauxnix: tar.exe not found (Windows 10+ ships bsdtar as tar.exe)')",
221
222
  ' $script:fx_exit = 1',
@@ -47,11 +47,15 @@ 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
  /* ------------------------------------------------------------------ */
53
57
  const ls = (args) => {
54
- const { flags, longs, values, operandWords } = parseWords(args, [], ['--format']);
58
+ const { flags, longs, values, operandWords } = parseWords(args, [], ['--format', '--color']);
55
59
  const long = flags.has('l') || longs.has('--long') || values.get('--format') === 'long';
56
60
  const all = flags.has('a') || longs.has('--all');
57
61
  const almost = flags.has('A') || longs.has('--almost-all');
@@ -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':
@@ -712,7 +725,7 @@ const find = (args) => {
712
725
  const preds = raw.slice(pathEnd);
713
726
  if (preds.includes('-exec') || preds.includes('-execdir')) {
714
727
  return ('[Console]::Error.WriteLine(' +
715
- psStr('find: -exec is not supported by fauxnix; pipe into the command instead (e.g. `find . -name "*.log" | xargs rm`)') +
728
+ psStr('find: -exec is not supported by fauxnix; use `find . -name "*.log" -delete` or grep -r instead') +
716
729
  '); $script:fx_exit = 1');
717
730
  }
718
731
  const plan = parseFindPreds(preds);
@@ -954,6 +967,8 @@ export const specs = [
954
967
  opt('S', undefined),
955
968
  opt('r', undefined),
956
969
  opt('R', '--recursive', 'unsupported', { reason: 'recursive listing' }),
970
+ // GNU optional WHEN; takesValue so --color=auto is valid. No ANSI.
971
+ opt(undefined, '--color', 'implemented', { takesValue: true }),
957
972
  ], ls),
958
973
  fileSpec(['mkdir'], ['write'], [opt('p', '--parents'), opt('v', '--verbose')], mkdir),
959
974
  fileSpec(['rmdir'], ['delete'], [], rmdir),
@@ -79,10 +79,9 @@ function synthWord(text) {
79
79
  }
80
80
  /** A native-exe invocation obeying the fauxnix contract (string lines + exit code). */
81
81
  function nativeCall(exe, argArray) {
82
- return [
83
- '& ' + psStr(exe) + ' @(' + argArray + ') | ForEach-Object { [string]$_ }',
84
- 'if ($LASTEXITCODE -gt 0) { $script:fx_exit = $LASTEXITCODE }',
85
- ].join('\n');
82
+ // fx-native captures stdout as pipeline strings and sets fx_exit.
83
+ // [object[]]@(...) keeps an empty argv from unwrapping to $null on PS 5.1.
84
+ return 'fx-native ' + psStr(exe) + ' ([object[]]@(' + argArray + '))';
86
85
  }
87
86
  /* ------------------------------------------------------------------ */
88
87
  /* curl */
@@ -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,26 +583,44 @@ const grep = (args) => {
488
583
  scan.push(' $fx_mleft--');
489
584
  if (onlyMatch && !inv) {
490
585
  if (fixed) {
491
- if (ci) {
586
+ // GNU -o: emit leftmost-longest matches in input order, not per-needle.
587
+ if (ci)
492
588
  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
589
  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');
590
+ const needleArr = multiFixed
591
+ ? ci
592
+ ? '$fx_needles_ll'
593
+ : '$fx_needles'
594
+ : '@(' + (ci ? '$fx_needle_ll' : '$fx_needle') + ')';
595
+ scan.push(' $fx_cands = New-Object System.Collections.Generic.List[object]');
596
+ scan.push(' foreach ($fx_needle in ' + needleArr + ') {');
597
+ scan.push(' if ($fx_needle.Length -lt 1) { continue }');
598
+ scan.push(' $p = ' + hay + '.IndexOf($fx_needle)');
599
+ scan.push(' while ($p -ge 0) {');
502
600
  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)) }');
601
+ scan.push(' $ok = $true');
602
+ scan.push(" if ($p -gt 0) { $c = " +
603
+ hay +
604
+ "[$p - 1]; $ok = -not ([char]::IsLetterOrDigit($c) -or $c -eq '_') }");
605
+ scan.push(' if ($ok) { $e = $p + $fx_needle.Length; if ($e -lt ' +
606
+ hay +
607
+ '.Length) { $c = ' +
608
+ hay +
609
+ "[$e]; $ok = -not ([char]::IsLetterOrDigit($c) -or $c -eq '_') } }");
610
+ scan.push(' if ($ok) { [void]$fx_cands.Add([pscustomobject]@{ Start = $p; Len = $fx_needle.Length }) }');
506
611
  }
507
612
  else {
508
- scan.push(' fx-emitline $fx_i (' + hay + '.Substring($p, ' + needle + '.Length))');
613
+ scan.push(' [void]$fx_cands.Add([pscustomobject]@{ Start = $p; Len = $fx_needle.Length })');
509
614
  }
510
- scan.push(' $p = ' + hay + '.IndexOf(' + needle + ', $p + 1)');
615
+ scan.push(' $p = ' + hay + '.IndexOf($fx_needle, $p + 1)');
616
+ scan.push(' }');
617
+ scan.push(' }');
618
+ scan.push(' $fx_end = 0');
619
+ scan.push(' foreach ($fx_c in @($fx_cands | Sort-Object Start, @{ Expression = { $_.Len }; Descending = $true })) {');
620
+ scan.push(' if ($fx_c.Start -ge $fx_end) {');
621
+ scan.push(' fx-emitline $fx_i (' + hay + '.Substring($fx_c.Start, $fx_c.Len))');
622
+ scan.push(' $fx_end = $fx_c.Start + $fx_c.Len');
623
+ scan.push(' }');
511
624
  scan.push(' }');
512
625
  }
513
626
  else {
@@ -558,7 +671,8 @@ const grep = (args) => {
558
671
  else {
559
672
  lines.push('$fx_pre = $false');
560
673
  lines.push("$fx_disp = '(standard input)'");
561
- lines.push('$fx_ls = ' + STDIN_LINES);
674
+ lines.push(STDIN_LINES);
675
+ lines.push('$fx_ls = $fx_in');
562
676
  for (const l of scan)
563
677
  lines.push(l);
564
678
  }
@@ -1130,7 +1244,8 @@ const sed = (args) => {
1130
1244
  }
1131
1245
  else {
1132
1246
  lines.push('$fx_err = $false');
1133
- lines.push('$fx_lines = ' + STDIN_LINES);
1247
+ lines.push(STDIN_LINES);
1248
+ lines.push('$fx_lines = $fx_in');
1134
1249
  lines.push('$fx_n = $fx_lines.Count');
1135
1250
  lines.push('$fx_out = New-Object System.Collections.Generic.List[string]');
1136
1251
  lines.push('$fx_stop = $false');
@@ -1881,7 +1996,8 @@ const awk = (args) => {
1881
1996
  }
1882
1997
  else {
1883
1998
  lines.push('$fx_err = $false');
1884
- lines.push('$fx_lines = ' + STDIN_LINES);
1999
+ lines.push(STDIN_LINES);
2000
+ lines.push('$fx_lines = $fx_in');
1885
2001
  }
1886
2002
  const mainLoop = [];
1887
2003
  if (fsMode === 'ws') {
@@ -1982,7 +2098,8 @@ const sort = (args) => {
1982
2098
  lines.push('foreach ($fx_f in $fx_srcs) { $fx_lines += fx-splitlines (fx-read $fx_f) }');
1983
2099
  }
1984
2100
  else {
1985
- lines.push('$fx_lines = ' + STDIN_LINES);
2101
+ lines.push(STDIN_LINES);
2102
+ lines.push('$fx_lines = $fx_in');
1986
2103
  }
1987
2104
  const fastPath = specs.length === 0 && !globalN && !globalB;
1988
2105
  if (fastPath) {
@@ -2102,7 +2219,8 @@ const uniq = (args) => {
2102
2219
  }
2103
2220
  else {
2104
2221
  lines.push('$fx_err = $false');
2105
- lines.push('$fx_lines = ' + STDIN_LINES);
2222
+ lines.push(STDIN_LINES);
2223
+ lines.push('$fx_lines = $fx_in');
2106
2224
  }
2107
2225
  lines.push('function fx-ueq($a, $b) {');
2108
2226
  if (ignoreCase) {
@@ -2208,7 +2326,8 @@ const cut = (args) => {
2208
2326
  lines.push('foreach ($fx_f in $fx_srcs) { $fx_lines += fx-splitlines (fx-read $fx_f) }');
2209
2327
  }
2210
2328
  else {
2211
- lines.push('$fx_lines = ' + STDIN_LINES);
2329
+ lines.push(STDIN_LINES);
2330
+ lines.push('$fx_lines = $fx_in');
2212
2331
  }
2213
2332
  if (charsMode) {
2214
2333
  lines.push('foreach ($fx_l in $fx_lines) {');
@@ -2339,7 +2458,7 @@ const tr = (args) => {
2339
2458
  mapPairs.push([set1[i], set2[Math.min(i, set2.length - 1)]]);
2340
2459
  }
2341
2460
  }
2342
- const lines = [];
2461
+ const lines = [PS_SPLITLINES_FN];
2343
2462
  lines.push('$fx_dl = @{}');
2344
2463
  if (del) {
2345
2464
  lines.push('foreach ($c in [char[]](' + set1.join(', ') + ')) { $fx_dl[[int]$c] = $true }');
@@ -2352,7 +2471,8 @@ const tr = (args) => {
2352
2471
  if (squeeze) {
2353
2472
  lines.push('foreach ($c in [char[]](' + sqSet.join(', ') + ')) { $fx_sq[[int]$c] = $true }');
2354
2473
  }
2355
- lines.push('foreach ($fx_line in ' + STDIN_LINES + ') {');
2474
+ lines.push(STDIN_LINES);
2475
+ lines.push('foreach ($fx_line in $fx_in) {');
2356
2476
  lines.push(' $fx_sb = New-Object System.Text.StringBuilder');
2357
2477
  lines.push(' $fx_prev = -1');
2358
2478
  lines.push(' foreach ($fx_ch in $fx_line.ToCharArray()) {');