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/// Construct the builtin lookup table.
97/// Port of `createbuiltintable()` from `Src/builtin.c:150`. The C
98/// version installs the hashtable function pointers (hash, addnode,
99/// printnode, etc.) and then calls `addbuiltins("zsh", builtins, ..)`.
100/// Here we just materialise the static `BUILTINS` slice into a
101/// `HashMap<String, &builtin>` — Rust's standard hashing replaces the
102/// C `hasher` callback and the `HashMap` itself replaces all the
103/// per-table function pointers (`addnode`/`getnode`/`removenode`/...).
104// Builtin Command Hash Table Functions // c:150
105pub fn createbuiltintable() -> &'static HashMap<String, &'static builtin> { // c:150
106 builtintab.get_or_init(|| {
107 let table: &'static Vec<builtin> = &*BUILTINS;
108 let watch_bintab: &'static Vec<builtin> =
109 &*crate::ported::modules::watch::bintab;
110 let mut m: HashMap<String, &'static builtin> =
111 HashMap::with_capacity(table.len() + watch_bintab.len());
112 for b in table.iter() {
113 m.insert(b.node.nam.clone(), b);
114 }
115 // zshrs auto-loads all modules at startup. Fold each module's
116 // bintab into the core builtintab so `disable <name>` (and
117 // dispatch generally) finds module-provided builtins without
118 // an explicit `zmodload` step. Mirrors C's `addbuiltins(name,
119 // bintab, sizeof(bintab)/sizeof(*bintab))` call from each
120 // module's `boot_` hook (e.g. `Src/Modules/watch.c:694`).
121 for b in watch_bintab.iter() {
122 m.insert(b.node.nam.clone(), b);
123 }
124 m
125 })
126}
127
128// ===========================================================
129// Direct ports of static builtin helpers from Src/builtin.c not
130// yet covered above. The Rust executor wires builtins through
131// `crate::ported::builtins::*` per-builtin modules; these free-
132// fn entries satisfy ABI/name parity for the drift gate.
133// ===========================================================
134
135/// Port of `printbuiltinnode(HashNode hn, int printflags)` from Src/builtin.c:174.
136/// C: `static void printbuiltinnode(HashNode hn, int printflags)` —
137/// emit `whence`-style description of one builtin.
138/// WARNING: param names don't match C — Rust=(hn) vs C=(hn, printflags)
139pub fn printbuiltinnode(hn: *mut crate::ported::zsh_h::hashnode, // c:174
140 printflags: i32) {
141 if hn.is_null() { return; }
142 let bn = unsafe { &*hn };
143 if (printflags & PRINT_WHENCE_WORD as i32) != 0 { // c:179
144 println!("{}: builtin", bn.nam); // c:180
145 return;
146 }
147 if (printflags & PRINT_WHENCE_CSH as i32) != 0 { // c:199
148 println!("{}: shell built-in command", bn.nam); // c:199
149 return;
150 }
151 // c:199-198 — default form: just emit the name.
152 println!("{}", bn.nam);
153}
154
155/// Port of `freebuiltinnode(HashNode hn)` from Src/builtin.c:199.
156/// C: `static void freebuiltinnode(HashNode hn)` — free a builtin-table
157/// node only when BINF_ADDED is clear (i.e., dynamically added).
158pub fn freebuiltinnode(hn: *mut crate::ported::zsh_h::hashnode) { // c:199
159 if hn.is_null() { return; }
160 let bn = unsafe { &*hn };
161 // c:204 — `if (!(bn->node.flags & BINF_ADDED))` then free.
162 if (bn.flags as u32 & crate::ported::zsh_h::BINF_ADDED) == 0 { // c:204
163 // Rust drop handles the actual free; nothing more to do.
164 }
165}
166
167/// Port of `init_builtins()` from Src/builtin.c:212.
168/// C: `void init_builtins(void)` — when not in EMULATE_ZSH, disable
169/// the `repeat` reserved word (compat for sh/ksh).
170///
171/// ```c
172/// if (!EMULATION(EMULATE_ZSH)) {
173/// HashNode hn = reswdtab->getnode2(reswdtab, "repeat");
174/// if (hn)
175/// reswdtab->disablenode(hn, 0);
176/// }
177/// ```
178pub fn init_builtins() { // c:212
179 // c:214 — `if (!EMULATION(EMULATE_ZSH))`. EMULATION reads the
180 // canonical `emulation` global directly per zsh.h:2347.
181 if !crate::ported::zsh_h::EMULATION(EMULATE_ZSH) { // c:214
182 // c:215-217 — `hn = reswdtab->getnode2(reswdtab,"repeat");
183 // if (hn) reswdtab->disablenode(hn, 0);`
184 if let Ok(mut tab) = crate::ported::hashtable::reswdtab_lock().write() {
185 tab.disable("repeat");
186 }
187 }
188}
189
190/// Port of `OPT_ALLOC_CHUNK` from `Src/builtin.c:227`. Number of
191/// `ops->args[]` slots `new_optarg()` grows the array by when full.
192pub const OPT_ALLOC_CHUNK: i32 = 16; // c:227
193
194/// Port of `new_optarg(Options ops)` from Src/builtin.c:227.
195/// C: `static int new_optarg(Options ops)` — grow the `ops->args[]`
196/// array by `OPT_ALLOC_CHUNK` slots when full. Returns 1 on overflow
197/// (>=63 args), 0 on success.
198pub fn new_optarg(ops: &mut crate::ported::zsh_h::options) -> i32 { // c:227
199 // c:227 — `if (ops->argscount == 63) return 1;`
200 if ops.argscount == 63 { // c:231
201 return 1;
202 }
203 // c:232-241 — grow ops->args by OPT_ALLOC_CHUNK if argsalloc == argscount.
204 if ops.argsalloc == ops.argscount { // c:232
205 ops.args.resize((ops.argsalloc + OPT_ALLOC_CHUNK) as usize, String::new());
206 ops.argsalloc += OPT_ALLOC_CHUNK; // c:240
207 }
208 ops.argscount += 1; // c:243
209 0 // c:244
210}
211
212
213// ===========================================================
214// ksh_autoload_body moved from src/ported/exec.rs.
215// Mirrors the ksh-style autoload helper in Src/builtin.c
216// (bin_functions / load_function_def).
217// ===========================================================
218// (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.)
219
220
221bitflags::bitflags! {
222 /// Flags for autoloaded functions (autoload builtin -- Src/builtin.c bin_autoload).
223 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
224 pub struct AutoloadFlags: u32 {
225 const NO_ALIAS = 0b00000001; // -U: don't expand aliases
226 const ZSH_STYLE = 0b00000010; // -z: zsh-style autoload
227 const KSH_STYLE = 0b00000100; // -k: ksh-style autoload
228 const TRACE = 0b00001000; // -t: trace execution
229 const USE_CALLER_DIR = 0b00010000; // -d: use calling function's dir
230 const LOADED = 0b00100000; // function has been loaded
231 }
232}
233
234/// Port of `execbuiltin(LinkList args, LinkList assigns, Builtin bn)` from Src/builtin.c:250.
235///
236/// C: `int execbuiltin(LinkList args, LinkList assigns, Builtin bn)` —
237/// execute a builtin handler function after parsing the arguments.
238///
239/// Walks `bn->optstr` against `args`, populating `ops.ind[c]` (`|= 1`
240/// for `-X`, `|= 2` for `+X`, `<< 2` arg-index for opts taking args
241/// per the `:`/`::`/`:%` suffix convention), then calls
242/// `bn->handlerfunc(name, argv, &ops, bn->funcid)`.
243///
244/// Signature note: C consumes the name via `ugetnode(args)` first
245/// (c:262); the Rust port receives `args` without the name and reads
246/// `bn->node.nam` directly. C's `LinkList assigns` ports to
247/// `Vec<asgment>` (closer to the C type than the earlier
248/// `Vec<(String, String)>` pair-tuple). `assignfunc` handler dispatch
249/// (c:495-502) — BINF_ASSIGN builtins taking two argument lists —
250/// isn't ported (no Rust-side caller passes a non-empty `assigns`),
251/// so XTRACE prints the structure but BINF_ASSIGN dispatch falls
252/// through to the plain handler.
253pub fn execbuiltin(args: Vec<String>, assigns: Vec<crate::ported::zsh_h::asgment>, // c:250
254 bn: *mut crate::ported::zsh_h::builtin) -> i32 {
255 if bn.is_null() {
256 return 1;
257 }
258 let bn_ref = unsafe { &*bn };
259
260 // c:252-254 — locals.
261 let pp: Option<&str>; // c:252 char *pp
262 let name: String; // c:252 char *name
263 let mut optstr: Option<String>; // c:252 char *optstr
264 let mut flags: i32; // c:253 int flags
265 let mut argc: i32; // c:253 int argc
266 let mut execop: u8; // c:253 int execop
267 let xtr: bool = isset(XTRACE); // c:253 int xtr = isset(XTRACE)
268
269 // c:256-259 — `memset(ops.ind, 0, ...); ops.args = NULL; ops.argscount=ops.argsalloc=0;`
270 let mut ops = options { ind: [0u8; MAX_OPS], args: Vec::new(), // c:257
271 argscount: 0, argsalloc: 0 }; // c:258-259
272
273 // c:262 — `name = (char *) ugetnode(args);` — Rust reads bn.node.nam.
274 name = bn_ref.node.nam.clone(); // c:262
275
276 // c:264-268 — `if (!bn->handlerfunc)` early-exit.
277 if bn_ref.handlerfunc.is_none() { // c:264
278 return 1; // c:267
279 }
280
281 // c:270-271 — `flags = bn->node.flags; optstr = bn->optstr;`
282 flags = bn_ref.node.flags; // c:270
283 optstr = bn_ref.optstr.clone(); // c:271
284
285 // c:275 — `argc = countlinknodes(args);` — total argv length.
286 argc = args.len() as i32; // c:275
287
288 // c:284-293 — `VARARR(char *, argarr, argc+1)` + copy args into argarr.
289 let argarr: Vec<String> = args; // c:284 argarr[]
290 let mut argv: usize = 0; // c:285 char **argv = argarr;
291
292 // c:296-411 — option parser body.
293 if let Some(ref os) = optstr.clone() { // c:296
294 let optstr_local = os.clone();
295 let mut optstr_bytes: Vec<u8> = optstr_local.into_bytes();
296 let mut skipinvalid = (flags & BINF_SKIPINVALID as i32) != 0;
297 // c:297 — `char *arg = *argv;`
298 loop {
299 // c:300-303 — outer arg-by-arg loop guard:
300 // `arg && ((sense = (*arg == '-')) || ((flags & BINF_PLUSOPTS) && *arg == '+'))`.
301 let arg_str: String = match argarr.get(argv) {
302 Some(s) => s.clone(),
303 None => break,
304 };
305 let arg_bytes = arg_str.as_bytes();
306 if arg_bytes.is_empty() { break; }
307 let sense: i32 = if arg_bytes[0] == b'-' { 1 } else { 0 }; // c:302
308 if sense == 0 && !((flags & BINF_PLUSOPTS as i32) != 0 // c:303
309 && arg_bytes[0] == b'+') {
310 break;
311 }
312 // c:305 — `if (!(flags & BINF_KEEPNUM) && idigit(arg[1])) break;`
313 if (flags & BINF_KEEPNUM as i32) == 0 // c:305
314 && arg_bytes.len() >= 2
315 && arg_bytes[1].is_ascii_digit() {
316 break;
317 }
318 // c:308 — `if ((flags & BINF_SKIPDASH) && !arg[1]) break;`
319 if (flags & BINF_SKIPDASH as i32) != 0 && arg_bytes.len() == 1 { // c:308
320 break;
321 }
322 // c:310-317 — `--` end-of-options if BINF_DASHDASHVALID.
323 if (flags & BINF_DASHDASHVALID as i32) != 0 && arg_str == "--" { // c:310
324 argv += 1; // c:315
325 break; // c:316
326 }
327 // c:327-332 — `BINF_SKIPINVALID`: if any char in arg[1..] is
328 // not in optstr, the whole arg is treated as a positional.
329 if skipinvalid { // c:327
330 let mut all_known = true;
331 for &c in &arg_bytes[1..] {
332 if !optstr_bytes.contains(&c) { all_known = false; break; }
333 }
334 if !all_known { break; } // c:331
335 }
336 // c:335-336 — `if (arg[1] == '-') arg++;` — consume the
337 // second `-` of `--long-style`.
338 let mut k: usize = 1; // walks arg[k..]
339 if arg_bytes.len() >= 2 && arg_bytes[1] == b'-' { // c:335
340 k = 2; // c:336
341 }
342 // c:337-341 — `if (!arg[1])` lone `-` / `+` indicator.
343 if arg_bytes.len() == k { // c:337
344 ops.ind[b'-' as usize] = 1; // c:338
345 if sense == 0 { // c:339
346 ops.ind[b'+' as usize] = 1; // c:340
347 }
348 }
349 // c:343-386 — inner loop over `*++arg` characters.
350 let mut bad_opt: Option<u8> = None;
351 while k < arg_bytes.len() { // c:343
352 let c = arg_bytes[k];
353 execop = c; // c:345
354 let optptr = optstr_bytes.iter().position(|&b| b == c); // c:345 strchr(optstr,c)
355 if let Some(optidx) = optptr { // c:345
356 ops.ind[c as usize] = if sense != 0 { 1 } else { 2 }; // c:346
357 // c:347 — `if (optptr[1] == ':')` — option takes arg.
358 if optidx + 1 < optstr_bytes.len() && optstr_bytes[optidx + 1] == b':' {
359 let mut argptr: Option<String> = None;
360 // c:349-352 — `if (optptr[2] == ':')` optional same-word.
361 if optidx + 2 < optstr_bytes.len() && optstr_bytes[optidx + 2] == b':' {
362 if k + 1 < arg_bytes.len() { // c:350
363 argptr = Some(String::from_utf8_lossy(&arg_bytes[k+1..]).into_owned()); // c:351
364 }
365 } else if optidx + 2 < optstr_bytes.len() && optstr_bytes[optidx + 2] == b'%' {
366 // c:353-359 — `:%` numeric optional same or next word.
367 if k + 1 < arg_bytes.len() && arg_bytes[k+1].is_ascii_digit() {
368 argptr = Some(String::from_utf8_lossy(&arg_bytes[k+1..]).into_owned());
369 } else if let Some(nxt) = argarr.get(argv + 1) {
370 if !nxt.is_empty() && nxt.as_bytes()[0].is_ascii_digit() {
371 argv += 1; // c:359 arg = *++argv
372 argptr = Some(nxt.clone());
373 }
374 }
375 } else {
376 // c:360-370 — plain `:` mandatory arg.
377 if k + 1 < arg_bytes.len() { // c:362
378 argptr = Some(String::from_utf8_lossy(&arg_bytes[k+1..]).into_owned()); // c:363
379 } else if let Some(nxt) = argarr.get(argv + 1) {
380 argv += 1; // c:364 arg = *++argv
381 argptr = Some(nxt.clone()); // c:365
382 } else {
383 // c:366-370 — `argument expected: -%c`.
384 crate::ported::utils::zwarnnam(&name,
385 &format!("argument expected: -{}", execop as char)); // c:367-368
386 return 1; // c:369
387 }
388 }
389 if let Some(ap) = argptr { // c:372
390 // c:373-377 — new_optarg overflow.
391 if new_optarg(&mut ops) != 0 { // c:373
392 crate::ported::utils::zwarnnam(&name,
393 "too many option arguments"); // c:374-375
394 return 1; // c:376
395 }
396 // c:378 — `ops.ind[execop] |= ops.argscount << 2;`
397 ops.ind[execop as usize] |= (ops.argscount as u8) << 2;
398 // c:379 — `ops.args[ops.argscount-1] = argptr;`
399 ops.args[(ops.argscount - 1) as usize] = ap;
400 // c:380-381 — `while (arg[1]) arg++;` consume the rest.
401 k = arg_bytes.len();
402 }
403 }
404 k += 1;
405 } else {
406 bad_opt = Some(c); // c:385 break
407 break;
408 }
409 }
410 // c:389-394 — if we exited mid-arg on a bad char, emit "bad option".
411 if let Some(badc) = bad_opt { // c:389
412 crate::ported::utils::zwarnnam(&name,
413 &format!("bad option: {}{}",
414 if sense != 0 { '-' } else { '+' }, badc as char)); // c:392
415 return 1; // c:393
416 }
417 // c:395 — `arg = *++argv;`
418 argv += 1; // c:395
419 // c:398-402 — BINF_PRINTOPTS R-mode switch to "ne" optstr.
420 if (flags & BINF_PRINTOPTS as i32) != 0 // c:398
421 && ops.ind[b'R' as usize] != 0
422 && ops.ind[b'f' as usize] == 0 {
423 optstr_bytes = b"ne".to_vec(); // c:400
424 flags |= BINF_SKIPINVALID as i32; // c:401
425 skipinvalid = true;
426 }
427 // c:404-405 — `if (ops.ind['-']) break;` — `--` terminates.
428 if ops.ind[b'-' as usize] != 0 { // c:404
429 break;
430 }
431 }
432 let _ = optstr_bytes;
433 } else if (flags & BINF_HANDLES_OPTS as i32) == 0 // c:407
434 && argarr.get(argv).map(|s| s == "--").unwrap_or(false) { // c:408
435 // c:409-410 — `ops.ind['-'] = 1; argv++;`
436 ops.ind[b'-' as usize] = 1; // c:409
437 argv += 1; // c:410
438 }
439 // Suppress optstr-unused warnings on the `else` path.
440 let _ = optstr.take();
441
442 // c:414-421 — apply `bn->defopts` defaults.
443 pp = bn_ref.defopts.as_deref(); // c:414
444 if let Some(pp_str) = pp { // c:414
445 for &b in pp_str.as_bytes() { // c:415
446 if ops.ind[b as usize] == 0 { // c:417
447 ops.ind[b as usize] = 1; // c:418
448 }
449 }
450 }
451
452 // c:424 — `argc -= argv - argarr;` — subtract consumed flag args.
453 argc -= argv as i32; // c:424
454
455 // c:426-429 — errflag check.
456 let ef = crate::ported::utils::errflag.load(std::sync::atomic::Ordering::Relaxed);
457 if (ef & ERRFLAG_ERROR) != 0 { // c:426
458 crate::ported::utils::errflag.fetch_and(!ERRFLAG_ERROR, std::sync::atomic::Ordering::Relaxed); // c:427
459 return 1; // c:428
460 }
461
462 // c:432-436 — argc bounds check.
463 if argc < bn_ref.minargs // c:432
464 || (argc > bn_ref.maxargs && bn_ref.maxargs != -1) {
465 crate::ported::utils::zwarnnam(&name, // c:433
466 if argc < bn_ref.minargs { "not enough arguments" }
467 else { "too many arguments" }); // c:434
468 return 1; // c:435
469 }
470
471 // c:438-494 — display execution trace information, if required.
472 if xtr { // c:439
473 // c:440-441 — `char **fullargv = argarr;` — use FULL argv
474 // (including consumed option words) so XTRACE shows what the
475 // user typed, not the option-stripped tail.
476 let fullargv = &argarr; // c:441
477 crate::ported::utils::printprompt4(); // c:442
478 // c:443 — `fprintf(xtrerr, "%s", name);`
479 eprint!("{}", name); // c:443
480 // c:444-447 — `while (*fullargv) { fputc(' ',xtrerr); quotedzputs(...); }`
481 for s in fullargv { // c:444
482 eprint!(" "); // c:445 fputc(' ', xtrerr)
483 eprint!("{}", crate::ported::utils::quotedzputs(s)); // c:446
484 }
485 // c:448-491 — `if (assigns) { for (node = firstnode(assigns); ...) }`.
486 for asg in &assigns { // c:450 firstnode/incnode
487 eprint!(" "); // c:452 fputc(' ', xtrerr)
488 eprint!("{}", crate::ported::utils::quotedzputs(&asg.name)); // c:453
489 if (asg.flags & crate::ported::zsh_h::ASG_ARRAY) != 0 { // c:454
490 eprint!("=("); // c:455
491 if let Some(ref list) = asg.array { // c:456
492 if (asg.flags & crate::ported::zsh_h::ASG_KEY_VALUE) != 0 { // c:457
493 // c:458-473 — `LinkNode keynode, valnode;` walk
494 // alternating key/value pairs, emitting
495 // `[key]=value` per pair. Uses the typed
496 // `LinkList<String>` accessors from
497 // `src/ported/linklist.rs` which port the
498 // `firstnode` / `nextnode` / `getdata` macros
499 // from `Src/zsh.h:576-588`.
500 let mut keynode = list.firstnode(); // c:459
501 loop { // c:460
502 // c:461-462 — `if (!keynode) break;`
503 let kidx = match keynode { // c:461
504 Some(i) => i,
505 None => break, // c:462
506 };
507 // c:463-465 — `valnode = nextnode(keynode); if (!valnode) break;`
508 let vidx = match list.nextnode(kidx) { // c:463
509 Some(i) => i,
510 None => break, // c:465
511 };
512 // c:466-468 — `fputc('['); quotedzputs(getdata(keynode));`
513 eprint!("["); // c:466
514 if let Some(k) = list.getdata(kidx) { // c:467 getdata
515 eprint!("{}", crate::ported::utils::quotedzputs(k)); // c:467
516 }
517 // c:469 — `fprintf(stderr, "]=");`
518 eprint!("]="); // c:469
519 // c:470-471 — `quotedzputs(getdata(valnode));`
520 if let Some(v) = list.getdata(vidx) { // c:470
521 eprint!("{}", crate::ported::utils::quotedzputs(v)); // c:470
522 }
523 // c:472 — `keynode = nextnode(valnode);`
524 keynode = list.nextnode(vidx); // c:472
525 }
526 } else { // c:474
527 // c:475-482 — plain array emit: walk every node
528 // and emit ` <quotedzputs(elem)>`.
529 let mut arrnode = list.firstnode(); // c:476
530 while let Some(idx) = arrnode { // c:477
531 eprint!(" "); // c:479 fputc(' ', xtrerr)
532 if let Some(elem) = list.getdata(idx) { // c:480 getdata
533 eprint!("{}", crate::ported::utils::quotedzputs(elem)); // c:480
534 }
535 arrnode = list.nextnode(idx); // c:478 incnode
536 }
537 }
538 }
539 eprint!(" )"); // c:485
540 } else if let Some(ref scalar) = asg.scalar { // c:486
541 eprint!("="); // c:487 fputc('=', xtrerr)
542 eprint!("{}", crate::ported::utils::quotedzputs(scalar)); // c:488
543 }
544 }
545 // c:492-493 — `fputc('\n', xtrerr); fflush(xtrerr);`
546 eprintln!(); // c:492
547 // c:493 — fflush is automatic on `eprintln!` (stderr line-buffered).
548 }
549
550 // c:506 — `return (*(bn->handlerfunc))(name, argv, &ops, bn->funcid);`
551 let trimmed: Vec<String> = argarr[argv..].to_vec();
552 let handler = bn_ref.handlerfunc.expect("handlerfunc checked at c:264");
553 handler(&name, &trimmed, &ops, bn_ref.funcid) // c:506
554}
555
556/// Port of `bin_enable(char *name, char **argv, Options ops, int func)` from Src/builtin.c:517.
557/// C: `int bin_enable(char *name, char **argv, Options ops, int func)` —
558/// enable/disable hashtab entries (default builtins; `-f`/`-r`/`-s`/`-a`
559/// pick alternate tables); `-p` routes to pat_enables (pattern toggles).
560/// WARNING: param names don't match C — Rust=(name, argv, func) vs C=(name, argv, ops, func)
561pub fn bin_enable(name: &str, argv: &[String], // c:517
562 ops: &crate::ported::zsh_h::options, func: i32) -> i32 {
563 enum Tab { Builtin, Shfunc, Reswd, Alias, SufAlias }
564 let mut returnval = 0i32; // c:524
565 let mut match_count = 0i32; // c:524
566 // c:527-538 — `-p` early-out + table selection.
567 if OPT_ISSET(ops, b'p') { // c:527
568 // c:528 — `return pat_enables(name, argv, func == BIN_ENABLE);`
569 return pat_enables(name, argv, func == BIN_ENABLE); // c:528
570 }
571 let tab = if OPT_ISSET(ops, b'f') { Tab::Shfunc } // c:529
572 else if OPT_ISSET(ops, b'r') { Tab::Reswd } // c:531
573 else if OPT_ISSET(ops, b's') { Tab::SufAlias } // c:533
574 else if OPT_ISSET(ops, b'a') { Tab::Alias } // c:535
575 else { Tab::Builtin }; // c:537
576
577 // c:540-547 — flags1/flags2 set based on enable vs disable direction.
578 let enable = func == BIN_ENABLE;
579 let (flags1, flags2) = if enable { // c:541
580 (0u32, DISABLED as u32) // c:542
581 } else {
582 (DISABLED as u32, 0u32) // c:545
583 };
584
585 // Helper closures over the chosen table.
586 let toggle_one = |tab: &Tab, nm: &str, on: bool| -> bool {
587 match tab {
588 Tab::Alias => crate::ported::hashtable::aliastab_lock().write()
589 .map(|mut t| if on { t.enable(nm) } else { t.disable(nm) })
590 .unwrap_or(false),
591 Tab::SufAlias => crate::ported::hashtable::sufaliastab_lock().write()
592 .map(|mut t| if on { t.enable(nm) } else { t.disable(nm) })
593 .unwrap_or(false),
594 // c:541-547 — `enable`/`disable -r` toggles DISABLED on the
595 // reswdtab entry; reswords resolve through getreswdnode in
596 // the lexer so toggling here is enough to mask/unmask.
597 Tab::Reswd => {
598 let exists = crate::ported::hashtable::reswdtab_lock().read()
599 .map(|t| t.get_including_disabled(nm).is_some())
600 .unwrap_or(false);
601 if !exists { return false; }
602 crate::ported::hashtable::reswdtab_lock().write()
603 .map(|mut t| if on { t.enable(nm) } else { t.disable(nm) })
604 .unwrap_or(false)
605 }
606 // c:541-547 — `enable`/`disable -f` toggles DISABLED on the
607 // shfunctab entry; ports to disableshfuncnode/enableshfuncnode
608 // which also unsettrap/settrap TRAP* fns.
609 Tab::Shfunc => {
610 let exists = crate::ported::hashtable::shfunctab_lock().read()
611 .map(|t| t.get_including_disabled(nm).is_some())
612 .unwrap_or(false);
613 if !exists { return false; }
614 if on {
615 crate::ported::hashtable::enableshfuncnode(nm);
616 } else {
617 crate::ported::hashtable::disableshfuncnode(nm);
618 }
619 true
620 }
621 // c:541-547 — `enable`/`disable` toggles DISABLED on the
622 // builtin. The C struct `builtintab` stores DISABLED in
623 // `node.flags`; Rust port keeps `builtintab` as an
624 // immutable static lookup and tracks the disabled set in
625 // BUILTINS_DISABLED so dispatch can mask the entry.
626 Tab::Builtin => {
627 if createbuiltintable().get(nm).is_none() { return false; }
628 if let Ok(mut set) = BUILTINS_DISABLED.lock() {
629 if on { set.remove(nm); } else { set.insert(nm.to_string()); }
630 return true;
631 }
632 false
633 }
634 }
635 };
636 let collect_names = |tab: &Tab| -> Vec<String> {
637 match tab {
638 Tab::Alias => crate::ported::hashtable::aliastab_lock().read()
639 .map(|t| t.iter().map(|(n,_)| n.clone()).collect()).unwrap_or_default(),
640 Tab::SufAlias => crate::ported::hashtable::sufaliastab_lock().read()
641 .map(|t| t.iter().map(|(n,_)| n.clone()).collect()).unwrap_or_default(),
642 Tab::Reswd => crate::ported::hashtable::reswdtab_lock().read()
643 .map(|t| t.iter().map(|(n,_)| n.clone()).collect()).unwrap_or_default(),
644 Tab::Shfunc => crate::ported::hashtable::shfunctab_lock().read()
645 .map(|t| t.iter().map(|(n,_)| n.clone()).collect()).unwrap_or_default(),
646 Tab::Builtin => createbuiltintable().keys().cloned().collect(),
647 }
648 };
649
650 // c:553-558 — no-args list.
651 if argv.is_empty() { // c:553
652 crate::ported::mem::queue_signals(); // c:554
653 // c:555 — `scanhashtable(ht, 1, flags1, flags2, ht->printnode, 0);`
654 for nm in collect_names(&tab) {
655 // print only nodes whose flags satisfy (flags & flags1)==flags1
656 // && (flags & flags2)==0. Best-effort: print all names.
657 println!("{}", nm);
658 }
659 let _ = (flags1, flags2);
660 crate::ported::mem::unqueue_signals(); // c:556
661 return 0; // c:557
662 }
663
664 // c:561-580 — `-m` glob branch.
665 if OPT_ISSET(ops, b'm') { // c:561
666 for arg in argv { // c:562
667 crate::ported::mem::queue_signals(); // c:563
668 let pprog = crate::ported::pattern::patcompile(arg, // c:566
669 crate::ported::zsh_h::PAT_HEAPDUP, None);
670 if let Some(prog) = pprog {
671 for nm in collect_names(&tab) {
672 if crate::ported::pattern::pattry(&prog, &nm) { // c:567
673 if toggle_one(&tab, &nm, enable) {
674 match_count += 1; // c:567
675 }
676 }
677 }
678 } else {
679 crate::ported::utils::zwarnnam(name,
680 &format!("bad pattern : {}", arg)); // c:572
681 returnval = 1; // c:573
682 }
683 crate::ported::mem::unqueue_signals(); // c:575
684 }
685 if match_count == 0 { // c:579
686 returnval = 1; // c:580
687 }
688 return returnval; // c:581
689 }
690
691 // c:585-594 — literal-name dispatch.
692 crate::ported::mem::queue_signals(); // c:585
693 for arg in argv { // c:586
694 if !toggle_one(&tab, arg, enable) { // c:587
695 crate::ported::utils::zwarnnam(name,
696 &format!("no such hash table element: {}", arg)); // c:590
697 returnval = 1; // c:591
698 }
699 }
700 crate::ported::mem::unqueue_signals(); // c:594
701 returnval // c:595
702}
703
704/// Port of `bin_set(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:601.
705/// C: `int bin_set(char *nam, char **args, UNUSED(Options ops),
706/// UNUSED(int func))` — set shell options, declare arrays,
707/// replace positional params, or display variables.
708/// WARNING: param names don't match C — Rust=(nam, args, _func) vs C=(nam, args, ops, func)
709pub fn bin_set(nam: &str, args: &[String], // c:601
710 _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
711
712 // PFA-SMR aspect: emit setopt/unsetopt events for the POSIX
713 // `set -o NAME` / `set +o NAME` form. This is the third option
714 // syntax (alongside setopt NAME / unsetopt NAME); a recorder
715 // user expects all three to surface in `zwhere -k setopt`.
716 #[cfg(feature = "recorder")]
717 if crate::recorder::is_enabled() && !args.is_empty() {
718 let ctx = crate::recorder::recorder_ctx_global();
719 let mut iter = args.iter().peekable();
720 while let Some(a) = iter.next() {
721 match a.as_str() {
722 "-o" => {
723 if let Some(name) = iter.next() {
724 crate::recorder::emit_setopt(name, ctx.clone());
725 }
726 }
727 "+o" => {
728 if let Some(name) = iter.next() {
729 crate::recorder::emit_unsetopt(name, ctx.clone());
730 }
731 }
732 _ => {}
733 }
734 }
735 }
736
737 let mut argv: Vec<String> = args.to_vec();
738 let mut hadopt = false; // c:603
739 let mut hadplus = false; // c:603
740 let mut hadend = false; // c:603
741 let mut sort: i32 = 0; // c:603
742 let mut array: i32 = 0; // c:603
743 let mut arrayname: Option<String> = None; // c:604
744
745 // c:608-614 — sh-compat: bare `set -` → +xv.
746 if !EMULATION(EMULATE_ZSH) // c:608
747 && !argv.is_empty() && argv[0] == "-"
748 {
749 // c:610-611 — `dosetopt(VERBOSE, 0, 0, opts); dosetopt(XTRACE, 0, 0, opts);`
750 let v = crate::ported::options::optlookup("verbose");
751 let x = crate::ported::options::optlookup("xtrace");
752 crate::ported::options::dosetopt(v, 0, 0); // c:610
753 crate::ported::options::dosetopt(x, 0, 0); // c:611
754 if argv.len() == 1 { return 0; } // c:612-613
755 argv.remove(0);
756 }
757
758 // c:617-668 — top-level option-arg loop.
759 let mut idx = 0usize;
760 'outer: while idx < argv.len() // c:617
761 && (argv[idx].starts_with('-') || argv[idx].starts_with('+'))
762 {
763 let arg = argv[idx].clone();
764 let action = arg.starts_with('-'); // c:619
765 if !action { hadplus = true; } // c:620
766 // c:621-622 — bare `-` / `+` → "--"
767 let body: String = if arg.len() == 1 { "--".to_string() }
768 else { arg.clone() };
769 // c:623 — `while (*++*args)`
770 let chars: Vec<char> = body[1..].chars().collect();
771 let mut ci = 0usize;
772 while ci < chars.len() { // c:623
773 let c = chars[ci];
774 if c != '-' || action { hadopt = true; } // c:626
775 // c:628-632 — `--` end-of-options.
776 if c == '-' { // c:628
777 hadend = true; // c:629
778 idx += 1; // c:630 args++
779 break 'outer;
780 }
781 // c:633-645 — `o` long-option name follows.
782 if c == 'o' { // c:633
783 let optname: String = if ci + 1 < chars.len() {
784 chars[ci + 1..].iter().collect::<String>()
785 } else {
786 idx += 1;
787 if idx >= argv.len() { // c:636
788 // c:637 — `printoptionstates(hadplus); inittyptab(); return 0;`
789 return 0;
790 }
791 argv[idx].clone()
792 };
793 let optno = crate::ported::options::optlookup(&optname); // c:642
794 if optno == 0 { // c:642
795 crate::ported::utils::zerr(&format!(
796 "no such option: {}", optname)); // c:642
797 } else if crate::ported::options::dosetopt(optno,
798 if action { 1 } else { 0 }, 0) != 0 // c:644
799 {
800 crate::ported::utils::zerr(&format!(
801 "can't change option: {}", optname)); // c:644
802 }
803 break;
804 }
805 // c:646-657 — `A` array-mode (with optional name arg).
806 if c == 'A' { // c:646
807 array = if action { 1 } else { -1 }; // c:649
808 let nameopt: Option<String> = if ci + 1 < chars.len() {
809 Some(chars[ci + 1..].iter().collect::<String>())
810 } else if idx + 1 < argv.len() {
811 idx += 1;
812 Some(argv[idx].clone())
813 } else { None };
814 arrayname = nameopt.clone();
815 if arrayname.is_none() { // c:651
816 idx += 1;
817 break 'outer;
818 }
819 let ksharrays = crate::ported::zsh_h::isset(crate::ported::options::optlookup("ksharrays"));
820 if !ksharrays { // c:653
821 idx += 1; // c:655 args++
822 break 'outer; // c:656
823 }
824 break;
825 }
826 // c:659-660 — `s` sort flag.
827 if c == 's' { // c:659
828 sort = if action { 1 } else { -1 }; // c:660
829 } else {
830 // c:662-666 — short-option letter: optlookupc + dosetopt.
831 let optno = crate::ported::options::optlookupc(c); // c:663
832 if optno == 0 { // c:663
833 crate::ported::utils::zerr(&format!("bad option: -{}", c)); // c:663
834 } else if crate::ported::options::dosetopt(optno,
835 if action { 1 } else { 0 }, 0) != 0 // c:664
836 {
837 crate::ported::utils::zerr(&format!("can't change option: -{}", c)); // c:664
838 }
839 }
840 ci += 1;
841 }
842 idx += 1; // c:668
843 }
844 let _ = nam;
845
846 // c:676 — `queue_signals();`
847 crate::ported::mem::queue_signals();
848 let remaining = &argv[idx..];
849
850 // c:678-694 — display path when no array/no args.
851 if arrayname.is_none() { // c:678
852 if !hadopt && remaining.is_empty() { // c:679
853 // c:680-681 — `scanhashtable(paramtab, 1, 0, 0,
854 // paramtab->printnode, hadplus ? PRINT_NAMEONLY : 0);`
855 //
856 // C walks the paramtab (sorted=1 → alphabetical). The previous
857 // Rust port walked `std::env::vars()` — the OS environment.
858 // Shell-internal vars (not exported to env) would never appear
859 // in the `set` listing, diverging from C where ALL paramtab
860 // entries are emitted.
861 //
862 // Same family of bug as the prior bin_unset -m fix.
863 let mut entries: Vec<(String, String)> = {
864 let tab = crate::ported::params::paramtab().read().unwrap();
865 tab.iter()
866 .filter(|(_, pm)| {
867 // c:scanhashtable filter: skip PM_UNSET. C also
868 // skips entries with flags2=0 (none extra filtered).
869 (pm.node.flags as u32 & crate::ported::zsh_h::PM_UNSET) == 0
870 })
871 .map(|(k, pm)| {
872 let v = pm.u_str.clone().unwrap_or_default();
873 (k.clone(), v)
874 })
875 .collect()
876 };
877 // c:680 sorted=1 → meta-aware sort via hnamcmp (already fixed
878 // to use ztrcmp earlier in the series).
879 entries.sort_by(|a, b| {
880 crate::ported::hashtable::hnamcmp(&a.0, &b.0)
881 });
882 for (k, v) in entries {
883 if hadplus { // c:681 PRINT_NAMEONLY
884 println!("{}", k);
885 } else {
886 println!("{}={}", k,
887 crate::ported::utils::quotedzputs(&v));
888 }
889 }
890 }
891 if array != 0 { // c:684
892 // c:685-687 — `scanhashtable(paramtab, 1, PM_ARRAY, 0,
893 // paramtab->printnode, hadplus ? PRINT_NAMEONLY : 0)`.
894 // Walk paramtab filtering by PM_ARRAY and emit each as
895 // `name=(elem1 elem2 ...)`. Previous Rust port stubbed
896 // this body with a "nothing to enumerate" comment — but
897 // paramtab does store arrays in `u_arr`, so `set -A` (no
898 // name) MUST list every PM_ARRAY entry. Sorted via
899 // hnamcmp (meta-aware compare) per `sorted=1` in the C
900 // scanhashtable call.
901 let mut arr_entries: Vec<(String, Vec<String>)> = {
902 use crate::ported::zsh_h::{PM_ARRAY, PM_TYPE};
903 let tab = crate::ported::params::paramtab().read().unwrap();
904 tab.iter()
905 .filter(|(_, pm)| {
906 PM_TYPE(pm.node.flags as u32) == PM_ARRAY
907 && (pm.node.flags as u32
908 & crate::ported::zsh_h::PM_UNSET) == 0
909 })
910 .map(|(k, pm)| {
911 (k.clone(), pm.u_arr.clone().unwrap_or_default())
912 })
913 .collect()
914 };
915 arr_entries.sort_by(|a, b|
916 crate::ported::hashtable::hnamcmp(&a.0, &b.0)); // c:685 sorted=1
917 for (k, arr) in arr_entries {
918 if hadplus { // c:686 PRINT_NAMEONLY
919 println!("{}", k);
920 } else {
921 let quoted: Vec<String> = arr.iter()
922 .map(|v| crate::ported::utils::quotedzputs(v))
923 .collect();
924 println!("{}=({})", k, quoted.join(" "));
925 }
926 }
927 }
928 if remaining.is_empty() && !hadend { // c:688
929 crate::ported::mem::unqueue_signals();
930 return 0; // c:690
931 }
932 }
933
934 // c:693-695 — `set -s` sort.
935 let sorted: Vec<String> = if sort != 0 {
936 let mut v = remaining.to_vec();
937 if sort < 0 { v.sort_by(|a, b| b.cmp(a)); } else { v.sort(); }
938 v
939 } else {
940 remaining.to_vec()
941 };
942
943 // c:696-708 — array assign or positional-param replace.
944 if array != 0 { // c:696
945 // c:697-708 — build array; `array < 0` appends to existing $name.
946 let aname = arrayname.unwrap_or_default();
947 let mut new_arr: Vec<String> = sorted;
948 if array < 0 { // c:701
949 // c:702-704 — `if ((a = getaparam(arrayname)) && arrlen_gt(a, len))`.
950 // Read paramtab.u_arr directly; was using `:`-
951 // split env value as a fake array.
952 let existing: Vec<String> = {
953 let tab = crate::ported::params::paramtab().read().unwrap();
954 tab.get(&aname).and_then(|pm| pm.u_arr.clone()).unwrap_or_default()
955 };
956 if existing.len() > new_arr.len() { // c:702
957 new_arr.extend(existing.into_iter().skip(new_arr.len())); // c:703
958 }
959 }
960 // c:709 — `setaparam(arrayname, x);`. Use setaparam (array
961 // setter) so the value lands as a proper PM_ARRAY,
962 // not a colon-joined scalar.
963 crate::ported::params::setaparam(&aname, new_arr);
964 } else {
965 // c:711-712 — `freearray(pparams); pparams = zarrdup(args);`
966 // PPARAMS is the single source of truth; fusevm reads via
967 // `exec.pparams()`.
968 if let Ok(mut pp) = PPARAMS.lock() {
969 *pp = sorted; // c:712
970 }
971 }
972 crate::ported::mem::unqueue_signals(); // c:714
973 0 // c:715
974}
975
976/// Port of `bin_pwd(UNUSED(char *name), UNUSED(char **argv), Options ops, UNUSED(int func))` from Src/builtin.c:728.
977/// C: `int bin_pwd(UNUSED(char *name), UNUSED(char **argv), Options ops,
978/// UNUSED(int func))` — `-r`/`-P` or (CHASELINKS && !`-L`) →
979/// print resolved cwd via zgetcwd; else print the cached `pwd`.
980// pwd: display the name of the current directory // c:728
981/// WARNING: param names don't match C — Rust=(_name, _argv, _func) vs C=(name, argv, ops, func)
982pub fn bin_pwd(_name: &str, _argv: &[String], // c:728
983 ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
984 let chaselinks = crate::ported::zsh_h::isset(crate::ported::options::optlookup("chaselinks"));
985 // c:730-731 — `if (OPT_ISSET(ops,'r') || OPT_ISSET(ops,'P') ||
986 // (isset(CHASELINKS) && !OPT_ISSET(ops,'L')))`
987 if OPT_ISSET(ops, b'r') || OPT_ISSET(ops, b'P') // c:730
988 || (chaselinks && !OPT_ISSET(ops, b'L')) // c:731
989 {
990 // c:732 — `printf("%s\n", zgetcwd());`
991 println!("{}", crate::ported::utils::zgetcwd().unwrap_or_default()); // c:732
992 } else {
993 // c:734 — `zputs(pwd, stdout); putchar('\n');`. C reads the
994 // shell-internal `pwd` global (Src/params.c:108). The
995 // canonical Rust accessor is `getsparam("PWD")` which reads
996 // from the paramtab (the source-of-truth backing for PWD).
997 //
998 // Previously this used `std::env::var("PWD")` which reads
999 // the OS environment — divergent. The OS env var is only
1000 // sync'd to the paramtab on export; the paramtab can hold
1001 // a more recent value, and `unset PWD; cd /foo; pwd` would
1002 // print the wrong thing under the env-var path (env was
1003 // already unset, so the read fell through to zgetcwd
1004 // bypassing the just-set paramtab PWD).
1005 let pwd = crate::ported::params::getsparam("PWD")
1006 .unwrap_or_else(|| crate::ported::utils::zgetcwd().unwrap_or_default());
1007 println!("{}", pwd); // c:734
1008 }
1009 0 // c:737
1010}
1011
1012/// Port of `bin_dirs(UNUSED(char *name), char **argv, Options ops, UNUSED(int func))` from Src/builtin.c:749.
1013/// C: `int bin_dirs(UNUSED(char *name), char **argv, Options ops, ...)` —
1014/// list dirstack (default / -v / -p / -l) or replace it with argv.
1015// dirs: list the directory stack, or replace it with a provided list // c:749
1016/// WARNING: param names don't match C — Rust=(_name, argv, _func) vs C=(name, argv, ops, func)
1017pub fn bin_dirs(_name: &str, argv: &[String], // c:749
1018 ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
1019 crate::ported::mem::queue_signals(); // c:753
1020 // c:755-756 — list mode: no args & no -c, OR -v / -p.
1021 if (argv.is_empty() && !OPT_ISSET(ops, b'c')) // c:755
1022 || OPT_ISSET(ops, b'v')
1023 || OPT_ISSET(ops, b'p')
1024 {
1025 let mut pos = 1; // c:760
1026 // c:763-769 — pick separator format.
1027 let fmt: &str = if OPT_ISSET(ops, b'v') { // c:763
1028 print!("0\t"); // c:764
1029 "\n{}\t" // c:765
1030 } else if OPT_ISSET(ops, b'p') { // c:767
1031 "\n"
1032 } else {
1033 " "
1034 };
1035 // c:771-774 — print pwd via fprintdir or zputs (`-l`).
1036 // Previous Rust port inlined a HOME-prefix replacement which
1037 // only abbreviated `$HOME/...` to `~/...` — missed every
1038 // user-defined nameddirtab entry (`hash -d proj=/big/path`).
1039 // Route through `utils::fprintdir` which calls `finddir`,
1040 // matching C's named-dir abbreviation.
1041 let pwd = crate::ported::params::getsparam("PWD")
1042 .unwrap_or_else(|| crate::ported::utils::zgetcwd().unwrap_or_default());
1043 if OPT_ISSET(ops, b'l') { // c:771
1044 print!("{}", pwd); // c:772
1045 } else {
1046 print!("{}", crate::ported::utils::fprintdir(&pwd)); // c:774
1047 }
1048 // c:775-781 — walk dirstack list.
1049 if let Ok(stack) = DIRSTACK.lock() { // c:775
1050 for entry in stack.iter() {
1051 if fmt == "\n{}\t" {
1052 print!("\n{}\t", pos);
1053 } else {
1054 print!("{}", fmt); // c:776
1055 }
1056 pos += 1; // c:776
1057 if OPT_ISSET(ops, b'l') { // c:777
1058 print!("{}", entry); // c:778
1059 } else {
1060 print!("{}", crate::ported::utils::fprintdir(entry)); // c:780
1061 }
1062 }
1063 }
1064 crate::ported::mem::unqueue_signals(); // c:783
1065 println!(); // c:784
1066 return 0; // c:785
1067 }
1068 // c:788-792 — replace dirstack with the supplied entries.
1069 if let Ok(mut stack) = DIRSTACK.lock() {
1070 stack.clear(); // c:790
1071 for arg in argv {
1072 stack.push(arg.clone()); // c:791
1073 }
1074 }
1075 crate::ported::mem::unqueue_signals(); // c:793
1076 0 // c:794
1077}
1078
1079/// Direct port of `void set_pwd_env(void)` from
1080/// `Src/builtin.c:800`. Refreshes both `$PWD` and `$OLDPWD` to mirror
1081/// the shell-side `pwd`/`oldpwd` globals. C clears `PM_READONLY` on
1082/// each if it's currently typed as scalar (paranoid guard for users
1083/// who did `typeset -r PWD`), then writes via `setsparam`.
1084///
1085/// Rust port reads `$PWD`/`$OLDPWD` from paramtab (the shell-side
1086/// truth), then writes them back via `setsparam` plus an OS-env
1087/// mirror so child processes inherit the values. Was a fake that
1088/// only wrote `getcwd()` into the OS env, bypassing paramtab and
1089/// silently dropping `$OLDPWD`.
1090pub fn set_pwd_env() { // c:800
1091 // c:805-810 — `if ((pm = paramtab->getnode("PWD")) && ...) pm->node.flags &= ~PM_READONLY;`
1092 // The PM_READONLY clear isn't ported (no PM_READONLY
1093 // consumer breaks downstream); the canonical
1094 // refresh goes through setsparam which handles the
1095 // flag set.
1096 // c:813 — `setsparam("PWD", pwd);`. Read paramtab's PWD if set;
1097 // fall back to getcwd so a fresh shell starts with PWD
1098 // populated.
1099 let pwd = crate::ported::params::getsparam("PWD")
1100 .or_else(|| std::env::current_dir().ok()
1101 .map(|p| p.to_string_lossy().into_owned()));
1102 if let Some(s) = pwd {
1103 crate::ported::params::setsparam("PWD", &s); // c:813
1104 std::env::set_var("PWD", &s);
1105 }
1106 // c:818 — `setsparam("OLDPWD", oldpwd);` mirror; only fires when
1107 // oldpwd is set (initially NULL on first shell).
1108 if let Some(s) = crate::ported::params::getsparam("OLDPWD") {
1109 std::env::set_var("OLDPWD", &s);
1110 }
1111}
1112
1113/// Port of `bin_cd(char *nam, char **argv, Options ops, int func)` from Src/builtin.c:840.
1114/// C: `int bin_cd(char *nam, char **argv, Options ops, int func)`.
1115///
1116/// Body (verbatim translation per c:842-859):
1117/// ```c
1118/// doprintdir = (doprintdir == -1);
1119/// chasinglinks = OPT_ISSET(ops,'P') ||
1120/// (isset(CHASELINKS) && !OPT_ISSET(ops,'L'));
1121/// queue_signals();
1122/// zpushnode(dirstack, ztrdup(pwd));
1123/// if (!(dir = cd_get_dest(nam, argv, OPT_ISSET(ops,'s'), func))) {
1124/// zsfree(getlinknode(dirstack));
1125/// unqueue_signals();
1126/// return 1;
1127/// }
1128/// cd_new_pwd(func, dir, OPT_ISSET(ops, 'q'));
1129/// unqueue_signals();
1130/// return 0;
1131/// ```
1132// cd, chdir, pushd, popd // c:796
1133pub fn bin_cd(nam: &str, argv: &[String], // c:840
1134 ops: &crate::ported::zsh_h::options, func: i32) -> i32 {
1135
1136 // c:844 — `doprintdir = (doprintdir == -1);`
1137 let prev = DOPRINTDIR.load(Ordering::Relaxed);
1138 DOPRINTDIR.store(if prev == -1 { 1 } else { 0 }, Ordering::Relaxed); // c:844
1139
1140 // c:846-847 — `chasinglinks = OPT_ISSET(ops,'P') ||
1141 // (isset(CHASELINKS) && !OPT_ISSET(ops,'L'));`
1142 let chase = OPT_ISSET(ops, b'P') // c:846
1143 || (crate::ported::zsh_h::isset(crate::ported::options::optlookup("chaselinks"))
1144 && !OPT_ISSET(ops, b'L'));
1145 CHASINGLINKS.store(chase as i32, Ordering::Relaxed);
1146
1147 crate::ported::mem::queue_signals(); // c:848
1148
1149 // c:849 — `zpushnode(dirstack, ztrdup(pwd));`. C uses the `pwd`
1150 // global (the in-shell logical cwd, kept in sync with
1151 // $PWD). Read from paramtab; fall back to getcwd if
1152 // unset.
1153 let pwd = crate::ported::params::getsparam("PWD")
1154 .unwrap_or_else(|| crate::ported::utils::zgetcwd().unwrap_or_default());
1155 if let Ok(mut d) = crate::ported::modules::parameter::DIRSTACK.lock() {
1156 d.insert(0, pwd); // c:849
1157 }
1158
1159 // c:850-854 — `if (!(dir = cd_get_dest(...))) { pop; unqueue; return 1; }`
1160 let dest = cd_get_dest(nam, argv, OPT_ISSET(ops, b's'), func);
1161 if dest.is_none() { // c:850
1162 // c:851 — `zsfree(getlinknode(dirstack));` — pop the placeholder.
1163 if let Ok(mut d) = crate::ported::modules::parameter::DIRSTACK.lock() {
1164 if !d.is_empty() { d.remove(0); } // c:851
1165 }
1166 crate::ported::mem::unqueue_signals(); // c:852
1167 return 1; // c:853
1168 }
1169 let dest_path = dest.unwrap();
1170
1171 // c:856 — `cd_new_pwd(func, dir, OPT_ISSET(ops, 'q'));`
1172 // Static-link path: do the actual chdir + PWD/OLDPWD env update.
1173 // c:1238 — `oldpwd = pwd;` snapshot pre-cd $PWD for $OLDPWD.
1174 // Read from paramtab (the canonical zsh-side `pwd`
1175 // global); was reading OS env which can lag behind.
1176 let old = crate::ported::params::getsparam("PWD");
1177 if std::env::set_current_dir(&dest_path).is_err() {
1178 // chdir failed — pop placeholder and bail.
1179 if let Ok(mut d) = crate::ported::modules::parameter::DIRSTACK.lock() {
1180 if !d.is_empty() { d.remove(0); }
1181 }
1182 crate::ported::mem::unqueue_signals();
1183 return 1;
1184 }
1185 if let Some(o) = old { // c:1239 oldpwd = pwd
1186 // c:1239 + setsparam path: write OLDPWD to paramtab so
1187 // subsequent expansions of $OLDPWD see the new value
1188 // (the OS env write below is the export side; the
1189 // shell-side read must come from paramtab).
1190 crate::ported::params::setsparam("OLDPWD", &o);
1191 std::env::set_var("OLDPWD", &o);
1192 }
1193 // c:1241 — `pwd = new_pwd;` writes the LOGICAL path (the dest
1194 // argument as given to cd, not `getcwd()`). Symlink resolution
1195 // only kicks in when `chasinglinks` is set (c:1203-1208,
1196 // c:1228-1231) — both fall back to `findpwd()`/`zgetcwd()`.
1197 // Earlier port called `std::env::current_dir()` (= `getcwd(3)`),
1198 // which always resolves symlinks (e.g. /tmp → /private/tmp on
1199 // macOS), breaking logical-PWD parity with zsh.
1200 let chase = CHASINGLINKS.load(std::sync::atomic::Ordering::Relaxed) != 0; // c:1203
1201 let pwd: String = if chase { // c:1203
1202 // c:1204 — `s = findpwd(new_pwd);` — resolved cwd.
1203 match std::env::current_dir() {
1204 Ok(c) => c.to_string_lossy().into_owned(),
1205 Err(_) => dest_path.clone(),
1206 }
1207 } else {
1208 dest_path.clone() // c:1241 pwd = new_pwd
1209 };
1210 // c:1242 — `setsparam("PWD", pwd);` + export side via env.
1211 crate::ported::params::setsparam("PWD", &pwd);
1212 std::env::set_var("PWD", &pwd);
1213 cd_new_pwd(func, 0, OPT_ISSET(ops, b'q') as i32); // c:856
1214
1215 crate::ported::mem::unqueue_signals(); // c:858
1216 0 // c:859
1217}
1218
1219/// Port of `cd_get_dest(char *nam, char **argv, int hard, int func)` from Src/builtin.c:865.
1220/// C: `static LinkNode cd_get_dest(char *nam, char **argv, int hard,
1221/// int func)` — resolve the `cd` argument (`-`, `+N`/`-N`,
1222/// bare → $HOME, two-arg substitution form) to a destination path.
1223/// Returns the resolved path on success, None on error (with the
1224/// appropriate zwarnnam already emitted).
1225/// WARNING: param names don't match C — Rust=() vs C=(nam, argv, hard, func)
1226pub fn cd_get_dest(nam: &str, argv: &[String], _hard: bool, func: i32) // c:865
1227 -> Option<String> {
1228
1229 if argv.is_empty() { // c:872
1230 // c:873-875 — popd needs at least 2 stack entries.
1231 if func == BIN_POPD {
1232 let depth = DIRSTACK.lock().map(|d| d.len()).unwrap_or(0);
1233 if depth < 2 { // c:873
1234 crate::ported::utils::zwarnnam(nam, "directory stack empty"); // c:874
1235 return None; // c:875
1236 }
1237 // c:885 — `dir = nextnode(firstnode(dirstack));`
1238 return DIRSTACK.lock().ok()
1239 .and_then(|d| d.get(1).cloned());
1240 }
1241 if func == BIN_PUSHD {
1242 // c:877 — `if (unset(PUSHDTOHOME)) dir = nextnode(firstnode(dirstack));`
1243 let pushdtohome = crate::ported::zsh_h::isset(crate::ported::options::optlookup("pushdtohome"));
1244 if !pushdtohome { // c:877
1245 return DIRSTACK.lock().ok()
1246 .and_then(|d| d.get(1).cloned());
1247 }
1248 }
1249 // c:880-884 — fall through to $HOME (paramtab, not OS env).
1250 match crate::ported::params::getsparam("HOME") {
1251 Some(h) if !h.is_empty() => Some(h), // c:884
1252 _ => {
1253 crate::ported::utils::zwarnnam(nam, "HOME not set"); // c:881
1254 None // c:882
1255 }
1256 }
1257 } else if argv.len() == 1 { // c:887
1258 let arg = &argv[0];
1259 DOPRINTDIR.fetch_add(1, Ordering::Relaxed); // c:891
1260 // c:892-908 — `+N`/`-N` numeric stack-index form.
1261 let posixcd = crate::ported::zsh_h::isset(crate::ported::options::optlookup("posixcd"));
1262 if !posixcd && arg.len() > 1
1263 && (arg.starts_with('+') || arg.starts_with('-'))
1264 && arg[1..].chars().all(|c| c.is_ascii_digit())
1265 {
1266 let dd: usize = arg[1..].parse().unwrap_or(0); // c:894
1267 let pushdminus = crate::ported::zsh_h::isset(crate::ported::options::optlookup("pushdminus"));
1268 let from_top = (arg.starts_with('+')) ^ pushdminus; // c:898
1269 return DIRSTACK.lock().ok().and_then(|d| {
1270 if from_top { d.get(dd).cloned() }
1271 else if d.len() > dd { d.get(d.len() - 1 - dd).cloned() }
1272 else { None }
1273 });
1274 }
1275 // c:910-911 — `-` alias for $OLDPWD; else literal arg.
1276 // C reads `oldpwd` global / `$OLDPWD` param;
1277 // route through paramtab via getsparam.
1278 if arg == "-" { // c:911
1279 DOPRINTDIR.fetch_sub(1, Ordering::Relaxed);
1280 crate::ported::params::getsparam("OLDPWD")
1281 } else {
1282 Some(arg.clone()) // c:911
1283 }
1284 } else {
1285 // c:914-924 — two-arg substitution: cd OLDPATTERN NEWPATTERN.
1286 // C reads `pwd` global / `$PWD` param via getsparam;
1287 // fall back to getcwd if the param isn't populated.
1288 let pwd = crate::ported::params::getsparam("PWD")
1289 .unwrap_or_else(|| crate::ported::utils::zgetcwd().unwrap_or_default());
1290 let pat = &argv[0];
1291 let new_pat = &argv[1];
1292 match pwd.find(pat.as_str()) { // c:917
1293 None => {
1294 crate::ported::utils::zwarnnam(nam,
1295 &format!("string not in pwd: {}", pat)); // c:918
1296 None // c:919
1297 }
1298 Some(idx) => {
1299 // c:921-924 — splice: pwd[..idx] + new_pat + pwd[idx+pat.len()..]
1300 let mut out = String::new();
1301 out.push_str(&pwd[..idx]); // c:921
1302 out.push_str(new_pat); // c:922
1303 out.push_str(&pwd[idx + pat.len()..]); // c:923
1304 DOPRINTDIR.fetch_add(1, Ordering::Relaxed);
1305 Some(out)
1306 }
1307 }
1308 }
1309}
1310
1311/// Port of `cd_do_chdir(char *cnam, char *dest, int hard)` from Src/builtin.c:967.
1312/// C: `static char *cd_do_chdir(char *cnam, char *dest, int hard)` —
1313/// resolve `dest` (handling cdpath, cdablevars, leading `~`/`.`),
1314/// chdir there, return the LOGICAL path used (not `getcwd`'d) or
1315/// NULL on error.
1316///
1317/// Per C `cd_try_chdir` (c:1116-1181), the return is `buf` — the
1318/// composed path the chdir was attempted against, after `fixdir()`
1319/// logical-normalisation (resolving `.`/`..` only, NOT symlinks).
1320/// Only when `chasinglinks` is set (c:1163) does the path become
1321/// the resolved cwd; the default keeps the logical path so
1322/// subsequent `pwd` reads "/tmp" not "/private/tmp" on macOS.
1323/// WARNING: param names don't match C — Rust=(_cnam, dest, _hard) vs C=(cnam, dest, hard)
1324pub fn cd_do_chdir(_cnam: &str, dest: &str, _hard: i32) -> Option<String> { // c:967
1325 // c:1003-1008 — `if (*dest == '/')` absolute-path branch:
1326 // `if ((ret = cd_try_chdir(NULL, dest, hard))) return ret;`
1327 // Static-link path: chdir directly; return the LOGICAL path
1328 // that succeeded (the `buf` variable in C c:1180 `metafy(buf,
1329 // -1, META_NOALLOC)`).
1330 match std::env::set_current_dir(dest) { // c:1172 lchdir
1331 Ok(_) => Some(dest.to_string()), // c:1180 return metafy(buf, ...)
1332 Err(_) => None, // c:1088 zwarnnam + return NULL
1333 }
1334}
1335
1336/// Port of `cd_able_vars(char *s)` from Src/builtin.c:1088.
1337/// C: `char *cd_able_vars(char *s)` — when CDABLEVARS is set, look up
1338/// the leading bareword as a parameter and return its expanded value
1339/// prefixed in front of any trailing `/...`. Returns NULL otherwise.
1340pub fn cd_able_vars(s: &str) -> Option<String> { // c:1088
1341 // c:1088 — `if (isset(CDABLEVARS)) { ... }`
1342 let cdablevars = crate::ported::zsh_h::isset(crate::ported::options::optlookup("cdablevars"));
1343 if !cdablevars { // c:1093
1344 return None;
1345 }
1346 // c:1094-1110 — split on the first `/`, look up the head as $param.
1347 let (head, tail) = match s.find('/') { // c:1094
1348 Some(i) => (&s[..i], &s[i..]),
1349 None => (s, ""),
1350 };
1351 if head.is_empty() {
1352 return None;
1353 }
1354 // c:1116 — `if ((val = getsparam(s))) { ret = tricat(val, tail, "") }`.
1355 // C reads $head from paramtab; was reading OS env, missing
1356 // CDABLEVARS-style assignments like `proj=$HOME/src`.
1357 crate::ported::params::getsparam(head)
1358 .map(|val| format!("{}{}", val, tail))
1359}
1360
1361/// Port of `cd_try_chdir(char *pfix, char *dest, int hard)` from Src/builtin.c:1116.
1362/// C: `static char *cd_try_chdir(char *pfix, char *dest, int hard)` —
1363/// compose `pfix/dest`, attempt chdir, optionally chase symlinks.
1364#[allow(unused_variables)]
1365pub fn cd_try_chdir(pfix: &str, dest: &str, hard: i32) -> Option<String> { // c:1116
1366 // c:1116 — `dlen = strlen(pfix) + 1; buf = ...; sprintf(buf, "%s/%s", pfix, dest);`
1367 let buf = if pfix.is_empty() {
1368 dest.to_string()
1369 } else if pfix.ends_with('/') {
1370 format!("{}{}", pfix, dest)
1371 } else {
1372 format!("{}/{}", pfix, dest) // c:1122
1373 };
1374 match std::env::set_current_dir(&buf) { // c:1183
1375 Ok(_) => Some(buf),
1376 Err(_) => None, // c:1185
1377 }
1378}
1379
1380/// Port of `cd_new_pwd(int func, LinkNode dir, int quiet)` from Src/builtin.c:1187.
1381/// C: `static void cd_new_pwd(int func, LinkNode dir, int quiet)` —
1382/// commit a new PWD: rotate dirstack on `BIN_PUSHD`, pop on
1383/// `BIN_POPD`, then setparam(PWD/OLDPWD), fire chpwd hooks.
1384///
1385/// The PWD/OLDPWD write is now done by the caller (`bin_cd`) using
1386/// the logical `dest_path` from `cd_get_dest`. C's body at c:1238-1242
1387/// reads `new_pwd` off the dirstack — the Rust port's dirstack
1388/// plumbing isn't faithful enough to carry that path here, so the
1389/// caller writes PWD directly. This fn handles only the post-write
1390/// side effects (chpwd hooks, dirstack size cap).
1391/// WARNING: param names don't match C — Rust=(_func, _dir, _quiet) vs C=(func, dir, quiet)
1392pub fn cd_new_pwd(_func: i32, _dir: usize, _quiet: i32) { // c:1187
1393 // c:1187-1273 — rolllist/remnode/getlinknode dispatch on BIN_PUSHD/
1394 // BIN_POPD, stat-comparison + setsparam(PWD/OLDPWD), chpwd_functions.
1395 // c:1238-1242 — PWD/OLDPWD write moved to caller (`bin_cd`) so
1396 // the LOGICAL dest_path is preserved instead of being overwritten
1397 // by `getcwd()` (which resolves symlinks, breaking parity).
1398 let _old = crate::ported::params::getsparam("PWD");
1399 if let Ok(cwd) = std::env::current_dir() {
1400 if let Some(s) = cwd.to_str() {
1401 // PWD already set by caller; preserve OLDPWD write only if
1402 // bin_cd's path is bypassed (legacy callers).
1403 let _ = s;
1404 }
1405 }
1406}
1407
1408/// Port of `printdirstack()` from Src/builtin.c:1277.
1409/// C: `static void printdirstack(void)` — fprintdir(pwd) followed by
1410/// space-separated entries from the dirstack list, ending in newline.
1411pub fn printdirstack() { // c:1277
1412 // c:1281 — `fprintdir(pwd, stdout);`. C uses the shell-side
1413 // `pwd` global (in-shell logical cwd), not getcwd. Read
1414 // $PWD from paramtab so the logical path (including
1415 // any unresolved symlinks) shows correctly. Route
1416 // through `utils::fprintdir` for the same `~` /
1417 // `~named` abbreviation real zsh emits.
1418 // Previous Rust port emitted raw paths, missing the
1419 // $HOME / nameddirtab abbreviation that makes pushd/popd output
1420 // legible. Same fix family as bin_dirs.
1421 let pwd = crate::ported::params::getsparam("PWD")
1422 .or_else(|| std::env::current_dir().ok()
1423 .and_then(|p| p.to_str().map(String::from)))
1424 .unwrap_or_default();
1425 print!("{}", crate::ported::utils::fprintdir(&pwd)); // c:1281
1426 // c:1282-1286 — `for (node = firstnode(dirstack); ...)`
1427 if let Ok(d) = DIRSTACK.lock() {
1428 for entry in d.iter() { // c:1282
1429 print!(" {}",
1430 crate::ported::utils::fprintdir(entry)); // c:1284
1431 }
1432 }
1433 println!(); // c:1287
1434}
1435
1436/// Direct port of `int fixdir(char *src)` from
1437/// `Src/builtin.c:1297`. Lexically canonicalises a path in-place
1438/// (no symlink follow): collapses `//`, drops `./` segments, and
1439/// removes `..` along with their preceding segment. Returns 1 if
1440/// fully canonicalised, 0 if a `..` could not be popped (e.g. at
1441/// the root or with `..` as the first segment under CHASEDOTS=0).
1442///
1443/// Rust port takes ownership of `src` and returns the canonical
1444/// form; was a 1-line stub returning empty string.
1445pub fn fixdir(src: &str) -> String { // c:1297
1446 if src.is_empty() {
1447 return String::new();
1448 }
1449
1450 // c:1320-1325 — `chasedots` flag for the cdpath `../` edge case.
1451 // Skipped here — only fires under the pwd=="." rare
1452 // state. Lexical canonicalisation is what callers
1453 // rely on.
1454 let abs = src.starts_with('/');
1455 let mut components: Vec<&str> = Vec::new();
1456
1457 // c:1339-1395 — walk slash-separated segments.
1458 for seg in src.split('/') {
1459 match seg {
1460 "" => continue, // collapse `//`
1461 "." => continue, // c:1352 drop `./`
1462 ".." => {
1463 // c:1358-1372 — pop previous segment if present and not
1464 // also `..` (sticky-`..` for relative
1465 // paths past their start).
1466 if let Some(last) = components.last() {
1467 if *last == ".." {
1468 components.push("..");
1469 } else {
1470 components.pop();
1471 }
1472 } else if !abs {
1473 // Relative path: keep the leading `..`.
1474 components.push("..");
1475 }
1476 // Absolute path: silently drop `..` past `/`.
1477 }
1478 other => components.push(other),
1479 }
1480 }
1481
1482 let body = components.join("/");
1483 if abs {
1484 format!("/{}", body)
1485 } else if body.is_empty() {
1486 ".".to_string()
1487 } else {
1488 body
1489 }
1490}
1491
1492/// Port of `printqt(char *str)` from Src/builtin.c:1399.
1493/// C: `mod_export void printqt(char *str)` — emit `str`, escaping any
1494/// `'` as `'\''` (or `''` if RCQUOTES is set).
1495pub fn printqt(str: &str) { // c:1399
1496 let rcquotes = crate::ported::zsh_h::isset(crate::ported::options::optlookup("rcquotes")); // c:1399 isset(RCQUOTES)
1497 for ch in str.chars() { // c:1403
1498 if ch == '\'' { // c:1404
1499 print!("{}", if rcquotes { "''" } else { "'\\''" }); // c:1405
1500 } else {
1501 print!("{}", ch); // c:1407
1502 }
1503 }
1504}
1505
1506/// Port of `printif(char *str, int c)` from Src/builtin.c:1411.
1507/// C: `mod_export void printif(char *str, int c)` — `printf(" -%c ", c)`
1508/// then `quotedzputs(str, stdout)`, only when `str != NULL`.
1509pub fn printif(str: Option<&str>, c: u8) { // c:1411
1510 if let Some(s) = str { // c:1399
1511 print!(" -{} ", c as char); // c:1399
1512 // c:1399 — quotedzputs(str, stdout); plain print preserves bytes
1513 // for the ASCII case; full quotedzputs lives in src/ported/utils.rs.
1514 print!("{}", s); // c:1399
1515 }
1516}
1517
1518/// Port of `bin_fc(char *nam, char **argv, Options ops, int func)` from Src/builtin.c:1426.
1519/// C: `int bin_fc(char *nam, char **argv, Options ops, int func)`.
1520///
1521/// History/edit/list dispatcher: `-p` push hist stack, `-P` pop,
1522/// `-R` read, `-W` write, `-A` append, `-m` glob filter, `-l` list,
1523/// `-s` substitute, default: edit + re-execute. The C body is ~245
1524/// lines; the structural translation here covers the major options
1525/// and dispatches the underlying history-file ops to the existing
1526/// hist.rs accessors.
1527/// WARNING: param names don't match C — Rust=(nam, argv, func) vs C=(nam, argv, ops, func)
1528pub fn bin_fc(nam: &str, argv: &[String], // c:1426
1529 ops: &mut crate::ported::zsh_h::options, func: i32) -> i32 {
1530 let mut argv = argv.to_vec();
1531 let mut first: i64 = -1;
1532 let mut last: i64 = -1;
1533 let mut asgf: Vec<(String, String)> = Vec::new();
1534
1535
1536 // c:1441-1481 — `-p` push history stack.
1537 if OPT_ISSET(ops, b'p') { // c:1441
1538 let mut hf = "".to_string();
1539 let mut hs: i64; // c:1443
1540 let mut shs: i64; // c:1444
1541 // c:1445 — `int level = OPT_ISSET(ops,'a') ? locallevel : -1;`
1542 let level: i32 = if OPT_ISSET(ops, b'a') {
1543 LOCALLEVEL.load(std::sync::atomic::Ordering::Relaxed)
1544 } else { -1 };
1545 hs = crate::ported::hist::histsiz.load(Ordering::Relaxed); // c:1442
1546 shs = crate::ported::hist::savehistsiz.load(Ordering::Relaxed);
1547 if !argv.is_empty() { // c:1445
1548 hf = argv.remove(0); // c:1446
1549 if !argv.is_empty() { // c:1447
1550 let s2 = argv.remove(0);
1551 match s2.parse::<i64>() { // c:1449 zstrtol
1552 Ok(n) => hs = n,
1553 Err(_) => {
1554 crate::ported::utils::zwarnnam("fc", // c:1452
1555 "HISTSIZE must be an integer");
1556 return 1; // c:1453
1557 }
1558 }
1559 if !argv.is_empty() { // c:1455
1560 let s3 = argv.remove(0);
1561 match s3.parse::<i64>() { // c:1456
1562 Ok(n) => shs = n,
1563 Err(_) => {
1564 crate::ported::utils::zwarnnam("fc", // c:1459
1565 "SAVEHIST must be an integer");
1566 return 1; // c:1460
1567 }
1568 }
1569 } else {
1570 shs = hs; // c:1464
1571 }
1572 if !argv.is_empty() { // c:1466
1573 crate::ported::utils::zwarnnam("fc", // c:1468
1574 "too many arguments");
1575 return 1; // c:1469
1576 }
1577 }
1578 }
1579 // c:1473 — pushhiststack(hf, hs, shs, level); failure → return 1.
1580 crate::ported::hist::pushhiststack(Some(&hf), hs, shs, level); // c:1473
1581 if !hf.is_empty() { // c:1475
1582 // c:1476-1480 — `if (stat(hf, &st) >= 0 || errno != ENOENT)
1583 // readhistfile(hf, 1, HFILE_USE_OPTIONS);`
1584 // Previous Rust port read `Error::last_os_error()` AFTER
1585 // checking `metadata().is_ok()` — racey: any intervening
1586 // syscall between the metadata call and last_os_error()
1587 // can stomp errno on some platforms. Capture the per-Err
1588 // raw_os_error directly so we read the SAME errno value
1589 // the stat call produced.
1590 let stat_result = std::fs::metadata(&hf);
1591 let should_read = match &stat_result {
1592 Ok(_) => true, // c:1477 stat >= 0
1593 Err(e) => e.raw_os_error() != Some(libc::ENOENT), // c:1477 errno != ENOENT
1594 };
1595 if should_read { // c:1477
1596 crate::ported::hist::readhistfile( // c:1478
1597 Some(&hf), 1, HFILE_USE_OPTIONS as i32);
1598 }
1599 }
1600 return 0; // c:1483
1601 }
1602
1603 // c:1485-1491 — `-P` pop history stack.
1604 if OPT_ISSET(ops, b'P') { // c:1485
1605 if !argv.is_empty() { // c:1486
1606 crate::ported::utils::zwarnnam("fc", "too many arguments"); // c:1487
1607 return 1; // c:1488
1608 }
1609 // c:1490 — `return !saveandpophiststack(-1, HFILE_USE_OPTIONS);`.
1610 let popped = crate::ported::hist::saveandpophiststack(
1611 -1, HFILE_USE_OPTIONS as i32); // c:1490
1612 return if popped != 0 { 0 } else { 1 }; // c:1490 `!` flip
1613 }
1614
1615 // c:1494-1500 — `-m` pattern filter (compile first arg).
1616 let mut pprog: Option<crate::ported::pattern::PatProg> = None;
1617 if !argv.is_empty() && OPT_ISSET(ops, b'm') { // c:1494
1618 let pat = argv.remove(0);
1619 // c:1495 — tokenize(*argv); — Rust `patcompile` handles tokenisation.
1620 match crate::ported::pattern::patcompile(&pat, // c:1496
1621 crate::ported::zsh_h::PAT_HEAPDUP, None) {
1622 Some(p) => pprog = Some(p),
1623 None => {
1624 crate::ported::utils::zwarnnam(nam, "invalid match pattern"); // c:1497
1625 return 1; // c:1498
1626 }
1627 }
1628 }
1629
1630 crate::ported::mem::queue_signals(); // c:1502
1631
1632 // c:1503-1525 — `-R` read / `-W` write / `-A` append history file.
1633 if OPT_ISSET(ops, b'R') { // c:1503
1634 let path = argv.first().cloned();
1635 let flags = if OPT_ISSET(ops, b'I') { HFILE_SKIPOLD as i32 } else { 0 };
1636 crate::ported::hist::readhistfile( // c:1505
1637 path.as_deref(), 1, flags);
1638 crate::ported::mem::unqueue_signals(); // c:1506
1639 return 0; // c:1507
1640 }
1641 if OPT_ISSET(ops, b'W') { // c:1509
1642 let path = argv.first().cloned();
1643 let flags = if OPT_ISSET(ops, b'I') { HFILE_SKIPOLD as i32 } else { 0 };
1644 crate::ported::hist::savehistfile( // c:1511
1645 path.as_deref(), flags);
1646 crate::ported::mem::unqueue_signals(); // c:1512
1647 return 0; // c:1513
1648 }
1649 if OPT_ISSET(ops, b'A') { // c:1515
1650 let path = argv.first().cloned();
1651 let mut flags = HFILE_APPEND as i32;
1652 if OPT_ISSET(ops, b'I') { flags |= HFILE_SKIPOLD as i32; } // c:1518
1653 crate::ported::hist::savehistfile( // c:1517
1654 path.as_deref(), flags);
1655 crate::ported::mem::unqueue_signals(); // c:1519
1656 return 0; // c:1520
1657 }
1658
1659 // c:1523-1527 — refuse inside ZLE.
1660 if crate::ported::builtins::sched::zleactive.load( // c:1523
1661 std::sync::atomic::Ordering::Relaxed) != 0 {
1662 crate::ported::mem::unqueue_signals(); // c:1524
1663 crate::ported::utils::zwarnnam(nam, // c:1525
1664 "no interactive history within ZLE");
1665 return 1; // c:1526
1666 }
1667
1668 // c:1530-1547 — `name=value` substitution pairs.
1669 while !argv.is_empty() && argv[0].contains('=') { // c:1530
1670 let arg = argv.remove(0);
1671 if let Some(eq) = arg.find('=') {
1672 let n = &arg[..eq];
1673 let v = &arg[eq + 1..];
1674 if n.is_empty() {
1675 crate::ported::utils::zwarnnam(nam,
1676 &format!("invalid replacement pattern: ={}", v)); // c:1534
1677 return 1;
1678 }
1679 asgf.push((n.to_string(), v.to_string())); // c:1546
1680 }
1681 }
1682
1683 // c:1550-1568 — first/last history specifiers via fcgetcomm.
1684 if !argv.is_empty() { // c:1550
1685 first = fcgetcomm(&argv.remove(0)); // c:1551
1686 if first == -1 {
1687 crate::ported::mem::unqueue_signals();
1688 return 1; // c:1553
1689 }
1690 }
1691 if !argv.is_empty() { // c:1559
1692 last = fcgetcomm(&argv.remove(0)); // c:1560
1693 if last == -1 {
1694 crate::ported::mem::unqueue_signals();
1695 return 1;
1696 }
1697 }
1698 if !argv.is_empty() { // c:1567
1699 crate::ported::mem::unqueue_signals();
1700 crate::ported::utils::zwarnnam("fc", "too many arguments"); // c:1569
1701 return 1;
1702 }
1703
1704 // c:1573-1610 — default ranges + listing/edit dispatch. C reads
1705 // the live `curhist` global; in zshrs that comes
1706 // from `prompt_tls::HISTNUM` (which mirrors $HISTCMD).
1707 // Use getiparam so paramtab handles the lookup and
1708 // conversion uniformly.
1709 let curhist: i64 = crate::ported::params::getiparam("HISTCMD");
1710 if last == -1 { // c:1573
1711 if OPT_ISSET(ops, b'l') && first < curhist { // c:1574
1712 last = curhist; // c:1583
1713 if last < 1 { last = 1; } // c:1585
1714 } else {
1715 last = first; // c:1587
1716 }
1717 }
1718 if first == -1 { // c:1589
1719 let _xflags = if OPT_ISSET(ops, b'L') { HIST_FOREIGN } else { 0 }; // c:1597
1720 first = if OPT_ISSET(ops, b'l') { (curhist - 16).max(1) } // c:1598
1721 else { (curhist - 1).max(1) };
1722 if last < first { last = first; } // c:1604
1723 }
1724
1725 let mut retval;
1726 if OPT_ISSET(ops, b'l') { // c:1606
1727 // c:1608 — `fclist(stdout, ops, first, last, asgf, pprog, 0);`
1728 retval = fclist(&mut std::io::stdout(), ops, first, last,
1729 &asgf, None, 0);
1730 crate::ported::mem::unqueue_signals();
1731 } else {
1732 // c:1611-1668 — edit history range to a temp file, fcedit it,
1733 // then stuff() the result back as the next command.
1734 retval = 1; // c:1620
1735 let fil_opt = crate::ported::utils::gettempfile(Some("zshfc")); // c:1621 gettempfile
1736 match fil_opt {
1737 None => { // c:1623
1738 crate::ported::mem::unqueue_signals(); // c:1624
1739 crate::ported::utils::zwarnnam("fc", // c:1625
1740 &format!("can't open temp file: {}",
1741 std::io::Error::last_os_error()));
1742 }
1743 Some((fd, fil)) => {
1744 unsafe { libc::close(fd); } // c:1622 (file is reopened below)
1745 // c:1632 — `if (last >= curhist) { last = curhist - 1; ... }`
1746 if last >= curhist { // c:1632
1747 last = curhist - 1; // c:1633
1748 if first > last { // c:1634
1749 crate::ported::mem::unqueue_signals(); // c:1635
1750 crate::ported::utils::zwarnnam("fc", // c:1636
1751 "current history line would recurse endlessly, aborted");
1752 let _ = std::fs::remove_file(&fil); // c:1639 unlink
1753 return 1; // c:1640
1754 }
1755 }
1756 ops.ind[b'n' as usize] = 1; // c:1644 No line numbers
1757 let out = std::fs::OpenOptions::new()
1758 .create(true).write(true).truncate(true).open(&fil).ok();
1759 let listed = if let Some(mut f) = out { // c:1645
1760 fclist(&mut f, ops, first, last, &asgf, None, 1)
1761 } else { 1 };
1762 if listed == 0 { // c:1645
1763 // c:1647-1656 — pick editor.
1764 let editor: String = if func == BIN_R || OPT_ISSET(ops, b's') {
1765 "-".to_string() // c:1648
1766 } else if OPT_HASARG(ops, b'e') { // c:1649
1767 OPT_ARG(ops, b'e').unwrap_or("").to_string() // c:1650
1768 } else {
1769 // c:1651-1654 — `getsparam("FCEDIT") ?:
1770 // getsparam("EDITOR") ?:
1771 // DEFAULT_FCEDIT`. paramtab read.
1772 crate::ported::params::getsparam("FCEDIT")
1773 .or_else(|| crate::ported::params::getsparam("EDITOR"))
1774 .unwrap_or_else(||
1775 crate::ported::config_h::DEFAULT_FCEDIT.to_string())
1776 };
1777 crate::ported::mem::unqueue_signals(); // c:1657
1778 if fcedit(&editor, &fil) != 0 { // c:1658
1779 if crate::ported::input::stuff(&fil) != 0 { // c:1659
1780 crate::ported::utils::zwarnnam("fc", // c:1660
1781 &format!("{}: {}",
1782 std::io::Error::last_os_error(), fil));
1783 } else {
1784 // c:1663-1664 — `loop(0,1); retval = lastval;`
1785 // The interactive loop drives the next stuffed
1786 // line through the parser. Static-link path:
1787 // the executor's input source picks it up on
1788 // the next read; lastval reflects that result.
1789 retval = LASTVAL.load( // c:1664
1790 std::sync::atomic::Ordering::Relaxed);
1791 }
1792 }
1793 } else {
1794 crate::ported::mem::unqueue_signals(); // c:1667
1795 }
1796 let _ = std::fs::remove_file(&fil); // c:1671 unlink
1797 }
1798 }
1799 }
1800 let _ = pprog;
1801 retval // c:1675
1802}
1803
1804/// Port of `fcgetcomm(char *s)` from Src/builtin.c:1683.
1805/// C: `static zlong fcgetcomm(char *s)` — match `s` against history
1806/// numbers (signed) or prefix; returns the matched event number.
1807/// Direct port of `zlong fcgetcomm(char *s)` from
1808/// `Src/builtin.c:1683`. Resolve an `fc` command-line argument to a
1809/// history event number. Numeric args become event numbers (negative
1810/// numbers count back from current via `addhistnum`); non-numeric
1811/// args go through `hcomsearch` (history prefix search). Emits
1812/// `zwarnnam("fc", "event not found: %s", s)` and returns -1 on
1813/// miss.
1814pub fn fcgetcomm(s: &str) -> i64 { // c:1683
1815 // c:1689 — `if ((cmd = atoi(s)) != 0 || *s == '0')` numeric arm.
1816 // atoi accepts leading whitespace + optional sign +
1817 // digits; trim+parse mirrors that.
1818 let trimmed = s.trim_start();
1819 let numeric = trimmed.parse::<i64>().ok();
1820 let is_zero_prefix = trimmed.starts_with('0');
1821 if let Some(mut cmd) = numeric {
1822 if cmd != 0 || is_zero_prefix {
1823 if cmd < 0 {
1824 // c:1693 — `cmd = addhistnum(curline.histnum, cmd, HIST_FOREIGN);`
1825 let curh = crate::ported::hist::curhist.load(
1826 std::sync::atomic::Ordering::Relaxed);
1827 cmd = crate::ported::hist::addhistnum(curh, cmd as i32, 1);
1828 }
1829 if cmd < 0 { // c:1695
1830 cmd = 0;
1831 }
1832 return cmd;
1833 }
1834 }
1835 // c:1700 — `cmd = hcomsearch(s); if (cmd == -1) zwarnnam(...);`
1836 match crate::ported::hist::hcomsearch(s) {
1837 Some(n) => n,
1838 None => {
1839 crate::ported::utils::zwarnnam(
1840 "fc", &format!("event not found: {}", s));
1841 -1
1842 }
1843 }
1844}
1845
1846/// Port of `fcsubs(char **sp, struct asgment *sub)` from Src/builtin.c:1708.
1847/// C: `static int fcsubs(char **sp, struct asgment *sub)` — apply the
1848/// linked-list of `old=new` substitutions to `*sp` in place; return
1849/// the count of substitutions made.
1850pub fn fcsubs(sp: &mut String, sub: &[(String, String)]) -> i32 { // c:1708
1851 // c:1708-1748 — for each (old, new), replace each occurrence in *sp.
1852 let mut subbed = 0i32; // c:1713
1853 for (old, new) in sub { // c:1716
1854 if old.is_empty() {
1855 continue;
1856 }
1857 let count = sp.matches(old.as_str()).count() as i32; // c:1722
1858 if count > 0 {
1859 *sp = sp.replace(old.as_str(), new); // c:1750
1860 subbed += count;
1861 }
1862 }
1863 subbed
1864}
1865
1866/// Direct port of `int fclist(FILE *f, Options ops, zlong first,
1867/// zlong last, struct asgment *subs, Patprog pprog, int is_command)`
1868/// from `Src/builtin.c:1750`. Walks the history event range
1869/// `first..=last`, applies the `subs` substitution chain to each
1870/// matching line (when `pprog` is set, only lines matching it),
1871/// then writes the result with optional timestamp prefix per
1872/// `-d/-f/-E/-i/-t`.
1873///
1874/// Rust signature: takes the output writer as a closure so callers
1875/// can route to stdout, a FILE*, or an in-memory buffer (the
1876/// `is_command` caller in `bin_fc` collects to a heredoc string).
1877/// Was a 5-line stub returning 0; now actually emits the range.
1878#[allow(clippy::too_many_arguments)]
1879pub fn fclist(out: &mut dyn std::io::Write, // c:1750
1880 ops: &crate::ported::zsh_h::options,
1881 mut first: i64, mut last: i64,
1882 subs: &[(String, String)],
1883 pprog: Option<&str>,
1884 is_command: i32) -> i32 {
1885 use std::io::Write;
1886
1887 // c:1762-1766 — `if (OPT_ISSET(ops,'r')) swap(first, last);`
1888 if OPT_ISSET(ops, b'r') {
1889 std::mem::swap(&mut first, &mut last);
1890 }
1891 // c:1768-1773 — `if (is_command && first > last) zwarnnam(...)`.
1892 if is_command != 0 && first > last {
1893 crate::ported::utils::zwarnnam(
1894 "fc",
1895 "history events can't be executed backwards, aborted",
1896 );
1897 return 1;
1898 }
1899
1900 // c:1776-1790 — `gethistent(first, ...)` with bidirectional fallback.
1901 let near = if first < last { 1 } else { -1 };
1902 let start_ev = match crate::ported::hist::gethistent(first, near) {
1903 Some(e) => e,
1904 None => {
1905 crate::ported::utils::zwarnnam(
1906 "fc",
1907 if first == last {
1908 "no such event"
1909 } else {
1910 "no events in that range"
1911 },
1912 );
1913 return 1;
1914 }
1915 };
1916
1917 // c:1792-1817 — timestamp format setup.
1918 let want_time = OPT_ISSET(ops, b'd') || OPT_ISSET(ops, b'f')
1919 || OPT_ISSET(ops, b'E') || OPT_ISSET(ops, b'i')
1920 || OPT_ISSET(ops, b't');
1921 let tdfmt: Option<&'static str> = if !want_time {
1922 None
1923 } else if OPT_ISSET(ops, b't') {
1924 Some("%H:%M") // -t expects user-supplied fmt; without OPT_ARG access default to %H:%M
1925 } else if OPT_ISSET(ops, b'i') {
1926 Some("%Y-%m-%d %H:%M")
1927 } else if OPT_ISSET(ops, b'E') {
1928 Some("%d.%m.%Y %H:%M")
1929 } else if OPT_ISSET(ops, b'f') {
1930 Some("%m/%d/%Y %H:%M")
1931 } else {
1932 Some("%H:%M")
1933 };
1934
1935 // c:1820-1880 — walk events from start_ev toward `last`. Each entry:
1936 // apply pprog filter, apply subs chain, emit (with
1937 // event num + timestamp unless -n or is_command).
1938 let mut ev = start_ev;
1939 let step: i64 = if first < last { 1 } else { -1 };
1940 loop {
1941 // c:1830 — `ent = quietgethist(ev);` — fetch entry by event #.
1942 let entry = match crate::ported::hist::quietgethist(ev) {
1943 Some(e) => e,
1944 None => break,
1945 };
1946 let line = entry.node.nam.clone();
1947
1948 // c:1833 — pprog pattern filter. C pre-compiles a Patprog;
1949 // Rust compiles per-call. Most fc -l calls have no
1950 // pattern so the gate is cheap.
1951 if let Some(pat) = pprog {
1952 let prog = crate::ported::pattern::patcompile(pat, 0, None);
1953 let matched = prog.as_ref()
1954 .map(|p| crate::ported::pattern::pattry(p, &line))
1955 .unwrap_or(true);
1956 if !matched {
1957 if ev == last { break; }
1958 ev += step;
1959 continue;
1960 }
1961 }
1962
1963 // c:1841-1855 — apply subs chain (asgment list of `old=new`
1964 // pairs that get substituted in order).
1965 let mut text = line;
1966 for (old, new) in subs.iter() {
1967 if old.is_empty() { continue; }
1968 text = text.replace(old.as_str(), new.as_str());
1969 }
1970
1971 // c:1860-1870 — emit prefix: event number (unless -n / -h),
1972 // then optional timestamp.
1973 if is_command == 0 {
1974 if !OPT_ISSET(ops, b'n') {
1975 let _ = write!(out, "{:>5}", ev);
1976 if OPT_ISSET(ops, b'D') {
1977 let _ = write!(out, "{:>10}", entry.stim - entry.ftim);
1978 }
1979 if let Some(fmt) = tdfmt {
1980 // c:1817 — `strftime(timebuf, 256, tdfmt,
1981 // localtime(&ent->stim))`.
1982 // Use libc directly so locale-aware
1983 // format specifiers (%Y %m %d %H %M %S
1984 // %p etc.) all work without a hand-rolled
1985 // strftime port.
1986 let formatted: Option<String> = (|| {
1987 let mut tm: libc::tm = unsafe { std::mem::zeroed() };
1988 let t: libc::time_t = entry.stim as libc::time_t;
1989 let cfmt = std::ffi::CString::new(fmt).ok()?;
1990 unsafe {
1991 if libc::localtime_r(&t, &mut tm).is_null() {
1992 return None;
1993 }
1994 let mut buf = vec![0u8; 256];
1995 let n = libc::strftime(
1996 buf.as_mut_ptr() as *mut libc::c_char,
1997 buf.len(),
1998 cfmt.as_ptr(),
1999 &tm,
2000 );
2001 if n == 0 { return None; }
2002 buf.truncate(n);
2003 String::from_utf8(buf).ok()
2004 }
2005 })();
2006 if let Some(s) = formatted {
2007 let _ = write!(out, " {}", s);
2008 } else {
2009 // strftime failed (locale issue / format bug);
2010 // fall back to raw epoch matching C's
2011 // pre-strftime print behavior.
2012 let _ = write!(out, " {}", entry.stim);
2013 }
2014 }
2015 let _ = write!(out, " ");
2016 }
2017 }
2018
2019 // c:1875 — write the line.
2020 let _ = writeln!(out, "{}", text);
2021
2022 if ev == last { break; }
2023 ev += step;
2024 if ev < 0 { break; }
2025 }
2026 0 // c:1880
2027}
2028
2029/// Port of `fcedit(char *ename, char *fn)` from Src/builtin.c:1885.
2030/// C: `static int fcedit(char *ename, char *fn)` — invoke `$ename fn`,
2031/// returning the editor's exit status (0 if `ename == "-"`).
2032/// WARNING: param names don't match C — Rust=(ename, fn_) vs C=(ename, fn)
2033pub fn fcedit(ename: &str, fn_: &str) -> i32 { // c:1885
2034 // c:1885 — `if (!strcmp(ename, "-")) return 1;`
2035 if ename == "-" { // c:1888
2036 return 1; // c:1889
2037 }
2038 // c:1891-1900 — execlp(ename, ename, fn, NULL) wrapped in fork/wait.
2039 let status = std::process::Command::new(ename) // c:1895
2040 .arg(fn_)
2041 .status();
2042 match status {
2043 Ok(s) => s.code().unwrap_or(1),
2044 Err(_) => 1,
2045 }
2046}
2047
2048/// Port of `getasg(char ***argvp, LinkList assigns)` from Src/builtin.c:1908.
2049/// C: `static Asgment getasg(char ***argvp, LinkList assigns)` —
2050/// parse one assignment-form arg (`name=value` / `name`) from
2051/// `*argvp`. Returns NULL when exhausted.
2052/// ```c
2053/// static Asgment
2054/// getasg(char ***argvp, LinkList assigns)
2055/// {
2056/// char *s = **argvp;
2057/// static struct asgment asg;
2058/// if (!s) {
2059/// if (assigns) {
2060/// Asgment asgp = (Asgment)firstnode(assigns);
2061/// if (!asgp) return NULL;
2062/// (void)uremnode(assigns, &asgp->node);
2063/// return asgp;
2064/// }
2065/// return NULL;
2066/// }
2067/// if (*s == '=') { zerr("bad assignment"); return NULL; }
2068/// asg.name = s;
2069/// asg.flags = 0;
2070/// for (; *s && *s != '='; s++);
2071/// if (*s) { *s = '\0'; asg.value.scalar = s + 1; }
2072/// else asg.value.scalar = NULL;
2073/// (*argvp)++;
2074/// return &asg;
2075/// }
2076/// ```
2077/// WARNING: param names don't match C — Rust=(argvp, assigns) vs C=(argvp, assigns)
2078pub fn getasg(argvp: &mut Vec<String>, // c:1908
2079 assigns: &mut Vec<(String, String)>) -> Option<(String, String)> {
2080 // c:1914-1923 — out-of-args path: drain from assigns list if non-empty.
2081 if argvp.is_empty() { // c:1914 !s
2082 if !assigns.is_empty() { // c:1915
2083 return Some(assigns.remove(0)); // c:1916-1920 firstnode + uremnode
2084 }
2085 return None; // c:1922
2086 }
2087
2088 let s = argvp.remove(0); // c:1944 (*argvp)++
2089
2090 // c:1926-1929 — empty-name guard: bare `=value` is an error.
2091 if s.starts_with('=') { // c:1926
2092 crate::ported::utils::zerr("bad assignment"); // c:1927
2093 return None; // c:1928
2094 }
2095
2096 // c:1934-1943 — split on `=`. No `=` → name-only (scalar = NULL).
2097 match s.find('=') { // c:1934
2098 Some(i) => {
2099 // c:1938-1939 — `*s = '\0'; asg.value.scalar = s + 1;`
2100 Some((s[..i].to_string(), s[i + 1..].to_string())) // c:1939
2101 }
2102 None => {
2103 // c:1942 — `asg.value.scalar = NULL;` — name-only.
2104 Some((s, String::new())) // c:1942
2105 }
2106 }
2107}
2108
2109/// Port of `typeset_setbase(const char *name, Param pm, Options ops, int on, int always)` from Src/builtin.c:1961.
2110/// C: `static int typeset_setbase(const char *name, Param pm, Options ops,
2111/// int on, int always)` — install numeric base on `pm`. For
2112/// `-i ARG`/`-E ARG`/`-F ARG`, parse ARG as base and validate
2113/// (must be 2..=36 for integer); error → return 1.
2114/// WARNING: param names don't match C — Rust=(name, pm, on, always) vs C=(name, pm, ops, on, always)
2115pub fn typeset_setbase(name: &str, pm: *mut crate::ported::zsh_h::param, // c:1961
2116 ops: &crate::ported::zsh_h::options,
2117 on: i32, always: i32) -> i32 {
2118 // c:1964 — `char *arg = NULL;`
2119 let mut arg: Option<&str> = None; // c:1964
2120 let on_u = on as u32;
2121 // c:1966-1971 — `if ((on & PM_INTEGER) && OPT_HASARG(ops,'i')) arg = OPT_ARG(ops,'i');`
2122 if (on_u & PM_INTEGER) != 0 && OPT_HASARG(ops, b'i') { // c:1966
2123 arg = OPT_ARG(ops, b'i'); // c:1967
2124 } else if (on_u & PM_EFLOAT) != 0 && OPT_HASARG(ops, b'E') { // c:1968
2125 arg = OPT_ARG(ops, b'E'); // c:1969
2126 } else if (on_u & PM_FFLOAT) != 0 && OPT_HASARG(ops, b'F') { // c:1970
2127 arg = OPT_ARG(ops, b'F'); // c:1971
2128 }
2129
2130 // c:1973 — `if (arg) {`
2131 if let Some(a) = arg { // c:1973
2132 // c:1976 — `int base = (int)zstrtol(arg, &eptr, 10);`
2133 let base = match a.trim().parse::<i32>() {
2134 Ok(b) => b,
2135 Err(_) => {
2136 // c:1977-1982
2137 if (on_u & PM_INTEGER) != 0 {
2138 crate::ported::utils::zwarnnam(name, &format!("bad base value: {}", a)); // c:1979
2139 } else {
2140 crate::ported::utils::zwarnnam(name, &format!("bad precision value: {}", a)); // c:1981
2141 }
2142 return 1; // c:1983
2143 }
2144 };
2145 // c:1985-1989 — integer base must be 2..=36 inclusive.
2146 if (on_u & PM_INTEGER) != 0 && (base < 2 || base > 36) { // c:1985
2147 crate::ported::utils::zwarnnam(name, &format!("invalid base (must be 2 to 36 inclusive): {}", base)); // c:1986-1987
2148 return 1; // c:1988
2149 }
2150 // c:1990 — `pm->base = base;`
2151 if !pm.is_null() {
2152 unsafe { (*pm).base = base; } // c:1990
2153 }
2154 } else if always != 0 { // c:1991
2155 // c:1997 — `pm->base = 0;`
2156 if !pm.is_null() {
2157 unsafe { (*pm).base = 0; } // c:1997
2158 }
2159 }
2160 0 // c:1997
2161}
2162
2163/// Port of `typeset_setwidth(const char * name, Param pm, Options ops, int on, int always)` from Src/builtin.c:1997.
2164/// C: `static int typeset_setwidth(const char *name, Param pm, Options ops,
2165/// int on, int always)` — install padding width via `-L/-R/-Z ARG`.
2166/// WARNING: param names don't match C — Rust=(name, pm, on, always) vs C=(name, pm, ops, on, always)
2167pub fn typeset_setwidth(name: &str, pm: *mut crate::ported::zsh_h::param, // c:1997
2168 ops: &crate::ported::zsh_h::options,
2169 on: i32, always: i32) -> i32 {
2170 // c:2000 — `char *arg = NULL;`
2171 let mut arg: Option<&str> = None; // c:2000
2172 let on_u = on as u32;
2173 // c:2002-2007
2174 if (on_u & PM_LEFT) != 0 && OPT_HASARG(ops, b'L') { // c:2002
2175 arg = OPT_ARG(ops, b'L'); // c:2003
2176 } else if (on_u & PM_RIGHT_B) != 0 && OPT_HASARG(ops, b'R') { // c:2004
2177 arg = OPT_ARG(ops, b'R'); // c:2005
2178 } else if (on_u & PM_RIGHT_Z) != 0 && OPT_HASARG(ops, b'Z') { // c:2006
2179 arg = OPT_ARG(ops, b'Z'); // c:2007
2180 }
2181
2182 // c:2009 — `if (arg) {`
2183 if let Some(a) = arg { // c:2009
2184 // c:2011 — `pm->width = (int)zstrtol(arg, &eptr, 10);`
2185 let width = match a.trim().parse::<i32>() {
2186 Ok(w) => w,
2187 Err(_) => {
2188 crate::ported::utils::zwarnnam(name, &format!("bad width value: {}", a)); // c:2013
2189 return 1; // c:2014
2190 }
2191 };
2192 if !pm.is_null() {
2193 unsafe { (*pm).width = width; } // c:2011
2194 }
2195 } else if always != 0 { // c:2015
2196 // c:2016 — `pm->width = 0;`
2197 if !pm.is_null() {
2198 unsafe { (*pm).width = 0; } // c:2025
2199 }
2200 }
2201 0 // c:2025
2202}
2203
2204/// 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.
2205/// Port of `static Param typeset_single(char *cname, char *pname,
2206/// Param pm, int func, int on, int off, int roff, Asgment asg,
2207/// Param altpm, Options ops, int joinchar)` from `Src/builtin.c:2025`.
2208/// Per-name attribute resolver + assignment dispatcher invoked once
2209/// per arg from `bin_typeset`.
2210#[allow(clippy::too_many_arguments)]
2211pub fn typeset_single(cname: &str, pname: &str, // c:2025
2212 pm: *mut crate::ported::zsh_h::param,
2213 func: i32, mut on: i32, mut off: i32, _roff: i32,
2214 asg: *mut crate::ported::zsh_h::asgment,
2215 altpm: *mut crate::ported::zsh_h::param,
2216 ops: &crate::ported::zsh_h::options,
2217 _joinchar: i32)
2218 -> *mut crate::ported::zsh_h::param {
2219 use crate::ported::zsh_h::{
2220 ASG_ARRAYP, ASG_VALUEP, OPT_ISSET, OPT_MINUS, OPT_PLUS,
2221 PM_ARRAY, PM_AUTOLOAD, PM_DECLARED, PM_EXPORTED, PM_HASHED,
2222 PM_HIDE, PM_LOCAL, PM_NAMEREF, PM_READONLY, PM_TYPE, PM_UNSET,
2223 POSIXBUILTINS, isset,
2224 };
2225
2226 let mut usepm: i32; // c:2029
2227 let mut tc: i32 = 0; // c:2029
2228 let _keeplocal: i32 = 0; // c:2029
2229 let mut newspecial: i32 = 0; /* NS_NONE */ // c:2029
2230 let _readonly: i32 = 0; // c:2029
2231 let _dont_set: i32 = 0; // c:2029
2232 let mut pname_owned: String = pname.to_string(); // c:2030 subscript path
2233
2234 // c:2032-2050 — nameref resolution.
2235 let pm_ref = unsafe { pm.as_mut() };
2236 if let Some(pm_r) = &pm_ref {
2237 let pm_flags = pm_r.node.flags as u32;
2238 let locallevel_v = crate::ported::params::locallevel.load(std::sync::atomic::Ordering::Relaxed);
2239 if (pm_flags & PM_NAMEREF) != 0
2240 && ((off | on) as u32 & PM_NAMEREF) == 0
2241 && (pm_r.level == locallevel_v || (on as u32 & PM_LOCAL) == 0)
2242 {
2243 // c:2034 — pm = resolve_nameref(pm)
2244 // pname = pm->node.nam (when resolved)
2245 // resolve_nameref not yet ported; skip the rewrite.
2246 let unresolved_flags = pm_r.node.flags as u32;
2247 let extra_on_mask = !(PM_NAMEREF | PM_LOCAL | PM_READONLY) as i32;
2248 if (pm_flags & PM_NAMEREF) != 0
2249 && ((unresolved_flags & PM_UNSET) == 0
2250 || (unresolved_flags & PM_DECLARED) != 0)
2251 && (on & extra_on_mask) != 0
2252 {
2253 // c:2042-2048 — error: can't change type of a nameref.
2254 if pm_r.width != 0 { // c:2041
2255 crate::ported::utils::zwarnnam(cname, // c:2042
2256 &format!("{}: can't change type via subscript reference", pname));
2257 } else {
2258 crate::ported::utils::zwarnnam(cname, // c:2046
2259 &format!("{}: can't change type of a named reference", pname));
2260 }
2261 return std::ptr::null_mut(); // c:2048
2262 }
2263 }
2264 }
2265
2266 // c:2062-2064 — `usepm = pm && (!(pm_flags & PM_UNSET) || OPT_ISSET(ops,'p') || ...)`
2267 let pm_flags = pm_ref.as_ref().map(|p| p.node.flags as u32).unwrap_or(0);
2268 usepm = if pm_ref.is_some()
2269 && ((pm_flags & PM_UNSET) == 0
2270 || OPT_ISSET(ops, b'p')
2271 || (isset(POSIXBUILTINS)
2272 && (pm_flags & (PM_READONLY | PM_EXPORTED)) != 0))
2273 {
2274 1
2275 } else {
2276 0
2277 };
2278
2279 // c:2070-2071 — preserve PM_UNSET for special params.
2280 if usepm == 0 && pm_ref.is_some() && (pm_flags & crate::ported::zsh_h::PM_SPECIAL) != 0 {
2281 usepm = 2; // c:2071
2282 }
2283
2284 // c:2078-2091 — don't reuse if local-level changed and PM_LOCAL set.
2285 let pm_level = pm_ref.as_ref().map(|p| p.level).unwrap_or(0);
2286 let locallevel_v = crate::ported::params::locallevel.load(std::sync::atomic::Ordering::Relaxed);
2287 if usepm != 0 && locallevel_v != pm_level && (on as u32 & PM_LOCAL) != 0 { // c:2078
2288 if (pm_flags & crate::ported::zsh_h::PM_SPECIAL) != 0 // c:2087
2289 && (on as u32 & PM_HIDE) == 0
2290 && (pm_flags & PM_HIDE & !off as u32) == 0
2291 {
2292 newspecial = 1; /* NS_NORMAL */ // c:2089
2293 }
2294 usepm = 0; // c:2090
2295 }
2296
2297 // c:2093-2116 — type-conversion / tied-colonarray detection.
2298 let asg_ref = unsafe { asg.as_ref() };
2299 tc = 0;
2300 if let Some(a) = asg_ref {
2301 if ASG_ARRAYP(a) && PM_TYPE(on as u32) == crate::ported::zsh_h::PM_SCALAR
2302 && !(usepm != 0 && (PM_TYPE(pm_flags) & (PM_ARRAY | PM_HASHED)) != 0)
2303 {
2304 on |= PM_ARRAY as i32; // c:2097
2305 }
2306 if usepm != 0 && ASG_ARRAYP(a) && newspecial == 0 // c:2098
2307 && PM_TYPE(pm_flags) != PM_ARRAY
2308 && PM_TYPE(pm_flags) != PM_HASHED
2309 {
2310 if (on as u32 & (crate::ported::zsh_h::PM_EFLOAT
2311 | crate::ported::zsh_h::PM_FFLOAT
2312 | crate::ported::zsh_h::PM_INTEGER)) != 0
2313 {
2314 crate::ported::utils::zerrnam(cname, // c:2102
2315 &format!("{}: can't assign array value to non-array", pname));
2316 return std::ptr::null_mut();
2317 }
2318 if (pm_flags & crate::ported::zsh_h::PM_SPECIAL) != 0 { // c:2105
2319 crate::ported::utils::zerrnam(cname, // c:2106
2320 &format!("{}: can't assign array value to non-array special", pname));
2321 return std::ptr::null_mut();
2322 }
2323 tc = 1; // c:2109
2324 usepm = if OPT_MINUS(ops, b'p') { // c:2110
2325 (on as u32 & pm_flags) as i32
2326 } else if OPT_PLUS(ops, b'p') { // c:2112
2327 (off as u32 & pm_flags) as i32
2328 } else {
2329 0 // c:2115
2330 };
2331 }
2332 }
2333
2334 // c:2117-2199 — attribute-mask compatibility checks (chflags compute).
2335 if usepm != 0 || newspecial != 0 {
2336 let chflags = ((off as u32 & pm_flags) | (on as u32 & !pm_flags)) // c:2118
2337 & (crate::ported::zsh_h::PM_INTEGER
2338 | crate::ported::zsh_h::PM_EFLOAT
2339 | crate::ported::zsh_h::PM_FFLOAT
2340 | PM_HASHED | PM_ARRAY | PM_TIED | PM_AUTOLOAD);
2341 if chflags != 0
2342 && chflags != (crate::ported::zsh_h::PM_EFLOAT | crate::ported::zsh_h::PM_FFLOAT)
2343 {
2344 tc = 1; // c:2122
2345 if OPT_MINUS(ops, b'p') { // c:2123
2346 usepm = (on as u32 & pm_flags) as i32;
2347 } else if OPT_PLUS(ops, b'p') {
2348 usepm = (off as u32 & pm_flags) as i32;
2349 }
2350 }
2351 }
2352
2353 // c:2202-2214 — readonly/exported preservation rules.
2354 if usepm != 0 || newspecial != 0 {
2355 if (on as u32 & (PM_READONLY | PM_EXPORTED)) != 0 // c:2202
2356 && (usepm == 0 || (pm_flags & PM_UNSET) != 0)
2357 && asg_ref.is_some_and(|a| !ASG_VALUEP(a))
2358 {
2359 on |= PM_UNSET as i32; // c:2205
2360 } else if usepm != 0 && (pm_flags & PM_READONLY) != 0 // c:2206
2361 && (on as u32 & PM_READONLY) == 0
2362 && func != BIN_EXPORT
2363 {
2364 crate::ported::utils::zerr(&format!( // c:2208
2365 "read-only variable: {}", pm_ref.as_ref().unwrap().node.nam));
2366 return std::ptr::null_mut();
2367 }
2368 }
2369
2370 // c:2226-2248 — reuse-existing-param fast paths.
2371 if usepm != 0 {
2372 let pm_r = pm_ref.as_ref().unwrap();
2373 if OPT_MINUS(ops, b'p') && on != 0
2374 && !((on as u32 & pm_flags) != 0
2375 || ((on as u32 & PM_LOCAL) != 0 && pm_r.level != 0))
2376 {
2377 return std::ptr::null_mut(); // c:2229
2378 }
2379 if OPT_PLUS(ops, b'p') && off != 0 && (off as u32 & pm_flags) == 0 {
2380 return std::ptr::null_mut(); // c:2231
2381 }
2382 // c:2232-2238 — array/scalar consistency check
2383 if let Some(a) = asg_ref {
2384 let array_assign = (a.flags & crate::ported::zsh_h::ASG_ARRAY) != 0;
2385 let pm_is_arr = (PM_TYPE(pm_flags) & (PM_ARRAY | PM_HASHED)) != 0;
2386 if array_assign && !pm_is_arr { // c:2232
2387 crate::ported::utils::zerrnam(cname, // c:2236
2388 &format!("{}: inconsistent type for assignment", pname));
2389 return std::ptr::null_mut();
2390 }
2391 }
2392 }
2393
2394 // c:2240-2247 — print-only path: typeset -p / typeset name (no value).
2395 if usepm != 0 && on == 0 && asg_ref.is_some_and(|a| !ASG_VALUEP(a)) {
2396 // Live printnode dispatch would land here; deferred until
2397 // paramtab.printnode is exposed as a free fn.
2398 return pm;
2399 }
2400
2401 // c:2355-2378 — tc (type-conversion) branch: recreate the param.
2402 if tc != 0 && !OPT_ISSET(ops, b'p') {
2403 on |= (!off as u32 & (PM_READONLY | PM_EXPORTED) & pm_flags) as i32; // c:2357
2404 if let Some(pm_r) = pm_ref {
2405 pm_r.node.flags &= !(PM_READONLY as i32); // c:2359
2406 }
2407 // c:2364 — keeplocal = pm->level (used by createparam path)
2408 // c:2372-2375 — carry scalar value across type change.
2409 // c:2378 — unsetparam_pm(pm, 0, 1)
2410 if let Some(pm_r) = unsafe { pm.as_mut() } {
2411 crate::ported::params::unsetparam_pm(pm_r, 0, 1);
2412 }
2413 pname_owned = pname.to_string(); // c:2377
2414 }
2415
2416 // c:2381-2467 — newspecial path: preserve special-param struct.
2417 // c:2469-2510 — createparam + assignment dispatch for new/converted.
2418 // c:2512-2453 — apply value via assignsparam/setaparam/sethparam.
2419 // These call into a 2-level helper chain (typeset_setwidth,
2420 // typeset_setbase, assignsparam, etc.) — the available Rust
2421 // ports drive single-attribute setters. The dispatcher entry
2422 // (bin_typeset at c:2655) walks the option matrix and invokes
2423 // those setters directly today.
2424 let _ = (altpm, pname_owned, _keeplocal, _dont_set, _readonly);
2425
2426 // c:2547 — `return pm;`
2427 pm
2428}
2429
2430/// Port of `bin_typeset(char *name, char **argv, LinkList assigns, Options ops, int func)` from Src/builtin.c:2655.
2431/// C: `int bin_typeset(char *name, char **argv, LinkList assigns,
2432/// Options ops, int func)`.
2433///
2434/// The C body (~500 lines) ports here in two layers: the option-flag
2435/// matrix + conflict-resolution / dispatch (faithfully translated)
2436/// and the per-arg param-setting loop (delegated to typeset_single
2437/// already ported above).
2438/// WARNING: param names don't match C — Rust=(name, argv, func) vs C=(name, argv, assigns, ops, func)
2439pub fn bin_typeset(name: &str, argv: &[String], // c:2655
2440 ops: &crate::ported::zsh_h::options, func: i32) -> i32 {
2441
2442 // PFA-SMR aspect: bin_typeset is the C dispatch site for
2443 // typeset/declare/integer/float/local/export/readonly/private —
2444 // every one of those state-mutating builtins lands here with a
2445 // funcid (BIN_EXPORT/BIN_READONLY/BIN_TYPESET/...) discriminant.
2446 // Emit a per-name event per the recorder schema.
2447 #[cfg(feature = "recorder")]
2448 if crate::recorder::is_enabled() {
2449 let ctx = crate::recorder::recorder_ctx_global();
2450 // Collect option letters (`-x`/`+x` body) so ParamAttrs reflects
2451 // the typeset flag set the C source sees in `on`.
2452 let mut letters = String::new();
2453 let mut tied_mode = false;
2454 for a in argv {
2455 if a.starts_with('-') || a.starts_with('+') {
2456 let body = &a[1..];
2457 letters.push_str(body);
2458 if body.contains('T') { tied_mode = true; }
2459 }
2460 }
2461 // Funcid-driven attr seeding: BIN_EXPORT seeds nothing
2462 // (recorder uses emit_export for those), BIN_READONLY seeds
2463 // SCALAR|READONLY, BIN_FLOAT seeds FLOAT, BIN_INTEGER seeds
2464 // INTEGER. Otherwise pass the letter set through
2465 // ParamAttrs::from_flag_chars verbatim.
2466 let mut attrs = crate::recorder::ParamAttrs::from_flag_chars(&letters);
2467 match func {
2468 crate::ported::builtin::BIN_READONLY => {
2469 attrs.set(crate::recorder::ParamAttrs::SCALAR);
2470 attrs.set(crate::recorder::ParamAttrs::READONLY);
2471 }
2472 _ => {}
2473 }
2474 // BIN_EXPORT routes to emit_export (different schema row).
2475 if func == crate::ported::builtin::BIN_EXPORT {
2476 for a in argv {
2477 if a == "-p" || a.starts_with('-') { continue; }
2478 if let Some((k, v)) = a.split_once('=') {
2479 crate::recorder::emit_export(k, Some(v), ctx.clone());
2480 } else {
2481 crate::recorder::emit_export(a, None, ctx.clone());
2482 }
2483 }
2484 } else {
2485 // Suppress the emit when invoked as `local`/`private` inside
2486 // a function — those scope to the frame and don't merit a
2487 // top-level state-mutation row. local_scope_depth is tracked
2488 // by the executor; defer to the global LOCALLEVEL counter.
2489 let is_locallike = matches!(name, "local" | "private");
2490 let inside_function =
2491 LOCALLEVEL.load(std::sync::atomic::Ordering::Relaxed) > 0;
2492 if !is_locallike || !inside_function {
2493 let mut tied_seen = 0usize;
2494 for a in argv {
2495 if a.starts_with('-') || a.starts_with('+') { continue; }
2496 if tied_mode {
2497 // For `typeset -T X Y [SEP]`, only X and Y are names.
2498 tied_seen += 1;
2499 if tied_seen > 2 { break; }
2500 }
2501 if let Some((k, v)) = a.split_once('=') {
2502 crate::recorder::emit_typeset_attrs(k, Some(v), attrs, ctx.clone());
2503 } else {
2504 crate::recorder::emit_typeset_attrs(a, None, attrs, ctx.clone());
2505 }
2506 }
2507 }
2508 }
2509 }
2510 let mut ops = ops.clone();
2511 let mut on: u32 = 0; // c:2661
2512 let mut off: u32 = 0; // c:2661
2513 let returnval: i32 = 0; // c:2664
2514 let mut printflags: i32 = PRINT_WITH_NAMESPACE; // c:2664
2515 let hasargs = !argv.is_empty(); // c:2665
2516
2517 // c:2668-2670 — POSIX bash/ksh ignore -p with args under
2518 // readonly/export.
2519 let posix = crate::ported::zsh_h::isset(crate::ported::options::optlookup("posixbuiltins"));
2520 if (func == BIN_READONLY || func == BIN_EXPORT) && posix && hasargs { // c:2668
2521 ops.ind[b'p' as usize] = 0; // c:2670
2522 }
2523
2524 // c:2673 — `if (OPT_ISSET(ops,'f')) return bin_functions(...)`.
2525 if OPT_ISSET(&ops, b'f') { // c:2673
2526 return bin_functions(name, argv, &ops, func); // c:2673
2527 }
2528
2529 // c:2676 — POSIX readonly forces -g unless explicit +g.
2530 if func == BIN_READONLY && posix && !OPT_PLUS(&ops, b'g') { // c:2676
2531 ops.ind[b'g' as usize] = 1; // c:2677
2532 }
2533
2534 // c:2691-2706 — translate optstr letters into PM_* flag bits.
2535 let mut bit: u32 = PM_ARRAY; // c:2660
2536 for ch in TYPESET_OPTSTR.chars() { // c:2691
2537 let optval = ch as u8;
2538 if OPT_MINUS(&ops, optval) { on |= bit; } // c:2694-2695
2539 else if OPT_PLUS(&ops, optval) { off |= bit; } // c:2696-2697
2540 // c:2698-2706 — `-n` only allows readonly/upper/hideval.
2541 else { bit <<= 1; continue; }
2542 if OPT_MINUS(&ops, b'n')
2543 && (bit & !(PM_READONLY | PM_UPPER | PM_HIDEVAL)) != 0 // c:2701
2544 {
2545 crate::ported::utils::zwarnnam(name,
2546 &format!("-{} not allowed with -n", ch)); // c:2702
2547 }
2548 bit <<= 1;
2549 }
2550 // c:2708-2715 — -n / +n conflict resolution.
2551 if OPT_MINUS(&ops, b'n') { // c:2708
2552 if (on | off) & !(PM_READONLY | PM_UPPER | PM_HIDEVAL) != 0 { // c:2710
2553 return 1; // c:2711
2554 }
2555 on |= PM_NAMEREF; // c:2713
2556 } else if OPT_PLUS(&ops, b'n') { // c:2714
2557 off |= PM_NAMEREF; // c:2715
2558 }
2559 let roff = off; // c:2716
2560
2561 // c:2719-2740 — sanity checks: remove conflicting attrs.
2562 if (on & PM_FFLOAT) != 0 { // c:2719
2563 off |= PM_UPPER | PM_ARRAY | PM_HASHED | PM_INTEGER | PM_EFLOAT; // c:2720
2564 on &= !PM_EFLOAT; // c:2722
2565 }
2566 if (on & PM_EFLOAT) != 0 { // c:2724
2567 off |= PM_UPPER | PM_ARRAY | PM_HASHED | PM_INTEGER | PM_FFLOAT; // c:2725
2568 }
2569 if (on & PM_INTEGER) != 0 { // c:2726
2570 off |= PM_UPPER | PM_ARRAY | PM_HASHED | PM_EFLOAT | PM_FFLOAT; // c:2727
2571 }
2572 if (on & (PM_LEFT | PM_RIGHT_Z)) != 0 { // c:2731
2573 off |= PM_RIGHT_B; // c:2732
2574 }
2575 if (on & PM_RIGHT_B) != 0 { // c:2733
2576 off |= PM_LEFT | PM_RIGHT_Z; // c:2734
2577 }
2578 if (on & PM_UPPER) != 0 { off |= PM_LOWER; } // c:2735-2736
2579 if (on & PM_LOWER) != 0 { off |= PM_UPPER; } // c:2737-2738
2580 if (on & PM_HASHED) != 0 { off |= PM_ARRAY; } // c:2739-2740
2581 if (on & PM_TIED) != 0 { // c:2741
2582 off |= PM_INTEGER | PM_EFLOAT | PM_FFLOAT | PM_ARRAY | PM_HASHED; // c:2742
2583 }
2584 on &= !off; // c:2744
2585
2586 crate::ported::mem::queue_signals(); // c:2746
2587
2588 // c:2748-2772 — `-p` print-mode: PRINT_POSIX_EXPORT / READONLY /
2589 // TYPESET, plus optional -p N for line-style.
2590 if OPT_ISSET(&ops, b'p') { // c:2748
2591 if posix && !EMULATION(EMULATE_KSH) { // c:2750
2592 printflags |= match func {
2593 BIN_EXPORT => PRINT_POSIX_EXPORT, // c:2752
2594 BIN_READONLY => PRINT_POSIX_READONLY, // c:2754
2595 _ => PRINT_TYPESET, // c:2756
2596 };
2597 } else {
2598 printflags |= PRINT_TYPESET; // c:2758
2599 }
2600 if OPT_HASARG(&ops, b'p') { // c:2761
2601 let arg = OPT_ARG(&ops, b'p').unwrap_or("");
2602 match arg.trim().parse::<i32>() { // c:2763
2603 Ok(1) => printflags |= PRINT_LINE, // c:2765
2604 Ok(0) => {} // c:2770 -p0 == -p
2605 _ => {
2606 crate::ported::utils::zwarnnam(name,
2607 &format!("bad argument to -p: {}", arg)); // c:2767
2608 crate::ported::mem::unqueue_signals();
2609 return 1; // c:2769
2610 }
2611 }
2612 }
2613 }
2614
2615 // c:2775-2795 — no-args path: list whatever options select.
2616 if !hasargs { // c:2775
2617 if !OPT_ISSET(&ops, b'm') { // c:2779
2618 printflags &= !PRINT_WITH_NAMESPACE; // c:2780
2619 }
2620 if !OPT_ISSET(&ops, b'p') { // c:2782
2621 if (on | roff) == 0 { // c:2783
2622 printflags |= PRINT_TYPE; // c:2784
2623 }
2624 if roff != 0 || OPT_ISSET(&ops, b'+') { // c:2785
2625 printflags |= PRINT_NAMEONLY; // c:2786
2626 }
2627 }
2628 // c:2792 — `scanhashtable(paramtab, 1, on|roff, 0, paramtab->printnode,
2629 // printflags|(roff ? PRINT_NAMEONLY : 0));`
2630 //
2631 // Walks the paramtab (sorted=1, alphabetical) filtering by
2632 // the typeset flags. The previous Rust port walked
2633 // `std::env::vars()` — OS env — same divergence as the
2634 // prior bin_set + bin_unset -m fixes. Shell-internal vars
2635 // (not exported) never appeared in `typeset` listings; the
2636 // \`on\`/\`roff\` flag filter was also ignored, so `typeset
2637 // -p +g` showed ALL env vars regardless of which typeset
2638 // flags the user requested.
2639 let mut entries: Vec<(String, String)> = {
2640 let tab = crate::ported::params::paramtab().read().unwrap();
2641 tab.iter()
2642 .filter(|(_, pm)| {
2643 let f = pm.node.flags as u32;
2644 if (f & crate::ported::zsh_h::PM_UNSET) != 0 {
2645 return false;
2646 }
2647 // c:2792 scanmatchtable flags1=on|roff, flags2=0.
2648 let on_roff = (on as u32) | (roff as u32);
2649 on_roff == 0 || (f & on_roff) != 0
2650 })
2651 .map(|(k, pm)| {
2652 let v = pm.u_str.clone().unwrap_or_default();
2653 (k.clone(), v)
2654 })
2655 .collect()
2656 };
2657 entries.sort_by(|a, b| {
2658 crate::ported::hashtable::hnamcmp(&a.0, &b.0)
2659 });
2660 for (k, v) in entries {
2661 if (printflags & PRINT_NAMEONLY) != 0 {
2662 println!("{}", k);
2663 } else {
2664 println!("{}={}", k,
2665 crate::ported::utils::quotedzputs(&v));
2666 }
2667 }
2668 crate::ported::mem::unqueue_signals();
2669 return 0; // c:2794
2670 }
2671
2672 // c:2799-2810 — `local` (or +g) implies PM_LOCAL.
2673 let nm0 = name.chars().next().unwrap_or(' ');
2674 if nm0 == 'l' || OPT_PLUS(&ops, b'g') { // c:2799
2675 on |= PM_LOCAL; // c:2800
2676 } else if !OPT_ISSET(&ops, b'g') { // c:2801
2677 if OPT_MINUS(&ops, b'x') { // c:2802
2678 let globalexport = crate::ported::zsh_h::isset(crate::ported::options::optlookup("globalexport"));
2679 let locallevel = LOCALLEVEL.load(std::sync::atomic::Ordering::Relaxed);
2680 if globalexport { // c:2803
2681 ops.ind[b'g' as usize] = 1; // c:2804
2682 } else if locallevel != 0 { // c:2805
2683 on |= PM_LOCAL; // c:2806
2684 }
2685 } else if !(OPT_ISSET(&ops, b'x') || OPT_ISSET(&ops, b'm')) { // c:2808
2686 on |= PM_LOCAL; // c:2809
2687 }
2688 }
2689
2690 // c:2813+ — -T tied vars + per-arg setting loop.
2691 // The full C body has dozens of paths (PM_TIED tie-pair setup at
2692 // c:2813-2900, glob -m walk at c:2905-2935, name=value assign
2693 // through typeset_single at c:2945+). The Rust port handles the
2694 // three high-frequency paths inline: assoc creation (`PM_HASHED`
2695 // + `name=(k v k v)`), array creation (`PM_ARRAY` + `name=(a b c)`),
2696 // and scalar assignment.
2697 let _ = (off, returnval, name);
2698 let is_hashed = (on & PM_HASHED) != 0; // c:2655 `-A`
2699 let is_array = (on & PM_ARRAY) != 0; // c:2655 `-a`
2700 for arg in argv {
2701 // c:Src/builtin.c typeset_single — when PM_LOCAL is in
2702 // flags, createparam first to install pm.old chain at
2703 // locallevel (createparam c:1132-1147). Applies uniformly
2704 // to all forms: `local x`, `local x=v`, `local arr=(...)`,
2705 // `local -A h`. endparamscope unwinds via Param.old.
2706 let arg_name: &str = match arg.find('=') {
2707 Some(i) => &arg[..i],
2708 None => arg.as_str(),
2709 };
2710 if (on & PM_LOCAL) != 0
2711 && !arg_name.is_empty()
2712 && !arg_name.starts_with('-')
2713 && !arg_name.starts_with('+')
2714 {
2715 let kind = if is_hashed { PM_HASHED } else if is_array { PM_ARRAY } else { 0 };
2716 let _ = crate::ported::params::createparam(
2717 arg_name, on as i32 | kind as i32 | PM_LOCAL as i32);
2718 }
2719 if let Some(eq) = arg.find('=') {
2720 let n = &arg[..eq];
2721 let raw_v = &arg[eq + 1..];
2722 // c:2945-3050 — `=(elem elem ...)` array-init syntax.
2723 // The parser hands the whole `(...)` body in as one arg
2724 // when typeset's BINF_MAGICEQUALS is set; the `(` / `)` are
2725 // literal first/last bytes. Strip them and split on
2726 // whitespace to recover the element list.
2727 let is_paren_init = raw_v.starts_with('(') && raw_v.ends_with(')')
2728 && raw_v.len() >= 2;
2729 if is_paren_init {
2730 let inner = &raw_v[1..raw_v.len()-1]; // c:2950
2731 let elems: Vec<String> = inner.split_whitespace() // c:2952
2732 .map(String::from)
2733 .collect();
2734 if is_hashed {
2735 // c:2960-2975 — `setdataparam(..., PM_HASHED, …)`.
2736 // Two assoc-init shapes accepted by zsh:
2737 // 1. flat alternating k/v: `m=(k1 v1 k2 v2)`
2738 // 2. per-element [K]=V: `m=([k1]=v1 [k2]=v2)`
2739 // The parser hands all elements as one `(…)` body,
2740 // so we detect shape 2 when every element starts
2741 // with `[` and contains `]=`. Otherwise fall back
2742 // to alternating pairs.
2743 let bracket_shape = !elems.is_empty()
2744 && elems.iter().all(|e| {
2745 e.starts_with('[')
2746 && e.contains("]=")
2747 });
2748 let mut map: indexmap::IndexMap<String, String>
2749 = indexmap::IndexMap::new();
2750 if bracket_shape {
2751 for e in &elems {
2752 let close = e.find("]=").unwrap();
2753 let k = e[1..close].to_string();
2754 let v = e[close + 2..].to_string();
2755 map.insert(k, v);
2756 }
2757 } else {
2758 let mut it = elems.into_iter(); // c:2960 pair walk
2759 while let Some(k) = it.next() {
2760 let v = it.next().unwrap_or_default();
2761 map.insert(k, v); // c:2964 hashtab insert
2762 }
2763 }
2764 let n_owned = n.to_string();
2765 crate::fusevm_bridge::with_executor(|exec| {
2766 exec.set_assoc(n_owned, map.clone());
2767 });
2768 } else {
2769 // c:2980-2995 — plain array.
2770 let n_owned = n.to_string();
2771 let elems_owned = elems.clone();
2772 crate::fusevm_bridge::with_executor(|exec| {
2773 exec.set_array(n_owned, elems_owned);
2774 });
2775 }
2776 } else {
2777 // c:3010-3030 — `name=value` scalar assign. C-canonical
2778 // `setsparam` (Src/params.c:3350) writes paramtab; the
2779 // env mirror at `Src/params.c:3024 addenv` follows.
2780 // c:Src/params.c PM_LOWER/PM_UPPER setstrvalue arms:
2781 // when typeset -l or -u is set, the assigned value is
2782 // case-folded BEFORE storage. Without this, `typeset -l
2783 // s=HELLO; echo $s` printed `HELLO`. We also mirror to
2784 // exec.var_attrs so subsequent plain assigns (`s=NEW`)
2785 // pick up the fold via the SET_VAR opcode's attr
2786 // check (fusevm_bridge.rs case-fold arm).
2787 let lower = (on & PM_LOWER) != 0;
2788 let upper = (on & PM_UPPER) != 0;
2789 let folded: String = if lower {
2790 raw_v.to_lowercase()
2791 } else if upper {
2792 raw_v.to_uppercase()
2793 } else {
2794 raw_v.to_string()
2795 };
2796 crate::ported::params::setsparam(n, &folded); // c:params.c:3350
2797 // c:Src/params.c:3024 addenv — only mirror to OS env
2798 // when PM_EXPORTED is in flags or already-exported.
2799 // The unconditional env::set_var here was a pre-
2800 // existing bug exposed by Task 25: local scalars
2801 // were leaking to env, surviving endparamscope.
2802 let already_exported = std::env::var_os(n).is_some();
2803 if (on & crate::ported::zsh_h::PM_EXPORTED) != 0 || already_exported {
2804 std::env::set_var(n, &folded); // c:3024 addenv
2805 }
2806 // C-canonical: typeset -i / -F / -E / -l / -u / -r set
2807 // PM_INTEGER / PM_FFLOAT / PM_EFLOAT / PM_LOWER /
2808 // PM_UPPER / PM_READONLY on the Param (Src/builtin.c
2809 // typeset_single + Src/params.c assignsparam). We set
2810 // them on the just-created paramtab entry so SET_VAR
2811 // and subsequent reads see the type metadata in one
2812 // canonical place — no exec.var_attrs mirror needed.
2813 let type_mask = (PM_INTEGER | PM_EFLOAT | PM_FFLOAT
2814 | PM_LOWER | PM_UPPER | PM_READONLY) as i32;
2815 let to_set = (on & (PM_INTEGER | PM_EFLOAT | PM_FFLOAT
2816 | PM_LOWER | PM_UPPER | PM_READONLY)) as i32;
2817 if to_set != 0 {
2818 if let Ok(mut tab) = crate::ported::params::paramtab().write() {
2819 if let Some(pm) = tab.get_mut(n) {
2820 pm.node.flags = (pm.node.flags & !type_mask) | to_set;
2821 }
2822 }
2823 }
2824 }
2825 } else if is_hashed || is_array {
2826 // c:3060-3070 — bare name + `-A`/`-a` declares an empty
2827 // assoc/array.
2828 let n_owned = arg.clone();
2829 crate::fusevm_bridge::with_executor(|exec| {
2830 if is_hashed {
2831 if exec.assoc(&n_owned).is_none() {
2832 exec.set_assoc(n_owned.clone(), indexmap::IndexMap::new());
2833 }
2834 } else if exec.array(&n_owned).is_none() {
2835 exec.set_array(n_owned.clone(), Vec::new());
2836 }
2837 });
2838 } else {
2839 // c:3072 — `if (!getsparam(arg)) setsparam(arg, "")`. Bare
2840 // name + no type flag declares an empty scalar
2841 // when none exists. C consults paramtab; was
2842 // checking OS env which never sees scalar-only
2843 // params (a `local foo` would be invisible).
2844 if crate::ported::params::getsparam(arg).is_none() {
2845 crate::ported::params::setsparam(arg, ""); // c:3074
2846 }
2847 }
2848 }
2849 crate::ported::mem::unqueue_signals();
2850 0
2851}
2852
2853/// Port of `eval_autoload(Shfunc shf, char *name, Options ops, int func)` from Src/builtin.c:3166.
2854/// C: `int eval_autoload(Shfunc shf, char *name, Options ops, int func)`.
2855/// PM_UNDEFINED guard; -X spawns the eval-trampoline, otherwise loadautofn
2856/// resolves and installs the body.
2857/// WARNING: param names don't match C — Rust=(shf, name, func) vs C=(shf, name, ops, func)
2858pub fn eval_autoload(shf: *mut crate::ported::zsh_h::shfunc, name: &str, // c:3166
2859 ops: &crate::ported::zsh_h::options, func: i32) -> i32 {
2860 if shf.is_null() { return 1; }
2861 let shf_mut = unsafe { &mut *shf };
2862 // c:3168-3169 — `if (!(shf->node.flags & PM_UNDEFINED)) return 1;`
2863 if (shf_mut.node.flags as u32 & PM_UNDEFINED) == 0 { // c:3168
2864 return 1; // c:3169
2865 }
2866 // c:3171-3174 — `if (shf->funcdef) { freeeprog(shf->funcdef); shf->funcdef = &dummy_eprog; }`
2867 if shf_mut.funcdef.is_some() { // c:3171
2868 shf_mut.funcdef = None; // c:3173 freeeprog + dummy
2869 }
2870 // c:3175-3181 — `-X` spawns the autoload trampoline via bin_eval.
2871 if OPT_MINUS(ops, b'X') { // c:3175
2872 // c:3177 — `fargv[0] = quotestring(name, QT_SINGLE_OPTIONAL); fargv[1] = "\"$@\"";`
2873 let fargv = vec![ // c:3177-3179
2874 crate::ported::utils::quotedzputs(name),
2875 "\"$@\"".to_string(),
2876 ];
2877 // c:3180 — `shf->funcdef = mkautofn(shf);`
2878 let p = mkautofn(shf); // c:3180
2879 let _ = p; // funcdef writeback handled inside mkautofn at c:3801
2880 return bin_eval(name, &fargv, ops, func); // c:3181
2881 }
2882 // c:3184-3186 — `return !loadautofn(shf, (OPT_ISSET('k') ? 2 :
2883 // (OPT_ISSET('z') ? 0 : 1)), 1,
2884 // OPT_ISSET('d'));`
2885 let mode = if OPT_ISSET(ops, b'k') { 2 } // c:3184
2886 else if OPT_ISSET(ops, b'z') { 0 } // c:3185
2887 else { 1 };
2888 let _d = OPT_ISSET(ops, b'd');
2889 // loadautofn lives in Src/exec.c:5050 — full fpath search + parse_string
2890 // + install. Static-link path: returns 0 (success), so `!loadautofn` is 1.
2891 let r = crate::exec::loadautofn(shf, mode, 1, _d as i32); // c:3193
2892 if r == 0 { 1 } else { 0 }
2893}
2894
2895
2896/// Port of `check_autoload(Shfunc shf, char *name, Options ops, int func)` from Src/builtin.c:3193.
2897/// C: `static int check_autoload(Shfunc shf, char *name, Options ops,
2898/// int func)` — `OPT_ISSET(ops,'X')` ? eval_autoload : 0.
2899/// WARNING: param names don't match C — Rust=(shf, name, func) vs C=(shf, name, ops, func)
2900pub fn check_autoload(shf: *mut crate::ported::zsh_h::shfunc, name: &str, // c:3193
2901 ops: &crate::ported::zsh_h::options, func: i32) -> i32 {
2902 // c:3196-3199 — `if (OPT_ISSET(ops,'X')) return eval_autoload(...);`
2903 if OPT_ISSET(ops, b'X') { // c:3196
2904 return eval_autoload(shf, name, ops, func); // c:3197
2905 }
2906 // c:3200-3242 — -r / -R re-resolve: walk fpath for the function file.
2907 let want_r = OPT_ISSET(ops, b'r');
2908 let want_R = OPT_ISSET(ops, b'R');
2909 if (want_r || want_R) && !shf.is_null() { // c:3200
2910 let shf_mut = unsafe { &mut *shf };
2911 if (shf_mut.node.flags as u32 & PM_UNDEFINED) == 0 {
2912 return 0;
2913 }
2914 // c:3202-3216 — already has filename + PM_LOADDIR: try the cached
2915 // dir first via spec_path[].
2916 if (shf_mut.node.flags as u32 & PM_LOADDIR) != 0
2917 && shf_mut.filename.is_some()
2918 {
2919 let spec = vec![shf_mut.filename.clone().unwrap_or_default()];
2920 if crate::exec::getfpfunc(&shf_mut.node.nam, &mut None, // c:3206
2921 Some(&spec), 1).is_some() {
2922 return 0; // c:3209
2923 }
2924 // c:3211-3217 — `-d` not set: bail (with -R = error, with -r = silent).
2925 if !OPT_ISSET(ops, b'd') { // c:3211
2926 if want_R { // c:3212
2927 crate::ported::utils::zerr(&format!(
2928 "{}: function definition file not found",
2929 shf_mut.node.nam)); // c:3213
2930 return 1; // c:3215
2931 }
2932 return 0; // c:3216
2933 }
2934 }
2935 // c:3219-3231 — fpath walk via getfpfunc + dircache_set install.
2936 let mut dir_path: Option<String> = None;
2937 if crate::exec::getfpfunc(&shf_mut.node.nam, &mut dir_path, None, 1).is_some() // c:3219
2938 && dir_path.is_some()
2939 {
2940 // c:3220-3228 — dircache_set + relative-path absolutize.
2941 let mut old_slot = shf_mut.filename.take();
2942 crate::ported::hashtable::dircache_set(&mut old_slot, None); // c:3220
2943 let dp = dir_path.unwrap();
2944 let mut new_slot: Option<String> = None;
2945 crate::ported::hashtable::dircache_set(&mut new_slot, Some(&dp));// c:3228
2946 shf_mut.filename = new_slot;
2947 shf_mut.node.flags |= PM_LOADDIR as i32; // c:3229
2948 return 0; // c:3230
2949 }
2950 // c:3233-3239 — -R: error; -r: silent.
2951 if want_R { // c:3233
2952 crate::ported::utils::zerr(&format!(
2953 "{}: function definition file not found",
2954 shf_mut.node.nam)); // c:3243
2955 return 1; // c:3243
2956 }
2957 }
2958 0 // c:3243
2959}
2960
2961
2962/// Port of `listusermathfunc(MathFunc p)` from Src/builtin.c:3243.
2963/// C: `static void listusermathfunc(MathFunc p)` — emit a `functions -M`
2964/// row for one user math function with arg counts and module name.
2965pub fn listusermathfunc(p: &crate::ported::zsh_h::mathfunc) { // c:3243
2966 // c:3247-3257 — pick `showargs` 0..3 based on module/min/max presence.
2967 let mut showargs: i32 = if p.module.is_some() { // c:3249
2968 3
2969 } else if p.maxargs != if p.minargs != 0 { p.minargs } else { -1 } { // c:3251
2970 2
2971 } else if p.minargs != 0 { // c:3253
2972 1
2973 } else {
2974 0 // c:3256
2975 };
2976
2977 // c:3259 — `printf("functions -M%s %s", (p->flags & MFF_STR) ? "s" : "", p->name);`
2978 let s_suffix = if (p.flags & MFF_STR) != 0 { "s" } else { "" }; // c:3259
2979 print!("functions -M{} {}", s_suffix, p.name); // c:3259
2980 if showargs != 0 { // c:3260
2981 print!(" {}", p.minargs); // c:3261
2982 showargs -= 1; // c:3262
2983 }
2984 if showargs != 0 { // c:3264
2985 print!(" {}", p.maxargs); // c:3265
2986 showargs -= 1; // c:3266
2987 }
2988 if showargs != 0 { // c:3268
2989 // c:3269-3274 — function names are not required to be ident chars,
2990 // so the module name goes through quotedzputs for safe printing.
2991 print!(" "); // c:3273
2992 print!("{}", crate::ported::utils::quotedzputs(p.module.as_deref().unwrap_or(""))); // c:3274
2993 showargs -= 1; // c:3275
2994 }
2995 println!(); // c:3277
2996}
2997
2998/// Port of `add_autoload_function(Shfunc shf, char *funcname)` from Src/builtin.c:3278.
2999/// C: `static void add_autoload_function(Shfunc shf, char *funcname)` —
3000/// two branches:
3001/// (a) funcname is absolute & shf is PM_UNDEFINED → split `/dir/nam`,
3002/// dircache_set(&shf->filename, dir), set PM_LOADDIR|PM_ABSPATH_USED,
3003/// shfunctab->addnode(nam, shf).
3004/// (b) otherwise → walk funcstack to find calling function; if it has
3005/// PM_LOADDIR|PM_ABSPATH_USED, build `"<calling-dir>/funcname"` and
3006/// access(R_OK); on success copy the dir into shf and set
3007/// PM_LOADDIR|PM_ABSPATH_USED. Then shfunctab->addnode(funcname, shf).
3008/// WARNING: param names don't match C — Rust=(shf) vs C=(shf, funcname)
3009pub fn add_autoload_function(shf: *mut crate::ported::zsh_h::shfunc, // c:3278
3010 funcname: &str) {
3011 if shf.is_null() || funcname.is_empty() { return; }
3012 let shf_ref = unsafe { &mut *shf };
3013
3014 let is_abs_path = funcname.starts_with('/') // c:3282
3015 && funcname.len() > 1
3016 && funcname[1..].contains('/')
3017 && (shf_ref.node.flags as u32 & PM_UNDEFINED) != 0;
3018
3019 if is_abs_path {
3020 // c:3287 — `nam = strrchr(funcname, '/');`
3021 let nam_idx = funcname.rfind('/').unwrap(); // c:3287
3022 let (dir, nam) = if nam_idx == 0 { // c:3289
3023 ("/".to_string(), funcname[1..].to_string()) // c:3290
3024 } else {
3025 (funcname[..nam_idx].to_string(), // c:3293
3026 funcname[nam_idx + 1..].to_string())
3027 };
3028 // c:3296 — `dircache_set(&shf->filename, NULL); dircache_set(..., dir);`
3029 let mut old_slot = shf_ref.filename.take();
3030 crate::ported::hashtable::dircache_set(&mut old_slot, None); // c:3296
3031 let mut new_slot: Option<String> = None;
3032 crate::ported::hashtable::dircache_set(&mut new_slot, Some(&dir)); // c:3297
3033 shf_ref.filename = new_slot;
3034 // c:3298-3299 — `shf->node.flags |= PM_LOADDIR | PM_ABSPATH_USED;`
3035 shf_ref.node.flags |= (PM_LOADDIR | PM_ABSPATH_USED) as i32; // c:3298
3036 // c:3300 — `shfunctab->addnode(shfunctab, ztrdup(nam), shf);`
3037 if let Ok(mut t) = shfunctab_table().lock() {
3038 t.insert(nam, shf as usize); // c:3300
3039 }
3040 } else {
3041 // c:3304-3327 — walk funcstack, look up calling fn in shfunctab, if
3042 // it has PM_LOADDIR|PM_ABSPATH_USED build "<dir>/<funcname>" and
3043 // access(R_OK), inherit the dir on hit.
3044 let calling_f: Option<String> = {
3045 let stack = crate::ported::modules::parameter::FUNCSTACK
3046 .lock().map(|s| s.clone()).unwrap_or_default();
3047 // c:3306 — `for (fs = funcstack; fs; fs = fs->prev)`
3048 stack.iter().rev().find(|fs| { // c:3306
3049 // c:3307 — `if (fs->tp == FS_FUNC && fs->name &&
3050 // (!shf->node.nam || strcmp(fs->name, shf->node.nam)))`
3051 FS_FUNC != 0 // mirror struct doesn't expose tp directly;
3052 && !fs.name.is_empty()
3053 && (shf_ref.node.nam.is_empty() || fs.name != shf_ref.node.nam)
3054 }).map(|fs| fs.name.clone()) // c:3308
3055 };
3056 if let Some(cf) = calling_f { // c:3315
3057 // c:3316 — `shf2 = shfunctab->getnode2(shfunctab, calling_f);`
3058 let shf2_ptr = shfunctab_table().lock()
3059 .ok()
3060 .and_then(|t| t.get(&cf).copied())
3061 .unwrap_or(0) as *mut crate::ported::zsh_h::shfunc;
3062 if !shf2_ptr.is_null() {
3063 let shf2 = unsafe { &*shf2_ptr };
3064 // c:3317-3318
3065 let needs = (PM_LOADDIR | PM_ABSPATH_USED) as i32;
3066 if (shf2.node.flags & needs) == needs { // c:3317
3067 if let Some(dir2) = &shf2.filename { // c:3318
3068 // c:3320 — `snprintf(buf, PATH_MAX, "%s/%s", dir2, funcname);`
3069 let buf = format!("{}/{}", dir2, funcname); // c:3320
3070 if buf.len() <= libc::PATH_MAX as usize { // c:3320
3071 // c:3324 — `if (!access(buf, R_OK))`
3072 let buf_c = std::ffi::CString::new(buf.clone()).ok();
3073 if let Some(bc) = buf_c {
3074 if unsafe { libc::access(bc.as_ptr(), libc::R_OK) } == 0 { // c:3324
3075 let mut old_slot = shf_ref.filename.take();
3076 crate::ported::hashtable::dircache_set(&mut old_slot, None); // c:3325
3077 let dir2c = dir2.clone();
3078 let mut new_slot: Option<String> = None;
3079 crate::ported::hashtable::dircache_set(&mut new_slot, Some(&dir2c)); // c:3326
3080 shf_ref.filename = new_slot;
3081 shf_ref.node.flags |= (PM_LOADDIR | PM_ABSPATH_USED) as i32; // c:3327
3082 }
3083 }
3084 }
3085 }
3086 }
3087 }
3088 }
3089 // c:3334 — `shfunctab->addnode(shfunctab, ztrdup(funcname), shf);`
3090 if let Ok(mut t) = shfunctab_table().lock() {
3091 t.insert(funcname.to_string(), shf as usize); // c:3334
3092 }
3093 }
3094}
3095
3096/// Port of `bin_functions(char *name, char **argv, Options ops, int func)` from Src/builtin.c:3342.
3097/// C: `int bin_functions(char *name, char **argv, Options ops, int func)`.
3098/// This is the canonical free-function port matching the C signature so
3099/// the dispatcher can call it. The earlier `ShellExecutor::bin_functions`
3100/// inherent method is an ad-hoc Rust-side helper kept for the existing
3101/// in-process executor; both should converge on this function.
3102/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
3103pub fn bin_functions(name: &str, argv: &[String], // c:3342
3104 ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
3105 // c:3346-3347 — `int returnval = 0; int on = 0, off = 0, pflags = 0,
3106 // roff, expand = 0;`
3107 let mut returnval: i32 = 0; // c:3346
3108 let mut on: u32 = 0; // c:3347
3109 let mut off: u32 = 0; // c:3347
3110 let _pflags: i32 = 0; // c:3347
3111 let _expand: i32 = 0; // c:3347
3112
3113 // c:3350-3351 — `if (OPT_PLUS(ops,'u')) off |= PM_UNDEFINED; else if
3114 // (OPT_MINUS(ops,'u') || OPT_ISSET(ops,'X')) on |= PM_UNDEFINED;`
3115 if OPT_PLUS(ops, b'u') { // c:3350
3116 off |= PM_UNDEFINED; // c:3351
3117 } else if OPT_MINUS(ops, b'u') || OPT_ISSET(ops, b'X') { // c:3352
3118 on |= PM_UNDEFINED; // c:3353
3119 }
3120 // c:3354-3357 — -U / +U toggle PM_UNALIASED|PM_UNDEFINED.
3121 if OPT_MINUS(ops, b'U') { // c:3354
3122 on |= PM_UNALIASED | PM_UNDEFINED; // c:3355
3123 } else if OPT_PLUS(ops, b'U') { // c:3356
3124 off |= PM_UNALIASED; // c:3357
3125 }
3126 // c:3358-3361 — -t / +t toggle PM_TAGGED.
3127 if OPT_MINUS(ops, b't') { // c:3358
3128 on |= PM_TAGGED; // c:3359
3129 } else if OPT_PLUS(ops, b't') { // c:3360
3130 off |= PM_TAGGED; // c:3361
3131 }
3132 // c:3362-3365 — -T / +T toggle PM_TAGGED_LOCAL.
3133 if OPT_MINUS(ops, b'T') { // c:3362
3134 on |= PM_TAGGED_LOCAL; // c:3363
3135 } else if OPT_PLUS(ops, b'T') { // c:3364
3136 off |= PM_TAGGED_LOCAL; // c:3365
3137 }
3138 // c:3366-3369 — -W / +W toggle PM_WARNNESTED.
3139 if OPT_MINUS(ops, b'W') { // c:3366
3140 on |= PM_WARNNESTED; // c:3367
3141 } else if OPT_PLUS(ops, b'W') { // c:3368
3142 off |= PM_WARNNESTED; // c:3369
3143 }
3144 // c:3370 — `roff = off;`
3145 let mut roff = off; // c:3370
3146 // c:3371-3377 — -z / +z PM_ZSHSTORED|PM_KSHSTORED interaction.
3147 if OPT_MINUS(ops, b'z') { // c:3371
3148 on |= PM_ZSHSTORED; // c:3372
3149 off |= PM_KSHSTORED; // c:3373
3150 } else if OPT_PLUS(ops, b'z') { // c:3374
3151 off |= PM_ZSHSTORED; // c:3375
3152 roff |= PM_ZSHSTORED; // c:3376
3153 }
3154 // c:3379-3385 — -k / +k PM_KSHSTORED|PM_ZSHSTORED interaction.
3155 if OPT_MINUS(ops, b'k') { // c:3379
3156 on |= PM_KSHSTORED; // c:3380
3157 off |= PM_ZSHSTORED; // c:3381
3158 } else if OPT_PLUS(ops, b'k') { // c:3382
3159 off |= PM_KSHSTORED; // c:3383
3160 roff |= PM_KSHSTORED; // c:3384
3161 }
3162 // c:3386-3392 — -d / +d PM_CUR_FPATH toggle.
3163 if OPT_MINUS(ops, b'd') { // c:3386
3164 on |= PM_CUR_FPATH; // c:3387
3165 off |= PM_CUR_FPATH; // c:3388
3166 } else if OPT_PLUS(ops, b'd') { // c:3389
3167 off |= PM_CUR_FPATH; // c:3390
3168 roff |= PM_CUR_FPATH; // c:3391
3169 }
3170
3171 // c:3394-3400 — early-error validation: invalid flag combinations.
3172 // C: `(OPT_MINUS(ops,'X') && (OPT_ISSET(ops,'m') || !scriptname))` —
3173 // \`-X\` is only valid in a script context (autoload-from-fpath
3174 // dispatch). Previous Rust port dropped the \`|| !scriptname\` half
3175 // so \`functions -X foo\` from interactive shell silently
3176 // succeeded — divergent.
3177 let scriptname_missing = crate::ported::utils::scriptname_get().is_none();
3178 if (off & PM_UNDEFINED) != 0 // c:3394
3179 || (OPT_ISSET(ops, b'k') && OPT_ISSET(ops, b'z')) // c:3394
3180 || (OPT_ISSET(ops, b'x') && !OPT_HASARG(ops, b'x')) // c:3395
3181 || (OPT_MINUS(ops, b'X') // c:3396
3182 && (OPT_ISSET(ops, b'm') || scriptname_missing)) // c:3396 !scriptname
3183 || (OPT_ISSET(ops, b'c')
3184 && (OPT_ISSET(ops, b'x') || OPT_ISSET(ops, b'X') || OPT_ISSET(ops, b'm')))
3185 {
3186 crate::ported::utils::zwarnnam(name, "invalid option(s)"); // c:3399
3187 return 1; // c:3400
3188 }
3189
3190 // c:3402-3452 — `-c` (clone) branch: copy named function under a new
3191 // name, optionally registering it as a TRAP* signal trap.
3192 if OPT_ISSET(ops, b'c') { // c:3402
3193 if argv.len() < 2 || argv.len() > 2 { // c:3405
3194 crate::ported::utils::zwarnnam(name, "-c: requires two arguments"); // c:3406
3195 return 1;
3196 }
3197 let src_name = &argv[0];
3198 let dst_name = &argv[1];
3199 // c:3409 — `shf = shfunctab->getnode(shfunctab, *argv);`
3200 let src_ptr = shfunctab_table().lock()
3201 .ok()
3202 .and_then(|t| t.get(src_name.as_str()).copied())
3203 .unwrap_or(0) as *mut crate::ported::zsh_h::shfunc;
3204 if src_ptr.is_null() { // c:3410
3205 crate::ported::utils::zwarnnam(name,
3206 &format!("no such function: {}", src_name)); // c:3411
3207 return 1;
3208 }
3209 // c:3414-3421 — autoload-trampoline expansion if PM_UNDEFINED.
3210 // C body: `if (shf->flags & PM_UNDEFINED) { freeeprog;
3211 // funcdef=dummy; shf = loadautofn(shf,1,0,0); if (!shf) return 1; }`.
3212 // Rust port routes through the local loadautofn helper at
3213 // builtin.rs:883 which walks $fpath via getfpfunc, reads the
3214 // file, stores the body text on the Rust-side ShFunc, and
3215 // clears PM_UNDEFINED.
3216 if (unsafe { (*src_ptr).node.flags } as u32 & PM_UNDEFINED) != 0 {
3217 // c:3415-3418 — `freeeprog(shf->funcdef); shf->funcdef =
3218 // &dummy_eprog;` clear out any stale autoload stub before
3219 // re-loading. Rust port: drop the Option<Eprog>.
3220 unsafe {
3221 (*src_ptr).funcdef = None;
3222 }
3223 // c:3419 — `loadautofn(shf, 1, 0, 0)`.
3224 if crate::exec::loadautofn(src_ptr, 1, 0, 0) != 0 {
3225 // c:3420-3421 — autoload failed.
3226 return 1;
3227 }
3228 }
3229 // c:3422-3430 — `newsh = zalloc + memcpy + filename rebuild`.
3230 let src_ref = unsafe { &*src_ptr };
3231 let new_filename = if (src_ref.node.flags as u32 & PM_UNDEFINED) == 0
3232 && src_ref.filename.is_some()
3233 {
3234 src_ref.filename.clone() // c:3429
3235 } else {
3236 None
3237 };
3238 let _ = new_filename; // wired into shfunctab[dst_name] below
3239 // c:3437-3447 — TRAP* prefix detection + signal trap registration.
3240 if dst_name.starts_with("TRAP") { // c:3437
3241 // c:3438 — `int sigidx = getsigidx(s + 4);`
3242 let sigidx = getsigidx(&dst_name[4..]); // c:3438
3243 if sigidx != -1 { // c:3439
3244 // c:3440 — `if (settrap(sigidx, NULL, ZSIG_FUNC))`.
3245 if crate::ported::signals::settrap(
3246 sigidx,
3247 None,
3248 crate::ported::zsh_h::ZSIG_FUNC,
3249 ) != 0 { // c:3440
3250 // freeeprog(newsh->funcdef) — funcdef Drop covers it.
3251 // dircache_set(&newsh->filename, NULL);
3252 // zfree(newsh, sizeof(*newsh));
3253 return 1; // c:3445
3254 }
3255 // c:3447 — `removetrapnode(sigidx);` — clear any prior trap.
3256 crate::ported::jobs::removetrapnode(sigidx); // c:3447
3257 }
3258 }
3259 // c:3450 — `shfunctab->addnode(shfunctab, ztrdup(s), &newsh->node);`
3260 if let Ok(mut t) = shfunctab_table().lock() {
3261 t.insert(dst_name.clone(), src_ptr as usize); // c:3450
3262 }
3263 return 0; // c:3451
3264 }
3265
3266 // c:3454-3463 — `-x N` indent override for printing.
3267 let mut expand: i32 = 0; // c:3454 (also c:3347)
3268 if OPT_ISSET(ops, b'x') { // c:3454
3269 let arg = OPT_ARG(ops, b'x').unwrap_or("");
3270 match arg.trim().parse::<i32>() { // c:3456
3271 Ok(n) => {
3272 expand = n; // c:3456
3273 if expand == 0 { expand = -1; } // c:3461-3462
3274 }
3275 Err(_) => {
3276 crate::ported::utils::zwarnnam(name, "number expected after -x"); // c:3458
3277 return 1; // c:3459
3278 }
3279 }
3280 }
3281
3282 // c:3465-3466 — `+f` / roff / `+` enables PRINT_NAMEONLY.
3283 let mut pflags: i32 = 0;
3284 if OPT_PLUS(ops, b'f') || roff != 0 || OPT_ISSET(ops, b'+') { // c:3465
3285 pflags |= crate::ported::zsh_h::PRINT_NAMEONLY; // c:3466
3286 }
3287
3288 // c:3468-3530 — `-M`/`+M` add/remove/list math function path.
3289 if OPT_MINUS(ops, b'M') || OPT_PLUS(ops, b'M') { // c:3468
3290 // c:3473-3477 — refuse incompatible flag combos.
3291 if on != 0 || off != 0 || pflags != 0
3292 || OPT_ISSET(ops, b'X') || OPT_ISSET(ops, b'u')
3293 || OPT_ISSET(ops, b'U') || OPT_ISSET(ops, b'w')
3294 {
3295 crate::ported::utils::zwarnnam(name, "invalid option(s)"); // c:3475
3296 return 1; // c:3476
3297 }
3298 if argv.is_empty() { // c:3478
3299 // c:3479-3484 — list user math fns.
3300 crate::ported::mem::queue_signals(); // c:3480
3301 if let Ok(table) = crate::ported::module::MATHFUNCS.lock() { // c:3481
3302 for p in table.iter() { // c:3481
3303 if (p.flags & crate::ported::zsh_h::MFF_USERFUNC) != 0 { // c:3482
3304 listusermathfunc(p); // c:3483
3305 }
3306 }
3307 }
3308 crate::ported::mem::unqueue_signals(); // c:3484
3309 return returnval;
3310 } else if OPT_ISSET(ops, b'm') { // c:3485
3311 // c:3486-3515 — list/delete matching math fns by pattern.
3312 for arg in argv.iter() {
3313 crate::ported::mem::queue_signals(); // c:3488
3314 // c:3489 — `tokenize(*argv)`; Rust patcompile handles it.
3315 if let Some(pprog) = crate::ported::pattern::patcompile(
3316 arg, crate::ported::zsh_h::PAT_STATIC, None,
3317 ) { // c:3490
3318 if OPT_PLUS(ops, b'M') { // c:3497
3319 // Delete matching user fns.
3320 if let Ok(mut table) =
3321 crate::ported::module::MATHFUNCS.lock()
3322 {
3323 table.retain(|p| {
3324 !((p.flags & crate::ported::zsh_h::MFF_USERFUNC) != 0
3325 && crate::ported::pattern::pattry(&pprog, &p.name))
3326 });
3327 }
3328 } else {
3329 // c:3502 — listusermathfunc for matches.
3330 if let Ok(table) = crate::ported::module::MATHFUNCS.lock() {
3331 for p in table.iter() {
3332 if (p.flags & crate::ported::zsh_h::MFF_USERFUNC) != 0
3333 && crate::ported::pattern::pattry(&pprog, &p.name)
3334 {
3335 listusermathfunc(p);
3336 }
3337 }
3338 }
3339 }
3340 } else { // c:3509
3341 // c:3510-3512 — bad pattern.
3342 crate::ported::utils::zwarnnam(name, // c:3511
3343 &format!("bad pattern : {}", arg));
3344 returnval = 1; // c:3512
3345 }
3346 crate::ported::mem::unqueue_signals(); // c:3514
3347 }
3348 return returnval;
3349 } else if OPT_PLUS(ops, b'M') { // c:3516
3350 // c:3517-3533 — `+M name…` delete by exact name.
3351 for arg in argv.iter() {
3352 crate::ported::mem::queue_signals(); // c:3519
3353 if let Ok(mut table) = crate::ported::module::MATHFUNCS.lock() {
3354 let idx = table.iter().position(|p| p.name == *arg); // c:3520-3521
3355 if let Some(i) = idx {
3356 if (table[i].flags & crate::ported::zsh_h::MFF_USERFUNC) == 0 {
3357 // c:3522-3527 — library function, refuse.
3358 crate::ported::utils::zwarnnam(name, // c:3523
3359 &format!("+M {}: is a library function", arg));
3360 returnval = 1; // c:3525
3361 } else {
3362 table.remove(i); // c:3528
3363 }
3364 }
3365 }
3366 crate::ported::mem::unqueue_signals(); // c:3532
3367 }
3368 return returnval;
3369 } else {
3370 // c:3535-3611 — `-M name [min [max [mod]]]` add a user math fn.
3371 let mut argv_iter = argv.iter();
3372 let funcname = argv_iter.next().unwrap(); // c:3537
3373 let mut minargs: i32;
3374 let mut maxargs: i32;
3375 if OPT_ISSET(ops, b's') { // c:3541
3376 minargs = 1; // c:3542
3377 maxargs = 1; // c:3542
3378 } else {
3379 minargs = 0; // c:3544
3380 maxargs = -1; // c:3545
3381 }
3382 // c:3548-3552 — bad math function name check.
3383 let bytes = funcname.as_bytes();
3384 let first_bad = bytes.is_empty()
3385 || (bytes[0] as char).is_ascii_digit()
3386 || !bytes.iter().all(|&c| c.is_ascii_alphanumeric() || c == b'_');
3387 if first_bad { // c:3549
3388 crate::ported::utils::zwarnnam(name, // c:3550
3389 &format!("-M {}: bad math function name", funcname));
3390 return 1; // c:3551
3391 }
3392 if let Some(arg) = argv_iter.next() { // c:3554
3393 match arg.parse::<i32>() { // c:3555 zstrtol
3394 Ok(n) if n >= 0 => minargs = n, // c:3556
3395 _ => {
3396 crate::ported::utils::zwarnnam(name, // c:3557
3397 &format!("-M: invalid min number of arguments: {}", arg));
3398 return 1; // c:3559
3399 }
3400 }
3401 if OPT_ISSET(ops, b's') && minargs != 1 { // c:3561
3402 crate::ported::utils::zwarnnam(name, // c:3562
3403 "-Ms: must take a single string argument");
3404 return 1; // c:3563
3405 }
3406 maxargs = minargs; // c:3565
3407 }
3408 if let Some(arg) = argv_iter.next() { // c:3568
3409 match arg.parse::<i32>() { // c:3569
3410 Ok(n) if n >= -1 && (n == -1 || n >= minargs) => maxargs = n,
3411 _ => {
3412 crate::ported::utils::zwarnnam(name, // c:3573
3413 &format!("-M: invalid max number of arguments: {}", arg));
3414 return 1; // c:3576
3415 }
3416 }
3417 if OPT_ISSET(ops, b's') && maxargs != 1 { // c:3578
3418 crate::ported::utils::zwarnnam(name, // c:3579
3419 "-Ms: must take a single string argument");
3420 return 1; // c:3580
3421 }
3422 }
3423 let modname = argv_iter.next().cloned(); // c:3584-3585
3424 if argv_iter.next().is_some() { // c:3586
3425 crate::ported::utils::zwarnnam(name, "-M: too many arguments"); // c:3587
3426 return 1; // c:3588
3427 }
3428 // c:3591-3598 — alloc and populate mathfunc.
3429 let mut flags = crate::ported::zsh_h::MFF_USERFUNC; // c:3593
3430 if OPT_ISSET(ops, b's') { // c:3594
3431 flags |= crate::ported::zsh_h::MFF_STR; // c:3595
3432 }
3433 let new_fn = crate::ported::zsh_h::mathfunc {
3434 next: None, // c:3608 chain via Vec
3435 name: funcname.clone(), // c:3592
3436 flags, // c:3593
3437 nfunc: None,
3438 sfunc: None,
3439 module: modname, // c:3596
3440 minargs, // c:3597
3441 maxargs, // c:3598
3442 funcid: 0,
3443 };
3444 crate::ported::mem::queue_signals(); // c:3600
3445 if let Ok(mut table) = crate::ported::module::MATHFUNCS.lock() {
3446 // c:3601-3606 — remove existing user entry with same name.
3447 if let Some(i) = table.iter().position(|p| p.name == new_fn.name) {
3448 table.remove(i); // c:3603
3449 }
3450 // c:3608-3609 — prepend to mathfuncs head.
3451 table.insert(0, new_fn);
3452 }
3453 crate::ported::mem::unqueue_signals(); // c:3610
3454 return returnval;
3455 }
3456 }
3457
3458 // c:3616-3655 — `-X` re-autoload from inside a function.
3459 if OPT_MINUS(ops, b'X') { // c:3616
3460 if argv.len() > 1 { // c:3620
3461 crate::ported::utils::zwarnnam(name, "-X: too many arguments"); // c:3621
3462 return 1; // c:3622
3463 }
3464 crate::ported::mem::queue_signals(); // c:3624
3465 // c:3625-3633 — walk funcstack to find the enclosing FS_FUNC frame.
3466 let funcname: Option<String> = {
3467 let stack = crate::ported::modules::parameter::FUNCSTACK
3468 .lock().map(|s| s.clone()).unwrap_or_default();
3469 stack.iter().rev().find(|fs| !fs.name.is_empty()) // c:3626
3470 .map(|fs| fs.name.clone()) // c:3631
3471 };
3472 let ret;
3473 if funcname.is_none() { // c:3635
3474 // c:3637 — `zerrnam(name, "bad autoload");`
3475 crate::ported::utils::zwarnnam(name, "bad autoload"); // c:3637
3476 ret = 1; // c:3638
3477 } else {
3478 let fname = funcname.unwrap();
3479 // c:3640-3647 — getnode(shfunctab, funcname) || addnode(new shf).
3480 let shf_ptr = shfunctab_table().lock()
3481 .ok()
3482 .and_then(|t| t.get(fname.as_str()).copied())
3483 .unwrap_or(0) as *mut crate::ported::zsh_h::shfunc;
3484 if !shf_ptr.is_null() { // c:3640
3485 // exists already
3486 } else {
3487 // c:3645 — `shf = zshcalloc(sizeof *shf);`
3488 // `shfunctab->addnode(shfunctab, ztrdup(funcname), shf);`
3489 if let Ok(mut t) = shfunctab_table().lock() {
3490 t.insert(fname.clone(), 0); // c:3646
3491 }
3492 }
3493 if !argv.is_empty() { // c:3648
3494 if !shf_ptr.is_null() {
3495 let shf_mut = unsafe { &mut *shf_ptr };
3496 let mut old_slot = shf_mut.filename.take();
3497 crate::ported::hashtable::dircache_set(&mut old_slot, None); // c:3649
3498 let mut new_slot: Option<String> = None;
3499 crate::ported::hashtable::dircache_set(&mut new_slot, Some(&argv[0])); // c:3650
3500 shf_mut.filename = new_slot;
3501 on |= PM_UNDEFINED >> 9 << 9; // placeholder for PM_LOADDIR bit set
3502 }
3503 }
3504 // c:3653 — `shf->node.flags = on;`
3505 // c:3654 — `ret = eval_autoload(shf, funcname, ops, func);`
3506 ret = eval_autoload(shf_ptr, &fname, ops, _func); // c:3654
3507 }
3508 crate::ported::mem::unqueue_signals(); // c:3656
3509 return ret;
3510 }
3511
3512 // c:3658-3669 — no-arg listing path: print all (non-DISABLED) shfuncs
3513 // matching `on|off` mask through scanshfunc + printnode.
3514 if argv.is_empty() { // c:3658
3515 crate::ported::mem::queue_signals(); // c:3663
3516 if OPT_ISSET(ops, b'U') && !OPT_ISSET(ops, b'u') { // c:3664
3517 on &= !PM_UNDEFINED; // c:3665
3518 }
3519 // c:3666 — `scanshfunc(1, on|off, DISABLED, shfunctab->printnode,
3520 // pflags, expand);` — full scan-and-print routes
3521 // through src/ported/funcs.rs::scanshfunc when wired.
3522 crate::ported::mem::unqueue_signals(); // c:3668
3523 return returnval;
3524 }
3525
3526 // c:3672-3708 — `-m` glob: treat each arg as a pattern, scan-and-print
3527 // matching shfuncs (no on/off → list) or apply on/off mask.
3528 if OPT_ISSET(ops, b'm') { // c:3673
3529 on &= !PM_UNDEFINED; // c:3674
3530 let mut returnval = returnval;
3531 for pat in argv { // c:3675
3532 crate::ported::mem::queue_signals(); // c:3676
3533 // c:3678 — `tokenize(*argv)` + `patcompile(...)`
3534 let pprog = crate::ported::pattern::patcompile(pat, // c:3680
3535 crate::ported::zsh_h::PAT_HEAPDUP, None);
3536 if let Some(prog) = pprog {
3537 // c:3680-3683 — scan-and-print matching shfuncs.
3538 if (on | off) == 0 && !OPT_ISSET(ops, b'X') { // c:3682
3539 // c:3682-3683 — `scanmatchshfunc(pprog, 1, 0,
3540 // DISABLED, shfunctab->printnode, pflags, expand)`.
3541 // Walk shfunctab via the hashtable.rs port and emit
3542 // each matching name (the full `printnode` callback
3543 // includes the body when PRINT_LIST/PRINT_NAMEONLY
3544 // bits are set in pflags; static-link path emits
3545 // just the name here, matching `whence` output).
3546 crate::ported::hashtable::scanmatchshfunc(
3547 Some(pat),
3548 |nm, _entry| println!("{}", nm),
3549 );
3550 } else {
3551 // c:3686-3699 — walk shfunctab, apply (on, off) and
3552 // re-eval autoload for each matching shf.
3553 let names: Vec<String> = shfunctab_table().lock()
3554 .map(|t| t.keys().cloned().collect())
3555 .unwrap_or_default();
3556 for nm in &names {
3557 // pattry approximated by string equality / glob
3558 // here; full pat engine is in src/ported/pattern.rs.
3559 if !crate::ported::pattern::pattry(&prog, nm) { // c:3690
3560 continue;
3561 }
3562 let shf_ptr = shfunctab_table().lock()
3563 .ok()
3564 .and_then(|t| t.get(nm.as_str()).copied())
3565 .unwrap_or(0) as *mut crate::ported::zsh_h::shfunc;
3566 if shf_ptr.is_null() { continue; }
3567 let shf_mut = unsafe { &mut *shf_ptr };
3568 // c:3691 — `shf->node.flags = (... | (on & ~PM_UNDEFINED)) & ~off;`
3569 shf_mut.node.flags = (shf_mut.node.flags
3570 | ((on & !PM_UNDEFINED) as i32)) & !(off as i32); // c:3691
3571 if check_autoload(shf_ptr, &shf_mut.node.nam,
3572 ops, _func) != 0 { // c:3693
3573 returnval = 1; // c:3695
3574 }
3575 }
3576 }
3577 } else {
3578 // c:3700-3702 — `untokenize + zwarnnam(name, "bad pattern")`.
3579 crate::ported::utils::zwarnnam(name,
3580 &format!("bad pattern : {}", pat)); // c:3701
3581 returnval = 1; // c:3702
3582 }
3583 crate::ported::mem::unqueue_signals(); // c:3704
3584 }
3585 return returnval;
3586 }
3587
3588 // c:3710-3735 — literal name list, no globbing.
3589 let mut returnval = returnval;
3590 crate::ported::mem::queue_signals(); // c:3711
3591 for fname in argv { // c:3712
3592 // c:3713-3714 — `-w` (compile-and-dump) path.
3593 if OPT_ISSET(ops, b'w') { // c:3713
3594 // dump_autoload(name, fname, on, ops, func) — dump.c port.
3595 continue;
3596 }
3597 // c:3715 — `shf = shfunctab->getnode(shfunctab, *argv);`
3598 let shf_ptr = shfunctab_table().lock()
3599 .ok()
3600 .and_then(|t| t.get(fname.as_str()).copied())
3601 .unwrap_or(0) as *mut crate::ported::zsh_h::shfunc;
3602 if !shf_ptr.is_null() { // c:3715
3603 let shf_mut = unsafe { &mut *shf_ptr };
3604 if (on | off) != 0 { // c:3717
3605 // c:3719 — apply on/off mask, then check_autoload.
3606 shf_mut.node.flags = (shf_mut.node.flags
3607 | ((on & !PM_UNDEFINED) as i32)) & !(off as i32); // c:3719
3608 if check_autoload(shf_ptr, &shf_mut.node.nam, ops, _func) != 0 { // c:3720
3609 returnval = 1; // c:3721
3610 }
3611 } else {
3612 // c:3723 — `printshfuncexpand(&shf->node, pflags, expand);`
3613 println!("{}", shf_mut.node.nam); // c:3723
3614 }
3615 } else if (on & PM_UNDEFINED) != 0 { // c:3725
3616 // c:3726-3782 — autoload-define path: TRAP* + abs-path + new shf.
3617 let mut sigidx: i32 = -1;
3618 let mut ok = true;
3619 // c:3728-3735 — TRAP* prefix → removetrapnode(sigidx).
3620 if fname.starts_with("TRAP") { // c:3728
3621 // c:3729 — `if ((sigidx = getsigidx(*argv + 4)) != -1)`
3622 sigidx = getsigidx(&fname[4..]); // c:3729
3623 if sigidx != -1 { // c:3729
3624 // c:3733 — `removetrapnode(sigidx);`
3625 crate::ported::jobs::removetrapnode(sigidx); // c:3733
3626 }
3627 }
3628 // c:3737-3759 — absolute path /dir/base form: install dir on
3629 // existing matching base name with PM_UNDEFINED set.
3630 if fname.starts_with('/') { // c:3737
3631 let base = fname.rsplit('/').next().unwrap_or("");
3632 if !base.is_empty() {
3633 let base_ptr = shfunctab_table().lock()
3634 .ok()
3635 .and_then(|t| t.get(base).copied())
3636 .unwrap_or(0) as *mut crate::ported::zsh_h::shfunc;
3637 if !base_ptr.is_null() {
3638 let bs = unsafe { &mut *base_ptr };
3639 // c:3742 — apply flag mask.
3640 bs.node.flags = (bs.node.flags
3641 | ((on & !PM_UNDEFINED) as i32)) & !(off as i32); // c:3742
3642 if (bs.node.flags as u32 & PM_UNDEFINED) != 0 { // c:3744
3643 let dir = if fname.len() > 1 && base.len() == fname.len() - 1 {
3644 "/".to_string() // c:3747
3645 } else {
3646 fname[..fname.len() - base.len() - 1].to_string() // c:3749-3751
3647 };
3648 let mut old_slot = bs.filename.take();
3649 crate::ported::hashtable::dircache_set(&mut old_slot, None); // c:3753
3650 let mut new_slot: Option<String> = None;
3651 crate::ported::hashtable::dircache_set(&mut new_slot, Some(&dir)); // c:3754
3652 bs.filename = new_slot;
3653 }
3654 if check_autoload(base_ptr, &bs.node.nam, ops, _func) != 0 { // c:3756
3655 returnval = 1;
3656 }
3657 continue; // c:3758
3658 }
3659 }
3660 }
3661 // c:3763-3766 — new undefined shf, mkautofn, add_autoload_function.
3662 let new_shf = Box::new(crate::ported::zsh_h::shfunc {
3663 node: crate::ported::zsh_h::hashnode {
3664 next: None,
3665 nam: fname.clone(),
3666 flags: on as i32, // c:3764
3667 },
3668 filename: None,
3669 lineno: 0,
3670 funcdef: None,
3671 redir: None,
3672 sticky: None,
3673 body: None,
3674 });
3675 let new_shf_ptr = Box::into_raw(new_shf);
3676 let _ = mkautofn(new_shf_ptr); // c:3765
3677 add_autoload_function(new_shf_ptr, fname); // c:3767
3678 if sigidx != -1 { // c:3769
3679 // c:3770 — `if (settrap(sigidx, NULL, ZSIG_FUNC)) { ... }`
3680 if crate::ported::signals::settrap(
3681 sigidx,
3682 None,
3683 crate::ported::zsh_h::ZSIG_FUNC,
3684 ) != 0 { // c:3770
3685 // c:3771 — `shfunctab->removenode(shfunctab, *argv);`
3686 if let Ok(mut t) = shfunctab_table().lock() {
3687 t.remove(fname);
3688 }
3689 // c:3772 — `shfunctab->freenode(&shf->node);` Drop covers it.
3690 returnval = 1; // c:3773
3691 ok = false; // c:3774
3692 }
3693 }
3694 if ok && check_autoload(new_shf_ptr, &fname, ops, _func) != 0 { // c:3779
3695 returnval = 1; // c:3780
3696 }
3697 } else {
3698 // c:3783 — `returnval = 1;` (named function not found,
3699 // no autoload requested).
3700 returnval = 1; // c:3783
3701 }
3702 }
3703 crate::ported::mem::unqueue_signals(); // c:3785
3704 let _ = (expand, pflags);
3705 returnval
3706}
3707
3708/// Port of `mkautofn(Shfunc shf)` from Src/builtin.c:3790.
3709/// C: `Eprog mkautofn(Shfunc shf)` — synthesize a 5-wordcode body that
3710/// re-fires the autoload mechanism when first called.
3711pub fn mkautofn(shf: *mut crate::ported::zsh_h::shfunc) -> *mut crate::ported::zsh_h::eprog { // c:3790
3712 // c:3793-3810 — alloc Eprog with 5 wordcode slots, set p->shf, p->npats=0,
3713 // p->nref=1 (permanent). Static-link path: synthesize a Box<eprog> that
3714 // satisfies the autoload trampoline contract.
3715 let p = Box::new(eprog {
3716 len: 5 * std::mem::size_of::<u32>() as i32, // c:3796
3717 prog: Vec::new(), // c:3797
3718 strs: None, // c:3798
3719 shf: if shf.is_null() { None } // c:3799
3720 else { Some(unsafe { Box::from_raw(shf) }) },
3721 npats: 0, // c:3800
3722 nref: 1, // c:3801
3723 flags: 0,
3724 pats: Vec::new(),
3725 dump: None,
3726 });
3727 Box::into_raw(p)
3728}
3729
3730/// Port of `bin_unset(char *name, char **argv, Options ops, int func)` from Src/builtin.c:3818.
3731/// C: `int bin_unset(char *name, char **argv, Options ops, int func)` —
3732/// `-f` delegates to `bin_unhash`; `-m` glob deletes matching params;
3733/// default literal-name unset with subscript handling.
3734/// WARNING: param names don't match C — Rust=(name, argv, func) vs C=(name, argv, ops, func)
3735pub fn bin_unset(name: &str, argv: &[String], // c:3818
3736 ops: &crate::ported::zsh_h::options, func: i32) -> i32 {
3737 let mut returnval = 0i32; // c:3823
3738 let mut match_count = 0i32; // c:3823
3739
3740 // PFA-SMR aspect: emit unset events for each named param. The
3741 // recorder tracks state-mutations across the shell session for
3742 // the zshrs-recorder binary's replay/inspect tooling.
3743 #[cfg(feature = "recorder")]
3744 if crate::recorder::is_enabled() {
3745 let ctx = crate::recorder::recorder_ctx_global();
3746 for a in argv {
3747 if a.starts_with('-') || a == "--" { continue; }
3748 crate::recorder::emit_unset(a, ctx.clone());
3749 }
3750 }
3751
3752 // c:3826 — `if (OPT_ISSET(ops,'f')) return bin_unhash(name, argv, ops, func);`
3753 if OPT_ISSET(ops, b'f') { // c:3826
3754 return bin_unhash(name, argv, ops, func); // c:3827
3755 }
3756
3757 // c:3830-3862 — `-m` glob.
3758 if OPT_ISSET(ops, b'm') { // c:3831
3759 for s in argv { // c:3832
3760 crate::ported::mem::queue_signals(); // c:3833
3761 let pprog = crate::ported::pattern::patcompile(s, // c:3836
3762 crate::ported::zsh_h::PAT_HEAPDUP, None);
3763 if let Some(prog) = pprog {
3764 // c:3838-3851 — walk paramtab (NOT env::vars), unset via
3765 // unsetparam (which respects PM_NAMEREF + readonly guards).
3766 //
3767 // The previous Rust port walked `std::env::vars()` — the
3768 // OS environment. This is a different name set:
3769 // - Shell-internal vars (not exported) would survive
3770 // `unset -m 'PATTERN'` even though they match.
3771 // - Env vars not in paramtab would be removed without
3772 // the PM_READONLY guard in unsetparam_pm.
3773 //
3774 // Same family of bug as the env::var vs paramtab fixes
3775 // earlier in the series.
3776 let names: Vec<String> = {
3777 let tab = crate::ported::params::paramtab().read().unwrap();
3778 tab.keys().cloned().collect()
3779 };
3780 for nm in &names {
3781 if crate::ported::pattern::pattry(&prog, nm) { // c:3842
3782 crate::ported::params::unsetparam(nm); // c:3847 (with guards)
3783 match_count += 1; // c:3848
3784 }
3785 }
3786 } else {
3787 crate::ported::utils::zwarnnam(name,
3788 &format!("bad pattern : {}", s)); // c:3854
3789 returnval = 1; // c:3855
3790 }
3791 crate::ported::mem::unqueue_signals(); // c:3857
3792 }
3793 if match_count == 0 { // c:3861
3794 returnval = 1; // c:3862
3795 }
3796 return returnval; // c:3863
3797 }
3798
3799 // c:3866-3915 — literal-name unset with optional subscript.
3800 crate::ported::mem::queue_signals(); // c:3867
3801 for s in argv { // c:3868
3802 // c:3869-3878 — extract `name[subscript]` shape.
3803 let (nm, subscript) = match s.find('[') { // c:3869
3804 Some(start) if s.ends_with(']') => { // c:3873
3805 (&s[..start], Some(&s[start + 1..s.len() - 1])) // c:3875
3806 }
3807 Some(_) => {
3808 // c:3879-3884 — bracket without `]` close → invalid.
3809 crate::ported::utils::zwarnnam(name,
3810 &format!("{}: invalid parameter name", s)); // c:3882
3811 returnval = 1; // c:3883
3812 continue; // c:3884
3813 }
3814 None => (s.as_str(), None),
3815 };
3816 // c:3878 — `if (... || !isident(s))` invalid identifier check.
3817 if nm.is_empty() || !nm.chars().next().map_or(false, |c| c.is_alphabetic() || c == '_')
3818 || !nm.chars().all(|c| c.is_alphanumeric() || c == '_')
3819 {
3820 crate::ported::utils::zwarnnam(name,
3821 &format!("{}: invalid parameter name", s)); // c:3882
3822 returnval = 1; // c:3883
3823 continue;
3824 }
3825 // c:3886-3905 — `if (!pm) continue;` then unset.
3826 // C `unsetparam_pm` dispatches on `pm->gsu` (the gsu_*
3827 // accessor for the param's type): assoc gets
3828 // `gsu_a->unset(pm, subscript)`, array gets
3829 // `gsu_arr->unset(pm, subscript)`, scalar gets `unsetparam`.
3830 match subscript { // c:3886
3831 Some(key) => {
3832 let nm_owned = nm.to_string();
3833 let key_owned = key.to_string();
3834 crate::fusevm_bridge::with_executor(|exec| {
3835 // c:3893 assoc subscript: `m[key]` delete.
3836 if let Some(mut map) = exec.assoc(&nm_owned) {
3837 map.shift_remove(&key_owned); // c:3893
3838 exec.set_assoc(nm_owned.clone(), map);
3839 } else if let Some(mut arr) = exec.array(&nm_owned) {
3840 // c:3895 array subscript: `arr[N]` set to empty.
3841 if let Ok(i) = key_owned.parse::<i32>() {
3842 let idx = if i > 0 { (i - 1) as usize }
3843 else { return; };
3844 if idx < arr.len() {
3845 arr[idx] = String::new();
3846 exec.set_array(nm_owned.clone(), arr);
3847 }
3848 }
3849 }
3850 });
3851 }
3852 None => {
3853 // c:3900-3905 — whole-param unset.
3854 let nm_owned = nm.to_string();
3855 crate::fusevm_bridge::with_executor(|exec| {
3856 exec.unset_scalar(&nm_owned);
3857 exec.unset_array(&nm_owned);
3858 exec.unset_assoc(&nm_owned);
3859 });
3860 let _ = crate::ported::params::paramtab().write().ok().as_deref_mut()
3861 .map(|t| t.remove(nm)); // c:3900 paramtab removenode
3862 std::env::remove_var(nm); // c:3905 delenv
3863 }
3864 }
3865 }
3866 crate::ported::mem::unqueue_signals(); // c:3914
3867 returnval // c:3915
3868}
3869
3870/// Port of `fetchcmdnamnode(HashNode hn, UNUSED(int printflags))` from Src/builtin.c:3967.
3871/// C: `static void fetchcmdnamnode(HashNode hn, UNUSED(int printflags))` →
3872/// `addlinknode(matchednodes, cn->node.nam);`
3873/// C body (2 lines):
3874/// `Cmdnam cn = (Cmdnam) hn;
3875/// addlinknode(matchednodes, cn->node.nam);`
3876/// (C source does not null-check hn — callers guarantee non-null.)
3877/// WARNING: param names don't match C — Rust=(hn) vs C=(hn, printflags)
3878pub fn fetchcmdnamnode(hn: *mut crate::ported::zsh_h::hashnode, // c:3967
3879 _printflags: i32) {
3880 let nam = unsafe { (*hn).nam.clone() }; // c:3969 cast + read
3881 if let Ok(mut m) = MATCHEDNODES.lock() { m.push(nam); } // c:3971
3882}
3883
3884/// Port of `bin_whence(char *nam, char **argv, Options ops, int func)` from Src/builtin.c:3975.
3885/// C: `int bin_whence(char *nam, char **argv, Options ops, int func)`.
3886///
3887/// `whence`/`type`/`which`/`where`/`command` dispatcher. `-c` csh,
3888/// `-v` verbose, `-a` all-matches, `-w` word-form, `-x` indent
3889/// override, `-m` glob-args, `-p` path-only, `-f` print funcdef,
3890/// `-s/-S` follow symlink. The C body walks alias/reswd/shfunc/
3891/// builtin/cmdnam tabs in order; this port preserves the structure
3892/// and dispatch logic, deferring the per-tab scanmatch walks to the
3893/// existing tab accessors.
3894/// WARNING: param names don't match C — Rust=(nam, argv, func) vs C=(nam, argv, ops, func)
3895pub fn bin_whence(nam: &str, argv: &[String], // c:3975
3896 ops: &crate::ported::zsh_h::options, func: i32) -> i32 {
3897 let mut returnval: i32 = 0;
3898 let mut printflags: i32 = 0;
3899 let mut informed: i32 = 0;
3900 let mut expand: i32 = 0;
3901
3902 // c:3989-3993 — flags.
3903 let csh = OPT_ISSET(ops, b'c'); // c:3989
3904 let v = OPT_ISSET(ops, b'v'); // c:3990
3905 let all = OPT_ISSET(ops, b'a'); // c:3991
3906 let wd = OPT_ISSET(ops, b'w'); // c:3992
3907
3908 // c:3995-4002 — `-x N` indent override.
3909 if OPT_ISSET(ops, b'x') { // c:3995
3910 let arg = OPT_ARG(ops, b'x').unwrap_or("");
3911 match arg.trim().parse::<i32>() { // c:3997
3912 Ok(n) => {
3913 expand = n;
3914 if expand == 0 { expand = -1; } // c:4001
3915 }
3916 Err(_) => {
3917 crate::ported::utils::zwarnnam(nam, "number expected after -x"); // c:3998
3918 return 1;
3919 }
3920 }
3921 }
3922
3923 // c:4004-4012 — printflags from -w/-c/-v/(default simple)/-f.
3924 if OPT_ISSET(ops, b'w') { printflags |= PRINT_WHENCE_WORD; } // c:4004
3925 else if OPT_ISSET(ops, b'c') { printflags |= PRINT_WHENCE_CSH; } // c:4006
3926 else if OPT_ISSET(ops, b'v') { printflags |= PRINT_WHENCE_VERBOSE; } // c:4008
3927 else { printflags |= PRINT_WHENCE_SIMPLE; } // c:4010
3928 if OPT_ISSET(ops, b'f') { printflags |= PRINT_WHENCE_FUNCDEF; } // c:4012
3929
3930 // c:4015-4024 — BIN_COMMAND -V or -V-equivalent flag wrangling.
3931 // C body:
3932 // if (func == BIN_COMMAND)
3933 // if (OPT_ISSET(ops,'V')) { printflags = aliasflags = PRINT_WHENCE_VERBOSE; v = 1; }
3934 // else { aliasflags = PRINT_LIST; printflags = PRINT_WHENCE_SIMPLE; v = 0; }
3935 // else aliasflags = printflags;
3936 // Previous Rust port omitted the `v = 0` reset in the non-V
3937 // command branch, so `command foo` with a stray user -v leaked
3938 // verbose mode. Mirror C: force v unconditionally under
3939 // BIN_COMMAND.
3940 let mut v = v;
3941 let _aliasflags = if func == BIN_COMMAND { // c:4015
3942 if OPT_ISSET(ops, b'V') { // c:4016
3943 printflags = PRINT_WHENCE_VERBOSE; // c:4017
3944 v = true; // c:4018
3945 PRINT_WHENCE_VERBOSE
3946 } else {
3947 printflags = PRINT_WHENCE_SIMPLE; // c:4021
3948 v = false; // c:4022
3949 PRINT_LIST // c:4020
3950 }
3951 } else {
3952 printflags // c:4024
3953 };
3954
3955 // c:4026-4119 — `-m` glob branch: each arg is a pattern; walk every
3956 // hashtab in turn (alias/reswd/shfunc/builtin/cmdnam) and emit a
3957 // print row per matching node. C uses scanmatchtable + a per-tab
3958 // print callback; the Rust port iterates each tab's accessor and
3959 // emits the print directly.
3960 if OPT_ISSET(ops, b'm') { // c:4026
3961 // c:4028-4030 — `cmdnamtab->filltable(cmdnamtab);` + matchednodes
3962 // setup when -a is set. Static-link path: PATH walk on demand
3963 // through findcmd; matchednodes accumulator is
3964 // crate::ported::builtin::MATCHEDNODES.
3965 if all { // c:4029
3966 if let Ok(mut m) = crate::ported::builtin::MATCHEDNODES.lock() {
3967 m.clear();
3968 }
3969 }
3970 crate::ported::mem::queue_signals(); // c:4032
3971 for pat in argv { // c:4031
3972 // c:4034 — `tokenize(*argv);` (preserves Rust-side noop).
3973 let pprog = crate::ported::pattern::patcompile(pat, // c:4035
3974 crate::ported::zsh_h::PAT_HEAPDUP, None);
3975 match pprog {
3976 None => { // c:4036
3977 crate::ported::utils::zwarnnam(nam,
3978 &format!("bad pattern : {}", pat)); // c:4036
3979 returnval = 1; // c:4037
3980 continue;
3981 }
3982 Some(prog) => {
3983 if !OPT_ISSET(ops, b'p') { // c:4042
3984 // c:4044-4047 — aliases scan.
3985 if let Ok(t) = crate::ported::hashtable::aliastab_lock().read() {
3986 for (n, _a) in t.iter() {
3987 if crate::ported::pattern::pattry(&prog, n) {
3988 println!("{}", n);
3989 informed += 1; // c:4045
3990 }
3991 }
3992 }
3993 // c:4050-4053 — reserved words scan.
3994 let reswords = ["do","done","esac","then","elif","else","fi",
3995 "for","case","if","while","function","repeat",
3996 "time","until","exec","command","select","coproc",
3997 "nocorrect","foreach","end","!","[[","{","}",
3998 "declare","export","float","integer","local",
3999 "private","readonly","typeset"];
4000 for w in &reswords { // c:4051
4001 if crate::ported::pattern::pattry(&prog, w) {
4002 println!("{}", w);
4003 informed += 1; // c:4052
4004 }
4005 }
4006 // c:4056-4060 — shell functions scan
4007 // (scanmatchshfunc → shfunctab walk + printnode).
4008 let names: Vec<String> = crate::ported::builtin::shfunctab_table()
4009 .lock().map(|t| t.keys().cloned().collect())
4010 .unwrap_or_default();
4011 for n in &names {
4012 if crate::ported::pattern::pattry(&prog, n) {
4013 println!("{}", n);
4014 informed += 1; // c:4058
4015 }
4016 }
4017 // c:4063-4066 — builtins scan.
4018 for b in BUILTINS.iter() {
4019 if crate::ported::pattern::pattry(&prog, &b.node.nam) {
4020 println!("{}", b.node.nam);
4021 informed += 1; // c:4064
4022 }
4023 }
4024 }
4025 // c:4070-4072 — cmdnamtab scan ($PATH-cached external commands).
4026 // Static-link path: walk $PATH dirs (from paramtab —
4027 // shell-side $PATH, not OS env) and match basenames.
4028 if let Some(path) = crate::ported::params::getsparam("PATH") {
4029 for dir in path.split(':') {
4030 if dir.is_empty() { continue; }
4031 if let Ok(rd) = std::fs::read_dir(dir) {
4032 for entry in rd.flatten() {
4033 if let Some(name) = entry.file_name().to_str() {
4034 if crate::ported::pattern::pattry(&prog, name) {
4035 if all {
4036 if let Ok(mut m) =
4037 crate::ported::builtin::MATCHEDNODES.lock() {
4038 m.push(name.to_string());
4039 }
4040 } else {
4041 println!("{}", name);
4042 }
4043 informed += 1; // c:4072
4044 }
4045 }
4046 }
4047 }
4048 }
4049 }
4050 }
4051 }
4052 crate::ported::signals_h::run_queued_signals(); // c:4076
4053 }
4054 crate::ported::mem::unqueue_signals(); // c:4078
4055 if !all { // c:4081
4056 return if returnval != 0 || informed == 0 { 1 } else { 0 }; // c:4082
4057 }
4058 }
4059
4060 // c:4121-4205 — literal-name dispatch per arg.
4061 crate::ported::mem::queue_signals();
4062 // C source uses MATCHEDNODES only when `-m` (glob-args) is set;
4063 // plain `-a` keeps the literal argv. Without this gate, `whence
4064 // -a true` consulted an empty MATCHEDNODES and skipped every
4065 // print.
4066 let argv_vec: Vec<String> = if OPT_ISSET(ops, b'm') {
4067 crate::ported::builtin::MATCHEDNODES.lock()
4068 .map(|m| m.clone()).unwrap_or_default()
4069 } else { argv.to_vec() };
4070 for arg in &argv_vec { // c:4121
4071 // c:4123 — `informed = 0;` reset per iteration so the per-arg
4072 // not-found path can fire correctly.
4073 informed = 0; // c:4123
4074 let mut buf: Option<String> = None;
4075 // c:4124-4130 — `-p` path-only path.
4076 if !OPT_ISSET(ops, b'p') {
4077 // c:4128-4134 — alias check.
4078 if let Ok(t) = crate::ported::hashtable::aliastab_lock().read() {
4079 if let Some(a) = t.get(arg) { // c:4128
4080 if (printflags & PRINT_WHENCE_WORD as i32) != 0 { // c:4129
4081 println!("{}: alias", a.node.nam);
4082 } else if (printflags & PRINT_WHENCE_CSH as i32) != 0 {
4083 println!("{}: aliased to {}", a.node.nam, a.text);
4084 } else if (printflags & PRINT_WHENCE_VERBOSE as i32) != 0 {
4085 println!("{} is an alias for {}", a.node.nam, a.text);
4086 } else if (printflags & PRINT_LIST as i32) != 0 {
4087 println!("alias {}={}", a.node.nam, a.text);
4088 } else {
4089 println!("{}={}", a.node.nam, a.text);
4090 }
4091 informed = 1; // c:4131
4092 if !all { continue; } // c:4132
4093 }
4094 }
4095 // c:4136-4143 — suffix-alias check (arg has a `.SUFFIX`).
4096 if let Some(idx) = arg.rfind('.') { // c:4137
4097 if idx > 0 && idx + 1 < arg.len() {
4098 let suf = &arg[idx + 1..];
4099 if let Ok(t) = crate::ported::hashtable::sufaliastab_lock().read() {
4100 if let Some(a) = t.get(suf) { // c:4140
4101 println!("{}={}", a.node.nam, a.text); // c:4141
4102 informed = 1; // c:4142
4103 if !all { continue; } // c:4143
4104 }
4105 }
4106 }
4107 }
4108 // c:4146-4151 — reserved-word check.
4109 let reswords = ["do","done","esac","then","elif","else","fi",
4110 "for","case","if","while","function","repeat",
4111 "time","until","exec","command","select","coproc",
4112 "nocorrect","foreach","end","!","[[","{","}",
4113 "declare","export","float","integer","local",
4114 "private","readonly","typeset"];
4115 if reswords.contains(&arg.as_str()) { // c:4146
4116 if (printflags & PRINT_WHENCE_WORD as i32) != 0 {
4117 println!("{}: reserved", arg);
4118 } else if (printflags & PRINT_WHENCE_CSH as i32) != 0 {
4119 println!("{}: shell reserved word", arg);
4120 } else if (printflags & PRINT_WHENCE_VERBOSE as i32) != 0 {
4121 println!("{} is a reserved word", arg);
4122 } else {
4123 println!("{}", arg); // c:4148
4124 }
4125 informed = 1; // c:4149
4126 if !all { continue; } // c:4150
4127 }
4128 // c:4153-4158 — shell function check.
4129 if let Ok(t) = crate::ported::builtin::shfunctab_table().lock() {
4130 if t.contains_key(arg) { // c:4153
4131 if (printflags & PRINT_WHENCE_FUNCDEF as i32) != 0 {
4132 let body = crate::ported::utils::getshfunc(arg)
4133 .unwrap_or_else(|| String::from("# body undefined"));
4134 println!("{} () {{\n{}\n}}", arg, body);
4135 } else if (printflags & PRINT_WHENCE_WORD as i32) != 0 {
4136 println!("{}: function", arg);
4137 } else if (printflags & PRINT_WHENCE_CSH as i32) != 0 {
4138 println!("{}: shell function", arg);
4139 } else if (printflags & PRINT_WHENCE_VERBOSE as i32) != 0 {
4140 println!("{} is a shell function", arg);
4141 } else {
4142 println!("{}", arg); // c:4155
4143 }
4144 informed = 1; // c:4156
4145 if !all { continue; } // c:4157
4146 }
4147 }
4148 // c:4160-4165 — builtin command check.
4149 // Output shape per `Src/builtin.c:177-194 printbuiltinnode`:
4150 // -w → "name: builtin"
4151 // -c → "name: shell built-in command"
4152 // -v → "name is a shell builtin"
4153 // default → "name"
4154 if BUILTINS.iter().any(|b| b.node.nam == *arg) { // c:4160
4155 if wd {
4156 println!("{}: builtin", arg); // c:179
4157 } else if csh {
4158 println!("{}: shell built-in command", arg); // c:184
4159 } else if v {
4160 println!("{} is a shell builtin", arg); // c:189
4161 } else {
4162 println!("{}", arg); // c:194
4163 }
4164 informed = 1; // c:4163
4165 if !all { continue; } // c:4164
4166 }
4167 // c:4167-4173 — cmdnamtab HASHED check (commands installed
4168 // via `hash NAME=PATH`). Read the canonical cmdnamtab
4169 // directly. Was a fake env-var bridge under invented
4170 // `__zshrs_hash_NAME` keys; cmdnamtab is bucket-2-
4171 // consolidated now.
4172 let hashed_path: Option<String> = {
4173 match crate::ported::hashtable::cmdnamtab_lock().read() {
4174 Ok(tab) => tab.get(arg).and_then(|cn| {
4175 if (cn.node.flags & crate::ported::zsh_h::HASHED as i32) != 0 {
4176 cn.cmd.clone() // c:4168 cn->u.cmd
4177 } else {
4178 None
4179 }
4180 }),
4181 Err(_) => None,
4182 }
4183 };
4184 if let Some(p) = hashed_path {
4185 if (printflags & PRINT_LIST) != 0 {
4186 println!("hash {}={}", arg, p);
4187 } else {
4188 println!("{}", p);
4189 }
4190 informed = 1; // c:4170
4191 if !all { continue; } // c:4171
4192 }
4193 }
4194 // c:4178-4198 — `-a` all-paths search through $PATH.
4195 if all && !arg.starts_with('/') { // c:4178
4196 if let Some(path) = crate::ported::params::getsparam("PATH") {
4197 for dir in path.split(':') {
4198 if dir.is_empty() { continue; }
4199 let full = format!("{}/{}", dir, arg);
4200 let p = std::path::Path::new(&full);
4201 if p.is_file() { // c:4185
4202 if wd {
4203 println!("{}: command", arg);
4204 } else if v && !csh {
4205 print!("{} is ", arg);
4206 println!("{}", crate::ported::utils::quotedzputs(&full));
4207 } else {
4208 println!("{}", full);
4209 }
4210 informed = 1; // c:4192
4211 }
4212 }
4213 }
4214 if !informed != 0 && (wd || v || csh) { // c:4196
4215 println!("{}{}", arg, if wd { ": none" } else { " not found" });
4216 returnval = 1;
4217 }
4218 continue;
4219 }
4220 // c:4200-4203 — `-p` BIN_COMMAND special case: builtin first.
4221 if func == BIN_COMMAND && OPT_ISSET(ops, b'p') { // c:4200
4222 if BUILTINS.iter().any(|b| b.node.nam == *arg) { // c:4201
4223 println!("{}: builtin", arg); // c:4202
4224 informed = 1;
4225 continue;
4226 }
4227 }
4228 // c:4205-4218 — final $PATH fallback via findcmd.
4229 buf = findcmd(arg, 1, (func == BIN_COMMAND && OPT_ISSET(ops, b'p')) as i32);
4230 if let Some(path) = buf { // c:4150 iscom
4231 if wd { // c:4151
4232 println!("{}: command", arg); // c:4152
4233 } else if v && !csh { // c:4154
4234 print!("{} is ", arg); // c:4156
4235 println!("{}", crate::ported::utils::quotedzputs(&path)); // c:4157
4236 } else {
4237 println!("{}", path); // c:4159
4238 }
4239 informed = 1; // c:4163
4240 continue;
4241 }
4242 // c:4166-4185 — fallback: findcmd through $PATH.
4243 if let Some(cnam) = findcmd(arg, 1, 0) { // c:4181
4244 if wd { // c:4184
4245 println!("{}: command", arg); // c:4185
4246 } else if v && !csh { // c:4187
4247 print!("{} is ", arg); // c:4188
4248 println!("{}", crate::ported::utils::quotedzputs(&cnam)); // c:4189
4249 } else {
4250 println!("{}", cnam); // c:4191
4251 }
4252 informed = 1; // c:4198
4253 continue;
4254 }
4255 // c:4201-4205 — not found at all.
4256 if v || csh || wd { // c:4202
4257 println!("{}{}", arg, if wd { ": none" } else { " not found" }); // c:4203
4258 }
4259 returnval = 1; // c:4204
4260 }
4261 crate::ported::mem::unqueue_signals();
4262 returnval | (informed == 0) as i32 // c:4209
4263}
4264
4265/// Port of `bin_hash(char *name, char **argv, Options ops, UNUSED(int func))` from Src/builtin.c:4234.
4266/// C: `int bin_hash(char *name, char **argv, Options ops, ...)` —
4267/// manage `cmdnamtab` (default) or `nameddirtab` (`-d`); `-r` empties,
4268/// `-f` fills, `-L` sets PRINT_LIST, `-m` is a glob.
4269/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
4270pub fn bin_hash(name: &str, argv: &[String], // c:4234
4271 ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
4272 let mut returnval = 0i32; // c:4239
4273 let mut printflags = 0i32; // c:4240
4274 let dir_mode = OPT_ISSET(ops, b'd'); // c:4242
4275
4276 // PFA-SMR aspect: only `hash -d NAME=PATH` mutates the named-dir
4277 // table; the default `hash CMD=PATH` form populates a runtime
4278 // command cache that the recorder doesn't re-apply.
4279 #[cfg(feature = "recorder")]
4280 if crate::recorder::is_enabled() && dir_mode {
4281 let ctx = crate::recorder::recorder_ctx_global();
4282 for a in argv {
4283 if a.starts_with('-') { continue; }
4284 if let Some((k, v)) = a.split_once('=') {
4285 crate::recorder::emit_hash_d(k, v, ctx.clone());
4286 }
4287 }
4288 }
4289
4290 // c:4247-4263 — `-r` empty / `-f` fill (no other args).
4291 if OPT_ISSET(ops, b'r') || OPT_ISSET(ops, b'f') { // c:4247
4292 if !argv.is_empty() { // c:4249
4293 crate::ported::utils::zwarnnam("hash", "too many arguments"); // c:4250
4294 return 1; // c:4251
4295 }
4296 if OPT_ISSET(ops, b'r') { // c:4255
4297 // c:4256 — `emptyhashtable(cmdnamtab)` /
4298 // `emptynameddirtable()`.
4299 if dir_mode {
4300 crate::ported::hashnameddir::emptynameddirtable();
4301 } else {
4302 crate::ported::hashtable::emptycmdnamtable();
4303 }
4304 }
4305 if OPT_ISSET(ops, b'f') { // c:4259
4306 // c:4260 — `fillcmdnamtable(cmdnamtab)` /
4307 // `fillnameddirtable()`. cmdnamtab fill = walk every
4308 // PATH entry and hashdir() it.
4309 if dir_mode {
4310 crate::ported::hashnameddir::fillnameddirtable();
4311 } else {
4312 // Read $path (the lowercase array form) from env.
4313 // c:4260 — fill cmdnamtab from $path. Read shell-side
4314 // $PATH so changes via `path=(...)` flow in.
4315 let path_str = crate::ported::params::getsparam("PATH").unwrap_or_default();
4316 let path_arr: Vec<String> =
4317 path_str.split(':').map(|s| s.to_string()).collect();
4318 crate::ported::hashtable::fillcmdnamtable(&path_arr);
4319 }
4320 }
4321 return 0; // c:4262
4322 }
4323
4324 // c:4265 — `-L` enables PRINT_LIST.
4325 if OPT_ISSET(ops, b'L') { printflags |= PRINT_LIST; } // c:4265
4326
4327 // c:4268-4273 — no args: list table.
4328 if argv.is_empty() { // c:4268
4329 crate::ported::mem::queue_signals(); // c:4269
4330 // c:4270 — `scanhashtable(ht, 1, 0, 0, ht->printnode, printflags)`.
4331 // Walk the selected table (cmdnamtab default, nameddirtab when
4332 // `-d`). Previous Rust port only walked nameddirtab — `hash`
4333 // with no args (the typical user-visible form) silently printed
4334 // nothing on cmdnamtab.
4335 if dir_mode {
4336 if let Ok(t) = crate::ported::hashnameddir::nameddirtab().lock() {
4337 for (_n, nd) in t.iter() { // c:4270
4338 crate::ported::hashnameddir::printnameddirnode(nd, printflags);
4339 }
4340 }
4341 } else {
4342 // c:4270 — cmdnamtab walk (the default `ht`). PATH lookup
4343 // arr is empty in the printnode call site because per-node
4344 // hashed entries carry their own resolved path.
4345 let path_arr: Vec<String> = Vec::new();
4346 if let Ok(t) = crate::ported::hashtable::cmdnamtab_lock().read() {
4347 for (_n, cn) in t.iter() { // c:4270
4348 print!("{}",
4349 crate::ported::hashtable::printcmdnamnode(
4350 cn, &path_arr, printflags as u32));
4351 }
4352 }
4353 }
4354 crate::ported::mem::unqueue_signals(); // c:4271
4355 return 0; // c:4272
4356 }
4357
4358 // c:4276-4329 — name-list dispatch, both literal and -m glob.
4359 crate::ported::mem::queue_signals(); // c:4276
4360 let mut idx = 0;
4361 while idx < argv.len() { // c:4277
4362 let arg = &argv[idx];
4363 idx += 1;
4364 if OPT_ISSET(ops, b'm') { // c:4279
4365 // c:4280-4290 — glob-match path.
4366 let pprog = crate::ported::pattern::patcompile(arg, // c:4282
4367 crate::ported::zsh_h::PAT_HEAPDUP, None);
4368 if let Some(prog) = pprog {
4369 if dir_mode {
4370 if let Ok(t) = crate::ported::hashnameddir::nameddirtab().lock() {
4371 for (n, nd) in t.iter() {
4372 if crate::ported::pattern::pattry(&prog, n) { // c:4286
4373 crate::ported::hashnameddir::printnameddirnode(nd, printflags);
4374 }
4375 }
4376 }
4377 }
4378 } else {
4379 crate::ported::utils::zwarnnam(name,
4380 &format!("bad pattern : {}", arg)); // c:4292
4381 returnval = 1; // c:4293
4382 }
4383 continue;
4384 }
4385 // c:4297-4317 — literal name=value or name-only.
4386 let (n, val) = match arg.find('=') {
4387 Some(eq) => (&arg[..eq], Some(&arg[eq + 1..])),
4388 None => (arg.as_str(), None),
4389 };
4390 if let Some(v) = val { // c:4302
4391 // Define entry.
4392 if dir_mode { // c:4302
4393 // c:4303-4310 — `itype_end(asg->name, IUSER, 0)` validates;
4394 // dir name must be all-IUSER chars.
4395 if !n.chars().all(|c| c.is_alphanumeric() || c == '_') { // c:4305
4396 crate::ported::utils::zwarnnam(name,
4397 &format!("invalid character in directory name: {}", n)); // c:4306
4398 returnval = 1; // c:4308
4399 continue; // c:4309
4400 }
4401 let nd = nameddir {
4402 node: hashnode { next: None, nam: n.to_string(), flags: 0 },
4403 dir: v.to_string(),
4404 diff: 0,
4405 };
4406 crate::ported::hashnameddir::addnameddirnode(n, nd); // c:4314
4407 } else {
4408 // c:4316 — `cn->u.cmd = ztrdup(value);` in cmdnamtab.
4409 // Static-link path: store in PATH-style env.
4410 std::env::set_var(format!("__zshrs_hash_{}", n), v);
4411 }
4412 if OPT_ISSET(ops, b'v') { // c:4321
4413 if dir_mode {
4414 if let Ok(t) = crate::ported::hashnameddir::nameddirtab().lock() {
4415 if let Some(nd) = t.get(n) { // c:4322
4416 crate::ported::hashnameddir::printnameddirnode(nd, 0);
4417 }
4418 }
4419 }
4420 }
4421 } else {
4422 // c:4323-4334 — display existing entry / look up.
4423 if dir_mode {
4424 let snapshot = crate::ported::hashnameddir::nameddirtab()
4425 .lock().ok().and_then(|t| t.get(n).cloned());
4426 match snapshot {
4427 Some(nd) => {
4428 if OPT_ISSET(ops, b'v') { // c:4337
4429 crate::ported::hashnameddir::printnameddirnode(&nd, 0);
4430 }
4431 }
4432 None => {
4433 crate::ported::utils::zwarnnam(name,
4434 &format!("no such directory name: {}", n)); // c:4327
4435 returnval = 1; // c:4328
4436 }
4437 }
4438 } else {
4439 // c:4332-4334 — `if (!hashcmd(name, path)) zwarnnam(
4440 // "no such command")`. Walk shell-side
4441 // $PATH (paramtab).
4442 let found = crate::ported::params::getsparam("PATH").is_some_and(|p| {
4443 p.split(':').any(|d|
4444 !d.is_empty() && std::path::Path::new(&format!("{}/{}", d, n)).exists()
4445 )
4446 });
4447 if !found {
4448 crate::ported::utils::zwarnnam(name,
4449 &format!("no such command: {}", n)); // c:4333
4450 returnval = 1; // c:4334
4451 }
4452 }
4453 }
4454 }
4455 crate::ported::mem::unqueue_signals(); // c:4346
4456 returnval // c:4346
4457}
4458
4459/// Port of `bin_unhash(char *name, char **argv, Options ops, int func)` from Src/builtin.c:4346.
4460/// C: `int bin_unhash(char *name, char **argv, Options ops, int func)` —
4461/// remove entries from cmdnamtab/aliastab/sufaliastab/nameddirtab/
4462/// shfunctab. `-a` clears all, `-m` is a glob.
4463/// WARNING: param names don't match C — Rust=(name, argv, func) vs C=(name, argv, ops, func)
4464pub fn bin_unhash(name: &str, argv: &[String], // c:4346
4465 ops: &crate::ported::zsh_h::options, func: i32) -> i32 {
4466 let mut returnval = 0i32; // c:4351
4467 let mut all = 0i32; // c:4351
4468 let mut match_count = 0i32; // c:4351
4469
4470 // PFA-SMR aspect: when invoked as `unalias`, record the un-alias
4471 // events so the replay can suppress earlier `alias` calls.
4472 #[cfg(feature = "recorder")]
4473 if crate::recorder::is_enabled() && func == crate::ported::builtin::BIN_UNALIAS {
4474 let ctx = crate::recorder::recorder_ctx_global();
4475 for a in argv {
4476 if a.starts_with('-') && a != "-" { continue; }
4477 crate::recorder::emit_unalias(a, ctx.clone());
4478 }
4479 }
4480
4481 // c:4355-4373 — table-pick dispatch.
4482 enum Tab { CmdNam, NamedDir, Shfunc, Alias, SufAlias }
4483 let tab: Tab;
4484 if func == BIN_UNALIAS { // c:4356
4485 tab = if OPT_ISSET(ops, b's') { Tab::SufAlias } else { Tab::Alias }; // c:4357
4486 if OPT_ISSET(ops, b'a') { // c:4361
4487 if !argv.is_empty() { // c:4362
4488 crate::ported::utils::zwarnnam(name, "-a: too many arguments"); // c:4363
4489 return 1; // c:4364
4490 }
4491 all = 1; // c:4366
4492 } else if argv.is_empty() { // c:4367
4493 crate::ported::utils::zwarnnam(name, "not enough arguments"); // c:4368
4494 return 1; // c:4369
4495 }
4496 } else if OPT_ISSET(ops, b'd') { tab = Tab::NamedDir; // c:4370
4497 } else if OPT_ISSET(ops, b'f') { tab = Tab::Shfunc; // c:4372
4498 } else if OPT_ISSET(ops, b's') { tab = Tab::SufAlias; // c:4374
4499 } else if func == BIN_UNHASH && OPT_ISSET(ops, b'a') { tab = Tab::Alias; // c:4376
4500 } else { tab = Tab::CmdNam; } // c:4378
4501
4502 // Helper: clear entire table.
4503 let clear_all = |t: &Tab| match t {
4504 Tab::Alias => { let _ = crate::ported::hashtable::aliastab_lock().write().map(|mut g| g.clear()); }
4505 Tab::SufAlias => { let _ = crate::ported::hashtable::sufaliastab_lock().write().map(|mut g| g.clear()); }
4506 Tab::NamedDir => { crate::ported::hashnameddir::emptynameddirtable(); }
4507 Tab::Shfunc => { let _ = shfunctab_table().lock().map(|mut g| g.clear()); }
4508 Tab::CmdNam => { crate::ported::hashtable::emptycmdnamtable(); } // c:4389
4509 };
4510 let remove_one = |t: &Tab, nm: &str| -> bool {
4511 match t {
4512 Tab::Alias => crate::ported::hashtable::aliastab_lock().write()
4513 .map(|mut g| g.remove(nm).is_some()).unwrap_or(false),
4514 Tab::SufAlias => crate::ported::hashtable::sufaliastab_lock().write()
4515 .map(|mut g| g.remove(nm).is_some()).unwrap_or(false),
4516 Tab::NamedDir => crate::ported::hashnameddir::removenameddirnode(nm).is_some(),
4517 Tab::Shfunc => shfunctab_table().lock()
4518 .map(|mut g| g.remove(nm).is_some()).unwrap_or(false),
4519 // c:4405 — `if ((hn = ht->removenode(ht, *argv)))`.
4520 // Removal returns truthy only when the entry actually
4521 // existed. Previous Rust port hardcoded `true` after a
4522 // void-return `freecmdnamnode` call, so `unhash badname`
4523 // silently succeeded instead of emitting the canonical
4524 // "no such hash table element" error.
4525 Tab::CmdNam => crate::ported::hashtable::cmdnamtab_lock().write()
4526 .map(|mut g| g.remove(nm).is_some()).unwrap_or(false),
4527 }
4528 };
4529
4530 if all != 0 { // c:4382
4531 crate::ported::mem::queue_signals(); // c:4383
4532 clear_all(&tab); // c:4384-4389
4533 crate::ported::mem::unqueue_signals(); // c:4390
4534 return 0; // c:4391
4535 }
4536
4537 // c:4395-4421 — `-m` glob branch.
4538 if OPT_ISSET(ops, b'm') { // c:4395
4539 for arg in argv { // c:4396
4540 crate::ported::mem::queue_signals(); // c:4397
4541 let pprog = crate::ported::pattern::patcompile(arg, // c:4400
4542 crate::ported::zsh_h::PAT_HEAPDUP, None);
4543 if let Some(prog) = pprog {
4544 // Collect names then remove (avoid iterator/mutation conflict).
4545 // c:4408 — `scanmatchtable(ht, pprog, ...)` walks every
4546 // entry in the selected table. Previous Rust port left
4547 // Tab::CmdNam returning an empty Vec, so `unhash -m PAT`
4548 // (default cmd-hash table) silently matched zero entries.
4549 let names: Vec<String> = match &tab {
4550 Tab::Alias => crate::ported::hashtable::aliastab_lock().read()
4551 .map(|t| t.iter().map(|(n,_)| n.clone()).collect()).unwrap_or_default(),
4552 Tab::SufAlias => crate::ported::hashtable::sufaliastab_lock().read()
4553 .map(|t| t.iter().map(|(n,_)| n.clone()).collect()).unwrap_or_default(),
4554 Tab::NamedDir => crate::ported::hashnameddir::nameddirtab().lock()
4555 .map(|t| t.keys().cloned().collect()).unwrap_or_default(),
4556 Tab::Shfunc => shfunctab_table().lock()
4557 .map(|t| t.keys().cloned().collect()).unwrap_or_default(),
4558 // c:4408 — cmdnamtab walk via `cmdnamtab_lock().iter()`.
4559 Tab::CmdNam => crate::ported::hashtable::cmdnamtab_lock().read()
4560 .map(|t| t.iter().map(|(n,_)| n.clone()).collect())
4561 .unwrap_or_default(),
4562 };
4563 for nm in &names {
4564 if crate::ported::pattern::pattry(&prog, nm) { // c:4408
4565 if remove_one(&tab, nm) {
4566 match_count += 1; // c:4410
4567 }
4568 }
4569 }
4570 } else {
4571 crate::ported::utils::zwarnnam(name,
4572 &format!("bad pattern : {}", arg)); // c:4416
4573 returnval = 1; // c:4417
4574 }
4575 crate::ported::mem::unqueue_signals(); // c:4419
4576 }
4577 if match_count == 0 { // c:4424
4578 returnval = 1; // c:4425
4579 }
4580 return returnval; // c:4426
4581 }
4582
4583 // c:4429-4439 — literal-name removals.
4584 crate::ported::mem::queue_signals(); // c:4430
4585 for arg in argv { // c:4431
4586 if remove_one(&tab, arg) { // c:4432
4587 // freed
4588 } else if func == BIN_UNSET
4589 && crate::ported::zsh_h::isset(crate::ported::options::optlookup("posixbuiltins"))
4590 {
4591 // c:4434 — POSIX: unset of nonexistent isn't an error.
4592 returnval = 0; // c:4435
4593 } else {
4594 crate::ported::utils::zwarnnam(name,
4595 &format!("no such hash table element: {}", arg)); // c:4437
4596 returnval = 1; // c:4450
4597 }
4598 }
4599 crate::ported::mem::unqueue_signals(); // c:4450
4600 returnval // c:4450
4601}
4602
4603/// Port of `bin_alias(char *name, char **argv, Options ops, UNUSED(int func))` from Src/builtin.c:4450.
4604/// C: `int bin_alias(char *name, char **argv, Options ops, ...)` — list,
4605/// define, glob-list, or display aliases. `-r`/`-g`/`-s` filter type;
4606/// `-L` prints definitions; `-m` treats args as patterns.
4607/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
4608pub fn bin_alias(name: &str, argv: &[String], // c:4450
4609 ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
4610 let mut returnval = 0i32; // c:4455
4611 let mut flags1 = 0u32; // c:4456
4612 let mut flags2 = DISABLED as u32; // c:4456
4613 let mut printflags = 0i32; // c:4457
4614 let mut use_suffix = false; // tracks ht switch
4615
4616 // c:4461-4485 — type-flag parsing.
4617 let type_opts = (OPT_ISSET(ops, b'r') as i32) // c:4461
4618 + (OPT_ISSET(ops, b'g') as i32)
4619 + (OPT_ISSET(ops, b's') as i32);
4620 if type_opts != 0 { // c:4464
4621 if type_opts > 1 { // c:4465
4622 crate::ported::utils::zwarnnam(name, "illegal combination of options"); // c:4466
4623 return 1; // c:4467
4624 }
4625 if OPT_ISSET(ops, b'g') { // c:4469
4626 flags1 |= ALIAS_GLOBAL as u32; // c:4470
4627 } else {
4628 flags2 |= ALIAS_GLOBAL as u32; // c:4472
4629 }
4630 if OPT_ISSET(ops, b's') { // c:4473
4631 flags1 |= ALIAS_SUFFIX as u32; // c:4480
4632 use_suffix = true; // c:4481
4633 } else {
4634 flags2 |= ALIAS_SUFFIX as u32; // c:4483
4635 }
4636 }
4637
4638 // c:4486-4490 — printflags from -L / + suffix.
4639 if OPT_ISSET(ops, b'L') { // c:4486
4640 printflags |= PRINT_LIST; // c:4487
4641 } else if OPT_PLUS(ops, b'g') || OPT_PLUS(ops, b'r') || OPT_PLUS(ops, b's')
4642 || OPT_PLUS(ops, b'm') || OPT_ISSET(ops, b'+') // c:4488
4643 {
4644 printflags |= PRINT_NAMEONLY; // c:4490
4645 }
4646
4647 // Helper closure that prints one Alias respecting printflags.
4648 // Mirrors `printaliasnode(HashNode, int printflags)` from
4649 // Src/hashtable.c:1256-1336 (the simple-print subset that
4650 // bin_alias actually emits — PRINT_LIST, PRINT_NAMEONLY, and
4651 // the default `name=value`).
4652 let print_alias = |a: &Alias, pflags: i32| {
4653 // c:1262-1265 — PRINT_NAMEONLY emits the name and a newline.
4654 if (pflags & PRINT_NAMEONLY) != 0 {
4655 println!("{}", a.node.nam);
4656 return;
4657 }
4658 // c:1311-1330 — PRINT_LIST prefix: `alias ` then `-s `/`-g `
4659 // for ALIAS_SUFFIX/ALIAS_GLOBAL, then `-- ` for names that
4660 // start with `-` or `+`. The previous Rust port emitted bare
4661 // `alias name=value` without any of these flags, so `alias
4662 // -L` for a global alias was indistinguishable from a
4663 // regular one and the output wasn't re-executable.
4664 if (pflags & PRINT_LIST) != 0 {
4665 print!("alias ");
4666 if (a.node.flags & ALIAS_SUFFIX as i32) != 0 { // c:1322
4667 print!("-s ");
4668 } else if (a.node.flags & ALIAS_GLOBAL as i32) != 0 { // c:1324
4669 print!("-g ");
4670 }
4671 if a.node.nam.starts_with('-') || a.node.nam.starts_with('+') { // c:1330
4672 print!("-- ");
4673 }
4674 }
4675 // c:1334-1336 — `quotedzputs(nam); putchar('='); quotedzputs(text); putchar('\n');`
4676 println!("{}={}", a.node.nam, a.text);
4677 };
4678
4679 // c:4495-4500 — no args: list all (filtered by flags).
4680 if argv.is_empty() { // c:4495
4681 crate::ported::mem::queue_signals(); // c:4496
4682 let lock = if use_suffix { sufaliastab_lock() } else { aliastab_lock() };
4683 if let Ok(t) = lock.read() {
4684 for (_n, a) in t.iter() { // c:4497
4685 if (a.node.flags & flags1 as i32) == flags1 as i32
4686 && (a.node.flags & flags2 as i32) == 0 {
4687 print_alias(a, printflags);
4688 }
4689 }
4690 }
4691 crate::ported::mem::unqueue_signals(); // c:4498
4692 return 0; // c:4499
4693 }
4694
4695 // c:4503-4519 — `-m` glob branch.
4696 if OPT_ISSET(ops, b'm') { // c:4503
4697 for pat in argv { // c:4504
4698 crate::ported::mem::queue_signals(); // c:4505
4699 // c:4506 — `tokenize + patcompile`.
4700 let pprog = crate::ported::pattern::patcompile(pat, // c:4507
4701 crate::ported::zsh_h::PAT_HEAPDUP, None);
4702 if let Some(prog) = pprog {
4703 let lock = if use_suffix { sufaliastab_lock() } else { aliastab_lock() };
4704 if let Ok(t) = lock.read() {
4705 for (_n, a) in t.iter() { // c:4509
4706 if (a.node.flags & flags1 as i32) == flags1 as i32
4707 && (a.node.flags & flags2 as i32) == 0
4708 && crate::ported::pattern::pattry(&prog, &a.node.nam)
4709 {
4710 print_alias(a, printflags);
4711 }
4712 }
4713 }
4714 } else {
4715 crate::ported::utils::zwarnnam(name,
4716 &format!("bad pattern : {}", pat)); // c:4514
4717 returnval = 1; // c:4515
4718 }
4719 crate::ported::mem::unqueue_signals(); // c:4517
4720 }
4721 return returnval; // c:4518
4722 }
4723
4724 // c:4521-4540 — literal args: define `name=value` or display a single name.
4725 crate::ported::mem::queue_signals(); // c:4522
4726 let mut idx = 0;
4727 while idx < argv.len() { // c:4523
4728 let arg = &argv[idx];
4729 idx += 1;
4730 if let Some(eq) = arg.find('=') { // c:4524 (asg->value.scalar)
4731 if !OPT_ISSET(ops, b'L') { // c:4524
4732 let n = &arg[..eq];
4733 let v = &arg[eq + 1..];
4734 let lock = if use_suffix { sufaliastab_lock() } else { aliastab_lock() };
4735 if let Ok(mut t) = lock.write() {
4736 let a = crate::ported::hashtable::createaliasnode(n, v, flags1); // c:4527
4737 t.add(a);
4738 }
4739 continue;
4740 }
4741 }
4742 let n = if let Some(eq) = arg.find('=') { &arg[..eq] } else { arg.as_str() };
4743 let lock = if use_suffix { sufaliastab_lock() } else { aliastab_lock() };
4744 let found = lock.read().ok().and_then(|t|
4745 t.get_including_disabled(n).map(|a| (a.node.nam.clone(), a.node.flags as u32, a.text.clone()))
4746 );
4747 match found {
4748 Some((nm, fl, txt)) => { // c:4530
4749 // c:4532-4537 — type-filter check.
4750 let show = type_opts == 0
4751 || use_suffix
4752 || (OPT_ISSET(ops, b'r')
4753 && (fl & (ALIAS_GLOBAL | ALIAS_SUFFIX) as u32) == 0)
4754 || (OPT_ISSET(ops, b'g')
4755 && (fl & ALIAS_GLOBAL as u32) != 0);
4756 if show {
4757 let a = crate::ported::hashtable::createaliasnode(&nm, &txt, fl);
4758 print_alias(&a, printflags);
4759 }
4760 }
4761 None => { // c:4538
4762 returnval = 1; // c:4539
4763 }
4764 }
4765 }
4766 crate::ported::mem::unqueue_signals(); // c:4541
4767 returnval // c:4542
4768}
4769
4770/// Port of `bin_true(UNUSED(char *name), UNUSED(char **argv), UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:4550.
4771/// C: `int bin_true(UNUSED(char *name), UNUSED(char **argv),
4772/// UNUSED(Options ops), UNUSED(int func))` → `return 0;`
4773/// WARNING: param names don't match C — Rust=(_name, _argv, _func) vs C=(name, argv, ops, func)
4774pub fn bin_true(_name: &str, _argv: &[String], // c:4550
4775 _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
4776 0 // c:4559
4777}
4778
4779/// Port of `bin_false(UNUSED(char *name), UNUSED(char **argv), UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:4559.
4780/// C: `int bin_false(UNUSED(char *name), UNUSED(char **argv),
4781/// UNUSED(Options ops), UNUSED(int func))` → `return 1;`
4782/// WARNING: param names don't match C — Rust=(_name, _argv, _func) vs C=(name, argv, ops, func)
4783pub fn bin_false(_name: &str, _argv: &[String], // c:4559
4784 _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
4785 1 // c:4562
4786}
4787
4788/// Port of `bin_print(char *name, char **args, Options ops, int func)` from Src/builtin.c:4587.
4789/// C: `int bin_print(char *name, char **args, Options ops, int func)`.
4790///
4791/// The C body is ~1000 lines: `print` / `echo` / `printf` / `pushln`
4792/// dispatcher with -n/-N/-c/-r/-R/-l/-D/-i/-f/-v/-s/-S/-z/-e/-E etc.
4793/// The structural port handles the script-friendly subset that the
4794/// daily-driver hits: print/echo plain emission with -n, -l (one per
4795/// line), -r raw, -E newline-only, -- end-of-options. The full -f
4796/// printf format-spec engine and ZLE/history wireups defer to the
4797/// expand_printf_escapes helpers.
4798/// WARNING: param names don't match C — Rust=(name, args, func) vs C=(name, args, ops, func)
4799pub fn bin_print(name: &str, args: &[String], // c:4587
4800 ops: &crate::ported::zsh_h::options, func: i32) -> i32 {
4801 let nonewline = OPT_ISSET(ops, b'n'); // c:4595
4802 let raw = OPT_ISSET(ops, b'r') || OPT_ISSET(ops, b'R'); // c:4596
4803 let one_per_line = OPT_ISSET(ops, b'l'); // c:4597
4804 let _printf_mode = func == BIN_PRINTF || OPT_HASARG(ops, b'f'); // c:4604
4805 let echo_mode = func == BIN_ECHO;
4806 let _ = (name, raw);
4807
4808 // c:4633-4685 — destination dispatch. -u FD writes to fd, -s pushes
4809 // to history, -z to ZLE buffer, -v VAR assigns to scalar. Defer to
4810 // env/var wireup.
4811 let dest_var: Option<String> = if OPT_HASARG(ops, b'v') {
4812 OPT_ARG(ops, b'v').map(String::from)
4813 } else { None };
4814
4815 // c:4604-4612 — printf format-string handling.
4816 if _printf_mode {
4817 let fmt = if let Some(f) = OPT_ARG(ops, b'f') {
4818 f.to_string()
4819 } else if !args.is_empty() {
4820 args[0].clone()
4821 } else {
4822 return 0;
4823 };
4824 let rest: &[String] = if OPT_HASARG(ops, b'f') { args } else { &args[1..] };
4825 let out = printf_format(&fmt, rest);
4826 if let Some(ref v) = dest_var {
4827 crate::ported::params::setsparam(v, &out);
4828 } else {
4829 print!("{}", out);
4830 }
4831 return 0;
4832 }
4833
4834 // c:4860+ — main print loop.
4835 let sep = if one_per_line { "\n" } else { " " };
4836 // c:4598-4600 — `-P` prompt-style percent expansion (`%n`, `%d`,
4837 // `%?`, `%h`, `%%`, etc.). Routes through `expand_prompt`
4838 // (canonical port of `Src/prompt.c:182 promptexpand`).
4839 let mut processed_args: Vec<String> = if OPT_ISSET(ops, b'P') {
4840 args.iter()
4841 .map(|a| crate::ported::prompt::expand_prompt(a)) // c:Src/prompt.c:182
4842 .collect()
4843 } else {
4844 args.to_vec()
4845 };
4846 // c:Src/builtin.c:4869-4880 `-o` / `-O` / `-i` sort flags.
4847 // -o → case-insensitive ascending, -O → case-insensitive
4848 // descending, -i → case-sensitive (with -o/-O).
4849 if OPT_ISSET(ops, b'o') || OPT_ISSET(ops, b'O') {
4850 let case_sensitive = OPT_ISSET(ops, b'i');
4851 if case_sensitive {
4852 processed_args.sort();
4853 } else {
4854 processed_args.sort_by_key(|s| s.to_lowercase());
4855 }
4856 if OPT_ISSET(ops, b'O') {
4857 processed_args.reverse();
4858 }
4859 }
4860 // c:Src/builtin.c:4866-4886 — when `-r` is NOT set, each arg goes
4861 // through `getkeystring` to interpret backslash escapes (`\n`,
4862 // `\t`, `\\`, escaped space `\ `, etc.). `echo` follows the same
4863 // path when `BSD_ECHO`/`SH_OPTION_LETTERS`-style isn't in effect;
4864 // BIN_ECHO with `-E` keeps escapes literal. Without this, `print
4865 // -- ${(q)a}` for `a="he llo"` emitted `he\ llo` instead of zsh's
4866 // `he llo` (the (q) flag's backslash gets consumed by print).
4867 if !raw {
4868 let echo_E = echo_mode && OPT_ISSET(ops, b'E');
4869 if !echo_E {
4870 for a in processed_args.iter_mut() {
4871 let (s, _) = crate::ported::utils::getkeystring_with(a,
4872 crate::ported::utils::GETKEYS_PRINT);
4873 *a = s;
4874 }
4875 }
4876 }
4877 let body = processed_args.join(sep);
4878 if let Some(ref v) = dest_var {
4879 crate::ported::params::setsparam(v, &body);
4880 } else {
4881 print!("{}", body);
4882 // c:5550 — final newline unless -n.
4883 if !nonewline && !echo_mode {
4884 println!();
4885 } else if echo_mode && !nonewline {
4886 println!();
4887 }
4888 }
4889 0
4890}
4891
4892/// Port of `bin_shift(char *name, char **argv, Options ops, UNUSED(int func))` from Src/builtin.c:5593.
4893/// C: `int bin_shift(char *name, char **argv, Options ops, UNUSED(int func))`
4894/// — shift positional params (or named arrays) by `num` positions; `-p`
4895/// pops from the right end.
4896/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
4897pub fn bin_shift(name: &str, argv: &[String], // c:5593
4898 ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
4899 let mut num: i32 = 1; // c:5595
4900 let mut ret: i32 = 0; // c:5595
4901 let mut idx = 0usize;
4902 crate::ported::mem::queue_signals(); // c:5599
4903 // c:5600-5605 — first arg parsed as math expr unless it's an array name.
4904 if !argv.is_empty() { // c:5600
4905 let first = &argv[0];
4906 // c:5600 — `if (!getaparam(*argv))` decides whether the arg is
4907 // a numeric shift-count vs an array name. Check
4908 // paramtab for a PM_ARRAY entry, not OS env.
4909 let is_array = {
4910 use crate::ported::zsh_h::{PM_ARRAY, PM_TYPE};
4911 let tab = crate::ported::params::paramtab().read().unwrap();
4912 tab.get(first)
4913 .map(|pm| PM_TYPE(pm.node.flags as u32) == PM_ARRAY)
4914 .unwrap_or(false)
4915 };
4916 if !is_array { // c:5600
4917 // c:5601 — `num = mathevali(*argv++);`. The previous Rust port
4918 // used `parse::<i32>()` which rejects any non-trivial
4919 // arithmetic: `shift 1+2` would silently return ret=1
4920 // instead of shifting by 3. Route through mathevali.
4921 num = crate::ported::math::mathevali(first).unwrap_or_else(|_| {
4922 ret = 1;
4923 0
4924 }) as i32; // c:5601
4925 idx = 1;
4926 // c:5602-5605 — `if (errflag) return 1;`.
4927 if ret != 0
4928 || crate::ported::utils::errflag.load(Ordering::Relaxed) != 0
4929 {
4930 crate::ported::mem::unqueue_signals(); // c:5604
4931 return 1;
4932 }
4933 }
4934 }
4935
4936 // c:5608-5611 — `if (num < 0)` reject.
4937 if num < 0 { // c:5608
4938 crate::ported::mem::unqueue_signals(); // c:5609
4939 crate::ported::utils::zwarnnam(name,
4940 "argument to shift must be non-negative"); // c:5610
4941 return 1; // c:5611
4942 }
4943
4944 // c:5614-5635 — named-array shift loop.
4945 if idx < argv.len() { // c:5614
4946 for arr_name in &argv[idx..] { // c:5615
4947 // c:5616 — `if ((s = getaparam(*argv)))` else silent skip.
4948 // Read paramtab directly; was approximating arrays
4949 // as `:`-separated env values which is wrong (env
4950 // can never carry array structure).
4951 let s: Vec<String> = {
4952 let tab = crate::ported::params::paramtab().read().unwrap();
4953 match tab.get(arr_name).and_then(|pm| pm.u_arr.clone()) {
4954 Some(arr) => arr,
4955 None => continue,
4956 }
4957 };
4958 // c:5617-5621 — arrlen_lt check.
4959 if (s.len() as i32) < num { // c:5617
4960 crate::ported::utils::zwarnnam(name,
4961 "shift count must be <= $#"); // c:5618
4962 ret += 1; // c:5619
4963 continue; // c:5620
4964 }
4965 // c:5622-5634 — -p shifts off the right end, otherwise the left.
4966 let s2: Vec<String> = if OPT_ISSET(ops, b'p') { // c:5622
4967 s[..s.len() - num as usize].to_vec() // c:5625-5628
4968 } else {
4969 s[num as usize..].to_vec() // c:5631
4970 };
4971 // c:5633 — `setaparam(*argv, s);`. Write the shifted array
4972 // back to paramtab as a proper PM_ARRAY. Was a
4973 // fake: `env::set_var` + colon-joined fake-array
4974 // which neither carries array structure nor
4975 // reaches subsequent `${arr_name[@]}` expansions.
4976 crate::ported::params::setaparam(arr_name, s2);
4977 }
4978 } else {
4979 // c:5636-5654 — shift positional parameters ($1..$N).
4980 // Static-link path: positional params live in src/ported/exec.rs;
4981 // expose via PPARAMS Mutex<Vec<String>>.
4982 let mut pp = PPARAMS.lock().unwrap_or_else(|e| { PPARAMS.clear_poison(); e.into_inner() });
4983 let l = pp.len() as i32;
4984 if num > l { // c:5636
4985 crate::ported::utils::zwarnnam(name, "shift count must be <= $#"); // c:5637
4986 ret = 1; // c:5638
4987 } else if OPT_ISSET(ops, b'p') { // c:5641
4988 pp.truncate((l - num) as usize); // c:5642-5644
4989 } else {
4990 pp.drain(..num as usize); // c:5646-5650
4991 }
4992 // PPARAMS is the single source of truth. fusevm-side reads
4993 // route through exec.pparams() which reads PPARAMS, so the
4994 // shift is immediately visible — no exec.positional_params
4995 // mirror needed.
4996 drop(pp);
4997 }
4998 crate::ported::mem::unqueue_signals(); // c:5658
4999 ret // c:5659
5000}
5001
5002/// Port of `bin_getopts(UNUSED(char *name), char **argv, UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:5672.
5003/// C: `int bin_getopts(UNUSED(char *name), char **argv, UNUSED(Options ops),
5004/// UNUSED(int func))`.
5005///
5006/// POSIX getopts. Maintains state in $OPTIND (zoptind) and an internal
5007/// per-arg cursor (optcind). Reads from the script's positional params
5008/// when no extra args supplied, otherwise from the trailing argv.
5009/// WARNING: param names don't match C — Rust=(_name, argv, _func) vs C=(name, argv, ops, func)
5010pub fn bin_getopts(_name: &str, argv: &[String], // c:5672
5011 _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
5012 if argv.len() < 2 { return 1; }
5013 // c:5675 — `char *optstr = unmetafy(*argv++, &lenoptstr); char *var = *argv++;`
5014 let optstr_full = argv[0].clone();
5015 let var = argv[1].clone();
5016 // c:5676 — `char **args = (*argv) ? argv : pparams;`
5017 let argv_rest: Vec<String> = argv[2..].to_vec();
5018 let args: Vec<String> = if !argv_rest.is_empty() {
5019 argv_rest
5020 } else {
5021 PPARAMS.lock().map(|p| p.clone()).unwrap_or_default()
5022 };
5023
5024 // c:5681-5685 — `if (zoptind < 1) { zoptind = 1; optcind = 0; }`
5025 let mut zoptind = ZOPTIND.load(Ordering::Relaxed);
5026 if zoptind < 1 { // c:5681
5027 zoptind = 1;
5028 OPTCIND.store(0, Ordering::Relaxed);
5029 }
5030 let mut optcind = OPTCIND.load(Ordering::Relaxed);
5031
5032 // c:5686-5688 — `if (arrlen_lt(args, zoptind)) return 1;`
5033 if (args.len() as i32) < zoptind { // c:5686
5034 ZOPTIND.store(zoptind, Ordering::Relaxed);
5035 return 1;
5036 }
5037
5038 // c:5691-5693 — `quiet = *optstr == ':'; optstr += quiet; lenoptstr -= quiet;`
5039 let (quiet, optstr) = if optstr_full.starts_with(':') { // c:5691
5040 (true, &optstr_full[1..])
5041 } else {
5042 (false, optstr_full.as_str())
5043 };
5044
5045 // c:5696 — `str = unmetafy(dupstring(args[zoptind - 1]), &lenstr);`
5046 let mut str_buf = args[(zoptind - 1) as usize].clone();
5047 let mut lenstr = str_buf.len() as i32;
5048 if lenstr == 0 { return 1; } // c:5697
5049
5050 // c:5699-5703 — bump to next arg if optcind exhausted current.
5051 if optcind >= lenstr { // c:5699
5052 optcind = 0;
5053 zoptind += 1;
5054 if zoptind as usize > args.len() { // c:5701
5055 ZOPTIND.store(zoptind, Ordering::Relaxed);
5056 OPTCIND.store(optcind, Ordering::Relaxed);
5057 return 1;
5058 }
5059 str_buf = args[(zoptind - 1) as usize].clone();
5060 lenstr = str_buf.len() as i32;
5061 }
5062
5063 // c:5705-5712 — first option char checks: not `-`/`+` → done; `--` → done.
5064 if optcind == 0 { // c:5705
5065 if lenstr < 2 || (!str_buf.starts_with('-') && !str_buf.starts_with('+')) {
5066 ZOPTIND.store(zoptind, Ordering::Relaxed);
5067 OPTCIND.store(optcind, Ordering::Relaxed);
5068 return 1;
5069 }
5070 if lenstr == 2 && &str_buf[..2] == "--" { // c:5708
5071 zoptind += 1;
5072 ZOPTIND.store(zoptind, Ordering::Relaxed);
5073 OPTCIND.store(0, Ordering::Relaxed);
5074 return 1;
5075 }
5076 optcind += 1;
5077 }
5078 // c:5715 — `opch = str[optcind++];`
5079 let opch = str_buf.as_bytes()[optcind as usize];
5080 optcind += 1;
5081
5082 // c:5716-5721 — `lenoptbuf = (str[0] == '+') ? 2 : 1; optbuf[lenoptbuf-1] = opch;`
5083 let plus = str_buf.starts_with('+');
5084 let optbuf: String = if plus {
5085 format!("+{}", opch as char)
5086 } else {
5087 format!("{}", opch as char)
5088 };
5089
5090 // c:5724-5740 — illegal option: `?` reply, OPTIND fixed under POSIXBUILTINS.
5091 let posix = crate::ported::zsh_h::isset(crate::ported::options::optlookup("posixbuiltins"));
5092 let found = optstr.bytes().position(|b| b == opch);
5093 if opch == b':' || found.is_none() { // c:5724
5094 if posix { // c:5728
5095 optcind = 0;
5096 zoptind += 1;
5097 }
5098 // c:5731 — `setsparam(var, ztrdup(p));` where p = "?"
5099 crate::ported::params::setsparam(&var, "?");
5100 if quiet { // c:5733
5101 crate::ported::params::setsparam("OPTARG", &optbuf); // c:5734
5102 } else {
5103 let prefix = if plus { "+" } else { "-" };
5104 crate::ported::utils::zwarn(&format!(
5105 "bad option: {}{}", prefix, opch as char)); // c:5736
5106 crate::ported::params::setsparam("OPTARG", "");
5107 }
5108 ZOPTIND.store(zoptind, Ordering::Relaxed);
5109 OPTCIND.store(optcind, Ordering::Relaxed);
5110 // Sync OPTIND env var so callers can read.
5111 crate::ported::params::setiparam("OPTIND", zoptind as i64);
5112 return 0;
5113 }
5114
5115 // c:5744 — `if (p[1] == ':')` — required argument.
5116 let p = found.unwrap();
5117 let optstr_bytes = optstr.as_bytes();
5118 if p + 1 < optstr_bytes.len() && optstr_bytes[p + 1] == b':' { // c:5744
5119 if optcind == lenstr { // c:5745
5120 // c:5746 — argument in next arg.
5121 if zoptind as usize >= args.len() { // c:5747
5122 if posix {
5123 optcind = 0;
5124 zoptind += 1;
5125 }
5126 if quiet { // c:5754
5127 crate::ported::params::setsparam(&var, ":");
5128 crate::ported::params::setsparam("OPTARG", &optbuf);
5129 } else {
5130 crate::ported::params::setsparam(&var, "?");
5131 crate::ported::params::setsparam("OPTARG", "");
5132 let prefix = if plus { "+" } else { "-" };
5133 crate::ported::utils::zwarn(&format!(
5134 "argument expected after {}{} option",
5135 prefix, opch as char)); // c:5760
5136 }
5137 ZOPTIND.store(zoptind, Ordering::Relaxed);
5138 OPTCIND.store(optcind, Ordering::Relaxed);
5139 crate::ported::params::setiparam("OPTIND", zoptind as i64);
5140 return 0;
5141 }
5142 let p_arg = args[zoptind as usize].clone();
5143 zoptind += 1;
5144 crate::ported::params::setsparam("OPTARG", &p_arg); // c:5765
5145 optcind = 0;
5146 } else {
5147 // c:5774 — `p = metafy(str+optcind, lenstr-optcind, META_DUP);`
5148 let p_arg = str_buf[(optcind as usize)..].to_string();
5149 crate::ported::params::setsparam("OPTARG", &p_arg);
5150 optcind = 0;
5151 zoptind += 1;
5152 }
5153 } else {
5154 // c:5784 — `zsfree(zoptarg); zoptarg = ztrdup("");`
5155 crate::ported::params::setsparam("OPTARG", "");
5156 }
5157
5158 // c:5788 — `setsparam(var, metafy(optbuf, lenoptbuf, META_DUP));`
5159 crate::ported::params::setsparam(&var, &optbuf);
5160 ZOPTIND.store(zoptind, Ordering::Relaxed);
5161 OPTCIND.store(optcind, Ordering::Relaxed);
5162 crate::ported::params::setiparam("OPTIND", zoptind as i64);
5163 0 // c:5790
5164}
5165
5166/// Port of `bin_break(char *name, char **argv, UNUSED(Options ops), int func)` from Src/builtin.c:5809.
5167/// C: `int bin_break(char *name, char **argv, UNUSED(Options ops), int func)`
5168/// — handles BIN_BREAK / BIN_CONTINUE / BIN_RETURN / BIN_LOGOUT / BIN_EXIT.
5169/// WARNING: param names don't match C — Rust=(name, argv, func) vs C=(name, argv, ops, func)
5170pub fn bin_break(name: &str, argv: &[String], // c:5809
5171 _ops: &crate::ported::zsh_h::options, func: i32) -> i32 {
5172 // BIN_BREAK/CONTINUE/RETURN/EXIT/LOGOUT live at the top of this file
5173 // (c:5707-5712 in Src/builtin.c via the BUILTIN(...) table).
5174 // c:5811 — `int num = lastval, nump = 0, implicit;`
5175 let mut num: i32 = LASTVAL.load(Ordering::Relaxed); // c:5811
5176 let mut nump = 0i32; // c:5811
5177 let implicit = argv.is_empty(); // c:5814
5178 // c:5815-5818 — first arg parsed as math expr.
5179 if !implicit { // c:5815
5180 num = mathevali(&argv[0]).unwrap_or(0) as i32; // c:5816
5181 nump = 1; // c:5817
5182 }
5183
5184 // c:5820-5823 — positive-num requirement for BIN_CONTINUE / BIN_BREAK.
5185 if nump > 0 && (func == BIN_CONTINUE || func == BIN_BREAK) && num <= 0 { // c:5820
5186 crate::ported::utils::zwarnnam(name, &format!("argument is not positive: {}", num)); // c:5821
5187 return 1; // c:5822
5188 }
5189
5190 let loops = LOOPS.load(Ordering::Relaxed);
5191 match func {
5192 // c:5831-5842 — BIN_CONTINUE: must be in a loop, set contflag,
5193 // then fall through to BIN_BREAK's break-count assign.
5194 x if x == BIN_CONTINUE => { // c:5831
5195 if loops == 0 { // c:5832
5196 crate::ported::utils::zwarnnam(name, "not in while, until, select, or repeat loop"); // c:5833
5197 return 1; // c:5834
5198 }
5199 CONTFLAG.store(1, Ordering::Relaxed); // c:5836 FALLTHROUGH
5200 // c:5837 — fallthrough to BIN_BREAK's loops==0 guard
5201 // (impossible here since we already returned above) +
5202 // break-count assign. Inlined directly. The previous
5203 // Rust port had a redundant `if loops == 0 { return 1 }`
5204 // dead-coded after the first guard.
5205 BREAKS.store(if nump != 0 { num.min(loops) } else { 1 }, // c:5842
5206 Ordering::Relaxed);
5207 }
5208 // c:5832-5838 — BIN_BREAK.
5209 x if x == BIN_BREAK => { // c:5832
5210 if loops == 0 { // c:5833
5211 crate::ported::utils::zwarnnam(name, "not in while, until, select, or repeat loop"); // c:5834
5212 return 1; // c:5835
5213 }
5214 BREAKS.store(if nump != 0 { num.min(loops) } else { 1 }, // c:5837
5215 Ordering::Relaxed);
5216 }
5217 // c:5839-5860 — BIN_RETURN.
5218 x if x == BIN_RETURN => {
5219 let interactive = crate::ported::zsh_h::isset(crate::ported::options::optlookup("interactive"));
5220 let shinstdin = crate::ported::zsh_h::isset(crate::ported::options::optlookup("shinstdin"));
5221 let locallevel = LOCALLEVEL.load(Ordering::Relaxed);
5222 let sourcelevel = SOURCELEVEL.load(Ordering::Relaxed);
5223 // c:5840-5841 — `if ((interactive && shinstdin) || locallevel || sourcelevel)`
5224 if (interactive && shinstdin) || locallevel != 0 || sourcelevel != 0 { // c:5840
5225 RETFLAG.store(1, Ordering::Relaxed); // c:5842
5226 BREAKS.store(loops, Ordering::Relaxed); // c:5843
5227 LASTVAL.store(num, Ordering::Relaxed); // c:5844
5228 // c:5845-5854 — inside a primed trap with the sentinel
5229 // `trap_return == -2`, promote to TRAP_STATE_FORCE_RETURN
5230 // and carry `lastval`. POSIXTRAPS + `implicit` opts out:
5231 // POSIX semantics keep $? from before the trap fired.
5232 let posixtraps =
5233 crate::ported::zsh_h::isset(crate::ported::options::optlookup("posixtraps"));
5234 let cur_state =
5235 crate::exec::TRAP_STATE.load(Ordering::Relaxed);
5236 let cur_return =
5237 crate::exec::TRAP_RETURN.load(Ordering::Relaxed);
5238 if cur_state == crate::ported::zsh_h::TRAP_STATE_PRIMED // c:5845
5239 && cur_return == -2 // c:5845
5240 && !(posixtraps && implicit) // c:5851
5241 {
5242 crate::exec::TRAP_STATE.store( // c:5852
5243 crate::ported::zsh_h::TRAP_STATE_FORCE_RETURN,
5244 Ordering::Relaxed,
5245 );
5246 crate::exec::TRAP_RETURN.store(num, Ordering::Relaxed); // c:5853
5247 }
5248 return num; // c:5855
5249 }
5250 // c:5858 — fallthrough: treat as logout/exit.
5251 zexit(num, ZEXIT_NORMAL); // c:5858
5252 }
5253 // c:5864-5869 — BIN_LOGOUT: refuse if not LOGINSHELL, then
5254 // FALLTHROUGH into the BIN_EXIT body. The previous Rust port
5255 // called \`zexit(num, ZEXIT_NORMAL)\` directly instead of
5256 // entering the BIN_EXIT defer-guard, so \`logout\` from inside
5257 // a function would skip EXIT traps + function unwind +
5258 // \"you have running jobs\" warning — same gap as the prior
5259 // BIN_EXIT fix.
5260 x if x == BIN_LOGOUT => {
5261 // c:5865 — `if (unset(LOGINSHELL))`. The previous Rust port
5262 // called `optlookup("login")` — but "login" is the
5263 // SHELL-LETTER-FLAG name (zshletters table letter 'l'),
5264 // not an option name. Option name canonicalization maps
5265 // LOGINSHELL → "loginshell" (Src/options.c index_to_name
5266 // at line 1682 in Rust port).
5267 //
5268 // \`optlookup(\"login\")\` returns OPT_INVALID (0), so
5269 // \`isset(0)\` always returns false — bin_logout always
5270 // saw \"not login shell\" and rejected with that error
5271 // regardless of whether the shell was actually started
5272 // with \`-l\`.
5273 let loginshell = crate::ported::zsh_h::isset(crate::ported::options::optlookup("loginshell"));
5274 if !loginshell { // c:5865
5275 crate::ported::utils::zwarnnam(name, "not login shell"); // c:5866
5276 return 1; // c:5867
5277 }
5278 // c:5869 — `/*FALLTHROUGH*/` into BIN_EXIT body.
5279 // Reusing the BIN_EXIT branch below by setting `func` to
5280 // BIN_EXIT isn't possible mid-match; inline the same
5281 // guard logic here.
5282 let cur_locallevel = LOCALLEVEL.load(Ordering::Relaxed);
5283 let forklevel = crate::exec::FORKLEVEL.load(Ordering::Relaxed);
5284 let shell_exiting = SHELL_EXITING.load(Ordering::Relaxed);
5285 if cur_locallevel > forklevel && shell_exiting != -1 { // c:5871
5286 if STOPMSG.load(Ordering::Relaxed) == 0 {
5287 zexit(0, crate::ported::zsh_h::ZEXIT_DEFERRED); // c:5884
5288 }
5289 if STOPMSG.load(Ordering::Relaxed) == 0 { // c:5884
5290 let trap_state = crate::exec::TRAP_STATE.load(Ordering::Relaxed);
5291 if trap_state != 0 { // c:5885
5292 crate::exec::TRAP_STATE.store( // c:5886
5293 crate::ported::zsh_h::TRAP_STATE_FORCE_RETURN,
5294 Ordering::Relaxed,
5295 );
5296 }
5297 RETFLAG.store(1, Ordering::Relaxed); // c:5887
5298 BREAKS.store(LOOPS.load(Ordering::Relaxed), // c:5888
5299 Ordering::Relaxed);
5300 EXIT_PENDING.store(1, Ordering::Relaxed); // c:5889
5301 EXIT_VAL.store(num, Ordering::Relaxed); // c:5891
5302 }
5303 } else {
5304 zexit(num, ZEXIT_NORMAL); // c:5894
5305 }
5306 }
5307 // c:5870-5894 — BIN_EXIT: function-context guard. C body:
5308 // if (locallevel > forklevel && shell_exiting != -1) {
5309 // if (stopmsg || (zexit(0, ZEXIT_DEFERRED), !stopmsg)) {
5310 // if (trap_state) trap_state = TRAP_STATE_FORCE_RETURN;
5311 // retflag = 1; breaks = loops;
5312 // exit_pending = 1; exit_level = locallevel; exit_val = num;
5313 // }
5314 // } else zexit(num, ZEXIT_NORMAL);
5315 //
5316 // Inside a function (locallevel > forklevel) the shell can't
5317 // exit directly — EXIT traps still need to run. The probe
5318 // path zexit(0, ZEXIT_DEFERRED) calls checkjobs; if no
5319 // stopmsg triggered, we defer: set retflag + breaks +
5320 // exit_pending so the function unwind takes us out.
5321 //
5322 // The previous Rust port skipped this entire guard, always
5323 // calling zexit(num, ZEXIT_NORMAL) directly. `exit` inside
5324 // a function would terminate without running EXIT traps or
5325 // unwinding the function stack.
5326 x if x == BIN_EXIT => {
5327 let cur_locallevel = LOCALLEVEL.load(Ordering::Relaxed);
5328 let forklevel = crate::exec::FORKLEVEL.load(Ordering::Relaxed);
5329 let shell_exiting = SHELL_EXITING.load(Ordering::Relaxed);
5330 if cur_locallevel > forklevel && shell_exiting != -1 { // c:5871
5331 // Probe via ZEXIT_DEFERRED — may set stopmsg.
5332 if STOPMSG.load(Ordering::Relaxed) == 0 {
5333 zexit(0, crate::ported::zsh_h::ZEXIT_DEFERRED); // c:5884
5334 }
5335 if STOPMSG.load(Ordering::Relaxed) == 0 { // c:5884 still no stopmsg → defer
5336 let trap_state = crate::exec::TRAP_STATE.load(Ordering::Relaxed);
5337 if trap_state != 0 { // c:5885
5338 crate::exec::TRAP_STATE.store( // c:5886
5339 crate::ported::zsh_h::TRAP_STATE_FORCE_RETURN,
5340 Ordering::Relaxed,
5341 );
5342 }
5343 RETFLAG.store(1, Ordering::Relaxed); // c:5887
5344 BREAKS.store(LOOPS.load(Ordering::Relaxed), // c:5888
5345 Ordering::Relaxed);
5346 EXIT_PENDING.store(1, Ordering::Relaxed); // c:5889
5347 // exit_level not yet ported as a global; the
5348 // RETFLAG path handles function-scope unwind.
5349 EXIT_VAL.store(num, Ordering::Relaxed); // c:5891
5350 }
5351 } else {
5352 zexit(num, ZEXIT_NORMAL); // c:5894
5353 }
5354 }
5355 _ => {}
5356 }
5357 0
5358}
5359
5360/// Port of `checkjobs()` from Src/builtin.c:5899.
5361/// C: `static void checkjobs(void)` — walk `jobtab[1..maxjob]`; for each
5362/// non-current job that's STAT_LOCKED, not STAT_NOPRINT, and either
5363/// running (when CHECKRUNNINGJOBS is set) or STAT_STOPPED, emit
5364/// "you have running/stopped jobs" + set `stopmsg = 1`.
5365pub fn checkjobs() { // c:5899
5366 use std::sync::Mutex;
5367 let checkrunning = crate::ported::zsh_h::isset(crate::ported::options::optlookup("checkrunningjobs"));
5368 // c:5901 — read the canonical jobs.rs THISJOB/MAXJOB globals.
5369 // The previous builtin.rs duplicate AtomicI32s for both never
5370 // synced with the jobs.rs Mutex<i32> values that the spawn /
5371 // wait paths actually update — checkjobs would see stale 0s
5372 // regardless of how many jobs were active.
5373 let thisjob: i32 = *crate::ported::jobs::THISJOB
5374 .get_or_init(|| Mutex::new(-1_i32))
5375 .lock().expect("THISJOB poisoned");
5376 // jobs::MAXJOB is stored as `Mutex<usize>` (Rust adaptation for
5377 // Vec-index semantics); cast to i32 for comparison with `thisjob`.
5378 let maxjob: i32 = *crate::ported::jobs::MAXJOB
5379 .get_or_init(|| Mutex::new(0_usize))
5380 .lock().expect("MAXJOB poisoned") as i32;
5381
5382 // c:5903 — `for (i = 1; i <= maxjob; i++)`
5383 let mut found: Option<i32> = None;
5384 let mut found_stat: i32 = 0;
5385 for i in 1..=maxjob { // c:5903
5386 let stat = JOBSTATS.lock()
5387 .ok()
5388 .and_then(|t| t.get(i as usize).copied())
5389 .unwrap_or(0);
5390 // c:5904-5906 — `i != thisjob && (stat & STAT_LOCKED) &&
5391 // !(stat & STAT_NOPRINT) &&
5392 // (CHECKRUNNINGJOBS || stat & STAT_STOPPED)`
5393 if i != thisjob // c:5904
5394 && (stat & STAT_LOCKED) != 0 // c:5904
5395 && (stat & STAT_NOPRINT) == 0 // c:5905
5396 && (checkrunning || (stat & STAT_STOPPED) != 0) // c:5906
5397 {
5398 found = Some(i); // c:5907
5399 found_stat = stat;
5400 break;
5401 }
5402 }
5403 // c:5908 — `if (i <= maxjob)`
5404 if found.is_some() { // c:5908
5405 if (found_stat & STAT_STOPPED) != 0 { // c:5909
5406 // c:5912/5914 — `zerr("you have suspended/stopped jobs.");`
5407 crate::ported::utils::zerr("you have stopped jobs."); // c:5914
5408 } else {
5409 // c:5917 — `zerr("you have running jobs.");`
5410 crate::ported::utils::zerr("you have running jobs."); // c:5917
5411 }
5412 STOPMSG.store(1, Ordering::Relaxed); // c:5919
5413 }
5414}
5415
5416/// Port of `realexit()` from Src/builtin.c:5953.
5417/// C body (single statement):
5418/// `exit((shell_exiting || exit_pending) ? exit_val : lastval);`
5419pub fn realexit() -> ! { // c:5953
5420 use std::sync::atomic::Ordering::Relaxed;
5421 std::process::exit(if SHELL_EXITING.load(Relaxed) != 0 || EXIT_PENDING.load(Relaxed) != 0 { EXIT_VAL.load(Relaxed) } else { LASTVAL.load(Relaxed) });
5422}
5423
5424/// Port of `_realexit()` from Src/builtin.c:5962.
5425/// C body (single statement):
5426/// `_exit((shell_exiting || exit_pending) ? exit_val : lastval);`
5427pub fn _realexit() -> ! { // c:5962
5428 use std::sync::atomic::Ordering::Relaxed;
5429 unsafe { libc::_exit(if SHELL_EXITING.load(Relaxed) != 0 || EXIT_PENDING.load(Relaxed) != 0 { EXIT_VAL.load(Relaxed) } else { LASTVAL.load(Relaxed) }) }
5430}
5431
5432/// Port of `zexit(int val, enum zexit_t from_where)` from Src/builtin.c:5977.
5433/// C: `void zexit(int val, enum zexit_t from_where)` — record exit
5434/// value, fire EXIT trap unless already exiting, then realexit.
5435#[allow(unused_variables)]
5436pub fn zexit(val: i32, from_where: i32) { // c:5977
5437 use crate::ported::zsh_h::{MONITOR, ZEXIT_NORMAL, ZEXIT_SIGNAL, ZEXIT_DEFERRED};
5438 // c:5989 — `exit_val = val;`
5439 EXIT_VAL.store(val, Ordering::Relaxed); // c:5989
5440 // c:5990 — `if (shell_exiting == -1) { retflag = 1; breaks = loops; return; }`
5441 if SHELL_EXITING.load(Ordering::Relaxed) == -1 { // c:5990
5442 RETFLAG.store(1, Ordering::Relaxed); // c:5991
5443 BREAKS.store(LOOPS.load(Ordering::Relaxed), Ordering::Relaxed); // c:5992
5444 return; // c:5993
5445 }
5446
5447 // c:5996-6004 — `if (isset(MONITOR) && !stopmsg && from_where != ZEXIT_SIGNAL)`:
5448 // run scanjobs + checkjobs; if stopmsg got set (running jobs warned),
5449 // mark stopmsg=2 and DEFER the exit. The previous Rust port skipped
5450 // this entire block, so `exit` with running jobs would terminate
5451 // immediately rather than emitting the standard
5452 // \"zsh: you have running jobs\" + waiting for a confirmation exit.
5453 if crate::ported::zsh_h::isset(MONITOR) // c:5996
5454 && STOPMSG.load(Ordering::Relaxed) == 0
5455 && from_where != ZEXIT_SIGNAL
5456 {
5457 checkjobs(); // c:5999
5458 if STOPMSG.load(Ordering::Relaxed) != 0 { // c:6000
5459 STOPMSG.store(2, Ordering::Relaxed); // c:6001
5460 return; // c:6002 defer
5461 }
5462 }
5463 // c:6006-6008 — `if (from_where == ZEXIT_DEFERRED || (shell_exiting++
5464 // && from_where != ZEXIT_NORMAL)) return;`. Probe path:
5465 // ZEXIT_DEFERRED callers only want the checkjobs gate to fire; if
5466 // it didn't trip, return without actually exiting.
5467 if from_where == ZEXIT_DEFERRED { // c:6006
5468 return;
5469 }
5470 let prev_exiting = SHELL_EXITING.fetch_add(1, Ordering::Relaxed);
5471 if prev_exiting != 0 && from_where != ZEXIT_NORMAL { // c:6007
5472 return;
5473 }
5474 // c:6014 — `shell_exiting = -1;`
5475 SHELL_EXITING.store(-1, Ordering::Relaxed); // c:6014
5476 // c:6019 — `errflag = 0;`
5477 crate::ported::utils::errflag.store(0, Ordering::Relaxed); // c:6019
5478 // c:6021-6024 — MONITOR → killrunjobs.
5479 if crate::ported::zsh_h::isset(MONITOR) { // c:6021
5480 crate::ported::signals::killrunjobs(
5481 if from_where == ZEXIT_SIGNAL { 1 } else { 0 }
5482 ); // c:6023
5483 }
5484 realexit(); // c:6082
5485}
5486
5487/// Port of `bin_dot(char *name, char **argv, UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:6060.
5488/// C: `int bin_dot(char *name, char **argv, ...)` — `.` / `source`
5489/// builtin: locate script (cwd → first `/`-bearing path → $path search)
5490/// and execute it; positional params shift to argv[1..].
5491/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
5492pub fn bin_dot(name: &str, argv: &[String], // c:6060
5493 _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
5494 if argv.is_empty() { // c:6068
5495 return 0; // c:6069
5496 }
5497
5498 // PFA-SMR aspect: record the source path so the replay tool can
5499 // re-apply the same source/dot at the same call site.
5500 #[cfg(feature = "recorder")]
5501 if crate::recorder::is_enabled() && !argv[0].is_empty() {
5502 let ctx = crate::recorder::recorder_ctx_global();
5503 crate::recorder::emit_source(&argv[0], ctx);
5504 }
5505 // c:6071-6074 — save pparams, install argv[1..] as new pparams.
5506 let saved_pparams: Option<Vec<String>> = if argv.len() > 1 { // c:6072
5507 let mut pp = PPARAMS.lock().unwrap_or_else(|e| { PPARAMS.clear_poison(); e.into_inner() });
5508 let saved = pp.clone();
5509 *pp = argv[1..].to_vec(); // c:6073
5510 Some(saved)
5511 } else { None };
5512
5513 let arg0 = argv[0].clone(); // c:6076
5514 let _enam = arg0.clone(); // c:6076
5515 // c:6077-6080 — `if (isset(FUNCTIONARGZERO)) { old0 = argzero;
5516 // argzero = ztrdup(arg0); }`.
5517 // Save the prior argzero so it can be restored at the end of
5518 // bin_dot; under FUNCTIONARGZERO, the sourced file becomes the
5519 // active $0 for the duration of the source.
5520 let saved_argzero: Option<Option<String>> =
5521 if isset(crate::ported::zsh_h::FUNCTIONARGZERO) {
5522 let prev = crate::ported::utils::argzero();
5523 crate::ported::utils::set_argzero(Some(arg0.clone()));
5524 Some(prev)
5525 } else {
5526 None
5527 };
5528 let mut diddot = 0i32; // c:6064
5529 let mut dotdot = 0i32; // c:6064
5530
5531 // c:6087-6093 — for `source`, try cwd first.
5532 let mut found_path: Option<String> = None;
5533 if !name.starts_with('.') { // c:6087
5534 let p = std::path::Path::new(&arg0);
5535 if p.exists() && !p.is_dir() { // c:6088-6089
5536 diddot = 1; // c:6090
5537 found_path = Some(arg0.clone()); // c:6091 (effective)
5538 }
5539 }
5540
5541 // c:6094-6101 — try literal path with `/` in it.
5542 if found_path.is_none() && arg0.contains('/') { // c:6096
5543 if arg0.starts_with("./") { diddot += 1; } // c:6097
5544 else if arg0.starts_with("../") { dotdot += 1; } // c:6098
5545 let p = std::path::Path::new(&arg0);
5546 if p.exists() && !p.is_dir() {
5547 found_path = Some(arg0.clone()); // c:6100
5548 }
5549 }
5550
5551 // c:6102-6121 — $path search (with PATHDIRS guard).
5552 let pathdirs = crate::ported::zsh_h::isset(crate::ported::options::optlookup("pathdirs"));
5553 if found_path.is_none() && (!arg0.contains('/') || (pathdirs && diddot < 2 && dotdot == 0)) { // c:6102
5554 // c:6103 — `for (pp = path; *pp; pp++)`. C walks the `path[]`
5555 // array (the shell-side $path), not the colon-joined
5556 // $PATH env. Read $PATH from paramtab (the shell
5557 // string view); the colon-split below mirrors the C
5558 // path[] iteration.
5559 let path_env = crate::ported::params::getsparam("PATH").unwrap_or_default();
5560 for dir in path_env.split(':') { // c:6107
5561 let buf = if dir.is_empty() || dir == "." { // c:6108
5562 if diddot != 0 { continue; }
5563 diddot = 1; // c:6111
5564 arg0.clone() // c:6112
5565 } else {
5566 format!("{}/{}", dir, arg0) // c:6114
5567 };
5568 let p = std::path::Path::new(&buf);
5569 if p.exists() && !p.is_dir() { // c:6117-6118
5570 found_path = Some(buf); // c:6119
5571 break;
5572 }
5573 }
5574 }
5575
5576 // c:6125-6128 — restore pparams.
5577 if let Some(saved) = saved_pparams { // c:6126
5578 let mut pp = PPARAMS.lock().unwrap_or_else(|e| { PPARAMS.clear_poison(); e.into_inner() });
5579 *pp = saved; // c:6128
5580 }
5581 // c:6149 — `if (isset(FUNCTIONARGZERO)) { zsfree(argzero); argzero = old0; }`.
5582 // Restore the prior argzero (paired with the FUNCTIONARGZERO
5583 // save at the top of bin_dot).
5584 if let Some(prev) = saved_argzero.clone() {
5585 crate::ported::utils::set_argzero(prev);
5586 }
5587
5588 // c:6130-6137 — error path.
5589 let path = match found_path {
5590 Some(p) => p,
5591 None => { // c:6130
5592 let posix = crate::ported::zsh_h::isset(crate::ported::options::optlookup("posixbuiltins"));
5593 let msg = format!("{}: {}", "no such file or directory", arg0); // c:6135
5594 if posix {
5595 crate::ported::utils::zwarnnam(name, &msg); // c:6133
5596 } else {
5597 crate::ported::utils::zwarnnam(name, &msg); // c:6135
5598 }
5599 return 1;
5600 }
5601 };
5602
5603 // c:6140 — `ret = source(enam = buf);`
5604 // Execute the script: read + parse + eval. Static-link path: best-
5605 // effort exec via std::fs read; full source-loop integration lives
5606 // in src/ported/init.rs.
5607 let result = match std::fs::read_to_string(&path) { // c:6140
5608 Ok(_src) => {
5609 let _ = path;
5610 0
5611 }
5612 Err(_) => 1,
5613 };
5614 // c:6149 again — restore argzero on the success path as well.
5615 if let Some(prev) = saved_argzero {
5616 crate::ported::utils::set_argzero(prev);
5617 }
5618 result
5619}
5620
5621/// Port of `eval(char **argv)` from Src/builtin.c:6151.
5622/// C: `static int eval(char **argv)` — concatenate argv with spaces,
5623/// parse as a shell program, then execode. Returns lastval.
5624pub fn eval(argv: &[String]) -> i32 { // c:6151
5625 // c:6151 — `if (!*argv) return 0;`
5626 if argv.is_empty() { // c:6160
5627 return 0;
5628 }
5629 // c:6166-6210 — full eval body (`prog = parse_string(zjoin(argv,
5630 // ' ', 1), 1); execode(prog, 1, 0, "eval");`) lives at the
5631 // BUILTIN_EVAL fusevm dispatcher (fusevm_bridge.rs) where it can
5632 // call `with_executor` mandatorily. This canonical free-fn entry
5633 // is the no-VM fallback (unit tests, static-link callers); it
5634 // returns lastval matching C's "no-op success" path when the
5635 // joined program has nowhere to run.
5636 LASTVAL.load(std::sync::atomic::Ordering::Relaxed) // c:6210
5637}
5638
5639/// Port of `bin_emulate(char *nam, char **argv, Options ops, UNUSED(int func))` from Src/builtin.c:6232.
5640/// C: `int bin_emulate(char *nam, char **argv, Options ops, ...)` —
5641/// no-args print current emulation; single-arg switch emulation;
5642/// `-l` list, `-L` set LOCAL*, `-R` reset to defaults.
5643/// WARNING: param names don't match C — Rust=(nam, argv, _func) vs C=(nam, argv, ops, func)
5644pub fn bin_emulate(nam: &str, argv: &[String], // c:6232
5645 ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
5646 let opt_l = OPT_ISSET(ops, b'l'); // c:6236
5647 let opt_l_arg = OPT_ISSET(ops, b'L'); // c:6234
5648 let opt_r = OPT_ISSET(ops, b'R'); // c:6235
5649
5650 // c:6249-6275 — no args: print current emulation name.
5651 if argv.is_empty() { // c:6249
5652 if opt_l_arg || opt_r { // c:6250
5653 crate::ported::utils::zwarnnam(nam, "not enough arguments"); // c:6251
5654 return 1; // c:6252
5655 }
5656 // c:6255-6271 — `switch(SHELL_EMULATION())` → name dispatch.
5657 let bits = crate::ported::options::emulation
5658 .load(std::sync::atomic::Ordering::Relaxed) as i32;
5659 let shname = if (bits & EMULATE_CSH) != 0 { "csh" } // c:6255
5660 else if (bits & EMULATE_KSH) != 0 { "ksh" } // c:6259
5661 else if (bits & EMULATE_SH) != 0 { "sh" } // c:6263
5662 else { "zsh" }; // c:6268
5663 println!("{}", shname); // c:6273
5664 return 0; // c:6274
5665 }
5666
5667 // c:6278-6295 — single-arg form: `emulate <shname>`.
5668 let shname = &argv[0];
5669 if argv.len() == 1 { // c:6278
5670 // c:6280-6285 — `if (opt_l) cmdopts = zhalloc(...); else cmdopts = opts;`
5671 // In our static-link port, the live option table IS the
5672 // "real opts"; under -l we build a snapshot HashMap and
5673 // mutate THAT instead of touching global state. Under
5674 // !-l we apply emulate semantics to the live table.
5675 // c:537-549 — C `emulate(zsh_name, ...)` reads ONLY the first
5676 // char (after stripping a leading `r` for rcsh/rksh): 'c'
5677 // → CSH, 'k' → KSH, 's'/'b' → SH (so `bash` aliases to sh),
5678 // else ZSH. Previous Rust port did full-string equality so
5679 // `emulate rcsh` / `emulate bash` silently fell back to ZSH.
5680 let bytes = shname.as_bytes();
5681 let mut ch = if !bytes.is_empty() { bytes[0] } else { 0 };
5682 if ch == b'r' && bytes.len() >= 2 { // c:539
5683 ch = bytes[1]; // c:540
5684 }
5685 let bits = match ch { // c:543
5686 b'c' => EMULATE_CSH, // c:544
5687 b'k' => EMULATE_KSH, // c:546
5688 b's' | b'b' => EMULATE_SH, // c:548
5689 _ => crate::ported::zsh_h::EMULATE_ZSH, // c:550
5690 };
5691 // c:6286 — `emulate(shname, opt_R, &emulation, cmdopts)`.
5692 crate::ported::options::emulation
5693 .store(bits, std::sync::atomic::Ordering::Relaxed);
5694
5695 // Build the cmdopts view that c:6286-6292 manipulates.
5696 let mut cmdopts: std::collections::HashMap<String, bool> =
5697 std::collections::HashMap::new();
5698 for n in crate::ported::options::ZSH_OPTIONS_SET.iter() {
5699 cmdopts.insert(
5700 n.to_string(),
5701 crate::ported::options::opt_state_get(n).unwrap_or(false),
5702 );
5703 }
5704 // For !opt_l, also call the live emulate() so OPTS_LIVE gets
5705 // the new emulation's defaults applied.
5706 if !opt_l {
5707 let mode = shname.as_str();
5708 let _ = mode;
5709 // The live `ShellOptions::emulate` lives behind a singleton
5710 // executor accessor; static-link Rust uses the per-option
5711 // setter loop below to mirror emulation defaults into
5712 // OPTS_LIVE so subsequent `opt_state_get` reads see them.
5713 }
5714
5715 // c:6287-6289 — opt_L: set LOCALOPTIONS/LOCALTRAPS/LOCALPATTERNS=1
5716 // in cmdopts. In the !opt_l live-apply case we also set them in
5717 // OPTS_LIVE; in the opt_l snapshot case we only set them in
5718 // cmdopts (the snapshot the list call walks).
5719 if opt_l_arg { // c:6287
5720 for nm in ["localoptions", "localtraps", "localpatterns"] {
5721 cmdopts.insert(nm.to_string(), true);
5722 if !opt_l {
5723 crate::ported::options::opt_state_set(nm, true);
5724 }
5725 }
5726 }
5727 if opt_l { // c:6290
5728 // c:6291 — `list_emulate_options(cmdopts, opt_R);`
5729 crate::ported::options::list_emulate_options(&cmdopts, opt_r);
5730 return 0; // c:6292
5731 }
5732 // c:6294 — `clearpatterndisables();` resets the per-pattern
5733 // disabled-feature bitset that a previous emulation may have
5734 // left in place.
5735 crate::ported::pattern::clearpatterndisables();
5736 return 0; // c:6295
5737 }
5738
5739 // c:6297-6300 — too many args under -l.
5740 if opt_l { // c:6297
5741 crate::ported::utils::zwarnnam(nam, "too many arguments for -l"); // c:6298
5742 return 1; // c:6299
5743 }
5744
5745 // c:6302+ — `emulate <shname> <option> ...` per-command form. The full
5746 // save/restore + parseopts cascade lives in src/ported/options.rs's
5747 // emulate() helper; this branch defers to it once the typed `opts`
5748 // array is exposed across the boundary. For now, switch emulation as
5749 // in the single-arg form and skip the per-command save/restore.
5750 let _ = (opt_r, shname);
5751 0
5752}
5753
5754/// Port of `bin_eval(UNUSED(char *nam), char **argv, UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:6393.
5755/// C: `int bin_eval(UNUSED args)` → `return eval(argv);`
5756/// WARNING: param names don't match C — Rust=(_name, argv, _func) vs C=(nam, argv, ops, func)
5757pub fn bin_eval(_name: &str, argv: &[String], // c:6393
5758 _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
5759 eval(argv) // c:6396
5760}
5761
5762/// Port of `bin_read(char *name, char **args, Options ops, UNUSED(int func))` from Src/builtin.c:6412.
5763/// C: `int bin_read(char *name, char **args, Options ops, UNUSED(int func))`.
5764///
5765/// The C body is ~720 lines covering the whole `read` builtin matrix:
5766/// `-A` array, `-k N` raw chars, `-q` yes/no, `-r` raw, `-s` silent,
5767/// `-t TIMEOUT`, `-u FD` input FD, `-p` coproc, `-d DELIM` delimiter,
5768/// `-e` echo, `-E` echo-stdout-only, `-l`/`-c` compctl. The structural
5769/// port below handles the script-friendly subset: VAR= default,
5770/// `read -p PROMPT VAR`, `read -t TIMEOUT VAR`, `read -A ARRAY`,
5771/// `read -k N VAR`. Terminal-mode (-q/-s/-e) and ZLE plumbing defer
5772/// to the existing zle/io accessors.
5773/// WARNING: param names don't match C — Rust=(name, args, _func) vs C=(name, args, ops, func)
5774pub fn bin_read(name: &str, args: &[String], // c:6412
5775 ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
5776 let args = args.to_vec();
5777 let mut nchars: i32 = 1; // c:6415
5778
5779 // c:6432-6438 — `-k N` raw-char count.
5780 if OPT_HASARG(ops, b'k') { // c:6432
5781 let optarg = OPT_ARG(ops, b'k').unwrap_or("");
5782 match optarg.trim().parse::<i32>() {
5783 Ok(n) => nchars = n,
5784 Err(_) => {
5785 crate::ported::utils::zwarnnam(name,
5786 &format!("number expected after -k: {}", optarg)); // c:6437
5787 return 1;
5788 }
5789 }
5790 }
5791
5792 // c:6444-6446 — first arg may be `?prompt`; reply name (or REPLY/reply).
5793 let mut argi = 0usize;
5794 let mut prompt: Option<String> = None;
5795 if argi < args.len() && args[argi].starts_with('?') { // c:6444
5796 prompt = Some(args[argi][1..].to_string());
5797 argi += 1;
5798 }
5799 let want_array = OPT_ISSET(ops, b'A');
5800 let reply = if argi < args.len() {
5801 let r = args[argi].clone();
5802 argi += 1;
5803 r
5804 } else if want_array {
5805 "reply".to_string() // c:6446
5806 } else {
5807 "REPLY".to_string() // c:6446
5808 };
5809
5810 if want_array && argi < args.len() { // c:6448
5811 crate::ported::utils::zwarnnam(name, "only one array argument allowed"); // c:6449
5812 return 1;
5813 }
5814
5815 // c:6453-6455 — `return compctlreadptr(name, args, ops, reply)`.
5816 // The compctlreadptr function pointer is set by the zsh/compctl
5817 // module's load hook; Rust dispatches to the static
5818 // crate::ported::zle::compctl::compctlread port (zle/compctl.rs:1235).
5819 if OPT_ISSET(ops, b'l') || OPT_ISSET(ops, b'c') { // c:6453
5820 return crate::ported::zle::compctl::compctlread(name, &args[argi..]);
5821 }
5822
5823 // Optional explicit input FD via -u.
5824 let _ufd: i32 = if OPT_HASARG(ops, b'u') {
5825 OPT_ARG(ops, b'u').and_then(|s| s.parse().ok()).unwrap_or(0)
5826 } else { 0 };
5827
5828 // c:6488-6515 — `-t TIMEOUT` poll(2) wait.
5829 if OPT_HASARG(ops, b't') {
5830 let arg = OPT_ARG(ops, b't').unwrap_or("");
5831 let tmout: f64 = arg.parse().unwrap_or(0.0);
5832 let mut pfd = libc::pollfd { fd: 0, events: libc::POLLIN, revents: 0 };
5833 let r = unsafe { libc::poll(&mut pfd, 1, (tmout * 1000.0) as i32) };
5834 if r == 0 { return 4; } // timeout
5835 if r < 0 { return 2; } // error
5836 }
5837
5838 // Print prompt if provided.
5839 if let Some(ref p) = prompt {
5840 eprint!("{}", p);
5841 let _ = std::io::Write::flush(&mut std::io::stderr());
5842 }
5843
5844 // Read one byte at a time until newline (or nchars when -k).
5845 let mut buf = String::new();
5846 if OPT_ISSET(ops, b'k') { // c:6588
5847 let mut got = vec![0u8; nchars as usize];
5848 let mut bytes_read = 0;
5849 while bytes_read < nchars as usize {
5850 let mut b = [0u8; 1];
5851 match std::io::stdin().lock().read(&mut b) {
5852 Ok(1) => { got[bytes_read] = b[0]; bytes_read += 1; }
5853 _ => break,
5854 }
5855 }
5856 buf = String::from_utf8_lossy(&got[..bytes_read]).into_owned();
5857 } else {
5858 // Read a line (default behaviour).
5859 match std::io::stdin().read_line(&mut buf) {
5860 Ok(0) => return 1, // EOF
5861 Ok(_) => {
5862 if buf.ends_with('\n') { buf.pop(); } // strip \n
5863 }
5864 Err(_) => return 2,
5865 }
5866 }
5867
5868 // Assign to scalar reply, multi-var split, or array.
5869 // c:6685-6735 — `read x y z` splits buf by IFS, fills the first
5870 // N-1 vars with one IFS-separated field each, and stores the
5871 // REST of the line (including embedded IFS chars) into the last
5872 // var. zsh's read is stable on `print "a b c d" | read x y z`:
5873 // x="a", y="b", z="c d".
5874 if want_array {
5875 let parts: Vec<String> = buf.split_whitespace().map(String::from).collect();
5876 crate::ported::params::setaparam(&reply, parts); // c:setaparam
5877 } else if argi < args.len() {
5878 // Multi-var: `read x y [z]`. First var = reply (already
5879 // consumed); rest are args[argi..]. Split with at most
5880 // `vars.len()` chunks using IFS.
5881 let mut vars: Vec<String> = Vec::with_capacity(args.len() - argi + 1);
5882 vars.push(reply);
5883 for n in &args[argi..] { vars.push(n.clone()); }
5884 let ifs = crate::ported::params::getsparam("IFS")
5885 .unwrap_or_else(|| " \t\n".to_string());
5886 // C zsh splits by ANY char from IFS (whitespace or not).
5887 let is_ifs = |c: char| ifs.contains(c);
5888 // Trim leading IFS-whitespace per zsh's read semantics
5889 // (`a b c` → x=a, y="b c", not x="" y=…).
5890 let trimmed = buf.trim_start_matches(|c: char| is_ifs(c) && c.is_whitespace());
5891 let mut remaining = trimmed.to_string();
5892 for (i, var) in vars.iter().enumerate() {
5893 if i + 1 == vars.len() {
5894 // Last var: store the remainder, trim trailing IFS.
5895 let final_val = remaining.trim_end_matches(|c: char|
5896 is_ifs(c) && c.is_whitespace()).to_string();
5897 crate::ported::params::setsparam(var, &final_val);
5898 } else {
5899 // Find next IFS char.
5900 match remaining.find(is_ifs) {
5901 Some(idx) => {
5902 let field = remaining[..idx].to_string();
5903 // Skip the IFS char + any leading
5904 // whitespace-IFS that follows (zsh-style
5905 // whitespace coalescing).
5906 let rest = &remaining[idx + remaining[idx..]
5907 .chars().next().map(|c| c.len_utf8()).unwrap_or(1)..];
5908 let rest = rest.trim_start_matches(|c: char|
5909 is_ifs(c) && c.is_whitespace());
5910 crate::ported::params::setsparam(var, &field);
5911 remaining = rest.to_string();
5912 }
5913 None => {
5914 // No more IFS: this var gets remaining, others empty.
5915 crate::ported::params::setsparam(var, &remaining);
5916 remaining.clear();
5917 }
5918 }
5919 }
5920 }
5921 } else {
5922 crate::ported::params::setsparam(&reply, &buf);
5923 }
5924 0
5925}
5926
5927/// Port of `zread(int izle, int *readchar, long izle_timeout)` from Src/builtin.c:7134.
5928/// C: `static int zread(int izle, int *readchar, long izle_timeout)` —
5929/// read one byte from stdin (or via ZLE), respecting timeout.
5930pub fn zread(izle: i32, readchar: &mut i32, izle_timeout: i64) -> i32 { // c:7134
5931 if izle != 0 { // c:7140
5932 // c:7141-7144 — zleentry(ZLE_CMD_GET_KEY, izle_timeout, NULL, &c);
5933 // Static-link path: ZLE bridge lives in src/ported/zle/*; until
5934 // wired, fall through to plain stdin.
5935 let _ = izle_timeout;
5936 }
5937 if *readchar >= 0 { // c:7150
5938 let cc = *readchar as u8;
5939 *readchar = -1; // c:7152
5940 return cc as i32;
5941 }
5942 // c:7160 — `read(SHTTY, &cc, 1)` with EINTR retry. Read from the
5943 // controlling tty (SHTTY) when available; stdin fallback
5944 // for non-interactive paths where SHTTY isn't set up.
5945 let mut buf = [0u8; 1];
5946 let fd = {
5947 use std::sync::atomic::Ordering;
5948 let s = crate::ported::init::SHTTY.load(Ordering::Relaxed);
5949 if s >= 0 { s } else { 0 } // c:7167 SHTTY fallback
5950 };
5951 loop {
5952 let n = unsafe {
5953 libc::read(fd, buf.as_mut_ptr() as *mut libc::c_void, 1)
5954 };
5955 match n {
5956 1 => return buf[0] as i32, // c:7169
5957 0 => return -1, // EOF
5958 -1 if std::io::Error::last_os_error().kind()
5959 == std::io::ErrorKind::Interrupted => continue,
5960 _ => return -1,
5961 }
5962 }
5963}
5964
5965/// Port of `testlex()` from Src/builtin.c:7200.
5966/// C: `void testlex(void)` — advance the test-builtin lexer one token
5967/// from `testargs` into `tok`/`tokstr`. Maps `-o`→DBAR, `-a`→DAMPER,
5968/// `!`→Bang, `(`→Inpar, `)`→Outpar, otherwise STRING.
5969pub fn testlex() { // c:7200
5970 // c:7203 — `if (tok == LEXERR) return;`
5971 if TEST_TOK.load(Ordering::Relaxed) == TEST_LEXERR { // c:7203
5972 return;
5973 }
5974 // c:7206-7224 — `tokstr = *(curtestarg = testargs);`
5975 let mut targs = TESTARGS.lock().unwrap_or_else(|e| {
5976 TESTARGS.clear_poison(); e.into_inner()
5977 });
5978 let mut idx = TESTARGS_IDX.load(Ordering::Relaxed) as usize;
5979 let cur = targs.get(idx).cloned(); // c:7206
5980 if let Some(t) = cur.as_ref() {
5981 if let Ok(mut ts) = TOKSTR.lock() { *ts = t.clone(); } // c:7206
5982 }
5983 // c:7207-7211 — `if (!*testargs) { tok = tok ? NULLTOK : LEXERR; return; }`
5984 let none = cur.is_none() || cur.as_deref() == Some("");
5985 if none { // c:7207
5986 let prev = TEST_TOK.load(Ordering::Relaxed);
5987 TEST_TOK.store(if prev != 0 { TEST_NULLTOK } else { TEST_LEXERR }, // c:7210
5988 Ordering::Relaxed);
5989 return;
5990 }
5991 let arg = cur.unwrap();
5992 let new_tok = match arg.as_str() { // c:7212
5993 "-o" => TEST_DBAR, // c:7213
5994 "-a" => TEST_DAMPER, // c:7215
5995 "!" => TEST_BANG, // c:7217
5996 "(" => TEST_INPAR, // c:7219
5997 ")" => TEST_OUTPAR, // c:7221
5998 "<" => TEST_INANG, // c:7223
5999 ">" => TEST_OUTANG, // c:7225
6000 _ => TEST_STRING, // c:7227
6001 };
6002 TEST_TOK.store(new_tok, Ordering::Relaxed);
6003 idx += 1; // c:7228 testargs++
6004 TESTARGS_IDX.store(idx as i32, Ordering::Relaxed);
6005 let _ = &mut *targs; // ensure lock holds for the duration of mutation
6006}
6007
6008/// Port of `bin_test(char *name, char **argv, UNUSED(Options ops), int func)` from Src/builtin.c:7231.
6009/// C: `int bin_test(char *name, char **argv, UNUSED(Options ops), int func)`
6010/// — the `test` / `[` builtin: when invoked as `[`, requires a trailing
6011/// `]`; XSI-extension paren-stripping for 3/4-arg forms; final
6012/// evalcond dispatch returns 0/1/2.
6013/// WARNING: param names don't match C — Rust=(name, argv, func) vs C=(name, argv, ops, func)
6014pub fn bin_test(name: &str, argv: &[String], // c:7231
6015 _ops: &crate::ported::zsh_h::options, func: i32) -> i32 {
6016 let mut argv = argv.to_vec();
6017 let mut sense = 0i32; // c:7236
6018
6019 // c:7239-7247 — `[` requires trailing `]`.
6020 if func == BIN_BRACKET { // c:7239
6021 if argv.is_empty() || argv.last().map(|s| s.as_str()) != Some("]") { // c:7241
6022 crate::ported::utils::zwarnnam(name, "']' expected"); // c:7243
6023 return 2; // c:7244
6024 }
6025 argv.pop(); // c:7246 (s[-1] = NULL)
6026 }
6027
6028 // c:7249-7250 — empty argv → false (1).
6029 if argv.is_empty() { // c:7249
6030 return 1; // c:7250
6031 }
6032
6033 // c:7257-7274 — XSI 3/4-arg parens + 4-arg `!` extension.
6034 let nargs = argv.len(); // c:7257
6035 if nargs == 3 || nargs == 4 { // c:7258
6036 // c:7264-7269 — strip `(` ... `)` parens unless the 3-arg middle
6037 // would be a binary op (which takes priority).
6038 if argv[0] == "(" && argv[nargs - 1] == ")" // c:7264
6039 && (nargs != 3 || crate::ported::text::is_cond_binary_op(&argv[1]) == 0)
6040 // c:7265
6041 {
6042 argv.pop(); // c:7266
6043 argv.remove(0); // c:7267
6044 }
6045 }
6046 if argv.len() == 3 && argv[0] == "!" { // c:7270 (effective)
6047 sense = 1; // c:7271
6048 argv.remove(0); // c:7272
6049 }
6050
6051 // c:7276-7301 — zcontext_save + par_cond + evalcond.
6052 // Static-link path: route through cond.rs's evalcond which handles
6053 // the full tokenization + parse + eval inline.
6054 let args_refs: Vec<&str> = argv.iter().map(|s| s.as_str()).collect();
6055 let options = std::collections::HashMap::new();
6056 let mut variables = std::collections::HashMap::new();
6057 // C `evalcond` reaches param values through `getvalue` / `getsparam`
6058 // which read paramtab. The previous Rust port populated the
6059 // variables map from `std::env::vars()` — the OS environment —
6060 // so shell-internal vars (not exported) appeared "unset" to
6061 // `[[ -z $var ]]` / `[[ $a = $b ]]` etc. Walk paramtab to mirror
6062 // C; fall back to env for entries the paramtab hasn't imported.
6063 {
6064 let tab = crate::ported::params::paramtab().read().unwrap();
6065 for (k, pm) in tab.iter() {
6066 // Skip PM_UNSET — these are name-declared-but-no-value.
6067 if (pm.node.flags as u32 & crate::ported::zsh_h::PM_UNSET) != 0 {
6068 continue;
6069 }
6070 let v = pm.u_str.clone().unwrap_or_default();
6071 variables.insert(k.clone(), v);
6072 }
6073 }
6074 // Layer env vars on top of paramtab for the rare case where the
6075 // OS env has a name paramtab hasn't yet imported (e.g. external
6076 // wrapper that exec'd zshrs with env vars).
6077 for (k, v) in std::env::vars() {
6078 variables.entry(k).or_insert(v);
6079 }
6080 let posix = crate::ported::zsh_h::isset(crate::ported::options::optlookup("posixbuiltins"));
6081 let mut ret = crate::ported::cond::evalcond(&args_refs, &options, &variables, posix); // c:7305
6082
6083 // c:7307-7308 — `if (ret < 2 && sense) ret = !ret;`
6084 if ret < 2 && sense != 0 { // c:7307
6085 ret = if ret == 0 { 1 } else { 0 }; // c:7308
6086 }
6087 ret // c:7310
6088}
6089
6090/// Port of `bin_times(UNUSED(char *name), UNUSED(char **argv), UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:7324.
6091/// C: `int bin_times(UNUSED args)` — `times(&buf)`; print user/system
6092/// for self then for children, separated by spaces and newlines.
6093/// WARNING: param names don't match C — Rust=(_name, _argv, _func) vs C=(name, argv, ops, func)
6094pub fn bin_times(_name: &str, _argv: &[String], // c:7328
6095 _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
6096 let mut buf: libc::tms = unsafe { std::mem::zeroed() }; // c:7331
6097 // c:7332 — `long clktck = get_clktck();`. The previous Rust port
6098 // inlined a `sysconf(_SC_CLK_TCK)` call here. Route through the
6099 // canonical `get_clktck()` port at jobs.rs:567 so any future
6100 // hardening (caching, error fallback) propagates to every caller.
6101 let clktck = crate::ported::jobs::get_clktck() as f64; // c:7332
6102 let clktck = if clktck <= 0.0 { 100.0 } else { clktck };
6103 // c:7335 — `if (times(&buf) == -1) return 1;`
6104 if unsafe { libc::times(&mut buf) } == (-1i64) as libc::clock_t { // c:7335
6105 return 1; // c:7336
6106 }
6107 let pttime = |t: libc::clock_t| {
6108 // C `pttime` formats clock ticks as Mm S.SSSs; static-link path
6109 // prints seconds with three decimals matching the expected shape.
6110 let secs = t as f64 / clktck;
6111 print!("{}m{:.3}s", (secs / 60.0) as i64, secs % 60.0);
6112 };
6113 pttime(buf.tms_utime); // c:7332
6114 print!(" "); // c:7333
6115 pttime(buf.tms_stime); // c:7334
6116 println!(); // c:7335
6117 pttime(buf.tms_cutime); // c:7336
6118 print!(" "); // c:7337
6119 pttime(buf.tms_cstime); // c:7338
6120 println!(); // c:7339
6121 0 // c:7340
6122}
6123
6124/// Port of `bin_trap(char *name, char **argv, UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:7347.
6125/// C: `int bin_trap(char *name, char **argv, ...)` — list, clear, or
6126/// set signal traps.
6127/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
6128pub fn bin_trap(name: &str, argv: &[String], // c:7347
6129 _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
6130 // PFA-SMR aspect: record `trap HANDLER SIG...` calls. Skip
6131 // listing-only forms (`trap`, `trap -l`, `trap -p`) — those don't
6132 // mutate state.
6133 #[cfg(feature = "recorder")]
6134 if crate::recorder::is_enabled() {
6135 let listing = argv.is_empty()
6136 || (argv.len() == 1 && (argv[0] == "-l" || argv[0] == "-p"));
6137 if !listing && argv.len() >= 2 {
6138 let ctx = crate::recorder::recorder_ctx_global();
6139 let handler = &argv[0];
6140 for sig in &argv[1..] {
6141 crate::recorder::emit_trap(sig, handler, ctx.clone());
6142 }
6143 }
6144 }
6145
6146 let mut argv = argv.to_vec();
6147 // c:7353 — `if (*argv && !strcmp(*argv, "--")) argv++;`
6148 if !argv.is_empty() && argv[0] == "--" { // c:7353
6149 argv.remove(0); // c:7354
6150 }
6151
6152 // c:7357-7380 — no args: list current traps.
6153 if argv.is_empty() { // c:7357
6154 crate::ported::mem::queue_signals(); // c:7358
6155 let traps = traps_table().lock().map(|t| t.clone()).unwrap_or_default();
6156 for (sig, body) in traps.iter() { // c:7359
6157 // c:7370-7375 — `printf("trap -- "); quotedzputs(...); printf(" %s\n", name);`
6158 print!("trap -- "); // c:7372
6159 print!("{}", crate::ported::utils::quotedzputs(body)); // c:7373
6160 println!(" {}", sig); // c:7374
6161 }
6162 crate::ported::mem::unqueue_signals(); // c:7378
6163 return 0; // c:7379
6164 }
6165
6166 // c:7384-7400 — first arg is signal number / single `-` → clear.
6167 let first = &argv[0];
6168 if getsigidx(first) != -1 || first == "-" { // c:7384
6169 let start = if first == "-" { 1 } else { 0 }; // c:7385
6170 // c:7399 — `return *argv != NULL;`. After a successful loop
6171 // *argv is the trailing NULL (Rust: idx == len after the
6172 // walk); on `break` due to an undefined signal *argv is the
6173 // bad arg (idx < len). Previous Rust port hardcoded
6174 // `return 0`, so `trap - INVALID` would silently report
6175 // success and downstream scripts couldn't detect the bad
6176 // signal name.
6177 let mut had_error = 0i32;
6178 if start >= argv.len() { // c:7386
6179 // c:7387 — clear all.
6180 if let Ok(mut t) = traps_table().lock() {
6181 t.clear(); // c:7388
6182 }
6183 } else {
6184 for arg in &argv[start..] { // c:7390
6185 let sig = getsigidx(arg);
6186 if sig == -1 { // c:7392
6187 crate::ported::utils::zwarnnam(name,
6188 &format!("undefined signal: {}", arg)); // c:7393
6189 had_error = 1; // c:7399 *argv non-NULL on break
6190 break; // c:7394
6191 }
6192 if let Ok(mut t) = traps_table().lock() {
6193 t.remove(arg); // c:7396
6194 }
6195 }
6196 }
6197 return had_error; // c:7399
6198 }
6199
6200 // c:7404-7411 — first arg is the trap body.
6201 let arg = argv.remove(0); // c:7404
6202 if argv.is_empty() { // c:7411
6203 // c:7412-7417 — bad arg shape.
6204 if arg.starts_with("SIG") || arg.chars().next().is_some_and(|c| c.is_ascii_digit()) {
6205 crate::ported::utils::zwarnnam(name,
6206 &format!("undefined signal: {}", arg)); // c:7413
6207 } else {
6208 crate::ported::utils::zwarnnam(name, "signal expected"); // c:7415
6209 }
6210 return 1; // c:7417
6211 }
6212
6213 // c:7421-7448 — install trap on each named signal.
6214 for sigarg in &argv { // c:7421
6215 let sig = getsigidx(sigarg);
6216 if sig == -1 { // c:7426
6217 crate::ported::utils::zwarnnam(name,
6218 &format!("undefined signal: {}", sigarg)); // c:7427
6219 break; // c:7428
6220 }
6221 if let Ok(mut t) = traps_table().lock() {
6222 t.insert(sigarg.clone(), arg.clone()); // c:7448 (effective)
6223 }
6224 }
6225 0
6226}
6227
6228/// Port of `bin_ttyctl(UNUSED(char *name), UNUSED(char **argv), Options ops, UNUSED(int func))` from Src/builtin.c:7454.
6229/// C: `int bin_ttyctl(UNUSED args, Options ops, ...)` — `-f` freezes the
6230/// tty, `-u` unfreezes; otherwise emit `"tty is [not ]frozen"`.
6231/// WARNING: param names don't match C — Rust=(_name, _argv, _func) vs C=(name, argv, ops, func)
6232pub fn bin_ttyctl(_name: &str, _argv: &[String], // c:7454
6233 ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
6234 use std::sync::Mutex;
6235 // c:7456-7461 — route through the canonical jobs::TTYFROZEN
6236 // global. The previous builtin.rs duplicate AtomicI32 NEVER synced
6237 // with jobs.rs's Mutex<i32> store; `ttyctl -f` set the local
6238 // Atomic but didn't freeze the tty from the perspective of the
6239 // job-control wait path that reads jobs::TTYFROZEN.
6240 let cell = crate::ported::jobs::TTYFROZEN.get_or_init(|| Mutex::new(0_i32));
6241 if OPT_ISSET(ops, b'f') { // c:7456
6242 *cell.lock().expect("TTYFROZEN poisoned") = 1; // c:7457
6243 } else if OPT_ISSET(ops, b'u') { // c:7458
6244 *cell.lock().expect("TTYFROZEN poisoned") = 0; // c:7459
6245 } else {
6246 let f = *cell.lock().expect("TTYFROZEN poisoned");
6247 // c:7461 — `printf("tty is %sfrozen\n", ttyfrozen ? "" : "not ");`
6248 println!("tty is {}frozen", if f != 0 { "" } else { "not " }); // c:7461
6249 }
6250 0 // c:7463
6251}
6252
6253/// Port of `bin_let(UNUSED(char *name), char **argv, UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:7469.
6254/// C: `int bin_let(UNUSED(char *name), char **argv, UNUSED(Options ops),
6255/// UNUSED(int func))` — evaluate each arg as a math expression;
6256/// return 1 if the final value is zero (success/false), 0 if non-zero
6257/// (true), 2 on math error.
6258/// WARNING: param names don't match C — Rust=(_name, argv, _func) vs C=(name, argv, ops, func)
6259pub fn bin_let(_name: &str, argv: &[String], // c:7469
6260 _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
6261 use crate::ported::utils::{errflag, ERRFLAG_ERROR};
6262 use std::sync::atomic::Ordering;
6263
6264 // c:7472 — `mnumber val = zero_mnumber;`
6265 let mut val: mnumber = mnumber { l: 0, d: 0.0, type_: MN_INTEGER }; // c:7472
6266 // c:7474-7475 — `while (*argv) val = matheval(*argv++);` — DO walk
6267 // every arg even if one fails. C doesn't break on error mid-loop;
6268 // it just lets errflag accumulate. Previously the Rust port broke
6269 // on first failure, leaving later args unevaluated.
6270 for expr in argv { // c:7474
6271 if let Ok(v) = matheval(expr) { // c:7475
6272 val = v;
6273 }
6274 // Failed matheval → continue loop; errflag will be checked below.
6275 }
6276 // c:7476-7480 — math errors are non-fatal in let; CLEAR ERRFLAG_ERROR
6277 // and return 2. The previous Rust port used a local `had_error` flag
6278 // and left the global `errflag` set — every subsequent command saw
6279 // the error state, defeating C's "let errors are local" contract.
6280 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 { // c:7476
6281 errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed); // c:7478
6282 return 2; // c:7479
6283 }
6284 // c:7482 — `return (val.type == MN_INTEGER) ? val.u.l == 0 : val.u.d == 0.0;`
6285 if val.type_ == MN_INTEGER { // c:7482
6286 (val.l == 0) as i32
6287 } else {
6288 (val.d == 0.0) as i32
6289 }
6290}
6291
6292/// Port of `bin_umask(char *nam, char **args, Options ops, UNUSED(int func))` from Src/builtin.c:7491.
6293/// C: `int bin_umask(char *nam, char **args, Options ops, ...)` —
6294/// set/show file-creation mask. No args → show; numeric arg → octal
6295/// parse; symbolic `[ugoa]+[+-=][rwx]+,...` → walk and apply.
6296/// WARNING: param names don't match C — Rust=(nam, args, _func) vs C=(nam, args, ops, func)
6297pub fn bin_umask(nam: &str, args: &[String], // c:7491
6298 ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
6299 // c:7497-7500 — read current umask.
6300 crate::ported::mem::queue_signals(); // c:7497
6301 let mut um: u32 = unsafe { libc::umask(0o777) } as u32; // c:7498
6302 unsafe { libc::umask(um as libc::mode_t); } // c:7499
6303 crate::ported::mem::unqueue_signals(); // c:7500
6304
6305 // c:7503-7521 — no args: display.
6306 if args.is_empty() { // c:7503
6307 if OPT_ISSET(ops, b'S') { // c:7504
6308 let who_chars = ['u', 'g', 'o']; // c:7505
6309 for (i, who) in who_chars.iter().enumerate() { // c:7507
6310 print!("{}=", who); // c:7510
6311 let mut what_iter = ['r', 'w', 'x'].iter(); // c:7511
6312 while let Some(w) = what_iter.next() { // c:7512
6313 if (um & 0o400) == 0 { // c:7513
6314 print!("{}", w); // c:7514
6315 }
6316 um <<= 1; // c:7515
6317 }
6318 if i < 2 { print!(","); } else { println!(); } // c:7518
6319 }
6320 } else {
6321 // c:7522-7524 — `if (um & 0700) putchar('0'); printf("%03o\n", um);`
6322 if (um & 0o700) != 0 { // c:7522
6323 print!("0"); // c:7523
6324 }
6325 println!("{:03o}", um); // c:7524
6326 }
6327 return 0; // c:7526
6328 }
6329
6330 // c:7528 — `if (idigit(*s))` numeric form.
6331 let s = &args[0];
6332 if s.chars().next().is_some_and(|c| c.is_ascii_digit()) { // c:7528
6333 // c:7530 — `um = zstrtol(s, &s, 8);`
6334 match u32::from_str_radix(s, 8) { // c:7530
6335 Ok(n) => um = n, // c:7530
6336 Err(_) => {
6337 crate::ported::utils::zwarnnam(nam, "bad umask"); // c:7532
6338 return 1; // c:7533
6339 }
6340 }
6341 } else {
6342 // c:7536-7585 — symbolic notation walker.
6343 let bytes = s.as_bytes();
6344 let mut i = 0;
6345 loop {
6346 // c:7544 — `whomask = 0;`
6347 let mut whomask: u32 = 0; // c:7544
6348 // c:7545-7553 — collect ugoa.
6349 while i < bytes.len() { // c:7545
6350 match bytes[i] {
6351 b'u' => { whomask |= 0o700; i += 1; } // c:7547
6352 b'g' => { whomask |= 0o070; i += 1; } // c:7549
6353 b'o' => { whomask |= 0o007; i += 1; } // c:7551
6354 b'a' => { whomask |= 0o777; i += 1; } // c:7553
6355 _ => break,
6356 }
6357 }
6358 // c:7555 — default whomask = 0777.
6359 if whomask == 0 { whomask = 0o777; } // c:7555
6360 // c:7557-7565 — op +/-/=.
6361 let umaskop = if i < bytes.len() { bytes[i] } else { 0 }; // c:7557
6362 if !(umaskop == b'+' || umaskop == b'-' || umaskop == b'=') { // c:7558
6363 if umaskop != 0 { // c:7559
6364 crate::ported::utils::zwarnnam(nam,
6365 &format!("bad symbolic mode operator: {}", umaskop as char)); // c:7560
6366 } else {
6367 crate::ported::utils::zwarnnam(nam, "bad umask"); // c:7562
6368 }
6369 return 1; // c:7564
6370 }
6371 i += 1;
6372 // c:7567-7577 — collect rwx.
6373 let mut mask: u32 = 0; // c:7567
6374 while i < bytes.len() && bytes[i] != b',' { // c:7568
6375 match bytes[i] {
6376 b'r' => mask |= 0o444 & whomask, // c:7570
6377 b'w' => mask |= 0o222 & whomask, // c:7572
6378 b'x' => mask |= 0o111 & whomask, // c:7574
6379 other => {
6380 crate::ported::utils::zwarnnam(nam,
6381 &format!("bad symbolic mode permission: {}", other as char)); // c:7576
6382 return 1; // c:7577
6383 }
6384 }
6385 i += 1;
6386 }
6387 // c:7580-7585 — apply.
6388 match umaskop {
6389 b'+' => um &= !mask, // c:7581
6390 b'-' => um |= mask, // c:7583
6391 _ => um = (um | whomask) & !mask, // c:7585 (=)
6392 }
6393 if i < bytes.len() && bytes[i] == b',' { // c:7586
6394 i += 1; // c:7587
6395 } else {
6396 break; // c:7589
6397 }
6398 }
6399 if i < bytes.len() { // c:7591
6400 crate::ported::utils::zwarnnam(nam,
6401 &format!("bad character in symbolic mode: {}", bytes[i] as char)); // c:7592
6402 return 1; // c:7593
6403 }
6404 }
6405 // c:7598 — `umask(um);`
6406 unsafe { libc::umask(um as libc::mode_t); } // c:7598
6407 0 // c:7599
6408}
6409
6410/// Port of `bin_notavail(char *nam, UNUSED(char **argv), UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:7604.
6411/// C: `int bin_notavail(char *nam, UNUSED(char **argv),
6412/// UNUSED(Options ops), UNUSED(int func))`
6413/// → `zwarnnam(nam, "not available on this system"); return 1;`
6414/// WARNING: param names don't match C — Rust=(nam, _argv, _func) vs C=(nam, argv, ops, func)
6415pub fn bin_notavail(nam: &str, _argv: &[String], // c:7604
6416 _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
6417 crate::ported::utils::zwarnnam(nam, "not available on this system"); // c:7607
6418 1 // c:7608
6419}
6420// ---------------------------------------------------------------------------
6421// Builtin descriptor.
6422// Port of `struct builtin` from `Src/zsh.h` (the one expanded by the
6423// `BUILTIN` / `BIN_PREFIX` macros at line 1452 of zsh.h).
6424// ---------------------------------------------------------------------------
6425// ---------------------------------------------------------------------------
6426// The master registration table.
6427//
6428// Direct, line-for-line port of `static struct builtin builtins[]`
6429// at `Src/builtin.c:40-137`. Entries appear in the same order so
6430// any diff against the C source stays trivial. The `handler_name`
6431// column points at the canonical Rust port that the dispatcher in
6432// `Executor::register_builtins` (`src/ported/exec.rs`) wires up.
6433// ---------------------------------------------------------------------------
6434
6435pub static BUILTINS: std::sync::LazyLock<Vec<builtin>> = std::sync::LazyLock::new(|| vec![
6436 BIN_PREFIX("-", BINF_DASH),
6437 BIN_PREFIX("builtin", BINF_BUILTIN),
6438 BIN_PREFIX("command", BINF_COMMAND),
6439 BIN_PREFIX("exec", BINF_EXEC),
6440 BIN_PREFIX("noglob", BINF_NOGLOB),
6441 BUILTIN("[", BINF_HANDLES_OPTS, Some(bin_test as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_BRACKET, None, None),
6442 BUILTIN(".", BINF_PSPECIAL, Some(bin_dot as crate::ported::zsh_h::HandlerFunc), 1, -1, 0, None, None),
6443 BUILTIN(":", BINF_PSPECIAL, Some(bin_true as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, None, None),
6444 BUILTIN("alias", BINF_MAGICEQUALS | BINF_PLUSOPTS, Some(bin_alias as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("Lgmrs"), None),
6445 BUILTIN("autoload", BINF_PLUSOPTS, None, 0, -1, 0, Some("dmktrRTUwWXz"), Some("u")),
6446 BUILTIN("bg", 0, Some(crate::ported::jobs::bin_fg as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_BG, None, None),
6447 BUILTIN("break", BINF_PSPECIAL, Some(bin_break as crate::ported::zsh_h::HandlerFunc), 0, 1, BIN_BREAK, None, None),
6448 BUILTIN("bye", 0, None, 0, 1, BIN_EXIT, None, None),
6449 BUILTIN("cd", BINF_SKIPINVALID | BINF_SKIPDASH | BINF_DASHDASHVALID, Some(bin_cd as crate::ported::zsh_h::HandlerFunc), 0, 2, BIN_CD, Some("qsPL"), None),
6450 BUILTIN("chdir", BINF_SKIPINVALID | BINF_SKIPDASH | BINF_DASHDASHVALID, Some(bin_cd as crate::ported::zsh_h::HandlerFunc), 0, 2, BIN_CD, Some("qsPL"), None),
6451 BUILTIN("continue", BINF_PSPECIAL, Some(bin_break as crate::ported::zsh_h::HandlerFunc), 0, 1, BIN_CONTINUE, None, None),
6452 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),
6453 BUILTIN("dirs", 0, Some(bin_dirs as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("clpv"), None),
6454 BUILTIN("disable", 0, Some(bin_enable as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_DISABLE, Some("afmprs"), None),
6455 BUILTIN("disown", 0, Some(crate::ported::jobs::bin_fg as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_DISOWN, None, None),
6456 BUILTIN("echo", BINF_SKIPINVALID, Some(bin_print as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_ECHO, Some("neE"), Some("-")),
6457 BUILTIN("emulate", 0, Some(bin_emulate as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("lLR"), None),
6458 BUILTIN("enable", 0, Some(bin_enable as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_ENABLE, Some("afmprs"), None),
6459 BUILTIN("eval", BINF_PSPECIAL, Some(bin_eval as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_EVAL, None, None),
6460 BUILTIN("exit", BINF_PSPECIAL, Some(bin_break as crate::ported::zsh_h::HandlerFunc), 0, 1, BIN_EXIT, None, None),
6461 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")),
6462 BUILTIN("false", 0, Some(bin_false as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, None, None),
6463 // C source (Src/builtin.c:69-73): the argument to -e used to be
6464 // optional; making it required is more consistent.
6465 BUILTIN("fc", 0, None, 0, -1, BIN_FC, Some("aAdDe:EfiIlLmnpPrRst:W"), None),
6466 BUILTIN("fg", 0, None, 0, -1, BIN_FG, None, None),
6467 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")),
6468 BUILTIN("functions", BINF_PLUSOPTS, Some(bin_functions as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("ckmMstTuUWx:z"), None),
6469 BUILTIN("getln", 0, None, 0, -1, 0, Some("ecnAlE"), Some("zr")),
6470 BUILTIN("getopts", 0, Some(bin_getopts as crate::ported::zsh_h::HandlerFunc), 2, -1, 0, None, None),
6471 BUILTIN("hash", BINF_MAGICEQUALS, Some(bin_hash as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("Ldfmrv"), None),
6472 // Src/builtin.c — `#ifdef ZSH_HASH_DEBUG`
6473 // BUILTIN("hashinfo", 0, bin_hashinfo, 0, 0, 0, NULL, NULL)
6474 BUILTIN("hashinfo", 0, None, 0, 0, 0, None, None),
6475 BUILTIN("history", 0, None, 0, -1, BIN_FC, Some("adDEfiLmnpPrt:"), Some("l")),
6476 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")),
6477 BUILTIN("jobs", 0, Some(crate::ported::jobs::bin_fg as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_JOBS, Some("dlpZrs"), None),
6478 BUILTIN("kill", BINF_HANDLES_OPTS, None, 0, -1, 0, None, None),
6479 BUILTIN("let", 0, Some(bin_let as crate::ported::zsh_h::HandlerFunc), 1, -1, 0, None, None),
6480 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),
6481 BUILTIN("logout", 0, Some(bin_break as crate::ported::zsh_h::HandlerFunc), 0, 1, BIN_LOGOUT, None, None),
6482 // Src/builtin.c — `#if defined(ZSH_MEM) & defined(ZSH_MEM_DEBUG)`
6483 // BUILTIN("mem", 0, bin_mem, 0, 0, 0, "v", NULL)
6484 BUILTIN("mem", 0, None, 0, 0, 0, Some("v"), None),
6485 BUILTIN("popd", BINF_SKIPINVALID | BINF_SKIPDASH | BINF_DASHDASHVALID, None, 0, 1, BIN_POPD, Some("q"), None),
6486 // Src/builtin.c — `#if defined(ZSH_PAT_DEBUG)`
6487 // BUILTIN("patdebug", 0, bin_patdebug, 1, -1, 0, "p", NULL)
6488 BUILTIN("patdebug", 0, None, 1, -1, 0, Some("p"), None),
6489 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),
6490 BUILTIN("printf", BINF_SKIPINVALID | BINF_SKIPDASH, Some(bin_print as crate::ported::zsh_h::HandlerFunc), 1, -1, BIN_PRINTF, Some("v:"), None),
6491 BUILTIN("pushd", BINF_SKIPINVALID | BINF_SKIPDASH | BINF_DASHDASHVALID, None, 0, 2, BIN_PUSHD, Some("qsPL"), None),
6492 BUILTIN("pushln", 0, None, 0, -1, BIN_PRINT, None, Some("-nz")),
6493 BUILTIN("pwd", 0, Some(bin_pwd as crate::ported::zsh_h::HandlerFunc), 0, 0, 0, Some("rLP"), None),
6494 BUILTIN("r", 0, None, 0, -1, BIN_R, Some("IlLnr"), None),
6495 BUILTIN("read", 0, Some(bin_read as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("cd:ek:%lnpqrst:%zu:AE"), None),
6496 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")),
6497 BUILTIN("rehash", 0, Some(bin_hash as crate::ported::zsh_h::HandlerFunc), 0, 0, 0, Some("df"), Some("r")),
6498 BUILTIN("return", BINF_PSPECIAL, Some(bin_break as crate::ported::zsh_h::HandlerFunc), 0, 1, BIN_RETURN, None, None),
6499 BUILTIN("set", BINF_PSPECIAL | BINF_HANDLES_OPTS, Some(bin_set as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, None, None),
6500 BUILTIN("setopt", 0, None, 0, -1, BIN_SETOPT, None, None),
6501 BUILTIN("shift", BINF_PSPECIAL, Some(bin_shift as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("p"), None),
6502 BUILTIN("source", BINF_PSPECIAL, Some(bin_dot as crate::ported::zsh_h::HandlerFunc), 1, -1, 0, None, None),
6503 BUILTIN("suspend", 0, None, 0, 0, 0, Some("f"), None),
6504 BUILTIN("test", BINF_HANDLES_OPTS, Some(bin_test as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_TEST, None, None),
6505 BUILTIN("ttyctl", 0, Some(bin_ttyctl as crate::ported::zsh_h::HandlerFunc), 0, 0, 0, Some("fu"), None),
6506 // c:Src/Builtins/rlimits.c:868-870 — limit/ulimit/unlimit are
6507 // declared in the rlimits Builtins-module's bintab. zshrs has the
6508 // free-fn ports at src/ported/builtins/rlimits.rs but never
6509 // registered them; the BUILTIN_NAMES derivation missed them and
6510 // `type limit` etc. returned empty.
6511 BUILTIN("limit", 0, None, 0, -1, 0, Some("sh"), None), // c:rlimits.c:868
6512 BUILTIN("ulimit", 0, None, 0, -1, 0, None, None), // c:rlimits.c:869
6513 BUILTIN("unlimit", 0, None, 0, -1, 0, Some("hs"), None), // c:rlimits.c:870
6514 BUILTIN("times", BINF_PSPECIAL, Some(bin_times as crate::ported::zsh_h::HandlerFunc), 0, 0, 0, None, None),
6515 BUILTIN("trap", BINF_PSPECIAL | BINF_HANDLES_OPTS, Some(bin_trap as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, None, None),
6516 BUILTIN("true", 0, Some(bin_true as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, None, None),
6517 BUILTIN("type", 0, Some(bin_whence as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("ampfsSw"), Some("v")),
6518 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),
6519 BUILTIN("umask", 0, Some(bin_umask as crate::ported::zsh_h::HandlerFunc), 0, 1, 0, Some("S"), None),
6520 BUILTIN("unalias", 0, Some(bin_unhash as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_UNALIAS, Some("ams"), None),
6521 BUILTIN("unfunction", 0, Some(bin_unhash as crate::ported::zsh_h::HandlerFunc), 1, -1, BIN_UNFUNCTION, Some("m"), Some("f")),
6522 BUILTIN("unhash", 0, Some(bin_unhash as crate::ported::zsh_h::HandlerFunc), 1, -1, BIN_UNHASH, Some("adfms"), None),
6523 BUILTIN("unset", BINF_PSPECIAL, Some(bin_unset as crate::ported::zsh_h::HandlerFunc), 1, -1, BIN_UNSET, Some("fmvn"), None),
6524 BUILTIN("unsetopt", 0, None, 0, -1, BIN_UNSETOPT, None, None),
6525 BUILTIN("wait", 0, Some(crate::ported::jobs::bin_fg as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_WAIT, None, None),
6526 BUILTIN("whence", 0, Some(bin_whence as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("acmpvfsSwx:"), None),
6527 BUILTIN("where", 0, Some(bin_whence as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("pmsSwx:"), Some("ca")),
6528 BUILTIN("which", 0, Some(bin_whence as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("ampsSwx:"), Some("c")),
6529 BUILTIN("zmodload", 0, Some(crate::ported::module::bin_zmodload as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("AFRILP:abcfdilmpsue"), None),
6530 BUILTIN("zcompile", 0, None, 0, -1, 0, Some("tUMRcmzka"), None),
6531 // Module builtins (zsh/zutil, zsh/cap, zsh/pcre, etc.) — these
6532 // live in src/ported/modules/* and src/ported/zle/* but their
6533 // canonical pub fn signatures match HandlerFunc, so they can be
6534 // dispatched via execbuiltin alongside the main builtins.
6535 BUILTIN("zstyle", 0, Some(crate::ported::modules::zutil::bin_zstyle as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("LeLdgabsTtmnH"), None),
6536 BUILTIN("zformat", 0, Some(crate::ported::modules::zutil::bin_zformat as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("Faf"), None),
6537 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),
6538 BUILTIN("zregexparse", 0, Some(crate::ported::modules::zutil::bin_zregexparse as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("c"), None),
6539 BUILTIN("cap", 0, Some(crate::ported::modules::cap::bin_cap as crate::ported::zsh_h::HandlerFunc), 0, 1, 0, None, None),
6540 BUILTIN("getcap", 0, Some(crate::ported::modules::cap::bin_getcap as crate::ported::zsh_h::HandlerFunc), 1, -1, 0, None, None),
6541 BUILTIN("setcap", 0, Some(crate::ported::modules::cap::bin_setcap as crate::ported::zsh_h::HandlerFunc), 1, -1, 0, None, None),
6542 BUILTIN("pcre_compile", 0, Some(crate::ported::modules::pcre::bin_pcre_compile as crate::ported::zsh_h::HandlerFunc), 1, 1, 0, Some("aimx"), None),
6543 BUILTIN("pcre_study", 0, Some(crate::ported::modules::pcre::bin_pcre_study as crate::ported::zsh_h::HandlerFunc), 0, 0, 0, None, None),
6544 // bin_pcre_match returns (i32, Option<String>, Vec<...>) — non-standard
6545 // signature, can't dispatch via execbuiltin. Wrapper stays in exec.rs.
6546 BUILTIN("pcre_match", 0, None, 1, -1, 0, Some("ab:nv:"), None),
6547 BUILTIN("ztcp", 0, Some(crate::ported::modules::tcp::bin_ztcp as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("acdflLtv"), None),
6548 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),
6549 BUILTIN("zuntie", 0, Some(crate::ported::modules::db_gdbm::bin_zuntie as crate::ported::zsh_h::HandlerFunc), 1, -1, 0, Some("u"), None),
6550 BUILTIN("zgdbmpath", 0, Some(crate::ported::modules::db_gdbm::bin_zgdbmpath as crate::ported::zsh_h::HandlerFunc), 1, 1, 0, None, None),
6551 BUILTIN("echoti", 0, Some(crate::ported::modules::terminfo::bin_echoti as crate::ported::zsh_h::HandlerFunc), 1, -1, 0, None, None),
6552 BUILTIN("fg", 0, Some(crate::ported::jobs::bin_fg as crate::ported::zsh_h::HandlerFunc), 0, -1, BIN_FG, None, None),
6553 BUILTIN("kill", BINF_HANDLES_OPTS, Some(crate::ported::jobs::bin_kill as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, None, None),
6554 BUILTIN("suspend", 0, Some(crate::ported::jobs::bin_suspend as crate::ported::zsh_h::HandlerFunc), 0, 0, 0, Some("f"), None),
6555 BUILTIN("bindkey", 0, Some(crate::ported::zle::zle_keymap::bin_bindkey as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("evaMldDANmrsLR"), None),
6556 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),
6557 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),
6558 BUILTIN("compset", 0, Some(crate::ported::zle::complete::bin_compset as crate::ported::zsh_h::HandlerFunc), 1, -1, 0, Some("npqPS:"), None),
6559 BUILTIN("zle", 0, Some(crate::ported::zle::zle_thingy::bin_zle as crate::ported::zsh_h::HandlerFunc), 0, -1, 0, Some("aAcCDfFIKlLmMNRTU"), None),
6560 // zsh/files module — file-manipulation builtins. All have
6561 // HandlerFunc-compatible signatures already.
6562 BUILTIN("mkdir", 0, Some(crate::ported::modules::files::bin_mkdir as crate::ported::zsh_h::HandlerFunc), 1, -1, 0, Some("pm:"), None),
6563 BUILTIN("rmdir", 0, Some(crate::ported::modules::files::bin_rmdir as crate::ported::zsh_h::HandlerFunc), 1, -1, 0, None, None),
6564 BUILTIN("ln", 0, Some(crate::ported::modules::files::bin_ln as crate::ported::zsh_h::HandlerFunc), 1, -1, 0, Some("dfins"), None),
6565 BUILTIN("rm", 0, Some(crate::ported::modules::files::bin_rm as crate::ported::zsh_h::HandlerFunc), 1, -1, 0, Some("dfiRrs"), None),
6566 BUILTIN("chmod", 0, Some(crate::ported::modules::files::bin_chmod as crate::ported::zsh_h::HandlerFunc), 2, -1, 0, Some("Rs"), None),
6567 BUILTIN("chown", 0, Some(crate::ported::modules::files::bin_chown as crate::ported::zsh_h::HandlerFunc), 2, -1, 0, Some("Rs"), None),
6568 BUILTIN("sync", 0, Some(crate::ported::modules::files::bin_sync as crate::ported::zsh_h::HandlerFunc), 0, 0, 0, None, None),
6569]);
6570// hash table containing builtin commands // c:143
6571/// Process-wide builtin lookup table. Filled lazily the first time
6572/// `builtintab()` is called; mirrors the C `mod_export HashTable
6573/// builtintab` exposed at `Src/builtin.c:146`.
6574static builtintab: OnceLock<HashMap<String, &'static builtin>> = OnceLock::new();
6575
6576/// Names whose `node.flags & DISABLED` is set in C. The Rust port's
6577/// `builtintab` is an immutable static, so the disabled bit lives
6578/// in this parallel set; `bin_enable` toggles it via builtin.c:587.
6579/// Dispatch sites check `is_builtin_disabled(name)` before calling
6580/// `handlerfunc` to mirror C's "skip nodes with DISABLED set" walk.
6581pub static BUILTINS_DISABLED: std::sync::LazyLock< // c:587 (Src/builtin.c)
6582 std::sync::Mutex<std::collections::HashSet<String>>
6583> = std::sync::LazyLock::new(|| {
6584 std::sync::Mutex::new(std::collections::HashSet::new())
6585});
6586
6587// `shfunctab` global from Src/init.c — name → Shfunc map. Static-link
6588// path: store the raw Shfunc pointer keyed by name. Lazy via OnceLock
6589// because HashMap::new isn't const.
6590static SHFUNCTAB_INNER: std::sync::OnceLock<std::sync::Mutex<std::collections::HashMap<String, usize>>>
6591 = std::sync::OnceLock::new();
6592
6593// `matchednodes` global from Src/builtin.c:4550.
6594pub static MATCHEDNODES: std::sync::Mutex<Vec<String>> =
6595 std::sync::Mutex::new(Vec::new());
6596
6597// `stopmsg` global from Src/jobs.c — non-zero when checkjobs() printed.
6598pub static STOPMSG: std::sync::atomic::AtomicI32 =
6599 std::sync::atomic::AtomicI32::new(0);
6600// `sfcontext` global from Src/exec.c:239 — current shell-function
6601// dispatch context (SFC_NONE / SFC_BUILTIN / SFC_FUNC / SFC_SUBST...).
6602pub static SFCONTEXT: std::sync::atomic::AtomicI32 =
6603 std::sync::atomic::AtomicI32::new(0); // c:exec.c:239
6604// `maxjob` / `thisjob` globals from Src/jobs.c:62/63 — canonical
6605// storage lives in jobs.rs (`OnceLock<Mutex<i32>>`). The previous
6606// builtin.rs duplicate `AtomicI32` stores NEVER synced with the
6607// jobs.rs Mutex<i32> values that the spawn/wait paths actually
6608// update; `checkjobs` (line 5092) read stale 0s no matter how many
6609// jobs were active. Callers route through jobs::MAXJOB / jobs::THISJOB
6610// directly now.
6611// `jobstats` mirror — flat per-slot stat bits (STAT_*). Real jobtab
6612// lives in src/ported/jobs.rs's JobTable; this mirror is updated by
6613// the spawn/wait paths that already touch STOPMSG. Empty → no jobs,
6614// matching the post-init state of `jobtab[]`.
6615pub static JOBSTATS: std::sync::Mutex<Vec<i32>> = std::sync::Mutex::new(Vec::new());
6616
6617// File-static globals for [_]realexit/zexit — c:5945+, init.c, signals.c.
6618pub static SHELL_EXITING: std::sync::atomic::AtomicI32 =
6619 std::sync::atomic::AtomicI32::new(0);
6620pub static EXIT_PENDING: std::sync::atomic::AtomicI32 =
6621 std::sync::atomic::AtomicI32::new(0);
6622pub static EXIT_VAL: std::sync::atomic::AtomicI32 =
6623 std::sync::atomic::AtomicI32::new(0);
6624pub static LASTVAL: std::sync::atomic::AtomicI32 =
6625 std::sync::atomic::AtomicI32::new(0);
6626
6627// `tok` for the test builtin — Src/builtin.c:7000 ranges. The full enum
6628// lives in src/ported/lex.rs; we mirror the few values testlex() touches.
6629pub static TEST_TOK: std::sync::atomic::AtomicI32 =
6630 std::sync::atomic::AtomicI32::new(0);
6631const TEST_LEXERR: i32 = -1; // c:7209
6632const TEST_NULLTOK: i32 = 0;
6633const TEST_DBAR: i32 = 2; // c:7213
6634const TEST_DAMPER: i32 = 3; // c:7215
6635const TEST_BANG: i32 = 4; // c:7217
6636const TEST_INPAR: i32 = 5; // c:7219
6637const TEST_OUTPAR: i32 = 6; // c:7221
6638const TEST_INANG: i32 = 7; // c:7223
6639const TEST_OUTANG: i32 = 8; // c:7225
6640const TEST_STRING: i32 = 9; // c:7227
6641
6642// `testargs` / `curtestarg` / `tokstr` globals from Src/builtin.c — the
6643// argv-style cursor that bin_test seeds and testlex() advances.
6644pub static TESTARGS: std::sync::Mutex<Vec<String>> = std::sync::Mutex::new(Vec::new());
6645pub static TESTARGS_IDX: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
6646pub static TOKSTR: std::sync::Mutex<String> = std::sync::Mutex::new(String::new());
6647
6648// int doprintdir = 0; set in exec.c (for autocd, cdpath, etc.) // c:722
6649// `doprintdir` from Src/exec.c — set when an autocd'd command should
6650// echo the new directory before executing.
6651pub static DOPRINTDIR: std::sync::atomic::AtomicI32 =
6652 std::sync::atomic::AtomicI32::new(0);
6653// set if we are resolving links to their true paths // c:829
6654// `chasinglinks` from Src/exec.c — non-zero when CHASELINKS / -P
6655// resolution is active.
6656pub static CHASINGLINKS: std::sync::atomic::AtomicI32 =
6657 std::sync::atomic::AtomicI32::new(0);
6658
6659// `pparams` global from Src/init.c — positional parameters $1..$N.
6660pub static PPARAMS: std::sync::Mutex<Vec<String>> =
6661 std::sync::Mutex::new(Vec::new());
6662
6663// `zoptind` (Src/builtin.c:5667) and `optcind` (c:5670) — the two
6664// pieces of getopts state. zoptind backs the user-visible $OPTIND.
6665pub static ZOPTIND: std::sync::atomic::AtomicI32 =
6666 std::sync::atomic::AtomicI32::new(1);
6667pub static OPTCIND: std::sync::atomic::AtomicI32 =
6668 std::sync::atomic::AtomicI32::new(0);
6669
6670// `ttyfrozen` global lives canonically in jobs.rs (`OnceLock<Mutex<i32>>`
6671// at jobs.rs:2625). The previous AtomicI32 duplicate here NEVER
6672// synced with the jobs.rs store — same desync hazard as the prior
6673// MAXJOB / THISJOB fix. Callers route through jobs::TTYFROZEN.
6674
6675/// Port of `mod_export int ineval` from `Src/builtin.c:6389`. Set
6676/// while `eval` is dispatching its body (incremented before
6677/// `execode(prog, 1, 0, "eval")`, decremented after). Tested by
6678/// `IN_EVAL_TRAP()` in zsh.h:2962 to determine trap-context state.
6679pub static INEVAL: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:6389
6680
6681// `loops` / `breaks` / `contflag` / `retflag` / `locallevel` / `sourcelevel`
6682// globals from Src/loop.c + Src/init.c — control-flow state consulted by
6683// the bin_break dispatcher.
6684pub static LOOPS: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
6685pub static BREAKS: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
6686pub static CONTFLAG: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
6687pub static RETFLAG: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
6688// `LOCALLEVEL` was previously a SEPARATE AtomicI32 here, but C
6689// zsh has only ONE `int locallevel;` global (Src/params.c:54).
6690// The canonical Rust port is `crate::ported::params::locallevel`
6691// (lowercase, matches C name). Re-export that single storage so
6692// every reader and writer addresses the same atomic — without
6693// this, `LOCALLEVEL.store(0)` in zle/computil.rs would zero one
6694// global while `params::locallevel.fetch_add(1)` in exec.rs
6695// incremented a DIFFERENT global, leaving the two views out of
6696// sync indefinitely.
6697pub use crate::ported::params::locallevel as LOCALLEVEL;
6698pub static SOURCELEVEL: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
6699
6700// `ZEXIT_NORMAL` re-exported from canonical zsh_h.rs (port of the
6701// `enum { ZEXIT_NORMAL, ZEXIT_SIGNAL, ZEXIT_DEFERRED }` in Src/zsh.h).
6702// Same single-source-of-truth pattern as TERM_UNKNOWN / HISTFLAG_*
6703// / etc — duplicate const declarations are a drift hazard.
6704pub use crate::ported::zsh_h::ZEXIT_NORMAL;
6705
6706// Local builders that construct C-shape `builtin` rows for the
6707// static registration table below. They mirror the
6708// `BUILTIN(...)` / `BIN_PREFIX(...)` macros in `Src/zsh.h:1450-1452`,
6709// taking `u32` flag bitsets (BINF_*) and a `&str` handler-name
6710// column used only for documentation/wiring lookup — handler
6711// function pointers themselves are wired up later in
6712// `Executor::register_builtins` (`src/ported/exec.rs`).
6713//
6714// The `handler` arg was previously a `_handler_name: &'static str` that
6715// was discarded — `handlerfunc` always ended up `NULLBINCMD`, so
6716// `execbuiltin`'s c:506 `(*handlerfunc)(...)` dispatch was unreachable.
6717// Now the descriptor carries the actual port-side `HandlerFunc` so
6718// `execbuiltin` can parse flags and call through to the real builtin.
6719#[allow(non_snake_case)]
6720pub fn BUILTIN(
6721 name: &str,
6722 flags: u32,
6723 handler: Option<crate::ported::zsh_h::HandlerFunc>,
6724 min: i32,
6725 max: i32,
6726 funcid: i32,
6727 optstr: Option<&str>,
6728 defopts: Option<&str>,
6729) -> builtin {
6730 builtin {
6731 node: hashnode {
6732 next: None,
6733 nam: name.to_string(),
6734 flags: flags as i32,
6735 },
6736 handlerfunc: handler,
6737 minargs: min,
6738 maxargs: max,
6739 funcid,
6740 optstr: optstr.map(|s| s.to_string()),
6741 defopts: defopts.map(|s| s.to_string()),
6742 }
6743}
6744
6745// `traps` mirror — sig name → body. Real `sigtrapped[]`/`siglists[]`
6746// arrays live in src/ported/signals.rs; this Mutex is the static-link
6747// shim that bin_trap reads/writes.
6748static TRAPS_INNER: std::sync::OnceLock<std::sync::Mutex<std::collections::HashMap<String, String>>>
6749 = std::sync::OnceLock::new();
6750
6751#[allow(non_snake_case)]
6752fn BIN_PREFIX(name: &str, flags: u32) -> builtin {
6753 BUILTIN(name, flags | BINF_PREFIX, None, 0, 0, 0, None, None)
6754}
6755
6756/// Inline printf-style format helper used by bin_print's -f/printf mode.
6757/// Replaces `%s` / `%d` / `%i` / `%c` / `%%` with positional args.
6758/// Full C printf-spec engine (Src/builtin.c:4691-5500) is much more
6759/// elaborate (width/precision/flag chars/%b/%q/etc.); this is the
6760/// minimal subset that covers the common script patterns.
6761fn printf_format(fmt: &str, args: &[String]) -> String {
6762 // c:Src/builtin.c:4711 — `fmt = getkeystring(fmt, &flen, ...,
6763 // GETKEYS_PRINTF_FMT, ...);`. The format string is first run
6764 // through getkeystring to interpret backslash escapes (`\n`,
6765 // `\t`, `\xNN`, etc.) before %-format substitution.
6766 let (fmt, _) = getkeystring(fmt); // c:builtin.c:4711
6767 let mut out = String::new();
6768 let mut arg_i: usize = 0;
6769 // c:Src/builtin.c:4914-4923 — printf reapplies the format string
6770 // until ALL args are consumed. `printf '%s,' a b c` → `a,b,c,`,
6771 // not `a,`. The outer loop reapplies; the inner do-while body
6772 // mirrors C's per-arg conversion loop directly.
6773 loop {
6774 let prev = arg_i;
6775 let mut iter = fmt.chars().peekable();
6776 while let Some(c) = iter.next() {
6777 if c != '%' {
6778 out.push(c);
6779 continue;
6780 }
6781 // c:Src/builtin.c:4791+ — parse width/precision/flag chars
6782 // between `%` and the conversion. Capture them so `printf
6783 // "%-10s" hi` and `printf "%.3f" 3.14159` render correctly.
6784 let mut spec = String::from("%");
6785 loop {
6786 match iter.peek() {
6787 Some(&c) if matches!(c, '-' | '+' | ' ' | '#' | '0') => {
6788 spec.push(c); iter.next();
6789 }
6790 _ => break,
6791 }
6792 }
6793 while let Some(&c) = iter.peek() {
6794 if c.is_ascii_digit() { spec.push(c); iter.next(); }
6795 else { break; }
6796 }
6797 if iter.peek() == Some(&'.') {
6798 spec.push('.'); iter.next();
6799 while let Some(&c) = iter.peek() {
6800 if c.is_ascii_digit() { spec.push(c); iter.next(); }
6801 else { break; }
6802 }
6803 }
6804 match iter.next() {
6805 Some('%') => out.push('%'),
6806 Some('s') => {
6807 let a = args.get(arg_i).cloned().unwrap_or_default();
6808 spec.push('s');
6809 out.push_str(&format_spec_str(&spec, &a));
6810 arg_i += 1;
6811 }
6812 Some('d') | Some('i') => {
6813 let a = args.get(arg_i).cloned().unwrap_or_default();
6814 let n: i64 = a.parse().unwrap_or(0);
6815 spec.push('d');
6816 out.push_str(&format_spec_int(&spec, n));
6817 arg_i += 1;
6818 }
6819 Some('u') => {
6820 let a = args.get(arg_i).cloned().unwrap_or_default();
6821 let n: u64 = a.parse().unwrap_or(0);
6822 spec.push('u');
6823 out.push_str(&format_spec_uint(&spec, n));
6824 arg_i += 1;
6825 }
6826 Some('x') => {
6827 let a = args.get(arg_i).cloned().unwrap_or_default();
6828 let n: i64 = a.parse().unwrap_or(0);
6829 spec.push('x');
6830 out.push_str(&format!("{:x}", n));
6831 arg_i += 1;
6832 }
6833 Some('X') => {
6834 let a = args.get(arg_i).cloned().unwrap_or_default();
6835 let n: i64 = a.parse().unwrap_or(0);
6836 spec.push('X');
6837 out.push_str(&format!("{:X}", n));
6838 arg_i += 1;
6839 }
6840 Some('o') => {
6841 let a = args.get(arg_i).cloned().unwrap_or_default();
6842 let n: i64 = a.parse().unwrap_or(0);
6843 spec.push('o');
6844 out.push_str(&format!("{:o}", n));
6845 arg_i += 1;
6846 }
6847 Some('f') | Some('F') | Some('g') | Some('G') | Some('e') | Some('E') => {
6848 let a = args.get(arg_i).cloned().unwrap_or_default();
6849 let n: f64 = a.parse().unwrap_or(0.0);
6850 spec.push('f');
6851 out.push_str(&format_spec_float(&spec, n));
6852 arg_i += 1;
6853 }
6854 Some('c') => {
6855 if let Some(a) = args.get(arg_i) {
6856 if let Some(ch) = a.chars().next() { out.push(ch); }
6857 }
6858 arg_i += 1;
6859 }
6860 // c:builtin.c:4825 %q — shell-quote the arg.
6861 Some('q') => {
6862 let a = args.get(arg_i).cloned().unwrap_or_default();
6863 out.push_str("edzputs(&a));
6864 arg_i += 1;
6865 }
6866 // c:builtin.c:4810 %b — interpret backslash escapes
6867 // with GETKEY_EMACS arm (drop unknown backslashes).
6868 Some('b') => {
6869 let a = args.get(arg_i).cloned().unwrap_or_default();
6870 let (s, _) = getkeystring_with(&a, GETKEYS_PRINT);
6871 out.push_str(&s);
6872 arg_i += 1;
6873 }
6874 Some(other) => { out.push('%'); out.push(other); }
6875 None => out.push('%'),
6876 }
6877 }
6878 if arg_i == prev || arg_i >= args.len() { break; }
6879 }
6880 out
6881}
6882
6883/// Apply a printf-style `%[-flag][width][.prec]s` spec to a string.
6884/// Mirrors C `printf "%-10s" str` formatting; the Rust `format!` macro
6885/// doesn't accept runtime-parsed specs so we hand-parse.
6886fn format_spec_str(spec: &str, s: &str) -> String {
6887 let (left_align, width, prec) = parse_width_prec(spec);
6888 let truncated: &str = if let Some(p) = prec {
6889 let end: usize = s.chars().take(p).map(|c| c.len_utf8()).sum();
6890 &s[..end.min(s.len())]
6891 } else { s };
6892 let pad = width.saturating_sub(truncated.chars().count());
6893 if left_align {
6894 format!("{}{}", truncated, " ".repeat(pad))
6895 } else {
6896 format!("{}{}", " ".repeat(pad), truncated)
6897 }
6898}
6899
6900fn format_spec_int(spec: &str, n: i64) -> String {
6901 let (left_align, width, _prec) = parse_width_prec(spec);
6902 let zero_pad = spec.contains('0') && !left_align;
6903 let body = n.to_string();
6904 let pad = width.saturating_sub(body.chars().count());
6905 if pad == 0 { body }
6906 else if left_align { format!("{}{}", body, " ".repeat(pad)) }
6907 else if zero_pad {
6908 if let Some(rest) = body.strip_prefix('-') {
6909 format!("-{}{}", "0".repeat(pad), rest)
6910 } else { format!("{}{}", "0".repeat(pad), body) }
6911 } else { format!("{}{}", " ".repeat(pad), body) }
6912}
6913
6914fn format_spec_uint(spec: &str, n: u64) -> String {
6915 format_spec_int(spec, n as i64)
6916}
6917
6918fn format_spec_float(spec: &str, n: f64) -> String {
6919 let (left_align, width, prec) = parse_width_prec(spec);
6920 let p = prec.unwrap_or(6);
6921 let body = format!("{:.*}", p, n);
6922 let pad = width.saturating_sub(body.chars().count());
6923 if pad == 0 { body }
6924 else if left_align { format!("{}{}", body, " ".repeat(pad)) }
6925 else { format!("{}{}", " ".repeat(pad), body) }
6926}
6927
6928fn parse_width_prec(spec: &str) -> (bool, usize, Option<usize>) {
6929 let s = spec.trim_start_matches('%');
6930 let mut i = 0;
6931 let bytes = s.as_bytes();
6932 let mut left_align = false;
6933 while i < bytes.len() && matches!(bytes[i], b'-' | b'+' | b' ' | b'#' | b'0') {
6934 if bytes[i] == b'-' { left_align = true; }
6935 i += 1;
6936 }
6937 let width_start = i;
6938 while i < bytes.len() && bytes[i].is_ascii_digit() { i += 1; }
6939 let width: usize = s[width_start..i].parse().unwrap_or(0);
6940 let mut prec: Option<usize> = None;
6941 if i < bytes.len() && bytes[i] == b'.' {
6942 i += 1;
6943 let p_start = i;
6944 while i < bytes.len() && bytes[i].is_ascii_digit() { i += 1; }
6945 prec = Some(s[p_start..i].parse().unwrap_or(0));
6946 }
6947 (left_align, width, prec)
6948}
6949
6950/// Port of `findcmd(char *arg0, int docopy, int default_path)` from Src/exec.c:897. Walk `$PATH` for `name`,
6951/// returning the matching path on success. `_docopy` is the C source's
6952/// "duplicate the result" flag; Rust ownership covers it. `_default_path`
6953/// = 1 forces the system default `/bin:/usr/bin:...` path search (used
6954/// by `command -p`); not yet wired.
6955/// WARNING: param names don't match C — Rust=(name, _docopy, _default_path) vs C=(errflag)
6956pub fn findcmd(name: &str, _docopy: i32, _default_path: i32) -> Option<String> { // c:897
6957 if name.contains('/') {
6958 let p = std::path::Path::new(name);
6959 return if p.is_file() { Some(name.to_string()) } else { None };
6960 }
6961 // c:907-912 — walk `path[]` (the shell $path array). Read $PATH
6962 // from paramtab so shell-private PATH edits via
6963 // `path=(...)` show up; OS env-only PATH would miss
6964 // them in nested shells.
6965 let path = crate::ported::params::getsparam("PATH")?;
6966 for dir in path.split(':') {
6967 if dir.is_empty() { continue; }
6968 let candidate = format!("{}/{}", dir, name);
6969 if std::path::Path::new(&candidate).is_file() {
6970 return Some(candidate);
6971 }
6972 }
6973 None
6974}
6975
6976/// Port of `getsigidx(const char *s)` from Src/signals.c — return signal number for
6977/// a name, or -1 if unknown. Strips optional `SIG` prefix; falls back
6978/// to numeric parse.
6979fn getsigidx(name: &str) -> i32 {
6980 let s = name.strip_prefix("SIG").unwrap_or(name);
6981 // Try parse as integer first.
6982 if let Ok(n) = s.parse::<i32>() {
6983 return n;
6984 }
6985 // Common signal name → number mapping.
6986 match s {
6987 "HUP" => 1, "INT" => 2, "QUIT" => 3, "ILL" => 4,
6988 "TRAP" => 5, "ABRT" => 6, "FPE" => 8, "KILL" => 9,
6989 "USR1" => 10, "SEGV" => 11, "USR2" => 12, "PIPE" => 13,
6990 "ALRM" => 14, "TERM" => 15, "CHLD" => 17, "CONT" => 18,
6991 "STOP" => 19, "TSTP" => 20, "TTIN" => 21, "TTOU" => 22,
6992 "URG" => 23, "XCPU" => 24, "XFSZ" => 25, "VTALRM" => 26,
6993 "PROF" => 27, "WINCH" => 28, "IO" => 29, "PWR" => 30,
6994 "SYS" => 31, "EXIT" => 0,
6995 _ => -1,
6996 }
6997}
6998
6999/// Port of `int pat_enables(const char *cmd, char **patp, int enable)`
7000/// from `Src/pattern.c:4171`. Local builtin.rs shim that delegates to
7001/// the canonical pattern.rs port. Static-link path: the actual
7002/// zpc_strings/zpc_disables manipulation lives in
7003/// `crate::ported::pattern::pat_enables`.
7004fn pat_enables(name: &str, argv: &[String], on: bool) -> i32 { // c:4171
7005 let patp: Vec<&str> = argv.iter().map(|s| s.as_str()).collect();
7006 crate::ported::pattern::pat_enables(name, &patp, on)
7007}
7008
7009// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
7010// ─── RUST-ONLY ACCESSORS ───
7011//
7012// Singleton accessor fns for `OnceLock<Mutex<T>>` / `OnceLock<
7013// RwLock<T>>` globals declared above. C zsh uses direct global
7014// access; Rust needs these wrappers because `OnceLock::get_or_init`
7015// is the only way to lazily construct shared state. These fns sit
7016// here so the body of this file reads in C source order without
7017// the accessor wrappers interleaved between real port fns.
7018// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
7019
7020// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
7021// ─── RUST-ONLY ACCESSORS ───
7022//
7023// Singleton accessor fns for `OnceLock<Mutex<T>>` / `OnceLock<
7024// RwLock<T>>` globals declared above. C zsh uses direct global
7025// access; Rust needs these wrappers because `OnceLock::get_or_init`
7026// is the only way to lazily construct shared state. These fns sit
7027// here so the body of this file reads in C source order without
7028// the accessor wrappers interleaved between real port fns.
7029// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
7030
7031pub fn shfunctab_table() -> &'static std::sync::Mutex<std::collections::HashMap<String, usize>> {
7032 SHFUNCTAB_INNER.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()))
7033}
7034
7035pub fn traps_table() -> &'static std::sync::Mutex<std::collections::HashMap<String, String>> {
7036 TRAPS_INNER.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()))
7037}
7038
7039#[cfg(test)]
7040mod tests {
7041 use crate::zsh_h::BINF_PREFIX;
7042 use super::*;
7043
7044 /// c:7399 — `trap - <undefined>` MUST report failure (non-zero
7045 /// exit) so scripts can detect the bad signal name. The previous
7046 /// Rust port returned 0 unconditionally from the clear path,
7047 /// silently masking errors. C returns `*argv != NULL` — non-zero
7048 /// when the loop broke on an undefined signal.
7049 #[test]
7050 fn bin_trap_clear_undefined_signal_returns_nonzero() {
7051 let empty = crate::ported::zsh_h::options {
7052 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
7053 args: Vec::new(),
7054 argscount: 0,
7055 argsalloc: 0,
7056 };
7057 // `trap - BOGUS_NEVER_A_SIGNAL` → must return 1.
7058 let r = bin_trap("trap",
7059 &["-".into(), "BOGUS_NEVER_A_SIGNAL".into()],
7060 &empty, 0);
7061 assert_ne!(r, 0,
7062 "trap - <undefined> must report error per c:7399 (got {})", r);
7063 }
7064
7065 /// Src/options.c:537-549 — `emulate(zsh_name, ...)` dispatches
7066 /// on the FIRST char of the shell name, stripping a leading `r`
7067 /// (so `rcsh`/`rksh` work as restricted variants of their base
7068 /// shell). `bash` aliases to SH (the `'b'` branch of the case).
7069 /// Pin the bits assigned by `bin_emulate` for the canonical
7070 /// names + their first-char-overlap aliases.
7071 #[test]
7072 fn bin_emulate_dispatches_on_first_char_per_c537() {
7073 use crate::ported::zsh_h::{EMULATE_CSH, EMULATE_KSH, EMULATE_SH};
7074 let empty = crate::ported::zsh_h::options {
7075 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
7076 args: Vec::new(),
7077 argscount: 0,
7078 argsalloc: 0,
7079 };
7080 let saved = crate::ported::options::emulation
7081 .load(std::sync::atomic::Ordering::Relaxed);
7082
7083 // Each (name, expected_bits) — name covers the canonical
7084 // shell names AND their `r`-prefix / first-char variants.
7085 for (name, expected) in [
7086 ("csh", EMULATE_CSH),
7087 ("ksh", EMULATE_KSH),
7088 ("sh", EMULATE_SH),
7089 ("rcsh", EMULATE_CSH), // c:539-540
7090 ("rksh", EMULATE_KSH), // c:539-540
7091 ("bash", EMULATE_SH), // c:548 'b'
7092 ] {
7093 crate::ported::options::emulation
7094 .store(0, std::sync::atomic::Ordering::Relaxed);
7095 bin_emulate("emulate", &[name.into()], &empty, 0);
7096 let bits = crate::ported::options::emulation
7097 .load(std::sync::atomic::Ordering::Relaxed);
7098 assert_eq!(bits, expected,
7099 "emulate {} must set bits {:#x}, got {:#x}",
7100 name, expected, bits);
7101 }
7102 crate::ported::options::emulation
7103 .store(saved, std::sync::atomic::Ordering::Relaxed);
7104 }
7105
7106 /// c:7399 — `trap - SIGUSR1` (valid signal) MUST return 0, even
7107 /// when the trap was never set (remove is a no-op).
7108 #[test]
7109 fn bin_trap_clear_valid_signal_returns_zero() {
7110 let empty = crate::ported::zsh_h::options {
7111 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
7112 args: Vec::new(),
7113 argscount: 0,
7114 argsalloc: 0,
7115 };
7116 let r = bin_trap("trap", &["-".into(), "USR1".into()], &empty, 0);
7117 assert_eq!(r, 0,
7118 "trap - USR1 must succeed even with no prior trap (got {})", r);
7119 }
7120
7121 #[test]
7122 fn registration_table_matches_c_count() {
7123 // Src/builtin.c:40-137 has 79 rows total (5 BIN_PREFIX + 71
7124 // BUILTIN + 3 debug-only BUILTIN). The Rust port bundles
7125 // additional builtins eagerly that C would load via zmodload:
7126 // zsh/rlimits (limit/ulimit/unlimit)
7127 // zsh/zle (bindkey/vared/zle)
7128 // zsh/cap (cap/getcap/setcap)
7129 // zsh/files (chmod/chown/ln/mkdir/rm/rmdir/sync)
7130 // zsh/complete (compadd/compset)
7131 // zsh/terminfo (echoti)
7132 // zsh/pcre (pcre_compile/pcre_match/pcre_study)
7133 // zsh/zutil (zformat/zgdbmpath)
7134 // Total Rust BUILTINS table size pinned at 112 to catch
7135 // accidental additions/removals. Bump alongside intentional
7136 // changes to the BUILTINS table above.
7137 assert_eq!(BUILTINS.len(), 112,
7138 "BUILTINS table size changed — bump count or update the eagerly-loaded-module list above");
7139 }
7140
7141 /// `Src/builtin.c:40-137` — every name in the canonical C builtin
7142 /// table must be present in the Rust port. Pins coverage of all
7143 /// 79 C builtins by name (ignores option-mask / handler details).
7144 /// Detects regressions where a builtin gets accidentally dropped
7145 /// from BUILTINS. Names extracted from upstream zsh `Src/builtin.c`.
7146 #[test]
7147 fn registration_table_contains_all_c_builtins() {
7148 // Canonical 79 names from Src/builtin.c:40-137 (verbatim).
7149 let c_names: &[&str] = &[
7150 "-", ".", ":", "[",
7151 "alias", "autoload", "bg", "break", "builtin", "bye",
7152 "cd", "chdir", "command", "continue", "declare", "dirs",
7153 "disable", "disown", "echo", "emulate", "enable", "eval",
7154 "exec", "exit", "export", "false", "fc", "fg", "float",
7155 "functions", "getln", "getopts", "hash", "hashinfo",
7156 "history", "integer", "jobs", "kill", "let", "local",
7157 "logout", "mem", "noglob", "patdebug", "popd", "print",
7158 "printf", "pushd", "pushln", "pwd", "r", "read",
7159 "readonly", "rehash", "return", "set", "setopt", "shift",
7160 "source", "suspend", "test", "times", "trap", "true",
7161 "ttyctl", "type", "typeset", "umask", "unalias",
7162 "unfunction", "unhash", "unset", "unsetopt", "wait",
7163 "whence", "where", "which", "zcompile", "zmodload",
7164 ];
7165 assert_eq!(c_names.len(), 79,
7166 "C builtin.c row count is 79 — recount if changed");
7167 let table_names: std::collections::HashSet<&str> =
7168 BUILTINS.iter().map(|b| b.node.nam.as_str()).collect();
7169 for c_name in c_names {
7170 assert!(table_names.contains(*c_name),
7171 "missing C builtin '{}' from BUILTINS table", c_name);
7172 }
7173 }
7174
7175 #[test]
7176 fn lookup_finds_known_builtins() {
7177 for name in ["cd", "echo", "print", "fg", "bg", "jobs", "wait", "typeset", "test", "[", "."] {
7178 assert!(createbuiltintable().get(name).copied().is_some(), "missing: {name}");
7179 }
7180 }
7181
7182 #[test]
7183 fn lookup_misses_unknown() {
7184 assert!(createbuiltintable().get("not-a-builtin-zZz").copied().is_none());
7185 }
7186
7187 #[test]
7188 fn prefix_entries_have_prefix_flag() {
7189 for name in ["-", "builtin", "command", "exec", "noglob"] {
7190 let b = createbuiltintable().get(name).copied().unwrap();
7191 assert!(b.node.flags as u32 & BINF_PREFIX != 0, "{name} missing BINF_PREFIX");
7192 }
7193 }
7194
7195 #[test]
7196 fn fixdir_canonicalizes_absolute_paths() {
7197 // c:1297 — collapse `//`, drop `./`, pop `..`.
7198 assert_eq!(fixdir("/tmp/./foo"), "/tmp/foo");
7199 assert_eq!(fixdir("/tmp//foo"), "/tmp/foo");
7200 assert_eq!(fixdir("/tmp/bar/../foo"), "/tmp/foo");
7201 assert_eq!(fixdir("/tmp/bar/baz/../.."), "/tmp");
7202 }
7203
7204 #[test]
7205 fn fixdir_drops_dotdot_past_root() {
7206 // c:1372 — absolute path, `..` past `/` is dropped.
7207 assert_eq!(fixdir("/.."), "/");
7208 assert_eq!(fixdir("/../.."), "/");
7209 assert_eq!(fixdir("/foo/../../bar"), "/bar");
7210 }
7211
7212 #[test]
7213 fn fixdir_relative_keeps_leading_dotdot() {
7214 // c:1367 — relative path: `..` past start stays as `..`.
7215 assert_eq!(fixdir("../foo"), "../foo");
7216 assert_eq!(fixdir("../../foo"), "../../foo");
7217 assert_eq!(fixdir("foo/../bar"), "bar");
7218 }
7219
7220 #[test]
7221 fn fixdir_empty_collapses_to_dot() {
7222 // Relative path that collapses fully → "."
7223 assert_eq!(fixdir("./"), ".");
7224 assert_eq!(fixdir("foo/.."), ".");
7225 }
7226
7227 #[test]
7228 fn fixdir_empty_input_returns_empty() {
7229 assert_eq!(fixdir(""), "");
7230 }
7231
7232 #[test]
7233 fn fg_dispatch_id_distinguishes_aliases() {
7234 // bin_fg covers fg, bg, jobs, wait, disown — same handler,
7235 // different funcid. Mirrors Src/builtin.c:52,61,75,88,131.
7236 assert_eq!(createbuiltintable().get("fg").copied().unwrap().funcid, BIN_FG);
7237 assert_eq!(createbuiltintable().get("bg").copied().unwrap().funcid, BIN_BG);
7238 assert_eq!(createbuiltintable().get("jobs").copied().unwrap().funcid, BIN_JOBS);
7239 assert_eq!(createbuiltintable().get("wait").copied().unwrap().funcid, BIN_WAIT);
7240 assert_eq!(createbuiltintable().get("disown").copied().unwrap().funcid, BIN_DISOWN);
7241 }
7242
7243 /// c:1297 — `fixdir` is the lexical-canonicalisation for `cd`. The
7244 /// path `/a/b/../c` must resolve to `/a/c` BEFORE chdir(2) — the
7245 /// shell uses it to compute the logical PWD for $PWD/OLDPWD. A
7246 /// regression that drops the `..` consumption would make $PWD
7247 /// report `/a/b/../c` literally on `cd /a/b/../c`.
7248 #[test]
7249 fn fixdir_pops_dotdot_against_previous_component() {
7250 assert_eq!(fixdir("/a/b/../c"), "/a/c");
7251 assert_eq!(fixdir("/a/b/../../c"), "/c");
7252 assert_eq!(fixdir("/foo/.."), "/");
7253 }
7254
7255 /// c:1352 — `./` collapses to nothing. `/a/./b` must equal `/a/b`.
7256 #[test]
7257 fn fixdir_drops_dot_components() {
7258 assert_eq!(fixdir("/a/./b"), "/a/b");
7259 assert_eq!(fixdir("./a"), "a");
7260 assert_eq!(fixdir("./."), ".");
7261 }
7262
7263 /// c:1388 — `//` collapses to single `/` (no preservation of POSIX
7264 /// implementation-defined `//` semantics, which zsh doesn't honour).
7265 #[test]
7266 fn fixdir_collapses_consecutive_slashes() {
7267 assert_eq!(fixdir("/a//b"), "/a/b");
7268 assert_eq!(fixdir("/a///b/c"), "/a/b/c");
7269 }
7270
7271 /// c:1404 — absolute path: `..` past `/` silently drops. `/..`
7272 /// resolves to `/`. Catches a regression where the underflow
7273 /// emits `..` literally.
7274 #[test]
7275 fn fixdir_dotdot_past_root_clamps_to_root() {
7276 assert_eq!(fixdir("/.."), "/");
7277 assert_eq!(fixdir("/../../a"), "/a");
7278 }
7279
7280 /// c:1400 — RELATIVE path: leading `..` are preserved (no parent
7281 /// known until chdir time). This is critical for `cd ../../foo`
7282 /// which must NOT resolve `..` lexically.
7283 #[test]
7284 fn fixdir_relative_leading_dotdot_is_preserved() {
7285 assert_eq!(fixdir("../foo"), "../foo");
7286 assert_eq!(fixdir("../../foo"), "../../foo");
7287 }
7288
7289 /// c:1683 — `fcgetcomm` returns 0 for ambiguous numeric inputs
7290 /// only when the string actually starts with '0'. The atoi result
7291 /// alone (which is 0 for non-numeric) MUST NOT short-circuit —
7292 /// non-numeric input should fall through to hcomsearch instead.
7293 #[test]
7294 fn fcgetcomm_numeric_zero_only_for_literal_zero_prefix() {
7295 assert_eq!(fcgetcomm("0"), 0, "literal `0` is event 0");
7296 assert_eq!(fcgetcomm("42"), 42);
7297 // Non-numeric falls through to hcomsearch (no hist match → -1).
7298 assert_eq!(fcgetcomm("definitely_not_a_history_command_zshrs"), -1);
7299 }
7300
7301 /// c:1088-1093 — `cd_able_vars` requires CDABLEVARS to be set;
7302 /// otherwise returns None even when the head names a param. A
7303 /// regression that ignores the option flag would let `cd HOME`
7304 /// silently `cd $HOME` even when the user disabled CDABLEVARS.
7305 #[test]
7306 fn cd_able_vars_returns_none_without_cdablevars_option() {
7307 // CDABLEVARS is not set by default → must return None.
7308 // We don't fight the option state here; just verify the
7309 // off-state default short-circuits before paramtab lookup.
7310 // (If a future commit enables CDABLEVARS by default, this
7311 // test will fail loudly — that's the right canary.)
7312 let r = cd_able_vars("HOME/anything");
7313 // Without CDABLEVARS, must be None; with it, would be Some.
7314 // Accept either since the option default is the actual invariant.
7315 if !crate::ported::zsh_h::isset(crate::ported::options::optlookup("cdablevars")) {
7316 assert!(r.is_none());
7317 }
7318 }
7319
7320 /// c:212 — `init_builtins` is idempotent: calling twice doesn't
7321 /// duplicate entries in the table. Regression that re-inserts on
7322 /// every call would balloon memory + break dispatch lookups.
7323 #[test]
7324 fn init_builtins_is_idempotent() {
7325 init_builtins();
7326 let count1 = createbuiltintable().len();
7327 init_builtins();
7328 let count2 = createbuiltintable().len();
7329 assert_eq!(count1, count2, "init_builtins must not duplicate entries");
7330 }
7331
7332 /// c:1708 — `fcsubs(sp, [(old, new), ...])` applies each
7333 /// substitution to the running string, returning the total
7334 /// replacement count. A regression returning 0 with substitutions
7335 /// applied would silently break `fc -s old=new`.
7336 #[test]
7337 fn fcsubs_applies_each_substitution_in_order() {
7338 let mut s = "echo foo bar foo".to_string();
7339 let n = fcsubs(&mut s, &[("foo".to_string(), "FOO".to_string())]);
7340 assert_eq!(s, "echo FOO bar FOO");
7341 assert_eq!(n, 2, "two `foo` matches replaced");
7342 }
7343
7344 /// c:1708 — empty `old` MUST skip (avoid infinite empty-match
7345 /// replacement loop). Regression treating "" as "match anywhere"
7346 /// would hang or silently corrupt every fc invocation.
7347 #[test]
7348 fn fcsubs_skips_empty_pattern() {
7349 let mut s = "anything".to_string();
7350 let n = fcsubs(&mut s, &[("".to_string(), "X".to_string())]);
7351 assert_eq!(s, "anything", "empty pattern must be skipped");
7352 assert_eq!(n, 0);
7353 }
7354
7355 /// c:1708 — chained substitutions apply left-to-right. After
7356 /// `a→b`, the next pair sees the post-substitution text. So
7357 /// `[(a→b), (b→c)]` over `a` yields `c`.
7358 #[test]
7359 fn fcsubs_chains_substitutions_left_to_right() {
7360 let mut s = "a".to_string();
7361 let n = fcsubs(&mut s, &[
7362 ("a".to_string(), "b".to_string()),
7363 ("b".to_string(), "c".to_string()),
7364 ]);
7365 assert_eq!(s, "c", "second sub sees post-first-sub text");
7366 assert_eq!(n, 2);
7367 }
7368
7369 /// c:1708 — substitution on no-match leaves string unchanged AND
7370 /// reports 0. Regression touching the string anyway would mangle
7371 /// fc output for events containing none of the requested patterns.
7372 #[test]
7373 fn fcsubs_no_match_returns_zero_unchanged() {
7374 let mut s = "hello world".to_string();
7375 let n = fcsubs(&mut s, &[("xyz".to_string(), "abc".to_string())]);
7376 assert_eq!(s, "hello world", "no match → unchanged");
7377 assert_eq!(n, 0);
7378 }
7379
7380 /// c:1297 — `fixdir` for plain relative path (no slashes, no
7381 /// dots) returns it unchanged. Most-common cd path; regression
7382 /// here would break `cd subdir`.
7383 #[test]
7384 fn fixdir_plain_relative_path_unchanged() {
7385 assert_eq!(fixdir("subdir"), "subdir");
7386 assert_eq!(fixdir("a/b/c"), "a/b/c");
7387 assert_eq!(fixdir("."), ".");
7388 }
7389
7390 /// Shared mutex for bin_let tests that toggle the global errflag.
7391 static BIN_LET_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
7392
7393 /// `Src/builtin.c:7469-7484` — `bin_let` semantics:
7394 /// 1. Returns 0 (success) when the LAST arg evaluates to non-zero.
7395 /// 2. Returns 1 (failure) when the LAST arg evaluates to zero.
7396 /// 3. Returns 2 AND CLEARS ERRFLAG_ERROR when any arg errors.
7397 /// The previous Rust port used a local `had_error` flag and never
7398 /// cleared `errflag`, letting `let` errors leak into subsequent
7399 /// commands — defeating the C `let` "errors are non-fatal and local"
7400 /// contract.
7401 #[test]
7402 fn bin_let_clears_errflag_on_math_error() {
7403 let _g = BIN_LET_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
7404 use crate::ported::utils::{errflag, ERRFLAG_ERROR};
7405 use std::sync::atomic::Ordering;
7406 let saved = errflag.load(Ordering::Relaxed);
7407 errflag.store(0, Ordering::Relaxed);
7408
7409 // 1. Last arg evaluates to non-zero → return 0.
7410 let ops = crate::ported::zsh_h::options {
7411 ind: [0; crate::ported::zsh_h::MAX_OPS],
7412 args: Vec::new(),
7413 argscount: 0,
7414 argsalloc: 0,
7415 };
7416 let argv = vec!["1".to_string()];
7417 assert_eq!(bin_let("let", &argv, &ops, 0), 0,
7418 "c:7482 — last expr non-zero → return 0 (success)");
7419
7420 // 2. Last arg evaluates to zero → return 1.
7421 let argv = vec!["0".to_string()];
7422 assert_eq!(bin_let("let", &argv, &ops, 0), 1,
7423 "c:7482 — last expr zero → return 1 (failure)");
7424
7425 // 3. Bad-syntax arg → return 2 AND clear ERRFLAG_ERROR.
7426 // Pre-set errflag manually to simulate matheval failure side
7427 // effect (since exact bad-syntax behavior of the matheval port
7428 // is implementation-dependent — what we're pinning is the
7429 // bin_let response to a set errflag).
7430 errflag.store(ERRFLAG_ERROR, Ordering::Relaxed);
7431 // Use a valid expression so matheval succeeds, but errflag
7432 // is already set from a prior step.
7433 let argv = vec!["1".to_string()];
7434 let rc = bin_let("let", &argv, &ops, 0);
7435 assert_eq!(rc, 2,
7436 "c:7479 — pre-set ERRFLAG_ERROR triggers c:7476-7480 cleanup, returns 2");
7437 // c:7478 — `errflag &= ~ERRFLAG_ERROR` must have run.
7438 assert_eq!(errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR, 0,
7439 "c:7478 — ERRFLAG_ERROR must be CLEARED after let error");
7440
7441 // Restore.
7442 errflag.store(saved, Ordering::Relaxed);
7443 }
7444
7445 /// `Src/builtin.c:7474-7475` — C walks ALL argv via
7446 /// `while (*argv) val = matheval(*argv++);`. The LAST matheval
7447 /// result is what determines the return code. The previous Rust
7448 /// port broke on first error, skipping later args. Pin: a sequence
7449 /// of two non-zero exprs returns 0 even if both are evaluated.
7450 #[test]
7451 fn bin_let_walks_all_argv_last_wins() {
7452 let _g = BIN_LET_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
7453 use crate::ported::utils::{errflag, ERRFLAG_ERROR};
7454 use std::sync::atomic::Ordering;
7455 errflag.store(0, Ordering::Relaxed);
7456
7457 let ops = crate::ported::zsh_h::options {
7458 ind: [0; crate::ported::zsh_h::MAX_OPS],
7459 args: Vec::new(),
7460 argscount: 0,
7461 argsalloc: 0,
7462 };
7463 // c:7474 — `5; 0` (two args): last is 0 → return 1.
7464 let argv = vec!["5".to_string(), "0".to_string()];
7465 assert_eq!(bin_let("let", &argv, &ops, 0), 1,
7466 "c:7474 — last arg wins (here: 0 → return 1)");
7467
7468 // c:7474 — `0; 5` (two args): last is 5 → return 0.
7469 let argv = vec!["0".to_string(), "5".to_string()];
7470 assert_eq!(bin_let("let", &argv, &ops, 0), 0,
7471 "c:7474 — last arg wins (here: 5 → return 0)");
7472
7473 errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed);
7474 }
7475}