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("edzputs(&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