fauxnix-cli 0.9.3 → 0.12.0
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 +84 -26
- package/dist/ast.d.ts +38 -3
- package/dist/ast.js +22 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +84 -20
- package/dist/commands/archive.d.ts +2 -1
- package/dist/commands/archive.js +169 -25
- package/dist/commands/install-all.js +4 -2
- package/dist/commands/net.d.ts +9 -0
- package/dist/commands/net.js +51 -15
- package/dist/commands/sysinfo.d.ts +2 -1
- package/dist/commands/sysinfo.js +610 -82
- package/dist/commands/text-filters.d.ts +1 -0
- package/dist/commands/text-filters.js +99 -13
- package/dist/commands/text-io.js +72 -43
- package/dist/doctor.d.ts +21 -0
- package/dist/doctor.js +292 -0
- package/dist/errors.js +14 -4
- package/dist/executor.d.ts +4 -1
- package/dist/executor.js +194 -51
- package/dist/install.d.ts +15 -0
- package/dist/install.js +247 -0
- package/dist/mcp.d.ts +19 -0
- package/dist/mcp.js +49 -26
- package/dist/parser.js +432 -35
- package/dist/powershell.d.ts +22 -0
- package/dist/powershell.js +129 -0
- package/dist/ps-host.d.ts +41 -13
- package/dist/ps-host.js +302 -40
- package/dist/qwen-launch.d.ts +12 -0
- package/dist/qwen-launch.js +29 -0
- package/dist/registry.d.ts +22 -0
- package/dist/registry.js +109 -1
- package/dist/translator.d.ts +42 -9
- package/dist/translator.js +697 -95
- package/package.json +3 -1
package/dist/translator.js
CHANGED
|
@@ -1,7 +1,181 @@
|
|
|
1
|
-
import { FauxnixParseError, isUnquotedLiteral, } from './ast.js';
|
|
1
|
+
import { FauxnixParseError, isUnquotedLiteral, wordToString, } from './ast.js';
|
|
2
2
|
import { parseCommand } from './parser.js';
|
|
3
3
|
import { lookup, psStr } from './registry.js';
|
|
4
4
|
import { PYTHON3_WINDOWS_HINT, SH_SCRIPT_WINDOWS_HINT } from './errors.js';
|
|
5
|
+
export const EXECUTE_TRANSLATION = Object.freeze({ mode: 'execute' });
|
|
6
|
+
export const PURE_TRANSLATION = Object.freeze({ mode: 'pure' });
|
|
7
|
+
export const PURE_SED_FILE_MESSAGE = 'fauxnix: translate does not read sed script files; use -e with the script text, or run the command to use -f';
|
|
8
|
+
/** Match the sed option rules without opening the referenced script file. */
|
|
9
|
+
function sedUsesScriptFile(args) {
|
|
10
|
+
const raw = args.map((word) => wordToString(word));
|
|
11
|
+
let onlyOperands = false;
|
|
12
|
+
for (let i = 0; i < raw.length; i++) {
|
|
13
|
+
const arg = raw[i];
|
|
14
|
+
if (!onlyOperands && arg === '--') {
|
|
15
|
+
onlyOperands = true;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (onlyOperands || !arg.startsWith('-') || arg.length === 1 || arg.startsWith('--')) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const body = arg.slice(1);
|
|
22
|
+
for (let c = 0; c < body.length; c++) {
|
|
23
|
+
const flag = body[c];
|
|
24
|
+
if (flag === 'f')
|
|
25
|
+
return c < body.length - 1 || i + 1 < raw.length;
|
|
26
|
+
if (flag === 'e') {
|
|
27
|
+
if (c === body.length - 1)
|
|
28
|
+
i++;
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
if (flag === 'i')
|
|
32
|
+
break;
|
|
33
|
+
if (!['n', 'E', 'r', 's', 'u', 'z'].includes(flag))
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
function assertPureWord(word) {
|
|
40
|
+
const visitPart = (part) => {
|
|
41
|
+
if (part.kind === 'CmdSub') {
|
|
42
|
+
assertPureCommandList(parseCommand(part.cmd));
|
|
43
|
+
}
|
|
44
|
+
else if (part.kind === 'DoubleQuoted' || part.kind === 'Arith') {
|
|
45
|
+
for (const nested of part.parts)
|
|
46
|
+
visitPart(nested);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
for (const part of word)
|
|
50
|
+
visitPart(part);
|
|
51
|
+
}
|
|
52
|
+
function wrappedSimpleCommand(command, name) {
|
|
53
|
+
const raw = command.args.map((word) => wordToString(word));
|
|
54
|
+
let commandIndex = -1;
|
|
55
|
+
if (name === 'env') {
|
|
56
|
+
for (let i = 0; i < raw.length;) {
|
|
57
|
+
const arg = raw[i];
|
|
58
|
+
if (arg === '--') {
|
|
59
|
+
commandIndex = i + 1;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
if (arg === '-i' || arg === '--ignore-environment')
|
|
63
|
+
return null;
|
|
64
|
+
if (arg === '-u' || arg === '--unset') {
|
|
65
|
+
i += 2;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (arg.startsWith('-u=') || arg.startsWith('--unset=')) {
|
|
69
|
+
i++;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (arg.startsWith('-')) {
|
|
73
|
+
i++;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(arg)) {
|
|
77
|
+
i++;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
commandIndex = i;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else if (name === 'command') {
|
|
85
|
+
let identify = false;
|
|
86
|
+
let i = 0;
|
|
87
|
+
while (i < raw.length) {
|
|
88
|
+
const arg = raw[i];
|
|
89
|
+
if (arg === '-v' || arg === '-V') {
|
|
90
|
+
identify = true;
|
|
91
|
+
i++;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (arg === '--') {
|
|
95
|
+
i++;
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
if (arg.startsWith('-')) {
|
|
99
|
+
i++;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
if (identify)
|
|
105
|
+
return null;
|
|
106
|
+
commandIndex = i;
|
|
107
|
+
}
|
|
108
|
+
else if (name === 'timeout') {
|
|
109
|
+
let i = 0;
|
|
110
|
+
while (i < raw.length && raw[i].startsWith('-') && raw[i] !== '-' && raw[i] !== '--')
|
|
111
|
+
i++;
|
|
112
|
+
if (i < raw.length && raw[i] === '--')
|
|
113
|
+
i++;
|
|
114
|
+
commandIndex = i + 1;
|
|
115
|
+
}
|
|
116
|
+
if (commandIndex < 0 || commandIndex >= command.args.length)
|
|
117
|
+
return null;
|
|
118
|
+
return {
|
|
119
|
+
kind: 'SimpleCommand',
|
|
120
|
+
assignments: [],
|
|
121
|
+
name: command.args[commandIndex],
|
|
122
|
+
args: command.args.slice(commandIndex + 1),
|
|
123
|
+
redirects: [],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function assertPureShellCommand(command) {
|
|
127
|
+
if (command.kind === 'SimpleCommand') {
|
|
128
|
+
const name = command.name === null ? null : literalOfWord(command.name);
|
|
129
|
+
if (name === 'sed' && sedUsesScriptFile(command.args)) {
|
|
130
|
+
throw new FauxnixParseError(PURE_SED_FILE_MESSAGE);
|
|
131
|
+
}
|
|
132
|
+
if (command.name)
|
|
133
|
+
assertPureWord(command.name);
|
|
134
|
+
for (const arg of command.args)
|
|
135
|
+
assertPureWord(arg);
|
|
136
|
+
for (const assignment of command.assignments) {
|
|
137
|
+
assertPureWord(assignment.value);
|
|
138
|
+
for (const value of assignment.values ?? [])
|
|
139
|
+
assertPureWord(value);
|
|
140
|
+
}
|
|
141
|
+
if (name === 'env' || name === 'command' || name === 'timeout') {
|
|
142
|
+
const nested = wrappedSimpleCommand(command, name);
|
|
143
|
+
if (nested)
|
|
144
|
+
assertPureShellCommand(nested);
|
|
145
|
+
}
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (command.kind === 'If') {
|
|
149
|
+
assertPureCommandList(command.test);
|
|
150
|
+
assertPureCommandList(command.then);
|
|
151
|
+
if (command.else)
|
|
152
|
+
assertPureCommandList(command.else);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (command.kind === 'For') {
|
|
156
|
+
for (const word of command.words)
|
|
157
|
+
assertPureWord(word);
|
|
158
|
+
assertPureCommandList(command.body);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (command.kind === 'While') {
|
|
162
|
+
assertPureCommandList(command.test);
|
|
163
|
+
assertPureCommandList(command.body);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
assertPureWord(command.word);
|
|
167
|
+
for (const arm of command.arms) {
|
|
168
|
+
for (const pattern of arm.patterns)
|
|
169
|
+
assertPureWord(pattern);
|
|
170
|
+
assertPureCommandList(arm.body);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function assertPureCommandList(list) {
|
|
174
|
+
for (const segment of list.segments) {
|
|
175
|
+
for (const command of segment.pipeline.commands)
|
|
176
|
+
assertPureShellCommand(command);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
5
179
|
/* ------------------------------------------------------------------ */
|
|
6
180
|
/* Variable mapping */
|
|
7
181
|
/* ------------------------------------------------------------------ */
|
|
@@ -39,8 +213,43 @@ export function paramExpr(name, op, word) {
|
|
|
39
213
|
psStr('bash: ' + msg) +
|
|
40
214
|
'); $script:fx_exit = 1; \'\' } else { $fx_pv } )');
|
|
41
215
|
}
|
|
216
|
+
function sliceArgExpr(s) {
|
|
217
|
+
if (s.length > 0 && s[0] === '$') {
|
|
218
|
+
return '(fx-scalar0 ' + psStr(s.slice(1)) + ')';
|
|
219
|
+
}
|
|
220
|
+
return psStr(s);
|
|
221
|
+
}
|
|
222
|
+
export function varExtraOf(p) {
|
|
223
|
+
if (p.kind !== 'Var')
|
|
224
|
+
return undefined;
|
|
225
|
+
if (!p.replace && !p.slice)
|
|
226
|
+
return undefined;
|
|
227
|
+
const extra = {};
|
|
228
|
+
if (p.replace)
|
|
229
|
+
extra.replace = p.replace;
|
|
230
|
+
if (p.slice)
|
|
231
|
+
extra.slice = p.slice;
|
|
232
|
+
return extra;
|
|
233
|
+
}
|
|
42
234
|
/** Map a bash $VAR name to a PowerShell expression (usable inside $(...)). */
|
|
43
|
-
export function varExpr(name, index, param, length = false) {
|
|
235
|
+
export function varExpr(name, index, param, length = false, extra) {
|
|
236
|
+
if (extra && extra.replace) {
|
|
237
|
+
const r = extra.replace;
|
|
238
|
+
return ('(fx-subst (fx-scalar0 ' +
|
|
239
|
+
psStr(name) +
|
|
240
|
+
') ' +
|
|
241
|
+
psStr(r.pat) +
|
|
242
|
+
' ' +
|
|
243
|
+
psStr(r.repl) +
|
|
244
|
+
' ' +
|
|
245
|
+
(r.global ? '$true' : '$false') +
|
|
246
|
+
')');
|
|
247
|
+
}
|
|
248
|
+
if (extra && extra.slice) {
|
|
249
|
+
const off = sliceArgExpr(extra.slice.offset);
|
|
250
|
+
const len = extra.slice.length !== undefined ? sliceArgExpr(extra.slice.length) : '$null';
|
|
251
|
+
return '(fx-slice (fx-scalar0 ' + psStr(name) + ') ' + off + ' ' + len + ')';
|
|
252
|
+
}
|
|
44
253
|
if (param)
|
|
45
254
|
return paramExpr(name, param.op, param.word);
|
|
46
255
|
if (length) {
|
|
@@ -60,6 +269,12 @@ export function varExpr(name, index, param, length = false) {
|
|
|
60
269
|
if (index !== undefined) {
|
|
61
270
|
return '(fx-subget ' + psStr(name) + ' ' + psStr(index) + ')';
|
|
62
271
|
}
|
|
272
|
+
if (/^[0-9]+$/.test(name)) {
|
|
273
|
+
if (name === '0') {
|
|
274
|
+
return "$(if ($env:FAUXNIX_ARG0) { [string]$env:FAUXNIX_ARG0 } else { 'fauxnix' })";
|
|
275
|
+
}
|
|
276
|
+
return '(fx-posget ' + name + ')';
|
|
277
|
+
}
|
|
63
278
|
switch (name) {
|
|
64
279
|
case 'HOME':
|
|
65
280
|
return '$HOME';
|
|
@@ -82,10 +297,24 @@ export function varExpr(name, index, param, length = false) {
|
|
|
82
297
|
return '[string]$PID';
|
|
83
298
|
case 'HOSTNAME':
|
|
84
299
|
return '$env:COMPUTERNAME';
|
|
300
|
+
case '#':
|
|
301
|
+
return '@(fx-posload).Count';
|
|
302
|
+
case '@':
|
|
303
|
+
case '*':
|
|
304
|
+
return '((@(fx-posload) -join (fx-ifs1)))';
|
|
85
305
|
default:
|
|
86
306
|
return '$env:' + name;
|
|
87
307
|
}
|
|
88
308
|
}
|
|
309
|
+
/** `$?` `$$` `$0`–`$n` `$#` `$@` `$*` — not ordinary `$env:` names. */
|
|
310
|
+
export function isSpecialShellVar(name) {
|
|
311
|
+
return (name === '?' ||
|
|
312
|
+
name === '$' ||
|
|
313
|
+
name === '#' ||
|
|
314
|
+
name === '@' ||
|
|
315
|
+
name === '*' ||
|
|
316
|
+
/^[0-9]+$/.test(name));
|
|
317
|
+
}
|
|
89
318
|
/* ------------------------------------------------------------------ */
|
|
90
319
|
/* Word → PowerShell expression */
|
|
91
320
|
/* ------------------------------------------------------------------ */
|
|
@@ -155,7 +384,7 @@ export function exprOfWord(w, opts) {
|
|
|
155
384
|
// single bare variable → bare expression
|
|
156
385
|
if (expanded.length === 1 && expanded[0].kind === 'Var') {
|
|
157
386
|
const v = expanded[0];
|
|
158
|
-
return varExpr(v.name, v.index, v.param, v.length === true);
|
|
387
|
+
return varExpr(v.name, v.index, v.param, v.length === true, varExtraOf(v));
|
|
159
388
|
}
|
|
160
389
|
// Bare `$(...)` must not sit inside a PS expandable string: the
|
|
161
390
|
// substitution body contains `"` / `$_` that would break interpolation.
|
|
@@ -185,7 +414,7 @@ export function exprOfWord(w, opts) {
|
|
|
185
414
|
emitPart(q, true);
|
|
186
415
|
break;
|
|
187
416
|
case 'Var':
|
|
188
|
-
out += '$(' + varExpr(p.name, p.index, p.param, p.length === true) + ')';
|
|
417
|
+
out += '$(' + varExpr(p.name, p.index, p.param, p.length === true, varExtraOf(p)) + ')';
|
|
189
418
|
break;
|
|
190
419
|
case 'CmdSub':
|
|
191
420
|
out += '$(' + translateCmdSub(p.cmd, quoted || opts?.preserveCmdSub === true) + ')';
|
|
@@ -241,7 +470,7 @@ function arithSourceExpr(parts) {
|
|
|
241
470
|
emit(q);
|
|
242
471
|
break;
|
|
243
472
|
case 'Var':
|
|
244
|
-
out += '$(' + varExpr(p.name, p.index, p.param, p.length === true) + ')';
|
|
473
|
+
out += '$(' + varExpr(p.name, p.index, p.param, p.length === true, varExtraOf(p)) + ')';
|
|
245
474
|
break;
|
|
246
475
|
case 'CmdSub':
|
|
247
476
|
out += '$(' + translateCmdSub(p.cmd, true) + ')';
|
|
@@ -292,6 +521,7 @@ function wordPartsForSplat(w) {
|
|
|
292
521
|
/**
|
|
293
522
|
* `${name[@]}` / `"pre${name[@]}post"` — one argv per element.
|
|
294
523
|
* Unquoted `${name[*]}` also splats (bash); quoted `"${name[*]}"` stays one join.
|
|
524
|
+
* `$@` / unquoted `$*` splat like `${arr[@]}`; quoted `"$@"` still splats.
|
|
295
525
|
*/
|
|
296
526
|
export function splatSpec(w) {
|
|
297
527
|
const parts = wordPartsForSplat(w);
|
|
@@ -302,7 +532,10 @@ export function splatSpec(w) {
|
|
|
302
532
|
for (const { part: p, quoted } of parts) {
|
|
303
533
|
const splat = p.kind === 'Var' &&
|
|
304
534
|
!p.length &&
|
|
305
|
-
(p.
|
|
535
|
+
(p.name === '@' ||
|
|
536
|
+
(p.name === '*' && !quoted) ||
|
|
537
|
+
p.index === '@' ||
|
|
538
|
+
(p.index === '*' && !quoted));
|
|
306
539
|
if (splat) {
|
|
307
540
|
if (seen)
|
|
308
541
|
return null;
|
|
@@ -319,6 +552,12 @@ export function splatSpec(w) {
|
|
|
319
552
|
}
|
|
320
553
|
return name ? { name, prefix, suffix } : null;
|
|
321
554
|
}
|
|
555
|
+
/** Load the splat source: positionals (`@`/`*`) vs named arrays. */
|
|
556
|
+
function splatLoadCall(name) {
|
|
557
|
+
if (name === '@' || name === '*')
|
|
558
|
+
return 'fx-posload';
|
|
559
|
+
return 'fx-arrload ' + psStr(name);
|
|
560
|
+
}
|
|
322
561
|
/** PS expression of a string[]: `@` words splat, others stay one element. */
|
|
323
562
|
export function argListExpr(words, fn = exprOfWord) {
|
|
324
563
|
if (words.length === 0)
|
|
@@ -330,9 +569,9 @@ export function argListExpr(words, fn = exprOfWord) {
|
|
|
330
569
|
if (!s)
|
|
331
570
|
return '@(' + fn(w) + ')';
|
|
332
571
|
if (!s.prefix && !s.suffix)
|
|
333
|
-
return '@(
|
|
334
|
-
return ('@($( $fx_sp = @(
|
|
335
|
-
|
|
572
|
+
return '@(' + splatLoadCall(s.name) + ')';
|
|
573
|
+
return ('@($( $fx_sp = @(' +
|
|
574
|
+
splatLoadCall(s.name) +
|
|
336
575
|
'); if ($fx_sp.Count -eq 0) { $fx_sp = @(' +
|
|
337
576
|
psStr(s.prefix + s.suffix) +
|
|
338
577
|
') } else { $fx_sp[0] = ' +
|
|
@@ -353,14 +592,11 @@ export function argListExpr(words, fn = exprOfWord) {
|
|
|
353
592
|
* Unquoted command words join non-empty lines with a space (IFS
|
|
354
593
|
* word-split approximation). Handlers often emit one string object, so
|
|
355
594
|
* a bare `$(…)` interpolation would keep those newlines.
|
|
595
|
+
* Lists (`;` `&&` `||`) reuse translateListInline inside the fx-csub
|
|
596
|
+
* scriptblock so the newline contract is unchanged.
|
|
356
597
|
*/
|
|
357
|
-
export function translateCmdSub(cmdText, keepNl = false) {
|
|
358
|
-
const
|
|
359
|
-
if (list.segments.length !== 1) {
|
|
360
|
-
throw new FauxnixParseError('fauxnix: command substitution with ; && || is not supported yet');
|
|
361
|
-
}
|
|
362
|
-
const { defs, call } = translatePipelineBody(list.segments[0].pipeline);
|
|
363
|
-
const inner = defs ? defs + '\n' + call : call;
|
|
598
|
+
export function translateCmdSub(cmdText, keepNl = false, translation = EXECUTE_TRANSLATION) {
|
|
599
|
+
const inner = translateListInline(parseCommand(cmdText), translation);
|
|
364
600
|
const collected = '(fx-csub { ' + inner + ' })';
|
|
365
601
|
if (keepNl)
|
|
366
602
|
return collected;
|
|
@@ -377,18 +613,32 @@ function indentBlock(s) {
|
|
|
377
613
|
.map((l) => (l ? ' ' + l : l))
|
|
378
614
|
.join('\n');
|
|
379
615
|
}
|
|
380
|
-
export function translateSimple(cmd, position, hasStdin) {
|
|
616
|
+
export function translateSimple(cmd, position, hasStdin, translation = EXECUTE_TRANSLATION) {
|
|
617
|
+
const nativeTerm = "(-not $script:fx_csub -and (($MyInvocation.MyCommand.Name -eq '') -or " +
|
|
618
|
+
(position === 'last' ? '$true' : '$false') +
|
|
619
|
+
'))';
|
|
381
620
|
// assignment-only segment (`X=1; cmd`): bash semantics are "set for the
|
|
382
621
|
// rest of the shell". Reuse the export code path — persist + env shadow —
|
|
383
622
|
// so empty values (`X=`) and `[[ -v X ]]` behave like bash (documented
|
|
384
623
|
// deviation: shell var vs exported var are indistinguishable here).
|
|
385
624
|
if (cmd.name === null) {
|
|
386
625
|
const exportHandler = lookup('export');
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
626
|
+
const chunks = [];
|
|
627
|
+
for (const a of cmd.assignments) {
|
|
628
|
+
if (a.values) {
|
|
629
|
+
chunks.push('fx-arrput ' +
|
|
630
|
+
psStr(a.name) +
|
|
631
|
+
' ' +
|
|
632
|
+
argListExpr(a.values, (w) => exprOfWord(w, { preserveCmdSub: true })));
|
|
633
|
+
}
|
|
634
|
+
else {
|
|
635
|
+
const words = [[{ kind: 'Text', text: a.name + '=' }, ...a.value]];
|
|
636
|
+
if (exportHandler) {
|
|
637
|
+
chunks.push(exportHandler(words, { position, hasStdin, translationMode: translation.mode }));
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return chunks.join('\n');
|
|
392
642
|
}
|
|
393
643
|
const nameLit = literalOfWord(cmd.name);
|
|
394
644
|
const nameSplat = splatSpec(cmd.name);
|
|
@@ -403,9 +653,9 @@ export function translateSimple(cmd, position, hasStdin) {
|
|
|
403
653
|
name: cmd.args[0],
|
|
404
654
|
args: cmd.args.slice(1),
|
|
405
655
|
redirects: cmd.redirects,
|
|
406
|
-
}, position, hasStdin);
|
|
656
|
+
}, position, hasStdin, translation);
|
|
407
657
|
const emptyCmdLines = [
|
|
408
|
-
'$fx_cw = @(
|
|
658
|
+
'$fx_cw = @(' + splatLoadCall(nameSplat.name) + ')',
|
|
409
659
|
];
|
|
410
660
|
if (hasAffix) {
|
|
411
661
|
emptyCmdLines.push('if ($fx_cw.Count -eq 0) { $fx_cw = @(' +
|
|
@@ -419,20 +669,23 @@ export function translateSimple(cmd, position, hasStdin) {
|
|
|
419
669
|
emptyCmdLines.push('if ($fx_cw.Count -eq 0) {',
|
|
420
670
|
// No words left → bash null command (exit 0). Remaining words are
|
|
421
671
|
// known at compile time, so reuse translateSimple (handlers, not `&`).
|
|
422
|
-
promoted ? indentBlock(promoted) : ' ', '} else {', ' $fx_na = [object[]](' + argListExpr(cmd.args) + ')', ' $fx_cmd = [string]$fx_cw[0]', ' if ($fx_cw.Count -gt 1) { $fx_na = [object[]](@($fx_cw[1..($fx_cw.Count - 1)]) + $fx_na) }', ' ' +
|
|
672
|
+
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) }', ' ' +
|
|
673
|
+
(hasStdin
|
|
674
|
+
? '($input | fx-native $fx_cmd $fx_na ' + nativeTerm + ')'
|
|
675
|
+
: 'fx-native $fx_cmd $fx_na ' + nativeTerm), '}');
|
|
423
676
|
body = emptyCmdLines.join('\n');
|
|
424
677
|
}
|
|
425
678
|
else if (nameLit !== null) {
|
|
426
679
|
const handler = lookup(nameLit);
|
|
427
680
|
if (handler && !(nameLit === '[[' && !isUnquotedLiteral(cmd.name, '[['))) {
|
|
428
|
-
body = handler(cmd.args, { position, hasStdin });
|
|
681
|
+
body = handler(cmd.args, { position, hasStdin, translationMode: translation.mode });
|
|
429
682
|
}
|
|
430
683
|
else {
|
|
431
684
|
// passthrough: native command (git, node, npm, python, cargo, ...)
|
|
432
685
|
// via fx-native (Win32 command line + Process). `& name @array` on
|
|
433
686
|
// PS 5.1 drops empty argv entries and eats embedded quotes.
|
|
434
687
|
const nameExpr = psStr(nameLit);
|
|
435
|
-
const invoke = 'fx-native ' + nameExpr + ' $fx_na';
|
|
688
|
+
const invoke = 'fx-native ' + nameExpr + ' $fx_na ' + nativeTerm;
|
|
436
689
|
body = [
|
|
437
690
|
'$fx_na = [object[]](' + argListExpr(cmd.args) + ')',
|
|
438
691
|
(hasStdin ? '($input | ' + invoke + ')' : invoke),
|
|
@@ -442,7 +695,7 @@ export function translateSimple(cmd, position, hasStdin) {
|
|
|
442
695
|
else {
|
|
443
696
|
// dynamic command name — evaluate it
|
|
444
697
|
const nameExpr = exprOfWord(cmd.name);
|
|
445
|
-
const invoke = 'fx-native (' + nameExpr + ') $fx_na';
|
|
698
|
+
const invoke = 'fx-native (' + nameExpr + ') $fx_na ' + nativeTerm;
|
|
446
699
|
body = [
|
|
447
700
|
'$fx_na = [object[]](' + argListExpr(cmd.args) + ')',
|
|
448
701
|
(hasStdin ? '($input | ' + invoke + ')' : invoke),
|
|
@@ -547,10 +800,20 @@ export function wrapTempEnv(sets, body, extra) {
|
|
|
547
800
|
lines.push(arrSave + '[' + psStr(n) + '] = (fx-arrpackget ' + psStr(n) + ')');
|
|
548
801
|
}
|
|
549
802
|
const valVars = [];
|
|
803
|
+
const valIsArr = [];
|
|
550
804
|
for (let i = 0; i < sets.length; i++) {
|
|
551
805
|
const vn = '$fx_ev' + id + '_' + i;
|
|
552
806
|
valVars.push(vn);
|
|
553
|
-
|
|
807
|
+
if (sets[i].values) {
|
|
808
|
+
valIsArr.push(true);
|
|
809
|
+
lines.push(vn +
|
|
810
|
+
' = ' +
|
|
811
|
+
argListExpr(sets[i].values, (w) => exprOfWord(w, { preserveCmdSub: true })));
|
|
812
|
+
}
|
|
813
|
+
else {
|
|
814
|
+
valIsArr.push(false);
|
|
815
|
+
lines.push(vn + ' = ' + exprOfWord(sets[i].value, { preserveCmdSub: true }));
|
|
816
|
+
}
|
|
554
817
|
}
|
|
555
818
|
lines.push('try {');
|
|
556
819
|
for (const u of unsets) {
|
|
@@ -576,6 +839,10 @@ export function wrapTempEnv(sets, body, extra) {
|
|
|
576
839
|
for (let i = 0; i < sets.length; i++) {
|
|
577
840
|
const n = sets[i].name;
|
|
578
841
|
const nq = n.replace(/'/g, "''");
|
|
842
|
+
if (valIsArr[i]) {
|
|
843
|
+
lines.push(' fx-arrput ' + psStr(n) + ' ' + valVars[i]);
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
579
846
|
lines.push(' $env:' + n + ' = ' + valVars[i]);
|
|
580
847
|
lines.push(' fx-arrdrop ' + psStr(n));
|
|
581
848
|
lines.push(" $env:FAUXNIX_SETVARS = ((@($env:FAUXNIX_SETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne '" +
|
|
@@ -721,10 +988,10 @@ let pipelineSeq = 0;
|
|
|
721
988
|
* multi-command pipelines become generated functions chained with `|`
|
|
722
989
|
* (PS 5.1 forbids parenthesized expressions as non-first pipeline elements).
|
|
723
990
|
*/
|
|
724
|
-
function translateListInline(list) {
|
|
991
|
+
function translateListInline(list, translation = EXECUTE_TRANSLATION) {
|
|
725
992
|
const chunks = [];
|
|
726
993
|
for (const seg of list.segments) {
|
|
727
|
-
const { defs, call } = translatePipelineBody(seg.pipeline);
|
|
994
|
+
const { defs, call } = translatePipelineBody(seg.pipeline, translation);
|
|
728
995
|
const body = (defs ? defs + '\n' : '') + call;
|
|
729
996
|
if (seg.op === '&&') {
|
|
730
997
|
chunks.push('if ($script:fx_exit -eq 0) {\n' + body + '\n}');
|
|
@@ -738,22 +1005,38 @@ function translateListInline(list) {
|
|
|
738
1005
|
}
|
|
739
1006
|
return chunks.join('\n');
|
|
740
1007
|
}
|
|
741
|
-
function translateIf(cmd) {
|
|
1008
|
+
function translateIf(cmd, translation) {
|
|
742
1009
|
// Branch bodies reset fx_exit first: the compound's exit status must come
|
|
743
1010
|
// from the taken branch's last command (bash semantics), not leak the test's
|
|
744
1011
|
// failure — `if false; then A; else B; fi` exits 0 in bash.
|
|
745
|
-
const lines = [translateListInline(cmd.test), 'if ($script:fx_exit -eq 0) {', ' $script:fx_exit = 0'];
|
|
746
|
-
for (const l of translateListInline(cmd.then).split('\n'))
|
|
1012
|
+
const lines = [translateListInline(cmd.test, translation), 'if ($script:fx_exit -eq 0) {', ' $script:fx_exit = 0'];
|
|
1013
|
+
for (const l of translateListInline(cmd.then, translation).split('\n'))
|
|
747
1014
|
lines.push(l ? ' ' + l : l);
|
|
748
1015
|
lines.push('} else {', ' $script:fx_exit = 0');
|
|
749
1016
|
if (cmd.else) {
|
|
750
|
-
for (const l of translateListInline(cmd.else).split('\n'))
|
|
1017
|
+
for (const l of translateListInline(cmd.else, translation).split('\n'))
|
|
751
1018
|
lines.push(l ? ' ' + l : l);
|
|
752
1019
|
}
|
|
753
1020
|
lines.push('}');
|
|
754
1021
|
return lines.join('\n');
|
|
755
1022
|
}
|
|
756
|
-
function
|
|
1023
|
+
function translateCase(cmd, translation) {
|
|
1024
|
+
const lines = ['$script:fx_exit = 0', '$fx_cw = ' + exprOfWord(cmd.word)];
|
|
1025
|
+
for (let i = 0; i < cmd.arms.length; i++) {
|
|
1026
|
+
const arm = cmd.arms[i];
|
|
1027
|
+
const pats = arm.patterns.map((w) => exprOfWord(w)).join(',');
|
|
1028
|
+
const head = i === 0 ? 'if' : 'elseif';
|
|
1029
|
+
lines.push(head + ' (fx-casematch $fx_cw @(' + pats + ')) {');
|
|
1030
|
+
const body = translateListInline(arm.body, translation);
|
|
1031
|
+
if (body) {
|
|
1032
|
+
for (const l of body.split('\n'))
|
|
1033
|
+
lines.push(l ? ' ' + l : l);
|
|
1034
|
+
}
|
|
1035
|
+
lines.push('}');
|
|
1036
|
+
}
|
|
1037
|
+
return lines.join('\n');
|
|
1038
|
+
}
|
|
1039
|
+
function translateFor(cmd, translation) {
|
|
757
1040
|
const n = cmd.name.replace(/'/g, "''");
|
|
758
1041
|
const lines = [
|
|
759
1042
|
'$fx_for = ' + argListExpr(cmd.words),
|
|
@@ -775,11 +1058,27 @@ function translateFor(cmd) {
|
|
|
775
1058
|
n +
|
|
776
1059
|
"' + [string][char]61 + (fx-svenc ([string]$fx_it))); $env:FAUXNIX_SETVALS = ($fx_sv -join [string][char]10)",
|
|
777
1060
|
];
|
|
778
|
-
for (const l of translateListInline(cmd.body).split('\n'))
|
|
1061
|
+
for (const l of translateListInline(cmd.body, translation).split('\n'))
|
|
779
1062
|
lines.push(l ? ' ' + l : l);
|
|
780
1063
|
lines.push('}');
|
|
781
1064
|
return lines.join('\n');
|
|
782
1065
|
}
|
|
1066
|
+
function translateWhile(cmd, translation) {
|
|
1067
|
+
// Bash: last executed body owns status; a test that ends the loop does not.
|
|
1068
|
+
// Never-entered loops (`while false; do …; done`, `until true; do …; done`)
|
|
1069
|
+
// exit 0. Save the body status and restore it on the failing test.
|
|
1070
|
+
const fail = cmd.until ? '$script:fx_exit -eq 0' : '$script:fx_exit -ne 0';
|
|
1071
|
+
const lines = ['$fx_wst = 0', 'do {'];
|
|
1072
|
+
for (const l of translateListInline(cmd.test, translation).split('\n'))
|
|
1073
|
+
lines.push(l ? ' ' + l : l);
|
|
1074
|
+
lines.push(' if (' + fail + ') { $script:fx_exit = $fx_wst; break }');
|
|
1075
|
+
lines.push(' $script:fx_exit = 0');
|
|
1076
|
+
for (const l of translateListInline(cmd.body, translation).split('\n'))
|
|
1077
|
+
lines.push(l ? ' ' + l : l);
|
|
1078
|
+
lines.push(' $fx_wst = $script:fx_exit');
|
|
1079
|
+
lines.push('} while ($true)');
|
|
1080
|
+
return lines.join('\n');
|
|
1081
|
+
}
|
|
783
1082
|
function stdinTarget(c) {
|
|
784
1083
|
let t = null;
|
|
785
1084
|
for (const r of c.redirects) {
|
|
@@ -794,7 +1093,46 @@ function stdinReadExpr(target) {
|
|
|
794
1093
|
return '@()';
|
|
795
1094
|
return '@(fx-readlines ' + pathExpr(n) + ')';
|
|
796
1095
|
}
|
|
797
|
-
|
|
1096
|
+
/** `>` `>>` `&>` `&>>` — Node last-stage apply cannot honor these on earlier stages. */
|
|
1097
|
+
function isStdoutFileRedirect(op) {
|
|
1098
|
+
return op === '>' || op === '>>' || op === '&>' || op === '&>>';
|
|
1099
|
+
}
|
|
1100
|
+
const NONLAST_STDOUT_REDIRECT_MSG = 'fauxnix: stdout redirect on a non-last pipeline stage is not supported yet; write the file in a previous list segment (cmd >f; cat f) or wait for per-stage fds (#157)';
|
|
1101
|
+
const NONLAST_STDERR_FILE_REDIRECT_MSG = 'fauxnix: stderr redirect ({op}) on a non-last pipeline stage is not supported yet; spool the stage first (cmd >out {op}err; cat out | next) or wait for per-stage fds (#157)';
|
|
1102
|
+
function nonLastFdRedirectMessage(op) {
|
|
1103
|
+
if (isStdoutFileRedirect(op))
|
|
1104
|
+
return NONLAST_STDOUT_REDIRECT_MSG;
|
|
1105
|
+
if (op === '2>' || op === '2>>') {
|
|
1106
|
+
return NONLAST_STDERR_FILE_REDIRECT_MSG.replaceAll('{op}', op);
|
|
1107
|
+
}
|
|
1108
|
+
if (op === '2>&1') {
|
|
1109
|
+
return 'fauxnix: 2>&1 on a non-last pipeline stage is not supported yet; spool the merged output first (cmd >out 2>&1; cat out | next) or wait for per-stage fds (#157)';
|
|
1110
|
+
}
|
|
1111
|
+
if (op === '1>&2') {
|
|
1112
|
+
return 'fauxnix: 1>&2 on a non-last pipeline stage is not supported yet; run the stage separately (cmd 1>&2; next </dev/null) or wait for per-stage fds (#157)';
|
|
1113
|
+
}
|
|
1114
|
+
return null;
|
|
1115
|
+
}
|
|
1116
|
+
function rejectNonLastFdRedirects(commands) {
|
|
1117
|
+
if (commands.length < 2)
|
|
1118
|
+
return;
|
|
1119
|
+
for (let i = 0; i < commands.length - 1; i++) {
|
|
1120
|
+
for (const redirect of commands[i].redirects) {
|
|
1121
|
+
const message = nonLastFdRedirectMessage(redirect.op);
|
|
1122
|
+
if (message)
|
|
1123
|
+
throw new FauxnixParseError(message);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
export function translatePipelineBody(p, translation = EXECUTE_TRANSLATION) {
|
|
1128
|
+
if (translation.mode === 'pure') {
|
|
1129
|
+
for (const command of p.commands)
|
|
1130
|
+
assertPureShellCommand(command);
|
|
1131
|
+
}
|
|
1132
|
+
// Last-stage output fds are applied by Node. On an earlier stage they would
|
|
1133
|
+
// be prepared but not owned by that stage, so output would still reach the
|
|
1134
|
+
// wrong destination. Fail loud until routed in-stage fds land (#157).
|
|
1135
|
+
rejectNonLastFdRedirects(p.commands);
|
|
798
1136
|
// Every pipeline stage needs its own status slot. Handlers deliberately use
|
|
799
1137
|
// `$script:fx_exit` because their helper functions run in child scopes; in a
|
|
800
1138
|
// pipeline that shared flag lets an earlier failure leak into a successful
|
|
@@ -807,11 +1145,15 @@ export function translatePipelineBody(p) {
|
|
|
807
1145
|
const hasStdin = i > 0 || c.redirects.some((r) => r.op === '<');
|
|
808
1146
|
const position = i === 0 ? 'first' : i === p.commands.length - 1 ? 'last' : 'middle';
|
|
809
1147
|
if (c.kind === 'If')
|
|
810
|
-
bodies.push(translateIf(c));
|
|
1148
|
+
bodies.push(translateIf(c, translation));
|
|
811
1149
|
else if (c.kind === 'For')
|
|
812
|
-
bodies.push(translateFor(c));
|
|
1150
|
+
bodies.push(translateFor(c, translation));
|
|
1151
|
+
else if (c.kind === 'While')
|
|
1152
|
+
bodies.push(translateWhile(c, translation));
|
|
1153
|
+
else if (c.kind === 'Case')
|
|
1154
|
+
bodies.push(translateCase(c, translation));
|
|
813
1155
|
else
|
|
814
|
-
bodies.push(translateSimple(c, position, hasStdin));
|
|
1156
|
+
bodies.push(translateSimple(c, position, hasStdin, translation));
|
|
815
1157
|
}
|
|
816
1158
|
if (bodies.length === 1) {
|
|
817
1159
|
return { defs: '', call: '(& {\n' + bodies[0] + '\n})' };
|
|
@@ -870,7 +1212,7 @@ export function translatePipelineBody(p) {
|
|
|
870
1212
|
].join('\n'));
|
|
871
1213
|
return { defs: defs.join('\n'), call: pipelineName };
|
|
872
1214
|
}
|
|
873
|
-
export function translateCommandList(list) {
|
|
1215
|
+
export function translateCommandList(list, translation = EXECUTE_TRANSLATION) {
|
|
874
1216
|
const plans = [];
|
|
875
1217
|
for (const seg of list.segments) {
|
|
876
1218
|
const cmds = seg.pipeline.commands;
|
|
@@ -881,7 +1223,7 @@ export function translateCommandList(list) {
|
|
|
881
1223
|
const stdinRedirects = cmds.length
|
|
882
1224
|
? cmds[0].redirects.filter((r) => r.op === '<')
|
|
883
1225
|
: [];
|
|
884
|
-
const { defs, call } = translatePipelineBody(seg.pipeline);
|
|
1226
|
+
const { defs, call } = translatePipelineBody(seg.pipeline, translation);
|
|
885
1227
|
let body = defs ? defs + '\n' + call : call;
|
|
886
1228
|
// First-stage `< file` feeds stage zero via FAUXNIX_STDIN_FILE.
|
|
887
1229
|
// Later-stage `<` is owned inside the pipeline body, not this wrapper.
|
|
@@ -913,6 +1255,10 @@ const WRAP_HELPER_ORDER = [
|
|
|
913
1255
|
'fx-csub',
|
|
914
1256
|
'fx-svenc',
|
|
915
1257
|
'fx-svdec',
|
|
1258
|
+
'fx-posload',
|
|
1259
|
+
'fx-posset',
|
|
1260
|
+
'fx-posget',
|
|
1261
|
+
'fx-posshift',
|
|
916
1262
|
'fx-arrload',
|
|
917
1263
|
'fx-scalar0',
|
|
918
1264
|
'fx-ifs1',
|
|
@@ -923,7 +1269,11 @@ const WRAP_HELPER_ORDER = [
|
|
|
923
1269
|
'fx-arrput',
|
|
924
1270
|
'fx-arrclr',
|
|
925
1271
|
'fx-subget',
|
|
1272
|
+
'fx-casematch',
|
|
1273
|
+
'fx-subst',
|
|
1274
|
+
'fx-slice',
|
|
926
1275
|
'fx-winargv',
|
|
1276
|
+
'fx-cmdargv',
|
|
927
1277
|
'fx-native',
|
|
928
1278
|
];
|
|
929
1279
|
const WRAP_HELPER_DEPS = {
|
|
@@ -931,6 +1281,10 @@ const WRAP_HELPER_DEPS = {
|
|
|
931
1281
|
'fx-csub': [],
|
|
932
1282
|
'fx-svenc': [],
|
|
933
1283
|
'fx-svdec': [],
|
|
1284
|
+
'fx-posload': ['fx-svdec'],
|
|
1285
|
+
'fx-posset': ['fx-svenc'],
|
|
1286
|
+
'fx-posget': ['fx-posload'],
|
|
1287
|
+
'fx-posshift': ['fx-posload', 'fx-posset'],
|
|
934
1288
|
'fx-arrload': ['fx-scalar0', 'fx-svdec'],
|
|
935
1289
|
'fx-scalar0': ['fx-svdec'],
|
|
936
1290
|
'fx-ifs1': ['fx-scalar0'],
|
|
@@ -941,8 +1295,12 @@ const WRAP_HELPER_DEPS = {
|
|
|
941
1295
|
'fx-arrput': ['fx-arrdrop', 'fx-svenc'],
|
|
942
1296
|
'fx-arrclr': ['fx-arrdrop'],
|
|
943
1297
|
'fx-subget': ['fx-arrload', 'fx-ifs1'],
|
|
1298
|
+
'fx-casematch': [],
|
|
1299
|
+
'fx-subst': [],
|
|
1300
|
+
'fx-slice': [],
|
|
944
1301
|
'fx-winargv': [],
|
|
945
|
-
'fx-
|
|
1302
|
+
'fx-cmdargv': ['fx-winargv'],
|
|
1303
|
+
'fx-native': ['fx-cmdargv'],
|
|
946
1304
|
};
|
|
947
1305
|
/** Helpers the body calls that wrapScript still has to emit (not already defined there). */
|
|
948
1306
|
function wrapHelpersNeeded(body) {
|
|
@@ -1058,7 +1416,16 @@ export function wrapScript(body, opts = {}) {
|
|
|
1058
1416
|
' $script:fx_csub = $true',
|
|
1059
1417
|
' try { $fx_o = @(& $b | ForEach-Object { [string]$_ }) }',
|
|
1060
1418
|
' finally { $script:fx_csub = $fx_prevcs }',
|
|
1061
|
-
|
|
1419
|
+
// Line items (no trailing NL) get a NL between them. Chunks that
|
|
1420
|
+
// already end in NL concatenate, so $(echo a; echo b) is a\nb.
|
|
1421
|
+
" $fx_s = ''",
|
|
1422
|
+
' foreach ($fx_x in $fx_o) {',
|
|
1423
|
+
' $fx_t = [string]$fx_x',
|
|
1424
|
+
' if ($fx_s.Length -gt 0 -and $fx_s[$fx_s.Length - 1] -ne [char]10) {',
|
|
1425
|
+
' $fx_s += [string][char]10',
|
|
1426
|
+
' }',
|
|
1427
|
+
' $fx_s += $fx_t',
|
|
1428
|
+
' }',
|
|
1062
1429
|
' while ($fx_s.Length -gt 0 -and $fx_s[$fx_s.Length - 1] -eq [char]10) {',
|
|
1063
1430
|
' $fx_s = $fx_s.Substring(0, $fx_s.Length - 1)',
|
|
1064
1431
|
' }',
|
|
@@ -1089,6 +1456,52 @@ export function wrapScript(body, opts = {}) {
|
|
|
1089
1456
|
' return [string]$sb',
|
|
1090
1457
|
'}',
|
|
1091
1458
|
],
|
|
1459
|
+
'fx-posload': [
|
|
1460
|
+
'function fx-posload {',
|
|
1461
|
+
' if ($null -eq $env:FAUXNIX_POS -or [string]$env:FAUXNIX_POS -eq \'\') { return @() }',
|
|
1462
|
+
' $out = @()',
|
|
1463
|
+
' foreach ($el in @($env:FAUXNIX_POS -split [string][char]30)) { $out += ,(fx-svdec $el) }',
|
|
1464
|
+
' return $out',
|
|
1465
|
+
'}',
|
|
1466
|
+
],
|
|
1467
|
+
'fx-posset': [
|
|
1468
|
+
'function fx-posset($vals) {',
|
|
1469
|
+
' if ($null -eq $vals) { $vals = @() }',
|
|
1470
|
+
' $vals = @($vals)',
|
|
1471
|
+
' if ($vals.Count -eq 0) { $env:FAUXNIX_POS = \'\'; return }',
|
|
1472
|
+
' $encs = @(); foreach ($v in $vals) { $encs += (fx-svenc $v) }',
|
|
1473
|
+
' $env:FAUXNIX_POS = ($encs -join [string][char]30)',
|
|
1474
|
+
'}',
|
|
1475
|
+
],
|
|
1476
|
+
'fx-posget': [
|
|
1477
|
+
'function fx-posget($i) {',
|
|
1478
|
+
' $arr = @(fx-posload)',
|
|
1479
|
+
' $n = 0',
|
|
1480
|
+
' if (-not [int]::TryParse([string]$i, [ref]$n)) { return \'\' }',
|
|
1481
|
+
' if ($n -lt 1 -or $n -gt $arr.Count) { return \'\' }',
|
|
1482
|
+
' return [string]$arr[$n - 1]',
|
|
1483
|
+
'}',
|
|
1484
|
+
],
|
|
1485
|
+
'fx-posshift': [
|
|
1486
|
+
'function fx-posshift($n) {',
|
|
1487
|
+
' $arr = @(fx-posload)',
|
|
1488
|
+
' $i = 1',
|
|
1489
|
+
' if ($null -ne $n -and [string]$n -ne \'\') {',
|
|
1490
|
+
' $parsed = 0',
|
|
1491
|
+
' if (-not [int]::TryParse([string]$n, [ref]$parsed)) {',
|
|
1492
|
+
' [Console]::Error.WriteLine(\'bash: shift: \' + [string]$n + \': numeric argument required\')',
|
|
1493
|
+
' $script:fx_exit = 1',
|
|
1494
|
+
' return',
|
|
1495
|
+
' }',
|
|
1496
|
+
' $i = $parsed',
|
|
1497
|
+
' }',
|
|
1498
|
+
' if ($i -lt 0 -or $i -gt $arr.Count) { $script:fx_exit = 1; return }',
|
|
1499
|
+
' if ($i -eq 0) { return }',
|
|
1500
|
+
' if ($i -eq $arr.Count) { fx-posset @(); return }',
|
|
1501
|
+
' $new = @($arr[$i..($arr.Count - 1)])',
|
|
1502
|
+
' fx-posset $new',
|
|
1503
|
+
'}',
|
|
1504
|
+
],
|
|
1092
1505
|
'fx-arrload': [
|
|
1093
1506
|
'function fx-arrload($n) {',
|
|
1094
1507
|
' $n = [string]$n',
|
|
@@ -1096,8 +1509,10 @@ export function wrapScript(body, opts = {}) {
|
|
|
1096
1509
|
' $fx_eq = $fx_pair.IndexOf([char]61)',
|
|
1097
1510
|
' if ($fx_eq -lt 1) { continue }',
|
|
1098
1511
|
' if ($fx_pair.Substring(0, $fx_eq) -cne $n) { continue }',
|
|
1512
|
+
' $pay = $fx_pair.Substring($fx_eq + 1)',
|
|
1513
|
+
' if ($pay -eq [string][char]1) { return @() }',
|
|
1099
1514
|
' $out = @()',
|
|
1100
|
-
' foreach ($el in @($
|
|
1515
|
+
' foreach ($el in @($pay -split [string][char]30)) { $out += ,(fx-svdec $el) }',
|
|
1101
1516
|
' return $out',
|
|
1102
1517
|
' }',
|
|
1103
1518
|
' $s0 = fx-scalar0 $n',
|
|
@@ -1179,9 +1594,12 @@ export function wrapScript(body, opts = {}) {
|
|
|
1179
1594
|
'fx-arrput': [
|
|
1180
1595
|
'function fx-arrput($n, $vals) {',
|
|
1181
1596
|
' $n = [string]$n',
|
|
1182
|
-
' $vals = @($vals)',
|
|
1597
|
+
' if ($null -eq $vals) { $vals = @() } else { $vals = @($vals) }',
|
|
1183
1598
|
' fx-arrdrop $n',
|
|
1184
|
-
' if ($vals.Count -eq 0) {
|
|
1599
|
+
' if ($vals.Count -eq 0) {',
|
|
1600
|
+
// SOH payload: empty array, distinct from scalar '' and from A=('')
|
|
1601
|
+
" $env:FAUXNIX_ARRS = ((@($env:FAUXNIX_ARRS -split [string][char]10 | Where-Object { $_ -ne '' }) + ($n + [string][char]61 + [string][char]1)) -join [string][char]10)",
|
|
1602
|
+
' } else {',
|
|
1185
1603
|
' $encs = @(); foreach ($v in $vals) { $encs += (fx-svenc $v) }',
|
|
1186
1604
|
" $env:FAUXNIX_ARRS = ((@($env:FAUXNIX_ARRS -split [string][char]10 | Where-Object { $_ -ne '' }) + ($n + [string][char]61 + ($encs -join [string][char]30))) -join [string][char]10)",
|
|
1187
1605
|
' }',
|
|
@@ -1216,6 +1634,70 @@ export function wrapScript(body, opts = {}) {
|
|
|
1216
1634
|
' return [string]$arr[$i]',
|
|
1217
1635
|
'}',
|
|
1218
1636
|
],
|
|
1637
|
+
'fx-casematch': [
|
|
1638
|
+
'function fx-casematch($w, $pats) {',
|
|
1639
|
+
' $w = [string]$w',
|
|
1640
|
+
' foreach ($p in @($pats)) {',
|
|
1641
|
+
' $pat = [string]$p',
|
|
1642
|
+
' try {',
|
|
1643
|
+
' $wp = [WildcardPattern]::new($pat, [System.Management.Automation.WildcardOptions]::None)',
|
|
1644
|
+
' if ($wp.IsMatch($w)) { return $true }',
|
|
1645
|
+
' } catch {}',
|
|
1646
|
+
' }',
|
|
1647
|
+
' return $false',
|
|
1648
|
+
'}',
|
|
1649
|
+
],
|
|
1650
|
+
'fx-subst': [
|
|
1651
|
+
'function fx-subst($s, $pat, $repl, $all) {',
|
|
1652
|
+
' if ($null -eq $s) { return \'\' }',
|
|
1653
|
+
' $s = [string]$s',
|
|
1654
|
+
' $pat = [string]$pat',
|
|
1655
|
+
' $repl = [string]$repl',
|
|
1656
|
+
' if ($pat -eq \'\') { return $s }',
|
|
1657
|
+
' $glob = $false',
|
|
1658
|
+
' foreach ($fx_c in $pat.ToCharArray()) {',
|
|
1659
|
+
' if ($fx_c -eq [char]42 -or $fx_c -eq [char]63) { $glob = $true; break }',
|
|
1660
|
+
' }',
|
|
1661
|
+
' if (-not $glob) {',
|
|
1662
|
+
' if ($all) { return $s.Replace($pat, $repl) }',
|
|
1663
|
+
' $i = $s.IndexOf($pat)',
|
|
1664
|
+
' if ($i -lt 0) { return $s }',
|
|
1665
|
+
' return $s.Substring(0, $i) + $repl + $s.Substring($i + $pat.Length)',
|
|
1666
|
+
' }',
|
|
1667
|
+
' $sb = New-Object System.Text.StringBuilder',
|
|
1668
|
+
' foreach ($fx_c in $pat.ToCharArray()) {',
|
|
1669
|
+
' if ($fx_c -eq [char]42) { [void]$sb.Append(\'.*\'); continue }',
|
|
1670
|
+
' if ($fx_c -eq [char]63) { [void]$sb.Append(\'.\'); continue }',
|
|
1671
|
+
' [void]$sb.Append([regex]::Escape([string]$fx_c))',
|
|
1672
|
+
' }',
|
|
1673
|
+
' $rx = New-Object System.Text.RegularExpressions.Regex($sb.ToString(), [System.Text.RegularExpressions.RegexOptions]::Singleline)',
|
|
1674
|
+
' $fx_rep = $repl.Replace([string][char]36, ([string][char]36 + [string][char]36))',
|
|
1675
|
+
' if ($all) { return $rx.Replace($s, $fx_rep) }',
|
|
1676
|
+
' return $rx.Replace($s, $fx_rep, 1)',
|
|
1677
|
+
'}',
|
|
1678
|
+
],
|
|
1679
|
+
'fx-slice': [
|
|
1680
|
+
'function fx-slice($s, $off, $len) {',
|
|
1681
|
+
' if ($null -eq $s) { return \'\' }',
|
|
1682
|
+
' $s = [string]$s',
|
|
1683
|
+
' $n = $s.Length',
|
|
1684
|
+
' $o = 0',
|
|
1685
|
+
' if (-not [int]::TryParse([string]$off, [ref]$o)) { return \'\' }',
|
|
1686
|
+
' if ($o -lt 0) { $o = $n + $o }',
|
|
1687
|
+
' if ($o -lt 0 -or $o -ge $n) { return \'\' }',
|
|
1688
|
+
' if ($null -eq $len -or [string]$len -eq \'\') { return $s.Substring($o) }',
|
|
1689
|
+
' $l = 0',
|
|
1690
|
+
' if (-not [int]::TryParse([string]$len, [ref]$l)) { return \'\' }',
|
|
1691
|
+
' if ($l -lt 0) {',
|
|
1692
|
+
' $fx_end = $n + $l',
|
|
1693
|
+
' if ($fx_end -lt $o) { return \'\' }',
|
|
1694
|
+
' $l = $fx_end - $o',
|
|
1695
|
+
' }',
|
|
1696
|
+
' if ($l -le 0) { return \'\' }',
|
|
1697
|
+
' if (($o + $l) -gt $n) { $l = $n - $o }',
|
|
1698
|
+
' return $s.Substring($o, $l)',
|
|
1699
|
+
'}',
|
|
1700
|
+
],
|
|
1219
1701
|
'fx-winargv': [
|
|
1220
1702
|
'function fx-winargv($argv, $cmdmeta) {',
|
|
1221
1703
|
// Empty [object[]] unwraps to $null on PS 5.1; @($null) is one empty arg.
|
|
@@ -1252,8 +1734,48 @@ export function wrapScript(body, opts = {}) {
|
|
|
1252
1734
|
" return (($parts.ToArray()) -join ' ')",
|
|
1253
1735
|
'}',
|
|
1254
1736
|
],
|
|
1737
|
+
'fx-cmdargv': [
|
|
1738
|
+
'function fx-cmdargv($argv) {',
|
|
1739
|
+
// cmd.exe performs percent expansion even inside quotes, and embedded
|
|
1740
|
+
// quotes can reopen its metacharacter grammar. CR/LF/NUL cannot be
|
|
1741
|
+
// represented as one batch argument. Reject those values instead of
|
|
1742
|
+
// silently handing a different argv to the shim.
|
|
1743
|
+
' if ($null -eq $argv) { $argv = @() }',
|
|
1744
|
+
' foreach ($a in @($argv)) {',
|
|
1745
|
+
' $s = [string]$a',
|
|
1746
|
+
" if ($s.IndexOf('%') -ge 0) { throw \"fauxnix: cannot pass '%' to a .cmd/.bat file without changing the argument; invoke the underlying executable directly\" }",
|
|
1747
|
+
' if ($s.IndexOf([char]34) -ge 0) { throw \'fauxnix: cannot pass a double quote to a .cmd/.bat file without changing the argument; invoke the underlying executable directly\' }',
|
|
1748
|
+
' if ($s.IndexOf([char]13) -ge 0 -or $s.IndexOf([char]10) -ge 0) { throw \'fauxnix: cannot pass a line break to a .cmd/.bat file as one argument; invoke the underlying executable directly\' }',
|
|
1749
|
+
' if ($s.IndexOf([char]0) -ge 0) { throw \'fauxnix: cannot pass NUL to a .cmd/.bat file as one argument; invoke the underlying executable directly\' }',
|
|
1750
|
+
' }',
|
|
1751
|
+
' return (fx-winargv $argv $true)',
|
|
1752
|
+
'}',
|
|
1753
|
+
],
|
|
1255
1754
|
'fx-native': [
|
|
1256
|
-
|
|
1755
|
+
"if (-not ('FauxnixTextPump' -as [type])) {",
|
|
1756
|
+
" Add-Type -TypeDefinition @'",
|
|
1757
|
+
'using System;',
|
|
1758
|
+
'using System.IO;',
|
|
1759
|
+
'using System.Text;',
|
|
1760
|
+
'using System.Threading.Tasks;',
|
|
1761
|
+
'public static class FauxnixTextPump {',
|
|
1762
|
+
' public static async Task CopyAsync(TextReader reader, TextWriter writer) {',
|
|
1763
|
+
' var buffer = new char[4096];',
|
|
1764
|
+
' int read;',
|
|
1765
|
+
' while ((read = await reader.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) > 0) {',
|
|
1766
|
+
' await writer.WriteAsync(buffer, 0, read).ConfigureAwait(false);',
|
|
1767
|
+
' }',
|
|
1768
|
+
' await writer.FlushAsync().ConfigureAwait(false);',
|
|
1769
|
+
' }',
|
|
1770
|
+
' public static async Task CopyFileAsync(TextReader reader, string path) {',
|
|
1771
|
+
' using (var writer = new StreamWriter(path, false, new UTF8Encoding(false))) {',
|
|
1772
|
+
' await CopyAsync(reader, writer).ConfigureAwait(false);',
|
|
1773
|
+
' }',
|
|
1774
|
+
' }',
|
|
1775
|
+
'}',
|
|
1776
|
+
"'@",
|
|
1777
|
+
'}',
|
|
1778
|
+
'function fx-native($name, $argv, $term) {',
|
|
1257
1779
|
' if ($null -eq $argv) { $argv = @() } else { $argv = [object[]]@($argv) }',
|
|
1258
1780
|
' $app = Get-Command -Name $name -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1',
|
|
1259
1781
|
' if ($null -eq $app) {',
|
|
@@ -1282,8 +1804,8 @@ export function wrapScript(body, opts = {}) {
|
|
|
1282
1804
|
' $psi = New-Object System.Diagnostics.ProcessStartInfo',
|
|
1283
1805
|
' $ext = [IO.Path]::GetExtension([string]$app.Source)',
|
|
1284
1806
|
// CreateProcess cannot launch .cmd/.bat with UseShellExecute=false (npm.cmd).
|
|
1285
|
-
// /s strips one outer quote pair from the /c tail
|
|
1286
|
-
//
|
|
1807
|
+
// /s strips one outer quote pair from the /c tail. Build only the
|
|
1808
|
+
// subset of batch argv that cmd.exe can pass through unchanged.
|
|
1287
1809
|
" if ($ext -eq '.cmd' -or $ext -eq '.bat') {",
|
|
1288
1810
|
' $comspec = Get-Command -Name cmd -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1',
|
|
1289
1811
|
' if ($null -eq $comspec) {',
|
|
@@ -1292,10 +1814,16 @@ export function wrapScript(body, opts = {}) {
|
|
|
1292
1814
|
' return',
|
|
1293
1815
|
' }',
|
|
1294
1816
|
' $psi.FileName = $comspec.Source',
|
|
1295
|
-
'
|
|
1296
|
-
'
|
|
1817
|
+
' try {',
|
|
1818
|
+
' $fx_app = fx-cmdargv $app.Source',
|
|
1819
|
+
' $fx_rest = fx-cmdargv $argv',
|
|
1820
|
+
' } catch {',
|
|
1821
|
+
// Let the common wrapper report the validation error and stop the
|
|
1822
|
+
// current pipeline. Returning here would let xargs mask the failure.
|
|
1823
|
+
' throw $_.Exception',
|
|
1824
|
+
' }',
|
|
1297
1825
|
" 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 + \'"\'',
|
|
1826
|
+
' $psi.Arguments = \'/d /s /v:off /c "\' + $fx_tail + \'"\'',
|
|
1299
1827
|
' } else {',
|
|
1300
1828
|
' $psi.FileName = $app.Source',
|
|
1301
1829
|
' $psi.Arguments = fx-winargv $argv',
|
|
@@ -1306,35 +1834,40 @@ export function wrapScript(body, opts = {}) {
|
|
|
1306
1834
|
' $psi.RedirectStandardError = $true',
|
|
1307
1835
|
' $psi.CreateNoWindow = $true',
|
|
1308
1836
|
' $psi.WorkingDirectory = [Environment]::CurrentDirectory',
|
|
1309
|
-
//
|
|
1310
|
-
//
|
|
1837
|
+
// Drain both child pipes concurrently into disk-backed spools before
|
|
1838
|
+
// replaying them. This prevents either 64KB OS pipe from blocking the
|
|
1839
|
+
// child and avoids retaining the complete output in a .NET string.
|
|
1311
1840
|
" if ($env:FAUXNIX_NATIVE_ENCODING -eq 'ansi') { $enc = [System.Text.Encoding]::GetEncoding(936) } else { $enc = New-Object System.Text.UTF8Encoding $false }",
|
|
1312
1841
|
' $psi.StandardOutputEncoding = $enc',
|
|
1313
1842
|
' $psi.StandardErrorEncoding = $enc',
|
|
1314
1843
|
' $p = New-Object System.Diagnostics.Process',
|
|
1315
1844
|
' $p.StartInfo = $psi',
|
|
1316
|
-
'
|
|
1317
|
-
' $
|
|
1318
|
-
'
|
|
1319
|
-
'
|
|
1320
|
-
|
|
1321
|
-
'
|
|
1322
|
-
'
|
|
1323
|
-
'
|
|
1324
|
-
'
|
|
1325
|
-
'
|
|
1326
|
-
'
|
|
1327
|
-
'
|
|
1328
|
-
'
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
'
|
|
1332
|
-
|
|
1333
|
-
'
|
|
1845
|
+
' $fx_no = $null',
|
|
1846
|
+
' $fx_spoolUtf8 = New-Object System.Text.UTF8Encoding $false',
|
|
1847
|
+
' try {',
|
|
1848
|
+
' if (-not $term) {',
|
|
1849
|
+
" if ($env:FAUXNIX_NATIVE_SPOOL_DIR) { $fx_no = Join-Path $env:FAUXNIX_NATIVE_SPOOL_DIR (([guid]::NewGuid().ToString('N')) + '.out') }",
|
|
1850
|
+
' else { $fx_no = [IO.Path]::GetTempFileName() }',
|
|
1851
|
+
' }',
|
|
1852
|
+
' [void]$p.Start()',
|
|
1853
|
+
' if ($term) { $outTask = [FauxnixTextPump]::CopyAsync($p.StandardOutput, [Console]::Out) }',
|
|
1854
|
+
' else { $outTask = [FauxnixTextPump]::CopyFileAsync($p.StandardOutput, $fx_no) }',
|
|
1855
|
+
' $errTask = [FauxnixTextPump]::CopyAsync($p.StandardError, [Console]::Error)',
|
|
1856
|
+
' foreach ($fx_ln in $input) { $p.StandardInput.WriteLine([string]$fx_ln) }',
|
|
1857
|
+
' $p.StandardInput.Close()',
|
|
1858
|
+
' [void][System.Threading.Tasks.Task]::WaitAll(@($outTask, $errTask))',
|
|
1859
|
+
' [void]$p.WaitForExit()',
|
|
1860
|
+
' if (-not $term) {',
|
|
1861
|
+
' $fx_or = New-Object System.IO.StreamReader($fx_no, $fx_spoolUtf8)',
|
|
1862
|
+
' try { while (($fx_line = $fx_or.ReadLine()) -ne $null) { $fx_line } }',
|
|
1863
|
+
' finally { $fx_or.Dispose() }',
|
|
1864
|
+
' }',
|
|
1865
|
+
' $code = [int]$p.ExitCode',
|
|
1866
|
+
' if ($code -gt 0) { $script:fx_exit = $code } elseif ($code -lt 0) { $script:fx_exit = 1 }',
|
|
1867
|
+
' } finally {',
|
|
1868
|
+
' try { $p.Close() } catch {}',
|
|
1869
|
+
' if ($null -ne $fx_no) { Remove-Item -LiteralPath $fx_no -Force -ErrorAction SilentlyContinue }',
|
|
1334
1870
|
' }',
|
|
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
1871
|
'}',
|
|
1339
1872
|
],
|
|
1340
1873
|
};
|
|
@@ -1354,7 +1887,7 @@ function wrapHelperCatalog() {
|
|
|
1354
1887
|
}
|
|
1355
1888
|
/**
|
|
1356
1889
|
* Resident-host bootstrap: encoding + full fx-* catalog + JSON-line RPC loop.
|
|
1357
|
-
* Loaded once via
|
|
1890
|
+
* Loaded once via the selected PowerShell's `-File`. Must never `exit` a successful frame.
|
|
1358
1891
|
*/
|
|
1359
1892
|
export function hostBootstrapScript() {
|
|
1360
1893
|
const helpers = wrapHelperCatalog();
|
|
@@ -1370,6 +1903,46 @@ export function hostBootstrapScript() {
|
|
|
1370
1903
|
}
|
|
1371
1904
|
/** Raw UTF-8 JSON lines on stdin/stdout; command streams captured per frame. */
|
|
1372
1905
|
const HOST_RPC_LOOP = `
|
|
1906
|
+
if (-not ('FauxnixBoundedStream' -as [type])) {
|
|
1907
|
+
Add-Type -TypeDefinition @'
|
|
1908
|
+
using System;
|
|
1909
|
+
using System.IO;
|
|
1910
|
+
public sealed class FauxnixBoundedStream : Stream {
|
|
1911
|
+
private readonly MemoryStream inner;
|
|
1912
|
+
private readonly long limit;
|
|
1913
|
+
private readonly long storageLimit;
|
|
1914
|
+
private long totalWritten;
|
|
1915
|
+
public bool Truncated { get; private set; }
|
|
1916
|
+
public FauxnixBoundedStream(long limit) {
|
|
1917
|
+
this.limit = Math.Max(0, limit);
|
|
1918
|
+
this.storageLimit = this.limit + 3;
|
|
1919
|
+
this.inner = new MemoryStream((int)Math.Min(this.storageLimit, 65536));
|
|
1920
|
+
}
|
|
1921
|
+
public byte[] ToArray() { return inner.ToArray(); }
|
|
1922
|
+
public override bool CanRead { get { return false; } }
|
|
1923
|
+
public override bool CanSeek { get { return false; } }
|
|
1924
|
+
public override bool CanWrite { get { return true; } }
|
|
1925
|
+
public override long Length { get { return inner.Length; } }
|
|
1926
|
+
public override long Position { get { return inner.Position; } set { throw new NotSupportedException(); } }
|
|
1927
|
+
public override void Flush() { }
|
|
1928
|
+
public override int Read(byte[] buffer, int offset, int count) { throw new NotSupportedException(); }
|
|
1929
|
+
public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); }
|
|
1930
|
+
public override void SetLength(long value) { throw new NotSupportedException(); }
|
|
1931
|
+
public override void Write(byte[] buffer, int offset, int count) {
|
|
1932
|
+
long remaining = Math.Max(0, storageLimit - inner.Length);
|
|
1933
|
+
int keep = (int)Math.Min((long)count, remaining);
|
|
1934
|
+
if (keep > 0) inner.Write(buffer, offset, keep);
|
|
1935
|
+
totalWritten += count;
|
|
1936
|
+
if (totalWritten > limit) Truncated = true;
|
|
1937
|
+
}
|
|
1938
|
+
public override void WriteByte(byte value) {
|
|
1939
|
+
if (inner.Length < storageLimit) inner.WriteByte(value);
|
|
1940
|
+
totalWritten++;
|
|
1941
|
+
if (totalWritten > limit) Truncated = true;
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
'@
|
|
1945
|
+
}
|
|
1373
1946
|
$fx_utf8 = New-Object System.Text.UTF8Encoding $false
|
|
1374
1947
|
$fx_in = [Console]::OpenStandardInput()
|
|
1375
1948
|
$fx_out = [Console]::OpenStandardOutput()
|
|
@@ -1389,8 +1962,8 @@ function fx-emit-chunks($type, $id, [byte[]]$bytes, $limit, [ref]$seq) {
|
|
|
1389
1962
|
if ($null -ne $bytes) { $n = $bytes.Length }
|
|
1390
1963
|
$use = $n
|
|
1391
1964
|
$trunc = $false
|
|
1392
|
-
# limit
|
|
1393
|
-
if ($limit -
|
|
1965
|
+
# limit -1 = uncapped; zero is a real empty caller budget
|
|
1966
|
+
if ($limit -ge 0 -and $use -gt $limit) {
|
|
1394
1967
|
$use = $limit
|
|
1395
1968
|
$trunc = $true
|
|
1396
1969
|
# back the cut off to a valid UTF-8 boundary — a split codepoint makes
|
|
@@ -1416,6 +1989,18 @@ function fx-emit-chunks($type, $id, [byte[]]$bytes, $limit, [ref]$seq) {
|
|
|
1416
1989
|
}
|
|
1417
1990
|
return $trunc
|
|
1418
1991
|
}
|
|
1992
|
+
function fx-new-capture($mode, $limit, $spoolPath) {
|
|
1993
|
+
if ([string]$mode -eq 'discard') { return [System.IO.Stream]::Null }
|
|
1994
|
+
if ([string]$mode -eq 'spool') {
|
|
1995
|
+
return (New-Object System.IO.FileStream([string]$spoolPath, [System.IO.FileMode]::Create, [System.IO.FileAccess]::Write, [System.IO.FileShare]::Read))
|
|
1996
|
+
}
|
|
1997
|
+
return (New-Object FauxnixBoundedStream ([Math]::Max(0, [long]$limit)))
|
|
1998
|
+
}
|
|
1999
|
+
function fx-capture-bytes($stream) {
|
|
2000
|
+
if ($stream -is [FauxnixBoundedStream]) { return $stream.ToArray() }
|
|
2001
|
+
if ($stream -is [System.IO.MemoryStream]) { return $stream.ToArray() }
|
|
2002
|
+
return (New-Object byte[] 0)
|
|
2003
|
+
}
|
|
1419
2004
|
while ($true) {
|
|
1420
2005
|
$fx_line = $fx_reader.ReadLine()
|
|
1421
2006
|
if ($null -eq $fx_line) { break }
|
|
@@ -1443,12 +2028,24 @@ while ($true) {
|
|
|
1443
2028
|
}
|
|
1444
2029
|
}
|
|
1445
2030
|
$fx_script = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String([string]$fx_req.scriptB64))
|
|
1446
|
-
$
|
|
1447
|
-
$
|
|
2031
|
+
$fx_outLimit = 8388608
|
|
2032
|
+
$fx_errLimit = 1048576
|
|
2033
|
+
if ($null -ne $fx_req.PSObject.Properties['stdoutLimit']) { $fx_outLimit = [int]$fx_req.stdoutLimit }
|
|
2034
|
+
if ($null -ne $fx_req.PSObject.Properties['stderrLimit']) { $fx_errLimit = [int]$fx_req.stderrLimit }
|
|
2035
|
+
$fx_outMode = 'capture'
|
|
2036
|
+
$fx_errMode = 'capture'
|
|
2037
|
+
$fx_outSpool = ''
|
|
2038
|
+
$fx_errSpool = ''
|
|
2039
|
+
if ($null -ne $fx_req.PSObject.Properties['stdoutMode']) { $fx_outMode = [string]$fx_req.stdoutMode }
|
|
2040
|
+
if ($null -ne $fx_req.PSObject.Properties['stderrMode']) { $fx_errMode = [string]$fx_req.stderrMode }
|
|
2041
|
+
if ($null -ne $fx_req.PSObject.Properties['stdoutSpoolPath']) { $fx_outSpool = [string]$fx_req.stdoutSpoolPath }
|
|
2042
|
+
if ($null -ne $fx_req.PSObject.Properties['stderrSpoolPath']) { $fx_errSpool = [string]$fx_req.stderrSpoolPath }
|
|
2043
|
+
$fx_msOut = fx-new-capture $fx_outMode $fx_outLimit $fx_outSpool
|
|
2044
|
+
$fx_msErr = fx-new-capture $fx_errMode $fx_errLimit $fx_errSpool
|
|
1448
2045
|
$fx_outW = New-Object System.IO.StreamWriter($fx_msOut, $fx_utf8, 1024, $true)
|
|
1449
2046
|
$fx_errW = New-Object System.IO.StreamWriter($fx_msErr, $fx_utf8, 1024, $true)
|
|
1450
|
-
$fx_outW.NewLine = [string][char]
|
|
1451
|
-
$fx_errW.NewLine = [string][char]
|
|
2047
|
+
$fx_outW.NewLine = [string][char]10
|
|
2048
|
+
$fx_errW.NewLine = [string][char]10
|
|
1452
2049
|
$fx_outW.AutoFlush = $true
|
|
1453
2050
|
$fx_errW.AutoFlush = $true
|
|
1454
2051
|
[Console]::SetOut($fx_outW)
|
|
@@ -1476,24 +2073,29 @@ while ($true) {
|
|
|
1476
2073
|
}
|
|
1477
2074
|
$fx_outBytes = New-Object byte[] 0
|
|
1478
2075
|
$fx_errBytes = New-Object byte[] 0
|
|
1479
|
-
if ($null -ne $fx_msOut) { $fx_outBytes = $fx_msOut
|
|
1480
|
-
if ($null -ne $fx_msErr) { $fx_errBytes = $fx_msErr
|
|
2076
|
+
if ($null -ne $fx_msOut) { $fx_outBytes = fx-capture-bytes $fx_msOut }
|
|
2077
|
+
if ($null -ne $fx_msErr) { $fx_errBytes = fx-capture-bytes $fx_msErr }
|
|
2078
|
+
try { if ($null -ne $fx_outW) { $fx_outW.Dispose() } } catch {}
|
|
2079
|
+
try { if ($null -ne $fx_errW) { $fx_errW.Dispose() } } catch {}
|
|
2080
|
+
try { if ($null -ne $fx_msOut -and $fx_msOut -ne [System.IO.Stream]::Null) { $fx_msOut.Dispose() } } catch {}
|
|
2081
|
+
try { if ($null -ne $fx_msErr -and $fx_msErr -ne [System.IO.Stream]::Null) { $fx_msErr.Dispose() } } catch {}
|
|
1481
2082
|
if ($fx_v2) {
|
|
1482
|
-
$fx_outLimit = 8388608
|
|
1483
|
-
$fx_errLimit = 1048576
|
|
1484
|
-
if ($null -ne $fx_req.PSObject.Properties['stdoutLimit']) { $fx_outLimit = [int]$fx_req.stdoutLimit }
|
|
1485
|
-
if ($null -ne $fx_req.PSObject.Properties['stderrLimit']) { $fx_errLimit = [int]$fx_req.stderrLimit }
|
|
1486
2083
|
$fx_outSeq = 0
|
|
1487
2084
|
$fx_errSeq = 0
|
|
1488
|
-
$
|
|
1489
|
-
|
|
1490
|
-
if (
|
|
2085
|
+
$fx_outTrunc = $false
|
|
2086
|
+
$fx_errTrunc = $false
|
|
2087
|
+
if ($fx_msOut -is [FauxnixBoundedStream] -and $fx_msOut.Truncated) { $fx_outTrunc = $true }
|
|
2088
|
+
if ($fx_msErr -is [FauxnixBoundedStream] -and $fx_msErr.Truncated) { $fx_errTrunc = $true }
|
|
2089
|
+
$fx_outEmitLimit = $(if ($fx_outMode -eq 'capture') { $fx_outLimit } else { -1 })
|
|
2090
|
+
$fx_errEmitLimit = $(if ($fx_errMode -eq 'capture') { $fx_errLimit } else { -1 })
|
|
2091
|
+
if (fx-emit-chunks 'stdout' $fx_id $fx_outBytes $fx_outEmitLimit ([ref]$fx_outSeq)) { $fx_outTrunc = $true }
|
|
2092
|
+
if (fx-emit-chunks 'stderr' $fx_id $fx_errBytes $fx_errEmitLimit ([ref]$fx_errSeq)) { $fx_errTrunc = $true }
|
|
1491
2093
|
$fx_nativeErr = [Console]::OpenStandardError()
|
|
1492
2094
|
$fx_mark = $fx_utf8.GetBytes(('FAUXNIX_ERR_END:' + $fx_id + [char]10))
|
|
1493
2095
|
$fx_nativeErr.Write($fx_mark, 0, $fx_mark.Length)
|
|
1494
2096
|
$fx_nativeErr.Flush()
|
|
1495
|
-
$
|
|
1496
|
-
|
|
2097
|
+
$fx_trunc = $fx_outTrunc -or $fx_errTrunc
|
|
2098
|
+
$fx_end = '{"v":2,"type":"end","id":"' + $fx_id + '","exitCode":' + $fx_code + ',"timedOut":false,"cancelled":false,"truncated":' + ([string]$fx_trunc).ToLowerInvariant() + ',"stdoutTruncated":' + ([string]$fx_outTrunc).ToLowerInvariant() + ',"stderrTruncated":' + ([string]$fx_errTrunc).ToLowerInvariant() + '}'
|
|
1497
2099
|
$fx_proto.WriteLine($fx_end)
|
|
1498
2100
|
} else {
|
|
1499
2101
|
$fx_outB64 = ''
|