fauxnix-cli 0.9.3 → 0.11.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 CHANGED
@@ -98,6 +98,10 @@ fauxnix translate "find . -name '*.log' -mtime +7 -delete"
98
98
 
99
99
  # check your environment
100
100
  fauxnix check
101
+ fauxnix doctor # check + encoding, harness config, MCP
102
+
103
+ # write user-level MCP config (idempotent; also --codex/--opencode/--kimi/--qwen)
104
+ fauxnix install --claude
101
105
 
102
106
  # run the MCP stdio server (what agent harnesses connect to)
103
107
  fauxnix mcp
@@ -109,7 +113,11 @@ with argv-style quoting — no string re-parsing, no quoting bugs.
109
113
  ## Use with your agent harness
110
114
 
111
115
  fauxnix ships an MCP stdio server exposing a `bash` tool (plus `fauxnix_translate` and
112
- `fauxnix_session`). Point any MCP-capable harness at it:
116
+ `fauxnix_session`). Point any MCP-capable harness at it with
117
+ `fauxnix install --claude` (or `--codex` / `--opencode` / `--kimi` / `--qwen`).
118
+ Idempotent; prints what changed. Manual configs below.
119
+
120
+ - **Quickstarts** — copy-paste config + a 10-command smoke: [`docs/examples/`](docs/examples/)
113
121
 
114
122
  **Claude Code**
115
123
  ```bash
@@ -145,12 +153,23 @@ TOML config: `~/.kimi-code/mcp.json`
145
153
  }
146
154
  ```
147
155
 
156
+ **Qwen Code** (`~/.qwen/settings.json`)
157
+ ```json
158
+ {
159
+ "mcpServers": {
160
+ "fauxnix": { "command": "fauxnix", "args": ["mcp"] }
161
+ }
162
+ }
163
+ ```
164
+
148
165
  **Any MCP client** — stdio server: `fauxnix mcp`. The tool name is `bash` (override with
149
166
  `FAUXNIX_TOOL_NAME`). Tool description already teaches the model the supported subset, so no
150
167
  system-prompt changes are required.
151
168
 
152
- The MCP session persists `cwd`, environment variables, `export`/`unset` and `cd -`/OLDPWD across
153
- tool calls — it behaves like a logged-in shell, not a stateless `exec`.
169
+ The MCP session persists `cwd`, environment variables, `export`/`unset`, `cd -`/OLDPWD, and
170
+ positional parameters (`set --` / `$1` / `"$@"`) across tool calls — it behaves like a logged-in
171
+ shell, not a stateless `exec`. `$0` is the MCP tool name (`bash` / `FAUXNIX_TOOL_NAME`), not a
172
+ Windows path.
154
173
 
155
174
  ## What's translated
156
175
 
@@ -164,20 +183,24 @@ development:
164
183
 
165
184
  `cp` / `mv` / `rm` / `touch` / `du` / `ls` / `ll` / `mkdir` / `rmdir` / `mktemp` / `ln` /
166
185
  `readlink` / `realpath` / `basename` / `dirname` / `stat` / `file` / `df` / `chmod` / `chown` /
167
- `diff` / `tee` / `grep` / `head` / `echo` / `printf` / `cat` / `tail` / `wc` carry a `CommandSpec`: unknown options fail with a GNU-style
186
+ `diff` / `tee` / `grep` / `head` / `echo` / `printf` / `cat` / `tail` / `wc` / `sort` / `uniq` /
187
+ `cut` / `tr` / `gzip` / `gunzip` / `zcat` / `zip` / `unzip` carry a `CommandSpec`: unknown options fail with a GNU-style
168
188
  usage error instead of being ignored (`find` stays unspec'd so predicates like `-name` still
169
- compile). Implemented GNU holes: `cp -n` / `mv -n` / `touch -c` / `tee --append` / `grep -m` /
189
+ compile; `sed`/`awk`/`egrep` stay unspec'd; `tar` stays unspec'd because it is fx-native to
190
+ `tar.exe` and unknown GNU flags must reach bsdtar). Implemented GNU holes: `cp -n` / `mv -n` / `touch -c` / `tee --append` / `grep -m` /
170
191
  `head --lines` / `du --max-depth`. `fauxnix list --json` and `docs/command-specs.md` dump the
171
192
  same metadata.
