fauxnix-cli 0.6.0 → 0.7.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 +6 -4
- package/dist/ast.d.ts +5 -2
- package/dist/ast.js +4 -2
- package/dist/commands/sysinfo.js +8 -1
- package/dist/executor.d.ts +2 -0
- package/dist/executor.js +4 -0
- package/dist/mcp.js +4 -2
- package/dist/parser.js +73 -8
- package/dist/ps-host.d.ts +2 -0
- package/dist/ps-host.js +4 -0
- package/dist/translator.d.ts +5 -1
- package/dist/translator.js +63 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -193,11 +193,10 @@ fauxnix optimizes for the commands agents actually run. Documented deviations:
|
|
|
193
193
|
word expansion precedes the temporary environment).
|
|
194
194
|
- `yes` is capped at 65,536 lines — PS 5.1 pipelines cannot signal upstream producers to stop, so
|
|
195
195
|
an unbounded `yes | head` would hang.
|
|
196
|
-
- `tail -f`, `eval`, `alias`, heredocs, `while`/`until`/`case`,
|
|
197
|
-
`$((...))` arithmetic expansion
|
|
196
|
+
- `tail -f`, `eval`, `alias`, heredocs, `while`/`until`/`case`,
|
|
198
197
|
and background `&` are rejected with actionable error messages instead of misbehaving.
|
|
199
|
-
(`if/then/else/fi`, `for x in ...`, backtick substitution, `command -v`, pipeline `read`,
|
|
200
|
-
|
|
198
|
+
(`if/then/elif/else/fi`, `for x in ...`, backtick substitution, `command -v`, pipeline `read`,
|
|
199
|
+
dotenv-style `source`, and word-level `$((...))` arithmetic expansion are supported.)
|
|
201
200
|
- `command -v <builtin>` prints `/usr/bin/<name>` where bash prints the bare builtin name;
|
|
202
201
|
exit codes and empty-result semantics match.
|
|
203
202
|
- `chmod` maps only the read-only bit; exec bits are no-ops on Windows. `chown` is a silent no-op
|
|
@@ -231,6 +230,9 @@ Architecture map: `src/parser.ts` (bash subset → AST) · `src/translator.ts` (
|
|
|
231
230
|
executor wrapper) · `src/executor.ts` (spawn, redirects, session persistence) ·
|
|
232
231
|
`src/commands/*.ts` (per-command generators) · `src/mcp.ts` (MCP server) · `src/cli.ts`.
|
|
233
232
|
|
|
233
|
+
Roadmap: [docs/rfc-roadmap-to-1.0.md](docs/rfc-roadmap-to-1.0.md) — tracks, milestones,
|
|
234
|
+
and the RFC process for proposing waves.
|
|
235
|
+
|
|
234
236
|
## License
|
|
235
237
|
|
|
236
238
|
MIT © 20000419
|
package/dist/ast.d.ts
CHANGED
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
* - quoting: 'literal' "interp $VAR $(cmd)"
|
|
9
9
|
* - variables: $VAR ${VAR} ${VAR[n]} plus special cases ($HOME $USER $PATH ...)
|
|
10
10
|
* - command substitution: $(...) and `...` (recursively translated)
|
|
11
|
+
* - arithmetic expansion: $((...)) (existing fx-arith engine)
|
|
11
12
|
* - env assignment prefix: VAR=value cmd
|
|
12
13
|
*
|
|
13
14
|
* Explicitly unsupported (parser throws a helpful FauxnixError):
|
|
14
15
|
* heredocs, subshells (...), background &, while/until/case,
|
|
15
|
-
*
|
|
16
|
-
* process substitution <(...).
|
|
16
|
+
* globs inside quotes, process substitution <(...).
|
|
17
17
|
*/
|
|
18
18
|
export interface CommandList {
|
|
19
19
|
kind: 'CommandList';
|
|
@@ -89,6 +89,9 @@ export type WordPart = {
|
|
|
89
89
|
} | {
|
|
90
90
|
kind: 'CmdSub';
|
|
91
91
|
cmd: string;
|
|
92
|
+
} | {
|
|
93
|
+
kind: 'Arith';
|
|
94
|
+
parts: WordPart[];
|
|
92
95
|
};
|
|
93
96
|
export declare function wordToString(w: Word): string;
|
|
94
97
|
/** True when every part is unquoted Text and the concatenation equals `tok`. */
|
package/dist/ast.js
CHANGED
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
* - quoting: 'literal' "interp $VAR $(cmd)"
|
|
9
9
|
* - variables: $VAR ${VAR} ${VAR[n]} plus special cases ($HOME $USER $PATH ...)
|
|
10
10
|
* - command substitution: $(...) and `...` (recursively translated)
|
|
11
|
+
* - arithmetic expansion: $((...)) (existing fx-arith engine)
|
|
11
12
|
* - env assignment prefix: VAR=value cmd
|
|
12
13
|
*
|
|
13
14
|
* Explicitly unsupported (parser throws a helpful FauxnixError):
|
|
14
15
|
* heredocs, subshells (...), background &, while/until/case,
|
|
15
|
-
*
|
|
16
|
-
* process substitution <(...).
|
|
16
|
+
* globs inside quotes, process substitution <(...).
|
|
17
17
|
*/
|
|
18
18
|
export function wordToString(w) {
|
|
19
19
|
return w.map(partToString).join('');
|
|
@@ -40,6 +40,8 @@ function partToString(p) {
|
|
|
40
40
|
return p.index !== undefined ? `\${${p.name}[${p.index}]}` : `$${p.name}`;
|
|
41
41
|
case 'CmdSub':
|
|
42
42
|
return '$(' + p.cmd + ')';
|
|
43
|
+
case 'Arith':
|
|
44
|
+
return '$((' + p.parts.map(partToString).join('') + '))';
|
|
43
45
|
}
|
|
44
46
|
}
|
|
45
47
|
/** Best-effort "raw literal" view: is this word free of interpolation? */
|
package/dist/commands/sysinfo.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { FauxnixParseError, isUnquotedLiteral, wordToString } from '../ast.js';
|
|
2
2
|
import { lookup, parseWords, psStr, registeredNames } from '../registry.js';
|
|
3
|
-
import { argListExpr, exprOfWord, operandExpr, translateSimple, wrapTempEnv, encodeSetValExpr, escapeDq, translateCmdSub, normalizeLiteralPath, pathExpr, paramExpr, varExpr, } from '../translator.js';
|
|
3
|
+
import { argListExpr, exprOfWord, operandExpr, translateSimple, wrapTempEnv, encodeSetValExpr, escapeDq, translateCmdSub, normalizeLiteralPath, pathExpr, paramExpr, varExpr, arithExpr, setArithHelperPreamble, } from '../translator.js';
|
|
4
4
|
import { handlers as textIoHandlers } from './text-io.js';
|
|
5
5
|
/* ------------------------------------------------------------------ */
|
|
6
6
|
/* Shared TS helpers */
|
|
@@ -1373,6 +1373,7 @@ const FX_ENVGET_FN = [
|
|
|
1373
1373
|
' return [string]$fx_ev.Value',
|
|
1374
1374
|
'}',
|
|
1375
1375
|
].join('\n');
|
|
1376
|
+
setArithHelperPreamble(FX_TNK_FN + '\n' + FX_ENVGET_FN);
|
|
1376
1377
|
/** Like exprOfWord, but $var is an exact-case env lookup (bash, not $env:). */
|
|
1377
1378
|
function kshExprOfWord(w) {
|
|
1378
1379
|
const expanded = [];
|
|
@@ -1392,6 +1393,9 @@ function kshExprOfWord(w) {
|
|
|
1392
1393
|
}
|
|
1393
1394
|
if (tilde && expanded.length === 0)
|
|
1394
1395
|
return '(fx-home)';
|
|
1396
|
+
if (!tilde && expanded.length === 1 && expanded[0].kind === 'Arith') {
|
|
1397
|
+
return arithExpr(expanded[0].parts);
|
|
1398
|
+
}
|
|
1395
1399
|
if (!tilde && expanded.length === 1 && expanded[0].kind === 'Var') {
|
|
1396
1400
|
if (expanded[0].param) {
|
|
1397
1401
|
return paramExpr(expanded[0].name, expanded[0].param.op, expanded[0].param.word);
|
|
@@ -1434,6 +1438,9 @@ function kshExprOfWord(w) {
|
|
|
1434
1438
|
// [[ ]] does not IFS-split, so keep the newline contract.
|
|
1435
1439
|
out += '$(' + translateCmdSub(p.cmd, true) + ')';
|
|
1436
1440
|
break;
|
|
1441
|
+
case 'Arith':
|
|
1442
|
+
out += arithExpr(p.parts);
|
|
1443
|
+
break;
|
|
1437
1444
|
}
|
|
1438
1445
|
};
|
|
1439
1446
|
for (const p of expanded)
|
package/dist/executor.d.ts
CHANGED
|
@@ -25,6 +25,8 @@ export declare class FauxnixSession {
|
|
|
25
25
|
private bindFiles;
|
|
26
26
|
private syncFromDisk;
|
|
27
27
|
private ensureHost;
|
|
28
|
+
/** Boot powershell.exe now so the first run() is not the 1.1s cold start. */
|
|
29
|
+
prewarm(): Promise<void>;
|
|
28
30
|
dispose(): Promise<void>;
|
|
29
31
|
/** env for the child powershell process. */
|
|
30
32
|
childEnv(cwdOverride?: string, stdinFile?: string | null): NodeJS.ProcessEnv;
|
package/dist/executor.js
CHANGED
|
@@ -195,6 +195,10 @@ export class FauxnixSession {
|
|
|
195
195
|
}
|
|
196
196
|
return this.host;
|
|
197
197
|
}
|
|
198
|
+
/** Boot powershell.exe now so the first run() is not the 1.1s cold start. */
|
|
199
|
+
async prewarm() {
|
|
200
|
+
await this.ensureHost().ready();
|
|
201
|
+
}
|
|
198
202
|
async dispose() {
|
|
199
203
|
if (this.host) {
|
|
200
204
|
await this.host.stop();
|
package/dist/mcp.js
CHANGED
|
@@ -37,15 +37,16 @@ Output is formatted to look like GNU/Linux tooling (ls -l, ps aux, df -h ...), e
|
|
|
37
37
|
|
|
38
38
|
Supported: pipes (|), && / || / ;, redirections (> >> 2> 2>&1 < /dev/null), variables ($VAR $HOME ~), command substitution $(...), and ${registeredNames().length}+ coreutils-style commands (${registeredNames().slice(0, 18).join(', ')}...).
|
|
39
39
|
Unknown commands (git, node, npm, python, cargo...) are passed through and executed natively with argv-style quoting.
|
|
40
|
-
Not supported: heredocs, while/until/case,
|
|
40
|
+
Not supported: heredocs, while/until/case, background jobs. if/then/elif/else/fi, for-in loops, and word-level \$((...)) arithmetic expansion are supported.
|
|
41
41
|
|
|
42
|
-
CWD, environment variables, export/unset and cd persist across calls within this session — a resident PowerShell 5.1 host is
|
|
42
|
+
CWD, environment variables, export/unset and cd persist across calls within this session — a resident PowerShell 5.1 host is started when the MCP session begins (and after reset), so the first bash tool call is already warm.
|
|
43
43
|
Exit codes follow bash conventions (0 ok, 1 fail, 2 usage/serious, 127 command not found, 124 timeout).
|
|
44
44
|
|
|
45
45
|
Platform requirement: the execution backend is native Windows PowerShell 5.1+. On hosts without PowerShell on PATH (e.g. Linux containers/sandboxes), the bash tool returns exit code 127 with an actionable error instead of running the command.`;
|
|
46
46
|
export async function startMcpServer() {
|
|
47
47
|
const server = new McpServer({ name: 'fauxnix', version: pkgVersion }, { capabilities: { tools: {} } });
|
|
48
48
|
let session = new FauxnixSession();
|
|
49
|
+
await session.prewarm();
|
|
49
50
|
server.tool(TOOL_NAME, TOOL_DESCRIPTION, {
|
|
50
51
|
command: z.string().describe('The bash-style command line to run'),
|
|
51
52
|
timeout_ms: z
|
|
@@ -95,6 +96,7 @@ export async function startMcpServer() {
|
|
|
95
96
|
if (action === 'reset') {
|
|
96
97
|
await session.dispose();
|
|
97
98
|
session = new FauxnixSession();
|
|
99
|
+
await session.prewarm();
|
|
98
100
|
return { content: [{ type: 'text', text: 'fauxnix: session reset' }] };
|
|
99
101
|
}
|
|
100
102
|
const envKeys = Object.keys(session.env).sort();
|
package/dist/parser.js
CHANGED
|
@@ -217,7 +217,32 @@ function readBacktick(input, i) {
|
|
|
217
217
|
}
|
|
218
218
|
throw new FauxnixParseError('fauxnix: unclosed backtick');
|
|
219
219
|
}
|
|
220
|
-
/**
|
|
220
|
+
/** Expand `$…` constructs that appear inside `$((…))`. */
|
|
221
|
+
function readArithParts(input) {
|
|
222
|
+
const parts = [];
|
|
223
|
+
let buf = '';
|
|
224
|
+
let i = 0;
|
|
225
|
+
while (i < input.length) {
|
|
226
|
+
if (input[i] === '$') {
|
|
227
|
+
const v = readDollar(input, i);
|
|
228
|
+
if (v) {
|
|
229
|
+
if (buf) {
|
|
230
|
+
parts.push({ kind: 'Text', text: buf });
|
|
231
|
+
buf = '';
|
|
232
|
+
}
|
|
233
|
+
parts.push(v.part);
|
|
234
|
+
i = v.next;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
buf += input[i];
|
|
239
|
+
i++;
|
|
240
|
+
}
|
|
241
|
+
if (buf)
|
|
242
|
+
parts.push({ kind: 'Text', text: buf });
|
|
243
|
+
return parts;
|
|
244
|
+
}
|
|
245
|
+
/** Parse $VAR, ${VAR}, $(cmd substitution), $((arith)). Returns null when not a valid dollar construct. */
|
|
221
246
|
function readDollar(input, i) {
|
|
222
247
|
const n = input.length;
|
|
223
248
|
if (input[i] !== '$')
|
|
@@ -256,11 +281,41 @@ function readDollar(input, i) {
|
|
|
256
281
|
}
|
|
257
282
|
return { part: { kind: 'Var', name }, next: end + 1 };
|
|
258
283
|
}
|
|
259
|
-
// $((...))
|
|
260
|
-
//
|
|
261
|
-
// empty/confusing command. Reject loudly until word-level arith lands.
|
|
284
|
+
// $((...)) arithmetic expansion — distinct from `$( (cmd) )` (space after
|
|
285
|
+
// the first paren is command substitution of a grouped body).
|
|
262
286
|
if (input[j] === '(' && j + 1 < n && input[j + 1] === '(') {
|
|
263
|
-
|
|
287
|
+
let depth = 0;
|
|
288
|
+
let k = j;
|
|
289
|
+
while (k < n) {
|
|
290
|
+
const c = input[k];
|
|
291
|
+
if (c === "'" || c === '"') {
|
|
292
|
+
const q = c;
|
|
293
|
+
k++;
|
|
294
|
+
while (k < n && input[k] !== q) {
|
|
295
|
+
if (input[k] === '\\')
|
|
296
|
+
k++;
|
|
297
|
+
k++;
|
|
298
|
+
}
|
|
299
|
+
k++;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (c === '(')
|
|
303
|
+
depth++;
|
|
304
|
+
else if (c === ')') {
|
|
305
|
+
depth--;
|
|
306
|
+
if (depth === 0) {
|
|
307
|
+
k++;
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
k++;
|
|
312
|
+
}
|
|
313
|
+
if (depth !== 0)
|
|
314
|
+
throw new FauxnixParseError('fauxnix: unclosed $(( ))');
|
|
315
|
+
return {
|
|
316
|
+
part: { kind: 'Arith', parts: readArithParts(input.slice(j + 2, k - 2)) },
|
|
317
|
+
next: k,
|
|
318
|
+
};
|
|
264
319
|
}
|
|
265
320
|
// $(cmd substitution) — captured with balanced parens; the translator
|
|
266
321
|
// recursively translates this text before embedding it.
|
|
@@ -642,14 +697,24 @@ export function parseCommand(input) {
|
|
|
642
697
|
}
|
|
643
698
|
return { kind: 'CommandList', segments };
|
|
644
699
|
};
|
|
645
|
-
const parseIf = () => {
|
|
646
|
-
expectKw(
|
|
700
|
+
const parseIf = (start = 'if') => {
|
|
701
|
+
expectKw(start);
|
|
647
702
|
const test = parseListUntil(['then']);
|
|
648
703
|
expectKw('then');
|
|
649
704
|
const thenL = parseListUntil(['else', 'elif', 'fi']);
|
|
650
705
|
let elseL;
|
|
651
706
|
if (peekKw() === 'elif') {
|
|
652
|
-
|
|
707
|
+
// bash `elif` is `else` + nested `if`; the innermost clause eats `fi`.
|
|
708
|
+
elseL = {
|
|
709
|
+
kind: 'CommandList',
|
|
710
|
+
segments: [
|
|
711
|
+
{
|
|
712
|
+
op: ';',
|
|
713
|
+
pipeline: { kind: 'Pipeline', commands: [parseIf('elif')] },
|
|
714
|
+
},
|
|
715
|
+
],
|
|
716
|
+
};
|
|
717
|
+
return { kind: 'If', test, then: thenL, else: elseL, redirects: [] };
|
|
653
718
|
}
|
|
654
719
|
if (peekKw() === 'else') {
|
|
655
720
|
next();
|
package/dist/ps-host.d.ts
CHANGED
|
@@ -37,6 +37,8 @@ export declare class PowerShellHost {
|
|
|
37
37
|
private startLock;
|
|
38
38
|
private invokeLock;
|
|
39
39
|
constructor(hostFile: string, envFn: () => NodeJS.ProcessEnv);
|
|
40
|
+
/** Start the resident process and wait for the ready handshake (B1 prewarm). */
|
|
41
|
+
ready(): Promise<HostInvokeResult | null>;
|
|
40
42
|
invoke(script: string, env: HostRequestEnv, timeoutMs: number): Promise<HostInvokeResult>;
|
|
41
43
|
stop(): Promise<void>;
|
|
42
44
|
private invokeSerial;
|
package/dist/ps-host.js
CHANGED
|
@@ -46,6 +46,10 @@ export class PowerShellHost {
|
|
|
46
46
|
this.hostFile = hostFile;
|
|
47
47
|
this.envFn = envFn;
|
|
48
48
|
}
|
|
49
|
+
/** Start the resident process and wait for the ready handshake (B1 prewarm). */
|
|
50
|
+
async ready() {
|
|
51
|
+
return this.ensureStarted();
|
|
52
|
+
}
|
|
49
53
|
async invoke(script, env, timeoutMs) {
|
|
50
54
|
const run = this.invokeLock.then(() => this.invokeSerial(script, env, timeoutMs));
|
|
51
55
|
this.invokeLock = run.then(() => undefined, () => undefined);
|
package/dist/translator.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Assignment, CommandList, Redirect, SimpleCommand, IfCommand, ForCommand, Word } from './ast.js';
|
|
1
|
+
import { Assignment, CommandList, Redirect, SimpleCommand, IfCommand, ForCommand, Word, WordPart } from './ast.js';
|
|
2
2
|
import { PipelineCtx } from './registry.js';
|
|
3
3
|
/** `${name:-word}` and friends using case-exact fx-scalar0. */
|
|
4
4
|
export declare function paramExpr(name: string, op: ':-' | ':=' | ':+' | ':?' | '-' | '+' | '?', word: string): string;
|
|
@@ -25,6 +25,10 @@ export declare function pathExpr(s: string): string;
|
|
|
25
25
|
export declare function exprOfWord(w: Word, opts?: {
|
|
26
26
|
preserveCmdSub?: boolean;
|
|
27
27
|
}): string;
|
|
28
|
+
/** Registered by sysinfo so wrapScript can emit fx-arith without a circular import. */
|
|
29
|
+
export declare function setArithHelperPreamble(s: string): void;
|
|
30
|
+
/** PowerShell expression: evaluate `$((…))` via fx-arith; errors are loud, expansion empty. */
|
|
31
|
+
export declare function arithExpr(parts: WordPart[]): string;
|
|
28
32
|
/** Literal text of a word when it contains no interpolation, else null. */
|
|
29
33
|
export declare function literalOfWord(w: Word): string | null;
|
|
30
34
|
/**
|
package/dist/translator.js
CHANGED
|
@@ -161,6 +161,9 @@ export function exprOfWord(w, opts) {
|
|
|
161
161
|
if (expanded.length === 1 && expanded[0].kind === 'CmdSub') {
|
|
162
162
|
return '$(' + translateCmdSub(expanded[0].cmd, opts?.preserveCmdSub === true) + ')';
|
|
163
163
|
}
|
|
164
|
+
if (expanded.length === 1 && expanded[0].kind === 'Arith') {
|
|
165
|
+
return arithExpr(expanded[0].parts);
|
|
166
|
+
}
|
|
164
167
|
const literal = expanded.every((p) => p.kind === 'Text' || p.kind === 'SingleQuoted');
|
|
165
168
|
if (literal) {
|
|
166
169
|
const text = expanded.map((p) => p.text).join('');
|
|
@@ -186,6 +189,9 @@ export function exprOfWord(w, opts) {
|
|
|
186
189
|
case 'CmdSub':
|
|
187
190
|
out += '$(' + translateCmdSub(p.cmd, quoted || opts?.preserveCmdSub === true) + ')';
|
|
188
191
|
break;
|
|
192
|
+
case 'Arith':
|
|
193
|
+
out += arithExpr(p.parts);
|
|
194
|
+
break;
|
|
189
195
|
}
|
|
190
196
|
};
|
|
191
197
|
for (const p of expanded)
|
|
@@ -193,6 +199,62 @@ export function exprOfWord(w, opts) {
|
|
|
193
199
|
out += '"';
|
|
194
200
|
return out;
|
|
195
201
|
}
|
|
202
|
+
let arithHelperPreamble = '';
|
|
203
|
+
/** Registered by sysinfo so wrapScript can emit fx-arith without a circular import. */
|
|
204
|
+
export function setArithHelperPreamble(s) {
|
|
205
|
+
arithHelperPreamble = s;
|
|
206
|
+
}
|
|
207
|
+
function injectArithHelpers(body) {
|
|
208
|
+
if (!arithHelperPreamble)
|
|
209
|
+
return body;
|
|
210
|
+
if (!/\bfx-arith\b/.test(body))
|
|
211
|
+
return body;
|
|
212
|
+
if (/function\s+fx-arith\b/.test(body))
|
|
213
|
+
return body;
|
|
214
|
+
return arithHelperPreamble + '\n' + body;
|
|
215
|
+
}
|
|
216
|
+
/** PowerShell expression: evaluate `$((…))` via fx-arith; errors are loud, expansion empty. */
|
|
217
|
+
export function arithExpr(parts) {
|
|
218
|
+
const src = arithSourceExpr(parts);
|
|
219
|
+
return ('$(try { [string](fx-arith (' +
|
|
220
|
+
src +
|
|
221
|
+
')) } catch { [Console]::Error.WriteLine((\'bash: \' + [string](' +
|
|
222
|
+
src +
|
|
223
|
+
') + \': integer expression expected\')); $script:fx_exit = 1; \'\' })');
|
|
224
|
+
}
|
|
225
|
+
function arithSourceExpr(parts) {
|
|
226
|
+
if (parts.length === 0)
|
|
227
|
+
return "''";
|
|
228
|
+
if (parts.every((p) => p.kind === 'Text')) {
|
|
229
|
+
return psStr(parts.map((p) => p.text).join(''));
|
|
230
|
+
}
|
|
231
|
+
let out = '"';
|
|
232
|
+
const emit = (p) => {
|
|
233
|
+
switch (p.kind) {
|
|
234
|
+
case 'Text':
|
|
235
|
+
case 'SingleQuoted':
|
|
236
|
+
out += escapeDq(p.text);
|
|
237
|
+
break;
|
|
238
|
+
case 'DoubleQuoted':
|
|
239
|
+
for (const q of p.parts)
|
|
240
|
+
emit(q);
|
|
241
|
+
break;
|
|
242
|
+
case 'Var':
|
|
243
|
+
out += '$(' + varExpr(p.name, p.index, p.param, p.length === true) + ')';
|
|
244
|
+
break;
|
|
245
|
+
case 'CmdSub':
|
|
246
|
+
out += '$(' + translateCmdSub(p.cmd, true) + ')';
|
|
247
|
+
break;
|
|
248
|
+
case 'Arith':
|
|
249
|
+
out += arithExpr(p.parts);
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
for (const p of parts)
|
|
254
|
+
emit(p);
|
|
255
|
+
out += '"';
|
|
256
|
+
return out;
|
|
257
|
+
}
|
|
196
258
|
/** Literal text of a word when it contains no interpolation, else null. */
|
|
197
259
|
export function literalOfWord(w) {
|
|
198
260
|
if (!w.every((p) => p.kind === 'Text' || p.kind === 'SingleQuoted'))
|
|
@@ -895,6 +957,7 @@ function wrapBodyAndPersist(body, exitProcess) {
|
|
|
895
957
|
*/
|
|
896
958
|
export function wrapScript(body, opts = {}) {
|
|
897
959
|
const mode = opts.mode ?? 'spawn';
|
|
960
|
+
body = injectArithHelpers(body);
|
|
898
961
|
const needed = mode === 'host' ? new Set() : wrapHelpersNeeded(body);
|
|
899
962
|
const lines = mode === 'host'
|
|
900
963
|
? wrapCwdPreamble()
|
package/package.json
CHANGED