fauxnix-cli 0.1.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/dist/parser.js ADDED
@@ -0,0 +1,400 @@
1
+ import { FauxnixParseError, } from './ast.js';
2
+ const OPERATORS = [
3
+ '&&', '||', '>>', '<<', '2>&1', '1>&2', '2>', '&>>', '&>', '>', '<', '|', ';',
4
+ ];
5
+ export function tokenize(input) {
6
+ const tokens = [];
7
+ let i = 0;
8
+ const n = input.length;
9
+ const pushWord = (parts) => {
10
+ if (parts.length > 0)
11
+ tokens.push({ type: 'WORD', parts });
12
+ };
13
+ let cur = [];
14
+ let fdDigits = '';
15
+ const flush = () => {
16
+ pushWord(cur);
17
+ cur = [];
18
+ fdDigits = '';
19
+ };
20
+ while (i < n) {
21
+ const ch = input[i];
22
+ // whitespace separates words; newline acts as ';'
23
+ if (ch === ' ' || ch === '\t' || ch === '\r') {
24
+ flush();
25
+ i++;
26
+ continue;
27
+ }
28
+ if (ch === '\n') {
29
+ flush();
30
+ tokens.push({ type: 'OP', op: ';' });
31
+ i++;
32
+ continue;
33
+ }
34
+ // comments
35
+ if (ch === '#' && cur.length === 0) {
36
+ while (i < n && input[i] !== '\n')
37
+ i++;
38
+ continue;
39
+ }
40
+ // line continuation
41
+ if (ch === '\\' && i + 1 < n && input[i + 1] === '\n') {
42
+ i += 2;
43
+ continue;
44
+ }
45
+ // try operators (longest first — list above is ordered)
46
+ let matched;
47
+ for (const op of OPERATORS) {
48
+ if (input.startsWith(op, i)) {
49
+ matched = op;
50
+ break;
51
+ }
52
+ }
53
+ if (matched) {
54
+ // bash folds a leading fd digit into the redirect: `2>`, `2>>`, `2>&1`
55
+ const isRedirectish = matched[0] === '>' || matched[0] === '<' || matched === '&>' || matched === '&>>';
56
+ const curIsFd = fdDigits.length > 0 &&
57
+ cur.length === fdDigits.length &&
58
+ cur.every((p, idx) => p.kind === 'Text' && p.text === fdDigits[idx]);
59
+ let advance = matched.length;
60
+ if (isRedirectish && matched !== '<<' && curIsFd) {
61
+ cur = []; // drop the digits from the pending word
62
+ if (matched === '>>')
63
+ matched = fdDigits + '>>';
64
+ else if (matched === '>') {
65
+ if (input.startsWith('&1', i + 1))
66
+ matched = fdDigits + '>&1';
67
+ else if (input.startsWith('&2', i + 1))
68
+ matched = fdDigits + '>&2';
69
+ else
70
+ matched = fdDigits + '>';
71
+ }
72
+ // the fd digits were consumed earlier; advance past the operator tail only
73
+ advance = matched.length - fdDigits.length;
74
+ fdDigits = '';
75
+ }
76
+ if (matched === '<<') {
77
+ throw new FauxnixParseError('fauxnix: heredocs (<<) are not supported yet. Pass the text via echo pipe or a temp file instead.');
78
+ }
79
+ flush();
80
+ tokens.push({ type: 'OP', op: matched });
81
+ i += advance;
82
+ continue;
83
+ }
84
+ // single quotes — fully literal
85
+ if (ch === "'") {
86
+ const end = input.indexOf("'", i + 1);
87
+ if (end === -1)
88
+ throw new FauxnixParseError('fauxnix: unclosed single quote');
89
+ cur.push({ kind: 'SingleQuoted', text: input.slice(i + 1, end) });
90
+ i = end + 1;
91
+ continue;
92
+ }
93
+ // double quotes — interpolated
94
+ if (ch === '"') {
95
+ i++;
96
+ const parts = [];
97
+ let buf = '';
98
+ while (i < n && input[i] !== '"') {
99
+ const c = input[i];
100
+ if (c === '\\' && i + 1 < n && '"$`\\'.includes(input[i + 1])) {
101
+ buf += input[i + 1];
102
+ i += 2;
103
+ continue;
104
+ }
105
+ if (c === '$') {
106
+ const v = readDollar(input, i);
107
+ if (v) {
108
+ if (buf) {
109
+ parts.push({ kind: 'Text', text: buf });
110
+ buf = '';
111
+ }
112
+ parts.push(v.part);
113
+ i = v.next;
114
+ continue;
115
+ }
116
+ }
117
+ buf += c;
118
+ i++;
119
+ }
120
+ if (i >= n)
121
+ throw new FauxnixParseError('fauxnix: unclosed double quote');
122
+ i++;
123
+ if (buf)
124
+ parts.push({ kind: 'Text', text: buf });
125
+ cur.push({ kind: 'DoubleQuoted', parts });
126
+ continue;
127
+ }
128
+ // dollar — variable or command substitution
129
+ if (ch === '$') {
130
+ const v = readDollar(input, i);
131
+ if (v) {
132
+ cur.push(v.part);
133
+ i = v.next;
134
+ continue;
135
+ }
136
+ cur.push({ kind: 'Text', text: '$' });
137
+ i++;
138
+ continue;
139
+ }
140
+ if (ch === '`') {
141
+ throw new FauxnixParseError('fauxnix: backticks are not supported. Use $(...) command substitution instead.');
142
+ }
143
+ // escape outside quotes
144
+ if (ch === '\\' && i + 1 < n) {
145
+ cur.push({ kind: 'Text', text: input[i + 1] });
146
+ i += 2;
147
+ continue;
148
+ }
149
+ // track leading digits (potential fd number for redirects)
150
+ if (/[0-9]/.test(ch) && cur.length === 0 && fdDigits.length < 2) {
151
+ fdDigits += ch;
152
+ cur.push({ kind: 'Text', text: ch });
153
+ i++;
154
+ continue;
155
+ }
156
+ fdDigits = '';
157
+ cur.push({ kind: 'Text', text: ch });
158
+ i++;
159
+ }
160
+ flush();
161
+ tokens.push({ type: 'EOF' });
162
+ return tokens;
163
+ }
164
+ function isNameStart(c) {
165
+ return /[A-Za-z_]/.test(c);
166
+ }
167
+ function isNameChar(c) {
168
+ return /[A-Za-z0-9_]/.test(c);
169
+ }
170
+ /** Parse $VAR, ${VAR}, $(cmd substitution). Returns null when not a valid dollar construct. */
171
+ function readDollar(input, i) {
172
+ const n = input.length;
173
+ if (input[i] !== '$')
174
+ return null;
175
+ let j = i + 1;
176
+ if (j >= n)
177
+ return null;
178
+ // ${...}
179
+ if (input[j] === '{') {
180
+ const end = input.indexOf('}', j);
181
+ if (end === -1)
182
+ throw new FauxnixParseError('fauxnix: unclosed ${');
183
+ const name = input.slice(j + 1, end);
184
+ if (!isNameStart(name[0]) || !name.split('').every(isNameChar)) {
185
+ // ${VAR:-default} etc. — unsupported, kept as raw text
186
+ return { part: { kind: 'Text', text: input.slice(i, end + 1) }, next: end + 1 };
187
+ }
188
+ return { part: { kind: 'Var', name }, next: end + 1 };
189
+ }
190
+ // $(cmd substitution) — captured with balanced parens; the translator
191
+ // recursively translates this text before embedding it.
192
+ if (input[j] === '(') {
193
+ let depth = 1;
194
+ let k = j + 1;
195
+ while (k < n && depth > 0) {
196
+ if (input[k] === '(')
197
+ depth++;
198
+ else if (input[k] === ')')
199
+ depth--;
200
+ else if (input[k] === "'" || input[k] === '"') {
201
+ const q = input[k];
202
+ k++;
203
+ while (k < n && input[k] !== q) {
204
+ if (input[k] === '\\')
205
+ k++;
206
+ k++;
207
+ }
208
+ }
209
+ k++;
210
+ }
211
+ if (depth !== 0)
212
+ throw new FauxnixParseError('fauxnix: unclosed $( )');
213
+ const cmdText = input.slice(j + 1, k - 1);
214
+ return { part: { kind: 'CmdSub', cmd: cmdText }, next: k };
215
+ }
216
+ // $NAME
217
+ if (isNameStart(input[j])) {
218
+ let len = 1;
219
+ while (j + len < n && isNameChar(input[j + len]))
220
+ len++;
221
+ return { part: { kind: 'Var', name: input.slice(j, j + len) }, next: j + len };
222
+ }
223
+ // special: $? $$ $0-$9 — kept as symbolic Var; the translator maps them
224
+ if ('?$_'.includes(input[j]) || /[0-9]/.test(input[j])) {
225
+ return { part: { kind: 'Var', name: input[j] }, next: j + 1 };
226
+ }
227
+ return null;
228
+ }
229
+ /* ------------------------------------------------------------------ */
230
+ /* Parser */
231
+ /* ------------------------------------------------------------------ */
232
+ export function parseCommand(input) {
233
+ const tokens = tokenize(input);
234
+ let pos = 0;
235
+ const peek = () => tokens[pos];
236
+ const next = () => tokens[pos++];
237
+ const parseList = () => {
238
+ const segments = [];
239
+ let op = ';';
240
+ while (peek().type === 'OP' && peek().op === ';')
241
+ next();
242
+ while (peek().type !== 'EOF') {
243
+ const pipeline = parsePipeline();
244
+ segments.push({ pipeline, op });
245
+ const t = peek();
246
+ if (t.type === 'OP' && (t.op === '&&' || t.op === '||' || t.op === ';')) {
247
+ op = t.op;
248
+ next();
249
+ while (peek().type === 'OP' && peek().op === ';')
250
+ next(); // trailing / duplicate ;
251
+ }
252
+ else if (t.type === 'EOF') {
253
+ break;
254
+ }
255
+ else {
256
+ throw new FauxnixParseError('fauxnix: unexpected token after pipeline');
257
+ }
258
+ }
259
+ if (segments.length === 0)
260
+ throw new FauxnixParseError('fauxnix: empty command');
261
+ return { kind: 'CommandList', segments };
262
+ };
263
+ const parsePipeline = () => {
264
+ const commands = [];
265
+ for (;;) {
266
+ commands.push(parseSimple());
267
+ const t = peek();
268
+ if (t.type === 'OP' && t.op === '|') {
269
+ next();
270
+ continue;
271
+ }
272
+ break;
273
+ }
274
+ return { kind: 'Pipeline', commands };
275
+ };
276
+ const parseSimple = () => {
277
+ const assignments = [];
278
+ let redirects = [];
279
+ let name = null;
280
+ const args = [];
281
+ for (;;) {
282
+ const t = peek();
283
+ if (t.type === 'EOF')
284
+ break;
285
+ // possible redirect operator
286
+ if (t.type === 'OP' && isRedirectOp(t.op)) {
287
+ const op = t.op;
288
+ next();
289
+ // fd-dup operators (2>&1, 1>&2) carry their own target
290
+ const target = op === '2>&1' || op === '1>&2' ? '' : readRedirectTarget();
291
+ redirects.push({ op: normalizeRedirect(op), target });
292
+ continue;
293
+ }
294
+ if (t.type === 'OP')
295
+ break;
296
+ const word = t.parts;
297
+ // assignment prefix before command name?
298
+ if (name === null && isAssignment(word)) {
299
+ next();
300
+ const split = splitAssignment(word);
301
+ if (split) {
302
+ assignments.push(split);
303
+ continue;
304
+ }
305
+ }
306
+ if (name === null) {
307
+ name = word;
308
+ next();
309
+ }
310
+ else {
311
+ args.push(word);
312
+ next();
313
+ }
314
+ }
315
+ if (!name)
316
+ throw new FauxnixParseError('fauxnix: expected a command');
317
+ return { kind: 'SimpleCommand', assignments, name, args, redirects };
318
+ };
319
+ const readRedirectTarget = () => {
320
+ const t = next();
321
+ if (t.type !== 'WORD')
322
+ throw new FauxnixParseError('fauxnix: redirect target expected');
323
+ return wordToStringSafe(t.parts);
324
+ };
325
+ // ---- helpers on words (joined raw view) ----
326
+ function wordToStringSafe(w) {
327
+ return w
328
+ .map((p) => {
329
+ switch (p.kind) {
330
+ case 'Text':
331
+ case 'SingleQuoted':
332
+ return p.text;
333
+ case 'DoubleQuoted':
334
+ return p.parts
335
+ .map((q) => (q.kind === 'Text' || q.kind === 'SingleQuoted' ? q.text : ''))
336
+ .join('');
337
+ default:
338
+ return '';
339
+ }
340
+ })
341
+ .join('');
342
+ }
343
+ function isAssignment(w) {
344
+ return splitAssignment(w) !== null;
345
+ }
346
+ /**
347
+ * Split `NAME=value` at the first unquoted '=' — char-level, so single-part
348
+ * words like [Text 'FOO=bar'] split correctly. Returns null when the word
349
+ * is not a valid assignment.
350
+ */
351
+ function splitAssignment(w) {
352
+ // merge adjacent Text parts first — the tokenizer emits per-char parts
353
+ const merged = [];
354
+ for (const p of w) {
355
+ const last = merged[merged.length - 1];
356
+ if (p.kind === 'Text' && last && last.kind === 'Text') {
357
+ last.text += p.text;
358
+ }
359
+ else if (p.kind === 'SingleQuoted' && last && last.kind === 'SingleQuoted') {
360
+ last.text += p.text;
361
+ }
362
+ else {
363
+ merged.push({ ...p });
364
+ }
365
+ }
366
+ for (let idx = 0; idx < merged.length; idx++) {
367
+ const p = merged[idx];
368
+ if (p.kind !== 'Text')
369
+ continue;
370
+ const eq = p.text.indexOf('=');
371
+ if (eq <= 0)
372
+ continue;
373
+ const name = merged
374
+ .slice(0, idx)
375
+ .map((q) => (q.kind === 'Text' || q.kind === 'SingleQuoted' ? q.text : ''))
376
+ .join('') + p.text.slice(0, eq);
377
+ if (!isNameStart(name[0]) || !name.split('').every(isNameChar))
378
+ continue;
379
+ const value = [];
380
+ const tail = p.text.slice(eq + 1);
381
+ if (tail)
382
+ value.push({ kind: 'Text', text: tail });
383
+ value.push(...merged.slice(idx + 1));
384
+ return { name, value };
385
+ }
386
+ return null;
387
+ }
388
+ return parseList();
389
+ }
390
+ function isRedirectOp(op) {
391
+ if (!op)
392
+ return false;
393
+ return ['>', '>>', '2>', '2>>', '&>', '&>>', '2>&1', '1>&2', '<'].includes(op);
394
+ }
395
+ function normalizeRedirect(op) {
396
+ const known = ['>', '>>', '2>', '2>>', '&>', '&>>', '2>&1', '1>&2', '<'];
397
+ if (known.includes(op))
398
+ return op;
399
+ throw new FauxnixParseError('fauxnix: unsupported redirect: ' + op);
400
+ }
@@ -0,0 +1,79 @@
1
+ import { Word } from './ast.js';
2
+ /**
3
+ * Command registry — each Linux command maps to a PowerShell generator.
4
+ *
5
+ * Contract for generated code (the "Fauxnix contract"):
6
+ *
7
+ * 1. The generator returns a block of PowerShell *statements* (NO `& { }`
8
+ * wrapper, NO `exit`). The translator either embeds the block in
9
+ * `(& { ... })` (single command) or turns it into a generated function
10
+ * body chained in a pipeline (multi-command pipelines).
11
+ * 2. Everything the block emits is a string line (Unix text-stream semantics).
12
+ * Never emit raw .NET objects — stringify them ("$_", [string]$x).
13
+ * 3. Failure must NOT `exit` (that would kill sibling pipeline stages).
14
+ * Instead write the bash-style message to stderr and set the shared flag:
15
+ * [Console]::Error.WriteLine('cat: foo: No such file or directory')
16
+ * $script:fx_exit = 1
17
+ * NOTE the exact spelling: `.Error.WriteLine` with a DOT (a `::` chain
18
+ * resolves as a static call and throws), and `$script:` (handlers run in
19
+ * child scopes — plain `$fx_exit` writes stay local and are lost).
20
+ * The executor wrapper reads $script:fx_exit after the pipeline completes.
21
+ * 4. Stdin = `$input` inside your block: @($input | ForEach-Object { [string]$_ })
22
+ * (empty when there is no upstream). Use ctx.hasStdin to decide whether
23
+ * reading stdin is legal.
24
+ * 5. Output formatting mimics GNU coreutils so agents feel at home.
25
+ * 6. Target Windows PowerShell 5.1: no ternary, no ?? operator, no
26
+ * chainable null-conditional. Plain if/else everywhere.
27
+ * 7. Blocks must be self-contained (local helper functions allowed) — they
28
+ * also run inside $(...) command substitutions without the wrapper preamble.
29
+ */
30
+ export interface PipelineCtx {
31
+ /** Position of this command inside the pipeline. */
32
+ position: 'first' | 'middle' | 'last';
33
+ /** True when stdin is available (piped input or `< file` redirect). */
34
+ hasStdin: boolean;
35
+ }
36
+ export type Handler = (args: Word[], ctx: PipelineCtx) => string;
37
+ export declare function register(name: string, handler: Handler): void;
38
+ export declare function registerAll(mod: Record<string, Handler>): void;
39
+ export declare function lookup(name: string): Handler | undefined;
40
+ export declare function registeredNames(): string[];
41
+ /** Escape a JS string into a single-quoted PowerShell string literal. */
42
+ export declare function psStr(s: string): string;
43
+ /** bash-style stderr line + exit-flag, as PS statements. */
44
+ export declare function psErr(cmd: string, msg: string): string;
45
+ /** bash-style stderr line with exit code 2 (serious trouble, like ls). */
46
+ export declare function psErr2(cmd: string, msg: string): string;
47
+ /** Does this argument text contain glob characters? */
48
+ export declare function hasGlob(s: string): boolean;
49
+ /** Options parsing helper: returns { flags: Set<string>, operands: string[] } */
50
+ export interface ParsedArgs {
51
+ flags: Set<string>;
52
+ /** long options like --all (kept with dashes) */
53
+ longs: Set<string>;
54
+ operands: string[];
55
+ /** option values consumed via -n 5 style */
56
+ values: Map<string, string>;
57
+ }
58
+ /**
59
+ * Parse a Unix-style argv. Supports:
60
+ * -a -abc (bundled) --long (with =value or following value via valueOpts)
61
+ */
62
+ export declare function parseArgs(args: {
63
+ toDisplay: string;
64
+ }[], valueOpts?: Set<string>): ParsedArgs;
65
+ export interface WordArgs {
66
+ flags: Set<string>;
67
+ longs: Set<string>;
68
+ values: Map<string, string>;
69
+ /** Options that take a value but were not followed by one. */
70
+ missingValue: string[];
71
+ operandWords: Word[];
72
+ }
73
+ /**
74
+ * Parse argv while keeping operand *Words* (so they can still be translated
75
+ * with exprOfWord / operandExpr). Supports:
76
+ * -a -abc --long --long=v --long v -n 5 -n5
77
+ * shortValues: single-char options that consume a value (e.g. ['n']).
78
+ */
79
+ export declare function parseWords(args: Word[], shortValues?: string[], longValues?: string[]): WordArgs;
@@ -0,0 +1,145 @@
1
+ import { wordToString } from './ast.js';
2
+ const registry = new Map();
3
+ export function register(name, handler) {
4
+ registry.set(name, handler);
5
+ }
6
+ export function registerAll(mod) {
7
+ for (const [name, handler] of Object.entries(mod))
8
+ register(name, handler);
9
+ }
10
+ export function lookup(name) {
11
+ return registry.get(name);
12
+ }
13
+ export function registeredNames() {
14
+ return [...registry.keys()].sort();
15
+ }
16
+ /* ------------------------------------------------------------------ */
17
+ /* Shared helpers used by command generators */
18
+ /* ------------------------------------------------------------------ */
19
+ /** Escape a JS string into a single-quoted PowerShell string literal. */
20
+ export function psStr(s) {
21
+ return "'" + s.replace(/'/g, "''") + "'";
22
+ }
23
+ /** bash-style stderr line + exit-flag, as PS statements. */
24
+ export function psErr(cmd, msg) {
25
+ return '[Console]::Error.WriteLine(' + psStr(cmd + ': ' + msg) + '); $script:fx_exit = 1';
26
+ }
27
+ /** bash-style stderr line with exit code 2 (serious trouble, like ls). */
28
+ export function psErr2(cmd, msg) {
29
+ return '[Console]::Error.WriteLine(' + psStr(cmd + ': ' + msg) + '); $script:fx_exit = 2';
30
+ }
31
+ /** Does this argument text contain glob characters? */
32
+ export function hasGlob(s) {
33
+ return /[*?]/.test(s);
34
+ }
35
+ /**
36
+ * Parse a Unix-style argv. Supports:
37
+ * -a -abc (bundled) --long (with =value or following value via valueOpts)
38
+ */
39
+ export function parseArgs(args, valueOpts = new Set()) {
40
+ const flags = new Set();
41
+ const longs = new Set();
42
+ const operands = [];
43
+ const values = new Map();
44
+ let i = 0;
45
+ let onlyOperands = false;
46
+ while (i < args.length) {
47
+ const a = args[i].toDisplay;
48
+ if (!onlyOperands && a === '--') {
49
+ onlyOperands = true;
50
+ }
51
+ else if (!onlyOperands && a.startsWith('--')) {
52
+ const eq = a.indexOf('=');
53
+ if (eq >= 0) {
54
+ longs.add(a.slice(0, eq));
55
+ values.set(a.slice(0, eq), a.slice(eq + 1));
56
+ }
57
+ else if (valueOpts.has(a)) {
58
+ longs.add(a);
59
+ if (i + 1 < args.length) {
60
+ values.set(a, args[i + 1].toDisplay);
61
+ i++;
62
+ }
63
+ }
64
+ else {
65
+ longs.add(a);
66
+ }
67
+ }
68
+ else if (!onlyOperands && a.startsWith('-') && a.length > 1) {
69
+ for (const c of a.slice(1))
70
+ flags.add(c);
71
+ }
72
+ else {
73
+ operands.push(a);
74
+ }
75
+ i++;
76
+ }
77
+ return { flags, longs, operands, values };
78
+ }
79
+ /**
80
+ * Parse argv while keeping operand *Words* (so they can still be translated
81
+ * with exprOfWord / operandExpr). Supports:
82
+ * -a -abc --long --long=v --long v -n 5 -n5
83
+ * shortValues: single-char options that consume a value (e.g. ['n']).
84
+ */
85
+ export function parseWords(args, shortValues = [], longValues = []) {
86
+ const flags = new Set();
87
+ const longs = new Set();
88
+ const values = new Map();
89
+ const missingValue = [];
90
+ const operandWords = [];
91
+ let i = 0;
92
+ let onlyOperands = false;
93
+ while (i < args.length) {
94
+ const t = wordToString(args[i]);
95
+ if (!onlyOperands && t === '--') {
96
+ onlyOperands = true;
97
+ }
98
+ else if (!onlyOperands && t.startsWith('--')) {
99
+ const eq = t.indexOf('=');
100
+ if (eq >= 0) {
101
+ longs.add(t.slice(0, eq));
102
+ values.set(t.slice(0, eq), t.slice(eq + 1));
103
+ }
104
+ else if (longValues.includes(t)) {
105
+ longs.add(t);
106
+ if (i + 1 < args.length) {
107
+ values.set(t, wordToString(args[i + 1]));
108
+ i++;
109
+ }
110
+ else
111
+ missingValue.push(t);
112
+ }
113
+ else {
114
+ longs.add(t);
115
+ }
116
+ }
117
+ else if (!onlyOperands && t.startsWith('-') && t.length > 1 && !/^-?\d/.test(t.slice(1, 2))) {
118
+ // bundled short flags; a value-taking short opt consumes the rest (-n5)
119
+ const body = t.slice(1);
120
+ for (let c = 0; c < body.length; c++) {
121
+ const ch = body[c];
122
+ if (shortValues.includes(ch)) {
123
+ // value-taking options report via `values`, not `flags`
124
+ const rest = body.slice(c + 1);
125
+ if (rest) {
126
+ values.set('-' + ch, rest);
127
+ }
128
+ else if (i + 1 < args.length) {
129
+ values.set('-' + ch, wordToString(args[i + 1]));
130
+ i++;
131
+ }
132
+ else
133
+ missingValue.push('-' + ch);
134
+ break;
135
+ }
136
+ flags.add(ch);
137
+ }
138
+ }
139
+ else {
140
+ operandWords.push(args[i]);
141
+ }
142
+ i++;
143
+ }
144
+ return { flags, longs, values, missingValue, operandWords };
145
+ }
@@ -0,0 +1,55 @@
1
+ import { CommandList, Redirect, SimpleCommand, Word } from './ast.js';
2
+ import { PipelineCtx } from './registry.js';
3
+ /** Map a bash $VAR name to a PowerShell expression (usable inside $(...)). */
4
+ export declare function varExpr(name: string): string;
5
+ /** Normalize a literal POSIX-ish path to its Windows equivalent. */
6
+ export declare function normalizeLiteralPath(s: string): string;
7
+ /**
8
+ * Convert a normalized literal path (see normalizeLiteralPath) into a valid
9
+ * PowerShell string *expression*. Paths that normalize to `$env:TEMP...`
10
+ * must NOT go through single-quoting — the variable has to stay expandable.
11
+ */
12
+ export declare function pathExpr(s: string): string;
13
+ /**
14
+ * Convert a Word to a PowerShell string expression.
15
+ * Literal words become single-quoted strings; dynamic ones become
16
+ * double-quoted strings with $(...) interpolation.
17
+ */
18
+ export declare function exprOfWord(w: Word): string;
19
+ /** Literal text of a word when it contains no interpolation, else null. */
20
+ export declare function literalOfWord(w: Word): string | null;
21
+ /**
22
+ * Argument expression for an operand (file path-ish).
23
+ * Literal paths get POSIX-ish normalization (/dev/null, /tmp, /d/...).
24
+ */
25
+ export declare function operandExpr(w: Word): string;
26
+ /** Translate the inside of $(...) — pipelines only, no wrappers. */
27
+ export declare function translateCmdSub(cmdText: string): string;
28
+ export declare function translateSimple(cmd: SimpleCommand, position: PipelineCtx['position'], hasStdin: boolean): string;
29
+ export interface PipelineParts {
30
+ /** Generated function definitions (empty for single commands). */
31
+ defs: string;
32
+ /** The pipeline invocation itself. */
33
+ call: string;
34
+ }
35
+ /**
36
+ * Pipeline body. A lone command runs as a plain script-block expression;
37
+ * multi-command pipelines become generated functions chained with `|`
38
+ * (PS 5.1 forbids parenthesized expressions as non-first pipeline elements).
39
+ */
40
+ export declare function translatePipelineBody(p: {
41
+ commands: SimpleCommand[];
42
+ }): PipelineParts;
43
+ export interface SegmentPlan {
44
+ op: ';' | '&&' | '||';
45
+ /** Complete PowerShell script for one powershell.exe invocation. */
46
+ script: string;
47
+ /** All redirects collected from this segment (executor handles them). */
48
+ redirects: Redirect[];
49
+ }
50
+ export declare function translateCommandList(list: CommandList): SegmentPlan[];
51
+ /**
52
+ * Wrap a pipeline body with the Fauxnix executor contract:
53
+ * UTF-8 everywhere, bash-style exit codes, cwd/env persistence channels.
54
+ */
55
+ export declare function wrapScript(body: string): string;