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 +15 -4
- package/dist/cli.js +4 -0
- package/dist/commands/archive.js +3 -2
- package/dist/commands/files.js +29 -14
- package/dist/commands/net.js +3 -4
- package/dist/commands/text-filters.js +166 -46
- package/dist/commands/text-io.js +92 -11
- package/dist/errors.d.ts +4 -0
- package/dist/errors.js +18 -5
- package/dist/executor.js +116 -93
- package/dist/registry.d.ts +2 -0
- package/dist/registry.js +4 -1
- package/dist/translator.d.ts +5 -1
- package/dist/translator.js +189 -21
- package/package.json +1 -1
package/dist/translator.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { FauxnixParseError, isUnquotedLiteral, } from './ast.js';
|
|
2
2
|
import { parseCommand } from './parser.js';
|
|
3
3
|
import { lookup, psStr } from './registry.js';
|
|
4
|
+
import { PYTHON3_WINDOWS_HINT, SH_SCRIPT_WINDOWS_HINT } from './errors.js';
|
|
4
5
|
/* ------------------------------------------------------------------ */
|
|
5
6
|
/* Variable mapping */
|
|
6
7
|
/* ------------------------------------------------------------------ */
|
|
@@ -393,7 +394,6 @@ export function translateSimple(cmd, position, hasStdin) {
|
|
|
393
394
|
const nameSplat = splatSpec(cmd.name);
|
|
394
395
|
let body;
|
|
395
396
|
if (nameSplat) {
|
|
396
|
-
const invoke = '& $fx_cmd @fx_na';
|
|
397
397
|
const hasAffix = !!(nameSplat.prefix || nameSplat.suffix);
|
|
398
398
|
const promoted = cmd.args.length === 0
|
|
399
399
|
? ''
|
|
@@ -419,9 +419,7 @@ export function translateSimple(cmd, position, hasStdin) {
|
|
|
419
419
|
emptyCmdLines.push('if ($fx_cw.Count -eq 0) {',
|
|
420
420
|
// No words left → bash null command (exit 0). Remaining words are
|
|
421
421
|
// 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 }', '}');
|
|
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 = [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
423
|
body = emptyCmdLines.join('\n');
|
|
426
424
|
}
|
|
427
425
|
else if (nameLit !== null) {
|
|
@@ -431,26 +429,23 @@ export function translateSimple(cmd, position, hasStdin) {
|
|
|
431
429
|
}
|
|
432
430
|
else {
|
|
433
431
|
// passthrough: native command (git, node, npm, python, cargo, ...)
|
|
434
|
-
//
|
|
435
|
-
//
|
|
432
|
+
// via fx-native (Win32 command line + Process). `& name @array` on
|
|
433
|
+
// PS 5.1 drops empty argv entries and eats embedded quotes.
|
|
436
434
|
const nameExpr = psStr(nameLit);
|
|
437
|
-
const invoke = '
|
|
435
|
+
const invoke = 'fx-native ' + nameExpr + ' $fx_na';
|
|
438
436
|
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 }',
|
|
437
|
+
'$fx_na = [object[]](' + argListExpr(cmd.args) + ')',
|
|
438
|
+
(hasStdin ? '($input | ' + invoke + ')' : invoke),
|
|
443
439
|
].join('\n');
|
|
444
440
|
}
|
|
445
441
|
}
|
|
446
442
|
else {
|
|
447
443
|
// dynamic command name — evaluate it
|
|
448
444
|
const nameExpr = exprOfWord(cmd.name);
|
|
449
|
-
const invoke = '
|
|
445
|
+
const invoke = 'fx-native (' + nameExpr + ') $fx_na';
|
|
450
446
|
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 }',
|
|
447
|
+
'$fx_na = [object[]](' + argListExpr(cmd.args) + ')',
|
|
448
|
+
(hasStdin ? '($input | ' + invoke + ')' : invoke),
|
|
454
449
|
].join('\n');
|
|
455
450
|
}
|
|
456
451
|
// `VAR=value cmd` is command-scoped. Values are captured in the
|
|
@@ -785,6 +780,20 @@ function translateFor(cmd) {
|
|
|
785
780
|
lines.push('}');
|
|
786
781
|
return lines.join('\n');
|
|
787
782
|
}
|
|
783
|
+
function stdinTarget(c) {
|
|
784
|
+
let t = null;
|
|
785
|
+
for (const r of c.redirects) {
|
|
786
|
+
if (r.op === '<')
|
|
787
|
+
t = r.target;
|
|
788
|
+
}
|
|
789
|
+
return t;
|
|
790
|
+
}
|
|
791
|
+
function stdinReadExpr(target) {
|
|
792
|
+
const n = normalizeLiteralPath(target);
|
|
793
|
+
if (n === 'NUL')
|
|
794
|
+
return '@()';
|
|
795
|
+
return '@(fx-readlines ' + pathExpr(n) + ')';
|
|
796
|
+
}
|
|
788
797
|
export function translatePipelineBody(p) {
|
|
789
798
|
// Every pipeline stage needs its own status slot. Handlers deliberately use
|
|
790
799
|
// `$script:fx_exit` because their helper functions run in child scopes; in a
|
|
@@ -822,7 +831,28 @@ export function translatePipelineBody(p) {
|
|
|
822
831
|
}
|
|
823
832
|
const pipelineName = '__fx_p' + pipelineId;
|
|
824
833
|
const statuses = bodies.map(() => '0').join(', ');
|
|
825
|
-
const
|
|
834
|
+
const stageStdin = p.commands.map((c) => stdinTarget(c));
|
|
835
|
+
const middleStdin = stageStdin.some((t, i) => i > 0 && t !== null);
|
|
836
|
+
let pipelineInner;
|
|
837
|
+
if (middleStdin) {
|
|
838
|
+
// A non-first `< file` replaces the pipe as that stage's stdin. Run
|
|
839
|
+
// earlier stages anyway (they may have side effects) but do not feed
|
|
840
|
+
// their stream into the redirected stage.
|
|
841
|
+
const seq = [' $fx_cur = @($input)'];
|
|
842
|
+
for (let i = 0; i < names.length; i++) {
|
|
843
|
+
if (stageStdin[i] && i > 0)
|
|
844
|
+
seq.push(' $fx_cur = ' + stdinReadExpr(stageStdin[i]));
|
|
845
|
+
if (i === names.length - 1)
|
|
846
|
+
seq.push(' $fx_cur | ' + names[i]);
|
|
847
|
+
else
|
|
848
|
+
seq.push(' $fx_cur = @($fx_cur | ' + names[i] + ')');
|
|
849
|
+
}
|
|
850
|
+
pipelineInner = seq.join('\n');
|
|
851
|
+
}
|
|
852
|
+
else {
|
|
853
|
+
pipelineInner =
|
|
854
|
+
' $input | ' + names.join(' | ');
|
|
855
|
+
}
|
|
826
856
|
defs.push([
|
|
827
857
|
'function ' + pipelineName + ' {',
|
|
828
858
|
' ' + statusVar + ' = @(' + statuses + ')',
|
|
@@ -830,7 +860,7 @@ export function translatePipelineBody(p) {
|
|
|
830
860
|
// The wrapper forwards redirect input to stage zero. With no input,
|
|
831
861
|
// PowerShell still invokes a regular function once, which preserves the
|
|
832
862
|
// existing no-stdin pipeline behavior on Windows PowerShell 5.1.
|
|
833
|
-
|
|
863
|
+
pipelineInner,
|
|
834
864
|
' } finally {',
|
|
835
865
|
// Bash defaults to pipefail off: only the last stage controls the list
|
|
836
866
|
// status used by a following && / || segment.
|
|
@@ -843,13 +873,19 @@ export function translatePipelineBody(p) {
|
|
|
843
873
|
export function translateCommandList(list) {
|
|
844
874
|
const plans = [];
|
|
845
875
|
for (const seg of list.segments) {
|
|
876
|
+
const cmds = seg.pipeline.commands;
|
|
846
877
|
const redirects = [];
|
|
847
|
-
for (const c of
|
|
878
|
+
for (const c of cmds)
|
|
848
879
|
redirects.push(...c.redirects);
|
|
880
|
+
const outputRedirects = cmds.length ? cmds[cmds.length - 1].redirects.slice() : [];
|
|
881
|
+
const stdinRedirects = cmds.length
|
|
882
|
+
? cmds[0].redirects.filter((r) => r.op === '<')
|
|
883
|
+
: [];
|
|
849
884
|
const { defs, call } = translatePipelineBody(seg.pipeline);
|
|
850
885
|
let body = defs ? defs + '\n' + call : call;
|
|
851
|
-
// `< file`
|
|
852
|
-
|
|
886
|
+
// First-stage `< file` feeds stage zero via FAUXNIX_STDIN_FILE.
|
|
887
|
+
// Later-stage `<` is owned inside the pipeline body, not this wrapper.
|
|
888
|
+
if (stdinRedirects.length) {
|
|
853
889
|
// `& { ... }` (no parens) so the scriptblock can be a non-first
|
|
854
890
|
// pipeline element receiving the fed lines.
|
|
855
891
|
const pipeCall = call.startsWith('(& {') ? call.slice(1, -1) : call;
|
|
@@ -861,7 +897,14 @@ export function translateCommandList(list) {
|
|
|
861
897
|
call +
|
|
862
898
|
' }';
|
|
863
899
|
}
|
|
864
|
-
plans.push({
|
|
900
|
+
plans.push({
|
|
901
|
+
op: seg.op,
|
|
902
|
+
script: wrapScript(body),
|
|
903
|
+
body,
|
|
904
|
+
redirects,
|
|
905
|
+
outputRedirects,
|
|
906
|
+
stdinRedirects,
|
|
907
|
+
});
|
|
865
908
|
}
|
|
866
909
|
return plans;
|
|
867
910
|
}
|
|
@@ -880,6 +923,8 @@ const WRAP_HELPER_ORDER = [
|
|
|
880
923
|
'fx-arrput',
|
|
881
924
|
'fx-arrclr',
|
|
882
925
|
'fx-subget',
|
|
926
|
+
'fx-winargv',
|
|
927
|
+
'fx-native',
|
|
883
928
|
];
|
|
884
929
|
const WRAP_HELPER_DEPS = {
|
|
885
930
|
'fx-readlines': [],
|
|
@@ -896,6 +941,8 @@ const WRAP_HELPER_DEPS = {
|
|
|
896
941
|
'fx-arrput': ['fx-arrdrop', 'fx-svenc'],
|
|
897
942
|
'fx-arrclr': ['fx-arrdrop'],
|
|
898
943
|
'fx-subget': ['fx-arrload', 'fx-ifs1'],
|
|
944
|
+
'fx-winargv': [],
|
|
945
|
+
'fx-native': ['fx-winargv'],
|
|
899
946
|
};
|
|
900
947
|
/** Helpers the body calls that wrapScript still has to emit (not already defined there). */
|
|
901
948
|
function wrapHelpersNeeded(body) {
|
|
@@ -1169,6 +1216,127 @@ export function wrapScript(body, opts = {}) {
|
|
|
1169
1216
|
' return [string]$arr[$i]',
|
|
1170
1217
|
'}',
|
|
1171
1218
|
],
|
|
1219
|
+
'fx-winargv': [
|
|
1220
|
+
'function fx-winargv($argv, $cmdmeta) {',
|
|
1221
|
+
// Empty [object[]] unwraps to $null on PS 5.1; @($null) is one empty arg.
|
|
1222
|
+
// $cmdmeta: also quote & | () <> ^ so cmd.exe /c does not split the tail.
|
|
1223
|
+
' if ($null -eq $argv) { $argv = @() }',
|
|
1224
|
+
' $parts = New-Object System.Collections.Generic.List[string]',
|
|
1225
|
+
' foreach ($a in @($argv)) {',
|
|
1226
|
+
' $s = [string]$a',
|
|
1227
|
+
' if ($s.Length -eq 0) { $parts.Add(\'""\'); continue }',
|
|
1228
|
+
' $need = $false',
|
|
1229
|
+
' foreach ($ch in $s.ToCharArray()) {',
|
|
1230
|
+
" if ($ch -eq ' ' -or $ch -eq ([char]9) -or $ch -eq [char]34) { $need = $true; break }",
|
|
1231
|
+
" if ($cmdmeta -and ($ch -eq '&' -or $ch -eq '|' -or $ch -eq '(' -or $ch -eq ')' -or $ch -eq '<' -or $ch -eq '>' -or $ch -eq '^')) { $need = $true; break }",
|
|
1232
|
+
' }',
|
|
1233
|
+
' if (-not $need) { $parts.Add($s); continue }',
|
|
1234
|
+
' $sb = New-Object System.Text.StringBuilder',
|
|
1235
|
+
' [void]$sb.Append([char]34)',
|
|
1236
|
+
' $bs = 0',
|
|
1237
|
+
' foreach ($ch in $s.ToCharArray()) {',
|
|
1238
|
+
' if ($ch -eq [char]92) { $bs++ }',
|
|
1239
|
+
' elseif ($ch -eq [char]34) {',
|
|
1240
|
+
' [void]$sb.Append(([string][char]92) * (2 * $bs + 1))',
|
|
1241
|
+
' [void]$sb.Append([char]34)',
|
|
1242
|
+
' $bs = 0',
|
|
1243
|
+
' } else {',
|
|
1244
|
+
' if ($bs -gt 0) { [void]$sb.Append(([string][char]92) * $bs); $bs = 0 }',
|
|
1245
|
+
' [void]$sb.Append($ch)',
|
|
1246
|
+
' }',
|
|
1247
|
+
' }',
|
|
1248
|
+
' if ($bs -gt 0) { [void]$sb.Append(([string][char]92) * (2 * $bs)) }',
|
|
1249
|
+
' [void]$sb.Append([char]34)',
|
|
1250
|
+
' $parts.Add($sb.ToString())',
|
|
1251
|
+
' }',
|
|
1252
|
+
" return (($parts.ToArray()) -join ' ')",
|
|
1253
|
+
'}',
|
|
1254
|
+
],
|
|
1255
|
+
'fx-native': [
|
|
1256
|
+
'function fx-native($name, $argv) {',
|
|
1257
|
+
' if ($null -eq $argv) { $argv = @() } else { $argv = [object[]]@($argv) }',
|
|
1258
|
+
' $app = Get-Command -Name $name -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1',
|
|
1259
|
+
' if ($null -eq $app) {',
|
|
1260
|
+
// Dynamic/splat names can resolve to PS echo/cat aliases, not an .exe.
|
|
1261
|
+
// Application-first keeps node/git on the Win32 argv path; the call
|
|
1262
|
+
// operator is only for names that are not executables.
|
|
1263
|
+
' $cmd = Get-Command -Name $name -ErrorAction SilentlyContinue | Select-Object -First 1',
|
|
1264
|
+
' if ($null -eq $cmd) {',
|
|
1265
|
+
" $fx_nf = 'bash: ' + $name + ': command not found'",
|
|
1266
|
+
" $fx_n = [string]$name",
|
|
1267
|
+
// Hint only — never alias python3→python (wrong interpreter).
|
|
1268
|
+
" if ($fx_n -eq 'python3' -or $fx_n -eq 'python3.exe') { $fx_nf += '" +
|
|
1269
|
+
PYTHON3_WINDOWS_HINT +
|
|
1270
|
+
"' }",
|
|
1271
|
+
" elseif ($fx_n -like '*.sh') { $fx_nf += '" + SH_SCRIPT_WINDOWS_HINT + "' }",
|
|
1272
|
+
' [Console]::Error.WriteLine($fx_nf)',
|
|
1273
|
+
' $script:fx_exit = 127',
|
|
1274
|
+
' return',
|
|
1275
|
+
' }',
|
|
1276
|
+
' $ins = @($input)',
|
|
1277
|
+
' $global:LASTEXITCODE = 0',
|
|
1278
|
+
' if ($ins.Count -gt 0) { $ins | & $name @argv } else { & $name @argv }',
|
|
1279
|
+
' if ($LASTEXITCODE -gt 0) { $script:fx_exit = $LASTEXITCODE }',
|
|
1280
|
+
' return',
|
|
1281
|
+
' }',
|
|
1282
|
+
' $psi = New-Object System.Diagnostics.ProcessStartInfo',
|
|
1283
|
+
' $ext = [IO.Path]::GetExtension([string]$app.Source)',
|
|
1284
|
+
// CreateProcess cannot launch .cmd/.bat with UseShellExecute=false (npm.cmd).
|
|
1285
|
+
// /s strips one outer quote pair from the /c tail; CRT-quote cmd
|
|
1286
|
+
// metacharacters so `&`/`|`/`()` do not start a second command.
|
|
1287
|
+
" if ($ext -eq '.cmd' -or $ext -eq '.bat') {",
|
|
1288
|
+
' $comspec = Get-Command -Name cmd -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1',
|
|
1289
|
+
' if ($null -eq $comspec) {',
|
|
1290
|
+
" [Console]::Error.WriteLine('bash: cmd.exe: command not found')",
|
|
1291
|
+
' $script:fx_exit = 127',
|
|
1292
|
+
' return',
|
|
1293
|
+
' }',
|
|
1294
|
+
' $psi.FileName = $comspec.Source',
|
|
1295
|
+
' $fx_app = fx-winargv $app.Source $true',
|
|
1296
|
+
' $fx_rest = fx-winargv $argv $true',
|
|
1297
|
+
" if ($fx_rest.Length -gt 0) { $fx_tail = $fx_app + ' ' + $fx_rest } else { $fx_tail = $fx_app }",
|
|
1298
|
+
' $psi.Arguments = \'/d /s /c "\' + $fx_tail + \'"\'',
|
|
1299
|
+
' } else {',
|
|
1300
|
+
' $psi.FileName = $app.Source',
|
|
1301
|
+
' $psi.Arguments = fx-winargv $argv',
|
|
1302
|
+
' }',
|
|
1303
|
+
' $psi.UseShellExecute = $false',
|
|
1304
|
+
' $psi.RedirectStandardInput = $true',
|
|
1305
|
+
' $psi.RedirectStandardOutput = $true',
|
|
1306
|
+
' $psi.RedirectStandardError = $true',
|
|
1307
|
+
' $psi.CreateNoWindow = $true',
|
|
1308
|
+
' $psi.WorkingDirectory = [Environment]::CurrentDirectory',
|
|
1309
|
+
// StreamReader.ReadToEndAsync is .NET 4.5 (PS 5.1). Start readers
|
|
1310
|
+
// before writing stdin so a chatty child cannot fill the 64KB pipe.
|
|
1311
|
+
" if ($env:FAUXNIX_NATIVE_ENCODING -eq 'ansi') { $enc = [System.Text.Encoding]::GetEncoding(936) } else { $enc = New-Object System.Text.UTF8Encoding $false }",
|
|
1312
|
+
' $psi.StandardOutputEncoding = $enc',
|
|
1313
|
+
' $psi.StandardErrorEncoding = $enc',
|
|
1314
|
+
' $p = New-Object System.Diagnostics.Process',
|
|
1315
|
+
' $p.StartInfo = $psi',
|
|
1316
|
+
' [void]$p.Start()',
|
|
1317
|
+
' $outTask = $p.StandardOutput.ReadToEndAsync()',
|
|
1318
|
+
' $errTask = $p.StandardError.ReadToEndAsync()',
|
|
1319
|
+
' $ins = @($input)',
|
|
1320
|
+
' if ($ins.Count -gt 0) {',
|
|
1321
|
+
' foreach ($fx_ln in $ins) { $p.StandardInput.WriteLine([string]$fx_ln) }',
|
|
1322
|
+
' }',
|
|
1323
|
+
' $p.StandardInput.Close()',
|
|
1324
|
+
' [void][System.Threading.Tasks.Task]::WaitAll(@($outTask, $errTask))',
|
|
1325
|
+
' [void]$p.WaitForExit()',
|
|
1326
|
+
' $errt = [string]$errTask.Result',
|
|
1327
|
+
' if ($errt.Length -gt 0) { [Console]::Error.Write($errt) }',
|
|
1328
|
+
' $t = [string]$outTask.Result',
|
|
1329
|
+
" $t = $t.Replace(([string][char]13 + [string][char]10), [string][char]10).Replace([string][char]13, [string][char]10)",
|
|
1330
|
+
" if ($t -ne '') {",
|
|
1331
|
+
' $parts = @($t.Split([char]10))',
|
|
1332
|
+
" if ($parts.Count -gt 0 -and $parts[$parts.Count - 1] -eq '') { $parts = $parts[0..($parts.Count - 2)] }",
|
|
1333
|
+
' foreach ($fx_ol in $parts) { $fx_ol }',
|
|
1334
|
+
' }',
|
|
1335
|
+
' $code = [int]$p.ExitCode',
|
|
1336
|
+
' if ($code -gt 0) { $script:fx_exit = $code } elseif ($code -lt 0) { $script:fx_exit = 1 }',
|
|
1337
|
+
' try { $p.Close() } catch {}',
|
|
1338
|
+
'}',
|
|
1339
|
+
],
|
|
1172
1340
|
};
|
|
1173
1341
|
cachedWrapHelpers = helpers;
|
|
1174
1342
|
for (const name of WRAP_HELPER_ORDER) {
|
package/package.json
CHANGED