172
193
  - **shell/system**: `cd pwd export unset env printenv ps kill pkill pgrep sleep which type whoami
173
194
  id groups date uname hostname uptime free nproc clear true false test [ [[ : pushd popd dirs sudo
174
- timeout man history less more source . eval exit alias set`
195
+ timeout man history less more source . eval exit alias set shift`
175
196
  - **network**: `curl wget ping netstat ss ip ifconfig nslookup dig host`
176
197
  - **archives**: `tar gzip gunzip zcat zip unzip`
177
198
 
178
199
  Plus shell syntax: pipes, `&&` / `||` / `;`, redirections (`> >> 2> 2>&1 < &>`, `/dev/null`),
179
- quoting, `$VAR` `$(...)` command substitution, `VAR=x cmd` prefixes, `~` expansion, and
180
- POSIX-style path normalization (`/tmp`, `/d/foo` `D:\foo`).
200
+ quoting, `$VAR` `$1` `$#` `"$@"` `set --` `shift`, `${name:-word}` `${name//pat/str}`
201
+ `${name:off:len}` `${name[n]}` `${#name[@]}`, `A=(x y z)` array assignment, `$(...)` command
202
+ substitution, `VAR=x cmd` prefixes, `~` expansion, and POSIX-style path normalization
203
+ (`/tmp`, `/d/foo` → `D:\foo`).
181
204
 
182
205
  Exit codes follow bash conventions: 0 ok, 1 fail, 2 usage/serious, 127 command not found,
183
206
  124 timeout.
@@ -211,11 +234,15 @@ fauxnix optimizes for the commands agents actually run. Documented deviations:
211
234
  word expansion precedes the temporary environment).
212
235
  - `yes` is capped at 65,536 lines — PS 5.1 pipelines cannot signal upstream producers to stop, so
213
236
  an unbounded `yes | head` would hang.
214
- - `tail -f`, `eval`, `alias`, heredocs, `while`/`until`/`case`,
237
+ - `tail -f`, `eval`, `alias`, heredocs,
215
238
  `env -i`/`--ignore-environment`,
216
- and background `&` are rejected with actionable error messages instead of misbehaving.
217
- (`if/then/elif/else/fi`, `for x in ...`, backtick substitution, `command -v`, pipeline `read`,
218
- dotenv-style `source`, and word-level `$((...))` arithmetic expansion are supported.)
239
+ background `&`, and stdout redirects (`>` `>>` `&>` `&>>`) on a non-last
240
+ pipeline stage (`echo hi >f | cat`) are rejected with actionable error
241
+ messages instead of misbehaving.
242
+ (`if/then/elif/else/fi`, `for x in ...`, `while`/`until`, `case ... esac` (`;;` only; `;&`/`;;&` fail loud),
243
+ backtick substitution, `command -v`, pipeline `read`,
244
+ dotenv-style `source`, word-level `$((...))` arithmetic expansion, `A=(x y z)` arrays, and
245
+ `${name//pat/str}` / `${name:off:len}` are supported.)
219
246
  - `command -v <builtin>` prints `/usr/bin/<name>` where bash prints the bare builtin name;
220
247
  exit codes and empty-result semantics match.
221
248
  - `chmod` maps only the read-only bit; exec bits are no-ops on Windows. `chown` is a silent no-op
@@ -245,6 +272,8 @@ npm run build
245
272
  npx tsx scratch/run.mjs "any bash command" # quick live check
246
273
  ```
247
274
 
275
+ Differential vs Git Bash is opt-in (`FAUXNIX_DIFF_ORACLE=1`; skips if unset or `bash.exe` is missing — Git Bash is not required). See [`test/differential/README.md`](test/differential/README.md). This is grown from the 40-case RFC C-7 scaffold, not the 200-case 1.0 gate. The weekly oracle is opt-in via the scheduled workflow (`.github/workflows/differential.yml`) when Git Bash is on the runner.
276
+
248
277
  Architecture map: `src/parser.ts` (bash subset → AST) · `src/translator.ts` (AST → PowerShell +
249
278
  executor wrapper) · `src/executor.ts` (spawn, redirects, session persistence) ·
250
279
  `src/commands/*.ts` (per-command generators) · `src/mcp.ts` (MCP server) · `src/cli.ts`.
@@ -252,6 +281,11 @@ executor wrapper) · `src/executor.ts` (spawn, redirects, session persistence)
252
281
  Roadmap: [docs/rfc-roadmap-to-1.0.md](docs/rfc-roadmap-to-1.0.md) — tracks, milestones,
253
282
  and the RFC process for proposing waves.
254
283
 
284
+ ## Security
285
+
286
+ Trust model, host protocol, kill semantics, network guard, and reporting:
287
+ [SECURITY.md](SECURITY.md).
288
+
255
289
  ## License
256
290
 
257
291
  MIT © 20000419
package/dist/ast.d.ts CHANGED
@@ -6,13 +6,16 @@
6
6
  * - lists: cmd1 ; cmd2 && cmd3 || cmd4 (newlines act as ';')
7
7
  * - redirections: > >> 2> 2>> &> 2>&1 1>&2 <
8
8
  * - quoting: 'literal' "interp $VAR $(cmd)"
9
- * - variables: $VAR ${VAR} ${VAR[n]} plus special cases ($HOME $USER $PATH ...)
9
+ * - variables: $VAR ${VAR} ${VAR[n]} ${name//pat/str} ${name:off:len}
10
+ * plus special cases ($HOME $USER $PATH $1 $# $@ $0 ...)
10
11
  * - command substitution: $(...) and `...` (recursively translated)
11
12
  * - arithmetic expansion: $((...)) (existing fx-arith engine)
12
13
  * - env assignment prefix: VAR=value cmd
14
+ * - control: if/then/elif/else/fi, for-in, while/until, case
15
+ * - array assignment: A=(x y z) (sidecar FAUXNIX_ARRS)
13
16
  *
14
17
  * Explicitly unsupported (parser throws a helpful FauxnixError):
15
- * heredocs, subshells (...), background &, while/until/case,
18
+ * heredocs, subshells (...), background &,
16
19
  * globs inside quotes, process substitution <(...).
17
20
  */
