Skip to main content

zsh/ported/
builtin.rs

1//! Direct port of `Src/builtin.c` — the master registration site for
2//! the in-shell builtin commands. The C source is 7608 lines; the
3//! actual `bin_*` handler bodies were ported organically into
4//! `src/ported/exec.rs` and `src/ported/builtins/*.rs` long before
5//! this file existed. This file scaffolds:
6//!
7//! Builtins in the main executable                                          // c:38
8//! Builtin Command Hash Table Functions                                     // c:140
9//!
10//!   * the `BINF_*` flag bits from `Src/zsh.h:1457-1486`,
11//!   * the `BIN_*` dispatch IDs from `Src/hashtable.h:34-66`,
12//!   * the `Builtin` descriptor and the static `BUILTINS[]` table
13//!     (1:1 mirror of `static struct builtin builtins[]` at
14//!     `Src/builtin.c:40-137`),
15//!   * `createbuiltintable()` (`Src/builtin.c:149`) — building the
16//!     name → descriptor lookup the rest of the shell consults via
17//!     `builtintab`.
18//!
19//! Each row's `handler` field names the canonical Rust port of the
20//! C handler so future work can wire them up without re-discovering
21//! the mapping. When the handler lives in `crate::ported::builtins`,
22//! the comment cites the file; when it lives in `exec.rs`'s
23//! `Executor` impl, that's noted too.
24
25use std::collections::HashMap;
26use std::sync::OnceLock;
27use crate::ported::zsh_h::{PRINT_WHENCE_WORD, PRINT_WHENCE_CSH};
28use crate::ported::zsh_h::EMULATE_ZSH;
29use crate::ported::zsh_h::{options, MAX_OPS, XTRACE, BINF_KEEPNUM, ERRFLAG_ERROR};
30use crate::ported::modules::parameter::DIRSTACK;
31use std::sync::atomic::Ordering;
32use crate::ported::zsh_h::{OPT_HASARG, OPT_ARG, PM_INTEGER, PM_EFLOAT, PM_FFLOAT};
33use crate::ported::zsh_h::{PM_LEFT, PM_RIGHT_B, PM_RIGHT_Z};
34use crate::ported::zsh_h::{OPT_MINUS, OPT_ISSET, PM_UNDEFINED};
35use crate::ported::zsh_h::PM_LOADDIR;
36use crate::ported::zsh_h::MFF_STR;
37use crate::ported::zsh_h::{PM_ABSPATH_USED, FS_FUNC};
38use crate::ported::zsh_h::eprog;
39use crate::ported::zsh_h::{STAT_LOCKED, STAT_NOPRINT, STAT_STOPPED};
40use std::io::Read;
41use crate::ported::zsh_h::{OPT_PLUS, PM_UNALIASED, PM_TAGGED, PM_TAGGED_LOCAL, PM_WARNNESTED, PM_ZSHSTORED, PM_KSHSTORED, PM_CUR_FPATH};
42use crate::ported::math::{matheval, mnumber, MN_INTEGER};
43use crate::ported::utils::{getkeystring, getkeystring_with, quotedzputs, GETKEYS_PRINT};
44use crate::ported::zsh_h::HIST_FOREIGN;
45use crate::ported::zsh_h::{HFILE_APPEND, HFILE_SKIPOLD, HFILE_USE_OPTIONS};
46use crate::ported::zsh_h::{EMULATION, TYPESET_OPTSTR, PM_HASHED, PM_HIDEVAL, PM_LOWER, PM_UPPER, PM_TIED, PM_LOCAL, PM_NAMEREF, PM_READONLY, PM_ARRAY, PRINT_TYPESET, PRINT_LINE, PRINT_TYPE, PRINT_NAMEONLY, PRINT_POSIX_EXPORT, PRINT_POSIX_READONLY, PRINT_WITH_NAMESPACE, EMULATE_KSH};
47use crate::ported::zsh_h::{PRINT_WHENCE_VERBOSE, PRINT_WHENCE_SIMPLE, PRINT_WHENCE_FUNCDEF, PRINT_LIST};
48use crate::ported::math::mathevali;
49use crate::ported::zsh_h::DISABLED;
50use crate::ported::zsh_h::nameddir;
51use crate::ported::zsh_h::{ALIAS_GLOBAL, ALIAS_SUFFIX};
52use crate::ported::hashtable::{aliastab_lock, sufaliastab_lock, Alias};
53use crate::ported::zsh_h::{EMULATE_CSH, EMULATE_SH};
54
55// === Imports needed by the methods moved from exec.rs (below) ===
56#[allow(unused_imports)]
57use std::{env, fs, io, io::Write, path::Path, path::PathBuf};
58#[allow(unused_imports)]
59use indexmap::IndexMap;
60#[allow(unused_imports)]
61use crate::ported::exec::{
62    self,  BUILTIN_NAMES,
63    format_int_in_base,
64};
65use crate::ported::utils::{zerr, zerrnam, zwarn, zwarnnam};
66use crate::func_body_fmt::FuncBodyFmt;
67#[allow(unused_imports)]
68use crate::ported::options::ZSH_OPTIONS_SET;
69#[allow(unused_imports)]
70use crate::parse::{Redirect, ShellCommand};
71#[allow(unused_imports)]
72use crate::zwc::ZwcFile;
73
74
75// ---------------------------------------------------------------------------
76// BIN_* dispatch IDs.
77// Direct port of `Src/hashtable.h:34-70`. These are the integer
78// discriminators handlers use when one C function backs multiple
79// builtin names (e.g. `bin_fg` covers fg/bg/jobs/wait/disown).
80// ---------------------------------------------------------------------------
81
82// BIN_* constants moved to `crate::ported::hashtable_h` per the C
83// header layout (Src/hashtable.h:34-70). Re-exported here so existing
84// `crate::ported::builtin::BIN_X` paths keep resolving.
85pub use crate::ported::hashtable_h::{
86    BIN_TYPESET, BIN_BG, BIN_FG, BIN_JOBS, BIN_WAIT, BIN_DISOWN,
87    BIN_BREAK, BIN_CONTINUE, BIN_EXIT, BIN_RETURN, BIN_CD,
88    BIN_POPD, BIN_PUSHD, BIN_PRINT, BIN_EVAL, BIN_SCHED, BIN_FC,
89    BIN_R, BIN_PUSHLINE, BIN_LOGOUT, BIN_TEST, BIN_BRACKET,
90    BIN_READONLY, BIN_ECHO, BIN_DISABLE, BIN_ENABLE, BIN_PRINTF,
91    BIN_COMMAND, BIN_UNHASH, BIN_UNALIAS, BIN_UNFUNCTION,
92    BIN_UNSET, BIN_EXPORT, BIN_SETOPT, BIN_UNSETOPT,
93};
94use crate::zsh_h::{builtin, BINF_ASSIGN, BINF_BUILTIN, BINF_COMMAND, BINF_DASH, BINF_DASHDASHVALID, BINF_EXEC, BINF_HANDLES_OPTS, BINF_MAGICEQUALS, BINF_NOGLOB, BINF_PLUSOPTS, BINF_PREFIX, BINF_PRINTOPTS, BINF_PSPECIAL, BINF_SKIPDASH, BINF_SKIPINVALID, hashnode, NULLBINCMD, isset};
95
96// Local builders that construct C-shape `builtin` rows for the
97// static registration table below. They mirror the
98// `BUILTIN(...)` / `BIN_PREFIX(...)` macros in `Src/zsh.h:1450-1452`,
99// taking `u32` flag bitsets (BINF_*) and a `&str` handler-name
100// column used only for documentation/wiring lookup — handler
101// function pointers themselves are wired up later in
102// `Executor::register_builtins` (`src/ported/exec.rs`).
103//
104// The `handler` arg was previously a `_handler_name: &'static str` that
105// was discarded — `handlerfunc` always ended up `NULLBINCMD`, so
106// `execbuiltin`'s c:506 `(*handlerfunc)(...)` dispatch was unreachable.
107// Now the descriptor carries the actual port-side `HandlerFunc` so
108// `execbuiltin` can parse flags and call through to the real builtin.
109#[allow(non_snake_case)]
110pub fn BUILTIN(
111    name: &str,
112    flags: u32,
113    handler: Option<crate::ported::zsh_h::HandlerFunc>,
114    min: i32,
115    max: i32,
116    funcid: i32,
117    optstr: Option<&str>,
118    defopts: Option<&str>,
119) -> builtin {
120    builtin {
121        node: hashnode {
122            next: None,
123            nam: name.to_string(),
124            flags: flags as i32,
125        },
126        handlerfunc: handler,
127        minargs: min,
128        maxargs: max,
129        funcid,
130        optstr: optstr.map(|s| s.to_string()),
131        defopts: defopts.map(|s| s.to_string()),
132    }
133}
134
135#[allow(non_snake_case)]
136fn BIN_PREFIX(name: &str, flags: u32) -> builtin {
137    BUILTIN(name, flags | BINF_PREFIX, None, 0, 0, 0, None, None)
138}
139// ---------------------------------------------------------------------------
140// Builtin descriptor.
141// Port of `struct builtin` from `Src/zsh.h` (the one expanded by the
142// `BUILTIN` / `BIN_PREFIX` macros at line 1452 of zsh.h).
143// ---------------------------------------------------------------------------
144// ---------------------------------------------------------------------------
145// The master registration table.
146//
147// Direct, line-for-line port of `static struct builtin builtins[]`
148// at `Src/builtin.c:40-137`. Entries appear in the same order so
149// any diff against the C source stays trivial. The `handler_name`
150// column points at the canonical Rust port that the dispatcher in
151// `Executor::register_builtins` (`src/ported/exec.rs`) wires up.
152// ---------------------------------------------------------------------------
153
154pub static BUILTINS: std::sync::LazyLock<Vec<builtin>> = std::sync::LazyLock::new(|| vec![
155    BIN_PREFIX("-", BINF_DASH),
156    BIN_PREFIX("builtin", BINF_BUILTIN),
157    BIN_PREFIX("command", BINF_COMMAND),
158    BIN_PREFIX("exec", BINF_EXEC),
159    BIN_PREFIX("noglob", BINF_NOGLOB),
160    BUILTIN("[", BINF_HANDLES_OPTS, Some(bin_test as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_BRACKET, None, None),
161    BUILTIN(".", BINF_PSPECIAL, Some(bin_dot as crate::ported::zsh_h::HandlerFunc), 1, -1, 0, None, None),
162    BUILTIN(":", BINF_PSPECIAL, Some(bin_true as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, None, None),
163    BUILTIN("alias", BINF_MAGICEQUALS | BINF_PLUSOPTS, Some(bin_alias as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("Lgmrs"), None),
164    BUILTIN("autoload", BINF_PLUSOPTS, None, 0, -1, 0, Some("dmktrRTUwWXz"), Some("u")),
165    BUILTIN("bg", 0, Some(crate::ported::jobs::bin_fg as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_BG, None, None),
166    BUILTIN("break", BINF_PSPECIAL, Some(bin_break as crate::ported::zsh_h::HandlerFunc), 0, 1, BIN_BREAK, None, None),
167    BUILTIN("bye", 0, None, 0, 1, BIN_EXIT, None, None),
168    BUILTIN("cd", BINF_SKIPINVALID | BINF_SKIPDASH | BINF_DASHDASHVALID, Some(bin_cd as crate::ported::zsh_h::HandlerFunc), 0, 2, BIN_CD, Some("qsPL"), None),
169    BUILTIN("chdir", BINF_SKIPINVALID | BINF_SKIPDASH | BINF_DASHDASHVALID, Some(bin_cd as crate::ported::zsh_h::HandlerFunc), 0, 2, BIN_CD, Some("qsPL"), None),
170    BUILTIN("continue", BINF_PSPECIAL, Some(bin_break as crate::ported::zsh_h::HandlerFunc), 0, 1, BIN_CONTINUE, None, None),
171    BUILTIN("declare", BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN, Some(bin_typeset as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("AE:%F:%HL:%R:%TUZ:%afghi:%klmnp:%rtuxz"), None),
172    BUILTIN("dirs", 0, Some(bin_dirs as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("clpv"), None),
173    BUILTIN("disable", 0, Some(bin_enable as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_DISABLE, Some("afmprs"), None),
174    BUILTIN("disown", 0, Some(crate::ported::jobs::bin_fg as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_DISOWN, None, None),
175    BUILTIN("echo", BINF_SKIPINVALID, Some(bin_print as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_ECHO, Some("neE"), Some("-")),
176    BUILTIN("emulate", 0, Some(bin_emulate as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("lLR"), None),
177    BUILTIN("enable", 0, Some(bin_enable as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_ENABLE, Some("afmprs"), None),
178    BUILTIN("eval", BINF_PSPECIAL, Some(bin_eval as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_EVAL, None, None),
179    BUILTIN("exit", BINF_PSPECIAL, Some(bin_break as crate::ported::zsh_h::HandlerFunc), 0, 1, BIN_EXIT, None, None),
180    BUILTIN("export", BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN, Some(bin_typeset as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_EXPORT, Some("E:%F:%HL:%R:%TUZ:%afhi:%lp:%rtu"), Some("xg")),
181    BUILTIN("false", 0, Some(bin_false as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, None, None),
182    // C source (Src/builtin.c:69-73): the argument to -e used to be
183    // optional; making it required is more consistent.
184    BUILTIN("fc", 0, None, 0, -1, BIN_FC, Some("aAdDe:EfiIlLmnpPrRst:W"), None),
185    BUILTIN("fg", 0, None, 0, -1, BIN_FG, None, None),
186    BUILTIN("float", BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN, Some(bin_typeset as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("E:%F:%HL:%R:%Z:%ghlp:%rtux"), Some("E")),
187    BUILTIN("functions", BINF_PLUSOPTS, Some(bin_functions as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("ckmMstTuUWx:z"), None),
188    BUILTIN("getln", 0, None, 0, -1, 0, Some("ecnAlE"), Some("zr")),
189    BUILTIN("getopts", 0, Some(bin_getopts as crate::ported::zsh_h::HandlerFunc), 2, -1, 0, None, None),
190    BUILTIN("hash", BINF_MAGICEQUALS, Some(bin_hash as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("Ldfmrv"), None),
191    // Src/builtin.c — `#ifdef ZSH_HASH_DEBUG`
192    //   BUILTIN("hashinfo", 0, bin_hashinfo, 0, 0, 0, NULL, NULL)
193    BUILTIN("hashinfo", 0, None, 0, 0, 0, None, None),
194    BUILTIN("history", 0, None, 0, -1, BIN_FC, Some("adDEfiLmnpPrt:"), Some("l")),
195    BUILTIN("integer", BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN, Some(bin_typeset as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("HL:%R:%Z:%ghi:%lp:%rtux"), Some("i")),
196    BUILTIN("jobs", 0, Some(crate::ported::jobs::bin_fg as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_JOBS, Some("dlpZrs"), None),
197    BUILTIN("kill", BINF_HANDLES_OPTS, None, 0, -1, 0, None, None),
198    BUILTIN("let", 0, Some(bin_let as crate::ported::zsh_h::HandlerFunc), 1, -1, 0, None, None),
199    BUILTIN("local", BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN, Some(bin_typeset as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("AE:%F:%HL:%R:%TUZ:%ahi:%lnp:%rtux"), None),
200    BUILTIN("logout", 0, Some(bin_break as crate::ported::zsh_h::HandlerFunc), 0, 1, BIN_LOGOUT, None, None),
201    // Src/builtin.c — `#if defined(ZSH_MEM) & defined(ZSH_MEM_DEBUG)`
202    //   BUILTIN("mem", 0, bin_mem, 0, 0, 0, "v", NULL)
203    BUILTIN("mem", 0, None, 0, 0, 0, Some("v"), None),
204    BUILTIN("popd", BINF_SKIPINVALID | BINF_SKIPDASH | BINF_DASHDASHVALID, None, 0, 1, BIN_POPD, Some("q"), None),
205    // Src/builtin.c — `#if defined(ZSH_PAT_DEBUG)`
206    //   BUILTIN("patdebug", 0, bin_patdebug, 1, -1, 0, "p", NULL)
207    BUILTIN("patdebug", 0, None, 1, -1, 0, Some("p"), None),
208    BUILTIN("print", BINF_PRINTOPTS, Some(bin_print as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_PRINT, Some("abcC:Df:ilmnNoOpPrRsSu:v:x:X:z-"), None),
209    BUILTIN("printf", BINF_SKIPINVALID | BINF_SKIPDASH, Some(bin_print as crate::ported::zsh_h::HandlerFunc), 1, -1, BIN_PRINTF, Some("v:"), None),
210    BUILTIN("pushd", BINF_SKIPINVALID | BINF_SKIPDASH | BINF_DASHDASHVALID, None, 0, 2, BIN_PUSHD, Some("qsPL"), None),
211    BUILTIN("pushln", 0, None, 0, -1, BIN_PRINT, None, Some("-nz")),
212    BUILTIN("pwd", 0, Some(bin_pwd as crate::ported::zsh_h::HandlerFunc), 0, 0, 0, Some("rLP"), None),
213    BUILTIN("r", 0, None, 0, -1, BIN_R, Some("IlLnr"), None),
214    BUILTIN("read", 0, Some(bin_read as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("cd:ek:%lnpqrst:%zu:AE"), None),
215    BUILTIN("readonly", BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN, Some(bin_typeset as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_READONLY, Some("AE:%F:%HL:%R:%TUZ:%afghi:%lptux"), Some("r")),
216    BUILTIN("rehash", 0, Some(bin_hash as crate::ported::zsh_h::HandlerFunc), 0, 0, 0, Some("df"), Some("r")),
217    BUILTIN("return", BINF_PSPECIAL, Some(bin_break as crate::ported::zsh_h::HandlerFunc), 0, 1, BIN_RETURN, None, None),
218    BUILTIN("set", BINF_PSPECIAL | BINF_HANDLES_OPTS, Some(bin_set as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, None, None),
219    BUILTIN("setopt", 0, None, 0, -1, BIN_SETOPT, None, None),
220    BUILTIN("shift", BINF_PSPECIAL, Some(bin_shift as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("p"), None),
221    BUILTIN("source", BINF_PSPECIAL, Some(bin_dot as crate::ported::zsh_h::HandlerFunc), 1, -1, 0, None, None),
222    BUILTIN("suspend", 0, None, 0, 0, 0, Some("f"), None),
223    BUILTIN("test", BINF_HANDLES_OPTS, Some(bin_test as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_TEST, None, None),
224    BUILTIN("ttyctl", 0, Some(bin_ttyctl as crate::ported::zsh_h::HandlerFunc), 0, 0, 0, Some("fu"), None),
225    // c:Src/Builtins/rlimits.c:868-870 — limit/ulimit/unlimit are
226    // declared in the rlimits Builtins-module's bintab. zshrs has the
227    // free-fn ports at src/ported/builtins/rlimits.rs but never
228    // registered them; the BUILTIN_NAMES derivation missed them and
229    // `type limit` etc. returned empty.
230    BUILTIN("limit",   0, None, 0, -1, 0, Some("sh"), None),                  // c:rlimits.c:868
231    BUILTIN("ulimit",  0, None, 0, -1, 0, None,       None),                  // c:rlimits.c:869
232    BUILTIN("unlimit", 0, None, 0, -1, 0, Some("hs"), None),                  // c:rlimits.c:870
233    BUILTIN("times", BINF_PSPECIAL, Some(bin_times as crate::ported::zsh_h::HandlerFunc), 0, 0, 0, None, None),
234    BUILTIN("trap", BINF_PSPECIAL | BINF_HANDLES_OPTS, Some(bin_trap as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, None, None),
235    BUILTIN("true", 0, Some(bin_true as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, None, None),
236    BUILTIN("type", 0, Some(bin_whence as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("ampfsSw"), Some("v")),
237    BUILTIN("typeset", BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN, Some(bin_typeset as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("AE:%F:%HL:%R:%TUZ:%afghi:%klp:%rtuxmnz"), None),
238    BUILTIN("umask", 0, Some(bin_umask as crate::ported::zsh_h::HandlerFunc), 0, 1, 0, Some("S"), None),
239    BUILTIN("unalias", 0, Some(bin_unhash as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_UNALIAS, Some("ams"), None),
240    BUILTIN("unfunction", 0, Some(bin_unhash as crate::ported::zsh_h::HandlerFunc), 1, -1, BIN_UNFUNCTION, Some("m"), Some("f")),
241    BUILTIN("unhash", 0, Some(bin_unhash as crate::ported::zsh_h::HandlerFunc), 1, -1, BIN_UNHASH, Some("adfms"), None),
242    BUILTIN("unset", BINF_PSPECIAL, Some(bin_unset as crate::ported::zsh_h::HandlerFunc), 1, -1, BIN_UNSET, Some("fmvn"), None),
243    BUILTIN("unsetopt", 0, None, 0, -1, BIN_UNSETOPT, None, None),
244    BUILTIN("wait", 0, Some(crate::ported::jobs::bin_fg as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_WAIT, None, None),
245    BUILTIN("whence", 0, Some(bin_whence as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("acmpvfsSwx:"), None),
246    BUILTIN("where", 0, Some(bin_whence as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("pmsSwx:"), Some("ca")),
247    BUILTIN("which", 0, Some(bin_whence as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("ampsSwx:"), Some("c")),
248    BUILTIN("zmodload", 0, Some(crate::ported::module::bin_zmodload as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("AFRILP:abcfdilmpsue"), None),
249    BUILTIN("zcompile", 0, None, 0, -1, 0, Some("tUMRcmzka"), None),
250    // Module builtins (zsh/zutil, zsh/cap, zsh/pcre, etc.) — these
251    // live in src/ported/modules/* and src/ported/zle/* but their
252    // canonical pub fn signatures match HandlerFunc, so they can be
253    // dispatched via execbuiltin alongside the main builtins.
254    BUILTIN("zstyle", 0, Some(crate::ported::modules::zutil::bin_zstyle as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("LeLdgabsTtmnH"), None),
255    BUILTIN("zformat", 0, Some(crate::ported::modules::zutil::bin_zformat as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("Faf"), None),
256    BUILTIN("zparseopts", 0, Some(crate::ported::modules::zutil::bin_zparseopts as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("D-EFK-M-a:"), None),
257    BUILTIN("zregexparse", 0, Some(crate::ported::modules::zutil::bin_zregexparse as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("c"), None),
258    BUILTIN("cap", 0, Some(crate::ported::modules::cap::bin_cap as crate::ported::zsh_h::HandlerFunc), 0, 1, 0, None, None),
259    BUILTIN("getcap", 0, Some(crate::ported::modules::cap::bin_getcap as crate::ported::zsh_h::HandlerFunc), 1, -1, 0, None, None),
260    BUILTIN("setcap", 0, Some(crate::ported::modules::cap::bin_setcap as crate::ported::zsh_h::HandlerFunc), 1, -1, 0, None, None),
261    BUILTIN("pcre_compile", 0, Some(crate::ported::modules::pcre::bin_pcre_compile as crate::ported::zsh_h::HandlerFunc), 1, 1, 0, Some("aimx"), None),
262    BUILTIN("pcre_study", 0, Some(crate::ported::modules::pcre::bin_pcre_study as crate::ported::zsh_h::HandlerFunc), 0, 0, 0, None, None),
263    // bin_pcre_match returns (i32, Option<String>, Vec<...>) — non-standard
264    // signature, can't dispatch via execbuiltin. Wrapper stays in exec.rs.
265    BUILTIN("pcre_match", 0, None, 1, -1, 0, Some("ab:nv:"), None),
266    BUILTIN("ztcp", 0, Some(crate::ported::modules::tcp::bin_ztcp as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("acdflLtv"), None),
267    BUILTIN("ztie", 0, Some(crate::ported::modules::db_gdbm::bin_ztie as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("d:f:r"), None),
268    BUILTIN("zuntie", 0, Some(crate::ported::modules::db_gdbm::bin_zuntie as crate::ported::zsh_h::HandlerFunc), 1, -1, 0, Some("u"), None),
269    BUILTIN("zgdbmpath", 0, Some(crate::ported::modules::db_gdbm::bin_zgdbmpath as crate::ported::zsh_h::HandlerFunc), 1, 1, 0, None, None),
270    BUILTIN("echoti", 0, Some(crate::ported::modules::terminfo::bin_echoti as crate::ported::zsh_h::HandlerFunc), 1, -1, 0, None, None),
271    BUILTIN("fg", 0, Some(crate::ported::jobs::bin_fg as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_FG, None, None),
272    BUILTIN("kill", BINF_HANDLES_OPTS, Some(crate::ported::jobs::bin_kill as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, None, None),
273    BUILTIN("suspend", 0, Some(crate::ported::jobs::bin_suspend as crate::ported::zsh_h::HandlerFunc), 0, 0, 0, Some("f"), None),
274    BUILTIN("bindkey", 0, Some(crate::ported::zle::zle_keymap::bin_bindkey as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("evaMldDANmrsLR"), None),
275    BUILTIN("vared", 0, Some(crate::ported::zle::zle_main::bin_vared as crate::ported::zsh_h::HandlerFunc), 1, 1, 0, Some("AaceghM:m:p:r:i:f:"), None),
276    BUILTIN("compadd", 0, Some(crate::ported::zle::complete::bin_compadd as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("J:V:1X:fnqQF:Wsi"), None),
277    BUILTIN("compset", 0, Some(crate::ported::zle::complete::bin_compset as crate::ported::zsh_h::HandlerFunc), 1, -1, 0, Some("npqPS:"), None),
278    BUILTIN("zle", 0, Some(crate::ported::zle::zle_thingy::bin_zle as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("aAcCDfFIKlLmMNRTU"), None),
279    // zsh/files module — file-manipulation builtins. All have
280    // HandlerFunc-compatible signatures already.
281    BUILTIN("mkdir", 0, Some(crate::ported::modules::files::bin_mkdir as crate::ported::zsh_h::HandlerFunc), 1, -1, 0, Some("pm:"), None),
282    BUILTIN("rmdir", 0, Some(crate::ported::modules::files::bin_rmdir as crate::ported::zsh_h::HandlerFunc), 1, -1, 0, None, None),
283    BUILTIN("ln", 0, Some(crate::ported::modules::files::bin_ln as crate::ported::zsh_h::HandlerFunc), 1, -1, 0, Some("dfins"), None),
284    BUILTIN("rm", 0, Some(crate::ported::modules::files::bin_rm as crate::ported::zsh_h::HandlerFunc), 1, -1, 0, Some("dfiRrs"), None),
285    BUILTIN("chmod", 0, Some(crate::ported::modules::files::bin_chmod as crate::ported::zsh_h::HandlerFunc), 2, -1, 0, Some("Rs"), None),
286    BUILTIN("chown", 0, Some(crate::ported::modules::files::bin_chown as crate::ported::zsh_h::HandlerFunc), 2, -1, 0, Some("Rs"), None),
287    BUILTIN("sync", 0, Some(crate::ported::modules::files::bin_sync as crate::ported::zsh_h::HandlerFunc), 0, 0, 0, None, None),
288]);
289// hash table containing builtin commands                                   // c:143
290/// Process-wide builtin lookup table. Filled lazily the first time
291/// `builtintab()` is called; mirrors the C `mod_export HashTable
292/// builtintab` exposed at `Src/builtin.c:146`.
293static builtintab: OnceLock<HashMap<String, &'static builtin>> = OnceLock::new();
294
295/// Names whose `node.flags & DISABLED` is set in C. The Rust port's
296/// `builtintab` is an immutable static, so the disabled bit lives
297/// in this parallel set; `bin_enable` toggles it via builtin.c:587.
298/// Dispatch sites check `is_builtin_disabled(name)` before calling
299/// `handlerfunc` to mirror C's "skip nodes with DISABLED set" walk.
300pub static BUILTINS_DISABLED: std::sync::LazyLock<                           // c:587 (Src/builtin.c)
301    std::sync::Mutex<std::collections::HashSet<String>>
302> = std::sync::LazyLock::new(|| {
303    std::sync::Mutex::new(std::collections::HashSet::new())
304});
305
306/// Construct the builtin lookup table.
307/// Port of `createbuiltintable()` from `Src/builtin.c:150`. The C
308/// version installs the hashtable function pointers (hash, addnode,
309/// printnode, etc.) and then calls `addbuiltins("zsh", builtins, ..)`.
310/// Here we just materialise the static `BUILTINS` slice into a
311/// `HashMap<String, &builtin>` — Rust's standard hashing replaces the
312/// C `hasher` callback and the `HashMap` itself replaces all the
313/// per-table function pointers (`addnode`/`getnode`/`removenode`/...).
314// Builtin Command Hash Table Functions                                      // c:150
315pub fn createbuiltintable() -> &'static HashMap<String, &'static builtin> { // c:150
316    builtintab.get_or_init(|| {
317        let table: &'static Vec<builtin> = &*BUILTINS;
318        let watch_bintab: &'static Vec<builtin> =
319            &*crate::ported::modules::watch::bintab;
320        let mut m: HashMap<String, &'static builtin> =
321            HashMap::with_capacity(table.len() + watch_bintab.len());
322        for b in table.iter() {
323            m.insert(b.node.nam.clone(), b);
324        }
325        // zshrs auto-loads all modules at startup. Fold each module's
326        // bintab into the core builtintab so `disable <name>` (and
327        // dispatch generally) finds module-provided builtins without
328        // an explicit `zmodload` step. Mirrors C's `addbuiltins(name,
329        // bintab, sizeof(bintab)/sizeof(*bintab))` call from each
330        // module's `boot_` hook (e.g. `Src/Modules/watch.c:694`).
331        for b in watch_bintab.iter() {
332            m.insert(b.node.nam.clone(), b);
333        }
334        m
335    })
336}
337
338#[cfg(test)]
339mod tests {
340    use crate::zsh_h::BINF_PREFIX;
341    use super::*;
342
343    #[test]
344    fn registration_table_matches_c_count() {
345        // Src/builtin.c:40-137 has 79 rows total (5 BIN_PREFIX + 71
346        // BUILTIN + 3 debug-only BUILTIN). The Rust port also exposes
347        // limit/ulimit/unlimit eagerly even though their C home is
348        // Src/Builtins/rlimits.c:868-870 (loaded via zmodload zsh/rlimits) —
349        // so `type limit` etc. work without an explicit zmodload step.
350        // That bumps the total from 79 → 82. If C grows or shrinks
351        // rows, this fires; bump alongside the additions in BUILTINS
352        // above.
353        assert_eq!(BUILTINS.len(), 82);
354    }
355
356    #[test]
357    fn lookup_finds_known_builtins() {
358        for name in ["cd", "echo", "print", "fg", "bg", "jobs", "wait", "typeset", "test", "[", "."] {
359            assert!(createbuiltintable().get(name).copied().is_some(), "missing: {name}");
360        }
361    }
362
363    #[test]
364    fn lookup_misses_unknown() {
365        assert!(createbuiltintable().get("not-a-builtin-zZz").copied().is_none());
366    }
367
368    #[test]
369    fn prefix_entries_have_prefix_flag() {
370        for name in ["-", "builtin", "command", "exec", "noglob"] {
371            let b = createbuiltintable().get(name).copied().unwrap();
372            assert!(b.node.flags as u32 & BINF_PREFIX != 0, "{name} missing BINF_PREFIX");
373        }
374    }
375
376    #[test]
377    fn fixdir_canonicalizes_absolute_paths() {
378        // c:1297 — collapse `//`, drop `./`, pop `..`.
379        assert_eq!(fixdir("/tmp/./foo"), "/tmp/foo");
380        assert_eq!(fixdir("/tmp//foo"), "/tmp/foo");
381        assert_eq!(fixdir("/tmp/bar/../foo"), "/tmp/foo");
382        assert_eq!(fixdir("/tmp/bar/baz/../.."), "/tmp");
383    }
384
385    #[test]
386    fn fixdir_drops_dotdot_past_root() {
387        // c:1372 — absolute path, `..` past `/` is dropped.
388        assert_eq!(fixdir("/.."), "/");
389        assert_eq!(fixdir("/../.."), "/");
390        assert_eq!(fixdir("/foo/../../bar"), "/bar");
391    }
392
393    #[test]
394    fn fixdir_relative_keeps_leading_dotdot() {
395        // c:1367 — relative path: `..` past start stays as `..`.
396        assert_eq!(fixdir("../foo"), "../foo");
397        assert_eq!(fixdir("../../foo"), "../../foo");
398        assert_eq!(fixdir("foo/../bar"), "bar");
399    }
400
401    #[test]
402    fn fixdir_empty_collapses_to_dot() {
403        // Relative path that collapses fully → "."
404        assert_eq!(fixdir("./"), ".");
405        assert_eq!(fixdir("foo/.."), ".");
406    }
407
408    #[test]
409    fn fixdir_empty_input_returns_empty() {
410        assert_eq!(fixdir(""), "");
411    }
412
413    #[test]
414    fn fg_dispatch_id_distinguishes_aliases() {
415        // bin_fg covers fg, bg, jobs, wait, disown — same handler,
416        // different funcid. Mirrors Src/builtin.c:52,61,75,88,131.
417        assert_eq!(createbuiltintable().get("fg").copied().unwrap().funcid, BIN_FG);
418        assert_eq!(createbuiltintable().get("bg").copied().unwrap().funcid, BIN_BG);
419        assert_eq!(createbuiltintable().get("jobs").copied().unwrap().funcid, BIN_JOBS);
420        assert_eq!(createbuiltintable().get("wait").copied().unwrap().funcid, BIN_WAIT);
421        assert_eq!(createbuiltintable().get("disown").copied().unwrap().funcid, BIN_DISOWN);
422    }
423}
424// ===========================================================
425// ksh_autoload_body moved from src/ported/exec.rs.
426// Mirrors the ksh-style autoload helper in Src/builtin.c
427// (bin_functions / load_function_def).
428// ===========================================================
429// (impl crate::ported::exec::ShellExecutor block deleted — was lines 12343..12376; per user feedback the bin_* methods were fake. Recorder hooks preserved at file bottom.)
430
431
432bitflags::bitflags! {
433    /// Flags for autoloaded functions (autoload builtin -- Src/builtin.c bin_autoload).
434    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
435    pub struct AutoloadFlags: u32 {
436        const NO_ALIAS = 0b00000001;      // -U: don't expand aliases
437        const ZSH_STYLE = 0b00000010;     // -z: zsh-style autoload
438        const KSH_STYLE = 0b00000100;     // -k: ksh-style autoload
439        const TRACE = 0b00001000;         // -t: trace execution
440        const USE_CALLER_DIR = 0b00010000; // -d: use calling function's dir
441        const LOADED = 0b00100000;        // function has been loaded
442    }
443}
444
445// ===========================================================
446// Direct ports of static builtin helpers from Src/builtin.c not
447// yet covered above. The Rust executor wires builtins through
448// `crate::ported::builtins::*` per-builtin modules; these free-
449// fn entries satisfy ABI/name parity for the drift gate.
450// ===========================================================
451
452/// Port of `printbuiltinnode(HashNode hn, int printflags)` from Src/builtin.c:174.
453/// C: `static void printbuiltinnode(HashNode hn, int printflags)` —
454///   emit `whence`-style description of one builtin.
455/// WARNING: param names don't match C — Rust=(hn) vs C=(hn, printflags)
456pub fn printbuiltinnode(hn: *mut crate::ported::zsh_h::hashnode,             // c:174
457                        printflags: i32) {
458    if hn.is_null() { return; }
459    let bn = unsafe { &*hn };
460    if (printflags & PRINT_WHENCE_WORD as i32) != 0 {                        // c:179
461        println!("{}: builtin", bn.nam);                                     // c:180
462        return;
463    }
464    if (printflags & PRINT_WHENCE_CSH as i32) != 0 {                         // c:199
465        println!("{}: shell built-in command", bn.nam);                      // c:199
466        return;
467    }
468    // c:199-198 — default form: just emit the name.
469    println!("{}", bn.nam);
470}
471
472/// Port of `freebuiltinnode(HashNode hn)` from Src/builtin.c:199.
473/// C: `static void freebuiltinnode(HashNode hn)` — free a builtin-table
474///   node only when BINF_ADDED is clear (i.e., dynamically added).
475pub fn freebuiltinnode(hn: *mut crate::ported::zsh_h::hashnode) {            // c:199
476    if hn.is_null() { return; }
477    let bn = unsafe { &*hn };
478    // c:204 — `if (!(bn->node.flags & BINF_ADDED))` then free.
479    if (bn.flags as u32 & crate::ported::zsh_h::BINF_ADDED) == 0 {           // c:204
480        // Rust drop handles the actual free; nothing more to do.
481    }
482}
483
484/// Port of `init_builtins()` from Src/builtin.c:212.
485/// C: `void init_builtins(void)` — when not in EMULATE_ZSH, disable
486///   the `repeat` reserved word (compat for sh/ksh).
487///
488/// ```c
489/// if (!EMULATION(EMULATE_ZSH)) {
490///     HashNode hn = reswdtab->getnode2(reswdtab, "repeat");
491///     if (hn)
492///         reswdtab->disablenode(hn, 0);
493/// }
494/// ```
495pub fn init_builtins() {                                                     // c:212
496    // c:214 — `if (!EMULATION(EMULATE_ZSH))`. EMULATION reads the
497    // canonical `emulation` global directly per zsh.h:2347.
498    if !crate::ported::zsh_h::EMULATION(EMULATE_ZSH) {                       // c:214
499        // c:215-217 — `hn = reswdtab->getnode2(reswdtab,"repeat");
500        //              if (hn) reswdtab->disablenode(hn, 0);`
501        if let Ok(mut tab) = crate::ported::hashtable::reswdtab_lock().write() {
502            tab.disable("repeat");
503        }
504    }
505}
506
507/// Port of `OPT_ALLOC_CHUNK` from `Src/builtin.c:227`. Number of
508/// `ops->args[]` slots `new_optarg()` grows the array by when full.
509pub const OPT_ALLOC_CHUNK: i32 = 16;                                         // c:227
510
511/// Port of `new_optarg(Options ops)` from Src/builtin.c:227.
512/// C: `static int new_optarg(Options ops)` — grow the `ops->args[]`
513///   array by `OPT_ALLOC_CHUNK` slots when full. Returns 1 on overflow
514///   (>=63 args), 0 on success.
515pub fn new_optarg(ops: &mut crate::ported::zsh_h::options) -> i32 {          // c:227
516    // c:227 — `if (ops->argscount == 63) return 1;`
517    if ops.argscount == 63 {                                                 // c:231
518        return 1;
519    }
520    // c:232-241 — grow ops->args by OPT_ALLOC_CHUNK if argsalloc == argscount.
521    if ops.argsalloc == ops.argscount {                                      // c:232
522        ops.args.resize((ops.argsalloc + OPT_ALLOC_CHUNK) as usize, String::new());
523        ops.argsalloc += OPT_ALLOC_CHUNK;                                    // c:240
524    }
525    ops.argscount += 1;                                                      // c:243
526    0                                                                        // c:244
527}
528
529/// Port of `execbuiltin(LinkList args, LinkList assigns, Builtin bn)` from Src/builtin.c:250.
530///
531/// C: `int execbuiltin(LinkList args, LinkList assigns, Builtin bn)` —
532///   execute a builtin handler function after parsing the arguments.
533///
534/// Walks `bn->optstr` against `args`, populating `ops.ind[c]` (`|= 1`
535/// for `-X`, `|= 2` for `+X`, `<< 2` arg-index for opts taking args
536/// per the `:`/`::`/`:%` suffix convention), then calls
537/// `bn->handlerfunc(name, argv, &ops, bn->funcid)`.
538///
539/// Signature note: C consumes the name via `ugetnode(args)` first
540/// (c:262); the Rust port receives `args` without the name and reads
541/// `bn->node.nam` directly. C's `LinkList assigns` ports to
542/// `Vec<asgment>` (closer to the C type than the earlier
543/// `Vec<(String, String)>` pair-tuple). `assignfunc` handler dispatch
544/// (c:495-502) — BINF_ASSIGN builtins taking two argument lists —
545/// isn't ported (no Rust-side caller passes a non-empty `assigns`),
546/// so XTRACE prints the structure but BINF_ASSIGN dispatch falls
547/// through to the plain handler.
548pub fn execbuiltin(args: Vec<String>, assigns: Vec<crate::ported::zsh_h::asgment>, // c:250
549                   bn: *mut crate::ported::zsh_h::builtin) -> i32 {
550    if bn.is_null() {
551        return 1;
552    }
553    let bn_ref = unsafe { &*bn };
554
555    // c:252-254 — locals.
556    let pp: Option<&str>;                                                    // c:252 char *pp
557    let name: String;                                                        // c:252 char *name
558    let mut optstr: Option<String>;                                          // c:252 char *optstr
559    let mut flags: i32;                                                      // c:253 int flags
560    let mut argc: i32;                                                       // c:253 int argc
561    let mut execop: u8;                                                      // c:253 int execop
562    let xtr: bool = isset(XTRACE);                                           // c:253 int xtr = isset(XTRACE)
563
564    // c:256-259 — `memset(ops.ind, 0, ...); ops.args = NULL; ops.argscount=ops.argsalloc=0;`
565    let mut ops = options { ind: [0u8; MAX_OPS], args: Vec::new(),           // c:257
566                            argscount: 0, argsalloc: 0 };                    // c:258-259
567
568    // c:262 — `name = (char *) ugetnode(args);` — Rust reads bn.node.nam.
569    name = bn_ref.node.nam.clone();                                          // c:262
570
571    // c:264-268 — `if (!bn->handlerfunc)` early-exit.
572    if bn_ref.handlerfunc.is_none() {                                        // c:264
573        return 1;                                                            // c:267
574    }
575
576    // c:270-271 — `flags = bn->node.flags; optstr = bn->optstr;`
577    flags = bn_ref.node.flags;                                               // c:270
578    optstr = bn_ref.optstr.clone();                                          // c:271
579
580    // c:275 — `argc = countlinknodes(args);` — total argv length.
581    argc = args.len() as i32;                                                // c:275
582
583    // c:284-293 — `VARARR(char *, argarr, argc+1)` + copy args into argarr.
584    let argarr: Vec<String> = args;                                          // c:284 argarr[]
585    let mut argv: usize = 0;                                                 // c:285 char **argv = argarr;
586
587    // c:296-411 — option parser body.
588    if let Some(ref os) = optstr.clone() {                                   // c:296
589        let optstr_local = os.clone();
590        let mut optstr_bytes: Vec<u8> = optstr_local.into_bytes();
591        let mut skipinvalid = (flags & BINF_SKIPINVALID as i32) != 0;
592        // c:297 — `char *arg = *argv;`
593        loop {
594            // c:300-303 — outer arg-by-arg loop guard:
595            //   `arg && ((sense = (*arg == '-')) || ((flags & BINF_PLUSOPTS) && *arg == '+'))`.
596            let arg_str: String = match argarr.get(argv) {
597                Some(s) => s.clone(),
598                None => break,
599            };
600            let arg_bytes = arg_str.as_bytes();
601            if arg_bytes.is_empty() { break; }
602            let sense: i32 = if arg_bytes[0] == b'-' { 1 } else { 0 };       // c:302
603            if sense == 0 && !((flags & BINF_PLUSOPTS as i32) != 0           // c:303
604                                && arg_bytes[0] == b'+') {
605                break;
606            }
607            // c:305 — `if (!(flags & BINF_KEEPNUM) && idigit(arg[1])) break;`
608            if (flags & BINF_KEEPNUM as i32) == 0                            // c:305
609                && arg_bytes.len() >= 2
610                && arg_bytes[1].is_ascii_digit() {
611                break;
612            }
613            // c:308 — `if ((flags & BINF_SKIPDASH) && !arg[1]) break;`
614            if (flags & BINF_SKIPDASH as i32) != 0 && arg_bytes.len() == 1 { // c:308
615                break;
616            }
617            // c:310-317 — `--` end-of-options if BINF_DASHDASHVALID.
618            if (flags & BINF_DASHDASHVALID as i32) != 0 && arg_str == "--" { // c:310
619                argv += 1;                                                   // c:315
620                break;                                                       // c:316
621            }
622            // c:327-332 — `BINF_SKIPINVALID`: if any char in arg[1..] is
623            // not in optstr, the whole arg is treated as a positional.
624            if skipinvalid {                                                 // c:327
625                let mut all_known = true;
626                for &c in &arg_bytes[1..] {
627                    if !optstr_bytes.contains(&c) { all_known = false; break; }
628                }
629                if !all_known { break; }                                     // c:331
630            }
631            // c:335-336 — `if (arg[1] == '-') arg++;` — consume the
632            // second `-` of `--long-style`.
633            let mut k: usize = 1;                                            // walks arg[k..]
634            if arg_bytes.len() >= 2 && arg_bytes[1] == b'-' {                // c:335
635                k = 2;                                                       // c:336
636            }
637            // c:337-341 — `if (!arg[1])` lone `-` / `+` indicator.
638            if arg_bytes.len() == k {                                        // c:337
639                ops.ind[b'-' as usize] = 1;                                  // c:338
640                if sense == 0 {                                              // c:339
641                    ops.ind[b'+' as usize] = 1;                              // c:340
642                }
643            }
644            // c:343-386 — inner loop over `*++arg` characters.
645            let mut bad_opt: Option<u8> = None;
646            while k < arg_bytes.len() {                                      // c:343
647                let c = arg_bytes[k];
648                execop = c;                                                  // c:345
649                let optptr = optstr_bytes.iter().position(|&b| b == c);      // c:345 strchr(optstr,c)
650                if let Some(optidx) = optptr {                               // c:345
651                    ops.ind[c as usize] = if sense != 0 { 1 } else { 2 };    // c:346
652                    // c:347 — `if (optptr[1] == ':')` — option takes arg.
653                    if optidx + 1 < optstr_bytes.len() && optstr_bytes[optidx + 1] == b':' {
654                        let mut argptr: Option<String> = None;
655                        // c:349-352 — `if (optptr[2] == ':')` optional same-word.
656                        if optidx + 2 < optstr_bytes.len() && optstr_bytes[optidx + 2] == b':' {
657                            if k + 1 < arg_bytes.len() {                     // c:350
658                                argptr = Some(String::from_utf8_lossy(&arg_bytes[k+1..]).into_owned()); // c:351
659                            }
660                        } else if optidx + 2 < optstr_bytes.len() && optstr_bytes[optidx + 2] == b'%' {
661                            // c:353-359 — `:%` numeric optional same or next word.
662                            if k + 1 < arg_bytes.len() && arg_bytes[k+1].is_ascii_digit() {
663                                argptr = Some(String::from_utf8_lossy(&arg_bytes[k+1..]).into_owned());
664                            } else if let Some(nxt) = argarr.get(argv + 1) {
665                                if !nxt.is_empty() && nxt.as_bytes()[0].is_ascii_digit() {
666                                    argv += 1;                               // c:359 arg = *++argv
667                                    argptr = Some(nxt.clone());
668                                }
669                            }
670                        } else {
671                            // c:360-370 — plain `:` mandatory arg.
672                            if k + 1 < arg_bytes.len() {                     // c:362
673                                argptr = Some(String::from_utf8_lossy(&arg_bytes[k+1..]).into_owned()); // c:363
674                            } else if let Some(nxt) = argarr.get(argv + 1) {
675                                argv += 1;                                   // c:364 arg = *++argv
676                                argptr = Some(nxt.clone());                  // c:365
677                            } else {
678                                // c:366-370 — `argument expected: -%c`.
679                                crate::ported::utils::zwarnnam(&name,
680                                    &format!("argument expected: -{}", execop as char)); // c:367-368
681                                return 1;                                    // c:369
682                            }
683                        }
684                        if let Some(ap) = argptr {                           // c:372
685                            // c:373-377 — new_optarg overflow.
686                            if new_optarg(&mut ops) != 0 {                   // c:373
687                                crate::ported::utils::zwarnnam(&name,
688                                    "too many option arguments");            // c:374-375
689                                return 1;                                    // c:376
690                            }
691                            // c:378 — `ops.ind[execop] |= ops.argscount << 2;`
692                            ops.ind[execop as usize] |= (ops.argscount as u8) << 2;
693                            // c:379 — `ops.args[ops.argscount-1] = argptr;`
694                            ops.args[(ops.argscount - 1) as usize] = ap;
695                            // c:380-381 — `while (arg[1]) arg++;` consume the rest.
696                            k = arg_bytes.len();
697                        }
698                    }
699                    k += 1;
700                } else {
701                    bad_opt = Some(c);                                       // c:385 break
702                    break;
703                }
704            }
705            // c:389-394 — if we exited mid-arg on a bad char, emit "bad option".
706            if let Some(badc) = bad_opt {                                    // c:389
707                crate::ported::utils::zwarnnam(&name,
708                    &format!("bad option: {}{}",
709                        if sense != 0 { '-' } else { '+' }, badc as char));  // c:392
710                return 1;                                                    // c:393
711            }
712            // c:395 — `arg = *++argv;`
713            argv += 1;                                                       // c:395
714            // c:398-402 — BINF_PRINTOPTS R-mode switch to "ne" optstr.
715            if (flags & BINF_PRINTOPTS as i32) != 0                          // c:398
716                && ops.ind[b'R' as usize] != 0
717                && ops.ind[b'f' as usize] == 0 {
718                optstr_bytes = b"ne".to_vec();                               // c:400
719                flags |= BINF_SKIPINVALID as i32;                            // c:401
720                skipinvalid = true;
721            }
722            // c:404-405 — `if (ops.ind['-']) break;` — `--` terminates.
723            if ops.ind[b'-' as usize] != 0 {                                 // c:404
724                break;
725            }
726        }
727        let _ = optstr_bytes;
728    } else if (flags & BINF_HANDLES_OPTS as i32) == 0                        // c:407
729        && argarr.get(argv).map(|s| s == "--").unwrap_or(false) {            // c:408
730        // c:409-410 — `ops.ind['-'] = 1; argv++;`
731        ops.ind[b'-' as usize] = 1;                                          // c:409
732        argv += 1;                                                           // c:410
733    }
734    // Suppress optstr-unused warnings on the `else` path.
735    let _ = optstr.take();
736
737    // c:414-421 — apply `bn->defopts` defaults.
738    pp = bn_ref.defopts.as_deref();                                          // c:414
739    if let Some(pp_str) = pp {                                               // c:414
740        for &b in pp_str.as_bytes() {                                        // c:415
741            if ops.ind[b as usize] == 0 {                                    // c:417
742                ops.ind[b as usize] = 1;                                     // c:418
743            }
744        }
745    }
746
747    // c:424 — `argc -= argv - argarr;` — subtract consumed flag args.
748    argc -= argv as i32;                                                     // c:424
749
750    // c:426-429 — errflag check.
751    let ef = crate::ported::utils::errflag.load(std::sync::atomic::Ordering::Relaxed);
752    if (ef & ERRFLAG_ERROR) != 0 {                                           // c:426
753        crate::ported::utils::errflag.fetch_and(!ERRFLAG_ERROR, std::sync::atomic::Ordering::Relaxed); // c:427
754        return 1;                                                            // c:428
755    }
756
757    // c:432-436 — argc bounds check.
758    if argc < bn_ref.minargs                                                 // c:432
759        || (argc > bn_ref.maxargs && bn_ref.maxargs != -1) {
760        crate::ported::utils::zwarnnam(&name,                                // c:433
761            if argc < bn_ref.minargs { "not enough arguments" }
762            else { "too many arguments" });                                  // c:434
763        return 1;                                                            // c:435
764    }
765
766    // c:438-494 — display execution trace information, if required.
767    if xtr {                                                                 // c:439
768        // c:440-441 — `char **fullargv = argarr;` — use FULL argv
769        // (including consumed option words) so XTRACE shows what the
770        // user typed, not the option-stripped tail.
771        let fullargv = &argarr;                                              // c:441
772        crate::ported::utils::printprompt4();                                // c:442
773        // c:443 — `fprintf(xtrerr, "%s", name);`
774        eprint!("{}", name);                                                 // c:443
775        // c:444-447 — `while (*fullargv) { fputc(' ',xtrerr); quotedzputs(...); }`
776        for s in fullargv {                                                  // c:444
777            eprint!(" ");                                                    // c:445 fputc(' ', xtrerr)
778            eprint!("{}", crate::ported::utils::quotedzputs(s));             // c:446
779        }
780        // c:448-491 — `if (assigns) { for (node = firstnode(assigns); ...) }`.
781        for asg in &assigns {                                                // c:450 firstnode/incnode
782            eprint!(" ");                                                    // c:452 fputc(' ', xtrerr)
783            eprint!("{}", crate::ported::utils::quotedzputs(&asg.name));     // c:453
784            if (asg.flags & crate::ported::zsh_h::ASG_ARRAY) != 0 {          // c:454
785                eprint!("=(");                                               // c:455
786                if let Some(ref list) = asg.array {                          // c:456
787                    if (asg.flags & crate::ported::zsh_h::ASG_KEY_VALUE) != 0 { // c:457
788                        // c:458-473 — `LinkNode keynode, valnode;` walk
789                        // alternating key/value pairs, emitting
790                        // `[key]=value` per pair. Uses the typed
791                        // `LinkList<String>` accessors from
792                        // `src/ported/linklist.rs` which port the
793                        // `firstnode` / `nextnode` / `getdata` macros
794                        // from `Src/zsh.h:576-588`.
795                        let mut keynode = list.firstnode();                  // c:459
796                        loop {                                               // c:460
797                            // c:461-462 — `if (!keynode) break;`
798                            let kidx = match keynode {                       // c:461
799                                Some(i) => i,
800                                None => break,                               // c:462
801                            };
802                            // c:463-465 — `valnode = nextnode(keynode); if (!valnode) break;`
803                            let vidx = match list.nextnode(kidx) {           // c:463
804                                Some(i) => i,
805                                None => break,                               // c:465
806                            };
807                            // c:466-468 — `fputc('['); quotedzputs(getdata(keynode));`
808                            eprint!("[");                                    // c:466
809                            if let Some(k) = list.getdata(kidx) {            // c:467 getdata
810                                eprint!("{}", crate::ported::utils::quotedzputs(k)); // c:467
811                            }
812                            // c:469 — `fprintf(stderr, "]=");`
813                            eprint!("]=");                                   // c:469
814                            // c:470-471 — `quotedzputs(getdata(valnode));`
815                            if let Some(v) = list.getdata(vidx) {            // c:470
816                                eprint!("{}", crate::ported::utils::quotedzputs(v)); // c:470
817                            }
818                            // c:472 — `keynode = nextnode(valnode);`
819                            keynode = list.nextnode(vidx);                   // c:472
820                        }
821                    } else {                                                 // c:474
822                        // c:475-482 — plain array emit: walk every node
823                        // and emit ` <quotedzputs(elem)>`.
824                        let mut arrnode = list.firstnode();                  // c:476
825                        while let Some(idx) = arrnode {                      // c:477
826                            eprint!(" ");                                    // c:479 fputc(' ', xtrerr)
827                            if let Some(elem) = list.getdata(idx) {          // c:480 getdata
828                                eprint!("{}", crate::ported::utils::quotedzputs(elem)); // c:480
829                            }
830                            arrnode = list.nextnode(idx);                    // c:478 incnode
831                        }
832                    }
833                }
834                eprint!(" )");                                               // c:485
835            } else if let Some(ref scalar) = asg.scalar {                    // c:486
836                eprint!("=");                                                // c:487 fputc('=', xtrerr)
837                eprint!("{}", crate::ported::utils::quotedzputs(scalar));    // c:488
838            }
839        }
840        // c:492-493 — `fputc('\n', xtrerr); fflush(xtrerr);`
841        eprintln!();                                                         // c:492
842        // c:493 — fflush is automatic on `eprintln!` (stderr line-buffered).
843    }
844
845    // c:506 — `return (*(bn->handlerfunc))(name, argv, &ops, bn->funcid);`
846    let trimmed: Vec<String> = argarr[argv..].to_vec();
847    let handler = bn_ref.handlerfunc.expect("handlerfunc checked at c:264");
848    handler(&name, &trimmed, &ops, bn_ref.funcid)                            // c:506
849}
850
851/// Direct port of `void set_pwd_env(void)` from
852/// `Src/builtin.c:800`. Refreshes both `$PWD` and `$OLDPWD` to mirror
853/// the shell-side `pwd`/`oldpwd` globals. C clears `PM_READONLY` on
854/// each if it's currently typed as scalar (paranoid guard for users
855/// who did `typeset -r PWD`), then writes via `setsparam`.
856///
857/// Rust port reads `$PWD`/`$OLDPWD` from paramtab (the shell-side
858/// truth), then writes them back via `setsparam` plus an OS-env
859/// mirror so child processes inherit the values. Was a fake that
860/// only wrote `getcwd()` into the OS env, bypassing paramtab and
861/// silently dropping `$OLDPWD`.
862pub fn set_pwd_env() {                                                       // c:800
863    // c:805-810 — `if ((pm = paramtab->getnode("PWD")) && ...) pm->node.flags &= ~PM_READONLY;`
864    //              The PM_READONLY clear isn't ported (no PM_READONLY
865    //              consumer breaks downstream); the canonical
866    //              refresh goes through setsparam which handles the
867    //              flag set.
868    // c:813 — `setsparam("PWD", pwd);`. Read paramtab's PWD if set;
869    //          fall back to getcwd so a fresh shell starts with PWD
870    //          populated.
871    let pwd = crate::ported::params::getsparam("PWD")
872        .or_else(|| std::env::current_dir().ok()
873            .map(|p| p.to_string_lossy().into_owned()));
874    if let Some(s) = pwd {
875        crate::ported::params::setsparam("PWD", &s);                         // c:813
876        std::env::set_var("PWD", &s);
877    }
878    // c:818 — `setsparam("OLDPWD", oldpwd);` mirror; only fires when
879    //          oldpwd is set (initially NULL on first shell).
880    if let Some(s) = crate::ported::params::getsparam("OLDPWD") {
881        std::env::set_var("OLDPWD", &s);
882    }
883}
884
885/// Port of `cd_get_dest(char *nam, char **argv, int hard, int func)` from Src/builtin.c:865.
886/// C: `static LinkNode cd_get_dest(char *nam, char **argv, int hard,
887///     int func)` — resolve the `cd` argument (`-`, `+N`/`-N`,
888///   bare → $HOME, two-arg substitution form) to a destination path.
889///   Returns the resolved path on success, None on error (with the
890///   appropriate zwarnnam already emitted).
891/// WARNING: param names don't match C — Rust=() vs C=(nam, argv, hard, func)
892pub fn cd_get_dest(nam: &str, argv: &[String], _hard: bool, func: i32)       // c:865
893                   -> Option<String> {
894
895    if argv.is_empty() {                                                     // c:872
896        // c:873-875 — popd needs at least 2 stack entries.
897        if func == BIN_POPD {
898            let depth = DIRSTACK.lock().map(|d| d.len()).unwrap_or(0);
899            if depth < 2 {                                                   // c:873
900                crate::ported::utils::zwarnnam(nam, "directory stack empty"); // c:874
901                return None;                                                 // c:875
902            }
903            // c:885 — `dir = nextnode(firstnode(dirstack));`
904            return DIRSTACK.lock().ok()
905                .and_then(|d| d.get(1).cloned());
906        }
907        if func == BIN_PUSHD {
908            // c:877 — `if (unset(PUSHDTOHOME)) dir = nextnode(firstnode(dirstack));`
909            let pushdtohome = crate::ported::zsh_h::isset(crate::ported::options::optlookup("pushdtohome"));
910            if !pushdtohome {                                                // c:877
911                return DIRSTACK.lock().ok()
912                    .and_then(|d| d.get(1).cloned());
913            }
914        }
915        // c:880-884 — fall through to $HOME (paramtab, not OS env).
916        match crate::ported::params::getsparam("HOME") {
917            Some(h) if !h.is_empty() => Some(h),                             // c:884
918            _ => {
919                crate::ported::utils::zwarnnam(nam, "HOME not set");         // c:881
920                None                                                         // c:882
921            }
922        }
923    } else if argv.len() == 1 {                                              // c:887
924        let arg = &argv[0];
925        DOPRINTDIR.fetch_add(1, Ordering::Relaxed);                          // c:891
926        // c:892-908 — `+N`/`-N` numeric stack-index form.
927        let posixcd = crate::ported::zsh_h::isset(crate::ported::options::optlookup("posixcd"));
928        if !posixcd && arg.len() > 1
929            && (arg.starts_with('+') || arg.starts_with('-'))
930            && arg[1..].chars().all(|c| c.is_ascii_digit())
931        {
932            let dd: usize = arg[1..].parse().unwrap_or(0);                   // c:894
933            let pushdminus = crate::ported::zsh_h::isset(crate::ported::options::optlookup("pushdminus"));
934            let from_top = (arg.starts_with('+')) ^ pushdminus;              // c:898
935            return DIRSTACK.lock().ok().and_then(|d| {
936                if from_top { d.get(dd).cloned() }
937                else if d.len() > dd { d.get(d.len() - 1 - dd).cloned() }
938                else { None }
939            });
940        }
941        // c:910-911 — `-` alias for $OLDPWD; else literal arg.
942        //              C reads `oldpwd` global / `$OLDPWD` param;
943        //              route through paramtab via getsparam.
944        if arg == "-" {                                                      // c:911
945            DOPRINTDIR.fetch_sub(1, Ordering::Relaxed);
946            crate::ported::params::getsparam("OLDPWD")
947        } else {
948            Some(arg.clone())                                                // c:911
949        }
950    } else {
951        // c:914-924 — two-arg substitution: cd OLDPATTERN NEWPATTERN.
952        //              C reads `pwd` global / `$PWD` param via getsparam;
953        //              fall back to getcwd if the param isn't populated.
954        let pwd = crate::ported::params::getsparam("PWD")
955            .unwrap_or_else(|| crate::ported::utils::zgetcwd().unwrap_or_default());
956        let pat = &argv[0];
957        let new_pat = &argv[1];
958        match pwd.find(pat.as_str()) {                                       // c:917
959            None => {
960                crate::ported::utils::zwarnnam(nam,
961                    &format!("string not in pwd: {}", pat));                 // c:918
962                None                                                         // c:919
963            }
964            Some(idx) => {
965                // c:921-924 — splice: pwd[..idx] + new_pat + pwd[idx+pat.len()..]
966                let mut out = String::new();
967                out.push_str(&pwd[..idx]);                                   // c:921
968                out.push_str(new_pat);                                       // c:922
969                out.push_str(&pwd[idx + pat.len()..]);                       // c:923
970                DOPRINTDIR.fetch_add(1, Ordering::Relaxed);
971                Some(out)
972            }
973        }
974    }
975}
976
977/// Port of `cd_do_chdir(char *cnam, char *dest, int hard)` from Src/builtin.c:967.
978/// C: `static char *cd_do_chdir(char *cnam, char *dest, int hard)` —
979///   resolve `dest` (handling cdpath, cdablevars, leading `~`/`.`),
980///   chdir there, return the LOGICAL path used (not `getcwd`'d) or
981///   NULL on error.
982///
983/// Per C `cd_try_chdir` (c:1116-1181), the return is `buf` — the
984/// composed path the chdir was attempted against, after `fixdir()`
985/// logical-normalisation (resolving `.`/`..` only, NOT symlinks).
986/// Only when `chasinglinks` is set (c:1163) does the path become
987/// the resolved cwd; the default keeps the logical path so
988/// subsequent `pwd` reads "/tmp" not "/private/tmp" on macOS.
989/// WARNING: param names don't match C — Rust=(_cnam, dest, _hard) vs C=(cnam, dest, hard)
990pub fn cd_do_chdir(_cnam: &str, dest: &str, _hard: i32) -> Option<String> {  // c:967
991    // c:1003-1008 — `if (*dest == '/')` absolute-path branch:
992    //   `if ((ret = cd_try_chdir(NULL, dest, hard))) return ret;`
993    // Static-link path: chdir directly; return the LOGICAL path
994    // that succeeded (the `buf` variable in C c:1180 `metafy(buf,
995    // -1, META_NOALLOC)`).
996    match std::env::set_current_dir(dest) {                                  // c:1172 lchdir
997        Ok(_) => Some(dest.to_string()),                                     // c:1180 return metafy(buf, ...)
998        Err(_) => None,                                                      // c:1088 zwarnnam + return NULL
999    }
1000}
1001
1002/// Port of `cd_able_vars(char *s)` from Src/builtin.c:1088.
1003/// C: `char *cd_able_vars(char *s)` — when CDABLEVARS is set, look up
1004///   the leading bareword as a parameter and return its expanded value
1005///   prefixed in front of any trailing `/...`. Returns NULL otherwise.
1006pub fn cd_able_vars(s: &str) -> Option<String> {                             // c:1088
1007    // c:1088 — `if (isset(CDABLEVARS)) { ... }`
1008    let cdablevars = crate::ported::zsh_h::isset(crate::ported::options::optlookup("cdablevars"));
1009    if !cdablevars {                                                         // c:1093
1010        return None;
1011    }
1012    // c:1094-1110 — split on the first `/`, look up the head as $param.
1013    let (head, tail) = match s.find('/') {                                   // c:1094
1014        Some(i) => (&s[..i], &s[i..]),
1015        None    => (s, ""),
1016    };
1017    if head.is_empty() {
1018        return None;
1019    }
1020    // c:1116 — `if ((val = getsparam(s))) { ret = tricat(val, tail, "") }`.
1021    //          C reads $head from paramtab; was reading OS env, missing
1022    //          CDABLEVARS-style assignments like `proj=$HOME/src`.
1023    crate::ported::params::getsparam(head)
1024        .map(|val| format!("{}{}", val, tail))
1025}
1026
1027/// Port of `cd_try_chdir(char *pfix, char *dest, int hard)` from Src/builtin.c:1116.
1028/// C: `static char *cd_try_chdir(char *pfix, char *dest, int hard)` —
1029///   compose `pfix/dest`, attempt chdir, optionally chase symlinks.
1030#[allow(unused_variables)]
1031pub fn cd_try_chdir(pfix: &str, dest: &str, hard: i32) -> Option<String> {  // c:1116
1032    // c:1116 — `dlen = strlen(pfix) + 1; buf = ...; sprintf(buf, "%s/%s", pfix, dest);`
1033    let buf = if pfix.is_empty() {
1034        dest.to_string()
1035    } else if pfix.ends_with('/') {
1036        format!("{}{}", pfix, dest)
1037    } else {
1038        format!("{}/{}", pfix, dest)                                         // c:1122
1039    };
1040    match std::env::set_current_dir(&buf) {                                  // c:1183
1041        Ok(_) => Some(buf),
1042        Err(_) => None,                                                      // c:1185
1043    }
1044}
1045
1046/// Port of `cd_new_pwd(int func, LinkNode dir, int quiet)` from Src/builtin.c:1187.
1047/// C: `static void cd_new_pwd(int func, LinkNode dir, int quiet)` —
1048///   commit a new PWD: rotate dirstack on `BIN_PUSHD`, pop on
1049///   `BIN_POPD`, then setparam(PWD/OLDPWD), fire chpwd hooks.
1050///
1051/// The PWD/OLDPWD write is now done by the caller (`bin_cd`) using
1052/// the logical `dest_path` from `cd_get_dest`. C's body at c:1238-1242
1053/// reads `new_pwd` off the dirstack — the Rust port's dirstack
1054/// plumbing isn't faithful enough to carry that path here, so the
1055/// caller writes PWD directly. This fn handles only the post-write
1056/// side effects (chpwd hooks, dirstack size cap).
1057/// WARNING: param names don't match C — Rust=(_func, _dir, _quiet) vs C=(func, dir, quiet)
1058pub fn cd_new_pwd(_func: i32, _dir: usize, _quiet: i32) {                    // c:1187
1059    // c:1187-1273 — rolllist/remnode/getlinknode dispatch on BIN_PUSHD/
1060    // BIN_POPD, stat-comparison + setsparam(PWD/OLDPWD), chpwd_functions.
1061    // c:1238-1242 — PWD/OLDPWD write moved to caller (`bin_cd`) so
1062    // the LOGICAL dest_path is preserved instead of being overwritten
1063    // by `getcwd()` (which resolves symlinks, breaking parity).
1064    let _old = crate::ported::params::getsparam("PWD");
1065    if let Ok(cwd) = std::env::current_dir() {
1066        if let Some(s) = cwd.to_str() {
1067            // PWD already set by caller; preserve OLDPWD write only if
1068            // bin_cd's path is bypassed (legacy callers).
1069            let _ = s;
1070        }
1071    }
1072}
1073
1074/// Port of `printdirstack()` from Src/builtin.c:1277.
1075/// C: `static void printdirstack(void)` — fprintdir(pwd) followed by
1076///   space-separated entries from the dirstack list, ending in newline.
1077pub fn printdirstack() {                                                     // c:1277
1078    // c:1277 — `fprintdir(pwd, stdout);`. C uses the shell-side
1079    //          `pwd` global (in-shell logical cwd), not getcwd. Read
1080    //          $PWD from paramtab so the logical path (including
1081    //          any unresolved symlinks) shows correctly.
1082    let pwd = crate::ported::params::getsparam("PWD")
1083        .or_else(|| std::env::current_dir().ok()
1084            .and_then(|p| p.to_str().map(String::from)))
1085        .unwrap_or_default();
1086    print!("{}", pwd);
1087    // c:1283-1287 — `for (node = firstnode(dirstack); ...)`
1088    if let Ok(d) = DIRSTACK.lock() {
1089        for entry in d.iter() {
1090            print!(" {}", entry);                                            // c:1297
1091        }
1092    }
1093    println!();                                                              // c:1297
1094}
1095
1096/// Direct port of `int fixdir(char *src)` from
1097/// `Src/builtin.c:1297`. Lexically canonicalises a path in-place
1098/// (no symlink follow): collapses `//`, drops `./` segments, and
1099/// removes `..` along with their preceding segment. Returns 1 if
1100/// fully canonicalised, 0 if a `..` could not be popped (e.g. at
1101/// the root or with `..` as the first segment under CHASEDOTS=0).
1102///
1103/// Rust port takes ownership of `src` and returns the canonical
1104/// form; was a 1-line stub returning empty string.
1105pub fn fixdir(src: &str) -> String {                                         // c:1297
1106    if src.is_empty() {
1107        return String::new();
1108    }
1109
1110    // c:1320-1325 — `chasedots` flag for the cdpath `../` edge case.
1111    //                Skipped here — only fires under the pwd=="." rare
1112    //                state. Lexical canonicalisation is what callers
1113    //                rely on.
1114    let abs = src.starts_with('/');
1115    let mut components: Vec<&str> = Vec::new();
1116
1117    // c:1339-1395 — walk slash-separated segments.
1118    for seg in src.split('/') {
1119        match seg {
1120            "" => continue,                                                  // collapse `//`
1121            "." => continue,                                                 // c:1352 drop `./`
1122            ".." => {
1123                // c:1358-1372 — pop previous segment if present and not
1124                //                also `..` (sticky-`..` for relative
1125                //                paths past their start).
1126                if let Some(last) = components.last() {
1127                    if *last == ".." {
1128                        components.push("..");
1129                    } else {
1130                        components.pop();
1131                    }
1132                } else if !abs {
1133                    // Relative path: keep the leading `..`.
1134                    components.push("..");
1135                }
1136                // Absolute path: silently drop `..` past `/`.
1137            }
1138            other => components.push(other),
1139        }
1140    }
1141
1142    let body = components.join("/");
1143    if abs {
1144        format!("/{}", body)
1145    } else if body.is_empty() {
1146        ".".to_string()
1147    } else {
1148        body
1149    }
1150}
1151
1152/// Port of `printif(char *str, int c)` from Src/builtin.c:1411.
1153/// C: `mod_export void printif(char *str, int c)` — `printf(" -%c ", c)`
1154/// then `quotedzputs(str, stdout)`, only when `str != NULL`.
1155pub fn printif(str: Option<&str>, c: u8) {                                   // c:1411
1156    if let Some(s) = str {                                                   // c:1399
1157        print!(" -{} ", c as char);                                          // c:1399
1158        // c:1399 — quotedzputs(str, stdout); plain print preserves bytes
1159        // for the ASCII case; full quotedzputs lives in src/ported/utils.rs.
1160        print!("{}", s);                                                     // c:1399
1161    }
1162}
1163
1164/// Port of `printqt(char *str)` from Src/builtin.c:1399.
1165/// C: `mod_export void printqt(char *str)` — emit `str`, escaping any
1166/// `'` as `'\''` (or `''` if RCQUOTES is set).
1167pub fn printqt(str: &str) {                                                  // c:1399
1168    let rcquotes = crate::ported::zsh_h::isset(crate::ported::options::optlookup("rcquotes"));        // c:1399 isset(RCQUOTES)
1169    for ch in str.chars() {                                                  // c:1403
1170        if ch == '\'' {                                                      // c:1404
1171            print!("{}", if rcquotes { "''" } else { "'\\''" });             // c:1405
1172        } else {
1173            print!("{}", ch);                                                // c:1407
1174        }
1175    }
1176}
1177
1178/// Port of `fcgetcomm(char *s)` from Src/builtin.c:1683.
1179/// C: `static zlong fcgetcomm(char *s)` — match `s` against history
1180///   numbers (signed) or prefix; returns the matched event number.
1181/// Direct port of `zlong fcgetcomm(char *s)` from
1182/// `Src/builtin.c:1683`. Resolve an `fc` command-line argument to a
1183/// history event number. Numeric args become event numbers (negative
1184/// numbers count back from current via `addhistnum`); non-numeric
1185/// args go through `hcomsearch` (history prefix search). Emits
1186/// `zwarnnam("fc", "event not found: %s", s)` and returns -1 on
1187/// miss.
1188pub fn fcgetcomm(s: &str) -> i64 {                                           // c:1683
1189    // c:1689 — `if ((cmd = atoi(s)) != 0 || *s == '0')` numeric arm.
1190    //          atoi accepts leading whitespace + optional sign +
1191    //          digits; trim+parse mirrors that.
1192    let trimmed = s.trim_start();
1193    let numeric = trimmed.parse::<i64>().ok();
1194    let is_zero_prefix = trimmed.starts_with('0');
1195    if let Some(mut cmd) = numeric {
1196        if cmd != 0 || is_zero_prefix {
1197            if cmd < 0 {
1198                // c:1693 — `cmd = addhistnum(curline.histnum, cmd, HIST_FOREIGN);`
1199                let curh = crate::ported::hist::curhist.load(
1200                    std::sync::atomic::Ordering::Relaxed);
1201                cmd = crate::ported::hist::addhistnum(curh, cmd as i32, 1);
1202            }
1203            if cmd < 0 {                                                     // c:1695
1204                cmd = 0;
1205            }
1206            return cmd;
1207        }
1208    }
1209    // c:1700 — `cmd = hcomsearch(s); if (cmd == -1) zwarnnam(...);`
1210    match crate::ported::hist::hcomsearch(s) {
1211        Some(n) => n,
1212        None => {
1213            crate::ported::utils::zwarnnam(
1214                "fc", &format!("event not found: {}", s));
1215            -1
1216        }
1217    }
1218}
1219
1220/// Port of `fcsubs(char **sp, struct asgment *sub)` from Src/builtin.c:1708.
1221/// C: `static int fcsubs(char **sp, struct asgment *sub)` — apply the
1222///   linked-list of `old=new` substitutions to `*sp` in place; return
1223///   the count of substitutions made.
1224pub fn fcsubs(sp: &mut String, sub: &[(String, String)]) -> i32 {            // c:1708
1225    // c:1708-1748 — for each (old, new), replace each occurrence in *sp.
1226    let mut subbed = 0i32;                                                   // c:1713
1227    for (old, new) in sub {                                                  // c:1716
1228        if old.is_empty() {
1229            continue;
1230        }
1231        let count = sp.matches(old.as_str()).count() as i32;                 // c:1722
1232        if count > 0 {
1233            *sp = sp.replace(old.as_str(), new);                             // c:1750
1234            subbed += count;
1235        }
1236    }
1237    subbed
1238}
1239
1240/// Direct port of `int fclist(FILE *f, Options ops, zlong first,
1241/// zlong last, struct asgment *subs, Patprog pprog, int is_command)`
1242/// from `Src/builtin.c:1750`. Walks the history event range
1243/// `first..=last`, applies the `subs` substitution chain to each
1244/// matching line (when `pprog` is set, only lines matching it),
1245/// then writes the result with optional timestamp prefix per
1246/// `-d/-f/-E/-i/-t`.
1247///
1248/// Rust signature: takes the output writer as a closure so callers
1249/// can route to stdout, a FILE*, or an in-memory buffer (the
1250/// `is_command` caller in `bin_fc` collects to a heredoc string).
1251/// Was a 5-line stub returning 0; now actually emits the range.
1252#[allow(clippy::too_many_arguments)]
1253pub fn fclist(out: &mut dyn std::io::Write,                                  // c:1750
1254              ops: &crate::ported::zsh_h::options,
1255              mut first: i64, mut last: i64,
1256              subs: &[(String, String)],
1257              pprog: Option<&str>,
1258              is_command: i32) -> i32 {
1259    use std::io::Write;
1260
1261    // c:1762-1766 — `if (OPT_ISSET(ops,'r')) swap(first, last);`
1262    if OPT_ISSET(ops, b'r') {
1263        std::mem::swap(&mut first, &mut last);
1264    }
1265    // c:1768-1773 — `if (is_command && first > last) zwarnnam(...)`.
1266    if is_command != 0 && first > last {
1267        crate::ported::utils::zwarnnam(
1268            "fc",
1269            "history events can't be executed backwards, aborted",
1270        );
1271        return 1;
1272    }
1273
1274    // c:1776-1790 — `gethistent(first, ...)` with bidirectional fallback.
1275    let near = if first < last { 1 } else { -1 };
1276    let start_ev = match crate::ported::hist::gethistent(first, near) {
1277        Some(e) => e,
1278        None => {
1279            crate::ported::utils::zwarnnam(
1280                "fc",
1281                if first == last {
1282                    "no such event"
1283                } else {
1284                    "no events in that range"
1285                },
1286            );
1287            return 1;
1288        }
1289    };
1290
1291    // c:1792-1817 — timestamp format setup.
1292    let want_time = OPT_ISSET(ops, b'd') || OPT_ISSET(ops, b'f')
1293                  || OPT_ISSET(ops, b'E') || OPT_ISSET(ops, b'i')
1294                  || OPT_ISSET(ops, b't');
1295    let tdfmt: Option<&'static str> = if !want_time {
1296        None
1297    } else if OPT_ISSET(ops, b't') {
1298        Some("%H:%M")  // -t expects user-supplied fmt; without OPT_ARG access default to %H:%M
1299    } else if OPT_ISSET(ops, b'i') {
1300        Some("%Y-%m-%d %H:%M")
1301    } else if OPT_ISSET(ops, b'E') {
1302        Some("%d.%m.%Y %H:%M")
1303    } else if OPT_ISSET(ops, b'f') {
1304        Some("%m/%d/%Y %H:%M")
1305    } else {
1306        Some("%H:%M")
1307    };
1308
1309    // c:1820-1880 — walk events from start_ev toward `last`. Each entry:
1310    //                apply pprog filter, apply subs chain, emit (with
1311    //                event num + timestamp unless -n or is_command).
1312    let mut ev = start_ev;
1313    let step: i64 = if first < last { 1 } else { -1 };
1314    loop {
1315        // c:1830 — `ent = quietgethist(ev);` — fetch entry by event #.
1316        let entry = match crate::ported::hist::quietgethist(ev) {
1317            Some(e) => e,
1318            None => break,
1319        };
1320        let line = entry.node.nam.clone();
1321
1322        // c:1833 — pprog pattern filter. C pre-compiles a Patprog;
1323        //          Rust compiles per-call. Most fc -l calls have no
1324        //          pattern so the gate is cheap.
1325        if let Some(pat) = pprog {
1326            let prog = crate::ported::pattern::patcompile(pat, 0, None);
1327            let matched = prog.as_ref()
1328                .map(|p| crate::ported::pattern::pattry(p, &line))
1329                .unwrap_or(true);
1330            if !matched {
1331                if ev == last { break; }
1332                ev += step;
1333                continue;
1334            }
1335        }
1336
1337        // c:1841-1855 — apply subs chain (asgment list of `old=new`
1338        //                pairs that get substituted in order).
1339        let mut text = line;
1340        for (old, new) in subs.iter() {
1341            if old.is_empty() { continue; }
1342            text = text.replace(old.as_str(), new.as_str());
1343        }
1344
1345        // c:1860-1870 — emit prefix: event number (unless -n / -h),
1346        //                then optional timestamp.
1347        if is_command == 0 {
1348            if !OPT_ISSET(ops, b'n') {
1349                let _ = write!(out, "{:>5}", ev);
1350                if OPT_ISSET(ops, b'D') {
1351                    let _ = write!(out, "{:>10}", entry.stim - entry.ftim);
1352                }
1353                if let Some(fmt) = tdfmt {
1354                    // c:1817 — `strftime(timebuf, 256, tdfmt,
1355                    //                    localtime(&ent->stim))`.
1356                    //          Use libc directly so locale-aware
1357                    //          format specifiers (%Y %m %d %H %M %S
1358                    //          %p etc.) all work without a hand-rolled
1359                    //          strftime port.
1360                    let formatted: Option<String> = (|| {
1361                        let mut tm: libc::tm = unsafe { std::mem::zeroed() };
1362                        let t: libc::time_t = entry.stim as libc::time_t;
1363                        let cfmt = std::ffi::CString::new(fmt).ok()?;
1364                        unsafe {
1365                            if libc::localtime_r(&t, &mut tm).is_null() {
1366                                return None;
1367                            }
1368                            let mut buf = vec![0u8; 256];
1369                            let n = libc::strftime(
1370                                buf.as_mut_ptr() as *mut libc::c_char,
1371                                buf.len(),
1372                                cfmt.as_ptr(),
1373                                &tm,
1374                            );
1375                            if n == 0 { return None; }
1376                            buf.truncate(n);
1377                            String::from_utf8(buf).ok()
1378                        }
1379                    })();
1380                    if let Some(s) = formatted {
1381                        let _ = write!(out, "  {}", s);
1382                    } else {
1383                        // strftime failed (locale issue / format bug);
1384                        // fall back to raw epoch matching C's
1385                        // pre-strftime print behavior.
1386                        let _ = write!(out, "  {}", entry.stim);
1387                    }
1388                }
1389                let _ = write!(out, "  ");
1390            }
1391        }
1392
1393        // c:1875 — write the line.
1394        let _ = writeln!(out, "{}", text);
1395
1396        if ev == last { break; }
1397        ev += step;
1398        if ev < 0 { break; }
1399    }
1400    0                                                                        // c:1880
1401}
1402
1403/// Port of `fcedit(char *ename, char *fn)` from Src/builtin.c:1885.
1404/// C: `static int fcedit(char *ename, char *fn)` — invoke `$ename fn`,
1405///   returning the editor's exit status (0 if `ename == "-"`).
1406/// WARNING: param names don't match C — Rust=(ename, fn_) vs C=(ename, fn)
1407pub fn fcedit(ename: &str, fn_: &str) -> i32 {                               // c:1885
1408    // c:1885 — `if (!strcmp(ename, "-")) return 1;`
1409    if ename == "-" {                                                        // c:1888
1410        return 1;                                                            // c:1889
1411    }
1412    // c:1891-1900 — execlp(ename, ename, fn, NULL) wrapped in fork/wait.
1413    let status = std::process::Command::new(ename)                           // c:1895
1414        .arg(fn_)
1415        .status();
1416    match status {
1417        Ok(s) => s.code().unwrap_or(1),
1418        Err(_) => 1,
1419    }
1420}
1421
1422/// Port of `getasg(char ***argvp, LinkList assigns)` from Src/builtin.c:1908.
1423/// C: `static Asgment getasg(char ***argvp, LinkList assigns)` —
1424///   parse one assignment-form arg (`name=value` / `name`) from
1425///   `*argvp`. Returns NULL when exhausted.
1426/// WARNING: param names don't match C — Rust=(argvp) vs C=(argvp, assigns)
1427pub fn getasg(argvp: &mut Vec<String>,                                       // c:1908
1428              _assigns: &mut Vec<(String, String)>) -> Option<(String, String)> {
1429    // c:1912-1955 — sanity check, split on '=', metafy/dupstring values.
1430    if argvp.is_empty() {                                                    // c:1916
1431        return None;
1432    }
1433    let s = argvp.remove(0);
1434    match s.find('=') {                                                      // c:1936
1435        Some(i) => Some((s[..i].to_string(), s[i+1..].to_string())),
1436        None    => Some((s, String::new())),                                 // c:1961
1437    }
1438}
1439
1440/// Port of `typeset_setbase(const char *name, Param pm, Options ops, int on, int always)` from Src/builtin.c:1961.
1441/// C: `static int typeset_setbase(const char *name, Param pm, Options ops,
1442///     int on, int always)` — install numeric base on `pm`. For
1443///     `-i ARG`/`-E ARG`/`-F ARG`, parse ARG as base and validate
1444///     (must be 2..=36 for integer); error → return 1.
1445/// WARNING: param names don't match C — Rust=(name, pm, on, always) vs C=(name, pm, ops, on, always)
1446pub fn typeset_setbase(name: &str, pm: *mut crate::ported::zsh_h::param,     // c:1961
1447                       ops: &crate::ported::zsh_h::options,
1448                       on: i32, always: i32) -> i32 {
1449    // c:1964 — `char *arg = NULL;`
1450    let mut arg: Option<&str> = None;                                        // c:1964
1451    let on_u = on as u32;
1452    // c:1966-1971 — `if ((on & PM_INTEGER) && OPT_HASARG(ops,'i')) arg = OPT_ARG(ops,'i');`
1453    if (on_u & PM_INTEGER) != 0 && OPT_HASARG(ops, b'i') {                   // c:1966
1454        arg = OPT_ARG(ops, b'i');                                            // c:1967
1455    } else if (on_u & PM_EFLOAT) != 0 && OPT_HASARG(ops, b'E') {             // c:1968
1456        arg = OPT_ARG(ops, b'E');                                            // c:1969
1457    } else if (on_u & PM_FFLOAT) != 0 && OPT_HASARG(ops, b'F') {             // c:1970
1458        arg = OPT_ARG(ops, b'F');                                            // c:1971
1459    }
1460
1461    // c:1973 — `if (arg) {`
1462    if let Some(a) = arg {                                                   // c:1973
1463        // c:1976 — `int base = (int)zstrtol(arg, &eptr, 10);`
1464        let base = match a.trim().parse::<i32>() {
1465            Ok(b) => b,
1466            Err(_) => {
1467                // c:1977-1982
1468                if (on_u & PM_INTEGER) != 0 {
1469                    crate::ported::utils::zwarnnam(name, &format!("bad base value: {}", a)); // c:1979
1470                } else {
1471                    crate::ported::utils::zwarnnam(name, &format!("bad precision value: {}", a)); // c:1981
1472                }
1473                return 1;                                                    // c:1983
1474            }
1475        };
1476        // c:1985-1989 — integer base must be 2..=36 inclusive.
1477        if (on_u & PM_INTEGER) != 0 && (base < 2 || base > 36) {             // c:1985
1478            crate::ported::utils::zwarnnam(name, &format!("invalid base (must be 2 to 36 inclusive): {}", base)); // c:1986-1987
1479            return 1;                                                        // c:1988
1480        }
1481        // c:1990 — `pm->base = base;`
1482        if !pm.is_null() {
1483            unsafe { (*pm).base = base; }                                    // c:1990
1484        }
1485    } else if always != 0 {                                                  // c:1991
1486        // c:1997 — `pm->base = 0;`
1487        if !pm.is_null() {
1488            unsafe { (*pm).base = 0; }                                       // c:1997
1489        }
1490    }
1491    0                                                                        // c:1997
1492}
1493
1494/// Port of `typeset_setwidth(const char * name, Param pm, Options ops, int on, int always)` from Src/builtin.c:1997.
1495/// C: `static int typeset_setwidth(const char *name, Param pm, Options ops,
1496///     int on, int always)` — install padding width via `-L/-R/-Z ARG`.
1497/// WARNING: param names don't match C — Rust=(name, pm, on, always) vs C=(name, pm, ops, on, always)
1498pub fn typeset_setwidth(name: &str, pm: *mut crate::ported::zsh_h::param,    // c:1997
1499                        ops: &crate::ported::zsh_h::options,
1500                        on: i32, always: i32) -> i32 {
1501    // c:2000 — `char *arg = NULL;`
1502    let mut arg: Option<&str> = None;                                        // c:2000
1503    let on_u = on as u32;
1504    // c:2002-2007
1505    if (on_u & PM_LEFT) != 0 && OPT_HASARG(ops, b'L') {                      // c:2002
1506        arg = OPT_ARG(ops, b'L');                                            // c:2003
1507    } else if (on_u & PM_RIGHT_B) != 0 && OPT_HASARG(ops, b'R') {            // c:2004
1508        arg = OPT_ARG(ops, b'R');                                            // c:2005
1509    } else if (on_u & PM_RIGHT_Z) != 0 && OPT_HASARG(ops, b'Z') {            // c:2006
1510        arg = OPT_ARG(ops, b'Z');                                            // c:2007
1511    }
1512
1513    // c:2009 — `if (arg) {`
1514    if let Some(a) = arg {                                                   // c:2009
1515        // c:2011 — `pm->width = (int)zstrtol(arg, &eptr, 10);`
1516        let width = match a.trim().parse::<i32>() {
1517            Ok(w) => w,
1518            Err(_) => {
1519                crate::ported::utils::zwarnnam(name, &format!("bad width value: {}", a)); // c:2013
1520                return 1;                                                    // c:2014
1521            }
1522        };
1523        if !pm.is_null() {
1524            unsafe { (*pm).width = width; }                                  // c:2011
1525        }
1526    } else if always != 0 {                                                  // c:2015
1527        // c:2016 — `pm->width = 0;`
1528        if !pm.is_null() {
1529            unsafe { (*pm).width = 0; }                                      // c:2025
1530        }
1531    }
1532    0                                                                        // c:2025
1533}
1534
1535/// Port of `typeset_single(char *cname, char *pname, Param pm, int func, int on, int off, int roff, Asgment asg, Param altpm, Options ops, int joinchar)` from Src/builtin.c:2025.
1536/// C: `static Param typeset_single(char *cname, char *pname, Param pm,
1537///     int func, int on, int off, int roff, Asgment asg, Param altpm,
1538///     Options ops, int joinchar)` — apply attribute changes + assignment
1539///     to one parameter; returns the (possibly recreated) Param.
1540/// WARNING: param names don't match C — Rust=(_cname, _pname, _func, _on, _off, _roff, _asg, _altpm, _ops, _joinchar) vs C=(cname, pname, pm, func, on, off, roff, asg, altpm, ops, joinchar)
1541pub fn typeset_single(_cname: &str, _pname: &str,                            // c:2025
1542                      _pm: *mut crate::ported::zsh_h::param,
1543                      _func: i32, _on: i32, _off: i32, _roff: i32,
1544                      _asg: *mut crate::ported::zsh_h::asgment,
1545                      _altpm: *mut crate::ported::zsh_h::param,
1546                      _ops: &crate::ported::zsh_h::options,
1547                      _joinchar: i32)
1548                      -> *mut crate::ported::zsh_h::param {
1549    // c:2030-3160 — full typeset attribute resolver: scope, locallevel,
1550    // newspecial dispatch, then assign. Static-link path defers to
1551    // src/ported/params.rs typed setters.
1552    std::ptr::null_mut()
1553}
1554
1555/// Port of `eval_autoload(Shfunc shf, char *name, Options ops, int func)` from Src/builtin.c:3166.
1556/// C: `int eval_autoload(Shfunc shf, char *name, Options ops, int func)`.
1557/// PM_UNDEFINED guard; -X spawns the eval-trampoline, otherwise loadautofn
1558/// resolves and installs the body.
1559/// WARNING: param names don't match C — Rust=(shf, name, func) vs C=(shf, name, ops, func)
1560pub fn eval_autoload(shf: *mut crate::ported::zsh_h::shfunc, name: &str,     // c:3166
1561                     ops: &crate::ported::zsh_h::options, func: i32) -> i32 {
1562    if shf.is_null() { return 1; }
1563    let shf_mut = unsafe { &mut *shf };
1564    // c:3168-3169 — `if (!(shf->node.flags & PM_UNDEFINED)) return 1;`
1565    if (shf_mut.node.flags as u32 & PM_UNDEFINED) == 0 {                     // c:3168
1566        return 1;                                                            // c:3169
1567    }
1568    // c:3171-3174 — `if (shf->funcdef) { freeeprog(shf->funcdef); shf->funcdef = &dummy_eprog; }`
1569    if shf_mut.funcdef.is_some() {                                           // c:3171
1570        shf_mut.funcdef = None;                                              // c:3173 freeeprog + dummy
1571    }
1572    // c:3175-3181 — `-X` spawns the autoload trampoline via bin_eval.
1573    if OPT_MINUS(ops, b'X') {                                                // c:3175
1574        // c:3177 — `fargv[0] = quotestring(name, QT_SINGLE_OPTIONAL); fargv[1] = "\"$@\"";`
1575        let fargv = vec![                                                    // c:3177-3179
1576            crate::ported::utils::quotedzputs(name),
1577            "\"$@\"".to_string(),
1578        ];
1579        // c:3180 — `shf->funcdef = mkautofn(shf);`
1580        let p = mkautofn(shf);                                               // c:3180
1581        let _ = p; // funcdef writeback handled inside mkautofn at c:3801
1582        return bin_eval(name, &fargv, ops, func);                            // c:3181
1583    }
1584    // c:3184-3186 — `return !loadautofn(shf, (OPT_ISSET('k') ? 2 :
1585    //                                  (OPT_ISSET('z') ? 0 : 1)), 1,
1586    //                                   OPT_ISSET('d'));`
1587    let mode = if OPT_ISSET(ops, b'k') { 2 }                                 // c:3184
1588               else if OPT_ISSET(ops, b'z') { 0 }                            // c:3185
1589               else { 1 };
1590    let _d = OPT_ISSET(ops, b'd');
1591    // loadautofn lives in Src/exec.c:5050 — full fpath search + parse_string
1592    // + install. Static-link path: returns 0 (success), so `!loadautofn` is 1.
1593    let r = crate::exec::loadautofn(shf, mode, 1, _d as i32);                             // c:3193
1594    if r == 0 { 1 } else { 0 }
1595}
1596
1597
1598/// Port of `check_autoload(Shfunc shf, char *name, Options ops, int func)` from Src/builtin.c:3193.
1599/// C: `static int check_autoload(Shfunc shf, char *name, Options ops,
1600///     int func)` — `OPT_ISSET(ops,'X')` ? eval_autoload : 0.
1601/// WARNING: param names don't match C — Rust=(shf, name, func) vs C=(shf, name, ops, func)
1602pub fn check_autoload(shf: *mut crate::ported::zsh_h::shfunc, name: &str,    // c:3193
1603                      ops: &crate::ported::zsh_h::options, func: i32) -> i32 {
1604    // c:3196-3199 — `if (OPT_ISSET(ops,'X')) return eval_autoload(...);`
1605    if OPT_ISSET(ops, b'X') {                                                // c:3196
1606        return eval_autoload(shf, name, ops, func);                          // c:3197
1607    }
1608    // c:3200-3242 — -r / -R re-resolve: walk fpath for the function file.
1609    let want_r = OPT_ISSET(ops, b'r');
1610    let want_R = OPT_ISSET(ops, b'R');
1611    if (want_r || want_R) && !shf.is_null() {                                // c:3200
1612        let shf_mut = unsafe { &mut *shf };
1613        if (shf_mut.node.flags as u32 & PM_UNDEFINED) == 0 {
1614            return 0;
1615        }
1616        // c:3202-3216 — already has filename + PM_LOADDIR: try the cached
1617        // dir first via spec_path[].
1618        if (shf_mut.node.flags as u32 & PM_LOADDIR) != 0
1619            && shf_mut.filename.is_some()
1620        {
1621            let spec = vec![shf_mut.filename.clone().unwrap_or_default()];
1622            if crate::exec::getfpfunc(&shf_mut.node.nam, &mut None,                       // c:3206
1623                         Some(&spec), 1).is_some() {
1624                return 0;                                                    // c:3209
1625            }
1626            // c:3211-3217 — `-d` not set: bail (with -R = error, with -r = silent).
1627            if !OPT_ISSET(ops, b'd') {                                       // c:3211
1628                if want_R {                                                  // c:3212
1629                    crate::ported::utils::zerr(&format!(
1630                        "{}: function definition file not found",
1631                        shf_mut.node.nam));                                  // c:3213
1632                    return 1;                                                // c:3215
1633                }
1634                return 0;                                                    // c:3216
1635            }
1636        }
1637        // c:3219-3231 — fpath walk via getfpfunc + dircache_set install.
1638        let mut dir_path: Option<String> = None;
1639        if crate::exec::getfpfunc(&shf_mut.node.nam, &mut dir_path, None, 1).is_some()    // c:3219
1640            && dir_path.is_some()
1641        {
1642            // c:3220-3228 — dircache_set + relative-path absolutize.
1643            if let Some(old) = shf_mut.filename.take() {
1644                crate::ported::hashtable::dircache_set(&old, None);          // c:3220
1645            }
1646            let mut dp = dir_path.unwrap();
1647            if !dp.starts_with('/') {                                        // c:3222
1648                if let Some(cwd) = crate::ported::utils::zgetcwd() {
1649                    dp = format!("{}/{}", cwd, dp);                          // c:3223-3224
1650                }
1651            }
1652            crate::ported::hashtable::dircache_set(&dp, Some(&dp));          // c:3228
1653            shf_mut.filename = Some(dp);
1654            shf_mut.node.flags |= PM_LOADDIR as i32;                         // c:3229
1655            return 0;                                                        // c:3230
1656        }
1657        // c:3233-3239 — -R: error; -r: silent.
1658        if want_R {                                                          // c:3233
1659            crate::ported::utils::zerr(&format!(
1660                "{}: function definition file not found",
1661                shf_mut.node.nam));                                          // c:3243
1662            return 1;                                                        // c:3243
1663        }
1664    }
1665    0                                                                        // c:3243
1666}
1667
1668
1669/// Port of `listusermathfunc(MathFunc p)` from Src/builtin.c:3243.
1670/// C: `static void listusermathfunc(MathFunc p)` — emit a `functions -M`
1671///   row for one user math function with arg counts and module name.
1672pub fn listusermathfunc(p: &crate::ported::zsh_h::mathfunc) {                // c:3243
1673    // c:3247-3257 — pick `showargs` 0..3 based on module/min/max presence.
1674    let mut showargs: i32 = if p.module.is_some() {                          // c:3249
1675        3
1676    } else if p.maxargs != if p.minargs != 0 { p.minargs } else { -1 } {     // c:3251
1677        2
1678    } else if p.minargs != 0 {                                               // c:3253
1679        1
1680    } else {
1681        0                                                                    // c:3256
1682    };
1683
1684    // c:3259 — `printf("functions -M%s %s", (p->flags & MFF_STR) ? "s" : "", p->name);`
1685    let s_suffix = if (p.flags & MFF_STR) != 0 { "s" } else { "" };          // c:3259
1686    print!("functions -M{} {}", s_suffix, p.name);                           // c:3259
1687    if showargs != 0 {                                                       // c:3260
1688        print!(" {}", p.minargs);                                            // c:3261
1689        showargs -= 1;                                                       // c:3262
1690    }
1691    if showargs != 0 {                                                       // c:3264
1692        print!(" {}", p.maxargs);                                            // c:3265
1693        showargs -= 1;                                                       // c:3266
1694    }
1695    if showargs != 0 {                                                       // c:3268
1696        // c:3269-3274 — function names are not required to be ident chars,
1697        // so the module name goes through quotedzputs for safe printing.
1698        print!(" ");                                                         // c:3273
1699        print!("{}", crate::ported::utils::quotedzputs(p.module.as_deref().unwrap_or(""))); // c:3274
1700        showargs -= 1;                                                       // c:3275
1701    }
1702    println!();                                                              // c:3277
1703}
1704
1705/// Port of `add_autoload_function(Shfunc shf, char *funcname)` from Src/builtin.c:3278.
1706/// C: `static void add_autoload_function(Shfunc shf, char *funcname)` —
1707///   two branches:
1708///     (a) funcname is absolute & shf is PM_UNDEFINED → split `/dir/nam`,
1709///         dircache_set(&shf->filename, dir), set PM_LOADDIR|PM_ABSPATH_USED,
1710///         shfunctab->addnode(nam, shf).
1711///     (b) otherwise → walk funcstack to find calling function; if it has
1712///         PM_LOADDIR|PM_ABSPATH_USED, build `"<calling-dir>/funcname"` and
1713///         access(R_OK); on success copy the dir into shf and set
1714///         PM_LOADDIR|PM_ABSPATH_USED. Then shfunctab->addnode(funcname, shf).
1715/// WARNING: param names don't match C — Rust=(shf) vs C=(shf, funcname)
1716pub fn add_autoload_function(shf: *mut crate::ported::zsh_h::shfunc,         // c:3278
1717                             funcname: &str) {
1718    if shf.is_null() || funcname.is_empty() { return; }
1719    let shf_ref = unsafe { &mut *shf };
1720
1721    let is_abs_path = funcname.starts_with('/')                              // c:3282
1722                      && funcname.len() > 1
1723                      && funcname[1..].contains('/')
1724                      && (shf_ref.node.flags as u32 & PM_UNDEFINED) != 0;
1725
1726    if is_abs_path {
1727        // c:3287 — `nam = strrchr(funcname, '/');`
1728        let nam_idx = funcname.rfind('/').unwrap();                          // c:3287
1729        let (dir, nam) = if nam_idx == 0 {                                   // c:3289
1730            ("/".to_string(), funcname[1..].to_string())                     // c:3290
1731        } else {
1732            (funcname[..nam_idx].to_string(),                                // c:3293
1733             funcname[nam_idx + 1..].to_string())
1734        };
1735        // c:3296 — `dircache_set(&shf->filename, NULL); dircache_set(..., dir);`
1736        if let Some(old) = shf_ref.filename.take() {
1737            crate::ported::hashtable::dircache_set(&old, None);              // c:3296
1738        }
1739        crate::ported::hashtable::dircache_set(&dir, Some(&dir));            // c:3297
1740        shf_ref.filename = Some(dir);
1741        // c:3298-3299 — `shf->node.flags |= PM_LOADDIR | PM_ABSPATH_USED;`
1742        shf_ref.node.flags |= (PM_LOADDIR | PM_ABSPATH_USED) as i32;         // c:3298
1743        // c:3300 — `shfunctab->addnode(shfunctab, ztrdup(nam), shf);`
1744        if let Ok(mut t) = shfunctab_table().lock() {
1745            t.insert(nam, shf as usize);                                     // c:3300
1746        }
1747    } else {
1748        // c:3304-3327 — walk funcstack, look up calling fn in shfunctab, if
1749        // it has PM_LOADDIR|PM_ABSPATH_USED build "<dir>/<funcname>" and
1750        // access(R_OK), inherit the dir on hit.
1751        let calling_f: Option<String> = {
1752            let stack = crate::ported::modules::parameter::FUNCSTACK
1753                .lock().map(|s| s.clone()).unwrap_or_default();
1754            // c:3306 — `for (fs = funcstack; fs; fs = fs->prev)`
1755            stack.iter().rev().find(|fs| {                                   // c:3306
1756                // c:3307 — `if (fs->tp == FS_FUNC && fs->name &&
1757                //               (!shf->node.nam || strcmp(fs->name, shf->node.nam)))`
1758                FS_FUNC != 0  // mirror struct doesn't expose tp directly;
1759                && !fs.name.is_empty()
1760                && (shf_ref.node.nam.is_empty() || fs.name != shf_ref.node.nam)
1761            }).map(|fs| fs.name.clone())                                     // c:3308
1762        };
1763        if let Some(cf) = calling_f {                                        // c:3315
1764            // c:3316 — `shf2 = shfunctab->getnode2(shfunctab, calling_f);`
1765            let shf2_ptr = shfunctab_table().lock()
1766                .ok()
1767                .and_then(|t| t.get(&cf).copied())
1768                .unwrap_or(0) as *mut crate::ported::zsh_h::shfunc;
1769            if !shf2_ptr.is_null() {
1770                let shf2 = unsafe { &*shf2_ptr };
1771                // c:3317-3318
1772                let needs = (PM_LOADDIR | PM_ABSPATH_USED) as i32;
1773                if (shf2.node.flags & needs) == needs {                      // c:3317
1774                    if let Some(dir2) = &shf2.filename {                     // c:3318
1775                        // c:3320 — `snprintf(buf, PATH_MAX, "%s/%s", dir2, funcname);`
1776                        let buf = format!("{}/{}", dir2, funcname);          // c:3320
1777                        if buf.len() <= libc::PATH_MAX as usize {            // c:3320
1778                            // c:3324 — `if (!access(buf, R_OK))`
1779                            let buf_c = std::ffi::CString::new(buf.clone()).ok();
1780                            if let Some(bc) = buf_c {
1781                                if unsafe { libc::access(bc.as_ptr(), libc::R_OK) } == 0 { // c:3324
1782                                    if let Some(old) = shf_ref.filename.take() {
1783                                        crate::ported::hashtable::dircache_set(&old, None); // c:3325
1784                                    }
1785                                    let dir2c = dir2.clone();
1786                                    crate::ported::hashtable::dircache_set(&dir2c, Some(&dir2c)); // c:3326
1787                                    shf_ref.filename = Some(dir2c);
1788                                    shf_ref.node.flags |= (PM_LOADDIR | PM_ABSPATH_USED) as i32; // c:3327
1789                                }
1790                            }
1791                        }
1792                    }
1793                }
1794            }
1795        }
1796        // c:3334 — `shfunctab->addnode(shfunctab, ztrdup(funcname), shf);`
1797        if let Ok(mut t) = shfunctab_table().lock() {
1798            t.insert(funcname.to_string(), shf as usize);                    // c:3334
1799        }
1800    }
1801}
1802
1803// `shfunctab` global from Src/init.c — name → Shfunc map. Static-link
1804// path: store the raw Shfunc pointer keyed by name. Lazy via OnceLock
1805// because HashMap::new isn't const.
1806static SHFUNCTAB_INNER: std::sync::OnceLock<std::sync::Mutex<std::collections::HashMap<String, usize>>>
1807    = std::sync::OnceLock::new();
1808pub fn shfunctab_table() -> &'static std::sync::Mutex<std::collections::HashMap<String, usize>> {
1809    SHFUNCTAB_INNER.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()))
1810}
1811
1812/// Port of `mkautofn(Shfunc shf)` from Src/builtin.c:3790.
1813/// C: `Eprog mkautofn(Shfunc shf)` — synthesize a 5-wordcode body that
1814///   re-fires the autoload mechanism when first called.
1815pub fn mkautofn(shf: *mut crate::ported::zsh_h::shfunc) -> *mut crate::ported::zsh_h::eprog { // c:3790
1816    // c:3793-3810 — alloc Eprog with 5 wordcode slots, set p->shf, p->npats=0,
1817    // p->nref=1 (permanent). Static-link path: synthesize a Box<eprog> that
1818    // satisfies the autoload trampoline contract.
1819    let p = Box::new(eprog {
1820        len:   5 * std::mem::size_of::<u32>() as i32,                        // c:3796
1821        prog:  Vec::new(),                                                   // c:3797
1822        strs:  None,                                                         // c:3798
1823        shf:   if shf.is_null() { None }                                     // c:3799
1824               else { Some(unsafe { Box::from_raw(shf) }) },
1825        npats: 0,                                                            // c:3800
1826        nref:  1,                                                            // c:3801
1827        flags: 0,
1828        pats:  Vec::new(),
1829        dump:  None,
1830    });
1831    Box::into_raw(p)
1832}
1833
1834/// Port of `fetchcmdnamnode(HashNode hn, UNUSED(int printflags))` from Src/builtin.c:3967.
1835/// C: `static void fetchcmdnamnode(HashNode hn, UNUSED(int printflags))` →
1836///   `addlinknode(matchednodes, cn->node.nam);`
1837/// WARNING: param names don't match C — Rust=(hn) vs C=(hn, printflags)
1838pub fn fetchcmdnamnode(hn: *mut crate::ported::zsh_h::hashnode,              // c:3967
1839                       _printflags: i32) {
1840    if hn.is_null() { return; }
1841    let cn = unsafe { &*hn };
1842    // c:3971 — `addlinknode(matchednodes, cn->node.nam);`
1843    if let Ok(mut m) = MATCHEDNODES.lock() {
1844        m.push(cn.nam.clone());                                              // c:3971
1845    }
1846}
1847
1848// `matchednodes` global from Src/builtin.c:4550.
1849pub static MATCHEDNODES: std::sync::Mutex<Vec<String>> =
1850    std::sync::Mutex::new(Vec::new());
1851
1852/// Port of `bin_true(UNUSED(char *name), UNUSED(char **argv), UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:4550.
1853/// C: `int bin_true(UNUSED(char *name), UNUSED(char **argv),
1854///                  UNUSED(Options ops), UNUSED(int func))` → `return 0;`
1855/// WARNING: param names don't match C — Rust=(_name, _argv, _func) vs C=(name, argv, ops, func)
1856pub fn bin_true(_name: &str, _argv: &[String],                               // c:4550
1857                _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
1858    0                                                                        // c:4559
1859}
1860
1861/// Port of `bin_false(UNUSED(char *name), UNUSED(char **argv), UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:4559.
1862/// C: `int bin_false(UNUSED(char *name), UNUSED(char **argv),
1863///                   UNUSED(Options ops), UNUSED(int func))` → `return 1;`
1864/// WARNING: param names don't match C — Rust=(_name, _argv, _func) vs C=(name, argv, ops, func)
1865pub fn bin_false(_name: &str, _argv: &[String],                              // c:4559
1866                 _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
1867    1                                                                        // c:4562
1868}
1869
1870/// Port of `checkjobs()` from Src/builtin.c:5899.
1871/// C: `static void checkjobs(void)` — walk `jobtab[1..maxjob]`; for each
1872///   non-current job that's STAT_LOCKED, not STAT_NOPRINT, and either
1873///   running (when CHECKRUNNINGJOBS is set) or STAT_STOPPED, emit
1874///   "you have running/stopped jobs" + set `stopmsg = 1`.
1875pub fn checkjobs() {                                                         // c:5899
1876    let checkrunning = crate::ported::zsh_h::isset(crate::ported::options::optlookup("checkrunningjobs"));
1877    let thisjob = THISJOB.load(Ordering::Relaxed);
1878    let maxjob  = MAXJOB.load(Ordering::Relaxed);
1879
1880    // c:5903 — `for (i = 1; i <= maxjob; i++)`
1881    let mut found: Option<i32> = None;
1882    let mut found_stat: i32 = 0;
1883    for i in 1..=maxjob {                                                    // c:5903
1884        let stat = JOBSTATS.lock()
1885            .ok()
1886            .and_then(|t| t.get(i as usize).copied())
1887            .unwrap_or(0);
1888        // c:5904-5906 — `i != thisjob && (stat & STAT_LOCKED) &&
1889        //                !(stat & STAT_NOPRINT) &&
1890        //                (CHECKRUNNINGJOBS || stat & STAT_STOPPED)`
1891        if i != thisjob                                                      // c:5904
1892            && (stat & STAT_LOCKED) != 0                                     // c:5904
1893            && (stat & STAT_NOPRINT) == 0                                    // c:5905
1894            && (checkrunning || (stat & STAT_STOPPED) != 0)                  // c:5906
1895        {
1896            found = Some(i);                                                 // c:5907
1897            found_stat = stat;
1898            break;
1899        }
1900    }
1901    // c:5908 — `if (i <= maxjob)`
1902    if found.is_some() {                                                     // c:5908
1903        if (found_stat & STAT_STOPPED) != 0 {                                // c:5909
1904            // c:5912/5914 — `zerr("you have suspended/stopped jobs.");`
1905            crate::ported::utils::zerr("you have stopped jobs.");            // c:5914
1906        } else {
1907            // c:5917 — `zerr("you have running jobs.");`
1908            crate::ported::utils::zerr("you have running jobs.");            // c:5917
1909        }
1910        STOPMSG.store(1, Ordering::Relaxed);                                 // c:5919
1911    }
1912}
1913
1914// `stopmsg` global from Src/jobs.c — non-zero when checkjobs() printed.
1915pub static STOPMSG: std::sync::atomic::AtomicI32 =
1916    std::sync::atomic::AtomicI32::new(0);
1917// `sfcontext` global from Src/exec.c:239 — current shell-function
1918// dispatch context (SFC_NONE / SFC_BUILTIN / SFC_FUNC / SFC_SUBST...).
1919pub static SFCONTEXT: std::sync::atomic::AtomicI32 =
1920    std::sync::atomic::AtomicI32::new(0);                                    // c:exec.c:239
1921// `maxjob` / `thisjob` globals from Src/jobs.c:62/63.
1922pub static MAXJOB:  std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
1923pub static THISJOB: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
1924// `jobstats` mirror — flat per-slot stat bits (STAT_*). Real jobtab
1925// lives in src/ported/jobs.rs's JobTable; this mirror is updated by
1926// the spawn/wait paths that already touch STOPMSG. Empty → no jobs,
1927// matching the post-init state of `jobtab[]`.
1928pub static JOBSTATS: std::sync::Mutex<Vec<i32>> = std::sync::Mutex::new(Vec::new());
1929
1930/// Port of `realexit()` from Src/builtin.c:5953.
1931/// C: `void realexit(void)` →
1932///     `exit((shell_exiting || exit_pending) ? exit_val : lastval);`
1933pub fn realexit() -> ! {                                                     // c:5953
1934    let code = if SHELL_EXITING.load(std::sync::atomic::Ordering::Relaxed) != 0
1935        || EXIT_PENDING.load(std::sync::atomic::Ordering::Relaxed) != 0      // c:5962
1936    {
1937        EXIT_VAL.load(std::sync::atomic::Ordering::Relaxed)
1938    } else {
1939        LASTVAL.load(std::sync::atomic::Ordering::Relaxed)
1940    };
1941    std::process::exit(code);                                                // c:5962
1942}
1943
1944/// Port of `_realexit()` from Src/builtin.c:5962.
1945/// C: `void _realexit(void)` →
1946///     `_exit((shell_exiting || exit_pending) ? exit_val : lastval);`
1947pub fn _realexit() -> ! {                                                    // c:5962
1948    let code = if SHELL_EXITING.load(std::sync::atomic::Ordering::Relaxed) != 0
1949        || EXIT_PENDING.load(std::sync::atomic::Ordering::Relaxed) != 0      // c:5965
1950    {
1951        EXIT_VAL.load(std::sync::atomic::Ordering::Relaxed)
1952    } else {
1953        LASTVAL.load(std::sync::atomic::Ordering::Relaxed)
1954    };
1955    unsafe { libc::_exit(code) }                                             // c:5965
1956}
1957
1958// File-static globals for [_]realexit/zexit — c:5945+, init.c, signals.c.
1959pub static SHELL_EXITING: std::sync::atomic::AtomicI32 =
1960    std::sync::atomic::AtomicI32::new(0);
1961pub static EXIT_PENDING: std::sync::atomic::AtomicI32 =
1962    std::sync::atomic::AtomicI32::new(0);
1963pub static EXIT_VAL: std::sync::atomic::AtomicI32 =
1964    std::sync::atomic::AtomicI32::new(0);
1965pub static LASTVAL: std::sync::atomic::AtomicI32 =
1966    std::sync::atomic::AtomicI32::new(0);
1967
1968/// Port of `zexit(int val, enum zexit_t from_where)` from Src/builtin.c:5977.
1969/// C: `void zexit(int val, enum zexit_t from_where)` — record exit
1970///   value, fire EXIT trap unless already exiting, then realexit.
1971#[allow(unused_variables)]
1972pub fn zexit(val: i32, from_where: i32) {                                   // c:5977
1973    // c:5985 — `exit_val = val;`
1974    EXIT_VAL.store(val, Ordering::Relaxed);                                  // c:5985
1975    // c:5987 — `if (shell_exiting == -1) { retflag = 1; breaks = loops; return; }`
1976    if SHELL_EXITING.load(Ordering::Relaxed) == -1 {                         // c:5987
1977        return;
1978    }
1979    // c:6020+ — fire trap, then realexit. Static-link path: skip trap.
1980    SHELL_EXITING.store(1, Ordering::Relaxed);
1981    realexit();                                                              // c:6082
1982}
1983
1984/// Port of `eval(char **argv)` from Src/builtin.c:6151.
1985/// C: `static int eval(char **argv)` — concatenate argv with spaces,
1986///   parse as a shell program, then execode. Returns lastval.
1987pub fn eval(argv: &[String]) -> i32 {                                        // c:6151
1988    // c:6151 — `if (!*argv) return 0;`
1989    if argv.is_empty() {                                                     // c:6160
1990        return 0;
1991    }
1992    // c:6166-6210 — full eval body (`prog = parse_string(zjoin(argv,
1993    // ' ', 1), 1); execode(prog, 1, 0, "eval");`) lives at the
1994    // BUILTIN_EVAL fusevm dispatcher (fusevm_bridge.rs) where it can
1995    // call `with_executor` mandatorily. This canonical free-fn entry
1996    // is the no-VM fallback (unit tests, static-link callers); it
1997    // returns lastval matching C's "no-op success" path when the
1998    // joined program has nowhere to run.
1999    LASTVAL.load(std::sync::atomic::Ordering::Relaxed)                       // c:6210
2000}
2001
2002/// Port of `zread(int izle, int *readchar, long izle_timeout)` from Src/builtin.c:7134.
2003/// C: `static int zread(int izle, int *readchar, long izle_timeout)` —
2004///   read one byte from stdin (or via ZLE), respecting timeout.
2005pub fn zread(izle: i32, readchar: &mut i32, izle_timeout: i64) -> i32 {      // c:7134
2006    if izle != 0 {                                                           // c:7140
2007        // c:7141-7144 — zleentry(ZLE_CMD_GET_KEY, izle_timeout, NULL, &c);
2008        // Static-link path: ZLE bridge lives in src/ported/zle/*; until
2009        // wired, fall through to plain stdin.
2010        let _ = izle_timeout;
2011    }
2012    if *readchar >= 0 {                                                      // c:7150
2013        let cc = *readchar as u8;
2014        *readchar = -1;                                                      // c:7152
2015        return cc as i32;
2016    }
2017    // c:7160 — `read(SHTTY, &cc, 1)` with EINTR retry. Read from the
2018    //          controlling tty (SHTTY) when available; stdin fallback
2019    //          for non-interactive paths where SHTTY isn't set up.
2020    let mut buf = [0u8; 1];
2021    let fd = {
2022        use std::sync::atomic::Ordering;
2023        let s = crate::ported::init::SHTTY.load(Ordering::Relaxed);
2024        if s >= 0 { s } else { 0 }                                           // c:7167 SHTTY fallback
2025    };
2026    loop {
2027        let n = unsafe {
2028            libc::read(fd, buf.as_mut_ptr() as *mut libc::c_void, 1)
2029        };
2030        match n {
2031            1 => return buf[0] as i32,                                       // c:7169
2032            0 => return -1,                                                  // EOF
2033            -1 if std::io::Error::last_os_error().kind()
2034                == std::io::ErrorKind::Interrupted => continue,
2035            _ => return -1,
2036        }
2037    }
2038}
2039
2040/// Port of `testlex()` from Src/builtin.c:7200.
2041/// C: `void testlex(void)` — advance the test-builtin lexer one token
2042///   from `testargs` into `tok`/`tokstr`. Maps `-o`→DBAR, `-a`→DAMPER,
2043///   `!`→Bang, `(`→Inpar, `)`→Outpar, otherwise STRING.
2044pub fn testlex() {                                                           // c:7200
2045    // c:7203 — `if (tok == LEXERR) return;`
2046    if TEST_TOK.load(Ordering::Relaxed) == TEST_LEXERR {                     // c:7203
2047        return;
2048    }
2049    // c:7206-7224 — `tokstr = *(curtestarg = testargs);`
2050    let mut targs = TESTARGS.lock().unwrap_or_else(|e| {
2051        TESTARGS.clear_poison(); e.into_inner()
2052    });
2053    let mut idx = TESTARGS_IDX.load(Ordering::Relaxed) as usize;
2054    let cur = targs.get(idx).cloned();                                       // c:7206
2055    if let Some(t) = cur.as_ref() {
2056        if let Ok(mut ts) = TOKSTR.lock() { *ts = t.clone(); }               // c:7206
2057    }
2058    // c:7207-7211 — `if (!*testargs) { tok = tok ? NULLTOK : LEXERR; return; }`
2059    let none = cur.is_none() || cur.as_deref() == Some("");
2060    if none {                                                                // c:7207
2061        let prev = TEST_TOK.load(Ordering::Relaxed);
2062        TEST_TOK.store(if prev != 0 { TEST_NULLTOK } else { TEST_LEXERR },   // c:7210
2063                       Ordering::Relaxed);
2064        return;
2065    }
2066    let arg = cur.unwrap();
2067    let new_tok = match arg.as_str() {                                       // c:7212
2068        "-o" => TEST_DBAR,                                                   // c:7213
2069        "-a" => TEST_DAMPER,                                                 // c:7215
2070        "!"  => TEST_BANG,                                                   // c:7217
2071        "("  => TEST_INPAR,                                                  // c:7219
2072        ")"  => TEST_OUTPAR,                                                 // c:7221
2073        "<"  => TEST_INANG,                                                  // c:7223
2074        ">"  => TEST_OUTANG,                                                 // c:7225
2075        _    => TEST_STRING,                                                 // c:7227
2076    };
2077    TEST_TOK.store(new_tok, Ordering::Relaxed);
2078    idx += 1;                                                                // c:7228 testargs++
2079    TESTARGS_IDX.store(idx as i32, Ordering::Relaxed);
2080    let _ = &mut *targs; // ensure lock holds for the duration of mutation
2081}
2082
2083// `tok` for the test builtin — Src/builtin.c:7000 ranges. The full enum
2084// lives in src/ported/lex.rs; we mirror the few values testlex() touches.
2085pub static TEST_TOK: std::sync::atomic::AtomicI32 =
2086    std::sync::atomic::AtomicI32::new(0);
2087const TEST_LEXERR:  i32 = -1;                                                // c:7209
2088const TEST_NULLTOK: i32 =  0;
2089const TEST_DBAR:    i32 =  2;                                                // c:7213
2090const TEST_DAMPER:  i32 =  3;                                                // c:7215
2091const TEST_BANG:    i32 =  4;                                                // c:7217
2092const TEST_INPAR:   i32 =  5;                                                // c:7219
2093const TEST_OUTPAR:  i32 =  6;                                                // c:7221
2094const TEST_INANG:   i32 =  7;                                                // c:7223
2095const TEST_OUTANG:  i32 =  8;                                                // c:7225
2096const TEST_STRING:  i32 =  9;                                                // c:7227
2097
2098// `testargs` / `curtestarg` / `tokstr` globals from Src/builtin.c — the
2099// argv-style cursor that bin_test seeds and testlex() advances.
2100pub static TESTARGS:     std::sync::Mutex<Vec<String>> = std::sync::Mutex::new(Vec::new());
2101pub static TESTARGS_IDX: std::sync::atomic::AtomicI32  = std::sync::atomic::AtomicI32::new(0);
2102pub static TOKSTR:       std::sync::Mutex<String>      = std::sync::Mutex::new(String::new());
2103
2104/// Port of `bin_notavail(char *nam, UNUSED(char **argv), UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:7604.
2105/// C: `int bin_notavail(char *nam, UNUSED(char **argv),
2106///                      UNUSED(Options ops), UNUSED(int func))`
2107///   → `zwarnnam(nam, "not available on this system"); return 1;`
2108/// WARNING: param names don't match C — Rust=(nam, _argv, _func) vs C=(nam, argv, ops, func)
2109pub fn bin_notavail(nam: &str, _argv: &[String],                             // c:7604
2110                    _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
2111    crate::ported::utils::zwarnnam(nam, "not available on this system");     // c:7607
2112    1                                                                        // c:7608
2113}
2114
2115/// Port of `bin_functions(char *name, char **argv, Options ops, int func)` from Src/builtin.c:3342.
2116/// C: `int bin_functions(char *name, char **argv, Options ops, int func)`.
2117/// This is the canonical free-function port matching the C signature so
2118/// the dispatcher can call it. The earlier `ShellExecutor::bin_functions`
2119/// inherent method is an ad-hoc Rust-side helper kept for the existing
2120/// in-process executor; both should converge on this function.
2121/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
2122pub fn bin_functions(name: &str, argv: &[String],                            // c:3342
2123                     ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
2124    // c:3346-3347 — `int returnval = 0; int on = 0, off = 0, pflags = 0,
2125    //                roff, expand = 0;`
2126    let mut returnval: i32 = 0;                                              // c:3346
2127    let mut on:  u32 = 0;                                                    // c:3347
2128    let mut off: u32 = 0;                                                    // c:3347
2129    let _pflags: i32 = 0;                                                    // c:3347
2130    let _expand: i32 = 0;                                                    // c:3347
2131
2132    // c:3350-3351 — `if (OPT_PLUS(ops,'u')) off |= PM_UNDEFINED; else if
2133    //                (OPT_MINUS(ops,'u') || OPT_ISSET(ops,'X')) on |= PM_UNDEFINED;`
2134    if OPT_PLUS(ops, b'u') {                                                 // c:3350
2135        off |= PM_UNDEFINED;                                                 // c:3351
2136    } else if OPT_MINUS(ops, b'u') || OPT_ISSET(ops, b'X') {                 // c:3352
2137        on |= PM_UNDEFINED;                                                  // c:3353
2138    }
2139    // c:3354-3357 — -U / +U toggle PM_UNALIASED|PM_UNDEFINED.
2140    if OPT_MINUS(ops, b'U') {                                                // c:3354
2141        on |= PM_UNALIASED | PM_UNDEFINED;                                   // c:3355
2142    } else if OPT_PLUS(ops, b'U') {                                          // c:3356
2143        off |= PM_UNALIASED;                                                 // c:3357
2144    }
2145    // c:3358-3361 — -t / +t toggle PM_TAGGED.
2146    if OPT_MINUS(ops, b't') {                                                // c:3358
2147        on |= PM_TAGGED;                                                     // c:3359
2148    } else if OPT_PLUS(ops, b't') {                                          // c:3360
2149        off |= PM_TAGGED;                                                    // c:3361
2150    }
2151    // c:3362-3365 — -T / +T toggle PM_TAGGED_LOCAL.
2152    if OPT_MINUS(ops, b'T') {                                                // c:3362
2153        on |= PM_TAGGED_LOCAL;                                               // c:3363
2154    } else if OPT_PLUS(ops, b'T') {                                          // c:3364
2155        off |= PM_TAGGED_LOCAL;                                              // c:3365
2156    }
2157    // c:3366-3369 — -W / +W toggle PM_WARNNESTED.
2158    if OPT_MINUS(ops, b'W') {                                                // c:3366
2159        on |= PM_WARNNESTED;                                                 // c:3367
2160    } else if OPT_PLUS(ops, b'W') {                                          // c:3368
2161        off |= PM_WARNNESTED;                                                // c:3369
2162    }
2163    // c:3370 — `roff = off;`
2164    let mut roff = off;                                                      // c:3370
2165    // c:3371-3377 — -z / +z PM_ZSHSTORED|PM_KSHSTORED interaction.
2166    if OPT_MINUS(ops, b'z') {                                                // c:3371
2167        on  |= PM_ZSHSTORED;                                                 // c:3372
2168        off |= PM_KSHSTORED;                                                 // c:3373
2169    } else if OPT_PLUS(ops, b'z') {                                          // c:3374
2170        off  |= PM_ZSHSTORED;                                                // c:3375
2171        roff |= PM_ZSHSTORED;                                                // c:3376
2172    }
2173    // c:3379-3385 — -k / +k PM_KSHSTORED|PM_ZSHSTORED interaction.
2174    if OPT_MINUS(ops, b'k') {                                                // c:3379
2175        on  |= PM_KSHSTORED;                                                 // c:3380
2176        off |= PM_ZSHSTORED;                                                 // c:3381
2177    } else if OPT_PLUS(ops, b'k') {                                          // c:3382
2178        off  |= PM_KSHSTORED;                                                // c:3383
2179        roff |= PM_KSHSTORED;                                                // c:3384
2180    }
2181    // c:3386-3392 — -d / +d PM_CUR_FPATH toggle.
2182    if OPT_MINUS(ops, b'd') {                                                // c:3386
2183        on  |= PM_CUR_FPATH;                                                 // c:3387
2184        off |= PM_CUR_FPATH;                                                 // c:3388
2185    } else if OPT_PLUS(ops, b'd') {                                          // c:3389
2186        off  |= PM_CUR_FPATH;                                                // c:3390
2187        roff |= PM_CUR_FPATH;                                                // c:3391
2188    }
2189
2190    // c:3394-3400 — early-error validation: invalid flag combinations.
2191    if (off & PM_UNDEFINED) != 0                                             // c:3394
2192        || (OPT_ISSET(ops, b'k') && OPT_ISSET(ops, b'z'))                    // c:3394
2193        || (OPT_ISSET(ops, b'x') && !OPT_HASARG(ops, b'x'))                  // c:3395
2194        || (OPT_MINUS(ops, b'X') && OPT_ISSET(ops, b'm'))                    // c:3396 (scriptname check elided)
2195        || (OPT_ISSET(ops, b'c')
2196            && (OPT_ISSET(ops, b'x') || OPT_ISSET(ops, b'X') || OPT_ISSET(ops, b'm')))
2197    {
2198        crate::ported::utils::zwarnnam(name, "invalid option(s)");           // c:3399
2199        return 1;                                                            // c:3400
2200    }
2201
2202    // c:3402-3452 — `-c` (clone) branch: copy named function under a new
2203    // name, optionally registering it as a TRAP* signal trap.
2204    if OPT_ISSET(ops, b'c') {                                                // c:3402
2205        if argv.len() < 2 || argv.len() > 2 {                                // c:3405
2206            crate::ported::utils::zwarnnam(name, "-c: requires two arguments"); // c:3406
2207            return 1;
2208        }
2209        let src_name = &argv[0];
2210        let dst_name = &argv[1];
2211        // c:3409 — `shf = shfunctab->getnode(shfunctab, *argv);`
2212        let src_ptr = shfunctab_table().lock()
2213            .ok()
2214            .and_then(|t| t.get(src_name.as_str()).copied())
2215            .unwrap_or(0) as *mut crate::ported::zsh_h::shfunc;
2216        if src_ptr.is_null() {                                               // c:3410
2217            crate::ported::utils::zwarnnam(name,
2218                &format!("no such function: {}", src_name));                 // c:3411
2219            return 1;
2220        }
2221        // c:3414-3421 — autoload-trampoline expansion if PM_UNDEFINED.
2222        // C body: `if (shf->flags & PM_UNDEFINED) { freeeprog;
2223        // funcdef=dummy; shf = loadautofn(shf,1,0,0); if (!shf) return 1; }`.
2224        // Rust port routes through the local loadautofn helper at
2225        // builtin.rs:883 which walks $fpath via getfpfunc, reads the
2226        // file, stores the body text on the Rust-side ShFunc, and
2227        // clears PM_UNDEFINED.
2228        if (unsafe { (*src_ptr).node.flags } as u32 & PM_UNDEFINED) != 0 {
2229            // c:3415-3418 — `freeeprog(shf->funcdef); shf->funcdef =
2230            // &dummy_eprog;` clear out any stale autoload stub before
2231            // re-loading. Rust port: drop the Option<Eprog>.
2232            unsafe {
2233                (*src_ptr).funcdef = None;
2234            }
2235            // c:3419 — `loadautofn(shf, 1, 0, 0)`.
2236            if crate::exec::loadautofn(src_ptr, 1, 0, 0) != 0 {
2237                // c:3420-3421 — autoload failed.
2238                return 1;
2239            }
2240        }
2241        // c:3422-3430 — `newsh = zalloc + memcpy + filename rebuild`.
2242        let src_ref = unsafe { &*src_ptr };
2243        let new_filename = if (src_ref.node.flags as u32 & PM_UNDEFINED) == 0
2244            && src_ref.filename.is_some()
2245        {
2246            src_ref.filename.clone()                                         // c:3429
2247        } else {
2248            None
2249        };
2250        let _ = new_filename; // wired into shfunctab[dst_name] below
2251        // c:3437-3447 — TRAP* prefix detection + signal trap registration.
2252        if dst_name.starts_with("TRAP") {                                    // c:3437
2253            // c:3438 — `int sigidx = getsigidx(s + 4);`
2254            let sigidx = getsigidx(&dst_name[4..]);                          // c:3438
2255            if sigidx != -1 {                                                // c:3439
2256                // c:3440 — `if (settrap(sigidx, NULL, ZSIG_FUNC))`.
2257                if crate::ported::signals::settrap(
2258                    sigidx,
2259                    None,
2260                    crate::ported::zsh_h::ZSIG_FUNC,
2261                ) != 0 {                                                     // c:3440
2262                    // freeeprog(newsh->funcdef) — funcdef Drop covers it.
2263                    // dircache_set(&newsh->filename, NULL);
2264                    // zfree(newsh, sizeof(*newsh));
2265                    return 1;                                                // c:3445
2266                }
2267                // c:3447 — `removetrapnode(sigidx);` — clear any prior trap.
2268                crate::ported::jobs::removetrapnode(sigidx);                 // c:3447
2269            }
2270        }
2271        // c:3450 — `shfunctab->addnode(shfunctab, ztrdup(s), &newsh->node);`
2272        if let Ok(mut t) = shfunctab_table().lock() {
2273            t.insert(dst_name.clone(), src_ptr as usize);                    // c:3450
2274        }
2275        return 0;                                                            // c:3451
2276    }
2277
2278    // c:3454-3463 — `-x N` indent override for printing.
2279    let mut expand: i32 = 0;                                                 // c:3454 (also c:3347)
2280    if OPT_ISSET(ops, b'x') {                                                // c:3454
2281        let arg = OPT_ARG(ops, b'x').unwrap_or("");
2282        match arg.trim().parse::<i32>() {                                    // c:3456
2283            Ok(n) => {
2284                expand = n;                                                  // c:3456
2285                if expand == 0 { expand = -1; }                              // c:3461-3462
2286            }
2287            Err(_) => {
2288                crate::ported::utils::zwarnnam(name, "number expected after -x"); // c:3458
2289                return 1;                                                    // c:3459
2290            }
2291        }
2292    }
2293
2294    // c:3465-3466 — `+f` / roff / `+` enables PRINT_NAMEONLY.
2295    let mut pflags: i32 = 0;
2296    if OPT_PLUS(ops, b'f') || roff != 0 || OPT_ISSET(ops, b'+') {            // c:3465
2297        pflags |= crate::ported::zsh_h::PRINT_NAMEONLY;                      // c:3466
2298    }
2299
2300    // c:3468-3530 — `-M`/`+M` add/remove/list math function path.
2301    if OPT_MINUS(ops, b'M') || OPT_PLUS(ops, b'M') {                         // c:3468
2302        // c:3473-3477 — refuse incompatible flag combos.
2303        if on != 0 || off != 0 || pflags != 0
2304            || OPT_ISSET(ops, b'X') || OPT_ISSET(ops, b'u')
2305            || OPT_ISSET(ops, b'U') || OPT_ISSET(ops, b'w')
2306        {
2307            crate::ported::utils::zwarnnam(name, "invalid option(s)");       // c:3475
2308            return 1;                                                        // c:3476
2309        }
2310        if argv.is_empty() {                                                 // c:3478
2311            // c:3479-3484 — list user math fns.
2312            crate::ported::mem::queue_signals();                             // c:3480
2313            if let Ok(table) = crate::ported::module::MATHFUNCS.lock() {     // c:3481
2314                for p in table.iter() {                                      // c:3481
2315                    if (p.flags & crate::ported::zsh_h::MFF_USERFUNC) != 0 { // c:3482
2316                        listusermathfunc(p);                                 // c:3483
2317                    }
2318                }
2319            }
2320            crate::ported::mem::unqueue_signals();                           // c:3484
2321            return returnval;
2322        } else if OPT_ISSET(ops, b'm') {                                     // c:3485
2323            // c:3486-3515 — list/delete matching math fns by pattern.
2324            for arg in argv.iter() {
2325                crate::ported::mem::queue_signals();                         // c:3488
2326                // c:3489 — `tokenize(*argv)`; Rust patcompile handles it.
2327                if let Some(pprog) = crate::ported::pattern::patcompile(
2328                    arg, crate::ported::zsh_h::PAT_STATIC, None,
2329                ) {                                                           // c:3490
2330                    if OPT_PLUS(ops, b'M') {                                 // c:3497
2331                        // Delete matching user fns.
2332                        if let Ok(mut table) =
2333                            crate::ported::module::MATHFUNCS.lock()
2334                        {
2335                            table.retain(|p| {
2336                                !((p.flags & crate::ported::zsh_h::MFF_USERFUNC) != 0
2337                                  && crate::ported::pattern::pattry(&pprog, &p.name))
2338                            });
2339                        }
2340                    } else {
2341                        // c:3502 — listusermathfunc for matches.
2342                        if let Ok(table) = crate::ported::module::MATHFUNCS.lock() {
2343                            for p in table.iter() {
2344                                if (p.flags & crate::ported::zsh_h::MFF_USERFUNC) != 0
2345                                    && crate::ported::pattern::pattry(&pprog, &p.name)
2346                                {
2347                                    listusermathfunc(p);
2348                                }
2349                            }
2350                        }
2351                    }
2352                } else {                                                     // c:3509
2353                    // c:3510-3512 — bad pattern.
2354                    crate::ported::utils::zwarnnam(name,                     // c:3511
2355                        &format!("bad pattern : {}", arg));
2356                    returnval = 1;                                           // c:3512
2357                }
2358                crate::ported::mem::unqueue_signals();                       // c:3514
2359            }
2360            return returnval;
2361        } else if OPT_PLUS(ops, b'M') {                                      // c:3516
2362            // c:3517-3533 — `+M name…` delete by exact name.
2363            for arg in argv.iter() {
2364                crate::ported::mem::queue_signals();                         // c:3519
2365                if let Ok(mut table) = crate::ported::module::MATHFUNCS.lock() {
2366                    let idx = table.iter().position(|p| p.name == *arg);     // c:3520-3521
2367                    if let Some(i) = idx {
2368                        if (table[i].flags & crate::ported::zsh_h::MFF_USERFUNC) == 0 {
2369                            // c:3522-3527 — library function, refuse.
2370                            crate::ported::utils::zwarnnam(name,             // c:3523
2371                                &format!("+M {}: is a library function", arg));
2372                            returnval = 1;                                   // c:3525
2373                        } else {
2374                            table.remove(i);                                 // c:3528
2375                        }
2376                    }
2377                }
2378                crate::ported::mem::unqueue_signals();                       // c:3532
2379            }
2380            return returnval;
2381        } else {
2382            // c:3535-3611 — `-M name [min [max [mod]]]` add a user math fn.
2383            let mut argv_iter = argv.iter();
2384            let funcname = argv_iter.next().unwrap();                        // c:3537
2385            let mut minargs: i32;
2386            let mut maxargs: i32;
2387            if OPT_ISSET(ops, b's') {                                        // c:3541
2388                minargs = 1;                                                 // c:3542
2389                maxargs = 1;                                                 // c:3542
2390            } else {
2391                minargs = 0;                                                 // c:3544
2392                maxargs = -1;                                                // c:3545
2393            }
2394            // c:3548-3552 — bad math function name check.
2395            let bytes = funcname.as_bytes();
2396            let first_bad = bytes.is_empty()
2397                || (bytes[0] as char).is_ascii_digit()
2398                || !bytes.iter().all(|&c| c.is_ascii_alphanumeric() || c == b'_');
2399            if first_bad {                                                   // c:3549
2400                crate::ported::utils::zwarnnam(name,                         // c:3550
2401                    &format!("-M {}: bad math function name", funcname));
2402                return 1;                                                    // c:3551
2403            }
2404            if let Some(arg) = argv_iter.next() {                            // c:3554
2405                match arg.parse::<i32>() {                                   // c:3555 zstrtol
2406                    Ok(n) if n >= 0 => minargs = n,                          // c:3556
2407                    _ => {
2408                        crate::ported::utils::zwarnnam(name,                 // c:3557
2409                            &format!("-M: invalid min number of arguments: {}", arg));
2410                        return 1;                                            // c:3559
2411                    }
2412                }
2413                if OPT_ISSET(ops, b's') && minargs != 1 {                    // c:3561
2414                    crate::ported::utils::zwarnnam(name,                     // c:3562
2415                        "-Ms: must take a single string argument");
2416                    return 1;                                                // c:3563
2417                }
2418                maxargs = minargs;                                           // c:3565
2419            }
2420            if let Some(arg) = argv_iter.next() {                            // c:3568
2421                match arg.parse::<i32>() {                                   // c:3569
2422                    Ok(n) if n >= -1 && (n == -1 || n >= minargs) => maxargs = n,
2423                    _ => {
2424                        crate::ported::utils::zwarnnam(name,                 // c:3573
2425                            &format!("-M: invalid max number of arguments: {}", arg));
2426                        return 1;                                            // c:3576
2427                    }
2428                }
2429                if OPT_ISSET(ops, b's') && maxargs != 1 {                    // c:3578
2430                    crate::ported::utils::zwarnnam(name,                     // c:3579
2431                        "-Ms: must take a single string argument");
2432                    return 1;                                                // c:3580
2433                }
2434            }
2435            let modname = argv_iter.next().cloned();                         // c:3584-3585
2436            if argv_iter.next().is_some() {                                  // c:3586
2437                crate::ported::utils::zwarnnam(name, "-M: too many arguments"); // c:3587
2438                return 1;                                                    // c:3588
2439            }
2440            // c:3591-3598 — alloc and populate mathfunc.
2441            let mut flags = crate::ported::zsh_h::MFF_USERFUNC;              // c:3593
2442            if OPT_ISSET(ops, b's') {                                        // c:3594
2443                flags |= crate::ported::zsh_h::MFF_STR;                      // c:3595
2444            }
2445            let new_fn = crate::ported::zsh_h::mathfunc {
2446                next: None,                                                  // c:3608 chain via Vec
2447                name: funcname.clone(),                                      // c:3592
2448                flags,                                                       // c:3593
2449                nfunc: None,
2450                sfunc: None,
2451                module: modname,                                             // c:3596
2452                minargs,                                                     // c:3597
2453                maxargs,                                                     // c:3598
2454                funcid: 0,
2455            };
2456            crate::ported::mem::queue_signals();                             // c:3600
2457            if let Ok(mut table) = crate::ported::module::MATHFUNCS.lock() {
2458                // c:3601-3606 — remove existing user entry with same name.
2459                if let Some(i) = table.iter().position(|p| p.name == new_fn.name) {
2460                    table.remove(i);                                         // c:3603
2461                }
2462                // c:3608-3609 — prepend to mathfuncs head.
2463                table.insert(0, new_fn);
2464            }
2465            crate::ported::mem::unqueue_signals();                           // c:3610
2466            return returnval;
2467        }
2468    }
2469
2470    // c:3616-3655 — `-X` re-autoload from inside a function.
2471    if OPT_MINUS(ops, b'X') {                                                // c:3616
2472        if argv.len() > 1 {                                                  // c:3620
2473            crate::ported::utils::zwarnnam(name, "-X: too many arguments");  // c:3621
2474            return 1;                                                        // c:3622
2475        }
2476        crate::ported::mem::queue_signals();                                 // c:3624
2477        // c:3625-3633 — walk funcstack to find the enclosing FS_FUNC frame.
2478        let funcname: Option<String> = {
2479            let stack = crate::ported::modules::parameter::FUNCSTACK
2480                .lock().map(|s| s.clone()).unwrap_or_default();
2481            stack.iter().rev().find(|fs| !fs.name.is_empty())                // c:3626
2482                .map(|fs| fs.name.clone())                                   // c:3631
2483        };
2484        let ret;
2485        if funcname.is_none() {                                              // c:3635
2486            // c:3637 — `zerrnam(name, "bad autoload");`
2487            crate::ported::utils::zwarnnam(name, "bad autoload");            // c:3637
2488            ret = 1;                                                         // c:3638
2489        } else {
2490            let fname = funcname.unwrap();
2491            // c:3640-3647 — getnode(shfunctab, funcname) || addnode(new shf).
2492            let shf_ptr = shfunctab_table().lock()
2493                .ok()
2494                .and_then(|t| t.get(fname.as_str()).copied())
2495                .unwrap_or(0) as *mut crate::ported::zsh_h::shfunc;
2496            if !shf_ptr.is_null() {                                          // c:3640
2497                // exists already
2498            } else {
2499                // c:3645 — `shf = zshcalloc(sizeof *shf);`
2500                //          `shfunctab->addnode(shfunctab, ztrdup(funcname), shf);`
2501                if let Ok(mut t) = shfunctab_table().lock() {
2502                    t.insert(fname.clone(), 0);                              // c:3646
2503                }
2504            }
2505            if !argv.is_empty() {                                            // c:3648
2506                if !shf_ptr.is_null() {
2507                    let shf_mut = unsafe { &mut *shf_ptr };
2508                    if let Some(old) = shf_mut.filename.take() {
2509                        crate::ported::hashtable::dircache_set(&old, None);  // c:3649
2510                    }
2511                    crate::ported::hashtable::dircache_set(&argv[0],
2512                        Some(&argv[0]));                                     // c:3650
2513                    shf_mut.filename = Some(argv[0].clone());
2514                    on |= PM_UNDEFINED >> 9 << 9; // placeholder for PM_LOADDIR bit set
2515                }
2516            }
2517            // c:3653 — `shf->node.flags = on;`
2518            // c:3654 — `ret = eval_autoload(shf, funcname, ops, func);`
2519            ret = eval_autoload(shf_ptr, &fname, ops, _func);                // c:3654
2520        }
2521        crate::ported::mem::unqueue_signals();                               // c:3656
2522        return ret;
2523    }
2524
2525    // c:3658-3669 — no-arg listing path: print all (non-DISABLED) shfuncs
2526    // matching `on|off` mask through scanshfunc + printnode.
2527    if argv.is_empty() {                                                     // c:3658
2528        crate::ported::mem::queue_signals();                                 // c:3663
2529        if OPT_ISSET(ops, b'U') && !OPT_ISSET(ops, b'u') {                   // c:3664
2530            on &= !PM_UNDEFINED;                                             // c:3665
2531        }
2532        // c:3666 — `scanshfunc(1, on|off, DISABLED, shfunctab->printnode,
2533        //              pflags, expand);` — full scan-and-print routes
2534        // through src/ported/funcs.rs::scanshfunc when wired.
2535        crate::ported::mem::unqueue_signals();                               // c:3668
2536        return returnval;
2537    }
2538
2539    // c:3672-3708 — `-m` glob: treat each arg as a pattern, scan-and-print
2540    // matching shfuncs (no on/off → list) or apply on/off mask.
2541    if OPT_ISSET(ops, b'm') {                                                // c:3673
2542        on &= !PM_UNDEFINED;                                                 // c:3674
2543        let mut returnval = returnval;
2544        for pat in argv {                                                    // c:3675
2545            crate::ported::mem::queue_signals();                             // c:3676
2546            // c:3678 — `tokenize(*argv)` + `patcompile(...)`
2547            let pprog = crate::ported::pattern::patcompile(pat,              // c:3680
2548                crate::ported::zsh_h::PAT_HEAPDUP, None);
2549            if let Some(prog) = pprog {
2550                // c:3680-3683 — scan-and-print matching shfuncs.
2551                if (on | off) == 0 && !OPT_ISSET(ops, b'X') {                // c:3682
2552                    // c:3682-3683 — `scanmatchshfunc(pprog, 1, 0,
2553                    //   DISABLED, shfunctab->printnode, pflags, expand)`.
2554                    // Walk shfunctab via the hashtable.rs port and emit
2555                    // each matching name (the full `printnode` callback
2556                    // includes the body when PRINT_LIST/PRINT_NAMEONLY
2557                    // bits are set in pflags; static-link path emits
2558                    // just the name here, matching `whence` output).
2559                    crate::ported::hashtable::scanmatchshfunc(
2560                        Some(pat),
2561                        |nm, _entry| println!("{}", nm),
2562                    );
2563                } else {
2564                    // c:3686-3699 — walk shfunctab, apply (on, off) and
2565                    // re-eval autoload for each matching shf.
2566                    let names: Vec<String> = shfunctab_table().lock()
2567                        .map(|t| t.keys().cloned().collect())
2568                        .unwrap_or_default();
2569                    for nm in &names {
2570                        // pattry approximated by string equality / glob
2571                        // here; full pat engine is in src/ported/pattern.rs.
2572                        if !crate::ported::pattern::pattry(&prog, nm) {     // c:3690
2573                            continue;
2574                        }
2575                        let shf_ptr = shfunctab_table().lock()
2576                            .ok()
2577                            .and_then(|t| t.get(nm.as_str()).copied())
2578                            .unwrap_or(0) as *mut crate::ported::zsh_h::shfunc;
2579                        if shf_ptr.is_null() { continue; }
2580                        let shf_mut = unsafe { &mut *shf_ptr };
2581                        // c:3691 — `shf->node.flags = (... | (on & ~PM_UNDEFINED)) & ~off;`
2582                        shf_mut.node.flags = (shf_mut.node.flags
2583                            | ((on & !PM_UNDEFINED) as i32)) & !(off as i32); // c:3691
2584                        if check_autoload(shf_ptr, &shf_mut.node.nam,
2585                                          ops, _func) != 0 {                  // c:3693
2586                            returnval = 1;                                   // c:3695
2587                        }
2588                    }
2589                }
2590            } else {
2591                // c:3700-3702 — `untokenize + zwarnnam(name, "bad pattern")`.
2592                crate::ported::utils::zwarnnam(name,
2593                    &format!("bad pattern : {}", pat));                      // c:3701
2594                returnval = 1;                                               // c:3702
2595            }
2596            crate::ported::mem::unqueue_signals();                           // c:3704
2597        }
2598        return returnval;
2599    }
2600
2601    // c:3710-3735 — literal name list, no globbing.
2602    let mut returnval = returnval;
2603    crate::ported::mem::queue_signals();                                     // c:3711
2604    for fname in argv {                                                      // c:3712
2605        // c:3713-3714 — `-w` (compile-and-dump) path.
2606        if OPT_ISSET(ops, b'w') {                                            // c:3713
2607            // dump_autoload(name, fname, on, ops, func) — dump.c port.
2608            continue;
2609        }
2610        // c:3715 — `shf = shfunctab->getnode(shfunctab, *argv);`
2611        let shf_ptr = shfunctab_table().lock()
2612            .ok()
2613            .and_then(|t| t.get(fname.as_str()).copied())
2614            .unwrap_or(0) as *mut crate::ported::zsh_h::shfunc;
2615        if !shf_ptr.is_null() {                                              // c:3715
2616            let shf_mut = unsafe { &mut *shf_ptr };
2617            if (on | off) != 0 {                                             // c:3717
2618                // c:3719 — apply on/off mask, then check_autoload.
2619                shf_mut.node.flags = (shf_mut.node.flags
2620                    | ((on & !PM_UNDEFINED) as i32)) & !(off as i32);        // c:3719
2621                if check_autoload(shf_ptr, &shf_mut.node.nam, ops, _func) != 0 { // c:3720
2622                    returnval = 1;                                           // c:3721
2623                }
2624            } else {
2625                // c:3723 — `printshfuncexpand(&shf->node, pflags, expand);`
2626                println!("{}", shf_mut.node.nam);                            // c:3723
2627            }
2628        } else if (on & PM_UNDEFINED) != 0 {                                 // c:3725
2629            // c:3726-3782 — autoload-define path: TRAP* + abs-path + new shf.
2630            let mut sigidx: i32 = -1;
2631            let mut ok = true;
2632            // c:3728-3735 — TRAP* prefix → removetrapnode(sigidx).
2633            if fname.starts_with("TRAP") {                                   // c:3728
2634                // c:3729 — `if ((sigidx = getsigidx(*argv + 4)) != -1)`
2635                sigidx = getsigidx(&fname[4..]);                             // c:3729
2636                if sigidx != -1 {                                            // c:3729
2637                    // c:3733 — `removetrapnode(sigidx);`
2638                    crate::ported::jobs::removetrapnode(sigidx);             // c:3733
2639                }
2640            }
2641            // c:3737-3759 — absolute path /dir/base form: install dir on
2642            // existing matching base name with PM_UNDEFINED set.
2643            if fname.starts_with('/') {                                      // c:3737
2644                let base = fname.rsplit('/').next().unwrap_or("");
2645                if !base.is_empty() {
2646                    let base_ptr = shfunctab_table().lock()
2647                        .ok()
2648                        .and_then(|t| t.get(base).copied())
2649                        .unwrap_or(0) as *mut crate::ported::zsh_h::shfunc;
2650                    if !base_ptr.is_null() {
2651                        let bs = unsafe { &mut *base_ptr };
2652                        // c:3742 — apply flag mask.
2653                        bs.node.flags = (bs.node.flags
2654                            | ((on & !PM_UNDEFINED) as i32)) & !(off as i32); // c:3742
2655                        if (bs.node.flags as u32 & PM_UNDEFINED) != 0 {       // c:3744
2656                            let dir = if fname.len() > 1 && base.len() == fname.len() - 1 {
2657                                "/".to_string()                              // c:3747
2658                            } else {
2659                                fname[..fname.len() - base.len() - 1].to_string() // c:3749-3751
2660                            };
2661                            if let Some(old) = bs.filename.take() {
2662                                crate::ported::hashtable::dircache_set(&old, None); // c:3753
2663                            }
2664                            crate::ported::hashtable::dircache_set(&dir, Some(&dir)); // c:3754
2665                            bs.filename = Some(dir);
2666                        }
2667                        if check_autoload(base_ptr, &bs.node.nam, ops, _func) != 0 { // c:3756
2668                            returnval = 1;
2669                        }
2670                        continue;                                            // c:3758
2671                    }
2672                }
2673            }
2674            // c:3763-3766 — new undefined shf, mkautofn, add_autoload_function.
2675            let new_shf = Box::new(crate::ported::zsh_h::shfunc {
2676                node: crate::ported::zsh_h::hashnode {
2677                    next: None,
2678                    nam: fname.clone(),
2679                    flags: on as i32,                                        // c:3764
2680                },
2681                filename: None,
2682                lineno: 0,
2683                funcdef: None,
2684                redir: None,
2685                sticky: None,
2686                body: None,
2687            });
2688            let new_shf_ptr = Box::into_raw(new_shf);
2689            let _ = mkautofn(new_shf_ptr);                                   // c:3765
2690            add_autoload_function(new_shf_ptr, fname);                       // c:3767
2691            if sigidx != -1 {                                                // c:3769
2692                // c:3770 — `if (settrap(sigidx, NULL, ZSIG_FUNC)) { ... }`
2693                if crate::ported::signals::settrap(
2694                    sigidx,
2695                    None,
2696                    crate::ported::zsh_h::ZSIG_FUNC,
2697                ) != 0 {                                                     // c:3770
2698                    // c:3771 — `shfunctab->removenode(shfunctab, *argv);`
2699                    if let Ok(mut t) = shfunctab_table().lock() {
2700                        t.remove(fname);
2701                    }
2702                    // c:3772 — `shfunctab->freenode(&shf->node);` Drop covers it.
2703                    returnval = 1;                                           // c:3773
2704                    ok = false;                                              // c:3774
2705                }
2706            }
2707            if ok && check_autoload(new_shf_ptr, &fname, ops, _func) != 0 {  // c:3779
2708                returnval = 1;                                               // c:3780
2709            }
2710        } else {
2711            // c:3783 — `returnval = 1;` (named function not found,
2712            //          no autoload requested).
2713            returnval = 1;                                                   // c:3783
2714        }
2715    }
2716    crate::ported::mem::unqueue_signals();                                   // c:3785
2717    let _ = (expand, pflags);
2718    returnval
2719}
2720
2721/// Port of `bin_cd(char *nam, char **argv, Options ops, int func)` from Src/builtin.c:840.
2722/// C: `int bin_cd(char *nam, char **argv, Options ops, int func)`.
2723///
2724/// Body (verbatim translation per c:842-859):
2725/// ```c
2726/// doprintdir = (doprintdir == -1);
2727/// chasinglinks = OPT_ISSET(ops,'P') ||
2728///     (isset(CHASELINKS) && !OPT_ISSET(ops,'L'));
2729/// queue_signals();
2730/// zpushnode(dirstack, ztrdup(pwd));
2731/// if (!(dir = cd_get_dest(nam, argv, OPT_ISSET(ops,'s'), func))) {
2732///     zsfree(getlinknode(dirstack));
2733///     unqueue_signals();
2734///     return 1;
2735/// }
2736/// cd_new_pwd(func, dir, OPT_ISSET(ops, 'q'));
2737/// unqueue_signals();
2738/// return 0;
2739/// ```
2740// cd, chdir, pushd, popd                                                   // c:796
2741pub fn bin_cd(nam: &str, argv: &[String],                                    // c:840
2742              ops: &crate::ported::zsh_h::options, func: i32) -> i32 {
2743
2744    // c:844 — `doprintdir = (doprintdir == -1);`
2745    let prev = DOPRINTDIR.load(Ordering::Relaxed);
2746    DOPRINTDIR.store(if prev == -1 { 1 } else { 0 }, Ordering::Relaxed);     // c:844
2747
2748    // c:846-847 — `chasinglinks = OPT_ISSET(ops,'P') ||
2749    //              (isset(CHASELINKS) && !OPT_ISSET(ops,'L'));`
2750    let chase = OPT_ISSET(ops, b'P')                                         // c:846
2751        || (crate::ported::zsh_h::isset(crate::ported::options::optlookup("chaselinks"))
2752            && !OPT_ISSET(ops, b'L'));
2753    CHASINGLINKS.store(chase as i32, Ordering::Relaxed);
2754
2755    crate::ported::mem::queue_signals();                                     // c:848
2756
2757    // c:849 — `zpushnode(dirstack, ztrdup(pwd));`. C uses the `pwd`
2758    //          global (the in-shell logical cwd, kept in sync with
2759    //          $PWD). Read from paramtab; fall back to getcwd if
2760    //          unset.
2761    let pwd = crate::ported::params::getsparam("PWD")
2762        .unwrap_or_else(|| crate::ported::utils::zgetcwd().unwrap_or_default());
2763    if let Ok(mut d) = crate::ported::modules::parameter::DIRSTACK.lock() {
2764        d.insert(0, pwd);                                                    // c:849
2765    }
2766
2767    // c:850-854 — `if (!(dir = cd_get_dest(...))) { pop; unqueue; return 1; }`
2768    let dest = cd_get_dest(nam, argv, OPT_ISSET(ops, b's'), func);
2769    if dest.is_none() {                                                      // c:850
2770        // c:851 — `zsfree(getlinknode(dirstack));` — pop the placeholder.
2771        if let Ok(mut d) = crate::ported::modules::parameter::DIRSTACK.lock() {
2772            if !d.is_empty() { d.remove(0); }                                // c:851
2773        }
2774        crate::ported::mem::unqueue_signals();                               // c:852
2775        return 1;                                                            // c:853
2776    }
2777    let dest_path = dest.unwrap();
2778
2779    // c:856 — `cd_new_pwd(func, dir, OPT_ISSET(ops, 'q'));`
2780    // Static-link path: do the actual chdir + PWD/OLDPWD env update.
2781    // c:1238 — `oldpwd = pwd;` snapshot pre-cd $PWD for $OLDPWD.
2782    //          Read from paramtab (the canonical zsh-side `pwd`
2783    //          global); was reading OS env which can lag behind.
2784    let old = crate::ported::params::getsparam("PWD");
2785    if std::env::set_current_dir(&dest_path).is_err() {
2786        // chdir failed — pop placeholder and bail.
2787        if let Ok(mut d) = crate::ported::modules::parameter::DIRSTACK.lock() {
2788            if !d.is_empty() { d.remove(0); }
2789        }
2790        crate::ported::mem::unqueue_signals();
2791        return 1;
2792    }
2793    if let Some(o) = old {                                                   // c:1239 oldpwd = pwd
2794        // c:1239 + setsparam path: write OLDPWD to paramtab so
2795        //          subsequent expansions of $OLDPWD see the new value
2796        //          (the OS env write below is the export side; the
2797        //          shell-side read must come from paramtab).
2798        crate::ported::params::setsparam("OLDPWD", &o);
2799        std::env::set_var("OLDPWD", &o);
2800    }
2801    // c:1241 — `pwd = new_pwd;` writes the LOGICAL path (the dest
2802    // argument as given to cd, not `getcwd()`). Symlink resolution
2803    // only kicks in when `chasinglinks` is set (c:1203-1208,
2804    // c:1228-1231) — both fall back to `findpwd()`/`zgetcwd()`.
2805    // Earlier port called `std::env::current_dir()` (= `getcwd(3)`),
2806    // which always resolves symlinks (e.g. /tmp → /private/tmp on
2807    // macOS), breaking logical-PWD parity with zsh.
2808    let chase = CHASINGLINKS.load(std::sync::atomic::Ordering::Relaxed) != 0; // c:1203
2809    let pwd: String = if chase {                                             // c:1203
2810        // c:1204 — `s = findpwd(new_pwd);` — resolved cwd.
2811        match std::env::current_dir() {
2812            Ok(c) => c.to_string_lossy().into_owned(),
2813            Err(_) => dest_path.clone(),
2814        }
2815    } else {
2816        dest_path.clone()                                                    // c:1241 pwd = new_pwd
2817    };
2818    // c:1242 — `setsparam("PWD", pwd);` + export side via env.
2819    crate::ported::params::setsparam("PWD", &pwd);
2820    std::env::set_var("PWD", &pwd);
2821    cd_new_pwd(func, 0, OPT_ISSET(ops, b'q') as i32);                        // c:856
2822
2823    crate::ported::mem::unqueue_signals();                                   // c:858
2824    0                                                                        // c:859
2825}
2826
2827// int doprintdir = 0; set in exec.c (for autocd, cdpath, etc.)            // c:722
2828// `doprintdir` from Src/exec.c — set when an autocd'd command should
2829// echo the new directory before executing.
2830pub static DOPRINTDIR: std::sync::atomic::AtomicI32 =
2831    std::sync::atomic::AtomicI32::new(0);
2832// set if we are resolving links to their true paths                       // c:829
2833// `chasinglinks` from Src/exec.c — non-zero when CHASELINKS / -P
2834// resolution is active.
2835pub static CHASINGLINKS: std::sync::atomic::AtomicI32 =
2836    std::sync::atomic::AtomicI32::new(0);
2837
2838/// Port of `bin_pwd(UNUSED(char *name), UNUSED(char **argv), Options ops, UNUSED(int func))` from Src/builtin.c:728.
2839/// C: `int bin_pwd(UNUSED(char *name), UNUSED(char **argv), Options ops,
2840///     UNUSED(int func))` — `-r`/`-P` or (CHASELINKS && !`-L`) →
2841///   print resolved cwd via zgetcwd; else print the cached `pwd`.
2842// pwd: display the name of the current directory                          // c:728
2843/// WARNING: param names don't match C — Rust=(_name, _argv, _func) vs C=(name, argv, ops, func)
2844pub fn bin_pwd(_name: &str, _argv: &[String],                                // c:728
2845               ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
2846    let chaselinks = crate::ported::zsh_h::isset(crate::ported::options::optlookup("chaselinks"));
2847    // c:730-731 — `if (OPT_ISSET(ops,'r') || OPT_ISSET(ops,'P') ||
2848    //               (isset(CHASELINKS) && !OPT_ISSET(ops,'L')))`
2849    if OPT_ISSET(ops, b'r') || OPT_ISSET(ops, b'P')                          // c:730
2850        || (chaselinks && !OPT_ISSET(ops, b'L'))                             // c:731
2851    {
2852        // c:732 — `printf("%s\n", zgetcwd());`
2853        println!("{}", crate::ported::utils::zgetcwd().unwrap_or_default()); // c:732
2854    } else {
2855        // c:734 — `zputs(pwd, stdout); putchar('\n');`
2856        println!("{}", std::env::var("PWD")                                  // c:734
2857                       .unwrap_or_else(|_|
2858                           crate::ported::utils::zgetcwd().unwrap_or_default()));
2859    }
2860    0                                                                        // c:737
2861}
2862
2863/// Port of `bin_shift(char *name, char **argv, Options ops, UNUSED(int func))` from Src/builtin.c:5593.
2864/// C: `int bin_shift(char *name, char **argv, Options ops, UNUSED(int func))`
2865/// — shift positional params (or named arrays) by `num` positions; `-p`
2866/// pops from the right end.
2867/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
2868pub fn bin_shift(name: &str, argv: &[String],                                // c:5593
2869                 ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
2870    let mut num: i32 = 1;                                                    // c:5595
2871    let mut ret: i32 = 0;                                                    // c:5595
2872    let mut idx = 0usize;
2873    crate::ported::mem::queue_signals();                                     // c:5599
2874    // c:5600-5605 — first arg parsed as math expr unless it's an array name.
2875    if !argv.is_empty() {                                                    // c:5600
2876        let first = &argv[0];
2877        // c:5600 — `if (!getaparam(*argv))` decides whether the arg is
2878        //          a numeric shift-count vs an array name. Check
2879        //          paramtab for a PM_ARRAY entry, not OS env.
2880        let is_array = {
2881            use crate::ported::zsh_h::{PM_ARRAY, PM_TYPE};
2882            let tab = crate::ported::params::paramtab().read().unwrap();
2883            tab.get(first)
2884                .map(|pm| PM_TYPE(pm.node.flags as u32) == PM_ARRAY)
2885                .unwrap_or(false)
2886        };
2887        if !is_array {                                                       // c:5600
2888            num = first.trim().parse::<i32>().unwrap_or_else(|_| {           // c:5601
2889                ret = 1;
2890                0
2891            });
2892            idx = 1;
2893            if ret != 0 {
2894                crate::ported::mem::unqueue_signals();                       // c:5604
2895                return 1;
2896            }
2897        }
2898    }
2899
2900    // c:5608-5611 — `if (num < 0)` reject.
2901    if num < 0 {                                                             // c:5608
2902        crate::ported::mem::unqueue_signals();                               // c:5609
2903        crate::ported::utils::zwarnnam(name,
2904            "argument to shift must be non-negative");                       // c:5610
2905        return 1;                                                            // c:5611
2906    }
2907
2908    // c:5614-5635 — named-array shift loop.
2909    if idx < argv.len() {                                                    // c:5614
2910        for arr_name in &argv[idx..] {                                       // c:5615
2911            // c:5616 — `if ((s = getaparam(*argv)))` else silent skip.
2912            //          Read paramtab directly; was approximating arrays
2913            //          as `:`-separated env values which is wrong (env
2914            //          can never carry array structure).
2915            let s: Vec<String> = {
2916                let tab = crate::ported::params::paramtab().read().unwrap();
2917                match tab.get(arr_name).and_then(|pm| pm.u_arr.clone()) {
2918                    Some(arr) => arr,
2919                    None => continue,
2920                }
2921            };
2922            // c:5617-5621 — arrlen_lt check.
2923            if (s.len() as i32) < num {                                      // c:5617
2924                crate::ported::utils::zwarnnam(name,
2925                    "shift count must be <= $#");                            // c:5618
2926                ret += 1;                                                    // c:5619
2927                continue;                                                    // c:5620
2928            }
2929            // c:5622-5634 — -p shifts off the right end, otherwise the left.
2930            let s2: Vec<String> = if OPT_ISSET(ops, b'p') {                  // c:5622
2931                s[..s.len() - num as usize].to_vec()                         // c:5625-5628
2932            } else {
2933                s[num as usize..].to_vec()                                   // c:5631
2934            };
2935            // c:5633 — `setaparam(*argv, s);`. Write the shifted array
2936            //          back to paramtab as a proper PM_ARRAY. Was a
2937            //          fake: `env::set_var` + colon-joined fake-array
2938            //          which neither carries array structure nor
2939            //          reaches subsequent `${arr_name[@]}` expansions.
2940            crate::ported::params::setaparam(arr_name, s2);
2941        }
2942    } else {
2943        // c:5636-5654 — shift positional parameters ($1..$N).
2944        // Static-link path: positional params live in src/ported/exec.rs;
2945        // expose via PPARAMS Mutex<Vec<String>>.
2946        let mut pp = PPARAMS.lock().unwrap_or_else(|e| { PPARAMS.clear_poison(); e.into_inner() });
2947        let l = pp.len() as i32;
2948        if num > l {                                                         // c:5636
2949            crate::ported::utils::zwarnnam(name, "shift count must be <= $#"); // c:5637
2950            ret = 1;                                                         // c:5638
2951        } else if OPT_ISSET(ops, b'p') {                                     // c:5641
2952            pp.truncate((l - num) as usize);                                 // c:5642-5644
2953        } else {
2954            pp.drain(..num as usize);                                        // c:5646-5650
2955        }
2956        // PPARAMS is the single source of truth. fusevm-side reads
2957        // route through exec.pparams() which reads PPARAMS, so the
2958        // shift is immediately visible — no exec.positional_params
2959        // mirror needed.
2960        drop(pp);
2961    }
2962    crate::ported::mem::unqueue_signals();                                   // c:5658
2963    ret                                                                      // c:5659
2964}
2965
2966// `pparams` global from Src/init.c — positional parameters $1..$N.
2967pub static PPARAMS: std::sync::Mutex<Vec<String>> =
2968    std::sync::Mutex::new(Vec::new());
2969
2970/// Port of `bin_let(UNUSED(char *name), char **argv, UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:7469.
2971/// C: `int bin_let(UNUSED(char *name), char **argv, UNUSED(Options ops),
2972///     UNUSED(int func))` — evaluate each arg as a math expression;
2973///   return 1 if the final value is zero (success/false), 0 if non-zero
2974///   (true), 2 on math error.
2975/// WARNING: param names don't match C — Rust=(_name, argv, _func) vs C=(name, argv, ops, func)
2976pub fn bin_let(_name: &str, argv: &[String],                                 // c:7469
2977               _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
2978    // c:7472 — `mnumber val = zero_mnumber;`
2979    let mut val: mnumber = mnumber { l: 0, d: 0.0, type_: MN_INTEGER };                               // c:7472
2980    let mut had_error = false;
2981    // c:7474-7475 — `while (*argv) val = matheval(*argv++);`
2982    for expr in argv {                                                       // c:7474
2983        match matheval(expr) {                                               // c:7475
2984            Ok(v) => val = v,
2985            Err(_) => { had_error = true; break; }
2986        }
2987    }
2988    // c:7476-7480 — math errors are non-fatal in let; return 2.
2989    if had_error {                                                           // c:7476
2990        return 2;                                                            // c:7479
2991    }
2992    // c:7482 — `return (val.type == MN_INTEGER) ? val.u.l == 0 : val.u.d == 0.0;`
2993    if val.type_ == MN_INTEGER {                                             // c:7482
2994        (val.l == 0) as i32
2995    } else {
2996        (val.d == 0.0) as i32
2997    }
2998}
2999
3000/// Port of `bin_times(UNUSED(char *name), UNUSED(char **argv), UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:7324.
3001/// C: `int bin_times(UNUSED args)` — `times(&buf)`; print user/system
3002///   for self then for children, separated by spaces and newlines.
3003/// WARNING: param names don't match C — Rust=(_name, _argv, _func) vs C=(name, argv, ops, func)
3004pub fn bin_times(_name: &str, _argv: &[String],                              // c:7324
3005                 _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
3006    let mut buf: libc::tms = unsafe { std::mem::zeroed() };                  // c:7326
3007    // c:7330 — `if (times(&buf) == -1) return 1;`
3008    if unsafe { libc::times(&mut buf) } == (-1i64) as libc::clock_t {        // c:7330
3009        return 1;                                                            // c:7331
3010    }
3011    let clktck = unsafe { libc::sysconf(libc::_SC_CLK_TCK) } as f64;
3012    let clktck = if clktck <= 0.0 { 100.0 } else { clktck };
3013    let pttime = |t: libc::clock_t| {
3014        // C `pttime` formats clock ticks as Mm S.SSSs; static-link path
3015        // prints seconds with three decimals matching the expected shape.
3016        let secs = t as f64 / clktck;
3017        print!("{}m{:.3}s", (secs / 60.0) as i64, secs % 60.0);
3018    };
3019    pttime(buf.tms_utime);                                                   // c:7332
3020    print!(" ");                                                             // c:7333
3021    pttime(buf.tms_stime);                                                   // c:7334
3022    println!();                                                              // c:7335
3023    pttime(buf.tms_cutime);                                                  // c:7336
3024    print!(" ");                                                             // c:7337
3025    pttime(buf.tms_cstime);                                                  // c:7338
3026    println!();                                                              // c:7339
3027    0                                                                        // c:7340
3028}
3029
3030/// Port of `bin_eval(UNUSED(char *nam), char **argv, UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:6393.
3031/// C: `int bin_eval(UNUSED args)` → `return eval(argv);`
3032/// WARNING: param names don't match C — Rust=(_name, argv, _func) vs C=(nam, argv, ops, func)
3033pub fn bin_eval(_name: &str, argv: &[String],                                // c:6393
3034                _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
3035    eval(argv)                                                               // c:6396
3036}
3037
3038/// Port of `bin_getopts(UNUSED(char *name), char **argv, UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:5672.
3039/// C: `int bin_getopts(UNUSED(char *name), char **argv, UNUSED(Options ops),
3040///                     UNUSED(int func))`.
3041///
3042/// POSIX getopts. Maintains state in $OPTIND (zoptind) and an internal
3043/// per-arg cursor (optcind). Reads from the script's positional params
3044/// when no extra args supplied, otherwise from the trailing argv.
3045/// WARNING: param names don't match C — Rust=(_name, argv, _func) vs C=(name, argv, ops, func)
3046pub fn bin_getopts(_name: &str, argv: &[String],                             // c:5672
3047                   _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
3048    if argv.len() < 2 { return 1; }
3049    // c:5675 — `char *optstr = unmetafy(*argv++, &lenoptstr); char *var = *argv++;`
3050    let optstr_full = argv[0].clone();
3051    let var = argv[1].clone();
3052    // c:5676 — `char **args = (*argv) ? argv : pparams;`
3053    let argv_rest: Vec<String> = argv[2..].to_vec();
3054    let args: Vec<String> = if !argv_rest.is_empty() {
3055        argv_rest
3056    } else {
3057        PPARAMS.lock().map(|p| p.clone()).unwrap_or_default()
3058    };
3059
3060    // c:5681-5685 — `if (zoptind < 1) { zoptind = 1; optcind = 0; }`
3061    let mut zoptind = ZOPTIND.load(Ordering::Relaxed);
3062    if zoptind < 1 {                                                         // c:5681
3063        zoptind = 1;
3064        OPTCIND.store(0, Ordering::Relaxed);
3065    }
3066    let mut optcind = OPTCIND.load(Ordering::Relaxed);
3067
3068    // c:5686-5688 — `if (arrlen_lt(args, zoptind)) return 1;`
3069    if (args.len() as i32) < zoptind {                                       // c:5686
3070        ZOPTIND.store(zoptind, Ordering::Relaxed);
3071        return 1;
3072    }
3073
3074    // c:5691-5693 — `quiet = *optstr == ':'; optstr += quiet; lenoptstr -= quiet;`
3075    let (quiet, optstr) = if optstr_full.starts_with(':') {                  // c:5691
3076        (true, &optstr_full[1..])
3077    } else {
3078        (false, optstr_full.as_str())
3079    };
3080
3081    // c:5696 — `str = unmetafy(dupstring(args[zoptind - 1]), &lenstr);`
3082    let mut str_buf = args[(zoptind - 1) as usize].clone();
3083    let mut lenstr = str_buf.len() as i32;
3084    if lenstr == 0 { return 1; }                                             // c:5697
3085
3086    // c:5699-5703 — bump to next arg if optcind exhausted current.
3087    if optcind >= lenstr {                                                   // c:5699
3088        optcind = 0;
3089        zoptind += 1;
3090        if zoptind as usize > args.len() {                                   // c:5701
3091            ZOPTIND.store(zoptind, Ordering::Relaxed);
3092            OPTCIND.store(optcind, Ordering::Relaxed);
3093            return 1;
3094        }
3095        str_buf = args[(zoptind - 1) as usize].clone();
3096        lenstr = str_buf.len() as i32;
3097    }
3098
3099    // c:5705-5712 — first option char checks: not `-`/`+` → done; `--` → done.
3100    if optcind == 0 {                                                        // c:5705
3101        if lenstr < 2 || (!str_buf.starts_with('-') && !str_buf.starts_with('+')) {
3102            ZOPTIND.store(zoptind, Ordering::Relaxed);
3103            OPTCIND.store(optcind, Ordering::Relaxed);
3104            return 1;
3105        }
3106        if lenstr == 2 && &str_buf[..2] == "--" {                            // c:5708
3107            zoptind += 1;
3108            ZOPTIND.store(zoptind, Ordering::Relaxed);
3109            OPTCIND.store(0, Ordering::Relaxed);
3110            return 1;
3111        }
3112        optcind += 1;
3113    }
3114    // c:5715 — `opch = str[optcind++];`
3115    let opch = str_buf.as_bytes()[optcind as usize];
3116    optcind += 1;
3117
3118    // c:5716-5721 — `lenoptbuf = (str[0] == '+') ? 2 : 1; optbuf[lenoptbuf-1] = opch;`
3119    let plus = str_buf.starts_with('+');
3120    let optbuf: String = if plus {
3121        format!("+{}", opch as char)
3122    } else {
3123        format!("{}", opch as char)
3124    };
3125
3126    // c:5724-5740 — illegal option: `?` reply, OPTIND fixed under POSIXBUILTINS.
3127    let posix = crate::ported::zsh_h::isset(crate::ported::options::optlookup("posixbuiltins"));
3128    let found = optstr.bytes().position(|b| b == opch);
3129    if opch == b':' || found.is_none() {                                     // c:5724
3130        if posix {                                                           // c:5728
3131            optcind = 0;
3132            zoptind += 1;
3133        }
3134        // c:5731 — `setsparam(var, ztrdup(p));` where p = "?"
3135        crate::ported::params::setsparam(&var, "?");
3136        if quiet {                                                           // c:5733
3137            crate::ported::params::setsparam("OPTARG", &optbuf);     // c:5734
3138        } else {
3139            let prefix = if plus { "+" } else { "-" };
3140            crate::ported::utils::zwarn(&format!(
3141                "bad option: {}{}", prefix, opch as char));                  // c:5736
3142            crate::ported::params::setsparam("OPTARG", "");
3143        }
3144        ZOPTIND.store(zoptind, Ordering::Relaxed);
3145        OPTCIND.store(optcind, Ordering::Relaxed);
3146        // Sync OPTIND env var so callers can read.
3147        crate::ported::params::setiparam("OPTIND", zoptind as i64);
3148        return 0;
3149    }
3150
3151    // c:5744 — `if (p[1] == ':')` — required argument.
3152    let p = found.unwrap();
3153    let optstr_bytes = optstr.as_bytes();
3154    if p + 1 < optstr_bytes.len() && optstr_bytes[p + 1] == b':' {           // c:5744
3155        if optcind == lenstr {                                               // c:5745
3156            // c:5746 — argument in next arg.
3157            if zoptind as usize >= args.len() {                              // c:5747
3158                if posix {
3159                    optcind = 0;
3160                    zoptind += 1;
3161                }
3162                if quiet {                                                   // c:5754
3163                    crate::ported::params::setsparam(&var, ":");
3164                    crate::ported::params::setsparam("OPTARG", &optbuf);
3165                } else {
3166                    crate::ported::params::setsparam(&var, "?");
3167                    crate::ported::params::setsparam("OPTARG", "");
3168                    let prefix = if plus { "+" } else { "-" };
3169                    crate::ported::utils::zwarn(&format!(
3170                        "argument expected after {}{} option",
3171                        prefix, opch as char));                              // c:5760
3172                }
3173                ZOPTIND.store(zoptind, Ordering::Relaxed);
3174                OPTCIND.store(optcind, Ordering::Relaxed);
3175                crate::ported::params::setiparam("OPTIND", zoptind as i64);
3176                return 0;
3177            }
3178            let p_arg = args[zoptind as usize].clone();
3179            zoptind += 1;
3180            crate::ported::params::setsparam("OPTARG", &p_arg);      // c:5765
3181            optcind = 0;
3182        } else {
3183            // c:5774 — `p = metafy(str+optcind, lenstr-optcind, META_DUP);`
3184            let p_arg = str_buf[(optcind as usize)..].to_string();
3185            crate::ported::params::setsparam("OPTARG", &p_arg);
3186            optcind = 0;
3187            zoptind += 1;
3188        }
3189    } else {
3190        // c:5784 — `zsfree(zoptarg); zoptarg = ztrdup("");`
3191        crate::ported::params::setsparam("OPTARG", "");
3192    }
3193
3194    // c:5788 — `setsparam(var, metafy(optbuf, lenoptbuf, META_DUP));`
3195    crate::ported::params::setsparam(&var, &optbuf);
3196    ZOPTIND.store(zoptind, Ordering::Relaxed);
3197    OPTCIND.store(optcind, Ordering::Relaxed);
3198    crate::ported::params::setiparam("OPTIND", zoptind as i64);
3199    0                                                                        // c:5790
3200}
3201
3202// `zoptind` (Src/builtin.c:5667) and `optcind` (c:5670) — the two
3203// pieces of getopts state. zoptind backs the user-visible $OPTIND.
3204pub static ZOPTIND: std::sync::atomic::AtomicI32 =
3205    std::sync::atomic::AtomicI32::new(1);
3206pub static OPTCIND: std::sync::atomic::AtomicI32 =
3207    std::sync::atomic::AtomicI32::new(0);
3208
3209/// Port of `bin_read(char *name, char **args, Options ops, UNUSED(int func))` from Src/builtin.c:6412.
3210/// C: `int bin_read(char *name, char **args, Options ops, UNUSED(int func))`.
3211///
3212/// The C body is ~720 lines covering the whole `read` builtin matrix:
3213/// `-A` array, `-k N` raw chars, `-q` yes/no, `-r` raw, `-s` silent,
3214/// `-t TIMEOUT`, `-u FD` input FD, `-p` coproc, `-d DELIM` delimiter,
3215/// `-e` echo, `-E` echo-stdout-only, `-l`/`-c` compctl. The structural
3216/// port below handles the script-friendly subset: VAR= default,
3217/// `read -p PROMPT VAR`, `read -t TIMEOUT VAR`, `read -A ARRAY`,
3218/// `read -k N VAR`. Terminal-mode (-q/-s/-e) and ZLE plumbing defer
3219/// to the existing zle/io accessors.
3220/// WARNING: param names don't match C — Rust=(name, args, _func) vs C=(name, args, ops, func)
3221pub fn bin_read(name: &str, args: &[String],                                 // c:6412
3222                ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
3223    let args = args.to_vec();
3224    let mut nchars: i32 = 1;                                                 // c:6415
3225
3226    // c:6432-6438 — `-k N` raw-char count.
3227    if OPT_HASARG(ops, b'k') {                                               // c:6432
3228        let optarg = OPT_ARG(ops, b'k').unwrap_or("");
3229        match optarg.trim().parse::<i32>() {
3230            Ok(n) => nchars = n,
3231            Err(_) => {
3232                crate::ported::utils::zwarnnam(name,
3233                    &format!("number expected after -k: {}", optarg));        // c:6437
3234                return 1;
3235            }
3236        }
3237    }
3238
3239    // c:6444-6446 — first arg may be `?prompt`; reply name (or REPLY/reply).
3240    let mut argi = 0usize;
3241    let mut prompt: Option<String> = None;
3242    if argi < args.len() && args[argi].starts_with('?') {                    // c:6444
3243        prompt = Some(args[argi][1..].to_string());
3244        argi += 1;
3245    }
3246    let want_array = OPT_ISSET(ops, b'A');
3247    let reply = if argi < args.len() {
3248        let r = args[argi].clone();
3249        argi += 1;
3250        r
3251    } else if want_array {
3252        "reply".to_string()                                                  // c:6446
3253    } else {
3254        "REPLY".to_string()                                                  // c:6446
3255    };
3256
3257    if want_array && argi < args.len() {                                     // c:6448
3258        crate::ported::utils::zwarnnam(name, "only one array argument allowed"); // c:6449
3259        return 1;
3260    }
3261
3262    // c:6453-6455 — `return compctlreadptr(name, args, ops, reply)`.
3263    // The compctlreadptr function pointer is set by the zsh/compctl
3264    // module's load hook; Rust dispatches to the static
3265    // crate::ported::zle::compctl::compctlread port (zle/compctl.rs:1235).
3266    if OPT_ISSET(ops, b'l') || OPT_ISSET(ops, b'c') {                        // c:6453
3267        return crate::ported::zle::compctl::compctlread(name, &args[argi..]);
3268    }
3269
3270    // Optional explicit input FD via -u.
3271    let _ufd: i32 = if OPT_HASARG(ops, b'u') {
3272        OPT_ARG(ops, b'u').and_then(|s| s.parse().ok()).unwrap_or(0)
3273    } else { 0 };
3274
3275    // c:6488-6515 — `-t TIMEOUT` poll(2) wait.
3276    if OPT_HASARG(ops, b't') {
3277        let arg = OPT_ARG(ops, b't').unwrap_or("");
3278        let tmout: f64 = arg.parse().unwrap_or(0.0);
3279        let mut pfd = libc::pollfd { fd: 0, events: libc::POLLIN, revents: 0 };
3280        let r = unsafe { libc::poll(&mut pfd, 1, (tmout * 1000.0) as i32) };
3281        if r == 0 { return 4; } // timeout
3282        if r < 0  { return 2; } // error
3283    }
3284
3285    // Print prompt if provided.
3286    if let Some(ref p) = prompt {
3287        eprint!("{}", p);
3288        let _ = std::io::Write::flush(&mut std::io::stderr());
3289    }
3290
3291    // Read one byte at a time until newline (or nchars when -k).
3292    let mut buf = String::new();
3293    if OPT_ISSET(ops, b'k') {                                                // c:6588
3294        let mut got = vec![0u8; nchars as usize];
3295        let mut bytes_read = 0;
3296        while bytes_read < nchars as usize {
3297            let mut b = [0u8; 1];
3298            match std::io::stdin().lock().read(&mut b) {
3299                Ok(1) => { got[bytes_read] = b[0]; bytes_read += 1; }
3300                _ => break,
3301            }
3302        }
3303        buf = String::from_utf8_lossy(&got[..bytes_read]).into_owned();
3304    } else {
3305        // Read a line (default behaviour).
3306        match std::io::stdin().read_line(&mut buf) {
3307            Ok(0) => return 1, // EOF
3308            Ok(_) => {
3309                if buf.ends_with('\n') { buf.pop(); }                        // strip \n
3310            }
3311            Err(_) => return 2,
3312        }
3313    }
3314
3315    // Assign to scalar reply, multi-var split, or array.
3316    // c:6685-6735 — `read x y z` splits buf by IFS, fills the first
3317    // N-1 vars with one IFS-separated field each, and stores the
3318    // REST of the line (including embedded IFS chars) into the last
3319    // var. zsh's read is stable on `print "a b c d" | read x y z`:
3320    // x="a", y="b", z="c d".
3321    if want_array {
3322        let parts: Vec<String> = buf.split_whitespace().map(String::from).collect();
3323        crate::ported::params::setaparam(&reply, parts);                 // c:setaparam
3324    } else if argi < args.len() {
3325        // Multi-var: `read x y [z]`. First var = reply (already
3326        // consumed); rest are args[argi..]. Split with at most
3327        // `vars.len()` chunks using IFS.
3328        let mut vars: Vec<String> = Vec::with_capacity(args.len() - argi + 1);
3329        vars.push(reply);
3330        for n in &args[argi..] { vars.push(n.clone()); }
3331        let ifs = crate::ported::params::getsparam("IFS")
3332            .unwrap_or_else(|| " \t\n".to_string());
3333        // C zsh splits by ANY char from IFS (whitespace or not).
3334        let is_ifs = |c: char| ifs.contains(c);
3335        // Trim leading IFS-whitespace per zsh's read semantics
3336        // (`a   b c` → x=a, y="b c", not x="" y=…).
3337        let trimmed = buf.trim_start_matches(|c: char| is_ifs(c) && c.is_whitespace());
3338        let mut remaining = trimmed.to_string();
3339        for (i, var) in vars.iter().enumerate() {
3340            if i + 1 == vars.len() {
3341                // Last var: store the remainder, trim trailing IFS.
3342                let final_val = remaining.trim_end_matches(|c: char|
3343                    is_ifs(c) && c.is_whitespace()).to_string();
3344                crate::ported::params::setsparam(var, &final_val);
3345            } else {
3346                // Find next IFS char.
3347                match remaining.find(is_ifs) {
3348                    Some(idx) => {
3349                        let field = remaining[..idx].to_string();
3350                        // Skip the IFS char + any leading
3351                        // whitespace-IFS that follows (zsh-style
3352                        // whitespace coalescing).
3353                        let rest = &remaining[idx + remaining[idx..]
3354                            .chars().next().map(|c| c.len_utf8()).unwrap_or(1)..];
3355                        let rest = rest.trim_start_matches(|c: char|
3356                            is_ifs(c) && c.is_whitespace());
3357                        crate::ported::params::setsparam(var, &field);
3358                        remaining = rest.to_string();
3359                    }
3360                    None => {
3361                        // No more IFS: this var gets remaining, others empty.
3362                        crate::ported::params::setsparam(var, &remaining);
3363                        remaining.clear();
3364                    }
3365                }
3366            }
3367        }
3368    } else {
3369        crate::ported::params::setsparam(&reply, &buf);
3370    }
3371    0
3372}
3373
3374/// Port of `bin_print(char *name, char **args, Options ops, int func)` from Src/builtin.c:4587.
3375/// C: `int bin_print(char *name, char **args, Options ops, int func)`.
3376///
3377/// The C body is ~1000 lines: `print` / `echo` / `printf` / `pushln`
3378/// dispatcher with -n/-N/-c/-r/-R/-l/-D/-i/-f/-v/-s/-S/-z/-e/-E etc.
3379/// The structural port handles the script-friendly subset that the
3380/// daily-driver hits: print/echo plain emission with -n, -l (one per
3381/// line), -r raw, -E newline-only, -- end-of-options. The full -f
3382/// printf format-spec engine and ZLE/history wireups defer to the
3383/// expand_printf_escapes helpers.
3384/// WARNING: param names don't match C — Rust=(name, args, func) vs C=(name, args, ops, func)
3385pub fn bin_print(name: &str, args: &[String],                                // c:4587
3386                 ops: &crate::ported::zsh_h::options, func: i32) -> i32 {
3387    let nonewline = OPT_ISSET(ops, b'n');                                    // c:4595
3388    let raw = OPT_ISSET(ops, b'r') || OPT_ISSET(ops, b'R');                  // c:4596
3389    let one_per_line = OPT_ISSET(ops, b'l');                                 // c:4597
3390    let _printf_mode = func == BIN_PRINTF || OPT_HASARG(ops, b'f');          // c:4604
3391    let echo_mode = func == BIN_ECHO;
3392    let _ = (name, raw);
3393
3394    // c:4633-4685 — destination dispatch. -u FD writes to fd, -s pushes
3395    // to history, -z to ZLE buffer, -v VAR assigns to scalar. Defer to
3396    // env/var wireup.
3397    let dest_var: Option<String> = if OPT_HASARG(ops, b'v') {
3398        OPT_ARG(ops, b'v').map(String::from)
3399    } else { None };
3400
3401    // c:4604-4612 — printf format-string handling.
3402    if _printf_mode {
3403        let fmt = if let Some(f) = OPT_ARG(ops, b'f') {
3404            f.to_string()
3405        } else if !args.is_empty() {
3406            args[0].clone()
3407        } else {
3408            return 0;
3409        };
3410        let rest: &[String] = if OPT_HASARG(ops, b'f') { args } else { &args[1..] };
3411        let out = printf_format(&fmt, rest);
3412        if let Some(ref v) = dest_var {
3413            crate::ported::params::setsparam(v, &out);
3414        } else {
3415            print!("{}", out);
3416        }
3417        return 0;
3418    }
3419
3420    // c:4860+ — main print loop.
3421    let sep = if one_per_line { "\n" } else { " " };
3422    // c:4598-4600 — `-P` prompt-style percent expansion (`%n`, `%d`,
3423    // `%?`, `%h`, `%%`, etc.). Routes through `expand_prompt`
3424    // (canonical port of `Src/prompt.c:182 promptexpand`).
3425    let mut processed_args: Vec<String> = if OPT_ISSET(ops, b'P') {
3426        args.iter()
3427            .map(|a| crate::ported::prompt::expand_prompt(a))                // c:Src/prompt.c:182
3428            .collect()
3429    } else {
3430        args.to_vec()
3431    };
3432    // c:Src/builtin.c:4869-4880 `-o` / `-O` / `-i` sort flags.
3433    // -o → case-insensitive ascending, -O → case-insensitive
3434    // descending, -i → case-sensitive (with -o/-O).
3435    if OPT_ISSET(ops, b'o') || OPT_ISSET(ops, b'O') {
3436        let case_sensitive = OPT_ISSET(ops, b'i');
3437        if case_sensitive {
3438            processed_args.sort();
3439        } else {
3440            processed_args.sort_by_key(|s| s.to_lowercase());
3441        }
3442        if OPT_ISSET(ops, b'O') {
3443            processed_args.reverse();
3444        }
3445    }
3446    // c:Src/builtin.c:4866-4886 — when `-r` is NOT set, each arg goes
3447    // through `getkeystring` to interpret backslash escapes (`\n`,
3448    // `\t`, `\\`, escaped space `\ `, etc.). `echo` follows the same
3449    // path when `BSD_ECHO`/`SH_OPTION_LETTERS`-style isn't in effect;
3450    // BIN_ECHO with `-E` keeps escapes literal. Without this, `print
3451    // -- ${(q)a}` for `a="he llo"` emitted `he\ llo` instead of zsh's
3452    // `he llo` (the (q) flag's backslash gets consumed by print).
3453    if !raw {
3454        let echo_E = echo_mode && OPT_ISSET(ops, b'E');
3455        if !echo_E {
3456            for a in processed_args.iter_mut() {
3457                let (s, _) = crate::ported::utils::getkeystring_with(a,
3458                    crate::ported::utils::GETKEYS_PRINT);
3459                *a = s;
3460            }
3461        }
3462    }
3463    let body = processed_args.join(sep);
3464    if let Some(ref v) = dest_var {
3465        crate::ported::params::setsparam(v, &body);
3466    } else {
3467        print!("{}", body);
3468        // c:5550 — final newline unless -n.
3469        if !nonewline && !echo_mode {
3470            println!();
3471        } else if echo_mode && !nonewline {
3472            println!();
3473        }
3474    }
3475    0
3476}
3477
3478/// Inline printf-style format helper used by bin_print's -f/printf mode.
3479/// Replaces `%s` / `%d` / `%i` / `%c` / `%%` with positional args.
3480/// Full C printf-spec engine (Src/builtin.c:4691-5500) is much more
3481/// elaborate (width/precision/flag chars/%b/%q/etc.); this is the
3482/// minimal subset that covers the common script patterns.
3483fn printf_format(fmt: &str, args: &[String]) -> String {
3484    // c:Src/builtin.c:4711 — `fmt = getkeystring(fmt, &flen, ...,
3485    // GETKEYS_PRINTF_FMT, ...);`. The format string is first run
3486    // through getkeystring to interpret backslash escapes (`\n`,
3487    // `\t`, `\xNN`, etc.) before %-format substitution.
3488    let (fmt, _) = getkeystring(fmt);                                        // c:builtin.c:4711
3489    let mut out = String::new();
3490    let mut arg_i: usize = 0;
3491    // c:Src/builtin.c:4914-4923 — printf reapplies the format string
3492    // until ALL args are consumed. `printf '%s,' a b c` → `a,b,c,`,
3493    // not `a,`. The outer loop reapplies; the inner do-while body
3494    // mirrors C's per-arg conversion loop directly.
3495    loop {
3496        let prev = arg_i;
3497        let mut iter = fmt.chars().peekable();
3498        while let Some(c) = iter.next() {
3499            if c != '%' {
3500                out.push(c);
3501                continue;
3502            }
3503            // c:Src/builtin.c:4791+ — parse width/precision/flag chars
3504            // between `%` and the conversion. Capture them so `printf
3505            // "%-10s" hi` and `printf "%.3f" 3.14159` render correctly.
3506            let mut spec = String::from("%");
3507            loop {
3508                match iter.peek() {
3509                    Some(&c) if matches!(c, '-' | '+' | ' ' | '#' | '0') => {
3510                        spec.push(c); iter.next();
3511                    }
3512                    _ => break,
3513                }
3514            }
3515            while let Some(&c) = iter.peek() {
3516                if c.is_ascii_digit() { spec.push(c); iter.next(); }
3517                else { break; }
3518            }
3519            if iter.peek() == Some(&'.') {
3520                spec.push('.'); iter.next();
3521                while let Some(&c) = iter.peek() {
3522                    if c.is_ascii_digit() { spec.push(c); iter.next(); }
3523                    else { break; }
3524                }
3525            }
3526            match iter.next() {
3527                Some('%') => out.push('%'),
3528                Some('s') => {
3529                    let a = args.get(arg_i).cloned().unwrap_or_default();
3530                    spec.push('s');
3531                    out.push_str(&format_spec_str(&spec, &a));
3532                    arg_i += 1;
3533                }
3534                Some('d') | Some('i') => {
3535                    let a = args.get(arg_i).cloned().unwrap_or_default();
3536                    let n: i64 = a.parse().unwrap_or(0);
3537                    spec.push('d');
3538                    out.push_str(&format_spec_int(&spec, n));
3539                    arg_i += 1;
3540                }
3541                Some('u') => {
3542                    let a = args.get(arg_i).cloned().unwrap_or_default();
3543                    let n: u64 = a.parse().unwrap_or(0);
3544                    spec.push('u');
3545                    out.push_str(&format_spec_uint(&spec, n));
3546                    arg_i += 1;
3547                }
3548                Some('x') => {
3549                    let a = args.get(arg_i).cloned().unwrap_or_default();
3550                    let n: i64 = a.parse().unwrap_or(0);
3551                    spec.push('x');
3552                    out.push_str(&format!("{:x}", n));
3553                    arg_i += 1;
3554                }
3555                Some('X') => {
3556                    let a = args.get(arg_i).cloned().unwrap_or_default();
3557                    let n: i64 = a.parse().unwrap_or(0);
3558                    spec.push('X');
3559                    out.push_str(&format!("{:X}", n));
3560                    arg_i += 1;
3561                }
3562                Some('o') => {
3563                    let a = args.get(arg_i).cloned().unwrap_or_default();
3564                    let n: i64 = a.parse().unwrap_or(0);
3565                    spec.push('o');
3566                    out.push_str(&format!("{:o}", n));
3567                    arg_i += 1;
3568                }
3569                Some('f') | Some('F') | Some('g') | Some('G') | Some('e') | Some('E') => {
3570                    let a = args.get(arg_i).cloned().unwrap_or_default();
3571                    let n: f64 = a.parse().unwrap_or(0.0);
3572                    spec.push('f');
3573                    out.push_str(&format_spec_float(&spec, n));
3574                    arg_i += 1;
3575                }
3576                Some('c') => {
3577                    if let Some(a) = args.get(arg_i) {
3578                        if let Some(ch) = a.chars().next() { out.push(ch); }
3579                    }
3580                    arg_i += 1;
3581                }
3582                // c:builtin.c:4825 %q — shell-quote the arg.
3583                Some('q') => {
3584                    let a = args.get(arg_i).cloned().unwrap_or_default();
3585                    out.push_str(&quotedzputs(&a));
3586                    arg_i += 1;
3587                }
3588                // c:builtin.c:4810 %b — interpret backslash escapes
3589                // with GETKEY_EMACS arm (drop unknown backslashes).
3590                Some('b') => {
3591                    let a = args.get(arg_i).cloned().unwrap_or_default();
3592                    let (s, _) = getkeystring_with(&a, GETKEYS_PRINT);
3593                    out.push_str(&s);
3594                    arg_i += 1;
3595                }
3596                Some(other) => { out.push('%'); out.push(other); }
3597                None => out.push('%'),
3598            }
3599        }
3600        if arg_i == prev || arg_i >= args.len() { break; }
3601    }
3602    out
3603}
3604
3605/// Apply a printf-style `%[-flag][width][.prec]s` spec to a string.
3606/// Mirrors C `printf "%-10s" str` formatting; the Rust `format!` macro
3607/// doesn't accept runtime-parsed specs so we hand-parse.
3608fn format_spec_str(spec: &str, s: &str) -> String {
3609    let (left_align, width, prec) = parse_width_prec(spec);
3610    let truncated: &str = if let Some(p) = prec {
3611        let end: usize = s.chars().take(p).map(|c| c.len_utf8()).sum();
3612        &s[..end.min(s.len())]
3613    } else { s };
3614    let pad = width.saturating_sub(truncated.chars().count());
3615    if left_align {
3616        format!("{}{}", truncated, " ".repeat(pad))
3617    } else {
3618        format!("{}{}", " ".repeat(pad), truncated)
3619    }
3620}
3621
3622fn format_spec_int(spec: &str, n: i64) -> String {
3623    let (left_align, width, _prec) = parse_width_prec(spec);
3624    let zero_pad = spec.contains('0') && !left_align;
3625    let body = n.to_string();
3626    let pad = width.saturating_sub(body.chars().count());
3627    if pad == 0 { body }
3628    else if left_align { format!("{}{}", body, " ".repeat(pad)) }
3629    else if zero_pad {
3630        if let Some(rest) = body.strip_prefix('-') {
3631            format!("-{}{}", "0".repeat(pad), rest)
3632        } else { format!("{}{}", "0".repeat(pad), body) }
3633    } else { format!("{}{}", " ".repeat(pad), body) }
3634}
3635
3636fn format_spec_uint(spec: &str, n: u64) -> String {
3637    format_spec_int(spec, n as i64)
3638}
3639
3640fn format_spec_float(spec: &str, n: f64) -> String {
3641    let (left_align, width, prec) = parse_width_prec(spec);
3642    let p = prec.unwrap_or(6);
3643    let body = format!("{:.*}", p, n);
3644    let pad = width.saturating_sub(body.chars().count());
3645    if pad == 0 { body }
3646    else if left_align { format!("{}{}", body, " ".repeat(pad)) }
3647    else { format!("{}{}", " ".repeat(pad), body) }
3648}
3649
3650fn parse_width_prec(spec: &str) -> (bool, usize, Option<usize>) {
3651    let s = spec.trim_start_matches('%');
3652    let mut i = 0;
3653    let bytes = s.as_bytes();
3654    let mut left_align = false;
3655    while i < bytes.len() && matches!(bytes[i], b'-' | b'+' | b' ' | b'#' | b'0') {
3656        if bytes[i] == b'-' { left_align = true; }
3657        i += 1;
3658    }
3659    let width_start = i;
3660    while i < bytes.len() && bytes[i].is_ascii_digit() { i += 1; }
3661    let width: usize = s[width_start..i].parse().unwrap_or(0);
3662    let mut prec: Option<usize> = None;
3663    if i < bytes.len() && bytes[i] == b'.' {
3664        i += 1;
3665        let p_start = i;
3666        while i < bytes.len() && bytes[i].is_ascii_digit() { i += 1; }
3667        prec = Some(s[p_start..i].parse().unwrap_or(0));
3668    }
3669    (left_align, width, prec)
3670}
3671
3672/// Port of `bin_fc(char *nam, char **argv, Options ops, int func)` from Src/builtin.c:1426.
3673/// C: `int bin_fc(char *nam, char **argv, Options ops, int func)`.
3674///
3675/// History/edit/list dispatcher: `-p` push hist stack, `-P` pop,
3676/// `-R` read, `-W` write, `-A` append, `-m` glob filter, `-l` list,
3677/// `-s` substitute, default: edit + re-execute. The C body is ~245
3678/// lines; the structural translation here covers the major options
3679/// and dispatches the underlying history-file ops to the existing
3680/// hist.rs accessors.
3681/// WARNING: param names don't match C — Rust=(nam, argv, func) vs C=(nam, argv, ops, func)
3682pub fn bin_fc(nam: &str, argv: &[String],                                    // c:1426
3683              ops: &mut crate::ported::zsh_h::options, func: i32) -> i32 {
3684    let mut argv = argv.to_vec();
3685    let mut first: i64 = -1;
3686    let mut last: i64 = -1;
3687    let mut asgf: Vec<(String, String)> = Vec::new();
3688
3689
3690    // c:1441-1481 — `-p` push history stack.
3691    if OPT_ISSET(ops, b'p') {                                                // c:1441
3692        let mut hf = "".to_string();
3693        let mut hs: i64;                                                     // c:1443
3694        let mut shs: i64;                                                    // c:1444
3695        // c:1445 — `int level = OPT_ISSET(ops,'a') ? locallevel : -1;`
3696        let level: i32 = if OPT_ISSET(ops, b'a') {
3697            LOCALLEVEL.load(std::sync::atomic::Ordering::Relaxed)
3698        } else { -1 };
3699        hs = crate::ported::hist::histsiz.load(Ordering::Relaxed);           // c:1442
3700        shs = crate::ported::hist::savehistsiz.load(Ordering::Relaxed);
3701        if !argv.is_empty() {                                                // c:1445
3702            hf = argv.remove(0);                                             // c:1446
3703            if !argv.is_empty() {                                            // c:1447
3704                let s2 = argv.remove(0);
3705                match s2.parse::<i64>() {                                    // c:1449 zstrtol
3706                    Ok(n) => hs = n,
3707                    Err(_) => {
3708                        crate::ported::utils::zwarnnam("fc",                 // c:1452
3709                            "HISTSIZE must be an integer");
3710                        return 1;                                            // c:1453
3711                    }
3712                }
3713                if !argv.is_empty() {                                        // c:1455
3714                    let s3 = argv.remove(0);
3715                    match s3.parse::<i64>() {                                // c:1456
3716                        Ok(n) => shs = n,
3717                        Err(_) => {
3718                            crate::ported::utils::zwarnnam("fc",             // c:1459
3719                                "SAVEHIST must be an integer");
3720                            return 1;                                        // c:1460
3721                        }
3722                    }
3723                } else {
3724                    shs = hs;                                                // c:1464
3725                }
3726                if !argv.is_empty() {                                        // c:1466
3727                    crate::ported::utils::zwarnnam("fc",                     // c:1468
3728                        "too many arguments");
3729                    return 1;                                                // c:1469
3730                }
3731            }
3732        }
3733        // c:1473 — pushhiststack(hf, hs, shs, level); failure → return 1.
3734        crate::ported::hist::pushhiststack(Some(&hf), hs, shs, level);       // c:1473
3735        if !hf.is_empty() {                                                  // c:1475
3736            // c:1476-1480 — stat then readhistfile(hf, 1, HFILE_USE_OPTIONS).
3737            let exists = std::fs::metadata(&hf).is_ok();
3738            let enoent = std::io::Error::last_os_error().raw_os_error()
3739                == Some(libc::ENOENT);
3740            if exists || !enoent {                                           // c:1477
3741                crate::ported::hist::readhistfile(                           // c:1478
3742                    Some(&hf), 1, HFILE_USE_OPTIONS as i32);
3743            }
3744        }
3745        return 0;                                                            // c:1483
3746    }
3747
3748    // c:1485-1491 — `-P` pop history stack.
3749    if OPT_ISSET(ops, b'P') {                                                // c:1485
3750        if !argv.is_empty() {                                                // c:1486
3751            crate::ported::utils::zwarnnam("fc", "too many arguments");      // c:1487
3752            return 1;                                                        // c:1488
3753        }
3754        // c:1490 — `return !saveandpophiststack(-1, HFILE_USE_OPTIONS);`.
3755        crate::ported::hist::saveandpophiststack(HFILE_USE_OPTIONS as i32);  // c:1490
3756        return 0;
3757    }
3758
3759    // c:1494-1500 — `-m` pattern filter (compile first arg).
3760    let mut pprog: Option<crate::ported::pattern::PatProg> = None;
3761    if !argv.is_empty() && OPT_ISSET(ops, b'm') {                            // c:1494
3762        let pat = argv.remove(0);
3763        // c:1495 — tokenize(*argv); — Rust `patcompile` handles tokenisation.
3764        match crate::ported::pattern::patcompile(&pat,                       // c:1496
3765            crate::ported::zsh_h::PAT_HEAPDUP, None) {
3766            Some(p) => pprog = Some(p),
3767            None => {
3768                crate::ported::utils::zwarnnam(nam, "invalid match pattern"); // c:1497
3769                return 1;                                                    // c:1498
3770            }
3771        }
3772    }
3773
3774    crate::ported::mem::queue_signals();                                     // c:1502
3775
3776    // c:1503-1525 — `-R` read / `-W` write / `-A` append history file.
3777    if OPT_ISSET(ops, b'R') {                                                // c:1503
3778        let path = argv.first().cloned();
3779        let flags = if OPT_ISSET(ops, b'I') { HFILE_SKIPOLD as i32 } else { 0 };
3780        crate::ported::hist::readhistfile(                                   // c:1505
3781            path.as_deref(), 1, flags);
3782        crate::ported::mem::unqueue_signals();                               // c:1506
3783        return 0;                                                            // c:1507
3784    }
3785    if OPT_ISSET(ops, b'W') {                                                // c:1509
3786        let path = argv.first().cloned();
3787        let flags = if OPT_ISSET(ops, b'I') { HFILE_SKIPOLD as i32 } else { 0 };
3788        crate::ported::hist::savehistfile(                                   // c:1511
3789            path.as_deref(), flags);
3790        crate::ported::mem::unqueue_signals();                               // c:1512
3791        return 0;                                                            // c:1513
3792    }
3793    if OPT_ISSET(ops, b'A') {                                                // c:1515
3794        let path = argv.first().cloned();
3795        let mut flags = HFILE_APPEND as i32;
3796        if OPT_ISSET(ops, b'I') { flags |= HFILE_SKIPOLD as i32; }           // c:1518
3797        crate::ported::hist::savehistfile(                                   // c:1517
3798            path.as_deref(), flags);
3799        crate::ported::mem::unqueue_signals();                               // c:1519
3800        return 0;                                                            // c:1520
3801    }
3802
3803    // c:1523-1527 — refuse inside ZLE.
3804    if crate::ported::builtins::sched::zleactive.load(                       // c:1523
3805        std::sync::atomic::Ordering::Relaxed) != 0 {
3806        crate::ported::mem::unqueue_signals();                               // c:1524
3807        crate::ported::utils::zwarnnam(nam,                                  // c:1525
3808            "no interactive history within ZLE");
3809        return 1;                                                            // c:1526
3810    }
3811
3812    // c:1530-1547 — `name=value` substitution pairs.
3813    while !argv.is_empty() && argv[0].contains('=') {                        // c:1530
3814        let arg = argv.remove(0);
3815        if let Some(eq) = arg.find('=') {
3816            let n = &arg[..eq];
3817            let v = &arg[eq + 1..];
3818            if n.is_empty() {
3819                crate::ported::utils::zwarnnam(nam,
3820                    &format!("invalid replacement pattern: ={}", v));        // c:1534
3821                return 1;
3822            }
3823            asgf.push((n.to_string(), v.to_string()));                       // c:1546
3824        }
3825    }
3826
3827    // c:1550-1568 — first/last history specifiers via fcgetcomm.
3828    if !argv.is_empty() {                                                    // c:1550
3829        first = fcgetcomm(&argv.remove(0));                                  // c:1551
3830        if first == -1 {
3831            crate::ported::mem::unqueue_signals();
3832            return 1;                                                        // c:1553
3833        }
3834    }
3835    if !argv.is_empty() {                                                    // c:1559
3836        last = fcgetcomm(&argv.remove(0));                                   // c:1560
3837        if last == -1 {
3838            crate::ported::mem::unqueue_signals();
3839            return 1;
3840        }
3841    }
3842    if !argv.is_empty() {                                                    // c:1567
3843        crate::ported::mem::unqueue_signals();
3844        crate::ported::utils::zwarnnam("fc", "too many arguments");          // c:1569
3845        return 1;
3846    }
3847
3848    // c:1573-1610 — default ranges + listing/edit dispatch. C reads
3849    //                the live `curhist` global; in zshrs that comes
3850    //                from `prompt_tls::HISTNUM` (which mirrors $HISTCMD).
3851    //                Use getiparam so paramtab handles the lookup and
3852    //                conversion uniformly.
3853    let curhist: i64 = crate::ported::params::getiparam("HISTCMD");
3854    if last == -1 {                                                          // c:1573
3855        if OPT_ISSET(ops, b'l') && first < curhist {                         // c:1574
3856            last = curhist;                                                  // c:1583
3857            if last < 1 { last = 1; }                                        // c:1585
3858        } else {
3859            last = first;                                                    // c:1587
3860        }
3861    }
3862    if first == -1 {                                                         // c:1589
3863        let _xflags = if OPT_ISSET(ops, b'L') { HIST_FOREIGN } else { 0 };   // c:1597
3864        first = if OPT_ISSET(ops, b'l') { (curhist - 16).max(1) }            // c:1598
3865                else { (curhist - 1).max(1) };
3866        if last < first { last = first; }                                    // c:1604
3867    }
3868
3869    let mut retval;
3870    if OPT_ISSET(ops, b'l') {                                                // c:1606
3871        // c:1608 — `fclist(stdout, ops, first, last, asgf, pprog, 0);`
3872        retval = fclist(&mut std::io::stdout(), ops, first, last,
3873                        &asgf, None, 0);
3874        crate::ported::mem::unqueue_signals();
3875    } else {
3876        // c:1611-1668 — edit history range to a temp file, fcedit it,
3877        // then stuff() the result back as the next command.
3878        retval = 1;                                                          // c:1620
3879        let fil_opt = crate::ported::utils::gettempfile(Some("zshfc"));      // c:1621 gettempfile
3880        match fil_opt {
3881            None => {                                                        // c:1623
3882                crate::ported::mem::unqueue_signals();                       // c:1624
3883                crate::ported::utils::zwarnnam("fc",                         // c:1625
3884                    &format!("can't open temp file: {}",
3885                        std::io::Error::last_os_error()));
3886            }
3887            Some((fd, fil)) => {
3888                unsafe { libc::close(fd); }                                  // c:1622 (file is reopened below)
3889                // c:1632 — `if (last >= curhist) { last = curhist - 1; ... }`
3890                if last >= curhist {                                         // c:1632
3891                    last = curhist - 1;                                      // c:1633
3892                    if first > last {                                        // c:1634
3893                        crate::ported::mem::unqueue_signals();               // c:1635
3894                        crate::ported::utils::zwarnnam("fc",                 // c:1636
3895                            "current history line would recurse endlessly, aborted");
3896                        let _ = std::fs::remove_file(&fil);                  // c:1639 unlink
3897                        return 1;                                            // c:1640
3898                    }
3899                }
3900                ops.ind[b'n' as usize] = 1;                                  // c:1644 No line numbers
3901                let out = std::fs::OpenOptions::new()
3902                    .create(true).write(true).truncate(true).open(&fil).ok();
3903                let listed = if let Some(mut f) = out {                      // c:1645
3904                    fclist(&mut f, ops, first, last, &asgf, None, 1)
3905                } else { 1 };
3906                if listed == 0 {                                             // c:1645
3907                    // c:1647-1656 — pick editor.
3908                    let editor: String = if func == BIN_R || OPT_ISSET(ops, b's') {
3909                        "-".to_string()                                      // c:1648
3910                    } else if OPT_HASARG(ops, b'e') {                        // c:1649
3911                        OPT_ARG(ops, b'e').unwrap_or("").to_string()         // c:1650
3912                    } else {
3913                        // c:1651-1654 — `getsparam("FCEDIT") ?:
3914                        //                  getsparam("EDITOR") ?:
3915                        //                  DEFAULT_FCEDIT`. paramtab read.
3916                        crate::ported::params::getsparam("FCEDIT")
3917                            .or_else(|| crate::ported::params::getsparam("EDITOR"))
3918                            .unwrap_or_else(||
3919                                crate::ported::config_h::DEFAULT_FCEDIT.to_string())
3920                    };
3921                    crate::ported::mem::unqueue_signals();                   // c:1657
3922                    if fcedit(&editor, &fil) != 0 {                          // c:1658
3923                        if crate::ported::input::stuff(&fil) != 0 {          // c:1659
3924                            crate::ported::utils::zwarnnam("fc",             // c:1660
3925                                &format!("{}: {}",
3926                                    std::io::Error::last_os_error(), fil));
3927                        } else {
3928                            // c:1663-1664 — `loop(0,1); retval = lastval;`
3929                            // The interactive loop drives the next stuffed
3930                            // line through the parser. Static-link path:
3931                            // the executor's input source picks it up on
3932                            // the next read; lastval reflects that result.
3933                            retval = LASTVAL.load(                           // c:1664
3934                                std::sync::atomic::Ordering::Relaxed);
3935                        }
3936                    }
3937                } else {
3938                    crate::ported::mem::unqueue_signals();                   // c:1667
3939                }
3940                let _ = std::fs::remove_file(&fil);                          // c:1671 unlink
3941            }
3942        }
3943    }
3944    let _ = pprog;
3945    retval                                                                   // c:1675
3946}
3947
3948/// Port of `bin_typeset(char *name, char **argv, LinkList assigns, Options ops, int func)` from Src/builtin.c:2655.
3949/// C: `int bin_typeset(char *name, char **argv, LinkList assigns,
3950///     Options ops, int func)`.
3951///
3952/// The C body (~500 lines) ports here in two layers: the option-flag
3953/// matrix + conflict-resolution / dispatch (faithfully translated)
3954/// and the per-arg param-setting loop (delegated to typeset_single
3955/// already ported above).
3956/// WARNING: param names don't match C — Rust=(name, argv, func) vs C=(name, argv, assigns, ops, func)
3957pub fn bin_typeset(name: &str, argv: &[String],                              // c:2655
3958                   ops: &crate::ported::zsh_h::options, func: i32) -> i32 {
3959
3960    // PFA-SMR aspect: bin_typeset is the C dispatch site for
3961    // typeset/declare/integer/float/local/export/readonly/private —
3962    // every one of those state-mutating builtins lands here with a
3963    // funcid (BIN_EXPORT/BIN_READONLY/BIN_TYPESET/...) discriminant.
3964    // Emit a per-name event per the recorder schema.
3965    #[cfg(feature = "recorder")]
3966    if crate::recorder::is_enabled() {
3967        let ctx = crate::recorder::recorder_ctx_global();
3968        // Collect option letters (`-x`/`+x` body) so ParamAttrs reflects
3969        // the typeset flag set the C source sees in `on`.
3970        let mut letters = String::new();
3971        let mut tied_mode = false;
3972        for a in argv {
3973            if a.starts_with('-') || a.starts_with('+') {
3974                let body = &a[1..];
3975                letters.push_str(body);
3976                if body.contains('T') { tied_mode = true; }
3977            }
3978        }
3979        // Funcid-driven attr seeding: BIN_EXPORT seeds nothing
3980        // (recorder uses emit_export for those), BIN_READONLY seeds
3981        // SCALAR|READONLY, BIN_FLOAT seeds FLOAT, BIN_INTEGER seeds
3982        // INTEGER. Otherwise pass the letter set through
3983        // ParamAttrs::from_flag_chars verbatim.
3984        let mut attrs = crate::recorder::ParamAttrs::from_flag_chars(&letters);
3985        match func {
3986            crate::ported::builtin::BIN_READONLY => {
3987                attrs.set(crate::recorder::ParamAttrs::SCALAR);
3988                attrs.set(crate::recorder::ParamAttrs::READONLY);
3989            }
3990            _ => {}
3991        }
3992        // BIN_EXPORT routes to emit_export (different schema row).
3993        if func == crate::ported::builtin::BIN_EXPORT {
3994            for a in argv {
3995                if a == "-p" || a.starts_with('-') { continue; }
3996                if let Some((k, v)) = a.split_once('=') {
3997                    crate::recorder::emit_export(k, Some(v), ctx.clone());
3998                } else {
3999                    crate::recorder::emit_export(a, None, ctx.clone());
4000                }
4001            }
4002        } else {
4003            // Suppress the emit when invoked as `local`/`private` inside
4004            // a function — those scope to the frame and don't merit a
4005            // top-level state-mutation row. local_scope_depth is tracked
4006            // by the executor; defer to the global LOCALLEVEL counter.
4007            let is_locallike = matches!(name, "local" | "private");
4008            let inside_function =
4009                LOCALLEVEL.load(std::sync::atomic::Ordering::Relaxed) > 0;
4010            if !is_locallike || !inside_function {
4011                let mut tied_seen = 0usize;
4012                for a in argv {
4013                    if a.starts_with('-') || a.starts_with('+') { continue; }
4014                    if tied_mode {
4015                        // For `typeset -T X Y [SEP]`, only X and Y are names.
4016                        tied_seen += 1;
4017                        if tied_seen > 2 { break; }
4018                    }
4019                    if let Some((k, v)) = a.split_once('=') {
4020                        crate::recorder::emit_typeset_attrs(k, Some(v), attrs, ctx.clone());
4021                    } else {
4022                        crate::recorder::emit_typeset_attrs(a, None, attrs, ctx.clone());
4023                    }
4024                }
4025            }
4026        }
4027    }
4028    let mut ops = ops.clone();
4029    let mut on: u32 = 0;                                                     // c:2661
4030    let mut off: u32 = 0;                                                    // c:2661
4031    let returnval: i32 = 0;                                                  // c:2664
4032    let mut printflags: i32 = PRINT_WITH_NAMESPACE;                          // c:2664
4033    let hasargs = !argv.is_empty();                                          // c:2665
4034
4035    // c:2668-2670 — POSIX bash/ksh ignore -p with args under
4036    // readonly/export.
4037    let posix = crate::ported::zsh_h::isset(crate::ported::options::optlookup("posixbuiltins"));
4038    if (func == BIN_READONLY || func == BIN_EXPORT) && posix && hasargs {    // c:2668
4039        ops.ind[b'p' as usize] = 0;                                          // c:2670
4040    }
4041
4042    // c:2673 — `if (OPT_ISSET(ops,'f')) return bin_functions(...)`.
4043    if OPT_ISSET(&ops, b'f') {                                               // c:2673
4044        return bin_functions(name, argv, &ops, func);                        // c:2673
4045    }
4046
4047    // c:2676 — POSIX readonly forces -g unless explicit +g.
4048    if func == BIN_READONLY && posix && !OPT_PLUS(&ops, b'g') {              // c:2676
4049        ops.ind[b'g' as usize] = 1;                                          // c:2677
4050    }
4051
4052    // c:2691-2706 — translate optstr letters into PM_* flag bits.
4053    let mut bit: u32 = PM_ARRAY;                                             // c:2660
4054    for ch in TYPESET_OPTSTR.chars() {                                       // c:2691
4055        let optval = ch as u8;
4056        if OPT_MINUS(&ops, optval) { on |= bit; }                            // c:2694-2695
4057        else if OPT_PLUS(&ops, optval) { off |= bit; }                       // c:2696-2697
4058        // c:2698-2706 — `-n` only allows readonly/upper/hideval.
4059        else { bit <<= 1; continue; }
4060        if OPT_MINUS(&ops, b'n')
4061            && (bit & !(PM_READONLY | PM_UPPER | PM_HIDEVAL)) != 0           // c:2701
4062        {
4063            crate::ported::utils::zwarnnam(name,
4064                &format!("-{} not allowed with -n", ch));                    // c:2702
4065        }
4066        bit <<= 1;
4067    }
4068    // c:2708-2715 — -n / +n conflict resolution.
4069    if OPT_MINUS(&ops, b'n') {                                               // c:2708
4070        if (on | off) & !(PM_READONLY | PM_UPPER | PM_HIDEVAL) != 0 {        // c:2710
4071            return 1;                                                        // c:2711
4072        }
4073        on |= PM_NAMEREF;                                                    // c:2713
4074    } else if OPT_PLUS(&ops, b'n') {                                         // c:2714
4075        off |= PM_NAMEREF;                                                   // c:2715
4076    }
4077    let roff = off;                                                          // c:2716
4078
4079    // c:2719-2740 — sanity checks: remove conflicting attrs.
4080    if (on & PM_FFLOAT) != 0 {                                               // c:2719
4081        off |= PM_UPPER | PM_ARRAY | PM_HASHED | PM_INTEGER | PM_EFLOAT;     // c:2720
4082        on &= !PM_EFLOAT;                                                    // c:2722
4083    }
4084    if (on & PM_EFLOAT) != 0 {                                               // c:2724
4085        off |= PM_UPPER | PM_ARRAY | PM_HASHED | PM_INTEGER | PM_FFLOAT;     // c:2725
4086    }
4087    if (on & PM_INTEGER) != 0 {                                              // c:2726
4088        off |= PM_UPPER | PM_ARRAY | PM_HASHED | PM_EFLOAT | PM_FFLOAT;      // c:2727
4089    }
4090    if (on & (PM_LEFT | PM_RIGHT_Z)) != 0 {                                  // c:2731
4091        off |= PM_RIGHT_B;                                                   // c:2732
4092    }
4093    if (on & PM_RIGHT_B) != 0 {                                              // c:2733
4094        off |= PM_LEFT | PM_RIGHT_Z;                                         // c:2734
4095    }
4096    if (on & PM_UPPER) != 0 { off |= PM_LOWER; }                             // c:2735-2736
4097    if (on & PM_LOWER) != 0 { off |= PM_UPPER; }                             // c:2737-2738
4098    if (on & PM_HASHED) != 0 { off |= PM_ARRAY; }                            // c:2739-2740
4099    if (on & PM_TIED) != 0 {                                                 // c:2741
4100        off |= PM_INTEGER | PM_EFLOAT | PM_FFLOAT | PM_ARRAY | PM_HASHED;    // c:2742
4101    }
4102    on &= !off;                                                              // c:2744
4103
4104    crate::ported::mem::queue_signals();                                     // c:2746
4105
4106    // c:2748-2772 — `-p` print-mode: PRINT_POSIX_EXPORT / READONLY /
4107    // TYPESET, plus optional -p N for line-style.
4108    if OPT_ISSET(&ops, b'p') {                                               // c:2748
4109        if posix && !EMULATION(EMULATE_KSH) {                                // c:2750
4110            printflags |= match func {
4111                BIN_EXPORT   => PRINT_POSIX_EXPORT,                          // c:2752
4112                BIN_READONLY => PRINT_POSIX_READONLY,                        // c:2754
4113                _            => PRINT_TYPESET,                               // c:2756
4114            };
4115        } else {
4116            printflags |= PRINT_TYPESET;                                     // c:2758
4117        }
4118        if OPT_HASARG(&ops, b'p') {                                          // c:2761
4119            let arg = OPT_ARG(&ops, b'p').unwrap_or("");
4120            match arg.trim().parse::<i32>() {                                // c:2763
4121                Ok(1) => printflags |= PRINT_LINE,                           // c:2765
4122                Ok(0) => {}                                                  // c:2770 -p0 == -p
4123                _ => {
4124                    crate::ported::utils::zwarnnam(name,
4125                        &format!("bad argument to -p: {}", arg));            // c:2767
4126                    crate::ported::mem::unqueue_signals();
4127                    return 1;                                                // c:2769
4128                }
4129            }
4130        }
4131    }
4132
4133    // c:2775-2795 — no-args path: list whatever options select.
4134    if !hasargs {                                                            // c:2775
4135        if !OPT_ISSET(&ops, b'm') {                                          // c:2779
4136            printflags &= !PRINT_WITH_NAMESPACE;                             // c:2780
4137        }
4138        if !OPT_ISSET(&ops, b'p') {                                          // c:2782
4139            if (on | roff) == 0 {                                            // c:2783
4140                printflags |= PRINT_TYPE;                                    // c:2784
4141            }
4142            if roff != 0 || OPT_ISSET(&ops, b'+') {                          // c:2785
4143                printflags |= PRINT_NAMEONLY;                                // c:2786
4144            }
4145        }
4146        // c:2792 — scanhashtable(paramtab, ...) listing path. Defer
4147        // to env walk for static-link path (real paramtab walk lives
4148        // in src/ported/params.rs).
4149        for (k, v) in std::env::vars() {                                     // c:2792
4150            if (printflags & PRINT_NAMEONLY) != 0 {
4151                println!("{}", k);
4152            } else {
4153                println!("{}={}", k,
4154                    crate::ported::utils::quotedzputs(&v));
4155            }
4156        }
4157        crate::ported::mem::unqueue_signals();
4158        return 0;                                                            // c:2794
4159    }
4160
4161    // c:2799-2810 — `local` (or +g) implies PM_LOCAL.
4162    let nm0 = name.chars().next().unwrap_or(' ');
4163    if nm0 == 'l' || OPT_PLUS(&ops, b'g') {                                  // c:2799
4164        on |= PM_LOCAL;                                                      // c:2800
4165    } else if !OPT_ISSET(&ops, b'g') {                                       // c:2801
4166        if OPT_MINUS(&ops, b'x') {                                           // c:2802
4167            let globalexport = crate::ported::zsh_h::isset(crate::ported::options::optlookup("globalexport"));
4168            let locallevel = LOCALLEVEL.load(std::sync::atomic::Ordering::Relaxed);
4169            if globalexport {                                                // c:2803
4170                ops.ind[b'g' as usize] = 1;                                  // c:2804
4171            } else if locallevel != 0 {                                      // c:2805
4172                on |= PM_LOCAL;                                              // c:2806
4173            }
4174        } else if !(OPT_ISSET(&ops, b'x') || OPT_ISSET(&ops, b'm')) {        // c:2808
4175            on |= PM_LOCAL;                                                  // c:2809
4176        }
4177    }
4178
4179    // c:2813+ — -T tied vars + per-arg setting loop.
4180    // The full C body has dozens of paths (PM_TIED tie-pair setup at
4181    // c:2813-2900, glob -m walk at c:2905-2935, name=value assign
4182    // through typeset_single at c:2945+). The Rust port handles the
4183    // three high-frequency paths inline: assoc creation (`PM_HASHED`
4184    // + `name=(k v k v)`), array creation (`PM_ARRAY` + `name=(a b c)`),
4185    // and scalar assignment.
4186    let _ = (off, returnval, name);
4187    let is_hashed = (on & PM_HASHED) != 0;                                   // c:2655 `-A`
4188    let is_array  = (on & PM_ARRAY)  != 0;                                   // c:2655 `-a`
4189    for arg in argv {
4190        // c:Src/builtin.c typeset_single — when PM_LOCAL is in
4191        // flags, createparam first to install pm.old chain at
4192        // locallevel (createparam c:1132-1147). Applies uniformly
4193        // to all forms: `local x`, `local x=v`, `local arr=(...)`,
4194        // `local -A h`. endparamscope unwinds via Param.old.
4195        let arg_name: &str = match arg.find('=') {
4196            Some(i) => &arg[..i],
4197            None => arg.as_str(),
4198        };
4199        if (on & PM_LOCAL) != 0
4200            && !arg_name.is_empty()
4201            && !arg_name.starts_with('-')
4202            && !arg_name.starts_with('+')
4203        {
4204            let kind = if is_hashed { PM_HASHED } else if is_array { PM_ARRAY } else { 0 };
4205            let _ = crate::ported::params::createparam(
4206                arg_name, on as i32 | kind as i32 | PM_LOCAL as i32);
4207        }
4208        if let Some(eq) = arg.find('=') {
4209            let n = &arg[..eq];
4210            let raw_v = &arg[eq + 1..];
4211            // c:2945-3050 — `=(elem elem ...)` array-init syntax.
4212            // The parser hands the whole `(...)` body in as one arg
4213            // when typeset's BINF_MAGICEQUALS is set; the `(` / `)` are
4214            // literal first/last bytes. Strip them and split on
4215            // whitespace to recover the element list.
4216            let is_paren_init = raw_v.starts_with('(') && raw_v.ends_with(')')
4217                && raw_v.len() >= 2;
4218            if is_paren_init {
4219                let inner = &raw_v[1..raw_v.len()-1];                        // c:2950
4220                let elems: Vec<String> = inner.split_whitespace()            // c:2952
4221                    .map(String::from)
4222                    .collect();
4223                if is_hashed {
4224                    // c:2960-2975 — `setdataparam(..., PM_HASHED, …)`.
4225                    // Two assoc-init shapes accepted by zsh:
4226                    //  1. flat alternating k/v: `m=(k1 v1 k2 v2)`
4227                    //  2. per-element [K]=V:    `m=([k1]=v1 [k2]=v2)`
4228                    // The parser hands all elements as one `(…)` body,
4229                    // so we detect shape 2 when every element starts
4230                    // with `[` and contains `]=`. Otherwise fall back
4231                    // to alternating pairs.
4232                    let bracket_shape = !elems.is_empty()
4233                        && elems.iter().all(|e| {
4234                            e.starts_with('[')
4235                                && e.contains("]=")
4236                        });
4237                    let mut map: indexmap::IndexMap<String, String>
4238                        = indexmap::IndexMap::new();
4239                    if bracket_shape {
4240                        for e in &elems {
4241                            let close = e.find("]=").unwrap();
4242                            let k = e[1..close].to_string();
4243                            let v = e[close + 2..].to_string();
4244                            map.insert(k, v);
4245                        }
4246                    } else {
4247                        let mut it = elems.into_iter();                      // c:2960 pair walk
4248                        while let Some(k) = it.next() {
4249                            let v = it.next().unwrap_or_default();
4250                            map.insert(k, v);                                // c:2964 hashtab insert
4251                        }
4252                    }
4253                    let n_owned = n.to_string();
4254                    crate::fusevm_bridge::with_executor(|exec| {
4255                        exec.set_assoc(n_owned, map.clone());
4256                    });
4257                } else {
4258                    // c:2980-2995 — plain array.
4259                    let n_owned = n.to_string();
4260                    let elems_owned = elems.clone();
4261                    crate::fusevm_bridge::with_executor(|exec| {
4262                        exec.set_array(n_owned, elems_owned);
4263                    });
4264                }
4265            } else {
4266                // c:3010-3030 — `name=value` scalar assign. C-canonical
4267                // `setsparam` (Src/params.c:3350) writes paramtab; the
4268                // env mirror at `Src/params.c:3024 addenv` follows.
4269                // c:Src/params.c PM_LOWER/PM_UPPER setstrvalue arms:
4270                // when typeset -l or -u is set, the assigned value is
4271                // case-folded BEFORE storage. Without this, `typeset -l
4272                // s=HELLO; echo $s` printed `HELLO`. We also mirror to
4273                // exec.var_attrs so subsequent plain assigns (`s=NEW`)
4274                // pick up the fold via the SET_VAR opcode's attr
4275                // check (fusevm_bridge.rs case-fold arm).
4276                let lower = (on & PM_LOWER) != 0;
4277                let upper = (on & PM_UPPER) != 0;
4278                let folded: String = if lower {
4279                    raw_v.to_lowercase()
4280                } else if upper {
4281                    raw_v.to_uppercase()
4282                } else {
4283                    raw_v.to_string()
4284                };
4285                crate::ported::params::setsparam(n, &folded);                // c:params.c:3350
4286                // c:Src/params.c:3024 addenv — only mirror to OS env
4287                // when PM_EXPORTED is in flags or already-exported.
4288                // The unconditional env::set_var here was a pre-
4289                // existing bug exposed by Task 25: local scalars
4290                // were leaking to env, surviving endparamscope.
4291                let already_exported = std::env::var_os(n).is_some();
4292                if (on & crate::ported::zsh_h::PM_EXPORTED) != 0 || already_exported {
4293                    std::env::set_var(n, &folded);                           // c:3024 addenv
4294                }
4295                // C-canonical: typeset -i / -F / -E / -l / -u / -r set
4296                // PM_INTEGER / PM_FFLOAT / PM_EFLOAT / PM_LOWER /
4297                // PM_UPPER / PM_READONLY on the Param (Src/builtin.c
4298                // typeset_single + Src/params.c assignsparam). We set
4299                // them on the just-created paramtab entry so SET_VAR
4300                // and subsequent reads see the type metadata in one
4301                // canonical place — no exec.var_attrs mirror needed.
4302                let type_mask = (PM_INTEGER | PM_EFLOAT | PM_FFLOAT
4303                    | PM_LOWER | PM_UPPER | PM_READONLY) as i32;
4304                let to_set = (on & (PM_INTEGER | PM_EFLOAT | PM_FFLOAT
4305                    | PM_LOWER | PM_UPPER | PM_READONLY)) as i32;
4306                if to_set != 0 {
4307                    if let Ok(mut tab) = crate::ported::params::paramtab().write() {
4308                        if let Some(pm) = tab.get_mut(n) {
4309                            pm.node.flags = (pm.node.flags & !type_mask) | to_set;
4310                        }
4311                    }
4312                }
4313            }
4314        } else if is_hashed || is_array {
4315            // c:3060-3070 — bare name + `-A`/`-a` declares an empty
4316            // assoc/array.
4317            let n_owned = arg.clone();
4318            crate::fusevm_bridge::with_executor(|exec| {
4319                if is_hashed {
4320                    if exec.assoc(&n_owned).is_none() {
4321                        exec.set_assoc(n_owned.clone(), indexmap::IndexMap::new());
4322                    }
4323                } else if exec.array(&n_owned).is_none() {
4324                    exec.set_array(n_owned.clone(), Vec::new());
4325                }
4326            });
4327        } else {
4328            // c:3072 — `if (!getsparam(arg)) setsparam(arg, "")`. Bare
4329            //          name + no type flag declares an empty scalar
4330            //          when none exists. C consults paramtab; was
4331            //          checking OS env which never sees scalar-only
4332            //          params (a `local foo` would be invisible).
4333            if crate::ported::params::getsparam(arg).is_none() {
4334                crate::ported::params::setsparam(arg, "");                   // c:3074
4335            }
4336        }
4337    }
4338    crate::ported::mem::unqueue_signals();
4339    0
4340}
4341
4342/// Port of `bin_whence(char *nam, char **argv, Options ops, int func)` from Src/builtin.c:3975.
4343/// C: `int bin_whence(char *nam, char **argv, Options ops, int func)`.
4344///
4345/// `whence`/`type`/`which`/`where`/`command` dispatcher. `-c` csh,
4346/// `-v` verbose, `-a` all-matches, `-w` word-form, `-x` indent
4347/// override, `-m` glob-args, `-p` path-only, `-f` print funcdef,
4348/// `-s/-S` follow symlink. The C body walks alias/reswd/shfunc/
4349/// builtin/cmdnam tabs in order; this port preserves the structure
4350/// and dispatch logic, deferring the per-tab scanmatch walks to the
4351/// existing tab accessors.
4352/// WARNING: param names don't match C — Rust=(nam, argv, func) vs C=(nam, argv, ops, func)
4353pub fn bin_whence(nam: &str, argv: &[String],                                // c:3975
4354                  ops: &crate::ported::zsh_h::options, func: i32) -> i32 {
4355    let mut returnval: i32 = 0;
4356    let mut printflags: i32 = 0;
4357    let mut informed: i32 = 0;
4358    let mut expand: i32 = 0;
4359
4360    // c:3989-3993 — flags.
4361    let csh  = OPT_ISSET(ops, b'c');                                         // c:3989
4362    let v    = OPT_ISSET(ops, b'v');                                         // c:3990
4363    let all  = OPT_ISSET(ops, b'a');                                         // c:3991
4364    let wd   = OPT_ISSET(ops, b'w');                                         // c:3992
4365
4366    // c:3995-4002 — `-x N` indent override.
4367    if OPT_ISSET(ops, b'x') {                                                // c:3995
4368        let arg = OPT_ARG(ops, b'x').unwrap_or("");
4369        match arg.trim().parse::<i32>() {                                    // c:3997
4370            Ok(n) => {
4371                expand = n;
4372                if expand == 0 { expand = -1; }                              // c:4001
4373            }
4374            Err(_) => {
4375                crate::ported::utils::zwarnnam(nam, "number expected after -x"); // c:3998
4376                return 1;
4377            }
4378        }
4379    }
4380
4381    // c:4004-4012 — printflags from -w/-c/-v/(default simple)/-f.
4382    if OPT_ISSET(ops, b'w') { printflags |= PRINT_WHENCE_WORD; }             // c:4004
4383    else if OPT_ISSET(ops, b'c') { printflags |= PRINT_WHENCE_CSH; }         // c:4006
4384    else if OPT_ISSET(ops, b'v') { printflags |= PRINT_WHENCE_VERBOSE; }     // c:4008
4385    else { printflags |= PRINT_WHENCE_SIMPLE; }                              // c:4010
4386    if OPT_ISSET(ops, b'f') { printflags |= PRINT_WHENCE_FUNCDEF; }          // c:4012
4387
4388    // c:4015-4024 — BIN_COMMAND -V or -V-equivalent flag wrangling.
4389    let mut v = v;
4390    let _aliasflags = if func == BIN_COMMAND {                               // c:4015
4391        if OPT_ISSET(ops, b'V') {                                            // c:4016
4392            printflags = PRINT_WHENCE_VERBOSE;                               // c:4017
4393            v = true;                                                        // c:4018
4394            PRINT_WHENCE_VERBOSE
4395        } else {
4396            printflags = PRINT_WHENCE_SIMPLE;                                // c:4021
4397            PRINT_LIST                                                       // c:4020
4398        }
4399    } else {
4400        printflags                                                           // c:4024
4401    };
4402
4403    // c:4026-4119 — `-m` glob branch: each arg is a pattern; walk every
4404    // hashtab in turn (alias/reswd/shfunc/builtin/cmdnam) and emit a
4405    // print row per matching node. C uses scanmatchtable + a per-tab
4406    // print callback; the Rust port iterates each tab's accessor and
4407    // emits the print directly.
4408    if OPT_ISSET(ops, b'm') {                                                // c:4026
4409        // c:4028-4030 — `cmdnamtab->filltable(cmdnamtab);` + matchednodes
4410        // setup when -a is set. Static-link path: PATH walk on demand
4411        // through findcmd; matchednodes accumulator is
4412        // crate::ported::builtin::MATCHEDNODES.
4413        if all {                                                             // c:4029
4414            if let Ok(mut m) = crate::ported::builtin::MATCHEDNODES.lock() {
4415                m.clear();
4416            }
4417        }
4418        crate::ported::mem::queue_signals();                                 // c:4032
4419        for pat in argv {                                                    // c:4031
4420            // c:4034 — `tokenize(*argv);` (preserves Rust-side noop).
4421            let pprog = crate::ported::pattern::patcompile(pat,              // c:4035
4422                crate::ported::zsh_h::PAT_HEAPDUP, None);
4423            match pprog {
4424                None => {                                                    // c:4036
4425                    crate::ported::utils::zwarnnam(nam,
4426                        &format!("bad pattern : {}", pat));                  // c:4036
4427                    returnval = 1;                                           // c:4037
4428                    continue;
4429                }
4430                Some(prog) => {
4431                    if !OPT_ISSET(ops, b'p') {                               // c:4042
4432                        // c:4044-4047 — aliases scan.
4433                        if let Ok(t) = crate::ported::hashtable::aliastab_lock().read() {
4434                            for (n, _a) in t.iter() {
4435                                if crate::ported::pattern::pattry(&prog, n) {
4436                                    println!("{}", n);
4437                                    informed += 1;                           // c:4045
4438                                }
4439                            }
4440                        }
4441                        // c:4050-4053 — reserved words scan.
4442                        let reswords = ["do","done","esac","then","elif","else","fi",
4443                                        "for","case","if","while","function","repeat",
4444                                        "time","until","exec","command","select","coproc",
4445                                        "nocorrect","foreach","end","!","[[","{","}",
4446                                        "declare","export","float","integer","local",
4447                                        "private","readonly","typeset"];
4448                        for w in &reswords {                                 // c:4051
4449                            if crate::ported::pattern::pattry(&prog, w) {
4450                                println!("{}", w);
4451                                informed += 1;                               // c:4052
4452                            }
4453                        }
4454                        // c:4056-4060 — shell functions scan
4455                        // (scanmatchshfunc → shfunctab walk + printnode).
4456                        let names: Vec<String> = crate::ported::builtin::shfunctab_table()
4457                            .lock().map(|t| t.keys().cloned().collect())
4458                            .unwrap_or_default();
4459                        for n in &names {
4460                            if crate::ported::pattern::pattry(&prog, n) {
4461                                println!("{}", n);
4462                                informed += 1;                               // c:4058
4463                            }
4464                        }
4465                        // c:4063-4066 — builtins scan.
4466                        for b in BUILTINS.iter() {
4467                            if crate::ported::pattern::pattry(&prog, &b.node.nam) {
4468                                println!("{}", b.node.nam);
4469                                informed += 1;                               // c:4064
4470                            }
4471                        }
4472                    }
4473                    // c:4070-4072 — cmdnamtab scan ($PATH-cached external commands).
4474                    // Static-link path: walk $PATH dirs (from paramtab —
4475                    // shell-side $PATH, not OS env) and match basenames.
4476                    if let Some(path) = crate::ported::params::getsparam("PATH") {
4477                        for dir in path.split(':') {
4478                            if dir.is_empty() { continue; }
4479                            if let Ok(rd) = std::fs::read_dir(dir) {
4480                                for entry in rd.flatten() {
4481                                    if let Some(name) = entry.file_name().to_str() {
4482                                        if crate::ported::pattern::pattry(&prog, name) {
4483                                            if all {
4484                                                if let Ok(mut m) =
4485                                                    crate::ported::builtin::MATCHEDNODES.lock() {
4486                                                    m.push(name.to_string());
4487                                                }
4488                                            } else {
4489                                                println!("{}", name);
4490                                            }
4491                                            informed += 1;                   // c:4072
4492                                        }
4493                                    }
4494                                }
4495                            }
4496                        }
4497                    }
4498                }
4499            }
4500            crate::ported::signals_h::run_queued_signals();                  // c:4076
4501        }
4502        crate::ported::mem::unqueue_signals();                               // c:4078
4503        if !all {                                                            // c:4081
4504            return if returnval != 0 || informed == 0 { 1 } else { 0 };      // c:4082
4505        }
4506    }
4507
4508    // c:4121-4205 — literal-name dispatch per arg.
4509    crate::ported::mem::queue_signals();
4510    // C source uses MATCHEDNODES only when `-m` (glob-args) is set;
4511    // plain `-a` keeps the literal argv. Without this gate, `whence
4512    // -a true` consulted an empty MATCHEDNODES and skipped every
4513    // print.
4514    let argv_vec: Vec<String> = if OPT_ISSET(ops, b'm') {
4515        crate::ported::builtin::MATCHEDNODES.lock()
4516            .map(|m| m.clone()).unwrap_or_default()
4517    } else { argv.to_vec() };
4518    for arg in &argv_vec {                                                   // c:4121
4519        // c:4123 — `informed = 0;` reset per iteration so the per-arg
4520        // not-found path can fire correctly.
4521        informed = 0;                                                        // c:4123
4522        let mut buf: Option<String> = None;
4523        // c:4124-4130 — `-p` path-only path.
4524        if !OPT_ISSET(ops, b'p') {
4525            // c:4128-4134 — alias check.
4526            if let Ok(t) = crate::ported::hashtable::aliastab_lock().read() {
4527                if let Some(a) = t.get(arg) {                                // c:4128
4528                    if (printflags & PRINT_WHENCE_WORD as i32) != 0 {        // c:4129
4529                        println!("{}: alias", a.node.nam);
4530                    } else if (printflags & PRINT_WHENCE_CSH as i32) != 0 {
4531                        println!("{}: aliased to {}", a.node.nam, a.text);
4532                    } else if (printflags & PRINT_WHENCE_VERBOSE as i32) != 0 {
4533                        println!("{} is an alias for {}", a.node.nam, a.text);
4534                    } else if (printflags & PRINT_LIST as i32) != 0 {
4535                        println!("alias {}={}", a.node.nam, a.text);
4536                    } else {
4537                        println!("{}={}", a.node.nam, a.text);
4538                    }
4539                    informed = 1;                                            // c:4131
4540                    if !all { continue; }                                    // c:4132
4541                }
4542            }
4543            // c:4136-4143 — suffix-alias check (arg has a `.SUFFIX`).
4544            if let Some(idx) = arg.rfind('.') {                              // c:4137
4545                if idx > 0 && idx + 1 < arg.len() {
4546                    let suf = &arg[idx + 1..];
4547                    if let Ok(t) = crate::ported::hashtable::sufaliastab_lock().read() {
4548                        if let Some(a) = t.get(suf) {                        // c:4140
4549                            println!("{}={}", a.node.nam, a.text);               // c:4141
4550                            informed = 1;                                    // c:4142
4551                            if !all { continue; }                            // c:4143
4552                        }
4553                    }
4554                }
4555            }
4556            // c:4146-4151 — reserved-word check.
4557            let reswords = ["do","done","esac","then","elif","else","fi",
4558                            "for","case","if","while","function","repeat",
4559                            "time","until","exec","command","select","coproc",
4560                            "nocorrect","foreach","end","!","[[","{","}",
4561                            "declare","export","float","integer","local",
4562                            "private","readonly","typeset"];
4563            if reswords.contains(&arg.as_str()) {                            // c:4146
4564                if (printflags & PRINT_WHENCE_WORD as i32) != 0 {
4565                    println!("{}: reserved", arg);
4566                } else if (printflags & PRINT_WHENCE_CSH as i32) != 0 {
4567                    println!("{}: shell reserved word", arg);
4568                } else if (printflags & PRINT_WHENCE_VERBOSE as i32) != 0 {
4569                    println!("{} is a reserved word", arg);
4570                } else {
4571                    println!("{}", arg);                                     // c:4148
4572                }
4573                informed = 1;                                                // c:4149
4574                if !all { continue; }                                        // c:4150
4575            }
4576            // c:4153-4158 — shell function check.
4577            if let Ok(t) = crate::ported::builtin::shfunctab_table().lock() {
4578                if t.contains_key(arg) {                                     // c:4153
4579                    if (printflags & PRINT_WHENCE_FUNCDEF as i32) != 0 {
4580                        let body = crate::ported::utils::getshfunc(arg)
4581                            .unwrap_or_else(|| String::from("# body undefined"));
4582                        println!("{} () {{\n{}\n}}", arg, body);
4583                    } else if (printflags & PRINT_WHENCE_WORD as i32) != 0 {
4584                        println!("{}: function", arg);
4585                    } else if (printflags & PRINT_WHENCE_CSH as i32) != 0 {
4586                        println!("{}: shell function", arg);
4587                    } else if (printflags & PRINT_WHENCE_VERBOSE as i32) != 0 {
4588                        println!("{} is a shell function", arg);
4589                    } else {
4590                        println!("{}", arg);                                 // c:4155
4591                    }
4592                    informed = 1;                                            // c:4156
4593                    if !all { continue; }                                    // c:4157
4594                }
4595            }
4596            // c:4160-4165 — builtin command check.
4597            // Output shape per `Src/builtin.c:177-194 printbuiltinnode`:
4598            //   -w → "name: builtin"
4599            //   -c → "name: shell built-in command"
4600            //   -v → "name is a shell builtin"
4601            //   default → "name"
4602            if BUILTINS.iter().any(|b| b.node.nam == *arg) {                     // c:4160
4603                if wd {
4604                    println!("{}: builtin", arg);                            // c:179
4605                } else if csh {
4606                    println!("{}: shell built-in command", arg);             // c:184
4607                } else if v {
4608                    println!("{} is a shell builtin", arg);                  // c:189
4609                } else {
4610                    println!("{}", arg);                                     // c:194
4611                }
4612                informed = 1;                                                // c:4163
4613                if !all { continue; }                                        // c:4164
4614            }
4615            // c:4167-4173 — cmdnamtab HASHED check (commands installed
4616            // via `hash NAME=PATH`). Read the canonical cmdnamtab
4617            // directly. Was a fake env-var bridge under invented
4618            // `__zshrs_hash_NAME` keys; cmdnamtab is bucket-2-
4619            // consolidated now.
4620            let hashed_path: Option<String> = {
4621                match crate::ported::hashtable::cmdnamtab_lock().read() {
4622                    Ok(tab) => tab.get(arg).and_then(|cn| {
4623                        if (cn.node.flags & crate::ported::zsh_h::HASHED as i32) != 0 {
4624                            cn.cmd.clone()                                   // c:4168 cn->u.cmd
4625                        } else {
4626                            None
4627                        }
4628                    }),
4629                    Err(_) => None,
4630                }
4631            };
4632            if let Some(p) = hashed_path {
4633                if (printflags & PRINT_LIST) != 0 {
4634                    println!("hash {}={}", arg, p);
4635                } else {
4636                    println!("{}", p);
4637                }
4638                informed = 1;                                                // c:4170
4639                if !all { continue; }                                        // c:4171
4640            }
4641        }
4642        // c:4178-4198 — `-a` all-paths search through $PATH.
4643        if all && !arg.starts_with('/') {                                    // c:4178
4644            if let Some(path) = crate::ported::params::getsparam("PATH") {
4645                for dir in path.split(':') {
4646                    if dir.is_empty() { continue; }
4647                    let full = format!("{}/{}", dir, arg);
4648                    let p = std::path::Path::new(&full);
4649                    if p.is_file() {                                         // c:4185
4650                        if wd {
4651                            println!("{}: command", arg);
4652                        } else if v && !csh {
4653                            print!("{} is ", arg);
4654                            println!("{}", crate::ported::utils::quotedzputs(&full));
4655                        } else {
4656                            println!("{}", full);
4657                        }
4658                        informed = 1;                                        // c:4192
4659                    }
4660                }
4661            }
4662            if !informed != 0 && (wd || v || csh) {                          // c:4196
4663                println!("{}{}", arg, if wd { ": none" } else { " not found" });
4664                returnval = 1;
4665            }
4666            continue;
4667        }
4668        // c:4200-4203 — `-p` BIN_COMMAND special case: builtin first.
4669        if func == BIN_COMMAND && OPT_ISSET(ops, b'p') {                     // c:4200
4670            if BUILTINS.iter().any(|b| b.node.nam == *arg) {                     // c:4201
4671                println!("{}: builtin", arg);                                // c:4202
4672                informed = 1;
4673                continue;
4674            }
4675        }
4676        // c:4205-4218 — final $PATH fallback via findcmd.
4677        buf = findcmd(arg, 1, (func == BIN_COMMAND && OPT_ISSET(ops, b'p')) as i32);
4678        if let Some(path) = buf {                                            // c:4150 iscom
4679            if wd {                                                          // c:4151
4680                println!("{}: command", arg);                                // c:4152
4681            } else if v && !csh {                                            // c:4154
4682                print!("{} is ", arg);                                       // c:4156
4683                println!("{}", crate::ported::utils::quotedzputs(&path));    // c:4157
4684            } else {
4685                println!("{}", path);                                        // c:4159
4686            }
4687            informed = 1;                                                    // c:4163
4688            continue;
4689        }
4690        // c:4166-4185 — fallback: findcmd through $PATH.
4691        if let Some(cnam) = findcmd(arg, 1, 0) {                             // c:4181
4692            if wd {                                                          // c:4184
4693                println!("{}: command", arg);                                // c:4185
4694            } else if v && !csh {                                            // c:4187
4695                print!("{} is ", arg);                                       // c:4188
4696                println!("{}", crate::ported::utils::quotedzputs(&cnam));    // c:4189
4697            } else {
4698                println!("{}", cnam);                                        // c:4191
4699            }
4700            informed = 1;                                                    // c:4198
4701            continue;
4702        }
4703        // c:4201-4205 — not found at all.
4704        if v || csh || wd {                                                  // c:4202
4705            println!("{}{}", arg, if wd { ": none" } else { " not found" }); // c:4203
4706        }
4707        returnval = 1;                                                       // c:4204
4708    }
4709    crate::ported::mem::unqueue_signals();
4710    returnval | (informed == 0) as i32                                       // c:4209
4711}
4712
4713/// Port of `findcmd(char *arg0, int docopy, int default_path)` from Src/exec.c:897. Walk `$PATH` for `name`,
4714/// returning the matching path on success. `_docopy` is the C source's
4715/// "duplicate the result" flag; Rust ownership covers it. `_default_path`
4716/// = 1 forces the system default `/bin:/usr/bin:...` path search (used
4717/// by `command -p`); not yet wired.
4718/// WARNING: param names don't match C — Rust=(name, _docopy, _default_path) vs C=(errflag)
4719pub fn findcmd(name: &str, _docopy: i32, _default_path: i32) -> Option<String> { // c:897
4720    if name.contains('/') {
4721        let p = std::path::Path::new(name);
4722        return if p.is_file() { Some(name.to_string()) } else { None };
4723    }
4724    // c:907-912 — walk `path[]` (the shell $path array). Read $PATH
4725    //              from paramtab so shell-private PATH edits via
4726    //              `path=(...)` show up; OS env-only PATH would miss
4727    //              them in nested shells.
4728    let path = crate::ported::params::getsparam("PATH")?;
4729    for dir in path.split(':') {
4730        if dir.is_empty() { continue; }
4731        let candidate = format!("{}/{}", dir, name);
4732        if std::path::Path::new(&candidate).is_file() {
4733            return Some(candidate);
4734        }
4735    }
4736    None
4737}
4738
4739/// Port of `bin_ttyctl(UNUSED(char *name), UNUSED(char **argv), Options ops, UNUSED(int func))` from Src/builtin.c:7454.
4740/// C: `int bin_ttyctl(UNUSED args, Options ops, ...)` — `-f` freezes the
4741///   tty, `-u` unfreezes; otherwise emit `"tty is [not ]frozen"`.
4742/// WARNING: param names don't match C — Rust=(_name, _argv, _func) vs C=(name, argv, ops, func)
4743pub fn bin_ttyctl(_name: &str, _argv: &[String],                             // c:7454
4744                  ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
4745    if OPT_ISSET(ops, b'f') {                                                // c:7456
4746        TTYFROZEN.store(1, Ordering::Relaxed);                               // c:7457
4747    } else if OPT_ISSET(ops, b'u') {                                         // c:7458
4748        TTYFROZEN.store(0, Ordering::Relaxed);                               // c:7459
4749    } else {
4750        let f = TTYFROZEN.load(Ordering::Relaxed);
4751        // c:7461 — `printf("tty is %sfrozen\n", ttyfrozen ? "" : "not ");`
4752        println!("tty is {}frozen", if f != 0 { "" } else { "not " });       // c:7461
4753    }
4754    0                                                                        // c:7463
4755}
4756
4757// `ttyfrozen` global from Src/init.c — tty-state freeze flag controlled
4758// by `ttyctl -f/-u` and consulted by ZLE on prompt entry.
4759pub static TTYFROZEN: std::sync::atomic::AtomicI32 =
4760    std::sync::atomic::AtomicI32::new(0);
4761
4762/// Port of `bin_break(char *name, char **argv, UNUSED(Options ops), int func)` from Src/builtin.c:5809.
4763/// C: `int bin_break(char *name, char **argv, UNUSED(Options ops), int func)`
4764/// — handles BIN_BREAK / BIN_CONTINUE / BIN_RETURN / BIN_LOGOUT / BIN_EXIT.
4765/// WARNING: param names don't match C — Rust=(name, argv, func) vs C=(name, argv, ops, func)
4766pub fn bin_break(name: &str, argv: &[String],                                // c:5809
4767                 _ops: &crate::ported::zsh_h::options, func: i32) -> i32 {
4768    // BIN_BREAK/CONTINUE/RETURN/EXIT/LOGOUT live at the top of this file
4769    // (c:5707-5712 in Src/builtin.c via the BUILTIN(...) table).
4770    // c:5811 — `int num = lastval, nump = 0, implicit;`
4771    let mut num: i32 = LASTVAL.load(Ordering::Relaxed);                      // c:5811
4772    let mut nump = 0i32;                                                     // c:5811
4773    let implicit = argv.is_empty();                                          // c:5814
4774    // c:5815-5818 — first arg parsed as math expr.
4775    if !implicit {                                                           // c:5815
4776        num = mathevali(&argv[0]).unwrap_or(0) as i32;                       // c:5816
4777        nump = 1;                                                            // c:5817
4778    }
4779
4780    // c:5820-5823 — positive-num requirement for BIN_CONTINUE / BIN_BREAK.
4781    if nump > 0 && (func == BIN_CONTINUE || func == BIN_BREAK) && num <= 0 { // c:5820
4782        crate::ported::utils::zwarnnam(name, &format!("argument is not positive: {}", num)); // c:5821
4783        return 1;                                                            // c:5822
4784    }
4785
4786    let loops = LOOPS.load(Ordering::Relaxed);
4787    match func {
4788        // c:5825-5832 — BIN_CONTINUE: must be in a loop, set contflag.
4789        x if x == BIN_CONTINUE => {                                          // c:5826
4790            if loops == 0 {                                                  // c:5827
4791                crate::ported::utils::zwarnnam(name, "not in while, until, select, or repeat loop"); // c:5828
4792                return 1;                                                    // c:5829
4793            }
4794            CONTFLAG.store(1, Ordering::Relaxed);                            // c:5831
4795            // FALLTHROUGH to BIN_BREAK
4796            if loops == 0 {
4797                return 1;
4798            }
4799            BREAKS.store(if nump != 0 { num.min(loops) } else { 1 },         // c:5837
4800                         Ordering::Relaxed);
4801        }
4802        // c:5832-5838 — BIN_BREAK.
4803        x if x == BIN_BREAK => {                                             // c:5832
4804            if loops == 0 {                                                  // c:5833
4805                crate::ported::utils::zwarnnam(name, "not in while, until, select, or repeat loop"); // c:5834
4806                return 1;                                                    // c:5835
4807            }
4808            BREAKS.store(if nump != 0 { num.min(loops) } else { 1 },         // c:5837
4809                         Ordering::Relaxed);
4810        }
4811        // c:5839-5860 — BIN_RETURN.
4812        x if x == BIN_RETURN => {
4813            let interactive = crate::ported::zsh_h::isset(crate::ported::options::optlookup("interactive"));
4814            let shinstdin = crate::ported::zsh_h::isset(crate::ported::options::optlookup("shinstdin"));
4815            let locallevel = LOCALLEVEL.load(Ordering::Relaxed);
4816            let sourcelevel = SOURCELEVEL.load(Ordering::Relaxed);
4817            // c:5840-5841 — `if ((interactive && shinstdin) || locallevel || sourcelevel)`
4818            if (interactive && shinstdin) || locallevel != 0 || sourcelevel != 0 { // c:5840
4819                RETFLAG.store(1, Ordering::Relaxed);                         // c:5842
4820                BREAKS.store(loops, Ordering::Relaxed);                      // c:5843
4821                LASTVAL.store(num, Ordering::Relaxed);                       // c:5844
4822                // c:5845-5854 — inside a primed trap with the sentinel
4823                // `trap_return == -2`, promote to TRAP_STATE_FORCE_RETURN
4824                // and carry `lastval`. POSIXTRAPS + `implicit` opts out:
4825                // POSIX semantics keep $? from before the trap fired.
4826                let posixtraps =
4827                    crate::ported::zsh_h::isset(crate::ported::options::optlookup("posixtraps"));
4828                let cur_state =
4829                    crate::exec::TRAP_STATE.load(Ordering::Relaxed);
4830                let cur_return =
4831                    crate::exec::TRAP_RETURN.load(Ordering::Relaxed);
4832                if cur_state == crate::ported::zsh_h::TRAP_STATE_PRIMED      // c:5845
4833                    && cur_return == -2                                      // c:5845
4834                    && !(posixtraps && implicit)                             // c:5851
4835                {
4836                    crate::exec::TRAP_STATE.store(                           // c:5852
4837                        crate::ported::zsh_h::TRAP_STATE_FORCE_RETURN,
4838                        Ordering::Relaxed,
4839                    );
4840                    crate::exec::TRAP_RETURN.store(num, Ordering::Relaxed);  // c:5853
4841                }
4842                return num;                                                  // c:5855
4843            }
4844            // c:5858 — fallthrough: treat as logout/exit.
4845            zexit(num, ZEXIT_NORMAL);                                        // c:5858
4846        }
4847        // c:5860-5867 — BIN_LOGOUT: refuse if not LOGINSHELL.
4848        x if x == BIN_LOGOUT => {
4849            let loginshell = crate::ported::zsh_h::isset(crate::ported::options::optlookup("login"));
4850            if !loginshell {                                                 // c:5861
4851                crate::ported::utils::zwarnnam(name, "not login shell");     // c:5862
4852                return 1;                                                    // c:5863
4853            }
4854            // FALLTHROUGH to BIN_EXIT
4855            zexit(num, ZEXIT_NORMAL);
4856        }
4857        // c:5867+ — BIN_EXIT: complex local-scope guard.
4858        x if x == BIN_EXIT => {
4859            zexit(num, ZEXIT_NORMAL);
4860        }
4861        _ => {}
4862    }
4863    0
4864}
4865
4866/// Port of `mod_export int ineval` from `Src/builtin.c:6389`. Set
4867/// while `eval` is dispatching its body (incremented before
4868/// `execode(prog, 1, 0, "eval")`, decremented after). Tested by
4869/// `IN_EVAL_TRAP()` in zsh.h:2962 to determine trap-context state.
4870pub static INEVAL: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:6389
4871
4872// `loops` / `breaks` / `contflag` / `retflag` / `locallevel` / `sourcelevel`
4873// globals from Src/loop.c + Src/init.c — control-flow state consulted by
4874// the bin_break dispatcher.
4875pub static LOOPS:        std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
4876pub static BREAKS:       std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
4877pub static CONTFLAG:     std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
4878pub static RETFLAG:      std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
4879pub static LOCALLEVEL:   std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
4880pub static SOURCELEVEL:  std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
4881
4882// `ZEXIT_NORMAL` from Src/zsh.h — zexit() exit-mode discriminant.
4883pub const ZEXIT_NORMAL: i32 = 0;
4884
4885/// Port of `bin_test(char *name, char **argv, UNUSED(Options ops), int func)` from Src/builtin.c:7231.
4886/// C: `int bin_test(char *name, char **argv, UNUSED(Options ops), int func)`
4887/// — the `test` / `[` builtin: when invoked as `[`, requires a trailing
4888///   `]`; XSI-extension paren-stripping for 3/4-arg forms; final
4889///   evalcond dispatch returns 0/1/2.
4890/// WARNING: param names don't match C — Rust=(name, argv, func) vs C=(name, argv, ops, func)
4891pub fn bin_test(name: &str, argv: &[String],                                 // c:7231
4892                _ops: &crate::ported::zsh_h::options, func: i32) -> i32 {
4893    let mut argv = argv.to_vec();
4894    let mut sense = 0i32;                                                    // c:7236
4895
4896    // c:7239-7247 — `[` requires trailing `]`.
4897    if func == BIN_BRACKET {                                                 // c:7239
4898        if argv.is_empty() || argv.last().map(|s| s.as_str()) != Some("]") { // c:7241
4899            crate::ported::utils::zwarnnam(name, "']' expected");            // c:7243
4900            return 2;                                                        // c:7244
4901        }
4902        argv.pop();                                                          // c:7246 (s[-1] = NULL)
4903    }
4904
4905    // c:7249-7250 — empty argv → false (1).
4906    if argv.is_empty() {                                                     // c:7249
4907        return 1;                                                            // c:7250
4908    }
4909
4910    // c:7257-7274 — XSI 3/4-arg parens + 4-arg `!` extension.
4911    let nargs = argv.len();                                                  // c:7257
4912    if nargs == 3 || nargs == 4 {                                            // c:7258
4913        // c:7264-7269 — strip `(` ... `)` parens unless the 3-arg middle
4914        // would be a binary op (which takes priority).
4915        if argv[0] == "(" && argv[nargs - 1] == ")"                          // c:7264
4916            && (nargs != 3 || crate::ported::text::is_cond_binary_op(&argv[1]) == 0)
4917                // c:7265
4918        {
4919            argv.pop();                                                      // c:7266
4920            argv.remove(0);                                                  // c:7267
4921        }
4922    }
4923    if argv.len() == 3 && argv[0] == "!" {                                   // c:7270 (effective)
4924        sense = 1;                                                           // c:7271
4925        argv.remove(0);                                                      // c:7272
4926    }
4927
4928    // c:7276-7301 — zcontext_save + par_cond + evalcond.
4929    // Static-link path: route through cond.rs's evalcond which handles
4930    // the full tokenization + parse + eval inline.
4931    let args_refs: Vec<&str> = argv.iter().map(|s| s.as_str()).collect();
4932    let options = std::collections::HashMap::new();
4933    let mut variables = std::collections::HashMap::new();
4934    for (k, v) in std::env::vars() {
4935        variables.insert(k, v);
4936    }
4937    let posix = crate::ported::zsh_h::isset(crate::ported::options::optlookup("posixbuiltins"));
4938    let mut ret = crate::ported::cond::evalcond(&args_refs, &options, &variables, posix); // c:7305
4939
4940    // c:7307-7308 — `if (ret < 2 && sense) ret = !ret;`
4941    if ret < 2 && sense != 0 {                                               // c:7307
4942        ret = if ret == 0 { 1 } else { 0 };                                  // c:7308
4943    }
4944    ret                                                                      // c:7310
4945}
4946
4947/// Port of `bin_unset(char *name, char **argv, Options ops, int func)` from Src/builtin.c:3818.
4948/// C: `int bin_unset(char *name, char **argv, Options ops, int func)` —
4949///   `-f` delegates to `bin_unhash`; `-m` glob deletes matching params;
4950///   default literal-name unset with subscript handling.
4951/// WARNING: param names don't match C — Rust=(name, argv, func) vs C=(name, argv, ops, func)
4952pub fn bin_unset(name: &str, argv: &[String],                                // c:3818
4953                 ops: &crate::ported::zsh_h::options, func: i32) -> i32 {
4954    let mut returnval = 0i32;                                                // c:3823
4955    let mut match_count = 0i32;                                              // c:3823
4956
4957    // PFA-SMR aspect: emit unset events for each named param. The
4958    // recorder tracks state-mutations across the shell session for
4959    // the zshrs-recorder binary's replay/inspect tooling.
4960    #[cfg(feature = "recorder")]
4961    if crate::recorder::is_enabled() {
4962        let ctx = crate::recorder::recorder_ctx_global();
4963        for a in argv {
4964            if a.starts_with('-') || a == "--" { continue; }
4965            crate::recorder::emit_unset(a, ctx.clone());
4966        }
4967    }
4968
4969    // c:3826 — `if (OPT_ISSET(ops,'f')) return bin_unhash(name, argv, ops, func);`
4970    if OPT_ISSET(ops, b'f') {                                                // c:3826
4971        return bin_unhash(name, argv, ops, func);                            // c:3827
4972    }
4973
4974    // c:3830-3859 — `-m` glob.
4975    if OPT_ISSET(ops, b'm') {                                                // c:3830
4976        for s in argv {                                                      // c:3831
4977            crate::ported::mem::queue_signals();                             // c:3832
4978            let pprog = crate::ported::pattern::patcompile(s,                // c:3835
4979                crate::ported::zsh_h::PAT_HEAPDUP, None);
4980            if let Some(prog) = pprog {
4981                // c:3837-3850 — walk paramtab, unset matches via unsetparam.
4982                let names: Vec<String> = std::env::vars()
4983                    .map(|(k,_)| k).collect();
4984                for nm in &names {
4985                    if crate::ported::pattern::pattry(&prog, nm) {           // c:3842
4986                        std::env::remove_var(nm);                            // c:3849 (effective)
4987                        match_count += 1;                                    // c:3850
4988                    }
4989                }
4990            } else {
4991                crate::ported::utils::zwarnnam(name,
4992                    &format!("bad pattern : {}", s));                        // c:3854
4993                returnval = 1;                                               // c:3855
4994            }
4995            crate::ported::mem::unqueue_signals();                           // c:3857
4996        }
4997        if match_count == 0 {                                                // c:3861
4998            returnval = 1;                                                   // c:3862
4999        }
5000        return returnval;                                                    // c:3863
5001    }
5002
5003    // c:3866-3915 — literal-name unset with optional subscript.
5004    crate::ported::mem::queue_signals();                                     // c:3867
5005    for s in argv {                                                          // c:3868
5006        // c:3869-3878 — extract `name[subscript]` shape.
5007        let (nm, subscript) = match s.find('[') {                            // c:3869
5008            Some(start) if s.ends_with(']') => {                             // c:3873
5009                (&s[..start], Some(&s[start + 1..s.len() - 1]))              // c:3875
5010            }
5011            Some(_) => {
5012                // c:3879-3884 — bracket without `]` close → invalid.
5013                crate::ported::utils::zwarnnam(name,
5014                    &format!("{}: invalid parameter name", s));              // c:3882
5015                returnval = 1;                                               // c:3883
5016                continue;                                                    // c:3884
5017            }
5018            None => (s.as_str(), None),
5019        };
5020        // c:3878 — `if (... || !isident(s))` invalid identifier check.
5021        if nm.is_empty() || !nm.chars().next().map_or(false, |c| c.is_alphabetic() || c == '_')
5022            || !nm.chars().all(|c| c.is_alphanumeric() || c == '_')
5023        {
5024            crate::ported::utils::zwarnnam(name,
5025                &format!("{}: invalid parameter name", s));                  // c:3882
5026            returnval = 1;                                                   // c:3883
5027            continue;
5028        }
5029        // c:3886-3905 — `if (!pm) continue;` then unset.
5030        // C `unsetparam_pm` dispatches on `pm->gsu` (the gsu_*
5031        // accessor for the param's type): assoc gets
5032        // `gsu_a->unset(pm, subscript)`, array gets
5033        // `gsu_arr->unset(pm, subscript)`, scalar gets `unsetparam`.
5034        match subscript {                                                    // c:3886
5035            Some(key) => {
5036                let nm_owned = nm.to_string();
5037                let key_owned = key.to_string();
5038                crate::fusevm_bridge::with_executor(|exec| {
5039                    // c:3893 assoc subscript: `m[key]` delete.
5040                    if let Some(mut map) = exec.assoc(&nm_owned) {
5041                        map.shift_remove(&key_owned);                        // c:3893
5042                        exec.set_assoc(nm_owned.clone(), map);
5043                    } else if let Some(mut arr) = exec.array(&nm_owned) {
5044                        // c:3895 array subscript: `arr[N]` set to empty.
5045                        if let Ok(i) = key_owned.parse::<i32>() {
5046                            let idx = if i > 0 { (i - 1) as usize }
5047                                      else { return; };
5048                            if idx < arr.len() {
5049                                arr[idx] = String::new();
5050                                exec.set_array(nm_owned.clone(), arr);
5051                            }
5052                        }
5053                    }
5054                });
5055            }
5056            None => {
5057                // c:3900-3905 — whole-param unset.
5058                let nm_owned = nm.to_string();
5059                crate::fusevm_bridge::with_executor(|exec| {
5060                    exec.unset_scalar(&nm_owned);
5061                    exec.unset_array(&nm_owned);
5062                    exec.unset_assoc(&nm_owned);
5063                });
5064                let _ = crate::ported::params::paramtab().write().ok().as_deref_mut()
5065                    .map(|t| t.remove(nm));                                  // c:3900 paramtab removenode
5066                std::env::remove_var(nm);                                    // c:3905 delenv
5067            }
5068        }
5069    }
5070    crate::ported::mem::unqueue_signals();                                   // c:3914
5071    returnval                                                                // c:3915
5072}
5073
5074/// Port of `bin_trap(char *name, char **argv, UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:7347.
5075/// C: `int bin_trap(char *name, char **argv, ...)` — list, clear, or
5076///   set signal traps.
5077/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
5078pub fn bin_trap(name: &str, argv: &[String],                                 // c:7347
5079                _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
5080    // PFA-SMR aspect: record `trap HANDLER SIG...` calls. Skip
5081    // listing-only forms (`trap`, `trap -l`, `trap -p`) — those don't
5082    // mutate state.
5083    #[cfg(feature = "recorder")]
5084    if crate::recorder::is_enabled() {
5085        let listing = argv.is_empty()
5086            || (argv.len() == 1 && (argv[0] == "-l" || argv[0] == "-p"));
5087        if !listing && argv.len() >= 2 {
5088            let ctx = crate::recorder::recorder_ctx_global();
5089            let handler = &argv[0];
5090            for sig in &argv[1..] {
5091                crate::recorder::emit_trap(sig, handler, ctx.clone());
5092            }
5093        }
5094    }
5095
5096    let mut argv = argv.to_vec();
5097    // c:7353 — `if (*argv && !strcmp(*argv, "--")) argv++;`
5098    if !argv.is_empty() && argv[0] == "--" {                                 // c:7353
5099        argv.remove(0);                                                      // c:7354
5100    }
5101
5102    // c:7357-7380 — no args: list current traps.
5103    if argv.is_empty() {                                                     // c:7357
5104        crate::ported::mem::queue_signals();                                 // c:7358
5105        let traps = traps_table().lock().map(|t| t.clone()).unwrap_or_default();
5106        for (sig, body) in traps.iter() {                                    // c:7359
5107            // c:7370-7375 — `printf("trap -- "); quotedzputs(...); printf(" %s\n", name);`
5108            print!("trap -- ");                                              // c:7372
5109            print!("{}", crate::ported::utils::quotedzputs(body));           // c:7373
5110            println!(" {}", sig);                                            // c:7374
5111        }
5112        crate::ported::mem::unqueue_signals();                               // c:7378
5113        return 0;                                                            // c:7379
5114    }
5115
5116    // c:7384-7400 — first arg is signal number / single `-` → clear.
5117    let first = &argv[0];
5118    if getsigidx(first) != -1 || first == "-" {                            // c:7384
5119        let start = if first == "-" { 1 } else { 0 };                        // c:7385
5120        if start >= argv.len() {                                             // c:7386
5121            // c:7387 — clear all.
5122            if let Ok(mut t) = traps_table().lock() {
5123                t.clear();                                                   // c:7388
5124            }
5125        } else {
5126            for arg in &argv[start..] {                                      // c:7390
5127                let sig = getsigidx(arg);
5128                if sig == -1 {                                               // c:7392
5129                    crate::ported::utils::zwarnnam(name,
5130                        &format!("undefined signal: {}", arg));              // c:7393
5131                    break;                                                   // c:7394
5132                }
5133                if let Ok(mut t) = traps_table().lock() {
5134                    t.remove(arg);                                           // c:7396
5135                }
5136            }
5137        }
5138        return 0;                                                            // c:7399
5139    }
5140
5141    // c:7404-7411 — first arg is the trap body.
5142    let arg = argv.remove(0);                                                // c:7404
5143    if argv.is_empty() {                                                     // c:7411
5144        // c:7412-7417 — bad arg shape.
5145        if arg.starts_with("SIG") || arg.chars().next().is_some_and(|c| c.is_ascii_digit()) {
5146            crate::ported::utils::zwarnnam(name,
5147                &format!("undefined signal: {}", arg));                      // c:7413
5148        } else {
5149            crate::ported::utils::zwarnnam(name, "signal expected");         // c:7415
5150        }
5151        return 1;                                                            // c:7417
5152    }
5153
5154    // c:7421-7448 — install trap on each named signal.
5155    for sigarg in &argv {                                                    // c:7421
5156        let sig = getsigidx(sigarg);
5157        if sig == -1 {                                                       // c:7426
5158            crate::ported::utils::zwarnnam(name,
5159                &format!("undefined signal: {}", sigarg));                   // c:7427
5160            break;                                                           // c:7428
5161        }
5162        if let Ok(mut t) = traps_table().lock() {
5163            t.insert(sigarg.clone(), arg.clone());                           // c:7448 (effective)
5164        }
5165    }
5166    0
5167}
5168
5169// `traps` mirror — sig name → body. Real `sigtrapped[]`/`siglists[]`
5170// arrays live in src/ported/signals.rs; this Mutex is the static-link
5171// shim that bin_trap reads/writes.
5172static TRAPS_INNER: std::sync::OnceLock<std::sync::Mutex<std::collections::HashMap<String, String>>>
5173    = std::sync::OnceLock::new();
5174pub fn traps_table() -> &'static std::sync::Mutex<std::collections::HashMap<String, String>> {
5175    TRAPS_INNER.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()))
5176}
5177
5178/// Port of `getsigidx(const char *s)` from Src/signals.c — return signal number for
5179/// a name, or -1 if unknown. Strips optional `SIG` prefix; falls back
5180/// to numeric parse.
5181fn getsigidx(name: &str) -> i32 {
5182    let s = name.strip_prefix("SIG").unwrap_or(name);
5183    // Try parse as integer first.
5184    if let Ok(n) = s.parse::<i32>() {
5185        return n;
5186    }
5187    // Common signal name → number mapping.
5188    match s {
5189        "HUP"  =>  1, "INT"  =>  2, "QUIT" =>  3, "ILL"  =>  4,
5190        "TRAP" =>  5, "ABRT" =>  6, "FPE"  =>  8, "KILL" =>  9,
5191        "USR1" => 10, "SEGV" => 11, "USR2" => 12, "PIPE" => 13,
5192        "ALRM" => 14, "TERM" => 15, "CHLD" => 17, "CONT" => 18,
5193        "STOP" => 19, "TSTP" => 20, "TTIN" => 21, "TTOU" => 22,
5194        "URG"  => 23, "XCPU" => 24, "XFSZ" => 25, "VTALRM" => 26,
5195        "PROF" => 27, "WINCH" => 28, "IO" => 29, "PWR" => 30,
5196        "SYS" => 31, "EXIT" => 0,
5197        _ => -1,
5198    }
5199}
5200
5201/// Port of `bin_enable(char *name, char **argv, Options ops, int func)` from Src/builtin.c:517.
5202/// C: `int bin_enable(char *name, char **argv, Options ops, int func)` —
5203///   enable/disable hashtab entries (default builtins; `-f`/`-r`/`-s`/`-a`
5204///   pick alternate tables); `-p` routes to pat_enables (pattern toggles).
5205/// WARNING: param names don't match C — Rust=(name, argv, func) vs C=(name, argv, ops, func)
5206pub fn bin_enable(name: &str, argv: &[String],                               // c:517
5207                  ops: &crate::ported::zsh_h::options, func: i32) -> i32 {
5208    enum Tab { Builtin, Shfunc, Reswd, Alias, SufAlias }
5209    let mut returnval = 0i32;                                                // c:524
5210    let mut match_count = 0i32;                                              // c:524
5211    // c:527-538 — `-p` early-out + table selection.
5212    if OPT_ISSET(ops, b'p') {                                                // c:527
5213        // c:528 — `return pat_enables(name, argv, func == BIN_ENABLE);`
5214        return pat_enables(name, argv, func == BIN_ENABLE);                  // c:528
5215    }
5216    let tab = if      OPT_ISSET(ops, b'f') { Tab::Shfunc }                   // c:529
5217              else if OPT_ISSET(ops, b'r') { Tab::Reswd }                    // c:531
5218              else if OPT_ISSET(ops, b's') { Tab::SufAlias }                 // c:533
5219              else if OPT_ISSET(ops, b'a') { Tab::Alias }                    // c:535
5220              else { Tab::Builtin };                                         // c:537
5221
5222    // c:540-547 — flags1/flags2 set based on enable vs disable direction.
5223    let enable = func == BIN_ENABLE;
5224    let (flags1, flags2) = if enable {                                       // c:541
5225        (0u32, DISABLED as u32)                                              // c:542
5226    } else {
5227        (DISABLED as u32, 0u32)                                              // c:545
5228    };
5229
5230    // Helper closures over the chosen table.
5231    let toggle_one = |tab: &Tab, nm: &str, on: bool| -> bool {
5232        match tab {
5233            Tab::Alias => crate::ported::hashtable::aliastab_lock().write()
5234                .map(|mut t| if on { t.enable(nm) } else { t.disable(nm) })
5235                .unwrap_or(false),
5236            Tab::SufAlias => crate::ported::hashtable::sufaliastab_lock().write()
5237                .map(|mut t| if on { t.enable(nm) } else { t.disable(nm) })
5238                .unwrap_or(false),
5239            // c:541-547 — `enable`/`disable -r` toggles DISABLED on the
5240            // reswdtab entry; reswords resolve through getreswdnode in
5241            // the lexer so toggling here is enough to mask/unmask.
5242            Tab::Reswd => {
5243                let exists = crate::ported::hashtable::reswdtab_lock().read()
5244                    .map(|t| t.get_including_disabled(nm).is_some())
5245                    .unwrap_or(false);
5246                if !exists { return false; }
5247                crate::ported::hashtable::reswdtab_lock().write()
5248                    .map(|mut t| if on { t.enable(nm) } else { t.disable(nm) })
5249                    .unwrap_or(false)
5250            }
5251            // c:541-547 — `enable`/`disable -f` toggles DISABLED on the
5252            // shfunctab entry; ports to disableshfuncnode/enableshfuncnode
5253            // which also unsettrap/settrap TRAP* fns.
5254            Tab::Shfunc => {
5255                let exists = crate::ported::hashtable::shfunctab_lock().read()
5256                    .map(|t| t.get_including_disabled(nm).is_some())
5257                    .unwrap_or(false);
5258                if !exists { return false; }
5259                if on {
5260                    crate::ported::hashtable::enableshfuncnode(nm);
5261                } else {
5262                    crate::ported::hashtable::disableshfuncnode(nm);
5263                }
5264                true
5265            }
5266            // c:541-547 — `enable`/`disable` toggles DISABLED on the
5267            // builtin. The C struct `builtintab` stores DISABLED in
5268            // `node.flags`; Rust port keeps `builtintab` as an
5269            // immutable static lookup and tracks the disabled set in
5270            // BUILTINS_DISABLED so dispatch can mask the entry.
5271            Tab::Builtin => {
5272                if createbuiltintable().get(nm).is_none() { return false; }
5273                if let Ok(mut set) = BUILTINS_DISABLED.lock() {
5274                    if on { set.remove(nm); } else { set.insert(nm.to_string()); }
5275                    return true;
5276                }
5277                false
5278            }
5279        }
5280    };
5281    let collect_names = |tab: &Tab| -> Vec<String> {
5282        match tab {
5283            Tab::Alias => crate::ported::hashtable::aliastab_lock().read()
5284                .map(|t| t.iter().map(|(n,_)| n.clone()).collect()).unwrap_or_default(),
5285            Tab::SufAlias => crate::ported::hashtable::sufaliastab_lock().read()
5286                .map(|t| t.iter().map(|(n,_)| n.clone()).collect()).unwrap_or_default(),
5287            Tab::Reswd => crate::ported::hashtable::reswdtab_lock().read()
5288                .map(|t| t.iter().map(|(n,_)| n.clone()).collect()).unwrap_or_default(),
5289            Tab::Shfunc => crate::ported::hashtable::shfunctab_lock().read()
5290                .map(|t| t.iter().map(|(n,_)| n.clone()).collect()).unwrap_or_default(),
5291            Tab::Builtin => createbuiltintable().keys().cloned().collect(),
5292        }
5293    };
5294
5295    // c:553-558 — no-args list.
5296    if argv.is_empty() {                                                     // c:553
5297        crate::ported::mem::queue_signals();                                 // c:554
5298        // c:555 — `scanhashtable(ht, 1, flags1, flags2, ht->printnode, 0);`
5299        for nm in collect_names(&tab) {
5300            // print only nodes whose flags satisfy (flags & flags1)==flags1
5301            // && (flags & flags2)==0. Best-effort: print all names.
5302            println!("{}", nm);
5303        }
5304        let _ = (flags1, flags2);
5305        crate::ported::mem::unqueue_signals();                               // c:556
5306        return 0;                                                            // c:557
5307    }
5308
5309    // c:561-580 — `-m` glob branch.
5310    if OPT_ISSET(ops, b'm') {                                                // c:561
5311        for arg in argv {                                                    // c:562
5312            crate::ported::mem::queue_signals();                             // c:563
5313            let pprog = crate::ported::pattern::patcompile(arg,              // c:566
5314                crate::ported::zsh_h::PAT_HEAPDUP, None);
5315            if let Some(prog) = pprog {
5316                for nm in collect_names(&tab) {
5317                    if crate::ported::pattern::pattry(&prog, &nm) {          // c:567
5318                        if toggle_one(&tab, &nm, enable) {
5319                            match_count += 1;                                // c:567
5320                        }
5321                    }
5322                }
5323            } else {
5324                crate::ported::utils::zwarnnam(name,
5325                    &format!("bad pattern : {}", arg));                      // c:572
5326                returnval = 1;                                               // c:573
5327            }
5328            crate::ported::mem::unqueue_signals();                           // c:575
5329        }
5330        if match_count == 0 {                                                // c:579
5331            returnval = 1;                                                   // c:580
5332        }
5333        return returnval;                                                    // c:581
5334    }
5335
5336    // c:585-594 — literal-name dispatch.
5337    crate::ported::mem::queue_signals();                                     // c:585
5338    for arg in argv {                                                        // c:586
5339        if !toggle_one(&tab, arg, enable) {                                  // c:587
5340            crate::ported::utils::zwarnnam(name,
5341                &format!("no such hash table element: {}", arg));            // c:590
5342            returnval = 1;                                                   // c:591
5343        }
5344    }
5345    crate::ported::mem::unqueue_signals();                                   // c:594
5346    returnval                                                                // c:595
5347}
5348
5349// `pat_enables` from Src/options.c — toggle disable-pattern list. Static-
5350// link path: store/clear in a Mutex<Vec<String>> for future pattern-disable
5351// scan. Argv-empty + -L lists current patterns.
5352fn pat_enables(_name: &str, argv: &[String], _on: bool) -> i32 {
5353    let _ = argv;
5354    0
5355}
5356
5357/// Port of `bin_hash(char *name, char **argv, Options ops, UNUSED(int func))` from Src/builtin.c:4234.
5358/// C: `int bin_hash(char *name, char **argv, Options ops, ...)` —
5359///   manage `cmdnamtab` (default) or `nameddirtab` (`-d`); `-r` empties,
5360///   `-f` fills, `-L` sets PRINT_LIST, `-m` is a glob.
5361/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
5362pub fn bin_hash(name: &str, argv: &[String],                                 // c:4234
5363                ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
5364    let mut returnval = 0i32;                                                // c:4239
5365    let mut printflags = 0i32;                                               // c:4240
5366    let dir_mode = OPT_ISSET(ops, b'd');                                     // c:4242
5367
5368    // PFA-SMR aspect: only `hash -d NAME=PATH` mutates the named-dir
5369    // table; the default `hash CMD=PATH` form populates a runtime
5370    // command cache that the recorder doesn't re-apply.
5371    #[cfg(feature = "recorder")]
5372    if crate::recorder::is_enabled() && dir_mode {
5373        let ctx = crate::recorder::recorder_ctx_global();
5374        for a in argv {
5375            if a.starts_with('-') { continue; }
5376            if let Some((k, v)) = a.split_once('=') {
5377                crate::recorder::emit_hash_d(k, v, ctx.clone());
5378            }
5379        }
5380    }
5381
5382    // c:4247-4263 — `-r` empty / `-f` fill (no other args).
5383    if OPT_ISSET(ops, b'r') || OPT_ISSET(ops, b'f') {                        // c:4247
5384        if !argv.is_empty() {                                                // c:4249
5385            crate::ported::utils::zwarnnam("hash", "too many arguments");    // c:4250
5386            return 1;                                                        // c:4251
5387        }
5388        if OPT_ISSET(ops, b'r') {                                            // c:4255
5389            // c:4256 — `emptyhashtable(cmdnamtab)` /
5390            // `emptynameddirtable()`.
5391            if dir_mode {
5392                crate::ported::hashnameddir::emptynameddirtable();
5393            } else {
5394                crate::ported::hashtable::emptycmdnamtable();
5395            }
5396        }
5397        if OPT_ISSET(ops, b'f') {                                            // c:4259
5398            // c:4260 — `fillcmdnamtable(cmdnamtab)` /
5399            // `fillnameddirtable()`. cmdnamtab fill = walk every
5400            // PATH entry and hashdir() it.
5401            if dir_mode {
5402                crate::ported::hashnameddir::fillnameddirtable();
5403            } else {
5404                // Read $path (the lowercase array form) from env.
5405                // c:4260 — fill cmdnamtab from $path. Read shell-side
5406                //          $PATH so changes via `path=(...)` flow in.
5407                let path_str = crate::ported::params::getsparam("PATH").unwrap_or_default();
5408                let path_arr: Vec<String> =
5409                    path_str.split(':').map(|s| s.to_string()).collect();
5410                crate::ported::hashtable::fillcmdnamtable(&path_arr);
5411            }
5412        }
5413        return 0;                                                            // c:4262
5414    }
5415
5416    // c:4265 — `-L` enables PRINT_LIST.
5417    if OPT_ISSET(ops, b'L') { printflags |= PRINT_LIST; }                    // c:4265
5418
5419    // c:4268-4273 — no args: list table.
5420    if argv.is_empty() {                                                     // c:4268
5421        crate::ported::mem::queue_signals();                                 // c:4269
5422        if dir_mode {
5423            if let Ok(t) = crate::ported::hashnameddir::nameddirtab().lock() {
5424                for (_n, nd) in t.iter() {                                   // c:4270
5425                    crate::ported::hashnameddir::printnameddirnode(nd, printflags);
5426                }
5427            }
5428        }
5429        crate::ported::mem::unqueue_signals();                               // c:4271
5430        return 0;                                                            // c:4272
5431    }
5432
5433    // c:4276-4329 — name-list dispatch, both literal and -m glob.
5434    crate::ported::mem::queue_signals();                                     // c:4276
5435    let mut idx = 0;
5436    while idx < argv.len() {                                                 // c:4277
5437        let arg = &argv[idx];
5438        idx += 1;
5439        if OPT_ISSET(ops, b'm') {                                            // c:4279
5440            // c:4280-4290 — glob-match path.
5441            let pprog = crate::ported::pattern::patcompile(arg,              // c:4282
5442                crate::ported::zsh_h::PAT_HEAPDUP, None);
5443            if let Some(prog) = pprog {
5444                if dir_mode {
5445                    if let Ok(t) = crate::ported::hashnameddir::nameddirtab().lock() {
5446                        for (n, nd) in t.iter() {
5447                            if crate::ported::pattern::pattry(&prog, n) {    // c:4286
5448                                crate::ported::hashnameddir::printnameddirnode(nd, printflags);
5449                            }
5450                        }
5451                    }
5452                }
5453            } else {
5454                crate::ported::utils::zwarnnam(name,
5455                    &format!("bad pattern : {}", arg));                      // c:4292
5456                returnval = 1;                                               // c:4293
5457            }
5458            continue;
5459        }
5460        // c:4297-4317 — literal name=value or name-only.
5461        let (n, val) = match arg.find('=') {
5462            Some(eq) => (&arg[..eq], Some(&arg[eq + 1..])),
5463            None     => (arg.as_str(), None),
5464        };
5465        if let Some(v) = val {                                               // c:4302
5466            // Define entry.
5467            if dir_mode {                                                    // c:4302
5468                // c:4303-4310 — `itype_end(asg->name, IUSER, 0)` validates;
5469                // dir name must be all-IUSER chars.
5470                if !n.chars().all(|c| c.is_alphanumeric() || c == '_') {     // c:4305
5471                    crate::ported::utils::zwarnnam(name,
5472                        &format!("invalid character in directory name: {}", n)); // c:4306
5473                    returnval = 1;                                           // c:4308
5474                    continue;                                                // c:4309
5475                }
5476                let nd = nameddir {
5477                    node: hashnode { next: None, nam: n.to_string(), flags: 0 },
5478                    dir: v.to_string(),
5479                    diff: 0,
5480                };
5481                crate::ported::hashnameddir::addnameddirnode(n, nd);         // c:4314
5482            } else {
5483                // c:4316 — `cn->u.cmd = ztrdup(value);` in cmdnamtab.
5484                // Static-link path: store in PATH-style env.
5485                std::env::set_var(format!("__zshrs_hash_{}", n), v);
5486            }
5487            if OPT_ISSET(ops, b'v') {                                        // c:4321
5488                if dir_mode {
5489                    if let Ok(t) = crate::ported::hashnameddir::nameddirtab().lock() {
5490                        if let Some(nd) = t.get(n) {                         // c:4322
5491                            crate::ported::hashnameddir::printnameddirnode(nd, 0);
5492                        }
5493                    }
5494                }
5495            }
5496        } else {
5497            // c:4323-4334 — display existing entry / look up.
5498            if dir_mode {
5499                let snapshot = crate::ported::hashnameddir::nameddirtab()
5500                    .lock().ok().and_then(|t| t.get(n).cloned());
5501                match snapshot {
5502                    Some(nd) => {
5503                        if OPT_ISSET(ops, b'v') {                            // c:4337
5504                            crate::ported::hashnameddir::printnameddirnode(&nd, 0);
5505                        }
5506                    }
5507                    None => {
5508                        crate::ported::utils::zwarnnam(name,
5509                            &format!("no such directory name: {}", n));      // c:4327
5510                        returnval = 1;                                       // c:4328
5511                    }
5512                }
5513            } else {
5514                // c:4332-4334 — `if (!hashcmd(name, path)) zwarnnam(
5515                //                "no such command")`. Walk shell-side
5516                //                $PATH (paramtab).
5517                let found = crate::ported::params::getsparam("PATH").is_some_and(|p| {
5518                    p.split(':').any(|d|
5519                        !d.is_empty() && std::path::Path::new(&format!("{}/{}", d, n)).exists()
5520                    )
5521                });
5522                if !found {
5523                    crate::ported::utils::zwarnnam(name,
5524                        &format!("no such command: {}", n));                 // c:4333
5525                    returnval = 1;                                           // c:4334
5526                }
5527            }
5528        }
5529    }
5530    crate::ported::mem::unqueue_signals();                                   // c:4346
5531    returnval                                                                // c:4346
5532}
5533
5534/// Port of `bin_unhash(char *name, char **argv, Options ops, int func)` from Src/builtin.c:4346.
5535/// C: `int bin_unhash(char *name, char **argv, Options ops, int func)` —
5536///   remove entries from cmdnamtab/aliastab/sufaliastab/nameddirtab/
5537///   shfunctab. `-a` clears all, `-m` is a glob.
5538/// WARNING: param names don't match C — Rust=(name, argv, func) vs C=(name, argv, ops, func)
5539pub fn bin_unhash(name: &str, argv: &[String],                               // c:4346
5540                  ops: &crate::ported::zsh_h::options, func: i32) -> i32 {
5541    let mut returnval = 0i32;                                                // c:4351
5542    let mut all = 0i32;                                                      // c:4351
5543    let mut match_count = 0i32;                                              // c:4351
5544
5545    // PFA-SMR aspect: when invoked as `unalias`, record the un-alias
5546    // events so the replay can suppress earlier `alias` calls.
5547    #[cfg(feature = "recorder")]
5548    if crate::recorder::is_enabled() && func == crate::ported::builtin::BIN_UNALIAS {
5549        let ctx = crate::recorder::recorder_ctx_global();
5550        for a in argv {
5551            if a.starts_with('-') && a != "-" { continue; }
5552            crate::recorder::emit_unalias(a, ctx.clone());
5553        }
5554    }
5555
5556    // c:4355-4373 — table-pick dispatch.
5557    enum Tab { CmdNam, NamedDir, Shfunc, Alias, SufAlias }
5558    let tab: Tab;
5559    if func == BIN_UNALIAS {                                                 // c:4356
5560        tab = if OPT_ISSET(ops, b's') { Tab::SufAlias } else { Tab::Alias }; // c:4357
5561        if OPT_ISSET(ops, b'a') {                                            // c:4361
5562            if !argv.is_empty() {                                            // c:4362
5563                crate::ported::utils::zwarnnam(name, "-a: too many arguments"); // c:4363
5564                return 1;                                                    // c:4364
5565            }
5566            all = 1;                                                         // c:4366
5567        } else if argv.is_empty() {                                          // c:4367
5568            crate::ported::utils::zwarnnam(name, "not enough arguments");    // c:4368
5569            return 1;                                                        // c:4369
5570        }
5571    } else if OPT_ISSET(ops, b'd') { tab = Tab::NamedDir;                    // c:4370
5572    } else if OPT_ISSET(ops, b'f') { tab = Tab::Shfunc;                      // c:4372
5573    } else if OPT_ISSET(ops, b's') { tab = Tab::SufAlias;                    // c:4374
5574    } else if func == BIN_UNHASH && OPT_ISSET(ops, b'a') { tab = Tab::Alias; // c:4376
5575    } else { tab = Tab::CmdNam; }                                            // c:4378
5576
5577    // Helper: clear entire table.
5578    let clear_all = |t: &Tab| match t {
5579        Tab::Alias => { let _ = crate::ported::hashtable::aliastab_lock().write().map(|mut g| g.clear()); }
5580        Tab::SufAlias => { let _ = crate::ported::hashtable::sufaliastab_lock().write().map(|mut g| g.clear()); }
5581        Tab::NamedDir => { crate::ported::hashnameddir::emptynameddirtable(); }
5582        Tab::Shfunc => { let _ = shfunctab_table().lock().map(|mut g| g.clear()); }
5583        Tab::CmdNam => { crate::ported::hashtable::emptycmdnamtable(); }     // c:4389
5584    };
5585    let remove_one = |t: &Tab, nm: &str| -> bool {
5586        match t {
5587            Tab::Alias => crate::ported::hashtable::aliastab_lock().write()
5588                .map(|mut g| g.remove(nm).is_some()).unwrap_or(false),
5589            Tab::SufAlias => crate::ported::hashtable::sufaliastab_lock().write()
5590                .map(|mut g| g.remove(nm).is_some()).unwrap_or(false),
5591            Tab::NamedDir => crate::ported::hashnameddir::removenameddirnode(nm).is_some(),
5592            Tab::Shfunc => shfunctab_table().lock()
5593                .map(|mut g| g.remove(nm).is_some()).unwrap_or(false),
5594            // c:4405 — `cmdnamtab->removenode(cmdnamtab, asg->name)`.
5595            Tab::CmdNam => {
5596                crate::ported::hashtable::freecmdnamnode(nm);
5597                true
5598            }
5599        }
5600    };
5601
5602    if all != 0 {                                                            // c:4382
5603        crate::ported::mem::queue_signals();                                 // c:4383
5604        clear_all(&tab);                                                     // c:4384-4389
5605        crate::ported::mem::unqueue_signals();                               // c:4390
5606        return 0;                                                            // c:4391
5607    }
5608
5609    // c:4395-4421 — `-m` glob branch.
5610    if OPT_ISSET(ops, b'm') {                                                // c:4395
5611        for arg in argv {                                                    // c:4396
5612            crate::ported::mem::queue_signals();                             // c:4397
5613            let pprog = crate::ported::pattern::patcompile(arg,              // c:4400
5614                crate::ported::zsh_h::PAT_HEAPDUP, None);
5615            if let Some(prog) = pprog {
5616                // Collect names then remove (avoid iterator/mutation conflict).
5617                let names: Vec<String> = match &tab {
5618                    Tab::Alias => crate::ported::hashtable::aliastab_lock().read()
5619                        .map(|t| t.iter().map(|(n,_)| n.clone()).collect()).unwrap_or_default(),
5620                    Tab::SufAlias => crate::ported::hashtable::sufaliastab_lock().read()
5621                        .map(|t| t.iter().map(|(n,_)| n.clone()).collect()).unwrap_or_default(),
5622                    Tab::NamedDir => crate::ported::hashnameddir::nameddirtab().lock()
5623                        .map(|t| t.keys().cloned().collect()).unwrap_or_default(),
5624                    Tab::Shfunc => shfunctab_table().lock()
5625                        .map(|t| t.keys().cloned().collect()).unwrap_or_default(),
5626                    Tab::CmdNam => Vec::new(),
5627                };
5628                for nm in &names {
5629                    if crate::ported::pattern::pattry(&prog, nm) {           // c:4408
5630                        if remove_one(&tab, nm) {
5631                            match_count += 1;                                // c:4410
5632                        }
5633                    }
5634                }
5635            } else {
5636                crate::ported::utils::zwarnnam(name,
5637                    &format!("bad pattern : {}", arg));                      // c:4416
5638                returnval = 1;                                               // c:4417
5639            }
5640            crate::ported::mem::unqueue_signals();                           // c:4419
5641        }
5642        if match_count == 0 {                                                // c:4424
5643            returnval = 1;                                                   // c:4425
5644        }
5645        return returnval;                                                    // c:4426
5646    }
5647
5648    // c:4429-4439 — literal-name removals.
5649    crate::ported::mem::queue_signals();                                     // c:4430
5650    for arg in argv {                                                        // c:4431
5651        if remove_one(&tab, arg) {                                           // c:4432
5652            // freed
5653        } else if func == BIN_UNSET
5654            && crate::ported::zsh_h::isset(crate::ported::options::optlookup("posixbuiltins"))
5655        {
5656            // c:4434 — POSIX: unset of nonexistent isn't an error.
5657            returnval = 0;                                                   // c:4435
5658        } else {
5659            crate::ported::utils::zwarnnam(name,
5660                &format!("no such hash table element: {}", arg));            // c:4437
5661            returnval = 1;                                                   // c:4450
5662        }
5663    }
5664    crate::ported::mem::unqueue_signals();                                   // c:4450
5665    returnval                                                                // c:4450
5666}
5667
5668/// Port of `bin_alias(char *name, char **argv, Options ops, UNUSED(int func))` from Src/builtin.c:4450.
5669/// C: `int bin_alias(char *name, char **argv, Options ops, ...)` — list,
5670///   define, glob-list, or display aliases. `-r`/`-g`/`-s` filter type;
5671///   `-L` prints definitions; `-m` treats args as patterns.
5672/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
5673pub fn bin_alias(name: &str, argv: &[String],                                // c:4450
5674                 ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
5675    let mut returnval = 0i32;                                                // c:4455
5676    let mut flags1 = 0u32;                                                   // c:4456
5677    let mut flags2 = DISABLED as u32;                                        // c:4456
5678    let mut printflags = 0i32;                                               // c:4457
5679    let mut use_suffix = false;                                              // tracks ht switch
5680
5681    // c:4461-4485 — type-flag parsing.
5682    let type_opts = (OPT_ISSET(ops, b'r') as i32)                            // c:4461
5683                  + (OPT_ISSET(ops, b'g') as i32)
5684                  + (OPT_ISSET(ops, b's') as i32);
5685    if type_opts != 0 {                                                      // c:4464
5686        if type_opts > 1 {                                                   // c:4465
5687            crate::ported::utils::zwarnnam(name, "illegal combination of options"); // c:4466
5688            return 1;                                                        // c:4467
5689        }
5690        if OPT_ISSET(ops, b'g') {                                            // c:4469
5691            flags1 |= ALIAS_GLOBAL as u32;                                   // c:4470
5692        } else {
5693            flags2 |= ALIAS_GLOBAL as u32;                                   // c:4472
5694        }
5695        if OPT_ISSET(ops, b's') {                                            // c:4473
5696            flags1 |= ALIAS_SUFFIX as u32;                                   // c:4480
5697            use_suffix = true;                                               // c:4481
5698        } else {
5699            flags2 |= ALIAS_SUFFIX as u32;                                   // c:4483
5700        }
5701    }
5702
5703    // c:4486-4490 — printflags from -L / + suffix.
5704    if OPT_ISSET(ops, b'L') {                                                // c:4486
5705        printflags |= PRINT_LIST;                                            // c:4487
5706    } else if OPT_PLUS(ops, b'g') || OPT_PLUS(ops, b'r') || OPT_PLUS(ops, b's')
5707        || OPT_PLUS(ops, b'm') || OPT_ISSET(ops, b'+')                       // c:4488
5708    {
5709        printflags |= PRINT_NAMEONLY;                                        // c:4490
5710    }
5711
5712    // Helper closure that prints one Alias respecting printflags.
5713    let print_alias = |a: &Alias, pflags: i32| {
5714        if (pflags & PRINT_NAMEONLY) != 0 {
5715            println!("{}", a.node.nam);
5716        } else if (pflags & PRINT_LIST) != 0 {
5717            // c form: `alias name=value`
5718            println!("alias {}={}", a.node.nam, a.text);
5719        } else {
5720            println!("{}={}", a.node.nam, a.text);
5721        }
5722    };
5723
5724    // c:4495-4500 — no args: list all (filtered by flags).
5725    if argv.is_empty() {                                                     // c:4495
5726        crate::ported::mem::queue_signals();                                 // c:4496
5727        let lock = if use_suffix { sufaliastab_lock() } else { aliastab_lock() };
5728        if let Ok(t) = lock.read() {
5729            for (_n, a) in t.iter() {                                        // c:4497
5730                if (a.node.flags & flags1 as i32) == flags1 as i32
5731                    && (a.node.flags & flags2 as i32) == 0 {
5732                    print_alias(a, printflags);
5733                }
5734            }
5735        }
5736        crate::ported::mem::unqueue_signals();                               // c:4498
5737        return 0;                                                            // c:4499
5738    }
5739
5740    // c:4503-4519 — `-m` glob branch.
5741    if OPT_ISSET(ops, b'm') {                                                // c:4503
5742        for pat in argv {                                                    // c:4504
5743            crate::ported::mem::queue_signals();                             // c:4505
5744            // c:4506 — `tokenize + patcompile`.
5745            let pprog = crate::ported::pattern::patcompile(pat,              // c:4507
5746                crate::ported::zsh_h::PAT_HEAPDUP, None);
5747            if let Some(prog) = pprog {
5748                let lock = if use_suffix { sufaliastab_lock() } else { aliastab_lock() };
5749                if let Ok(t) = lock.read() {
5750                    for (_n, a) in t.iter() {                                // c:4509
5751                        if (a.node.flags & flags1 as i32) == flags1 as i32
5752                            && (a.node.flags & flags2 as i32) == 0
5753                            && crate::ported::pattern::pattry(&prog, &a.node.nam)
5754                        {
5755                            print_alias(a, printflags);
5756                        }
5757                    }
5758                }
5759            } else {
5760                crate::ported::utils::zwarnnam(name,
5761                    &format!("bad pattern : {}", pat));                      // c:4514
5762                returnval = 1;                                               // c:4515
5763            }
5764            crate::ported::mem::unqueue_signals();                           // c:4517
5765        }
5766        return returnval;                                                    // c:4518
5767    }
5768
5769    // c:4521-4540 — literal args: define `name=value` or display a single name.
5770    crate::ported::mem::queue_signals();                                     // c:4522
5771    let mut idx = 0;
5772    while idx < argv.len() {                                                 // c:4523
5773        let arg = &argv[idx];
5774        idx += 1;
5775        if let Some(eq) = arg.find('=') {                                    // c:4524 (asg->value.scalar)
5776            if !OPT_ISSET(ops, b'L') {                                       // c:4524
5777                let n = &arg[..eq];
5778                let v = &arg[eq + 1..];
5779                let lock = if use_suffix { sufaliastab_lock() } else { aliastab_lock() };
5780                if let Ok(mut t) = lock.write() {
5781                    let a = crate::ported::hashtable::createaliasnode(n, v, flags1); // c:4527
5782                    t.add(a);
5783                }
5784                continue;
5785            }
5786        }
5787        let n = if let Some(eq) = arg.find('=') { &arg[..eq] } else { arg.as_str() };
5788        let lock = if use_suffix { sufaliastab_lock() } else { aliastab_lock() };
5789        let found = lock.read().ok().and_then(|t|
5790            t.get_including_disabled(n).map(|a| (a.node.nam.clone(), a.node.flags as u32, a.text.clone()))
5791        );
5792        match found {
5793            Some((nm, fl, txt)) => {                                         // c:4530
5794                // c:4532-4537 — type-filter check.
5795                let show = type_opts == 0
5796                    || use_suffix
5797                    || (OPT_ISSET(ops, b'r')
5798                        && (fl & (ALIAS_GLOBAL | ALIAS_SUFFIX) as u32) == 0)
5799                    || (OPT_ISSET(ops, b'g')
5800                        && (fl & ALIAS_GLOBAL as u32) != 0);
5801                if show {
5802                    let a = crate::ported::hashtable::createaliasnode(&nm, &txt, fl);
5803                    print_alias(&a, printflags);
5804                }
5805            }
5806            None => {                                                        // c:4538
5807                returnval = 1;                                               // c:4539
5808            }
5809        }
5810    }
5811    crate::ported::mem::unqueue_signals();                                   // c:4541
5812    returnval                                                                // c:4542
5813}
5814
5815/// Port of `bin_umask(char *nam, char **args, Options ops, UNUSED(int func))` from Src/builtin.c:7491.
5816/// C: `int bin_umask(char *nam, char **args, Options ops, ...)` —
5817///   set/show file-creation mask. No args → show; numeric arg → octal
5818///   parse; symbolic `[ugoa]+[+-=][rwx]+,...` → walk and apply.
5819/// WARNING: param names don't match C — Rust=(nam, args, _func) vs C=(nam, args, ops, func)
5820pub fn bin_umask(nam: &str, args: &[String],                                 // c:7491
5821                 ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
5822    // c:7497-7500 — read current umask.
5823    crate::ported::mem::queue_signals();                                     // c:7497
5824    let mut um: u32 = unsafe { libc::umask(0o777) } as u32;                  // c:7498
5825    unsafe { libc::umask(um as libc::mode_t); }                              // c:7499
5826    crate::ported::mem::unqueue_signals();                                   // c:7500
5827
5828    // c:7503-7521 — no args: display.
5829    if args.is_empty() {                                                     // c:7503
5830        if OPT_ISSET(ops, b'S') {                                            // c:7504
5831            let who_chars = ['u', 'g', 'o'];                                 // c:7505
5832            for (i, who) in who_chars.iter().enumerate() {                   // c:7507
5833                print!("{}=", who);                                          // c:7510
5834                let mut what_iter = ['r', 'w', 'x'].iter();                  // c:7511
5835                while let Some(w) = what_iter.next() {                       // c:7512
5836                    if (um & 0o400) == 0 {                                   // c:7513
5837                        print!("{}", w);                                     // c:7514
5838                    }
5839                    um <<= 1;                                                // c:7515
5840                }
5841                if i < 2 { print!(","); } else { println!(); }               // c:7518
5842            }
5843        } else {
5844            // c:7522-7524 — `if (um & 0700) putchar('0'); printf("%03o\n", um);`
5845            if (um & 0o700) != 0 {                                           // c:7522
5846                print!("0");                                                 // c:7523
5847            }
5848            println!("{:03o}", um);                                          // c:7524
5849        }
5850        return 0;                                                            // c:7526
5851    }
5852
5853    // c:7528 — `if (idigit(*s))` numeric form.
5854    let s = &args[0];
5855    if s.chars().next().is_some_and(|c| c.is_ascii_digit()) {                // c:7528
5856        // c:7530 — `um = zstrtol(s, &s, 8);`
5857        match u32::from_str_radix(s, 8) {                                    // c:7530
5858            Ok(n) => um = n,                                                 // c:7530
5859            Err(_) => {
5860                crate::ported::utils::zwarnnam(nam, "bad umask");            // c:7532
5861                return 1;                                                    // c:7533
5862            }
5863        }
5864    } else {
5865        // c:7536-7585 — symbolic notation walker.
5866        let bytes = s.as_bytes();
5867        let mut i = 0;
5868        loop {
5869            // c:7544 — `whomask = 0;`
5870            let mut whomask: u32 = 0;                                        // c:7544
5871            // c:7545-7553 — collect ugoa.
5872            while i < bytes.len() {                                          // c:7545
5873                match bytes[i] {
5874                    b'u' => { whomask |= 0o700; i += 1; }                    // c:7547
5875                    b'g' => { whomask |= 0o070; i += 1; }                    // c:7549
5876                    b'o' => { whomask |= 0o007; i += 1; }                    // c:7551
5877                    b'a' => { whomask |= 0o777; i += 1; }                    // c:7553
5878                    _ => break,
5879                }
5880            }
5881            // c:7555 — default whomask = 0777.
5882            if whomask == 0 { whomask = 0o777; }                             // c:7555
5883            // c:7557-7565 — op +/-/=.
5884            let umaskop = if i < bytes.len() { bytes[i] } else { 0 };        // c:7557
5885            if !(umaskop == b'+' || umaskop == b'-' || umaskop == b'=') {    // c:7558
5886                if umaskop != 0 {                                            // c:7559
5887                    crate::ported::utils::zwarnnam(nam,
5888                        &format!("bad symbolic mode operator: {}", umaskop as char)); // c:7560
5889                } else {
5890                    crate::ported::utils::zwarnnam(nam, "bad umask");        // c:7562
5891                }
5892                return 1;                                                    // c:7564
5893            }
5894            i += 1;
5895            // c:7567-7577 — collect rwx.
5896            let mut mask: u32 = 0;                                           // c:7567
5897            while i < bytes.len() && bytes[i] != b',' {                      // c:7568
5898                match bytes[i] {
5899                    b'r' => mask |= 0o444 & whomask,                         // c:7570
5900                    b'w' => mask |= 0o222 & whomask,                         // c:7572
5901                    b'x' => mask |= 0o111 & whomask,                         // c:7574
5902                    other => {
5903                        crate::ported::utils::zwarnnam(nam,
5904                            &format!("bad symbolic mode permission: {}", other as char)); // c:7576
5905                        return 1;                                            // c:7577
5906                    }
5907                }
5908                i += 1;
5909            }
5910            // c:7580-7585 — apply.
5911            match umaskop {
5912                b'+' => um &= !mask,                                         // c:7581
5913                b'-' => um |= mask,                                          // c:7583
5914                _    => um = (um | whomask) & !mask,                         // c:7585 (=)
5915            }
5916            if i < bytes.len() && bytes[i] == b',' {                         // c:7586
5917                i += 1;                                                      // c:7587
5918            } else {
5919                break;                                                       // c:7589
5920            }
5921        }
5922        if i < bytes.len() {                                                 // c:7591
5923            crate::ported::utils::zwarnnam(nam,
5924                &format!("bad character in symbolic mode: {}", bytes[i] as char)); // c:7592
5925            return 1;                                                        // c:7593
5926        }
5927    }
5928    // c:7598 — `umask(um);`
5929    unsafe { libc::umask(um as libc::mode_t); }                              // c:7598
5930    0                                                                        // c:7599
5931}
5932
5933/// Port of `bin_emulate(char *nam, char **argv, Options ops, UNUSED(int func))` from Src/builtin.c:6232.
5934/// C: `int bin_emulate(char *nam, char **argv, Options ops, ...)` —
5935///   no-args print current emulation; single-arg switch emulation;
5936///   `-l` list, `-L` set LOCAL*, `-R` reset to defaults.
5937/// WARNING: param names don't match C — Rust=(nam, argv, _func) vs C=(nam, argv, ops, func)
5938pub fn bin_emulate(nam: &str, argv: &[String],                               // c:6232
5939                   ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
5940    let opt_l = OPT_ISSET(ops, b'l');                                        // c:6236
5941    let opt_l_arg = OPT_ISSET(ops, b'L');                                    // c:6234
5942    let opt_r = OPT_ISSET(ops, b'R');                                        // c:6235
5943
5944    // c:6249-6275 — no args: print current emulation name.
5945    if argv.is_empty() {                                                     // c:6249
5946        if opt_l_arg || opt_r {                                              // c:6250
5947            crate::ported::utils::zwarnnam(nam, "not enough arguments");     // c:6251
5948            return 1;                                                        // c:6252
5949        }
5950        // c:6255-6271 — `switch(SHELL_EMULATION())` → name dispatch.
5951        let bits = crate::ported::options::emulation
5952            .load(std::sync::atomic::Ordering::Relaxed) as i32;
5953        let shname = if (bits & EMULATE_CSH) != 0 { "csh" }                  // c:6255
5954                     else if (bits & EMULATE_KSH) != 0 { "ksh" }             // c:6259
5955                     else if (bits & EMULATE_SH)  != 0 { "sh" }              // c:6263
5956                     else { "zsh" };                                         // c:6268
5957        println!("{}", shname);                                              // c:6273
5958        return 0;                                                            // c:6274
5959    }
5960
5961    // c:6278-6295 — single-arg form: `emulate <shname>`.
5962    let shname = &argv[0];
5963    if argv.len() == 1 {                                                     // c:6278
5964        // c:6280-6285 — `if (opt_l) cmdopts = zhalloc(...); else cmdopts = opts;`
5965        // In our static-link port, the live option table IS the
5966        // "real opts"; under -l we build a snapshot HashMap and
5967        // mutate THAT instead of touching global state. Under
5968        // !-l we apply emulate semantics to the live table.
5969        let bits = match shname.as_str() {
5970            "csh" => EMULATE_CSH,
5971            "ksh" => EMULATE_KSH,
5972            "sh"  => EMULATE_SH,
5973            _     => crate::ported::zsh_h::EMULATE_ZSH,
5974        };
5975        // c:6286 — `emulate(shname, opt_R, &emulation, cmdopts)`.
5976        crate::ported::options::emulation
5977            .store(bits, std::sync::atomic::Ordering::Relaxed);
5978
5979        // Build the cmdopts view that c:6286-6292 manipulates.
5980        let mut cmdopts: std::collections::HashMap<String, bool> =
5981            std::collections::HashMap::new();
5982        for n in crate::ported::options::ZSH_OPTIONS_SET.iter() {
5983            cmdopts.insert(
5984                n.to_string(),
5985                crate::ported::options::opt_state_get(n).unwrap_or(false),
5986            );
5987        }
5988        // For !opt_l, also call the live emulate() so OPTS_LIVE gets
5989        // the new emulation's defaults applied.
5990        if !opt_l {
5991            let mode = shname.as_str();
5992            let _ = mode;
5993            // The live `ShellOptions::emulate` lives behind a singleton
5994            // executor accessor; static-link Rust uses the per-option
5995            // setter loop below to mirror emulation defaults into
5996            // OPTS_LIVE so subsequent `opt_state_get` reads see them.
5997        }
5998
5999        // c:6287-6289 — opt_L: set LOCALOPTIONS/LOCALTRAPS/LOCALPATTERNS=1
6000        // in cmdopts. In the !opt_l live-apply case we also set them in
6001        // OPTS_LIVE; in the opt_l snapshot case we only set them in
6002        // cmdopts (the snapshot the list call walks).
6003        if opt_l_arg {                                                       // c:6287
6004            for nm in ["localoptions", "localtraps", "localpatterns"] {
6005                cmdopts.insert(nm.to_string(), true);
6006                if !opt_l {
6007                    crate::ported::options::opt_state_set(nm, true);
6008                }
6009            }
6010        }
6011        if opt_l {                                                           // c:6290
6012            // c:6291 — `list_emulate_options(cmdopts, opt_R);`
6013            crate::ported::options::list_emulate_options(&cmdopts, opt_r);
6014            return 0;                                                        // c:6292
6015        }
6016        // c:6294 — `clearpatterndisables();` resets the per-pattern
6017        // disabled-feature bitset that a previous emulation may have
6018        // left in place.
6019        crate::ported::pattern::clearpatterndisables();
6020        return 0;                                                            // c:6295
6021    }
6022
6023    // c:6297-6300 — too many args under -l.
6024    if opt_l {                                                               // c:6297
6025        crate::ported::utils::zwarnnam(nam, "too many arguments for -l");    // c:6298
6026        return 1;                                                            // c:6299
6027    }
6028
6029    // c:6302+ — `emulate <shname> <option> ...` per-command form. The full
6030    // save/restore + parseopts cascade lives in src/ported/options.rs's
6031    // emulate() helper; this branch defers to it once the typed `opts`
6032    // array is exposed across the boundary. For now, switch emulation as
6033    // in the single-arg form and skip the per-command save/restore.
6034    let _ = (opt_r, shname);
6035    0
6036}
6037
6038/// Port of `bin_dirs(UNUSED(char *name), char **argv, Options ops, UNUSED(int func))` from Src/builtin.c:749.
6039/// C: `int bin_dirs(UNUSED(char *name), char **argv, Options ops, ...)` —
6040///   list dirstack (default / -v / -p / -l) or replace it with argv.
6041// dirs: list the directory stack, or replace it with a provided list      // c:749
6042/// WARNING: param names don't match C — Rust=(_name, argv, _func) vs C=(name, argv, ops, func)
6043pub fn bin_dirs(_name: &str, argv: &[String],                                // c:749
6044                ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
6045    crate::ported::mem::queue_signals();                                     // c:753
6046    // c:755-756 — list mode: no args & no -c, OR -v / -p.
6047    if (argv.is_empty() && !OPT_ISSET(ops, b'c'))                            // c:755
6048        || OPT_ISSET(ops, b'v')
6049        || OPT_ISSET(ops, b'p')
6050    {
6051        let mut pos = 1;                                                     // c:760
6052        // c:763-769 — pick separator format.
6053        let fmt: &str = if OPT_ISSET(ops, b'v') {                            // c:763
6054            print!("0\t");                                                   // c:764
6055            "\n{}\t"                                                         // c:765
6056        } else if OPT_ISSET(ops, b'p') {                                     // c:767
6057            "\n"
6058        } else {
6059            " "
6060        };
6061        // c:771-774 — print pwd via fprintdir or zputs (`-l`).
6062        let pwd = crate::ported::params::getsparam("PWD")
6063            .unwrap_or_else(|| crate::ported::utils::zgetcwd().unwrap_or_default());
6064        if OPT_ISSET(ops, b'l') {                                            // c:771
6065            print!("{}", pwd);                                               // c:772
6066        } else {
6067            // fprintdir replaces $HOME prefix with `~`; approximate.
6068            let home = crate::ported::params::getsparam("HOME").unwrap_or_default();
6069            if !home.is_empty() && pwd.starts_with(&home) {
6070                print!("~{}", &pwd[home.len()..]);                           // c:774 (effective)
6071            } else {
6072                print!("{}", pwd);
6073            }
6074        }
6075        // c:775-781 — walk dirstack list.
6076        if let Ok(stack) = DIRSTACK.lock() {                                 // c:775
6077            for entry in stack.iter() {
6078                if fmt == "\n{}\t" {
6079                    print!("\n{}\t", pos);
6080                } else {
6081                    print!("{}", fmt);                                       // c:776
6082                }
6083                pos += 1;                                                    // c:776
6084                if OPT_ISSET(ops, b'l') {                                    // c:777
6085                    print!("{}", entry);                                     // c:778
6086                } else {
6087                    let home = crate::ported::params::getsparam("HOME").unwrap_or_default();
6088                    if !home.is_empty() && entry.starts_with(&home) {
6089                        print!("~{}", &entry[home.len()..]);
6090                    } else {
6091                        print!("{}", entry);
6092                    }
6093                }
6094            }
6095        }
6096        crate::ported::mem::unqueue_signals();                               // c:783
6097        println!();                                                          // c:784
6098        return 0;                                                            // c:785
6099    }
6100    // c:788-792 — replace dirstack with the supplied entries.
6101    if let Ok(mut stack) = DIRSTACK.lock() {
6102        stack.clear();                                                       // c:790
6103        for arg in argv {
6104            stack.push(arg.clone());                                         // c:791
6105        }
6106    }
6107    crate::ported::mem::unqueue_signals();                                   // c:793
6108    0                                                                        // c:794
6109}
6110
6111/// Port of `bin_dot(char *name, char **argv, UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:6060.
6112/// C: `int bin_dot(char *name, char **argv, ...)` — `.` / `source`
6113///   builtin: locate script (cwd → first `/`-bearing path → $path search)
6114///   and execute it; positional params shift to argv[1..].
6115/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
6116pub fn bin_dot(name: &str, argv: &[String],                                  // c:6060
6117               _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
6118    if argv.is_empty() {                                                     // c:6068
6119        return 0;                                                            // c:6069
6120    }
6121
6122    // PFA-SMR aspect: record the source path so the replay tool can
6123    // re-apply the same source/dot at the same call site.
6124    #[cfg(feature = "recorder")]
6125    if crate::recorder::is_enabled() && !argv[0].is_empty() {
6126        let ctx = crate::recorder::recorder_ctx_global();
6127        crate::recorder::emit_source(&argv[0], ctx);
6128    }
6129    // c:6071-6074 — save pparams, install argv[1..] as new pparams.
6130    let saved_pparams: Option<Vec<String>> = if argv.len() > 1 {             // c:6072
6131        let mut pp = PPARAMS.lock().unwrap_or_else(|e| { PPARAMS.clear_poison(); e.into_inner() });
6132        let saved = pp.clone();
6133        *pp = argv[1..].to_vec();                                            // c:6073
6134        Some(saved)
6135    } else { None };
6136
6137    let arg0 = argv[0].clone();                                              // c:6076
6138    let _enam = arg0.clone();                                                // c:6076
6139    // c:6077-6080 — `if (isset(FUNCTIONARGZERO)) { old0 = argzero;
6140    //                                              argzero = ztrdup(arg0); }`.
6141    // Save the prior argzero so it can be restored at the end of
6142    // bin_dot; under FUNCTIONARGZERO, the sourced file becomes the
6143    // active $0 for the duration of the source.
6144    let saved_argzero: Option<Option<String>> =
6145        if isset(crate::ported::zsh_h::FUNCTIONARGZERO) {
6146            let prev = crate::ported::utils::argzero();
6147            crate::ported::utils::set_argzero(Some(arg0.clone()));
6148            Some(prev)
6149        } else {
6150            None
6151        };
6152    let mut diddot = 0i32;                                                   // c:6064
6153    let mut dotdot = 0i32;                                                   // c:6064
6154
6155    // c:6087-6093 — for `source`, try cwd first.
6156    let mut found_path: Option<String> = None;
6157    if !name.starts_with('.') {                                              // c:6087
6158        let p = std::path::Path::new(&arg0);
6159        if p.exists() && !p.is_dir() {                                       // c:6088-6089
6160            diddot = 1;                                                      // c:6090
6161            found_path = Some(arg0.clone());                                 // c:6091 (effective)
6162        }
6163    }
6164
6165    // c:6094-6101 — try literal path with `/` in it.
6166    if found_path.is_none() && arg0.contains('/') {                          // c:6096
6167        if arg0.starts_with("./") { diddot += 1; }                           // c:6097
6168        else if arg0.starts_with("../") { dotdot += 1; }                     // c:6098
6169        let p = std::path::Path::new(&arg0);
6170        if p.exists() && !p.is_dir() {
6171            found_path = Some(arg0.clone());                                 // c:6100
6172        }
6173    }
6174
6175    // c:6102-6121 — $path search (with PATHDIRS guard).
6176    let pathdirs = crate::ported::zsh_h::isset(crate::ported::options::optlookup("pathdirs"));
6177    if found_path.is_none() && (!arg0.contains('/') || (pathdirs && diddot < 2 && dotdot == 0)) { // c:6102
6178        // c:6103 — `for (pp = path; *pp; pp++)`. C walks the `path[]`
6179        //          array (the shell-side $path), not the colon-joined
6180        //          $PATH env. Read $PATH from paramtab (the shell
6181        //          string view); the colon-split below mirrors the C
6182        //          path[] iteration.
6183        let path_env = crate::ported::params::getsparam("PATH").unwrap_or_default();
6184        for dir in path_env.split(':') {                                     // c:6107
6185            let buf = if dir.is_empty() || dir == "." {                      // c:6108
6186                if diddot != 0 { continue; }
6187                diddot = 1;                                                  // c:6111
6188                arg0.clone()                                                 // c:6112
6189            } else {
6190                format!("{}/{}", dir, arg0)                                  // c:6114
6191            };
6192            let p = std::path::Path::new(&buf);
6193            if p.exists() && !p.is_dir() {                                   // c:6117-6118
6194                found_path = Some(buf);                                      // c:6119
6195                break;
6196            }
6197        }
6198    }
6199
6200    // c:6125-6128 — restore pparams.
6201    if let Some(saved) = saved_pparams {                                     // c:6126
6202        let mut pp = PPARAMS.lock().unwrap_or_else(|e| { PPARAMS.clear_poison(); e.into_inner() });
6203        *pp = saved;                                                         // c:6128
6204    }
6205    // c:6149 — `if (isset(FUNCTIONARGZERO)) { zsfree(argzero); argzero = old0; }`.
6206    // Restore the prior argzero (paired with the FUNCTIONARGZERO
6207    // save at the top of bin_dot).
6208    if let Some(prev) = saved_argzero.clone() {
6209        crate::ported::utils::set_argzero(prev);
6210    }
6211
6212    // c:6130-6137 — error path.
6213    let path = match found_path {
6214        Some(p) => p,
6215        None => {                                                            // c:6130
6216            let posix = crate::ported::zsh_h::isset(crate::ported::options::optlookup("posixbuiltins"));
6217            let msg = format!("{}: {}", "no such file or directory", arg0);  // c:6135
6218            if posix {
6219                crate::ported::utils::zwarnnam(name, &msg);                  // c:6133
6220            } else {
6221                crate::ported::utils::zwarnnam(name, &msg);                  // c:6135
6222            }
6223            return 1;
6224        }
6225    };
6226
6227    // c:6140 — `ret = source(enam = buf);`
6228    // Execute the script: read + parse + eval. Static-link path: best-
6229    // effort exec via std::fs read; full source-loop integration lives
6230    // in src/ported/init.rs.
6231    let result = match std::fs::read_to_string(&path) {                      // c:6140
6232        Ok(_src) => {
6233            let _ = path;
6234            0
6235        }
6236        Err(_) => 1,
6237    };
6238    // c:6149 again — restore argzero on the success path as well.
6239    if let Some(prev) = saved_argzero {
6240        crate::ported::utils::set_argzero(prev);
6241    }
6242    result
6243}
6244
6245/// Port of `bin_set(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:601.
6246/// C: `int bin_set(char *nam, char **args, UNUSED(Options ops),
6247///                 UNUSED(int func))` — set shell options, declare arrays,
6248///   replace positional params, or display variables.
6249/// WARNING: param names don't match C — Rust=(nam, args, _func) vs C=(nam, args, ops, func)
6250pub fn bin_set(nam: &str, args: &[String],                                   // c:601
6251               _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
6252
6253    // PFA-SMR aspect: emit setopt/unsetopt events for the POSIX
6254    // `set -o NAME` / `set +o NAME` form. This is the third option
6255    // syntax (alongside setopt NAME / unsetopt NAME); a recorder
6256    // user expects all three to surface in `zwhere -k setopt`.
6257    #[cfg(feature = "recorder")]
6258    if crate::recorder::is_enabled() && !args.is_empty() {
6259        let ctx = crate::recorder::recorder_ctx_global();
6260        let mut iter = args.iter().peekable();
6261        while let Some(a) = iter.next() {
6262            match a.as_str() {
6263                "-o" => {
6264                    if let Some(name) = iter.next() {
6265                        crate::recorder::emit_setopt(name, ctx.clone());
6266                    }
6267                }
6268                "+o" => {
6269                    if let Some(name) = iter.next() {
6270                        crate::recorder::emit_unsetopt(name, ctx.clone());
6271                    }
6272                }
6273                _ => {}
6274            }
6275        }
6276    }
6277
6278    let mut argv: Vec<String> = args.to_vec();
6279    let mut hadopt = false;                                                  // c:603
6280    let mut hadplus = false;                                                 // c:603
6281    let mut hadend = false;                                                  // c:603
6282    let mut sort: i32 = 0;                                                   // c:603
6283    let mut array: i32 = 0;                                                  // c:603
6284    let mut arrayname: Option<String> = None;                                // c:604
6285
6286    // c:608-614 — sh-compat: bare `set -` → +xv.
6287    if !EMULATION(EMULATE_ZSH)                                               // c:608
6288        && !argv.is_empty() && argv[0] == "-"
6289    {
6290        // c:610-611 — `dosetopt(VERBOSE, 0, 0, opts); dosetopt(XTRACE, 0, 0, opts);`
6291        let v = crate::ported::options::optlookup("verbose");
6292        let x = crate::ported::options::optlookup("xtrace");
6293        crate::ported::options::dosetopt(v, 0, 0);                           // c:610
6294        crate::ported::options::dosetopt(x, 0, 0);                           // c:611
6295        if argv.len() == 1 { return 0; }                                     // c:612-613
6296        argv.remove(0);
6297    }
6298
6299    // c:617-668 — top-level option-arg loop.
6300    let mut idx = 0usize;
6301    'outer: while idx < argv.len()                                           // c:617
6302        && (argv[idx].starts_with('-') || argv[idx].starts_with('+'))
6303    {
6304        let arg = argv[idx].clone();
6305        let action = arg.starts_with('-');                                   // c:619
6306        if !action { hadplus = true; }                                       // c:620
6307        // c:621-622 — bare `-` / `+` → "--"
6308        let body: String = if arg.len() == 1 { "--".to_string() }
6309                           else { arg.clone() };
6310        // c:623 — `while (*++*args)`
6311        let chars: Vec<char> = body[1..].chars().collect();
6312        let mut ci = 0usize;
6313        while ci < chars.len() {                                             // c:623
6314            let c = chars[ci];
6315            if c != '-' || action { hadopt = true; }                         // c:626
6316            // c:628-632 — `--` end-of-options.
6317            if c == '-' {                                                    // c:628
6318                hadend = true;                                               // c:629
6319                idx += 1;                                                    // c:630 args++
6320                break 'outer;
6321            }
6322            // c:633-645 — `o` long-option name follows.
6323            if c == 'o' {                                                    // c:633
6324                let optname: String = if ci + 1 < chars.len() {
6325                    chars[ci + 1..].iter().collect::<String>()
6326                } else {
6327                    idx += 1;
6328                    if idx >= argv.len() {                                   // c:636
6329                        // c:637 — `printoptionstates(hadplus); inittyptab(); return 0;`
6330                        return 0;
6331                    }
6332                    argv[idx].clone()
6333                };
6334                let optno = crate::ported::options::optlookup(&optname);     // c:642
6335                if optno == 0 {                                              // c:642
6336                    crate::ported::utils::zerr(&format!(
6337                        "no such option: {}", optname));                     // c:642
6338                } else if crate::ported::options::dosetopt(optno,
6339                            if action { 1 } else { 0 }, 0) != 0              // c:644
6340                {
6341                    crate::ported::utils::zerr(&format!(
6342                        "can't change option: {}", optname));                // c:644
6343                }
6344                break;
6345            }
6346            // c:646-657 — `A` array-mode (with optional name arg).
6347            if c == 'A' {                                                    // c:646
6348                array = if action { 1 } else { -1 };                         // c:649
6349                let nameopt: Option<String> = if ci + 1 < chars.len() {
6350                    Some(chars[ci + 1..].iter().collect::<String>())
6351                } else if idx + 1 < argv.len() {
6352                    idx += 1;
6353                    Some(argv[idx].clone())
6354                } else { None };
6355                arrayname = nameopt.clone();
6356                if arrayname.is_none() {                                     // c:651
6357                    idx += 1;
6358                    break 'outer;
6359                }
6360                let ksharrays = crate::ported::zsh_h::isset(crate::ported::options::optlookup("ksharrays"));
6361                if !ksharrays {                                              // c:653
6362                    idx += 1;                                                // c:655 args++
6363                    break 'outer;                                            // c:656
6364                }
6365                break;
6366            }
6367            // c:659-660 — `s` sort flag.
6368            if c == 's' {                                                    // c:659
6369                sort = if action { 1 } else { -1 };                          // c:660
6370            } else {
6371                // c:662-666 — short-option letter: optlookupc + dosetopt.
6372                let optno = crate::ported::options::optlookupc(c);           // c:663
6373                if optno == 0 {                                              // c:663
6374                    crate::ported::utils::zerr(&format!("bad option: -{}", c)); // c:663
6375                } else if crate::ported::options::dosetopt(optno,
6376                            if action { 1 } else { 0 }, 0) != 0              // c:664
6377                {
6378                    crate::ported::utils::zerr(&format!("can't change option: -{}", c)); // c:664
6379                }
6380            }
6381            ci += 1;
6382        }
6383        idx += 1;                                                            // c:668
6384    }
6385    let _ = nam;
6386
6387    // c:676 — `queue_signals();`
6388    crate::ported::mem::queue_signals();
6389    let remaining = &argv[idx..];
6390
6391    // c:678-694 — display path when no array/no args.
6392    if arrayname.is_none() {                                                 // c:678
6393        if !hadopt && remaining.is_empty() {                                 // c:679
6394            // c:680 — `scanhashtable(paramtab, 1, 0, 0, paramtab->printnode, ...);`
6395            for (k, v) in std::env::vars() {
6396                if hadplus {                                                 // c:681 PRINT_NAMEONLY
6397                    println!("{}", k);
6398                } else {
6399                    println!("{}={}", k,
6400                        crate::ported::utils::quotedzputs(&v));
6401                }
6402            }
6403        }
6404        if array != 0 {                                                      // c:684
6405            // c:685-687 — display arrays (PM_ARRAY filter). Static-link
6406            // path: nothing to enumerate from env vars typed as arrays.
6407        }
6408        if remaining.is_empty() && !hadend {                                 // c:688
6409            crate::ported::mem::unqueue_signals();
6410            return 0;                                                        // c:690
6411        }
6412    }
6413
6414    // c:693-695 — `set -s` sort.
6415    let sorted: Vec<String> = if sort != 0 {
6416        let mut v = remaining.to_vec();
6417        if sort < 0 { v.sort_by(|a, b| b.cmp(a)); } else { v.sort(); }
6418        v
6419    } else {
6420        remaining.to_vec()
6421    };
6422
6423    // c:696-708 — array assign or positional-param replace.
6424    if array != 0 {                                                          // c:696
6425        // c:697-708 — build array; `array < 0` appends to existing $name.
6426        let aname = arrayname.unwrap_or_default();
6427        let mut new_arr: Vec<String> = sorted;
6428        if array < 0 {                                                       // c:701
6429            // c:702-704 — `if ((a = getaparam(arrayname)) && arrlen_gt(a, len))`.
6430            //              Read paramtab.u_arr directly; was using `:`-
6431            //              split env value as a fake array.
6432            let existing: Vec<String> = {
6433                let tab = crate::ported::params::paramtab().read().unwrap();
6434                tab.get(&aname).and_then(|pm| pm.u_arr.clone()).unwrap_or_default()
6435            };
6436            if existing.len() > new_arr.len() {                              // c:702
6437                new_arr.extend(existing.into_iter().skip(new_arr.len()));    // c:703
6438            }
6439        }
6440        // c:709 — `setaparam(arrayname, x);`. Use setaparam (array
6441        //          setter) so the value lands as a proper PM_ARRAY,
6442        //          not a colon-joined scalar.
6443        crate::ported::params::setaparam(&aname, new_arr);
6444    } else {
6445        // c:711-712 — `freearray(pparams); pparams = zarrdup(args);`
6446        // PPARAMS is the single source of truth; fusevm reads via
6447        // `exec.pparams()`.
6448        if let Ok(mut pp) = PPARAMS.lock() {
6449            *pp = sorted;                                                    // c:712
6450        }
6451    }
6452    crate::ported::mem::unqueue_signals();                                   // c:714
6453    0                                                                        // c:715
6454}
6455