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/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] }', '}');
|
|
@@ -1091,6 +1093,9 @@ const xargs = (args) => {
|
|
|
1091
1093
|
noRunIfEmpty = true;
|
|
1092
1094
|
else if (ch === 't')
|
|
1093
1095
|
trace = true;
|
|
1096
|
+
else if (ch === '0') {
|
|
1097
|
+
return psErrExpr(psStr('xargs: -0 is not supported by fauxnix'));
|
|
1098
|
+
}
|
|
1094
1099
|
else if (ch === 'n' || ch === 'I' || ch === 'L') {
|
|
1095
1100
|
const restv = body.slice(c + 1);
|
|
1096
1101
|
let val;
|
|
@@ -1138,12 +1143,12 @@ const xargs = (args) => {
|
|
|
1138
1143
|
}
|
|
1139
1144
|
const n = chunkN !== null && Number.isFinite(chunkN) && chunkN > 0 ? chunkN : 0;
|
|
1140
1145
|
const replExpr = repl !== null ? psStr(repl) : null;
|
|
1146
|
+
// fx-native already records $script:fx_exit from ExitCode.
|
|
1141
1147
|
const invoke = [
|
|
1142
1148
|
' if (' +
|
|
1143
1149
|
pb(trace) +
|
|
1144
1150
|
") { [Console]::Error.WriteLine(((@($fx_cmd) + @($fx_argv)) -join ' ')) }",
|
|
1145
|
-
'
|
|
1146
|
-
' if ($LASTEXITCODE -ne 0 -and $script:fx_exit -eq 0) { $script:fx_exit = $LASTEXITCODE }',
|
|
1151
|
+
' fx-native $fx_cmd $fx_argv',
|
|
1147
1152
|
];
|
|
1148
1153
|
let dispatch;
|
|
1149
1154
|
const guard = ' if (' + pb(noRunIfEmpty) + ' -and $fx_args.Count -eq 0) { }' + '\n' + ' else {';
|
|
@@ -1152,7 +1157,7 @@ const xargs = (args) => {
|
|
|
1152
1157
|
dispatch = [
|
|
1153
1158
|
guard,
|
|
1154
1159
|
' foreach ($fx_l in $fx_args) {',
|
|
1155
|
-
' $fx_argv = @()',
|
|
1160
|
+
' $fx_argv = [object[]]@()',
|
|
1156
1161
|
' $fx_hit = $false',
|
|
1157
1162
|
' foreach ($fx_a in $fx_base) {',
|
|
1158
1163
|
' if ($fx_a.Contains(' + replExpr + ')) {',
|
|
@@ -1173,7 +1178,7 @@ const xargs = (args) => {
|
|
|
1173
1178
|
' $fx_i = 0',
|
|
1174
1179
|
' $fx_ran = $false',
|
|
1175
1180
|
' while ($fx_i -lt $fx_args.Count) {',
|
|
1176
|
-
' $fx_argv = @($fx_base)',
|
|
1181
|
+
' $fx_argv = [object[]]@($fx_base)',
|
|
1177
1182
|
' $fx_j = 0',
|
|
1178
1183
|
' while ($fx_j -lt ' + n + ' -and $fx_i -lt $fx_args.Count) {',
|
|
1179
1184
|
' $fx_argv += $fx_args[$fx_i]',
|
|
@@ -1183,10 +1188,8 @@ const xargs = (args) => {
|
|
|
1183
1188
|
...invoke,
|
|
1184
1189
|
' }',
|
|
1185
1190
|
' if (-not $fx_ran) {',
|
|
1186
|
-
' $fx_argv = @($fx_base)',
|
|
1187
|
-
'
|
|
1188
|
-
' ' + invoke[1],
|
|
1189
|
-
' ' + invoke[2],
|
|
1191
|
+
' $fx_argv = [object[]]@($fx_base)',
|
|
1192
|
+
...invoke.map((line) => ' ' + line),
|
|
1190
1193
|
' }',
|
|
1191
1194
|
' }',
|
|
1192
1195
|
];
|
|
@@ -1194,17 +1197,27 @@ const xargs = (args) => {
|
|
|
1194
1197
|
else {
|
|
1195
1198
|
dispatch = [
|
|
1196
1199
|
guard,
|
|
1197
|
-
' $fx_argv = @($fx_base) + @($fx_args)',
|
|
1200
|
+
' $fx_argv = [object[]](@($fx_base) + @($fx_args))',
|
|
1198
1201
|
...invoke,
|
|
1199
1202
|
' }',
|
|
1200
1203
|
];
|
|
1201
1204
|
}
|
|
1205
|
+
// Default GNU xargs splits on blanks; -I keeps whole lines as one item.
|
|
1206
|
+
const collectArgs = replExpr !== null
|
|
1207
|
+
? "$fx_args = @($fx_in | Where-Object { $_ -ne '' })"
|
|
1208
|
+
: [
|
|
1209
|
+
'$fx_args = @()',
|
|
1210
|
+
'foreach ($fx_l in $fx_in) {',
|
|
1211
|
+
" if ($fx_l -eq '') { continue }",
|
|
1212
|
+
" $fx_args += @($fx_l -split '[ \\t]+' | Where-Object { $_ -ne '' })",
|
|
1213
|
+
'}',
|
|
1214
|
+
].join('\n');
|
|
1202
1215
|
return [
|
|
1203
1216
|
PS_SPLITLINES_FN,
|
|
1204
1217
|
STDIN_INLINES,
|
|
1205
1218
|
'$fx_tg = ' + argListExpr(target, exprOfWord),
|
|
1206
1219
|
"if ($fx_tg.Count -eq 0) { $fx_cmd = ''; $fx_base = @() } else { $fx_cmd = [string]$fx_tg[0]; $fx_base = $(if ($fx_tg.Count -gt 1) { @($fx_tg[1..($fx_tg.Count - 1)]) } else { @() }) }",
|
|
1207
|
-
|
|
1220
|
+
collectArgs,
|
|
1208
1221
|
...dispatch,
|
|
1209
1222
|
].join('\n');
|
|
1210
1223
|
};
|
|
@@ -1234,6 +1247,74 @@ export const specs = [
|
|
|
1234
1247
|
dispatch: 'translated',
|
|
1235
1248
|
handler: head,
|
|
1236
1249
|
},
|
|
1250
|
+
{
|
|
1251
|
+
names: ['echo'],
|
|
1252
|
+
options: [
|
|
1253
|
+
{ short: 'n', support: 'implemented' },
|
|
1254
|
+
{ short: 'e', support: 'implemented' },
|
|
1255
|
+
{ short: 'E', support: 'implemented' },
|
|
1256
|
+
],
|
|
1257
|
+
effects: [],
|
|
1258
|
+
platform: 'windows-ps51',
|
|
1259
|
+
dispatch: 'translated',
|
|
1260
|
+
usageExit: 2,
|
|
1261
|
+
leadingOptions: true,
|
|
1262
|
+
handler: echo,
|
|
1263
|
+
},
|
|
1264
|
+
{
|
|
1265
|
+
names: ['printf'],
|
|
1266
|
+
options: [],
|
|
1267
|
+
effects: [],
|
|
1268
|
+
platform: 'windows-ps51',
|
|
1269
|
+
dispatch: 'translated',
|
|
1270
|
+
usageExit: 2,
|
|
1271
|
+
leadingOptions: true,
|
|
1272
|
+
handler: printf,
|
|
1273
|
+
},
|
|
1274
|
+
{
|
|
1275
|
+
names: ['cat'],
|
|
1276
|
+
options: [
|
|
1277
|
+
{ short: 'n', support: 'implemented' },
|
|
1278
|
+
{ short: 'b', support: 'implemented' },
|
|
1279
|
+
{ short: 's', support: 'implemented' },
|
|
1280
|
+
{ short: 'E', support: 'implemented' },
|
|
1281
|
+
{ short: 'T', support: 'implemented' },
|
|
1282
|
+
{ short: 'A', support: 'implemented' },
|
|
1283
|
+
],
|
|
1284
|
+
effects: ['read'],
|
|
1285
|
+
platform: 'windows-ps51',
|
|
1286
|
+
dispatch: 'translated',
|
|
1287
|
+
handler: cat,
|
|
1288
|
+
},
|
|
1289
|
+
{
|
|
1290
|
+
names: ['tail'],
|
|
1291
|
+
options: [
|
|
1292
|
+
{ short: 'n', long: '--lines', takesValue: true, support: 'implemented' },
|
|
1293
|
+
{ short: 'c', long: '--bytes', takesValue: true, support: 'implemented' },
|
|
1294
|
+
{ short: 'q', long: '--quiet', support: 'implemented' },
|
|
1295
|
+
{ long: '--silent', support: 'implemented' },
|
|
1296
|
+
{ short: 'v', long: '--verbose', support: 'implemented' },
|
|
1297
|
+
{ short: 'f', support: 'unsupported', reason: 'no persistent tty' },
|
|
1298
|
+
{ short: 'F', support: 'unsupported', reason: 'no persistent tty' },
|
|
1299
|
+
],
|
|
1300
|
+
effects: ['read'],
|
|
1301
|
+
platform: 'windows-ps51',
|
|
1302
|
+
dispatch: 'translated',
|
|
1303
|
+
handler: tail,
|
|
1304
|
+
},
|
|
1305
|
+
{
|
|
1306
|
+
names: ['wc'],
|
|
1307
|
+
options: [
|
|
1308
|
+
{ short: 'l', support: 'implemented' },
|
|
1309
|
+
{ short: 'w', support: 'implemented' },
|
|
1310
|
+
{ short: 'c', support: 'implemented' },
|
|
1311
|
+
{ short: 'm', support: 'implemented' },
|
|
1312
|
+
],
|
|
1313
|
+
effects: ['read'],
|
|
1314
|
+
platform: 'windows-ps51',
|
|
1315
|
+
dispatch: 'translated',
|
|
1316
|
+
handler: wc,
|
|
1317
|
+
},
|
|
1237
1318
|
];
|
|
1238
1319
|
export const handlers = {
|
|
1239
1320
|
echo,
|
package/dist/errors.d.ts
CHANGED
|
@@ -2,4 +2,8 @@
|
|
|
2
2
|
* Error normalization — make PowerShell failures look like bash failures
|
|
3
3
|
* so agents can pattern-match on familiar Linux error styles.
|
|
4
4
|
*/
|
|
5
|
+
/** Missing `python3` on Windows (Mac/Linux agents emit this; do not alias). */
|
|
6
|
+
export declare const PYTHON3_WINDOWS_HINT = " (fauxnix: try `python` or `py` on Windows)";
|
|
7
|
+
/** `.sh` cannot be CreateProcess'd; fx-native never emits the PS not-recognized line. */
|
|
8
|
+
export declare const SH_SCRIPT_WINDOWS_HINT = " (fauxnix: .sh scripts cannot run natively on Windows)";
|
|
5
9
|
export declare function normalizeStderr(stderr: string): string;
|
package/dist/errors.js
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
* Error normalization — make PowerShell failures look like bash failures
|
|
3
3
|
* so agents can pattern-match on familiar Linux error styles.
|
|
4
4
|
*/
|
|
5
|
+
/** Missing `python3` on Windows (Mac/Linux agents emit this; do not alias). */
|
|
6
|
+
export const PYTHON3_WINDOWS_HINT = ' (fauxnix: try `python` or `py` on Windows)';
|
|
7
|
+
/** `.sh` cannot be CreateProcess'd; fx-native never emits the PS not-recognized line. */
|
|
8
|
+
export const SH_SCRIPT_WINDOWS_HINT = ' (fauxnix: .sh scripts cannot run natively on Windows)';
|
|
9
|
+
function commandNotFound(name) {
|
|
10
|
+
const msg = 'bash: ' + name + ': command not found';
|
|
11
|
+
const base = name.replace(/^.*[/\\]/, '');
|
|
12
|
+
if (/^python3(\.exe)?$/i.test(base))
|
|
13
|
+
return msg + PYTHON3_WINDOWS_HINT;
|
|
14
|
+
if (/\.sh$/i.test(name))
|
|
15
|
+
return msg + SH_SCRIPT_WINDOWS_HINT;
|
|
16
|
+
return msg;
|
|
17
|
+
}
|
|
5
18
|
/** Lines produced by PowerShell error formatting that bash would never show. */
|
|
6
19
|
const PS_NOISE = [
|
|
7
20
|
/^\s*\+ CategoryInfo\s*:/,
|
|
@@ -62,15 +75,15 @@ export function normalizeStderr(stderr) {
|
|
|
62
75
|
// "The term 'x' is not recognized as a name of a cmdlet, function, ..."
|
|
63
76
|
let m = line.match(/^The term '(.+?)' is not recognized/);
|
|
64
77
|
if (m)
|
|
65
|
-
return
|
|
78
|
+
return commandNotFound(m[1]);
|
|
66
79
|
// zh-CN: 无法将"x"项识别为 cmdlet、函数、脚本文件或可运行程序的名称
|
|
67
80
|
m = line.match(/^无法将["'”]?([^"'”]+)["'”]?项识别为/);
|
|
68
81
|
if (m)
|
|
69
|
-
return
|
|
82
|
+
return commandNotFound(m[1]);
|
|
70
83
|
// "x : The term 'y' is not recognized ..." (with source prefix)
|
|
71
84
|
m = line.match(/^(\S+)\s*:\s*The term '(.+?)' is not recognized/);
|
|
72
85
|
if (m)
|
|
73
|
-
return
|
|
86
|
+
return commandNotFound(m[2]);
|
|
74
87
|
// "cat : Cannot find path 'D:\x' because it does not exist."
|
|
75
88
|
m = line.match(/^(\S+)\s*:\s*Cannot find path '(.+?)' because it does not exist\.?$/);
|
|
76
89
|
if (m) {
|
|
@@ -93,9 +106,9 @@ export function normalizeStderr(stderr) {
|
|
|
93
106
|
m = line.match(/^(\S+)\s*:\s*(.*)Access to the path '(.+?)' is denied\.?$/);
|
|
94
107
|
if (m)
|
|
95
108
|
return m[1].toLowerCase() + ': cannot remove \'' + m[3] + '\': Permission denied';
|
|
96
|
-
//
|
|
109
|
+
// leftover PS not-recognized lines that the rewrites above did not catch
|
|
97
110
|
if (/\.sh'?/.test(line) && /is not recognized/.test(line)) {
|
|
98
|
-
return line +
|
|
111
|
+
return line + SH_SCRIPT_WINDOWS_HINT;
|
|
99
112
|
}
|
|
100
113
|
return line;
|
|
101
114
|
});
|
package/dist/executor.js
CHANGED
|
@@ -21,7 +21,40 @@ function winTarget(target) {
|
|
|
21
21
|
return path.join(os.tmpdir(), p.slice('$env:TEMP\\'.length));
|
|
22
22
|
return p;
|
|
23
23
|
}
|
|
24
|
-
|
|
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
|
+
}
|
|
29
|
+
function applyRedirectDest(op, target, stdout, stderr) {
|
|
30
|
+
if (op === '2>&1')
|
|
31
|
+
return { stdout, stderr: stdout };
|
|
32
|
+
if (op === '1>&2')
|
|
33
|
+
return { stdout: stderr, stderr };
|
|
34
|
+
if (op === '<' || target === undefined)
|
|
35
|
+
return { stdout, stderr };
|
|
36
|
+
const dest = isNulPath(target) ? { kind: 'nul' } : { kind: 'file', path: target };
|
|
37
|
+
if (op === '>' || op === '>>')
|
|
38
|
+
return { stdout: dest, stderr };
|
|
39
|
+
if (op === '2>' || op === '2>>')
|
|
40
|
+
return { stdout, stderr: dest };
|
|
41
|
+
if (op === '&>' || op === '&>>')
|
|
42
|
+
return { stdout: dest, stderr: dest };
|
|
43
|
+
return { stdout, stderr };
|
|
44
|
+
}
|
|
45
|
+
/** Last-stage output fds only — captured stdout/stderr apply. */
|
|
46
|
+
function lastStageOutputDests(redirects, resolveTarget) {
|
|
47
|
+
let stdout = { kind: 'caller', fd: 1 };
|
|
48
|
+
let stderr = { kind: 'caller', fd: 2 };
|
|
49
|
+
for (const r of redirects) {
|
|
50
|
+
const target = r.op === '2>&1' || r.op === '1>&2' || r.op === '<'
|
|
51
|
+
? undefined
|
|
52
|
+
: resolveTarget(winTarget(r.target));
|
|
53
|
+
({ stdout, stderr } = applyRedirectDest(r.op, target, stdout, stderr));
|
|
54
|
+
}
|
|
55
|
+
return { stdout, stderr };
|
|
56
|
+
}
|
|
57
|
+
function emitToPrepDest(dest, msg, fds, caller) {
|
|
25
58
|
if (dest.kind === 'nul')
|
|
26
59
|
return;
|
|
27
60
|
if (dest.kind === 'file') {
|
|
@@ -30,10 +63,14 @@ function emitToPrepDest(dest, msg, fds, fallback) {
|
|
|
30
63
|
return;
|
|
31
64
|
}
|
|
32
65
|
catch {
|
|
33
|
-
|
|
66
|
+
caller.stderr(msg);
|
|
67
|
+
return;
|
|
34
68
|
}
|
|
35
69
|
}
|
|
36
|
-
|
|
70
|
+
if (dest.fd === 1)
|
|
71
|
+
caller.stdout(msg);
|
|
72
|
+
else
|
|
73
|
+
caller.stderr(msg);
|
|
37
74
|
}
|
|
38
75
|
function writeAllSync(fd, data) {
|
|
39
76
|
let off = 0;
|
|
@@ -89,19 +126,20 @@ function planRedirects(redirects) {
|
|
|
89
126
|
appendStdout: false,
|
|
90
127
|
stderrFile: null,
|
|
91
128
|
appendStderr: false,
|
|
92
|
-
mergeStderr: false,
|
|
93
|
-
devNull: false,
|
|
94
129
|
};
|
|
95
130
|
for (const red of redirects) {
|
|
96
131
|
const target = winTarget(red.target);
|
|
97
132
|
switch (red.op) {
|
|
98
133
|
case '<':
|
|
99
|
-
r.stdinFile = target;
|
|
134
|
+
r.stdinFile = isNulPath(target) ? null : target;
|
|
100
135
|
break;
|
|
101
136
|
case '>':
|
|
102
137
|
case '&>':
|
|
103
|
-
if (target
|
|
104
|
-
r.
|
|
138
|
+
if (isNulPath(target)) {
|
|
139
|
+
r.stdoutFile = null;
|
|
140
|
+
if (red.op === '&>')
|
|
141
|
+
r.stderrFile = null;
|
|
142
|
+
}
|
|
105
143
|
else {
|
|
106
144
|
r.stdoutFile = target;
|
|
107
145
|
r.appendStdout = false;
|
|
@@ -111,8 +149,11 @@ function planRedirects(redirects) {
|
|
|
111
149
|
break;
|
|
112
150
|
case '>>':
|
|
113
151
|
case '&>>':
|
|
114
|
-
if (target
|
|
115
|
-
r.
|
|
152
|
+
if (isNulPath(target)) {
|
|
153
|
+
r.stdoutFile = null;
|
|
154
|
+
if (red.op === '&>>')
|
|
155
|
+
r.stderrFile = null;
|
|
156
|
+
}
|
|
116
157
|
else {
|
|
117
158
|
r.stdoutFile = target;
|
|
118
159
|
r.appendStdout = true;
|
|
@@ -123,11 +164,9 @@ function planRedirects(redirects) {
|
|
|
123
164
|
}
|
|
124
165
|
break;
|
|
125
166
|
case '2>':
|
|
126
|
-
if (target
|
|
127
|
-
//
|
|
167
|
+
if (isNulPath(target)) {
|
|
168
|
+
// stderr only — must not undo a prior >/dev/null
|
|
128
169
|
r.stderrFile = null;
|
|
129
|
-
r.devNull = false;
|
|
130
|
-
r.swallowStderr = true;
|
|
131
170
|
}
|
|
132
171
|
else {
|
|
133
172
|
r.stderrFile = target;
|
|
@@ -135,17 +174,15 @@ function planRedirects(redirects) {
|
|
|
135
174
|
}
|
|
136
175
|
break;
|
|
137
176
|
case '2>>':
|
|
138
|
-
if (target
|
|
177
|
+
if (isNulPath(target)) {
|
|
178
|
+
r.stderrFile = null;
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
139
181
|
r.stderrFile = target;
|
|
140
182
|
r.appendStderr = true;
|
|
141
183
|
}
|
|
142
184
|
break;
|
|
143
|
-
|
|
144
|
-
r.mergeStderr = true;
|
|
145
|
-
break;
|
|
146
|
-
case '1>&2':
|
|
147
|
-
r.mergeStderr = false;
|
|
148
|
-
r.stdoutToStderr = true;
|
|
185
|
+
default:
|
|
149
186
|
break;
|
|
150
187
|
}
|
|
151
188
|
}
|
|
@@ -319,7 +356,9 @@ async function runPlans(plans, session, opts, afterSegment, ensureHost) {
|
|
|
319
356
|
session.prevExit = exitCode;
|
|
320
357
|
break;
|
|
321
358
|
}
|
|
322
|
-
const red = planRedirects(plan.
|
|
359
|
+
const red = planRedirects(plan.outputRedirects);
|
|
360
|
+
const inRed = planRedirects(plan.stdinRedirects);
|
|
361
|
+
red.stdinFile = inRed.stdinFile;
|
|
323
362
|
red.stdinFile = red.stdinFile ? resolveTarget(red.stdinFile) : null;
|
|
324
363
|
red.stdoutFile = red.stdoutFile ? resolveTarget(red.stdoutFile) : null;
|
|
325
364
|
red.stderrFile = red.stderrFile ? resolveTarget(red.stderrFile) : null;
|
|
@@ -334,22 +373,25 @@ async function runPlans(plans, session, opts, afterSegment, ensureHost) {
|
|
|
334
373
|
let redirectPrepFailed = false;
|
|
335
374
|
// Snapshot fd destinations as we walk. `2>&1` copies stdout *at that
|
|
336
375
|
// moment*; a later `>file` must not drag stderr along (bash fd dup).
|
|
337
|
-
let prepStdout = { kind: 'caller' };
|
|
338
|
-
let prepStderr = { kind: 'caller' };
|
|
339
|
-
const emitPrepError = (msg) => emitToPrepDest(prepStderr, msg, prepFds,
|
|
340
|
-
|
|
376
|
+
let prepStdout = { kind: 'caller', fd: 1 };
|
|
377
|
+
let prepStderr = { kind: 'caller', fd: 2 };
|
|
378
|
+
const emitPrepError = (msg) => emitToPrepDest(prepStderr, msg, prepFds, {
|
|
379
|
+
stdout: (s) => {
|
|
380
|
+
stdout += s;
|
|
381
|
+
},
|
|
382
|
+
stderr: (s) => {
|
|
383
|
+
stderr += s;
|
|
384
|
+
},
|
|
341
385
|
});
|
|
342
386
|
for (const r of plan.redirects) {
|
|
343
|
-
if (r.op === '2>&1') {
|
|
344
|
-
prepStderr = prepStdout;
|
|
345
|
-
continue;
|
|
346
|
-
}
|
|
347
|
-
if (r.op === '1>&2') {
|
|
348
|
-
prepStdout = prepStderr;
|
|
387
|
+
if (r.op === '2>&1' || r.op === '1>&2') {
|
|
388
|
+
({ stdout: prepStdout, stderr: prepStderr } = applyRedirectDest(r.op, undefined, prepStdout, prepStderr));
|
|
349
389
|
continue;
|
|
350
390
|
}
|
|
351
391
|
const target = resolveTarget(winTarget(r.target));
|
|
352
392
|
if (r.op === '<') {
|
|
393
|
+
if (isNulPath(target))
|
|
394
|
+
continue;
|
|
353
395
|
if (!existsSync(target)) {
|
|
354
396
|
emitPrepError('bash: ' + target + ': No such file or directory\n');
|
|
355
397
|
redirectPrepFailed = true;
|
|
@@ -357,33 +399,16 @@ async function runPlans(plans, session, opts, afterSegment, ensureHost) {
|
|
|
357
399
|
}
|
|
358
400
|
continue;
|
|
359
401
|
}
|
|
360
|
-
if (target
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
prepStderr = { kind: 'nul' };
|
|
402
|
+
if (!isNulPath(target)) {
|
|
403
|
+
const append = r.op === '>>' || r.op === '2>>' || r.op === '&>>';
|
|
404
|
+
const fail = prepareRedirectFile(target, append, prepFds);
|
|
405
|
+
if (fail) {
|
|
406
|
+
emitPrepError('bash: ' + fail + '\n');
|
|
407
|
+
redirectPrepFailed = true;
|
|
408
|
+
break;
|
|
368
409
|
}
|
|
369
|
-
continue;
|
|
370
|
-
}
|
|
371
|
-
const append = r.op === '>>' || r.op === '2>>' || r.op === '&>>';
|
|
372
|
-
const fail = prepareRedirectFile(target, append, prepFds);
|
|
373
|
-
if (fail) {
|
|
374
|
-
emitPrepError('bash: ' + fail + '\n');
|
|
375
|
-
redirectPrepFailed = true;
|
|
376
|
-
break;
|
|
377
|
-
}
|
|
378
|
-
const fileDest = { kind: 'file', path: target };
|
|
379
|
-
if (r.op === '>' || r.op === '>>')
|
|
380
|
-
prepStdout = fileDest;
|
|
381
|
-
else if (r.op === '2>' || r.op === '2>>')
|
|
382
|
-
prepStderr = fileDest;
|
|
383
|
-
else if (r.op === '&>' || r.op === '&>>') {
|
|
384
|
-
prepStdout = fileDest;
|
|
385
|
-
prepStderr = fileDest;
|
|
386
410
|
}
|
|
411
|
+
({ stdout: prepStdout, stderr: prepStderr } = applyRedirectDest(r.op, target, prepStdout, prepStderr));
|
|
387
412
|
}
|
|
388
413
|
if (redirectPrepFailed) {
|
|
389
414
|
exitCode = 1;
|
|
@@ -406,11 +431,15 @@ async function runPlans(plans, session, opts, afterSegment, ensureHost) {
|
|
|
406
431
|
break;
|
|
407
432
|
}
|
|
408
433
|
const encoded = wrapScript(plan.body, { mode: 'host' });
|
|
434
|
+
// Last-stage fds last-win independently. `2>&1 >/dev/null` snapshots
|
|
435
|
+
// stderr onto the caller's stdout before stdout is pointed at NUL, so
|
|
436
|
+
// captured stderr is still returned; `>/dev/null 2>&1` points both at NUL.
|
|
437
|
+
const applyDests = lastStageOutputDests(plan.outputRedirects, resolveTarget);
|
|
409
438
|
// Response budgets cap what the CALLER receives — streams redirected to
|
|
410
439
|
// files must never be truncated by them (Codex-review P1: `printf … > f`
|
|
411
440
|
// with a small stdoutLimit was writing a clipped file). The final
|
|
412
441
|
// clipUtf8 below still enforces the returned-data budget.
|
|
413
|
-
const fileRedirected =
|
|
442
|
+
const fileRedirected = applyDests.stdout.kind === 'file' || applyDests.stderr.kind === 'file';
|
|
414
443
|
const inv = await ensureHost().invoke(encoded, {
|
|
415
444
|
FAUXNIX_CWD: currentDir,
|
|
416
445
|
FAUXNIX_PREV_EXIT: session.prevExit === null ? '' : String(session.prevExit),
|
|
@@ -438,51 +467,45 @@ async function runPlans(plans, session, opts, afterSegment, ensureHost) {
|
|
|
438
467
|
// GNU line discipline: the PS host terminates Write-Output lines with
|
|
439
468
|
// CRLF. Exact writers (fx-write / printf / echo -n) must keep embedded
|
|
440
469
|
// CR so `printf 'a\r\nb' > out` stays 4 bytes.
|
|
441
|
-
|
|
470
|
+
const segOut = normalizeHostNewlines(decodeOutput(inv.stdout, decodePref));
|
|
442
471
|
let segErr = normalizeHostNewlines(normalizeStderr(decodeOutput(inv.stderr, decodePref)));
|
|
443
472
|
if (inv.timedOut) {
|
|
444
473
|
segErr += timeoutMessage;
|
|
445
474
|
}
|
|
446
|
-
if (red.mergeStderr) {
|
|
447
|
-
segOut += (segOut && !segOut.endsWith('\n') && segErr ? '\n' : '') + segErr;
|
|
448
|
-
segErr = '';
|
|
449
|
-
}
|
|
450
|
-
const stdoutToStderr = red.stdoutToStderr;
|
|
451
|
-
if (stdoutToStderr) {
|
|
452
|
-
segErr += segOut;
|
|
453
|
-
segOut = '';
|
|
454
|
-
}
|
|
455
|
-
const swallowStderr = red.swallowStderr;
|
|
456
|
-
if (swallowStderr)
|
|
457
|
-
segErr = '';
|
|
458
475
|
// Write captured streams through the fds opened during preflight
|
|
459
476
|
// (bash: the redirect refers to the open file, not the path). Reopening
|
|
460
477
|
// the path would recreate a file the command just unlinked
|
|
461
478
|
// (`rm out.txt > out.txt`).
|
|
462
479
|
let redirectOk = true;
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
480
|
+
const deliverCaptured = (dest, data, fromStdout) => {
|
|
481
|
+
if (!data)
|
|
482
|
+
return;
|
|
483
|
+
if (dest.kind === 'nul')
|
|
484
|
+
return;
|
|
485
|
+
if (dest.kind === 'file') {
|
|
486
|
+
try {
|
|
487
|
+
writeToPrepFd(prepFds, dest.path, data);
|
|
488
|
+
}
|
|
489
|
+
catch (e) {
|
|
490
|
+
if (fromStdout) {
|
|
491
|
+
stderr += 'bash: ' + dest.path + ': cannot create: ' + e.message + '\n';
|
|
492
|
+
exitCode = 1;
|
|
493
|
+
redirectOk = false;
|
|
494
|
+
stdout += data;
|
|
495
|
+
}
|
|
496
|
+
else {
|
|
497
|
+
stderr += data;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return;
|
|
482
501
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
502
|
+
if (dest.fd === 1)
|
|
503
|
+
stdout += data;
|
|
504
|
+
else
|
|
505
|
+
stderr += data;
|
|
506
|
+
};
|
|
507
|
+
deliverCaptured(applyDests.stdout, segOut, true);
|
|
508
|
+
deliverCaptured(applyDests.stderr, segErr, false);
|
|
486
509
|
if (inv.truncated)
|
|
487
510
|
truncated = true;
|
|
488
511
|
exitCode = inv.timedOut ? 124 : inv.cancelled ? 130 : inv.exitCode;
|
package/dist/registry.d.ts
CHANGED
|
@@ -99,6 +99,8 @@ export interface CommandSpec {
|
|
|
99
99
|
dispatch?: 'translated' | 'native' | 'dynamic';
|
|
100
100
|
/** GNU usage/syntax exit (grep uses 2; cp/mv/rm use 1). */
|
|
101
101
|
usageExit?: number;
|
|
102
|
+
/** First non-option operand ends option scanning (echo/printf). */
|
|
103
|
+
leadingOptions?: boolean;
|
|
102
104
|
handler: Handler;
|
|
103
105
|
}
|
|
104
106
|
/** Register a spec'd command. Unknown/unsupported options become GNU-style usage errors. */
|
package/dist/registry.js
CHANGED
|
@@ -197,7 +197,8 @@ export function specsMarkdown() {
|
|
|
197
197
|
for (const spec of registeredSpecs()) {
|
|
198
198
|
lines.push('## `' + spec.names.join('` / `') + '`');
|
|
199
199
|
lines.push('');
|
|
200
|
-
lines.push('Effects: ' +
|
|
200
|
+
lines.push('Effects: ' +
|
|
201
|
+
(spec.effects.length ? spec.effects.map((e) => '`' + e + '`').join(', ') : 'none'));
|
|
201
202
|
lines.push('');
|
|
202
203
|
if (!spec.options.length) {
|
|
203
204
|
lines.push('No options declared.');
|
|
@@ -319,6 +320,8 @@ export function specOptionError(spec, args, cmdName) {
|
|
|
319
320
|
i++;
|
|
320
321
|
continue;
|
|
321
322
|
}
|
|
323
|
+
if (spec.leadingOptions)
|
|
324
|
+
onlyOperands = true;
|
|
322
325
|
i++;
|
|
323
326
|
}
|
|
324
327
|
return null;
|
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';
|