18
21
  export interface CommandList {
@@ -25,7 +28,7 @@ export interface ListSegment {
25
28
  /** ';' for the first segment, otherwise the operator seen before this one. */
26
29
  op: ';' | '&&' | '||';
27
30
  }
28
- export type ShellCommand = SimpleCommand | IfCommand | ForCommand;
31
+ export type ShellCommand = SimpleCommand | IfCommand | ForCommand | WhileCommand | CaseCommand;
29
32
  export interface Pipeline {
30
33
  kind: 'Pipeline';
31
34
  commands: ShellCommand[];
@@ -44,6 +47,24 @@ export interface ForCommand {
44
47
  body: CommandList;
45
48
  redirects: Redirect[];
46
49
  }
50
+ export interface WhileCommand {
51
+ kind: 'While';
52
+ /** true for `until TEST; do BODY; done`. */
53
+ until: boolean;
54
+ test: CommandList;
55
+ body: CommandList;
56
+ redirects: Redirect[];
57
+ }
58
+ export interface CaseArm {
59
+ patterns: Word[];
60
+ body: CommandList;
61
+ }
62
+ export interface CaseCommand {
63
+ kind: 'Case';
64
+ word: Word;
65
+ arms: CaseArm[];
66
+ redirects: Redirect[];
67
+ }
47
68
  export interface SimpleCommand {
48
69
  kind: 'SimpleCommand';
49
70
  /** `VAR=value` prefixes before the command name. */
@@ -55,7 +76,10 @@ export interface SimpleCommand {
55
76
  }
56
77
  export interface Assignment {
57
78
  name: string;
79
+ /** Scalar value; for arrays, first element (or empty Word) so export paths stay valid. */
58
80
  value: Word;
81
+ /** Set ⇒ bash array assignment `A=(x y z)`. */
82
+ values?: Word[];
59
83
  }
60
84
  export type RedirectOp = '>' | '>>' | '2>' | '2>>' | '&>' | '&>>' | '2>&1' | '1>&2' | '<';
61
85
  export interface Redirect {
@@ -86,6 +110,17 @@ export type WordPart = {
86
110
  };
87
111
  /** `${#name}` / `${#name[@]}` — string/array length expansion. */
88
112
  length?: boolean;
113
+ /** `${name/pat/str}` (first) / `${name//pat/str}` (global). Pattern is a bash glob. */
114
+ replace?: {
115
+ global: boolean;
116
+ pat: string;
117
+ repl: string;
118
+ };
119
+ /** `${name:offset}` / `${name:offset:length}` — scalar substring. */
120
+ slice?: {
121
+ offset: string;
122
+ length?: string;
123
+ };
89
124
  } | {
90
125
  kind: 'CmdSub';
91
126
  cmd: string;
package/dist/ast.js CHANGED
@@ -6,13 +6,16 @@
6
6
  * - lists: cmd1 ; cmd2 && cmd3 || cmd4 (newlines act as ';')
7
7
  * - redirections: > >> 2> 2>> &> 2>&1 1>&2 <
8
8
  * - quoting: 'literal' "interp $VAR $(cmd)"
9
- * - variables: $VAR ${VAR} ${VAR[n]} plus special cases ($HOME $USER $PATH ...)
9
+ * - variables: $VAR ${VAR} ${VAR[n]} ${name//pat/str} ${name:off:len}
10
+ * plus special cases ($HOME $USER $PATH $1 $# $@ $0 ...)
10
11
  * - command substitution: $(...) and `...` (recursively translated)
11
12
  * - arithmetic expansion: $((...)) (existing fx-arith engine)
12
13
  * - env assignment prefix: VAR=value cmd
14
+ * - control: if/then/elif/else/fi, for-in, while/until, case
15
+ * - array assignment: A=(x y z) (sidecar FAUXNIX_ARRS)
13
16
  *
14
17
  * Explicitly unsupported (parser throws a helpful FauxnixError):
15
- * heredocs, subshells (...), background &, while/until/case,
18
+ * heredocs, subshells (...), background &,
16
19
  * globs inside quotes, process substitution <(...).
17
20
  */
18
21
  export function wordToString(w) {
@@ -37,6 +40,23 @@ function partToString(p) {
37
40
  case 'DoubleQuoted':
38
41
  return p.parts.map(partToString).join('');
39
42
  case 'Var':
43
+ if (p.replace) {
44
+ const sep = p.replace.global ? '//' : '/';
45
+ return `\${${p.name}${sep}${p.replace.pat}/${p.replace.repl}}`;
46
+ }
47
+ if (p.slice) {
48
+ return p.slice.length !== undefined
49
+ ? `\${${p.name}:${p.slice.offset}:${p.slice.length}}`
50
+ : `\${${p.name}:${p.slice.offset}}`;
51
+ }
52
+ if (p.length) {
53
+ return p.index !== undefined
54
+ ? `\${#${p.name}[${p.index}]}`
55
+ : `\${#${p.name}}`;
56
+ }
57
+ if (p.param) {
58
+ return `\${${p.name}${p.param.op}${p.param.word}}`;
59
+ }
40
60
  return p.index !== undefined ? `\${${p.name}[${p.index}]}` : `$${p.name}`;
41
61
  case 'CmdSub':
42
62
  return '$(' + p.cmd + ')';
package/dist/cli.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  import './commands/install-all.js';
2
+ export declare const USAGE = "fauxnix \u2014 run Linux-style commands on Windows via PowerShell translation\n\nUsage:\n fauxnix \"ls -la | head -5\" translate + execute a bash-style command\n fauxnix -c \"cmd\" same as above\n fauxnix translate \"cmd\" show the PowerShell translation only\n fauxnix mcp start the MCP stdio server (for agent harnesses)\n fauxnix list list translated commands\n fauxnix list --json same list as machine-readable capability metadata\n fauxnix list --markdown CommandSpec tables (same text as docs/command-specs.md)\n fauxnix check verify the local PowerShell environment\n fauxnix doctor check + encoding, harness config, MCP readiness\n fauxnix install --claude|--codex|--opencode|--kimi|--qwen\n fauxnix --version\n\nNotes:\n Unknown commands (git, node, npm, python, cargo, ...) pass through and run natively.";
2
3
  export declare function runCli(argv: string[]): Promise<void>;
package/dist/cli.js CHANGED
@@ -5,9 +5,11 @@ import { translateCommandList } from './translator.js';
5
5
  import { listCommandsJson, registeredNames, specsMarkdown } from './registry.js';
6
6
  import { encodeCommand } from './encoding.js';
7
7
  import { startMcpServer } from './mcp.js';
8
+ import { collectDoctorReport } from './doctor.js';
9
+ import { runInstall } from './install.js';
8
10
  import { packageVersion } from './version.js';
9
11
  import './commands/install-all.js';
10
- const USAGE = `fauxnix — run Linux-style commands on Windows via PowerShell translation
12
+ export const USAGE = `fauxnix — run Linux-style commands on Windows via PowerShell translation
11
13
 
12
14
  Usage:
13
15
  fauxnix "ls -la | head -5" translate + execute a bash-style command
@@ -18,6 +20,8 @@ Usage:
18
20
  fauxnix list --json same list as machine-readable capability metadata
19
21
  fauxnix list --markdown CommandSpec tables (same text as docs/command-specs.md)
20
22
  fauxnix check verify the local PowerShell environment
23
+ fauxnix doctor check + encoding, harness config, MCP readiness
24
+ fauxnix install --claude|--codex|--opencode|--kimi|--qwen
21
25
  fauxnix --version
22
26
 
23
27
  Notes:
@@ -52,6 +56,18 @@ export async function runCli(argv) {
52
56
  await runCheck();
53
57
  return;
54
58
  }
59
+ if (verb === 'doctor') {
60
+ await runDoctor();
61
+ return;
62
+ }
63
+ if (verb === 'install') {
64
+ const result = runInstall(rest);
65
+ for (const line of result.lines)
66
+ console.log(line);
67
+ if (!result.ok)
68
+ process.exitCode = 1;
69
+ return;
70
+ }
55
71
  if (verb === 'mcp') {
56
72
  await startMcpServer();
57
73
  return;
@@ -80,6 +96,14 @@ export async function runCli(argv) {
80
96
  process.exit(result.exitCode);
81
97
  }
82
98
  const PS_ARGS = ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass'];
99
+ async function runDoctor() {
100
+ await runCheck();
101
+ const report = await collectDoctorReport();
102
+ for (const line of report.lines)
103
+ console.log(line);
104
+ if (!report.ok)
105
+ process.exitCode = 1;
106
+ }
83
107
  async function runCheck() {
84
108
  console.log('powershell : powershell.exe (Windows built-in)');
85
109
  const probeCmd = '$PSVersionTable.PSVersion.ToString()';
@@ -1,2 +1,3 @@
1
- import { Handler } from '../registry.js';
1
+ import { CommandSpec, Handler } from '../registry.js';
2
+ export declare const specs: CommandSpec[];
2
3
  export declare const handlers: Record<string, Handler>;
@@ -201,6 +201,8 @@ const gunzip = (args, ctx) => gzBlock(args, ctx, { decompress: true });
201
201
  const zcat = (args, ctx) => gzBlock(args, ctx, { decompress: true, stdout: true });
202
202
  /* ------------------------------------------------------------------ */
203
203
  /* tar — Windows 10+ ships bsdtar as tar.exe, pass everything through */
204
+ /* Intentionally unspec'd: registerSpec fail-loud would reject GNU */
205
+ /* flags bsdtar accepts (--numeric-owner, …). Same class as find. */
204
206
  /* ------------------------------------------------------------------ */
205
207
  const tar = (args) => {
206
208
  return [
@@ -374,8 +376,87 @@ const unzip = (args) => {
374
376
  ].join('\n');
375
377
  };
376
378
  /* ------------------------------------------------------------------ */
377
- /* exports */
379
+ /* CommandSpec (C-5 archive slice) */
380
+ /* tar stays unspec'd: fx-native to tar.exe; unknown GNU flags must */
381
+ /* reach bsdtar. gzip -f is a no-op today and the stdin-from-terminal */
382
+ /* message advertises it, so it is unsupported (force from terminal) */
383
+ /* rather than a silent ignore. */
378
384
  /* ------------------------------------------------------------------ */
385
+ const gzipOptions = [
386
+ { short: 'd', long: '--decompress', support: 'implemented' },
387
+ { long: '--uncompress', support: 'implemented' },
388
+ { short: 'k', long: '--keep', support: 'implemented' },
389
+ { short: 'c', long: '--stdout', support: 'implemented' },
390
+ { long: '--to-stdout', support: 'implemented' },
391
+ { short: 't', long: '--test', support: 'implemented' },
392
+ { short: '1', long: '--fast', support: 'implemented' },
393
+ { short: '2', support: 'implemented' },
394
+ { short: '3', support: 'implemented' },
395
+ { short: '4', support: 'implemented' },
396
+ { short: '5', support: 'implemented' },
397
+ { short: '6', support: 'implemented' },
398
+ { short: '7', support: 'implemented' },
399
+ { short: '8', support: 'implemented' },
400
+ { short: '9', long: '--best', support: 'implemented' },
401
+ { short: 'f', long: '--force', support: 'unsupported', reason: 'force from terminal' },
402
+ { short: 'q', long: '--quiet', support: 'unsupported', reason: 'quiet' },
403
+ { short: 'v', long: '--verbose', support: 'unsupported', reason: 'verbose' },
404
+ { short: 'n', long: '--no-name', support: 'unsupported', reason: 'no-name' },
405
+ { short: 'r', long: '--recursive', support: 'unsupported', reason: 'recursive' },
406
+ { short: 'S', long: '--suffix', takesValue: true, support: 'unsupported', reason: 'suffix' },
407
+ ];
408
+ export const specs = [
409
+ {
410
+ names: ['gzip'],
411
+ options: gzipOptions,
412
+ effects: ['read', 'write', 'delete'],
413
+ platform: 'windows-ps51',
414
+ dispatch: 'translated',
415
+ handler: gzip,
416
+ },
417
+ {
418
+ names: ['gunzip'],
419
+ options: gzipOptions,
420
+ effects: ['read', 'write', 'delete'],
421
+ platform: 'windows-ps51',
422
+ dispatch: 'translated',
423
+ handler: gunzip,
424
+ },
425
+ {
426
+ names: ['zcat'],
427
+ options: gzipOptions,
428
+ effects: ['read'],
429
+ platform: 'windows-ps51',
430
+ dispatch: 'translated',
431
+ handler: zcat,
432
+ },
433
+ {
434
+ names: ['zip'],
435
+ options: [
436
+ // Compress-Archive always recurses directory inputs; -q is already quiet.
437
+ { short: 'r', support: 'implemented' },
438
+ { short: 'q', support: 'implemented' },
439
+ { short: 'x', long: '--exclude', takesValue: true, support: 'unsupported', reason: 'exclude patterns' },
440
+ ],
441
+ effects: ['read', 'write'],
442
+ platform: 'windows-ps51',
443
+ dispatch: 'translated',
444
+ handler: zip,
445
+ },
446
+ {
447
+ names: ['unzip'],
448
+ options: [
449
+ { short: 'l', support: 'implemented' },
450
+ { short: 'o', support: 'implemented' },
451
+ { short: 'q', support: 'implemented' },
452
+ { short: 'd', long: '--directory', takesValue: true, support: 'implemented' },
453
+ ],
454
+ effects: ['read', 'write'],
455
+ platform: 'windows-ps51',
456
+ dispatch: 'translated',
457
+ handler: unzip,
458
+ },
459
+ ];
379
460
  export const handlers = {
380
461
  tar,
381
462
  gzip,
@@ -4,7 +4,7 @@ import { handlers as textFilters, specs as textFilterSpecs } from './text-filter
4
4
  import { handlers as textIo, specs as textIoSpecs } from './text-io.js';
5
5
  import { handlers as sysinfo } from './sysinfo.js';
6
6
  import { handlers as net } from './net.js';
7
- import { handlers as archive } from './archive.js';
7
+ import { handlers as archive, specs as archiveSpecs } from './archive.js';
8
8
  /** Register every built-in Linux command translator. Side-effectful import. */
9
9
  export function installAll() {
10
10
  registerAll(files);
@@ -16,5 +16,6 @@ export function installAll() {
16
16
  registerSpecs(fileSpecs);
17
17
  registerSpecs(textIoSpecs);
18
18
  registerSpecs(textFilterSpecs);
19
+ registerSpecs(archiveSpecs);
19
20
  }
20
21
  installAll();
@@ -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, arithExpr, setArithHelperPreamble, } from '../translator.js';
3
+ import { argListExpr, exprOfWord, operandExpr, translateSimple, wrapTempEnv, encodeSetValExpr, escapeDq, translateCmdSub, normalizeLiteralPath, pathExpr, paramExpr, varExpr, varExtraOf, arithExpr, setArithHelperPreamble, isSpecialShellVar, } from '../translator.js';
4
4
  import { handlers as textIoHandlers } from './text-io.js';
5
5
  /* ------------------------------------------------------------------ */
6
6
  /* Shared TS helpers */
@@ -210,7 +210,7 @@ const env = (args, ctx) => {
210
210
  }
211
211
  if (t === '-i' || t === '--ignore-environment') {
212
212
  return ('[Console]::Error.WriteLine(' +
213
- psStr('fauxnix: env -i/--ignore-environment is not supported (would silently keep inherited secrets)') +
213
+ psStr('fauxnix: env -i/--ignore-environment is not supported (would silently keep inherited secrets). Use env -u NAME or unset first instead.') +
214
214
  '); $script:fx_exit = 2');
215
215
  }
216
216
  if (t === '-u' || t === '--unset') {
@@ -1398,6 +1398,10 @@ function kshExprOfWord(w) {
1398
1398
  return arithExpr(expanded[0].parts);
1399
1399
  }
1400
1400
  if (!tilde && expanded.length === 1 && expanded[0].kind === 'Var') {
1401
+ const extra = varExtraOf(expanded[0]);
1402
+ if (extra && (extra.replace || extra.slice)) {
1403
+ return varExpr(expanded[0].name, expanded[0].index, expanded[0].param, expanded[0].length === true, extra);
1404
+ }
1401
1405
  if (expanded[0].param) {
1402
1406
  return paramExpr(expanded[0].name, expanded[0].param.op, expanded[0].param.word);
1403
1407
  }
@@ -1407,6 +1411,8 @@ function kshExprOfWord(w) {
1407
1411
  if (expanded[0].index !== undefined) {
1408
1412
  return '(fx-subget ' + psStr(expanded[0].name) + ' ' + psStr(expanded[0].index) + ')';
1409
1413
  }
1414
+ if (isSpecialShellVar(expanded[0].name))
1415
+ return varExpr(expanded[0].name);
1410
1416
  return '(fx-envget ' + psStr(expanded[0].name) + ')';
1411
1417
  }
1412
1418
  const literal = !tilde && expanded.every((p) => p.kind === 'Text' || p.kind === 'SingleQuoted');
@@ -1427,14 +1433,28 @@ function kshExprOfWord(w) {
1427
1433
  for (const q of p.parts)
1428
1434
  emitPart(q);
1429
1435
  break;
1430
- case 'Var':
1431
- out +=
1432
- p.param
1433
- ? '$(' + paramExpr(p.name, p.param.op, p.param.word) + ')'
1434
- : p.index !== undefined
1435
- ? '$(fx-subget ' + psStr(p.name) + ' ' + psStr(p.index) + ')'
1436
- : '$(fx-envget ' + psStr(p.name) + ')';
1436
+ case 'Var': {
1437
+ const extra = varExtraOf(p);
1438
+ if (extra && (extra.replace || extra.slice)) {
1439
+ out += '$(' + varExpr(p.name, p.index, p.param, p.length === true, extra) + ')';
1440
+ }
1441
+ else if (p.param) {
1442
+ out += '$(' + paramExpr(p.name, p.param.op, p.param.word) + ')';
1443
+ }
1444
+ else if (p.length) {
1445
+ out += '$(' + varExpr(p.name, p.index, undefined, true) + ')';
1446
+ }
1447
+ else if (p.index !== undefined) {
1448
+ out += '$(fx-subget ' + psStr(p.name) + ' ' + psStr(p.index) + ')';
1449
+ }
1450
+ else if (isSpecialShellVar(p.name)) {
1451
+ out += '$(' + varExpr(p.name) + ')';
1452
+ }
1453
+ else {
1454
+ out += '$(fx-envget ' + psStr(p.name) + ')';
1455
+ }
1437
1456
  break;
1457
+ }
1438
1458
  case 'CmdSub':
1439
1459
  // [[ ]] does not IFS-split, so keep the newline contract.
1440
1460
  out += '$(' + translateCmdSub(p.cmd, true) + ')';
@@ -2478,7 +2498,7 @@ const source = (args) => {
2478
2498
  ].join('\n');
2479
2499
  };
2480
2500
  const evalCmd = () => {
2481
- return ("[Console]::Error.WriteLine('fauxnix: eval is not supported; pass the command itself'); $script:fx_exit = 1");
2501
+ return ("[Console]::Error.WriteLine('fauxnix: eval is not supported; pass the command itself instead'); $script:fx_exit = 1");
2482
2502
  };
2483
2503
  const exitCmd = (args) => {
2484
2504
  const w = args[0];
@@ -2510,9 +2530,15 @@ const alias = (args) => {
2510
2530
  });
2511
2531
  if (!has)
2512
2532
  return '';
2513
- return "[Console]::Error.WriteLine('fauxnix: alias is not supported'); $script:fx_exit = 1";
2533
+ return "[Console]::Error.WriteLine('fauxnix: alias is not supported. Invoke the real command instead.'); $script:fx_exit = 1";
2514
2534
  };
2515
2535
  const set = (args) => {
2536
+ if (args.length > 0 && wordToString(args[0]) === '--') {
2537
+ return [
2538
+ '$fx_pv = [object[]](' + argListExpr(args.slice(1)) + ')',
2539
+ 'fx-posset $fx_pv',
2540
+ ].join('\n');
2541
+ }
2516
2542
  const raw = args.map(wordToString);
2517
2543
  const unsupported = raw.filter((t) => t === '-e' ||
2518
2544
  t === '-u' ||
@@ -2529,11 +2555,20 @@ const set = (args) => {
2529
2555
  /^-.*[eux]/.test(t));
2530
2556
  if (unsupported.length > 0) {
2531
2557
  return ('[Console]::Error.WriteLine(' +
2532
- psStr('fauxnix: set -e/-u/-x is not supported (would silently lie); use explicit || exit') +
2558
+ psStr('fauxnix: set -e/-u/-x is not supported (would silently lie); use explicit || exit instead') +
2533
2559
  '); $script:fx_exit = 2');
2534
2560
  }
2535
2561
  return '';
2536
2562
  };
2563
+ const shiftCmd = (args) => {
2564
+ if (args.length === 0)
2565
+ return 'fx-posshift 1';
2566
+ return [
2567
+ '$fx_sn = [string](' + exprOfWord(args[0]) + ')',
2568
+ "if ($fx_sn -notmatch '^-?[0-9]+$') { [Console]::Error.WriteLine('bash: shift: ' + $fx_sn + ': numeric argument required'); $script:fx_exit = 1 }",
2569
+ 'else { fx-posshift $fx_sn }',
2570
+ ].join('\n');
2571
+ };
2537
2572
  /* ------------------------------------------------------------------ */
2538
2573
  export const handlers = {
2539
2574
  cd,
@@ -2582,4 +2617,5 @@ export const handlers = {
2582
2617
  exit: exitCmd,
2583
2618
  alias,
2584
2619
  set,
2620
+ shift: shiftCmd,
2585
2621
  };
@@ -1,3 +1,4 @@
1
1
  import { CommandSpec, Handler } from '../registry.js';
2
2
  export declare const specs: CommandSpec[];
3
+ /** sed/awk stay unspec'd (custom script parsers). egrep injects -E and stays its own handler. */
3
4
  export declare const handlers: Record<string, Handler>;
@@ -2289,8 +2289,9 @@ function parseCutList(list) {
2289
2289
  const cut = (args) => {
2290
2290
  const { flags, longs, values, operandWords } = parseWords(args, ['d', 'f', 'c', 'b'], []);
2291
2291
  const complement = longs.has('--complement');
2292
- const charsMode = flags.has('c') || flags.has('b');
2293
- const fieldsMode = flags.has('f');
2292
+ // -f/-c/-b are value-taking; parseWords records them in `values`, not `flags`.
2293
+ const charsMode = values.has('-c') || values.has('-b');
2294
+ const fieldsMode = values.has('-f');
2294
2295
  const suppress = flags.has('s');
2295
2296
  if (charsMode && fieldsMode) {
2296
2297
  throw new FauxnixParseError('fauxnix: cut only one type of list may be specified');
@@ -2527,7 +2528,67 @@ export const specs = [
2527
2528
  usageExit: 2,
2528
2529
  handler: grep,
2529
2530
  },
2531
+ {
2532
+ names: ['sort'],
2533
+ options: [
2534
+ { short: 'r', long: '--reverse', support: 'implemented' },
2535
+ { short: 'n', long: '--numeric-sort', support: 'implemented' },
2536
+ { short: 'u', long: '--unique', support: 'implemented' },
2537
+ { short: 'f', long: '--ignore-case', support: 'implemented' },
2538
+ { short: 'b', long: '--ignore-leading-blanks', support: 'implemented' },
2539
+ { short: 't', takesValue: true, support: 'implemented' },
2540
+ { short: 'k', takesValue: true, support: 'implemented' },
2541
+ { short: 'z', long: '--zero-terminated', support: 'unsupported', reason: 'NUL-terminated records' },
2542
+ ],
2543
+ effects: ['read'],
2544
+ platform: 'windows-ps51',
2545
+ dispatch: 'translated',
2546
+ usageExit: 2,
2547
+ handler: sort,
2548
+ },
2549
+ {
2550
+ names: ['uniq'],
2551
+ options: [
2552
+ { short: 'c', support: 'implemented' },
2553
+ { short: 'd', support: 'implemented' },
2554
+ { short: 'u', support: 'implemented' },
2555
+ { short: 'i', support: 'implemented' },
2556
+ ],
2557
+ effects: ['read'],
2558
+ platform: 'windows-ps51',
2559
+ dispatch: 'translated',
2560
+ handler: uniq,
2561
+ },
2562
+ {
2563
+ names: ['cut'],
2564
+ options: [
2565
+ { short: 'd', takesValue: true, support: 'implemented' },
2566
+ { short: 'f', takesValue: true, support: 'implemented' },
2567
+ { short: 'c', takesValue: true, support: 'implemented' },
2568
+ { short: 'b', takesValue: true, support: 'implemented' },
2569
+ { short: 's', support: 'implemented' },
2570
+ { long: '--complement', support: 'implemented' },
2571
+ ],
2572
+ effects: ['read'],
2573
+ platform: 'windows-ps51',
2574
+ dispatch: 'translated',
2575
+ handler: cut,
2576
+ },
2577
+ {
2578
+ names: ['tr'],
2579
+ options: [
2580
+ { short: 'd', support: 'implemented' },
2581
+ { short: 's', support: 'implemented' },
2582
+ { short: 'c', long: '--complement', support: 'unsupported', reason: 'complement' },
2583
+ { short: 'C', support: 'unsupported', reason: 'complement' },
2584
+ ],
2585
+ effects: ['read'],
2586
+ platform: 'windows-ps51',
2587
+ dispatch: 'translated',
2588
+ handler: tr,
2589
+ },
2530
2590
  ];
2591
+ /** sed/awk stay unspec'd (custom script parsers). egrep injects -E and stays its own handler. */
2531
2592
  export const handlers = {
2532
2593
  egrep: (args, ctx) => grep([[{ kind: 'Text', text: '-E' }], ...args], ctx), // egrep = grep -E
2533
2594
  sed,