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 +7 -4
- package/dist/cli.js +4 -0
- package/dist/commands/files.js +25 -12
- package/dist/commands/text-filters.js +171 -48
- package/dist/commands/text-io.js +3 -1
- package/dist/executor.js +45 -12
- package/dist/translator.d.ts +5 -1
- package/dist/translator.js +175 -21
- package/package.json +1 -1
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` / `
|
|
158
|
-
|
|
159
|
-
`
|
|
160
|
-
|
|
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)));
|
package/dist/commands/files.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
401
|
-
' if ($fx_it.
|
|
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) {
|
|
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) {
|
|
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) {
|
|
422
|
+
' if ($fx_nul) { $fx_g + \': data\'; continue }',
|
|
410
423
|
' $fx_txt = fx-read $fx_g',
|
|
411
|
-
' if ($fx_txt.StartsWith(\'#!/\')) {
|
|
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) {
|
|
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 '
|
|
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
|
-
|
|
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
|
|
308
|
-
if (
|
|
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
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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()) {');
|
package/dist/commands/text-io.js
CHANGED
|
@@ -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',
|
|
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
|
|
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
|
|
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
|
|
127
|
-
//
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
package/dist/translator.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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';
|
package/dist/translator.js
CHANGED
|
@@ -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[]]
|
|
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
|
-
//
|
|
435
|
-
//
|
|
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 = '
|
|
434
|
+
const invoke = 'fx-native ' + nameExpr + ' $fx_na';
|
|
438
435
|
body = [
|
|
439
|
-
'$fx_na = ' + argListExpr(cmd.args),
|
|
440
|
-
|
|
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 = '
|
|
444
|
+
const invoke = 'fx-native (' + nameExpr + ') $fx_na';
|
|
450
445
|
body = [
|
|
451
|
-
'$fx_na = ' + argListExpr(cmd.args),
|
|
452
|
-
(hasStdin ? '($input | ' + invoke + ')' : invoke)
|
|
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
|
|
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
|
-
|
|
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
|
|
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`
|
|
852
|
-
|
|
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({
|
|
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