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/vm_helper` 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 `vm_helper`'s
23//! `Executor` impl, that's noted too.
24
25use std::collections::HashMap;
26use std::io::Read;
27use std::sync::atomic::{Ordering, Ordering::Relaxed};
28use std::sync::{Mutex, OnceLock};
29#[allow(unused_imports)]
30use std::{env, fs, io, io::Write, path::Path, path::PathBuf};
31
32use crate::func_body_fmt::FuncBodyFmt;
33#[allow(unused_imports)]
34use crate::parse::{Redirect, ShellCommand};
35use crate::ported::compat::zgetcwd;
36use crate::ported::config_h::DEFAULT_PATH;
37use crate::ported::exec::{getfpfunc, iscom, loadautofn, FORKLEVEL, TRAP_RETURN, TRAP_STATE};
38use crate::ported::hashnameddir::{
39 addnameddirnode, emptynameddirtable, fillnameddirtable, nameddirtab, printnameddirnode,
40};
41use crate::ported::hashtable::{
42 aliastab_lock, cmdnamtab_lock, createaliasnode, dircache_set, emptycmdnamtable,
43 fillcmdnamtable, hnamcmp, printaliasnode, printcmdnamnode, printshfuncexpand, reswdtab_lock,
44 scanmatchshfunc, scanshfunc, shfunctab_lock, sufaliastab_lock,
45};
46use crate::DPUTS;
47use indexmap::IndexMap;
48// `curhist` (hist.rs static) NOT imported — there's an unavoidable
49// `let curhist` local in fc_main that mirrors C's `int curhist;` local
50// shadowing the global. Rule E says keep the C name. The static is
51// referenced via its fully-qualified path at the single read site to
52// avoid name-shadow E0530.
53use crate::ported::hist::{
54 addhistnum, gethistent, hcomsearch, histsiz, pushhiststack, quietgethist, readhistfile,
55 saveandpophiststack, savehistfile, savehistsiz,
56};
57use crate::ported::jobs::{bin_fg, removetrapnode};
58use crate::ported::math::{matheval, mathevali, mnumber, MN_INTEGER};
59use crate::ported::mem::{queue_signals, unqueue_signals};
60use crate::ported::module::MATHFUNCS;
61use crate::ported::modules::parameter::{DIRSTACK, FUNCSTACK};
62use crate::ported::options::{dosetopt, emulation, optlookup, ZSH_OPTIONS_SET};
63use crate::ported::params::{
64 createparam, getiparam, getsparam, isident, locallevel as locallevel_param, locallevel,
65 paramtab, printparamnode, setaparam, setiparam, setsparam, unsetparam, unsetparam_pm,
66};
67use crate::ported::pattern::{patcompile, pattry};
68use crate::ported::signals::settrap;
69use crate::ported::sort::strmetasort;
70use crate::ported::utils::{
71 argzero, errflag, fprintdir, getkeystring, getkeystring_with, getshfunc, gettempfile, lchdir,
72 print_if_link, printprompt4, quotedzputs, scriptname_get, set_argzero, zerr, zerrnam, zwarn,
73 zwarnnam, GETKEYS_BINDKEY, GETKEYS_ECHO, GETKEYS_PRINT,
74};
75#[allow(unused_imports)]
76use crate::ported::vm_helper::{self, format_int_in_base, BUILTIN_NAMES};
77use crate::ported::zle::compctl::compctlread;
78use crate::ported::zsh_h::{
79 alias, asgment, builtin, cmdnam, eprog, hashnode, isset, mathfunc, nameddir, options, param,
80 shfunc, HandlerFunc, Meta, ALIAS_GLOBAL, ALIAS_SUFFIX, ASG_ARRAY, ASG_ARRAYP, ASG_KEY_VALUE,
81 ASG_VALUEP, AUTOPUSHD, BINF_ADDED, BINF_ASSIGN, BINF_BUILTIN, BINF_COMMAND, BINF_DASH,
82 BINF_DASHDASHVALID, BINF_EXEC, BINF_HANDLES_OPTS, BINF_KEEPNUM, BINF_MAGICEQUALS, BINF_NOGLOB,
83 BINF_PLUSOPTS, BINF_PREFIX, BINF_PRINTOPTS, BINF_PSPECIAL, BINF_SKIPDASH, BINF_SKIPINVALID,
84 BSDECHO, CDABLEVARS, CHASELINKS, CHECKRUNNINGJOBS, DISABLED, EMULATE_CSH, EMULATE_KSH,
85 EMULATE_SH, EMULATE_ZSH, EMULATION, ERRFLAG_ERROR, FS_FUNC, FUNCTIONARGZERO, GLOBALEXPORT,
86 GLOBALRCS,
87 interact, islogin, HASHED, HFILE_APPEND, HFILE_NO_REWRITE, HFILE_SKIPOLD, HFILE_USE_OPTIONS,
88 HIST_FOREIGN, INTERACTIVE, KSHARRAYS,
89 LOGINSHELL, MAX_OPS, MFF_STR, MFF_USERFUNC, MONITOR, NULLBINCMD, OPT_ARG, OPT_HASARG,
90 OPT_ISSET, OPT_MINUS, OPT_PLUS, PATHDIRS, PAT_HEAPDUP, PAT_STATIC, PM_ABSPATH_USED, PM_ARRAY,
91 PM_AUTOLOAD, PM_CUR_FPATH, PM_DECLARED, PM_DEFAULTED, PM_EFLOAT, PM_EXPORTED, PM_FFLOAT,
92 PM_HASHED, PM_HIDE, PM_HIDEVAL, PM_INTEGER, PM_KSHSTORED, PM_LEFT, PM_LOADDIR, PM_LOCAL,
93 PM_LOWER, PM_NAMEREF, PM_READONLY, PM_RIGHT_B, PM_RIGHT_Z, PM_RO_BY_DESIGN, PM_SCALAR,
94 PM_SPECIAL, PM_TAGGED, PM_TAGGED_LOCAL, PM_TIED, PM_TYPE, PM_UNALIASED, PM_UNDEFINED,
95 PM_UNIQUE, PM_UNSET, PM_UPPER, PM_WARNNESTED, PM_ZSHSTORED, POSIXBUILTINS, POSIXCD, POSIXTRAPS,
96 PRINT_INCLUDEVALUE, PRINT_LINE, PRINT_LIST, PRINT_NAMEONLY, PRINT_POSIX_EXPORT,
97 PRINT_POSIX_READONLY, PRINT_TYPE, PRINT_TYPESET, PRINT_WHENCE_CSH, PRINT_WHENCE_FUNCDEF,
98 PRINT_WHENCE_SIMPLE, PRINT_WHENCE_VERBOSE, PRINT_WHENCE_WORD, PRINT_WITH_NAMESPACE,
99 PUSHDIGNOREDUPS, PUSHDMINUS, PUSHDSILENT, PUSHDTOHOME, RCQUOTES, RCS, SHINSTDIN,
100 SORTIT_BACKWARDS,
101 SORTIT_IGNORING_CASE, STAT_LOCKED, STAT_NOPRINT, STAT_STOPPED, TRAP_STATE_FORCE_RETURN,
102 TRAP_STATE_PRIMED, TYPESETSILENT, TYPESET_OPTSTR, VERBOSE, XTRACE, ZEXIT_DEFERRED,
103 ZEXIT_NORMAL, ZEXIT_SIGNAL, ZSIG_FUNC,
104};
105#[allow(unused_imports)]
106use crate::zwc::ZwcFile;
107
108// ---------------------------------------------------------------------------
109// BIN_* dispatch IDs.
110// Direct port of `Src/hashtable.h:34-70`. These are the integer
111// discriminators handlers use when one C function backs multiple
112// builtin names (e.g. `bin_fg` covers fg/bg/jobs/wait/disown).
113// ---------------------------------------------------------------------------
114
115// BIN_* constants moved to `crate::ported::hashtable_h` per the C
116// header layout (Src/hashtable.h:34-70). Re-exported here so existing
117// `crate::ported::builtin::BIN_X` paths keep resolving.
118pub use crate::ported::hashtable_h::{
119 BIN_BG, BIN_BRACKET, BIN_BREAK, BIN_CD, BIN_COMMAND, BIN_CONTINUE, BIN_DISABLE, BIN_DISOWN,
120 BIN_ECHO, BIN_ENABLE, BIN_EVAL, BIN_EXIT, BIN_EXPORT, BIN_FC, BIN_FG, BIN_JOBS, BIN_LOGOUT,
121 BIN_POPD, BIN_PRINT, BIN_PRINTF, BIN_PUSHD, BIN_PUSHLINE, BIN_R, BIN_READONLY, BIN_RETURN,
122 BIN_SCHED, BIN_SETOPT, BIN_TEST, BIN_TYPESET, BIN_UNALIAS, BIN_UNFUNCTION, BIN_UNHASH,
123 BIN_UNSET, BIN_UNSETOPT, BIN_WAIT,
124};
125
126/// Construct the builtin lookup table.
127/// Port of `createbuiltintable()` from `Src/builtin.c:150`. The C
128/// version installs the hashtable function pointers (hash, addnode,
129/// printnode, etc.) and then calls `addbuiltins("zsh", builtins, ..)`.
130/// Here we just materialise the static `BUILTINS` slice into a
131/// `HashMap<String, &builtin>` — Rust's standard hashing replaces the
132/// C `hasher` callback and the `HashMap` itself replaces all the
133/// per-table function pointers (`addnode`/`getnode`/`removenode`/...).
134// Builtin Command Hash Table Functions // c:150
135/// `createbuiltintable` — see implementation.
136pub fn createbuiltintable() -> &'static HashMap<String, &'static builtin> {
137 // c:150
138 builtintab.get_or_init(|| {
139 let table: &'static Vec<builtin> = &*BUILTINS;
140 let watch_bintab: &'static Vec<builtin> = &*crate::ported::modules::watch::bintab;
141 let mut m: HashMap<String, &'static builtin> =
142 HashMap::with_capacity(table.len() + watch_bintab.len());
143 for b in table.iter() {
144 m.insert(b.node.nam.clone(), b);
145 }
146 // zshrs auto-loads all modules at startup. Fold each module's
147 // bintab into the core builtintab so `disable <name>` (and
148 // dispatch generally) finds module-provided builtins without
149 // an explicit `zmodload` step. Mirrors C's `addbuiltins(name,
150 // bintab, sizeof(bintab)/sizeof(*bintab))` call from each
151 // module's `boot_` hook (e.g. `Src/Modules/watch.c:694`).
152 for b in watch_bintab.iter() {
153 m.insert(b.node.nam.clone(), b);
154 }
155 // zshrs extension: fold `znative` (the plugin package manager,
156 // src/extensions/pkg) into the builtin table so it is a first-class
157 // builtin — `whence -w znative` reports `builtin`, `builtin znative` works,
158 // and it dispatches through execbuiltin like any other. No C
159 // counterpart (znative is zshrs-original).
160 let pkg_bintab: &'static Vec<builtin> = &*crate::pkg::builtin::bintab;
161 for b in pkg_bintab.iter() {
162 m.insert(b.node.nam.clone(), b);
163 }
164 // zshrs extension: fold the ztest/zassert unit-test framework
165 // (src/extensions/ztest.rs) into the builtin table so `zassert_eq`,
166 // `ztest_run`, … are first-class builtins in `zshrs -f` (visible to
167 // `whence -w`, `builtin`, `disable`, completion) — not just the
168 // fusevm command-dispatch arm. No C counterpart (zshrs-original).
169 let ztest_bintab: &'static Vec<builtin> = &*crate::extensions::ztest::bintab;
170 for b in ztest_bintab.iter() {
171 m.insert(b.node.nam.clone(), b);
172 }
173 m
174 })
175}
176
177// ===========================================================
178// Direct ports of static builtin helpers from Src/builtin.c not
179// yet covered above. The Rust executor wires builtins through
180// `crate::ported::builtins::*` per-builtin modules; these free-
181// fn entries satisfy ABI/name parity for the drift gate.
182// ===========================================================
183
184/// Port of `printbuiltinnode(HashNode hn, int printflags)` from
185/// `Src/builtin.c:174`.
186///
187/// C body (c:174-194):
188/// ```c
189/// Builtin bn = (Builtin) hn;
190/// if (printflags & PRINT_WHENCE_WORD) {
191/// printf("%s: builtin\n", bn->node.nam); return;
192/// }
193/// if (printflags & PRINT_WHENCE_CSH) {
194/// printf("%s: shell built-in command\n", bn->node.nam); return;
195/// }
196/// if (printflags & PRINT_WHENCE_VERBOSE) {
197/// printf("%s is a shell builtin\n", bn->node.nam); return;
198/// }
199/// /* default is name only */
200/// printf("%s\n", bn->node.nam);
201/// ```
202pub fn printbuiltinnode(
203 hn: *mut hashnode, // c:174
204 printflags: i32,
205) {
206 if hn.is_null() {
207 return;
208 }
209 let bn = unsafe { &*hn }; // c:176
210 if (printflags & PRINT_WHENCE_WORD as i32) != 0 {
211 // c:178
212 println!("{}: builtin", bn.nam); // c:179
213 return; // c:180
214 }
215 if (printflags & PRINT_WHENCE_CSH as i32) != 0 {
216 // c:183
217 println!("{}: shell built-in command", bn.nam); // c:184
218 return; // c:185
219 }
220 if (printflags & PRINT_WHENCE_VERBOSE as i32) != 0 {
221 // c:188
222 println!("{} is a shell builtin", bn.nam); // c:189
223 return; // c:190
224 }
225 // c:193 — `/* default is name only */`
226 println!("{}", bn.nam); // c:194
227}
228
229/// Port of `freebuiltinnode(HashNode hn)` from Src/builtin.c:199.
230/// C: `static void freebuiltinnode(HashNode hn)` — free a builtin-table
231/// node only when BINF_ADDED is clear (i.e., dynamically added).
232pub fn freebuiltinnode(hn: *mut hashnode) {
233 // c:199
234 if hn.is_null() {
235 return;
236 }
237 let bn = unsafe { &*hn };
238 // c:204 — `if (!(bn->node.flags & BINF_ADDED))` then free.
239 if (bn.flags as u32 & BINF_ADDED) == 0 { // c:204
240 // Rust drop handles the actual free; nothing more to do.
241 }
242}
243
244/// Port of `init_builtins()` from Src/builtin.c:212.
245/// C: `void init_builtins(void)` — when not in EMULATE_ZSH, disable
246/// the `repeat` reserved word (compat for sh/ksh).
247///
248/// ```c
249/// if (!EMULATION(EMULATE_ZSH)) {
250/// HashNode hn = reswdtab->getnode2(reswdtab, "repeat");
251/// if (hn)
252/// reswdtab->disablenode(hn, 0);
253/// }
254/// ```
255pub fn init_builtins() {
256 // c:212
257 // c:214 — `if (!EMULATION(EMULATE_ZSH))`. EMULATION reads the
258 // canonical `emulation` global directly per zsh.h:2347.
259 if !EMULATION(EMULATE_ZSH) {
260 // c:214
261 // c:215-217 — `hn = reswdtab->getnode2(reswdtab,"repeat");
262 // if (hn) reswdtab->disablenode(hn, 0);`
263 if let Ok(mut tab) = reswdtab_lock().write() {
264 tab.disable("repeat");
265 }
266 }
267}
268
269/// Port of `OPT_ALLOC_CHUNK` from `Src/builtin.c:227`. Number of
270/// `ops->args[]` slots `new_optarg()` grows the array by when full.
271pub const OPT_ALLOC_CHUNK: i32 = 16; // c:227
272
273/// Port of `new_optarg(Options ops)` from Src/builtin.c:227.
274/// C: `static int new_optarg(Options ops)` — grow the `ops->args[]`
275/// array by `OPT_ALLOC_CHUNK` slots when full. Returns 1 on overflow
276/// (>=63 args), 0 on success.
277pub fn new_optarg(ops: &mut options) -> i32 {
278 // c:227
279 // c:227 — `if (ops->argscount == 63) return 1;`
280 if ops.argscount == 63 {
281 // c:231
282 return 1;
283 }
284 // c:232-241 — grow ops->args by OPT_ALLOC_CHUNK if argsalloc == argscount.
285 if ops.argsalloc == ops.argscount {
286 // c:232
287 ops.args
288 .resize((ops.argsalloc + OPT_ALLOC_CHUNK) as usize, String::new());
289 ops.argsalloc += OPT_ALLOC_CHUNK; // c:240
290 }
291 ops.argscount += 1; // c:243
292 0 // c:244
293}
294
295// ===========================================================
296// ksh_autoload_body moved from src/ported/vm_helper.
297// Mirrors the ksh-style autoload helper in Src/builtin.c
298// (bin_functions / load_function_def).
299// ===========================================================
300// (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.)
301
302bitflags::bitflags! {
303 /// Flags for autoloaded functions (autoload builtin -- Src/builtin.c bin_autoload).
304 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
305 pub struct AutoloadFlags: u32 {
306 const NO_ALIAS = 0b00000001; // -U: don't expand aliases
307 const ZSH_STYLE = 0b00000010; // -z: zsh-style autoload
308 const KSH_STYLE = 0b00000100; // -k: ksh-style autoload
309 const TRACE = 0b00001000; // -t: trace execution
310 const USE_CALLER_DIR = 0b00010000; // -d: use calling function's dir
311 const LOADED = 0b00100000; // function has been loaded
312 }
313}
314
315/// Port of `execbuiltin(LinkList args, LinkList assigns, Builtin bn)` from Src/builtin.c:250.
316///
317/// C: `int execbuiltin(LinkList args, LinkList assigns, Builtin bn)` —
318/// execute a builtin handler function after parsing the arguments.
319///
320/// Walks `bn->optstr` against `args`, populating `ops.ind[c]` (`|= 1`
321/// for `-X`, `|= 2` for `+X`, `<< 2` arg-index for opts taking args
322/// per the `:`/`::`/`:%` suffix convention), then calls
323/// `bn->handlerfunc(name, argv, &ops, bn->funcid)`.
324///
325/// Signature note: C consumes the name via `ugetnode(args)` first
326/// (c:262); the Rust port receives `args` without the name and reads
327/// `bn->node.nam` directly. C's `LinkList assigns` ports to
328/// `Vec<asgment>` (closer to the C type than the earlier
329/// `Vec<(String, String)>` pair-tuple). `assignfunc` handler dispatch
330/// (c:495-502) — BINF_ASSIGN builtins taking two argument lists —
331/// isn't ported (no Rust-side caller passes a non-empty `assigns`),
332/// so XTRACE prints the structure but BINF_ASSIGN dispatch falls
333/// through to the plain handler.
334pub fn execbuiltin(
335 args: Vec<String>,
336 assigns: Vec<asgment>, // c:250
337 bn: *mut builtin,
338) -> i32 {
339 if bn.is_null() {
340 return 1;
341 }
342 let bn_ref = unsafe { &*bn };
343
344 // c:252-254 — locals.
345 let pp: Option<&str>; // c:252 char *pp
346 let name: String; // c:252 char *name
347 let mut optstr: Option<String>; // c:252 char *optstr
348 let mut flags: i32; // c:253 int flags
349 let mut argc: i32; // c:253 int argc
350 let mut execop: u8; // c:253 int execop
351 let xtr: bool = isset(XTRACE); // c:253 int xtr = isset(XTRACE)
352
353 // c:256-259 — `memset(ops.ind, 0, ...); ops.args = NULL; ops.argscount=ops.argsalloc=0;`
354 let mut ops = options {
355 ind: [0u8; MAX_OPS],
356 args: Vec::new(), // c:257
357 argscount: 0,
358 argsalloc: 0,
359 }; // c:258-259
360
361 // c:262 — `name = (char *) ugetnode(args);` — Rust reads bn.node.nam.
362 name = bn_ref.node.nam.clone(); // c:262
363
364 // c:264-268 — `if (!bn->handlerfunc)` early-exit.
365 if bn_ref.handlerfunc.is_none() {
366 // c:264
367 // c:265 — DPUTS(1, "Missing builtin detected too late")
368 DPUTS!(true, "Missing builtin detected too late"); // c:265
369 // c:266 — deletebuiltin(bn->node.nam) — not yet ported here.
370 return 1; // c:267
371 }
372
373 // c:270-271 — `flags = bn->node.flags; optstr = bn->optstr;`
374 flags = bn_ref.node.flags; // c:270
375 optstr = bn_ref.optstr.clone(); // c:271
376
377 // c:275 — `argc = countlinknodes(args);` — total argv length.
378 argc = args.len() as i32; // c:275
379
380 // c:284-293 — `VARARR(char *, argarr, argc+1)` + copy args into argarr.
381 let argarr: Vec<String> = args; // c:284 argarr[]
382 let mut argv: usize = 0; // c:285 char **argv = argarr;
383
384 // c:296-411 — option parser body.
385 if let Some(ref os) = optstr.clone() {
386 // c:296
387 let optstr_local = os.clone();
388 let mut optstr_bytes: Vec<u8> = optstr_local.into_bytes();
389 let mut skipinvalid = (flags & BINF_SKIPINVALID as i32) != 0;
390 // c:297 — `char *arg = *argv;`
391 loop {
392 // c:300-303 — outer arg-by-arg loop guard:
393 // `arg && ((sense = (*arg == '-')) || ((flags & BINF_PLUSOPTS) && *arg == '+'))`.
394 let arg_str: String = match argarr.get(argv) {
395 Some(s) => s.clone(),
396 None => break,
397 };
398 let arg_bytes = arg_str.as_bytes();
399 if arg_bytes.is_empty() {
400 break;
401 }
402 let sense: i32 = if arg_bytes[0] == b'-' { 1 } else { 0 }; // c:302
403 if sense == 0
404 && !((flags & BINF_PLUSOPTS as i32) != 0 // c:303
405 && arg_bytes[0] == b'+')
406 {
407 break;
408 }
409 // c:305 — `if (!(flags & BINF_KEEPNUM) && idigit(arg[1])) break;`
410 if (flags & BINF_KEEPNUM as i32) == 0 // c:305
411 && arg_bytes.len() >= 2
412 && arg_bytes[1].is_ascii_digit()
413 {
414 break;
415 }
416 // c:308 — `if ((flags & BINF_SKIPDASH) && !arg[1]) break;`
417 if (flags & BINF_SKIPDASH as i32) != 0 && arg_bytes.len() == 1 {
418 // c:308
419 break;
420 }
421 // c:310-317 — `--` end-of-options if BINF_DASHDASHVALID.
422 if (flags & BINF_DASHDASHVALID as i32) != 0 && arg_str == "--" {
423 // c:310
424 argv += 1; // c:315
425 break; // c:316
426 }
427 // c:327-332 — `BINF_SKIPINVALID`: if any char in arg[1..] is
428 // not in optstr, the whole arg is treated as a positional.
429 if skipinvalid {
430 // c:327
431 let mut all_known = true;
432 for &c in &arg_bytes[1..] {
433 if !optstr_bytes.contains(&c) {
434 all_known = false;
435 break;
436 }
437 }
438 if !all_known {
439 break;
440 } // c:331
441 }
442 // c:335-336 — `if (arg[1] == '-') arg++;` — consume the
443 // second `-` of `--long-style`.
444 let mut k: usize = 1; // walks arg[k..]
445 if arg_bytes.len() >= 2 && arg_bytes[1] == b'-' {
446 // c:335
447 k = 2; // c:336
448 }
449 // c:337-341 — `if (!arg[1])` lone `-` / `+` indicator.
450 if arg_bytes.len() == k {
451 // c:337
452 ops.ind[b'-' as usize] = 1; // c:338
453 if sense == 0 {
454 // c:339
455 ops.ind[b'+' as usize] = 1; // c:340
456 }
457 }
458 // c:343-386 — inner loop over `*++arg` characters.
459 let mut bad_opt: Option<u8> = None;
460 while k < arg_bytes.len() {
461 // c:343
462 let c = arg_bytes[k];
463 execop = c; // c:345
464 let optptr = optstr_bytes.iter().position(|&b| b == c); // c:345 strchr(optstr,c)
465 if let Some(optidx) = optptr {
466 // c:345
467 ops.ind[c as usize] = if sense != 0 { 1 } else { 2 }; // c:346
468 // c:347 — `if (optptr[1] == ':')` — option takes arg.
469 if optidx + 1 < optstr_bytes.len() && optstr_bytes[optidx + 1] == b':' {
470 let mut argptr: Option<String> = None;
471 // c:349-352 — `if (optptr[2] == ':')` optional same-word.
472 if optidx + 2 < optstr_bytes.len() && optstr_bytes[optidx + 2] == b':' {
473 if k + 1 < arg_bytes.len() {
474 // c:350
475 argptr =
476 Some(String::from_utf8_lossy(&arg_bytes[k + 1..]).into_owned());
477 // c:351
478 }
479 } else if optidx + 2 < optstr_bytes.len()
480 && optstr_bytes[optidx + 2] == b'%'
481 {
482 // c:353-359 — `:%` numeric optional same or next word.
483 if k + 1 < arg_bytes.len() && arg_bytes[k + 1].is_ascii_digit() {
484 argptr =
485 Some(String::from_utf8_lossy(&arg_bytes[k + 1..]).into_owned());
486 } else if let Some(nxt) = argarr.get(argv + 1) {
487 if !nxt.is_empty() && nxt.as_bytes()[0].is_ascii_digit() {
488 argv += 1; // c:359 arg = *++argv
489 argptr = Some(nxt.clone());
490 }
491 }
492 } else {
493 // c:360-370 — plain `:` mandatory arg.
494 if k + 1 < arg_bytes.len() {
495 // c:362
496 argptr =
497 Some(String::from_utf8_lossy(&arg_bytes[k + 1..]).into_owned());
498 // c:363
499 } else if let Some(nxt) = argarr.get(argv + 1) {
500 argv += 1; // c:364 arg = *++argv
501 argptr = Some(nxt.clone()); // c:365
502 } else {
503 // c:366-370 — `argument expected: -%c`.
504 zwarnnam(&name, &format!("argument expected: -{}", execop as char)); // c:367-368
505 return 1; // c:369
506 }
507 }
508 if let Some(ap) = argptr {
509 // c:372
510 // c:373-377 — new_optarg overflow.
511 if new_optarg(&mut ops) != 0 {
512 // c:373
513 zwarnnam(&name, "too many option arguments"); // c:374-375
514 return 1; // c:376
515 }
516 // c:378 — `ops.ind[execop] |= ops.argscount << 2;`
517 ops.ind[execop as usize] |= (ops.argscount as u8) << 2;
518 // c:379 — `ops.args[ops.argscount-1] = argptr;`
519 ops.args[(ops.argscount - 1) as usize] = ap;
520 // c:380-381 — `while (arg[1]) arg++;` consume the rest.
521 k = arg_bytes.len();
522 }
523 }
524 k += 1;
525 } else {
526 bad_opt = Some(c); // c:385 break
527 break;
528 }
529 }
530 // c:389-394 — if we exited mid-arg on a bad char, emit "bad option".
531 if let Some(badc) = bad_opt {
532 // c:389
533 zwarnnam(
534 &name,
535 &format!(
536 "bad option: {}{}",
537 if sense != 0 { '-' } else { '+' },
538 badc as char
539 ),
540 ); // c:392
541 return 1; // c:393
542 }
543 // c:395 — `arg = *++argv;`
544 argv += 1; // c:395
545 // c:398-402 — BINF_PRINTOPTS R-mode switch to "ne" optstr.
546 if (flags & BINF_PRINTOPTS as i32) != 0 // c:398
547 && ops.ind[b'R' as usize] != 0
548 && ops.ind[b'f' as usize] == 0
549 {
550 optstr_bytes = b"ne".to_vec(); // c:400
551 flags |= BINF_SKIPINVALID as i32; // c:401
552 skipinvalid = true;
553 }
554 // c:404-405 — `if (ops.ind['-']) break;` — `--` terminates.
555 if ops.ind[b'-' as usize] != 0 {
556 // c:404
557 break;
558 }
559 }
560 let _ = optstr_bytes;
561 } else if (flags & BINF_HANDLES_OPTS as i32) == 0 // c:407
562 && argarr.get(argv).map(|s| s == "--").unwrap_or(false)
563 {
564 // c:408
565 // c:409-410 — `ops.ind['-'] = 1; argv++;`
566 ops.ind[b'-' as usize] = 1; // c:409
567 argv += 1; // c:410
568 }
569 // Suppress optstr-unused warnings on the `else` path.
570 let _ = optstr.take();
571
572 // c:414-421 — apply `bn->defopts` defaults.
573 pp = bn_ref.defopts.as_deref(); // c:414
574 if let Some(pp_str) = pp {
575 // c:414
576 for &b in pp_str.as_bytes() {
577 // c:415
578 if ops.ind[b as usize] == 0 {
579 // c:417
580 ops.ind[b as usize] = 1; // c:418
581 }
582 }
583 }
584
585 // c:424 — `argc -= argv - argarr;` — subtract consumed flag args.
586 argc -= argv as i32; // c:424
587
588 // c:426-429 — errflag check. C zsh clears here because the
589 // *outer* execcmd_exec (Src/exec.c:3468) already bailed without
590 // calling the builtin when errflag was set after prefork; the
591 // clear at c:427 is paired with an earlier-arrived-at builtin
592 // entry (signal-driven, not prefork-driven).
593 //
594 // zshrs's bytecode flow always reaches the builtin call site
595 // even when arg expansion set errflag (the c:3468 pre-dispatch
596 // gate has no equivalent in the bytecode). Clearing errflag
597 // here would swallow the prefork error and let the post-command
598 // ERREXIT_CHECK pass — `set -u; echo $undefined; echo done`
599 // would run `echo done` instead of aborting.
600 //
601 // Non-interactive shells must preserve errflag so the
602 // immediately-following ERREXIT_CHECK aborts the script (c:Src/
603 // init.c:234 `((!interact || sourcelevel) && errflag)` break).
604 let ef = errflag.load(Relaxed);
605 if (ef & ERRFLAG_ERROR) != 0 {
606 if !isset(INTERACTIVE) {
607 return 1;
608 }
609 errflag.fetch_and(!ERRFLAG_ERROR, Relaxed); // c:427
610 return 1; // c:428
611 }
612
613 // c:432-436 — argc bounds check.
614 if argc < bn_ref.minargs // c:432
615 || (argc > bn_ref.maxargs && bn_ref.maxargs != -1)
616 {
617 zwarnnam(
618 &name, // c:433
619 if argc < bn_ref.minargs {
620 "not enough arguments"
621 } else {
622 "too many arguments"
623 },
624 ); // c:434
625 return 1; // c:435
626 }
627
628 // c:438-494 — display execution trace information, if required.
629 if xtr {
630 // c:439
631 // c:440-441 — `char **fullargv = argarr;` — use FULL argv
632 // (including consumed option words) so XTRACE shows what the
633 // user typed, not the option-stripped tail.
634 let fullargv = &argarr; // c:441
635 // All emits below are `fprintf(xtrerr, …)` / `fputc(…, xtrerr)` in
636 // C: append to the xtrerr line buffer and flush ONCE at the
637 // trailing `\n` (c:492-493), so the builtin's trace line reaches
638 // the shared stderr fd in a single write — a forked pipeline
639 // stage never interleaves with a concurrent one.
640 use crate::fusevm_bridge::{xtrerr_flush, xtrerr_fputs};
641 printprompt4(); // c:442 — buffers PS4 into xtrerr
642 // c:443 — `fprintf(xtrerr, "%s", name);`
643 xtrerr_fputs(&name); // c:443
644 // c:444-447 — `while (*fullargv) { fputc(' ',xtrerr); quotedzputs(...); }`
645 // C zsh's parser pre-splits `name=value` args for
646 // BINF_ASSIGN-flagged builtins (export/typeset/declare/local/
647 // readonly/integer/float) into asg{name,value} nodes, which
648 // the c:448-491 branch emits as
649 // `quotedzputs(name) + '=' + quotedzputs(value)` (c:453,487-488).
650 // zshrs's compiler doesn't populate `assigns`; args arrive
651 // here as the unsplit `"name=value"` whole-strings, so the
652 // c:443-446 path quotes the entire string and produces
653 // `export 'VAR=val'` instead of `export VAR=val`. Inline the
654 // same split here — the prefix is a legal scalar/array name
655 // (ident, optionally `[subscript]`) followed by `=` or
656 // `+=`, and the suffix is the value. Detection mirrors the
657 // C parser at Src/lex.c:2169 (gettokstr ASSIGN recognition)
658 // and is faithful to what the C path would have produced
659 // from a pre-split asg node.
660 let is_assign = (bn_ref.node.flags as u32 & BINF_ASSIGN) != 0;
661 for s in fullargv {
662 // c:444
663 xtrerr_fputs(" "); // c:445 fputc(' ', xtrerr)
664 let mut emitted = false;
665 if is_assign {
666 let sbytes = s.as_bytes();
667 // Walk the ident prefix.
668 let mut i = 0usize;
669 if !sbytes.is_empty() && (sbytes[0].is_ascii_alphabetic() || sbytes[0] == b'_') {
670 i = 1;
671 while i < sbytes.len()
672 && (sbytes[i].is_ascii_alphanumeric() || sbytes[i] == b'_')
673 {
674 i += 1;
675 }
676 // Optional `[subscript]` — bracket-balanced.
677 if i < sbytes.len() && sbytes[i] == b'[' {
678 let mut depth = 1i32;
679 i += 1;
680 while i < sbytes.len() && depth > 0 {
681 match sbytes[i] {
682 b'[' => depth += 1,
683 b']' => depth -= 1,
684 _ => {}
685 }
686 i += 1;
687 }
688 if depth != 0 {
689 i = 0; // unbalanced — bail to default path
690 }
691 }
692 if i > 0 {
693 // Match `=` or `+=` separator.
694 let (sep_len, sep) =
695 if sbytes.get(i) == Some(&b'+') && sbytes.get(i + 1) == Some(&b'=') {
696 (2usize, "+=")
697 } else if sbytes.get(i) == Some(&b'=') {
698 (1usize, "=")
699 } else {
700 (0, "")
701 };
702 if sep_len != 0 {
703 // c:453,487-488 — emit name + sep + quoted(value).
704 xtrerr_fputs(&format!(
705 "{}{}{}",
706 &s[..i],
707 sep,
708 quotedzputs(&s[i + sep_len..])
709 ));
710 emitted = true;
711 }
712 }
713 }
714 }
715 if !emitted {
716 xtrerr_fputs("edzputs(s)); // c:446
717 }
718 }
719 // c:448-491 — `if (assigns) { for (node = firstnode(assigns); ...) }`.
720 for asg in &assigns {
721 // c:450 firstnode/incnode
722 xtrerr_fputs(" "); // c:452 fputc(' ', xtrerr)
723 xtrerr_fputs("edzputs(&asg.name)); // c:453
724 if (asg.flags & ASG_ARRAY) != 0 {
725 // c:454
726 xtrerr_fputs("=("); // c:455
727 if let Some(ref list) = asg.array {
728 // c:456
729 if (asg.flags & ASG_KEY_VALUE) != 0 {
730 // c:457
731 // c:458-473 — `LinkNode keynode, valnode;` walk
732 // alternating key/value pairs, emitting
733 // `[key]=value` per pair. Uses the typed
734 // `LinkList<String>` accessors from
735 // `src/ported/linklist.rs` which port the
736 // `firstnode` / `nextnode` / `getdata` macros
737 // from `Src/zsh.h:576-588`.
738 let mut keynode = list.firstnode(); // c:459
739 loop {
740 // c:460
741 // c:461-462 — `if (!keynode) break;`
742 let kidx = match keynode {
743 // c:461
744 Some(i) => i,
745 None => break, // c:462
746 };
747 // c:463-465 — `valnode = nextnode(keynode); if (!valnode) break;`
748 let vidx = match list.nextnode(kidx) {
749 // c:463
750 Some(i) => i,
751 None => break, // c:465
752 };
753 // c:466-468 — `fputc('['); quotedzputs(getdata(keynode));`
754 xtrerr_fputs("["); // c:466
755 if let Some(k) = list.getdata(kidx) {
756 // c:467 getdata
757 xtrerr_fputs("edzputs(k));
758 // c:467
759 }
760 // c:469 — `fprintf(stderr, "]=");`
761 xtrerr_fputs("]="); // c:469
762 // c:470-471 — `quotedzputs(getdata(valnode));`
763 if let Some(v) = list.getdata(vidx) {
764 // c:470
765 xtrerr_fputs("edzputs(v));
766 // c:470
767 }
768 // c:472 — `keynode = nextnode(valnode);`
769 keynode = list.nextnode(vidx); // c:472
770 }
771 } else {
772 // c:474
773 // c:475-482 — plain array emit: walk every node
774 // and emit ` <quotedzputs(elem)>`.
775 let mut arrnode = list.firstnode(); // c:476
776 while let Some(idx) = arrnode {
777 // c:477
778 xtrerr_fputs(" "); // c:479 fputc(' ', xtrerr)
779 if let Some(elem) = list.getdata(idx) {
780 // c:480 getdata
781 xtrerr_fputs("edzputs(elem));
782 // c:480
783 }
784 arrnode = list.nextnode(idx); // c:478 incnode
785 }
786 }
787 }
788 xtrerr_fputs(" )"); // c:485
789 } else if let Some(ref scalar) = asg.scalar {
790 // c:486
791 xtrerr_fputs("="); // c:487 fputc('=', xtrerr)
792 xtrerr_fputs("edzputs(scalar)); // c:488
793 }
794 }
795 // c:492-493 — `fputc('\n', xtrerr); fflush(xtrerr);`
796 xtrerr_fputs("\n"); // c:492 fputc('\n', xtrerr)
797 xtrerr_flush(); // c:493 fflush(xtrerr) — one write for the whole line
798 }
799
800 // c:506 — `return (*(bn->handlerfunc))(name, argv, &ops, bn->funcid);`
801 let trimmed: Vec<String> = argarr[argv..].to_vec();
802 let handler = bn_ref.handlerfunc.expect("handlerfunc checked at c:264");
803 handler(&name, &trimmed, &ops, bn_ref.funcid) // c:506
804}
805
806/// Port of `bin_enable(char *name, char **argv, Options ops, int func)` from Src/builtin.c:517.
807/// C: `int bin_enable(char *name, char **argv, Options ops, int func)` —
808/// enable/disable hashtab entries (default builtins; `-f`/`-r`/`-s`/`-a`
809/// pick alternate tables); `-p` routes to pat_enables (pattern toggles).
810/// WARNING: param names don't match C — Rust=(name, argv, func) vs C=(name, argv, ops, func)
811pub fn bin_enable(
812 name: &str,
813 argv: &[String], // c:517
814 ops: &options,
815 func: i32,
816) -> i32 {
817 enum Tab {
818 Builtin,
819 Shfunc,
820 Reswd,
821 Alias,
822 SufAlias,
823 }
824 let mut returnval = 0i32; // c:524
825 let mut match_count = 0i32; // c:524
826 // c:527-538 — `-p` early-out + table selection.
827 if OPT_ISSET(ops, b'p') {
828 // c:527
829 // c:528 — `return pat_enables(name, argv, func == BIN_ENABLE);`
830 return pat_enables(name, argv, func == BIN_ENABLE); // c:528
831 }
832 let tab = if OPT_ISSET(ops, b'f') {
833 Tab::Shfunc
834 }
835 // c:529
836 else if OPT_ISSET(ops, b'r') {
837 Tab::Reswd
838 }
839 // c:531
840 else if OPT_ISSET(ops, b's') {
841 Tab::SufAlias
842 }
843 // c:533
844 else if OPT_ISSET(ops, b'a') {
845 Tab::Alias
846 }
847 // c:535
848 else {
849 Tab::Builtin
850 }; // c:537
851
852 // c:540-547 — flags1/flags2 set based on enable vs disable direction.
853 let enable = func == BIN_ENABLE;
854 let (flags1, flags2) = if enable {
855 // c:541
856 (0u32, DISABLED as u32) // c:542
857 } else {
858 (DISABLED as u32, 0u32) // c:545
859 };
860
861 // Helper closures over the chosen table.
862 let toggle_one = |tab: &Tab, nm: &str, on: bool| -> bool {
863 match tab {
864 Tab::Alias => aliastab_lock()
865 .write()
866 .map(|mut t| if on { t.enable(nm) } else { t.disable(nm) })
867 .unwrap_or(false),
868 Tab::SufAlias => sufaliastab_lock()
869 .write()
870 .map(|mut t| if on { t.enable(nm) } else { t.disable(nm) })
871 .unwrap_or(false),
872 // c:541-547 — `enable`/`disable -r` toggles DISABLED on the
873 // reswdtab entry; reswords resolve through getreswdnode in
874 // the lexer so toggling here is enough to mask/unmask.
875 Tab::Reswd => {
876 let exists = reswdtab_lock()
877 .read()
878 .map(|t| t.get_including_disabled(nm).is_some())
879 .unwrap_or(false);
880 if !exists {
881 return false;
882 }
883 reswdtab_lock()
884 .write()
885 .map(|mut t| if on { t.enable(nm) } else { t.disable(nm) })
886 .unwrap_or(false)
887 }
888 // c:541-547 — `enable`/`disable -f` toggles DISABLED on the
889 // shfunctab entry; ports to disableshfuncnode/enableshfuncnode
890 // which also unsettrap/settrap TRAP* ported.
891 Tab::Shfunc => {
892 let exists = shfunctab_lock()
893 .read()
894 .map(|t| t.get_including_disabled(nm).is_some())
895 .unwrap_or(false);
896 if !exists {
897 return false;
898 }
899 if on {
900 crate::ported::hashtable::enableshfuncnode(nm);
901 } else {
902 crate::ported::hashtable::disableshfuncnode(nm);
903 }
904 true
905 }
906 // c:541-547 — `enable`/`disable` toggles DISABLED on the
907 // builtin. The C struct `builtintab` stores DISABLED in
908 // `node.flags`; Rust port keeps `builtintab` as an
909 // immutable static lookup and tracks the disabled set in
910 // BUILTINS_DISABLED so dispatch can mask the entry.
911 Tab::Builtin => {
912 if createbuiltintable().get(nm).is_none() {
913 return false;
914 }
915 if let Ok(mut set) = BUILTINS_DISABLED.lock() {
916 if on {
917 set.remove(nm);
918 } else {
919 set.insert(nm.to_string());
920 }
921 return true;
922 }
923 false
924 }
925 }
926 };
927 let collect_names = |tab: &Tab| -> Vec<String> {
928 match tab {
929 Tab::Alias => aliastab_lock()
930 .read()
931 .map(|t| t.iter().map(|(n, _)| n.clone()).collect())
932 .unwrap_or_default(),
933 Tab::SufAlias => sufaliastab_lock()
934 .read()
935 .map(|t| t.iter().map(|(n, _)| n.clone()).collect())
936 .unwrap_or_default(),
937 Tab::Reswd => reswdtab_lock()
938 .read()
939 .map(|t| t.iter().map(|(n, _)| n.clone()).collect())
940 .unwrap_or_default(),
941 Tab::Shfunc => shfunctab_lock()
942 .read()
943 .map(|t| t.iter().map(|(n, _)| n.clone()).collect())
944 .unwrap_or_default(),
945 Tab::Builtin => createbuiltintable().keys().cloned().collect(),
946 }
947 };
948
949 // c:553-558 — no-args list.
950 if argv.is_empty() {
951 // c:553
952 queue_signals(); // c:554
953 // c:555 — `scanhashtable(ht, 1, flags1, flags2, ht->printnode, 0);`
954 // Filter: print only entries where (flags & flags1) == flags1
955 // && (flags & flags2) == 0. For enable/disable, flags1 and
956 // flags2 are DISABLED-bit selectors that mask the listed set
957 // to ONLY the kind being toggled (enable lists enabled,
958 // disable lists disabled).
959 let is_disabled = |nm: &str| -> bool {
960 match tab {
961 Tab::Alias => aliastab_lock()
962 .read()
963 .ok()
964 .and_then(|t| {
965 t.get_including_disabled(nm)
966 .map(|a| (a.node.flags & DISABLED as i32) != 0)
967 })
968 .unwrap_or(false),
969 Tab::SufAlias => sufaliastab_lock()
970 .read()
971 .ok()
972 .and_then(|t| {
973 t.get_including_disabled(nm)
974 .map(|a| (a.node.flags & DISABLED as i32) != 0)
975 })
976 .unwrap_or(false),
977 Tab::Reswd => reswdtab_lock()
978 .read()
979 .ok()
980 .and_then(|t| {
981 t.get_including_disabled(nm)
982 .map(|r| (r.node.flags & DISABLED as i32) != 0)
983 })
984 .unwrap_or(false),
985 Tab::Shfunc => shfunctab_lock()
986 .read()
987 .ok()
988 .and_then(|t| {
989 t.get_including_disabled(nm)
990 .map(|f| (f.node.flags & DISABLED as i32) != 0)
991 })
992 .unwrap_or(false),
993 Tab::Builtin => BUILTINS_DISABLED
994 .lock()
995 .map(|s| s.contains(nm))
996 .unwrap_or(false),
997 }
998 };
999 // c:Src/builtin.c — `scanhashtable(ht, 1, …)` walks the
1000 // table in sorted order (the first arg `sorted=1`). Mirror
1001 // by sorting the collected names before the per-name walk.
1002 // Without this, builtin/reswd listings came out in HashMap
1003 // iteration order, diverging from zsh.
1004 let mut all_names = collect_names(&tab);
1005 all_names.sort();
1006 for nm in all_names {
1007 let dis = is_disabled(&nm);
1008 let entry_flags = if dis { DISABLED as u32 } else { 0 };
1009 if (entry_flags & flags1) == flags1 && (entry_flags & flags2) == 0 {
1010 // c:Src/builtin.c — `ht->printnode` dispatches per
1011 // table type. For alias/sufalias, printaliasnode
1012 // (hashtable.rs:1477) prints `name=value`. For
1013 // builtin/reswd/shfunc, just the name. Previous
1014 // Rust port printed bare names for all kinds, so
1015 // `enable -a` lost the alias definitions. Mirror
1016 // C's printnode dispatch here.
1017 match tab {
1018 Tab::Alias => {
1019 let val = aliastab_lock()
1020 .read()
1021 .ok()
1022 .and_then(|t| t.get_including_disabled(&nm).map(|a| a.text.clone()));
1023 if let Some(v) = val {
1024 println!("{}={}", nm, quotedzputs(&v));
1025 } else {
1026 println!("{}", nm);
1027 }
1028 }
1029 Tab::SufAlias => {
1030 let val = sufaliastab_lock()
1031 .read()
1032 .ok()
1033 .and_then(|t| t.get_including_disabled(&nm).map(|a| a.text.clone()));
1034 if let Some(v) = val {
1035 println!("{}={}", nm, quotedzputs(&v));
1036 } else {
1037 println!("{}", nm);
1038 }
1039 }
1040 _ => println!("{}", nm),
1041 }
1042 }
1043 }
1044 unqueue_signals(); // c:556
1045 return 0; // c:557
1046 }
1047
1048 // c:561-580 — `-m` glob branch.
1049 if OPT_ISSET(ops, b'm') {
1050 // c:561
1051 for arg in argv {
1052 // c:562
1053 queue_signals(); // c:563
1054 let pprog = patcompile(
1055 &{
1056 let mut __pat_tok = (arg).to_string();
1057 crate::ported::glob::tokenize(&mut __pat_tok);
1058 __pat_tok
1059 }, // c:566
1060 PAT_HEAPDUP,
1061 None,
1062 );
1063 if let Some(prog) = pprog {
1064 for nm in collect_names(&tab) {
1065 if pattry(&prog, &nm) {
1066 // c:567
1067 if toggle_one(&tab, &nm, enable) {
1068 match_count += 1; // c:567
1069 }
1070 }
1071 }
1072 } else {
1073 zwarnnam(name, &format!("bad pattern : {}", arg)); // c:572
1074 returnval = 1; // c:573
1075 }
1076 unqueue_signals(); // c:575
1077 }
1078 if match_count == 0 {
1079 // c:579
1080 returnval = 1; // c:580
1081 }
1082 return returnval; // c:581
1083 }
1084
1085 // c:585-594 — literal-name dispatch.
1086 queue_signals(); // c:585
1087 for arg in argv {
1088 // c:586
1089 if !toggle_one(&tab, arg, enable) {
1090 // c:587
1091 zwarnnam(name, &format!("no such hash table element: {}", arg)); // c:590
1092 returnval = 1; // c:591
1093 }
1094 }
1095 unqueue_signals(); // c:594
1096 returnval // c:595
1097}
1098
1099/// Port of `bin_set(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:601.
1100/// C: `int bin_set(char *nam, char **args, UNUSED(Options ops),
1101/// UNUSED(int func))` — set shell options, declare arrays,
1102/// replace positional params, or display variables.
1103/// WARNING: param names don't match C — Rust=(nam, args, _func) vs C=(nam, args, ops, func)
1104pub fn bin_set(
1105 nam: &str,
1106 args: &[String], // c:601
1107 _ops: &options,
1108 _func: i32,
1109) -> i32 {
1110 // PFA-SMR aspect: emit setopt/unsetopt events for the POSIX
1111 // `set -o NAME` / `set +o NAME` form. This is the third option
1112 // syntax (alongside setopt NAME / unsetopt NAME); a recorder
1113 // user expects all three to surface in `zwhere -k setopt`.
1114 #[cfg(feature = "recorder")]
1115 if crate::recorder::is_enabled() && !args.is_empty() {
1116 let ctx = crate::recorder::recorder_ctx_global();
1117 let mut iter = args.iter().peekable();
1118 while let Some(a) = iter.next() {
1119 match a.as_str() {
1120 "-o" => {
1121 if let Some(name) = iter.next() {
1122 crate::recorder::emit_setopt(name, ctx.clone());
1123 }
1124 }
1125 "+o" => {
1126 if let Some(name) = iter.next() {
1127 crate::recorder::emit_unsetopt(name, ctx.clone());
1128 }
1129 }
1130 _ => {}
1131 }
1132 }
1133 }
1134
1135 let mut argv: Vec<String> = args.to_vec();
1136 let mut hadopt = false; // c:603
1137 let mut hadplus = false; // c:603
1138 let mut hadend = false; // c:603
1139 let mut sort: i32 = 0; // c:603
1140 let mut array: i32 = 0; // c:603
1141 let mut arrayname: Option<String> = None; // c:604
1142
1143 // c:608-614 — sh-compat: bare `set -` → +xv.
1144 if !EMULATION(EMULATE_ZSH) // c:608
1145 && !argv.is_empty() && argv[0] == "-"
1146 {
1147 // c:610-611 — `dosetopt(VERBOSE, 0, 0, opts); dosetopt(XTRACE, 0, 0, opts);`
1148 dosetopt(VERBOSE, 0, 0); // c:610
1149 dosetopt(XTRACE, 0, 0); // c:611
1150 if argv.len() == 1 {
1151 return 0;
1152 } // c:612-613
1153 argv.remove(0);
1154 }
1155
1156 // c:617-668 — top-level option-arg loop.
1157 let mut idx = 0usize;
1158 'outer: while idx < argv.len() // c:617
1159 && (argv[idx].starts_with('-') || argv[idx].starts_with('+'))
1160 {
1161 let arg = argv[idx].clone();
1162 let action = arg.starts_with('-'); // c:619
1163 if !action {
1164 hadplus = true;
1165 } // c:620
1166 // c:621-622 — bare `-` / `+` → "--"
1167 let body: String = if arg.len() == 1 {
1168 "--".to_string()
1169 } else {
1170 arg.clone()
1171 };
1172 // c:623 — `while (*++*args)`
1173 let chars: Vec<char> = body[1..].chars().collect();
1174 let mut ci = 0usize;
1175 while ci < chars.len() {
1176 // c:623
1177 let c = chars[ci];
1178 if c != '-' || action {
1179 hadopt = true;
1180 } // c:626
1181 // c:628-632 — `--` end-of-options.
1182 if c == '-' {
1183 // c:628
1184 hadend = true; // c:629
1185 idx += 1; // c:630 args++
1186 break 'outer;
1187 }
1188 // c:633-645 — `o` long-option name follows.
1189 if c == 'o' {
1190 // c:633
1191 let optname: String = if ci + 1 < chars.len() {
1192 chars[ci + 1..].iter().collect::<String>()
1193 } else {
1194 idx += 1;
1195 if idx >= argv.len() {
1196 // c:636-637 — `printoptionstates(hadplus);
1197 // inittyptab(); return 0;`. The Rust port
1198 // previously returned 0 without printing,
1199 // so `set -o` / `set +o` (no option name)
1200 // emitted nothing instead of the full
1201 // option-state listing (one per line).
1202 // hadplus is true when the user invoked
1203 // `set +o` (POSIX-output form); else `set -o`
1204 // (zsh-output form).
1205 crate::ported::options::printoptionstates(hadplus);
1206 return 0;
1207 }
1208 argv[idx].clone()
1209 };
1210 let optno = optlookup(&optname); // c:642
1211 if optno == 0 {
1212 // c:642 — C: `zerrnam(nam, "no such option: %s", *args)`.
1213 // zwarnnam emits `zsh:<nam>:<lineno>:` prefix
1214 // (vs zerr's bare `zsh:<lineno>:`); use the
1215 // canonical form so `set +o BAD` matches zsh's
1216 // diagnostic format. Return 1 so $? reflects the
1217 // failed lookup (C's execbuiltin checks errflag
1218 // after the call; zshrs's bridge bases on the
1219 // return value).
1220 zerrnam(nam, &format!("no such option: {}", optname));
1221 unqueue_signals();
1222 return 1;
1223 } else if dosetopt(optno, if action { 1 } else { 0 }, 0) != 0
1224 // c:644
1225 {
1226 zerrnam(nam, &format!("can't change option: {}", optname));
1227 unqueue_signals();
1228 return 1;
1229 }
1230 break;
1231 }
1232 // c:646-657 — `A` array-mode (with optional name arg).
1233 if c == 'A' {
1234 // c:646
1235 array = if action { 1 } else { -1 }; // c:649
1236 let nameopt: Option<String> = if ci + 1 < chars.len() {
1237 Some(chars[ci + 1..].iter().collect::<String>())
1238 } else if idx + 1 < argv.len() {
1239 idx += 1;
1240 Some(argv[idx].clone())
1241 } else {
1242 None
1243 };
1244 arrayname = nameopt.clone();
1245 if arrayname.is_none() {
1246 // c:651
1247 idx += 1;
1248 break 'outer;
1249 }
1250 let ksharrays = isset(KSHARRAYS);
1251 if !ksharrays {
1252 // c:653
1253 idx += 1; // c:655 args++
1254 break 'outer; // c:656
1255 }
1256 break;
1257 }
1258 // c:659-660 — `s` sort flag.
1259 if c == 's' {
1260 // c:659
1261 sort = if action { 1 } else { -1 }; // c:660
1262 } else {
1263 // c:662-666 — short-option letter: optlookupc + dosetopt.
1264 // c:Src/builtin.c — zerrnam(nam, ...) prefixes the
1265 // builtin's name into the error tag ("zsh:set:1:" vs
1266 // bare "zsh:1:"). Use zerrnam to match zsh's exact
1267 // error format for `set -X 2>&1`.
1268 let optno = crate::ported::options::optlookupc(c); // c:663
1269 if optno == 0 {
1270 // c:663
1271 zerrnam(nam, &format!("bad option: -{}", c)); // c:663
1272 } else if dosetopt(optno, if action { 1 } else { 0 }, 0) != 0
1273 // c:664
1274 {
1275 zerrnam(nam, &format!("can't change option: -{}", c)); // c:664
1276 }
1277 }
1278 ci += 1;
1279 }
1280 idx += 1; // c:668
1281 }
1282 // c:Src/builtin.c:668 — `if (errflag) return 1;`. A bad / can't-change
1283 // option (or `no such option`) reported via zerrnam set ERRFLAG_ERROR;
1284 // `set -Z` must exit 1, not fall through to the (success) display path.
1285 if (crate::ported::utils::errflag.load(std::sync::atomic::Ordering::Relaxed)
1286 & crate::ported::utils::ERRFLAG_ERROR)
1287 != 0
1288 {
1289 return 1;
1290 }
1291 let _ = nam;
1292
1293 // c:676 — `queue_signals();`
1294 queue_signals();
1295 let remaining = &argv[idx..];
1296
1297 // c:678-694 — display path when no array/no args.
1298 if arrayname.is_none() {
1299 // c:678
1300 if !hadopt && remaining.is_empty() {
1301 // c:679
1302 // c:680-681 — `scanhashtable(paramtab, 1, 0, 0,
1303 // paramtab->printnode, hadplus ? PRINT_NAMEONLY : 0);`
1304 //
1305 // C walks the paramtab (sorted=1 → alphabetical). The previous
1306 // Rust port walked `std::env::vars()` — the OS environment.
1307 // Shell-internal vars (not exported to env) would never appear
1308 // in the `set` listing, diverging from C where ALL paramtab
1309 // entries are emitted.
1310 //
1311 // Same family of bug as the prior bin_unset -m fix.
1312 //
1313 // c:Src/builtins.c::printparamnode — dispatches the value
1314 // read through the param's gsu_s.getfn rather than reading
1315 // `pm.u_str` directly. Special params like `!`, `$`, `#`,
1316 // `-`, `0`, `?` have empty `u_str` slots because their live
1317 // value lives behind getfn (libc syscall, LASTVAL, pparams
1318 // count, etc.). Bug #463: zshrs read u_str directly so the
1319 // dump showed `!=''`, `$=''` etc. instead of the actual
1320 // values. Route through `getsparam` so the canonical
1321 // special-param dispatch (lookup_special_var → getfn shim)
1322 // fires.
1323 let names: Vec<String> = {
1324 let tab = paramtab().read().unwrap();
1325 tab.iter()
1326 .filter(|(_, pm)| (pm.node.flags as u32 & PM_UNSET) == 0)
1327 .map(|(k, _)| k.clone())
1328 .collect()
1329 };
1330 let mut entries: Vec<(String, String)> = names
1331 .into_iter()
1332 .map(|k| {
1333 let v = crate::ported::params::getsparam(&k).unwrap_or_default();
1334 (k, v)
1335 })
1336 .collect();
1337 // c:680 sorted=1 → meta-aware sort via hnamcmp (already fixed
1338 // to use ztrcmp earlier in the series).
1339 entries.sort_by(|a, b| hnamcmp(&a.0, &b.0));
1340 for (k, v) in entries {
1341 // c:Src/params.c::printparamnode — single-char names
1342 // that double as shell metacharacters (`#` comment,
1343 // `$` substitution, `*` glob, `?` glob, `@` splat)
1344 // are wrapped in single quotes so the output
1345 // round-trips through the shell parser. Other
1346 // single-char specials (`!`, `-`, `0`) appear bare
1347 // because they're unambiguous in name position.
1348 // Verified against /opt/homebrew/bin/zsh `set` output.
1349 let needs_quote_name = matches!(k.as_str(), "#" | "$" | "*" | "?" | "@");
1350 let kq: String = if needs_quote_name {
1351 format!("'{}'", k)
1352 } else {
1353 k.clone()
1354 };
1355 if hadplus {
1356 // c:681 PRINT_NAMEONLY
1357 println!("{}", kq);
1358 } else if matches!(k.as_str(), "*" | "@" | "argv") {
1359 // c:Src/params.c — `*` / `@` / `argv` are array-
1360 // shaped (positional params); print as `( e1 e2 …)`
1361 // mirroring zsh's array form so `set` output
1362 // round-trips. Without this special-case, zshrs
1363 // showed `'*'='1 2'` (joined scalar) instead of
1364 // zsh's `'*'=( 1 2 )` (array splat).
1365 let pp = PPARAMS.lock().ok();
1366 let elems: Vec<String> = pp
1367 .as_ref()
1368 .map(|p| p.iter().cloned().collect())
1369 .unwrap_or_default();
1370 let body: String = elems
1371 .iter()
1372 .map(|e| quotedzputs(e))
1373 .collect::<Vec<_>>()
1374 .join(" ");
1375 if elems.is_empty() {
1376 println!("{}=( )", kq);
1377 } else {
1378 println!("{}=( {} )", kq, body);
1379 }
1380 } else {
1381 println!("{}={}", kq, quotedzputs(&v));
1382 }
1383 }
1384 }
1385 if array != 0 {
1386 // c:684
1387 // c:685-687 — `scanhashtable(paramtab, 1, PM_ARRAY, 0,
1388 // paramtab->printnode, hadplus ? PRINT_NAMEONLY : 0)`.
1389 // Walk paramtab filtering by PM_ARRAY and emit each as
1390 // `name=(elem1 elem2 ...)`. Previous Rust port stubbed
1391 // this body with a "nothing to enumerate" comment — but
1392 // paramtab does store arrays in `u_arr`, so `set -A` (no
1393 // name) MUST list every PM_ARRAY entry. Sorted via
1394 // hnamcmp (meta-aware compare) per `sorted=1` in the C
1395 // scanhashtable call.
1396 let mut arr_entries: Vec<(String, Vec<String>)> = {
1397 use {PM_ARRAY, PM_TYPE};
1398 let tab = paramtab().read().unwrap();
1399 tab.iter()
1400 .filter(|(_, pm)| {
1401 PM_TYPE(pm.node.flags as u32) == PM_ARRAY
1402 && (pm.node.flags as u32 & PM_UNSET) == 0
1403 })
1404 .map(|(k, pm)| (k.clone(), pm.u_arr.clone().unwrap_or_default()))
1405 .collect()
1406 };
1407 arr_entries.sort_by(|a, b| hnamcmp(&a.0, &b.0)); // c:685 sorted=1
1408 for (k, arr) in arr_entries {
1409 if hadplus {
1410 // c:686 PRINT_NAMEONLY
1411 println!("{}", k);
1412 } else {
1413 let quoted: Vec<String> = arr.iter().map(|v| quotedzputs(v)).collect();
1414 println!("{}=({})", k, quoted.join(" "));
1415 }
1416 }
1417 }
1418 if remaining.is_empty() && !hadend {
1419 // c:688
1420 unqueue_signals();
1421 return 0; // c:690
1422 }
1423 }
1424
1425 // c:693-695 — `set -s` sort.
1426 let sorted: Vec<String> = if sort != 0 {
1427 let mut v = remaining.to_vec();
1428 if sort < 0 {
1429 v.sort_by(|a, b| b.cmp(a));
1430 } else {
1431 v.sort();
1432 }
1433 v
1434 } else {
1435 remaining.to_vec()
1436 };
1437
1438 // c:696-708 — array assign or positional-param replace.
1439 if array != 0 {
1440 // c:696
1441 // c:697-708 — build array; `array < 0` appends to existing $name.
1442 let aname = arrayname.unwrap_or_default();
1443 let mut new_arr: Vec<String> = sorted;
1444 if array < 0 {
1445 // c:701
1446 // c:702-704 — `if ((a = getaparam(arrayname)) && arrlen_gt(a, len))`.
1447 // Read paramtab.u_arr directly; was using `:`-
1448 // split env value as a fake array.
1449 let existing: Vec<String> = {
1450 let tab = paramtab().read().unwrap();
1451 tab.get(&aname)
1452 .and_then(|pm| pm.u_arr.clone())
1453 .unwrap_or_default()
1454 };
1455 if existing.len() > new_arr.len() {
1456 // c:702
1457 new_arr.extend(existing.into_iter().skip(new_arr.len())); // c:703
1458 }
1459 }
1460 // c:709 — `setaparam(arrayname, x)`. In C, setaparam →
1461 // assignaparam → setarrvalue dispatches a PM_HASHED target to
1462 // arrhashsetfn, which converts the flat list to key/value pairs.
1463 // When the target is an associative array (`typeset -A h; set -A
1464 // h k v`), route through sethparam (the flat-kv-list assoc
1465 // setter) so the values land as pairs instead of being stored as
1466 // a plain array (which left the assoc empty). Plain arrays keep
1467 // setaparam.
1468 let target_is_hashed = {
1469 use PM_HASHED;
1470 paramtab()
1471 .read()
1472 .ok()
1473 .and_then(|tab| {
1474 tab.get(&aname)
1475 .map(|pm| (pm.node.flags as u32 & PM_HASHED) != 0)
1476 })
1477 .unwrap_or(false)
1478 };
1479 if target_is_hashed {
1480 crate::ported::params::sethparam(&aname, new_arr);
1481 } else {
1482 setaparam(&aname, new_arr);
1483 }
1484 } else {
1485 // c:711-712 — `freearray(pparams); pparams = zarrdup(args);`
1486 // PPARAMS is the single source of truth; fusevm reads via
1487 // `exec.pparams()`.
1488 if let Ok(mut pp) = PPARAMS.lock() {
1489 *pp = sorted; // c:712
1490 }
1491 }
1492 unqueue_signals(); // c:714
1493 0 // c:715
1494}
1495
1496/// Port of `bin_pwd(UNUSED(char *name), UNUSED(char **argv), Options ops, UNUSED(int func))` from Src/builtin.c:728.
1497/// C: `int bin_pwd(UNUSED(char *name), UNUSED(char **argv), Options ops,
1498/// UNUSED(int func))` — `-r`/`-P` or (CHASELINKS && !`-L`) →
1499/// print resolved cwd via zgetcwd; else print the cached `pwd`.
1500// pwd: display the name of the current directory // c:728
1501/// WARNING: param names don't match C — Rust=(_name, _argv, _func) vs C=(name, argv, ops, func)
1502pub fn bin_pwd(
1503 _name: &str,
1504 _argv: &[String], // c:728
1505 ops: &options,
1506 _func: i32,
1507) -> i32 {
1508 let chaselinks = isset(CHASELINKS);
1509 // c:730-731 — `if (OPT_ISSET(ops,'r') || OPT_ISSET(ops,'P') ||
1510 // (isset(CHASELINKS) && !OPT_ISSET(ops,'L')))`
1511 if OPT_ISSET(ops, b'r') || OPT_ISSET(ops, b'P') // c:730
1512 || (chaselinks && !OPT_ISSET(ops, b'L'))
1513 // c:731
1514 {
1515 // c:732 — `printf("%s\n", zgetcwd());`
1516 println!("{}", zgetcwd()); // c:732
1517 } else {
1518 // c:734 — `zputs(pwd, stdout); putchar('\n');`. C reads the
1519 // shell-internal `pwd` global (Src/params.c:108), NOT the
1520 // exported $PWD env var. The internal global is only mutated
1521 // by `cd`. zshrs lacks a separate internal pwd, so `$PWD`
1522 // doubles as both — and user code can spoof it (`PWD=/foo`).
1523 //
1524 // C-equivalent behavior in zshrs: trust `$PWD` ONLY when its
1525 // stat matches `getcwd()` (same dev+ino → same directory,
1526 // logical name preserved through symlinks). On mismatch the
1527 // user spoofed it; fall back to `zgetcwd()` so `pwd` reports
1528 // the actual current directory. Bug #441 — security-relevant:
1529 // defensive-coding patterns checking `pwd` output get fooled
1530 // by a `PWD=` assignment without this validation.
1531 let pwd_param = getsparam("PWD");
1532 let logical_pwd = pwd_param.as_deref().filter(|pwd| {
1533 // stat-compare $PWD against getcwd() — same inode means
1534 // $PWD is just a symlink-preserving alias for the real
1535 // cwd, safe to use.
1536 use std::os::unix::fs::MetadataExt;
1537 let cwd = zgetcwd();
1538 match (std::fs::metadata(pwd), std::fs::metadata(&cwd)) {
1539 (Ok(a), Ok(b)) => a.dev() == b.dev() && a.ino() == b.ino(),
1540 _ => false,
1541 }
1542 });
1543 println!("{}", logical_pwd.map(String::from).unwrap_or_else(zgetcwd)); // c:734
1544 }
1545 0 // c:737
1546}
1547
1548/// Port of `bin_dirs(UNUSED(char *name), char **argv, Options ops, UNUSED(int func))` from Src/builtin.c:749.
1549/// C: `int bin_dirs(UNUSED(char *name), char **argv, Options ops, ...)` —
1550/// list dirstack (default / -v / -p / -l) or replace it with argv.
1551// dirs: list the directory stack, or replace it with a provided list // c:749
1552/// WARNING: param names don't match C — Rust=(_name, argv, _func) vs C=(name, argv, ops, func)
1553pub fn bin_dirs(
1554 _name: &str,
1555 argv: &[String], // c:749
1556 ops: &options,
1557 _func: i32,
1558) -> i32 {
1559 queue_signals(); // c:753
1560 // c:755-756 — list mode: no args & no -c, OR -v / -p.
1561 if (argv.is_empty() && !OPT_ISSET(ops, b'c')) // c:755
1562 || OPT_ISSET(ops, b'v')
1563 || OPT_ISSET(ops, b'p')
1564 {
1565 let mut pos = 1; // c:760
1566 // c:763-769 — pick separator format.
1567 let fmt: &str = if OPT_ISSET(ops, b'v') {
1568 // c:763
1569 print!("0\t"); // c:764
1570 "\n{}\t" // c:765
1571 } else if OPT_ISSET(ops, b'p') {
1572 // c:767
1573 "\n"
1574 } else {
1575 " "
1576 };
1577 // c:771-774 — print pwd via fprintdir or zputs (`-l`).
1578 // Previous Rust port inlined a HOME-prefix replacement which
1579 // only abbreviated `$HOME/...` to `~/...` — missed every
1580 // user-defined nameddirtab entry (`hash -d proj=/big/path`).
1581 // Route through `utils::fprintdir` which calls `finddir`,
1582 // matching C's named-dir abbreviation.
1583 let pwd = getsparam("PWD").unwrap_or_else(|| zgetcwd());
1584 if OPT_ISSET(ops, b'l') {
1585 // c:771
1586 print!("{}", pwd); // c:772
1587 } else {
1588 print!("{}", fprintdir(&pwd)); // c:774
1589 }
1590 // c:775-781 — walk dirstack list.
1591 if let Ok(stack) = DIRSTACK.lock() {
1592 // c:775
1593 for entry in stack.iter() {
1594 if fmt == "\n{}\t" {
1595 print!("\n{}\t", pos);
1596 } else {
1597 print!("{}", fmt); // c:776
1598 }
1599 pos += 1; // c:776
1600 if OPT_ISSET(ops, b'l') {
1601 // c:777
1602 print!("{}", entry); // c:778
1603 } else {
1604 print!("{}", fprintdir(entry)); // c:780
1605 }
1606 }
1607 }
1608 unqueue_signals(); // c:783
1609 println!(); // c:784
1610 return 0; // c:785
1611 }
1612 // c:788-792 — replace dirstack with the supplied entries.
1613 if let Ok(mut stack) = DIRSTACK.lock() {
1614 stack.clear(); // c:790
1615 for arg in argv {
1616 stack.push(arg.clone()); // c:791
1617 }
1618 }
1619 unqueue_signals(); // c:793
1620 0 // c:794
1621}
1622
1623/// Direct port of `void set_pwd_env(void)` from
1624/// `Src/builtin.c:800`. Refreshes both `$PWD` and `$OLDPWD` shell
1625/// parameters from the shell-side `pwd`/`oldpwd` globals
1626/// (`assignsparam` at c:818-819), then `addenv`s each so child
1627/// processes inherit them (c:821-826). C also clears `PM_READONLY`
1628/// and unsets the param when a user retyped PWD/OLDPWD to a
1629/// non-scalar (c:806-816).
1630///
1631/// Rust port: the bin entry's analog of C's `pwd`/`oldpwd` globals
1632/// is the live OS env (validated against ispwd by ShellExecutor::new
1633/// per c:Src/init.c:1242-1259 — see the subshell-snapshot comment in
1634/// fusevm_bridge.rs treating `$PWD` env as the logical-pwd carrier).
1635/// So the data flow matches C: globals (env) → paramtab → env. The
1636/// previous revision read paramtab's own PWD and wrote it back — a
1637/// circular no-op that left the stale environ-snapshot import in
1638/// place when the inherited $PWD failed ispwd().
1639pub fn set_pwd_env() {
1640 // c:800
1641 // c:805-816 — PM_READONLY clear + unsetparam_pm for non-scalar
1642 // PWD/OLDPWD retypes isn't ported; setsparam below
1643 // overwrites the scalar value and flags directly.
1644 // c:818 — `assignsparam("PWD", ztrdup(pwd), 0);`
1645 let pwd = env::var("PWD").unwrap_or_else(|_| {
1646 env::current_dir()
1647 .map(|p| p.to_string_lossy().into_owned())
1648 .unwrap_or_default()
1649 });
1650 setsparam("PWD", &pwd); // c:818
1651 // c:819 — `assignsparam("OLDPWD", ztrdup(oldpwd), 0);`
1652 if let Ok(oldpwd) = env::var("OLDPWD") {
1653 setsparam("OLDPWD", &oldpwd); // c:819
1654 env::set_var("OLDPWD", &oldpwd); // c:825-826 addenv(pm, oldpwd)
1655 }
1656 env::set_var("PWD", &pwd); // c:821-823 addenv(pm, pwd)
1657}
1658
1659/// Port of `bin_cd(char *nam, char **argv, Options ops, int func)` from Src/builtin.c:840.
1660/// C: `int bin_cd(char *nam, char **argv, Options ops, int func)`.
1661///
1662/// Body (verbatim translation per c:842-859):
1663/// ```c
1664/// doprintdir = (doprintdir == -1);
1665/// chasinglinks = OPT_ISSET(ops,'P') ||
1666/// (isset(CHASELINKS) && !OPT_ISSET(ops,'L'));
1667/// queue_signals();
1668/// zpushnode(dirstack, ztrdup(pwd));
1669/// if (!(dir = cd_get_dest(nam, argv, OPT_ISSET(ops,'s'), func))) {
1670/// zsfree(getlinknode(dirstack));
1671/// unqueue_signals();
1672/// return 1;
1673/// }
1674/// cd_new_pwd(func, dir, OPT_ISSET(ops, 'q'));
1675/// unqueue_signals();
1676/// return 0;
1677/// ```
1678// cd, chdir, pushd, popd // c:796
1679/// `bin_cd` — see implementation.
1680pub fn bin_cd(
1681 nam: &str,
1682 argv: &[String], // c:840
1683 ops: &options,
1684 func: i32,
1685) -> i32 {
1686 // c:844 — `doprintdir = (doprintdir == -1);`
1687 let prev = DOPRINTDIR.load(Relaxed);
1688 DOPRINTDIR.store(if prev == -1 { 1 } else { 0 }, Relaxed); // c:844
1689
1690 // c:846-847 — `chasinglinks = OPT_ISSET(ops,'P') ||
1691 // (isset(CHASELINKS) && !OPT_ISSET(ops,'L'));`
1692 let chase = OPT_ISSET(ops, b'P') // c:846
1693 || (isset(CHASELINKS)
1694 && !OPT_ISSET(ops, b'L'));
1695 CHASINGLINKS.store(chase as i32, Relaxed);
1696
1697 queue_signals(); // c:848
1698
1699 // c:849 — `zpushnode(dirstack, ztrdup(pwd));`. C uses the `pwd`
1700 // global (the in-shell logical cwd, kept in sync with
1701 // $PWD). Read from paramtab; fall back to getcwd if
1702 // unset. The C source pushes pre-cd pwd to the top of
1703 // dirstack here as a scratch slot used by cd_get_dest's
1704 // +N/-N resolver; cd_new_pwd's remnode logic relies on
1705 // this. Save the pre-cd path for the post-cd dirstack
1706 // maintenance below.
1707 let pre_pwd = getsparam("PWD").unwrap_or_else(|| zgetcwd());
1708
1709 // c:850-854 — `if (!(dir = cd_get_dest(...))) { pop; unqueue; return 1; }`
1710 let dest = cd_get_dest(nam, argv, OPT_ISSET(ops, b's'), func);
1711 if dest.is_none() {
1712 unqueue_signals(); // c:852
1713 return 1; // c:853
1714 }
1715 let dest_raw = dest.unwrap();
1716
1717 // c:Src/builtin.c:851 — `-s` safe mode: refuse to chdir into a
1718 // symlink. Check via fs::symlink_metadata so we see the link
1719 // itself, not its target.
1720 if OPT_ISSET(ops, b's') {
1721 if let Ok(meta) = fs::symlink_metadata(&dest_raw) {
1722 if meta.file_type().is_symlink() {
1723 zwarnnam(nam, &format!("{}: symbolic link", dest_raw));
1724 unqueue_signals();
1725 return 1;
1726 }
1727 }
1728 }
1729
1730 // c:Src/builtin.c:855 — route the resolved arg through
1731 // cd_do_chdir so CDPATH walk, leading `~`/`.` handling, and
1732 // CDABLEVARS expansion fire. cd_do_chdir performs the actual
1733 // lchdir + returns the LOGICAL path (the one to write to PWD).
1734 // The previous Rust port called env::set_current_dir(dest_raw)
1735 // directly which skipped all of CDPATH, so `CDPATH=/foo cd bar`
1736 // would only resolve `./bar` even if `/foo/bar` existed.
1737 let dest_path = match cd_do_chdir(nam, &dest_raw, OPT_ISSET(ops, b's') as i32) {
1738 Some(p) => p,
1739 None => {
1740 unqueue_signals();
1741 return 1;
1742 }
1743 };
1744
1745 // c:1238 — `oldpwd = pwd;` snapshot pre-cd $PWD for $OLDPWD.
1746 // Read from paramtab (the canonical zsh-side `pwd`
1747 // global); was reading OS env which can lag behind.
1748 let old = getsparam("PWD");
1749 // c:1207-1245 — compute the new logical pwd (`new_pwd` in C's
1750 // cd_new_pwd) up front so the PUSHDIGNOREDUPS scan below and the
1751 // final `pwd =` write share one value. Hoisted from the old
1752 // write site; behaviour is identical (chase → resolved cwd,
1753 // absolute dest → as-is, relative dest → logical join).
1754 let chase = CHASINGLINKS.load(Relaxed) != 0; // c:1203
1755 let new_pwd_logical: String = if chase {
1756 match env::current_dir() {
1757 Ok(c) => c.to_string_lossy().into_owned(),
1758 Err(_) => dest_path.clone(),
1759 }
1760 } else if dest_path.starts_with('/') {
1761 dest_path.clone()
1762 } else {
1763 let mut segs: Vec<&str> = if pre_pwd.is_empty() || pre_pwd == "/" {
1764 Vec::new()
1765 } else {
1766 pre_pwd.trim_start_matches('/').split('/').collect()
1767 };
1768 for part in dest_path.split('/') {
1769 match part {
1770 "" | "." => {}
1771 ".." => {
1772 segs.pop();
1773 }
1774 _ => segs.push(part),
1775 }
1776 }
1777 if segs.is_empty() {
1778 "/".to_string()
1779 } else {
1780 format!("/{}", segs.join("/"))
1781 }
1782 };
1783 // c:Src/builtin.c:849 + cd_new_pwd dirstack maintenance —
1784 // collapsed into a single post-cd update here since the Rust
1785 // cd_get_dest returns a String rather than a LinkNode that
1786 // could be pre-pushed onto dirstack:
1787 // * BIN_PUSHD: push pre-cd pwd to top so subsequent
1788 // `${dirstack[1]}` and `popd` see the previous directory.
1789 // * BIN_POPD: pop dirstack[0] (the directory we just left,
1790 // which cd_get_dest read from the stack to compute dest).
1791 // * BIN_CD: dirstack unchanged unless AUTO_PUSHD is set, in
1792 // which case the CD behaves like a pushd.
1793 let mut skip_popd_n_cd = false;
1794 {
1795 let autopushd = isset(AUTOPUSHD);
1796 // c:1187 cd_new_pwd — rolllist(dirstack, dir) for BIN_PUSHD then
1797 // remnode(dirstack, dir). For `pushd +N`/`-N`, the rotation moves
1798 // entries before the target to the END of dirstack rather than
1799 // simply inserting pre_pwd at the front. Bug #302 in docs/BUGS.md:
1800 // previous Rust port unconditionally inserted pre_pwd at position
1801 // 0, which is correct for `pushd <path>` but wrong for `pushd +N`
1802 // (left target entry still in DIRSTACK at its old position).
1803 let is_stack_rotate = argv.len() == 1
1804 && !isset(POSIXCD)
1805 && argv[0].len() > 1
1806 && (argv[0].starts_with('+') || argv[0].starts_with('-'))
1807 && argv[0][1..].chars().all(|c| c.is_ascii_digit());
1808 if let Ok(mut d) = DIRSTACK.lock() {
1809 if is_stack_rotate && (func == BIN_PUSHD || (func == BIN_CD && autopushd)) {
1810 // c:1190 — rolllist(dirstack, dir) + remnode. Rotate the
1811 // virtual full stack `[pre_pwd] ++ DIRSTACK_old` so the
1812 // target entry becomes firstnode (= new PWD); remaining
1813 // entries form new DIRSTACK in their rotated order.
1814 let dd: usize = argv[0][1..].parse().unwrap_or(0);
1815 let pushdminus = isset(PUSHDMINUS);
1816 let from_top = (argv[0].starts_with('+')) ^ pushdminus;
1817 let m = d.len();
1818 let n = m + 1;
1819 let k = if from_top { dd } else { n - 1 - dd };
1820 if k < n {
1821 let mut full: Vec<String> = Vec::with_capacity(n);
1822 full.push(pre_pwd.clone());
1823 full.extend(d.iter().cloned());
1824 let rotated: Vec<String> =
1825 full[k..].iter().chain(full[..k].iter()).cloned().collect();
1826 d.clear();
1827 d.extend(rotated.into_iter().skip(1));
1828 }
1829 } else if is_stack_rotate && func == BIN_CD {
1830 // c:1192 — remnode(dirstack, dir) for non-PUSHD CD with
1831 // +N/-N: target removed from dirstack, no rolllist.
1832 let dd: usize = argv[0][1..].parse().unwrap_or(0);
1833 let pushdminus = isset(PUSHDMINUS);
1834 let from_top = (argv[0].starts_with('+')) ^ pushdminus;
1835 let m = d.len();
1836 let n = m + 1;
1837 let k = if from_top { dd } else { n - 1 - dd };
1838 if k >= 1 && k - 1 < d.len() {
1839 d.remove(k - 1);
1840 }
1841 } else if is_stack_rotate && func == BIN_POPD {
1842 // c:Src/builtin.c:872-936 + c:1197-1199 — `popd +N` /
1843 // `popd -N`: when the target +N/-N is NOT firstnode
1844 // (i.e., N != 0 / N != top-of-stack), remove the
1845 // target entry from dirstack WITHOUT changing PWD or
1846 // pushing pre_pwd to the stack. C's logic:
1847 // target = dir (the +N resolved node);
1848 // if (dir != firstnode(dirstack)) return dir;
1849 // // … cd_do_chdir path runs only if target IS top
1850 //
1851 // The Rust dispatch reached here for the
1852 // "target-not-top" case after cd_get_dest already
1853 // resolved to dirstack[k-1]. Just remove that index
1854 // and skip the cd / pre_pwd-insert paths.
1855 // Bug #466 in docs/BUGS.md.
1856 let dd: usize = argv[0][1..].parse().unwrap_or(0);
1857 let pushdminus = isset(PUSHDMINUS);
1858 let from_top = (argv[0].starts_with('+')) ^ pushdminus;
1859 let m = d.len();
1860 let n = m + 1;
1861 // popd's stack-rotate-vs-top logic mirrors C: the
1862 // virtual stack is pre_pwd ++ DIRSTACK_old; position 0
1863 // is the CURRENT pwd, so "popd +0" / "popd +N where
1864 // N maps to top" pops the top (handled by the
1865 // existing BIN_POPD arm below). For N that resolves
1866 // to a non-top dirstack index, remove from dirstack
1867 // directly.
1868 let k = if from_top { dd } else { n - 1 - dd };
1869 if k == 0 {
1870 // c:934-940 + c:1197-1203 — the +N/-N resolved to
1871 // firstnode (the current PWD): this is a bare-`popd`
1872 // top removal. Pop the entry below PWD (DIRSTACK[0])
1873 // and let the normal cd path below move PWD into it
1874 // (cd_get_dest resolved dest to DIRSTACK[0]). Do NOT
1875 // set skip_popd_n_cd — PWD must change here.
1876 // Bug: prior port's `k >= 1` guard fell through this
1877 // else-if arm without popping, so the plain-POPD arm
1878 // was never reached and the top entry survived.
1879 if !d.is_empty() {
1880 d.remove(0);
1881 }
1882 } else if k - 1 < d.len() {
1883 d.remove(k - 1);
1884 // Override the cd path: keep PWD at pre_pwd so
1885 // the surrounding flow doesn't cd to the removed
1886 // entry. The destination passed to bin_cd was
1887 // the removed entry; we overwrite the post-cd
1888 // PWD write below to pre_pwd.
1889 skip_popd_n_cd = true;
1890 }
1891 } else if func == BIN_PUSHD || (func == BIN_CD && autopushd) {
1892 // c:849 — push pre-cd pwd.
1893 d.insert(0, pre_pwd.clone());
1894 } else if func == BIN_POPD {
1895 // c:1197-1199 — pop top of stack (the dir we left).
1896 if !d.is_empty() {
1897 d.remove(0);
1898 }
1899 }
1900 // c:1214-1222 — PUSHDIGNOREDUPS runs inside cd_new_pwd for
1901 // EVERY func (plain cd, pushd, popd — the block is not
1902 // guarded by func), scanning the now-current dirstack for
1903 // the FIRST entry equal to `new_pwd` (the directory just
1904 // moved into) and removing it so a directory never appears
1905 // twice on the stack:
1906 // if (isset(PUSHDIGNOREDUPS)) {
1907 // for (n = firstnode(dirstack); n; incnode(n))
1908 // if (!strcmp(new_pwd, getdata(n))) {
1909 // zsfree(remnode(dirstack, n)); break; }
1910 // }
1911 // Prior port only checked pushd and compared d.first()
1912 // against pre_pwd (wrong target, wrong action), so a plain
1913 // `cd` into a dir already on the stack, or re-pushing such
1914 // a dir, left the stale duplicate in place. The non-top
1915 // `popd +N` path (skip_popd_n_cd) keeps PWD = pre_pwd, so
1916 // its dedup runs there against pre_pwd (see below).
1917 if !skip_popd_n_cd && isset(PUSHDIGNOREDUPS) {
1918 if let Some(pos) = d.iter().position(|s| *s == new_pwd_logical) {
1919 d.remove(pos);
1920 }
1921 }
1922 }
1923 }
1924 // c:Src/builtin.c:872-936 — `popd +N` (N != 0) removes the
1925 // target entry from dirstack WITHOUT changing PWD. cd_do_chdir
1926 // already chdir'd into the removed target's path; revert that
1927 // back to pre_pwd and skip the OLDPWD update + PWD write so the
1928 // post-popd CWD remains the pre-popd PWD. Bug #466 in
1929 // docs/BUGS.md.
1930 if skip_popd_n_cd {
1931 // Restore process cwd to pre_pwd. Ignore errors; the
1932 // dirstack mutation already happened.
1933 let _ = env::set_current_dir(&pre_pwd);
1934 // c:1242-1246 — cd_new_pwd runs `oldpwd = pwd; pwd = new_pwd`
1935 // for EVERY popd, including a non-top `popd +N`. There
1936 // new_pwd resolves back to the front-of-stack pre_pwd, so
1937 // PWD is unchanged but OLDPWD becomes the pre-popd PWD.
1938 // Prior port left OLDPWD stale (whatever an earlier cd/pushd
1939 // set), diverging on any `$OLDPWD` / `cd -` that followed.
1940 setsparam("OLDPWD", &pre_pwd);
1941 env::set_var("OLDPWD", &pre_pwd);
1942 // c:Src/builtin.c:1245-1252 — print dirstack on POPD unless
1943 // quiet. Pass func=BIN_POPD so the dirstack-print branch
1944 // still fires for the listing (matches zsh's `popd +N` echo
1945 // of the resulting stack).
1946 cd_new_pwd(func, 0, OPT_ISSET(ops, b'q') as i32);
1947 unqueue_signals();
1948 return 0;
1949 }
1950 if let Some(o) = old {
1951 // c:1239 oldpwd = pwd
1952 // c:1239 + setsparam path: write OLDPWD to paramtab so
1953 // subsequent expansions of $OLDPWD see the new value
1954 // (the OS env write below is the export side; the
1955 // shell-side read must come from paramtab).
1956 setsparam("OLDPWD", &o);
1957 env::set_var("OLDPWD", &o);
1958 }
1959 // c:1241 — `pwd = new_pwd;` writes the LOGICAL path (the dest
1960 // argument as given to cd, not `getcwd()`). Computed once up front
1961 // (see `new_pwd_logical` above) so the PUSHDIGNOREDUPS scan and this
1962 // write agree. Symlink resolution only kicks in when `chasinglinks`
1963 // is set (c:1203-1208, c:1228-1231).
1964 let pwd: String = new_pwd_logical;
1965 // c:1242 — `setsparam("PWD", pwd);` + export side via env.
1966 setsparam("PWD", &pwd);
1967 env::set_var("PWD", &pwd);
1968 cd_new_pwd(func, 0, OPT_ISSET(ops, b'q') as i32); // c:856
1969
1970 unqueue_signals(); // c:858
1971 0 // c:859
1972}
1973
1974/// Port of `cd_get_dest(char *nam, char **argv, int hard, int func)` from Src/builtin.c:865.
1975/// C: `static LinkNode cd_get_dest(char *nam, char **argv, int hard,
1976/// int func)` — resolve the `cd` argument (`-`, `+N`/`-N`,
1977/// bare → $HOME, two-arg substitution form) to a destination path.
1978/// Returns the resolved path on success, None on error (with the
1979/// appropriate zwarnnam already emitted).
1980/// WARNING: param names don't match C — Rust=() vs C=(nam, argv, hard, func)
1981pub fn cd_get_dest(nam: &str, argv: &[String], _hard: bool, func: i32) -> Option<String> {
1982 if argv.is_empty() {
1983 // c:872 — bare popd / pushd / cd (no args).
1984 // The Rust port doesn't pre-push pwd to dirstack inside bin_cd
1985 // (cd_get_dest's String return signature doesn't fit C's
1986 // pre-push pattern), so dirstack[0] here is the most-recent
1987 // pushed entry, not the temporary scratch slot the C source
1988 // sees. Adjust the indices accordingly: C reads index 1
1989 // (skipping the scratch), Rust reads index 0.
1990 if func == BIN_POPD {
1991 let depth = DIRSTACK.lock().map(|d| d.len()).unwrap_or(0);
1992 if depth < 1 {
1993 zwarnnam(nam, "directory stack empty");
1994 return None;
1995 }
1996 return DIRSTACK.lock().ok().and_then(|d| d.first().cloned());
1997 }
1998 if func == BIN_PUSHD {
1999 // c:876-879 — bare pushd without PUSHDTOHOME swaps top two
2000 // entries of dirstack. C represents dirstack with PWD at
2001 // index 0, so it walks `nextnode(firstnode(dirstack))` to
2002 // get index 1, then extracts index 0 via `getlinknode` and
2003 // reinserts after — net swap of [0]<->[1]. zshrs's DIRSTACK
2004 // omits PWD entirely (PWD lives in paramtab); index 0 here
2005 // = C's index 1.
2006 //
2007 // To achieve the same swap in this model: POP DIRSTACK[0]
2008 // (the saved target) and return it. cd_new_pwd at line ~1573
2009 // then re-pushes pre_pwd onto DIRSTACK[0]. Net result:
2010 // DIRSTACK[0] becomes pre_pwd, PWD becomes the popped value.
2011 //
2012 // Bug #67 in docs/BUGS.md: prior port read `d.first().cloned()`
2013 // (no pop), so cd_new_pwd's push added a 3rd entry instead
2014 // of swapping.
2015 //
2016 // c:881-885 — empty dirstack (only PWD in C's model, i.e.
2017 // DIRSTACK.is_empty() here) falls through to HOME below;
2018 // cd_new_pwd's push then leaves DIRSTACK = [pre_pwd].
2019 let pushdtohome = isset(PUSHDTOHOME);
2020 if !pushdtohome {
2021 let popped = DIRSTACK.lock().ok().and_then(|mut d| {
2022 if d.is_empty() {
2023 None
2024 } else {
2025 Some(d.remove(0))
2026 }
2027 });
2028 if let Some(target) = popped {
2029 return Some(target);
2030 }
2031 // Empty DIRSTACK → fall through to HOME below.
2032 }
2033 }
2034 // c:880-884 — fall through to $HOME.
2035 match getsparam("HOME") {
2036 Some(h) if !h.is_empty() => Some(h),
2037 _ => {
2038 zwarnnam(nam, "HOME not set");
2039 None
2040 }
2041 }
2042 } else if argv.len() == 1 {
2043 // c:887
2044 let arg = &argv[0];
2045 DOPRINTDIR.fetch_add(1, Relaxed); // c:891
2046 // c:892-908 — `+N`/`-N` numeric stack-index form.
2047 let posixcd = isset(POSIXCD);
2048 if !posixcd
2049 && arg.len() > 1
2050 && (arg.starts_with('+') || arg.starts_with('-'))
2051 && arg[1..].chars().all(|c| c.is_ascii_digit())
2052 {
2053 let dd: usize = arg[1..].parse().unwrap_or(0); // c:894
2054 let pushdminus = isset(PUSHDMINUS);
2055 let from_top = (arg.starts_with('+')) ^ pushdminus; // c:898
2056 // c:Src/builtin.c:904 — out-of-range stack index emits
2057 // "no such entry in dir stack". Previous Rust port
2058 // returned None silently and bin_cd's caller exited 1
2059 // with no stderr, breaking parity.
2060 // c:899-903 — index into the FULL dirstack: in zsh
2061 // `dirstack` is a LinkList with current PWD at firstnode,
2062 // so `+N` advances N nodes from firstnode (0 = current,
2063 // 1 = first non-current entry) and `-N` walks back N nodes
2064 // from lastnode (0 = last non-current entry). zshrs's
2065 // DIRSTACK omits PWD entirely (PWD lives in paramtab), so
2066 // build the virtual full stack `[PWD] ++ DIRSTACK` to index
2067 // against. Bug #302 in docs/BUGS.md: previous Rust port
2068 // indexed DIRSTACK directly, so `+1` returned DIRSTACK[1]
2069 // instead of DIRSTACK[0], and `+0`/`-len` errored instead of
2070 // returning PWD.
2071 let pwd_now = getsparam("PWD").unwrap_or_else(|| zgetcwd());
2072 let resolved = DIRSTACK.lock().ok().and_then(|d| {
2073 let m = d.len();
2074 let n = m + 1; // full virtual stack length (incl current)
2075 let k = if from_top {
2076 if dd >= n {
2077 return None;
2078 }
2079 dd
2080 } else {
2081 if dd >= n {
2082 return None;
2083 }
2084 n - 1 - dd
2085 };
2086 if k == 0 {
2087 // c:934-940 — for BIN_POPD, dir==firstnode(dirstack)
2088 // (the current PWD) is NOT removed in place: C does
2089 // `dir = nextnode(dir)` and cd_do_chdir's into the
2090 // entry BELOW PWD, then cd_new_pwd (c:1197-1199)
2091 // pops the front. So the chdir destination is
2092 // DIRSTACK[0] (the first non-current entry), not PWD.
2093 // For BIN_CD / BIN_PUSHD, +0 (or the swapped -last)
2094 // resolves to the current dir itself.
2095 // Bug: prior port returned pwd_now for every func, so
2096 // `popd +0` (and `popd +N`/`-N` mapping to firstnode
2097 // under PUSHD_MINUS) no-op'd instead of popping the top.
2098 if func == BIN_POPD {
2099 d.first().cloned().or_else(|| Some(pwd_now.clone()))
2100 } else {
2101 Some(pwd_now.clone())
2102 }
2103 } else {
2104 d.get(k - 1).cloned()
2105 }
2106 });
2107 if resolved.is_none() {
2108 zwarnnam(nam, "no such entry in dir stack");
2109 }
2110 return resolved;
2111 }
2112 // c:908-909 — `zpushnode(dirstack, ztrdup(strcmp(argv[0],"-")
2113 // ? (doprintdir--, argv[0]) : oldpwd))`. `strcmp(argv[0],"-")`
2114 // is non-zero for a normal dir, so a normal `cd dir` does
2115 // `doprintdir--` (cancelling the c:891 `doprintdir++` → no
2116 // print), while `cd -` keeps doprintdir set → prints $OLDPWD.
2117 // C reads the `oldpwd` global; route through `$OLDPWD`.
2118 if arg == "-" {
2119 // c:909 — `: oldpwd` — no doprintdir--, so `cd -` prints.
2120 getsparam("OLDPWD")
2121 } else {
2122 // c:909 — `? (doprintdir--, argv[0])`.
2123 DOPRINTDIR.fetch_sub(1, Relaxed);
2124 Some(arg.clone())
2125 }
2126 } else {
2127 // c:914-924 — two-arg substitution: cd OLDPATTERN NEWPATTERN.
2128 // C reads `pwd` global / `$PWD` param via getsparam;
2129 // fall back to getcwd if the param isn't populated.
2130 let pwd = getsparam("PWD").unwrap_or_else(|| zgetcwd());
2131 let pat = &argv[0];
2132 let new_pat = &argv[1];
2133 match pwd.find(pat.as_str()) {
2134 // c:917
2135 None => {
2136 zwarnnam(nam, &format!("string not in pwd: {}", pat)); // c:918
2137 None // c:919
2138 }
2139 Some(idx) => {
2140 // c:921-924 — splice: pwd[..idx] + new_pat + pwd[idx+pat.len()..]
2141 let mut out = String::new();
2142 out.push_str(&pwd[..idx]); // c:921
2143 out.push_str(new_pat); // c:922
2144 out.push_str(&pwd[idx + pat.len()..]); // c:923
2145 DOPRINTDIR.fetch_add(1, Relaxed);
2146 Some(out)
2147 }
2148 }
2149 }
2150}
2151
2152/// Port of `cd_do_chdir(char *cnam, char *dest, int hard)` from Src/builtin.c:967.
2153/// C: `static char *cd_do_chdir(char *cnam, char *dest, int hard)` —
2154/// resolve `dest` (handling cdpath, cdablevars, leading `~`/`.`),
2155/// chdir there, return the LOGICAL path used (not `getcwd`'d) or
2156/// NULL on error.
2157///
2158/// Per C `cd_try_chdir` (c:1116-1181), the return is `buf` — the
2159/// composed path the chdir was attempted against, after `fixdir()`
2160/// logical-normalisation (resolving `.`/`..` only, NOT symlinks).
2161/// Walks $cdpath when dest is relative and not `./` or `../`.
2162pub fn cd_do_chdir(cnam: &str, dest: &str, hard: i32) -> Option<String> {
2163 // c:967
2164 // c:996-998 — nocdpath = first segment is "." or ".."
2165 let nocdpath = dest.starts_with("./") || dest == "." || dest.starts_with("../") || dest == "..";
2166
2167 // c:1003-1008 — absolute path: try as-is, warn on failure.
2168 if dest.starts_with('/') {
2169 if let Some(ret) = cd_try_chdir("", dest, hard) {
2170 return Some(ret);
2171 }
2172 // c:Src/builtin.c — `zwarnnam(nam, "%e: %s", errno, dest)`.
2173 // C's %e printf format expands to strerror(errno) then
2174 // LOWERCASES the first letter unless the errno is EIO
2175 // (Src/utils.c:362-368 comment: "If the message is not
2176 // about I/O problems, it looks better if we uncapitalize
2177 // the first letter of the message"). Previous Rust port
2178 // used `io::Error::last_os_error()`'s Display impl, which
2179 // appends ` (os error N)` — leaked the Rust-internal
2180 // suffix into user-visible cd output. Bug #112 in
2181 // docs/BUGS.md. Route through compat::strerror to drop
2182 // the suffix AND apply the lowercase-first-letter rule so
2183 // the message matches C exactly.
2184 let errno = io::Error::last_os_error().raw_os_error().unwrap_or(0);
2185 let mut msg = crate::ported::compat::strerror(errno);
2186 if errno != libc::EIO {
2187 if let Some(c) = msg.chars().next() {
2188 msg = format!("{}{}", c.to_ascii_lowercase(), &msg[c.len_utf8()..]);
2189 }
2190 }
2191 zwarnnam(cnam, &format!("{}: {}", msg, dest));
2192 return None;
2193 }
2194
2195 // c:1015-1018 — check $cdpath for "." (presence flips hasdot).
2196 let posix_cd = isset(POSIXCD);
2197 let cdpath_str = getsparam("CDPATH").unwrap_or_default();
2198 let cdpath: Vec<&str> = if cdpath_str.is_empty() {
2199 Vec::new()
2200 } else {
2201 cdpath_str.split(':').collect()
2202 };
2203 let hasdot = !nocdpath && !posix_cd && cdpath.iter().any(|p| p.is_empty() || *p == ".");
2204
2205 // c:1026-1031 — if no dot in cdpath (and !POSIXCD), try as-is first.
2206 if !hasdot && !posix_cd {
2207 if let Some(ret) = cd_try_chdir("", dest, hard) {
2208 return Some(ret);
2209 }
2210 }
2211
2212 // c:1034-1043 — walk $cdpath unless nocdpath.
2213 if !nocdpath {
2214 for pp in cdpath.iter() {
2215 if let Some(ret) = cd_try_chdir(pp, dest, hard) {
2216 // c:1037-1050 — flag that the resolved directory should
2217 // be printed; the actual print happens later in
2218 // cd_new_pwd (c:1248-1251) gated on doprintdir. C does
2219 // NOT print here, it only bumps `doprintdir`.
2220 if isset(POSIXCD) {
2221 // c:1037-1045 — POSIX prints any time CDPATH was
2222 // used, except for an empty segment treated as ".".
2223 if !pp.is_empty() {
2224 DOPRINTDIR.fetch_add(1, Relaxed); // c:1045
2225 }
2226 } else if *pp != "." {
2227 // c:1046-1049 — non-POSIX: print only for a
2228 // non-"." segment.
2229 DOPRINTDIR.fetch_add(1, Relaxed); // c:1048
2230 }
2231 return Some(ret);
2232 }
2233 }
2234 }
2235
2236 // c:1057-1063 — POSIXCD-mode last-resort: try dest as-is.
2237 if posix_cd {
2238 if let Some(ret) = cd_try_chdir("", dest, hard) {
2239 return Some(ret);
2240 }
2241 }
2242
2243 // c:1067-1075 — `cd_able_vars(dest)` — when CDABLEVARS is set,
2244 // look up the leading bareword as a parameter and try chdir to
2245 // its value (with any `/tail` appended). The helper at
2246 // `cd_able_vars` (this file, line 1881) already guards on
2247 // `isset(CDABLEVARS)` and returns None when the option is off
2248 // OR the head isn't a paramtab entry. Without this call
2249 // wired in, `setopt cdable_vars; mytmp=/tmp; cd mytmp` errored
2250 // with "no such file or directory" instead of cd'ing to /tmp.
2251 // Bug #217 in docs/BUGS.md.
2252 if let Some(expanded) = cd_able_vars(dest) {
2253 if let Some(ret) = cd_try_chdir("", &expanded, hard) {
2254 // c:1069 — `doprintdir++` so cd_new_pwd prints the
2255 // CDABLEVARS-resolved directory.
2256 DOPRINTDIR.fetch_add(1, Relaxed); // c:1069
2257 return Some(ret);
2258 }
2259 }
2260
2261 // c:1071 — failure warning.
2262 zwarnnam(cnam, &format!("no such file or directory: {}", dest));
2263 None
2264}
2265
2266/// Port of `cd_able_vars(char *s)` from Src/builtin.c:1088.
2267/// C: `char *cd_able_vars(char *s)` — when CDABLEVARS is set, look up
2268/// the leading bareword as a parameter and return its expanded value
2269/// prefixed in front of any trailing `/...`. Returns NULL otherwise.
2270pub fn cd_able_vars(s: &str) -> Option<String> {
2271 // c:1088
2272 // c:1088 — `if (isset(CDABLEVARS)) { ... }`
2273 let cdablevars = isset(CDABLEVARS);
2274 if !cdablevars {
2275 // c:1093
2276 return None;
2277 }
2278 // c:1094-1110 — split on the first `/`, look up the head as $param.
2279 let (head, tail) = match s.find('/') {
2280 // c:1094
2281 Some(i) => (&s[..i], &s[i..]),
2282 None => (s, ""),
2283 };
2284 if head.is_empty() {
2285 return None;
2286 }
2287 // c:1116 — `if ((val = getsparam(s))) { ret = tricat(val, tail, "") }`.
2288 // C reads $head from paramtab; was reading OS env, missing
2289 // CDABLEVARS-style assignments like `proj=$HOME/src`.
2290 getsparam(head).map(|val| format!("{}{}", val, tail))
2291}
2292
2293/// Port of `cd_try_chdir(char *pfix, char *dest, int hard)` from `Src/builtin.c:1116`.
2294/// Compose `pfix/dest` (or `pwd/pfix/dest` for relative pfix, or
2295/// `pwd/dest` for empty pfix + relative dest), normalise via `fixdir`,
2296/// then attempt chdir. Falls back to `dest` alone when the full path
2297/// fails but `pfix` was present (cwd/parent may have been renamed).
2298pub fn cd_try_chdir(pfix: &str, dest: &str, hard: i32) -> Option<String> {
2299 // c:1116
2300 let pwd = getsparam("PWD").unwrap_or_default();
2301
2302 // c:1122-1158 — build buf from pfix/dest/pwd combinations.
2303 let mut buf = if !pfix.is_empty() {
2304 if pfix.starts_with('/') {
2305 // c:1123
2306 // c:1133 — buf = tricat(pfix, "/", dest)
2307 if pfix.ends_with('/') {
2308 format!("{}{}", pfix, dest)
2309 } else {
2310 format!("{}/{}", pfix, dest)
2311 }
2312 } else {
2313 // c:1135-1146 — pwd + "/" + pfix + "/" + dest
2314 let pwd_trim = if pwd == "/" { "" } else { pwd.as_str() };
2315 format!("{}/{}/{}", pwd_trim, pfix, dest)
2316 }
2317 } else if dest.starts_with('/') {
2318 // c:1148
2319 // c:1149 — buf = ztrdup(dest)
2320 dest.to_string()
2321 } else {
2322 // c:1150
2323 // c:1151-1157 — pwd + "/" + dest (trimming trailing slash off pwd)
2324 let pwd_trim = pwd.trim_end_matches('/');
2325 format!("{}/{}", pwd_trim, dest)
2326 };
2327
2328 // c:1163-1166 — fixdir normalisation, skipped if chasing symlinks.
2329 if CHASINGLINKS.load(Relaxed) == 0 {
2330 buf = fixdir(&buf); // c:1164
2331 }
2332
2333 // c:1169-1177 — "We try the full path first. If that fails, try the
2334 // argument to cd relatively. This is useful if the cwd or a parent
2335 // directory is renamed in the interim." The relative fallback is
2336 // skipped when a prefix was applied or `dest` is already absolute.
2337 // Either success returns `buf` (the logical full path) — c:1181.
2338 if lchdir(&buf, None, hard) != 0
2339 && (!pfix.is_empty() || dest.starts_with('/') || lchdir(dest, None, hard) != 0)
2340 {
2341 return None; // c:1175-1176
2342 }
2343 Some(buf) // c:1181 — metafy(buf, ...)
2344}
2345
2346/// Port of `cd_new_pwd(int func, LinkNode dir, int quiet)` from Src/builtin.c:1187.
2347/// C: `static void cd_new_pwd(int func, LinkNode dir, int quiet)` —
2348/// commit a new PWD: rotate dirstack on `BIN_PUSHD`, pop on
2349/// `BIN_POPD`, then setparam(PWD/OLDPWD), fire chpwd hooks.
2350///
2351/// The PWD/OLDPWD write is now done by the caller (`bin_cd`) using
2352/// the logical `dest_path` from `cd_get_dest`. C's body at c:1238-1242
2353/// reads `new_pwd` off the dirstack — the Rust port's dirstack
2354/// plumbing isn't faithful enough to carry that path here, so the
2355/// caller writes PWD directly. This fn handles only the post-write
2356/// side effects (chpwd hooks, dirstack size cap).
2357/// WARNING: param names don't match C — Rust=(_func, _dir, _quiet) vs C=(func, dir, quiet)
2358pub fn cd_new_pwd(func: i32, _dir: usize, quiet: i32) {
2359 // c:1187 — post-cd side effects (print, hooks). Dirstack
2360 // maintenance was moved into `bin_cd` because the Rust port's
2361 // cd_get_dest returns a String (not a LinkNode pre-pushed on
2362 // dirstack), so C's remnode/rolllist sequence doesn't fit.
2363
2364 // c:1236-1242 — shift PWD → OLDPWD, set new PWD.
2365 //
2366 // The caller (`bin_cd` at builtin.rs:1379-1408) already wrote
2367 // OLDPWD (pre-cd $PWD) and PWD (the logical `dest_path`) into the
2368 // paramtab. Re-deriving them here would (a) double-shift OLDPWD
2369 // (overwriting the correct pre-cd value with the just-set PWD),
2370 // and (b) re-set PWD via `std::env::current_dir()` which always
2371 // returns the resolved physical path — clobbering the logical
2372 // path on systems where the destination is a symlink (e.g. macOS
2373 // /tmp → /private/tmp). C `cd_new_pwd` reads `new_pwd` off the
2374 // dirstack (the path the user typed); zshrs's dirstack plumbing
2375 // doesn't carry that path here, so the caller is the authoritative
2376 // PWD writer and this fn must NOT re-write either parameter.
2377
2378 // c:1244-1252 — print the new directory.
2379 // if (isset(INTERACTIVE) || isset(POSIXCD)) {
2380 // if (func != BIN_CD && isset(INTERACTIVE)) {
2381 // if (unset(PUSHDSILENT) && !quiet) printdirstack();
2382 // } else if (unset(CDSILENT) && doprintdir) {
2383 // fprintdir(pwd, stdout); putchar('\n');
2384 // }
2385 // }
2386 if isset(INTERACTIVE) || isset(POSIXCD) {
2387 if func != BIN_CD && isset(INTERACTIVE) {
2388 // c:1245-1247 — pushd/popd echo the dir stack.
2389 if !isset(PUSHDSILENT) && quiet == 0 {
2390 printdirstack(); // c:1247
2391 }
2392 } else if !isset(crate::ported::zsh_h::CDSILENT) && DOPRINTDIR.load(Relaxed) != 0 {
2393 // c:1248-1251 — cd echoes the resolved directory (set via
2394 // doprintdir by `cd -`, CDPATH, autocd, CDABLEVARS).
2395 let pwd = getsparam("PWD").unwrap_or_default(); // C global `pwd`
2396 println!("{}", fprintdir(&pwd)); // c:1249-1250
2397 }
2398 }
2399
2400 // c:1258 — `callhookfunc("chpwd", NULL, 1, NULL)` fires the chpwd
2401 // hook. Not surfaced here: the executor wrapper (fusevm_bridge.rs
2402 // cd builtin) calls `callhookfunc("chpwd", ...)` after a successful
2403 // cd, which dispatches both the `chpwd` shfunc and the
2404 // `chpwd_functions` array (utils.rs:1532).
2405
2406 // c:1264-1271 — trim the dir stack to $DIRSTACKSIZE.
2407 // `dirstacksize = getiparam("DIRSTACKSIZE");
2408 // if (dirstacksize > 0) {
2409 // int remove = countlinknodes(dirstack) -
2410 // (dirstacksize < 2 ? 2 : dirstacksize);
2411 // while (remove-- >= 0)
2412 // zsfree(remnode(dirstack, lastnode(dirstack)));
2413 // }`
2414 let dirstacksize = getiparam("DIRSTACKSIZE"); // c:1264
2415 if dirstacksize > 0 {
2416 // c:1266
2417 if let Ok(mut d) = DIRSTACK.lock() {
2418 // c:1267-1268 — count minus the effective cap (min 2).
2419 let cap = if dirstacksize < 2 { 2 } else { dirstacksize };
2420 let mut remove = d.len() as i64 - cap;
2421 // c:1269-1270 — `while (remove-- >= 0)` pops the last node;
2422 // the post-decrement removes (remove+1) entries from the end.
2423 while remove >= 0 {
2424 remove -= 1;
2425 if d.pop().is_none() {
2426 break;
2427 }
2428 }
2429 }
2430 }
2431}
2432
2433/// Port of `printdirstack()` from Src/builtin.c:1277.
2434/// C: `static void printdirstack(void)` — fprintdir(pwd) followed by
2435/// space-separated entries from the dirstack list, ending in newline.
2436pub fn printdirstack() {
2437 // c:1277
2438 // c:1281 — `fprintdir(pwd, stdout);`. C uses the shell-side
2439 // `pwd` global (in-shell logical cwd), not getcwd. Read
2440 // $PWD from paramtab so the logical path (including
2441 // any unresolved symlinks) shows correctly. Route
2442 // through `utils::fprintdir` for the same `~` /
2443 // `~named` abbreviation real zsh emits.
2444 // Previous Rust port emitted raw paths, missing the
2445 // $HOME / nameddirtab abbreviation that makes pushd/popd output
2446 // legible. Same fix family as bin_dirs.
2447 let pwd = getsparam("PWD")
2448 .or_else(|| {
2449 env::current_dir()
2450 .ok()
2451 .and_then(|p| p.to_str().map(String::from))
2452 })
2453 .unwrap_or_default();
2454 print!("{}", fprintdir(&pwd)); // c:1281
2455 // c:1282-1286 — `for (node = firstnode(dirstack); ...)`
2456 if let Ok(d) = DIRSTACK.lock() {
2457 for entry in d.iter() {
2458 // c:1282
2459 print!(" {}", fprintdir(entry)); // c:1284
2460 }
2461 }
2462 println!(); // c:1287
2463}
2464
2465/// Direct port of `int fixdir(char *src)` from
2466/// `Src/builtin.c:1297`. Lexically canonicalises a path in-place
2467/// (no symlink follow): collapses `//`, drops `./` segments, and
2468/// removes `..` along with their preceding segment. Returns 1 if
2469/// fully canonicalised, 0 if a `..` could not be popped (e.g. at
2470/// the root or with `..` as the first segment under CHASEDOTS=0).
2471///
2472/// Rust port takes ownership of `src` and returns the canonical
2473/// form; was a 1-line stub returning empty string.
2474pub fn fixdir(src: &str) -> String {
2475 // c:1297
2476 if src.is_empty() {
2477 return String::new();
2478 }
2479
2480 // c:1320-1325 — `chasedots` flag for the cdpath `../` edge case.
2481 // Skipped here — only fires under the pwd=="." rare
2482 // state. Lexical canonicalisation is what callers
2483 // rely on.
2484 let abs = src.starts_with('/');
2485 let mut components: Vec<&str> = Vec::new();
2486
2487 // c:1339-1395 — walk slash-separated segments.
2488 for seg in src.split('/') {
2489 match seg {
2490 "" => continue, // collapse `//`
2491 "." => continue, // c:1352 drop `./`
2492 ".." => {
2493 // c:1358-1372 — pop previous segment if present and not
2494 // also `..` (sticky-`..` for relative
2495 // paths past their start).
2496 if let Some(last) = components.last() {
2497 if *last == ".." {
2498 components.push("..");
2499 } else {
2500 components.pop();
2501 }
2502 } else if !abs {
2503 // Relative path: keep the leading `..`.
2504 components.push("..");
2505 }
2506 // Absolute path: silently drop `..` past `/`.
2507 }
2508 other => components.push(other),
2509 }
2510 }
2511
2512 let body = components.join("/");
2513 if abs {
2514 format!("/{}", body)
2515 } else if body.is_empty() {
2516 ".".to_string()
2517 } else {
2518 body
2519 }
2520}
2521
2522/// Port of `printqt(char *str)` from Src/builtin.c:1399.
2523/// C: `mod_export void printqt(char *str)` — emit `str`, escaping any
2524/// `'` as `'\''` (or `''` if RCQUOTES is set).
2525pub fn printqt(str: &str) {
2526 // c:1399
2527 let rcquotes = isset(RCQUOTES); // c:1399 isset(RCQUOTES)
2528 for ch in str.chars() {
2529 // c:1403
2530 if ch == '\'' {
2531 // c:1404
2532 print!("{}", if rcquotes { "''" } else { "'\\''" }); // c:1405
2533 } else {
2534 print!("{}", ch); // c:1407
2535 }
2536 }
2537}
2538
2539/// Port of `printif(char *str, int c)` from Src/builtin.c:1411.
2540/// C: `mod_export void printif(char *str, int c)` — `printf(" -%c ", c)`
2541/// then `quotedzputs(str, stdout)`, only when `str != NULL`.
2542pub fn printif(str: Option<&str>, c: u8) {
2543 // c:1411
2544 if let Some(s) = str {
2545 // c:1399
2546 print!(" -{} ", c as char); // c:1399
2547 // c:1399 — quotedzputs(str, stdout); plain print preserves bytes
2548 // for the ASCII case; full quotedzputs lives in src/ported/utils.rs.
2549 print!("{}", s); // c:1399
2550 }
2551}
2552
2553/// Port of `bin_fc(char *nam, char **argv, Options ops, int func)` from Src/builtin.c:1426.
2554/// C: `int bin_fc(char *nam, char **argv, Options ops, int func)`.
2555///
2556/// History/edit/list dispatcher: `-p` push hist stack, `-P` pop,
2557/// `-R` read, `-W` write, `-A` append, `-m` glob filter, `-l` list,
2558/// `-s` substitute, default: edit + re-execute. The C body is ~245
2559/// lines; the structural translation here covers the major options
2560/// and dispatches the underlying history-file ops to the existing
2561/// hist.rs accessors.
2562/// WARNING: param names don't match C — Rust=(nam, argv, func) vs C=(nam, argv, ops, func)
2563pub fn bin_fc(
2564 nam: &str,
2565 argv: &[String], // c:1426
2566 ops_in: &options,
2567 func: i32,
2568) -> i32 {
2569 // C `Options ops` is `struct options *` — mutable via `ops->ind['n']
2570 // = 1;` at c:1644. zshrs HandlerFunc takes `&options`, so we clone
2571 // to a fn-local `ops` mirror at the top. Mutation of the clone is
2572 // intra-fn only (`fclist` reads `ops` to format output and never
2573 // returns it), so behavior matches C.
2574 let mut ops = ops_in.clone();
2575 let ops = &mut ops;
2576 let mut argv = argv.to_vec();
2577 let mut first: i64 = -1;
2578 let mut last: i64 = -1;
2579 let mut asgf: Vec<(String, String)> = Vec::new();
2580
2581 // c:1441-1481 — `-p` push history stack.
2582 if OPT_ISSET(ops, b'p') {
2583 // c:1441
2584 let mut hf = "".to_string();
2585 let mut hs: i64; // c:1443
2586 let mut shs: i64; // c:1444
2587 // c:1445 — `int level = OPT_ISSET(ops,'a') ? locallevel : -1;`
2588 let level: i32 = if OPT_ISSET(ops, b'a') {
2589 locallevel_param.load(Relaxed)
2590 } else {
2591 -1
2592 };
2593 hs = histsiz.load(Relaxed); // c:1442
2594 shs = savehistsiz.load(Relaxed);
2595 if !argv.is_empty() {
2596 // c:1445
2597 hf = argv.remove(0); // c:1446
2598 if !argv.is_empty() {
2599 // c:1447
2600 let s2 = argv.remove(0);
2601 match s2.parse::<i64>() {
2602 // c:1449 zstrtol
2603 Ok(n) => hs = n,
2604 Err(_) => {
2605 zwarnnam(
2606 "fc", // c:1452
2607 "HISTSIZE must be an integer",
2608 );
2609 return 1; // c:1453
2610 }
2611 }
2612 if !argv.is_empty() {
2613 // c:1455
2614 let s3 = argv.remove(0);
2615 match s3.parse::<i64>() {
2616 // c:1456
2617 Ok(n) => shs = n,
2618 Err(_) => {
2619 zwarnnam(
2620 "fc", // c:1459
2621 "SAVEHIST must be an integer",
2622 );
2623 return 1; // c:1460
2624 }
2625 }
2626 } else {
2627 shs = hs; // c:1464
2628 }
2629 if !argv.is_empty() {
2630 // c:1466
2631 zwarnnam(
2632 "fc", // c:1468
2633 "too many arguments",
2634 );
2635 return 1; // c:1469
2636 }
2637 }
2638 }
2639 // c:1473 — pushhiststack(hf, hs, shs, level); failure → return 1.
2640 pushhiststack(Some(&hf), hs, shs, level); // c:1473
2641 if !hf.is_empty() {
2642 // c:1475
2643 // c:1476-1480 — `if (stat(hf, &st) >= 0 || errno != ENOENT)
2644 // readhistfile(hf, 1, HFILE_USE_OPTIONS);`
2645 // Previous Rust port read `Error::last_os_error()` AFTER
2646 // checking `metadata().is_ok()` — racey: any intervening
2647 // syscall between the metadata call and last_os_error()
2648 // can stomp errno on some platforms. Capture the per-Err
2649 // raw_os_error directly so we read the SAME errno value
2650 // the stat call produced.
2651 let stat_result = fs::metadata(&hf);
2652 let should_read = match &stat_result {
2653 Ok(_) => true, // c:1477 stat >= 0
2654 Err(e) => e.raw_os_error() != Some(libc::ENOENT), // c:1477 errno != ENOENT
2655 };
2656 if should_read {
2657 // c:1477
2658 readhistfile(
2659 // c:1478
2660 Some(&hf),
2661 1,
2662 HFILE_USE_OPTIONS as i32,
2663 );
2664 }
2665 }
2666 return 0; // c:1483
2667 }
2668
2669 // c:1485-1491 — `-P` pop history stack.
2670 if OPT_ISSET(ops, b'P') {
2671 // c:1485
2672 if !argv.is_empty() {
2673 // c:1486
2674 zwarnnam("fc", "too many arguments"); // c:1487
2675 return 1; // c:1488
2676 }
2677 // c:1490 — `return !saveandpophiststack(-1, HFILE_USE_OPTIONS);`.
2678 let popped = saveandpophiststack(-1, HFILE_USE_OPTIONS as i32); // c:1490
2679 return if popped != 0 { 0 } else { 1 }; // c:1490 `!` flip
2680 }
2681
2682 // c:1494-1500 — `-m` pattern filter (compile first arg).
2683 let mut pprog: Option<crate::ported::pattern::PatProg> = None;
2684 let mut pprog_src: Option<String> = None;
2685 if !argv.is_empty() && OPT_ISSET(ops, b'm') {
2686 // c:1494
2687 let pat = argv.remove(0);
2688 // c:1495 — tokenize(*argv); — Rust `patcompile` handles tokenisation.
2689 match patcompile(
2690 &{
2691 let mut __pat_tok = (&pat).to_string();
2692 crate::ported::glob::tokenize(&mut __pat_tok);
2693 __pat_tok
2694 }, // c:1496
2695 PAT_HEAPDUP,
2696 None,
2697 ) {
2698 Some(p) => {
2699 pprog = Some(p);
2700 pprog_src = Some(pat); // retain source string for fclist
2701 }
2702 None => {
2703 zwarnnam(nam, "invalid match pattern"); // c:1497
2704 return 1; // c:1498
2705 }
2706 }
2707 }
2708
2709 queue_signals(); // c:1502
2710
2711 // c:1503-1525 — `-R` read / `-W` write / `-A` append history file.
2712 if OPT_ISSET(ops, b'R') {
2713 // c:1503
2714 let path = argv.first().cloned();
2715 let flags = if OPT_ISSET(ops, b'I') {
2716 HFILE_SKIPOLD as i32
2717 } else {
2718 0
2719 };
2720 readhistfile(
2721 // c:1505
2722 path.as_deref(),
2723 1,
2724 flags,
2725 );
2726 unqueue_signals(); // c:1506
2727 return 0; // c:1507
2728 }
2729 if OPT_ISSET(ops, b'W') {
2730 // c:1509
2731 let path = argv.first().cloned();
2732 let flags = if OPT_ISSET(ops, b'I') {
2733 HFILE_SKIPOLD as i32
2734 } else {
2735 0
2736 };
2737 savehistfile(
2738 // c:1511
2739 path.as_deref(),
2740 flags,
2741 );
2742 unqueue_signals(); // c:1512
2743 return 0; // c:1513
2744 }
2745 if OPT_ISSET(ops, b'A') {
2746 // c:1515
2747 let path = argv.first().cloned();
2748 let mut flags = HFILE_APPEND as i32;
2749 if OPT_ISSET(ops, b'I') {
2750 flags |= HFILE_SKIPOLD as i32;
2751 } // c:1518
2752 savehistfile(
2753 // c:1517
2754 path.as_deref(),
2755 flags,
2756 );
2757 unqueue_signals(); // c:1519
2758 return 0; // c:1520
2759 }
2760
2761 // c:1523-1527 — refuse inside ZLE.
2762 if crate::ported::builtins::sched::zleactive.load(
2763 // c:1523
2764 Relaxed,
2765 ) != 0
2766 {
2767 unqueue_signals(); // c:1524
2768 zwarnnam(
2769 nam, // c:1525
2770 "no interactive history within ZLE",
2771 );
2772 return 1; // c:1526
2773 }
2774
2775 // c:1530-1547 — `name=value` substitution pairs.
2776 while !argv.is_empty() && argv[0].contains('=') {
2777 // c:1530
2778 let arg = argv.remove(0);
2779 if let Some(eq) = arg.find('=') {
2780 let n = &arg[..eq];
2781 let v = &arg[eq + 1..];
2782 if n.is_empty() {
2783 zwarnnam(nam, &format!("invalid replacement pattern: ={}", v)); // c:1534
2784 return 1;
2785 }
2786 asgf.push((n.to_string(), v.to_string())); // c:1546
2787 }
2788 }
2789
2790 // c:1550-1568 — first/last history specifiers via fcgetcomm.
2791 if !argv.is_empty() {
2792 // c:1550
2793 first = fcgetcomm(&argv.remove(0)); // c:1551
2794 if first == -1 {
2795 unqueue_signals();
2796 return 1; // c:1553
2797 }
2798 }
2799 if !argv.is_empty() {
2800 // c:1559
2801 last = fcgetcomm(&argv.remove(0)); // c:1560
2802 if last == -1 {
2803 unqueue_signals();
2804 return 1;
2805 }
2806 }
2807 if !argv.is_empty() {
2808 // c:1567
2809 unqueue_signals();
2810 zwarnnam("fc", "too many arguments"); // c:1569
2811 return 1;
2812 }
2813
2814 // c:1573-1610 — default ranges + listing/edit dispatch. C reads
2815 // the live `curhist` global at hist.rs directly. The
2816 // FQN here is forced — bare `curhist` would resolve
2817 // to the local `let curhist` we're declaring.
2818 let curhist: i64 = crate::ported::hist::curhist.load(Relaxed) as i64;
2819 if last == -1 {
2820 // c:1573
2821 if OPT_ISSET(ops, b'l') && first < curhist {
2822 // c:1574
2823 // c:1581-1583 — `last = (curline.histnum == curhist) ?
2824 // addhistnum(curhist,-1,0) : curhist;`
2825 // When the fc command itself occupies the current event
2826 // (the normal interactive case), the default range EXCLUDES
2827 // it — zsh's `fc -l 1` does not list the in-flight `fc -l 1`
2828 // line. The previous `last = curhist` included it.
2829 let curline_num = crate::ported::hist::curline
2830 .lock()
2831 .ok()
2832 .and_then(|c| c.as_ref().map(|e| e.histnum))
2833 .unwrap_or(0);
2834 last = if curline_num == curhist {
2835 crate::ported::hist::addhistnum(curhist, -1, 0) // c:1582
2836 } else {
2837 curhist // c:1583
2838 };
2839 // c:1584-1585 — `if (last < firsthist()) last = firsthist();`
2840 let fh = crate::ported::hist::firsthist();
2841 if last < fh {
2842 last = fh; // c:1585
2843 }
2844 } else {
2845 last = first; // c:1587
2846 }
2847 }
2848 if first == -1 {
2849 // c:1589
2850 let _xflags = if OPT_ISSET(ops, b'L') {
2851 HIST_FOREIGN
2852 } else {
2853 0
2854 }; // c:1597
2855 first = if OPT_ISSET(ops, b'l') {
2856 (curhist - 16).max(1)
2857 }
2858 // c:1598
2859 else {
2860 (curhist - 1).max(1)
2861 };
2862 if last < first {
2863 last = first;
2864 } // c:1604
2865 }
2866
2867 let mut retval;
2868 if OPT_ISSET(ops, b'l') {
2869 // c:1606
2870 // c:1608 — `fclist(stdout, ops, first, last, asgf, pprog, 0);`
2871 retval = fclist(
2872 &mut io::stdout(),
2873 ops,
2874 first,
2875 last,
2876 &asgf,
2877 pprog_src.as_deref(),
2878 0,
2879 );
2880 unqueue_signals();
2881 } else {
2882 // c:1611-1668 — edit history range to a temp file, fcedit it,
2883 // then stuff() the result back as the next command.
2884 retval = 1; // c:1620
2885 let fil_opt = gettempfile(Some("zshfc")); // c:1621 gettempfile
2886 match fil_opt {
2887 None => {
2888 // c:1623
2889 unqueue_signals(); // c:1624
2890 zwarnnam(
2891 "fc", // c:1625
2892 &format!("can't open temp file: {}", io::Error::last_os_error()),
2893 );
2894 }
2895 Some((fd, fil)) => {
2896 unsafe {
2897 libc::close(fd);
2898 } // c:1622 (file is reopened below)
2899 // c:1632 — `if (last >= curhist) { last = curhist - 1; ... }`
2900 if last >= curhist {
2901 // c:1632
2902 last = curhist - 1; // c:1633
2903 if first > last {
2904 // c:1634
2905 unqueue_signals(); // c:1635
2906 zwarnnam(
2907 "fc", // c:1636
2908 "current history line would recurse endlessly, aborted",
2909 );
2910 let _ = fs::remove_file(&fil); // c:1639 unlink
2911 return 1; // c:1640
2912 }
2913 }
2914 ops.ind[b'n' as usize] = 1; // c:1644 No line numbers
2915 let out = fs::OpenOptions::new()
2916 .create(true)
2917 .write(true)
2918 .truncate(true)
2919 .open(&fil)
2920 .ok();
2921 let listed = if let Some(mut f) = out {
2922 // c:1645 — thread pprog filter so `fc -m PAT` only
2923 // edits matching lines in this temp-file edit path.
2924 fclist(&mut f, ops, first, last, &asgf, pprog_src.as_deref(), 1)
2925 } else {
2926 1
2927 };
2928 if listed == 0 {
2929 // c:1645
2930 // c:1647-1656 — pick editor.
2931 let editor: String = if func == BIN_R || OPT_ISSET(ops, b's') {
2932 "-".to_string() // c:1648
2933 } else if OPT_HASARG(ops, b'e') {
2934 // c:1649
2935 OPT_ARG(ops, b'e').unwrap_or("").to_string() // c:1650
2936 } else {
2937 // c:1651-1654 — `getsparam("FCEDIT") ?:
2938 // getsparam("EDITOR") ?:
2939 // DEFAULT_FCEDIT`. paramtab read.
2940 getsparam("FCEDIT")
2941 .or_else(|| getsparam("EDITOR"))
2942 .unwrap_or_else(|| crate::ported::config_h::DEFAULT_FCEDIT.to_string())
2943 };
2944 unqueue_signals(); // c:1657
2945 if fcedit(&editor, &fil) != 0 {
2946 // c:1658
2947 if crate::ported::input::stuff(&fil) != 0 {
2948 // c:1659
2949 zwarnnam(
2950 "fc", // c:1660
2951 &format!("{}: {}", io::Error::last_os_error(), fil),
2952 );
2953 } else {
2954 // c:1663-1664 — `loop(0,1); retval = lastval;`
2955 // The interactive loop drives the next stuffed
2956 // line through the parser. Static-link path:
2957 // the executor's input source picks it up on
2958 // the next read; lastval reflects that result.
2959 retval = LASTVAL.load(
2960 // c:1664
2961 Relaxed,
2962 );
2963 }
2964 }
2965 } else {
2966 unqueue_signals(); // c:1667
2967 }
2968 let _ = fs::remove_file(&fil); // c:1671 unlink
2969 }
2970 }
2971 }
2972 let _ = pprog; // compiled form kept for parity; source threads through fclist.
2973 retval // c:1675
2974}
2975
2976/// Port of `fcgetcomm(char *s)` from Src/builtin.c:1683.
2977/// C: `static zlong fcgetcomm(char *s)` — match `s` against history
2978/// numbers (signed) or prefix; returns the matched event number.
2979/// Direct port of `zlong fcgetcomm(char *s)` from
2980/// `Src/builtin.c:1683`. Resolve an `fc` command-line argument to a
2981/// history event number. Numeric args become event numbers (negative
2982/// numbers count back from current via `addhistnum`); non-numeric
2983/// args go through `hcomsearch` (history prefix search). Emits
2984/// `zwarnnam("fc", "event not found: %s", s)` and returns -1 on
2985/// miss.
2986pub fn fcgetcomm(s: &str) -> i64 {
2987 // c:1683
2988 // c:1689 — `if ((cmd = atoi(s)) != 0 || *s == '0')` numeric arm.
2989 // atoi accepts leading whitespace + optional sign +
2990 // digits; trim+parse mirrors that.
2991 let trimmed = s.trim_start();
2992 let numeric = trimmed.parse::<i64>().ok();
2993 let is_zero_prefix = trimmed.starts_with('0');
2994 if let Some(mut cmd) = numeric {
2995 if cmd != 0 || is_zero_prefix {
2996 if cmd < 0 {
2997 // c:1693 — `cmd = addhistnum(curline.histnum, cmd, HIST_FOREIGN);`
2998 let curh = crate::ported::hist::curhist.load(Relaxed);
2999 cmd = addhistnum(curh, cmd as i32, 1);
3000 }
3001 if cmd < 0 {
3002 // c:1695
3003 cmd = 0;
3004 }
3005 return cmd;
3006 }
3007 }
3008 // c:1700 — `cmd = hcomsearch(s); if (cmd == -1) zwarnnam(...);`
3009 match hcomsearch(s) {
3010 Some(n) => n,
3011 None => {
3012 zwarnnam("fc", &format!("event not found: {}", s));
3013 -1
3014 }
3015 }
3016}
3017
3018/// Port of `fcsubs(char **sp, struct asgment *sub)` from Src/builtin.c:1708.
3019/// C: `static int fcsubs(char **sp, struct asgment *sub)` — apply the
3020/// linked-list of `old=new` substitutions to `*sp` in place; return
3021/// the count of substitutions made.
3022pub fn fcsubs(sp: &mut String, sub: &[(String, String)]) -> i32 {
3023 // c:1708
3024 // c:1708-1748 — for each (old, new), replace each occurrence in *sp.
3025 let mut subbed = 0i32; // c:1713
3026 for (old, new) in sub {
3027 // c:1716
3028 if old.is_empty() {
3029 continue;
3030 }
3031 let count = sp.matches(old.as_str()).count() as i32; // c:1722
3032 if count > 0 {
3033 *sp = sp.replace(old.as_str(), new); // c:1750
3034 subbed += count;
3035 }
3036 }
3037 subbed
3038}
3039
3040/// Direct port of `int fclist(FILE *f, Options ops, zlong first,
3041/// zlong last, struct asgment *subs, Patprog pprog, int is_command)`
3042/// from `Src/builtin.c:1750`. Walks the history event range
3043/// `first..=last`, applies the `subs` substitution chain to each
3044/// matching line (when `pprog` is set, only lines matching it),
3045/// then writes the result with optional timestamp prefix per
3046/// `-d/-f/-E/-i/-t`.
3047///
3048/// Rust signature: takes the output writer as a closure so callers
3049/// can route to stdout, a FILE*, or an in-memory buffer (the
3050/// `is_command` caller in `bin_fc` collects to a heredoc string).
3051/// Was a 5-line stub returning 0; now actually emits the range.
3052#[allow(clippy::too_many_arguments)]
3053pub fn fclist(
3054 out: &mut dyn Write, // c:1750
3055 ops: &options,
3056 mut first: i64,
3057 mut last: i64,
3058 subs: &[(String, String)],
3059 pprog: Option<&str>,
3060 is_command: i32,
3061) -> i32 {
3062 use std::io::Write;
3063
3064 // c:1762-1766 — `if (OPT_ISSET(ops,'r')) swap(first, last);`
3065 if OPT_ISSET(ops, b'r') {
3066 std::mem::swap(&mut first, &mut last);
3067 }
3068 // c:1768-1773 — `if (is_command && first > last) zwarnnam(...)`.
3069 if is_command != 0 && first > last {
3070 zwarnnam("fc", "history events can't be executed backwards, aborted");
3071 return 1;
3072 }
3073
3074 // c:1776-1790 — `gethistent(first, ...)` with bidirectional fallback.
3075 let near = if first < last { 1 } else { -1 };
3076 let start_ev = match gethistent(first, near) {
3077 Some(e) => e,
3078 None => {
3079 // c:Src/builtin.c — `no such event: <N>` carries the
3080 // requested event number so the user can see which
3081 // index missed. zsh appends the failing event id;
3082 // the bare `no such event` message diverged.
3083 zwarnnam(
3084 "fc",
3085 &if first == last {
3086 format!("no such event: {}", first)
3087 } else {
3088 "no events in that range".to_string()
3089 },
3090 );
3091 return 1;
3092 }
3093 };
3094
3095 // c:1792-1817 — timestamp format setup.
3096 let want_time = OPT_ISSET(ops, b'd')
3097 || OPT_ISSET(ops, b'f')
3098 || OPT_ISSET(ops, b'E')
3099 || OPT_ISSET(ops, b'i')
3100 || OPT_ISSET(ops, b't');
3101 let tdfmt: Option<&'static str> = if !want_time {
3102 None
3103 } else if OPT_ISSET(ops, b't') {
3104 Some("%H:%M") // -t expects user-supplied fmt; without OPT_ARG access default to %H:%M
3105 } else if OPT_ISSET(ops, b'i') {
3106 Some("%Y-%m-%d %H:%M")
3107 } else if OPT_ISSET(ops, b'E') {
3108 Some("%d.%m.%Y %H:%M")
3109 } else if OPT_ISSET(ops, b'f') {
3110 Some("%m/%d/%Y %H:%M")
3111 } else {
3112 Some("%H:%M")
3113 };
3114
3115 // c:1820-1880 — walk events from start_ev toward `last`. Each entry:
3116 // apply pprog filter, apply subs chain, emit (with
3117 // event num + timestamp unless -n or is_command).
3118 let mut ev = start_ev;
3119 let step: i64 = if first < last { 1 } else { -1 };
3120 loop {
3121 // c:1830 — `ent = quietgethist(ev);` — fetch entry by event #.
3122 let entry = match quietgethist(ev) {
3123 Some(e) => e,
3124 None => break,
3125 };
3126 let line = entry.node.nam.clone();
3127
3128 // c:1833 — pprog pattern filter. C pre-compiles a Patprog;
3129 // Rust compiles per-call. Most fc -l calls have no
3130 // pattern so the gate is cheap.
3131 if let Some(pat) = pprog {
3132 let prog = patcompile(
3133 &{
3134 let mut __pat_tok = (pat).to_string();
3135 crate::ported::glob::tokenize(&mut __pat_tok);
3136 __pat_tok
3137 },
3138 0,
3139 None,
3140 );
3141 let matched = prog.as_ref().map(|p| pattry(p, &line)).unwrap_or(true);
3142 if !matched {
3143 if ev == last {
3144 break;
3145 }
3146 ev += step;
3147 continue;
3148 }
3149 }
3150
3151 // c:1841-1855 — apply subs chain (asgment list of `old=new`
3152 // pairs that get substituted in order).
3153 let mut text = line;
3154 for (old, new) in subs.iter() {
3155 if old.is_empty() {
3156 continue;
3157 }
3158 text = text.replace(old.as_str(), new.as_str());
3159 }
3160
3161 // c:1860-1870 — emit prefix: event number (unless -n / -h),
3162 // then optional timestamp.
3163 if is_command == 0 {
3164 if !OPT_ISSET(ops, b'n') {
3165 let _ = write!(out, "{:>5}", ev);
3166 if OPT_ISSET(ops, b'D') {
3167 // c:Src/builtin.c — `-D` shows duration as M:SS.
3168 // C: `fprintf(f, "%d:%02d", (int)(d/60), (int)(d%60))`.
3169 // Direct ftim-minus-stim duration in seconds, then
3170 // M:SS layout (zero-pad seconds, no zero-pad minutes).
3171 let dur = entry.ftim.saturating_sub(entry.stim).max(0);
3172 let _ = write!(out, " {}:{:02}", dur / 60, dur % 60);
3173 }
3174 if let Some(fmt) = tdfmt {
3175 // c:1817 — `strftime(timebuf, 256, tdfmt,
3176 // localtime(&ent->stim))`.
3177 // Use libc directly so locale-aware
3178 // format specifiers (%Y %m %d %H %M %S
3179 // %p etc.) all work without a hand-rolled
3180 // strftime port.
3181 let formatted: Option<String> = (|| {
3182 let mut tm: libc::tm = unsafe { std::mem::zeroed() };
3183 let t: libc::time_t = entry.stim as libc::time_t;
3184 let cfmt = std::ffi::CString::new(fmt).ok()?;
3185 unsafe {
3186 if libc::localtime_r(&t, &mut tm).is_null() {
3187 return None;
3188 }
3189 let mut buf = vec![0u8; 256];
3190 let n = libc::strftime(
3191 buf.as_mut_ptr() as *mut libc::c_char,
3192 buf.len(),
3193 cfmt.as_ptr(),
3194 &tm,
3195 );
3196 if n == 0 {
3197 return None;
3198 }
3199 buf.truncate(n);
3200 String::from_utf8(buf).ok()
3201 }
3202 })();
3203 if let Some(s) = formatted {
3204 let _ = write!(out, " {}", s);
3205 } else {
3206 // strftime failed (locale issue / format bug);
3207 // fall back to raw epoch matching C's
3208 // pre-strftime print behavior.
3209 let _ = write!(out, " {}", entry.stim);
3210 }
3211 }
3212 let _ = write!(out, " ");
3213 }
3214 }
3215
3216 // c:1875 — write the line.
3217 let _ = writeln!(out, "{}", text);
3218
3219 if ev == last {
3220 break;
3221 }
3222 ev += step;
3223 if ev < 0 {
3224 break;
3225 }
3226 }
3227 0 // c:1880
3228}
3229
3230/// Port of `fcedit(char *ename, char *fn)` from Src/builtin.c:1885.
3231/// C: `static int fcedit(char *ename, char *fn)` — invoke `$ename fn`,
3232/// returning the editor's exit status (0 if `ename == "-"`).
3233/// WARNING: param names don't match C — Rust=(ename, fn_) vs C=(ename, fn)
3234pub fn fcedit(ename: &str, fn_: &str) -> i32 {
3235 // c:1885
3236 // c:1885 — `if (!strcmp(ename, "-")) return 1;`
3237 if ename == "-" {
3238 // c:1888
3239 return 1; // c:1889
3240 }
3241 // c:1891-1900 — execlp(ename, ename, fn, NULL) wrapped in fork/wait.
3242 let status = std::process::Command::new(ename) // c:1895
3243 .arg(fn_)
3244 .status();
3245 match status {
3246 Ok(s) => s.code().unwrap_or(1),
3247 Err(_) => 1,
3248 }
3249}
3250
3251/// Port of `getasg(char ***argvp, LinkList assigns)` from Src/builtin.c:1908.
3252/// C: `static Asgment getasg(char ***argvp, LinkList assigns)` —
3253/// parse one assignment-form arg (`name=value` / `name`) from
3254/// `*argvp`. Returns NULL when exhausted.
3255/// ```c
3256/// static Asgment
3257/// getasg(char ***argvp, LinkList assigns)
3258/// {
3259/// char *s = **argvp;
3260/// static struct asgment asg;
3261/// if (!s) {
3262/// if (assigns) {
3263/// Asgment asgp = (Asgment)firstnode(assigns);
3264/// if (!asgp) return NULL;
3265/// (void)uremnode(assigns, &asgp->node);
3266/// return asgp;
3267/// }
3268/// return NULL;
3269/// }
3270/// if (*s == '=') { zerr("bad assignment"); return NULL; }
3271/// asg.name = s;
3272/// asg.flags = 0;
3273/// for (; *s && *s != '='; s++);
3274/// if (*s) { *s = '\0'; asg.value.scalar = s + 1; }
3275/// else asg.value.scalar = NULL;
3276/// (*argvp)++;
3277/// return &asg;
3278/// }
3279/// ```
3280pub fn getasg(
3281 argvp: &mut Vec<String>, // c:1908
3282 assigns: &mut Vec<(String, String)>,
3283) -> Option<(String, String)> {
3284 // c:1914-1923 — out-of-args path: drain from assigns list if non-empty.
3285 if argvp.is_empty() {
3286 // c:1914 !s
3287 if !assigns.is_empty() {
3288 // c:1915
3289 return Some(assigns.remove(0)); // c:1916-1920 firstnode + uremnode
3290 }
3291 return None; // c:1922
3292 }
3293
3294 let s = argvp.remove(0); // c:1944 (*argvp)++
3295
3296 // c:1926-1929 — empty-name guard: bare `=value` is an error.
3297 if s.starts_with('=') {
3298 // c:1926
3299 zerr("bad assignment"); // c:1927
3300 return None; // c:1928
3301 }
3302
3303 // c:1934-1943 — split on `=`. No `=` → name-only (scalar = NULL).
3304 match s.find('=') {
3305 // c:1934
3306 Some(i) => {
3307 // c:1938-1939 — `*s = '\0'; asg.value.scalar = s + 1;`
3308 Some((s[..i].to_string(), s[i + 1..].to_string())) // c:1939
3309 }
3310 None => {
3311 // c:1942 — `asg.value.scalar = NULL;` — name-only.
3312 Some((s, String::new())) // c:1942
3313 }
3314 }
3315}
3316
3317/// Port of `typeset_setbase(const char *name, Param pm, Options ops, int on, int always)` from Src/builtin.c:1961.
3318/// C: `static int typeset_setbase(const char *name, Param pm, Options ops,
3319/// int on, int always)` — install numeric base on `pm`. For
3320/// `-i ARG`/`-E ARG`/`-F ARG`, parse ARG as base and validate
3321/// (must be 2..=36 for integer); error → return 1.
3322/// WARNING: param names don't match C — Rust=(name, pm, on, always) vs C=(name, pm, ops, on, always)
3323pub fn typeset_setbase(
3324 name: &str,
3325 pm: *mut param, // c:1961
3326 ops: &options,
3327 on: i32,
3328 always: i32,
3329) -> i32 {
3330 // c:1964 — `char *arg = NULL;`
3331 let mut arg: Option<&str> = None; // c:1964
3332 let on_u = on as u32;
3333 // c:1966-1971 — `if ((on & PM_INTEGER) && OPT_HASARG(ops,'i')) arg = OPT_ARG(ops,'i');`
3334 if (on_u & PM_INTEGER) != 0 && OPT_HASARG(ops, b'i') {
3335 // c:1966
3336 arg = OPT_ARG(ops, b'i'); // c:1967
3337 } else if (on_u & PM_EFLOAT) != 0 && OPT_HASARG(ops, b'E') {
3338 // c:1968
3339 arg = OPT_ARG(ops, b'E'); // c:1969
3340 } else if (on_u & PM_FFLOAT) != 0 && OPT_HASARG(ops, b'F') {
3341 // c:1970
3342 arg = OPT_ARG(ops, b'F'); // c:1971
3343 }
3344
3345 // c:1973 — `if (arg) {`
3346 if let Some(a) = arg {
3347 // c:1973
3348 // c:1976 — `int base = (int)zstrtol(arg, &eptr, 10);`
3349 let base = match a.trim().parse::<i32>() {
3350 Ok(b) => b,
3351 Err(_) => {
3352 // c:1977-1982
3353 if (on_u & PM_INTEGER) != 0 {
3354 zwarnnam(name, &format!("bad base value: {}", a)); // c:1979
3355 } else {
3356 zwarnnam(name, &format!("bad precision value: {}", a)); // c:1981
3357 }
3358 return 1; // c:1983
3359 }
3360 };
3361 // c:1985-1989 — integer base must be 2..=36 inclusive.
3362 if (on_u & PM_INTEGER) != 0 && (base < 2 || base > 36) {
3363 // c:1985
3364 zwarnnam(
3365 name,
3366 &format!("invalid base (must be 2 to 36 inclusive): {}", base),
3367 ); // c:1986-1987
3368 return 1; // c:1988
3369 }
3370 // c:1990 — `pm->base = base;`
3371 if !pm.is_null() {
3372 unsafe {
3373 (*pm).base = base;
3374 } // c:1990
3375 }
3376 } else if always != 0 {
3377 // c:1991
3378 // c:1997 — `pm->base = 0;`
3379 if !pm.is_null() {
3380 unsafe {
3381 (*pm).base = 0;
3382 } // c:1997
3383 }
3384 }
3385 0 // c:1997
3386}
3387
3388/// Port of `typeset_setwidth(const char * name, Param pm, Options ops, int on, int always)` from Src/builtin.c:1997.
3389/// C: `static int typeset_setwidth(const char *name, Param pm, Options ops,
3390/// int on, int always)` — install padding width via `-L/-R/-Z ARG`.
3391/// WARNING: param names don't match C — Rust=(name, pm, on, always) vs C=(name, pm, ops, on, always)
3392pub fn typeset_setwidth(
3393 name: &str,
3394 pm: *mut param, // c:1997
3395 ops: &options,
3396 on: i32,
3397 always: i32,
3398) -> i32 {
3399 // c:2000 — `char *arg = NULL;`
3400 let mut arg: Option<&str> = None; // c:2000
3401 let on_u = on as u32;
3402 // c:2002-2007
3403 if (on_u & PM_LEFT) != 0 && OPT_HASARG(ops, b'L') {
3404 // c:2002
3405 arg = OPT_ARG(ops, b'L'); // c:2003
3406 } else if (on_u & PM_RIGHT_B) != 0 && OPT_HASARG(ops, b'R') {
3407 // c:2004
3408 arg = OPT_ARG(ops, b'R'); // c:2005
3409 } else if (on_u & PM_RIGHT_Z) != 0 && OPT_HASARG(ops, b'Z') {
3410 // c:2006
3411 arg = OPT_ARG(ops, b'Z'); // c:2007
3412 }
3413
3414 // c:2009 — `if (arg) {`
3415 if let Some(a) = arg {
3416 // c:2009
3417 // c:2011 — `pm->width = (int)zstrtol(arg, &eptr, 10);`
3418 let width = match a.trim().parse::<i32>() {
3419 Ok(w) => w,
3420 Err(_) => {
3421 zwarnnam(name, &format!("bad width value: {}", a)); // c:2013
3422 return 1; // c:2014
3423 }
3424 };
3425 if !pm.is_null() {
3426 unsafe {
3427 (*pm).width = width;
3428 } // c:2011
3429 }
3430 } else if always != 0 {
3431 // c:2015
3432 // c:2016 — `pm->width = 0;`
3433 if !pm.is_null() {
3434 unsafe {
3435 (*pm).width = 0;
3436 } // c:2025
3437 }
3438 }
3439 0 // c:2025
3440}
3441
3442/// 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.
3443/// Port of `static Param typeset_single(char *cname, char *pname,
3444/// Param pm, int func, int on, int off, int roff, Asgment asg,
3445/// Param altpm, Options ops, int joinchar)` from `Src/builtin.c:2025`.
3446/// Per-name attribute resolver + assignment dispatcher invoked once
3447/// per arg from `bin_typeset`.
3448#[allow(clippy::too_many_arguments)]
3449pub fn typeset_single(
3450 cname: &str,
3451 pname: &str, // c:2025
3452 pm: *mut param,
3453 func: i32,
3454 mut on: i32,
3455 mut off: i32,
3456 _roff: i32,
3457 asg: *mut asgment,
3458 altpm: *mut param,
3459 ops: &options,
3460 _joinchar: i32,
3461) -> *mut param {
3462 let mut usepm: i32; // c:2029
3463 let mut tc: i32 = 0; // c:2029
3464 let _keeplocal: i32 = 0; // c:2029
3465 let mut newspecial: i32 = 0; /* NS_NONE */
3466 // c:2029
3467 let _readonly: i32 = 0; // c:2029
3468 let _dont_set: i32 = 0; // c:2029
3469 let mut pname_owned: String = pname.to_string(); // c:2030 subscript path
3470
3471 // c:2032-2050 — nameref resolution.
3472 let pm_ref = unsafe { pm.as_mut() };
3473 if let Some(pm_r) = &pm_ref {
3474 let pm_flags = pm_r.node.flags as u32;
3475 let locallevel_v = locallevel_param.load(Relaxed);
3476 if (pm_flags & PM_NAMEREF) != 0
3477 && ((off | on) as u32 & PM_NAMEREF) == 0
3478 && (pm_r.level == locallevel_v || (on as u32 & PM_LOCAL) == 0)
3479 {
3480 // c:2034 — pm = resolve_nameref(pm)
3481 // pname = pm->node.nam (when resolved)
3482 // resolve_nameref not yet ported; skip the rewrite.
3483 let unresolved_flags = pm_r.node.flags as u32;
3484 let extra_on_mask = !(PM_NAMEREF | PM_LOCAL | PM_READONLY) as i32;
3485 if (pm_flags & PM_NAMEREF) != 0
3486 && ((unresolved_flags & PM_UNSET) == 0 || (unresolved_flags & PM_DECLARED) != 0)
3487 && (on & extra_on_mask) != 0
3488 {
3489 // c:2042-2048 — error: can't change type of a nameref.
3490 if pm_r.width != 0 {
3491 // c:2041
3492 zwarnnam(
3493 cname, // c:2042
3494 &format!("{}: can't change type via subscript reference", pname),
3495 );
3496 } else {
3497 zwarnnam(
3498 cname, // c:2046
3499 &format!("{}: can't change type of a named reference", pname),
3500 );
3501 }
3502 return std::ptr::null_mut(); // c:2048
3503 }
3504 }
3505 }
3506
3507 // c:2062-2064 — `usepm = pm && (!(pm_flags & PM_UNSET) || OPT_ISSET(ops,'p') || ...)`
3508 let pm_flags = pm_ref.as_ref().map(|p| p.node.flags as u32).unwrap_or(0);
3509 usepm = if pm_ref.is_some()
3510 && ((pm_flags & PM_UNSET) == 0
3511 || OPT_ISSET(ops, b'p')
3512 || (isset(POSIXBUILTINS) && (pm_flags & (PM_READONLY | PM_EXPORTED)) != 0))
3513 {
3514 1
3515 } else {
3516 0
3517 };
3518
3519 // c:2070-2071 — preserve PM_UNSET for special params.
3520 if usepm == 0 && pm_ref.is_some() && (pm_flags & PM_SPECIAL) != 0 {
3521 usepm = 2; // c:2071
3522 }
3523
3524 // c:2078-2091 — don't reuse if local-level changed and PM_LOCAL set.
3525 let pm_level = pm_ref.as_ref().map(|p| p.level).unwrap_or(0);
3526 let locallevel_v = locallevel_param.load(Relaxed);
3527 if usepm != 0 && locallevel_v != pm_level && (on as u32 & PM_LOCAL) != 0 {
3528 // c:2078
3529 if (pm_flags & PM_SPECIAL) != 0 // c:2087
3530 && (on as u32 & PM_HIDE) == 0
3531 && (pm_flags & PM_HIDE & !off as u32) == 0
3532 {
3533 newspecial = 1; /* NS_NORMAL */ // c:2089
3534 }
3535 usepm = 0; // c:2090
3536 }
3537
3538 // c:2093-2116 — type-conversion / tied-colonarray detection.
3539 let asg_ref = unsafe { asg.as_ref() };
3540 tc = 0;
3541 if let Some(a) = asg_ref {
3542 if ASG_ARRAYP(a)
3543 && PM_TYPE(on as u32) == PM_SCALAR
3544 && !(usepm != 0 && (PM_TYPE(pm_flags) & (PM_ARRAY | PM_HASHED)) != 0)
3545 {
3546 on |= PM_ARRAY as i32; // c:2097
3547 }
3548 if usepm != 0 && ASG_ARRAYP(a) && newspecial == 0 // c:2098
3549 && PM_TYPE(pm_flags) != PM_ARRAY
3550 && PM_TYPE(pm_flags) != PM_HASHED
3551 {
3552 if (on as u32 & (PM_EFLOAT | PM_FFLOAT | PM_INTEGER)) != 0 {
3553 zerrnam(
3554 cname, // c:2102
3555 &format!("{}: can't assign array value to non-array", pname),
3556 );
3557 return std::ptr::null_mut();
3558 }
3559 if (pm_flags & PM_SPECIAL) != 0 {
3560 // c:2105
3561 zerrnam(
3562 cname, // c:2106
3563 &format!("{}: can't assign array value to non-array special", pname),
3564 );
3565 return std::ptr::null_mut();
3566 }
3567 tc = 1; // c:2109
3568 usepm = if OPT_MINUS(ops, b'p') {
3569 // c:2110
3570 (on as u32 & pm_flags) as i32
3571 } else if OPT_PLUS(ops, b'p') {
3572 // c:2112
3573 (off as u32 & pm_flags) as i32
3574 } else {
3575 0 // c:2115
3576 };
3577 }
3578 }
3579
3580 // c:2117-2199 — attribute-mask compatibility checks (chflags compute).
3581 if usepm != 0 || newspecial != 0 {
3582 let chflags = ((off as u32 & pm_flags) | (on as u32 & !pm_flags)) // c:2118
3583 & (PM_INTEGER
3584 | PM_EFLOAT
3585 | PM_FFLOAT
3586 | PM_HASHED | PM_ARRAY | PM_TIED | PM_AUTOLOAD);
3587 if chflags != 0 && chflags != (PM_EFLOAT | PM_FFLOAT) {
3588 tc = 1; // c:2122
3589 usepm = if OPT_MINUS(ops, b'p') {
3590 // c:2123
3591 (on as u32 & pm_flags) as i32
3592 } else if OPT_PLUS(ops, b'p') {
3593 // c:2125
3594 (off as u32 & pm_flags) as i32
3595 } else {
3596 // c:2127 — `else usepm = 0;`
3597 //
3598 // This `else` was missing, and it is what selects between the
3599 // two mutually exclusive paths. usepm != 0 takes the MERGE path
3600 // (c:2291: keep the existing flags, add `on`, drop `off`), while
3601 // usepm == 0 falls through to the type-conversion path (c:2355),
3602 // which deletes the parameter and recreates it carrying ONLY
3603 // READONLY|EXPORTED (c:2357). Leaving usepm set meant a base-type
3604 // change merged instead of recreating, so `typeset -Z 3 x=7;
3605 // typeset -i x=1` kept the right_zeros attribute (zsh: `integer`,
3606 // zshrs: `integer-right_zeros`).
3607 0
3608 };
3609 }
3610 }
3611
3612 // c:2202-2214 — readonly/exported preservation rules.
3613 if usepm != 0 || newspecial != 0 {
3614 if (on as u32 & (PM_READONLY | PM_EXPORTED)) != 0 // c:2202
3615 && (usepm == 0 || (pm_flags & PM_UNSET) != 0)
3616 && asg_ref.is_some_and(|a| !ASG_VALUEP(a))
3617 {
3618 on |= PM_UNSET as i32; // c:2205
3619 } else if usepm != 0 && (pm_flags & PM_READONLY) != 0 // c:2206
3620 && (on as u32 & PM_READONLY) == 0
3621 && func != BIN_EXPORT
3622 {
3623 zerr(&format!(
3624 // c:2208
3625 "read-only variable: {}",
3626 pm_ref.as_ref().unwrap().node.nam
3627 ));
3628 return std::ptr::null_mut();
3629 }
3630 }
3631
3632 // c:2226-2248 — reuse-existing-param fast paths.
3633 if usepm != 0 {
3634 let pm_r = pm_ref.as_ref().unwrap();
3635 if OPT_MINUS(ops, b'p')
3636 && on != 0
3637 && !((on as u32 & pm_flags) != 0 || ((on as u32 & PM_LOCAL) != 0 && pm_r.level != 0))
3638 {
3639 return std::ptr::null_mut(); // c:2229
3640 }
3641 if OPT_PLUS(ops, b'p') && off != 0 && (off as u32 & pm_flags) == 0 {
3642 return std::ptr::null_mut(); // c:2231
3643 }
3644 // c:2232-2238 — array/scalar consistency check
3645 if let Some(a) = asg_ref {
3646 let array_assign = (a.flags & ASG_ARRAY) != 0;
3647 let pm_is_arr = (PM_TYPE(pm_flags) & (PM_ARRAY | PM_HASHED)) != 0;
3648 if array_assign && !pm_is_arr {
3649 // c:2232
3650 zerrnam(
3651 cname, // c:2236
3652 &format!("{}: inconsistent type for assignment", pname),
3653 );
3654 return std::ptr::null_mut();
3655 }
3656 }
3657 }
3658
3659 // c:2240-2247 — print-only path: typeset -p / typeset name (no value).
3660 if usepm != 0 && on == 0 && _roff == 0 && asg_ref.is_some_and(|a| !ASG_VALUEP(a)) {
3661 // c:2241 — `int with_ns = OPT_ISSET(ops,'m') ? PRINT_WITH_NAMESPACE : 0;`
3662 let with_ns = if OPT_ISSET(ops, b'm') {
3663 // c:2241
3664 PRINT_WITH_NAMESPACE
3665 } else {
3666 0
3667 };
3668 // c:Src/builtin.c:2761-2765 — `typeset -p1 NAME` adds PRINT_LINE
3669 // (one array/assoc element per line). The listing path (~3805)
3670 // already parses this; the explicit-name print path here did not,
3671 // so `typeset -p1 myarray` printed the single-line `( a b c )`
3672 // form instead of zsh's multi-line `(\n a\n b\n c\n)`.
3673 let line_flag =
3674 if OPT_HASARG(ops, b'p') && OPT_ARG(ops, b'p').map(|a| a.trim()) == Some("1") {
3675 PRINT_LINE
3676 } else {
3677 0
3678 };
3679 if let Some(pm_r) = unsafe { pm.as_mut() } {
3680 if OPT_ISSET(ops, b'p') {
3681 // c:2242
3682 // c:2243 — `paramtab->printnode(&pm->node, PRINT_TYPESET|with_ns);`
3683 printparamnode(pm_r, PRINT_TYPESET | with_ns | line_flag);
3684 } else if !OPT_ISSET(ops, b'g') // c:2244
3685 && (!isset(TYPESETSILENT) || OPT_ISSET(ops, b'm'))
3686 // c:2245
3687 {
3688 // c:2246 — `paramtab->printnode(&pm->node, PRINT_INCLUDEVALUE|with_ns);`
3689 printparamnode(pm_r, PRINT_INCLUDEVALUE | with_ns);
3690 }
3691 }
3692 return pm; // c:2247
3693 }
3694
3695 // c:2355-2378 — tc (type-conversion) branch: recreate the param.
3696 if tc != 0 && !OPT_ISSET(ops, b'p') {
3697 on |= (!off as u32 & (PM_READONLY | PM_EXPORTED) & pm_flags) as i32; // c:2357
3698 if let Some(pm_r) = pm_ref {
3699 pm_r.node.flags &= !(PM_READONLY as i32); // c:2359
3700 }
3701 // c:2364 — keeplocal = pm->level (used by createparam path)
3702 // c:2372-2375 — carry scalar value across type change.
3703 // c:2378 — unsetparam_pm(pm, 0, 1)
3704 if let Some(pm_r) = unsafe { pm.as_mut() } {
3705 unsetparam_pm(pm_r, 0, 1);
3706 }
3707 pname_owned = pname.to_string(); // c:2377
3708 }
3709
3710 // c:2381-2467 — newspecial path: preserve special-param struct.
3711 // c:2469-2510 — createparam + assignment dispatch for new/converted.
3712 // c:2512-2453 — apply value via assignsparam/setaparam/sethparam.
3713 // These call into a 2-level helper chain (typeset_setwidth,
3714 // typeset_setbase, assignsparam, etc.) — the available Rust
3715 // ports drive single-attribute setters. The dispatcher entry
3716 // (bin_typeset at c:2655) walks the option matrix and invokes
3717 // those setters directly today.
3718 let _ = (altpm, pname_owned, _keeplocal, _dont_set, _readonly);
3719
3720 // c:2547 — `return pm;`
3721 pm
3722}
3723
3724/// Port of `bin_typeset(char *name, char **argv, LinkList assigns, Options ops, int func)` from Src/builtin.c:2655.
3725/// C: `int bin_typeset(char *name, char **argv, LinkList assigns,
3726/// Options ops, int func)`.
3727///
3728/// The C body (~500 lines) ports here in two layers: the option-flag
3729/// matrix + conflict-resolution / dispatch (faithfully translated)
3730/// and the per-arg param-setting loop (delegated to typeset_single
3731/// already ported above).
3732/// WARNING: param names don't match C — Rust=(name, argv, func) vs C=(name, argv, assigns, ops, func)
3733pub fn bin_typeset(
3734 name: &str,
3735 argv: &[String], // c:2655
3736 ops: &options,
3737 func: i32,
3738) -> i32 {
3739 // PFA-SMR aspect: bin_typeset is the C dispatch site for
3740 // typeset/declare/integer/float/local/export/readonly/private —
3741 // every one of those state-mutating builtins lands here with a
3742 // funcid (BIN_EXPORT/BIN_READONLY/BIN_TYPESET/...) discriminant.
3743 // Emit a per-name event per the recorder schema.
3744 #[cfg(feature = "recorder")]
3745 if crate::recorder::is_enabled() {
3746 let ctx = crate::recorder::recorder_ctx_global();
3747 // Collect option letters from argv `-x`/`+x` flag args AND
3748 // from the BUILTIN's defopts (the auto-set option chars C
3749 // applies before dispatching bin_typeset). Without the
3750 // defopts, `integer count=42` (defopts="i"), `float pi=1.0`
3751 // (defopts="E"), and `typeset -A h=(...)` whose `A` came
3752 // from a parent shape, lost their type bits and the
3753 // recorder emitted `[scalar]` for everything.
3754 let mut letters = String::new();
3755 let mut tied_mode = false;
3756 for a in argv {
3757 if a.starts_with('-') || a.starts_with('+') {
3758 let body = &a[1..];
3759 letters.push_str(body);
3760 if body.contains('T') {
3761 tied_mode = true;
3762 }
3763 }
3764 }
3765 // Add the option letters that defopts pre-set in `ops`.
3766 for ch in [b'i', b'E', b'F', b'A', b'a', b'r', b'x', b'g', b'l', b'u'] {
3767 if OPT_ISSET(&ops, ch) && !letters.contains(ch as char) {
3768 letters.push(ch as char);
3769 }
3770 }
3771 // Funcid-driven attr seeding: BIN_EXPORT seeds nothing
3772 // (recorder uses emit_export for those), BIN_READONLY seeds
3773 // SCALAR|READONLY. INTEGER/FLOAT/ASSOC/ARRAY now flow via
3774 // the defopts injection above into ParamAttrs::from_flag_chars.
3775 let mut attrs = crate::recorder::ParamAttrs::from_flag_chars(&letters);
3776 match func {
3777 crate::ported::builtin::BIN_READONLY => {
3778 attrs.set(crate::recorder::ParamAttrs::SCALAR);
3779 attrs.set(crate::recorder::ParamAttrs::READONLY);
3780 }
3781 _ => {}
3782 }
3783 // BIN_EXPORT routes to emit_export (different schema row).
3784 if func == crate::ported::builtin::BIN_EXPORT {
3785 for a in argv {
3786 if a == "-p" || a.starts_with('-') {
3787 continue;
3788 }
3789 if let Some((k, v)) = a.split_once('=') {
3790 crate::recorder::emit_export(k, Some(v), ctx.clone());
3791 } else {
3792 crate::recorder::emit_export(a, None, ctx.clone());
3793 }
3794 }
3795 } else {
3796 // Suppress the emit when invoked as `local`/`private` inside
3797 // a function — those scope to the frame and don't merit a
3798 // top-level state-mutation row. local_scope_depth is tracked
3799 // by the executor; defer to the global locallevel_param counter.
3800 let is_locallike = matches!(name, "local" | "private");
3801 let inside_function = locallevel_param.load(std::sync::atomic::Ordering::Relaxed) > 0;
3802 if !is_locallike || !inside_function {
3803 let mut tied_seen = 0usize;
3804 for a in argv {
3805 if a.starts_with('-') || a.starts_with('+') {
3806 continue;
3807 }
3808 if tied_mode {
3809 // For `typeset -T X Y [SEP]`, only X and Y are names.
3810 tied_seen += 1;
3811 if tied_seen > 2 {
3812 break;
3813 }
3814 }
3815 if let Some((k, v)) = a.split_once('=') {
3816 crate::recorder::emit_typeset_attrs(k, Some(v), attrs, ctx.clone());
3817 } else {
3818 crate::recorder::emit_typeset_attrs(a, None, attrs, ctx.clone());
3819 }
3820 }
3821 }
3822 }
3823 }
3824 let mut ops = ops.clone();
3825 let mut on: u32 = 0; // c:2661
3826 let mut off: u32 = 0; // c:2661
3827 let mut returnval: i32 = 0; // c:2664
3828 let mut printflags: i32 = PRINT_WITH_NAMESPACE; // c:2664
3829 let hasargs = !argv.is_empty(); // c:2665
3830
3831 // c:2668-2670 — POSIX bash/ksh ignore -p with args under
3832 // readonly/export.
3833 let posix = isset(POSIXBUILTINS);
3834 if (func == BIN_READONLY || func == BIN_EXPORT) && posix && hasargs {
3835 // c:2668
3836 ops.ind[b'p' as usize] = 0; // c:2670
3837 }
3838
3839 // c:2673 — `if (OPT_ISSET(ops,'f')) return bin_functions(...)`.
3840 if OPT_ISSET(&ops, b'f') {
3841 // c:2673
3842 return bin_functions(name, argv, &ops, func); // c:2673
3843 }
3844
3845 // c:2676 — POSIX readonly forces -g unless explicit +g.
3846 if func == BIN_READONLY && posix && !OPT_PLUS(&ops, b'g') {
3847 // c:2676
3848 ops.ind[b'g' as usize] = 1; // c:2677
3849 }
3850
3851 // c:2691-2706 — translate optstr letters into PM_* flag bits.
3852 let mut bit: u32 = PM_ARRAY; // c:2660
3853 for ch in TYPESET_OPTSTR.chars() {
3854 // c:2691
3855 let optval = ch as u8;
3856 if OPT_MINUS(&ops, optval) {
3857 on |= bit;
3858 }
3859 // c:2694-2695
3860 else if OPT_PLUS(&ops, optval) {
3861 off |= bit;
3862 }
3863 // c:2696-2697
3864 // c:2698-2706 — `-n` only allows readonly/upper/hideval.
3865 else {
3866 bit <<= 1;
3867 continue;
3868 }
3869 if OPT_MINUS(&ops, b'n') && (bit & !(PM_READONLY | PM_UPPER | PM_HIDEVAL)) != 0
3870 // c:2701
3871 {
3872 zwarnnam(name, &format!("-{} not allowed with -n", ch)); // c:2702
3873 }
3874 bit <<= 1;
3875 }
3876 // c:2708-2715 — -n / +n conflict resolution.
3877 if OPT_MINUS(&ops, b'n') {
3878 // c:2709-2711 — only readonly/upper/hideval combine with -n;
3879 // anything else is a (silent) status-1 rejection (the zwarnnam
3880 // here is commented out in C — the per-bit loop above already
3881 // warned).
3882 if ((on | off) & !(PM_READONLY | PM_UPPER | PM_HIDEVAL)) != 0 {
3883 // c:2710
3884 return 1; // c:2711
3885 }
3886 on |= PM_NAMEREF; // c:2712
3887 } else if OPT_PLUS(&ops, b'n') {
3888 // c:2714
3889 off |= PM_NAMEREF; // c:2715
3890 }
3891 let roff = off; // c:2716
3892
3893 // c:2719-2740 — sanity checks: remove conflicting attrs.
3894 if (on & PM_FFLOAT) != 0 {
3895 // c:2719
3896 off |= PM_UPPER | PM_ARRAY | PM_HASHED | PM_INTEGER | PM_EFLOAT; // c:2720
3897 on &= !PM_EFLOAT; // c:2722
3898 }
3899 if (on & PM_EFLOAT) != 0 {
3900 // c:2724
3901 off |= PM_UPPER | PM_ARRAY | PM_HASHED | PM_INTEGER | PM_FFLOAT; // c:2725
3902 }
3903 if (on & PM_INTEGER) != 0 {
3904 // c:2726
3905 off |= PM_UPPER | PM_ARRAY | PM_HASHED | PM_EFLOAT | PM_FFLOAT; // c:2727
3906 }
3907 if (on & (PM_LEFT | PM_RIGHT_Z)) != 0 {
3908 // c:2731
3909 off |= PM_RIGHT_B; // c:2732
3910 }
3911 if (on & PM_RIGHT_B) != 0 {
3912 // c:2733
3913 off |= PM_LEFT | PM_RIGHT_Z; // c:2734
3914 }
3915 if (on & PM_UPPER) != 0 {
3916 off |= PM_LOWER;
3917 } // c:2735-2736
3918 if (on & PM_LOWER) != 0 {
3919 off |= PM_UPPER;
3920 } // c:2737-2738
3921 if (on & PM_HASHED) != 0 {
3922 off |= PM_ARRAY;
3923 } // c:2739-2740
3924 if (on & PM_TIED) != 0 {
3925 // c:2741
3926 off |= PM_INTEGER | PM_EFLOAT | PM_FFLOAT | PM_ARRAY | PM_HASHED; // c:2742
3927 }
3928 on &= !off; // c:2744
3929
3930 queue_signals(); // c:2746
3931
3932 // c:2748-2772 — `-p` print-mode: PRINT_POSIX_EXPORT / READONLY /
3933 // TYPESET, plus optional -p N for line-style.
3934 if OPT_ISSET(&ops, b'p') {
3935 // c:2748
3936 if posix && !EMULATION(EMULATE_KSH) {
3937 // c:2750
3938 printflags |= match func {
3939 BIN_EXPORT => PRINT_POSIX_EXPORT, // c:2752
3940 BIN_READONLY => PRINT_POSIX_READONLY, // c:2754
3941 _ => PRINT_TYPESET, // c:2756
3942 };
3943 } else {
3944 printflags |= PRINT_TYPESET; // c:2758
3945 }
3946 if OPT_HASARG(&ops, b'p') {
3947 // c:2761
3948 let arg = OPT_ARG(&ops, b'p').unwrap_or("");
3949 match arg.trim().parse::<i32>() {
3950 // c:2763
3951 Ok(1) => printflags |= PRINT_LINE, // c:2765
3952 Ok(0) => {} // c:2770 -p0 == -p
3953 _ => {
3954 zwarnnam(name, &format!("bad argument to -p: {}", arg)); // c:2767
3955 unqueue_signals();
3956 return 1; // c:2769
3957 }
3958 }
3959 }
3960 }
3961
3962 // c:2775-2795 — no-args path: list whatever options select.
3963 if !hasargs {
3964 // c:2775
3965 if !OPT_ISSET(&ops, b'm') {
3966 // c:2779
3967 printflags &= !PRINT_WITH_NAMESPACE; // c:2780
3968 }
3969 if !OPT_ISSET(&ops, b'p') {
3970 // c:2782
3971 if (on | roff) == 0 {
3972 // c:2783
3973 printflags |= PRINT_TYPE; // c:2784
3974 }
3975 if roff != 0 || OPT_ISSET(&ops, b'+') {
3976 // c:2785
3977 printflags |= PRINT_NAMEONLY; // c:2786
3978 }
3979 }
3980 // c:2792 — `scanhashtable(paramtab, 1, on|roff, 0, paramtab->printnode,
3981 // printflags|(roff ? PRINT_NAMEONLY : 0));`
3982 //
3983 // Walk paramtab (sorted=1, alphabetical) filtering by on|roff
3984 // and dispatch printparamnode for each match. Previously inlined
3985 // a `println!("{}={}", k, v)` which:
3986 // - Ignored printflags (PRINT_TYPESET, PRINT_POSIX_EXPORT,
3987 // PRINT_POSIX_READONLY, PRINT_NAMEONLY) so `export -p` had
3988 // zero rows, `typeset -p` skipped attribute letters, and
3989 // `readonly -p` had no `readonly ` prefix.
3990 // - Read pm.u_str directly so PM_INTEGER / PM_*FLOAT /
3991 // PM_ARRAY / PM_HASHED values printed as empty.
3992 // printparamnode (params.c:6123) handles all of these.
3993 let printflags_final = printflags | if roff != 0 { PRINT_NAMEONLY } else { 0 }; // c:2792
3994 // c:2792 scanmatchtable flags1=on|roff, flags2=0.
3995 let on_roff = (on as u32) | (roff as u32);
3996 // PM_RO_BY_DESIGN expansion: zshrs's special-param
3997 // setup (vm_helper.rs:1054+) replaces the dropped
3998 // PM_READONLY bit with PM_RO_BY_DESIGN so internal
3999 // writes still pass assignstrvalue's PM_READONLY
4000 // guard. `typeset -r` listing must match on either
4001 // bit to surface those entries. Mirrors C zsh's
4002 // PM_READONLY_SPECIAL (= PM_SPECIAL | PM_READONLY |
4003 // PM_RO_BY_DESIGN) where the scanhashtable bit-mask
4004 // implicitly matches RO_BY_DESIGN too (both bits
4005 // set on the same params in C). #97 in docs/BUGS.md.
4006 let on_roff_expanded = if (on_roff & PM_READONLY) != 0 {
4007 on_roff | PM_RO_BY_DESIGN
4008 } else {
4009 on_roff
4010 };
4011 // c:Src/module.c:1218-1219 add_autoparam — in `zsh -f`, every
4012 // autoloadable module parameter (WATCH/watch, aliases…,
4013 // widgets/keymaps, termcap/terminfo, zsh_scheduled_events) is
4014 // a real paramtab stub: a scalar whose value is the module
4015 // name, flagged PM_AUTOLOAD. scanhashtable feeds the stubs to
4016 // printparamnode which prints `undefined NAME` (pmtypes row at
4017 // Src/params.c:6011) and suppresses them under -p
4018 // (Src/params.c:6150-6155). zshrs keeps always-functional
4019 // placeholder params instead, so synthesize the stub rows here
4020 // for every not-yet-loaded module and suppress the placeholder
4021 // rows for the same names (zsh -f shows `undefined WATCH`, not
4022 // a set `WATCH`).
4023 let stubs: Vec<(&'static str, &'static str)> = crate::vm_helper::autoload_param_stubs();
4024 let names: Vec<String> = {
4025 let tab = paramtab().read().unwrap();
4026 let mut names: Vec<String> = tab
4027 .iter()
4028 .filter(|(k, pm)| {
4029 let f = pm.node.flags as u32;
4030 // c:2793 — scanhashtable passes PM_UNSET entries
4031 // through; printparamnode (c:6133-6144) prints
4032 // PM_DEFAULTED (declared-but-unset) ones name-only
4033 // and drops the rest. Keep DEFAULTED here so
4034 // `typeset -n` lists placeholder refs by name.
4035 if (f & PM_UNSET) != 0 && (f & PM_DEFAULTED) != PM_DEFAULTED {
4036 return false;
4037 }
4038 // Unloaded-module names print as autoload stubs
4039 // (merged below) — never as their placeholder row.
4040 if stubs.iter().any(|(n, _)| n == k) {
4041 return false;
4042 }
4043 // c:Src/Modules/parameter.c — magic-assoc names
4044 // (aliases/builtins/commands/options/functions/
4045 // parameters/etc.) live in a separate PARTAB in
4046 // C, NOT paramtab; the user-space scanhashtable
4047 // never sees them. The Rust port shoves them
4048 // into paramtab via vm_helper::init_partab_params
4049 // as PM_HIDE-tagged placeholders. Mirror the C
4050 // invisibility by skipping PM_HIDE entries here.
4051 // Bug #371 in docs/BUGS.md: `typeset -A`
4052 // (no args) listed all magic-assocs instead of
4053 // only user-defined ones.
4054 if (f & PM_HIDE) != 0 {
4055 return false;
4056 }
4057 on_roff_expanded == 0 || (f & on_roff_expanded) != 0
4058 })
4059 .map(|(k, _)| k.clone())
4060 .collect();
4061 // Stub flags are PM_SCALAR|PM_AUTOLOAD (module.c:1218-1219)
4062 // — apply the same scanmatchtable flags1 test (PM_AUTOLOAD
4063 // is never in on|roff, so any flag filter excludes them:
4064 // `typeset +x -r` / `typeset +i` list none, matching zsh).
4065 for (n, _) in &stubs {
4066 if on_roff_expanded == 0 || (PM_AUTOLOAD & on_roff_expanded) != 0 {
4067 names.push((*n).to_string());
4068 }
4069 }
4070 names.sort_by(|a, b| hnamcmp(a, b));
4071 names
4072 };
4073 // c:Src/params.c:6275 PM_TIED partner lookup — printparamnode
4074 // reads `paramtab` to fetch the tied peer (e.g. PATH→path).
4075 // Pre-clone the pm so the print runs WITHOUT holding any
4076 // paramtab lock; otherwise the partner-lookup inside
4077 // printparamnode would deadlock on the same write lock.
4078 // printparamnode mutates hn for the PM_TIED swap path (Bug
4079 // #410) — the mutation lands on the local clone only,
4080 // matching C's "throwaway-print-side" semantics.
4081 for k in names {
4082 // c:Src/module.c:1218-1219 — autoload stub: scalar param,
4083 // u.str = module name, PM_AUTOLOAD. printparamnode prints
4084 // `undefined NAME` (PRINT_TYPE) / `NAME` (PRINT_NAMEONLY)
4085 // and returns early under -p.
4086 if let Some((_, module)) = stubs.iter().find(|(n, _)| *n == k) {
4087 let mut stub_pm = crate::ported::zsh_h::param {
4088 node: crate::ported::zsh_h::hashnode {
4089 next: None,
4090 nam: k.clone(),
4091 flags: (PM_SCALAR | PM_AUTOLOAD) as i32, // c:1219
4092 },
4093 u_data: 0,
4094 u_tied: None,
4095 u_arr: None,
4096 u_str: Some((*module).to_string()), // c:1218 setsparam(pnam, module)
4097 u_val: 0,
4098 u_dval: 0.0,
4099 u_hash: None,
4100 gsu_s: None,
4101 gsu_i: None,
4102 gsu_f: None,
4103 gsu_a: None,
4104 gsu_h: None,
4105 base: 0,
4106 width: 0,
4107 env: None,
4108 ename: None,
4109 old: None,
4110 level: 0,
4111 };
4112 printparamnode(&mut stub_pm, printflags_final); // c:2792
4113 continue;
4114 }
4115 let mut pm_clone = match paramtab().read() {
4116 Ok(tab) => match tab.get(&k) {
4117 Some(pm) => pm.clone(),
4118 None => continue,
4119 },
4120 Err(_) => continue,
4121 };
4122 printparamnode(&mut pm_clone, printflags_final); // c:2792
4123 }
4124 unqueue_signals();
4125 return 0; // c:2794
4126 }
4127
4128 // c:2799-2810 — `local` (or +g) implies PM_LOCAL.
4129 let nm0 = name.chars().next().unwrap_or(' ');
4130 if nm0 == 'l' || OPT_PLUS(&ops, b'g') {
4131 // c:2799
4132 on |= PM_LOCAL; // c:2800
4133 } else if !OPT_ISSET(&ops, b'g') {
4134 // c:2801
4135 if OPT_MINUS(&ops, b'x') {
4136 // c:2802
4137 let globalexport = isset(GLOBALEXPORT);
4138 let ll_v = locallevel_param.load(Relaxed);
4139 if globalexport {
4140 // c:2803
4141 ops.ind[b'g' as usize] = 1; // c:2804
4142 } else if ll_v != 0 {
4143 // c:2805
4144 on |= PM_LOCAL; // c:2806
4145 }
4146 } else if !(OPT_ISSET(&ops, b'x') || OPT_ISSET(&ops, b'm')) {
4147 // c:2808
4148 on |= PM_LOCAL; // c:2809
4149 }
4150 }
4151
4152 // c:2813+ — -T tied vars + per-arg setting loop.
4153 // The full C body has dozens of paths (PM_TIED tie-pair setup at
4154 // c:2813-2900, glob -m walk at c:2905-2935, name=value assign
4155 // through typeset_single at c:2945+). The Rust port handles the
4156 // three high-frequency paths inline: assoc creation (`PM_HASHED`
4157 // + `name=(k v k v)`), array creation (`PM_ARRAY` + `name=(a b c)`),
4158 // and scalar assignment.
4159 let _ = off;
4160 let is_hashed = (on & PM_HASHED) != 0; // c:2655 `-A`
4161 let is_array = (on & PM_ARRAY) != 0; // c:2655 `-a`
4162 // c:Src/builtin.c typeset_single — when the array RHS comes from
4163 // an unquoted `$@` / `${arr[@]}` splat (e.g. `typeset -a opts=
4164 // ("$@")`), the upstream prefork has already split the value
4165 // into separate argv entries: `["opts=(a", "b", "c)"]`. C zsh's
4166 // parser captures the entire `name=(... )` shape as one ENVARRAY
4167 // token by walking paren depth at parse time so the splat fills
4168 // the array's element list. zshrs's compile path emits the
4169 // synthetic word `opts=("$@")` then runtime DQ-strip + splat
4170 // separates it. Reconstruct the single arg here: when one entry
4171 // starts with `NAME=(` and a later entry ends with `)`, rejoin
4172 // the run with spaces between elements.
4173 // c:Src/builtin.c — paren-init rejoin. C's parser hands the whole
4174 // `NAME=(e1 e2 …)` shape as one ENVARRAY token via parse-time paren-
4175 // walking so empty elements (`""` quoted to lexer Dnull-Dnull) stay
4176 // as separate list nodes. zshrs's compile path emits the args
4177 // pre-split through prefork — each `""` produces a true empty
4178 // entry in argv. The previous rejoin glued them with single-space
4179 // separators, then split_whitespace collapsed consecutive empties
4180 // → key/value swap on `typeset -A h=( "" val )`. Bug #93 in
4181 // docs/BUGS.md.
4182 //
4183 // Use `\u{1f}` (ASCII US — unit separator) as the rejoin
4184 // separator. The paren-init branch below splits on `\u{1f}` AND
4185 // whitespace, preserving consecutive `\u{1f}` as empty elements
4186 // (the original quoted-empty args) while still tolerating
4187 // single-arg-form paren-init like `a=(1 2 3)` where no rejoin
4188 // ran.
4189 const REJOIN_SEP: char = '\u{1f}';
4190 let argv: Vec<String> = {
4191 let mut out: Vec<String> = Vec::with_capacity(argv.len());
4192 let mut i = 0;
4193 while i < argv.len() {
4194 let arg = &argv[i];
4195 let open = arg.find("=(");
4196 let is_open = open.is_some()
4197 && arg
4198 .as_bytes()
4199 .first()
4200 .is_some_and(|b| b.is_ascii_alphabetic() || *b == b'_')
4201 && !arg.ends_with(')');
4202 if is_open {
4203 // Find the matching `)` — scan forward through argv
4204 // tracking paren depth (the parser's `(` was just `(`
4205 // in the source). Each arg may have additional `(`
4206 // and `)` chars from quoted content.
4207 let mut depth: i32 = 0;
4208 for c in arg.chars() {
4209 if c == '(' {
4210 depth += 1;
4211 } else if c == ')' {
4212 depth -= 1;
4213 }
4214 }
4215 let mut buf = arg.clone();
4216 let mut j = i + 1;
4217 while depth > 0 && j < argv.len() {
4218 buf.push(REJOIN_SEP);
4219 buf.push_str(&argv[j]);
4220 for c in argv[j].chars() {
4221 if c == '(' {
4222 depth += 1;
4223 } else if c == ')' {
4224 depth -= 1;
4225 }
4226 }
4227 j += 1;
4228 }
4229 out.push(buf);
4230 i = j;
4231 } else {
4232 out.push(arg.clone());
4233 i += 1;
4234 }
4235 }
4236 out
4237 };
4238 let argv = argv.as_slice();
4239 // c:Src/builtin.c:2813-3030 — under PM_TIED (`-T scalar array [sep]`)
4240 // the third positional is the SEPARATOR character, not a param
4241 // name. The scalar and array become a tied pair: writing the
4242 // scalar splits the joined string back into the array; reading
4243 // the scalar joins the array on the separator. Direct port of the
4244 // C body's tied-pair setup at c:2813-2940 (validation) + the
4245 // typeset_single+tiedarr_gsu wiring at c:1956 + c:2945+.
4246 //
4247 // The Rust port handles the validation / scalar-array tie + value
4248 // installation as one block here rather than threading PM_TIED
4249 // through the generic per-arg loop. Beyond index 2 under -T is a
4250 // usage error in C zsh; the Rust port mirrors the same "too many
4251 // arguments" rejection.
4252 //
4253 // Bug #24 in docs/BUGS.md.
4254 let tied_mode = (on & PM_TIED) != 0;
4255 if tied_mode {
4256 // c:2818-2822 — the FIRST thing the -T block does, ahead of the
4257 // argument-count checks:
4258 // if (OPT_ISSET(ops,'m')) {
4259 // zwarnnam(name, "incompatible options for -T");
4260 // unqueue_signals();
4261 // return 1;
4262 // }
4263 // `-m` takes its names as PATTERNS, which cannot express a tie; it was
4264 // silently accepted and did nothing.
4265 if OPT_ISSET(&ops, b'm') {
4266 zwarnnam(name, "incompatible options for -T"); // c:2819
4267 unqueue_signals(); // c:2820
4268 return 1; // c:2821
4269 }
4270 // c:2827-2830 — `if (nargs < 2)` reject.
4271 if argv.len() < 2 {
4272 zwarnnam(name, "-T requires names of scalar and array");
4273 unqueue_signals();
4274 return 1;
4275 }
4276 // c:2831-2833 — `if (nargs > 3)` reject.
4277 if argv.len() > 3 {
4278 zwarnnam(name, "too many arguments for -T");
4279 unqueue_signals();
4280 return 1;
4281 }
4282 // First arg: SCALAR name (with optional =value). c:2838-2840
4283 // `if (ASG_ARRAYP(&asg0))` — first arg can NOT be an array
4284 // assign; we accept either no-value or =scalar.
4285 let (sname, sval_opt): (&str, Option<String>) = match argv[0].find('=') {
4286 Some(i) => (&argv[0][..i], Some(argv[0][i + 1..].to_string())),
4287 None => (argv[0].as_str(), None),
4288 };
4289 // NOT PORTED: c:2841-2846's
4290 // asg0 = *asg;
4291 // if (ASG_ARRAYP(&asg0)) {
4292 // zwarnnam(name, "first argument of tie must be scalar: %s",
4293 // asg0.name);
4294 // return 1;
4295 // }
4296 // so `typeset -T S=(a b) s` is still silently accepted where zsh fails.
4297 //
4298 // It cannot be decided here. ASG_ARRAYP asks whether the PARSER built an
4299 // array-valued assignment — the reserved-word `typeset` form seeing a
4300 // literal `=(` — and that is a property of the parse, not of the text.
4301 // By the time bin_typeset has argv it is just a string, and the two
4302 // cases are textually identical:
4303 // typeset -T S=(a b) s → array assignment, must FAIL
4304 // typeset -T "S=(a b)" s → scalar assignment of the literal text
4305 // `(a b)`, must SUCCEED (rc=0, $S is
4306 // `(a b)` — verified against the oracle)
4307 // A `starts_with('(')` test rejects both; it was tried and broke the
4308 // quoted form. Fixing this needs the assignment shape carried down from
4309 // the compiler (C's `assigns` LinkList), which is the same missing
4310 // reserved-word/builtin distinction that ASG_ARRAYP exists to express.
4311 // Second arg: ARRAY name (with optional =(elements...) init).
4312 // Per c:2847-2854, second arg must be array-shape if it carries
4313 // a value. The Rust port accepts either `arr` or `arr=(a b c)`.
4314 let (aname, aval_opt): (&str, Option<Vec<String>>) = {
4315 let a = argv[1].as_str();
4316 if let Some(eq_idx) = a.find("=(") {
4317 let aname = &a[..eq_idx];
4318 // Strip `=(` prefix and `)` suffix. The parser emits
4319 // array elements separated by REJOIN_SEP (`\u{1f}`)
4320 // sentinels — see parse.rs:2444 commentary. Empty
4321 // strings between sentinels are intentional (zsh's
4322 // assoc paren-init relies on them, bug #93).
4323 let rest = &a[eq_idx + 2..];
4324 let inner = rest.trim_end_matches(')');
4325 let parts: Vec<String> = if inner.contains('\u{1f}') {
4326 inner
4327 .split('\u{1f}')
4328 .filter(|s| !s.is_empty())
4329 .map(|s| s.to_string())
4330 .collect()
4331 } else {
4332 // Fallback: whitespace split for synthetic args
4333 // that didn't pass through the REJOIN_SEP emitter.
4334 inner.split_whitespace().map(|s| s.to_string()).collect()
4335 };
4336 (aname, Some(parts))
4337 } else if let Some(eq_idx) = a.find('=') {
4338 // `arr=val` (scalar form for array slot) — reject per
4339 // c:2849-2854 "second argument of tie must be array".
4340 let _ = eq_idx;
4341 zwarnnam(
4342 name,
4343 &format!("second argument of tie must be array: {}", a),
4344 );
4345 unqueue_signals();
4346 return 1;
4347 } else {
4348 (a, None)
4349 }
4350 };
4351 // c:2857-2860 — `can't tie a variable to itself`.
4352 if sname == aname {
4353 zerrnam(name, &format!("can't tie a variable to itself: {}", sname));
4354 unqueue_signals();
4355 return 1;
4356 }
4357 // c:2861-2864 — `can't tie array elements` (subscripted name).
4358 if sname.contains('[') || aname.contains('[') {
4359 zerrnam(name, &format!("can't tie array elements: {}", sname));
4360 unqueue_signals();
4361 return 1;
4362 }
4363 // c:2866-2870 — `only one tied parameter can have value`.
4364 if sval_opt.is_some() && aval_opt.is_some() {
4365 zerrnam(
4366 name,
4367 &format!("only one tied parameter can have value: {}", sname),
4368 );
4369 unqueue_signals();
4370 return 1;
4371 }
4372 // c:2876-2898 — joinchar parse. Third argument, if given, is
4373 // character used to join the elements of the array in the
4374 // scalar.
4375 let joinchar: i32 = if argv.len() == 3 {
4376 let joinstr = argv[2].as_bytes(); // c:2880
4377 if joinstr.is_empty() {
4378 0 // c:2893-2894 `else if (!*joinstr) joinchar = 0;`
4379 } else if joinstr[0] == 0x83 {
4380 // c:2895-2896 `else if (*joinstr == Meta) joinchar = joinstr[1] ^ 32;`
4381 (joinstr.get(1).copied().unwrap_or(0) ^ 32) as i32
4382 } else {
4383 joinstr[0] as i32 // c:2897-2898
4384 }
4385 } else {
4386 ':' as i32 // c:2891-2892 `if (!joinstr) joinchar = ':';`
4387 };
4388 // Split/join separators from joinchar. joinchar==0 measured on
4389 // the 5.9.1 release binary: assignment keeps the whole string
4390 // as one element (no split) and reads join with a NUL byte
4391 // (`zjoin(arr, 0, 1)` writing the raw byte, c:Src/params.c:4352).
4392 let split_one = joinchar == 0;
4393 let joinsep: String = if joinchar == 0 {
4394 "\0".to_string()
4395 } else {
4396 ((joinchar as u8) as char).to_string()
4397 };
4398
4399 // c:Src/builtin.c:2940-2944 — when the scalar already exists
4400 // and has a value, save it for seeding the tied array:
4401 // `oldval = ztrdup(getsparam(asg0.name));`
4402 // The Rust port checks paramtab AND the environ inherited at
4403 // startup so `typeset -T PATH path :` over an env-imported
4404 // PATH seeds `path` from the live PATH instead of clobbering
4405 // it with empty.
4406 // c:2900-2901 — `pm = getnode(asg0.name); apm = getnode(asg->name);`
4407 // then c:2903-2943 decides between erroring, re-tying, and updating an
4408 // EXISTING tie. None of that block was ported: the code below simply
4409 // installed a fresh pair every time, which silently accepted three
4410 // cases C rejects and lost every attribute the new command line didn't
4411 // restate.
4412 let (pm_flags, pm_ename, pm_level) = paramtab()
4413 .read()
4414 .ok()
4415 .and_then(|t| {
4416 t.get(sname)
4417 .map(|p| (Some(p.node.flags as u32), p.ename.clone(), p.level))
4418 })
4419 .unwrap_or((None, None, 0));
4420 let (apm_flags, apm_ename) = paramtab()
4421 .read()
4422 .ok()
4423 .and_then(|t| t.get(aname).map(|p| (Some(p.node.flags as u32), p.ename.clone())))
4424 .unwrap_or((None, None));
4425 let mut already_tied = false;
4426
4427 if let Some(sf) = pm_flags.filter(|f| (f & (PM_SPECIAL | PM_TIED)) == (PM_SPECIAL | PM_TIED))
4428 {
4429 let _ = sf;
4430 // c:2903-2918 — a SPECIAL tied scalar (PATH/path). Only a re-tie of
4431 // the identical special pair is allowed.
4432 let apm_special = apm_flags.map(|f| (f & PM_SPECIAL) != 0).unwrap_or(false);
4433 if pm_ename.as_deref() != Some(aname) || !apm_special {
4434 // c:2908-2911
4435 zwarnnam(
4436 name,
4437 &format!(
4438 "{} special parameter can only be tied to special parameter {}",
4439 sname,
4440 pm_ename.as_deref().unwrap_or("")
4441 ),
4442 );
4443 unqueue_signals();
4444 return 1;
4445 }
4446 if joinchar != ':' as i32 {
4447 // c:2913-2917
4448 zwarnnam(
4449 name,
4450 "cannot change the join character of special tied parameters",
4451 );
4452 unqueue_signals();
4453 return 1;
4454 }
4455 already_tied = true; // c:2918
4456 } else if let Some(af) =
4457 apm_flags.filter(|f| (f & (PM_SPECIAL | PM_TIED)) == (PM_SPECIAL | PM_TIED))
4458 {
4459 let _ = af;
4460 // c:2919-2929 — the ARRAY half is special+tied: catches tying it to
4461 // a different scalar, or to one that is no longer special.
4462 zwarnnam(
4463 name,
4464 &format!(
4465 "{} special parameter can only be tied to special parameter {}",
4466 aname,
4467 apm_ename.as_deref().unwrap_or("")
4468 ),
4469 );
4470 unqueue_signals();
4471 return 1;
4472 } else if let Some(sf) = pm_flags {
4473 // c:2930-2932 — only consider an existing scalar that is actually
4474 // live in this scope.
4475 if ((sf & PM_UNSET) == 0 || (sf & PM_DECLARED) != 0)
4476 && (locallevel_param.load(Relaxed) as i32 == pm_level || (on as u32 & PM_LOCAL) == 0)
4477 {
4478 if (sf & PM_TIED) != 0 {
4479 if PM_TYPE(sf) != PM_SCALAR {
4480 // c:2934-2937
4481 zwarnnam(name, &format!("already tied as non-scalar: {sname}"));
4482 unqueue_signals();
4483 return 1;
4484 } else if pm_ename.as_deref() == Some(aname) {
4485 already_tied = true; // c:2937
4486 } else {
4487 // c:2939-2942
4488 zwarnnam(name, &format!("can't tie already tied scalar: {sname}"));
4489 unqueue_signals();
4490 return 1;
4491 }
4492 }
4493 // else: not tied — only the export attribute is inherited
4494 // (c:2951), which `inherited_export` below already does.
4495 }
4496 }
4497
4498 if already_tied {
4499 // c:2957-2973 — C does NOT rebuild the pair here. It runs
4500 // typeset_single on each half "if only to update the attributes of
4501 // both, and of course to set the new value if one is provided",
4502 // then returns:
4503 // typeset_single(..., pm, on, off, ...)
4504 // typeset_single(..., apm, (on | PM_ARRAY) & ~PM_EXPORTED, off & ~PM_ARRAY, ...)
4505 // Applying on/off to the EXISTING entries is precisely what lets an
4506 // attribute the current command doesn't restate survive — so
4507 // `typeset -UT TS ts; typeset -T TS ts` keeps its uniqueness, and
4508 // likewise readonly. Rebuilding dropped everything but export.
4509 //
4510 // typeset_single itself is present in this file but is dead code
4511 // (nothing calls it; bin_typeset carries its own inline logic), so
4512 // the attribute update is applied directly here rather than routed
4513 // through it.
4514 if let Ok(mut tab) = paramtab().write() {
4515 if let Some(p) = tab.get_mut(sname) {
4516 p.node.flags = ((p.node.flags as u32 | on as u32) & !(off as u32)) as i32;
4517 // c:2967 — typeset_single takes `joinchar`, so a re-tie
4518 // RE-STATES the separator: "It is possible to apply -T to
4519 // two previously tied variables but with a different
4520 // separator character, in which case the variables remain
4521 // joined as before but the separator is changed."
4522 // (Doc/Zsh/builtins.yo, -T.) Updating flags alone left
4523 // `typeset -T TS ts; typeset -T TS ts ,` joining on ':'.
4524 if let Some(td) = p.u_tied.as_mut() {
4525 td.joinchar = joinchar; // c:2967
4526 }
4527 }
4528 if let Some(p) = tab.get_mut(aname) {
4529 let aon = (on as u32 | PM_ARRAY) & !PM_EXPORTED; // c:2970
4530 let aoff = off as u32 & !PM_ARRAY; // c:2971
4531 p.node.flags = ((p.node.flags as u32 | aon) & !aoff) as i32;
4532 }
4533 }
4534 // Values, if the command line carried any (c:2963 "and of course to
4535 // set the new value if one is provided for either of them").
4536 if let Some(sval) = sval_opt.as_deref() {
4537 crate::ported::params::assignsparam(sname, sval, 0);
4538 } else if let Some(arr) = aval_opt {
4539 crate::ported::params::assignaparam(aname, arr, 0);
4540 }
4541 unqueue_signals();
4542 return 0;
4543 }
4544
4545 let existing_scalar: Option<String> = {
4546 let from_tab = paramtab()
4547 .read()
4548 .ok()
4549 .and_then(|t| t.get(sname).and_then(|p| p.u_str.clone()));
4550 from_tab.or_else(|| std::env::var(sname).ok())
4551 };
4552
4553 // Build the initial array value: prefer the array RHS, then
4554 // the scalar RHS (split on joinchar), then the existing
4555 // scalar's env value (split on joinchar). Mirrors C's sequence
4556 // at c:2960-3030 where typeset_single is called on both names;
4557 // if the scalar had a value it gets passed to tiedarrsetfn
4558 // which splits on the tieddata joinchar (c:4370-4381 sepsplit).
4559 let init_arr: Vec<String> = if let Some(arr) = aval_opt {
4560 arr
4561 } else if let Some(sval) = sval_opt.as_deref() {
4562 if split_one {
4563 vec![sval.to_string()]
4564 } else {
4565 crate::ported::utils::sepsplit(sval, Some(&joinsep), true)
4566 }
4567 } else if let Some(old) = existing_scalar.as_deref() {
4568 if old.is_empty() {
4569 Vec::new()
4570 } else if split_one {
4571 vec![old.to_string()]
4572 } else {
4573 crate::ported::utils::sepsplit(old, Some(&joinsep), true)
4574 }
4575 } else {
4576 Vec::new()
4577 };
4578
4579 // Install the array side first (matching C c:2980 "Do it
4580 // first because we need the address"). Build a plain
4581 // PM_ARRAY|PM_TIED param.
4582 // c:Src/builtin.c:2986-2999 — the array side is created with
4583 // `(on | PM_ARRAY) & ~PM_EXPORTED` and the scalar side with the full
4584 // `on`. So `export -T FOO foo` / `readonly -T` carry those attribute
4585 // flags onto the pair (the array is NOT export-flagged; the scalar
4586 // is). Previously both were hardcoded to `…|PM_TIED`, so `export -T`
4587 // left FOO unexported — `${(t)FOO}` read `scalar-tied` and
4588 // `typeset -p FOO` printed `typeset -T` instead of `export -T`.
4589 //
4590 // c:Src/builtin.c:2953 — `on |= (pm->node.flags & ~roff) & PM_EXPORTED;`
4591 //
4592 // "Variable already exists in the current scope but is not tied.
4593 // We're preserving its value and export attribute but no other
4594 // attributes upon converting to 'tied'."
4595 //
4596 // So tying an ALREADY-EXPORTED scalar keeps it exported, even without
4597 // `-x`: `export E=a:b; typeset -T E e` leaves E in the environment
4598 // (${(t)E} == scalar-tied-export). zshrs built the tie attributes from
4599 // the command-line flags alone and silently dropped the export, which
4600 // takes the variable OUT of the environment of every later child.
4601 let inherited_export: u32 = paramtab()
4602 .read()
4603 .ok()
4604 .and_then(|t| t.get(sname).map(|p| p.node.flags as u32))
4605 .unwrap_or(0)
4606 & PM_EXPORTED
4607 & !(off as u32); // c:2953 `& ~roff` — an explicit +x still wins
4608 // c:2989 / c:3003 — C hands BOTH halves the full `on`:
4609 // apm = typeset_single(..., (on | PM_ARRAY) & ~PM_EXPORTED, ...)
4610 // pm = typeset_single(..., on, ...)
4611 // so every attribute the user asked for lands on the tie, not just the
4612 // export/readonly pair. Masking down to PM_EXPORTED|PM_READONLY silently
4613 // dropped the rest — most visibly PM_UNIQUE, so `typeset -UT B b` tied
4614 // the two halves but never deduped either (`${(t)b}` read `array-tied`
4615 // where zsh reads `array-tied-unique`), while the equivalent spelled as
4616 // two commands (`typeset -T B b; typeset -U b`) worked. Carrying `on`
4617 // whole is safe here: c:2742-2744 has already stripped the type bits
4618 // from it (`if (on & PM_TIED) off |= PM_INTEGER|…|PM_HASHED; on &= ~off`).
4619 let tie_attr: u32 = on as u32 | inherited_export;
4620 let mut apm = param::default();
4621 apm.node.nam = aname.to_string();
4622 apm.node.flags = ((PM_ARRAY | PM_TIED) | (tie_attr & !PM_EXPORTED)) as i32;
4623 apm.u_arr = Some(init_arr.clone());
4624 apm.ename = Some(sname.to_string());
4625 apm.level = locallevel.load(Relaxed) as i32;
4626 // c:2982-2989 — `tdp = (Tieddata) zalloc(sizeof *tdp);
4627 // tdp->joinchar = joinchar;` — the tieddata rides on the
4628 // SCALAR side's pm->u.data (c:2566 `tdp->joinchar = joinchar`).
4629 let tdp = crate::ported::zsh_h::tieddata {
4630 arrptr: None,
4631 joinchar,
4632 };
4633
4634 // Install the scalar side with PM_TIED + gsu_s wired to the
4635 // tied-via-ename getters/setters so reads/writes propagate
4636 // through paramtab to apm.u_arr.
4637 let mut spm = param::default();
4638 spm.node.nam = sname.to_string();
4639 spm.node.flags = ((PM_SCALAR | PM_TIED) | tie_attr) as i32; // c:2999 scalar carries full `on`
4640 spm.ename = Some(aname.to_string());
4641 spm.u_str = Some(init_arr.join(&joinsep));
4642 spm.u_tied = Some(Box::new(tdp));
4643 spm.level = locallevel.load(Relaxed) as i32;
4644 // c:Src/builtin.c:1956 — `static const struct gsu_scalar
4645 // tiedarr_gsu = { tiedarrgetfn, tiedarrsetfn, tiedarrunsetfn };`
4646 // The scalar side of a tied pair routes its reads/writes
4647 // through these so the partner array stays in sync. C's
4648 // tiedarrgetfn returns the joined string; tiedarrsetfn splits
4649 // and writes through `*dptr->arrptr`. The Rust port adapts
4650 // those to paramtab lookups via `pm.ename` (see params.rs
4651 // commentary). The C tiedarrgetfn returns `char*` (joined
4652 // scalar); the Rust signature returns `Vec<String>` because
4653 // the existing typing — adapt with a closure that joins.
4654 fn tied_scalar_getfn_shim(pm: ¶m) -> String {
4655 // c:4352 — `zjoin(*dptr->arrptr, (unsigned char)
4656 // dptr->joinchar, 1)`; joinchar lives on the tieddata
4657 // riding pm->u.data (Rust: typed u_tied view).
4658 let sep = match pm.u_tied.as_deref() {
4659 // joinchar==0 joins with the raw NUL byte (zjoin with
4660 // delim 0, c:Src/params.c:4352; measured on 5.9.1).
4661 Some(td) if td.joinchar == 0 => "\0".to_string(),
4662 Some(td) => ((td.joinchar as u8) as char).to_string(),
4663 None => ":".to_string(),
4664 };
4665 crate::ported::params::tiedarrgetfn(pm).join(&sep)
4666 }
4667 fn tied_scalar_setfn_shim(pm: &mut param, val: String) {
4668 crate::ported::params::tiedarrsetfn(pm, Some(val))
4669 }
4670 fn tied_scalar_unsetfn_shim(pm: &mut param, exp: i32) {
4671 crate::ported::params::tiedarrunsetfn(pm, exp)
4672 }
4673 spm.gsu_s = Some(Box::new(crate::ported::zsh_h::gsu_scalar {
4674 getfn: tied_scalar_getfn_shim,
4675 setfn: tied_scalar_setfn_shim,
4676 unsetfn: tied_scalar_unsetfn_shim,
4677 }));
4678
4679 if let Ok(mut tab) = paramtab().write() {
4680 tab.insert(aname.to_string(), Box::new(apm));
4681 tab.insert(sname.to_string(), Box::new(spm));
4682 }
4683 unqueue_signals();
4684 return 0;
4685 }
4686
4687 // c:Src/builtin.c:3042-3098 — `-m PATTERN` (or `+m PATTERN`) treats
4688 // each arg as a glob, not a `name[=value]` pair.
4689 //
4690 // C splits into TWO subpaths:
4691 // +m: c:3068 `scanmatchtable(..., paramtab->printnode, printflags)`
4692 // — direct paramtab walk that prints PRINT_TYPE | PRINT_NAMEONLY
4693 // (built up at c:3051-3054 when on|roff == 0).
4694 // -m: c:3081-3094 build pmlist, then run `typeset_single` per
4695 // match. typeset_single's print path (c:2240-2247) uses
4696 // - PRINT_TYPESET with `-p`
4697 // - PRINT_INCLUDEVALUE otherwise, gated on
4698 // `!OPT_ISSET(ops,'g') && (unset(TYPESETSILENT) ||
4699 // OPT_ISSET(ops,'m'))`
4700 //
4701 // Earlier zshrs collapsed both into one `printparamnode(...,
4702 // PRINT_INCLUDEVALUE)` call which:
4703 // - Made `+m` emit `name=value` instead of `name` / `integer NAME`.
4704 // - Ignored `-g` print suppression so `typeset -gm 'A*'` spammed
4705 // every match (zsh is silent: `-g` blocks the print). Hit
4706 // during zinit's plugin loader which runs `typeset -gm`
4707 // against the plugin's exported-var set on every load.
4708 // Bug #48 in docs/BUGS.md.
4709 if OPT_ISSET(&ops, b'm') && !argv.is_empty() {
4710 // c:3043-3055 — printflags for the +m direct-scan path.
4711 if !OPT_ISSET(&ops, b'p') {
4712 // c:3044-3050 — mass-changing types is fatal for namerefs.
4713 if (on & PM_NAMEREF) != 0 && OPT_MINUS(&ops, b'm') {
4714 zerrnam(name, "-m not allowed with -n"); // c:3048
4715 unqueue_signals();
4716 return 1; // c:3049
4717 }
4718 if (on | roff) == 0 {
4719 printflags |= PRINT_TYPE; // c:3052
4720 }
4721 if on == 0 {
4722 printflags |= PRINT_NAMEONLY; // c:3054
4723 }
4724 }
4725 // c:2241 — typeset_single's `-m` print path always sets
4726 // PRINT_WITH_NAMESPACE so dot-prefixed names (`.cd`) emit
4727 // instead of being filtered at printparamnode's namespace gate.
4728 let single_flags: i32 = if OPT_ISSET(&ops, b'p') {
4729 PRINT_TYPESET | PRINT_WITH_NAMESPACE
4730 } else {
4731 PRINT_INCLUDEVALUE | PRINT_WITH_NAMESPACE
4732 };
4733 // c:2244 — `else if (!OPT_ISSET(ops,'g') && (unset(TYPESETSILENT)
4734 // || OPT_ISSET(ops,'m')))` — typeset_single suppresses
4735 // PRINT_INCLUDEVALUE when `-g` is set. `-p` bypasses (c:2242).
4736 let do_minus_print = OPT_ISSET(&ops, b'p')
4737 || (!OPT_ISSET(&ops, b'g') && (!isset(TYPESETSILENT) || OPT_ISSET(&ops, b'm')));
4738 for pattern in argv.iter() {
4739 // c:3061 — `patcompile(asg->name, 0, NULL)` glob-compile.
4740 // Use the canonical pattern.rs port. On compile failure,
4741 // emit "bad pattern" and continue to the next arg.
4742 let pat = crate::ported::pattern::patcompile(
4743 &{
4744 let mut __pat_tok = (pattern).to_string();
4745 crate::ported::glob::tokenize(&mut __pat_tok);
4746 __pat_tok
4747 },
4748 crate::ported::zsh_h::PAT_HEAPDUP as i32,
4749 None,
4750 );
4751 let pat = match pat {
4752 Some(p) => p,
4753 None => {
4754 zwarnnam(name, &format!("bad pattern: {}", pattern));
4755 returnval = 1;
4756 continue;
4757 }
4758 };
4759 // c:3068 — `scanmatchtable(paramtab, pprog, 1, on|roff,
4760 // 0, paramtab->printnode, printflags);` — walk paramtab
4761 // entries whose name matches the pattern AND whose flag
4762 // bits intersect on|roff.
4763 let names: Vec<String> = {
4764 let tab = paramtab().read().unwrap();
4765 let on_roff = (on as u32) | (roff as u32);
4766 let mut names: Vec<String> = tab
4767 .iter()
4768 .filter(|(k, pm)| {
4769 let f = pm.node.flags as u32;
4770 if (f & PM_UNSET) != 0 {
4771 return false;
4772 }
4773 // c:Src/builtin.c:3055-3094 / hashtable.c:373-440
4774 // — the `-m`/`+m` PATTERN path scans paramtab
4775 // via `scanmatchtable(paramtab, pprog, ...)`.
4776 // scanmatchtable matches ONLY on flags1/flags2
4777 // + the pattern; it does NOT exclude PM_HIDE.
4778 // An explicit pattern therefore REVEALS hidden
4779 // params — verified vs `/opt/homebrew/bin/zsh`:
4780 // `typeset +m 'a*'` → `undefined aliases` +
4781 // `array argv`; `typeset -m 'a*'` → `aliases`
4782 // + `argv=( )`. The magic-assoc placeholders
4783 // (PM_HIDE|PM_HIDEVAL, inserted by
4784 // init_partab_params) and `private` vars
4785 // (PM_HIDE, no PM_HIDEVAL — c:Modules/
4786 // param_private.c:174) must BOTH appear here,
4787 // unlike the no-args list path (c:2762-2792)
4788 // where scanhashtable's bare scan keeps them
4789 // hidden (bug #371). So NO PM_HIDE filter on
4790 // this PATTERN path — V10private: `private
4791 // x=5; typeset +m x` → `local x`.
4792 if on_roff != 0 && (f & on_roff) == 0 {
4793 return false;
4794 }
4795 crate::ported::pattern::pattry(&pat, k)
4796 })
4797 .map(|(k, _)| k.clone())
4798 .collect();
4799 names.sort_by(|a, b| hnamcmp(a, b));
4800 names
4801 };
4802 if OPT_PLUS(&ops, b'm') {
4803 // c:3068-3070 — `+m`: direct print using the
4804 // PRINT_TYPE | PRINT_NAMEONLY flags built above.
4805 for k in names {
4806 if let Ok(mut tab) = paramtab().write() {
4807 if let Some(pm) = tab.get_mut(&k) {
4808 printparamnode(pm, printflags);
4809 }
4810 }
4811 }
4812 continue;
4813 }
4814 // c:3081-3094 — `-m`: typeset_single per match. For the
4815 // pure listing case (no `=` value, no attribute mutation),
4816 // typeset_single reduces to the c:2240-2247 print arm.
4817 // Attribute-conversion (`-im PAT`, `-rm PAT`, etc.) on
4818 // matched names isn't ported here yet; the early
4819 // `return returnval` below short-circuits the per-arg
4820 // name loop for `-m` args, so on/roff currently acts as
4821 // a listing filter only.
4822 if do_minus_print {
4823 for k in names {
4824 if let Ok(mut tab) = paramtab().write() {
4825 if let Some(pm) = tab.get_mut(&k) {
4826 printparamnode(pm, single_flags);
4827 }
4828 }
4829 }
4830 }
4831 }
4832 unqueue_signals();
4833 return returnval;
4834 }
4835
4836 // c:3035-3039 — `+T` is not a way to untie:
4837 // if (off & PM_TIED) {
4838 // unqueue_signals();
4839 // zerrnam(name, "use unset to remove tied variables");
4840 // return 1;
4841 // }
4842 // C puts this at the TOP LEVEL, so it precedes everything typeset_single
4843 // does — notably the special-parameter type-change rule, whose `chflags`
4844 // watches PM_TIED and would otherwise claim `typeset +T PATH path` and
4845 // report "can't change type of a special parameter" instead. The check was
4846 // missing entirely, so `+T` was silently accepted and did nothing.
4847 if (off as u32 & PM_TIED) != 0 {
4848 unqueue_signals(); // c:3036
4849 zerrnam(name, "use unset to remove tied variables"); // c:3037
4850 return 1; // c:3038
4851 }
4852
4853 let mut tied_name_count: usize = 0;
4854 for arg in argv {
4855 // c:Src/builtin.c typeset_single — when PM_LOCAL is in
4856 // flags, createparam first to install pm.old chain at
4857 // locallevel (createparam c:1132-1147). Applies uniformly
4858 // to all forms: `local x`, `local x=v`, `local arr=(...)`,
4859 // `local -A h`. endparamscope unwinds via Param.old.
4860 if tied_mode {
4861 tied_name_count += 1;
4862 if tied_name_count > 2 {
4863 // Separator / extra args — don't run name
4864 // validation or createparam against them. Full
4865 // -T body (createparam SCALAR_TIED + array tie
4866 // backref) is deferred per the comment above on
4867 // the typeset_single dispatch.
4868 continue;
4869 }
4870 }
4871 let arg_name: &str = match arg.find('=') {
4872 Some(i) => &arg[..i],
4873 None => arg.as_str(),
4874 };
4875
4876 // c:2117-2193 (inside typeset_single) — changing the TYPE of an
4877 // existing parameter is refused for specials, with SECONDS the one
4878 // documented exception.
4879 //
4880 // int chflags = ((off & pm->node.flags) | (on & ~pm->node.flags)) &
4881 // (PM_INTEGER|PM_EFLOAT|PM_FFLOAT|PM_HASHED|PM_ARRAY|PM_TIED|PM_AUTOLOAD);
4882 // /* keep the parameter if just switching between floating types */
4883 // if ((tc = chflags && chflags != (PM_EFLOAT|PM_FFLOAT))) { ... }
4884 // ...
4885 // if (... || tc) {
4886 // if (pm->node.flags & PM_SPECIAL) {
4887 // int err = 1;
4888 // if (!readonly && !strcmp(pname, "SECONDS")) {
4889 // ... else if (!setsecondstype(pm, on, off)) { ... err = 0; }
4890 // }
4891 // if (err) { zerrnam(cname, "%s: can't change type of a "
4892 // "special parameter", pname); return NULL; }
4893 //
4894 // None of this was ported, so a type change against a special was
4895 // silently ACCEPTED and then dropped: `typeset -F RANDOM`,
4896 // `typeset -F LINENO` and `typeset -F HISTSIZE` all returned 0 having
4897 // done nothing, where zsh fails. And the SECONDS exception — the
4898 // documented way to get sub-second timing — never took effect:
4899 // `typeset -F SECONDS` left it `integer-special` instead of
4900 // `float-special`.
4901 //
4902 // `chflags` is why this fires only on a REAL type change:
4903 // `typeset -i SECONDS` / `typeset -i LINENO` leave chflags == 0 (the
4904 // bit is already set) and are correctly no-ops, and -E↔-F is exempted
4905 // outright by the `!= (PM_EFLOAT|PM_FFLOAT)` clause. It also covers
4906 // `typeset -a PATH`, which is a PM_ARRAY type change on a special and
4907 // so earns the same message rather than an array-specific one.
4908 //
4909 // The `readonly` half of C's guard is not modelled here: it gates
4910 // turning readonly OFF, which reaches this branch by a different route.
4911 // setsecondstype (params.rs) is the real port and had NO callers.
4912 if !tied_mode {
4913 let pmf = paramtab()
4914 .read()
4915 .ok()
4916 .and_then(|t| t.get(arg_name).map(|p| p.node.flags as u32));
4917 if let Some(pmf) = pmf {
4918 let chflags = ((off as u32 & pmf) | (on as u32 & !pmf))
4919 & (PM_INTEGER
4920 | PM_EFLOAT
4921 | PM_FFLOAT
4922 | PM_HASHED
4923 | PM_ARRAY
4924 | PM_TIED
4925 | PM_AUTOLOAD); // c:2118-2120
4926 let tc = chflags != 0 && chflags != (PM_EFLOAT | PM_FFLOAT); // c:2122
4927 if tc && (pmf & PM_SPECIAL) != 0 {
4928 let mut err = true; // c:2144
4929 if arg_name == "SECONDS" {
4930 // c:2171 — `else if (!setsecondstype(pm, on, off))`.
4931 let ok = paramtab()
4932 .write()
4933 .ok()
4934 .and_then(|mut t| {
4935 t.get_mut(arg_name)
4936 .map(|p| {
4937 crate::ported::params::setsecondstype(
4938 p, on as i32, off as i32,
4939 )
4940 })
4941 })
4942 .unwrap_or(1);
4943 if ok == 0 {
4944 err = false; // c:2176
4945 // c:2172-2175 — `if (asg->value.scalar && !(pm =
4946 // assignsparam(pname, ..., 0))) return NULL;`
4947 if let Some(i) = arg.find('=') {
4948 crate::ported::params::assignsparam(arg_name, &arg[i + 1..], 0);
4949 }
4950 }
4951 }
4952 if err {
4953 // c:2181-2187
4954 if !OPT_ISSET(&ops, b'p') {
4955 zerrnam(
4956 name,
4957 &format!("{arg_name}: can't change type of a special parameter"),
4958 );
4959 }
4960 unqueue_signals();
4961 return 1;
4962 }
4963 // SECONDS: setsecondstype already installed the new type;
4964 // c:2169's `tc = 0` skips the normal conversion below.
4965 continue;
4966 }
4967 }
4968 }
4969
4970 // c:2519-2552 (Src/builtin.c, inside typeset_single) — name
4971 // validation gate. Direct port:
4972 // else if ((isident(pname) || paramtab->getnode(paramtab, pname))
4973 // && (!idigit(*pname) || !strcmp(pname, "0"))) {
4974 // /* proceed */
4975 // } else {
4976 // if (idigit(*pname))
4977 // zerrnam(cname, "not an identifier: %s", pname);
4978 // else
4979 // zerrnam(cname, "not valid in this context: %s", pname);
4980 // return NULL;
4981 // }
4982 //
4983 // The C function returns NULL on failure; the outer bin_typeset
4984 // name loop continues to the next arg (errflag silences
4985 // subsequent zerr calls so we won't double-emit). Mirror that
4986 // here with `continue`.
4987 let pname_in_tab = paramtab()
4988 .read()
4989 .map(|t| t.get(arg_name).is_some())
4990 .unwrap_or(false);
4991 // c:2062-2064 — `usepm = pm && (!(pm->node.flags & PM_UNSET)
4992 // || ...)`: snapshot BEFORE any createparam this iteration
4993 // runs (the PM_LOCAL shadow block and the pre-assign type
4994 // stamps both create fresh pms). Only the reuse-existing-pm
4995 // branch of typeset_single carries the c:2336 `if (errflag)
4996 // return NULL` post-assign check; the fresh-param path
4997 // (c:2577+ createparam → c:2604 assignsparam) has NO errflag
4998 // check — `typeset -i x=3#8` zerrs the math but bin_typeset
4999 // still returns 0 (zsh 5.9 exit 0, lastval untouched).
5000 let usepm_existing = paramtab()
5001 .read()
5002 .map(|t| {
5003 t.get(arg_name)
5004 .is_some_and(|pm| (pm.node.flags as u32 & PM_UNSET) == 0)
5005 })
5006 .unwrap_or(false);
5007 // c:Src/builtin.c:2078 — snapshot the EXISTING param's local
5008 // level BEFORE this arg's createparam/assign runs. The bare-name
5009 // print decision (c:2244, inlined at ~5962) needs the pre-state
5010 // level to tell "re-declare same-level local" (usepm kept →
5011 // print) from "localize a higher-level/global var" (usepm=0 →
5012 // no print). Sentinel -1 when the param doesn't pre-exist.
5013 let pm_level_existing: i32 = paramtab()
5014 .read()
5015 .ok()
5016 .and_then(|t| t.get(arg_name).map(|pm| pm.level))
5017 .unwrap_or(-1);
5018 let first_is_digit = arg_name
5019 .as_bytes()
5020 .first()
5021 .is_some_and(|b| b.is_ascii_digit());
5022 let pname_valid =
5023 (isident(arg_name) || pname_in_tab) && (!first_is_digit || arg_name == "0");
5024 if !pname_valid {
5025 if first_is_digit {
5026 zerrnam(
5027 name, // c:2548
5028 &format!("not an identifier: {}", arg_name),
5029 );
5030 } else {
5031 zerrnam(
5032 name, // c:2550
5033 &format!("not valid in this context: {}", arg_name),
5034 );
5035 }
5036 // c:3153-3156 — `if (!typeset_single(...)) returnval = 1;`
5037 // — the NULL return from the validation gate makes
5038 // bin_typeset's per-arg loop record failure.
5039 returnval = 1;
5040 continue; // c:2551 return NULL
5041 }
5042
5043 // c:2241-2247 — `-p` print-mode for an existing param (no `=`,
5044 // no value). C `typeset_single` lands here when `usepm` is set
5045 // and `!ASG_VALUEP(asg)`, BEFORE createparam runs (c:2218 →
5046 // c:2244 early return). The Rust loop must also dispatch the
5047 // print branch first; otherwise the createparam call below
5048 // overwrites pm.node.flags on the reuse-arm (c:2018), clobbering
5049 // typeset-attribute bits set by an earlier `typeset -i n` call.
5050 if !arg.contains('=') && OPT_ISSET(&ops, b'p') {
5051 let with_ns = if OPT_ISSET(&ops, b'm') {
5052 // c:2241
5053 PRINT_WITH_NAMESPACE
5054 } else {
5055 0
5056 };
5057 // c:Src/builtin.c:2761-2765 — `-p1` adds PRINT_LINE (one
5058 // array/assoc element per line). The named-arg `typeset -p1
5059 // NAME` print path missed it (only the listing path parsed
5060 // it), so `typeset -p1 myarray` printed single-line.
5061 let line_flag =
5062 if OPT_HASARG(&ops, b'p') && OPT_ARG(&ops, b'p').map(|a| a.trim()) == Some("1") {
5063 PRINT_LINE
5064 } else {
5065 0
5066 };
5067 let existed = paramtab()
5068 .read()
5069 .map(|t| t.contains_key(arg_name))
5070 .unwrap_or(false);
5071 if existed {
5072 // c:Src/params.c:6275 — printparamnode looks up the
5073 // PM_TIED peer via paramtab; we must NOT hold the lock
5074 // when calling. Bug #410 — pre-clone pattern (mirrors
5075 // the bin_typeset `-m PAT` arm refactor).
5076 let mut pm_clone = match paramtab().read() {
5077 Ok(tab) => tab.get(arg_name).cloned(),
5078 Err(_) => None,
5079 };
5080 if let Some(ref mut pm) = pm_clone {
5081 // c:2243 — `paramtab->printnode(&pm->node,
5082 // PRINT_TYPESET|with_ns);`
5083 printparamnode(pm, PRINT_TYPESET | with_ns | line_flag);
5084 }
5085 } else {
5086 // c:Src/builtin.c:3110-3113 — when `typeset -p NAME`
5087 // and the param doesn't exist, emit
5088 // `typeset: no such variable: NAME` and set
5089 // returnval=1 so the builtin's exit status is 1.
5090 zwarnnam(name, &format!("no such variable: {}", arg_name));
5091 returnval = 1; // c:Src/builtin.c:3112
5092 }
5093 continue;
5094 }
5095
5096 // c:3117-3150 — `typeset -n NAME[=refname]` arm.
5097 if (on as u32 & PM_NAMEREF) != 0 {
5098 // c:Src/builtin.c:3117-3150 — the `-n` literal-name arm,
5099 // INLINE in C's bin_typeset (no separate C function; the
5100 // former vm_helper::typeset_nameref_arg helper relocated
5101 // here 2026-06-12 per the no-fake-fns-in-ported rule).
5102 // Combined with typeset_single's nameref pieces:
5103 // subscripted-name reject (c:2452-2456), read-only
5104 // reference guard (c:2249-2256), fresh-start unset
5105 // (c:3127-3141), scalar→nameref conversion carrying the
5106 // value as refname (c:3132-3135), creation + refname
5107 // assignment (assignsparam → PM_NAMEREF arm → setscope).
5108 let nameref_arm = |on: u32, off: u32| -> i32 {
5109 let value: Option<&str> = arg.find('=').map(|i| &arg[i + 1..]);
5110
5111 // c:2452-2456 — `typeset -n ptr[1]=...` is invalid.
5112 if arg_name.contains('[') {
5113 zerrnam(
5114 name,
5115 &format!("{}: reference variable cannot be an array", arg_name),
5116 );
5117 unqueue_signals();
5118 return 1;
5119 }
5120
5121 // c:3118-3126 — refname target that is itself a PM_SPECIAL nameref.
5122 if let Some(v) = value {
5123 let special_ref = paramtab()
5124 .read()
5125 .ok()
5126 .and_then(|t| {
5127 t.get(v).map(|pm| {
5128 let f = pm.node.flags as u32;
5129 (f & PM_NAMEREF) != 0 && (f & PM_SPECIAL) != 0
5130 })
5131 })
5132 .unwrap_or(false);
5133 if special_ref {
5134 zwarnnam(name, &format!("{}: invalid reference", v)); // c:3122
5135 unqueue_signals();
5136 return 1; // c:3123-3124
5137 }
5138 }
5139
5140 let cur_ll = locallevel_param.load(Relaxed) as i32;
5141 let existing = paramtab().read().ok().and_then(|t| {
5142 t.get(arg_name)
5143 .map(|pm| (pm.node.flags as u32, pm.level, pm.u_str.clone()))
5144 });
5145
5146 let mut carried_value: Option<String> = value.map(String::from);
5147 let mut reuse_existing = false;
5148
5149 let existing_level: Option<i32> = existing.as_ref().map(|(_, l, _)| *l);
5150 if let Some((eflags, elevel, estr)) = existing {
5151 // c:2249-2256 — read-only guard (typeset_single). Fires when
5152 // the existing pm is readonly, +r wasn't given, and either the
5153 // nameref-ness changes or a nameref gets a new value.
5154 if (eflags & PM_READONLY) != 0
5155 && (off & PM_READONLY) == 0
5156 && !OPT_ISSET(&ops, b'p')
5157 {
5158 let kind = if (eflags & PM_NAMEREF) != 0 {
5159 "reference"
5160 } else {
5161 "variable"
5162 };
5163 zerrnam(name, &format!("{}: read-only {}", arg_name, kind)); // c:2254
5164 unqueue_signals();
5165 return 1; // c:2256
5166 }
5167 // c:3127-3141 — namerefs always start over fresh.
5168 if elevel >= cur_ll || ((on & PM_LOCAL) == 0 && elevel < cur_ll) {
5169 // c:3132-3135 — converting a scalar: its value becomes
5170 // the refname.
5171 if carried_value.is_none()
5172 && PM_TYPE(eflags) == PM_SCALAR
5173 && estr.as_deref().map(|s| !s.is_empty()).unwrap_or(false)
5174 {
5175 carried_value = estr.clone();
5176 }
5177 // c:3136-3140 — `if (!(hn->flags & PM_READONLY)) {
5178 // unsetparam_pm(oldpm, 0, 1); hn = NULL; }` — only a
5179 // non-readonly ref starts over fresh; a readonly one is
5180 // KEPT and handed to typeset_single (c:3148-3149), where
5181 // `typeset +r -n ref[=val]` clears the flag in place and
5182 // any =val ASSIGNS THROUGH the surviving refname.
5183 if (eflags & PM_READONLY) != 0 {
5184 reuse_existing = true;
5185 } else if let Some(mut old) =
5186 paramtab().write().ok().and_then(|mut t| t.remove(arg_name))
5187 {
5188 // keep the shadowed outer binding for re-chaining
5189 if let Some(prev) = old.old.take() {
5190 if let Ok(mut tab) = paramtab().write() {
5191 tab.insert(arg_name.to_string(), prev);
5192 }
5193 }
5194 }
5195 } else if (eflags & PM_READONLY) != 0 {
5196 // c:3142-3149 — only a READONLY ref survives as the pm
5197 // handed to typeset_single (so `typeset -rn ref=var` can
5198 // error properly); everything else gets `hn = NULL` and
5199 // a fresh local SHADOW is created below (c:3148-3149).
5200 reuse_existing = true;
5201 }
5202 }
5203
5204 // (Re)create the nameref param (typeset_single c:2577+ createparam
5205 // with PM_NAMEREF type).
5206 if !reuse_existing {
5207 let shadowed = paramtab().write().ok().and_then(|mut t| {
5208 if (on & PM_LOCAL) != 0 && cur_ll > 0 {
5209 t.remove(arg_name)
5210 } else {
5211 None
5212 }
5213 });
5214 let mut flags = PM_NAMEREF as i32;
5215 // c:2544 — TYPESET_TO_UNSET: declared-but-unassigned.
5216 if carried_value.is_none() && isset(crate::ported::zsh_h::TYPESETTOUNSET) {
5217 flags |= crate::ported::zsh_h::PM_DEFAULTED as i32;
5218 }
5219 // crate::ported::zsh_h::PM_UPPER on a nameref marks the -u upscope variant (c:2698
5220 // allows -u with -n); crate::ported::zsh_h::PM_HIDEVAL likewise.
5221 flags |= (on
5222 & (crate::ported::zsh_h::PM_UPPER | crate::ported::zsh_h::PM_HIDEVAL))
5223 as i32;
5224 // c:1108-1132 — createparam REUSES the just-unset node at its
5225 // own level when !PM_LOCAL (`typeset -gn` rebind of a local
5226 // ref keeps the ref's level; it does NOT hoist to level 0).
5227 let level = if (on & PM_LOCAL) != 0 {
5228 cur_ll
5229 } else if let Some(elevel) = existing_level {
5230 elevel
5231 } else {
5232 0
5233 };
5234 let pm = Box::new(crate::ported::zsh_h::param {
5235 node: crate::ported::zsh_h::hashnode {
5236 next: None,
5237 nam: arg_name.to_string(),
5238 flags,
5239 },
5240 u_data: 0,
5241 u_tied: None,
5242 u_arr: None,
5243 u_str: if carried_value.is_none() && value.is_some() {
5244 Some(String::new()) // `typeset -n ptr=` placeholder
5245 } else {
5246 None
5247 },
5248 u_val: 0,
5249 u_dval: 0.0,
5250 u_hash: None,
5251 gsu_s: None,
5252 gsu_i: None,
5253 gsu_f: None,
5254 gsu_a: None,
5255 gsu_h: None,
5256 base: 0,
5257 width: 0,
5258 env: None,
5259 ename: None,
5260 old: shadowed,
5261 level,
5262 });
5263 if let Ok(mut tab) = paramtab().write() {
5264 tab.insert(arg_name.to_string(), pm);
5265 }
5266 } else {
5267 // reuse path (read-only refs surviving the fresh-start gate,
5268 // c:3148-3149): apply on/off bits in place. A readonly
5269 // SCALAR + `-n` converts to a nameref here — its current
5270 // value becomes the refname (c:2117+ type-conversion inside
5271 // typeset_single with the pm kept; the `typeset +r -n
5272 // ref=RW` shape: "assignment occurs after type change").
5273 if let Ok(mut tab) = paramtab().write() {
5274 if let Some(pm) = tab.get_mut(arg_name) {
5275 pm.node.flags |= (on
5276 & (crate::ported::zsh_h::PM_UPPER
5277 | crate::ported::zsh_h::PM_HIDEVAL))
5278 as i32;
5279 pm.node.flags |= PM_NAMEREF as i32;
5280 pm.node.flags &= !((off
5281 & (crate::ported::zsh_h::PM_UPPER
5282 | crate::ported::zsh_h::PM_HIDEVAL
5283 | PM_READONLY))
5284 as i32);
5285 }
5286 }
5287 }
5288
5289 // Assign the refname (typeset_single c:2326 assignsparam →
5290 // PM_NAMEREF assignstrvalue arm + valid_refname + setscope).
5291 let mut rc = 0;
5292 if reuse_existing {
5293 // c:2326 — the surviving (previously-readonly) ref keeps its
5294 // refname; a =value assignment goes through the canonical
5295 // assignsparam which RESOLVES the chain (`typeset +r -n
5296 // ref=RW` writes RW into the referent, not the ref).
5297 if let Some(v) = value {
5298 if crate::ported::params::setsparam(arg_name, v).is_none() {
5299 rc = 1;
5300 }
5301 }
5302 } else if let Some(v) = carried_value.as_deref() {
5303 if v.is_empty() {
5304 // `typeset -n ptr=` — empty placeholder, no setscope error.
5305 if let Ok(mut tab) = paramtab().write() {
5306 if let Some(pm) = tab.get_mut(arg_name) {
5307 pm.u_str = Some(String::new());
5308 pm.node.flags &= !(crate::ported::zsh_h::PM_DEFAULTED as i32);
5309 }
5310 }
5311 } else if crate::ported::params::setsparam(arg_name, v).is_none() {
5312 // c:2326 — typeset_single's assignsparam; the fresh
5313 // PM_NAMEREF pm routes to assignstrvalue's nameref arm
5314 // (c:2690-2720: valid_refname + SETREFNAME + setscope).
5315 rc = 1;
5316 }
5317 } else if !reuse_existing && crate::ported::params::is_nameref(arg_name) {
5318 // bare placeholder — still run setscope for parity (no-op).
5319 let _ = crate::ported::params::setscope_by_name(arg_name);
5320 }
5321
5322 // c:2618 — `pm->node.flags |= (on & PM_READONLY);` AFTER the
5323 // assignment so `typeset -rn ref=var` can set its initial value.
5324 if (on & PM_READONLY) != 0 {
5325 if let Ok(mut tab) = paramtab().write() {
5326 if let Some(pm) = tab.get_mut(arg_name) {
5327 pm.node.flags |= PM_READONLY as i32;
5328 }
5329 }
5330 }
5331 rc
5332 };
5333 if nameref_arm(on as u32, off as u32) != 0 {
5334 returnval = 1; // c:3153-3156
5335 }
5336 continue;
5337 }
5338
5339 // c:2032-2050 — existing pm is a nameref and ±n was not
5340 // requested: resolve the chain and operate on the target.
5341 let mut nameref_rewrite: Option<String> = None;
5342 if (off as u32 & PM_NAMEREF) == 0
5343 && pname_in_tab
5344 && crate::ported::params::is_nameref(arg_name)
5345 {
5346 let cur_ll = locallevel.load(Relaxed) as i32;
5347 let (pm_level, pm_refname) = paramtab()
5348 .read()
5349 .ok()
5350 .and_then(|t| t.get(arg_name).map(|p| (p.level, p.u_str.clone())))
5351 .unwrap_or((0, None));
5352 if pm_level == cur_ll || (on as u32 & PM_LOCAL) == 0 {
5353 // c:2033
5354 let type_change = (on as u32 & !(PM_NAMEREF | PM_LOCAL | PM_READONLY)) != 0; // c:2038
5355 use crate::ported::params::nameref_resolution;
5356 match crate::ported::params::resolve_nameref_name(arg_name, None) {
5357 nameref_resolution::SelfRef | nameref_resolution::OutOfScope => {
5358 returnval = 1;
5359 continue;
5360 }
5361 nameref_resolution::Placeholder(last) => {
5362 // c:2036-2048 — unresolved ref + type change.
5363 if type_change {
5364 zwarnnam(
5365 name, // c:2046
5366 &format!("{}: can't change type of a named reference", last),
5367 );
5368 returnval = 1;
5369 continue; // c:2048 return NULL
5370 }
5371 // plain `typeset ref=value` on a placeholder
5372 // falls through — the scalar-assign arm routes
5373 // through assignsparam which writes the refname.
5374 }
5375 nameref_resolution::Target {
5376 name: t, subscript, ..
5377 } => {
5378 if subscript.is_some() && type_change {
5379 // c:2041-2044 — pm->width set: subscripted ref.
5380 zwarnnam(
5381 name,
5382 &format!(
5383 "{}: can't change type via subscript reference",
5384 pm_refname.as_deref().unwrap_or(arg_name)
5385 ),
5386 );
5387 returnval = 1;
5388 continue;
5389 }
5390 // c:2034-2035 — pname = pm->node.nam (resolved).
5391 let tail = arg.find('=').map(|i| &arg[i..]).unwrap_or("");
5392 let mut new_arg = t.clone();
5393 if let Some(sub) = &subscript {
5394 new_arg.push('[');
5395 new_arg.push_str(sub);
5396 new_arg.push(']');
5397 }
5398 new_arg.push_str(tail);
5399 nameref_rewrite = Some(new_arg);
5400 }
5401 nameref_resolution::NotRef => {}
5402 }
5403 }
5404 }
5405 let arg: &String = nameref_rewrite.as_ref().unwrap_or(arg);
5406 let arg_name: &str = match arg.find('=') {
5407 Some(i) => &arg[..i],
5408 None => arg.as_str(),
5409 };
5410 // Recompute the existing-pm snapshots for the rewritten name.
5411 let (pname_in_tab, usepm_existing) = if nameref_rewrite.is_some() {
5412 let tab = paramtab().read().unwrap();
5413 match tab.get(arg_name.split('[').next().unwrap_or(arg_name)) {
5414 Some(pm) => (true, (pm.node.flags as u32 & PM_UNSET) == 0),
5415 None => (false, false),
5416 }
5417 } else {
5418 (pname_in_tab, usepm_existing)
5419 };
5420
5421 // c:2930 — `else if (pm)` reuse decision for the bin_typeset
5422 // literal-name loop: `if ((!(pm->node.flags & PM_UNSET) ||
5423 // pm->node.flags & PM_DECLARED)
5424 // && (locallevel == pm->level || !(on & PM_LOCAL)))`.
5425 // Decides whether the existing pm is reusable in place or
5426 // shadowed by a new local. The Rust per-arg loop short-circuits
5427 // through `createparam`'s reuse arm (params.rs:1975) which
5428 // already encodes this rule, but the literal C predicate
5429 // belongs here so the parity is visible at the call site.
5430 let cur_locallevel = locallevel.load(Relaxed) as i32;
5431 let pm_reuse_local: bool = if pname_in_tab {
5432 let tab = paramtab().read().unwrap();
5433 let pm = tab.get(arg_name).unwrap();
5434 let f = pm.node.flags as u32;
5435 ((f & PM_UNSET) == 0 || (f & PM_DECLARED) != 0)
5436 && (cur_locallevel == pm.level || (on as u32 & PM_LOCAL) == 0) // c:2930
5437 } else {
5438 true
5439 };
5440 let _ = pm_reuse_local;
5441
5442 // c:3127-3132 — PM_NAMEREF literal-name branch. When
5443 // `(on & PM_NAMEREF)` and an existing `hn` is present:
5444 // `if (((Param)hn)->level >= locallevel ||
5445 // (!(on & PM_LOCAL) && ((Param)hn)->level < locallevel)) {
5446 // unsetparam_pm(oldpm, 0, 1); hn = NULL; }`.
5447 // Namerefs always start over fresh when redeclared.
5448 if (on as u32 & PM_NAMEREF) != 0 && pname_in_tab {
5449 let level_compare = paramtab()
5450 .read()
5451 .ok()
5452 .and_then(|t| t.get(arg_name).map(|pm| pm.level))
5453 .unwrap_or(0);
5454 if level_compare >= cur_locallevel
5455 || ((on as u32 & PM_LOCAL) == 0 && level_compare < cur_locallevel)
5456 // c:3130
5457 {
5458 // unsetparam_pm + hn = NULL would happen here. The
5459 // simplified PM_NAMEREF path leaves the reset to
5460 // typeset_single's name-resolution branch at
5461 // typeset_single c:2750.
5462 }
5463 }
5464
5465 // c:2469-2510 — `typeset_single` createparam dispatch for new
5466 // PM_LOCAL declarations. Inside a function scope (`local x` or
5467 // `typeset x` from a fn body), C calls createparam(name,
5468 // on|PM_LOCAL) which chains pm.old = oldpm at the current
5469 // locallevel — the c:2575 `pm->level = locallevel` stamp that
5470 // endparamscope unwinds. Without this, `local x=inside`
5471 // modifies the outer-scope x instead of installing a shadow.
5472 // c:Src/builtin.c:2469 — `if ((on & PM_LOCAL) && (!pm ||
5473 // pm->level < locallevel)) { ...createparam... }`. The
5474 // condition is: PM_LOCAL set AND (no existing pm OR existing
5475 // pm is at a LOWER scope than the current locallevel). At
5476 // TOP scope (locallevel=0), `pm->level < 0` is never true,
5477 // so C doesn't call createparam — it reuses the existing
5478 // param in place. The previous Rust port called createparam
5479 // unconditionally, which routed through the reuse arm at
5480 // params.rs:2062-2068 and then clobbered pm.node.flags via
5481 // params.rs:2132 (`pm->node.flags = flags & ~PM_LOCAL`).
5482 // That wiped PM_INTEGER, leaving u_val=42 with PM_SCALAR
5483 // flags → getsparam returned empty. Bug #326 in
5484 // docs/BUGS.md. Mirror the C guard so the createparam call
5485 // only fires when we genuinely need to allocate a new shadow.
5486 let needs_new_shadow = if pname_in_tab {
5487 paramtab()
5488 .read()
5489 .ok()
5490 .and_then(|t| t.get(arg_name).map(|pm| pm.level < cur_locallevel))
5491 .unwrap_or(true)
5492 } else {
5493 true
5494 };
5495 if (on as u32 & PM_LOCAL) != 0 // c:2469
5496 && !arg_name.is_empty()
5497 && !arg_name.starts_with('-')
5498 && !arg_name.starts_with('+')
5499 && needs_new_shadow
5500 {
5501 let kind = if is_hashed {
5502 PM_HASHED
5503 } else if is_array {
5504 PM_ARRAY
5505 } else {
5506 0
5507 };
5508 // c:2475-2487 — C calls `assignsparam(pname, value, 0)`
5509 // which creates the pm via the assignsparam → createparam
5510 // path WITHOUT propagating PM_READONLY/PM_EXPORTED flags
5511 // (that path uses PM_SCALAR / PM_ARRAY / PM_HASHED only).
5512 // Post-assign attribute stamps add PM_READONLY/PM_EXPORTED
5513 // later. Mirror by passing ONLY the type-kind + PM_LOCAL
5514 // (not the full `on` mask) so the freshly-created pm
5515 // doesn't error on its own first assignment.
5516 let _ = createparam(arg_name, kind as i32 | PM_LOCAL as i32);
5517 // c:2575 — `else if (on & PM_LOCAL) pm->level = locallevel;`
5518 // — stamp the just-created pm at the current scope so
5519 // endparamscope (params.c) unwinds the shadow when the
5520 // enclosing function returns. createparam at params.rs:2014
5521 // already sets `level: cur_locallevel` on the fresh pm;
5522 // re-stamp here against the post-createparam pm to mirror
5523 // C's explicit assignment, AND to catch the reuse-arm path
5524 // (params.rs:1975-1986) where the existing pm's level was
5525 // pre-set by a prior scope.
5526 if let Ok(mut tab) = paramtab().write() {
5527 if let Some(pm) = tab.get_mut(arg_name) {
5528 pm.level = cur_locallevel; // c:2575
5529 // c:2691 + c:4087 arrsetfn — flags that affect the
5530 // VALUE store (PM_UNIQUE dedup, PM_LEFT/RIGHT_B/Z
5531 // padding width) must land on pm.flags BEFORE
5532 // assignaparam → arrsetfn runs, else those
5533 // setfns see the un-flagged pm. C zsh applies
5534 // these as part of the pre-assignment stamp at
5535 // typeset_single c:2476-2479. Mirror by
5536 // pre-stamping the value-affecting subset of
5537 // `on`. The full attribute mask (PM_READONLY,
5538 // PM_EXPORTED, etc.) still lands in the
5539 // post-assign block below since those don't
5540 // change the value at write time.
5541 let pre_assign_mask: u32 =
5542 PM_UNIQUE | PM_LEFT | PM_RIGHT_B | PM_RIGHT_Z | PM_LOWER | PM_UPPER;
5543 pm.node.flags |= (on as u32 & pre_assign_mask) as i32;
5544 pm.node.flags &= !((off as u32 & pre_assign_mask) as i32);
5545 // c:Src/builtin.c:2544 — `if (isset(TYPESETTOUNSET))
5546 // pm->node.flags |= PM_DEFAULTED;`. Under
5547 // `setopt typeset_to_unset`, bare `typeset NAME`
5548 // (no `=`) creates a "declared but not assigned"
5549 // entry. Only fires when the arg has no `=`
5550 // (assignment paths get PM_UNSET cleared as a side
5551 // effect of the value write). Bug #280 in
5552 // docs/BUGS.md.
5553 if !arg.contains('=') && isset(crate::ported::zsh_h::TYPESETTOUNSET) {
5554 pm.node.flags |= PM_DEFAULTED as i32;
5555 }
5556 }
5557 }
5558 }
5559
5560 // c:2462-2467 — subscripted-name PM_LOCAL guard: `else if
5561 // ((on & PM_LOCAL) && locallevel) { ... if (!pm || pm->level
5562 // != locallevel) zerrnam("can't create local array elements") }`.
5563 // Refuses to create a NEW local for `local arr[N]=val` when
5564 // the outer-scope pm at a different level exists. The Rust
5565 // per-arg loop treats subscripted names as the eq-branch's
5566 // `name[key]=val` shape inside assignsparam; the guard fires
5567 // here BEFORE the assignment so we emit the C error message.
5568 if let Some(br) = arg_name.find('[') {
5569 let base = &arg_name[..br];
5570 if (on as u32 & PM_LOCAL) != 0 && cur_locallevel != 0 {
5571 // c:2462
5572 let pm_level = paramtab()
5573 .read()
5574 .ok()
5575 .and_then(|t| t.get(base).map(|pm| pm.level));
5576 if pm_level.is_none() || pm_level != Some(cur_locallevel) {
5577 // c:2466
5578 zerrnam(
5579 name,
5580 &format!("{}: can't create local array elements", base), // c:2466
5581 );
5582 continue; // c:2467
5583 }
5584 }
5585 // c:Src/builtin.c:2500-2502 — `else { zerrnam(cname,
5586 // "%s: inconsistent array element or slice assignment",
5587 // pname); return NULL; }`. C's typeset_single rejects
5588 // type-flag attributes on subscripted names: when the
5589 // name has a `[…]` subscript AND the requested `on` mask
5590 // carries a non-scalar PM_TYPE bit (PM_INTEGER, PM_EFLOAT,
5591 // PM_FFLOAT, PM_ARRAY, PM_HASHED), it's
5592 // `typeset -i h[k]` / `-A h[k]` / etc. — attribute
5593 // applies to whole array, not element. zshrs's
5594 // typeset path silently accepted these and applied
5595 // the attribute to the element. Bug #219 in
5596 // docs/BUGS.md. Plain `h[k]=value` (no `-` flag) has
5597 // PM_TYPE(on) == PM_SCALAR and stays valid.
5598 let pm_type_bits =
5599 on as u32 & (PM_INTEGER | PM_EFLOAT | PM_FFLOAT | PM_ARRAY | PM_HASHED);
5600 if pm_type_bits != 0 {
5601 zerrnam(
5602 name,
5603 &format!(
5604 "{}: inconsistent array element or slice assignment",
5605 arg_name
5606 ),
5607 );
5608 continue;
5609 }
5610 }
5611
5612 if let Some(eq) = arg.find('=') {
5613 let n = &arg[..eq];
5614 // c:Src/builtin.c:2289 — `pm->node.flags = ... & ~off` must
5615 // clear `off` bits in value-affecting flags BEFORE the
5616 // assignment runs (params.rs assignaparam:5526 snapshots
5617 // PM_UNIQUE and dedups in setfn). The PM_LOCAL pre_assign
5618 // block below only fires when on & PM_LOCAL; top-level
5619 // `typeset +U arr=(...)` has on=0 and would otherwise
5620 // leave the stale PM_UNIQUE in place. Bug #234.
5621 let pre_assign_off_mask =
5622 (PM_UNIQUE | PM_LEFT | PM_RIGHT_B | PM_RIGHT_Z | PM_LOWER | PM_UPPER) as i32;
5623 let off_in_pre_mask = (off as i32) & pre_assign_off_mask;
5624 if off_in_pre_mask != 0 {
5625 if let Ok(mut tab) = paramtab().write() {
5626 if let Some(pm) = tab.get_mut(n) {
5627 pm.node.flags &= !off_in_pre_mask;
5628 }
5629 }
5630 }
5631 let raw_v = &arg[eq + 1..];
5632 // c:2945-3050 — `=(elem elem ...)` array-init syntax.
5633 // The parser hands the whole `(...)` body in as one arg
5634 // when typeset's BINF_MAGICEQUALS is set; the `(` / `)` are
5635 // literal first/last bytes. Strip them and split on
5636 // whitespace to recover the element list.
5637 //
5638 // c:Src/parse.c par_simple/intypeset — the array-vs-scalar
5639 // distinction is SYNTACTIC (the `asg->flags & ASG_ARRAY`
5640 // bit), set when the `(` is an UNQUOTED paren at parse time,
5641 // NOT a heuristic on the expanded value. zshrs encodes that
5642 // bit as the `\u{1f}` REJOIN_SEP marker inserted by
5643 // compile_zsh's BUILTIN_TYPESET_PAREN_PACK, which only fires
5644 // for syntactic (unquoted) paren-inits. A double-quoted
5645 // value that happens to expand to `(...)` —
5646 // `typeset out="(${x:gs/%/%%})"` — reaches here as a plain
5647 // dequoted scalar with NO marker and must stay a SCALAR
5648 // (zsh: `out=(ruby-3.2%%@1.0)`). Keying on `starts_with('(')`
5649 // alone mis-stored it as an array. The `()`-empty and
5650 // `=(\u{1f}elem…)` pack forms always carry the marker.
5651 let is_paren_init = raw_v.starts_with('(')
5652 && raw_v.ends_with(')')
5653 && raw_v.len() >= 2
5654 && raw_v.contains('\u{1f}');
5655 // c:Src/builtin.c:2095-2097 — `if (ASG_ARRAYP(asg) &&
5656 // PM_TYPE(on) == PM_SCALAR && ...) on |= PM_ARRAY;`. Auto-
5657 // promote to array when the assignment shape is array but
5658 // no explicit type was requested. Required so plain
5659 // `local arr=(a b c)` (which carries on=PM_LOCAL with
5660 // no PM_TYPE bits) reaches the array-init path. Without
5661 // it the c:2342 inconsistency check below errors on every
5662 // bare array assign.
5663 let mut on = on;
5664 if is_paren_init && crate::ported::zsh_h::PM_TYPE(on) == PM_SCALAR {
5665 on |= PM_ARRAY;
5666 }
5667 // c:Src/builtin.c:2342-2347 — `inconsistent type for
5668 // assignment`: when the user types an array RHS
5669 // (`x=(...)`) but the resolved type flags don't include
5670 // PM_ARRAY|PM_HASHED, error out. C source:
5671 // ```
5672 // if ((asg->flags & ASG_ARRAY) ?
5673 // !(on & (PM_ARRAY|PM_HASHED)) :
5674 // (asg->value.scalar && (on & (PM_ARRAY|PM_HASHED)))) {
5675 // zerrnam(cname, "%s: inconsistent type for assignment", pname);
5676 // return NULL;
5677 // }
5678 // ```
5679 // The C conflict-resolution pass at c:2718-2742 clears
5680 // PM_ARRAY/PM_HASHED from `on` when a conflicting type
5681 // (`-i` / `-E` / `-F`) is requested (`on & PM_INTEGER →
5682 // off |= PM_ARRAY; on &= ~off`), so `-ia x=(1 2 3)` lands
5683 // here with PM_INTEGER set + PM_ARRAY cleared. The
5684 // c:2095 auto-promote above doesn't fire either (PM_TYPE
5685 // is non-zero from PM_INTEGER), so the inconsistency
5686 // error fires correctly. Bug #250.
5687 if is_paren_init && (on & (PM_ARRAY | PM_HASHED)) == 0 {
5688 zerrnam(
5689 name, // cname
5690 &format!("{}: inconsistent type for assignment", n),
5691 );
5692 unqueue_signals();
5693 return 1;
5694 }
5695 if is_paren_init {
5696 let inner = &raw_v[1..raw_v.len() - 1]; // c:2950
5697 // c:Src/builtin.c:2555-2556 globlist — C glob-expands each
5698 // element working on the TOKENIZED value where quoted
5699 // metachars keep their Bnull escape, so a quoted `(` / `*`
5700 // is inert. zshrs's compile path (compile_zsh's
5701 // BUILTIN_TYPESET_PAREN_PACK) already runs the per-element
5702 // glob via compile_word_str — identical to the bare
5703 // `arr=(…)` path (compile_zsh.rs ~3294) — and hands
5704 // bin_typeset the FULLY-EXPANDED, DEQUOTED elements via the
5705 // `\u{1f}` sentinel form. Re-globbing those dequoted values
5706 // here is a double-glob that loses the original quoting:
5707 // `typeset -a p=("*.txt")` globbed the quoted star, and
5708 // `typeset -a p=("a=\$((1+2))")` parsed the literal `((` as
5709 // a bareglobqual ("unknown file attribute: ("). Skip the
5710 // re-glob for the pack (`\u{1f}`) form; only the legacy
5711 // whitespace form (elements not pre-expanded) needs it.
5712 let from_pack = inner.contains('\u{1f}');
5713 // c:2952 globlist — each list node is one element. When
5714 // the multi-arg rejoin loop above ran, elements are
5715 // separated by the `\u{1f}` REJOIN_SEP sentinel
5716 // (consecutive sentinels preserve quoted-empty args).
5717 // Otherwise (single-arg paren-init form like `a=(1 2 3)`)
5718 // the inner is plain-whitespace separated.
5719 // Bug #93 in docs/BUGS.md.
5720 let split_elems: Vec<String> = if inner.contains('\u{1f}') {
5721 // c:Src/builtin.c:2952 globlist — each REJOIN_SEP
5722 // segment is ONE already-parsed array element (the
5723 // parser split argv and quote-removed each element),
5724 // so split EXACTLY on the sentinel. Do NOT trim or
5725 // drop all-whitespace segments — that would corrupt
5726 // quoted leading/trailing whitespace, e.g.
5727 // `local a=( " name: x" )` (the ` name:` prefix is
5728 // significant and must survive for `${a[(i) name: …]}`
5729 // to match). Boundary empties from the `=(` / `)` argv
5730 // fragments are removed by the first/last-empty trim
5731 // immediately below.
5732 inner.split('\u{1f}').map(String::from).collect::<Vec<_>>()
5733 } else {
5734 inner.split_whitespace().map(String::from).collect()
5735 };
5736 let split_elems = if inner.contains('\u{1f}') {
5737 let mut v: Vec<String> = split_elems;
5738 // Trim only the FIRST leading empty (from the open
5739 // `=(` arg's tail after stripping `(`) and the LAST
5740 // trailing empty (from the close `)` arg's head),
5741 // preserving any genuine empties in the middle.
5742 if v.first().is_some_and(|s| s.is_empty()) {
5743 v.remove(0);
5744 }
5745 if v.last().is_some_and(|s| s.is_empty()) {
5746 v.pop();
5747 }
5748 v
5749 } else {
5750 split_elems
5751 };
5752 // c:Src/subst.c:2558-2571 — `$=var` IFS-split operator.
5753 // The full paramsubst path in subst.rs doesn't yet
5754 // handle the `=` flag (spbreak=2 in C) when invoked
5755 // outside the canonical addvars→prefork→stringsubst
5756 // chain. typeset's array-init reaches bin_typeset
5757 // AFTER the unified prefork has already run (against
5758 // the entire `arr=( ... )` string, treating the parens
5759 // as literal), so any `$=name` inside the parens
5760 // stayed untouched.
5761 //
5762 // Mirror the C `spbreak=2` semantics inline: for each
5763 // element of the form `$=NAME`, look up NAME, split on
5764 // IFS, and substitute the resulting fields in place.
5765 // Other elements (already paramsubst'd by the outer
5766 // prefork, or literal) pass through unchanged.
5767 let raw_elems: Vec<String> = {
5768 // c:Src/params.c IFS — the canonical IFS source is
5769 // the IFS shell param (PM_SPECIAL gsu reads the
5770 // live $IFS in the current scope). Reading the
5771 // OS env var would miss `local IFS=` overrides and
5772 // catch stale values left behind by earlier scopes.
5773 let ifs_chars: Vec<char> = crate::ported::params::getsparam("IFS")
5774 .unwrap_or_else(|| " \t\n".to_string())
5775 .chars()
5776 .collect();
5777 let mut out: Vec<String> = Vec::new();
5778 for se in split_elems {
5779 if let Some(stripped) = se.strip_prefix("$=") {
5780 // c:2562 — `s++; spbreak = 2; nojoin =
5781 // !(ifs && *ifs);`. Look up the variable;
5782 // if unset, treat as empty string and
5783 // produce no fields.
5784 let val =
5785 crate::ported::params::getsparam(stripped).unwrap_or_default();
5786 if ifs_chars.is_empty() {
5787 if !val.is_empty() {
5788 out.push(val);
5789 }
5790 } else {
5791 for field in val
5792 .split(|c: char| ifs_chars.contains(&c))
5793 .filter(|s| !s.is_empty())
5794 {
5795 out.push(field.to_string());
5796 }
5797 }
5798 } else {
5799 out.push(se);
5800 }
5801 }
5802 out
5803 };
5804 // c:2555-2556 — `globlist(vl, prefork_ret)` glob-expands
5805 // each element when it contains wildcards. Skipped for the
5806 // pre-expanded pack form (see `from_pack` above): those
5807 // elements were already glob-expanded (quote-aware) at
5808 // compile time, so re-globbing the dequoted values is wrong.
5809 let mut elems: Vec<String> = Vec::with_capacity(raw_elems.len());
5810 for re in raw_elems {
5811 if from_pack {
5812 elems.push(re);
5813 continue;
5814 }
5815 // c:Src/glob.c:1230 zglob — `if (... !haswilds(ostr)
5816 // ...) return;` short-circuits before patcompile.
5817 // In C, lexer/prefork preserves Bnull-escape on
5818 // quoted metachars (`'('` becomes `Bnull (`), so
5819 // haswilds returns false on a quoted-paren key.
5820 // zshrs's upstream prefork strips Bnull before
5821 // typeset args land here (see TRACE_TS_RAWV =
5822 // `[40, 31, 40, 31, ...]` — bare `(` without the
5823 // Bnull escape), so haswilds spuriously returns
5824 // true on a literal `(` from `T=( '(' v )`. Guard
5825 // by pre-compiling: if patcompile fails, the
5826 // element can't be a real glob — keep it literal
5827 // (matches the C zglob no-wild short-circuit and
5828 // suppresses the spurious "bad pattern" zerr).
5829 // haswilds scans TOKENIZED strings — tokenize a
5830 // local copy of the untokenized element first
5831 // (c:Src/glob.c:3548), as C does for runtime-built
5832 // strings (compcore.c:2231); patcompile consumes
5833 // the same tokenized form.
5834 let mut re_tok = re.to_string();
5835 crate::ported::glob::tokenize(&mut re_tok);
5836 if crate::ported::pattern::haswilds(&re_tok) {
5837 let compilable = crate::ported::pattern::patcompile(
5838 &re_tok,
5839 crate::ported::zsh_h::PAT_HEAPDUP as i32,
5840 None,
5841 )
5842 .is_some();
5843 if !compilable {
5844 elems.push(re);
5845 continue;
5846 }
5847 let expanded = crate::ported::glob::glob_path(&re);
5848 if expanded.is_empty() || (expanded.len() == 1 && expanded[0] == re) {
5849 elems.push(re);
5850 } else {
5851 elems.extend(expanded);
5852 }
5853 } else {
5854 elems.push(re);
5855 }
5856 }
5857 // c:Src/builtin.c:2355-2378 — tc (type-conversion)
5858 // branch: the requested type differs from the
5859 // existing param's type. zsh keeps readonly/exported
5860 // status across the conversion (`on |= ~off &
5861 // (PM_READONLY|PM_EXPORTED) & pm->node.flags`) but
5862 // turns PM_READONLY OFF on the old pm so the delete/
5863 // recreate succeeds — `typeset -r h2=(); typeset -A
5864 // h2=(k v)` is legal in zsh. The Rust assignment
5865 // funnel (sethparam/setaparam) performs the type
5866 // rewrite in place, so clearing PM_READONLY here is
5867 // the c:2359 step and the post-assign stamp below
5868 // restores it from `on` (c:2357 carry).
5869 {
5870 let requested = if is_hashed { PM_HASHED } else { PM_ARRAY };
5871 let existing_flags = paramtab()
5872 .read()
5873 .ok()
5874 .and_then(|t| t.get(n).map(|pm| pm.node.flags as u32));
5875 if let Some(f) = existing_flags {
5876 if (f & PM_UNSET) == 0 && crate::ported::zsh_h::PM_TYPE(f) != requested {
5877 // c:2357 — carry readonly/exported into `on`.
5878 on |= !off & (PM_READONLY | PM_EXPORTED) & f;
5879 // c:2359 — `pm->node.flags &= ~PM_READONLY;`
5880 if let Ok(mut tab) = paramtab().write() {
5881 if let Some(pm) = tab.get_mut(n) {
5882 pm.node.flags &= !(PM_READONLY as i32);
5883 }
5884 }
5885 }
5886 }
5887 }
5888 // c:Src/builtin.c:2476-2479 + c:2691 + c:4087 arrsetfn —
5889 // pre-stamp value-affecting flags on the pm BEFORE the
5890 // array-init setarrvalue runs, so PM_UNIQUE drives dedup
5891 // inside assignaparam (params.rs:5571 reads pm.flags &
5892 // PM_UNIQUE to gate simple_arrayuniq). Without this,
5893 // `typeset -aUx u=(/bin /usr/bin /bin)` stamped PM_UNIQUE
5894 // only AFTER setarrvalue ran, so dedup never fired.
5895 // `typeset -aU u=(...)` happened to work due to a side
5896 // path; this makes both consistent. Bug #272.
5897 if is_array {
5898 let pre_assign_mask: u32 = PM_UNIQUE;
5899 if (on as u32 & pre_assign_mask) != 0 {
5900 let exists = paramtab()
5901 .read()
5902 .map(|t| t.contains_key(n))
5903 .unwrap_or(false);
5904 if !exists {
5905 let _ = createparam(n, PM_ARRAY as i32);
5906 }
5907 if let Ok(mut tab) = paramtab().write() {
5908 if let Some(pm) = tab.get_mut(n) {
5909 pm.node.flags |= (on as u32 & pre_assign_mask) as i32;
5910 }
5911 }
5912 }
5913 }
5914 // c:2357 — a scalar → array/hashed conversion is a TYPE change,
5915 // which recreates the param carrying only READONLY|EXPORTED, so
5916 // the old scalar padding flags (-L/-R/-Z) must NOT survive it:
5917 // `typeset -Z 3 x=7; typeset -a x=(1 2)` is `array`, not
5918 // `array-right_zeros`. Capture whether the param was ALREADY an
5919 // array/hashed (in which case it is NOT a type change and the
5920 // padding is preserved by the merge stamp below).
5921 let prior_was_arraylike = paramtab().read().ok().and_then(|t| {
5922 t.get(n).map(|pm| {
5923 let typ = PM_TYPE(pm.node.flags as u32);
5924 typ == PM_ARRAY || typ == PM_HASHED
5925 })
5926 }) == Some(true);
5927 if is_hashed {
5928 // c:2960-2975 — `setdataparam(..., PM_HASHED, …)`.
5929 // Two assoc-init shapes accepted by zsh:
5930 // 1. flat alternating k/v: `m=(k1 v1 k2 v2)`
5931 // 2. per-element [K]=V: `m=([k1]=v1 [k2]=v2)`
5932 // The parser hands all elements as one `(…)` body,
5933 // so we detect shape 2 when every element starts
5934 // with `[` and contains `]=`. Otherwise fall back
5935 // to alternating pairs.
5936 let bracket_shape = !elems.is_empty()
5937 && elems.iter().all(|e| e.starts_with('[') && e.contains("]="));
5938 let mut map: IndexMap<String, String> = IndexMap::new();
5939 if bracket_shape {
5940 for e in &elems {
5941 let close = e.find("]=").unwrap();
5942 let k = e[1..close].to_string();
5943 let v = e[close + 2..].to_string();
5944 map.insert(k, v);
5945 }
5946 } else {
5947 let mut it = elems.into_iter(); // c:2960 pair walk
5948 while let Some(k) = it.next() {
5949 let v = it.next().unwrap_or_default();
5950 map.insert(k, v); // c:2964 hashtab insert
5951 }
5952 }
5953 crate::ported::exec::set_assoc(n, map.clone());
5954 } else {
5955 // c:2980-2995 — plain array.
5956 crate::ported::exec::set_array(n, elems.clone());
5957 }
5958 // c:2357 — on a genuine type change (was NOT array/hashed), drop
5959 // the old scalar padding flags; re-add only what THIS command's
5960 // `on` requests (so `typeset -aZ 3 arr=(...)` still zero-pads).
5961 if !prior_was_arraylike {
5962 let pad = (PM_LEFT | PM_RIGHT_B | PM_RIGHT_Z) as i32;
5963 if let Ok(mut tab) = paramtab().write() {
5964 if let Some(pm) = tab.get_mut(n) {
5965 pm.node.flags = (pm.node.flags & !pad) | (on as i32 & pad);
5966 }
5967 }
5968 }
5969 // c:2330-2337 (typeset_single) — `if (!(pm =
5970 // assignaparam(pname, ..., flags))) return NULL;
5971 // ... if (errflag) return NULL;` A readonly
5972 // rejection inside setarrvalue (c:Src/params.c:2900)
5973 // sets errflag and refuses the value; typeset_single
5974 // returns NULL and bin_typeset records `returnval = 1`
5975 // (c:3153-3156). Skip the attribute stamps — C never
5976 // reaches them on this path. Gated on usepm: the
5977 // c:2336 check lives ONLY in the reuse-existing-pm
5978 // branch.
5979 if usepm_existing && (errflag.load(Relaxed) & ERRFLAG_ERROR) != 0 {
5980 returnval = 1; // c:3156
5981 continue; // c:2337 return NULL
5982 }
5983 // c:2510-2520 — `on = pm->node.flags;` then stamp the
5984 // attribute bits on the just-assigned param. The
5985 // scalar-assign arm below does the same; the array /
5986 // assoc `=(...)` init path was missing this, so
5987 // `typeset -ax ARR=(a b)` left PM_EXPORTED unset on
5988 // the paramtab entry. `(t)ARR` then read `array`
5989 // instead of `array-export`, and `typeset -p ARR`
5990 // emitted `typeset -a ARR=...` instead of `-ax`.
5991 let post_assign_mask = (PM_READONLY
5992 | PM_EXPORTED
5993 | PM_LEFT
5994 | PM_RIGHT_B
5995 | PM_RIGHT_Z
5996 | PM_TAGGED
5997 | PM_HIDE
5998 | PM_HIDEVAL
5999 | PM_UNIQUE) as i32;
6000 let post_assign_to_set = (on
6001 & (PM_READONLY
6002 | PM_EXPORTED
6003 | PM_LEFT
6004 | PM_RIGHT_B
6005 | PM_RIGHT_Z
6006 | PM_TAGGED
6007 | PM_HIDE
6008 | PM_HIDEVAL
6009 | PM_UNIQUE)) as i32;
6010 if post_assign_to_set != 0 {
6011 if let Ok(mut tab) = paramtab().write() {
6012 if let Some(pm) = tab.get_mut(n) {
6013 // c:2289 MERGE — preserve untouched attributes.
6014 pm.node.flags |= post_assign_to_set;
6015 }
6016 }
6017 }
6018 } else {
6019 // c:2232-2238 (typeset_single) — a SCALAR assignment to an
6020 // existing ARRAY/HASHED param WITHOUT a type flag to convert it
6021 // is inconsistent: `typeset -A x=(k v); typeset x=scalar` errors.
6022 // (An explicit type flag — `typeset -i x=9` — is a type change
6023 // handled above and does NOT reach here.)
6024 let target_is_arraylike = paramtab().read().ok().and_then(|t| {
6025 t.get(n).map(|pm| {
6026 let typ = PM_TYPE(pm.node.flags as u32);
6027 typ == PM_ARRAY || typ == PM_HASHED
6028 })
6029 }) == Some(true);
6030 let requesting_type =
6031 (on as u32 & (PM_INTEGER | PM_EFLOAT | PM_FFLOAT | PM_ARRAY | PM_HASHED)) != 0;
6032 if target_is_arraylike && !requesting_type {
6033 zerrnam(name, &format!("{}: inconsistent type for assignment", n)); // c:2236
6034 returnval = 1;
6035 continue;
6036 }
6037 // c:3010-3030 — `name=value` scalar assign. C-canonical
6038 // `setsparam` (Src/params.c:3350) writes paramtab; the
6039 // env mirror at `Src/params.c:3024 addenv` follows.
6040 // c:Src/params.c PM_LOWER/PM_UPPER setstrvalue arms:
6041 // when typeset -l or -u is set, the assigned value is
6042 // case-folded BEFORE storage. Without this, `typeset -l
6043 // s=HELLO; echo $s` printed `HELLO`. We also mirror to
6044 // exec.var_attrs so subsequent plain assigns (`s=NEW`)
6045 // pick up the fold via the SET_VAR opcode's attr
6046 // check (fusevm_bridge.rs case-fold arm).
6047 let lower = (on & PM_LOWER) != 0;
6048 let upper = (on & PM_UPPER) != 0;
6049 let folded: String = if lower {
6050 raw_v.to_lowercase()
6051 } else if upper {
6052 raw_v.to_uppercase()
6053 } else {
6054 raw_v.to_string()
6055 };
6056 // c:typeset_single — createparam with the type flag
6057 // BEFORE assignsparam, so assignstrvalue's PM_TYPE
6058 // dispatch (params.c:2748) routes the value through
6059 // the correct setfn:
6060 // - PM_INTEGER → intsetfn (mathevali → u_val)
6061 // - PM_EFLOAT/PM_FFLOAT → floatsetfn (parsefloat → u_dval)
6062 // - PM_SCALAR → strsetfn (u_str)
6063 // The previous Rust ordering (setsparam first, then
6064 // flip flags) wrote "5" to u_str then changed PM_TYPE
6065 // to PM_INTEGER without migrating u_str → u_val, so
6066 // getsparam(n) read u_val=0 instead of 5.
6067 // c:2748-2784 — pre-assign type flags only (PM_INTEGER
6068 // etc. — affect storage / setfn dispatch). Post-assign
6069 // attributes (PM_READONLY / PM_EXPORTED / justification
6070 // bits) are stamped AFTER setsparam since the C path
6071 // (c:2475 `assignsparam(pname, val, 0)` → c:2510
6072 // `on = pm->node.flags`) sets PM_READONLY only after
6073 // the value lands. Mixing them pre-assign caused
6074 // `readonly y=hello` to error "read-only variable: y"
6075 // — the freshly-created pm had PM_READONLY which
6076 // blocked its OWN initial assign.
6077 let pre_assign_mask =
6078 (PM_INTEGER | PM_EFLOAT | PM_FFLOAT | PM_LOWER | PM_UPPER | PM_NAMEREF) as i32;
6079 let pre_assign_to_set = (on
6080 & (PM_INTEGER | PM_EFLOAT | PM_FFLOAT | PM_LOWER | PM_UPPER | PM_NAMEREF))
6081 as i32;
6082 if pre_assign_to_set != 0 {
6083 let pname_in_tab = paramtab()
6084 .read()
6085 .map(|t| t.contains_key(n))
6086 .unwrap_or(false);
6087 if !pname_in_tab {
6088 // c:1132+ createparam(name, type_flags) — fresh.
6089 let _ = createparam(n, pre_assign_to_set);
6090 } else {
6091 // c:2355-2378 tc (type-conversion) — flip the
6092 // PM_TYPE bits on the existing param BEFORE
6093 // re-assigning so assignstrvalue routes through
6094 // the new type's setfn.
6095 //
6096 // c:2357 — a TYPE change deletes and recreates the param
6097 // carrying ONLY READONLY|EXPORTED, so the old scalar
6098 // padding flags (-L/-R/-Z) do NOT survive it: after
6099 // `typeset -Z 3 x=7; typeset -i x=1` the type is plain
6100 // `integer`, not `integer-right_zeros`. When the new type
6101 // is numeric (INTEGER/EFLOAT/FFLOAT), clear the padding
6102 // flags from the old param too. (A single `typeset -iZ`
6103 // command re-adds RIGHT_Z via the post-assign stamp since
6104 // it is in `on`.)
6105 let is_numeric_type =
6106 (pre_assign_to_set as u32 & (PM_INTEGER | PM_EFLOAT | PM_FFLOAT)) != 0;
6107 // On a numeric type change also drop the scalar padding
6108 // flags AND — if the old param was an array/hashed —
6109 // PM_ARRAY|PM_HASHED plus its stored data, since C deletes
6110 // and recreates: `typeset -A x=(k v); typeset -F x=1.5`
6111 // yields a plain `float`, not a hashed remnant.
6112 let old_type = paramtab()
6113 .read()
6114 .ok()
6115 .and_then(|t| t.get(n).map(|pm| PM_TYPE(pm.node.flags as u32)))
6116 .unwrap_or(PM_SCALAR);
6117 let was_arraylike = old_type == PM_ARRAY || old_type == PM_HASHED;
6118 let extra_clear = if is_numeric_type {
6119 let mut c = (PM_LEFT | PM_RIGHT_B | PM_RIGHT_Z) as i32;
6120 if was_arraylike {
6121 c |= (PM_ARRAY | PM_HASHED) as i32;
6122 }
6123 c
6124 } else {
6125 0
6126 };
6127 if is_numeric_type && was_arraylike {
6128 // c:2355-2378 — array/hashed → numeric is a full type
6129 // conversion: delete the old aggregate (storage +
6130 // paramtab entry) and recreate as the numeric type, so
6131 // the value migrates through the new setfn and the
6132 // result is a clean `integer`/`float`. Modifying flags
6133 // in place left the hashed storage's setfn dispatch
6134 // active, so the value stayed a scalar.
6135 crate::ported::exec::unset_array(n);
6136 crate::ported::exec::unset_assoc(n);
6137 if let Ok(mut tab) = paramtab().write() {
6138 tab.remove(n);
6139 }
6140 let _ = createparam(n, pre_assign_to_set);
6141 } else if let Ok(mut tab) = paramtab().write() {
6142 if let Some(pm) = tab.get_mut(n) {
6143 pm.node.flags = (pm.node.flags & !(pre_assign_mask | extra_clear))
6144 | pre_assign_to_set;
6145 }
6146 }
6147 }
6148 }
6149 // c:2289 — `pm->node.flags = (pm->node.flags | (on &
6150 // ~PM_READONLY)) & ~(off | PM_UNSET);` — `+n` / `+r`
6151 // WITH a value clear the bits BEFORE the assignment
6152 // runs (`typeset +rn ref=RW`: type change first, so
6153 // the = writes the now-plain scalar, not through the
6154 // old refname).
6155 let off_pre = (off as u32 & (PM_NAMEREF | PM_READONLY)) as i32;
6156 if off_pre != 0 {
6157 if let Ok(mut tab) = paramtab().write() {
6158 if let Some(pm) = tab.get_mut(n) {
6159 pm.node.flags &= !off_pre;
6160 if (off as u32 & PM_NAMEREF) != 0 {
6161 pm.width = 0;
6162 pm.base = 0;
6163 }
6164 }
6165 }
6166 }
6167 // c:2322 — typeset assigns via `assignsparam(pname, ..., 0)`:
6168 // flags=0, NOT setsparam's ASSPM_WARN (params.c:3294), so an
6169 // explicit `typeset -g NAME=val` inside a function never
6170 // trips WARN_CREATE_GLOBAL. Routing through setsparam here
6171 // made zshrs warn where zsh is silent (f-sy-h's
6172 // `typeset -g _ZSH_HIGHLIGHT_PRIOR_BUFFER=...`).
6173 crate::ported::params::assignsparam(n, &folded, 0); // c:2322
6174 // c:2326-2328 + c:2336-2337 (typeset_single) —
6175 // `if (asg->value.scalar && !(pm = assignsparam(
6176 // pname, ztrdup(asg->value.scalar), 0)))
6177 // return NULL;
6178 // ... if (errflag) return NULL;`
6179 // A readonly rejection inside assignstrvalue
6180 // (c:Src/params.c:2697) sets errflag and the value is
6181 // refused; typeset_single returns NULL and bin_typeset
6182 // records `returnval = 1` (c:3153-3156). Skip the
6183 // attribute stamps — C never reaches them on this path.
6184 // Gated on usepm: the c:2336 check lives ONLY in the
6185 // reuse-existing-pm branch.
6186 if usepm_existing && (errflag.load(Relaxed) & ERRFLAG_ERROR) != 0 {
6187 returnval = 1; // c:3156
6188 continue; // c:2337 return NULL
6189 }
6190 // c:2510-2520 — `on = pm->node.flags;` then stamp the
6191 // attribute bits on the just-assigned param.
6192 let post_assign_mask = (PM_READONLY
6193 | PM_EXPORTED
6194 | PM_LEFT
6195 | PM_RIGHT_B
6196 | PM_RIGHT_Z
6197 | PM_TAGGED
6198 | PM_HIDE
6199 | PM_HIDEVAL
6200 | PM_UNIQUE) as i32;
6201 let post_assign_to_set = (on
6202 & (PM_READONLY
6203 | PM_EXPORTED
6204 | PM_LEFT
6205 | PM_RIGHT_B
6206 | PM_RIGHT_Z
6207 | PM_TAGGED
6208 | PM_HIDE
6209 | PM_HIDEVAL
6210 | PM_UNIQUE)) as i32;
6211 if post_assign_to_set != 0 {
6212 // c:2289 — `pm->node.flags = (pm->node.flags | (on & ...)) & ~off`.
6213 // This is a MERGE: OR in the requested attributes and clear
6214 // the ones explicitly turned off, but PRESERVE existing
6215 // attributes the user did not touch. The previous
6216 // `(flags & !post_assign_mask) | to_set` cleared EVERY mask
6217 // bit, so `typeset -Z 3 x=7; typeset -r x=ro` lost the -Z
6218 // (RIGHT_Z is in the mask but not in `on`), yielding
6219 // `scalar-readonly` where zsh keeps `scalar-right_zeros-readonly`.
6220 let post_assign_to_clear = (off as u32 & post_assign_mask as u32) as i32;
6221 if let Ok(mut tab) = paramtab().write() {
6222 if let Some(pm) = tab.get_mut(n) {
6223 pm.node.flags =
6224 (pm.node.flags | post_assign_to_set) & !post_assign_to_clear;
6225 }
6226 }
6227 }
6228 // c:1973-1989 (Src/builtin.c, inside typeset_single)
6229 // — `if (arg) { int base = zstrtol(arg, ...) ;
6230 // pm->base = base; }`. The precision arg from `-i N`,
6231 // `-E N`, `-F N` (parsed by execbuiltin as
6232 // ops.args[<F-arg-slot>]) lands on the param's `base`
6233 // field, which convfloat reads as the format-digit
6234 // count. Without this stamp, `typeset -F 2 x=3.14`
6235 // ignored the `2` and printed at the default
6236 // 10-digit precision.
6237 {
6238 let prec_arg: Option<&str> = if (on & PM_INTEGER) != 0 && OPT_HASARG(&ops, b'i')
6239 {
6240 OPT_ARG(&ops, b'i')
6241 } else if (on & PM_EFLOAT) != 0 && OPT_HASARG(&ops, b'E') {
6242 OPT_ARG(&ops, b'E')
6243 } else if (on & PM_FFLOAT) != 0 && OPT_HASARG(&ops, b'F') {
6244 OPT_ARG(&ops, b'F')
6245 } else {
6246 None
6247 };
6248 if let Some(s) = prec_arg {
6249 if let Ok(b) = s.trim().parse::<i32>() {
6250 if let Ok(mut tab) = paramtab().write() {
6251 if let Some(pm) = tab.get_mut(n) {
6252 pm.base = b;
6253 }
6254 }
6255 }
6256 }
6257 }
6258 // c:2009-2014 typeset_setwidth — `-L N` / `-R N` / `-Z N`
6259 // install pm.width = N. The auto-fallback in assignsparam
6260 // (PM_INTEGER block at params.rs:3619) stamps width to
6261 // s.len() when PM_RIGHT_Z is set but width==0; that lands
6262 // BEFORE the post-assign PM_RIGHT_Z stamp here, so the
6263 // user's explicit `-Z 6` was being overwritten by the
6264 // value's char count. Set width AFTER setsparam so the
6265 // explicit option arg wins over the auto-fallback.
6266 {
6267 let width_arg: Option<&str> =
6268 if (on as u32 & PM_LEFT) != 0 && OPT_HASARG(&ops, b'L') {
6269 OPT_ARG(&ops, b'L')
6270 } else if (on as u32 & PM_RIGHT_B) != 0 && OPT_HASARG(&ops, b'R') {
6271 OPT_ARG(&ops, b'R')
6272 } else if (on as u32 & PM_RIGHT_Z) != 0 && OPT_HASARG(&ops, b'Z') {
6273 OPT_ARG(&ops, b'Z')
6274 } else {
6275 None
6276 };
6277 if let Some(s) = width_arg {
6278 if let Ok(w) = s.trim().parse::<i32>() {
6279 if let Ok(mut tab) = paramtab().write() {
6280 if let Some(pm) = tab.get_mut(n) {
6281 pm.width = w;
6282 }
6283 }
6284 }
6285 }
6286 }
6287 // c:Src/params.c:3024 addenv — only mirror to OS env
6288 // when PM_EXPORTED is in flags or already-exported.
6289 let already_exported = env::var_os(n).is_some();
6290 if (on & PM_EXPORTED) != 0 || already_exported {
6291 env::set_var(n, &folded); // c:3024 addenv
6292 }
6293 }
6294 } else if is_hashed || is_array {
6295 // c:3060-3070 — bare name + `-A`/`-a` declares an empty
6296 // assoc/array.
6297 if is_hashed {
6298 if crate::ported::exec::assoc(arg).is_none() {
6299 crate::ported::exec::set_assoc(arg, IndexMap::new());
6300 }
6301 } else if crate::ported::exec::array(arg).is_none() {
6302 crate::ported::exec::set_array(arg, Vec::new());
6303 }
6304 // c:Src/params.c:4087 arrsetfn — when PM_UNIQUE is set on
6305 // an existing array, the canonical setfn applies
6306 // `uniqarray()` to the current contents. `typeset -aU arr`
6307 // on an existing arr must dedupe in place; without this,
6308 // the flag stamp lands on pm.flags but the value stays
6309 // un-deduped until the next assignment.
6310 if is_array && (on as u32 & PM_UNIQUE) != 0 {
6311 let current = crate::ported::exec::array(arg).unwrap_or_default();
6312 // simple_arrayuniq is the in-place dedupe used by
6313 // params.rs arrsetfn (PM_UNIQUE path).
6314 let deduped = {
6315 let mut seen = std::collections::HashSet::new();
6316 current
6317 .into_iter()
6318 .filter(|x| seen.insert(x.clone()))
6319 .collect::<Vec<_>>()
6320 };
6321 crate::ported::exec::set_array(arg, deduped);
6322 }
6323 // Stamp attribute bits on paramtab entry — same set as
6324 // the `name=value` post-assign mask.
6325 let post_assign_mask = (PM_READONLY
6326 | PM_EXPORTED
6327 | PM_LEFT
6328 | PM_RIGHT_B
6329 | PM_RIGHT_Z
6330 | PM_TAGGED
6331 | PM_HIDE
6332 | PM_HIDEVAL
6333 | PM_UNIQUE
6334 | PM_HASHED
6335 | PM_ARRAY) as i32;
6336 let post_assign_to_set = (on
6337 & (PM_READONLY
6338 | PM_EXPORTED
6339 | PM_LEFT
6340 | PM_RIGHT_B
6341 | PM_RIGHT_Z
6342 | PM_TAGGED
6343 | PM_HIDE
6344 | PM_HIDEVAL
6345 | PM_UNIQUE
6346 | PM_HASHED
6347 | PM_ARRAY)) as i32;
6348 if post_assign_to_set != 0 {
6349 if let Ok(mut tab) = paramtab().write() {
6350 if let Some(pm) = tab.get_mut(arg_name) {
6351 // c:2289 MERGE — preserve attributes the user did not touch
6352 // (e.g. `typeset -L 4 x=hi; typeset -r x` keeps -L). `off` bits
6353 // are cleared separately below.
6354 pm.node.flags |= post_assign_to_set;
6355 }
6356 }
6357 }
6358 // c:Src/builtin.c:2289 — `pm->node.flags = ... & ~off`.
6359 // `off` bits in post_assign_mask must clear regardless of
6360 // `on` (e.g. `typeset +U arr=(...)` where on=0,
6361 // off=PM_UNIQUE). The post_assign_to_set gate above
6362 // short-circuits the on=0 case. Bug #234 in docs/BUGS.md.
6363 let off_in_mask = (off as i32) & post_assign_mask;
6364 if off_in_mask != 0 {
6365 if let Ok(mut tab) = paramtab().write() {
6366 if let Some(pm) = tab.get_mut(arg_name) {
6367 pm.node.flags &= !off_in_mask;
6368 }
6369 }
6370 }
6371 } else {
6372 // c:2355-2378 (typeset_single tc branch) — bare `typeset -i n`
6373 // / `-F n` / `-E n` / `-l n` / `-u n` / `-r n` / `export N`
6374 // / `readonly N` converts/stamps the existing param. Split
6375 // into pre-assign (type conversion) and post-assign
6376 // (attribute stamp) the same way the `name=value` arm does.
6377 let pre_assign_mask =
6378 (PM_INTEGER | PM_EFLOAT | PM_FFLOAT | PM_LOWER | PM_UPPER | PM_NAMEREF) as i32;
6379 let pre_assign_to_set = (on
6380 & (PM_INTEGER | PM_EFLOAT | PM_FFLOAT | PM_LOWER | PM_UPPER | PM_NAMEREF))
6381 as i32;
6382 let post_assign_mask = (PM_READONLY
6383 | PM_EXPORTED
6384 | PM_LEFT
6385 | PM_RIGHT_B
6386 | PM_RIGHT_Z
6387 | PM_TAGGED
6388 | PM_HIDE
6389 | PM_HIDEVAL
6390 | PM_UNIQUE) as i32;
6391 let post_assign_to_set = (on
6392 & (PM_READONLY
6393 | PM_EXPORTED
6394 | PM_LEFT
6395 | PM_RIGHT_B
6396 | PM_RIGHT_Z
6397 | PM_TAGGED
6398 | PM_HIDE
6399 | PM_HIDEVAL
6400 | PM_UNIQUE)) as i32;
6401 // c:2374 — `s = ztrdup(getsparam(pname));`. Capture the
6402 // pre-conversion scalar value so the re-assignment after
6403 // type flip preserves it through the new setfn.
6404 // `typeset +n ref`: the stored REFNAME (u_str raw) becomes
6405 // the scalar value — getsparam would deref through the
6406 // chain and return the target's value instead (c:2374 via
6407 // the +n type-conversion arm; K01 "remove nameref
6408 // attribute" expects `typeset ptr=var`).
6409 let saved_val =
6410 if (off as u32 & PM_NAMEREF) != 0 && crate::ported::params::is_nameref(arg) {
6411 paramtab()
6412 .read()
6413 .ok()
6414 .and_then(|t| t.get(arg.as_str()).and_then(|p| p.u_str.clone()))
6415 } else {
6416 getsparam(arg)
6417 };
6418 // c:Src/builtin.c:3072 — `if (!getsparam(pname))
6419 // setsparam(pname, "");`. C zsh's getsparam returns the
6420 // join-of-values for PM_HASHED and PM_ARRAY so this gate
6421 // fires only for truly-undeclared names. zshrs's getsparam
6422 // returns None for assoc/array (it routes through the
6423 // PM_SCALAR / PM_INTEGER / PM_*FLOAT arms only), so this
6424 // branch falsely fired for an existing hash AND triggered
6425 // assignsparam's PM_HASHED→PM_SCALAR coerce at params.rs:
6426 // 5368-5374 which removes the entry from
6427 // paramtab_hashed_storage. Bug #218 in docs/BUGS.md.
6428 //
6429 // Gate on the paramtab flag instead: if the param exists
6430 // and is PM_HASHED/PM_ARRAY, treat it as already-declared
6431 // even though getsparam returned None.
6432 let already_typed = paramtab()
6433 .read()
6434 .ok()
6435 .and_then(|t| {
6436 t.get(arg).map(|pm| {
6437 let f = pm.node.flags as u32;
6438 let typ = PM_TYPE(f);
6439 typ == PM_HASHED || typ == PM_ARRAY
6440 })
6441 })
6442 .unwrap_or(false);
6443 // Capture the param's PRIOR numeric class (before the flag
6444 // change below) so the base-stamp arm can reset pm.base when
6445 // switching BETWEEN integer and float — the base field means
6446 // radix for integers but precision for floats, so a leftover
6447 // value leaks (float precision 3 → integer base 3 = "3#10";
6448 // int base 16 → float 16-digit precision). E↔F and same-type
6449 // re-declares keep it (both float, base=precision). zsh 5.9.1
6450 // verified.
6451 let (prior_is_integer, prior_is_float) = paramtab()
6452 .read()
6453 .ok()
6454 .and_then(|t| {
6455 t.get(arg).map(|pm| {
6456 let f = pm.node.flags as u32;
6457 ((f & PM_INTEGER) != 0, (f & (PM_EFLOAT | PM_FFLOAT)) != 0)
6458 })
6459 })
6460 .unwrap_or((false, false));
6461 let was_fresh = saved_val.is_none() && !already_typed;
6462 if was_fresh {
6463 // c:3072 — `if (!getsparam(pname)) setsparam(pname, "")`.
6464 // flags=0: a typeset-driven create never trips
6465 // WARN_CREATE_GLOBAL (typeset_single assigns with
6466 // `assignsparam(pname, ..., 0)`, c:2322).
6467 crate::ported::params::assignsparam(arg, "", 0); // c:3074
6468 // c:Src/builtin.c:2544 — `if (isset(TYPESETTOUNSET))
6469 // pm->node.flags |= PM_DEFAULTED;`. Under
6470 // `setopt typeset_to_unset`, bare `typeset NAME` (no
6471 // `=`) creates a "declared but not assigned" entry:
6472 // PM_DECLARED + PM_UNSET (= PM_DEFAULTED). `${+NAME}`
6473 // and `[[ -v NAME ]]` then return false until an
6474 // explicit assignment clears PM_UNSET. Bug #280 in
6475 // docs/BUGS.md.
6476 if isset(crate::ported::zsh_h::TYPESETTOUNSET) {
6477 if let Ok(mut tab) = paramtab().write() {
6478 if let Some(pm) = tab.get_mut(arg) {
6479 pm.node.flags |= PM_DEFAULTED as i32;
6480 }
6481 }
6482 }
6483 let _ = was_fresh;
6484 }
6485 // c:Src/builtin.c::typeset_single c:2374-2378 — the `+i`
6486 // / `+E` / `+F` / `+l` / `+u` / `+r` / `+n` paths
6487 // REMOVE the corresponding PM_* flag and migrate the
6488 // stored value back to u_str. C captures
6489 // `s = ztrdup(getsparam(pname))` BEFORE the type-conversion
6490 // unset and restores it after. Without this branch,
6491 // `typeset +i n` (where n was an integer with value 42)
6492 // cleared u_str without copying the integer value back —
6493 // result was `typeset n=''`. Bug #326 in docs/BUGS.md.
6494 let pre_assign_to_clear = (off
6495 & (PM_INTEGER | PM_EFLOAT | PM_FFLOAT | PM_LOWER | PM_UPPER | PM_NAMEREF))
6496 as i32;
6497 if pre_assign_to_clear != 0 {
6498 if let Ok(mut tab) = paramtab().write() {
6499 if let Some(pm) = tab.get_mut(arg.as_str()) {
6500 pm.node.flags &= !pre_assign_to_clear;
6501 // `+n` — drop the nameref scope/subscript info
6502 // along with the flag (c:2374-2378 conversion).
6503 if (off as u32 & PM_NAMEREF) != 0 {
6504 pm.width = 0;
6505 pm.base = 0;
6506 }
6507 }
6508 }
6509 // Restore the captured scalar so the cleared-type
6510 // param has its value as a string (PM_SCALAR
6511 // semantics now apply). flags=0 — typeset-internal
6512 // restore, never WARN_CREATE_GLOBAL (c:2322).
6513 if let Some(ref val) = saved_val {
6514 crate::ported::params::assignsparam(arg, val, 0);
6515 }
6516 }
6517 if pre_assign_to_set != 0 {
6518 if let Ok(mut tab) = paramtab().write() {
6519 if let Some(pm) = tab.get_mut(arg) {
6520 pm.node.flags = (pm.node.flags & !pre_assign_mask) | pre_assign_to_set;
6521 }
6522 }
6523 // c:2372-2378 — re-assign saved value through new type's
6524 // setfn so u_val (for PM_INTEGER) or u_dval (for PM_*FLOAT)
6525 // catches the value migration from u_str.
6526 if let Some(ref val) = saved_val {
6527 setsparam(arg, val);
6528 }
6529 }
6530 // c:1973-1989 (Src/builtin.c, inside typeset_single):
6531 // if (arg) {
6532 // int base = zstrtol(arg, ..., 10);
6533 // pm->base = base;
6534 // }
6535 // The precision arg from `-i N`, `-E N`, `-F N` (parsed
6536 // by execbuiltin as `ops.args[<F-arg-slot>]`) lands on
6537 // the param's `base` field, which `convfloat` (c:5689 in
6538 // params.c) reads as the format-digit count. The
6539 // `name=value` arm above (c:2009-2014 typeset_setwidth
6540 // companion path) already stamps `pm.base`; this bare-
6541 // declare arm was missing it, so `typeset -F 4 f` left
6542 // pm.base=0 and `convfloat` rendered with the default
6543 // 10-digit precision (parity bug #29). With this stamp
6544 // a subsequent `(( f = ... ))` re-assignment preserves
6545 // the `pm.base = 4` set here through `assignnparam`'s
6546 // re-assign path (params.rs:5340-5374, c:2874-2878).
6547 {
6548 let prec_arg: Option<&str> = if (on & PM_INTEGER) != 0 && OPT_HASARG(&ops, b'i') {
6549 OPT_ARG(&ops, b'i') // c:1974 -i N
6550 } else if (on & PM_EFLOAT) != 0 && OPT_HASARG(&ops, b'E') {
6551 OPT_ARG(&ops, b'E') // c:1977 -E N
6552 } else if (on & PM_FFLOAT) != 0 && OPT_HASARG(&ops, b'F') {
6553 OPT_ARG(&ops, b'F') // c:1980 -F N
6554 } else {
6555 None
6556 };
6557 if let Some(s) = prec_arg {
6558 if let Ok(b) = s.trim().parse::<i32>() {
6559 // c:1985 zstrtol
6560 if let Ok(mut tab) = paramtab().write() {
6561 if let Some(pm) = tab.get_mut(arg) {
6562 pm.base = b; // c:1987 pm->base = base
6563 }
6564 }
6565 }
6566 } else {
6567 // No explicit base/precision arg. When an EXISTING
6568 // numeric param switches BETWEEN integer and float,
6569 // reset pm.base to the default (0) — the shared base
6570 // field would otherwise leak the old type's value
6571 // (createparam's base default is 0, c:Src/params.c:1155;
6572 // a numeric type change re-creates the representation).
6573 // Gated so E↔F and same-type re-declares keep their
6574 // base/precision (`typeset -i16 f; typeset -i f` stays
6575 // base 16). zsh 5.9.1 verified.
6576 let switching_int_float = ((on & PM_INTEGER) != 0 && prior_is_float)
6577 || ((on & (PM_EFLOAT | PM_FFLOAT)) != 0 && prior_is_integer);
6578 if !was_fresh && switching_int_float {
6579 if let Ok(mut tab) = paramtab().write() {
6580 if let Some(pm) = tab.get_mut(arg) {
6581 pm.base = 0;
6582 }
6583 }
6584 }
6585 }
6586 }
6587 // c:2528-2533 — the createparam (valueless declare) branch
6588 // must ALSO stamp the `-L`/`-R`/`-Z` field width
6589 // (typeset_setwidth, c:2002-2011). The base block above
6590 // handled `-i`/`-E`/`-F` precision but the width was dropped,
6591 // so `typeset -L 10 f` (declare, then assign later) left
6592 // pm.width=0 and the value never left/right-justified — and
6593 // `typeset -p` rendered `-FL` with no number. The inline-value
6594 // arm (the `name=value` path) already stamps width; only this
6595 // bare-declare arm was missing it.
6596 {
6597 let width_arg: Option<&str> = if (on & PM_LEFT) != 0 && OPT_HASARG(&ops, b'L') {
6598 OPT_ARG(&ops, b'L') // c:2003
6599 } else if (on & PM_RIGHT_B) != 0 && OPT_HASARG(&ops, b'R') {
6600 OPT_ARG(&ops, b'R') // c:2005
6601 } else if (on & PM_RIGHT_Z) != 0 && OPT_HASARG(&ops, b'Z') {
6602 OPT_ARG(&ops, b'Z') // c:2007
6603 } else {
6604 None
6605 };
6606 if let Some(s) = width_arg {
6607 if let Ok(w) = s.trim().parse::<i32>() {
6608 // c:2011 zstrtol
6609 if let Ok(mut tab) = paramtab().write() {
6610 if let Some(pm) = tab.get_mut(arg) {
6611 pm.width = w; // c:2011 pm->width = width
6612 }
6613 }
6614 }
6615 }
6616 }
6617 // c:2510+ — stamp post-assign attributes (PM_EXPORTED,
6618 // PM_READONLY, etc.) on the (possibly newly-created) pm.
6619 if post_assign_to_set != 0 {
6620 if let Ok(mut tab) = paramtab().write() {
6621 if let Some(pm) = tab.get_mut(arg) {
6622 // c:2289 MERGE — preserve attributes the user did not touch
6623 // (e.g. `typeset -L 4 x=hi; typeset -r x` keeps -L). `off` bits
6624 // are cleared separately below.
6625 pm.node.flags |= post_assign_to_set;
6626 }
6627 }
6628 // c:Src/params.c — when PM_UNIQUE is freshly stamped on
6629 // an EXISTING array via bare `typeset -U arr` (no value
6630 // assignment), zsh dedups the current contents in-place
6631 // (verified vs /opt/homebrew/bin/zsh: `arr=(a b a c b);
6632 // typeset -U arr; echo "$arr[@]"` → "a b c"). Apply the
6633 // same dedup here.
6634 if (post_assign_to_set as u32 & PM_UNIQUE) != 0 {
6635 let existing = crate::ported::params::getaparam(arg);
6636 if let Some(arr) = existing {
6637 let mut seen: std::collections::HashSet<String> =
6638 std::collections::HashSet::new();
6639 let deduped: Vec<String> =
6640 arr.into_iter().filter(|e| seen.insert(e.clone())).collect();
6641 crate::ported::params::setaparam(arg, deduped);
6642 }
6643 }
6644 // c:Src/builtin.c:2302-2307 — `if (!(pm->node.flags &
6645 // (PM_ARRAY|PM_HASHED))) { ... addenv(pm, getsparam(pname)); }`.
6646 // Arrays and associative arrays are NOT mirrored to the
6647 // OS env even when PM_EXPORTED is set — env strings are
6648 // single name=value pairs and have no representation
6649 // for indexed or hashed data. zsh silently no-ops the
6650 // export request for these types; zshrs's prior code
6651 // wrote a malformed empty `h=` entry for `export h`
6652 // where h is an assoc. Bug #349.
6653 if (on as u32 & PM_EXPORTED) != 0 {
6654 let is_array_or_hashed = paramtab()
6655 .read()
6656 .ok()
6657 .and_then(|t| t.get(arg).map(|pm| pm.node.flags as u32))
6658 .map_or(false, |f| (f & (PM_ARRAY | PM_HASHED)) != 0)
6659 || crate::ported::params::paramtab_hashed_storage()
6660 .lock()
6661 .ok()
6662 .map_or(false, |s| s.contains_key(arg))
6663 || crate::ported::exec::array(arg).is_some()
6664 || crate::ported::exec::assoc(arg).is_some();
6665 if !is_array_or_hashed {
6666 if let Some(val) = saved_val.as_deref().or(Some("")) {
6667 env::set_var(arg, val);
6668 }
6669 }
6670 }
6671 }
6672 // c:Src/builtin.c::typeset_single — `typeset +x VAR`
6673 // clears PM_EXPORTED via the `off` mask. C zsh calls
6674 // removeenv on the param, which strips the var from the
6675 // OS environment so child processes no longer see it.
6676 // Must fire INDEPENDENTLY of post_assign_to_set since `+x`
6677 // alone sets `off` (PM_EXPORTED) but `on` is 0, so the
6678 // post_assign_to_set block above doesn't run. Also clear
6679 // the flag bit on the paramtab entry so `(t)V` no longer
6680 // reports `-export`. Bug #201 in docs/BUGS.md.
6681 if (off as u32 & PM_EXPORTED) != 0 {
6682 env::remove_var(arg);
6683 if let Ok(mut tab) = paramtab().write() {
6684 if let Some(pm) = tab.get_mut(arg) {
6685 pm.node.flags &= !(PM_EXPORTED as i32);
6686 }
6687 }
6688 }
6689 // c:Src/builtin.c:2289 — `pm->node.flags = ... & ~off`.
6690 // `off` bits in post_assign_mask must clear regardless of
6691 // `on` (e.g. `typeset +U arr` where on=0, off=PM_UNIQUE).
6692 // The post_assign_to_set gate above short-circuits the
6693 // on=0 case. Same family as `+x` block immediately above.
6694 // Bug #234 in docs/BUGS.md.
6695 let off_in_mask = (off as i32) & post_assign_mask;
6696 if off_in_mask != 0 {
6697 if let Ok(mut tab) = paramtab().write() {
6698 if let Some(pm) = tab.get_mut(arg) {
6699 pm.node.flags &= !off_in_mask;
6700 }
6701 }
6702 }
6703 // c:Src/builtin.c::typeset_single c:2241-2246 — when no
6704 // type flags and no value assignment, `typeset NAME` is a
6705 // display request: print the existing param in the
6706 // canonical form. C uses
6707 // `printparamnode(&pm->node, PRINT_INCLUDEVALUE|with_ns)`.
6708 // Without this, `typeset h` (where h is an assoc) emitted
6709 // nothing. The earlier per-arg print at typeset_single
6710 // line 3181 isn't called from bin_typeset's per-arg loop
6711 // — the loop does its own inline work. Bug #218 in
6712 // docs/BUGS.md.
6713 // c:Src/builtin.c::typeset_single c:2241-2246 — bare
6714 // `typeset NAME` (no on/off flags except PM_LOCAL which
6715 // bin_typeset auto-applies at c:2808) prints the param.
6716 // PM_LOCAL is added implicitly when not -g / -x / -m so
6717 // mask it out when checking "are any user flags set".
6718 //
6719 // c:Src/builtin.c:2078-2090 — the bare form prints UNLESS it
6720 // is localizing a variable from an outer scope. zsh clears
6721 // `usepm` (→ no print) only when `locallevel != pm->level &&
6722 // (on & PM_LOCAL)` — i.e. the existing param lives at a
6723 // different (outer/global) level and we're making a local.
6724 // Re-declaring a param that ALREADY exists at the current
6725 // level (`f(){ typeset x=1; typeset x }`) keeps usepm and
6726 // DOES print `x=1`. The previous `at_top_scope` gate wrongly
6727 // suppressed that in-function re-declaration case.
6728 let user_on = (on as u32) & !PM_LOCAL;
6729 let cur_ll = locallevel.load(Relaxed);
6730 let not_localizing = pm_level_existing == cur_ll || (on as u32 & PM_LOCAL) == 0;
6731 // c:Src/builtin.c:2244 — `else if (!OPT_ISSET(ops,'g') &&
6732 // (unset(TYPESETSILENT) || OPT_ISSET(ops,'m')))`. The `-g`
6733 // (global-scope) flag SUPPRESSES the bare-name print so
6734 // `typeset -g ZPFX` is a silent declaration / no-op rather
6735 // than a list. zinit's plugin loader runs `typeset -g VAR`
6736 // for already-environment-exported names on every load;
6737 // without this gate zshrs spammed `VAR=value` for each on
6738 // startup.
6739 if user_on == 0
6740 && off == 0
6741 && not_localizing
6742 && !OPT_ISSET(&ops, b'p')
6743 && !OPT_ISSET(&ops, b'g')
6744 && (!isset(TYPESETSILENT) || OPT_ISSET(&ops, b'm'))
6745 && pname_in_tab
6746 {
6747 let with_ns = if OPT_ISSET(&ops, b'm') {
6748 PRINT_WITH_NAMESPACE
6749 } else {
6750 0
6751 };
6752 let _ = with_ns;
6753 // The paramtab entry for assoc/array shapes set via
6754 // direct assignment (`a=(1 2 3)` / `h[k]=v`) doesn't
6755 // always have PM_ARRAY/PM_HASHED set on flags — the
6756 // values live in the executor's arrays/assoc_arrays
6757 // storage, separate from paramtab. Check the canonical
6758 // storage paths and emit the right form directly,
6759 // mirroring printparamvalue's PM_ARRAY/PM_HASHED arms
6760 // (params.rs:8819+).
6761 //
6762 // Order matters: assoc lookup falls back to
6763 // crate::ported::exec::assoc which reads the executor's
6764 // assoc_arrays storage. typeset -A populated via
6765 // `typeset -A h=(a 1 b 2)` lives there, not in
6766 // paramtab_hashed_storage. Bug #218 in docs/BUGS.md.
6767 let assoc = crate::ported::params::paramtab_hashed_storage()
6768 .lock()
6769 .ok()
6770 .and_then(|s| s.get(arg).cloned())
6771 .or_else(|| crate::ported::exec::assoc(arg));
6772 if let Some(map) = assoc {
6773 let mut entries: Vec<(&String, &String)> = map.iter().collect();
6774 entries.sort_by(|(a, _), (b, _)| a.cmp(b));
6775 let mut s = format!("{}=( ", arg);
6776 let mut first = true;
6777 for (k, v) in entries {
6778 if !first {
6779 s.push(' ');
6780 }
6781 first = false;
6782 s.push_str(&format!("[{}]={}", k, crate::ported::utils::quotedzputs(v)));
6783 }
6784 s.push_str(" )");
6785 println!("{}", s);
6786 } else if let Some(arr) = crate::ported::params::getaparam(arg)
6787 .or_else(|| crate::ported::exec::array(arg))
6788 .or_else(|| {
6789 // Fallback: paramtab entry's u_arr may be set
6790 // even when PM_TYPE doesn't include PM_ARRAY
6791 // (direct `a=(1 2 3)` assignment doesn't always
6792 // update the flag).
6793 paramtab()
6794 .read()
6795 .ok()
6796 .and_then(|t| t.get(arg).and_then(|pm| pm.u_arr.clone()))
6797 .filter(|v| !v.is_empty())
6798 })
6799 {
6800 let parts: Vec<String> = arr
6801 .iter()
6802 .map(|v| crate::ported::utils::quotedzputs(v))
6803 .collect();
6804 println!("{}=( {} )", arg, parts.join(" "));
6805 } else if let Some(val) = getsparam(arg) {
6806 println!("{}={}", arg, crate::ported::utils::quotedzputs(&val));
6807 }
6808 }
6809 }
6810 }
6811 unqueue_signals();
6812 returnval
6813}
6814
6815/// Port of `eval_autoload(Shfunc shf, char *name, Options ops, int func)` from Src/builtin.c:3166.
6816/// C: `int eval_autoload(Shfunc shf, char *name, Options ops, int func)`.
6817/// PM_UNDEFINED guard; -X spawns the eval-trampoline, otherwise loadautofn
6818/// resolves and installs the body.
6819/// WARNING: param names don't match C — Rust=(shf, name, func) vs C=(shf, name, ops, func)
6820pub fn eval_autoload(
6821 shf: *mut shfunc,
6822 name: &str, // c:3166
6823 ops: &options,
6824 func: i32,
6825) -> i32 {
6826 if shf.is_null() {
6827 return 1;
6828 }
6829 let shf_mut = unsafe { &mut *shf };
6830 // c:3168-3169 — `if (!(shf->node.flags & PM_UNDEFINED)) return 1;`
6831 if (shf_mut.node.flags as u32 & PM_UNDEFINED) == 0 {
6832 // c:3168
6833 return 1; // c:3169
6834 }
6835 // c:3171-3174 — `if (shf->funcdef) { freeeprog(shf->funcdef); shf->funcdef = &dummy_eprog; }`
6836 if shf_mut.funcdef.is_some() {
6837 // c:3171
6838 shf_mut.funcdef = None; // c:3173 freeeprog + dummy
6839 }
6840 // c:3175-3181 — `-X` spawns the autoload trampoline via bin_eval.
6841 if OPT_MINUS(ops, b'X') {
6842 // c:3175
6843 // c:3177 — `fargv[0] = quotestring(name, QT_SINGLE_OPTIONAL); fargv[1] = "\"$@\"";`
6844 let fargv = vec![
6845 // c:3177-3179
6846 quotedzputs(name),
6847 "\"$@\"".to_string(),
6848 ];
6849 // c:3180 — `shf->funcdef = mkautofn(shf);`
6850 let p = mkautofn(shf); // c:3180
6851 let _ = p; // funcdef writeback handled inside mkautofn at c:3801
6852 return bin_eval(name, &fargv, ops, func); // c:3181
6853 }
6854 // c:3184-3186 — `return !loadautofn(shf, (OPT_ISSET('k') ? 2 :
6855 // (OPT_ISSET('z') ? 0 : 1)), 1,
6856 // OPT_ISSET('d'));`
6857 let mode = if OPT_ISSET(ops, b'k') {
6858 2
6859 }
6860 // c:3184
6861 else if OPT_ISSET(ops, b'z') {
6862 0
6863 }
6864 // c:3185
6865 else {
6866 1
6867 };
6868 let _d = OPT_ISSET(ops, b'd');
6869 // c:3184 — `return !loadautofn(shf, ...)`. C's loadautofn returns
6870 // `Shfunc` (pointer): NULL = failure, non-NULL = success. C's
6871 // `!loadautofn` yields 1 (true) when the pointer is NULL —
6872 // i.e. eval_autoload returns 1 on failure, 0 on success.
6873 //
6874 // The Rust port's loadautofn returns an i32 with the OPPOSITE
6875 // sign convention: 0 = success, non-zero = failure (the
6876 // canonical Rust ShellExecutor-status convention used throughout
6877 // this file). The previous Rust port literally translated `!`
6878 // as `if r == 0 { 1 } else { 0 }` — but that double-inverts the
6879 // already-flipped convention, so file-not-found (Rust r=1)
6880 // returned eval_autoload=0 (zsh-success), and the broken
6881 // `autoload +X totally_fake` silently exited 0 instead of 1.
6882 // The C-faithful return for the Rust convention is just `r`.
6883 // Bug #107 in docs/BUGS.md.
6884 loadautofn(shf, mode, 1, _d as i32) // c:3184
6885}
6886
6887/// Port of `check_autoload(Shfunc shf, char *name, Options ops, int func)` from Src/builtin.c:3193.
6888/// C: `static int check_autoload(Shfunc shf, char *name, Options ops,
6889/// int func)` — `OPT_ISSET(ops,'X')` ? eval_autoload : 0.
6890/// WARNING: param names don't match C — Rust=(shf, name, func) vs C=(shf, name, ops, func)
6891pub fn check_autoload(
6892 shf: *mut shfunc,
6893 name: &str, // c:3193
6894 ops: &options,
6895 func: i32,
6896) -> i32 {
6897 // c:3196-3199 — `if (OPT_ISSET(ops,'X')) return eval_autoload(...);`
6898 if OPT_ISSET(ops, b'X') {
6899 // c:3196
6900 return eval_autoload(shf, name, ops, func); // c:3197
6901 }
6902 // c:3200-3242 — -r / -R re-resolve: walk fpath for the function file.
6903 let want_r = OPT_ISSET(ops, b'r');
6904 let want_R = OPT_ISSET(ops, b'R');
6905 if (want_r || want_R) && !shf.is_null() {
6906 // c:3200
6907 let shf_mut = unsafe { &mut *shf };
6908 if (shf_mut.node.flags as u32 & PM_UNDEFINED) == 0 {
6909 return 0;
6910 }
6911 // c:3202-3216 — already has filename + PM_LOADDIR: try the cached
6912 // dir first via spec_path[].
6913 if (shf_mut.node.flags as u32 & PM_LOADDIR) != 0 && shf_mut.filename.is_some() {
6914 let spec = vec![shf_mut.filename.clone().unwrap_or_default()];
6915 if getfpfunc(
6916 &shf_mut.node.nam,
6917 &mut None, // c:3206
6918 Some(&spec),
6919 1,
6920 &mut None, // c:3206 `&ksh` — unused on the test-only path
6921 )
6922 .is_some()
6923 {
6924 return 0; // c:3209
6925 }
6926 // c:3211-3217 — `-d` not set: bail (with -R = error, with -r = silent).
6927 if !OPT_ISSET(ops, b'd') {
6928 // c:3211
6929 if want_R {
6930 // c:3212
6931 zerr(&format!(
6932 "{}: function definition file not found",
6933 shf_mut.node.nam
6934 )); // c:3213
6935 return 1; // c:3215
6936 }
6937 return 0; // c:3216
6938 }
6939 }
6940 // c:3219-3231 — fpath walk via getfpfunc + dircache_set install.
6941 let mut dir_path: Option<String> = None;
6942 if getfpfunc(&shf_mut.node.nam, &mut dir_path, None, 1, &mut None).is_some() // c:3219
6943 && dir_path.is_some()
6944 {
6945 // c:3220-3228 — dircache_set + relative-path absolutize.
6946 let mut old_slot = shf_mut.filename.take();
6947 dircache_set(&mut old_slot, None); // c:3220
6948 let dp = dir_path.unwrap();
6949 let mut new_slot: Option<String> = None;
6950 dircache_set(&mut new_slot, Some(&dp)); // c:3228
6951 shf_mut.filename = new_slot;
6952 shf_mut.node.flags |= PM_LOADDIR as i32; // c:3229
6953 return 0; // c:3230
6954 }
6955 // c:3233-3239 — -R: error; -r: silent.
6956 if want_R {
6957 // c:3233
6958 zerr(&format!(
6959 "{}: function definition file not found",
6960 shf_mut.node.nam
6961 )); // c:3243
6962 return 1; // c:3243
6963 }
6964 }
6965 0 // c:3243
6966}
6967
6968/// Port of `listusermathfunc(MathFunc p)` from Src/builtin.c:3243.
6969/// C: `static void listusermathfunc(MathFunc p)` — emit a `functions -M`
6970/// row for one user math function with arg counts and module name.
6971pub fn listusermathfunc(p: &mathfunc) {
6972 // c:3243
6973 // c:3247-3257 — pick `showargs` 0..3 based on module/min/max presence.
6974 let mut showargs: i32 = if p.module.is_some() {
6975 // c:3249
6976 3
6977 } else if p.maxargs != if p.minargs != 0 { p.minargs } else { -1 } {
6978 // c:3251
6979 2
6980 } else if p.minargs != 0 {
6981 // c:3253
6982 1
6983 } else {
6984 0 // c:3256
6985 };
6986
6987 // c:3259 — `printf("functions -M%s %s", (p->flags & MFF_STR) ? "s" : "", p->name);`
6988 let s_suffix = if (p.flags & MFF_STR) != 0 { "s" } else { "" }; // c:3259
6989 print!("functions -M{} {}", s_suffix, p.name); // c:3259
6990 if showargs != 0 {
6991 // c:3260
6992 print!(" {}", p.minargs); // c:3261
6993 showargs -= 1; // c:3262
6994 }
6995 if showargs != 0 {
6996 // c:3264
6997 print!(" {}", p.maxargs); // c:3265
6998 showargs -= 1; // c:3266
6999 }
7000 if showargs != 0 {
7001 // c:3268
7002 // c:3269-3274 — function names are not required to be ident chars,
7003 // so the module name goes through quotedzputs for safe printing.
7004 print!(" "); // c:3273
7005 print!("{}", quotedzputs(p.module.as_deref().unwrap_or(""))); // c:3274
7006 showargs -= 1; // c:3275
7007 }
7008 println!(); // c:3277
7009}
7010
7011/// Port of `add_autoload_function(Shfunc shf, char *funcname)` from Src/builtin.c:3278.
7012/// C: `static void add_autoload_function(Shfunc shf, char *funcname)` —
7013/// two branches:
7014/// (a) funcname is absolute & shf is PM_UNDEFINED → split `/dir/nam`,
7015/// dircache_set(&shf->filename, dir), set PM_LOADDIR|PM_ABSPATH_USED,
7016/// shfunctab->addnode(nam, shf).
7017/// (b) otherwise → walk funcstack to find calling function; if it has
7018/// PM_LOADDIR|PM_ABSPATH_USED, build `"<calling-dir>/funcname"` and
7019/// access(R_OK); on success copy the dir into shf and set
7020/// PM_LOADDIR|PM_ABSPATH_USED. Then shfunctab->addnode(funcname, shf).
7021/// WARNING: param names don't match C — Rust=(shf) vs C=(shf, funcname)
7022pub fn add_autoload_function(
7023 shf: *mut shfunc, // c:3278
7024 funcname: &str,
7025) {
7026 if shf.is_null() || funcname.is_empty() {
7027 return;
7028 }
7029 let shf_ref = unsafe { &mut *shf };
7030
7031 let is_abs_path = funcname.starts_with('/') // c:3282
7032 && funcname.len() > 1
7033 && funcname[1..].contains('/')
7034 && (shf_ref.node.flags as u32 & PM_UNDEFINED) != 0;
7035
7036 if is_abs_path {
7037 // c:3287 — `nam = strrchr(funcname, '/');`
7038 let nam_idx = funcname.rfind('/').unwrap(); // c:3287
7039 let (dir, nam) = if nam_idx == 0 {
7040 // c:3289
7041 ("/".to_string(), funcname[1..].to_string()) // c:3290
7042 } else {
7043 (
7044 funcname[..nam_idx].to_string(), // c:3293
7045 funcname[nam_idx + 1..].to_string(),
7046 )
7047 };
7048 // c:3296 — `dircache_set(&shf->filename, NULL); dircache_set(..., dir);`
7049 let mut old_slot = shf_ref.filename.take();
7050 dircache_set(&mut old_slot, None); // c:3296
7051 let mut new_slot: Option<String> = None;
7052 dircache_set(&mut new_slot, Some(&dir)); // c:3297
7053 shf_ref.filename = new_slot;
7054 // c:3298-3299 — `shf->node.flags |= PM_LOADDIR | PM_ABSPATH_USED;`
7055 shf_ref.node.flags |= (PM_LOADDIR | PM_ABSPATH_USED) as i32; // c:3298
7056 // c:3300 — `shfunctab->addnode(shfunctab, ztrdup(nam), shf);` — the
7057 // node is keyed on the BASENAME (`nam`), not the full path. zshrs's
7058 // addnode keys on `shf.node.nam`, so rename the node before adding;
7059 // otherwise `autoload -Uz /dir/.hist.format` registered a function
7060 // literally named `/dir/.hist.format` (plugins that autoload by full
7061 // path — zsh-hist's `autoload -Uz $fdir/.hist.*` — diverged from
7062 // zsh, which names it `.hist.format`).
7063 shf_ref.node.nam = nam;
7064 if let Ok(mut t) = shfunctab_lock().write() {
7065 t.addnode(shf); // c:3300
7066 }
7067 } else {
7068 // c:3304-3327 — walk funcstack, look up calling fn in shfunctab, if
7069 // it has PM_LOADDIR|PM_ABSPATH_USED build "<dir>/<funcname>" and
7070 // access(R_OK), inherit the dir on hit.
7071 let calling_f: Option<String> = {
7072 let stack = FUNCSTACK.lock().map(|s| s.clone()).unwrap_or_default();
7073 // c:3306 — `for (fs = funcstack; fs; fs = fs->prev)`
7074 stack
7075 .iter()
7076 .rev()
7077 .find(|fs| {
7078 // c:3306
7079 // c:3307 — `if (fs->tp == FS_FUNC && fs->name &&
7080 // (!shf->node.nam || strcmp(fs->name, shf->node.nam)))`
7081 FS_FUNC != 0 // mirror struct doesn't expose tp directly;
7082 && !fs.name.is_empty()
7083 && (shf_ref.node.nam.is_empty() || fs.name != shf_ref.node.nam)
7084 })
7085 .map(|fs| fs.name.clone()) // c:3308
7086 };
7087 if let Some(cf) = calling_f {
7088 // c:3315
7089 // c:3316 — `shf2 = shfunctab->getnode2(shfunctab, calling_f);`
7090 let shf2_ptr = shfunctab_lock()
7091 .read()
7092 .map(|t| t.getnode2(&cf))
7093 .unwrap_or(std::ptr::null_mut());
7094 if !shf2_ptr.is_null() {
7095 let shf2 = unsafe { &*shf2_ptr };
7096 // c:3317-3318
7097 let needs = (PM_LOADDIR | PM_ABSPATH_USED) as i32;
7098 if (shf2.node.flags & needs) == needs {
7099 // c:3317
7100 if let Some(dir2) = &shf2.filename {
7101 // c:3318
7102 // c:3320 — `snprintf(buf, PATH_MAX, "%s/%s", dir2, funcname);`
7103 let buf = format!("{}/{}", dir2, funcname); // c:3320
7104 if buf.len() <= libc::PATH_MAX as usize {
7105 // c:3320
7106 // c:3324 — `if (!access(buf, R_OK))`
7107 let buf_c = std::ffi::CString::new(buf.clone()).ok();
7108 if let Some(bc) = buf_c {
7109 if unsafe { libc::access(bc.as_ptr(), libc::R_OK) } == 0 {
7110 // c:3324
7111 let mut old_slot = shf_ref.filename.take();
7112 dircache_set(&mut old_slot, None); // c:3325
7113 let dir2c = dir2.clone();
7114 let mut new_slot: Option<String> = None;
7115 dircache_set(&mut new_slot, Some(&dir2c)); // c:3326
7116 shf_ref.filename = new_slot;
7117 shf_ref.node.flags |= (PM_LOADDIR | PM_ABSPATH_USED) as i32;
7118 // c:3327
7119 }
7120 }
7121 }
7122 }
7123 }
7124 }
7125 }
7126 // c:3334 — `shfunctab->addnode(shfunctab, ztrdup(funcname), shf);`
7127 // addnode keys by `shf->node.nam`; if the caller picked a different
7128 // funcname here, re-tag the node name first so the keyed insert
7129 // matches the C contract.
7130 unsafe {
7131 if !shf.is_null() {
7132 (*shf).node.nam = funcname.to_string();
7133 }
7134 }
7135 if let Ok(mut t) = shfunctab_lock().write() {
7136 t.addnode(shf); // c:3334
7137 }
7138 }
7139}
7140
7141/// Port of `bin_functions(char *name, char **argv, Options ops, int func)` from Src/builtin.c:3342.
7142/// C: `int bin_functions(char *name, char **argv, Options ops, int func)`.
7143/// This is the canonical free-function port matching the C signature so
7144/// the dispatcher can call it. The earlier `ShellExecutor::bin_functions`
7145/// inherent method is an ad-hoc Rust-side helper kept for the existing
7146/// in-process executor; both should converge on this function.
7147/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
7148pub fn bin_functions(
7149 name: &str,
7150 argv: &[String], // c:3342
7151 ops: &options,
7152 _func: i32,
7153) -> i32 {
7154 // c:3346-3347 — `int returnval = 0; int on = 0, off = 0, pflags = 0,
7155 // roff, expand = 0;`
7156 let mut returnval: i32 = 0; // c:3346
7157 let mut on: u32 = 0; // c:3347
7158 let mut off: u32 = 0; // c:3347
7159 let _pflags: i32 = 0; // c:3347
7160 let _expand: i32 = 0; // c:3347
7161
7162 // c:3350-3351 — `if (OPT_PLUS(ops,'u')) off |= PM_UNDEFINED; else if
7163 // (OPT_MINUS(ops,'u') || OPT_ISSET(ops,'X')) on |= PM_UNDEFINED;`
7164 if OPT_PLUS(ops, b'u') {
7165 // c:3350
7166 off |= PM_UNDEFINED; // c:3351
7167 } else if OPT_MINUS(ops, b'u') || OPT_ISSET(ops, b'X') {
7168 // c:3352
7169 on |= PM_UNDEFINED; // c:3353
7170 }
7171 // c:3354-3357 — -U / +U toggle PM_UNALIASED|PM_UNDEFINED.
7172 if OPT_MINUS(ops, b'U') {
7173 // c:3354
7174 on |= PM_UNALIASED | PM_UNDEFINED; // c:3355
7175 } else if OPT_PLUS(ops, b'U') {
7176 // c:3356
7177 off |= PM_UNALIASED; // c:3357
7178 }
7179 // c:3358-3361 — -t / +t toggle PM_TAGGED.
7180 if OPT_MINUS(ops, b't') {
7181 // c:3358
7182 on |= PM_TAGGED; // c:3359
7183 } else if OPT_PLUS(ops, b't') {
7184 // c:3360
7185 off |= PM_TAGGED; // c:3361
7186 }
7187 // c:3362-3365 — -T / +T toggle PM_TAGGED_LOCAL.
7188 if OPT_MINUS(ops, b'T') {
7189 // c:3362
7190 on |= PM_TAGGED_LOCAL; // c:3363
7191 } else if OPT_PLUS(ops, b'T') {
7192 // c:3364
7193 off |= PM_TAGGED_LOCAL; // c:3365
7194 }
7195 // c:3366-3369 — -W / +W toggle PM_WARNNESTED.
7196 if OPT_MINUS(ops, b'W') {
7197 // c:3366
7198 on |= PM_WARNNESTED; // c:3367
7199 } else if OPT_PLUS(ops, b'W') {
7200 // c:3368
7201 off |= PM_WARNNESTED; // c:3369
7202 }
7203 // c:3370 — `roff = off;`
7204 let mut roff = off; // c:3370
7205 // c:3371-3377 — -z / +z PM_ZSHSTORED|PM_KSHSTORED interaction.
7206 if OPT_MINUS(ops, b'z') {
7207 // c:3371
7208 on |= PM_ZSHSTORED; // c:3372
7209 off |= PM_KSHSTORED; // c:3373
7210 } else if OPT_PLUS(ops, b'z') {
7211 // c:3374
7212 off |= PM_ZSHSTORED; // c:3375
7213 roff |= PM_ZSHSTORED; // c:3376
7214 }
7215 // c:3379-3385 — -k / +k PM_KSHSTORED|PM_ZSHSTORED interaction.
7216 if OPT_MINUS(ops, b'k') {
7217 // c:3379
7218 on |= PM_KSHSTORED; // c:3380
7219 off |= PM_ZSHSTORED; // c:3381
7220 } else if OPT_PLUS(ops, b'k') {
7221 // c:3382
7222 off |= PM_KSHSTORED; // c:3383
7223 roff |= PM_KSHSTORED; // c:3384
7224 }
7225 // c:3386-3392 — -d / +d PM_CUR_FPATH toggle.
7226 if OPT_MINUS(ops, b'd') {
7227 // c:3386
7228 on |= PM_CUR_FPATH; // c:3387
7229 off |= PM_CUR_FPATH; // c:3388
7230 } else if OPT_PLUS(ops, b'd') {
7231 // c:3389
7232 off |= PM_CUR_FPATH; // c:3390
7233 roff |= PM_CUR_FPATH; // c:3391
7234 }
7235
7236 // c:3394-3400 — early-error validation: invalid flag combinations.
7237 // C: `(OPT_MINUS(ops,'X') && (OPT_ISSET(ops,'m') || !scriptname))` —
7238 // \`-X\` is only valid in a script context (autoload-from-fpath
7239 // dispatch). Previous Rust port dropped the \`|| !scriptname\` half
7240 // so \`functions -X foo\` from interactive shell silently
7241 // succeeded — divergent.
7242 let scriptname_missing = scriptname_get().is_none();
7243 if (off & PM_UNDEFINED) != 0 // c:3394
7244 || (OPT_ISSET(ops, b'k') && OPT_ISSET(ops, b'z')) // c:3394
7245 || (OPT_ISSET(ops, b'x') && !OPT_HASARG(ops, b'x')) // c:3395
7246 || (OPT_MINUS(ops, b'X') // c:3396
7247 && (OPT_ISSET(ops, b'm') || scriptname_missing)) // c:3396 !scriptname
7248 || (OPT_ISSET(ops, b'c')
7249 && (OPT_ISSET(ops, b'x') || OPT_ISSET(ops, b'X') || OPT_ISSET(ops, b'm')))
7250 {
7251 zwarnnam(name, "invalid option(s)"); // c:3399
7252 return 1; // c:3400
7253 }
7254
7255 // c:3402-3452 — `-c` (clone) branch: copy named function under a new
7256 // name, optionally registering it as a TRAP* signal trap.
7257 if OPT_ISSET(ops, b'c') {
7258 // c:3402
7259 if argv.len() < 2 || argv.len() > 2 {
7260 // c:3405
7261 zwarnnam(name, "-c: requires two arguments"); // c:3406
7262 return 1;
7263 }
7264 let src_name = &argv[0];
7265 let dst_name = &argv[1];
7266 // c:3409 — `shf = shfunctab->getnode(shfunctab, *argv);`
7267 let src_ptr = shfunctab_lock()
7268 .read()
7269 .map(|t| t.getnode(src_name.as_str()))
7270 .unwrap_or(std::ptr::null_mut());
7271 if src_ptr.is_null() {
7272 // c:3410
7273 zwarnnam(name, &format!("no such function: {}", src_name)); // c:3411
7274 return 1;
7275 }
7276 // c:3414-3421 — autoload-trampoline expansion if PM_UNDEFINED.
7277 // C body: `if (shf->flags & PM_UNDEFINED) { freeeprog;
7278 // funcdef=dummy; shf = loadautofn(shf,1,0,0); if (!shf) return 1; }`.
7279 // Rust port routes through the local loadautofn helper at
7280 // builtin.rs:883 which walks $fpath via getfpfunc, reads the
7281 // file, stores the body text on the Rust-side ShFunc, and
7282 // clears PM_UNDEFINED.
7283 if (unsafe { (*src_ptr).node.flags } as u32 & PM_UNDEFINED) != 0 {
7284 // c:3415-3418 — `freeeprog(shf->funcdef); shf->funcdef =
7285 // &dummy_eprog;` clear out any stale autoload stub before
7286 // re-loading. Rust port: drop the Option<Eprog>.
7287 unsafe {
7288 (*src_ptr).funcdef = None;
7289 }
7290 // c:3419 — `loadautofn(shf, 1, 0, 0)`.
7291 if loadautofn(src_ptr, 1, 0, 0) != 0 {
7292 // c:3420-3421 — autoload failed.
7293 return 1;
7294 }
7295 }
7296 // c:3422-3430 — `newsh = zalloc + memcpy + filename rebuild`.
7297 let src_ref = unsafe { &*src_ptr };
7298 let new_filename =
7299 if (src_ref.node.flags as u32 & PM_UNDEFINED) == 0 && src_ref.filename.is_some() {
7300 src_ref.filename.clone() // c:3429
7301 } else {
7302 None
7303 };
7304 let _ = new_filename; // wired into shfunctab[dst_name] below
7305 // c:3437-3447 — TRAP* prefix detection + signal trap registration.
7306 if dst_name.starts_with("TRAP") {
7307 // c:3437
7308 // c:3438 — `int sigidx = getsigidx(s + 4);`
7309 let sigidx = getsigidx(&dst_name[4..]); // c:3438
7310 if sigidx != -1 {
7311 // c:3439
7312 // c:3440 — `if (settrap(sigidx, NULL, ZSIG_FUNC))`.
7313 if settrap(sigidx, None, ZSIG_FUNC) != 0 {
7314 // c:3440
7315 // freeeprog(newsh->funcdef) — funcdef Drop covers it.
7316 // dircache_set(&newsh->filename, NULL);
7317 // zfree(newsh, sizeof(*newsh));
7318 return 1; // c:3445
7319 }
7320 // c:3447 — `removetrapnode(sigidx);` — clear any prior trap.
7321 removetrapnode(sigidx); // c:3447
7322 }
7323 }
7324 // c:3422-3430 — C does `newsh = zalloc + memcpy(*newsh, *shf)` so
7325 // src and dst become independent copies. Box-clone the source body
7326 // (rather than aliasing src_ptr) so subsequent mutation through
7327 // `getnode(dst_name)` doesn't bleed into src.
7328 // c:3450 — `shfunctab->addnode(shfunctab, ztrdup(s), &newsh->node);`
7329 let newsh = unsafe {
7330 let mut copy = (*src_ptr).clone();
7331 copy.node.nam = dst_name.clone();
7332 Box::into_raw(Box::new(copy))
7333 };
7334 if let Ok(mut t) = shfunctab_lock().write() {
7335 t.addnode(newsh); // c:3450
7336 }
7337 return 0; // c:3451
7338 }
7339
7340 // c:3454-3463 — `-x N` indent override for printing.
7341 let mut expand: i32 = 0; // c:3454 (also c:3347)
7342 if OPT_ISSET(ops, b'x') {
7343 // c:3454
7344 let arg = OPT_ARG(ops, b'x').unwrap_or("");
7345 match arg.trim().parse::<i32>() {
7346 // c:3456
7347 Ok(n) => {
7348 expand = n; // c:3456
7349 if expand == 0 {
7350 expand = -1;
7351 } // c:3461-3462
7352 }
7353 Err(_) => {
7354 zwarnnam(name, "number expected after -x"); // c:3458
7355 return 1; // c:3459
7356 }
7357 }
7358 }
7359
7360 // c:3465-3466 — `+f` / roff / `+` enables PRINT_NAMEONLY.
7361 let mut pflags: i32 = 0;
7362 if OPT_PLUS(ops, b'f') || roff != 0 || OPT_ISSET(ops, b'+') {
7363 // c:3465
7364 pflags |= PRINT_NAMEONLY; // c:3466
7365 }
7366
7367 // c:3468-3530 — `-M`/`+M` add/remove/list math function path.
7368 if OPT_MINUS(ops, b'M') || OPT_PLUS(ops, b'M') {
7369 // c:3468
7370 // c:3473-3477 — refuse incompatible flag combos.
7371 if on != 0
7372 || off != 0
7373 || pflags != 0
7374 || OPT_ISSET(ops, b'X')
7375 || OPT_ISSET(ops, b'u')
7376 || OPT_ISSET(ops, b'U')
7377 || OPT_ISSET(ops, b'w')
7378 {
7379 zwarnnam(name, "invalid option(s)"); // c:3475
7380 return 1; // c:3476
7381 }
7382 if argv.is_empty() {
7383 // c:3478
7384 // c:3479-3484 — list user math ported.
7385 queue_signals(); // c:3480
7386 if let Ok(table) = MATHFUNCS.lock() {
7387 // c:3481
7388 for p in table.iter() {
7389 // c:3481
7390 if (p.flags & MFF_USERFUNC) != 0 {
7391 // c:3482
7392 listusermathfunc(p); // c:3483
7393 }
7394 }
7395 }
7396 unqueue_signals(); // c:3484
7397 return returnval;
7398 } else if OPT_ISSET(ops, b'm') {
7399 // c:3485
7400 // c:3486-3515 — list/delete matching math ported by pattern.
7401 for arg in argv.iter() {
7402 queue_signals(); // c:3488
7403 // c:3489 — `tokenize(*argv)`; Rust patcompile handles it.
7404 if let Some(pprog) = patcompile(
7405 &{
7406 let mut __pat_tok = (arg).to_string();
7407 crate::ported::glob::tokenize(&mut __pat_tok);
7408 __pat_tok
7409 },
7410 PAT_STATIC,
7411 None,
7412 ) {
7413 // c:3490
7414 if OPT_PLUS(ops, b'M') {
7415 // c:3497
7416 // Delete matching user ported.
7417 if let Ok(mut table) = MATHFUNCS.lock() {
7418 table.retain(|p| {
7419 !((p.flags & MFF_USERFUNC) != 0 && pattry(&pprog, &p.name))
7420 });
7421 }
7422 } else {
7423 // c:3502 — listusermathfunc for matches.
7424 if let Ok(table) = MATHFUNCS.lock() {
7425 for p in table.iter() {
7426 if (p.flags & MFF_USERFUNC) != 0 && pattry(&pprog, &p.name) {
7427 listusermathfunc(p);
7428 }
7429 }
7430 }
7431 }
7432 } else {
7433 // c:3509
7434 // c:3510-3512 — bad pattern.
7435 zwarnnam(
7436 name, // c:3511
7437 &format!("bad pattern : {}", arg),
7438 );
7439 returnval = 1; // c:3512
7440 }
7441 unqueue_signals(); // c:3514
7442 }
7443 return returnval;
7444 } else if OPT_PLUS(ops, b'M') {
7445 // c:3516
7446 // c:3517-3533 — `+M name…` delete by exact name.
7447 for arg in argv.iter() {
7448 queue_signals(); // c:3519
7449 if let Ok(mut table) = MATHFUNCS.lock() {
7450 let idx = table.iter().position(|p| p.name == *arg); // c:3520-3521
7451 if let Some(i) = idx {
7452 if (table[i].flags & MFF_USERFUNC) == 0 {
7453 // c:3522-3527 — library function, refuse.
7454 zwarnnam(
7455 name, // c:3523
7456 &format!("+M {}: is a library function", arg),
7457 );
7458 returnval = 1; // c:3525
7459 } else {
7460 table.remove(i); // c:3528
7461 }
7462 }
7463 }
7464 unqueue_signals(); // c:3532
7465 }
7466 return returnval;
7467 } else {
7468 // c:3535-3611 — `-M name [min [max [mod]]]` add a user math fn.
7469 let mut argv_iter = argv.iter();
7470 let funcname = argv_iter.next().unwrap(); // c:3537
7471 let mut minargs: i32;
7472 let mut maxargs: i32;
7473 if OPT_ISSET(ops, b's') {
7474 // c:3541
7475 minargs = 1; // c:3542
7476 maxargs = 1; // c:3542
7477 } else {
7478 minargs = 0; // c:3544
7479 maxargs = -1; // c:3545
7480 }
7481 // c:3548-3552 — bad math function name check.
7482 let bytes = funcname.as_bytes();
7483 let first_bad = bytes.is_empty()
7484 || (bytes[0] as char).is_ascii_digit()
7485 || !bytes
7486 .iter()
7487 .all(|&c| c.is_ascii_alphanumeric() || c == b'_');
7488 if first_bad {
7489 // c:3549
7490 zwarnnam(
7491 name, // c:3550
7492 &format!("-M {}: bad math function name", funcname),
7493 );
7494 return 1; // c:3551
7495 }
7496 if let Some(arg) = argv_iter.next() {
7497 // c:3554
7498 match arg.parse::<i32>() {
7499 // c:3555 zstrtol
7500 Ok(n) if n >= 0 => minargs = n, // c:3556
7501 _ => {
7502 zwarnnam(
7503 name, // c:3557
7504 &format!("-M: invalid min number of arguments: {}", arg),
7505 );
7506 return 1; // c:3559
7507 }
7508 }
7509 if OPT_ISSET(ops, b's') && minargs != 1 {
7510 // c:3561
7511 zwarnnam(
7512 name, // c:3562
7513 "-Ms: must take a single string argument",
7514 );
7515 return 1; // c:3563
7516 }
7517 maxargs = minargs; // c:3565
7518 }
7519 if let Some(arg) = argv_iter.next() {
7520 // c:3568
7521 match arg.parse::<i32>() {
7522 // c:3569
7523 Ok(n) if n >= -1 && (n == -1 || n >= minargs) => maxargs = n,
7524 _ => {
7525 zwarnnam(
7526 name, // c:3573
7527 &format!("-M: invalid max number of arguments: {}", arg),
7528 );
7529 return 1; // c:3576
7530 }
7531 }
7532 if OPT_ISSET(ops, b's') && maxargs != 1 {
7533 // c:3578
7534 zwarnnam(
7535 name, // c:3579
7536 "-Ms: must take a single string argument",
7537 );
7538 return 1; // c:3580
7539 }
7540 }
7541 let modname = argv_iter.next().cloned(); // c:3584-3585
7542 if argv_iter.next().is_some() {
7543 // c:3586
7544 zwarnnam(name, "-M: too many arguments"); // c:3587
7545 return 1; // c:3588
7546 }
7547 // c:3591-3598 — alloc and populate mathfunc.
7548 let mut flags = MFF_USERFUNC; // c:3593
7549 if OPT_ISSET(ops, b's') {
7550 // c:3594
7551 flags |= MFF_STR; // c:3595
7552 }
7553 let new_fn = mathfunc {
7554 next: None, // c:3608 chain via Vec
7555 name: funcname.clone(), // c:3592
7556 flags, // c:3593
7557 nfunc: None,
7558 sfunc: None,
7559 module: modname, // c:3596
7560 minargs, // c:3597
7561 maxargs, // c:3598
7562 funcid: 0,
7563 };
7564 queue_signals(); // c:3600
7565 if let Ok(mut table) = MATHFUNCS.lock() {
7566 // c:3601-3606 — remove existing user entry with same name.
7567 if let Some(i) = table.iter().position(|p| p.name == new_fn.name) {
7568 table.remove(i); // c:3603
7569 }
7570 // c:3608-3609 — prepend to mathfuncs head.
7571 table.insert(0, new_fn);
7572 }
7573 unqueue_signals(); // c:3610
7574 return returnval;
7575 }
7576 }
7577
7578 // c:3616-3655 — `-X` re-autoload from inside a function.
7579 if OPT_MINUS(ops, b'X') {
7580 // c:3616
7581 if argv.len() > 1 {
7582 // c:3620
7583 zwarnnam(name, "-X: too many arguments"); // c:3621
7584 return 1; // c:3622
7585 }
7586 queue_signals(); // c:3624
7587 // c:3625-3633 — walk funcstack to find the enclosing FS_FUNC frame.
7588 let funcname: Option<String> = {
7589 let stack = FUNCSTACK.lock().map(|s| s.clone()).unwrap_or_default();
7590 stack
7591 .iter()
7592 .rev()
7593 .find(|fs| !fs.name.is_empty()) // c:3626
7594 .map(|fs| fs.name.clone()) // c:3631
7595 };
7596 let ret;
7597 if funcname.is_none() {
7598 // c:3635
7599 // c:3637 — `zerrnam(name, "bad autoload");`
7600 zwarnnam(name, "bad autoload"); // c:3637
7601 ret = 1; // c:3638
7602 } else {
7603 let fname = funcname.unwrap();
7604 // c:3640-3647 — getnode(shfunctab, funcname) || addnode(new shf).
7605 let mut shf_ptr = shfunctab_lock()
7606 .read()
7607 .map(|t| t.getnode(fname.as_str()))
7608 .unwrap_or(std::ptr::null_mut());
7609 if !shf_ptr.is_null() { // c:3640
7610 // exists already
7611 } else {
7612 // c:3645 — `shf = zshcalloc(sizeof *shf);`
7613 // `shfunctab->addnode(shfunctab, ztrdup(funcname), shf);`
7614 let new_shf = Box::into_raw(Box::new(shfunc {
7615 node: hashnode {
7616 next: None,
7617 nam: fname.clone(),
7618 flags: 0,
7619 },
7620 filename: None,
7621 lineno: 0,
7622 funcdef: None,
7623 redir: None,
7624 sticky: None,
7625 body: None,
7626 }));
7627 if let Ok(mut t) = shfunctab_lock().write() {
7628 t.addnode(new_shf); // c:3646
7629 }
7630 shf_ptr = new_shf;
7631 }
7632 if !argv.is_empty() {
7633 // c:3648
7634 if !shf_ptr.is_null() {
7635 let shf_mut = unsafe { &mut *shf_ptr };
7636 let mut old_slot = shf_mut.filename.take();
7637 dircache_set(&mut old_slot, None); // c:3649
7638 let mut new_slot: Option<String> = None;
7639 dircache_set(&mut new_slot, Some(&argv[0])); // c:3650
7640 shf_mut.filename = new_slot;
7641 on |= PM_LOADDIR; // c:3294 — `shf->node.flags |= PM_LOADDIR;`
7642 }
7643 }
7644 // c:3653 — `shf->node.flags = on;` — `on` carries
7645 // PM_UNDEFINED (set for -X at c:3352 / builtin.rs:6142).
7646 // This write was MISSING: eval_autoload's first gate is
7647 // `if (!(shf->node.flags & PM_UNDEFINED)) return 1;`
7648 // (c:3168), so zinit's NEW_AUTOLOAD=1 stubs —
7649 // functions[add-zsh-hook]="local -a fpath; fpath=(…);
7650 // builtin autoload -X -U -z"
7651 // — silently returned 1 and every wrapped plugin autoload
7652 // (add-zsh-hook, regexp-replace, add-zle-hook-widget)
7653 // reported `function definition file not found` at first
7654 // call in the interactive session.
7655 if !shf_ptr.is_null() {
7656 unsafe {
7657 (*shf_ptr).node.flags = on as i32; // c:3653
7658 }
7659 }
7660 // RUST-ONLY: drop the executor's compiled chunk for the
7661 // CURRENT (stub) definition. C's eval_autoload -X arm
7662 // replaces shf->funcdef with mkautofn (c:3180) so the
7663 // bin_eval re-call loads the real file; zshrs's dispatch
7664 // keeps a parallel functions_compiled chunk whose
7665 // presence skips the autoload prelude — re-dispatch ran
7666 // the STALE stub (zinit NEW_AUTOLOAD=1: `builtin
7667 // autoload -X` inside the stub) in an infinite mutual
7668 // recursion → stack overflow in the interactive session.
7669 let _ = crate::ported::exec::unregister_function(&fname);
7670 // c:3654 — `ret = eval_autoload(shf, funcname, ops, func);`
7671 ret = eval_autoload(shf_ptr, &fname, ops, _func); // c:3654
7672 }
7673 unqueue_signals(); // c:3656
7674 return ret;
7675 }
7676
7677 // c:3658-3669 — no-arg listing path: print all (non-DISABLED) shfuncs
7678 // matching `on|off` mask through scanshfunc + printnode.
7679 if argv.is_empty() {
7680 // c:3658
7681 queue_signals(); // c:3663
7682 if OPT_ISSET(ops, b'U') && !OPT_ISSET(ops, b'u') {
7683 // c:3664
7684 on &= !PM_UNDEFINED; // c:3665
7685 }
7686 // c:3666 — `scanshfunc(1, on|off, DISABLED, shfunctab->printnode,
7687 // pflags, expand);` — C's scanhashtable takes a
7688 // `flags1` filter mask: only entries where `(flags & flags1) ==
7689 // flags1` are emitted. Rust's `scanshfunc` doesn't accept the
7690 // filter, so apply it inside the closure. Without this gate
7691 // `functions -t` (no args) walked every function and printed
7692 // its body — should be empty / name-only restricted to
7693 // PM_TAGGED entries. Bug #468.
7694 let filter_mask = on | off;
7695 scanshfunc(|_nm, entry| {
7696 let f = entry.node.flags as u32;
7697 if filter_mask != 0 && (f & filter_mask) != filter_mask {
7698 return;
7699 }
7700 printshfuncexpand(entry, pflags, expand);
7701 });
7702 unqueue_signals(); // c:3668
7703 return returnval;
7704 }
7705
7706 // c:3672-3708 — `-m` glob: treat each arg as a pattern, scan-and-print
7707 // matching shfuncs (no on/off → list) or apply on/off mask.
7708 if OPT_ISSET(ops, b'm') {
7709 // c:3673
7710 on &= !PM_UNDEFINED; // c:3674
7711 let mut returnval = returnval;
7712 for pat in argv {
7713 // c:3675
7714 queue_signals(); // c:3676
7715 // c:3678 — `tokenize(*argv)` + `patcompile(...)`
7716 let pprog = patcompile(
7717 &{
7718 let mut __pat_tok = (pat).to_string();
7719 crate::ported::glob::tokenize(&mut __pat_tok);
7720 __pat_tok
7721 }, // c:3680
7722 PAT_HEAPDUP,
7723 None,
7724 );
7725 if let Some(prog) = pprog {
7726 // c:3680-3683 — scan-and-print matching shfuncs.
7727 if (on | off) == 0 && !OPT_ISSET(ops, b'X') {
7728 // c:3682
7729 // c:3682-3683 — `scanmatchshfunc(pprog, 1, 0,
7730 // DISABLED, shfunctab->printnode, pflags, expand)`.
7731 // Walk shfunctab via the hashtable.rs port and emit
7732 // each match through `printshfuncexpand` so autoload
7733 // stubs come out as `name () { # undefined; builtin
7734 // autoload -XU }` and loaded funcs print their body.
7735 scanmatchshfunc(Some(pat), |_nm, entry| {
7736 printshfuncexpand(entry, pflags, expand);
7737 });
7738 } else {
7739 // c:3686-3699 — walk shfunctab, apply (on, off) and
7740 // re-eval autoload for each matching shf.
7741 let names: Vec<String> = shfunctab_lock()
7742 .read()
7743 .map(|t| t.iter().map(|(k, _)| k.clone()).collect())
7744 .unwrap_or_default();
7745 for nm in &names {
7746 // pattry approximated by string equality / glob
7747 // here; full pat engine is in src/ported/pattern.rs.
7748 if !pattry(&prog, nm) {
7749 // c:3690
7750 continue;
7751 }
7752 let shf_ptr = shfunctab_lock()
7753 .read()
7754 .map(|t| t.getnode(nm.as_str()))
7755 .unwrap_or(std::ptr::null_mut());
7756 if shf_ptr.is_null() {
7757 continue;
7758 }
7759 let shf_mut = unsafe { &mut *shf_ptr };
7760 // c:3691 — `shf->node.flags = (... | (on & ~PM_UNDEFINED)) & ~off;`
7761 shf_mut.node.flags =
7762 (shf_mut.node.flags | ((on & !PM_UNDEFINED) as i32)) & !(off as i32); // c:3691
7763 if check_autoload(shf_ptr, &shf_mut.node.nam, ops, _func) != 0 {
7764 // c:3693
7765 returnval = 1; // c:3695
7766 }
7767 }
7768 }
7769 } else {
7770 // c:3700-3702 — `untokenize + zwarnnam(name, "bad pattern")`.
7771 zwarnnam(name, &format!("bad pattern : {}", pat)); // c:3701
7772 returnval = 1; // c:3702
7773 }
7774 unqueue_signals(); // c:3704
7775 }
7776 return returnval;
7777 }
7778
7779 // c:3710-3735 — literal name list, no globbing.
7780 let mut returnval = returnval;
7781 queue_signals(); // c:3711
7782 for fname in argv {
7783 // c:3712
7784 // c:3713-3714 — `-w` (compile-and-dump) path.
7785 if OPT_ISSET(ops, b'w') {
7786 // c:3713
7787 // dump_autoload(name, fname, on, ops, func) — dump.c port.
7788 continue;
7789 }
7790 // c:3715 — `shf = shfunctab->getnode(shfunctab, *argv);`
7791 let shf_ptr = shfunctab_lock()
7792 .read()
7793 .map(|t| t.getnode(fname.as_str()))
7794 .unwrap_or(std::ptr::null_mut());
7795 if !shf_ptr.is_null() {
7796 // c:3715
7797 let shf_mut = unsafe { &mut *shf_ptr };
7798 if (on | off) != 0 {
7799 // c:3717
7800 // c:3719 — apply on/off mask, then check_autoload.
7801 shf_mut.node.flags =
7802 (shf_mut.node.flags | ((on & !PM_UNDEFINED) as i32)) & !(off as i32); // c:3719
7803 if check_autoload(shf_ptr, &shf_mut.node.nam, ops, _func) != 0 {
7804 // c:3720
7805 returnval = 1; // c:3721
7806 }
7807 } else {
7808 // c:3723 — `printshfuncexpand(&shf->node, pflags, expand);`
7809 // C prints the function via shfunctab.printnode honoring
7810 // pflags (PRINT_NAMEONLY / verbose). The previous Rust
7811 // port just printed the name — `functions f` skipped
7812 // the `f () { ... body ... }` body listing entirely.
7813 printshfuncexpand(shf_mut, pflags, expand); // c:3723
7814 }
7815 } else if (on & PM_UNDEFINED) != 0 {
7816 // c:3725
7817 // c:3726-3782 — autoload-define path: TRAP* + abs-path + new shf.
7818 let mut sigidx: i32 = -1;
7819 let mut ok = true;
7820 // c:3728-3735 — TRAP* prefix → removetrapnode(sigidx).
7821 if fname.starts_with("TRAP") {
7822 // c:3728
7823 // c:3729 — `if ((sigidx = getsigidx(*argv + 4)) != -1)`
7824 sigidx = getsigidx(&fname[4..]); // c:3729
7825 if sigidx != -1 {
7826 // c:3729
7827 // c:3733 — `removetrapnode(sigidx);`
7828 removetrapnode(sigidx); // c:3733
7829 }
7830 }
7831 // c:3737-3759 — absolute path /dir/base form: install dir on
7832 // existing matching base name with PM_UNDEFINED set.
7833 if fname.starts_with('/') {
7834 // c:3737
7835 let base = fname.rsplit('/').next().unwrap_or("");
7836 if !base.is_empty() {
7837 let base_ptr = shfunctab_lock()
7838 .read()
7839 .map(|t| t.getnode(base))
7840 .unwrap_or(std::ptr::null_mut());
7841 if !base_ptr.is_null() {
7842 let bs = unsafe { &mut *base_ptr };
7843 // c:3742 — apply flag mask.
7844 bs.node.flags =
7845 (bs.node.flags | ((on & !PM_UNDEFINED) as i32)) & !(off as i32); // c:3742
7846 if (bs.node.flags as u32 & PM_UNDEFINED) != 0 {
7847 // c:3744
7848 let dir = if fname.len() > 1 && base.len() == fname.len() - 1 {
7849 "/".to_string() // c:3747
7850 } else {
7851 fname[..fname.len() - base.len() - 1].to_string()
7852 // c:3749-3751
7853 };
7854 let mut old_slot = bs.filename.take();
7855 dircache_set(&mut old_slot, None); // c:3753
7856 let mut new_slot: Option<String> = None;
7857 dircache_set(&mut new_slot, Some(&dir)); // c:3754
7858 bs.filename = new_slot;
7859 }
7860 if check_autoload(base_ptr, &bs.node.nam, ops, _func) != 0 {
7861 // c:3756
7862 returnval = 1;
7863 }
7864 continue; // c:3758
7865 }
7866 }
7867 }
7868 // c:3763-3766 — new undefined shf, mkautofn, add_autoload_function.
7869 let new_shf = Box::new(shfunc {
7870 node: hashnode {
7871 next: None,
7872 nam: fname.clone(),
7873 flags: on as i32, // c:3764
7874 },
7875 filename: None,
7876 lineno: 0,
7877 funcdef: None,
7878 redir: None,
7879 sticky: None,
7880 body: None,
7881 });
7882 let new_shf_ptr = Box::into_raw(new_shf);
7883 let _ = mkautofn(new_shf_ptr); // c:3765
7884 add_autoload_function(new_shf_ptr, fname); // c:3767
7885 // PFA-SMR: an `autoload NAME` registers a function stub
7886 // that fires on first call. Record it as a `function`
7887 // kind so replay re-registers the autoload. C zsh has
7888 // no recorder hook here; emit per Rust-only schema.
7889 #[cfg(feature = "recorder")]
7890 if crate::recorder::is_enabled() {
7891 let ctx = crate::recorder::recorder_ctx_global();
7892 crate::recorder::emit_function(fname, None, ctx);
7893 }
7894 if sigidx != -1 {
7895 // c:3769
7896 // c:3770 — `if (settrap(sigidx, NULL, ZSIG_FUNC)) { ... }`
7897 if settrap(sigidx, None, ZSIG_FUNC) != 0 {
7898 // c:3770
7899 // c:3771 — `shfunctab->removenode(shfunctab, *argv);`
7900 if let Ok(mut t) = shfunctab_lock().write() {
7901 t.remove(fname);
7902 }
7903 // c:3772 — `shfunctab->freenode(&shf->node);` Drop covers it.
7904 returnval = 1; // c:3773
7905 ok = false; // c:3774
7906 }
7907 }
7908 if ok && check_autoload(new_shf_ptr, &fname, ops, _func) != 0 {
7909 // c:3779
7910 returnval = 1; // c:3780
7911 }
7912 } else {
7913 // c:3783 — `returnval = 1;` (named function not found,
7914 // no autoload requested).
7915 returnval = 1; // c:3783
7916 }
7917 }
7918 unqueue_signals(); // c:3785
7919 let _ = (expand, pflags);
7920 returnval
7921}
7922
7923/// Port of `mkautofn(Shfunc shf)` from Src/builtin.c:3790.
7924/// C: `Eprog mkautofn(Shfunc shf)` — synthesize a 5-wordcode body that
7925/// re-fires the autoload mechanism when first called.
7926pub fn mkautofn(shf: *mut shfunc) -> *mut eprog {
7927 // c:3790
7928 // c:3793-3810 — alloc Eprog with 5 wordcode slots, set p->shf, p->npats=0,
7929 // p->nref=1 (permanent). Static-link path: synthesize a Box<eprog> that
7930 // satisfies the autoload trampoline contract.
7931 let p = Box::new(eprog {
7932 len: 5 * size_of::<u32>() as i32, // c:3796
7933 prog: Vec::new(), // c:3797
7934 strs: None, // c:3798
7935 shf: if shf.is_null() {
7936 None
7937 }
7938 // c:3799
7939 else {
7940 Some(unsafe { Box::from_raw(shf) })
7941 },
7942 npats: 0, // c:3800
7943 nref: 1, // c:3801
7944 flags: 0,
7945 pats: Vec::new(),
7946 dump: None,
7947 strs_metafied: false, // native pool — clean UTF-8
7948 });
7949 Box::into_raw(p)
7950}
7951
7952/// Port of `bin_unset(char *name, char **argv, Options ops, int func)` from Src/builtin.c:3818.
7953/// C: `int bin_unset(char *name, char **argv, Options ops, int func)` —
7954/// `-f` delegates to `bin_unhash`; `-m` glob deletes matching params;
7955/// default literal-name unset with subscript handling.
7956/// WARNING: param names don't match C — Rust=(name, argv, func) vs C=(name, argv, ops, func)
7957pub fn bin_unset(
7958 name: &str,
7959 argv: &[String], // c:3818
7960 ops: &options,
7961 func: i32,
7962) -> i32 {
7963 let mut returnval = 0i32; // c:3823
7964 let mut match_count = 0i32; // c:3823
7965
7966 // PFA-SMR aspect: emit unset events for each named param. The
7967 // recorder tracks state-mutations across the shell session for
7968 // the zshrs-recorder binary's replay/inspect tooling.
7969 #[cfg(feature = "recorder")]
7970 if crate::recorder::is_enabled() {
7971 let ctx = crate::recorder::recorder_ctx_global();
7972 for a in argv {
7973 if a.starts_with('-') || a == "--" {
7974 continue;
7975 }
7976 crate::recorder::emit_unset(a, ctx.clone());
7977 }
7978 }
7979
7980 // c:3826 — `if (OPT_ISSET(ops,'f')) return bin_unhash(name, argv, ops, func);`
7981 if OPT_ISSET(ops, b'f') {
7982 // c:3826
7983 return bin_unhash(name, argv, ops, func); // c:3827
7984 }
7985
7986 // c:3830-3862 — `-m` glob.
7987 if OPT_ISSET(ops, b'm') {
7988 // c:3831
7989 for s in argv {
7990 // c:3832
7991 queue_signals(); // c:3833
7992 let pprog = patcompile(
7993 &{
7994 let mut __pat_tok = (s).to_string();
7995 crate::ported::glob::tokenize(&mut __pat_tok);
7996 __pat_tok
7997 }, // c:3836
7998 PAT_HEAPDUP,
7999 None,
8000 );
8001 if let Some(prog) = pprog {
8002 // c:3838-3851 — walk paramtab (NOT env::vars), unset via
8003 // unsetparam (which respects PM_NAMEREF + readonly guards).
8004 //
8005 // The previous Rust port walked `std::env::vars()` — the
8006 // OS environment. This is a different name set:
8007 // - Shell-internal vars (not exported) would survive
8008 // `unset -m 'PATTERN'` even though they match.
8009 // - Env vars not in paramtab would be removed without
8010 // the PM_READONLY guard in unsetparam_pm.
8011 //
8012 // Same family of bug as the env::var vs paramtab fixes
8013 // earlier in the series.
8014 let names: Vec<String> = {
8015 let tab = paramtab().read().unwrap();
8016 tab.keys().cloned().collect()
8017 };
8018 for nm in &names {
8019 if pattry(&prog, nm) {
8020 // c:3842
8021 unsetparam(nm); // c:3847 (with guards)
8022 match_count += 1; // c:3848
8023 }
8024 }
8025 } else {
8026 zwarnnam(name, &format!("bad pattern : {}", s)); // c:3854
8027 returnval = 1; // c:3855
8028 }
8029 unqueue_signals(); // c:3857
8030 }
8031 if match_count == 0 {
8032 // c:3861
8033 returnval = 1; // c:3862
8034 }
8035 return returnval; // c:3863
8036 }
8037
8038 // c:3866-3915 — literal-name unset with optional subscript.
8039 queue_signals(); // c:3867
8040 for s in argv {
8041 // c:3868
8042 // c:3869-3878 — extract `name[subscript]` shape.
8043 let (nm, subscript) = match s.find('[') {
8044 // c:3869
8045 Some(start) if s.ends_with(']') => {
8046 // c:3873
8047 (&s[..start], Some(&s[start + 1..s.len() - 1])) // c:3875
8048 }
8049 Some(_) => {
8050 // c:3879-3884 — bracket without `]` close → invalid.
8051 zwarnnam(name, &format!("{}: invalid parameter name", s)); // c:3882
8052 returnval = 1; // c:3883
8053 continue; // c:3884
8054 }
8055 None => (s.as_str(), None),
8056 };
8057 // c:3878 — `if (... || !isident(s))` invalid identifier check.
8058 if nm.is_empty()
8059 || !nm
8060 .chars()
8061 .next()
8062 .map_or(false, |c| c.is_alphabetic() || c == '_')
8063 || !nm.chars().all(|c| c.is_alphanumeric() || c == '_')
8064 {
8065 zwarnnam(name, &format!("{}: invalid parameter name", s)); // c:3882
8066 returnval = 1; // c:3883
8067 continue;
8068 }
8069 // c:3886-3905 — `if (!pm) continue;` then unset.
8070 // C `unsetparam_pm` dispatches on `pm->gsu` (the gsu_*
8071 // accessor for the param's type): assoc gets
8072 // `gsu_a->unset(pm, subscript)`, array gets
8073 // `gsu_arr->unset(pm, subscript)`, scalar gets `unsetparam`.
8074 match subscript {
8075 // c:3886
8076 Some(key) => {
8077 // c:Src/Modules/mapfile.c:126 unsetpmmapfile — unsetting
8078 // an element of the special $mapfile assoc UNLINKS the
8079 // file named by the key. It is not a regular assoc, so
8080 // route here before the generic assoc/array delete.
8081 if nm == "mapfile" {
8082 crate::ported::modules::mapfile::unsetpmmapfile(key, false);
8083 continue;
8084 }
8085 // c:3893 assoc subscript: `m[key]` delete.
8086 if let Some(mut map) = crate::ported::exec::assoc(nm) {
8087 map.shift_remove(key); // c:3893
8088 crate::ported::exec::set_assoc(nm, map);
8089 } else if let Some(mut arr) = crate::ported::exec::array(nm) {
8090 // c:3895 — array subscript: `arr[N]` sets element
8091 // N to empty; `arr[N,M]` clears the inclusive
8092 // range. The previous Rust port only handled the
8093 // single-index form; `unset arr[2,3]` was a no-op.
8094 let len_i = arr.len() as i32;
8095 let resolve = |raw: i32| -> Option<usize> {
8096 let pos = if raw < 0 { len_i + raw + 1 } else { raw };
8097 if pos >= 1 && pos as usize <= arr.len() {
8098 Some((pos - 1) as usize)
8099 } else {
8100 None
8101 }
8102 };
8103 if let Some((s, e)) = key.split_once(',') {
8104 // c:Src/params.c — `unset arr[N,M]` replaces
8105 // the N..M range with ONE empty element. For
8106 // (a b c d), `unset arr[2,3]` → (a "" d).
8107 // Negative bounds aren't supported by zsh's
8108 // unset (it leaves the array unchanged), so
8109 // restrict to positive forms.
8110 let s_i = s.trim().parse::<i32>().ok();
8111 let e_i = e.trim().parse::<i32>().ok();
8112 if let (Some(start), Some(end)) = (s_i, e_i) {
8113 if start >= 1 && end >= 1 {
8114 let from = (start - 1) as usize;
8115 let to = (end - 1) as usize;
8116 if from < arr.len() && from <= to {
8117 let cap_to = to.min(arr.len() - 1);
8118 let mut new_arr: Vec<String> = Vec::with_capacity(arr.len());
8119 new_arr.extend(arr[..from].iter().cloned());
8120 new_arr.push(String::new());
8121 new_arr.extend(arr[cap_to + 1..].iter().cloned());
8122 crate::ported::exec::set_array(nm, new_arr);
8123 }
8124 }
8125 }
8126 } else if let Ok(i) = key.parse::<i32>() {
8127 // c:Src/params.c — single-index unset only
8128 // accepts positive subscripts and the special
8129 // -1 form (clear last element). Other negative
8130 // values are a no-op in zsh — verified
8131 // empirically: `unset arr[-2]`, `arr[-3]` etc.
8132 // leave the array unchanged.
8133 // `unset arr[0]` is invalid (unless
8134 // KSHZEROSUBSCRIPT) and emits a diagnostic.
8135 if i == 0 {
8136 zwarn(&format!("{}: assignment to invalid subscript range", nm));
8137 returnval = 1;
8138 } else if i == -1 {
8139 if !arr.is_empty() {
8140 let idx = arr.len() - 1;
8141 arr[idx] = String::new();
8142 crate::ported::exec::set_array(nm, arr);
8143 }
8144 } else if i > 0 {
8145 if let Some(idx) = resolve(i) {
8146 arr[idx] = String::new();
8147 crate::ported::exec::set_array(nm, arr);
8148 }
8149 }
8150 // Other negative values: no-op (zsh behavior).
8151 }
8152 // c:Src/params.c:2922 — a subscript unset of a tied
8153 // colon-array (`unset path[2]`) re-derives the scalar
8154 // side, just like the assignment paths in params.rs.
8155 if let Some((_, scalar)) = crate::ported::params::TIED_COLON_ARRAYS
8156 .iter()
8157 .find(|(a, _)| *a == nm)
8158 {
8159 let cur = crate::ported::exec::array(nm).unwrap_or_default();
8160 crate::ported::params::assignsparam(scalar, &cur.join(":"), 0);
8161 }
8162 }
8163 }
8164 None => {
8165 // c:3939-3951 — nameref handling. Without -n the ref
8166 // resolves and the TARGET is unset; refs bound to an
8167 // outer scope are just marked unset (kept in table,
8168 // PM_DECLARED). With -n the ref itself is removed.
8169 let mut nm: &str = nm;
8170 let mut resolved_target: Option<String> = None; // non-ref target to unset
8171 let mut ref_removal: Option<String> = None; // ref entry to remove
8172 let mut handled = false;
8173 if crate::ported::params::is_nameref(nm) {
8174 if OPT_ISSET(ops, b'n') {
8175 ref_removal = Some(nm.to_string()); // unset -n: the ref itself
8176 } else {
8177 use crate::ported::params::nameref_resolution;
8178 match crate::ported::params::resolve_nameref_name(nm, None) {
8179 // c:3942-3943 — `if (!(pm = resolve_nameref(pm))) continue;`
8180 nameref_resolution::SelfRef | nameref_resolution::OutOfScope => {
8181 continue
8182 }
8183 nameref_resolution::Placeholder(last) => {
8184 // chain ends at a ref → that ref is the
8185 // unset object (resolve_nameref returns it).
8186 ref_removal = Some(last);
8187 }
8188 nameref_resolution::Target {
8189 name: t,
8190 subscript,
8191 pm,
8192 level,
8193 } => {
8194 if subscript.is_some() {
8195 // c:3897 — subscripted ref: resolve_nameref
8196 // early-exits (pm->width); the REF itself
8197 // is the unset object.
8198 ref_removal = Some(nm.to_string());
8199 } else if pm.is_none() {
8200 // dangling — nothing to unset (c:3942).
8201 continue;
8202 } else {
8203 let cur_ll = locallevel.load(Relaxed) as i32;
8204 let ro = pm
8205 .as_ref()
8206 .map(|p| (p.node.flags as u32 & PM_READONLY) != 0)
8207 .unwrap_or(false);
8208 if level < cur_ll && !ro {
8209 // c:3944-3949 — mark unset, keep in
8210 // table (stdunsetfn + PM_DECLARED).
8211 if let Ok(mut tab) = paramtab().write() {
8212 if let Some(p) = tab.get_mut(&t) {
8213 p.node.flags |= (PM_UNSET | PM_DECLARED) as i32;
8214 p.u_str = None;
8215 p.u_arr = None;
8216 p.u_val = 0;
8217 }
8218 }
8219 let _ = crate::ported::params::paramtab_hashed_storage()
8220 .lock()
8221 .ok()
8222 .as_deref_mut()
8223 .map(|m| m.remove(&t));
8224 handled = true;
8225 } else {
8226 resolved_target = Some(t);
8227 }
8228 }
8229 }
8230 nameref_resolution::NotRef => {}
8231 }
8232 }
8233 if handled {
8234 continue;
8235 }
8236 if let Some(refnam) = ref_removal {
8237 // unsetparam() deliberately skips namerefs, so
8238 // do the table surgery here with the readonly
8239 // guard (c:3850 via unsetparam_pm).
8240 let ro = paramtab()
8241 .read()
8242 .ok()
8243 .and_then(|t| {
8244 t.get(&refnam)
8245 .map(|p| (p.node.flags as u32 & PM_READONLY) != 0)
8246 })
8247 .unwrap_or(false);
8248 if ro {
8249 zerr(&format!("read-only variable: {}", refnam)); // c:3850
8250 returnval = 1;
8251 continue;
8252 }
8253 // c:3911-3913 — a LOCAL ref is kept in the
8254 // table marked PM_UNSET (scope-end removes it);
8255 // only globals/level-0 refs drop out entirely.
8256 let cur_ll = locallevel.load(Relaxed) as i32;
8257 let keep_local = paramtab()
8258 .read()
8259 .ok()
8260 .and_then(|t| t.get(&refnam).map(|p| p.level > 0 && cur_ll >= p.level))
8261 .unwrap_or(false);
8262 if keep_local {
8263 if let Ok(mut tab) = paramtab().write() {
8264 if let Some(p) = tab.get_mut(&refnam) {
8265 // c:3858 + c:3870 stdunsetfn
8266 p.node.flags &= !(PM_DECLARED as i32);
8267 p.node.flags |= PM_UNSET as i32;
8268 p.u_str = None;
8269 p.base = 0;
8270 p.width = 0;
8271 }
8272 }
8273 } else if let Some(mut pm) =
8274 paramtab().write().ok().and_then(|mut t| t.remove(&refnam))
8275 {
8276 if let Some(prev) = pm.old.take() {
8277 if let Ok(mut tab) = paramtab().write() {
8278 tab.insert(refnam.clone(), prev);
8279 }
8280 }
8281 }
8282 continue;
8283 }
8284 }
8285 let target_buf;
8286 if let Some(t) = resolved_target {
8287 target_buf = t;
8288 nm = &target_buf;
8289 }
8290 // c:3900-3905 — whole-param unset.
8291 // Route through `unsetparam` (params.rs) so the
8292 // canonical readonly-guard + pm.old uncover restore
8293 // fires. Without this, `local x=inner; unset x`
8294 // would erase the OUTER binding too (the local pm's
8295 // pm.old chain dropped on the floor).
8296 //
8297 // Clear the parallel shadow storage that lives in
8298 // ShellExecutor (paramtab_hashed_storage for assoc,
8299 // and the per-executor arrays/assocs maps). These
8300 // are NOT touched by params.rs::unsetparam so we
8301 // wipe them directly here; using crate::ported::exec::unset_*
8302 // would loop back into unsetparam.
8303 // c:Src/builtin.c:3952-3953 — `if (unsetparam_pm(pm,
8304 // 0, 1)) returnval = 1;` (readonly rejection). On
8305 // rejection the param is untouched, so the shadow-
8306 // storage wipe and env delenv (which unsetparam_pm
8307 // only runs on its success path, c:Src/params.c:3872)
8308 // must not fire either.
8309 if unsetparam(nm) != 0 {
8310 returnval = 1; // c:3953
8311 } else {
8312 let _ = crate::ported::params::paramtab_hashed_storage()
8313 .lock()
8314 .ok()
8315 .as_deref_mut()
8316 .map(|m| m.remove(nm));
8317 env::remove_var(nm); // c:3905 delenv
8318 }
8319 }
8320 }
8321 }
8322 unqueue_signals(); // c:3914
8323 returnval // c:3915
8324}
8325
8326/// Port of `fetchcmdnamnode(HashNode hn, UNUSED(int printflags))` from Src/builtin.c:3967.
8327/// C: `static void fetchcmdnamnode(HashNode hn, UNUSED(int printflags))` →
8328/// `addlinknode(matchednodes, cn->node.nam);`
8329/// C body (2 lines):
8330/// `Cmdnam cn = (Cmdnam) hn;
8331/// addlinknode(matchednodes, cn->node.nam);`
8332/// (C source does not null-check hn — callers guarantee non-null.)
8333/// WARNING: param names don't match C — Rust=(hn) vs C=(hn, printflags)
8334pub fn fetchcmdnamnode(
8335 hn: *mut hashnode, // c:3967
8336 _printflags: i32,
8337) {
8338 let nam = unsafe { (*hn).nam.clone() }; // c:3969 cast + read
8339 if let Ok(mut m) = MATCHEDNODES.lock() {
8340 m.push(nam);
8341 } // c:3971
8342}
8343
8344/// Port of `bin_whence(char *nam, char **argv, Options ops, int func)` from Src/builtin.c:3975.
8345/// C: `int bin_whence(char *nam, char **argv, Options ops, int func)`.
8346///
8347/// `whence`/`type`/`which`/`where`/`command` dispatcher. `-c` csh,
8348/// `-v` verbose, `-a` all-matches, `-w` word-form, `-x` indent
8349/// override, `-m` glob-args, `-p` path-only, `-f` print funcdef,
8350/// `-s/-S` follow symlink. The C body walks alias/reswd/shfunc/
8351/// builtin/cmdnam tabs in order; this port preserves the structure
8352/// and dispatch logic, deferring the per-tab scanmatch walks to the
8353/// existing tab accessors.
8354/// WARNING: param names don't match C — Rust=(nam, argv, func) vs C=(nam, argv, ops, func)
8355pub fn bin_whence(
8356 nam: &str,
8357 argv: &[String], // c:3975
8358 ops: &options,
8359 func: i32,
8360) -> i32 {
8361 let mut returnval: i32 = 0;
8362 let mut printflags: i32 = 0;
8363 let mut informed: i32 = 0;
8364 let mut expand: i32 = 0;
8365
8366 // c:3989-3993 — flags.
8367 let csh = OPT_ISSET(ops, b'c'); // c:3989
8368 let v = OPT_ISSET(ops, b'v'); // c:3990
8369 let all = OPT_ISSET(ops, b'a'); // c:3991
8370 let wd = OPT_ISSET(ops, b'w'); // c:3992
8371
8372 // c:3995-4002 — `-x N` indent override.
8373 if OPT_ISSET(ops, b'x') {
8374 // c:3995
8375 let arg = OPT_ARG(ops, b'x').unwrap_or("");
8376 match arg.trim().parse::<i32>() {
8377 // c:3997
8378 Ok(n) => {
8379 expand = n;
8380 if expand == 0 {
8381 expand = -1;
8382 } // c:4001
8383 }
8384 Err(_) => {
8385 zwarnnam(nam, "number expected after -x"); // c:3998
8386 return 1;
8387 }
8388 }
8389 }
8390
8391 // c:4004-4012 — printflags from -w/-c/-v/(default simple)/-f.
8392 if OPT_ISSET(ops, b'w') {
8393 printflags |= PRINT_WHENCE_WORD;
8394 }
8395 // c:4004
8396 else if OPT_ISSET(ops, b'c') {
8397 printflags |= PRINT_WHENCE_CSH;
8398 }
8399 // c:4006
8400 else if OPT_ISSET(ops, b'v') {
8401 printflags |= PRINT_WHENCE_VERBOSE;
8402 }
8403 // c:4008
8404 else {
8405 printflags |= PRINT_WHENCE_SIMPLE;
8406 } // c:4010
8407 if OPT_ISSET(ops, b'f') {
8408 printflags |= PRINT_WHENCE_FUNCDEF;
8409 } // c:4012
8410
8411 // c:4015-4024 — BIN_COMMAND -V or -V-equivalent flag wrangling.
8412 // C body:
8413 // if (func == BIN_COMMAND)
8414 // if (OPT_ISSET(ops,'V')) { printflags = aliasflags = PRINT_WHENCE_VERBOSE; v = 1; }
8415 // else { aliasflags = PRINT_LIST; printflags = PRINT_WHENCE_SIMPLE; v = 0; }
8416 // else aliasflags = printflags;
8417 // Previous Rust port omitted the `v = 0` reset in the non-V
8418 // command branch, so `command foo` with a stray user -v leaked
8419 // verbose mode. Mirror C: force v unconditionally under
8420 // BIN_COMMAND.
8421 let mut v = v;
8422 let aliasflags = if func == BIN_COMMAND {
8423 // c:4015
8424 if OPT_ISSET(ops, b'V') {
8425 // c:4016
8426 printflags = PRINT_WHENCE_VERBOSE; // c:4017
8427 v = true; // c:4018
8428 PRINT_WHENCE_VERBOSE
8429 } else {
8430 printflags = PRINT_WHENCE_SIMPLE; // c:4021
8431 v = false; // c:4022
8432 PRINT_LIST // c:4020
8433 }
8434 } else {
8435 printflags // c:4024
8436 };
8437
8438 // c:4026-4119 — `-m` glob branch: each arg is a pattern; walk every
8439 // hashtab in turn (alias/reswd/shfunc/builtin/cmdnam) and emit a
8440 // print row per matching node. C uses scanmatchtable + a per-tab
8441 // print callback; the Rust port iterates each tab's accessor and
8442 // emits the print directly.
8443 if OPT_ISSET(ops, b'm') {
8444 // c:4028 — `cmdnamtab->filltable(cmdnamtab);` populates every
8445 // $PATH entry into cmdnamtab so the c:4070 scan below sees
8446 // every executable (not just hashed ones). C calls this once
8447 // per `-m` invocation; Rust mirrors with a single fillcmdnamtable
8448 // against the shell-side $PATH array.
8449 if let Some(path) = getsparam("PATH") {
8450 let path_arr: Vec<String> = path.split(':').map(|s| s.to_string()).collect();
8451 fillcmdnamtable(&path_arr);
8452 }
8453 // c:4030-4033 — `if (all) { pushheap(); matchednodes = newlinklist(); }`.
8454 // MATCHEDNODES is the Rust analog of `matchednodes`; pushheap
8455 // is a Rust no-op (no heap allocator).
8456 if all {
8457 // c:4030
8458 if let Ok(mut m) = MATCHEDNODES.lock() {
8459 m.clear();
8460 }
8461 }
8462 queue_signals(); // c:4034
8463 for pat in argv {
8464 // c:4035
8465 // c:4037 — `tokenize(*argv);` (Rust patcompile handles the
8466 // tokenize step internally; explicit call is a no-op here).
8467 let pprog = patcompile(
8468 &{
8469 let mut __pat_tok = (pat).to_string();
8470 crate::ported::glob::tokenize(&mut __pat_tok);
8471 __pat_tok
8472 }, // c:4038
8473 PAT_HEAPDUP,
8474 None,
8475 );
8476 match pprog {
8477 None => {
8478 // c:4039
8479 zwarnnam(nam, &format!("bad pattern : {}", pat)); // c:4040
8480 returnval = 1; // c:4041
8481 continue;
8482 }
8483 Some(prog) => {
8484 if !OPT_ISSET(ops, b'p') {
8485 // c:4044 — !`-p` path-only.
8486 // c:4049-4051 — `scanmatchtable(aliastab, pprog,
8487 // 1, 0, DISABLED, aliastab->printnode, printflags);`.
8488 // Route through the canonical printnode callback.
8489 let alias_matches: Vec<alias> = aliastab_lock()
8490 .read()
8491 .map(|t| {
8492 t.iter()
8493 .filter(|(n, _)| pattry(&prog, n))
8494 .map(|(_, a)| a.clone())
8495 .collect()
8496 })
8497 .unwrap_or_default();
8498 for a in &alias_matches {
8499 printaliasnode(a, printflags); // c:4051
8500 informed += 1; // c:4049
8501 }
8502 // c:4054-4056 — `scanmatchtable(reswdtab, pprog,
8503 // 1, 0, DISABLED, reswdtab->printnode, printflags);`.
8504 // reswdtab->printnode is `printreswdnode` at
8505 // Src/hashtable.c:1259 — its body is just
8506 // `zputs(hn->nam); putchar('\n')`. Inline the
8507 // print since no separate Rust callback yet
8508 // exists and the body is trivial.
8509 // Reserved words: collect matches + sort to
8510 // match zsh's scanmatchtable order (same logic
8511 // as the builtin walk below).
8512 let mut names: Vec<String> = reswdtab_lock()
8513 .read()
8514 .map(|t| {
8515 t.iter()
8516 .filter(|(k, _)| pattry(&prog, k))
8517 .map(|(k, _)| k.clone())
8518 .collect()
8519 })
8520 .unwrap_or_default();
8521 names.sort();
8522 for w in &names {
8523 println!("{}", w); // c:1259 zputs + newline
8524 informed += 1; // c:4054
8525 }
8526 // c:4059-4061 — `scanmatchshfunc(pprog, 1, 0,
8527 // DISABLED, shfunctab->printnode, printflags,
8528 // expand);`. Route through canonical
8529 // printshfuncexpand with `expand`.
8530 let func_matches: Vec<shfunc> = shfunctab_lock()
8531 .read()
8532 .map(|t| {
8533 t.iter()
8534 .filter(|(n, _)| pattry(&prog, n))
8535 .map(|(_, f)| f.clone())
8536 .collect()
8537 })
8538 .unwrap_or_default();
8539 for f in &func_matches {
8540 printshfuncexpand(f, printflags, expand); // c:4061
8541 informed += 1; // c:4059
8542 }
8543 // c:4064-4066 — `scanmatchtable(builtintab, pprog,
8544 // 1, 0, DISABLED, builtintab->printnode,
8545 // printflags);`. C's scanmatchtable walks the
8546 // hashtable in a deterministic order that, for
8547 // the matched-against name set, happens to be
8548 // sorted alphabetically — that's what makes
8549 // `whence -m "echo*"` emit echo, echotc,
8550 // echoti. zshrs's BUILTINS Vec is in
8551 // declaration order (echo, echoti, echotc) so
8552 // the unsorted walk produced a different
8553 // ordering. Collect+sort to match.
8554 let mut bn_matches: Vec<&builtin> = BUILTINS
8555 .iter()
8556 .chain(crate::extensions::ext_builtins::extension_builtin_defs())
8557 .filter(|b| pattry(&prog, &b.node.nam))
8558 .collect();
8559 bn_matches.sort_by(|a, b| a.node.nam.cmp(&b.node.nam));
8560 for b in bn_matches {
8561 printbuiltinnode(
8562 &b.node as *const hashnode as *mut hashnode,
8563 printflags,
8564 ); // c:4066
8565 informed += 1; // c:4064
8566 }
8567 }
8568 // c:4070-4073 — `scanmatchtable(cmdnamtab, pprog,
8569 // 1, 0, 0, (all ? fetchcmdnamnode :
8570 // cmdnamtab->printnode), printflags);`. After
8571 // fillcmdnamtable above, cmdnamtab has every
8572 // PATH-resident command name. Walk the canonical
8573 // table (not std::fs::read_dir) so HASHED/non-
8574 // HASHED distinction is preserved.
8575 // External cmd-name matches: sort by full path
8576 // (the same printed token) to match C zsh's
8577 // scanmatchtable iteration order. The hashtable
8578 // walk in zsh on /bin/echo + /opt/homebrew/bin/
8579 // ecpg yields /bin/echo first because `/bin` <
8580 // `/opt` lexicographically. zshrs's HashMap walks
8581 // in arbitrary order without an explicit sort.
8582 let mut cmd_matches: Vec<(String, cmdnam)> = cmdnamtab_lock()
8583 .read()
8584 .map(|t| {
8585 t.iter()
8586 .filter(|(n, _)| pattry(&prog, n))
8587 .map(|(n, c)| (n.clone(), c.clone()))
8588 .collect()
8589 })
8590 .unwrap_or_default();
8591 // printcmdnamnode emits c.value.string (the full
8592 // path) so sort on that to match the printed
8593 // order, not the bare basename `n`.
8594 // printcmdnamnode emits one of:
8595 // - HASHED entry: cmd (the full path)
8596 // - non-HASHED entry: name[0] + "/" + nam
8597 // Sort by the same string the print path will
8598 // emit so the output order matches zsh's
8599 // scanmatchtable walk.
8600 let printed_path = |c: &cmdnam| -> String {
8601 if (c.node.flags & HASHED as i32) != 0 {
8602 c.cmd.clone().unwrap_or_default()
8603 } else {
8604 let dir = c
8605 .name
8606 .as_ref()
8607 .and_then(|v| v.first())
8608 .cloned()
8609 .unwrap_or_default();
8610 format!("{}/{}", dir, c.node.nam)
8611 }
8612 };
8613 cmd_matches.sort_by(|a, b| printed_path(&a.1).cmp(&printed_path(&b.1)));
8614 for (n, c) in &cmd_matches {
8615 if all {
8616 // c:4072 fetchcmdnamnode — accumulates
8617 // matching node names into matchednodes.
8618 if let Ok(mut m) = MATCHEDNODES.lock() {
8619 m.push(n.clone());
8620 }
8621 } else {
8622 // c:4072 cmdnamtab->printnode — emits per
8623 // PRINT_WHENCE_WORD/CSH/VERBOSE branches.
8624 printcmdnamnode(c, printflags);
8625 }
8626 informed += 1; // c:4070
8627 }
8628 }
8629 }
8630 run_queued_signals(); // c:4079
8631 }
8632 unqueue_signals(); // c:4081
8633 if !all {
8634 // c:4082-4084 — `return returnval || !informed;` (early-out
8635 // when not in `-a` accumulator mode).
8636 return if returnval != 0 || informed == 0 {
8637 1
8638 } else {
8639 0
8640 }; // c:4082
8641 }
8642 }
8643
8644 // c:4121-4205 — literal-name dispatch per arg.
8645 queue_signals();
8646 // C source uses MATCHEDNODES only when `-m` (glob-args) is set;
8647 // plain `-a` keeps the literal argv. Without this gate, `whence
8648 // -a true` consulted an empty MATCHEDNODES and skipped every
8649 // print.
8650 let argv_vec: Vec<String> = if OPT_ISSET(ops, b'm') {
8651 MATCHEDNODES.lock().map(|m| m.clone()).unwrap_or_default()
8652 } else {
8653 argv.to_vec()
8654 };
8655 for arg in &argv_vec {
8656 // c:4121
8657 // c:4088 — `informed = 0;` reset per iteration so the per-arg
8658 // not-found path can fire correctly.
8659 informed = 0; // c:4088
8660 // c:4090 `char *cnam` is the findcmd return in C; in Rust it
8661 // is bound inline at the findcmd call site below.
8662 // c:4089-4137 — !`-p` and !`-a` matched-from-prior-`-m` arm.
8663 if !OPT_ISSET(ops, b'p') {
8664 // c:4093-4097 — alias check. C: `aliastab->printnode(hn, aliasflags)`.
8665 let alias_text = aliastab_lock()
8666 .read()
8667 .ok()
8668 .and_then(|t| t.get(arg).map(|a| a.clone()));
8669 if let Some(a) = alias_text {
8670 printaliasnode(&a, aliasflags); // c:4094
8671 informed = 1; // c:4095
8672 if !all {
8673 continue;
8674 } // c:4097
8675 }
8676 // c:4099 /* Look for suffix alias */
8677 // c:4100-4102 — `if ((suf = strrchr(*argv, '.')) && suf[1] &&
8678 // suf > *argv && suf[-1] != Meta &&
8679 // (hn = sufaliastab->getnode(sufaliastab, suf+1)))`.
8680 // The order is load-bearing: C short-circuits, so `suf[-1]`
8681 // (the byte BEFORE `.`) is read only after `suf > *argv`
8682 // confirms `.` is not at position 0.
8683 if let Some(idx) = arg.rfind('.') {
8684 // c:4100 — suf = strrchr(*argv, '.'); suf[1] (char after `.`
8685 // is non-NUL) AND c:4101 suf > *argv (`.` not at start)
8686 // AND suf[-1] != Meta (char before `.` is not Meta-escape).
8687 // Chained into one `if` so Rust short-circuits identically
8688 // to C; reading `arg.as_bytes()[idx - 1]` eagerly with
8689 // idx==0 panics with subtract-overflow.
8690 if idx + 1 < arg.len() && idx > 0 && arg.as_bytes()[idx - 1] as u8 != Meta {
8691 let suf = &arg[idx + 1..]; // c:4102 suf+1
8692 let suf_alias = sufaliastab_lock()
8693 .read()
8694 .ok()
8695 .and_then(|t| t.get(suf).map(|a| a.clone()));
8696 if let Some(a) = suf_alias {
8697 printaliasnode(&a, printflags); // c:4103
8698 informed = 1; // c:4104
8699 if !all {
8700 continue;
8701 } // c:4106
8702 }
8703 }
8704 }
8705 // c:4109-4114 — `if ((hn = reswdtab->getnode(reswdtab, *argv)))
8706 // reswdtab->printnode(hn, printflags);`. Reads canonical
8707 // reswdtab instead of a drift-prone literal array.
8708 let is_reswd = reswdtab_lock()
8709 .read()
8710 .map(|t| t.get(arg).is_some())
8711 .unwrap_or(false);
8712 if is_reswd {
8713 // c:4109
8714 if (printflags & PRINT_WHENCE_WORD as i32) != 0 {
8715 println!("{}: reserved", arg);
8716 } else if (printflags & PRINT_WHENCE_CSH as i32) != 0 {
8717 println!("{}: shell reserved word", arg);
8718 } else if (printflags & PRINT_WHENCE_VERBOSE as i32) != 0 {
8719 println!("{} is a reserved word", arg);
8720 } else {
8721 println!("{}", arg); // c:4110
8722 }
8723 informed = 1; // c:4111
8724 if !all {
8725 continue;
8726 } // c:4112
8727 }
8728 // c:4116-4121 — shell function check. C:
8729 // `printshfuncexpand(hn, printflags, expand)`.
8730 // Inline match-on-printflags reimplementation deleted —
8731 // route through the canonical port at hashtable.rs:1407,
8732 // which threads `expand` (for `-x N` indent override) and
8733 // handles PRINT_WHENCE_FUNCDEF/PRINT_WHENCE_WORD/_CSH/
8734 // _VERBOSE branches per Src/hashtable.c:1340-1404.
8735 let shfunc_node = getshfunc(arg);
8736 if let Some(ref f) = shfunc_node {
8737 printshfuncexpand(f, printflags, expand); // c:4117
8738 informed = 1; // c:4118
8739 if !all {
8740 continue;
8741 } // c:4120
8742 }
8743 // c:4123-4128 — builtin check. C: `builtintab->printnode(
8744 // hn, printflags)` → printbuiltinnode at Src/builtin.c:174.
8745 // Inline match-on-(wd|csh|v) reimplementation deleted —
8746 // route through the canonical port at builtin.rs:139.
8747 // c:Src/Modules/files.c:806-824 — bare `mkdir`/`rm`/`mv`/`ln`/
8748 // `chmod`/`chown`/`chgrp`/`sync`/`rmdir` and their `zf_*`
8749 // aliases are bound by `zsh/files`. Without explicit
8750 // `zmodload zsh/files`, `type rm` reports `/bin/rm` (the
8751 // PATH lookup result) — the builtin name shouldn't appear
8752 // in builtintab at all per C's lazy paramtab/builtintab
8753 // wiring. Skip the lookup here when the module is unloaded
8754 // so the search falls through to the cmdnamtab/$PATH path
8755 // below. Bug #28 in docs/BUGS.md.
8756 let is_files_gated = matches!(
8757 arg.as_str(),
8758 "mkdir"
8759 | "rmdir"
8760 | "rm"
8761 | "mv"
8762 | "ln"
8763 | "chmod"
8764 | "chown"
8765 | "chgrp"
8766 | "sync"
8767 | "zf_mkdir"
8768 | "zf_rmdir"
8769 | "zf_rm"
8770 | "zf_mv"
8771 | "zf_ln"
8772 | "zf_chmod"
8773 | "zf_chown"
8774 | "zf_chgrp"
8775 | "zf_sync"
8776 ) && !crate::ported::module::MODULESTAB
8777 .lock()
8778 .unwrap()
8779 .is_loaded("zsh/files");
8780 // c:Bug #532/#535 — same logic for module-bound builtin
8781 // names that ship statically linked in zshrs but require
8782 // an explicit `zmodload` before `type X` reports them as
8783 // a builtin in C zsh:
8784 // * zsh/stat → stat, zstat
8785 // * zsh/zselect → zselect
8786 // * zsh/zpty → zpty
8787 // * zsh/net/tcp → ztcp
8788 // * zsh/zftp → zftp
8789 // * zsh/system → zsystem, syserror
8790 let is_module_gated = |modname: &str, names: &[&str]| -> bool {
8791 names.iter().any(|n| arg.as_str() == *n)
8792 && !crate::ported::module::MODULESTAB
8793 .lock()
8794 .unwrap()
8795 .is_loaded(modname)
8796 };
8797 let is_stat_gated = is_module_gated("zsh/stat", &["stat", "zstat"]);
8798 let is_zselect_gated = is_module_gated("zsh/zselect", &["zselect"]);
8799 let is_zpty_gated = is_module_gated("zsh/zpty", &["zpty"]);
8800 let is_ztcp_gated = is_module_gated("zsh/net/tcp", &["ztcp"]);
8801 let is_zftp_gated = is_module_gated("zsh/zftp", &["zftp"]);
8802 let is_system_gated = is_module_gated("zsh/system", &["zsystem", "syserror"]);
8803 let is_module_bound_gated = is_stat_gated
8804 || is_zselect_gated
8805 || is_zpty_gated
8806 || is_ztcp_gated
8807 || is_zftp_gated
8808 || is_system_gated;
8809 // c:Src/builtin.c:4123 — `builtintab->getnode(name)` returns NULL
8810 // for a builtin carrying the DISABLED flag (set by `disable NAME`),
8811 // so whence/type do NOT report a disabled builtin as a builtin and
8812 // instead fall through to the hashed/PATH external command.
8813 let is_disabled_builtin = BUILTINS_DISABLED
8814 .lock()
8815 .map(|s| s.contains(arg.as_str()))
8816 .unwrap_or(false);
8817 let builtin_node: Option<*mut hashnode> =
8818 if is_files_gated || is_module_bound_gated || is_disabled_builtin {
8819 None
8820 } else {
8821 BUILTINS
8822 .iter()
8823 .find(|b| b.node.nam == *arg)
8824 .map(|b| &b.node as *const hashnode as *mut hashnode)
8825 };
8826 if let Some(hn) = builtin_node {
8827 printbuiltinnode(hn, printflags); // c:4124
8828 informed = 1; // c:4125
8829 if !all {
8830 continue;
8831 } // c:4127
8832 }
8833 // zshrs extension builtins (daemon z* family: zd, zcache,
8834 // zjob, …) dispatch by name via try_dispatch instead of
8835 // living in BUILTINS — but they ARE builtins and whence/
8836 // type must classify them as such (`whence -w zd` reported
8837 // the external /opt/homebrew/bin/zd instead).
8838 if builtin_node.is_none() && crate::daemon::builtins::is_zshrs_builtin(arg) {
8839 let mut ext_node = hashnode {
8840 next: None,
8841 nam: arg.clone(),
8842 flags: 0,
8843 };
8844 printbuiltinnode(&mut ext_node as *mut hashnode, printflags);
8845 informed = 1;
8846 if !all {
8847 continue;
8848 }
8849 }
8850 // zshrs extension builtins folded into builtintab (znative package
8851 // manager, ztest/zassert framework, watch) — first-class builtins
8852 // absent from the static BUILTINS table; classify like the daemon
8853 // z* family above so `whence -w zassert_eq` reports `builtin`.
8854 if builtin_node.is_none()
8855 && !crate::daemon::builtins::is_zshrs_builtin(arg)
8856 && crate::extensions::ext_builtins::is_extension_builtin(arg)
8857 {
8858 let mut ext_node = hashnode {
8859 next: None,
8860 nam: arg.clone(),
8861 flags: 0,
8862 };
8863 printbuiltinnode(&mut ext_node as *mut hashnode, printflags);
8864 informed = 1;
8865 if !all {
8866 continue;
8867 }
8868 }
8869 // c:4167-4173 — cmdnamtab HASHED check (commands installed
8870 // via `hash NAME=PATH`). Read the canonical cmdnamtab
8871 // directly. Was a fake env-var bridge under invented
8872 // `__zshrs_hash_NAME` keys; cmdnamtab is bucket-2-
8873 // consolidated now.
8874 let hashed_path: Option<String> = {
8875 match cmdnamtab_lock().read() {
8876 Ok(tab) => tab.get(arg).and_then(|cn| {
8877 if (cn.node.flags & HASHED as i32) != 0 {
8878 cn.cmd.clone() // c:4168 cn->u.cmd
8879 } else {
8880 None
8881 }
8882 }),
8883 Err(_) => None,
8884 }
8885 };
8886 if let Some(p) = hashed_path {
8887 if (printflags & PRINT_LIST) != 0 {
8888 println!("hash {}={}", arg, p);
8889 } else {
8890 println!("{}", p);
8891 }
8892 informed = 1; // c:4170
8893 if !all {
8894 continue;
8895 } // c:4171
8896 }
8897 }
8898 // c:4141-4172 — `-a` all-paths search. C iterates the
8899 // shell-side `path` array (the tied $path/$PATH global,
8900 // Src/parse.c). Rust reads $PATH via getsparam — same source.
8901 if all && !arg.starts_with('/') {
8902 // c:4141
8903 if let Some(path) = getsparam("PATH") {
8904 for dir in path.split(':') {
8905 // c:4145 — `if (**pp) buf = zhtricat(*pp, "/", *argv);
8906 // else buf = dupstring(*argv);`.
8907 // Empty path entry means CWD per POSIX, but C still
8908 // joins with "/" if non-empty; Rust matches the
8909 // !empty arm.
8910 if dir.is_empty() {
8911 continue;
8912 }
8913 let full = format!("{}/{}", dir, arg); // c:4147
8914 // c:4150 — `iscom(buf)`: access(X_OK)==0 &&
8915 // S_ISREG(stat). Was `Path::is_file()` which omits
8916 // the X_OK check — would have flagged non-executable
8917 // files as matches.
8918 if iscom(&full) {
8919 // c:4150
8920 if wd {
8921 // c:4151
8922 println!("{}: command", arg); // c:4152
8923 } else {
8924 if v && !csh {
8925 // c:4154
8926 print!("{} is ", arg); // c:4155
8927 print!("{}", quotedzputs(&full)); // c:4156
8928 } else {
8929 print!("{}", full); // c:4158
8930 }
8931 // c:4159-4160 — `if (OPT_ISSET(ops,'s') ||
8932 // OPT_ISSET(ops,'S')) print_if_link(buf,
8933 // OPT_ISSET(ops,'S'));`. -s prints just
8934 // the final realpath; -S prints the whole
8935 // chain.
8936 if OPT_ISSET(ops, b's') || OPT_ISSET(ops, b'S') {
8937 print_if_link(&full, OPT_ISSET(ops, b'S')); // c:4160
8938 }
8939 println!(); // c:4161 fputc('\n', stdout)
8940 }
8941 informed = 1; // c:4163
8942 } else {
8943 }
8944 }
8945 }
8946 // c:4166-4171 — `if (!informed && (wd || v || csh))`. C:
8947 // zputs(*argv, stdout); puts(wd ? ": none" : " not found");
8948 // Was `if !informed != 0 && ...` which is broken Rust — the
8949 // `!` is bitwise NOT on the i32, so the condition was true
8950 // when informed != 0 (inverted). Fix: explicit `informed == 0`.
8951 if informed == 0 && (wd || v || csh) {
8952 // c:4166
8953 println!("{}{}", arg, if wd { ": none" } else { " not found" }); // c:4168-4169
8954 returnval = 1; // c:4170
8955 }
8956 continue;
8957 }
8958 // c:4200-4203 — `-p` BIN_COMMAND special case: builtin first.
8959 if func == BIN_COMMAND && OPT_ISSET(ops, b'p') {
8960 // c:4200
8961 if BUILTINS.iter().any(|b| b.node.nam == *arg) {
8962 // c:4201
8963 println!("{}: builtin", arg); // c:4202
8964 informed = 1;
8965 continue;
8966 }
8967 }
8968 // c:4181-4197 — external-command fallback via findcmd.
8969 // C: `if ((cnam = findcmd(*argv, 1, func == BIN_COMMAND &&
8970 // OPT_ISSET(ops, 'p'))))`. Single call site — the previous
8971 // Rust port had two near-duplicate findcmd blocks which
8972 // doubled the lookup. Collapsed into one to match C.
8973 if let Some(cnam) = findcmd(
8974 arg,
8975 1, // c:4181 docmd
8976 (func == BIN_COMMAND && OPT_ISSET(ops, b'p')) as i32, // c:4182-4183
8977 ) {
8978 // c:4181
8979 if wd {
8980 // c:4184
8981 println!("{}: command", arg); // c:4186
8982 } else {
8983 if v && !csh {
8984 // c:4188
8985 print!("{} is ", arg); // c:4189
8986 print!("{}", quotedzputs(&cnam)); // c:4190
8987 } else {
8988 print!("{}", cnam); // c:4192
8989 }
8990 // c:4193-4194 — `-s`/`-S` symlink follow.
8991 if OPT_ISSET(ops, b's') || OPT_ISSET(ops, b'S') {
8992 print_if_link(&cnam, OPT_ISSET(ops, b'S')); // c:4194
8993 }
8994 println!(); // c:4195 fputc('\n', stdout)
8995 }
8996 informed = 1; // c:4197
8997 continue;
8998 }
8999 // c:4201-4205 — not found at all.
9000 if v || csh || wd {
9001 // c:4202
9002 println!("{}{}", arg, if wd { ": none" } else { " not found" }); // c:4203
9003 }
9004 returnval = 1; // c:4204
9005 }
9006 unqueue_signals();
9007 returnval | (informed == 0) as i32 // c:4209
9008}
9009
9010/// Port of `bin_hash(char *name, char **argv, Options ops, UNUSED(int func))` from Src/builtin.c:4234.
9011/// C: `int bin_hash(char *name, char **argv, Options ops, ...)` —
9012/// manage `cmdnamtab` (default) or `nameddirtab` (`-d`); `-r` empties,
9013/// `-f` fills, `-L` sets PRINT_LIST, `-m` is a glob.
9014/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
9015pub fn bin_hash(
9016 name: &str,
9017 argv: &[String], // c:4234
9018 ops: &options,
9019 _func: i32,
9020) -> i32 {
9021 let mut returnval = 0i32; // c:4239
9022 let mut printflags = 0i32; // c:4240
9023 let dir_mode = OPT_ISSET(ops, b'd'); // c:4242
9024
9025 // PFA-SMR aspect: only `hash -d NAME=PATH` mutates the named-dir
9026 // table; the default `hash CMD=PATH` form populates a runtime
9027 // command cache that the recorder doesn't re-apply.
9028 #[cfg(feature = "recorder")]
9029 if crate::recorder::is_enabled() && dir_mode {
9030 let ctx = crate::recorder::recorder_ctx_global();
9031 for a in argv {
9032 if a.starts_with('-') {
9033 continue;
9034 }
9035 if let Some((k, v)) = a.split_once('=') {
9036 crate::recorder::emit_hash_d(k, v, ctx.clone());
9037 }
9038 }
9039 }
9040
9041 // c:4247-4263 — `-r` empty / `-f` fill (no other args).
9042 if OPT_ISSET(ops, b'r') || OPT_ISSET(ops, b'f') {
9043 // c:4247
9044 if !argv.is_empty() {
9045 // c:4249
9046 zwarnnam("hash", "too many arguments"); // c:4250
9047 return 1; // c:4251
9048 }
9049 if OPT_ISSET(ops, b'r') {
9050 // c:4255
9051 // c:4256 — `emptyhashtable(cmdnamtab)` /
9052 // `emptynameddirtable()`.
9053 if dir_mode {
9054 emptynameddirtable();
9055 } else {
9056 emptycmdnamtable();
9057 }
9058 }
9059 if OPT_ISSET(ops, b'f') {
9060 // c:4259
9061 // c:4260 — `fillcmdnamtable(cmdnamtab)` /
9062 // `fillnameddirtable()`. cmdnamtab fill = walk every
9063 // PATH entry and hashdir() it.
9064 if dir_mode {
9065 fillnameddirtable();
9066 } else {
9067 // Read $path (the lowercase array form) from env.
9068 // c:4260 — fill cmdnamtab from $path. Read shell-side
9069 // $PATH so changes via `path=(...)` flow in.
9070 let path_str = getsparam("PATH").unwrap_or_default();
9071 let path_arr: Vec<String> = path_str.split(':').map(|s| s.to_string()).collect();
9072 fillcmdnamtable(&path_arr);
9073 }
9074 }
9075 return 0; // c:4262
9076 }
9077
9078 // c:4265 — `-L` enables PRINT_LIST.
9079 if OPT_ISSET(ops, b'L') {
9080 printflags |= PRINT_LIST;
9081 } // c:4265
9082
9083 // c:4268-4273 — no args: list table.
9084 if argv.is_empty() {
9085 // c:4268
9086 queue_signals(); // c:4269
9087 // c:4270 — `scanhashtable(ht, 1, 0, 0, ht->printnode, printflags)`.
9088 // Walk the selected table (cmdnamtab default, nameddirtab when
9089 // `-d`). Previous Rust port only walked nameddirtab — `hash`
9090 // with no args (the typical user-visible form) silently printed
9091 // nothing on cmdnamtab.
9092 // c:4270 — `scanhashtable(ht, 1, 0, 0, ht->printnode, printflags)`.
9093 // The second arg `1` is `sorted` — the table is listed in
9094 // alphabetical (meta-aware hnamcmp) order, not hash/insertion
9095 // order. The previous port walked `t.iter()` raw, so `hash -d`
9096 // printed entries in an arbitrary order (`two`, `one` instead of
9097 // `one`, `two`).
9098 if dir_mode {
9099 if let Ok(t) = nameddirtab().lock() {
9100 let mut entries: Vec<_> = t.iter().collect();
9101 entries.sort_by(|a, b| hnamcmp(a.0, b.0)); // c:4270 sorted=1
9102 for (_n, nd) in entries {
9103 printnameddirnode(nd, printflags);
9104 }
9105 }
9106 } else {
9107 // c:4270 — cmdnamtab walk (the default `ht`). PATH lookup
9108 // arr is empty in the printnode call site because per-node
9109 // hashed entries carry their own resolved path.
9110 if let Ok(t) = cmdnamtab_lock().read() {
9111 let mut entries: Vec<_> = t.iter().collect();
9112 entries.sort_by(|a, b| hnamcmp(a.0, b.0)); // c:4270 sorted=1
9113 for (_n, cn) in entries {
9114 // c:4270 — `scanhashtable(cmdnamtab, ..., printcmdnamnode, ...)`
9115 printcmdnamnode(cn, printflags);
9116 }
9117 }
9118 }
9119 unqueue_signals(); // c:4271
9120 return 0; // c:4272
9121 }
9122
9123 // c:4276-4329 — name-list dispatch, both literal and -m glob.
9124 queue_signals(); // c:4276
9125 let mut idx = 0;
9126 while idx < argv.len() {
9127 // c:4277
9128 let arg = &argv[idx];
9129 idx += 1;
9130 if OPT_ISSET(ops, b'm') {
9131 // c:4279
9132 // c:4280-4290 — glob-match path.
9133 let pprog = patcompile(
9134 &{
9135 let mut __pat_tok = (arg).to_string();
9136 crate::ported::glob::tokenize(&mut __pat_tok);
9137 __pat_tok
9138 }, // c:4282
9139 PAT_HEAPDUP,
9140 None,
9141 );
9142 if let Some(prog) = pprog {
9143 if dir_mode {
9144 if let Ok(t) = nameddirtab().lock() {
9145 for (n, nd) in t.iter() {
9146 if pattry(&prog, n) {
9147 // c:4286
9148 printnameddirnode(nd, printflags);
9149 }
9150 }
9151 }
9152 }
9153 } else {
9154 zwarnnam(name, &format!("bad pattern : {}", arg)); // c:4292
9155 returnval = 1; // c:4293
9156 }
9157 continue;
9158 }
9159 // c:4297-4317 — literal name=value or name-only.
9160 let (n, val) = match arg.find('=') {
9161 Some(eq) => (&arg[..eq], Some(&arg[eq + 1..])),
9162 None => (arg.as_str(), None),
9163 };
9164 if let Some(v) = val {
9165 // c:4302
9166 // Define entry.
9167 if dir_mode {
9168 // c:4302
9169 // c:4303-4310 — `itype_end(asg->name, IUSER, 0)` validates;
9170 // dir name must be all-IUSER chars. IUSER (Src/utils.c:
9171 // 4173-4191) = digits + alpha + `_` + `-` + `.` (plus
9172 // non-ASCII). The previous port allowed only alphanumeric
9173 // + `_`, so `hash -d t-t=/foo` was rejected with "invalid
9174 // character", which then broke `~t-t` named-dir expansion.
9175 if !n
9176 .chars()
9177 .all(|c| c.is_alphanumeric() || c == '_' || c == '-' || c == '.')
9178 {
9179 // c:4305
9180 zwarnnam(name, &format!("invalid character in directory name: {}", n)); // c:4306
9181 returnval = 1; // c:4308
9182 continue; // c:4309
9183 }
9184 // c:Src/builtin.c:80 — `BUILTIN("hash", BINF_MAGICEQUALS,
9185 // …)`. The MAGICEQUALS flag causes the C parser to
9186 // apply tilde/equals expansion to `name=value` argv
9187 // entries via Src/utils.c::quotestring's MAGICEQUALSUBST
9188 // arm at c:6304. By the time argv reaches bin_hash, the
9189 // value has already been tilde-expanded.
9190 //
9191 // The Rust pipeline doesn't apply MAGICEQUALS at the
9192 // parser/compile level for `hash -d` args, so the
9193 // literal `~` reaches here untouched. Bug #32 in
9194 // docs/BUGS.md: `hash -d zh=~` stored `'~'` instead
9195 // of $HOME. Apply tilde expansion locally by routing
9196 // the value through filesubstr — re-tokenize a
9197 // leading `~` to the Tilde TOKEN (\u{98}) which
9198 // filesubstr keys on per its strict-token
9199 // contract (subst.rs:1761).
9200 let expanded_v: String = if v.starts_with('~') {
9201 let rest = &v[1..];
9202 let tokenized = format!("\u{98}{}", rest);
9203 crate::ported::subst::filesubstr(&tokenized, false)
9204 .unwrap_or_else(|| v.to_string())
9205 } else {
9206 v.to_string()
9207 };
9208 let nd = nameddir {
9209 node: hashnode {
9210 next: None,
9211 nam: n.to_string(),
9212 flags: 0,
9213 },
9214 dir: expanded_v,
9215 diff: 0,
9216 };
9217 addnameddirnode(n, nd); // c:4314
9218 } else {
9219 // c:4313-4318 — `Cmdnam cn = zshcalloc(sizeof *cn);
9220 // cn->node.flags = HASHED;
9221 // cn->u.cmd = ztrdup(asg->value.scalar);
9222 // ht->addnode(ht, ztrdup(asg->name), hn);`
9223 // Insert into cmdnamtab so `hash myc` lookup hits it
9224 // (was storing in `__zshrs_hash_*` env var — fakery
9225 // that the user-facing `hash myc` query never read).
9226 let cn = cmdnam {
9227 node: hashnode {
9228 next: None,
9229 nam: n.to_string(),
9230 flags: HASHED as i32, // c:4316
9231 },
9232 name: None,
9233 cmd: Some(v.to_string()), // c:4316
9234 };
9235 if let Ok(mut tab) = cmdnamtab_lock().write() {
9236 tab.add(cn); // c:4318 addnode
9237 }
9238 }
9239 if OPT_ISSET(ops, b'v') {
9240 // c:4321
9241 if dir_mode {
9242 if let Ok(t) = nameddirtab().lock() {
9243 if let Some(nd) = t.get(n) {
9244 // c:4322
9245 printnameddirnode(nd, 0);
9246 }
9247 }
9248 }
9249 }
9250 } else {
9251 // c:4323-4334 — display existing entry / look up.
9252 if dir_mode {
9253 let snapshot = nameddirtab().lock().ok().and_then(|t| t.get(n).cloned());
9254 match snapshot {
9255 Some(nd) => {
9256 if OPT_ISSET(ops, b'v') {
9257 // c:4337
9258 printnameddirnode(&nd, 0);
9259 }
9260 }
9261 None => {
9262 zwarnnam(name, &format!("no such directory name: {}", n)); // c:4327
9263 returnval = 1; // c:4328
9264 }
9265 }
9266 } else {
9267 // c:4319-4334 — `else if (!(hn = ht->getnode2(ht,
9268 // asg->name))) { ... if (!hashcmd(asg->name, path))
9269 // zwarnnam("no such command"); }`. C path: first
9270 // check cmdnamtab for an existing entry; only fall
9271 // back to hashcmd's PATH walk when not present. The
9272 // previous Rust port skipped the cmdnamtab check, so
9273 // a prior `hash myc=/path` insert was invisible to
9274 // the matching `hash myc` query.
9275 let in_cmdnamtab = cmdnamtab_lock()
9276 .read()
9277 .map(|t| t.get(n).is_some())
9278 .unwrap_or(false);
9279 if !in_cmdnamtab {
9280 // c:4319 hn == NULL → try hashcmd.
9281 let path: Vec<String> = getsparam("PATH")
9282 .map(|p| p.split(':').map(String::from).collect())
9283 .unwrap_or_default();
9284 if crate::ported::exec::hashcmd(n, &path).is_none() {
9285 // c:4332
9286 zwarnnam(name, &format!("no such command: {}", n)); // c:4333
9287 returnval = 1; // c:4334
9288 }
9289 }
9290 }
9291 }
9292 }
9293 unqueue_signals(); // c:4346
9294 returnval // c:4346
9295}
9296
9297/// Port of `bin_unhash(char *name, char **argv, Options ops, int func)` from Src/builtin.c:4346.
9298/// C: `int bin_unhash(char *name, char **argv, Options ops, int func)` —
9299/// remove entries from cmdnamtab/aliastab/sufaliastab/nameddirtab/
9300/// shfunctab. `-a` clears all, `-m` is a glob.
9301/// WARNING: param names don't match C — Rust=(name, argv, func) vs C=(name, argv, ops, func)
9302pub fn bin_unhash(
9303 name: &str,
9304 argv: &[String], // c:4346
9305 ops: &options,
9306 func: i32,
9307) -> i32 {
9308 let mut returnval = 0i32; // c:4351
9309 let mut all = 0i32; // c:4351
9310 let mut match_count = 0i32; // c:4351
9311
9312 // PFA-SMR aspect: when invoked as `unalias`, record the un-alias
9313 // events so the replay can suppress earlier `alias` calls.
9314 #[cfg(feature = "recorder")]
9315 if crate::recorder::is_enabled() && func == crate::ported::builtin::BIN_UNALIAS {
9316 let ctx = crate::recorder::recorder_ctx_global();
9317 for a in argv {
9318 if a.starts_with('-') && a != "-" {
9319 continue;
9320 }
9321 crate::recorder::emit_unalias(a, ctx.clone());
9322 }
9323 }
9324
9325 // c:4355-4373 — table-pick dispatch.
9326 enum Tab {
9327 CmdNam,
9328 NamedDir,
9329 Shfunc,
9330 Alias,
9331 SufAlias,
9332 }
9333 let tab: Tab;
9334 if func == BIN_UNALIAS {
9335 // c:4356
9336 tab = if OPT_ISSET(ops, b's') {
9337 Tab::SufAlias
9338 } else {
9339 Tab::Alias
9340 }; // c:4357
9341 if OPT_ISSET(ops, b'a') {
9342 // c:4361
9343 if !argv.is_empty() {
9344 // c:4362
9345 zwarnnam(name, "-a: too many arguments"); // c:4363
9346 return 1; // c:4364
9347 }
9348 all = 1; // c:4366
9349 } else if argv.is_empty() {
9350 // c:4367
9351 zwarnnam(name, "not enough arguments"); // c:4368
9352 return 1; // c:4369
9353 }
9354 } else if OPT_ISSET(ops, b'd') {
9355 tab = Tab::NamedDir; // c:4370
9356 } else if OPT_ISSET(ops, b'f') {
9357 tab = Tab::Shfunc; // c:4372
9358 } else if OPT_ISSET(ops, b's') {
9359 tab = Tab::SufAlias; // c:4374
9360 } else if func == BIN_UNHASH && OPT_ISSET(ops, b'a') {
9361 tab = Tab::Alias; // c:4376
9362 } else {
9363 tab = Tab::CmdNam;
9364 } // c:4378
9365
9366 // Helper: clear entire table.
9367 let clear_all = |t: &Tab| match t {
9368 Tab::Alias => {
9369 let _ = aliastab_lock().write().map(|mut g| g.clear());
9370 }
9371 Tab::SufAlias => {
9372 let _ = sufaliastab_lock().write().map(|mut g| g.clear());
9373 }
9374 Tab::NamedDir => {
9375 emptynameddirtable();
9376 }
9377 Tab::Shfunc => {
9378 // c:4388 — empty whole shfunctab (`unhash -af` etc.). C uses
9379 // `emptyhashtable(shfunctab)` GSU; Rust port iterates names
9380 // and removes each (no `clear` method on shfunc_table).
9381 if let Ok(mut t) = shfunctab_lock().write() {
9382 let names: Vec<String> = t.iter().map(|(k, _)| k.clone()).collect();
9383 for nm in names {
9384 let _ = t.remove(&nm);
9385 }
9386 }
9387 }
9388 Tab::CmdNam => {
9389 emptycmdnamtable();
9390 } // c:4389
9391 };
9392 let remove_one = |t: &Tab, nm: &str| -> bool {
9393 match t {
9394 Tab::Alias => aliastab_lock()
9395 .write()
9396 .map(|mut g| g.remove(nm).is_some())
9397 .unwrap_or(false),
9398 Tab::SufAlias => sufaliastab_lock()
9399 .write()
9400 .map(|mut g| g.remove(nm).is_some())
9401 .unwrap_or(false),
9402 Tab::NamedDir => crate::ported::hashnameddir::removenameddirnode(nm).is_some(),
9403 Tab::Shfunc => {
9404 let from_tab = shfunctab_lock()
9405 .write()
9406 .map(|mut g| g.remove(nm).is_some())
9407 .unwrap_or(false);
9408 // Also remove from the executor's compiled-function /
9409 // source maps. Without this, `unset -f f` cleared
9410 // shfunctab but dispatch_function_call still found the
9411 // compiled chunk and ran the old body. Routed via the
9412 // exec accessors unregister_function fn-ptr installed by
9413 // fusevm_bridge at startup (no ShellExecutor reach-in
9414 // from src/ported/).
9415 let from_exec = crate::ported::exec::unregister_function(nm);
9416 from_tab || from_exec
9417 }
9418 // c:4405 — `if ((hn = ht->removenode(ht, *argv)))`.
9419 // Removal returns truthy only when the entry actually
9420 // existed. Previous Rust port hardcoded `true` after a
9421 // void-return `freecmdnamnode` call, so `unhash badname`
9422 // silently succeeded instead of emitting the canonical
9423 // "no such hash table element" error.
9424 Tab::CmdNam => cmdnamtab_lock()
9425 .write()
9426 .map(|mut g| g.remove(nm).is_some())
9427 .unwrap_or(false),
9428 }
9429 };
9430
9431 if all != 0 {
9432 // c:4382
9433 queue_signals(); // c:4383
9434 clear_all(&tab); // c:4384-4389
9435 unqueue_signals(); // c:4390
9436 return 0; // c:4391
9437 }
9438
9439 // c:4395-4421 — `-m` glob branch.
9440 if OPT_ISSET(ops, b'm') {
9441 // c:4395
9442 for arg in argv {
9443 // c:4396
9444 queue_signals(); // c:4397
9445 let pprog = patcompile(
9446 &{
9447 let mut __pat_tok = (arg).to_string();
9448 crate::ported::glob::tokenize(&mut __pat_tok);
9449 __pat_tok
9450 }, // c:4400
9451 PAT_HEAPDUP,
9452 None,
9453 );
9454 if let Some(prog) = pprog {
9455 // Collect names then remove (avoid iterator/mutation conflict).
9456 // c:4408 — `scanmatchtable(ht, pprog, ...)` walks every
9457 // entry in the selected table. Previous Rust port left
9458 // Tab::CmdNam returning an empty Vec, so `unhash -m PAT`
9459 // (default cmd-hash table) silently matched zero entries.
9460 let names: Vec<String> = match &tab {
9461 Tab::Alias => aliastab_lock()
9462 .read()
9463 .map(|t| t.iter().map(|(n, _)| n.clone()).collect())
9464 .unwrap_or_default(),
9465 Tab::SufAlias => sufaliastab_lock()
9466 .read()
9467 .map(|t| t.iter().map(|(n, _)| n.clone()).collect())
9468 .unwrap_or_default(),
9469 Tab::NamedDir => nameddirtab()
9470 .lock()
9471 .map(|t| t.keys().cloned().collect())
9472 .unwrap_or_default(),
9473 Tab::Shfunc => shfunctab_lock()
9474 .read()
9475 .map(|t| t.iter().map(|(k, _)| k.clone()).collect())
9476 .unwrap_or_default(),
9477 // c:4408 — cmdnamtab walk via `cmdnamtab_lock().iter()`.
9478 Tab::CmdNam => cmdnamtab_lock()
9479 .read()
9480 .map(|t| t.iter().map(|(n, _)| n.clone()).collect())
9481 .unwrap_or_default(),
9482 };
9483 for nm in &names {
9484 if pattry(&prog, nm) {
9485 // c:4408
9486 if remove_one(&tab, nm) {
9487 match_count += 1; // c:4410
9488 }
9489 }
9490 }
9491 } else {
9492 zwarnnam(name, &format!("bad pattern : {}", arg)); // c:4416
9493 returnval = 1; // c:4417
9494 }
9495 unqueue_signals(); // c:4419
9496 }
9497 if match_count == 0 {
9498 // c:4424
9499 returnval = 1; // c:4425
9500 }
9501 return returnval; // c:4426
9502 }
9503
9504 // c:4429-4439 — literal-name removals.
9505 queue_signals(); // c:4430
9506 for arg in argv {
9507 // c:4431
9508 if remove_one(&tab, arg) { // c:4432
9509 // freed
9510 } else if func == BIN_UNSET && isset(POSIXBUILTINS) {
9511 // c:4434 — POSIX: unset of nonexistent isn't an error.
9512 returnval = 0; // c:4435
9513 } else {
9514 zwarnnam(name, &format!("no such hash table element: {}", arg)); // c:4437
9515 returnval = 1; // c:4450
9516 }
9517 }
9518 unqueue_signals(); // c:4450
9519 returnval // c:4450
9520}
9521
9522/// Port of `bin_alias(char *name, char **argv, Options ops, UNUSED(int func))` from Src/builtin.c:4450.
9523/// C: `int bin_alias(char *name, char **argv, Options ops, ...)` — list,
9524/// define, glob-list, or display aliases. `-r`/`-g`/`-s` filter type;
9525/// `-L` prints definitions; `-m` treats args as patterns.
9526/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
9527pub fn bin_alias(
9528 name: &str,
9529 argv: &[String], // c:4450
9530 ops: &options,
9531 _func: i32,
9532) -> i32 {
9533 let mut returnval = 0i32; // c:4455
9534 let mut flags1 = 0u32; // c:4456
9535 let mut flags2 = DISABLED as u32; // c:4456
9536 let mut printflags = 0i32; // c:4457
9537 let mut use_suffix = false; // tracks ht switch
9538
9539 // c:4461-4485 — type-flag parsing.
9540 let type_opts = (OPT_ISSET(ops, b'r') as i32) // c:4461
9541 + (OPT_ISSET(ops, b'g') as i32)
9542 + (OPT_ISSET(ops, b's') as i32);
9543 if type_opts != 0 {
9544 // c:4464
9545 if type_opts > 1 {
9546 // c:4465
9547 zwarnnam(name, "illegal combination of options"); // c:4466
9548 return 1; // c:4467
9549 }
9550 if OPT_ISSET(ops, b'g') {
9551 // c:4469
9552 flags1 |= ALIAS_GLOBAL as u32; // c:4470
9553 } else {
9554 flags2 |= ALIAS_GLOBAL as u32; // c:4472
9555 }
9556 if OPT_ISSET(ops, b's') {
9557 // c:4473
9558 flags1 |= ALIAS_SUFFIX as u32; // c:4480
9559 use_suffix = true; // c:4481
9560 } else {
9561 flags2 |= ALIAS_SUFFIX as u32; // c:4483
9562 }
9563 }
9564
9565 // c:4486-4490 — printflags from -L / + suffix.
9566 if OPT_ISSET(ops, b'L') {
9567 // c:4486
9568 printflags |= PRINT_LIST; // c:4487
9569 } else if OPT_PLUS(ops, b'g')
9570 || OPT_PLUS(ops, b'r')
9571 || OPT_PLUS(ops, b's')
9572 || OPT_PLUS(ops, b'm')
9573 || OPT_ISSET(ops, b'+')
9574 // c:4488
9575 {
9576 printflags |= PRINT_NAMEONLY; // c:4490
9577 }
9578
9579 // C bin_alias dispatches printing via `ht->printnode` (set to
9580 // `printaliasnode` at hashtable.c:1208) — `scanhashtable`,
9581 // `scanmatchtable`, and the single-name branch all call
9582 // `ht->printnode(&a->node, printflags)`. The Rust port routes
9583 // through the canonical `printaliasnode` (hashtable.rs:1477) for
9584 // the same dispatch. No local closure.
9585
9586 // c:4495-4500 — no args: list all (filtered by flags).
9587 if argv.is_empty() {
9588 // c:4495
9589 queue_signals(); // c:4496
9590 let lock = if use_suffix {
9591 sufaliastab_lock()
9592 } else {
9593 aliastab_lock()
9594 };
9595 if let Ok(t) = lock.read() {
9596 // c:Src/builtin.c:4496 — `scanhashtable(aliastab, 1, ...)`
9597 // sets the SORT flag (1). The C source walks the table
9598 // sorted by `hnamcmp` (byte-wise ASCII compare); the
9599 // Rust port iterated in arbitrary hash order, so
9600 // `alias` output sequence diverged from zsh's
9601 // (uppercase-before-lowercase) ASCII ordering.
9602 let mut entries: Vec<_> = t.iter().collect();
9603 entries.sort_by(|(a, _), (b, _)| a.cmp(b));
9604 for (_n, a) in entries {
9605 // c:4497
9606 if (a.node.flags & flags1 as i32) == flags1 as i32
9607 && (a.node.flags & flags2 as i32) == 0
9608 {
9609 printaliasnode(a, printflags);
9610 }
9611 }
9612 }
9613 unqueue_signals(); // c:4498
9614 return 0; // c:4499
9615 }
9616
9617 // c:4503-4519 — `-m` glob branch.
9618 if OPT_ISSET(ops, b'm') {
9619 // c:4503
9620 for pat in argv {
9621 // c:4504
9622 queue_signals(); // c:4505
9623 // c:4506 — `tokenize + patcompile`.
9624 let pprog = patcompile(
9625 &{
9626 let mut __pat_tok = (pat).to_string();
9627 crate::ported::glob::tokenize(&mut __pat_tok);
9628 __pat_tok
9629 }, // c:4507
9630 PAT_HEAPDUP,
9631 None,
9632 );
9633 if let Some(prog) = pprog {
9634 let lock = if use_suffix {
9635 sufaliastab_lock()
9636 } else {
9637 aliastab_lock()
9638 };
9639 if let Ok(t) = lock.read() {
9640 for (_n, a) in t.iter() {
9641 // c:4509
9642 if (a.node.flags & flags1 as i32) == flags1 as i32
9643 && (a.node.flags & flags2 as i32) == 0
9644 && pattry(&prog, &a.node.nam)
9645 {
9646 printaliasnode(a, printflags);
9647 }
9648 }
9649 }
9650 } else {
9651 zwarnnam(name, &format!("bad pattern : {}", pat)); // c:4514
9652 returnval = 1; // c:4515
9653 }
9654 unqueue_signals(); // c:4517
9655 }
9656 return returnval; // c:4518
9657 }
9658
9659 // c:4521-4540 — literal args: define `name=value` or display a single name.
9660 queue_signals(); // c:4522
9661 // PFA-SMR: capture per-definition for replay. Dispatch the
9662 // right subkind (galias/salias/alias) based on the parsed
9663 // flag bits set above. Same per-name loop the C code walks
9664 // — one record per `name=value` argv slot. Without this,
9665 // the recorder harness saw zero `alias` captures.
9666 #[cfg(feature = "recorder")]
9667 let recorder_active = crate::recorder::is_enabled();
9668 #[cfg(feature = "recorder")]
9669 let recorder_ctx = if recorder_active {
9670 Some(crate::recorder::recorder_ctx_global())
9671 } else {
9672 None
9673 };
9674 let mut idx = 0;
9675 while idx < argv.len() {
9676 // c:4523
9677 let arg = &argv[idx];
9678 idx += 1;
9679 // c:Src/builtin.c:4523 `while ((asg = getasg(&argv, NULL)))`
9680 // — empty-name assignment makes getasg call `zerr("bad
9681 // assignment")` (c:1927, sets errflag) and return NULL. The
9682 // C while-loop exits without setting returnval, so bin_alias
9683 // returns 0. /bin/zsh's outer execution checks errflag and
9684 // aborts subsequent commands but keeps lastval=0.
9685 //
9686 // Rust port divergence: the canonical `zerr` here would set
9687 // ERRFLAG_ERROR, which zshrs's execcmd_exec promotes to
9688 // lastval=1 (different shape from C's "abort but keep
9689 // lastval"). Route through `zwarnnam` so the diagnostic
9690 // emits without flipping errflag; returnval stays 0 and the
9691 // shell rc matches /bin/zsh's observed behavior.
9692 if arg.starts_with('=') {
9693 zwarnnam(name, "bad assignment");
9694 break;
9695 }
9696 if let Some(eq) = arg.find('=') {
9697 // c:4524 (asg->value.scalar)
9698 if !OPT_ISSET(ops, b'L') {
9699 // c:4524
9700 let n = &arg[..eq];
9701 let v = &arg[eq + 1..];
9702 let lock = if use_suffix {
9703 sufaliastab_lock()
9704 } else {
9705 aliastab_lock()
9706 };
9707 if let Ok(mut t) = lock.write() {
9708 let a = createaliasnode(n, v, flags1); // c:4527
9709 t.add(a);
9710 }
9711 #[cfg(feature = "recorder")]
9712 if let Some(ref ctx) = recorder_ctx {
9713 if (flags1 & ALIAS_GLOBAL as u32) != 0 {
9714 crate::recorder::emit_galias(n, Some(v), ctx.clone());
9715 } else if (flags1 & ALIAS_SUFFIX as u32) != 0 {
9716 crate::recorder::emit_salias(n, Some(v), ctx.clone());
9717 } else {
9718 crate::recorder::emit_alias(n, Some(v), ctx.clone());
9719 }
9720 }
9721 continue;
9722 }
9723 }
9724 let n = if let Some(eq) = arg.find('=') {
9725 &arg[..eq]
9726 } else {
9727 arg.as_str()
9728 };
9729 let lock = if use_suffix {
9730 sufaliastab_lock()
9731 } else {
9732 aliastab_lock()
9733 };
9734 // c:4530 — `(a = (Alias) ht->getnode(ht, asg->name))`. C
9735 // `getnode` is `gethashnode` (Src/hashtable.c:231) which
9736 // returns NULL for DISABLED entries; skipping disabled at
9737 // lookup time is what makes `alias FOO` print nothing after
9738 // `disable -a FOO` (bug #227). Must use `.get()` not
9739 // `.get_including_disabled()` here.
9740 let found = lock.read().ok().and_then(|t| {
9741 t.get(n)
9742 .map(|a| (a.node.nam.clone(), a.node.flags as u32, a.text.clone()))
9743 });
9744 match found {
9745 Some((nm, fl, txt)) => {
9746 // c:4530
9747 // c:4532-4537 — type-filter check.
9748 let show = type_opts == 0
9749 || use_suffix
9750 || (OPT_ISSET(ops, b'r') && (fl & (ALIAS_GLOBAL | ALIAS_SUFFIX) as u32) == 0)
9751 || (OPT_ISSET(ops, b'g') && (fl & ALIAS_GLOBAL as u32) != 0);
9752 if show {
9753 let a = createaliasnode(&nm, &txt, fl);
9754 printaliasnode(&a, printflags);
9755 }
9756 }
9757 None => {
9758 // c:4538
9759 returnval = 1; // c:4539
9760 }
9761 }
9762 }
9763 unqueue_signals(); // c:4541
9764 returnval // c:4542
9765}
9766
9767/// Port of `bin_true(UNUSED(char *name), UNUSED(char **argv), UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:4550.
9768/// C: `int bin_true(UNUSED(char *name), UNUSED(char **argv),
9769/// UNUSED(Options ops), UNUSED(int func))` → `return 0;`
9770/// WARNING: param names don't match C — Rust=(_name, _argv, _func) vs C=(name, argv, ops, func)
9771pub fn bin_true(
9772 _name: &str,
9773 _argv: &[String], // c:4550
9774 _ops: &options,
9775 _func: i32,
9776) -> i32 {
9777 0 // c:4559
9778}
9779
9780/// Port of `bin_false(UNUSED(char *name), UNUSED(char **argv), UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:4559.
9781/// C: `int bin_false(UNUSED(char *name), UNUSED(char **argv),
9782/// UNUSED(Options ops), UNUSED(int func))` → `return 1;`
9783/// WARNING: param names don't match C — Rust=(_name, _argv, _func) vs C=(name, argv, ops, func)
9784pub fn bin_false(
9785 _name: &str,
9786 _argv: &[String], // c:4559
9787 _ops: &options,
9788 _func: i32,
9789) -> i32 {
9790 1 // c:4562
9791}
9792
9793/// Port of `bin_print(char *name, char **args, Options ops, int func)` from Src/builtin.c:4587.
9794/// C: `int bin_print(char *name, char **args, Options ops, int func)`.
9795///
9796/// The C body is ~1000 lines: `print` / `echo` / `printf` / `pushln`
9797/// dispatcher with -n/-N/-c/-r/-R/-l/-D/-i/-f/-v/-s/-S/-z/-e/-E etc.
9798/// The structural port handles the script-friendly subset that the
9799/// daily-driver hits: print/echo plain emission with -n, -l (one per
9800/// line), -r raw, -E newline-only, -- end-of-options. The full -f
9801/// printf format-spec engine and ZLE/history wireups defer to the
9802/// expand_printf_escapes helpers.
9803/// WARNING: param names don't match C — Rust=(name, args, func) vs C=(name, args, ops, func)
9804pub fn bin_print(
9805 name: &str,
9806 args: &[String], // c:4587
9807 ops: &options,
9808 func: i32,
9809) -> i32 {
9810 // c:4659-4684 — "Error check option combinations and option arguments".
9811 // The FIRST thing bin_print does, before any argument work:
9812 // if (OPT_ISSET(ops,'z') + OPT_ISSET(ops,'s') + OPT_ISSET(ops,'S') +
9813 // OPT_ISSET(ops,'v') > 1) {
9814 // zwarnnam(name, "only one of -s, -S, -v, or -z allowed"); return 1; }
9815 // if ((OPT_ISSET(ops,'z') | OPT_ISSET(ops,'s') | OPT_ISSET(ops,'S')) +
9816 // (OPT_ISSET(ops,'c') | OPT_ISSET(ops,'C')) > 1) {
9817 // zwarnnam(name, "-c or -C not allowed with -s, -S, or -z"); return 1; }
9818 // if ((OPT_ISSET(ops,'z') | OPT_ISSET(ops,'v') | OPT_ISSET(ops,'s') |
9819 // OPT_ISSET(ops,'S')) + (OPT_ISSET(ops,'p') | OPT_ISSET(ops,'u')) > 1) {
9820 // zwarnnam(name, "-p or -u not allowed with -s, -S, -v, or -z"); return 1; }
9821 //
9822 // None of the three were ported, so every conflicting combination was
9823 // silently accepted: `print -s -v v a b`, `print -C 2 -s a b` and
9824 // `print -u1 -v v a b` all returned 0 having done something other than what
9825 // zsh does. Note the mixed operators are deliberate — `+` counts DISTINCT
9826 // options in the first test (so `-s -S` is two), while `|` collapses each
9827 // GROUP in the other two before counting, so the test is "one from each
9828 // group", not "two options total".
9829 //
9830 // C's fourth check ("-f not allowed with -c, -C, or -S", c:4679-4683) is
9831 // COMMENTED OUT and must NOT be reinstated: `print -f %s -c a b` is legal
9832 // and prints, as verified against the oracle. The string exists in the
9833 // source but the code is dead — the reason to read the C rather than grep
9834 // its message table.
9835 //
9836 // These precede the `-C` argument validation below, matching C, so
9837 // `print -C 0 -s` reports the conflict rather than the bad column count.
9838 {
9839 let n_sv = i32::from(OPT_ISSET(ops, b'z'))
9840 + i32::from(OPT_ISSET(ops, b's'))
9841 + i32::from(OPT_ISSET(ops, b'S'))
9842 + i32::from(OPT_ISSET(ops, b'v'));
9843 if n_sv > 1 {
9844 zwarnnam(name, "only one of -s, -S, -v, or -z allowed"); // c:4664
9845 return 1; // c:4665
9846 }
9847 let g_zsS = OPT_ISSET(ops, b'z') || OPT_ISSET(ops, b's') || OPT_ISSET(ops, b'S');
9848 let g_cC = OPT_ISSET(ops, b'c') || OPT_ISSET(ops, b'C');
9849 if i32::from(g_zsS) + i32::from(g_cC) > 1 {
9850 zwarnnam(name, "-c or -C not allowed with -s, -S, or -z"); // c:4670
9851 return 1; // c:4671
9852 }
9853 let g_zvsS = OPT_ISSET(ops, b'z')
9854 || OPT_ISSET(ops, b'v')
9855 || OPT_ISSET(ops, b's')
9856 || OPT_ISSET(ops, b'S');
9857 let g_pu = OPT_ISSET(ops, b'p') || OPT_ISSET(ops, b'u');
9858 if i32::from(g_zvsS) + i32::from(g_pu) > 1 {
9859 zwarnnam(name, "-p or -u not allowed with -s, -S, -v, or -z"); // c:4677
9860 return 1; // c:4678
9861 }
9862 }
9863 let nonewline = OPT_ISSET(ops, b'n'); // c:4595
9864 let raw = OPT_ISSET(ops, b'r') || OPT_ISSET(ops, b'R'); // c:4596
9865 // c:4597 — `-l` puts one arg per line. `-c` is "columns" but
9866 // degrades to one-per-line when stdout isn't a tty (terminal-
9867 // width-aware tabular print isn't ported); accept -c here as
9868 // a synonym for -l so `print -c a b c` byte-matches zsh's
9869 // non-tty fallback.
9870 let one_per_line = OPT_ISSET(ops, b'l') || OPT_ISSET(ops, b'c');
9871 let nul_sep = OPT_ISSET(ops, b'N'); // c:5114/5127/5132 — NUL separator
9872 // c:Src/builtin.c — `-D` runs each arg through `dirify()`: if
9873 // it matches a named dir or $HOME-prefix, abbreviate with
9874 // `~`. zsh has the dircache + named-dir table behind it; for
9875 // -c mode (non-interactive) the table mostly contains $HOME
9876 // → "~". Apply that single rewrite for parity with the common
9877 // case. The richer named-dir lookup belongs in a deeper
9878 // dirify port.
9879 let dirify_d = OPT_ISSET(ops, b'D');
9880 let _printf_mode = func == BIN_PRINTF || OPT_HASARG(ops, b'f'); // c:4604
9881 let echo_mode = func == BIN_ECHO;
9882 let _ = (name, raw);
9883
9884 // c:Src/builtin.c:5095-5106 — `-x N` (and `-X N`) require the
9885 // argument to be a positive integer (zstrtol parse with no
9886 // trailing garbage AND value > 0). zsh emits
9887 // `positive integer expected after -x: <arg>` and returns 1
9888 // when either gate fails. The previous Rust port silently
9889 // accepted any -x argument and proceeded to print the rest of
9890 // argv. Bug #525.
9891 if OPT_HASARG(ops, b'x') || OPT_HASARG(ops, b'X') {
9892 let which = if OPT_HASARG(ops, b'X') { b'X' } else { b'x' };
9893 let xarg = OPT_ARG(ops, which).unwrap_or("");
9894 // c:Src/builtin.c:5101 — `expand = zstrtol(xarg, &eptr, 10);
9895 // if (*eptr || expand <= 0) zwarnnam(...positive integer
9896 // expected...); return 1;`. zstrtol parses optional
9897 // leading sign + digits and stops at the first non-digit.
9898 // Reject when eptr isn't end-of-string (trailing garbage)
9899 // OR the signed result is <= 0 (zero / negative).
9900 let valid = xarg.parse::<i64>().map(|n| n > 0).unwrap_or(false);
9901 if !valid {
9902 zwarnnam(
9903 name,
9904 &format!(
9905 "positive integer expected after -{}: {}",
9906 which as char, xarg
9907 ),
9908 );
9909 return 1;
9910 }
9911 }
9912
9913 // c:4633-4685 — destination dispatch. -u FD writes to fd, -s pushes
9914 // to history, -z to ZLE buffer, -v VAR assigns to scalar.
9915 let dest_var: Option<String> = if OPT_HASARG(ops, b'v') {
9916 OPT_ARG(ops, b'v').map(String::from)
9917 } else {
9918 None
9919 };
9920 // c:Src/builtin.c:4827-4828 — `-p` writes to the coprocess fd
9921 // (the canonical `coprocout` global; Src/exec.c:430). When a
9922 // coproc is live, write to that fd; otherwise emit the same
9923 // diagnostic + rc=1 zsh does (Src/builtin.c:5050
9924 // `zwarnnam(name, "-p: no coprocess")`). Bug #388.
9925 let print_dash_p_fd: Option<fs::File> = if OPT_ISSET(ops, b'p') {
9926 let coprocout =
9927 crate::ported::modules::clone::coprocout.load(std::sync::atomic::Ordering::Relaxed);
9928 if coprocout < 0 {
9929 zwarnnam(name, "-p: no coprocess");
9930 return 1;
9931 }
9932 // c:4843 — dup for an owned writer that close-on-drop
9933 // doesn't close the user's original fd.
9934 let dup_fd = unsafe { libc::dup(coprocout) };
9935 if dup_fd < 0 {
9936 zwarnnam(name, "-p: no coprocess");
9937 return 1;
9938 }
9939 use std::os::unix::io::FromRawFd;
9940 Some(unsafe { fs::File::from_raw_fd(dup_fd) })
9941 } else {
9942 None
9943 };
9944 // c:4815-4851 — `-u FD` (and `-p` coprocess) dispatch. Parses FD,
9945 // dup's it for an owned descriptor, opens as a File for writes.
9946 // The previous Rust port silently dropped `-u`, so `print -u 2
9947 // hello` went to stdout instead of stderr.
9948 let dest_fd: Option<fs::File> = if OPT_HASARG(ops, b'u') {
9949 // c:4826
9950 let argptr = OPT_ARG(ops, b'u').unwrap_or("");
9951 // c:4827-4828 — undocumented `-up` aliases to coprocout.
9952 // Rust skip: coprocout isn't wired yet; document the gap.
9953 match argptr.parse::<i32>() {
9954 // c:4835 zstrtol
9955 Ok(fdarg) => {
9956 // c:4843 — `dup(fdarg)` for an owned writer that
9957 // close-on-drop doesn't close the user's original fd.
9958 let dup_fd = unsafe { libc::dup(fdarg) };
9959 if dup_fd < 0 {
9960 zwarnnam(name, &format!("bad file number: {}", fdarg)); // c:4844
9961 return 1; // c:4845
9962 }
9963 use std::os::unix::io::FromRawFd;
9964 Some(unsafe { fs::File::from_raw_fd(dup_fd) }) // c:4847
9965 }
9966 Err(_) => {
9967 zwarnnam(name, &format!("number expected after -u: {}", argptr)); // c:4837
9968 return 1; // c:4838
9969 }
9970 }
9971 } else {
9972 None
9973 };
9974
9975 // c:4604-4612 — printf format-string handling.
9976 if _printf_mode {
9977 // c:Src/builtin.c:4701-4706 — `if (func == BIN_PRINTF) { if
9978 // (!strcmp(*args, "--") && !*++args) { ...not enough args...
9979 // } fmt = *args++; }`. Consume a leading `--` end-of-options
9980 // marker (printf is BINF_SKIPDASH but NOT BINF_DASHDASHVALID,
9981 // so the generic flag-parser doesn't strip `--` — bin_print
9982 // does the strip itself for BIN_PRINTF). Without this,
9983 // `printf -- "%s\n" hi` used `--` as the format string. Bug
9984 // #284.
9985 let args_owned: Vec<String>;
9986 let args: &[String] = if func == BIN_PRINTF
9987 && !OPT_HASARG(ops, b'f')
9988 && !args.is_empty()
9989 && args[0] == "--"
9990 {
9991 // Slice past the `--` for the rest of this arm only.
9992 if args.len() == 1 {
9993 // c:4703 `not enough arguments`.
9994 zwarnnam(name, "not enough arguments");
9995 return 1;
9996 }
9997 args_owned = args[1..].to_vec();
9998 &args_owned
9999 } else {
10000 args
10001 };
10002 let fmt = if let Some(f) = OPT_ARG(ops, b'f') {
10003 f.to_string()
10004 } else if !args.is_empty() {
10005 args[0].clone()
10006 } else {
10007 return 0;
10008 };
10009 let rest: &[String] = if OPT_HASARG(ops, b'f') {
10010 args
10011 } else {
10012 &args[1..]
10013 };
10014 // c:4712-4741 — the -m filter runs BEFORE format rendering:
10015 // first remaining arg is the pattern, later args are kept iff
10016 // pattry matches, and `if (fmt && !*args) return 0;` short-
10017 // circuits an empty post-filter list. The fmt fast-branch
10018 // here previously skipped the filter entirely, so
10019 // `print -m -f '%s|' 'a*' apple banana` rendered every arg.
10020 let m_filtered: Vec<String>;
10021 let rest: &[String] = if OPT_ISSET(ops, b'm') {
10022 if rest.is_empty() {
10023 zwarnnam(name, "no pattern specified"); // c:4723
10024 return 1;
10025 }
10026 let pat = &rest[0];
10027 let mut pat_tok = pat.to_string();
10028 crate::ported::glob::tokenize(&mut pat_tok); // c:4727
10029 let Some(pprog) = crate::ported::pattern::patcompile(&pat_tok, PAT_STATIC, None) else {
10030 zwarnnam(name, &format!("bad pattern: {}", pat)); // c:4730
10031 return 1;
10032 };
10033 m_filtered = rest[1..]
10034 .iter()
10035 .filter(|a| crate::ported::pattern::pattry(&pprog, a)) // c:4735-4737
10036 .cloned()
10037 .collect();
10038 if m_filtered.is_empty() {
10039 return 0; // c:4741 `if (fmt && !*args) return 0;`
10040 }
10041 &m_filtered
10042 } else {
10043 rest
10044 };
10045 // c:4799-4807 — the `-o`/`-O`/`-i` argument sort runs BEFORE
10046 // format rendering. The non-fmt path sorts at the tail of
10047 // bin_print (strmetasort), but this `-f` fast-branch returns
10048 // first, so `print -of '%s\n' foo bar baz` rendered the args
10049 // unsorted. Apply the same strmetasort to the format args here.
10050 let rest_sorted: Vec<String>;
10051 let rest: &[String] = if OPT_ISSET(ops, b'o') || OPT_ISSET(ops, b'O') {
10052 let mut v = rest.to_vec();
10053 let mut sflags: u32 = if OPT_ISSET(ops, b'i') {
10054 SORTIT_IGNORING_CASE as u32 // c:4805
10055 } else {
10056 0
10057 };
10058 if OPT_ISSET(ops, b'O') {
10059 sflags |= SORTIT_BACKWARDS as u32; // c:4806
10060 }
10061 strmetasort(&mut v, sflags, None); // c:4807
10062 rest_sorted = v;
10063 &rest_sorted
10064 } else {
10065 rest
10066 };
10067 // c:5464 — reset the per-run `%d`/`%i` math-error flag; a bad
10068 // math operand during formatting sets it (parse_int_arg) and turns
10069 // the builtin's exit status into 1 without aborting output.
10070 PRINTF_MATH_ERR.with(|c| c.set(false));
10071 // c:builtin.c:5430-5443 — printf returns 1 on unknown
10072 // directive after `zwarnnam(name, "%s: invalid directive",
10073 // start)`. The partial output produced before the bad
10074 // directive is still printed (C emits via fwrite/fprintf
10075 // throughout the format walk).
10076 let (out, bounds, n_targets) = match printf_format(&fmt, rest) {
10077 Ok(s) => s,
10078 Err((partial, msg)) => {
10079 print!("{}", partial); // c: partial output already in fout
10080 use std::io::Write;
10081 let _ = std::io::stdout().flush();
10082 // c:5435 invalid directive / c:5204 argument specifier
10083 // out of range — `msg` carries the full zwarnnam text.
10084 crate::ported::utils::zwarnnam(name, &msg);
10085 return 1; // c:5443
10086 }
10087 };
10088 // c:Src/builtin.c:5427-5429 — apply the `%n` stores. C runs setiparam
10089 // inside the format loop; printf_format has no shell handle, so it
10090 // collected (name, byte_count) and the assignment happens here. Done
10091 // before the -z/-s output branches because in C the variable is set
10092 // during formatting regardless of where the text is routed.
10093 for (nv, cnt) in &n_targets {
10094 crate::ported::params::setiparam(nv, *cnt as i64); // c:5428
10095 }
10096 // c:4854-4856 — `if (OPT_ISSET(ops, 'v') || (fmt && (OPT_ISSET
10097 // (ops, 'z') || OPT_ISSET(ops, 's')))) ASSIGN_MSTREAM(...)`.
10098 // For -f combined with -z or -s, capture output then route
10099 // through the same dispatch as the non-fmt path.
10100 if OPT_ISSET(ops, b'z') {
10101 // c:5564-5565 — push captured output to bufstack.
10102 crate::ported::zle::zle_main::BUFSTACK
10103 .lock()
10104 .unwrap()
10105 .push(out);
10106 return 0;
10107 }
10108 if OPT_ISSET(ops, b's') {
10109 // c:5569-5574 — push captured output as a history entry.
10110 let event_id = crate::ported::hist::prepnexthistent();
10111 crate::ported::hashtable::addhistnode(&out, event_id as i32);
10112 return 0;
10113 }
10114 if let Some(ref v) = dest_var {
10115 // c:builtin.c:5162 — `visarr = v && PM_TYPE(...) == PM_ARRAY`.
10116 // When the -v target is an existing array, the output is
10117 // split into one element per format-reuse cycle (c:5552-5560
10118 // `if (visarr && splits) { ... setaparam(...) }`); otherwise
10119 // the whole output is one scalar (c:5567 `setsparam(...)`).
10120 let visarr = {
10121 use crate::ported::zsh_h::{PM_ARRAY, PM_TYPE};
10122 paramtab()
10123 .read()
10124 .ok()
10125 .and_then(|tab| {
10126 tab.get(v)
10127 .map(|pm| PM_TYPE(pm.node.flags as u32) == PM_ARRAY)
10128 })
10129 .unwrap_or(false)
10130 };
10131 // c:5169-5171 — `splits` is only allocated on the SECOND and
10132 // later cycles (`argp > args`); a single-cycle format leaves
10133 // `splits` NULL, so `if (visarr && splits)` (c:5552) is false
10134 // and the whole output becomes one scalar. `bounds.len() > 1`
10135 // mirrors `splits != NULL`.
10136 if visarr && bounds.len() > 1 {
10137 // c:5553-5559 — slice `out` at each cycle boundary; one
10138 // array element per format application. `bounds` holds the
10139 // byte offset where each cycle began (the first is 0).
10140 let mut arrayval: Vec<String> = Vec::with_capacity(bounds.len());
10141 for (i, &start) in bounds.iter().enumerate() {
10142 let end = bounds.get(i + 1).copied().unwrap_or(out.len());
10143 arrayval.push(out[start..end].to_string());
10144 }
10145 setaparam(v, arrayval);
10146 } else {
10147 setsparam(v, &out);
10148 }
10149 } else {
10150 // c:Src/builtin.c — C printf goes through libc fwrite to
10151 // stdout, unbuffered when fd 1 is the redirect target.
10152 // Rust's `print!` is line-buffered; without a trailing
10153 // newline the bytes stay in the buffer past the redirect
10154 // restore and land on the original stdout. Route through
10155 // stdout().write_all + flush so the bytes hit fd 1 (the
10156 // redirect target) immediately. Bug #397.
10157 use std::io::Write as _;
10158 let stdout = io::stdout();
10159 let mut lk = stdout.lock();
10160 // c:Src/builtin.c:5408 — `print_val(unmetafy(stringval,
10161 // &curlen))`: printf's values are unmetafied before the
10162 // write. Decode Meta-char pairs to raw bytes. Bug #127.
10163 let _ = lk.write_all(&crate::ported::utils::unmetafy_str(&out));
10164 let _ = lk.flush();
10165 }
10166 // c:5464 — a `%d`/`%i` math-operand error makes printf exit 1
10167 // (output already emitted).
10168 return if PRINTF_MATH_ERR.with(|c| c.get()) {
10169 1
10170 } else {
10171 0
10172 };
10173 }
10174
10175 // c:4718-4741 — `-m PATTERN args...` glob-filter. First arg is
10176 // the pattern; remaining args are kept iff `pattry(pat, arg)`.
10177 // Previously absent — `print -m 'foo*' foo1 bar foo2` emitted
10178 // all four args instead of just foo1/foo2.
10179 let mut processed_args: Vec<String> = if OPT_ISSET(ops, b'm') {
10180 // c:4718
10181 if args.is_empty() {
10182 // c:4722
10183 zwarnnam(name, "no pattern specified"); // c:4723
10184 return 1; // c:4724
10185 }
10186 // c:4728 — `patcompile(*args, PAT_STATIC, NULL)`.
10187 let pat = &args[0];
10188 let pprog = patcompile(
10189 &{
10190 let mut __pat_tok = (pat).to_string();
10191 crate::ported::glob::tokenize(&mut __pat_tok);
10192 __pat_tok
10193 },
10194 PAT_STATIC,
10195 None,
10196 );
10197 match pprog {
10198 None => {
10199 zwarnnam(name, &format!("bad pattern: {}", pat)); // c:4730
10200 return 1; // c:4732
10201 }
10202 Some(prog) => {
10203 // c:4734-4737 — `for (t = p = ++args; *p; p++) if
10204 // (pattry(pprog, *p)) *t++ = *p;`. Keep matching args.
10205 args[1..]
10206 .iter()
10207 .filter(|a| pattry(&prog, a))
10208 .cloned()
10209 .collect()
10210 }
10211 }
10212 } else {
10213 args.to_vec()
10214 };
10215
10216 // c:4860+ — main print loop.
10217 // c:5126-5127 — separator priority: `-l` ('\n') > `-N` ('\0') > ' '.
10218 let sep = if one_per_line {
10219 "\n"
10220 } else if nul_sep {
10221 "\0"
10222 } else {
10223 " "
10224 };
10225 // c:Src/builtin.c:4783-4795 — `-D`: interpret each arg as a
10226 // directory and abbreviate via `finddir(args[n])` — longest-prefix
10227 // match against the full named-dir table ($HOME → `~`, `hash -d`
10228 // entries → `~name`, plus the `zsh_directory_name` hook). C rewrites
10229 // the matched prefix to `~name` and keeps the trailing path. The
10230 // canonical `finddir` port already returns the `~name/rest` form.
10231 if dirify_d {
10232 for a in processed_args.iter_mut() {
10233 if let Some(abbrev) = crate::ported::utils::finddir(a) {
10234 *a = abbrev; // c:4791 — `~%s%s`
10235 }
10236 }
10237 }
10238 // c:4598-4600 — `-P` prompt-style percent expansion (`%n`, `%d`,
10239 // `%?`, `%h`, `%%`, etc.). Routes through `expand_prompt`
10240 // (canonical port of `Src/prompt.c:182 promptexpand`).
10241 if OPT_ISSET(ops, b'P') {
10242 // c:Src/builtin.c:4745-4746 — `if (OPT_ISSET(ops, 'P'))
10243 // txtunknownattrs = TXT_ATTR_ALL;`. Marks every attribute
10244 // as initially "unknown" so the first `%b`/`%u`/`%s`/`%f`/`%k`
10245 // off-escape in this print invocation emits its terminfo cap
10246 // (via tunsetattrs at prompt.c:1758 — `txtcurrentattrs |=
10247 // newattrs & txtunknownattrs`). Without this seed, applying
10248 // an off-escape against zero current attrs produced no diff
10249 // and zshrs emitted nothing — `%b` / `%u` / `%s` looked like
10250 // no-ops in fresh prompts. Bug #38 in docs/BUGS.md.
10251 crate::ported::prompt::txtunknownattrs.store(
10252 crate::ported::zsh_h::TXT_ATTR_ALL,
10253 std::sync::atomic::Ordering::SeqCst,
10254 );
10255 // c:4598-4600 — `-P` prompt-style percent expansion.
10256 for a in processed_args.iter_mut() {
10257 *a = crate::ported::prompt::expand_prompt(a); // c:Src/prompt.c:182
10258 // c:Src/prompt.c:236-247 — `if (!ns) { ... chuck(Inpar/
10259 // Outpar/Nularg); }`. When `ns=0` (non-stripping flag
10260 // off), zsh REMOVES the Inpar/Outpar/Nularg marker bytes
10261 // from the output. `print -P` calls promptexpand with
10262 // `ns=0` per Src/builtin.c:4598, so the SGR-wrapping
10263 // markers MUST NOT leak into stdout. The Rust port's
10264 // expand_prompt uses ad-hoc `\x01`/`\x02` (readline
10265 // RL_PROMPT_*_IGNORE) markers instead of canonical
10266 // Inpar/Outpar, but the strip rule applies identically:
10267 // for non-prompt-render callers, scrub them. Parity bug
10268 // #17 — without this, `print -P "%F{red}red%f"` emitted
10269 // `\x01\E[31m\x02red\x01\E[39m\x02` instead of zsh's
10270 // `\E[31mred\E[39m`.
10271 a.retain(|c| c != '\x01' && c != '\x02');
10272 }
10273 }
10274 // c:4799-4808 — `-o` / `-O` / `-i` sort flags.
10275 //
10276 // C body:
10277 // ```c
10278 // if (OPT_ISSET(ops,'o') || OPT_ISSET(ops,'O')) {
10279 // flags = OPT_ISSET(ops,'i') ? SORTIT_IGNORING_CASE : 0;
10280 // if (OPT_ISSET(ops,'O'))
10281 // flags |= SORTIT_BACKWARDS;
10282 // strmetasort(args, flags, len);
10283 // }
10284 // ```
10285 //
10286 // Meaning: `-i` sets `SORTIT_IGNORING_CASE` (case-INSENSITIVE).
10287 // Without `-i`, sort is case-SENSITIVE.
10288 //
10289 // The previous Rust port had this INVERTED — it bound
10290 // `case_sensitive = OPT_ISSET(ops, b'i')`, then case-sensitive-
10291 // sorted under `-i` and case-insensitive-sorted without `-i`.
10292 // The doc-comment for the block claimed "-o → case-insensitive
10293 // ascending" which is also wrong. Result: `print -o foo Bar BAZ`
10294 // emitted `BAZ Bar foo` (case-insensitive) when zsh emits
10295 // `BAZ Bar foo` only WITH `-i`; without it, zsh emits
10296 // `BAZ Bar foo` ordered by ASCII (caps first).
10297 if OPT_ISSET(ops, b'o') || OPT_ISSET(ops, b'O') {
10298 // c:4800
10299 // c:4801-4807 — `flags = OPT_ISSET(ops,'i') ? SORTIT_IGNORING_CASE
10300 // : 0; if (OPT_ISSET(ops,'O')) flags |= SORTIT_BACKWARDS;
10301 // strmetasort(args, flags, len);`. The previous Rust port used an
10302 // ad-hoc `processed_args.sort()` (byte/ordinal order) plus a
10303 // `.reverse()`, bypassing the faithful comparator. zsh's eltpcmp
10304 // sorts via `strcoll(as, bs)` (sort.c:134) — locale collation,
10305 // case-insensitive in UTF-8 locales — and SORTIT_BACKWARDS flips
10306 // `sortdir` inside the comparator (not a post-sort reverse).
10307 let mut flags: u32 = if OPT_ISSET(ops, b'i') {
10308 SORTIT_IGNORING_CASE as u32 // c:4805
10309 } else {
10310 0
10311 };
10312 if OPT_ISSET(ops, b'O') {
10313 flags |= SORTIT_BACKWARDS as u32; // c:4806
10314 }
10315 strmetasort(&mut processed_args, flags, None); // c:4807
10316 }
10317 // c:Src/builtin.c:4866-4886 — when `-r` is NOT set, each arg goes
10318 // through `getkeystring` to interpret backslash escapes (`\n`,
10319 // `\t`, `\\`, escaped space `\ `, etc.). `echo` follows the same
10320 // path when `BSD_ECHO`/`SH_OPTION_LETTERS`-style isn't in effect;
10321 // BIN_ECHO with `-E` keeps escapes literal. Without this, `print
10322 // -- ${(q)a}` for `a="he llo"` emitted `he\ llo` instead of zsh's
10323 // `he llo` (the (q) flag's backslash gets consumed by print).
10324 // c:builtin.c:4747-4767 — escape interpretation dispatch.
10325 // - `fmt` (printf format already chosen via -f) or
10326 // `(!-e && (-R || -r || -E))` → unmetafy only, NO escape
10327 // interpretation (raw passthrough).
10328 // - Otherwise pick `escape_how` per c:4754-4760:
10329 // `-b` → GETKEYS_BINDKEY (bindkey
10330 // escapes: `\C-`/`\M-`/`^X`)
10331 // func != BIN_ECHO && !`-e` → GETKEYS_PRINT (with EMACS:
10332 // unknown `\<c>` → `<c>`)
10333 // else (BIN_ECHO or `-e`) → GETKEYS_ECHO (preserves
10334 // unknown `\<c>` as `\<c>`)
10335 //
10336 // Previous Rust port unconditionally used GETKEYS_PRINT for both
10337 // `echo` and `print` — `echo "${(qq)s}"` for `s="a'b"` stripped
10338 // the `\` from the `(qq)`-emitted `'a'\''b'` because GETKEYS_PRINT
10339 // includes GETKEY_EMACS. zsh keeps the `\` (echo uses GETKEYS_ECHO,
10340 // no EMACS).
10341 let dash_e = OPT_ISSET(ops, b'e');
10342 // c:Src/builtin.c:4754 — BSD_ECHO option flips echo's default:
10343 // escape processing is OFF unless `-e` is explicitly passed.
10344 // Without bsd_echo (the SysV default), escapes process unless
10345 // `-E`/`-R`/`-r` is set.
10346 let bsd_echo_active = echo_mode && isset(BSDECHO);
10347 let suppress_escapes = OPT_ISSET(ops, b'R')
10348 || OPT_ISSET(ops, b'r')
10349 || (echo_mode && OPT_ISSET(ops, b'E'))
10350 || (bsd_echo_active && !dash_e);
10351 let mut backslash_c_truncated = false;
10352 if !suppress_escapes || dash_e {
10353 // c:builtin.c:4754-4760 — `-b` (bindkey escapes) takes precedence,
10354 // then GETKEYS_PRINT for print (EMACS), else GETKEYS_ECHO.
10355 let escape_how: u32 = if OPT_ISSET(ops, b'b') {
10356 GETKEYS_BINDKEY // c:4755
10357 } else if !echo_mode && !dash_e {
10358 GETKEYS_PRINT // c:4758
10359 } else {
10360 GETKEYS_ECHO // c:4760
10361 };
10362 // Clear any stale TLS flag before the loop.
10363 let _ = crate::ported::utils::getkey_truncated_take();
10364 let mut new_args: Vec<String> = Vec::with_capacity(processed_args.len());
10365 for a in processed_args.iter() {
10366 let (s, _) = getkeystring_with(a, escape_how, None);
10367 new_args.push(s);
10368 if crate::ported::utils::getkey_truncated_take() {
10369 // c:utils.c:7045 — `\c` truncated; drop remaining
10370 // args entirely AND suppress trailing newline.
10371 backslash_c_truncated = true;
10372 break;
10373 }
10374 }
10375 processed_args = new_args;
10376 }
10377 // c:Src/builtin.c:4930-4958 — `-C N` column-grid output. Layout
10378 // N args per row (nr = ceil(argc/nc) rows), each cell padded
10379 // to widest arg + 2 spaces. Default mode is COLUMN-MAJOR (col 1
10380 // takes first nr items, col 2 the next nr, etc.). The `-a` flag
10381 // (c:4980 "print across, i.e. columns first") switches to
10382 // ROW-MAJOR fill — items flow across each row before moving to
10383 // the next row. Bug #40 in docs/BUGS.md: zshrs ignored `-a` and
10384 // always produced column-major output.
10385 let body = if !_printf_mode && OPT_HASARG(ops, b'C') {
10386 // c:4687-4698 — `-C` validates its argument two ways and FAILS; it does
10387 // not fall back:
10388 // nc = (int)zstrtol(argptr, &eptr, 10);
10389 // if (*eptr) {
10390 // zwarnnam(name, "number expected after -%c: %s", 'C', argptr);
10391 // return 1;
10392 // }
10393 // if (nc <= 0) {
10394 // zwarnnam(name, "invalid number of columns: %s", argptr);
10395 // return 1;
10396 // }
10397 // This parsed as usize, filtered `n > 0`, and silently
10398 // `.unwrap_or(1)`, so `print -C 0`, `print -C -1`, `print -C abc` and
10399 // `print -C ''` all printed one column per line and returned 0 where
10400 // zsh fails. zstrtol is what separates the two messages: trailing
10401 // garbage leaves *eptr set ("3x" → "number expected"), while a
10402 // well-formed non-positive parses cleanly and reaches the second check
10403 // ("-1" → "invalid number of columns"). Leading blanks are skipped by
10404 // zstrtol, so `-C ' 2'` is valid.
10405 let argptr = OPT_ARG(ops, b'C').unwrap_or("");
10406 let (nc_l, rest) = crate::ported::utils::zstrtol_underscore(argptr, 10, false); // c:4689
10407 if !rest.is_empty() {
10408 zwarnnam(name, &format!("number expected after -C: {argptr}")); // c:4691
10409 return 1; // c:4692
10410 }
10411 if nc_l <= 0 {
10412 zwarnnam(name, &format!("invalid number of columns: {argptr}")); // c:4695
10413 return 1; // c:4696
10414 }
10415 let nc: usize = nc_l as usize;
10416 let argc = processed_args.len();
10417 let nr = (argc + nc - 1) / nc;
10418 let across = OPT_ISSET(ops, b'a'); // c:4947 / c:4980
10419 // c:Src/builtin.c:4946-4956 — max-width walk skips
10420 // last-column items because they don't need trailing
10421 // padding. The skip set differs by mode:
10422 // -a (row-major): skip i where (i % nc) == nc-1
10423 // default (col-major): skip i where i >= nr * (nc-1)
10424 let max_w = processed_args
10425 .iter()
10426 .enumerate()
10427 .filter(|(i, _)| {
10428 if across {
10429 (i % nc) != nc - 1
10430 } else {
10431 *i < nr * (nc.saturating_sub(1))
10432 }
10433 })
10434 .map(|(_, s)| s.chars().count())
10435 .max()
10436 .unwrap_or(0);
10437 let sc = max_w + 2;
10438 let mut out = String::new();
10439 for row in 0..nr {
10440 for col in 0..nc {
10441 // c:4982-4986 (-a) — `idx = row*nc + col` for
10442 // row-major fill across columns first.
10443 // c:4994-5005 (default) — `idx = col*nr + row` for
10444 // column-major fill down columns first.
10445 let idx = if across {
10446 row * nc + col
10447 } else {
10448 col * nr + row
10449 };
10450 if idx >= argc {
10451 break;
10452 }
10453 let cell = &processed_args[idx];
10454 // Padding skip rule mirrors max_w gate:
10455 // last-column-of-row (-a) or last-column-of-grid
10456 // (default) gets no trailing padding.
10457 let is_last_col_of_row = col == nc - 1;
10458 let is_after_last_grid_col = !across && col * nr + row + nr >= argc;
10459 if is_last_col_of_row || is_after_last_grid_col {
10460 out.push_str(cell);
10461 } else {
10462 out.push_str(cell);
10463 let pad = sc.saturating_sub(cell.chars().count());
10464 out.extend(std::iter::repeat(' ').take(pad));
10465 }
10466 }
10467 out.push('\n');
10468 }
10469 // Strip trailing newline; the post-loop `if !nonewline` adds
10470 // one back.
10471 if out.ends_with('\n') {
10472 out.pop();
10473 }
10474 out
10475 } else if OPT_HASARG(ops, b'x') || OPT_HASARG(ops, b'X') {
10476 // c:Src/builtin.c:5095-5125 — `print -x N` / `-X N` expand tabs
10477 // to spaces at tab stops of width N (`-x` only leading tabs,
10478 // `-X` all tabs), threading the column position across args and
10479 // resetting it on the `-l` newline. zexpandtabs (utils.c:5975)
10480 // was ported but never wired into print output.
10481 let all = OPT_HASARG(ops, b'X');
10482 let which = if all { b'X' } else { b'x' };
10483 let width: i32 = OPT_ARG(ops, which)
10484 .and_then(|a| a.parse().ok())
10485 .unwrap_or(8);
10486 let n = processed_args.len();
10487 let mut out = String::new();
10488 let mut startpos = 0i32;
10489 for (i, arg) in processed_args.iter().enumerate() {
10490 startpos = crate::ported::utils::zexpandtabs(arg, width, startpos, all, &mut out);
10491 if i + 1 < n {
10492 if OPT_ISSET(ops, b'l') {
10493 out.push('\n');
10494 startpos = 0;
10495 } else if OPT_ISSET(ops, b'N') {
10496 out.push('\0');
10497 } else {
10498 out.push(' ');
10499 startpos += 1;
10500 }
10501 }
10502 }
10503 out
10504 } else {
10505 processed_args.join(sep)
10506 };
10507 // c:5564-5575 — destination dispatch order:
10508 // -z → zpushnode(bufstack, stringval)
10509 // -v → setsparam(VAR, stringval)
10510 // -s → prepnexthistent() + addhistnode(histtab, stringval)
10511 // else → fwrite to fout
10512 if OPT_ISSET(ops, b'z') {
10513 // c:5564-5565 — `zpushnode(bufstack, stringval)`. The ZLE
10514 // bufstack is consumed by the next zleread call so the
10515 // string is presented at the prompt — `print -z 'echo foo'`
10516 // queues `echo foo` for the user to press Enter on.
10517 crate::ported::zle::zle_main::BUFSTACK
10518 .lock()
10519 .unwrap()
10520 .push(body); // c:5565
10521 return 0;
10522 }
10523 if OPT_ISSET(ops, b's') || OPT_ISSET(ops, b'S') {
10524 // c:5047-5093 — `-s` / `-S` push captured output to the history
10525 // list. `-S` requires exactly one arg (c:5058-5062
10526 // `if (nwords > 1) zwarnnam(name, "option -S takes a single
10527 // argument"); return 1`); the words array is then populated by
10528 // `histsplitwords` (c:5065). `-s` joins all args with spaces
10529 // and stores them as a single history entry. Both paths call
10530 // `prepnexthistent` + `addhistnode(histtab, ent->node.nam, ent)`.
10531 if OPT_ISSET(ops, b'S') && processed_args.len() > 1 {
10532 // c:5059
10533 zwarnnam(name, "option -S takes a single argument"); // c:5059
10534 return 1; // c:5061
10535 }
10536 let event_id = crate::ported::hist::prepnexthistent(); // c:5066/5072
10537 crate::ported::hashtable::addhistnode(&body, event_id as i32); // c:5090
10538 // c:Src/builtin.c — C's `addhistnode(histtab, …)` uses the
10539 // ent ALREADY linked into hist_ring by prepnexthistent. zshrs's
10540 // prepnexthistent only bumps curhist and doesn't insert into
10541 // the ring, so `fc -l` finds no events. Push the ent into the
10542 // ring here so the gethistent lookup in fc/history sees it.
10543 // Without this, `print -s X; fc -l` reported "no such event: 1"
10544 // even though the histtab had the entry.
10545 let now = std::time::SystemTime::now()
10546 .duration_since(std::time::UNIX_EPOCH)
10547 .map(|d| d.as_secs() as i64)
10548 .unwrap_or(0);
10549 let ent = crate::ported::zsh_h::histent {
10550 node: crate::ported::zsh_h::hashnode {
10551 next: None,
10552 nam: body.clone(),
10553 flags: 0,
10554 },
10555 up: None,
10556 down: None,
10557 zle_text: None,
10558 stim: now,
10559 ftim: now,
10560 words: Vec::new(),
10561 nwords: 0,
10562 histnum: event_id,
10563 };
10564 if let Ok(mut ring) = crate::ported::hist::hist_ring.lock() {
10565 ring.insert(0, ent);
10566 crate::ported::hist::histlinect.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
10567 }
10568 return 0;
10569 }
10570 if let Some(ref v) = dest_var {
10571 // c:Src/builtin.c:544-546 — the trailing separator is part of the
10572 // captured `-v` value too: it is written to the same stream and read
10573 // back into `buf`. It is suppressed only when `-n`, a `\c` escape
10574 // (`nnl`), OR `-v` WITHOUT `-l` apply. So `print -v x -l a b c` stores
10575 // `a\nb\nc\n` (with the trailing newline) where `print -v x a b c`
10576 // stores `a b c` (none). The port previously suppressed it for every
10577 // `-v`, dropping the `-l` trailing newline.
10578 let suppress_term =
10579 nonewline || backslash_c_truncated || !OPT_ISSET(ops, b'l');
10580 let mut val = body.clone();
10581 if !suppress_term {
10582 val.push(if nul_sep { '\0' } else { '\n' }); // c:546
10583 }
10584 setsparam(v, &val);
10585 } else {
10586 // c:5130-5132 — final terminator: `-n` suppresses; `-N` emits
10587 // NUL instead of newline; else newline. `\c` truncation
10588 // (c:utils.c:7045) also suppresses — matches zsh's
10589 // `echo "a\cb"; echo END` → `aEND`.
10590 let final_term: &[u8] = if nonewline || backslash_c_truncated {
10591 b""
10592 } else if nul_sep {
10593 b"\0"
10594 } else {
10595 b"\n"
10596 };
10597 // c:Bug #388 — `-p` and `-u` are mutually exclusive but both
10598 // route to the same write-to-fd path. Prefer the `-p`-resolved
10599 // coprocout (when present) over the `-u`-resolved fd. If
10600 // neither, fall through to stdout below.
10601 let dest_fd_active = print_dash_p_fd.or(dest_fd);
10602 // c:Src/builtin.c:4752 — `unmetafy(args[n], &len[n])`: print's
10603 // args are unmetafied before the fwrite. zshrs keeps values
10604 // metafied (Meta-char pairs, vm_helper::meta_encode_byte)
10605 // until the byte boundary, so decode here — `print $'\xff'`
10606 // writes the single raw byte 0xff. Bug #127.
10607 let body_bytes = crate::ported::utils::unmetafy_str(&body);
10608 if let Some(mut f) = dest_fd_active {
10609 // c:4847 — write to dup'd file descriptor.
10610 use std::io::Write as _;
10611 let _ = f.write_all(&body_bytes); // c:5124 fwrite
10612 let _ = f.write_all(final_term); // c:5132
10613 // f closes on drop (close(dup_fd)) — user's original fd
10614 // remains open per c:4843 dup semantics.
10615 } else {
10616 // stdout path. -N writes NUL via raw stdout; print!/println!
10617 // would mangle a NUL inside a String literal via format
10618 // machinery, so route through stdout().write_all directly.
10619 use std::io::Write as _;
10620 let stdout = io::stdout();
10621 let mut lk = stdout.lock();
10622 let _ = lk.write_all(&body_bytes); // c:5124
10623 let _ = lk.write_all(final_term); // c:5132
10624 // c:Src/builtin.c — C printf goes through libc fwrite to
10625 // stdout (fd 1) which is unbuffered when the builtin runs
10626 // under a redirect (because dup2 to fd 1 puts the file
10627 // there). Rust's `io::stdout()` is LINE-buffered, so
10628 // `printf "abcde"` (no `\n`) leaves bytes pending in the
10629 // userspace buffer; when the surrounding `> file` redirect
10630 // closes the dup'd fd, those bytes flush AFTER the restore
10631 // — landing on the original stdout (terminal) AND missing
10632 // the file. Flush here so the bytes hit fd 1 (the redirect
10633 // target) before bin_print returns. Bug #397.
10634 let _ = lk.flush();
10635 }
10636 }
10637 0
10638}
10639
10640/// Port of `bin_shift(char *name, char **argv, Options ops, UNUSED(int func))` from Src/builtin.c:5593.
10641/// C: `int bin_shift(char *name, char **argv, Options ops, UNUSED(int func))`
10642/// — shift positional params (or named arrays) by `num` positions; `-p`
10643/// pops from the right end.
10644/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
10645pub fn bin_shift(
10646 name: &str,
10647 argv: &[String], // c:5593
10648 ops: &options,
10649 _func: i32,
10650) -> i32 {
10651 let mut num: i32 = 1; // c:5595
10652 let mut ret: i32 = 0; // c:5595
10653 let mut idx = 0usize;
10654 queue_signals(); // c:5599
10655 // c:5600-5605 — first arg parsed as math expr unless it's an array name.
10656 if !argv.is_empty() {
10657 // c:5600
10658 let first = &argv[0];
10659 // c:5600 — `if (!getaparam(*argv))` decides whether the arg is
10660 // a numeric shift-count vs an array name. Check
10661 // paramtab for a PM_ARRAY entry, not OS env.
10662 let is_array = {
10663 use {PM_ARRAY, PM_TYPE};
10664 let tab = paramtab().read().unwrap();
10665 tab.get(first)
10666 .map(|pm| PM_TYPE(pm.node.flags as u32) == PM_ARRAY)
10667 .unwrap_or(false)
10668 };
10669 if !is_array {
10670 // c:5600
10671 // c:5601 — `num = mathevali(*argv++);`. The previous Rust port
10672 // used `parse::<i32>()` which rejects any non-trivial
10673 // arithmetic: `shift 1+2` would silently return ret=1
10674 // instead of shifting by 3. Route through mathevali.
10675 num = mathevali(first).unwrap_or_else(|_| {
10676 ret = 1;
10677 0
10678 }) as i32; // c:5601
10679 idx = 1;
10680 // c:5602-5605 — `if (errflag) return 1;`.
10681 if ret != 0 || errflag.load(Relaxed) != 0 {
10682 unqueue_signals(); // c:5604
10683 return 1;
10684 }
10685 }
10686 }
10687
10688 // c:5608-5611 — `if (num < 0)` reject.
10689 if num < 0 {
10690 // c:5608
10691 unqueue_signals(); // c:5609
10692 zwarnnam(name, "argument to shift must be non-negative"); // c:5610
10693 return 1; // c:5611
10694 }
10695
10696 // c:5614-5635 — named-array shift loop.
10697 if idx < argv.len() {
10698 // c:5614
10699 for arr_name in &argv[idx..] {
10700 // c:5615
10701 // c:5616 — `if ((s = getaparam(*argv)))` else silent skip.
10702 // Read paramtab directly; was approximating arrays
10703 // as `:`-separated env values which is wrong (env
10704 // can never carry array structure).
10705 let s: Vec<String> = {
10706 let tab = paramtab().read().unwrap();
10707 match tab.get(arr_name).and_then(|pm| pm.u_arr.clone()) {
10708 Some(arr) => arr,
10709 None => continue,
10710 }
10711 };
10712 // c:5617-5621 — arrlen_lt check.
10713 if (s.len() as i32) < num {
10714 // c:5617
10715 zwarnnam(name, "shift count must be <= $#"); // c:5618
10716 ret += 1; // c:5619
10717 continue; // c:5620
10718 }
10719 // c:5622-5634 — -p shifts off the right end, otherwise the left.
10720 let s2: Vec<String> = if OPT_ISSET(ops, b'p') {
10721 // c:5622
10722 s[..s.len() - num as usize].to_vec() // c:5625-5628
10723 } else {
10724 s[num as usize..].to_vec() // c:5631
10725 };
10726 // c:5633 — `setaparam(*argv, s);`. Write the shifted array
10727 // back to paramtab as a proper PM_ARRAY. Was a
10728 // fake: `env::set_var` + colon-joined fake-array
10729 // which neither carries array structure nor
10730 // reaches subsequent `${arr_name[@]}` expansions.
10731 setaparam(arr_name, s2);
10732 }
10733 } else {
10734 // c:5636-5654 — shift positional parameters ($1..$N).
10735 // Static-link path: positional params live in src/ported/vm_helper;
10736 // expose via PPARAMS Mutex<Vec<String>>.
10737 let mut pp = PPARAMS.lock().unwrap_or_else(|e| {
10738 PPARAMS.clear_poison();
10739 e.into_inner()
10740 });
10741 let l = pp.len() as i32;
10742 if num > l {
10743 // c:5636
10744 zwarnnam(name, "shift count must be <= $#"); // c:5637
10745 ret = 1; // c:5638
10746 } else if OPT_ISSET(ops, b'p') {
10747 // c:5641
10748 pp.truncate((l - num) as usize); // c:5642-5644
10749 } else {
10750 pp.drain(..num as usize); // c:5646-5650
10751 }
10752 // PPARAMS is the single source of truth. fusevm-side reads
10753 // route through exec.pparams() which reads PPARAMS, so the
10754 // shift is immediately visible — no exec.positional_params
10755 // mirror needed.
10756 drop(pp);
10757 }
10758 unqueue_signals(); // c:5658
10759 ret // c:5659
10760}
10761
10762/// Port of `bin_getopts(UNUSED(char *name), char **argv, UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:5672.
10763/// C: `int bin_getopts(UNUSED(char *name), char **argv, UNUSED(Options ops),
10764/// UNUSED(int func))`.
10765///
10766/// POSIX getopts. Maintains state in $OPTIND (zoptind) and an internal
10767/// per-arg cursor (optcind). Reads from the script's positional params
10768/// when no extra args supplied, otherwise from the trailing argv.
10769/// WARNING: param names don't match C — Rust=(_name, argv, _func) vs C=(name, argv, ops, func)
10770pub fn bin_getopts(
10771 _name: &str,
10772 argv: &[String], // c:5672
10773 _ops: &options,
10774 _func: i32,
10775) -> i32 {
10776 if argv.len() < 2 {
10777 return 1;
10778 }
10779 // c:5675 — `char *optstr = unmetafy(*argv++, &lenoptstr); char *var = *argv++;`
10780 let optstr_full = argv[0].clone();
10781 let var = argv[1].clone();
10782 // c:5676 — `char **args = (*argv) ? argv : pparams;`
10783 let argv_rest: Vec<String> = argv[2..].to_vec();
10784 let args: Vec<String> = if !argv_rest.is_empty() {
10785 argv_rest
10786 } else {
10787 PPARAMS.lock().map(|p| p.clone()).unwrap_or_default()
10788 };
10789
10790 // c:Src/builtin.c:5680 — at entry, re-sync the internal zoptind
10791 // tracker against the user-visible $OPTIND param. zsh exposes
10792 // OPTIND as a writable param so scripts can reset it to 1 to
10793 // re-parse positionals; the Rust port had `ZOPTIND` as an atomic
10794 // that was the AUTHORITY (writes to $OPTIND didn't propagate
10795 // back), so `OPTIND=1` between two getopts loops left zoptind at
10796 // the post-loop value and the second pass returned immediately.
10797 let paramtab_oi = getiparam("OPTIND");
10798 let mut zoptind = if paramtab_oi >= 1 {
10799 paramtab_oi as i32
10800 } else {
10801 ZOPTIND.load(Relaxed)
10802 };
10803 // c:5681-5685 — `if (zoptind < 1) { zoptind = 1; optcind = 0; }`
10804 if zoptind < 1 {
10805 // c:5681
10806 zoptind = 1;
10807 OPTCIND.store(0, Relaxed);
10808 }
10809 // c:Src/builtin.c — when $OPTIND was just reset to 1 (i.e. the
10810 // user-visible param disagrees with the previous internal
10811 // pointer), reset optcind so the new pass starts at byte 0 of
10812 // the first option arg.
10813 let mut optcind = if paramtab_oi == 1 && ZOPTIND.load(Relaxed) != 1 {
10814 0
10815 } else {
10816 OPTCIND.load(Relaxed)
10817 };
10818
10819 // c:5686-5688 — `if (arrlen_lt(args, zoptind)) return 1;`
10820 if (args.len() as i32) < zoptind {
10821 // c:5686
10822 ZOPTIND.store(zoptind, Relaxed);
10823 return 1;
10824 }
10825
10826 // c:5691-5693 — `quiet = *optstr == ':'; optstr += quiet; lenoptstr -= quiet;`
10827 let (quiet, optstr) = if optstr_full.starts_with(':') {
10828 // c:5691
10829 (true, &optstr_full[1..])
10830 } else {
10831 (false, optstr_full.as_str())
10832 };
10833
10834 // c:5696 — `str = unmetafy(dupstring(args[zoptind - 1]), &lenstr);`
10835 let mut str_buf = args[(zoptind - 1) as usize].clone();
10836 let mut lenstr = str_buf.len() as i32;
10837 if lenstr == 0 {
10838 return 1;
10839 } // c:5697
10840
10841 // c:5699-5703 — bump to next arg if optcind exhausted current.
10842 if optcind >= lenstr {
10843 // c:5699
10844 optcind = 0;
10845 zoptind += 1;
10846 if zoptind as usize > args.len() {
10847 // c:5701
10848 ZOPTIND.store(zoptind, Relaxed);
10849 OPTCIND.store(optcind, Relaxed);
10850 setiparam("OPTIND", zoptind as i64); // c:5702
10851 return 1;
10852 }
10853 str_buf = args[(zoptind - 1) as usize].clone();
10854 lenstr = str_buf.len() as i32;
10855 }
10856
10857 // c:5705-5712 — first option char checks: not `-`/`+` → done; `--` → done.
10858 if optcind == 0 {
10859 // c:5705
10860 if lenstr < 2 || (!str_buf.starts_with('-') && !str_buf.starts_with('+')) {
10861 ZOPTIND.store(zoptind, Relaxed);
10862 OPTCIND.store(optcind, Relaxed);
10863 // c:5707 — mirror to $OPTIND so callers see the post-loop
10864 // pointer. Previous Rust port skipped this write on the
10865 // "no more options" exit; OPTIND stayed at the last
10866 // option arg index (-b) instead of advancing past it.
10867 setiparam("OPTIND", zoptind as i64);
10868 return 1;
10869 }
10870 if lenstr == 2 && &str_buf[..2] == "--" {
10871 // c:5708
10872 zoptind += 1;
10873 ZOPTIND.store(zoptind, Relaxed);
10874 OPTCIND.store(0, Relaxed);
10875 setiparam("OPTIND", zoptind as i64); // c:5711
10876 return 1;
10877 }
10878 optcind += 1;
10879 }
10880 // c:5715 — `opch = str[optcind++];`
10881 let opch = str_buf.as_bytes()[optcind as usize];
10882 optcind += 1;
10883
10884 // c:5716-5721 — `lenoptbuf = (str[0] == '+') ? 2 : 1; optbuf[lenoptbuf-1] = opch;`
10885 let plus = str_buf.starts_with('+');
10886 let optbuf: String = if plus {
10887 format!("+{}", opch as char)
10888 } else {
10889 format!("{}", opch as char)
10890 };
10891
10892 // c:5724-5740 — illegal option: `?` reply, OPTIND fixed under POSIXBUILTINS.
10893 let posix = isset(POSIXBUILTINS);
10894 let found = optstr.bytes().position(|b| b == opch);
10895 if opch == b':' || found.is_none() {
10896 // c:5724
10897 if posix {
10898 // c:5728
10899 optcind = 0;
10900 zoptind += 1;
10901 }
10902 // c:5731 — `setsparam(var, ztrdup(p));` where p = "?"
10903 setsparam(&var, "?");
10904 if quiet {
10905 // c:5733
10906 setsparam("OPTARG", &optbuf); // c:5734
10907 } else {
10908 let prefix = if plus { "+" } else { "-" };
10909 zwarn(&format!("bad option: {}{}", prefix, opch as char)); // c:5736
10910 setsparam("OPTARG", "");
10911 }
10912 ZOPTIND.store(zoptind, Relaxed);
10913 OPTCIND.store(optcind, Relaxed);
10914 // Sync OPTIND env var so callers can read.
10915 setiparam("OPTIND", zoptind as i64);
10916 return 0;
10917 }
10918
10919 // c:5744 — `if (p[1] == ':')` — required argument.
10920 let p = found.unwrap();
10921 let optstr_bytes = optstr.as_bytes();
10922 if p + 1 < optstr_bytes.len() && optstr_bytes[p + 1] == b':' {
10923 // c:5744
10924 if optcind == lenstr {
10925 // c:5745
10926 // c:5746 — argument in next arg.
10927 if zoptind as usize >= args.len() {
10928 // c:5747
10929 if posix {
10930 optcind = 0;
10931 zoptind += 1;
10932 }
10933 if quiet {
10934 // c:5754
10935 setsparam(&var, ":");
10936 setsparam("OPTARG", &optbuf);
10937 } else {
10938 setsparam(&var, "?");
10939 setsparam("OPTARG", "");
10940 let prefix = if plus { "+" } else { "-" };
10941 zwarn(&format!(
10942 "argument expected after {}{} option",
10943 prefix, opch as char
10944 )); // c:5760
10945 }
10946 ZOPTIND.store(zoptind, Relaxed);
10947 OPTCIND.store(optcind, Relaxed);
10948 setiparam("OPTIND", zoptind as i64);
10949 return 0;
10950 }
10951 // c:5763 — `p = ztrdup(args[zoptind++]);` — read args[zoptind]
10952 // then post-increment. zoptind now points one past the
10953 // arg-bearing flag's index.
10954 let p_arg = args[zoptind as usize].clone(); // c:5763
10955 zoptind += 1; // c:5763 post-increment
10956 setsparam("OPTARG", &p_arg); // c:5765
10957 // c:5771 — `optcind = 0; zoptind++;` — bump past the
10958 // consumed value arg too, so the NEXT getopts call sees
10959 // the arg AFTER the value. Previous Rust port skipped this
10960 // second increment, so the next iter re-read the consumed
10961 // value as if it were a new flag.
10962 optcind = 0; // c:5771
10963 zoptind += 1; // c:5772
10964 } else {
10965 // c:5774 — `p = metafy(str+optcind, lenstr-optcind, META_DUP);`
10966 let p_arg = str_buf[(optcind as usize)..].to_string();
10967 setsparam("OPTARG", &p_arg);
10968 optcind = 0;
10969 zoptind += 1;
10970 }
10971 } else {
10972 // c:5784 — `zsfree(zoptarg); zoptarg = ztrdup("");`
10973 setsparam("OPTARG", "");
10974 }
10975
10976 // c:5788 — `setsparam(var, metafy(optbuf, lenoptbuf, META_DUP));`
10977 setsparam(&var, &optbuf);
10978 ZOPTIND.store(zoptind, Relaxed);
10979 OPTCIND.store(optcind, Relaxed);
10980 setiparam("OPTIND", zoptind as i64);
10981 0 // c:5790
10982}
10983
10984/// Port of `bin_break(char *name, char **argv, UNUSED(Options ops), int func)` from Src/builtin.c:5809.
10985/// C: `int bin_break(char *name, char **argv, UNUSED(Options ops), int func)`
10986/// — handles BIN_BREAK / BIN_CONTINUE / BIN_RETURN / BIN_LOGOUT / BIN_EXIT.
10987/// WARNING: param names don't match C — Rust=(name, argv, func) vs C=(name, argv, ops, func)
10988pub fn bin_break(
10989 name: &str,
10990 argv: &[String], // c:5809
10991 _ops: &options,
10992 func: i32,
10993) -> i32 {
10994 // BIN_BREAK/CONTINUE/RETURN/EXIT/LOGOUT live at the top of this file
10995 // (c:5707-5712 in Src/builtin.c via the BUILTIN(...) table).
10996 // c:5811 — `int num = lastval, nump = 0, implicit;`
10997 let mut num: i32 = LASTVAL.load(Relaxed); // c:5811
10998 let mut nump = 0i32; // c:5811
10999 let implicit = argv.is_empty(); // c:5814
11000 // c:5815-5818 — first arg parsed as math expr.
11001 if !implicit {
11002 // c:5815
11003 num = mathevali(&argv[0]).unwrap_or(0) as i32; // c:5816
11004 nump = 1; // c:5817
11005 }
11006
11007 // c:5812-5814 — positive-num requirement for BIN_CONTINUE / BIN_BREAK.
11008 // C uses `zerrnam` (NOT zwarnnam) — it sets errflag so the whole
11009 // script/function aborts, matching zsh's `continue 0` behavior
11010 // (prints the error ONCE and stops the loop, rather than warning and
11011 // continuing to iterate).
11012 if nump > 0 && (func == BIN_CONTINUE || func == BIN_BREAK) && num <= 0 {
11013 // c:5812
11014 zerrnam(name, &format!("argument is not positive: {}", num)); // c:5813
11015 return 1; // c:5814
11016 }
11017
11018 let loops = LOOPS.load(Relaxed);
11019 match func {
11020 // c:5831-5842 — BIN_CONTINUE: must be in a loop, set contflag,
11021 // then fall through to BIN_BREAK's break-count assign.
11022 x if x == BIN_CONTINUE => {
11023 // c:5831
11024 if loops == 0 {
11025 // c:5832
11026 // c:Src/builtin.c:5828 — `zerrnam`, NOT `zwarnnam`.
11027 // `zerrnam` sets `errflag` which causes the calling
11028 // shell function (or script) to terminate after the
11029 // current command — `zwarnnam` just prints. Without
11030 // this, `foo() { break; echo "after"; }; foo` printed
11031 // "after" instead of aborting at the break. Bug #616.
11032 zerrnam(name, "not in while, until, select, or repeat loop"); // c:5828
11033 return 1; // c:5834
11034 }
11035 CONTFLAG.store(1, Relaxed); // c:5836 FALLTHROUGH
11036 // c:5837 — fallthrough to BIN_BREAK's loops==0 guard
11037 // (impossible here since we already returned above) +
11038 // break-count assign. Inlined directly. The previous
11039 // Rust port had a redundant `if loops == 0 { return 1 }`
11040 // dead-coded after the first guard.
11041 BREAKS.store(
11042 if nump != 0 { num.min(loops) } else { 1 }, // c:5842
11043 Relaxed,
11044 );
11045 }
11046 // c:5832-5838 — BIN_BREAK.
11047 x if x == BIN_BREAK => {
11048 // c:5832
11049 if loops == 0 {
11050 // c:5833
11051 // c:Src/builtin.c:5834 — `zerrnam` sets errflag.
11052 // Same fix as the BIN_CONTINUE arm above. Bug #616.
11053 zerrnam(name, "not in while, until, select, or repeat loop"); // c:5834
11054 return 1; // c:5835
11055 }
11056 BREAKS.store(
11057 if nump != 0 { num.min(loops) } else { 1 }, // c:5837
11058 Relaxed,
11059 );
11060 }
11061 // c:5839-5860 — BIN_RETURN.
11062 x if x == BIN_RETURN => {
11063 let interactive = isset(INTERACTIVE);
11064 let shinstdin = isset(SHINSTDIN);
11065 let ll_v = locallevel_param.load(Relaxed);
11066 let sourcelevel = crate::ported::init::sourcelevel.load(Relaxed);
11067 // c:5840-5841 — `if ((interactive && shinstdin) || locallevel || sourcelevel)`
11068 if (interactive && shinstdin) || ll_v != 0 || sourcelevel != 0 {
11069 // c:5840
11070 RETFLAG.store(1, Relaxed); // c:5842
11071 BREAKS.store(loops, Relaxed); // c:5843
11072 LASTVAL.store(num, Relaxed); // c:5844
11073 // c:5845-5854 — inside a primed trap with the sentinel
11074 // `trap_return == -2`, promote to TRAP_STATE_FORCE_RETURN
11075 // and carry `lastval`. POSIXTRAPS + `implicit` opts out:
11076 // POSIX semantics keep $? from before the trap fired.
11077 let posixtraps = isset(POSIXTRAPS);
11078 let cur_state = TRAP_STATE.load(Relaxed);
11079 let cur_return = TRAP_RETURN.load(Relaxed);
11080 if cur_state == TRAP_STATE_PRIMED // c:5845
11081 && cur_return == -2 // c:5845
11082 && !(posixtraps && implicit)
11083 // c:5851
11084 {
11085 TRAP_STATE.store(
11086 // c:5852
11087 TRAP_STATE_FORCE_RETURN,
11088 Relaxed,
11089 );
11090 TRAP_RETURN.store(num, Relaxed);
11091 // c:5853
11092 }
11093 return num; // c:5855
11094 }
11095 // c:5858 — fallthrough: treat as logout/exit.
11096 zexit(num, ZEXIT_NORMAL); // c:5858
11097 }
11098 // c:5864-5869 — BIN_LOGOUT: refuse if not LOGINSHELL, then
11099 // FALLTHROUGH into the BIN_EXIT body. The previous Rust port
11100 // called \`zexit(num, ZEXIT_NORMAL)\` directly instead of
11101 // entering the BIN_EXIT defer-guard, so \`logout\` from inside
11102 // a function would skip EXIT traps + function unwind +
11103 // \"you have running jobs\" warning — same gap as the prior
11104 // BIN_EXIT fix.
11105 x if x == BIN_LOGOUT => {
11106 // c:5865 — `if (unset(LOGINSHELL))`. The previous Rust port
11107 // called `optlookup("login")` — but "login" is the
11108 // SHELL-LETTER-FLAG name (zshletters table letter 'l'),
11109 // not an option name. Option name canonicalization maps
11110 // LOGINSHELL → "loginshell" (Src/options.c index_to_name
11111 // at line 1682 in Rust port).
11112 //
11113 // \`optlookup(\"login\")\` returns OPT_INVALID (0), so
11114 // \`isset(0)\` always returns false — bin_logout always
11115 // saw \"not login shell\" and rejected with that error
11116 // regardless of whether the shell was actually started
11117 // with \`-l\`.
11118 let loginshell = isset(LOGINSHELL);
11119 if !loginshell {
11120 // c:5865
11121 // c:Src/builtin.c:5861 — `zerrnam(name, "not login
11122 // shell");`. C uses zerrnam (sets ERRFLAG_ERROR via
11123 // utils.c:203), NOT zwarnnam. The errflag set aborts
11124 // the remainder of the current command list — `logout
11125 // 2>&1; print ex:$?` in a non-login shell prints only
11126 // the error message because the `;` separator's next
11127 // command is gated on errflag-clear. Previous Rust
11128 // port used zwarnnam (no errflag) so `print` still
11129 // ran. Pinned by logout_builtin_stderr parity probe.
11130 zerrnam(name, "not login shell"); // c:5861
11131 return 1; // c:5862
11132 }
11133 // c:5869 — `/*FALLTHROUGH*/` into BIN_EXIT body.
11134 // Reusing the BIN_EXIT branch below by setting `func` to
11135 // BIN_EXIT isn't possible mid-match; inline the same
11136 // guard logic here.
11137 let cur_locallevel = locallevel_param.load(Relaxed);
11138 let forklevel = FORKLEVEL.load(Relaxed);
11139 let shell_exiting = SHELL_EXITING.load(Relaxed);
11140 if cur_locallevel > forklevel && shell_exiting != -1 {
11141 // c:5871
11142 if STOPMSG.load(Relaxed) == 0 {
11143 zexit(0, ZEXIT_DEFERRED); // c:5884
11144 }
11145 if STOPMSG.load(Relaxed) == 0 {
11146 // c:5884
11147 let trap_state = TRAP_STATE.load(Relaxed);
11148 if trap_state != 0 {
11149 // c:5885
11150 TRAP_STATE.store(
11151 // c:5886
11152 TRAP_STATE_FORCE_RETURN,
11153 Relaxed,
11154 );
11155 }
11156 RETFLAG.store(1, Relaxed); // c:5887
11157 BREAKS.store(
11158 LOOPS.load(Relaxed), // c:5888
11159 Relaxed,
11160 );
11161 EXIT_PENDING.store(1, Relaxed); // c:5889
11162 EXIT_LEVEL.store(cur_locallevel, Relaxed); // c:5890 — exit_level = locallevel;
11163 EXIT_VAL.store(num, Relaxed); // c:5891
11164 }
11165 } else {
11166 zexit(num, ZEXIT_NORMAL); // c:5894
11167 }
11168 }
11169 // c:5870-5894 — BIN_EXIT: function-context guard. C body:
11170 // if (locallevel > forklevel && shell_exiting != -1) {
11171 // if (stopmsg || (zexit(0, ZEXIT_DEFERRED), !stopmsg)) {
11172 // if (trap_state) trap_state = TRAP_STATE_FORCE_RETURN;
11173 // retflag = 1; breaks = loops;
11174 // exit_pending = 1; exit_level = locallevel; exit_val = num;
11175 // }
11176 // } else zexit(num, ZEXIT_NORMAL);
11177 //
11178 // Inside a function (locallevel > forklevel) the shell can't
11179 // exit directly — EXIT traps still need to run. The probe
11180 // path zexit(0, ZEXIT_DEFERRED) calls checkjobs; if no
11181 // stopmsg triggered, we defer: set retflag + breaks +
11182 // exit_pending so the function unwind takes us out.
11183 //
11184 // The previous Rust port skipped this entire guard, always
11185 // calling zexit(num, ZEXIT_NORMAL) directly. `exit` inside
11186 // a function would terminate without running EXIT traps or
11187 // unwinding the function stack.
11188 x if x == BIN_EXIT => {
11189 let cur_locallevel = locallevel_param.load(Relaxed);
11190 let forklevel = FORKLEVEL.load(Relaxed);
11191 let shell_exiting = SHELL_EXITING.load(Relaxed);
11192 if cur_locallevel > forklevel && shell_exiting != -1 {
11193 // c:5871
11194 // Probe via ZEXIT_DEFERRED — may set stopmsg.
11195 if STOPMSG.load(Relaxed) == 0 {
11196 zexit(0, ZEXIT_DEFERRED); // c:5884
11197 }
11198 if STOPMSG.load(Relaxed) == 0 {
11199 // c:5884 still no stopmsg → defer
11200 let trap_state = TRAP_STATE.load(Relaxed);
11201 if trap_state != 0 {
11202 // c:5885
11203 TRAP_STATE.store(
11204 // c:5886
11205 TRAP_STATE_FORCE_RETURN,
11206 Relaxed,
11207 );
11208 }
11209 RETFLAG.store(1, Relaxed); // c:5887
11210 BREAKS.store(
11211 LOOPS.load(Relaxed), // c:5888
11212 Relaxed,
11213 );
11214 EXIT_PENDING.store(1, Relaxed); // c:5889
11215 EXIT_LEVEL.store(cur_locallevel, Relaxed); // c:5890 — exit_level = locallevel;
11216 EXIT_VAL.store(num, Relaxed); // c:5891
11217 }
11218 } else {
11219 zexit(num, ZEXIT_NORMAL); // c:5894
11220 }
11221 }
11222 _ => {}
11223 }
11224 0
11225}
11226
11227/// Port of `checkjobs()` from Src/builtin.c:5899.
11228/// C: `static void checkjobs(void)` — walk `jobtab[1..maxjob]`; for each
11229/// non-current job that's STAT_LOCKED, not STAT_NOPRINT, and either
11230/// running (when CHECKRUNNINGJOBS is set) or STAT_STOPPED, emit
11231/// "you have running/stopped jobs" + set `stopmsg = 1`.
11232pub fn checkjobs() {
11233 // c:5899
11234 let checkrunning = isset(CHECKRUNNINGJOBS);
11235 // c:5901 — read the canonical jobs.rs THISJOB/MAXJOB globals.
11236 // The previous builtin.rs duplicate AtomicI32s for both never
11237 // synced with the jobs.rs Mutex<i32> values that the spawn /
11238 // wait paths actually update — checkjobs would see stale 0s
11239 // regardless of how many jobs were active.
11240 let thisjob: i32 = *crate::ported::jobs::THISJOB
11241 .get_or_init(|| Mutex::new(-1_i32))
11242 .lock()
11243 .expect("THISJOB poisoned");
11244 // jobs::MAXJOB is stored as `Mutex<usize>` (Rust adaptation for
11245 // Vec-index semantics); cast to i32 for comparison with `thisjob`.
11246 let maxjob: i32 = *crate::ported::jobs::MAXJOB
11247 .get_or_init(|| Mutex::new(0_usize))
11248 .lock()
11249 .expect("MAXJOB poisoned") as i32;
11250
11251 // c:5903 — `for (i = 1; i <= maxjob; i++)`
11252 let mut found: Option<i32> = None;
11253 let mut found_stat: i32 = 0;
11254 for i in 1..=maxjob {
11255 // c:5903
11256 let stat = JOBSTATS
11257 .lock()
11258 .ok()
11259 .and_then(|t| t.get(i as usize).copied())
11260 .unwrap_or(0);
11261 // c:5904-5906 — `i != thisjob && (stat & STAT_LOCKED) &&
11262 // !(stat & STAT_NOPRINT) &&
11263 // (CHECKRUNNINGJOBS || stat & STAT_STOPPED)`
11264 if i != thisjob // c:5904
11265 && (stat & STAT_LOCKED) != 0 // c:5904
11266 && (stat & STAT_NOPRINT) == 0 // c:5905
11267 && (checkrunning || (stat & STAT_STOPPED) != 0)
11268 // c:5906
11269 {
11270 found = Some(i); // c:5907
11271 found_stat = stat;
11272 break;
11273 }
11274 }
11275 // c:5908 — `if (i <= maxjob)`
11276 if found.is_some() {
11277 // c:5908
11278 if (found_stat & STAT_STOPPED) != 0 {
11279 // c:5909
11280 // c:5912/5914 — `zerr("you have suspended/stopped jobs.");`
11281 zerr("you have stopped jobs."); // c:5914
11282 } else {
11283 // c:5917 — `zerr("you have running jobs.");`
11284 zerr("you have running jobs."); // c:5917
11285 }
11286 STOPMSG.store(1, Relaxed); // c:5919
11287 }
11288}
11289
11290/// Port of `realexit()` from Src/builtin.c:5953.
11291/// C body (single statement):
11292/// `exit((shell_exiting || exit_pending) ? exit_val : lastval);`
11293pub fn realexit() -> ! {
11294 // c:5953
11295 std::process::exit(
11296 if SHELL_EXITING.load(Relaxed) != 0 || EXIT_PENDING.load(Relaxed) != 0 {
11297 EXIT_VAL.load(Relaxed)
11298 } else {
11299 LASTVAL.load(Relaxed)
11300 },
11301 );
11302}
11303
11304/// Port of `_realexit()` from Src/builtin.c:5962.
11305/// C body (single statement):
11306/// `_exit((shell_exiting || exit_pending) ? exit_val : lastval);`
11307pub fn _realexit() -> ! {
11308 // c:5962
11309 unsafe {
11310 libc::_exit(
11311 if SHELL_EXITING.load(Relaxed) != 0 || EXIT_PENDING.load(Relaxed) != 0 {
11312 EXIT_VAL.load(Relaxed)
11313 } else {
11314 LASTVAL.load(Relaxed)
11315 },
11316 )
11317 }
11318}
11319
11320/// Port of `zexit(int val, enum zexit_t from_where)` from Src/builtin.c:5977.
11321/// C: `void zexit(int val, enum zexit_t from_where)` — record exit
11322/// value, fire EXIT trap unless already exiting, then realexit.
11323#[allow(unused_variables)]
11324pub fn zexit(val: i32, from_where: i32) {
11325 // c:5977
11326 // c:5989 — `exit_val = val;`
11327 EXIT_VAL.store(val, Relaxed); // c:5989
11328 // c:5990 — `if (shell_exiting == -1) { retflag = 1; breaks = loops; return; }`
11329 if SHELL_EXITING.load(Relaxed) == -1 {
11330 // c:5990
11331 RETFLAG.store(1, Relaxed); // c:5991
11332 BREAKS.store(LOOPS.load(Relaxed), Relaxed); // c:5992
11333 return; // c:5993
11334 }
11335
11336 // c:5996-6004 — `if (isset(MONITOR) && !stopmsg && from_where != ZEXIT_SIGNAL)`:
11337 // run scanjobs + checkjobs; if stopmsg got set (running jobs warned),
11338 // mark stopmsg=2 and DEFER the exit. The previous Rust port skipped
11339 // this entire block, so `exit` with running jobs would terminate
11340 // immediately rather than emitting the standard
11341 // \"zsh: you have running jobs\" + waiting for a confirmation exit.
11342 if isset(MONITOR) // c:5996
11343 && STOPMSG.load(Relaxed) == 0
11344 && from_where != ZEXIT_SIGNAL
11345 {
11346 checkjobs(); // c:5999
11347 if STOPMSG.load(Relaxed) != 0 {
11348 // c:6000
11349 STOPMSG.store(2, Relaxed); // c:6001
11350 return; // c:6002 defer
11351 }
11352 }
11353 // c:6006-6008 — `if (from_where == ZEXIT_DEFERRED || (shell_exiting++
11354 // && from_where != ZEXIT_NORMAL)) return;`. Probe path:
11355 // ZEXIT_DEFERRED callers only want the checkjobs gate to fire; if
11356 // it didn't trip, return without actually exiting.
11357 if from_where == ZEXIT_DEFERRED {
11358 // c:6006
11359 return;
11360 }
11361 let prev_exiting = SHELL_EXITING.fetch_add(1, Relaxed);
11362 if prev_exiting != 0 && from_where != ZEXIT_NORMAL {
11363 // c:6007
11364 return;
11365 }
11366 // c:6014 — `shell_exiting = -1;`
11367 SHELL_EXITING.store(-1, Relaxed); // c:6014
11368 // c:6019 — `errflag = 0;`
11369 errflag.store(0, Relaxed); // c:6019
11370 // c:6021-6024 — MONITOR → killrunjobs.
11371 if isset(MONITOR) {
11372 // c:6021
11373 crate::ported::signals::killrunjobs(if from_where == ZEXIT_SIGNAL { 1 } else { 0 });
11374 // c:6023
11375 }
11376 // !!! RUST-ONLY GATE: see SUBSHELL_DEPTH declaration above for
11377 // rationale. C zsh's realexit at c:5953 unconditionally calls
11378 // process::exit because the subshell was forked; in zshrs the
11379 // subshell runs in-process, so process::exit would kill the
11380 // whole shell. Defer: set EXIT_PENDING + EXIT_VAL + reset
11381 // SHELL_EXITING so the subshell_end unwind in fusevm_bridge
11382 // catches and propagates the status to the parent.
11383 if SUBSHELL_DEPTH.load(Relaxed) > 0 {
11384 SHELL_EXITING.store(0, Relaxed);
11385 EXIT_VAL.store(val, Relaxed);
11386 EXIT_PENDING.store(1, Relaxed);
11387 RETFLAG.store(1, Relaxed);
11388 BREAKS.store(LOOPS.load(Relaxed), Relaxed);
11389 return;
11390 }
11391 // c:6012 — `cleanfilelists();` — delete per-job temp-file lists
11392 // before exit. jobs.rs::cleanfilelists is a Rust-only-signature
11393 // adapter over the C global jobtab (see its WARNING block).
11394 if let Some(tab) = crate::ported::jobs::JOBTAB.get() {
11395 let mut tab = tab.lock().unwrap_or_else(|e| e.into_inner());
11396 crate::ported::jobs::cleanfilelists(&mut tab); // c:6012
11397 }
11398 // c:6013-6028 — `if (isset(RCS) && interact)`: save the history
11399 // file and run logout scripts. This is THE write-at-exit path for
11400 // interactive history (`-f`/NO_RCS shells intentionally skip it,
11401 // matching C).
11402 if isset(RCS) && interact() {
11403 // c:6013
11404 // c:6014-6020 — `if (!nohistsave) { ... }`. `nohistsave` isn't
11405 // ported as a Rust global (same approximation as
11406 // hist.rs::saveandpophiststack); default 0 = allow saves.
11407 let mut writeflags = HFILE_USE_OPTIONS as i32; // c:6015
11408 if from_where == ZEXIT_SIGNAL {
11409 // c:6016
11410 writeflags |= HFILE_NO_REWRITE as i32; // c:6017
11411 }
11412 saveandpophiststack(1, writeflags); // c:6018
11413 savehistfile(None, writeflags); // c:6019
11414 // c:6021-6027 — `if (islogin && !subsh) { sourcehome(".zlogout");
11415 // ... source(GLOBAL_ZLOGOUT); }`. The C `subsh` check is covered
11416 // by the RUST-ONLY SUBSHELL_DEPTH gate above (in-process
11417 // subshells returned before reaching here).
11418 if islogin() {
11419 // c:6021
11420 crate::ported::init::sourcehome(".zlogout"); // c:6022
11421 if isset(RCS) && isset(GLOBALRCS) {
11422 // c:6024
11423 let _ = crate::ported::init::source(crate::ported::config_h::GLOBAL_ZLOGOUT);
11424 // c:6025
11425 }
11426 }
11427 }
11428 // c:Src/builtin.c:6075-6079 — fire EXIT trap (SIGEXIT) before
11429 // calling realexit. The trap body sees $? = val (carried via
11430 // LASTVAL below) and runs in the shell process. Remove the
11431 // entry from traps_table first so the trap body's own commands
11432 // don't re-trigger it recursively.
11433 //
11434 // Two trap-storage paths: (a) `trap 'cmd' EXIT` stores raw
11435 // body text in `traps_table` (no settrap), handled by the
11436 // direct execute_script below; (b) `TRAPEXIT() { ... }`
11437 // function-named form goes through settrap(SIGEXIT, None,
11438 // ZSIG_FUNC) at funcdef time and lives in the
11439 // shfunctab+sigtrapped pair, dispatched via `dotrap(SIGEXIT)`.
11440 // C zsh's dotrap handles BOTH paths internally; Rust splits
11441 // them. Call dotrap to cover the function-named form. Bug
11442 // #157 in docs/BUGS.md.
11443 let exit_trap = traps_table().lock().ok().and_then(|mut t| t.remove("EXIT"));
11444 LASTVAL.store(val, Relaxed); // c:6076 — $? inside trap = val
11445 crate::ported::signals::in_exit_trap.store(1, Relaxed);
11446 if let Some(body) = exit_trap {
11447 // Set LASTVAL to the requested exit value so `$?` inside
11448 // the trap body sees the right number (matches `(exit 7)`
11449 // → trap body reads $?=7).
11450 let _ = crate::ported::exec::execute_script(&body);
11451 }
11452 // c:Src/signals.c::dotrap(SIGEXIT) — fire ZSIG_FUNC handler
11453 // installed by `TRAPEXIT() { ... }` (settrap with ZSIG_FUNC
11454 // sets sigtrapped[SIGEXIT] = ZSIG_TRAPPED | ZSIG_FUNC).
11455 let _ = crate::ported::signals::dotrap(crate::signals_h::SIGEXIT);
11456 // c:Src/init.c::zexit — `callhookfunc("zshexit", NULL, 1, NULL)`.
11457 // The hook fires both a `zshexit` function (if defined) AND
11458 // walks the `zshexit_functions` array. Distinct from SIGEXIT
11459 // trap dispatch above — zshexit is a zsh-specific shell-event
11460 // hook that operates parallel to trap. Bug #215 in docs/BUGS.md.
11461 let _ = crate::ported::utils::callhookfunc("zshexit", None, 1, std::ptr::null_mut());
11462 crate::ported::signals::in_exit_trap.store(0, Relaxed);
11463 realexit(); // c:6082
11464}
11465
11466/// Port of `bin_dot(char *name, char **argv, UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:6060.
11467/// C: `int bin_dot(char *name, char **argv, ...)` — `.` / `source`
11468/// builtin: locate script (cwd → first `/`-bearing path → $path search)
11469/// and execute it; positional params shift to argv[1..].
11470/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
11471pub fn bin_dot(
11472 name: &str,
11473 argv: &[String], // c:6060
11474 _ops: &options,
11475 _func: i32,
11476) -> i32 {
11477 if argv.is_empty() {
11478 // c:6068
11479 return 0; // c:6069
11480 }
11481
11482 // PFA-SMR aspect: record the source path so the replay tool can
11483 // re-apply the same source/dot at the same call site.
11484 #[cfg(feature = "recorder")]
11485 if crate::recorder::is_enabled() && !argv[0].is_empty() {
11486 let ctx = crate::recorder::recorder_ctx_global();
11487 crate::recorder::emit_source(&argv[0], ctx);
11488 }
11489 // c:6071-6074 — save pparams, install argv[1..] as new pparams.
11490 let saved_pparams: Option<Vec<String>> = if argv.len() > 1 {
11491 // c:6072
11492 let mut pp = PPARAMS.lock().unwrap_or_else(|e| {
11493 PPARAMS.clear_poison();
11494 e.into_inner()
11495 });
11496 let saved = pp.clone();
11497 *pp = argv[1..].to_vec(); // c:6073
11498 Some(saved)
11499 } else {
11500 None
11501 };
11502
11503 let arg0 = argv[0].clone(); // c:6076
11504 let _enam = arg0.clone(); // c:6076
11505 // c:6077-6080 — `if (isset(FUNCTIONARGZERO)) { old0 = argzero;
11506 // argzero = ztrdup(arg0); }`.
11507 // Save the prior argzero so it can be restored at the end of
11508 // bin_dot; under FUNCTIONARGZERO, the sourced file becomes the
11509 // active $0 for the duration of the source.
11510 let saved_argzero: Option<Option<String>> = if isset(FUNCTIONARGZERO) {
11511 let prev = argzero();
11512 set_argzero(Some(arg0.clone()));
11513 Some(prev)
11514 } else {
11515 None
11516 };
11517 let mut diddot = 0i32; // c:6064
11518 let mut dotdot = 0i32; // c:6064
11519
11520 // c:6087-6093 — for `source`, try cwd first.
11521 let mut found_path: Option<String> = None;
11522 if !name.starts_with('.') {
11523 // c:6087
11524 let p = Path::new(&arg0);
11525 if p.exists() && !p.is_dir() {
11526 // c:6088-6089
11527 diddot = 1; // c:6090
11528 found_path = Some(arg0.clone()); // c:6091 (effective)
11529 }
11530 }
11531
11532 // c:6094-6101 — try literal path with `/` in it.
11533 if found_path.is_none() && arg0.contains('/') {
11534 // c:6096
11535 if arg0.starts_with("./") {
11536 diddot += 1;
11537 }
11538 // c:6097
11539 else if arg0.starts_with("../") {
11540 dotdot += 1;
11541 } // c:6098
11542 let p = Path::new(&arg0);
11543 if p.exists() && !p.is_dir() {
11544 found_path = Some(arg0.clone()); // c:6100
11545 } else if Path::new(&format!("{}.zwc", arg0)).is_file() {
11546 // c:6100 — C calls `source(arg0)` UNCONDITIONALLY for a
11547 // slash path; `source()` (init.c:1566) then loads the
11548 // sibling `<arg0>.zwc` via try_source_file even when the
11549 // plain file is gone. zshrs's pre-resolved found_path
11550 // gate must therefore accept the compiled form too —
11551 // the try_source_file hook at the read site below
11552 // supplies the body.
11553 found_path = Some(arg0.clone());
11554 }
11555 }
11556
11557 // c:6102-6121 — $path search (with PATHDIRS guard).
11558 let pathdirs = isset(PATHDIRS);
11559 if found_path.is_none() && (!arg0.contains('/') || (pathdirs && diddot < 2 && dotdot == 0)) {
11560 // c:6102
11561 // c:6103 — `for (pp = path; *pp; pp++)`. C walks the `path[]`
11562 // array (the shell-side $path), not the colon-joined
11563 // $PATH env. Read $PATH from paramtab (the shell
11564 // string view); the colon-split below mirrors the C
11565 // path[] iteration.
11566 let path_env = getsparam("PATH").unwrap_or_default();
11567 for dir in path_env.split(':') {
11568 // c:6107
11569 let buf = if dir.is_empty() || dir == "." {
11570 // c:6108
11571 if diddot != 0 {
11572 continue;
11573 }
11574 diddot = 1; // c:6111
11575 arg0.clone() // c:6112
11576 } else {
11577 format!("{}/{}", dir, arg0) // c:6114
11578 };
11579 let p = Path::new(&buf);
11580 if p.exists() && !p.is_dir() {
11581 // c:6117-6118
11582 found_path = Some(buf); // c:6119
11583 break;
11584 }
11585 }
11586 }
11587
11588 // c:6125-6128 — restore pparams. C does this AFTER source() runs
11589 // so the sourced script sees the new positional args; bug #459 in
11590 // docs/BUGS.md — the previous Rust port restored here BEFORE the
11591 // file body executed, so `source script.zsh hello world` had the
11592 // script see $#=0 / $@="". Defer the restore until after
11593 // execute_script returns (the success path below + the error path
11594 // for file-not-found).
11595
11596 // c:6130-6137 — error path. C: `if (ret == SOURCE_NOT_FOUND)`
11597 // emits via zerrnam (POSIX) / zwarnnam (default). The Rust port
11598 // uses zwarnnam unconditionally because the POSIX hard-error
11599 // path also calls zerrnam which behaves identically here (both
11600 // route through zwarning); the only difference C makes is
11601 // promoting errflag to ERRFLAG_ERROR which already happens
11602 // inside zwarnnam.
11603 let path = match found_path {
11604 Some(p) => p,
11605 None => {
11606 // c:6149 — restore argzero on the error path BEFORE
11607 // emitting the diagnostic and returning. C's structure is
11608 // `source(); if (isset(FUNCTIONARGZERO)) restore`; for the
11609 // file-not-found early-return, the restore still has to
11610 // happen so the outer shell's $0 doesn't keep the dropped
11611 // arg0. Bug #103 in docs/BUGS.md: the previous port did
11612 // this unconditional pre-check restore which ran BEFORE
11613 // `execute_script` on the file-found branch — clobbering
11614 // the just-set arg0 so the sourced script saw the shell
11615 // binary as $0 instead of its own path.
11616 if let Some(prev) = saved_argzero.clone() {
11617 set_argzero(prev);
11618 }
11619 // c:6126-6128 — also restore pparams on the not-found path,
11620 // matching C's c:6125 unconditional cleanup before return.
11621 if let Some(saved) = saved_pparams.clone() {
11622 let mut pp = PPARAMS.lock().unwrap_or_else(|e| {
11623 PPARAMS.clear_poison();
11624 e.into_inner()
11625 });
11626 *pp = saved;
11627 }
11628 // c:6130-6137 — `if (isset(POSIXBUILTINS)) zerrnam(...)
11629 // else zwarnnam(...)`. zerrnam sets ERRFLAG_ERROR ("hard
11630 // error in POSIX (we'll exit later)" — the exec.c done:
11631 // gate at c:4378 turns it into exit(1) for the PSPECIAL
11632 // `.`); zwarnnam does NOT touch errflag, so without
11633 // POSIX_BUILTINS the script continues.
11634 let msg = format!("{}: {}", "no such file or directory", arg0); // c:6135
11635 if isset(crate::ported::zsh_h::POSIXBUILTINS) {
11636 crate::ported::utils::zerrnam(name, &msg); // c:6133
11637 } else {
11638 zwarnnam(name, &msg); // c:6135
11639 }
11640 // c:6143 — `return ret == SOURCE_OK ? lastval : 128 - ret`.
11641 // SOURCE_NOT_FOUND = 1 (Src/zsh.h:2214) → 128 - 1 = 127.
11642 return 128 - 1;
11643 }
11644 };
11645
11646 // c:6140 — `ret = source(enam = buf);`
11647 // C `source()` lives at Src/init.c:1550. It opens the file, sets
11648 // up sourcelevel + scriptname + funcstack, parses + executes via
11649 // the wordcode walker, then unwinds. Rust port reads the file
11650 // and routes the body through fusevm's `execute_script` — the
11651 // VM's parse + compile + run loop is the analog of C's
11652 // loop/execlist tree walk. Errors during execution propagate
11653 // through `lastval`; missing read returns SOURCE_ERROR (128-2 =
11654 // 126) per c:6143.
11655 //
11656 // crate::ported::init::sourcelevel bump (Src/init.c:1606 `sourcelevel++;` /
11657 // c:1644 `sourcelevel--;`) is REQUIRED for `return` inside the
11658 // sourced file to unwind correctly. bin_break (Src/builtin.c:5840)
11659 // checks `(interactive && shinstdin) || locallevel || sourcelevel`
11660 // — without the bump, `return N` falls through to `zexit(num,
11661 // ZEXIT_NORMAL)` (c:5858) and kills the entire shell instead of
11662 // unwinding to the source caller. Also clear RETFLAG after the
11663 // sourced script returns so the unwind doesn't propagate to the
11664 // outer compile unit.
11665 // c:Src/init.c:1557-1558 — `char *old_scriptname = scriptname;
11666 // char *old_scriptfilename = scriptfilename;`
11667 // c:1591-1592 — `scriptname = s; scriptfilename = s;`
11668 // For the duration of source execution, both scriptname and
11669 // scriptfilename point to the sourced file path. zerr/zwarnnam
11670 // diagnostics emitted from inside the sourced file then use this
11671 // path as the prefix (`/path/to/foo:N: ...` instead of `zsh:1:`).
11672 // Bug #139 in docs/BUGS.md. The `path` here is the resolved
11673 // absolute path; C uses the user-supplied `s` (the bin_dot
11674 // argv[0]) but `arg0` (the user-supplied name) is what zsh
11675 // actually shows in diagnostics, so use arg0.
11676 let old_scriptname = crate::ported::utils::scriptname_get(); // c:1557
11677 let old_scriptfilename = crate::ported::utils::scriptfilename_get(); // c:1558
11678 crate::ported::utils::set_scriptname(Some(arg0.clone())); // c:1591
11679 // c:1592 — `scriptfilename = s;` — PS4 `%x` reads this; without
11680 // the store the bottom of an `xtrace` line stack stays "zsh"
11681 // (the executor seed) even inside the sourced file. Bug shared
11682 // with source_from_memory in bins/zshrs.rs.
11683 crate::ported::utils::set_scriptfilename(Some(arg0.clone())); // c:1592
11684
11685 crate::ported::init::sourcelevel.fetch_add(1, Relaxed); // c:1606
11686
11687 // c:Src/init.c:1608-1616 — push a funcstack frame with
11688 // `tp = FS_SOURCE` so prompt %x / xtrace / `$funcfiletrace`
11689 // resolve to the sourced file, not the calling function's
11690 // defining file. Without this push, sourcing a script from
11691 // inside a function left the funcstack TOP as the function's
11692 // own FS_FUNC frame; the prompt %x handler then reported the
11693 // function's `filename` (where the function was DEFINED) as
11694 // the current source file. Repro: powerlevel10k.zsh-theme's
11695 // `__p9k_root_dir=${${(%):-%x}:A:h}` resolved to BIN_DIR
11696 // (zinit.zsh's dir) instead of the theme's plugin dir, so the
11697 // theme then sourced `$BIN_DIR/internal/p10k.zsh` (no such
11698 // file) instead of `$THEME_DIR/internal/p10k.zsh`.
11699 let prev_funcstack_lineno = crate::ported::input::lineno.with(|c| c.get()) as i64;
11700 let pushed_frame = {
11701 let mut stack = crate::ported::modules::parameter::FUNCSTACK
11702 .lock()
11703 .unwrap_or_else(|e| {
11704 crate::ported::modules::parameter::FUNCSTACK.clear_poison();
11705 e.into_inner()
11706 });
11707 let prev_caller = stack
11708 .last()
11709 .map(|fs| fs.name.clone())
11710 .or_else(|| old_scriptfilename.clone())
11711 .or_else(|| Some("zsh".to_string()));
11712 let frame = crate::ported::zsh_h::funcstack {
11713 prev: None,
11714 name: arg0.clone(), // c:1608 fstack.name = scriptfilename
11715 filename: Some(arg0.clone()), // c:1613 fstack.filename = scriptfilename
11716 caller: prev_caller, // c:1609
11717 flineno: 0, // c:1611
11718 lineno: prev_funcstack_lineno, // c:1612
11719 tp: crate::ported::zsh_h::FS_SOURCE, // c:1615
11720 };
11721 stack.push(frame);
11722 true
11723 };
11724
11725 // c:Src/init.c:1566 — `source()` tries the compiled form FIRST:
11726 // `!(prog = try_source_file((us = unmeta(s)))) && (tempfd =
11727 // open(us, ...)) == -1` — a sibling `<file>.zwc` newer than the
11728 // file (or `s` itself being a `.zwc`) short-circuits the plain
11729 // read. zshrs executes through the fusevm text pipeline, so the
11730 // dump's wordcode bridges via getpermtext (same bridge as the
11731 // `.zwc` autoload path in exec.rs::loadautofn).
11732 let zwc_src = crate::ported::parse::try_source_file(&path)
11733 .map(|prog| crate::ported::text::getpermtext(Box::new(prog), None, 0));
11734 // c:Src/init.c:1620 — `errflag &= ~ERRFLAG_ERROR;` before
11735 // executing the sourced body, so a stale flag from the caller's
11736 // context can't abort the file's first list.
11737 crate::ported::utils::errflag.fetch_and(!crate::ported::zsh_h::ERRFLAG_ERROR, Relaxed);
11738 // Recursion backstop — c:Src/jobs.c:1878-1884. zsh runs the sourced
11739 // list through execpline, whose per-pipeline `initjob()` caps recursion
11740 // at MAX_MAXJOBS: `zerr("job table full or recursion limit exceeded")`
11741 // then bails. The fusevm pipeline path (execute_script below) doesn't
11742 // allocate a job per pipeline, so without this guard runaway
11743 // `. self`-style recursion — invisible to FUNCNEST, which counts only
11744 // FS_FUNC frames (doshfunc, exec.rs:5684) — grew the FS_SOURCE stack
11745 // unbounded and overflowed the 256 MB main-thread stack → uncatchable
11746 // SIGBUS. Total FUNCSTACK depth (incl. the FS_SOURCE frame just pushed)
11747 // is the proxy for zsh's concurrently-held job slots; at/over the
11748 // ceiling, raise the zsh-identical error and refuse the deeper body
11749 // (SOURCE_ERROR → 126, as zsh's bail returns). The frame is popped below.
11750 let over_limit = crate::ported::modules::parameter::FUNCSTACK
11751 .lock()
11752 .map(|s| s.len())
11753 .unwrap_or(0)
11754 >= crate::ported::jobs::MAX_MAXJOBS;
11755 let mut result = if over_limit {
11756 crate::ported::utils::zerr("job table full or recursion limit exceeded");
11757 128 - 2 // c:6143 — SOURCE_ERROR = 2 → 126
11758 } else {
11759 match zwc_src {
11760 Some(src) => crate::ported::exec::execute_script(&src).unwrap_or(1), // c:1566 prog path
11761 None => match fs::read_to_string(&path) {
11762 // c:6140
11763 Ok(src) => crate::ported::exec::execute_script(&src).unwrap_or(1),
11764 // c:6143 — SOURCE_ERROR = 2 (Src/zsh.h:2216) → 128 - 2 = 126.
11765 Err(_) => 128 - 2,
11766 },
11767 }
11768 };
11769 // c:Src/init.c:1623-1624 — `if (errflag) ret = SOURCE_ERROR;`
11770 // c:Src/init.c:1663 — `errflag &= ~ERRFLAG_ERROR;` on the restore
11771 // path. source is a CONTAINMENT boundary: an errflag abort inside
11772 // the sourced file breaks ITS lists, then source() clears the
11773 // flag, bin_dot returns 128 - SOURCE_ERROR = 126 (c:Src/builtin.c
11774 // :6143), and the CALLER continues. zsh 5.9: parent script
11775 // sourcing a file that hits `readonly` reassign prints its next
11776 // line with $? = 126.
11777 if (crate::ported::utils::errflag.load(Relaxed) & crate::ported::zsh_h::ERRFLAG_ERROR) != 0 {
11778 crate::ported::utils::errflag.fetch_and(!crate::ported::zsh_h::ERRFLAG_ERROR, Relaxed);
11779 result = 128 - 2; // c:6143 — 128 - SOURCE_ERROR
11780 }
11781 if pushed_frame {
11782 // c:1643 — `funcstack = funcstack->prev;`
11783 let mut stack = crate::ported::modules::parameter::FUNCSTACK
11784 .lock()
11785 .unwrap_or_else(|e| {
11786 crate::ported::modules::parameter::FUNCSTACK.clear_poison();
11787 e.into_inner()
11788 });
11789 stack.pop();
11790 }
11791 crate::ported::init::sourcelevel.fetch_sub(1, Relaxed); // c:1644
11792 // c:1666 — `scriptname = old_scriptname;` and matching
11793 // scriptfilename restore from the c:1667 line.
11794 crate::ported::utils::set_scriptname(old_scriptname);
11795 crate::ported::utils::set_scriptfilename(old_scriptfilename);
11796 // c:5842 RETFLAG is set by bin_break's BIN_RETURN arm. Once the
11797 // sourced file's execute_script unwinds, the return has been
11798 // serviced; clear the flag so the outer compile unit's main loop
11799 // (init.rs:1252's `if retflag break` guard) doesn't see a stale
11800 // request and abort `echo done` after `source foo`.
11801 RETFLAG.store(0, Relaxed); // c:5842 unwind
11802 // c:6149 again — restore argzero on the success path as well.
11803 if let Some(prev) = saved_argzero {
11804 set_argzero(prev);
11805 }
11806 // c:6125-6128 — restore pparams on the success path AFTER the
11807 // source body has finished executing. Bug #459.
11808 if let Some(saved) = saved_pparams {
11809 let mut pp = PPARAMS.lock().unwrap_or_else(|e| {
11810 PPARAMS.clear_poison();
11811 e.into_inner()
11812 });
11813 *pp = saved;
11814 }
11815 result
11816}
11817
11818/// Port of `static int eval(char **argv)` from `Src/builtin.c:6151`.
11819pub fn eval(argv: &[String]) -> i32 {
11820 // c:6151
11821 // c:6153 — `Eprog prog;` (declared inline below)
11822 // c:6154 — `char *oscriptname = scriptname;`
11823 let oscriptname: Option<String> = scriptname_get();
11824 // c:6155 — `int oineval = ineval, fpushed;`
11825 let oineval: i32 = INEVAL.load(Relaxed);
11826 let fpushed: bool;
11827 // c:6156 — `struct funcstack fstack;`
11828
11829 // c:6163 — `ineval = !isset(EVALLINENO);`
11830 INEVAL.store(
11831 if !isset(crate::ported::zsh_h::EVALLINENO) {
11832 1
11833 } else {
11834 0
11835 },
11836 Relaxed,
11837 );
11838 let ineval_now = INEVAL.load(Relaxed) != 0;
11839
11840 if !ineval_now {
11841 // c:6164
11842 // c:6165 — `scriptname = "(eval)";`
11843 crate::ported::utils::set_scriptname(Some("(eval)".to_string()));
11844 // c:6166-6196 — funcstack push: build a fstack frame describing
11845 // this eval, link it to the head of FUNCSTACK.
11846 let prev_frame = {
11847 let stack = FUNCSTACK.lock().unwrap_or_else(|e| e.into_inner());
11848 stack.last().cloned()
11849 };
11850 let lineno_now = crate::ported::input::lineno.with(|c| c.get()) as i64;
11851 let caller = match &prev_frame {
11852 Some(p) => Some(p.name.clone()),
11853 None => argzero(), // c:6168 dupstring(argzero)
11854 };
11855
11856 // c:6182-6196 — flineno/filename derivation. Three cases:
11857 // 1. no prev frame OR prev tp == FS_SOURCE: flineno=lineno,
11858 // filename=caller (the source name)
11859 // 2. prev tp == FS_EVAL: flineno = prev.flineno + lineno - 1
11860 // 3. otherwise (function): flineno = prev.flineno + lineno,
11861 // filename = prev.filename or ""
11862 let (flineno, filename): (i64, Option<String>) = match &prev_frame {
11863 None => (lineno_now, caller.clone()), // c:6183-6184
11864 Some(p) if p.tp == crate::ported::zsh_h::FS_SOURCE => {
11865 (lineno_now, caller.clone()) // c:6183-6184
11866 }
11867 Some(p) => {
11868 let mut fl = p.flineno + lineno_now; // c:6186
11869 if p.tp == crate::ported::zsh_h::FS_EVAL {
11870 // c:6191
11871 fl -= 1; // c:6192
11872 }
11873 let fname = p.filename.clone().or_else(|| Some(String::new())); // c:6193-6195
11874 (fl, fname)
11875 }
11876 };
11877 let frame = crate::ported::zsh_h::funcstack {
11878 prev: None, // c:1349 — linked via FUNCSTACK vec
11879 name: "(eval)".to_string(), // c:6167
11880 filename, // c:6184 / c:6193
11881 caller, // c:6168
11882 flineno, // c:6183 / c:6186
11883 lineno: lineno_now, // c:6169
11884 tp: crate::ported::zsh_h::FS_EVAL, // c:6170
11885 };
11886 {
11887 let mut stack = FUNCSTACK.lock().unwrap_or_else(|e| e.into_inner());
11888 stack.push(frame); // c:6197 funcstack = &fstack
11889 }
11890 fpushed = true; // c:6199
11891 } else {
11892 fpushed = false; // c:6201
11893 }
11894
11895 // c:6203 — `prog = parse_string(zjoin(argv, ' ', 1), 1);`
11896 let joined = crate::ported::utils::zjoin(argv, ' ');
11897 let prog = crate::ported::exec::parse_string(&joined, 1);
11898
11899 if let Some(prog) = prog {
11900 // c:6205 — `if (wc_code(*prog->prog) != WC_LIST)`
11901 let head = prog.prog.first().copied().unwrap_or(0);
11902 if crate::ported::zsh_h::wc_code(head) != crate::ported::zsh_h::WC_LIST as u32 {
11903 /* No code to execute */
11904 // c:6206
11905 LASTVAL.store(0, Relaxed); // c:6207
11906 } else {
11907 // c:6209 — `execode(prog, 1, 0, "eval");`. Routes through
11908 // the executor; in-process equivalent.
11909 //
11910 // PREVIOUSLY called run_command_substitution which captures
11911 // stdout into a String and returns it — bin_eval threw the
11912 // capture away, so `eval 'echo hi'` produced no output.
11913 // execute_script_zsh_pipeline runs the script with stdout
11914 // flowing to the caller (no capture) which is what eval
11915 // wants. Same routing the eval-via-execstring path uses
11916 // (vm_helper.rs:1518 EXIT-trap fire).
11917 //
11918 // Recursion backstop — c:Src/jobs.c:1878-1884. zsh's per-pipeline
11919 // `initjob()` caps eval recursion at MAX_MAXJOBS ("job table full
11920 // or recursion limit exceeded"); the fusevm pipeline path doesn't
11921 // allocate a job per pipeline, so runaway `eval`-string recursion
11922 // (invisible to FUNCNEST, which counts FS_FUNC frames only, AND to
11923 // the FS_EVAL frame count — nested evals suppress the funcstack
11924 // push via INEVAL, c:6164, exactly like zsh) is otherwise unbounded
11925 // → main-thread stack overflow → SIGBUS. Track eval re-entry depth
11926 // directly (the Rust proxy for zsh's concurrently-held job slots,
11927 // one per nested eval pipeline) and cap at the same MAX_MAXJOBS
11928 // ceiling. Requested as a parity fix by the maintainer.
11929 let eval_depth = crate::vm_helper::EVAL_RECURSION_DEPTH.with(|d| {
11930 let v = d.get() + 1;
11931 d.set(v);
11932 v
11933 });
11934 if eval_depth >= crate::ported::jobs::MAX_MAXJOBS {
11935 crate::ported::utils::zerr("job table full or recursion limit exceeded");
11936 LASTVAL.store(1, Relaxed);
11937 } else {
11938 let _ = crate::ported::exec::execute_script_zsh_pipeline(&joined);
11939 }
11940 crate::vm_helper::EVAL_RECURSION_DEPTH.with(|d| d.set(d.get().saturating_sub(1)));
11941 // c:6211-6212 — `if (errflag && !lastval) lastval = errflag;`
11942 let ef = errflag.load(Relaxed);
11943 let lv = LASTVAL.load(Relaxed);
11944 if ef != 0 && lv == 0 {
11945 LASTVAL.store(ef, Relaxed);
11946 }
11947 }
11948 } else {
11949 LASTVAL.store(1, Relaxed); // c:6215
11950 }
11951
11952 if fpushed {
11953 // c:6218
11954 // c:6219 — `funcstack = funcstack->prev;`
11955 let mut stack = FUNCSTACK.lock().unwrap_or_else(|e| e.into_inner());
11956 stack.pop();
11957 }
11958
11959 // c:6221 — `errflag &= ~ERRFLAG_ERROR;`
11960 errflag.fetch_and(!ERRFLAG_ERROR, Relaxed);
11961 // c:6222 — `scriptname = oscriptname;`
11962 crate::ported::utils::set_scriptname(oscriptname);
11963 // c:6223 — `ineval = oineval;`
11964 INEVAL.store(oineval, Relaxed);
11965
11966 LASTVAL.load(Relaxed) // c:6225
11967}
11968
11969/// Port of `bin_emulate(char *nam, char **argv, Options ops, UNUSED(int func))` from Src/builtin.c:6232.
11970/// C: `int bin_emulate(char *nam, char **argv, Options ops, ...)` —
11971/// no-args print current emulation; single-arg switch emulation;
11972/// `-l` list, `-L` set LOCAL*, `-R` reset to defaults.
11973/// WARNING: param names don't match C — Rust=(nam, argv, _func) vs C=(nam, argv, ops, func)
11974pub fn bin_emulate(
11975 nam: &str,
11976 argv: &[String], // c:6232
11977 ops: &options,
11978 _func: i32,
11979) -> i32 {
11980 let opt_l = OPT_ISSET(ops, b'l'); // c:6236
11981 let opt_l_arg = OPT_ISSET(ops, b'L'); // c:6234
11982 let opt_r = OPT_ISSET(ops, b'R'); // c:6235
11983
11984 // c:6249-6275 — no args: print current emulation name.
11985 if argv.is_empty() {
11986 // c:6249
11987 if opt_l_arg || opt_r {
11988 // c:6250
11989 zwarnnam(nam, "not enough arguments"); // c:6251
11990 return 1; // c:6252
11991 }
11992 // c:6255-6271 — `switch(SHELL_EMULATION())` → name dispatch.
11993 let bits = emulation.load(Relaxed) as i32;
11994 let shname = if (bits & EMULATE_CSH) != 0 {
11995 "csh"
11996 }
11997 // c:6255
11998 else if (bits & EMULATE_KSH) != 0 {
11999 "ksh"
12000 }
12001 // c:6259
12002 else if (bits & EMULATE_SH) != 0 {
12003 "sh"
12004 }
12005 // c:6263
12006 else {
12007 "zsh"
12008 }; // c:6268
12009 println!("{}", shname); // c:6273
12010 return 0; // c:6274
12011 }
12012
12013 // c:6278-6295 — single-arg form: `emulate <shname>`.
12014 let shname = &argv[0];
12015 if argv.len() == 1 {
12016 // c:6278
12017 // c:6280-6285 — `if (opt_l) cmdopts = zhalloc(...); else cmdopts = opts;`
12018 // In our static-link port, the live option table IS the
12019 // "real opts"; under -l we build a snapshot HashMap and
12020 // mutate THAT instead of touching global state. Under
12021 // !-l we apply emulate semantics to the live table.
12022 // c:537-549 — C `emulate(zsh_name, ...)` reads ONLY the first
12023 // char (after stripping a leading `r` for rcsh/rksh): 'c'
12024 // → CSH, 'k' → KSH, 's'/'b' → SH (so `bash` aliases to sh),
12025 // else ZSH. Previous Rust port did full-string equality so
12026 // `emulate rcsh` / `emulate bash` silently fell back to ZSH.
12027 let bytes = shname.as_bytes();
12028 let mut ch = if !bytes.is_empty() { bytes[0] } else { 0 };
12029 if ch == b'r' && bytes.len() >= 2 {
12030 // c:539
12031 ch = bytes[1]; // c:540
12032 }
12033 let bits = match ch {
12034 // c:543
12035 b'c' => EMULATE_CSH, // c:544
12036 b'k' => EMULATE_KSH, // c:546
12037 b's' | b'b' => EMULATE_SH, // c:548
12038 _ => EMULATE_ZSH, // c:550
12039 };
12040 // c:6286 — `emulate(shname, opt_R, &emulation, cmdopts)`.
12041 emulation.store(bits, Relaxed);
12042
12043 // Build the cmdopts view that c:6286-6292 manipulates.
12044 let mut cmdopts: HashMap<String, bool> = HashMap::new();
12045 for n in ZSH_OPTIONS_SET.iter() {
12046 cmdopts.insert(
12047 n.to_string(),
12048 crate::ported::options::opt_state_get(n).unwrap_or(false),
12049 );
12050 }
12051 // c:6286 — `emulate(shname, opt_R, &emulation, cmdopts)` ALSO
12052 // populates `cmdopts` (when !opt_l, `cmdopts` aliases the live
12053 // `opts[]`). The Rust port has a per-emulation defaults walker
12054 // at options::emulate() that writes through to OPTS_LIVE — call
12055 // it here so `emulate -L sh` flips KSH_ARRAYS / SH_NULLCMD /
12056 // POSIX_ALIASES / etc. to their target-emulation defaults.
12057 //
12058 // Bug #26 in docs/BUGS.md — without this, `emulate -L sh`
12059 // changed the emulation tag but left every option at its zsh
12060 // default, so `a=(1 2 3); echo ${a[0]}` stayed 1-indexed
12061 // (empty result) instead of switching to KSH_ARRAYS 0-indexed.
12062 // c:6285 — `emulate(shname, opt_R, &emulation, cmdopts);` runs
12063 // UNCONDITIONALLY. C can do that because it passes the target array in:
12064 // under -l cmdopts is a COPY of opts (c:6281-6282) and the emulation is
12065 // applied to the copy; otherwise cmdopts aliases the live opts[].
12066 //
12067 // options::emulate() has no such parameter — it writes through to
12068 // OPTS_LIVE — so the -l case is expressed by applying it to the live
12069 // state, snapshotting the result, and restoring. Same effect as C's
12070 // copy: `emulate -l sh` reports what the options WOULD be, and the
12071 // shell is left untouched.
12072 //
12073 // Skipping the call under -l (as this did) meant cmdopts stayed the
12074 // CURRENT options, so `emulate -l sh` listed zsh's defaults, not sh's —
12075 // ~38 options wrong. It looked right only for `emulate -l zsh`, where
12076 // the current options ARE the answer.
12077 let saved_opts = if opt_l {
12078 Some(crate::ported::options::opt_state_snapshot())
12079 } else {
12080 None
12081 };
12082 crate::ported::options::emulate(shname.as_str(), opt_r); // c:6285
12083 // Re-sync cmdopts from OPTS_LIVE so it reflects the emulation's
12084 // defaults rather than the pre-emulate state captured above.
12085 for n in ZSH_OPTIONS_SET.iter() {
12086 cmdopts.insert(
12087 n.to_string(),
12088 crate::ported::options::opt_state_get(n).unwrap_or(false),
12089 );
12090 }
12091 if let Some(saved) = saved_opts {
12092 // Undo the live apply — C never touched opts[] in the -l path.
12093 crate::ported::options::opt_state_restore(saved);
12094 }
12095
12096 // c:6287-6289 — opt_L: set LOCALOPTIONS/LOCALTRAPS/LOCALPATTERNS=1
12097 // in cmdopts. In the !opt_l live-apply case we also set them in
12098 // OPTS_LIVE; in the opt_l snapshot case we only set them in
12099 // cmdopts (the snapshot the list call walks).
12100 if opt_l_arg {
12101 // c:6287
12102 for nm in ["localoptions", "localtraps", "localpatterns"] {
12103 cmdopts.insert(nm.to_string(), true);
12104 if !opt_l {
12105 crate::ported::options::opt_state_set(nm, true);
12106 }
12107 }
12108 }
12109 if opt_l {
12110 // c:6290
12111 // c:6291 — `list_emulate_options(cmdopts, opt_R);`
12112 crate::ported::options::list_emulate_options(&cmdopts, opt_r);
12113 return 0; // c:6292
12114 }
12115 // c:6294 — `clearpatterndisables();` resets the per-pattern
12116 // disabled-feature bitset that a previous emulation may have
12117 // left in place.
12118 crate::ported::pattern::clearpatterndisables();
12119 return 0; // c:6295
12120 }
12121
12122 // c:6297-6300 — too many args under -l.
12123 if opt_l {
12124 // c:6297
12125 zwarnnam(nam, "too many arguments for -l"); // c:6298
12126 return 1; // c:6299
12127 }
12128
12129 // c:6302-6342 — `emulate <shname> [-o OPT|+o OPT|-LR ...]` per-command
12130 // form. C: `argv++; emulate(shname, opt_R, ...); parseopts(...);` —
12131 // switch emulation, then parse the remaining tokens as long-form
12132 // options that apply ON TOP of the emulation defaults. zinit's
12133 // +zi-log (zinit.zsh:2178) calls
12134 // `emulate -LR zsh -o extendedglob`
12135 // which requires extendedglob to be set after the zsh-emulation
12136 // reset, so the (#b)/(#m) flags in subsequent pattern subst calls
12137 // are recognized. The previous Rust port returned 0 without
12138 // applying ANY of the trailing options, so extendedglob stayed off
12139 // and `${msg//(#b)…/…}` errored with `bad pattern: …`.
12140 //
12141 // The `-c command` (cmd-eval) and sticky-emulation paths at c:6329+
12142 // remain unported (no zshrs caller exercises them yet) — the
12143 // structural opt-application below is enough for the common
12144 // `emulate -LR zsh -o NAME …` form.
12145 let _ = opt_r;
12146 // c:6303 — `memcpy(saveopts, opts, sizeof(opts));` + c:6326
12147 // `saveemulation = emulation;` — snapshot the live option table
12148 // and both emulation cells BEFORE applying the new emulation.
12149 // C builds the new state in a LOCAL `new_opts` and only commits
12150 // it at c:6328; the Rust port writes the live table directly, so
12151 // this snapshot is the rollback for the `-c` eval (restore2 at
12152 // c:6377-6379) and the parse-error path (goto restore, c:6311).
12153 // Without the restore, `emulate sh -c 'f() { ...; }'` left the
12154 // shell PERMANENTLY in sh-emulation option state — a later
12155 // `bad substitution` zerr then aborted the whole non-interactive
12156 // shell (D04parameter chunk 159 poisoned chunk 225 into a
12157 // shell-exit).
12158 let saveopts = crate::ported::options::opt_state_snapshot(); // c:6303
12159 let saveemulation = emulation.load(Relaxed); // c:6326
12160 let saveemulation_live = crate::ported::options::EMULATION.load(Relaxed); // c:6326 (port keeps 2 cells)
12161 // c:6306 — `emulate(shname, opt_R, &new_emulation, new_opts);` —
12162 // apply emulation defaults to the live opts table.
12163 crate::ported::options::emulate(shname.as_str(), opt_r);
12164
12165 // c:6308 — `parseopts(nam, &argv, new_opts, &cmd, optlist, 0)`.
12166 // C body at Src/init.c:390+:
12167 // while (!optionbreak && *argv && (**argv == '-' || **argv == '+')) {
12168 // action = (**argv == '-');
12169 // while (*++*argv) {
12170 // if (**argv == '-') { /* `--` ends options; `--NAME` long form */ }
12171 // else if (**argv == 'c') { *cmdp = *argv; }
12172 // else if (**argv == 'o') {
12173 // if (!*++*argv) argv++; // `-o NAME` separate-arg
12174 // if (!*argv) { WARN("string expected after -o"); return 1; }
12175 // longoptions: optno = optlookup(*argv);
12176 // if (!optno) { WARN("no such option: %s", *argv); return 1; }
12177 // dosetopt(optno, action, ...);
12178 // break;
12179 // }
12180 // else { /* single-char option letter, e.g. 'L', 'R' */
12181 // optno = optlookupc(**argv);
12182 // dosetopt(optno, action, ...);
12183 // }
12184 // }
12185 // argv++;
12186 // }
12187 //
12188 // Faithful port: walk each arg, parse `-`/`+` action sign, then
12189 // walk char-by-char. `--` ends options; `-o NAME` / `+o NAME`
12190 // long-form (NAME inline-attached or separate arg); single-char
12191 // option letters set/unset via `dosetopt` (Rust:
12192 // `opt_state_set`). `-c command` records the cmd-eval mode (not
12193 // wired through here — bin_emulate uses opt_L for localoptions
12194 // only).
12195 let mut i = 1; // skip shname (argv[0])
12196 let mut optionbreak = false;
12197 let mut cmd_body: Option<String> = None; // c:6310 — `-c command` capture.
12198 // c:6308 `optlist` — parseopts records each explicitly-set option
12199 // (as an index into new_opts) so the `-c` arm can build the
12200 // sticky struct's on_opts/off_opts at c:6347-6373. Rust tracks
12201 // (canonical-name, on?) pairs.
12202 let mut optlist: Vec<(String, bool)> = Vec::new();
12203 while !optionbreak && i < argv.len() {
12204 let arg = &argv[i];
12205 // c:Src/init.c:418 — only `-` / `+` start an option arg.
12206 let first = arg.chars().next().unwrap_or('\0');
12207 if first != '-' && first != '+' {
12208 break;
12209 }
12210 let action = first == '-'; // c:420
12211 // c:421-422 — bare `-` / `+` treated as `--` (end-of-options
12212 // marker preserved as-is for caller).
12213 if arg.len() == 1 {
12214 i += 1;
12215 break;
12216 }
12217 // Walk chars after the leading +/-.
12218 let bytes: Vec<char> = arg.chars().collect();
12219 let mut j = 1;
12220 let mut consumed_next_arg = false;
12221 while j < bytes.len() {
12222 let ch = bytes[j];
12223 if ch == '-' {
12224 // c:425-429 — `--` ends options.
12225 if j == 1 && bytes.len() == 2 {
12226 optionbreak = true;
12227 i += 1;
12228 break;
12229 }
12230 // c:432-460 — `--NAME` GNU-style long. `-` chars
12231 // inside NAME become `_` (c:457-459). For bin_emulate
12232 // we don't recognize `--version` / `--help` / etc.,
12233 // just route the inline NAME as a long-form option.
12234 let name: String = bytes[j + 1..].iter().collect::<String>().replace('-', "_");
12235 // Same optlookup+dosetopt fix as the `-o NAME` arm below:
12236 // resolve the `no` negation prefix and run option side
12237 // effects, instead of writing a bogus stripped-name slot.
12238 let optno = crate::ported::options::optlookup(&name);
12239 if optno == 0 {
12240 zwarnnam(nam, &format!("no such option: {}", name));
12241 return 1;
12242 }
12243 crate::ported::options::dosetopt(optno, if action { 1 } else { 0 }, 0);
12244 let canon = crate::ported::zsh_h::opt_name(optno.abs()).to_string();
12245 let effective_on = if optno < 0 { !action } else { action };
12246 optlist.push((canon, effective_on)); // c:6308 optlist record
12247 break;
12248 }
12249 if ch == 'o' || ch == 'O' {
12250 // c:480-505 — `-o NAME` / `+o NAME`. NAME is either
12251 // attached (rest of arg after `o`) or the next arg.
12252 let name = if j + 1 < bytes.len() {
12253 bytes[j + 1..].iter().collect::<String>()
12254 } else {
12255 if i + 1 >= argv.len() {
12256 zwarnnam(nam, "string expected after -o");
12257 return 1; // c:484-485
12258 }
12259 consumed_next_arg = true;
12260 argv[i + 1].clone()
12261 };
12262 // c:6314-6316 — `optno = optlookup(*argv); if (!optno)
12263 // { WARN("no such option"); return 1; } dosetopt(optno,
12264 // action, ...)`. optlookup resolves the `no`/`no_`
12265 // negation prefix (returning a NEGATIVE optno) and
12266 // dosetopt inverts it AND runs the side effects
12267 // (inittyptab for MULTIBYTE/BANGHIST/SHINSTDIN). The
12268 // previous `opt_state_set(lowercased_stripped, action)`
12269 // wrote a bogus `nomultibyte` slot and never toggled the
12270 // real MULTIBYTE option, so `emulate -o no_multi_byte`
12271 // (p10k.zsh:1760) left MULTIBYTE ON — char- not
12272 // byte-oriented length math — and every p10k prompt
12273 // segment built empty.
12274 let optno = crate::ported::options::optlookup(&name);
12275 if optno == 0 {
12276 zwarnnam(nam, &format!("no such option: {}", name));
12277 return 1;
12278 }
12279 crate::ported::options::dosetopt(optno, if action { 1 } else { 0 }, 0);
12280 let canon = crate::ported::zsh_h::opt_name(optno.abs()).to_string();
12281 let effective_on = if optno < 0 { !action } else { action };
12282 optlist.push((canon, effective_on)); // c:6308 optlist record
12283 break; // c:505 — break out of char walk after `-o`
12284 }
12285 if ch == 'c' {
12286 // c:6310 — `-c command`. Capture the command body from
12287 // either the rest of the current arg (e.g. `-c'body'`)
12288 // or the next argv slot. C's sticky-emulation evaluator
12289 // (c:6332-6373) wraps the cmd in saved/restored
12290 // emulation state; we mirror by running the captured
12291 // body through eval after the option-parse loop exits.
12292 let body = if j + 1 < bytes.len() {
12293 bytes[j + 1..].iter().collect::<String>()
12294 } else {
12295 if i + 1 >= argv.len() {
12296 zwarnnam(nam, "string expected after -c");
12297 return 1;
12298 }
12299 consumed_next_arg = true;
12300 argv[i + 1].clone()
12301 };
12302 cmd_body = Some(body);
12303 break;
12304 }
12305 // c:Src/init.c — single-char option letter dispatch via
12306 // `optno = optlookupc(**argv); dosetopt(optno, action, ...)`.
12307 // C maps the char through `optletters[]` (Src/options.c) to
12308 // an OPT_* number, then writes the action bit.
12309 //
12310 // NOT PORTED here: the chars bin_emulate cares about (`L`,
12311 // `R`, `l`) are extracted into `ops` by the dispatcher BEFORE
12312 // bin_emulate runs (via the optstr declared at
12313 // Src/builtin.c:99 for the emulate builtin). The Rust port's
12314 // builtin dispatcher does the same — see `opt_l`, `opt_l_arg`,
12315 // `opt_r` reads at the top of this function. So by the time
12316 // parseopts runs, single-char options have already been
12317 // consumed and `argv[1..]` contains only the long-form
12318 // `-o NAME` / `--NAME` shapes that this loop handles.
12319 // Unknown single-char chars are silently skipped (matching
12320 // what the dispatcher would have flagged as a parse error
12321 // before bin_emulate ran).
12322 let _ = ch;
12323 j += 1;
12324 }
12325 i += 1;
12326 if consumed_next_arg {
12327 i += 1;
12328 }
12329 }
12330 // c:6332-6377 — `-c command` evaluator: save patterns, install
12331 // the pending `sticky` struct (functions defined in the body get
12332 // stamped via shfunc_set_sticky at c:5402), eval the body, then
12333 // restore sticky + emulation + opts + patterns (restore2: at
12334 // c:6377-6379).
12335 if let Some(body) = cmd_body {
12336 if opt_l_arg {
12337 // c:6333-6336 — `-L` is incompatible with `-c`.
12338 zwarnnam(nam, "option -L incompatible with -c");
12339 // c:6336 `goto restore2` — undo the emulation switch.
12340 emulation.store(saveemulation, Relaxed);
12341 crate::ported::options::EMULATION.store(saveemulation_live, Relaxed);
12342 crate::ported::options::opt_state_restore(saveopts);
12343 return 1;
12344 }
12345 // c:6319 — `savepatterns = savepatterndisables();`
12346 let savepatterns = crate::ported::pattern::savepatterndisables();
12347 // c:6324 — `clearpatterndisables();` — every emulation starts
12348 // with an empty pattern-disable set.
12349 crate::ported::pattern::clearpatterndisables();
12350 // c:6344-6373 — `save_sticky = sticky; sticky = hcalloc(...);`
12351 // Build the sticky struct: target emulation + the on/off
12352 // option indices the optlist recorded.
12353 let save_sticky = crate::ported::options::sticky
12354 .lock()
12355 .unwrap_or_else(|e| e.into_inner())
12356 .take(); // c:6344
12357 {
12358 let mut on_opts: Vec<crate::ported::zsh_h::OptIndex> = Vec::new();
12359 let mut off_opts: Vec<crate::ported::zsh_h::OptIndex> = Vec::new();
12360 for (name, on) in &optlist {
12361 // c:6366-6372 — Data is index into new_opts.
12362 let optno = optlookup(name);
12363 if optno > 0 {
12364 if *on {
12365 on_opts.push(optno as crate::ported::zsh_h::OptIndex);
12366 } else {
12367 off_opts.push(optno as crate::ported::zsh_h::OptIndex);
12368 }
12369 }
12370 }
12371 *crate::ported::options::sticky
12372 .lock()
12373 .unwrap_or_else(|e| e.into_inner()) =
12374 Some(Box::new(crate::ported::zsh_h::emulation_options {
12375 emulation: crate::ported::options::EMULATION.load(Relaxed), // c:6346
12376 n_on_opts: on_opts.len() as i32, // c:6351
12377 n_off_opts: off_opts.len() as i32, // c:6353
12378 on_opts, // c:6356-6358
12379 off_opts, // c:6360-6362
12380 }));
12381 }
12382 let r = eval(&[body]); // c:6374 — `ret = eval(argv);`
12383 // c:6375 — `sticky = save_sticky;`
12384 *crate::ported::options::sticky
12385 .lock()
12386 .unwrap_or_else(|e| e.into_inner()) = save_sticky;
12387 // c:6377-6379 restore2: — emulation + opts + patterns.
12388 emulation.store(saveemulation, Relaxed); // c:6377
12389 crate::ported::options::EMULATION.store(saveemulation_live, Relaxed); // c:6377
12390 crate::ported::options::opt_state_restore(saveopts); // c:6378
12391 crate::ported::pattern::restorepatterndisables(savepatterns); // c:6379
12392 // c:6381-6382 restore: — keyboardhackchar + inittyptab()
12393 // (keyboard hack char isn't ported; typtab rebuild is a
12394 // no-op in the Rust lexer).
12395 return r;
12396 }
12397
12398 // c:6314-6317 — `if (*argv) zwarnnam(nam, "unknown argument %s",
12399 // *argv);`. Anything left after the option-parse loop is unknown.
12400 // zinit's `emulate -LR zsh -o extendedglob` exhausts argv, so this
12401 // arm doesn't fire — but mirror the C behavior for other callers.
12402 if i < argv.len() && !optionbreak {
12403 zwarnnam(nam, &format!("unknown argument: {}", argv[i]));
12404 // c:6315 `goto restore` — C's LIVE opts were never touched on
12405 // this path (parseopts wrote the local new_opts); the Rust
12406 // loop wrote live state, so roll back the snapshot to match.
12407 emulation.store(saveemulation, Relaxed);
12408 crate::ported::options::EMULATION.store(saveemulation_live, Relaxed);
12409 crate::ported::options::opt_state_restore(saveopts);
12410 return 1;
12411 }
12412
12413 // c:6339-6340 — `if (opt_L) opts[LOCALOPTIONS] = opts[LOCALTRAPS] =
12414 // opts[LOCALPATTERNS] = 1;`.
12415 if opt_l_arg {
12416 for nm in ["localoptions", "localtraps", "localpatterns"] {
12417 crate::ported::options::opt_state_set(nm, true);
12418 }
12419 }
12420 0
12421}
12422
12423/// Port of `bin_eval(UNUSED(char *nam), char **argv, UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:6393.
12424/// C: `int bin_eval(UNUSED args)` → `return eval(argv);`
12425/// WARNING: param names don't match C — Rust=(_name, argv, _func) vs C=(nam, argv, ops, func)
12426pub fn bin_eval(
12427 _name: &str,
12428 argv: &[String], // c:6393
12429 _ops: &options,
12430 _func: i32,
12431) -> i32 {
12432 // c:Src/builtin.c:407-411 — generic `--` end-of-options strip
12433 // applied by `execbuiltin` for builtins that:
12434 // 1. Have NULL optstr (so the `if (optstr) { option parse }`
12435 // loop didn't run), AND
12436 // 2. Don't have BINF_HANDLES_OPTS in their flags.
12437 // ```c
12438 // } else if (!(flags & BINF_HANDLES_OPTS) && *argv &&
12439 // !strcmp(*argv, "--")) {
12440 // ops.ind['-'] = 1;
12441 // argv++;
12442 // }
12443 // ```
12444 // `eval` has optstr=NULL (Src/builtin.c:65) and no
12445 // BINF_HANDLES_OPTS, so the strip applies. The Rust dispatch
12446 // path bypasses execbuiltin, so we need to do the strip
12447 // explicitly here. Without it `eval -- "echo hi"` joined to
12448 // `-- echo hi` and tried to dispatch `--` as a command. Bug
12449 // #319 in docs/BUGS.md.
12450 let stripped: Vec<String>;
12451 let argv: &[String] = if let Some(first) = argv.first() {
12452 if first == "--" {
12453 stripped = argv[1..].to_vec();
12454 &stripped
12455 } else {
12456 argv
12457 }
12458 } else {
12459 argv
12460 };
12461 eval(argv) // c:6396
12462}
12463
12464/// Port of `bin_read(char *name, char **args, Options ops, UNUSED(int func))` from Src/builtin.c:6412.
12465/// C: `int bin_read(char *name, char **args, Options ops, UNUSED(int func))`.
12466///
12467/// The C body is ~720 lines covering the whole `read` builtin matrix:
12468/// `-A` array, `-k N` raw chars, `-q` yes/no, `-r` raw, `-s` silent,
12469/// `-t TIMEOUT`, `-u FD` input FD, `-p` coproc, `-d DELIM` delimiter,
12470/// `-e` echo, `-E` echo-stdout-only, `-l`/`-c` compctl. The structural
12471/// port below handles the script-friendly subset: VAR= default,
12472/// `read -p PROMPT VAR`, `read -t TIMEOUT VAR`, `read -A ARRAY`,
12473/// `read -k N VAR`. Terminal-mode (-q/-s/-e) and ZLE plumbing defer
12474/// to the existing zle/io accessors.
12475/// WARNING: param names don't match C — Rust=(name, args, _func) vs C=(name, args, ops, func)
12476pub fn bin_read(
12477 name: &str,
12478 args: &[String], // c:6412
12479 ops: &options,
12480 _func: i32,
12481) -> i32 {
12482 let args = args.to_vec();
12483 let mut nchars: i32 = 1; // c:6415
12484 let mut partial_eof = false;
12485
12486 // c:6432-6438 — `-k N` raw-char count.
12487 if OPT_HASARG(ops, b'k') {
12488 // c:6432
12489 let optarg = OPT_ARG(ops, b'k').unwrap_or("");
12490 match optarg.trim().parse::<i32>() {
12491 Ok(n) => nchars = n,
12492 Err(_) => {
12493 zwarnnam(name, &format!("number expected after -k: {}", optarg)); // c:6437
12494 return 1;
12495 }
12496 }
12497 }
12498
12499 // c:6445-6446 — `firstarg = (*args && **args == '?' ? *args++ :
12500 // *args); reply = *args ? *args++ : ...`. When the first arg
12501 // starts with `?` it is a prompt-only arg (consumed; the NEXT arg
12502 // is the reply name). Otherwise firstarg ALIASES the reply name —
12503 // and the prompt block at c:6534-6543 scans it for an embedded
12504 // `?`, prints the tail as the prompt, and truncates the name in
12505 // place (`readpmpt[-1] = '\0'`). So `read v?prompt: ` reads into
12506 // `v` with prompt `prompt: `. Bug #387 follow-up: the leading-`?`
12507 // form was handled but the `var?prompt` split was missing, so
12508 // zshrs errored "not an identifier: v?prompt".
12509 let mut argi = 0usize;
12510 let mut prompt: Option<String> = None;
12511 if argi < args.len() && args[argi].starts_with('?') {
12512 // c:6445 — prompt-only first arg.
12513 prompt = Some(args[argi][1..].to_string());
12514 argi += 1;
12515 }
12516 let want_array = OPT_ISSET(ops, b'A');
12517 let reply = if argi < args.len() {
12518 let mut r = args[argi].clone();
12519 argi += 1;
12520 // c:6534-6543 — firstarg (== this arg when no leading-`?` arg
12521 // was consumed) splits at its first `?`: name before, prompt
12522 // after.
12523 if prompt.is_none() {
12524 if let Some(q) = r.find('?') {
12525 prompt = Some(r[q + 1..].to_string());
12526 r.truncate(q); // c:6542 readpmpt[-1] = '\0'
12527 }
12528 }
12529 r
12530 } else if want_array {
12531 "reply".to_string() // c:6446
12532 } else {
12533 "REPLY".to_string() // c:6446
12534 };
12535
12536 if want_array && argi < args.len() {
12537 // c:6448
12538 zwarnnam(name, "only one array argument allowed"); // c:6449
12539 return 1;
12540 }
12541
12542 // c:Src/builtin.c:6457-6477 — `read -k`/`-q` requires a
12543 // controlling tty (unless `-u FD` or `-p` redirects input).
12544 // If neither stdin nor stderr is a tty, zsh emits the canonical
12545 // error and returns 1. Mirror here (the SHTTY substrate isn't
12546 // ported yet; the libc::isatty check approximates).
12547 if (OPT_ISSET(ops, b'k') || OPT_ISSET(ops, b'q'))
12548 && !OPT_HASARG(ops, b'u')
12549 && !OPT_ISSET(ops, b'p')
12550 {
12551 let stdin_tty = unsafe { libc::isatty(0) } != 0;
12552 let stderr_tty = unsafe { libc::isatty(2) } != 0;
12553 if !stdin_tty && !stderr_tty {
12554 eprintln!("not interactive and can't open terminal");
12555 return 1;
12556 }
12557 }
12558
12559 // c:Src/builtin.c:6510-6515 — `else if (OPT_ISSET(ops,'p')) {
12560 // readfd = coprocin; if (readfd < 0) { zwarnnam(name, "-p: no
12561 // coprocess"); return 1; } izle = 0; }`. C emits this BEFORE
12562 // identifier validation of the reply name, so a bare
12563 // `read -p "var name" REPLY` (no coproc started) errors with
12564 // "-p: no coprocess" rather than "not an identifier". Bug #387.
12565 // The `-u`-with-arg path falls through and shares this same gate
12566 // when its arg is "p" (c:6494) — also handled.
12567 if OPT_ISSET(ops, b'p') || (OPT_HASARG(ops, b'u') && OPT_ARG(ops, b'u').as_deref() == Some("p"))
12568 {
12569 let coprocin =
12570 crate::ported::modules::clone::coprocin.load(std::sync::atomic::Ordering::Relaxed);
12571 if coprocin < 0 {
12572 zwarnnam(name, "-p: no coprocess");
12573 return 1;
12574 }
12575 }
12576
12577 // c:6453-6455 — `return compctlreadptr(name, args, ops, reply)`.
12578 // The compctlreadptr function pointer defaults to
12579 // `fallback_compctlread` (init.c:1834) and gets reassigned to the
12580 // real `compctlread` when zsh/compctl loads via the module's
12581 // boot_ hook. zshrs auto-loads zsh/compctl in default mode so the
12582 // direct call to `compctlread` is correct there. In --zsh parity
12583 // mode zsh/compctl isn't loaded until explicit `zmodload`, so
12584 // route through `fallback_compctlread` instead — matching
12585 // `zsh -fc 'getln -c x'` which emits "no loaded module provides
12586 // read for completion context".
12587 if OPT_ISSET(ops, b'l') || OPT_ISSET(ops, b'c') {
12588 // c:6453
12589 let zsh_mode = crate::IS_ZSH_MODE.load(std::sync::atomic::Ordering::Relaxed);
12590 // In --zsh mode, regardless of MODULESTAB's "loaded" flag
12591 // (which we set true at startup for QoL), route through
12592 // fallback_compctlread because zsh -fc's reported behavior
12593 // for `getln -c` is the fallback message. The MODULESTAB-
12594 // load-flag check below would defeat parity: zshrs marks
12595 // zsh/compctl as default-loaded so its compctl/compcall
12596 // builtins work without explicit zmodload. zsh -fc has only
12597 // zsh/main loaded and only auto-loads compctl when the
12598 // compctl/compcall builtins themselves are called — `read -c`
12599 // does NOT trigger that auto-load (zsh calls
12600 // `compctlreadptr` which defaults to `fallback_compctlread`
12601 // until something explicitly zmodload's zsh/compctl).
12602 //
12603 // The simplest C-faithful parity: in --zsh mode, always emit
12604 // the fallback message. Default zshrs mode keeps calling the
12605 // real compctlread.
12606 if zsh_mode {
12607 return crate::ported::init::fallback_compctlread(name);
12608 }
12609 return compctlread(name, &args[argi..]);
12610 }
12611
12612 // Optional explicit input FD via -u, or coprocin via -p. When
12613 // unspecified, fall back to fd 0 (stdin). All read paths below
12614 // route bytes through `read_byte_from_fd` so `read -u 3 var`
12615 // after `exec 3< file` correctly pulls from the user fd; with
12616 // `-p`, the same path reads from coprocin (set by the BUILTIN_
12617 // RUN_COPROC handler at fusevm_bridge.rs around the
12618 // `coproc CMD` launch). Bug #388.
12619 let ufd: i32 = if OPT_ISSET(ops, b'p') {
12620 crate::ported::modules::clone::coprocin.load(std::sync::atomic::Ordering::Relaxed)
12621 } else if OPT_HASARG(ops, b'u') {
12622 // c:Src/builtin.c:6494-6500 — `fdarg = (int)zstrtol(argptr,
12623 // &eptr, 10); if (*eptr) { zwarnnam(name, "number expected
12624 // after -u: %s", argptr); return 1; }`. zshrs previously
12625 // `unwrap_or(0)`d, silently dropping `read -u abc v` errors.
12626 let argptr = OPT_ARG(ops, b'u').unwrap_or("");
12627 match argptr.parse::<i32>() {
12628 Ok(n) => n,
12629 Err(_) => {
12630 zwarnnam(name, &format!("number expected after -u: {}", argptr));
12631 return 1;
12632 }
12633 }
12634 } else {
12635 0
12636 };
12637 // c:Src/builtin.c:6418 — single-byte reader bound to `ufd`.
12638 // libc::read with len=1 keeps the file position advancing across
12639 // successive calls (matches zsh's per-byte read loop). Returns
12640 // Some(byte) on success, None on EOF, error sentinel on syscall
12641 // failure (caller maps to return 2).
12642 let read_byte = |fd: i32| -> io::Result<Option<u8>> {
12643 let mut b = [0u8; 1];
12644 loop {
12645 let n = unsafe { libc::read(fd, b.as_mut_ptr() as *mut libc::c_void, 1) };
12646 match n {
12647 1 => return Ok(Some(b[0])),
12648 0 => return Ok(None),
12649 -1 => {
12650 let err = io::Error::last_os_error();
12651 if err.kind() == io::ErrorKind::Interrupted {
12652 continue;
12653 }
12654 return Err(err);
12655 }
12656 _ => return Ok(None),
12657 }
12658 }
12659 };
12660
12661 // c:6488-6515 — `-t TIMEOUT` poll(2) wait.
12662 if OPT_HASARG(ops, b't') {
12663 let arg = OPT_ARG(ops, b't').unwrap_or("");
12664 let tmout: f64 = arg.parse().unwrap_or(0.0);
12665 let mut pfd = libc::pollfd {
12666 fd: 0,
12667 events: libc::POLLIN,
12668 revents: 0,
12669 };
12670 let r = unsafe { libc::poll(&mut pfd, 1, (tmout * 1000.0) as i32) };
12671 if r == 0 {
12672 return 4;
12673 } // timeout
12674 if r < 0 {
12675 return 2;
12676 } // error
12677 }
12678
12679 // c:Src/builtin.c:6499-6510 — `read "?prompt"` writes the prompt
12680 // ONLY when input is interactive (stdin is a terminal). C zsh
12681 // gates the write via `isatty(0)` inside the prompt-emit block;
12682 // when stdin is redirected from a pipe or file, the prompt is
12683 // suppressed entirely so the captured output isn't polluted. Bug
12684 // #248 in docs/BUGS.md — previously zshrs printed the prompt to
12685 // stderr unconditionally, so non-interactive callers saw the
12686 // prompt fragment leak into stderr.
12687 if let Some(ref p) = prompt {
12688 let stdin_is_tty = unsafe { libc::isatty(0) } != 0;
12689 if stdin_is_tty {
12690 eprint!("{}", p);
12691 let _ = Write::flush(&mut io::stderr());
12692 }
12693 }
12694
12695 // c:Src/builtin.c:6769-6770 — `-z` pops the front of the ZLE
12696 // bufstack into `zbuf` and reads from THAT instead of the file
12697 // descriptor. When the bufstack is empty the popped value is the
12698 // empty string, which makes `zread` return EOF immediately;
12699 // c:7110-7115 then returns 1 after the reply assignment.
12700 //
12701 // zbuforig = zbuf = (!OPT_ISSET(ops,'z')) ? NULL :
12702 // (nonempty(bufstack)) ? (char *)getlinknode(bufstack) :
12703 // ztrdup("");
12704 // ...
12705 // if (zbuforig) { char first = *zbuforig; ... if (!first) return 1; }
12706 //
12707 // The default delim handling (newline-terminated, raw or
12708 // backslash-cooked) still applies — read up to the first newline
12709 // in the popped buffer or to its end if no newline.
12710 let mut buf = String::new();
12711 if OPT_ISSET(ops, b'z') {
12712 let popped: Option<String> = {
12713 let mut stack = crate::ported::zle::zle_main::BUFSTACK.lock().unwrap();
12714 if stack.is_empty() {
12715 None
12716 } else {
12717 Some(stack.remove(0))
12718 }
12719 };
12720 let zbuf = popped.clone().unwrap_or_default();
12721 let raw_mode = OPT_ISSET(ops, b'r') || OPT_ISSET(ops, b'R');
12722 if OPT_HASARG(ops, b'd') {
12723 let arg = OPT_ARG(ops, b'd').unwrap_or("");
12724 let delim = arg.as_bytes().first().copied().unwrap_or(b'\0');
12725 let mut out = Vec::<u8>::new();
12726 for &b in zbuf.as_bytes() {
12727 if b == delim {
12728 break;
12729 }
12730 out.push(b);
12731 }
12732 buf = String::from_utf8_lossy(&out).into_owned();
12733 } else {
12734 let mut out = Vec::<u8>::new();
12735 let bytes = zbuf.as_bytes();
12736 let mut i = 0;
12737 while i < bytes.len() {
12738 let b = bytes[i];
12739 if !raw_mode && b == b'\\' && i + 1 < bytes.len() {
12740 let nx = bytes[i + 1];
12741 if nx == b'\n' {
12742 i += 2;
12743 continue;
12744 }
12745 out.push(nx);
12746 i += 2;
12747 continue;
12748 }
12749 if b == b'\n' {
12750 break;
12751 }
12752 out.push(b);
12753 i += 1;
12754 }
12755 buf = String::from_utf8_lossy(&out).into_owned();
12756 }
12757 // c:7102-7109 — -e/-E echo, -e suppresses assignment.
12758 let opt_e = OPT_ISSET(ops, b'e');
12759 let opt_big_e = OPT_ISSET(ops, b'E');
12760 if opt_e || opt_big_e {
12761 println!("{}", buf);
12762 }
12763 if !opt_e {
12764 setsparam(&reply, &buf);
12765 }
12766 // c:7110-7115 — empty bufstack pop → return 1 even after assign.
12767 return if popped.is_none() || popped.as_deref() == Some("") {
12768 1
12769 } else {
12770 0
12771 };
12772 }
12773 if OPT_ISSET(ops, b'k') || OPT_ISSET(ops, b'q') {
12774 // c:Src/builtin.c:6630 — `if (OPT_ISSET(ops,'k') ||
12775 // OPT_ISSET(ops,'q'))` — both read raw chars (no line/IFS
12776 // processing). `-q` reads exactly one char (nchars defaults to
12777 // 1) for the yes/no decision applied below. The previous port
12778 // gated only on `-k`, so `-q` fell through to the line reader
12779 // and never applied the y/Y check (always returned 0).
12780 // c:Src/builtin.c — `-k 0` (zero chars requested) is a no-op
12781 // read that zsh treats as failure (returns 1) because no
12782 // bytes can be consumed. Mirror so `read -k 0` exits 1
12783 // instead of "succeeding" with an empty buffer.
12784 if nchars <= 0 {
12785 return 1;
12786 }
12787 let mut got = vec![0u8; nchars as usize];
12788 let mut bytes_read = 0;
12789 while bytes_read < nchars as usize {
12790 match read_byte(ufd) {
12791 Ok(Some(b)) => {
12792 got[bytes_read] = b;
12793 bytes_read += 1;
12794 }
12795 _ => break,
12796 }
12797 }
12798 buf = String::from_utf8_lossy(&got[..bytes_read]).into_owned();
12799 } else if OPT_HASARG(ops, b'd') {
12800 // c:Src/builtin.c:6418 — `-d DELIM`: read until first byte of
12801 // DELIM (zsh uses only first char of arg). EOF mid-record
12802 // returns what was read so far, exit 1 like the default path.
12803 let arg = OPT_ARG(ops, b'd').unwrap_or("");
12804 // c:Src/builtin.c:6418 — empty `-d` arg means NUL delimiter.
12805 // zsh's `STOUC(*OPT_ARG(ops, 'd'))` reads the first byte of
12806 // the arg buffer; for an empty arg the buffer is a single NUL
12807 // terminator, so STOUC yields 0x00. The Rust port's
12808 // `.first().copied()` returns None for empty strings, which
12809 // we have to map to NUL explicitly (matches `read -d '' x`
12810 // reading until \0, used by `find -print0 | while read -d ''`).
12811 let delim = arg.as_bytes().first().copied().unwrap_or(b'\0');
12812 let mut buf_bytes = Vec::<u8>::new();
12813 // c:7045 — the loop breaks on `c == EOF || (c == delim && !zbuf)`, and
12814 // c:7116 keys the exit status off WHICH of the two ended it. "Did any
12815 // byte arrive" can't tell an EOF-terminated record from a
12816 // delimiter-terminated one, so track the delimiter itself.
12817 let mut saw_delim = false;
12818 // c:7055 — `bslash = c == '\\' && !bslash && !OPT_ISSET(ops,'r')`.
12819 // In C there is ONE read loop and `-d` only swaps the `delim` variable,
12820 // so the backslash rules apply to `-d` exactly as they do to a plain
12821 // `read`. zshrs split the loop in two and the `-d` copy dropped them,
12822 // so `read -d ' ' x <<< 'esc\ttab'` kept the backslash (`esc\ttab`)
12823 // where zsh yields `escttab`.
12824 let raw_mode = OPT_ISSET(ops, b'r') || OPT_ISSET(ops, b'R');
12825 let mut bslash = false;
12826 loop {
12827 match read_byte(ufd) {
12828 Ok(Some(b)) => {
12829 // c:7041-7043 — a backslash-escaped DELIMITER is a line
12830 // continuation: both bytes are dropped and the record keeps
12831 // going. `read -d t x <<< 'esc\ttab'` therefore yields `esc`
12832 // (the escaped `t` vanishes; the NEXT `t` terminates).
12833 if bslash && b == delim {
12834 bslash = false; // c:7042
12835 continue; // c:7043
12836 }
12837 if b == delim {
12838 saw_delim = true; // c:7045 (`c == delim`)
12839 break;
12840 }
12841 // c:7055-7057 — an unescaped backslash is consumed and
12842 // escapes the next byte (unless `-r`).
12843 bslash = b == b'\\' && !bslash && !raw_mode; // c:7055
12844 if bslash {
12845 continue; // c:7057
12846 }
12847 buf_bytes.push(b);
12848 }
12849 Ok(None) => break,
12850 // c:Src/builtin.c:7162-7188 zread — a read(2) error
12851 // (EBADF from `read foo <&-`, EIO, ...) returns EOF,
12852 // not a distinct error status; bin_read then reports
12853 // plain eof (exit 1), with no diagnostic.
12854 Err(_) => break,
12855 }
12856 }
12857 buf = String::from_utf8_lossy(&buf_bytes).into_owned();
12858 // c:Src/builtin.c:6418 — `read -d ''` (NUL delimiter) strips
12859 // trailing newlines from the captured content. This matches
12860 // the `find -print0 | while read -d ''` idiom which expects
12861 // path entries without trailing whitespace. zsh's read body
12862 // applies this trim only for the empty-delim case; non-empty
12863 // delimiters keep the raw bytes.
12864 if arg.is_empty() {
12865 while buf.ends_with('\n') {
12866 buf.pop();
12867 }
12868 }
12869 // c:7107 + c:7116-7122 — C assigns the parameter FIRST (`setsparam(reply,
12870 // buf)`) and only then returns 1 if the loop ended at EOF. So an
12871 // unterminated record still lands in the variable but reports failure:
12872 // `read -d : x <<< 'abc'` sets x=abc and returns 1, which is what makes
12873 // `while read -d : x` terminate on a final field with no trailing
12874 // delimiter. The old early `return 1` fired only when NOTHING was read,
12875 // so every EOF-terminated record wrongly reported success (rc=0) — and
12876 // it also skipped the assignment entirely, leaving the variable at its
12877 // previous value instead of clearing it.
12878 //
12879 // `partial_eof` is the same carrier the default line branch below uses;
12880 // the shared `if partial_eof { return 1 }` after the assignment block
12881 // applies the status.
12882 partial_eof = !saw_delim; // c:7116 (`else if (c == EOF)`)
12883 } else {
12884 // Read a line (default behaviour). c:Src/builtin.c:6505
12885 // — without `-r`, backslash-X eats the backslash and keeps
12886 // the literal X (backslash-newline is line continuation).
12887 let raw_mode = OPT_ISSET(ops, b'r') || OPT_ISSET(ops, b'R');
12888 let mut buf_bytes = Vec::<u8>::new();
12889 let mut got_any = false;
12890 let mut saw_newline = false;
12891 loop {
12892 match read_byte(ufd) {
12893 Ok(Some(b)) => {
12894 got_any = true;
12895 if !raw_mode && b == b'\\' {
12896 match read_byte(ufd) {
12897 Ok(Some(nx)) => {
12898 if nx == b'\n' {
12899 // Line continuation — drop both.
12900 continue;
12901 }
12902 buf_bytes.push(nx);
12903 continue;
12904 }
12905 Ok(None) => {
12906 buf_bytes.push(b'\\');
12907 break;
12908 }
12909 // c:7162-7188 zread — read error == EOF.
12910 Err(_) => break,
12911 }
12912 }
12913 if b == b'\n' {
12914 saw_newline = true;
12915 break;
12916 }
12917 buf_bytes.push(b);
12918 }
12919 Ok(None) => break,
12920 // c:7162-7188 zread — read error == EOF (exit 1, no
12921 // message): `exec 3<&-; read foo <&-` exits 1 in zsh.
12922 Err(_) => break,
12923 }
12924 }
12925 if !got_any {
12926 // c:Src/builtin.c — immediate EOF (no bytes read at all).
12927 // zsh clears the target variable(s) BEFORE returning 1
12928 // so `while read line; do …; done < file` leaves `\$line`
12929 // empty after the loop ends (not the last value).
12930 // Previously zshrs returned 1 without touching the var,
12931 // so the loop body's final iteration value persisted.
12932 buf = String::new();
12933 partial_eof = true;
12934 // Fall through to the assignment block which will write
12935 // "" to reply / vars / array. The trailing `if
12936 // partial_eof { return 1; }` (line 8628) preserves the
12937 // EOF status code.
12938 } else {
12939 buf = String::from_utf8_lossy(&buf_bytes).into_owned();
12940 partial_eof = !saw_newline;
12941 }
12942 }
12943
12944 // c:Src/builtin.c:6730-6742 — `-q` (read yes/no): the single char
12945 // read above is "yes" iff it is exactly 'y' or 'Y'; the reply var is
12946 // set to "y"/"n" and the exit status is 0 (yes) / 1 (no). (A timeout
12947 // would be status 2; not modeled here.) This must run BEFORE the
12948 // IFS/array assignment dispatch — `-q` never does line splitting.
12949 if OPT_ISSET(ops, b'q') {
12950 let is_yes = buf == "y" || buf == "Y"; // c:6741
12951 setsparam(&reply, if is_yes { "y" } else { "n" }); // c:6742
12952 return if is_yes { 0 } else { 1 };
12953 }
12954
12955 // Assign to scalar reply, multi-var split, or array.
12956 // c:6685-6735 — `read x y z` splits buf by IFS, fills the first
12957 // N-1 vars with one IFS-separated field each, and stores the
12958 // REST of the line (including embedded IFS chars) into the last
12959 // var. zsh's read is stable on `print "a b c d" | read x y z`:
12960 // x="a", y="b", z="c d".
12961 if want_array {
12962 // c:Src/builtin.c:6685-6735 — `read -A arr` splits on $IFS
12963 // (whitespace-IFS coalesces; non-whitespace-IFS each acts as
12964 // a single delimiter). The previous port hardcoded
12965 // split_whitespace(), which ignored custom IFS like `:` and
12966 // produced a single-element array for `IFS=: read -A arr
12967 // <<< "a:b:c"`. Mirror the multi-var path's IFS handling.
12968 let ifs = getsparam("IFS").unwrap_or_else(|| " \t\n".to_string());
12969 let is_ifs = |c: char| ifs.contains(c);
12970 // c:6863-6869 — the FIRST word's char loop skips leading
12971 // whitespace-IFS (bptr==buf && iwsep → continue), so leading
12972 // whitespace produces no empty element. Mirror with trim_start.
12973 let trimmed = buf.trim_start_matches(|c: char| is_ifs(c) && c.is_whitespace());
12974 // NB: do NOT trim trailing whitespace-IFS. C reads word-by-word
12975 // and, after the last real word terminates on a whitespace
12976 // separator, attempts one more word: it consumes the remaining
12977 // trailing whitespace and hits the delimiter with an empty buf
12978 // and gotnl=1, so c:6929 `(*buf || first || gotnl)` adds a
12979 // trailing empty element. `read -A arr <<< "a b "` → (a b "").
12980 // c:Src/utils.c:3711 spacesplit — a whitespace-IFS char collapses a
12981 // run, and whitespace ADJACENT to a non-whitespace IFS separator is
12982 // absorbed into it (so `a : b` with IFS=" :" is ONE delimiter → 2
12983 // fields), while consecutive non-whitespace separators each delimit
12984 // (`a :: b` → 3, empty preserved). A non-whitespace separator also
12985 // absorbs its own trailing whitespace.
12986 let is_ws_ifs = |c: char| is_ifs(c) && c.is_whitespace();
12987 let is_nonws_ifs = |c: char| is_ifs(c) && !c.is_whitespace();
12988 let mut parts: Vec<String> = Vec::new();
12989 let mut field = String::new();
12990 let mut chars = trimmed.chars().peekable();
12991 while let Some(c) = chars.next() {
12992 if is_ifs(c) {
12993 parts.push(std::mem::take(&mut field));
12994 if c.is_whitespace() {
12995 // Coalesce the whitespace-IFS run.
12996 while chars.peek().copied().is_some_and(is_ws_ifs) {
12997 chars.next();
12998 }
12999 // If that run is followed by a non-whitespace separator,
13000 // it belongs to the SAME delimiter — consume it and its
13001 // trailing whitespace so no empty field appears.
13002 if chars.peek().copied().is_some_and(is_nonws_ifs) {
13003 chars.next();
13004 while chars.peek().copied().is_some_and(is_ws_ifs) {
13005 chars.next();
13006 }
13007 }
13008 } else {
13009 // Non-whitespace separator: absorb its trailing whitespace.
13010 while chars.peek().copied().is_some_and(is_ws_ifs) {
13011 chars.next();
13012 }
13013 }
13014 } else {
13015 field.push(c);
13016 }
13017 }
13018 if !field.is_empty() || !parts.is_empty() {
13019 parts.push(field);
13020 }
13021 // c:Src/builtin.c:6929 — `if (*buf || first || gotnl)`. With
13022 // `gotnl=1` set on EOF (c:6898/6914) and `first=1` initial
13023 // value (c:6771), C's `read -A` adds the empty buf as one
13024 // element even when no bytes were ever read. The resulting
13025 // linked list at c:6949 then yields a 1-element array
13026 // containing "". Without this branch, immediate EOF produced
13027 // a 0-element array — diverging from zsh's "consumed one
13028 // (empty) field" semantics that downstream `${#arr}` checks
13029 // rely on to distinguish "empty line" from "no input".
13030 if parts.is_empty() {
13031 parts.push(String::new());
13032 }
13033 // c:Src/builtin.c:6910-6961 — `-A` with `-e`/`-E`. The word is
13034 // echoed (one per line) when `-e` (main per-word loop, c:6910)
13035 // OR `-E` (array-display loop, c:6949). The array is SET only
13036 // when `-e` is NOT set (c:6921 collects words, c:6945 allocates
13037 // p only when !-e). So `read -AE` echoes AND assigns; `read -Ae`
13038 // echoes but leaves the array untouched. The previous port
13039 // always assigned and never echoed.
13040 let opt_e = OPT_ISSET(ops, b'e');
13041 if opt_e || OPT_ISSET(ops, b'E') {
13042 for w in &parts {
13043 println!("{}", w);
13044 }
13045 }
13046 if !opt_e {
13047 setaparam(&reply, parts); // c:6960 setaparam
13048 }
13049 } else if argi < args.len() {
13050 // Multi-var: `read x y [z]`. First var = reply (already
13051 // consumed); rest are args[argi..]. Split with at most
13052 // `vars.len()` chunks using IFS.
13053 let mut vars: Vec<String> = Vec::with_capacity(args.len() - argi + 1);
13054 vars.push(reply);
13055 for n in &args[argi..] {
13056 vars.push(n.clone());
13057 }
13058 let ifs = getsparam("IFS").unwrap_or_else(|| " \t\n".to_string());
13059 // C zsh splits by ANY char from IFS (whitespace or not).
13060 let is_ifs = |c: char| ifs.contains(c);
13061 // Trim leading IFS-whitespace per zsh's read semantics
13062 // (`a b c` → x=a, y="b c", not x="" y=…).
13063 // c:Src/builtin.c — `-E` echoes each field to stdout as it is read,
13064 // one per line (c:6957 in the -A path, and the equivalent per-word
13065 // display on the multi-var path); `-e` does the same but ASSIGNS
13066 // NOTHING (c:7106 `if (!OPT_ISSET(ops,'e')) setsparam(...)`). This path
13067 // did neither, so `read -E a b` split correctly but printed no echo
13068 // (the whole-line -E echo worked only for a single variable).
13069 let opt_e = OPT_ISSET(ops, b'e');
13070 let opt_echo = opt_e || OPT_ISSET(ops, b'E');
13071 let emit = |var: &str, val: &str| {
13072 if opt_echo {
13073 println!("{val}"); // c:6958 zputs + putchar('\n')
13074 }
13075 if !opt_e {
13076 setsparam(var, val); // c:7106
13077 }
13078 };
13079 let trimmed = buf.trim_start_matches(|c: char| is_ifs(c) && c.is_whitespace());
13080 let mut remaining = trimmed.to_string();
13081 for (i, var) in vars.iter().enumerate() {
13082 if i + 1 == vars.len() {
13083 // Last var: store the remainder, trim trailing IFS.
13084 let final_val = remaining
13085 .trim_end_matches(|c: char| is_ifs(c) && c.is_whitespace())
13086 .to_string();
13087 emit(var, &final_val);
13088 } else {
13089 // Find next IFS char.
13090 match remaining.find(is_ifs) {
13091 Some(idx) => {
13092 let field = remaining[..idx].to_string();
13093 // c:Src/utils.c:3711 spacesplit — skip the whole
13094 // delimiter. The separator char plus the IFS-whitespace
13095 // ABSORBED around it form one delimiter: a whitespace
13096 // separator coalesces its run AND a following non-ws
13097 // separator (with that one's trailing whitespace); a
13098 // non-ws separator absorbs its own trailing whitespace.
13099 // So `x : y : z` (IFS=" :") reads as x, y, z — not
13100 // x, "", "y : z".
13101 let sep = remaining[idx..].chars().next().unwrap();
13102 let is_ws = |c: char| is_ifs(c) && c.is_whitespace();
13103 let after = &remaining[idx + sep.len_utf8()..];
13104 let rest: &str = if sep.is_whitespace() {
13105 let r = after.trim_start_matches(is_ws);
13106 match r.chars().next() {
13107 Some(nc) if is_ifs(nc) && !nc.is_whitespace() => {
13108 r[nc.len_utf8()..].trim_start_matches(is_ws)
13109 }
13110 _ => r,
13111 }
13112 } else {
13113 after.trim_start_matches(is_ws)
13114 };
13115 emit(var, &field);
13116 remaining = rest.to_string();
13117 }
13118 None => {
13119 // No more IFS: this var gets remaining, others empty.
13120 emit(var, &remaining);
13121 remaining.clear();
13122 }
13123 }
13124 }
13125 }
13126 } else {
13127 // c:Src/builtin.c:6843 — `read VAR` single-var path strips
13128 // leading/trailing IFS-whitespace exactly like the multi-var
13129 // last-var fallback above. C's read loop discards leading
13130 // whitespace-IFS via `if (c == sep && IS_SPACE(c)) ... loop`
13131 // (c:6843) before building each word, then on word-end it
13132 // either emits the word or, on the last var, keeps building
13133 // until newline — at which point the trailing whitespace-IFS
13134 // gets discarded by the same skip. Without this, `read line`
13135 // on ` hello ` set `line` to ` hello ` (with spaces)
13136 // instead of `hello` — bug #247.
13137 let ifs = getsparam("IFS").unwrap_or_else(|| " \t\n".to_string());
13138 let is_ifs = |c: char| ifs.contains(c);
13139 let trimmed = buf
13140 .trim_start_matches(|c: char| is_ifs(c) && c.is_whitespace())
13141 .trim_end_matches(|c: char| is_ifs(c) && c.is_whitespace());
13142 // c:Src/builtin.c:7102-7109 — `-e` / `-E` flags. Both echo
13143 // the read content to stdout (`zputs(buf, stdout); putchar
13144 // ('\n')`); `-e` ALSO skips the setsparam (echo-only, no
13145 // assign), while `-E` echoes AND assigns. Bug #434.
13146 let opt_e = OPT_ISSET(ops, b'e');
13147 let opt_big_e = OPT_ISSET(ops, b'E');
13148 if opt_e || opt_big_e {
13149 println!("{}", trimmed);
13150 }
13151 if !opt_e {
13152 setsparam(&reply, trimmed);
13153 }
13154 }
13155 // c:Src/builtin.c:6534 — partial-EOF post-assign exit.
13156 if partial_eof {
13157 return 1;
13158 }
13159 0
13160}
13161
13162/// Port of `zread(int izle, int *readchar, long izle_timeout)` from Src/builtin.c:7134.
13163/// C: `static int zread(int izle, int *readchar, long izle_timeout)` —
13164/// read one byte from stdin (or via ZLE), respecting timeout.
13165pub fn zread(izle: i32, readchar: &mut i32, izle_timeout: i64) -> i32 {
13166 // c:7134
13167 if izle != 0 {
13168 // c:7140
13169 // c:7141-7144 — zleentry(ZLE_CMD_GET_KEY, izle_timeout, NULL, &c);
13170 // Static-link path: ZLE bridge lives in src/ported/zle/*; until
13171 // wired, fall through to plain stdin.
13172 let _ = izle_timeout;
13173 }
13174 if *readchar >= 0 {
13175 // c:7150
13176 let cc = *readchar as u8;
13177 *readchar = -1; // c:7152
13178 return cc as i32;
13179 }
13180 // c:7160 — `read(SHTTY, &cc, 1)` with EINTR retry. Read from the
13181 // controlling tty (SHTTY) when available; stdin fallback
13182 // for non-interactive paths where SHTTY isn't set up.
13183 let mut buf = [0u8; 1];
13184 let fd = {
13185 use std::sync::atomic::Ordering;
13186 let s = crate::ported::init::SHTTY.load(Relaxed);
13187 if s >= 0 {
13188 s
13189 } else {
13190 0
13191 } // c:7167 SHTTY fallback
13192 };
13193 loop {
13194 let n = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut libc::c_void, 1) };
13195 match n {
13196 1 => return buf[0] as i32, // c:7169
13197 0 => return -1, // EOF
13198 -1 if io::Error::last_os_error().kind() == io::ErrorKind::Interrupted => continue,
13199 _ => return -1,
13200 }
13201 }
13202}
13203
13204/// Port of `testlex()` from Src/builtin.c:7200.
13205/// C: `void testlex(void)` — advance the test-builtin lexer one token
13206/// from `testargs` into `tok`/`tokstr`. Maps `-o`→DBAR, `-a`→DAMPER,
13207/// `!`→Bang, `(`→Inpar, `)`→Outpar, otherwise STRING.
13208pub fn testlex() {
13209 // c:7200
13210 // c:7203 — `if (tok == LEXERR) return;`
13211 if TEST_TOK.load(Relaxed) == TEST_LEXERR {
13212 // c:7203
13213 return;
13214 }
13215 // c:7206-7224 — `tokstr = *(curtestarg = testargs);`
13216 let mut targs = TESTARGS.lock().unwrap_or_else(|e| {
13217 TESTARGS.clear_poison();
13218 e.into_inner()
13219 });
13220 let mut idx = TESTARGS_IDX.load(Relaxed) as usize;
13221 let cur = targs.get(idx).cloned(); // c:7206
13222 if let Some(t) = cur.as_ref() {
13223 if let Ok(mut ts) = TOKSTR.lock() {
13224 *ts = t.clone();
13225 } // c:7206
13226 }
13227 // c:7207-7211 — `if (!*testargs) { tok = tok ? NULLTOK : LEXERR; return; }`
13228 let none = cur.is_none() || cur.as_deref() == Some("");
13229 if none {
13230 // c:7207
13231 let prev = TEST_TOK.load(Relaxed);
13232 TEST_TOK.store(
13233 if prev != 0 { TEST_NULLTOK } else { TEST_LEXERR }, // c:7210
13234 Relaxed,
13235 );
13236 return;
13237 }
13238 let arg = cur.unwrap();
13239 let new_tok = match arg.as_str() {
13240 // c:7212
13241 "-o" => TEST_DBAR, // c:7213
13242 "-a" => TEST_DAMPER, // c:7215
13243 "!" => TEST_BANG, // c:7217
13244 "(" => TEST_INPAR, // c:7219
13245 ")" => TEST_OUTPAR, // c:7221
13246 "<" => TEST_INANG, // c:7223
13247 ">" => TEST_OUTANG, // c:7225
13248 _ => TEST_STRING, // c:7227
13249 };
13250 TEST_TOK.store(new_tok, Relaxed);
13251 idx += 1; // c:7228 testargs++
13252 TESTARGS_IDX.store(idx as i32, Relaxed);
13253 let _ = &mut *targs; // ensure lock holds for the duration of mutation
13254}
13255
13256/// Port of `bin_test(char *name, char **argv, UNUSED(Options ops), int func)` from Src/builtin.c:7231.
13257/// C: `int bin_test(char *name, char **argv, UNUSED(Options ops), int func)`
13258/// — the `test` / `[` builtin: when invoked as `[`, requires a trailing
13259/// `]`; XSI-extension paren-stripping for 3/4-arg forms; final
13260/// evalcond dispatch returns 0/1/2.
13261/// WARNING: param names don't match C — Rust=(name, argv, func) vs C=(name, argv, ops, func)
13262pub fn bin_test(
13263 name: &str,
13264 argv: &[String], // c:7231
13265 _ops: &options,
13266 func: i32,
13267) -> i32 {
13268 let mut argv = argv.to_vec();
13269 let mut sense = 0i32; // c:7236
13270
13271 // c:7239-7247 — `[` requires trailing `]`.
13272 if func == BIN_BRACKET {
13273 // c:7239
13274 if argv.is_empty() || argv.last().map(|s| s.as_str()) != Some("]") {
13275 // c:7241
13276 zwarnnam(name, "']' expected"); // c:7243
13277 return 2; // c:7244
13278 }
13279 argv.pop(); // c:7246 (s[-1] = NULL)
13280 }
13281
13282 // c:7249-7250 — empty argv → false (1).
13283 if argv.is_empty() {
13284 // c:7249
13285 return 1; // c:7250
13286 }
13287
13288 // c:Src/parse.c par_cond / par_cond_1 — a SINGLE remaining token is
13289 // always a NON-EMPTY-STRING test (implicit `-n`), no matter what it
13290 // looks like: a unary op (`-z`/`-f`), `!`, a binary op (`<`/`>`/`=`),
13291 // or a paren all need at least one more token to form an operator, and
13292 // with one token there is none. So `[ -z ]` / `[ ! ]` / `[ ( ]` /
13293 // `[ < ]` are true iff the token is non-empty; `[ "" ]` is false.
13294 // Verified vs /opt/homebrew/bin/zsh 5.9. Handle it before the operator
13295 // special-cases below, which otherwise (mis)treat the lone token as an
13296 // operator with a missing operand.
13297 if argv.len() == 1 {
13298 return i32::from(argv[0].is_empty());
13299 }
13300
13301 // c:7257-7274 — XSI 3/4-arg parens + 4-arg `!` extension.
13302 let nargs = argv.len(); // c:7257
13303 if nargs == 3 || nargs == 4 {
13304 // c:7258
13305 // c:7264-7269 — strip `(` ... `)` parens unless the 3-arg middle
13306 // would be a binary op (which takes priority).
13307 if argv[0] == "(" && argv[nargs - 1] == ")" // c:7264
13308 && (nargs != 3 || crate::ported::text::is_cond_binary_op(&argv[1]) == 0)
13309 // c:7265
13310 {
13311 argv.pop(); // c:7266
13312 argv.remove(0); // c:7267
13313 }
13314 }
13315 if argv.len() == 3 && argv[0] == "!" {
13316 // c:7270 (effective)
13317 sense = 1; // c:7271
13318 argv.remove(0); // c:7272
13319 }
13320
13321 // c:Src/parse.c:2486-2492 par_cond_2 — a condition that is a single WORD
13322 // means `-n word`:
13323 // if (n_testargs == 1) {
13324 // /* one argument: [ foo ] is equivalent to [ -n foo ] */
13325 // s1 = tokstr; condlex();
13326 // return par_cond_double(dupstring("-n"), s1);
13327 // }
13328 //
13329 // C needs no special handling around `-a`/`-o` for this: bin_test hands the
13330 // whole argument list to the real grammar (c:7276-7280 `condlex = testlex;
13331 // parse_cond()`), and every operand of a connective is parsed by
13332 // par_cond_2 — so a lone word ANYWHERE becomes `-n word`. This port has no
13333 // grammar to lean on: bin_test is an ad-hoc argument-count parser,
13334 // `testlex` is dead code, and par_cond_2 hardcodes `let n_testargs = 0`,
13335 // disabling the POSIX-test rules it otherwise ports. So do the equivalent
13336 // rewrite explicitly — split on the top-level connectives and expand any
13337 // single-word operand:
13338 // test a -a b → -n a -a -n b → 0 (was "unknown condition: -a")
13339 // test '' -a '' → -n '' -a -n '' → 1
13340 // test -n -a a → -n -n -a -n a → 0 (that first `-n` is an OPERAND)
13341 // test -n a -a -n → -n a -a -n -n → 0 (2-word segment left alone)
13342 //
13343 // Skipped when parentheses are present: segmenting those needs the real
13344 // grammar, and the parenthesised forms already agree. Skipped when any
13345 // segment is empty (`test -a a`), which zsh reports as an error rather
13346 // than parsing.
13347 if !argv.iter().any(|a| a == "(" || a == ")") && argv.iter().any(|a| a == "-a" || a == "-o") {
13348 let segs: Vec<Vec<String>> = argv
13349 .split(|a| a == "-a" || a == "-o")
13350 .map(|s| s.to_vec())
13351 .collect();
13352 if segs.iter().all(|s| !s.is_empty()) {
13353 let mut rebuilt: Vec<String> = Vec::with_capacity(argv.len() + segs.len());
13354 let mut seg_iter = segs.iter();
13355 let mut pending = seg_iter.next();
13356 let mut emitted_in_seg = 0usize;
13357 for a in argv.iter() {
13358 if a == "-a" || a == "-o" {
13359 rebuilt.push(a.clone());
13360 pending = seg_iter.next();
13361 emitted_in_seg = 0;
13362 continue;
13363 }
13364 if emitted_in_seg == 0 {
13365 // c:2488 — one word ⇒ `-n word`. `!` is an operator, never
13366 // an operand, so it does not take the implicit -n.
13367 if let Some(seg) = pending {
13368 if seg.len() == 1 && seg[0] != "!" {
13369 rebuilt.push("-n".to_string());
13370 }
13371 }
13372 }
13373 rebuilt.push(a.clone());
13374 emitted_in_seg += 1;
13375 }
13376 argv = rebuilt;
13377 }
13378 }
13379
13380 // c:Src/parse.c par_cond — 3-arg form with binary op at args[0]
13381 // (instead of args[1]) is a parse error: zsh treats args[0] as a
13382 // unary condition probe; binary ops aren't valid unary ops →
13383 // "unknown condition: ARGV[0]" rc=2. Verified vs
13384 // /opt/homebrew/bin/zsh: `[ -lt 5 3 ]` →
13385 // "zsh:[:1: unknown condition: -lt" rc=2.
13386 // Also: 4-arg with binop at args[1] (the valid binop slot) →
13387 // "too many arguments" because the extra trailing arg can't be
13388 // consumed.
13389 if argv.len() == 3 && argv[0].starts_with('-') && argv[0].len() >= 3 {
13390 if matches!(
13391 argv[0].as_str(),
13392 "-eq" | "-ne" | "-lt" | "-gt" | "-le" | "-ge" | "-nt" | "-ot" | "-ef"
13393 ) {
13394 crate::ported::utils::zwarnnam(name, &format!("unknown condition: {}", argv[0]));
13395 return 2;
13396 }
13397 }
13398 // c:Src/parse.c par_cond — 3-arg form analysis:
13399 // (a) `[ -X arg extra ]` — unary at pos 0, extra at pos 2
13400 // → "too many arguments"
13401 // (b) `[ a OP b ]` with unknown OP → "unknown condition: OP"
13402 // (c) `[ a b c ]` (no recognised op at pos 1) →
13403 // "condition expected: b"
13404 let known_binops: &[&str] = &[
13405 "=",
13406 "==",
13407 "!=",
13408 "<",
13409 ">",
13410 "-eq",
13411 "-ne",
13412 "-lt",
13413 "-gt",
13414 "-le",
13415 "-ge",
13416 "-nt",
13417 "-ot",
13418 "-ef",
13419 "&&",
13420 "||",
13421 "-a",
13422 "-o",
13423 "=~",
13424 "-regex-match",
13425 ];
13426 if argv.len() == 3 && argv[0].starts_with('-') && argv[0].len() == 2 {
13427 let op_char = argv[0].chars().nth(1).unwrap_or(' ');
13428 let is_known_unary = matches!(
13429 op_char,
13430 'a' | 'b'
13431 | 'c'
13432 | 'd'
13433 | 'e'
13434 | 'f'
13435 | 'g'
13436 | 'h'
13437 | 'k'
13438 | 'L'
13439 | 'n'
13440 | 'o'
13441 | 'p'
13442 | 'r'
13443 | 's'
13444 | 'S'
13445 | 't'
13446 | 'u'
13447 | 'v'
13448 | 'w'
13449 | 'x'
13450 | 'z'
13451 | 'G'
13452 | 'N'
13453 | 'O'
13454 );
13455 // For unary-flag-at-pos-0 with a 3rd extra arg, the canonical
13456 // diagnostic is "too many arguments" — UNLESS args[1] is itself
13457 // a binary op (e.g., `-z -lt 5` which is malformed differently).
13458 if is_known_unary && !known_binops.contains(&argv[1].as_str()) {
13459 crate::ported::utils::zwarnnam(name, "too many arguments");
13460 return 2;
13461 }
13462 }
13463 if argv.len() == 3 && !argv[0].starts_with('-') && argv[0] != "!" && argv[0] != "(" {
13464 let mid = argv[1].as_str();
13465 if mid.starts_with('-') && argv[1].len() >= 2 && !known_binops.contains(&mid) {
13466 crate::ported::utils::zwarnnam(name, &format!("unknown condition: {}", argv[1]));
13467 return 2;
13468 }
13469 if !known_binops.contains(&mid) {
13470 crate::ported::utils::zwarn(&format!("condition expected: {}", argv[1]));
13471 return 2;
13472 }
13473 }
13474 if argv.len() == 4
13475 && matches!(
13476 argv[1].as_str(),
13477 "=" | "=="
13478 | "!="
13479 | "-eq"
13480 | "-ne"
13481 | "-lt"
13482 | "-gt"
13483 | "-le"
13484 | "-ge"
13485 | "-nt"
13486 | "-ot"
13487 | "-ef"
13488 )
13489 {
13490 crate::ported::utils::zwarnnam(name, "too many arguments");
13491 return 2;
13492 }
13493 // c:Src/parse.c par_cond — 4-arg form `-FLAG operand extra extra` /
13494 // any 4+ arg layout where args[0] is a recognized unary flag with
13495 // extras after the operand → "too many arguments".
13496 //
13497 // ...but NOT when the third argument is a CONNECTIVE. c:Src/parse.c:2495
13498 // par_cond_2 has no blanket 4-argument rejection: with n_testargs > 2 it
13499 // only special-cases a binary operator in the SECOND position (`=`, `<`,
13500 // `>`, `==`, `!=`, `-<condnum>`) and otherwise falls through to the general
13501 // condition grammar, which happily consumes `-FLAG operand` and then
13502 // applies the `-a`/`-o` that follows. So `test -n a -a b` is `(-n a) && b`
13503 // → 0, and `test -n -a -n x` is `(-n -a) && (-n x)`; both were rejected
13504 // here as "too many arguments".
13505 // The connective can sit at either position: `test -f a -a b` puts it at
13506 // index 2, while `test -n -a -n x` is `(-n -a) && (-n x)` and puts it at
13507 // index 1. C never has to look — the grammar just consumes tokens — so the
13508 // equivalent here is "a connective appears anywhere past argv[0]".
13509 if argv.len() == 4
13510 && argv[0].starts_with('-')
13511 && argv[0].len() == 2
13512 && !argv[1..].iter().any(|a| a == "-a" || a == "-o")
13513 {
13514 let op_char = argv[0].chars().nth(1).unwrap_or(' ');
13515 if matches!(
13516 op_char,
13517 'a' | 'b'
13518 | 'c'
13519 | 'd'
13520 | 'e'
13521 | 'f'
13522 | 'g'
13523 | 'h'
13524 | 'k'
13525 | 'L'
13526 | 'n'
13527 | 'o'
13528 | 'p'
13529 | 'r'
13530 | 's'
13531 | 'S'
13532 | 't'
13533 | 'u'
13534 | 'v'
13535 | 'w'
13536 | 'x'
13537 | 'z'
13538 | 'G'
13539 | 'N'
13540 | 'O'
13541 ) {
13542 crate::ported::utils::zwarnnam(name, "too many arguments");
13543 return 2;
13544 }
13545 }
13546 // c:Src/parse.c par_cond — 4+ args with no recognised connective:
13547 // "condition expected: ARGV[0]" rc=2 (zsh points at the first
13548 // operand, mirroring 2-arg bare-operands message).
13549 if argv.len() >= 4 && argv[0] != "(" {
13550 let has_connective = argv
13551 .iter()
13552 .any(|a| matches!(a.as_str(), "&&" | "||" | "-a" | "-o"));
13553 let has_paren = argv.iter().any(|a| a == "(");
13554 let has_known_binop_mid = argv.iter().any(|a| known_binops.contains(&a.as_str()));
13555 if !has_connective && !has_paren && !has_known_binop_mid {
13556 crate::ported::utils::zwarn(&format!("condition expected: {}", argv[0]));
13557 let _ = name;
13558 return 2;
13559 }
13560 }
13561
13562 // c:Src/builtin.c:7276-7280 + Src/parse.c par_cond — when the
13563 // 2-arg form is `[ -FLAG operand ]` and `-FLAG` isn't a recognized
13564 // unary op letter, par_cond emits `unknown condition: -FLAG` rc=2.
13565 // The cond.rs walker's primary arm falls through to "bare arg /
13566 // implicit -n" silently, returning 0 (truthy). Pre-flight: if
13567 // 2-arg AND argv[0] starts with `-` AND argv[0] isn't a known
13568 // unary op nor `!`/`(`, emit the canonical diagnostic.
13569 // c:Src/builtin.c:7257 + par_cond — paren-balance diagnostics:
13570 // - `[ ( ) ]` empty parens → "argument expected" rc=2
13571 // - `[ ( a ]` unmatched open → "argument expected" rc=2
13572 // - `[ a ) ]` unmatched close → "too many arguments" rc=2
13573 // zshrs's evalcond walker silently returned 2 with no diagnostic;
13574 // mirror C. Walk and check depth.
13575 {
13576 let mut depth: i32 = 0;
13577 let mut surplus_close = false;
13578 let mut had_open = false;
13579 for a in &argv {
13580 if a == "(" {
13581 depth += 1;
13582 had_open = true;
13583 } else if a == ")" {
13584 depth -= 1;
13585 if depth < 0 {
13586 surplus_close = true;
13587 break;
13588 }
13589 }
13590 }
13591 if surplus_close {
13592 crate::ported::utils::zwarnnam(name, "too many arguments");
13593 return 2;
13594 }
13595 if depth > 0 && had_open {
13596 crate::ported::utils::zwarnnam(name, "argument expected");
13597 return 2;
13598 }
13599 }
13600 if argv.len() == 2 && argv[0] == "(" && argv[1] == ")" {
13601 crate::ported::utils::zwarnnam(name, "argument expected");
13602 return 2;
13603 }
13604 if argv.len() == 2 && argv[0].starts_with('-') && argv[0].len() >= 2 && argv[0] != "!" {
13605 let op_char = argv[0].chars().nth(1).unwrap_or(' ');
13606 let is_known_unary = argv[0].len() == 2
13607 && matches!(
13608 op_char,
13609 'a' | 'b'
13610 | 'c'
13611 | 'd'
13612 | 'e'
13613 | 'f'
13614 | 'g'
13615 | 'h'
13616 | 'k'
13617 | 'L'
13618 | 'n'
13619 | 'o'
13620 | 'p'
13621 | 'r'
13622 | 's'
13623 | 'S'
13624 | 't'
13625 | 'u'
13626 | 'v'
13627 | 'w'
13628 | 'x'
13629 | 'z'
13630 | 'G'
13631 | 'N'
13632 | 'O'
13633 );
13634 if !is_known_unary {
13635 crate::ported::utils::zwarnnam(name, &format!("unknown condition: {}", argv[0]));
13636 return 2;
13637 }
13638 } else if argv.len() == 2
13639 && !argv[0].starts_with('-')
13640 && argv[0] != "!"
13641 && argv[0] != "("
13642 && argv[1] != ")"
13643 {
13644 // c:Src/parse.c par_cond — 2-arg form with two bare operands
13645 // (no flag, no `!`, no paren wrapping) is a parse error —
13646 // zsh emits "parse error: condition expected: SECOND_OPERAND"
13647 // (via zwarn, no builtin-name prefix). Verified vs
13648 // /opt/homebrew/bin/zsh: `[ "" "" ]` →
13649 // "zsh:1: parse error: condition expected: " rc=2
13650 crate::ported::utils::zwarn(&format!("parse error: condition expected: {}", argv[0]));
13651 let _ = name;
13652 return 2;
13653 }
13654 // c:Src/builtin.c:7276-7280 — `[ ]`/`test` uses `parse_cond`
13655 // which has no rule for TEST_INANG (`<`) or TEST_OUTANG (`>`) as
13656 // binary string-comparators; the parser errors with
13657 // `condition expected: <`. Only `[[ ]]` (which routes through
13658 // `execcond` instead of this builtin) accepts `<`/`>` for lex
13659 // compare. Bug #98 in docs/BUGS.md — zshrs's `bin_test` routed
13660 // through the shared `cond::evalcond` which accepts them as
13661 // COND_STRLT/COND_STRGTR, so `[ a \< b ]` silently evaluated.
13662 //
13663 // Detect `<`/`>` in a binary-op position (the middle of a 3-arg
13664 // form, or any position after an operand) and emit the canonical
13665 // diagnostic before reaching evalcond.
13666 if argv.iter().any(|a| a == "<" || a == ">") {
13667 let offending = argv
13668 .iter()
13669 .find(|a| a.as_str() == "<" || a.as_str() == ">")
13670 .map(|s| s.as_str())
13671 .unwrap_or("<");
13672 // Use zwarn (cmd=None) — zsh's `[ ]` parse-error format is
13673 // `<script>:<line>: condition expected: <`, NOT
13674 // `<script>:test:<line>:...`. zwarn omits the builtin-name
13675 // segment that zwarnnam adds; zwarn also doesn't set
13676 // errflag, so `echo $?` after the failed test still runs.
13677 crate::ported::utils::zwarn(&format!("condition expected: {}", offending));
13678 let _ = name;
13679 return 2;
13680 }
13681
13682 // c:7276-7301 — zcontext_save + par_cond + evalcond.
13683 // Static-link path: route through cond.rs's evalcond which handles
13684 // the full tokenization + parse + eval inline.
13685 let args_refs: Vec<&str> = argv.iter().map(|s| s.as_str()).collect();
13686 let options = HashMap::new();
13687 let mut variables = HashMap::new();
13688 // C `evalcond` reaches param values through `getvalue` / `getsparam`
13689 // which read paramtab. The previous Rust port populated the
13690 // variables map from `std::env::vars()` — the OS environment —
13691 // so shell-internal vars (not exported) appeared "unset" to
13692 // `[[ -z $var ]]` / `[[ $a = $b ]]` etc. Walk paramtab to mirror
13693 // C; fall back to env for entries the paramtab hasn't imported.
13694 {
13695 let tab = paramtab().read().unwrap();
13696 for (k, pm) in tab.iter() {
13697 // Skip PM_UNSET — these are name-declared-but-no-value.
13698 if (pm.node.flags as u32 & PM_UNSET) != 0 {
13699 continue;
13700 }
13701 let v = pm.u_str.clone().unwrap_or_default();
13702 variables.insert(k.clone(), v);
13703 }
13704 }
13705 // Layer env vars on top of paramtab for the rare case where the
13706 // OS env has a name paramtab hasn't yet imported (e.g. external
13707 // wrapper that exec'd zshrs with env vars).
13708 for (k, v) in env::vars() {
13709 variables.entry(k).or_insert(v);
13710 }
13711 let posix = isset(POSIXBUILTINS);
13712 // c:Src/builtin.c:7305 — `stat = evalcond(state, name);`. The
13713 // `name` argument is C's `fromtest` signal — non-NULL means "called
13714 // from test/[", which enables the strict integer-expression error
13715 // path (c:Src/cond.c:236-251). Bug #411.
13716 let mut ret =
13717 crate::ported::cond::evalcond(&args_refs, &options, &variables, posix, Some(name)); // c:7305
13718
13719 // c:7307-7308 — `if (ret < 2 && sense) ret = !ret;`
13720 if ret < 2 && sense != 0 {
13721 // c:7307
13722 ret = if ret == 0 { 1 } else { 0 }; // c:7308
13723 }
13724 ret // c:7310
13725}
13726
13727/// Port of `bin_times(UNUSED(char *name), UNUSED(char **argv), UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:7324.
13728/// C: `int bin_times(UNUSED args)` — `times(&buf)`; print user/system
13729/// for self then for children, separated by spaces and newlines.
13730/// WARNING: param names don't match C — Rust=(_name, _argv, _func) vs C=(name, argv, ops, func)
13731pub fn bin_times(
13732 _name: &str,
13733 _argv: &[String], // c:7328
13734 _ops: &options,
13735 _func: i32,
13736) -> i32 {
13737 let mut buf: libc::tms = unsafe { std::mem::zeroed() }; // c:7331
13738 // c:7332 — `long clktck = get_clktck();`. The previous Rust port
13739 // inlined a `sysconf(_SC_CLK_TCK)` call here. Route through the
13740 // canonical `get_clktck()` port at jobs.rs:567 so any future
13741 // hardening (caching, error fallback) propagates to every caller.
13742 let clktck = crate::ported::jobs::get_clktck() as f64; // c:7332
13743 let clktck = if clktck <= 0.0 { 100.0 } else { clktck };
13744 // c:7335 — `if (times(&buf) == -1) return 1;`
13745 if unsafe { libc::times(&mut buf) } == (-1i64) as libc::clock_t {
13746 // c:7335
13747 return 1; // c:7336
13748 }
13749 let pttime = |t: libc::clock_t| {
13750 // c:Src/builtin.c:7315-7318 — `printf("%ldm%ld.%02lds", X/(60*clktck),
13751 // X/clktck%clktck, X*100/clktck%100)`. Integer-arithmetic
13752 // breakdown: minutes (no zero-pad), seconds (no zero-pad), and
13753 // centiseconds (2-digit zero-pad). Bug #499 — previous Rust
13754 // port used floating-point `%.3f` with 3-decimal precision,
13755 // diverging from zsh's `0m0.00s`.
13756 let x = t as i64;
13757 let clktck_i = clktck as i64;
13758 let mins = x / (60 * clktck_i);
13759 let secs = (x / clktck_i) % clktck_i;
13760 let csec = (x * 100 / clktck_i) % 100;
13761 print!("{}m{}.{:02}s", mins, secs, csec);
13762 };
13763 pttime(buf.tms_utime); // c:7332
13764 print!(" "); // c:7333
13765 pttime(buf.tms_stime); // c:7334
13766 println!(); // c:7335
13767 pttime(buf.tms_cutime); // c:7336
13768 print!(" "); // c:7337
13769 pttime(buf.tms_cstime); // c:7338
13770 println!(); // c:7339
13771 0 // c:7340
13772}
13773
13774/// Port of `bin_trap(char *name, char **argv, UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:7347.
13775/// C: `int bin_trap(char *name, char **argv, ...)` — list, clear, or
13776/// set signal traps.
13777/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
13778pub fn bin_trap(
13779 name: &str,
13780 argv: &[String], // c:7347
13781 _ops: &options,
13782 _func: i32,
13783) -> i32 {
13784 // PFA-SMR aspect: record `trap HANDLER SIG...` calls. Skip
13785 // listing-only forms (`trap`, `trap -l`, `trap -p`) — those don't
13786 // mutate state.
13787 #[cfg(feature = "recorder")]
13788 if crate::recorder::is_enabled() {
13789 let listing = argv.is_empty() || (argv.len() == 1 && (argv[0] == "-l" || argv[0] == "-p"));
13790 if !listing && argv.len() >= 2 {
13791 let ctx = crate::recorder::recorder_ctx_global();
13792 let handler = &argv[0];
13793 for sig in &argv[1..] {
13794 crate::recorder::emit_trap(sig, handler, ctx.clone());
13795 }
13796 }
13797 }
13798
13799 let mut argv = argv.to_vec();
13800 // c:7353 — `if (*argv && !strcmp(*argv, "--")) argv++;`
13801 if !argv.is_empty() && argv[0] == "--" {
13802 // c:7353
13803 argv.remove(0); // c:7354
13804 }
13805
13806 // c:7357-7380 — no args: list current traps.
13807 if argv.is_empty() {
13808 // c:7357
13809 // Local enum for the merged function-form / string-form
13810 // listing walker below. RUST-ONLY scoping helper for the
13811 // c:Src/builtin.c:7359-7375 if/else-if structure.
13812 enum TrapEntry {
13813 Func(String),
13814 Str(String, String),
13815 }
13816 queue_signals(); // c:7358
13817 let traps = traps_table().lock().map(|t| t.clone()).unwrap_or_default();
13818 // c:Src/builtin.c:7359-7375 — C walks `sigtrapped[]` by signal
13819 // number index, so the listing is naturally sorted by sig num
13820 // (HUP=1, INT=2, …, USR1=30, …). zshrs stores traps in a
13821 // HashMap<name, body> whose iteration order is non-deterministic
13822 // — bug #68 in docs/BUGS.md: `trap` output came out in random
13823 // order vs zsh's stable signum-sorted view. Sort by getsigidx
13824 // before printing so the iteration matches C's array walk.
13825 //
13826 // c:Src/builtin.c:7360-7365 — `if (sigtrapped[sig] & ZSIG_FUNC)
13827 // { hn = gettrapnode(sig, 0); shfunctab->printnode(hn, 0); }`.
13828 // Walk shfunctab for `TRAP<signame>` functions and emit their
13829 // full definition alongside the string-form traps so the
13830 // listing matches zsh. Bug #461 in docs/BUGS.md.
13831 let trap_funcs: Vec<(String, i32, String)> = {
13832 let mut acc: Vec<(String, i32, String)> = Vec::new();
13833 if let Ok(tab) = crate::ported::hashtable::shfunctab_lock().read() {
13834 for (fname, _) in tab.iter() {
13835 if let Some(sig_name) = fname.strip_prefix("TRAP") {
13836 let idx = getsigidx(sig_name);
13837 if idx != -1 {
13838 acc.push((fname.clone(), idx, sig_name.to_string()));
13839 }
13840 }
13841 }
13842 }
13843 acc
13844 };
13845 // Merge function-form + string-form, sort by sig idx
13846 // (function-form FIRST per zsh ordering — c:Src/builtin.c:7359
13847 // walks each signal once; ZSIG_FUNC takes precedence within a
13848 // single signum slot since the C code uses `else if` between
13849 // the two arms).
13850 let mut combined: Vec<(i32, TrapEntry)> = Vec::new();
13851 for (fname, idx, _sig) in &trap_funcs {
13852 combined.push((*idx, TrapEntry::Func(fname.clone())));
13853 }
13854 for (sig, body) in traps.iter() {
13855 let idx = getsigidx(sig);
13856 // Skip if a function-form already exists for this sig
13857 // (C's `else if` semantics — function-form wins).
13858 if trap_funcs.iter().any(|(_, i, _)| *i == idx) {
13859 continue;
13860 }
13861 combined.push((
13862 if idx == -1 { i32::MAX } else { idx },
13863 TrapEntry::Str(sig.clone(), body.clone()),
13864 ));
13865 }
13866 combined.sort_by_key(|(idx, _)| *idx);
13867 for (_idx, entry) in &combined {
13868 match entry {
13869 TrapEntry::Func(fname) => {
13870 if let Ok(tab) = crate::ported::hashtable::shfunctab_lock().read() {
13871 if let Some(shf) = tab.get(fname) {
13872 crate::ported::hashtable::printshfuncnode(shf, 0);
13873 }
13874 }
13875 }
13876 TrapEntry::Str(sig, body) => {
13877 // c:7371 — `s = getpermtext(siglists[sig], NULL, 0);`. C
13878 // holds the body as a compiled Eprog and renders it back
13879 // to source for the listing, so what prints is CANONICAL
13880 // text rather than the string the user typed:
13881 // `trap 'print a; print b' EXIT` lists as
13882 // `trap -- $'print a\nprint b' EXIT` (separators become
13883 // newlines, `if`/`for`/`(` bodies get re-indented).
13884 // zshrs stores the body as raw text, so deparse it here
13885 // the same way. Rendering only at listing time keeps the
13886 // stored text authoritative for execution — a getpermtext
13887 // rendering bug then costs a wrong listing, not a wrong
13888 // trap body.
13889 //
13890 // An empty body takes C's `!siglists[sig]` branch
13891 // (c:7368-7369, prints `trap -- '' SIG`) and is never
13892 // rendered. parse_string is the wordcode parser, whose
13893 // coverage is narrower than the AST parser that executes
13894 // the body (e.g. `(pat)` case arms — see the pin test in
13895 // text.rs), so fall back to the raw text on a parse
13896 // failure rather than dropping the entry.
13897 let rendered = if body.is_empty() {
13898 body.clone()
13899 } else {
13900 crate::ported::exec::parse_string(body, 1)
13901 .map(|p| crate::ported::text::getpermtext(Box::new(p), None, 0))
13902 .unwrap_or_else(|| body.clone())
13903 };
13904 // c:7370-7375 — `printf("trap -- "); quotedzputs(...); printf(" %s\n", name);`
13905 print!("trap -- "); // c:7372
13906 print!("{}", quotedzputs(&rendered)); // c:7373
13907 println!(" {}", sig); // c:7374
13908 }
13909 }
13910 }
13911 unqueue_signals(); // c:7378
13912 return 0; // c:7379
13913 }
13914
13915 // c:7384-7400 — first arg is signal number / single `-` → clear.
13916 let first = &argv[0];
13917 if getsigidx(first) != -1 || first == "-" {
13918 // c:7384
13919 let start = if first == "-" { 1 } else { 0 }; // c:7385
13920 // c:7399 — `return *argv != NULL;`. After a successful loop
13921 // *argv is the trailing NULL (Rust: idx == len after the
13922 // walk); on `break` due to an undefined signal *argv is the
13923 // bad arg (idx < len). Previous Rust port hardcoded
13924 // `return 0`, so `trap - INVALID` would silently report
13925 // success and downstream scripts couldn't detect the bad
13926 // signal name.
13927 let mut had_error = 0i32;
13928 // c:7386-7398 — build the signal list, then run ONE
13929 // `unsettrap` body over it. `trap -` clears every slot
13930 // (c:7387-7388: `for (sig = 0; sig < TRAPCOUNT; sig++)
13931 // unsettrap(sig);`); `trap - SIG…` clears the named ones and
13932 // stops at the first undefined name (c:7390-7398), leaving
13933 // the already-walked signals cleared exactly as C does.
13934 let mut to_clear: Vec<i32> = Vec::new();
13935 if start >= argv.len() {
13936 // c:7386
13937 to_clear.extend(0..crate::ported::signals_h::TRAPCOUNT); // c:7387
13938 } else {
13939 for arg in &argv[start..] {
13940 // c:7390
13941 let sig = getsigidx(arg);
13942 if sig == -1 {
13943 // c:7392
13944 zwarnnam(name, &format!("undefined signal: {}", arg)); // c:7393
13945 had_error = 1; // c:7399 *argv non-NULL on break
13946 break; // c:7394
13947 }
13948 to_clear.push(sig);
13949 }
13950 }
13951 for sig in to_clear {
13952 // c:7388 / c:7397 — `unsettrap(sig)`. C's unsettrap
13953 // (c:Src/signals.c:759) → removetrap (c:Src/signals.c:772)
13954 // does three things the previous zshrs clear path skipped
13955 // by only doing `traps_table().remove()`:
13956 // - c:Src/signals.c:800 `sigtrapped[sig] = 0`, so
13957 // handletrap stops claiming the signal;
13958 // - c:Src/signals.c:815 `signal_default(sig)`, which
13959 // RESTORES THE DEFAULT DISPOSITION — this is why
13960 // `trap 'print caught' USR2; kill -USR2 $$; trap - USR2;
13961 // kill -USR2 $$` kills the shell in zsh instead of
13962 // silently ignoring the second signal;
13963 // - c:Src/signals.c:836-843 `removehashnode(shfunctab,
13964 // node->nam)`, dropping the `TRAP<sig>` function-form
13965 // trap (freed by unsettrap at c:765).
13966 // unsettrap MUST run FIRST: removetrap → dosavetrap (c:774)
13967 // snapshots the CURRENT body for the LOCAL_TRAPS restore.
13968 crate::ported::signals::unsettrap(sig);
13969 // c:Src/signals.c:846 `siglists[sig] = NULL` — the body
13970 // drop. zshrs keeps trap bodies in `traps_table` keyed by
13971 // canonical signal NAME (C indexes siglists[] by number),
13972 // so the number has to be mapped back to the name bin_trap
13973 // installed it under (`0` → `EXIT`).
13974 let key = if sig == 0 {
13975 "EXIT".to_string()
13976 } else {
13977 crate::ported::signals_h::sigs_name(sig)
13978 .map(|s| s.to_string())
13979 .unwrap_or_else(|| sig.to_string())
13980 };
13981 if let Ok(mut t) = traps_table().lock() {
13982 t.remove(&key);
13983 }
13984 // c:Src/signals.c:836-843 — ZSIG_FUNC arm.
13985 if let Ok(mut tab) = crate::ported::hashtable::shfunctab_lock().write() {
13986 tab.remove(&format!("TRAP{}", key));
13987 }
13988 }
13989 return had_error; // c:7399
13990 }
13991
13992 // c:7404-7411 — first arg is the trap body.
13993 let arg = argv.remove(0); // c:7404
13994 // c:7405-7409 — `if (!*arg) prog = &dummy_eprog; else if (!(prog =
13995 // parse_string(arg, 1))) { zwarnnam(name, "couldn't parse trap
13996 // command"); return 1; }`. The body is parsed when the trap is
13997 // installed, so an unparseable body is rejected here rather than
13998 // deferred to signal delivery. An empty body (`trap '' SIG`) takes
13999 // C's dummy_eprog branch and is never parsed. This gate precedes
14000 // the signal checks below, so `trap 'for' BOGUS` reports the parse
14001 // failure, not "undefined signal".
14002 //
14003 // C reaches its parser through parse_string (ported at
14004 // exec.rs:parse_string over the ported lexer). The trap body is
14005 // stored as text and later run through parse_isolated by
14006 // execute_script_zsh_pipeline (vm_helper.rs:2249), so validation
14007 // goes through the same parser that will execute it — otherwise a
14008 // body could pass here and still fail at delivery. Save/restore
14009 // errflag around the probe exactly as vm_helper.rs:2240-2251 does,
14010 // so a rejected body doesn't leave the error flag set and abort the
14011 // enclosing shell. The parser emits its own `parse error` via zerr
14012 // first; this adds C's second diagnostic.
14013 if !arg.is_empty() {
14014 let saved_errflag = errflag.load(Ordering::Relaxed);
14015 errflag.fetch_and(!crate::ported::zsh_h::ERRFLAG_ERROR, Ordering::Relaxed);
14016 let _ = crate::vm_helper::parse_isolated(&arg);
14017 let parse_failed =
14018 (errflag.load(Ordering::Relaxed) & crate::ported::zsh_h::ERRFLAG_ERROR) != 0;
14019 errflag.store(saved_errflag, Ordering::Relaxed);
14020 if parse_failed {
14021 zwarnnam(name, "couldn't parse trap command"); // c:7407
14022 return 1; // c:7408
14023 }
14024 }
14025 if argv.is_empty() {
14026 // c:7411 — when only one arg AND it looks like a signal
14027 // (SIG-prefix or numeric) but didn't resolve to a real
14028 // signal, emit "undefined signal". For an arbitrary string
14029 // body with no following signal, zsh silently accepts and
14030 // installs nothing (no diagnostic). Mirror zsh's behavior:
14031 // skip "signal expected" for non-signal-shaped strings.
14032 if arg.starts_with("SIG") || arg.chars().next().is_some_and(|c| c.is_ascii_digit()) {
14033 zwarnnam(name, &format!("undefined signal: {}", arg)); // c:7413
14034 return 1; // c:7417
14035 }
14036 // Bare string body with no signal — zsh accepts silently.
14037 return 0;
14038 }
14039
14040 // c:7421-7448 — install trap on each named signal.
14041 let mut trap_install_error = 0i32;
14042 for sigarg in &argv {
14043 // c:7421
14044 let sig = getsigidx(sigarg);
14045 if sig == -1 {
14046 // c:7426
14047 zwarnnam(name, &format!("undefined signal: {}", sigarg)); // c:7427
14048 trap_install_error = 1; // c:7445 *argv non-NULL on break
14049 break; // c:7428
14050 }
14051 // c:Src/signals.c — C zsh stores traps in a fixed array
14052 // indexed by signal number. Aliases (`0`, `EXIT`, `SIGEXIT`)
14053 // all resolve to index 0 and share the same slot. The Rust
14054 // port stores by name string, so we must normalize the key
14055 // to the canonical signal name (or "EXIT" for the 0 alias)
14056 // — otherwise `trap 'echo bye' 0` lands in a `"0"` slot
14057 // that nothing else looks up.
14058 let canonical = if sig == 0 {
14059 "EXIT".to_string()
14060 } else if let Some(name) = crate::ported::signals_h::sigs_name(sig) {
14061 // c:Src/builtin.c — when sigarg is numeric (`trap "" 2`),
14062 // the listing path must still emit the SYMBOLIC name
14063 // (`INT`) so the canonical name lookup downstream works.
14064 // Previously zshrs's `to_uppercase()` of "2" left the key
14065 // as "2", so `trap "" 2; trap` listed `trap -- '' 2`
14066 // instead of `trap -- '' INT`. Resolve to the canonical
14067 // signal name via sigs_name(idx).
14068 //
14069 // c:Src/jobs.c:2740 alt_sigs + jobs.c:3089 getsigname — a trap
14070 // set via an ALIAS name lists under the ALIAS, not the
14071 // canonical sigs[] name (C flags it ZSIG_ALIAS). The only
14072 // unconditional alt_sigs entry is `{ "ERR", SIGZERR }`, so
14073 // `trap … ERR; trap` must print `… ERR`, not `… ZERR`. The
14074 // Rust port stores traps by name string; preserve the alias
14075 // the user typed as the key. The dotrap dispatch already
14076 // resolves SIGZERR through both "ZERR" and "ERR"
14077 // (signals.rs:1505), so firing is unaffected.
14078 let normalized = sigarg
14079 .strip_prefix("SIG")
14080 .or_else(|| sigarg.strip_prefix("sig"))
14081 .unwrap_or(sigarg.as_str())
14082 .to_uppercase();
14083 if sig == crate::ported::signals_h::SIGZERR && normalized == "ERR" {
14084 "ERR".to_string()
14085 } else {
14086 name.to_string()
14087 }
14088 } else {
14089 // Strip SIG/sig prefix and uppercase so `SIGINT` / `int`
14090 // / `INT` all map to the same key.
14091 sigarg
14092 .strip_prefix("SIG")
14093 .or_else(|| sigarg.strip_prefix("sig"))
14094 .unwrap_or(sigarg.as_str())
14095 .to_uppercase()
14096 };
14097 // c:Src/signals.c settrap — register both the libc signal
14098 // handler AND the sigtrapped[idx] flag. Without setting
14099 // sigtrapped, handletrap() early-returns 0 (sees the slot
14100 // as "not trapped") and the dotrap dispatch never fires.
14101 // The traps_table entry alone isn't enough — handletrap
14102 // gates on sigtrapped[idx] != 0.
14103 //
14104 // c:Src/signals.c:693 settrap flag semantics:
14105 // ZSIG_FUNC → trap body is a shell function (TRAPSIG)
14106 // no flag → trap body is an Eprog in siglists[sig]
14107 // zshrs stores the body STRING in `traps_table` instead of
14108 // either, but the runtime's `endtrapscope`/`dotrap` dispatch
14109 // (signals.rs:1249) requires the !ZSIG_FUNC arm to fire so
14110 // the traps_table lookup happens. Passing ZSIG_FUNC sent it
14111 // down the shfunc-dispatch arm which fails silently when no
14112 // TRAPSIG function exists — bug #80 in docs/BUGS.md.
14113 //
14114 // SIGEXIT (sig=0) must also reach settrap so locallevel
14115 // tagging lands and `endtrapscope` fires the function-local
14116 // EXIT trap at function exit (the previous `sig > 0` gate
14117 // skipped it entirely).
14118 //
14119 // CRITICAL ORDER: settrap calls unsettrap → dosavetrap which
14120 // SNAPSHOTS the current traps_table body for the save-stack.
14121 // settrap MUST run BEFORE inserting the new body so the saved
14122 // entry captures the OUTER scope's body, not the one we're
14123 // about to install. (Reversed order led to the save loop
14124 // restoring the just-installed body back into traps_table
14125 // after the function exit, causing the script-end trap fire
14126 // to re-run the inner body.)
14127 if sig >= 0 && sig <= crate::ported::signals_h::SIGCOUNT && sig != libc::SIGCHLD as i32 {
14128 // c:Src/signals.c:712 — `if (!(flags & ZSIG_FUNC) && empty_eprog(l))`.
14129 // settrap treats an `l == None` (or empty Eprog) body as
14130 // `trap '' SIG` — i.e. ZSIG_IGNORED + signal_ignore(sig).
14131 // That suppresses the signal entirely AND skips
14132 // install_handler, so a non-empty `trap "echo TRAP" SIG`
14133 // would never reach handletrap → dotrap. C-zsh's bin_trap
14134 // parses the body string into an Eprog via parse_string()
14135 // before passing to settrap. The zshrs port stores the
14136 // body as plain text in `traps_table` and dispatches via
14137 // execute_script (the Eprog parser isn't on the
14138 // critical path), so we need a placeholder Eprog whose
14139 // only purpose is to make empty_eprog() return false and
14140 // steer settrap down the ZSIG_TRAPPED + install_handler
14141 // branch. The placeholder body is never executed — dotrap
14142 // reads the dispatch text from traps_table.
14143 //
14144 // Bug #104 in docs/BUGS.md: signal sent from inside a
14145 // function via `kill -X $$` was lost because settrap had
14146 // routed USR1 to signal_ignore at trap-install time, so
14147 // when the signal hit while doshfunc had queue_signals()
14148 // active, nothing in the queue → nothing to dispatch on
14149 // unqueue. Empty-body path (`trap '' SIG`) still passes
14150 // None as before so the ZSIG_IGNORED branch fires.
14151 let body_eprog: Option<crate::ported::zsh_h::Eprog> = if arg.is_empty() {
14152 None
14153 } else {
14154 let mut prog = crate::ported::zsh_h::eprog::default();
14155 // c:Src/parse.c:584 — empty_eprog returns true on
14156 // `prog[0] == WCB_END`; push any non-WCB_END value
14157 // (1 is arbitrary, matches the existing
14158 // `empty_eprog_non_empty_non_end_returns_false`
14159 // pin test in parse.rs).
14160 prog.prog.push(1);
14161 Some(Box::new(prog))
14162 };
14163 settrap(sig, body_eprog, 0);
14164 }
14165 // c:Src/signals.c:707 — settrap → unsettrap → removetrap
14166 // (c:836-843 removeshfuncnode) drops any pre-existing
14167 // FUNCTION-form trap for this signal BEFORE the new (string-form)
14168 // body is installed. zshrs's removetrap does not touch shfunctab,
14169 // so a leftover `TRAPZERR() { … }` would shadow a later `trap
14170 // 'body' ERR` — the dispatch prefers the function form, so the OLD
14171 // function fired instead of the new string body. Remove it here,
14172 // mirroring the `trap -` unset path above. Keyed by the CANONICAL
14173 // signal name (sigs_name), so both `ERR` and `ZERR` spellings drop
14174 // the same `TRAPZERR` node. Runs for VIRTUAL signals too (ZERR /
14175 // DEBUG / EXIT sit beyond SIGCOUNT, so the settrap block above is
14176 // skipped for them — the earlier placement inside that block never
14177 // fired for ZERR).
14178 let func_key = if sig == 0 {
14179 "EXIT".to_string()
14180 } else {
14181 crate::ported::signals_h::sigs_name(sig)
14182 .map(|s| s.to_string())
14183 .unwrap_or_else(|| sig.to_string())
14184 };
14185 if let Ok(mut tab) = crate::ported::hashtable::shfunctab_lock().write() {
14186 tab.remove(&format!("TRAP{}", func_key));
14187 }
14188 if let Ok(mut t) = traps_table().lock() {
14189 t.insert(canonical.clone(), arg.clone()); // c:7448 (effective)
14190 }
14191 // c:Src/signals.c::settrap — when a string-form trap
14192 // replaces an existing trap, settrap calls unsettrap which
14193 // clears the C `sigfuncs[sig]` shfunc slot. The zshrs port
14194 // dispatches function-form TRAPxxx through shfunctab
14195 // independently of sigfuncs, so a pre-existing TRAPxxx
14196 // function survives the string-form install and BOTH fire
14197 // on signal delivery. Remove any matching TRAPxxx shfunc
14198 // here so dotrap dispatches the just-installed string body
14199 // only. Bug #541 in docs/BUGS.md.
14200 let trap_fn_name = format!("TRAP{}", canonical);
14201 if let Ok(mut tab) = crate::ported::hashtable::shfunctab_lock().write() {
14202 tab.remove(&trap_fn_name);
14203 }
14204 }
14205 trap_install_error
14206}
14207
14208/// Port of `bin_ttyctl(UNUSED(char *name), UNUSED(char **argv), Options ops, UNUSED(int func))` from Src/builtin.c:7454.
14209/// C: `int bin_ttyctl(UNUSED args, Options ops, ...)` — `-f` freezes the
14210/// tty, `-u` unfreezes; otherwise emit `"tty is [not ]frozen"`.
14211/// WARNING: param names don't match C — Rust=(_name, _argv, _func) vs C=(name, argv, ops, func)
14212pub fn bin_ttyctl(
14213 _name: &str,
14214 _argv: &[String], // c:7454
14215 ops: &options,
14216 _func: i32,
14217) -> i32 {
14218 use std::sync::Mutex;
14219 // c:7456-7461 — route through the canonical jobs::TTYFROZEN
14220 // global. The previous builtin.rs duplicate AtomicI32 NEVER synced
14221 // with jobs.rs's Mutex<i32> store; `ttyctl -f` set the local
14222 // Atomic but didn't freeze the tty from the perspective of the
14223 // job-control wait path that reads jobs::TTYFROZEN.
14224 let cell = crate::ported::jobs::TTYFROZEN.get_or_init(|| Mutex::new(0_i32));
14225 if OPT_ISSET(ops, b'f') {
14226 // c:7456
14227 *cell.lock().expect("TTYFROZEN poisoned") = 1; // c:7457
14228 } else if OPT_ISSET(ops, b'u') {
14229 // c:7458
14230 *cell.lock().expect("TTYFROZEN poisoned") = 0; // c:7459
14231 } else {
14232 let f = *cell.lock().expect("TTYFROZEN poisoned");
14233 // c:7461 — `printf("tty is %sfrozen\n", ttyfrozen ? "" : "not ");`
14234 println!("tty is {}frozen", if f != 0 { "" } else { "not " }); // c:7461
14235 }
14236 0 // c:7463
14237}
14238
14239/// Port of `bin_let(UNUSED(char *name), char **argv, UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:7469.
14240/// C: `int bin_let(UNUSED(char *name), char **argv, UNUSED(Options ops),
14241/// UNUSED(int func))` — evaluate each arg as a math expression;
14242/// return 1 if the final value is zero (success/false), 0 if non-zero
14243/// (true), 2 on math error.
14244/// WARNING: param names don't match C — Rust=(_name, argv, _func) vs C=(name, argv, ops, func)
14245pub fn bin_let(
14246 _name: &str,
14247 argv: &[String], // c:7469
14248 _ops: &options,
14249 _func: i32,
14250) -> i32 {
14251 use crate::ported::utils::{errflag, ERRFLAG_ERROR};
14252 use std::sync::atomic::Ordering;
14253
14254 // c:7472 — `mnumber val = zero_mnumber;`
14255 let mut val: mnumber = mnumber {
14256 l: 0,
14257 d: 0.0,
14258 type_: MN_INTEGER,
14259 }; // c:7472
14260 // c:7474-7475 — `while (*argv) val = matheval(*argv++);` — DO walk
14261 // every arg even if one fails. C doesn't break on error mid-loop;
14262 // it just lets errflag accumulate. Previously the Rust port broke
14263 // on first failure, leaving later args unevaluated.
14264 for expr in argv {
14265 // c:7474
14266 match matheval(expr) {
14267 Ok(v) => val = v, // c:7475
14268 Err(msg) => {
14269 // c:Src/math.c:checkunary zerr side-effect — the C
14270 // path writes the parse-error string to stderr via
14271 // mathevali → checkunary → zerr. Rust's matheval
14272 // captures the message in Err and bin_let was
14273 // discarding it via `if let Ok(...)`. Surface it.
14274 zerr(&msg);
14275 // Continue loop; errflag set below resets to local 2.
14276 }
14277 }
14278 }
14279 // c:7476-7480 — math errors are non-fatal in let; CLEAR
14280 // ERRFLAG_ERROR and return the math-error code.
14281 //
14282 // The C source at Src/builtin.c:7479 says `return 2;`, but the
14283 // currently installed zsh 5.9.1 returns 1 (verified: `zsh -fc
14284 // 'let 1/0; echo $?'` → 1). Either the C source rev I'm reading
14285 // (5.9.0.3-test, src/zsh/Config/version.mk) diverges from 5.9.1
14286 // or zsh normalises the rc somewhere in execlist's post-builtin
14287 // path. Match the installed zsh's observable behaviour so the
14288 // `let_division_by_zero` parity probe passes:
14289 // `let 1/0 2>&1; print ex:$?` → "zsh:1: division by zero\nex:1\n"
14290 // Bug surfaces in the dispatch's $? side-channel — returning 1
14291 // here matches both the parity test AND the unit test below
14292 // (`bin_let_clears_errflag_on_math_error`, since the assertion
14293 // there pins the OBSERVED status, not the c:7479 literal).
14294 if (errflag.load(Relaxed) & ERRFLAG_ERROR) != 0 {
14295 // c:7476
14296 errflag.fetch_and(!ERRFLAG_ERROR, Relaxed); // c:7478
14297 return 1; // c:7479 (observed zsh 5.9.1 behaviour)
14298 }
14299 // c:7482 — `return (val.type == MN_INTEGER) ? val.u.l == 0 : val.u.d == 0.0;`
14300 if val.type_ == MN_INTEGER {
14301 // c:7482
14302 (val.l == 0) as i32
14303 } else {
14304 (val.d == 0.0) as i32
14305 }
14306}
14307
14308/// Port of `bin_umask(char *nam, char **args, Options ops, UNUSED(int func))` from Src/builtin.c:7491.
14309/// C: `int bin_umask(char *nam, char **args, Options ops, ...)` —
14310/// set/show file-creation mask. No args → show; numeric arg → octal
14311/// parse; symbolic `[ugoa]+[+-=][rwx]+,...` → walk and apply.
14312/// WARNING: param names don't match C — Rust=(nam, args, _func) vs C=(nam, args, ops, func)
14313pub fn bin_umask(
14314 nam: &str,
14315 args: &[String], // c:7491
14316 ops: &options,
14317 _func: i32,
14318) -> i32 {
14319 // c:7497-7500 — read current umask.
14320 queue_signals(); // c:7497
14321 let mut um: u32 = unsafe { libc::umask(0o777) } as u32; // c:7498
14322 unsafe {
14323 libc::umask(um as libc::mode_t);
14324 } // c:7499
14325 unqueue_signals(); // c:7500
14326
14327 // c:7503-7521 — no args: display.
14328 if args.is_empty() {
14329 // c:7503
14330 if OPT_ISSET(ops, b'S') {
14331 // c:7504
14332 let who_chars = ['u', 'g', 'o']; // c:7505
14333 for (i, who) in who_chars.iter().enumerate() {
14334 // c:7507
14335 print!("{}=", who); // c:7510
14336 let mut what_iter = ['r', 'w', 'x'].iter(); // c:7511
14337 while let Some(w) = what_iter.next() {
14338 // c:7512
14339 if (um & 0o400) == 0 {
14340 // c:7513
14341 print!("{}", w); // c:7514
14342 }
14343 um <<= 1; // c:7515
14344 }
14345 if i < 2 {
14346 print!(",");
14347 } else {
14348 println!();
14349 } // c:7518
14350 }
14351 } else {
14352 // c:7522-7524 — `if (um & 0700) putchar('0'); printf("%03o\n", um);`
14353 if (um & 0o700) != 0 {
14354 // c:7522
14355 print!("0"); // c:7523
14356 }
14357 println!("{:03o}", um); // c:7524
14358 }
14359 return 0; // c:7526
14360 }
14361
14362 // c:7528 — `if (idigit(*s))` numeric form.
14363 let s = &args[0];
14364 if s.chars().next().is_some_and(|c| c.is_ascii_digit()) {
14365 // c:7528
14366 // c:7530 — `um = zstrtol(s, &s, 8);`
14367 match u32::from_str_radix(s, 8) {
14368 // c:7530
14369 Ok(n) => um = n, // c:7530
14370 Err(_) => {
14371 zwarnnam(nam, "bad umask"); // c:7532
14372 return 1; // c:7533
14373 }
14374 }
14375 } else {
14376 // c:7536-7585 — symbolic notation walker.
14377 let bytes = s.as_bytes();
14378 let mut i = 0;
14379 loop {
14380 // c:7544 — `whomask = 0;`
14381 let mut whomask: u32 = 0; // c:7544
14382 // c:7545-7553 — collect ugoa.
14383 while i < bytes.len() {
14384 // c:7545
14385 match bytes[i] {
14386 b'u' => {
14387 whomask |= 0o700;
14388 i += 1;
14389 } // c:7547
14390 b'g' => {
14391 whomask |= 0o070;
14392 i += 1;
14393 } // c:7549
14394 b'o' => {
14395 whomask |= 0o007;
14396 i += 1;
14397 } // c:7551
14398 b'a' => {
14399 whomask |= 0o777;
14400 i += 1;
14401 } // c:7553
14402 _ => break,
14403 }
14404 }
14405 // c:7555 — default whomask = 0777.
14406 if whomask == 0 {
14407 whomask = 0o777;
14408 } // c:7555
14409 // c:7557-7565 — op +/-/=.
14410 let umaskop = if i < bytes.len() { bytes[i] } else { 0 }; // c:7557
14411 if !(umaskop == b'+' || umaskop == b'-' || umaskop == b'=') {
14412 // c:7558
14413 if umaskop != 0 {
14414 // c:7559
14415 zwarnnam(
14416 nam,
14417 &format!("bad symbolic mode operator: {}", umaskop as char),
14418 ); // c:7560
14419 } else {
14420 zwarnnam(nam, "bad umask"); // c:7562
14421 }
14422 return 1; // c:7564
14423 }
14424 i += 1;
14425 // c:7567-7577 — collect rwx.
14426 let mut mask: u32 = 0; // c:7567
14427 while i < bytes.len() && bytes[i] != b',' {
14428 // c:7568
14429 match bytes[i] {
14430 b'r' => mask |= 0o444 & whomask, // c:7570
14431 b'w' => mask |= 0o222 & whomask, // c:7572
14432 b'x' => mask |= 0o111 & whomask, // c:7574
14433 other => {
14434 zwarnnam(
14435 nam,
14436 &format!("bad symbolic mode permission: {}", other as char),
14437 ); // c:7576
14438 return 1; // c:7577
14439 }
14440 }
14441 i += 1;
14442 }
14443 // c:7580-7585 — apply.
14444 match umaskop {
14445 b'+' => um &= !mask, // c:7581
14446 b'-' => um |= mask, // c:7583
14447 _ => um = (um | whomask) & !mask, // c:7585 (=)
14448 }
14449 if i < bytes.len() && bytes[i] == b',' {
14450 // c:7586
14451 i += 1; // c:7587
14452 } else {
14453 break; // c:7589
14454 }
14455 }
14456 if i < bytes.len() {
14457 // c:7591
14458 zwarnnam(
14459 nam,
14460 &format!("bad character in symbolic mode: {}", bytes[i] as char),
14461 ); // c:7592
14462 return 1; // c:7593
14463 }
14464 }
14465 // c:7598 — `umask(um);`
14466 unsafe {
14467 libc::umask(um as libc::mode_t);
14468 } // c:7598
14469 0 // c:7599
14470}
14471
14472/// Port of `bin_notavail(char *nam, UNUSED(char **argv), UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:7604.
14473/// C: `int bin_notavail(char *nam, UNUSED(char **argv),
14474/// UNUSED(Options ops), UNUSED(int func))`
14475/// → `zwarnnam(nam, "not available on this system"); return 1;`
14476/// WARNING: param names don't match C — Rust=(nam, _argv, _func) vs C=(nam, argv, ops, func)
14477pub fn bin_notavail(
14478 nam: &str,
14479 _argv: &[String], // c:7604
14480 _ops: &options,
14481 _func: i32,
14482) -> i32 {
14483 zwarnnam(nam, "not available on this system"); // c:7607
14484 1 // c:7608
14485}
14486// ---------------------------------------------------------------------------
14487// Builtin descriptor.
14488// Port of `struct builtin` from `Src/zsh.h` (the one expanded by the
14489// `BUILTIN` / `BIN_PREFIX` macros at line 1452 of zsh.h).
14490// ---------------------------------------------------------------------------
14491// ---------------------------------------------------------------------------
14492// The master registration table.
14493//
14494// Direct, line-for-line port of `static struct builtin builtins[]`
14495// at `Src/builtin.c:40-137`. Entries appear in the same order so
14496// any diff against the C source stays trivial. The `handler_name`
14497// column points at the canonical Rust port that the dispatcher in
14498// `Executor::register_builtins` (`src/ported/vm_helper`) wires up.
14499// ---------------------------------------------------------------------------
14500/// `BUILTINS` static.
14501pub static BUILTINS: std::sync::LazyLock<Vec<builtin>> = std::sync::LazyLock::new(|| {
14502 vec![
14503 BIN_PREFIX("-", BINF_DASH),
14504 BIN_PREFIX("builtin", BINF_BUILTIN),
14505 BIN_PREFIX("command", BINF_COMMAND),
14506 BIN_PREFIX("exec", BINF_EXEC),
14507 BIN_PREFIX("noglob", BINF_NOGLOB),
14508 BUILTIN(
14509 "[",
14510 BINF_HANDLES_OPTS,
14511 Some(bin_test as HandlerFunc),
14512 0,
14513 -1,
14514 BIN_BRACKET,
14515 None,
14516 None,
14517 ),
14518 BUILTIN(
14519 ".",
14520 BINF_PSPECIAL,
14521 Some(bin_dot as HandlerFunc),
14522 1,
14523 -1,
14524 0,
14525 None,
14526 None,
14527 ),
14528 BUILTIN(
14529 ":",
14530 BINF_PSPECIAL,
14531 Some(bin_true as HandlerFunc),
14532 0,
14533 -1,
14534 0,
14535 None,
14536 None,
14537 ),
14538 BUILTIN(
14539 "alias",
14540 BINF_MAGICEQUALS | BINF_PLUSOPTS,
14541 Some(bin_alias as HandlerFunc),
14542 0,
14543 -1,
14544 0,
14545 Some("Lgmrs"),
14546 None,
14547 ),
14548 BUILTIN(
14549 "autoload",
14550 BINF_PLUSOPTS,
14551 Some(bin_functions as HandlerFunc),
14552 0,
14553 -1,
14554 0,
14555 Some("dmktrRTUwWXz"),
14556 Some("u"),
14557 ),
14558 BUILTIN(
14559 "bg",
14560 0,
14561 Some(bin_fg as HandlerFunc),
14562 0,
14563 -1,
14564 BIN_BG,
14565 None,
14566 None,
14567 ),
14568 BUILTIN(
14569 "break",
14570 BINF_PSPECIAL,
14571 Some(bin_break as HandlerFunc),
14572 0,
14573 1,
14574 BIN_BREAK,
14575 None,
14576 None,
14577 ),
14578 BUILTIN(
14579 "bye",
14580 0,
14581 Some(bin_break as HandlerFunc),
14582 0,
14583 1,
14584 BIN_EXIT,
14585 None,
14586 None,
14587 ),
14588 BUILTIN(
14589 "cd",
14590 BINF_SKIPINVALID | BINF_SKIPDASH | BINF_DASHDASHVALID,
14591 Some(bin_cd as HandlerFunc),
14592 0,
14593 2,
14594 BIN_CD,
14595 Some("qsPL"),
14596 None,
14597 ),
14598 BUILTIN(
14599 "chdir",
14600 BINF_SKIPINVALID | BINF_SKIPDASH | BINF_DASHDASHVALID,
14601 Some(bin_cd as HandlerFunc),
14602 0,
14603 2,
14604 BIN_CD,
14605 Some("qsPL"),
14606 None,
14607 ),
14608 BUILTIN(
14609 "continue",
14610 BINF_PSPECIAL,
14611 Some(bin_break as HandlerFunc),
14612 0,
14613 1,
14614 BIN_CONTINUE,
14615 None,
14616 None,
14617 ),
14618 BUILTIN(
14619 "declare",
14620 BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN,
14621 Some(bin_typeset as HandlerFunc),
14622 0,
14623 -1,
14624 0,
14625 Some("AE:%F:%HL:%R:%TUZ:%afghi:%klmnp:%rtuxz"),
14626 None,
14627 ),
14628 BUILTIN(
14629 "dirs",
14630 0,
14631 Some(bin_dirs as HandlerFunc),
14632 0,
14633 -1,
14634 0,
14635 Some("clpv"),
14636 None,
14637 ),
14638 BUILTIN(
14639 "disable",
14640 0,
14641 Some(bin_enable as HandlerFunc),
14642 0,
14643 -1,
14644 BIN_DISABLE,
14645 Some("afmprs"),
14646 None,
14647 ),
14648 BUILTIN(
14649 "disown",
14650 0,
14651 Some(bin_fg as HandlerFunc),
14652 0,
14653 -1,
14654 BIN_DISOWN,
14655 None,
14656 None,
14657 ),
14658 BUILTIN(
14659 "echo",
14660 BINF_SKIPINVALID,
14661 Some(bin_print as HandlerFunc),
14662 0,
14663 -1,
14664 BIN_ECHO,
14665 Some("neE"),
14666 Some("-"),
14667 ),
14668 BUILTIN(
14669 "emulate",
14670 0,
14671 Some(bin_emulate as HandlerFunc),
14672 0,
14673 -1,
14674 0,
14675 Some("lLR"),
14676 None,
14677 ),
14678 BUILTIN(
14679 "enable",
14680 0,
14681 Some(bin_enable as HandlerFunc),
14682 0,
14683 -1,
14684 BIN_ENABLE,
14685 Some("afmprs"),
14686 None,
14687 ),
14688 BUILTIN(
14689 "eval",
14690 BINF_PSPECIAL,
14691 Some(bin_eval as HandlerFunc),
14692 0,
14693 -1,
14694 BIN_EVAL,
14695 None,
14696 None,
14697 ),
14698 BUILTIN(
14699 "exit",
14700 BINF_PSPECIAL,
14701 Some(bin_break as HandlerFunc),
14702 0,
14703 1,
14704 BIN_EXIT,
14705 None,
14706 None,
14707 ),
14708 BUILTIN(
14709 "export",
14710 BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN,
14711 Some(bin_typeset as HandlerFunc),
14712 0,
14713 -1,
14714 BIN_EXPORT,
14715 Some("E:%F:%HL:%R:%TUZ:%afhi:%lp:%rtu"),
14716 Some("xg"),
14717 ),
14718 BUILTIN(
14719 "false",
14720 0,
14721 Some(bin_false as HandlerFunc),
14722 0,
14723 -1,
14724 0,
14725 None,
14726 None,
14727 ),
14728 // C source (Src/builtin.c:69-73): the argument to -e used to be
14729 // optional; making it required is more consistent.
14730 BUILTIN(
14731 "fc",
14732 0,
14733 Some(bin_fc as HandlerFunc),
14734 0,
14735 -1,
14736 BIN_FC,
14737 Some("aAdDe:EfiIlLmnpPrRst:W"),
14738 None,
14739 ),
14740 BUILTIN(
14741 "fg",
14742 0,
14743 Some(bin_fg as HandlerFunc),
14744 0,
14745 -1,
14746 BIN_FG,
14747 None,
14748 None,
14749 ),
14750 BUILTIN(
14751 "float",
14752 BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN,
14753 Some(bin_typeset as HandlerFunc),
14754 0,
14755 -1,
14756 0,
14757 Some("E:%F:%HL:%R:%Z:%ghlp:%rtux"),
14758 Some("E"),
14759 ),
14760 BUILTIN(
14761 "functions",
14762 BINF_PLUSOPTS,
14763 Some(bin_functions as HandlerFunc),
14764 0,
14765 -1,
14766 0,
14767 Some("ckmMstTuUWx:z"),
14768 None,
14769 ),
14770 BUILTIN(
14771 "getln",
14772 0,
14773 Some(bin_read as HandlerFunc),
14774 0,
14775 -1,
14776 0,
14777 Some("ecnAlE"),
14778 Some("zr"),
14779 ),
14780 BUILTIN(
14781 "getopts",
14782 0,
14783 Some(bin_getopts as HandlerFunc),
14784 2,
14785 -1,
14786 0,
14787 None,
14788 None,
14789 ),
14790 BUILTIN(
14791 "hash",
14792 BINF_MAGICEQUALS,
14793 Some(bin_hash as HandlerFunc),
14794 0,
14795 -1,
14796 0,
14797 Some("Ldfmrv"),
14798 None,
14799 ),
14800 // Src/builtin.c — `#ifdef ZSH_HASH_DEBUG`
14801 // BUILTIN("hashinfo", 0, bin_hashinfo, 0, 0, 0, NULL, NULL)
14802 BUILTIN(
14803 "hashinfo",
14804 0,
14805 Some(crate::ported::hashtable::bin_hashinfo as HandlerFunc),
14806 0,
14807 0,
14808 0,
14809 None,
14810 None,
14811 ),
14812 BUILTIN(
14813 "history",
14814 0,
14815 Some(bin_fc as HandlerFunc),
14816 0,
14817 -1,
14818 BIN_FC,
14819 Some("adDEfiLmnpPrt:"),
14820 Some("l"),
14821 ),
14822 BUILTIN(
14823 "integer",
14824 BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN,
14825 Some(bin_typeset as HandlerFunc),
14826 0,
14827 -1,
14828 0,
14829 Some("HL:%R:%Z:%ghi:%lp:%rtux"),
14830 Some("i"),
14831 ),
14832 BUILTIN(
14833 "jobs",
14834 0,
14835 Some(bin_fg as HandlerFunc),
14836 0,
14837 -1,
14838 BIN_JOBS,
14839 Some("dlpZrs"),
14840 None,
14841 ),
14842 BUILTIN(
14843 "kill",
14844 BINF_HANDLES_OPTS,
14845 Some(crate::ported::jobs::bin_kill as HandlerFunc),
14846 0,
14847 -1,
14848 0,
14849 None,
14850 None,
14851 ),
14852 BUILTIN("let", 0, Some(bin_let as HandlerFunc), 1, -1, 0, None, None),
14853 BUILTIN(
14854 "local",
14855 BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN,
14856 Some(bin_typeset as HandlerFunc),
14857 0,
14858 -1,
14859 0,
14860 Some("AE:%F:%HL:%R:%TUZ:%ahi:%lnp:%rtux"),
14861 None,
14862 ),
14863 BUILTIN(
14864 "logout",
14865 0,
14866 Some(bin_break as HandlerFunc),
14867 0,
14868 1,
14869 BIN_LOGOUT,
14870 None,
14871 None,
14872 ),
14873 // Src/builtin.c — `#if defined(ZSH_MEM) & defined(ZSH_MEM_DEBUG)`
14874 // BUILTIN("mem", 0, bin_mem, 0, 0, 0, "v", NULL)
14875 BUILTIN(
14876 "mem",
14877 0,
14878 Some(crate::ported::mem::bin_mem as HandlerFunc),
14879 0,
14880 0,
14881 0,
14882 Some("v"),
14883 None,
14884 ),
14885 BUILTIN(
14886 "popd",
14887 BINF_SKIPINVALID | BINF_SKIPDASH | BINF_DASHDASHVALID,
14888 Some(bin_cd as HandlerFunc),
14889 0,
14890 1,
14891 BIN_POPD,
14892 Some("q"),
14893 None,
14894 ),
14895 // Src/builtin.c — `#if defined(ZSH_PAT_DEBUG)`
14896 // BUILTIN("patdebug", 0, bin_patdebug, 1, -1, 0, "p", NULL)
14897 BUILTIN("patdebug", 0, None, 1, -1, 0, Some("p"), None),
14898 BUILTIN(
14899 "print",
14900 BINF_PRINTOPTS,
14901 Some(bin_print as HandlerFunc),
14902 0,
14903 -1,
14904 BIN_PRINT,
14905 Some("abcC:Df:ilmnNoOpPrRsSu:v:x:X:z-"),
14906 None,
14907 ),
14908 BUILTIN(
14909 "printf",
14910 BINF_SKIPINVALID | BINF_SKIPDASH,
14911 Some(bin_print as HandlerFunc),
14912 1,
14913 -1,
14914 BIN_PRINTF,
14915 Some("v:"),
14916 None,
14917 ),
14918 BUILTIN(
14919 "pushd",
14920 BINF_SKIPINVALID | BINF_SKIPDASH | BINF_DASHDASHVALID,
14921 Some(bin_cd as HandlerFunc),
14922 0,
14923 2,
14924 BIN_PUSHD,
14925 Some("qsPL"),
14926 None,
14927 ),
14928 BUILTIN(
14929 "pushln",
14930 0,
14931 Some(bin_print as HandlerFunc),
14932 0,
14933 -1,
14934 BIN_PRINT,
14935 None,
14936 Some("-nz"),
14937 ),
14938 BUILTIN(
14939 "pwd",
14940 0,
14941 Some(bin_pwd as HandlerFunc),
14942 0,
14943 0,
14944 0,
14945 Some("rLP"),
14946 None,
14947 ),
14948 BUILTIN(
14949 "r",
14950 0,
14951 Some(bin_fc as HandlerFunc),
14952 0,
14953 -1,
14954 BIN_R,
14955 Some("IlLnr"),
14956 None,
14957 ),
14958 BUILTIN(
14959 "read",
14960 0,
14961 Some(bin_read as HandlerFunc),
14962 0,
14963 -1,
14964 0,
14965 Some("cd:ek:%lnpqrst:%zu:AE"),
14966 None,
14967 ),
14968 BUILTIN(
14969 "readonly",
14970 BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN,
14971 Some(bin_typeset as HandlerFunc),
14972 0,
14973 -1,
14974 BIN_READONLY,
14975 Some("AE:%F:%HL:%R:%TUZ:%afghi:%lptux"),
14976 Some("r"),
14977 ),
14978 BUILTIN(
14979 "rehash",
14980 0,
14981 Some(bin_hash as HandlerFunc),
14982 0,
14983 0,
14984 0,
14985 Some("df"),
14986 Some("r"),
14987 ),
14988 BUILTIN(
14989 "return",
14990 BINF_PSPECIAL,
14991 Some(bin_break as HandlerFunc),
14992 0,
14993 1,
14994 BIN_RETURN,
14995 None,
14996 None,
14997 ),
14998 BUILTIN(
14999 "set",
15000 BINF_PSPECIAL | BINF_HANDLES_OPTS,
15001 Some(bin_set as HandlerFunc),
15002 0,
15003 -1,
15004 0,
15005 None,
15006 None,
15007 ),
15008 BUILTIN(
15009 "setopt",
15010 0,
15011 Some(crate::ported::options::bin_setopt as HandlerFunc),
15012 0,
15013 -1,
15014 BIN_SETOPT,
15015 None,
15016 None,
15017 ),
15018 // c:Src/Builtins/sched.c:375 — sched is a Builtins module
15019 // builtin (zsh/sched). bintab has only one entry. The Rust
15020 // port at builtins/sched.rs::bin_sched (325 lines) was not
15021 // registered, so `sched 09:00 echo morning` returned
15022 // "command not found".
15023 BUILTIN(
15024 "sched",
15025 0,
15026 Some(crate::ported::builtins::sched::bin_sched as HandlerFunc),
15027 0,
15028 -1,
15029 0,
15030 None,
15031 None,
15032 ),
15033 BUILTIN(
15034 "shift",
15035 BINF_PSPECIAL,
15036 Some(bin_shift as HandlerFunc),
15037 0,
15038 -1,
15039 0,
15040 Some("p"),
15041 None,
15042 ),
15043 BUILTIN(
15044 "source",
15045 BINF_PSPECIAL,
15046 Some(bin_dot as HandlerFunc),
15047 1,
15048 -1,
15049 0,
15050 None,
15051 None,
15052 ),
15053 BUILTIN(
15054 "suspend",
15055 0,
15056 Some(crate::ported::jobs::bin_suspend as HandlerFunc),
15057 0,
15058 0,
15059 0,
15060 Some("f"),
15061 None,
15062 ),
15063 BUILTIN(
15064 "test",
15065 BINF_HANDLES_OPTS,
15066 Some(bin_test as HandlerFunc),
15067 0,
15068 -1,
15069 BIN_TEST,
15070 None,
15071 None,
15072 ),
15073 BUILTIN(
15074 "ttyctl",
15075 0,
15076 Some(bin_ttyctl as HandlerFunc),
15077 0,
15078 0,
15079 0,
15080 Some("fu"),
15081 None,
15082 ),
15083 // c:Src/Builtins/rlimits.c:868-870 — limit/ulimit/unlimit are
15084 // declared in the rlimits Builtins-module's bintab. zshrs has the
15085 // free-fn ports at src/ported/builtins/rlimits.rs but never
15086 // registered them; the BUILTIN_NAMES derivation missed them and
15087 // `type limit` etc. returned empty.
15088 BUILTIN(
15089 "limit",
15090 0,
15091 Some(crate::ported::builtins::rlimits::bin_limit as HandlerFunc),
15092 0,
15093 -1,
15094 0,
15095 Some("sh"),
15096 None,
15097 ), // c:rlimits.c:868
15098 BUILTIN(
15099 "ulimit",
15100 0,
15101 Some(crate::ported::builtins::rlimits::bin_ulimit as HandlerFunc),
15102 0,
15103 -1,
15104 0,
15105 None,
15106 None,
15107 ), // c:rlimits.c:869
15108 BUILTIN(
15109 "unlimit",
15110 0,
15111 Some(crate::ported::builtins::rlimits::bin_unlimit as HandlerFunc),
15112 0,
15113 -1,
15114 0,
15115 Some("hs"),
15116 None,
15117 ), // c:rlimits.c:870
15118 BUILTIN(
15119 "times",
15120 BINF_PSPECIAL,
15121 Some(bin_times as HandlerFunc),
15122 0,
15123 0,
15124 0,
15125 None,
15126 None,
15127 ),
15128 BUILTIN(
15129 "trap",
15130 BINF_PSPECIAL | BINF_HANDLES_OPTS,
15131 Some(bin_trap as HandlerFunc),
15132 0,
15133 -1,
15134 0,
15135 None,
15136 None,
15137 ),
15138 BUILTIN(
15139 "true",
15140 0,
15141 Some(bin_true as HandlerFunc),
15142 0,
15143 -1,
15144 0,
15145 None,
15146 None,
15147 ),
15148 BUILTIN(
15149 "type",
15150 0,
15151 Some(bin_whence as HandlerFunc),
15152 0,
15153 -1,
15154 0,
15155 Some("ampfsSw"),
15156 Some("v"),
15157 ),
15158 BUILTIN(
15159 "typeset",
15160 BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN,
15161 Some(bin_typeset as HandlerFunc),
15162 0,
15163 -1,
15164 0,
15165 Some("AE:%F:%HL:%R:%TUZ:%afghi:%klp:%rtuxmnz"),
15166 None,
15167 ),
15168 // `nameref` — zsh/ksh93 module's named-reference declaration
15169 // builtin. Same handler as `typeset` with a tighter opt-string
15170 // (`-g`/`-p`/`-r`/`-u`) and the assign-style invocation. The
15171 // canonical Src/Modules/ksh93.c:bintab entry:
15172 // BUILTIN("nameref", BINF_ASSIGN, bin_typeset, 0, -1, 0,
15173 // "gpru", "n")
15174 // The 9th arg ("n") is the default-flag — typeset_flags inherits
15175 // PM_NAMEREF when the builtin is invoked under this name. zshrs's
15176 // bin_typeset wrapper doesn't read the 9th field yet, but the
15177 // registration still surfaces `nameref` in the tool window /
15178 // completion / hover docs.
15179 BUILTIN(
15180 "nameref",
15181 BINF_ASSIGN,
15182 Some(bin_typeset as HandlerFunc),
15183 0,
15184 -1,
15185 0,
15186 Some("gpru"),
15187 Some("n"),
15188 ),
15189 BUILTIN(
15190 "umask",
15191 0,
15192 Some(bin_umask as HandlerFunc),
15193 0,
15194 1,
15195 0,
15196 Some("S"),
15197 None,
15198 ),
15199 BUILTIN(
15200 "unalias",
15201 0,
15202 Some(bin_unhash as HandlerFunc),
15203 0,
15204 -1,
15205 BIN_UNALIAS,
15206 Some("ams"),
15207 None,
15208 ),
15209 BUILTIN(
15210 "unfunction",
15211 0,
15212 Some(bin_unhash as HandlerFunc),
15213 1,
15214 -1,
15215 BIN_UNFUNCTION,
15216 Some("m"),
15217 Some("f"),
15218 ),
15219 BUILTIN(
15220 "unhash",
15221 0,
15222 Some(bin_unhash as HandlerFunc),
15223 1,
15224 -1,
15225 BIN_UNHASH,
15226 Some("adfms"),
15227 None,
15228 ),
15229 BUILTIN(
15230 "unset",
15231 BINF_PSPECIAL,
15232 Some(bin_unset as HandlerFunc),
15233 1,
15234 -1,
15235 BIN_UNSET,
15236 Some("fmvn"),
15237 None,
15238 ),
15239 BUILTIN(
15240 "unsetopt",
15241 0,
15242 Some(crate::ported::options::bin_setopt as HandlerFunc),
15243 0,
15244 -1,
15245 BIN_UNSETOPT,
15246 None,
15247 None,
15248 ),
15249 BUILTIN(
15250 "wait",
15251 0,
15252 Some(bin_fg as HandlerFunc),
15253 0,
15254 -1,
15255 BIN_WAIT,
15256 None,
15257 None,
15258 ),
15259 BUILTIN(
15260 "whence",
15261 0,
15262 Some(bin_whence as HandlerFunc),
15263 0,
15264 -1,
15265 0,
15266 Some("acmpvfsSwx:"),
15267 None,
15268 ),
15269 BUILTIN(
15270 "where",
15271 0,
15272 Some(bin_whence as HandlerFunc),
15273 0,
15274 -1,
15275 0,
15276 Some("pmsSwx:"),
15277 Some("ca"),
15278 ),
15279 BUILTIN(
15280 "which",
15281 0,
15282 Some(bin_whence as HandlerFunc),
15283 0,
15284 -1,
15285 0,
15286 Some("ampsSwx:"),
15287 Some("c"),
15288 ),
15289 BUILTIN(
15290 "zmodload",
15291 0,
15292 Some(crate::ported::module::bin_zmodload as HandlerFunc),
15293 0,
15294 -1,
15295 0,
15296 Some("AFRILP:abcfdilmpsue"),
15297 None,
15298 ),
15299 BUILTIN(
15300 "zcompile",
15301 0,
15302 Some(crate::ported::parse::bin_zcompile as HandlerFunc),
15303 0,
15304 -1,
15305 0,
15306 Some("tUMRcmzka"),
15307 None,
15308 ),
15309 // Module builtins (zsh/zutil, zsh/cap, zsh/pcre, etc.) — these
15310 // live in src/ported/modules/* and src/ported/zle/* but their
15311 // canonical pub fn signatures match HandlerFunc, so they can be
15312 // dispatched via execbuiltin alongside the main builtins.
15313 BUILTIN(
15314 "zstyle",
15315 0,
15316 Some(crate::ported::modules::zutil::bin_zstyle as HandlerFunc),
15317 0,
15318 -1,
15319 0,
15320 // c:Src/Modules/zutil.c:2139 — `BUILTIN("zstyle", 0,
15321 // bin_zstyle, 0, -1, 0, NULL, NULL)`. NULL optstr: bin_zstyle
15322 // parses args[0] directly (see c:493-512 + c:587-600) so the
15323 // diagnostic format for unknown flags matches zsh's
15324 // "invalid option: -X" (the execbuiltin generic parser would
15325 // have emitted "bad option: -X" which diverges from zsh).
15326 // Bug surfaced via test_zstyle_unknown_flag_errors.
15327 None,
15328 None,
15329 ),
15330 BUILTIN(
15331 "zformat",
15332 0,
15333 Some(crate::ported::modules::zutil::bin_zformat as HandlerFunc),
15334 3,
15335 -1,
15336 0,
15337 // c:Src/Modules/zutil.c:2136 — `BUILTIN("zformat", 0,
15338 // bin_zformat, 3, -1, 0, NULL, NULL)`. NULL optstring:
15339 // bin_zformat reads args[0] as the `-X` selector itself, so
15340 // execbuiltin must not pre-eat the flag. Previously
15341 // optstring="Faf" pre-parsed -F/-a/-f into ops, leaving
15342 // args with the flag-letter stripped — combined with
15343 // minargs=0 this let `zformat -F` slip past the dispatcher
15344 // and emit "missing arguments to -f/-F" from the inner
15345 // check instead of zsh's canonical "not enough arguments".
15346 None,
15347 None,
15348 ),
15349 BUILTIN(
15350 "zparseopts",
15351 0,
15352 Some(crate::ported::modules::zutil::bin_zparseopts as HandlerFunc),
15353 1,
15354 -1,
15355 0,
15356 // c:Src/Modules/zutil.c:2137 — NULL optstring: bin_zparseopts
15357 // parses its own flags (-D/-E/-F/-K/-M/-a/-A/-v) inline. The
15358 // previous Rust spec ("D-EFK-M-a:") let execbuiltin pre-eat
15359 // them via the option-byte parser, leaving bin_zparseopts
15360 // with empty argv and `if i >= args.len()` firing
15361 // "missing option descriptions" for the canonical
15362 // `zparseopts -a foo --` invocation.
15363 None,
15364 None,
15365 ),
15366 BUILTIN(
15367 "zregexparse",
15368 0,
15369 Some(crate::ported::modules::zutil::bin_zregexparse as HandlerFunc),
15370 0,
15371 -1,
15372 0,
15373 Some("c"),
15374 None,
15375 ),
15376 BUILTIN(
15377 "cap",
15378 0,
15379 Some(crate::ported::modules::cap::bin_cap as HandlerFunc),
15380 0,
15381 1,
15382 0,
15383 None,
15384 None,
15385 ),
15386 BUILTIN(
15387 "getcap",
15388 0,
15389 Some(crate::ported::modules::cap::bin_getcap as HandlerFunc),
15390 1,
15391 -1,
15392 0,
15393 None,
15394 None,
15395 ),
15396 BUILTIN(
15397 "setcap",
15398 0,
15399 Some(crate::ported::modules::cap::bin_setcap as HandlerFunc),
15400 1,
15401 -1,
15402 0,
15403 None,
15404 None,
15405 ),
15406 BUILTIN(
15407 "pcre_compile",
15408 0,
15409 Some(crate::ported::modules::pcre::bin_pcre_compile as HandlerFunc),
15410 1,
15411 1,
15412 0,
15413 // c:Src/Modules/pcre.c:521 — `"aimxs"`. The `s` (PCRE2_DOTALL,
15414 // handled at pcre.rs:109) was missing, so `pcre_compile -s`
15415 // errored "bad option: -s".
15416 Some("aimxs"),
15417 None,
15418 ),
15419 BUILTIN(
15420 "pcre_study",
15421 0,
15422 Some(crate::ported::modules::pcre::bin_pcre_study as HandlerFunc),
15423 0,
15424 0,
15425 0,
15426 None,
15427 None,
15428 ),
15429 BUILTIN(
15430 "pcre_match",
15431 0,
15432 Some(crate::ported::modules::pcre::bin_pcre_match as HandlerFunc),
15433 1,
15434 -1,
15435 0,
15436 // c:Src/Modules/pcre.c:522 — `"A:a:v:n:bd"`. `A`(named assoc),
15437 // `a`(capture array), `v`(match var), `n`(start offset) take
15438 // args; `b`(offset pair) and `d`(dfa) are flags. The prior
15439 // "ab:nv:" made `-a` a flag (so `-a arr` ate the subject),
15440 // `-b` arg-taking, and dropped `-A`/`-n:`/`-d` entirely.
15441 Some("A:a:v:n:bd"),
15442 None,
15443 ),
15444 BUILTIN(
15445 "ztcp",
15446 0,
15447 Some(crate::ported::modules::tcp::bin_ztcp as HandlerFunc),
15448 0,
15449 -1,
15450 0,
15451 // c:Src/Modules/tcp.c — `"acd:flLtv"`. `-d` (target fd) takes
15452 // an argument; zshrs had "acdflLtv" (d a flag), so `ztcp -d 9`
15453 // treated 9 as a positional and OPT_ARG(d) was empty.
15454 Some("acd:flLtv"),
15455 None,
15456 ),
15457 BUILTIN(
15458 "ztie",
15459 0,
15460 Some(crate::ported::modules::db_gdbm::bin_ztie as HandlerFunc),
15461 0,
15462 -1,
15463 0,
15464 Some("d:f:r"),
15465 None,
15466 ),
15467 BUILTIN(
15468 "zuntie",
15469 0,
15470 Some(crate::ported::modules::db_gdbm::bin_zuntie as HandlerFunc),
15471 1,
15472 -1,
15473 0,
15474 Some("u"),
15475 None,
15476 ),
15477 BUILTIN(
15478 "zgdbmpath",
15479 0,
15480 Some(crate::ported::modules::db_gdbm::bin_zgdbmpath as HandlerFunc),
15481 1,
15482 1,
15483 0,
15484 None,
15485 None,
15486 ),
15487 BUILTIN(
15488 "echoti",
15489 0,
15490 Some(crate::ported::modules::terminfo::bin_echoti as HandlerFunc),
15491 1,
15492 -1,
15493 0,
15494 None,
15495 None,
15496 ),
15497 BUILTIN(
15498 "fg",
15499 0,
15500 Some(bin_fg as HandlerFunc),
15501 0,
15502 -1,
15503 BIN_FG,
15504 None,
15505 None,
15506 ),
15507 BUILTIN(
15508 "kill",
15509 BINF_HANDLES_OPTS,
15510 Some(crate::ported::jobs::bin_kill as HandlerFunc),
15511 0,
15512 -1,
15513 0,
15514 None,
15515 None,
15516 ),
15517 BUILTIN(
15518 "suspend",
15519 0,
15520 Some(crate::ported::jobs::bin_suspend as HandlerFunc),
15521 0,
15522 0,
15523 0,
15524 Some("f"),
15525 None,
15526 ),
15527 BUILTIN(
15528 "bindkey",
15529 0,
15530 Some(crate::ported::zle::zle_keymap::bin_bindkey as HandlerFunc),
15531 0,
15532 -1,
15533 0,
15534 // c:Src/Zle/zle_main.c:2209 — `"evaM:ldDANmrsLRp"`. The
15535 // `:` after `M` marks it as taking a required arg
15536 // (`-M <keymap>`); without it the parser consumes the
15537 // keymap name as a positional and `-M` falls back to
15538 // "main" → wrong keymap returned for every `-M X` call.
15539 Some("evaM:ldDANmrsLRp"),
15540 None,
15541 ),
15542 BUILTIN(
15543 "vared",
15544 0,
15545 Some(crate::ported::zle::zle_main::bin_vared as HandlerFunc),
15546 1,
15547 1,
15548 0,
15549 Some("aAcef:ghi:M:m:p:r:t:"),
15550 None,
15551 ),
15552 // C: `BUILTIN("compadd", BINF_HANDLES_OPTS, bin_compadd, 0, -1, 0,
15553 // NULL, NULL)` (complete.c:1693). compadd parses ALL its own
15554 // options (the char-by-char loop in bin_compadd_body). The earlier
15555 // registration gave it an incomplete getopt spec ("J:V:1X:fnqQF:Wsi")
15556 // WITHOUT BINF_HANDLES_OPTS, so the dispatcher pre-parsed and
15557 // STRIPPED -J/-V/-X/… into `ops` and handed bin_compadd only the
15558 // positional words — every `compadd -J group -X expl …` lost its
15559 // group name and explanation (second `compadd -J` groups vanished,
15560 // no group headers). Must be HANDLES_OPTS + no optspec so argv
15561 // reaches bin_compadd raw.
15562 BUILTIN(
15563 "compadd",
15564 BINF_HANDLES_OPTS,
15565 Some(crate::ported::zle::complete::bin_compadd as HandlerFunc),
15566 0,
15567 -1,
15568 0,
15569 None,
15570 None,
15571 ),
15572 // C: `BUILTIN("compset", 0, bin_compset, 1, 3, 0, NULL, NULL)`
15573 // (complete.c:1694). bin_compset parses `argv[0]` (-n/-p/-q/-P/-S)
15574 // ITSELF; the earlier Rust registration added a getopt spec
15575 // ("npqPS:") that C does NOT have, so the dispatcher pre-parsed and
15576 // STRIPPED those options from argv before bin_compset ran —
15577 // `argv[0].starts_with('-')` then failed and every shell-invoked
15578 // `compset -p/-q/-n …` no-opped (word-splitting completers broke).
15579 // Must be NULL optspec (+ max 3) so argv reaches bin_compset raw.
15580 BUILTIN(
15581 "compset",
15582 0,
15583 Some(crate::ported::zle::complete::bin_compset as HandlerFunc),
15584 1,
15585 3,
15586 0,
15587 None,
15588 None,
15589 ),
15590 // c:Src/Zle/computil.c:5103-5110 — zsh/computil module's 8
15591 // builtins drive compsys (the canonical completion system).
15592 // All have HandlerFunc-compatible signatures already; just
15593 // need BUILTINS-table registration. Without these,
15594 // _describe / _arguments / _values / _files / _groups / etc.
15595 // (compsys's primary entry points) silently no-op.
15596 BUILTIN(
15597 "comparguments",
15598 0,
15599 Some(crate::ported::zle::computil::bin_comparguments as HandlerFunc),
15600 1,
15601 -1,
15602 0,
15603 None,
15604 None,
15605 ), // c:5103
15606 BUILTIN(
15607 "compdescribe",
15608 0,
15609 Some(crate::ported::zle::computil::bin_compdescribe as HandlerFunc),
15610 3,
15611 -1,
15612 0,
15613 None,
15614 None,
15615 ), // c:5104
15616 BUILTIN(
15617 "compfiles",
15618 0,
15619 Some(crate::ported::zle::computil::bin_compfiles as HandlerFunc),
15620 1,
15621 -1,
15622 0,
15623 None,
15624 None,
15625 ), // c:5105
15626 BUILTIN(
15627 "compgroups",
15628 0,
15629 Some(crate::ported::zle::computil::bin_compgroups as HandlerFunc),
15630 1,
15631 -1,
15632 0,
15633 None,
15634 None,
15635 ), // c:5106
15636 BUILTIN(
15637 "compquote",
15638 0,
15639 Some(crate::ported::zle::computil::bin_compquote as HandlerFunc),
15640 1,
15641 -1,
15642 0,
15643 Some("p"),
15644 None,
15645 ), // c:5107
15646 BUILTIN(
15647 "comptags",
15648 0,
15649 Some(crate::ported::zle::computil::bin_comptags as HandlerFunc),
15650 1,
15651 -1,
15652 0,
15653 None,
15654 None,
15655 ), // c:5108
15656 BUILTIN(
15657 "comptry",
15658 0,
15659 Some(crate::ported::zle::computil::bin_comptry as HandlerFunc),
15660 0,
15661 -1,
15662 0,
15663 None,
15664 None,
15665 ), // c:5109
15666 BUILTIN(
15667 "compvalues",
15668 0,
15669 Some(crate::ported::zle::computil::bin_compvalues as HandlerFunc),
15670 1,
15671 -1,
15672 0,
15673 None,
15674 None,
15675 ), // c:5110
15676 // c:Src/Modules/system.c:819-824 — zsh/system module builtins.
15677 BUILTIN(
15678 "syserror",
15679 0,
15680 Some(crate::ported::modules::system::bin_syserror as HandlerFunc),
15681 0,
15682 1,
15683 0,
15684 Some("e:p:"),
15685 None,
15686 ), // c:819
15687 BUILTIN(
15688 "sysread",
15689 0,
15690 Some(crate::ported::modules::system::bin_sysread as HandlerFunc),
15691 0,
15692 1,
15693 0,
15694 Some("c:i:o:s:t:"),
15695 None,
15696 ), // c:820
15697 BUILTIN(
15698 "syswrite",
15699 0,
15700 Some(crate::ported::modules::system::bin_syswrite as HandlerFunc),
15701 1,
15702 1,
15703 0,
15704 Some("c:o:"),
15705 None,
15706 ), // c:821
15707 BUILTIN(
15708 "sysopen",
15709 0,
15710 Some(crate::ported::modules::system::bin_sysopen as HandlerFunc),
15711 1,
15712 1,
15713 0,
15714 Some("rwau:o:m:"),
15715 None,
15716 ), // c:822
15717 BUILTIN(
15718 "sysseek",
15719 0,
15720 Some(crate::ported::modules::system::bin_sysseek as HandlerFunc),
15721 1,
15722 1,
15723 0,
15724 Some("u:w:"),
15725 None,
15726 ), // c:823
15727 BUILTIN(
15728 "zsystem",
15729 0,
15730 Some(crate::ported::modules::system::bin_zsystem as HandlerFunc),
15731 1,
15732 -1,
15733 0,
15734 None,
15735 None,
15736 ), // c:824
15737 // c:Src/Modules/zselect.c:272 — zsh/zselect module.
15738 BUILTIN(
15739 "zselect",
15740 0,
15741 Some(crate::ported::modules::zselect::bin_zselect as HandlerFunc),
15742 0,
15743 -1,
15744 0,
15745 None,
15746 None,
15747 ), // c:272
15748 // c:Src/Modules/socket.c:276 — zsh/socket module.
15749 BUILTIN(
15750 "zsocket",
15751 0,
15752 Some(crate::ported::modules::socket::bin_zsocket as HandlerFunc),
15753 0,
15754 3,
15755 0,
15756 Some("ad:ltv"),
15757 None,
15758 ), // c:276
15759 // c:Src/Modules/stat.c:637 — zsh/stat module.
15760 BUILTIN(
15761 "stat",
15762 0,
15763 Some(crate::ported::modules::stat::bin_stat as HandlerFunc),
15764 0,
15765 -1,
15766 0,
15767 None,
15768 None,
15769 ), // c:637
15770 // c:Src/Modules/stat.c:638 — `zstat` alias, same handler.
15771 BUILTIN(
15772 "zstat",
15773 0,
15774 Some(crate::ported::modules::stat::bin_stat as HandlerFunc),
15775 0,
15776 -1,
15777 0,
15778 None,
15779 None,
15780 ), // c:638
15781 // c:Src/Modules/watch.c:694 — zsh/watch module's `log`.
15782 BUILTIN(
15783 "log",
15784 0,
15785 Some(crate::ported::modules::watch::bin_log as HandlerFunc),
15786 0,
15787 0,
15788 0,
15789 None,
15790 None,
15791 ), // c:694
15792 // c:Src/Modules/zprof.c:315 — zsh/zprof module.
15793 BUILTIN(
15794 "zprof",
15795 0,
15796 Some(crate::ported::modules::zprof::bin_zprof as HandlerFunc),
15797 0,
15798 0,
15799 0,
15800 Some("c"),
15801 None,
15802 ), // c:315
15803 // c:Src/Modules/datetime.c:239 — zsh/datetime module.
15804 BUILTIN(
15805 "strftime",
15806 0,
15807 Some(crate::ported::modules::datetime::bin_strftime as HandlerFunc),
15808 1,
15809 3,
15810 0,
15811 Some("nqrs:"),
15812 None,
15813 ), // c:239
15814 // c:Src/Modules/zftp.c:189 — zsh/zftp module.
15815 BUILTIN(
15816 "zftp",
15817 0,
15818 Some(crate::ported::modules::zftp::bin_zftp as HandlerFunc),
15819 1,
15820 -1,
15821 0,
15822 None,
15823 None,
15824 ), // c:189
15825 // c:Src/Modules/zpty.c:882 — zsh/zpty module.
15826 BUILTIN(
15827 "zpty",
15828 0,
15829 Some(crate::ported::modules::zpty::bin_zpty as HandlerFunc),
15830 0,
15831 -1,
15832 0,
15833 Some("ebdmrwLnt"),
15834 None,
15835 ), // c:882
15836 // c:Src/Modules/curses.c:1632 — zsh/curses module.
15837 BUILTIN(
15838 "zcurses",
15839 0,
15840 Some(crate::ported::modules::curses::bin_zcurses as HandlerFunc),
15841 1,
15842 -1,
15843 0,
15844 Some(""),
15845 None,
15846 ), // c:1632
15847 // c:Src/Modules/clone.c:110 — zsh/clone module (Linux only;
15848 // bin_clone on non-Linux is the "not available" stub).
15849 BUILTIN(
15850 "clone",
15851 0,
15852 Some(crate::ported::modules::clone::bin_clone as HandlerFunc),
15853 1,
15854 1,
15855 0,
15856 None,
15857 None,
15858 ), // c:110
15859 // c:Src/Modules/example.c — zsh/example module (template).
15860 BUILTIN(
15861 "example",
15862 0,
15863 Some(crate::ported::modules::example::bin_example as HandlerFunc),
15864 0,
15865 -1,
15866 0,
15867 Some("flags"),
15868 None,
15869 ),
15870 // c:Src/Modules/param_private.c:652 — zsh/param/private module.
15871 // BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN.
15872 BUILTIN(
15873 "private",
15874 BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN,
15875 Some(crate::ported::modules::param_private::bin_private as HandlerFunc),
15876 0,
15877 -1,
15878 0,
15879 Some("AE:%F:%HL:%PR:%TUZ:%ahi:%lnmrtux"),
15880 Some("P"),
15881 ), // c:652
15882 // c:Src/Modules/termcap.c:139 — zsh/termcap module.
15883 BUILTIN(
15884 "echotc",
15885 0,
15886 Some(crate::ported::modules::termcap::bin_echotc as HandlerFunc),
15887 1,
15888 -1,
15889 0,
15890 None,
15891 None,
15892 ), // c:139
15893 // c:Src/Zle/compctl.c:4000-4001 — zsh/compctl module.
15894 BUILTIN(
15895 "compcall",
15896 0,
15897 Some(crate::ported::zle::compctl::bin_compcall as HandlerFunc),
15898 0,
15899 0,
15900 0,
15901 Some("TD"),
15902 None,
15903 ), // c:4000
15904 BUILTIN(
15905 "compctl",
15906 0,
15907 Some(crate::ported::zle::compctl::bin_compctl as HandlerFunc),
15908 0,
15909 -1,
15910 0,
15911 None,
15912 None,
15913 ), // c:4001
15914 // c:Src/Modules/attr.c:220-223 — zsh/attr module (4 builtins).
15915 BUILTIN(
15916 "zgetattr",
15917 0,
15918 Some(crate::ported::modules::attr::bin_getattr as HandlerFunc),
15919 2,
15920 3,
15921 0,
15922 Some("h"),
15923 None,
15924 ), // c:220
15925 BUILTIN(
15926 "zsetattr",
15927 0,
15928 Some(crate::ported::modules::attr::bin_setattr as HandlerFunc),
15929 3,
15930 3,
15931 0,
15932 Some("h"),
15933 None,
15934 ), // c:221
15935 BUILTIN(
15936 "zdelattr",
15937 0,
15938 Some(crate::ported::modules::attr::bin_delattr as HandlerFunc),
15939 2,
15940 -1,
15941 0,
15942 Some("h"),
15943 None,
15944 ), // c:222
15945 BUILTIN(
15946 "zlistattr",
15947 0,
15948 Some(crate::ported::modules::attr::bin_listattr as HandlerFunc),
15949 1,
15950 2,
15951 0,
15952 Some("h"),
15953 None,
15954 ), // c:223
15955 BUILTIN(
15956 "zle",
15957 0,
15958 Some(crate::ported::zle::zle_thingy::bin_zle as HandlerFunc),
15959 0,
15960 -1,
15961 0,
15962 // c:Src/Zle/zle_main.c:2212 — exact C optstr. The previous
15963 // string dropped g/G/r/w, so `zle -w -F fd handler`
15964 // (zzcomplete's zz-deploy-code:27 widget-mode fd watcher)
15965 // died with "bad option: -w" before bin_zle ever ran.
15966 Some("aAcCDfFgGIKlLmMNrRTUw"),
15967 None,
15968 ),
15969 // zsh/files module — file-manipulation builtins. All have
15970 // HandlerFunc-compatible signatures already.
15971 BUILTIN(
15972 "mkdir",
15973 0,
15974 Some(crate::ported::modules::files::bin_mkdir as HandlerFunc),
15975 1,
15976 -1,
15977 0,
15978 Some("pm:"),
15979 None,
15980 ),
15981 BUILTIN(
15982 "rmdir",
15983 0,
15984 Some(crate::ported::modules::files::bin_rmdir as HandlerFunc),
15985 1,
15986 -1,
15987 0,
15988 None,
15989 None,
15990 ),
15991 BUILTIN(
15992 "ln",
15993 0,
15994 Some(crate::ported::modules::files::bin_ln as HandlerFunc),
15995 1,
15996 -1,
15997 0,
15998 Some("dfins"),
15999 None,
16000 ),
16001 // `mv` — zsh/files. Same handler as `ln` with BIN_MV dispatch.
16002 // c:Src/Modules/files.c — `BUILTIN("mv", 0, bin_ln, 2, -1, BIN_MV, "fi", NULL)`.
16003 BUILTIN(
16004 "mv",
16005 0,
16006 Some(crate::ported::modules::files::bin_ln as HandlerFunc),
16007 2,
16008 -1,
16009 crate::ported::modules::files::BIN_MV,
16010 Some("fi"),
16011 None,
16012 ),
16013 BUILTIN(
16014 "rm",
16015 0,
16016 Some(crate::ported::modules::files::bin_rm as HandlerFunc),
16017 1,
16018 -1,
16019 0,
16020 Some("dfiRrs"),
16021 None,
16022 ),
16023 BUILTIN(
16024 "chmod",
16025 0,
16026 Some(crate::ported::modules::files::bin_chmod as HandlerFunc),
16027 2,
16028 -1,
16029 0,
16030 Some("Rs"),
16031 None,
16032 ),
16033 // c:Src/Modules/files.c:806 — BUILTIN("chgrp", 0, bin_chown, 2, -1, BIN_CHGRP, "hRs", NULL)
16034 BUILTIN(
16035 "chgrp",
16036 0,
16037 Some(crate::ported::modules::files::bin_chown as HandlerFunc),
16038 2,
16039 -1,
16040 crate::ported::modules::files::BIN_CHGRP,
16041 Some("hRs"),
16042 None,
16043 ),
16044 // c:Src/Modules/files.c:808 — BUILTIN("chown", 0, bin_chown, 2, -1, BIN_CHOWN, "hRs", NULL)
16045 BUILTIN(
16046 "chown",
16047 0,
16048 Some(crate::ported::modules::files::bin_chown as HandlerFunc),
16049 2,
16050 -1,
16051 crate::ported::modules::files::BIN_CHOWN,
16052 Some("hRs"),
16053 None,
16054 ),
16055 BUILTIN(
16056 "sync",
16057 0,
16058 Some(crate::ported::modules::files::bin_sync as HandlerFunc),
16059 0,
16060 0,
16061 0,
16062 None,
16063 None,
16064 ),
16065 // c:Src/Modules/files.c:816-824 — zf_* aliases. Same handlers as
16066 // chmod/chown/ln/mkdir/rm/rmdir/sync but separate BUILTIN entries
16067 // so `autoload -U zf_*` resolves and `zsh -f` sees them all.
16068 BUILTIN(
16069 "zf_chgrp",
16070 0,
16071 Some(crate::ported::modules::files::bin_chown as HandlerFunc),
16072 2,
16073 -1,
16074 crate::ported::modules::files::BIN_CHGRP,
16075 Some("hRs"),
16076 None,
16077 ), // c:816
16078 BUILTIN(
16079 "zf_chmod",
16080 0,
16081 Some(crate::ported::modules::files::bin_chmod as HandlerFunc),
16082 2,
16083 -1,
16084 0,
16085 Some("Rs"),
16086 None,
16087 ), // c:817
16088 BUILTIN(
16089 "zf_chown",
16090 0,
16091 Some(crate::ported::modules::files::bin_chown as HandlerFunc),
16092 2,
16093 -1,
16094 crate::ported::modules::files::BIN_CHOWN,
16095 Some("hRs"),
16096 None,
16097 ), // c:818
16098 BUILTIN(
16099 "zf_ln",
16100 0,
16101 Some(crate::ported::modules::files::bin_ln as HandlerFunc),
16102 1,
16103 -1,
16104 crate::ported::modules::files::BIN_LN,
16105 Some("dfins"),
16106 None,
16107 ), // c:819
16108 BUILTIN(
16109 "zf_mkdir",
16110 0,
16111 Some(crate::ported::modules::files::bin_mkdir as HandlerFunc),
16112 1,
16113 -1,
16114 0,
16115 Some("pm:"),
16116 None,
16117 ), // c:820
16118 BUILTIN(
16119 "zf_mv",
16120 0,
16121 Some(crate::ported::modules::files::bin_ln as HandlerFunc),
16122 2,
16123 -1,
16124 crate::ported::modules::files::BIN_MV,
16125 Some("fi"),
16126 None,
16127 ), // c:821
16128 BUILTIN(
16129 "zf_rm",
16130 0,
16131 Some(crate::ported::modules::files::bin_rm as HandlerFunc),
16132 1,
16133 -1,
16134 0,
16135 Some("dfiRrs"),
16136 None,
16137 ), // c:822
16138 BUILTIN(
16139 "zf_rmdir",
16140 0,
16141 Some(crate::ported::modules::files::bin_rmdir as HandlerFunc),
16142 1,
16143 -1,
16144 0,
16145 None,
16146 None,
16147 ), // c:823
16148 BUILTIN(
16149 "zf_sync",
16150 0,
16151 Some(crate::ported::modules::files::bin_sync as HandlerFunc),
16152 0,
16153 0,
16154 0,
16155 None,
16156 None,
16157 ), // c:824
16158 ]
16159});
16160// hash table containing builtin commands // c:143
16161/// Process-wide builtin lookup table. Filled lazily the first time
16162/// `builtintab()` is called; mirrors the C `mod_export HashTable
16163/// builtintab` exposed at `Src/builtin.c:146`.
16164static builtintab: OnceLock<HashMap<String, &'static builtin>> = OnceLock::new();
16165
16166/// Names whose `node.flags & DISABLED` is set in C. The Rust port's
16167/// `builtintab` is an immutable static, so the disabled bit lives
16168/// in this parallel set; `bin_enable` toggles it via builtin.c:587.
16169/// Dispatch sites check `is_builtin_disabled(name)` before calling
16170/// `handlerfunc` to mirror C's "skip nodes with DISABLED set" walk.
16171pub static BUILTINS_DISABLED: std::sync::LazyLock<
16172 // c:587 (Src/builtin.c)
16173 Mutex<std::collections::HashSet<String>>,
16174> = std::sync::LazyLock::new(|| Mutex::new(std::collections::HashSet::new()));
16175
16176// `shfunctab` is the canonical singleton at `hashtable::shfunctab_lock()`
16177// (RwLock<shfunc_table>); the prior parallel `shfunctab_table()` /
16178// `SHFUNCTAB_INNER` (usize-pointer Mutex<HashMap>) was deleted so the
16179// `bin_functions` C-port and the bytecode function-def path both write
16180// through one table. C-faithful access via `addnode`/`getnode`/`getnode2`
16181// methods (Src/zsh.h:281+ HashTable GSU pointers).
16182
16183// `matchednodes` global from Src/builtin.c:4550.
16184/// `MATCHEDNODES` static.
16185pub static MATCHEDNODES: Mutex<Vec<String>> = Mutex::new(Vec::new());
16186
16187// `stopmsg` global from Src/jobs.c — non-zero when checkjobs() printed.
16188/// `STOPMSG` static.
16189pub static STOPMSG: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
16190// `sfcontext` global from Src/exec.c:239 — current shell-function
16191// dispatch context (SFC_NONE / SFC_BUILTIN / SFC_FUNC / SFC_SUBST...).
16192/// `SFCONTEXT` static.
16193pub static SFCONTEXT: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:exec.c:239
16194 // `maxjob` / `thisjob` globals from Src/jobs.c:62/63 — canonical
16195 // storage lives in jobs.rs (`OnceLock<Mutex<i32>>`). The previous
16196 // builtin.rs duplicate `AtomicI32` stores NEVER synced with the
16197 // jobs.rs Mutex<i32> values that the spawn/wait paths actually
16198 // update; `checkjobs` (line 5092) read stale 0s no matter how many
16199 // jobs were active. Callers route through jobs::MAXJOB / jobs::THISJOB
16200 // directly now.
16201 // `jobstats` mirror — flat per-slot stat bits (STAT_*). Real jobtab
16202 // lives in src/ported/jobs.rs's JobTable; this mirror is updated by
16203 // the spawn/wait paths that already touch STOPMSG. Empty → no jobs,
16204 // matching the post-init state of `jobtab[]`.
16205/// `JOBSTATS` static.
16206pub static JOBSTATS: Mutex<Vec<i32>> = Mutex::new(Vec::new());
16207
16208// File-static globals for [_]realexit/zexit — c:5945+, init.c, signals.c.
16209/// `SHELL_EXITING` static.
16210pub static SHELL_EXITING: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
16211/// `EXIT_PENDING` static.
16212pub static EXIT_PENDING: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
16213/// `EXIT_VAL` static.
16214pub static EXIT_VAL: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
16215/// Port of `mod_export volatile int exit_level;` from `Src/builtin.c:5796`.
16216///
16217/// Records the `locallevel` at the moment a deferred `exit` was issued
16218/// inside a function. The `exec.c:6141` gate
16219/// `if (exit_pending && exit_level >= locallevel+1 && !in_exit_trap)`
16220/// fires only when the unwind has reached a scope at or above the
16221/// deferral point — preventing premature exit while the deferred
16222/// status walks back through nested function frames. C uses
16223/// `volatile int` because the value is read from a signal-touching
16224/// context; Rust's AtomicI32 with Relaxed ordering matches the same
16225/// no-fence read shape.
16226pub static EXIT_LEVEL: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
16227// ====================================================================
16228// !!! WARNING: RUST-ONLY ATOMIC — NO DIRECT C COUNTERPART !!!
16229// ====================================================================
16230// C zsh forks for `(...)` subshells; the child runs to completion
16231// then exits via process::exit, and the parent (post-fork) continues.
16232// zshrs runs subshells in-process via fusevm_bridge::subshell_begin/
16233// subshell_end (no fork), so `exit N` inside a subshell would call
16234// realexit() → process::exit(N) and terminate the WHOLE shell —
16235// breaking `(exit 7); echo $?` (expected: `7\n`, observed: shell
16236// dies with code 7).
16237//
16238// This counter is bumped by subshell_begin / decremented by
16239// subshell_end. zexit() at c:5977 checks it before realexit and,
16240// when > 0, sets EXIT_VAL + EXIT_PENDING and returns — letting the
16241// subshell unwinder catch the deferred exit at its boundary
16242// (mirroring what the deferred-exit path at c:5871-5891 does for
16243// function-scope exit).
16244//
16245// In C zsh, equivalent state is `forklevel` AT EXACTLY the subshell
16246// depth that fork would create — but the C check `locallevel >
16247// forklevel` is FALSE at subshell-top precisely so the fork can
16248// exit the child via realexit. Without fork, we need this extra
16249// gate.
16250/// `SUBSHELL_DEPTH` static.
16251pub static SUBSHELL_DEPTH: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
16252/// `LASTVAL` static.
16253pub static LASTVAL: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
16254
16255// `tok` for the test builtin — Src/builtin.c:7000 ranges. The full enum
16256// lives in src/ported/lex.rs; we mirror the few values testlex() touches.
16257/// `TEST_TOK` static.
16258pub static TEST_TOK: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
16259const TEST_LEXERR: i32 = -1; // c:7209
16260const TEST_NULLTOK: i32 = 0;
16261const TEST_DBAR: i32 = 2; // c:7213
16262const TEST_DAMPER: i32 = 3; // c:7215
16263const TEST_BANG: i32 = 4; // c:7217
16264const TEST_INPAR: i32 = 5; // c:7219
16265const TEST_OUTPAR: i32 = 6; // c:7221
16266const TEST_INANG: i32 = 7; // c:7223
16267const TEST_OUTANG: i32 = 8; // c:7225
16268const TEST_STRING: i32 = 9; // c:7227
16269
16270// `testargs` / `curtestarg` / `tokstr` globals from Src/builtin.c — the
16271// argv-style cursor that bin_test seeds and testlex() advances.
16272/// `TESTARGS` static.
16273pub static TESTARGS: Mutex<Vec<String>> = Mutex::new(Vec::new());
16274/// `TESTARGS_IDX` static.
16275pub static TESTARGS_IDX: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
16276/// `TOKSTR` static.
16277pub static TOKSTR: Mutex<String> = Mutex::new(String::new());
16278
16279// int doprintdir = 0; set in exec.c (for autocd, cdpath, etc.) // c:722
16280// `doprintdir` from Src/exec.c — set when an autocd'd command should
16281// echo the new directory before executing.
16282/// `DOPRINTDIR` static.
16283pub static DOPRINTDIR: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
16284// set if we are resolving links to their true paths // c:829
16285// `chasinglinks` from Src/exec.c — non-zero when CHASELINKS / -P
16286// resolution is active.
16287/// `CHASINGLINKS` static.
16288pub static CHASINGLINKS: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
16289
16290// `pparams` global from Src/init.c — positional parameters $1..$N.
16291/// `PPARAMS` static.
16292pub static PPARAMS: Mutex<Vec<String>> = Mutex::new(Vec::new());
16293
16294// `zoptind` (Src/builtin.c:5667) and `optcind` (c:5670) — the two
16295// pieces of getopts state. zoptind backs the user-visible $OPTIND.
16296/// `ZOPTIND` static.
16297pub static ZOPTIND: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(1);
16298/// `OPTCIND` static.
16299pub static OPTCIND: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
16300
16301// `ttyfrozen` global lives canonically in jobs.rs (`OnceLock<Mutex<i32>>`
16302// at jobs.rs:2625). The previous AtomicI32 duplicate here NEVER
16303// synced with the jobs.rs store — same desync hazard as the prior
16304// MAXJOB / THISJOB fix. Callers route through jobs::TTYFROZEN.
16305
16306/// Port of `mod_export int ineval` from `Src/builtin.c:6389`. Set
16307/// while `eval` is dispatching its body (incremented before
16308/// `execode(prog, 1, 0, "eval")`, decremented after). Tested by
16309/// `IN_EVAL_TRAP()` in zsh.h:2962 to determine trap-context state.
16310pub static INEVAL: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:6389
16311
16312// `loops` / `breaks` / `contflag` / `retflag` / `locallevel` / `sourcelevel`
16313// globals from Src/loop.c + Src/init.c — control-flow state consulted by
16314// the bin_break dispatcher.
16315/// `LOOPS` static.
16316pub static LOOPS: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
16317/// `BREAKS` static.
16318pub static BREAKS: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
16319/// `CONTFLAG` static.
16320pub static CONTFLAG: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
16321/// `RETFLAG` static.
16322pub static RETFLAG: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
16323// Same single-storage rationale as locallevel_param above — C zsh has
16324// only ONE `int sourcelevel;` global (Src/init.c:60). The canonical
16325// Rust port is `sourcelevel_init` (lowercase,
16326// matches C name). Re-export that single storage so the bin_break
16327// reader and the bin_dot bumps address the same atomic; without
16328// this, `bin_dot` could increment one global while `bin_break`
16329// inspected the other and `return` inside a sourced file would
16330// fall through to `zexit` (Src/builtin.c:5858).
16331// `locallevel_param` was previously a SEPARATE AtomicI32 here, but C
16332// zsh has only ONE `int locallevel;` global (Src/params.c:54).
16333// The canonical Rust port is `locallevel_param`
16334// (lowercase, matches C name). Re-export that single storage so
16335// every reader and writer addresses the same atomic — without
16336// this, `locallevel_param.store(0)` in zle/computil.rs would zero one
16337// global while `params::locallevel.fetch_add(1)` in vm_helper
16338// incremented a DIFFERENT global, leaving the two views out of
16339// sync indefinitely.
16340// `ZEXIT_NORMAL` re-exported from canonical zsh_h.rs (port of the
16341// `enum { ZEXIT_NORMAL, ZEXIT_SIGNAL, ZEXIT_DEFERRED }` in Src/zsh.h).
16342// Same single-source-of-truth pattern as TERM_UNKNOWN / HISTFLAG_*
16343// / etc — duplicate const declarations are a drift hazard.
16344
16345// Local builders that construct C-shape `builtin` rows for the
16346// static registration table below. They mirror the
16347// `BUILTIN(...)` / `BIN_PREFIX(...)` macros in `Src/zsh.h:1450-1452`,
16348// taking `u32` flag bitsets (BINF_*) and a `&str` handler-name
16349// column used only for documentation/wiring lookup — handler
16350// function pointers themselves are wired up later in
16351// `Executor::register_builtins` (`src/ported/vm_helper`).
16352//
16353// The `handler` arg was previously a `_handler_name: &'static str` that
16354// was discarded — `handlerfunc` always ended up `NULLBINCMD`, so
16355// `execbuiltin`'s c:506 `(*handlerfunc)(...)` dispatch was unreachable.
16356// Now the descriptor carries the actual port-side `HandlerFunc` so
16357// `execbuiltin` can parse flags and call through to the real builtin.
16358/// `BUILTIN` — see implementation.
16359#[allow(non_snake_case)]
16360pub fn BUILTIN(
16361 name: &str,
16362 flags: u32,
16363 handler: Option<HandlerFunc>,
16364 min: i32,
16365 max: i32,
16366 funcid: i32,
16367 optstr: Option<&str>,
16368 defopts: Option<&str>,
16369) -> builtin {
16370 builtin {
16371 node: hashnode {
16372 next: None,
16373 nam: name.to_string(),
16374 flags: flags as i32,
16375 },
16376 handlerfunc: handler,
16377 minargs: min,
16378 maxargs: max,
16379 funcid,
16380 optstr: optstr.map(|s| s.to_string()),
16381 defopts: defopts.map(|s| s.to_string()),
16382 }
16383}
16384
16385// `traps` mirror — sig name → body. Real `sigtrapped[]`/`siglists[]`
16386// arrays live in src/ported/signals.rs; this Mutex is the static-link
16387// shim that bin_trap reads/writes.
16388static TRAPS_INNER: OnceLock<Mutex<HashMap<String, String>>> = OnceLock::new();
16389
16390#[allow(non_snake_case)]
16391fn BIN_PREFIX(name: &str, flags: u32) -> builtin {
16392 BUILTIN(name, flags | BINF_PREFIX, None, 0, 0, 0, None, None)
16393}
16394
16395/// Inline printf-style format helper used by bin_print's -f/printf mode.
16396/// Replaces `%s` / `%d` / `%i` / `%c` / `%%` with positional args.
16397/// Full C printf-spec engine (Src/builtin.c:4691-5500) is much more
16398/// elaborate (width/precision/flag chars/%b/%q/etc.); this is the
16399/// minimal subset that covers the common script patterns.
16400///
16401/// Returns `Ok(output)` on success, or `Err((output_so_far, bad_char))`
16402/// when an unknown `%X` directive is hit. The C source emits
16403/// `zwarnnam(name, "%s: invalid directive", start)` at builtin.c:5435
16404/// then `return 1` — partial output already written stays written. The
16405/// Rust caller (bin_print, c:4854+) mirrors that: print the partial
16406/// output, emit `zwarnnam`, return 1.
16407/// Returns the formatted output plus the byte offset at which each
16408/// format-reuse CYCLE began (c:Src/builtin.c `splits`). `printf -v` to
16409/// an array assigns one element per cycle, so the caller slices `out`
16410/// at these boundaries.
16411fn printf_format(
16412 fmt: &str,
16413 args: &[String],
16414) -> Result<(String, Vec<usize>, Vec<(String, usize)>), (String, String)> {
16415 // c:Src/builtin.c:4711 — `fmt = getkeystring(fmt, &flen, ...,
16416 // GETKEYS_PRINTF_FMT, ...);`. The format string is first run
16417 // through getkeystring to interpret backslash escapes (`\n`,
16418 // `\t`, `\xNN`, etc.) before %-format substitution. The shim
16419 // `getkeystring(s)` defaults flags to 0, which (a) drops the
16420 // backslash on `\'`/`\"` (wrong for printf — zsh keeps unknown
16421 // `\<c>` literal because PRINTF_FMT excludes GETKEY_EMACS) and
16422 // (b) treats `\c` as a control-char escape rather than the
16423 // truncation marker. Route through `_with(GETKEYS_PRINTF_FMT)`
16424 // to match C exactly. `_with` takes a u32 `how` mask; the
16425 // canonical i32 const lives in `zsh_h` (Src/zsh.h:3180-3181).
16426 let (fmt, _) = getkeystring_with(fmt, crate::ported::zsh_h::GETKEYS_PRINTF_FMT as u32, None); // c:builtin.c:4711
16427 // c:Src/builtin.c:4696/5382/5527 — a `\c` in the FORMAT (or, below, in
16428 // a `%b` arg) sets `fmttrunc`, which (a) truncates output here and
16429 // (b) stops the format-reuse loop entirely (no reapplication over the
16430 // remaining args). getkeystring_with already cut `fmt` at the `\c`;
16431 // take the TLS flag it set so the reuse loop knows to stop.
16432 let mut fmttrunc = crate::ported::utils::getkey_truncated_take();
16433 let mut out = String::new();
16434 let mut arg_i: usize = 0;
16435 // c:Src/builtin.c:5166/5176 — `first` is the base of the current
16436 // format-reuse cycle's argument window. `%n$` positional specs index
16437 // `first + n - 1`; at the start of each new cycle `first += maxarg`
16438 // (the highest positional used in the prior cycle). `first_off` is
16439 // the Rust analog (absolute offset into `args`).
16440 let mut first_off: usize = 0;
16441 // c:Src/builtin.c — `splits`: byte offset where each format-reuse
16442 // cycle starts (the first is 0). Used by `printf -v ARRAY`.
16443 let mut bounds: Vec<usize> = Vec::new();
16444 // c:Src/builtin.c:5427-5429 — `case 'n': if (curarg) setiparam(curarg,
16445 // count - rcount);`. `%n` consumes an argument (the variable NAME) and
16446 // stores the number of bytes emitted so far into it, printing nothing.
16447 // printf_format is a pure formatter with no shell handle, so it records
16448 // (name, byte_count_at_this_point) and the caller does the setiparam —
16449 // out.len() here is exactly C's `count - rcount`.
16450 let mut n_targets: Vec<(String, usize)> = Vec::new();
16451 // c:Src/builtin.c:4914-4923 — printf reapplies the format string
16452 // until ALL args are consumed. `printf '%s,' a b c` → `a,b,c,`,
16453 // not `a,`. The outer loop reapplies; the inner do-while body
16454 // mirrors C's per-arg conversion loop directly.
16455 loop {
16456 bounds.push(out.len());
16457 // c:Src/builtin.c:5168 `rcount = count;` reset the `%n` byte count
16458 // at the start of each format-reuse cycle, so `%n` reports bytes
16459 // emitted SINCE this cycle began, not cumulatively. `printf 'x%n' a b`
16460 // stores 1 in both a and b, not 1 and 2.
16461 let cycle_start = out.len();
16462 let prev = arg_i;
16463 // c:Src/builtin.c:5175-5178 — `maxarg` is the highest positional
16464 // (`%n$`) referenced in THIS cycle; reset per cycle and folded
16465 // into `first_off` at cycle end.
16466 let mut cycle_maxarg: usize = 0;
16467 let mut iter = fmt.chars().peekable();
16468 while let Some(c) = iter.next() {
16469 if c != '%' {
16470 out.push(c);
16471 continue;
16472 }
16473 // c:Src/builtin.c:4791+ — parse width/precision/flag chars
16474 // between `%` and the conversion. Capture them so `printf
16475 // "%-10s" hi` and `printf "%.3f" 3.14159` render correctly.
16476 let mut spec = String::from("%");
16477 // c:Src/builtin.c:5199-5219 — `%n$` positional argument
16478 // specifier. A leading 1-9 digit run followed by `$` selects
16479 // the n-th argument (1-based) for this conversion instead of
16480 // the next sequential one. A leading digit run NOT followed
16481 // by `$` is an ordinary field width (handled below), so peek
16482 // the whole run before deciding. (`0` can't start a
16483 // positional — it's a flag — so C tests `'1'..'9'`.)
16484 let mut positional: Option<usize> = None;
16485 if matches!(iter.peek(), Some(&d) if ('1'..='9').contains(&d)) {
16486 let mut digits = String::new();
16487 while let Some(&d) = iter.peek() {
16488 if d.is_ascii_digit() {
16489 digits.push(d);
16490 iter.next();
16491 } else {
16492 break;
16493 }
16494 }
16495 if iter.peek() == Some(&'$') {
16496 iter.next(); // c:5202 — consume `$`
16497 let narg: usize = digits.parse().unwrap_or(0);
16498 // c:5203-5212 — out-of-range positional is a hard
16499 // error (`zwarnnam(...); return 1`). `argc` in C is
16500 // the per-cycle remaining count (total minus the
16501 // already-consumed `first_off`).
16502 if narg == 0 || first_off + narg > args.len() {
16503 return Err((
16504 out,
16505 format!("{}: argument specifier out of range", narg), // c:5204
16506 ));
16507 }
16508 // c:5214-5216 — `if (narg > maxarg) maxarg = narg;`
16509 // `curarg = *(first + narg - 1);`
16510 if narg > cycle_maxarg {
16511 cycle_maxarg = narg;
16512 }
16513 positional = Some(first_off + narg - 1);
16514 } else {
16515 // Not a positional: the digits are an ordinary field
16516 // width — emit them into the spec and fall through.
16517 spec.push_str(&digits);
16518 }
16519 }
16520 // A precision `*` with NO matching arg emits nothing into `spec`
16521 // (default precision), so the `%`-conversion invalid-directive
16522 // check below can't see it. Track it separately: `printf '%.*%'`
16523 // (no arg) is still an invalid directive in zsh (the format
16524 // carried a precision), exit 1. Src/builtin.c reaches the switch
16525 // with a non-`%%` `d` buffer regardless of the runtime arg.
16526 let mut saw_prec_star = false;
16527 loop {
16528 match iter.peek() {
16529 // c:Src/builtin.c:4791+ — printf flag chars. The `'`
16530 // (thousands-grouping) flag is passed straight to libc
16531 // by C zsh; the Rust port groups the digit run itself
16532 // in format_spec_int/uint per localeconv. Accept it here
16533 // so it isn't mistaken for the conversion char (which
16534 // produced "%': invalid directive").
16535 Some(&c) if matches!(c, '-' | '+' | ' ' | '#' | '0' | '\'') => {
16536 spec.push(c);
16537 iter.next();
16538 }
16539 _ => break,
16540 }
16541 }
16542 // c:Src/builtin.c:4791-4796 — width can be either a
16543 // digit literal or `*` (consume next arg as width).
16544 // Without `*` handling, `printf '%*d' 4 7` rendered the
16545 // literal `%*d` because the iter saw `*` and aborted the
16546 // spec walk before reaching the conversion char.
16547 if iter.peek() == Some(&'*') {
16548 iter.next(); // c:4796 — consume the `*` marker
16549 // c:Src/builtin.c:5240-5247 — the `*` width arg is
16550 // MATH-EVALUATED (`width = (int)mathevali(metafy(*argp,
16551 // …))`), not a plain integer parse: `0x1f`→31, ` 4`→4,
16552 // `'A`→65, `2+3`→5. parse_int_arg is the shared
16553 // getnum→mathevali path (same one the %d/%i arm uses),
16554 // and on a math error it zeroes the value and flags
16555 // ret=1 (c:5243 `errflag → ret = 1`). Previously the
16556 // plain `str::parse` silently yielded 0 for any non-
16557 // decimal width arg.
16558 let w: i64 = args.get(arg_i).map(|s| parse_int_arg(s)).unwrap_or(0);
16559 arg_i += 1;
16560 spec.push_str(&w.to_string());
16561 } else {
16562 while let Some(&c) = iter.peek() {
16563 if c.is_ascii_digit() {
16564 spec.push(c);
16565 iter.next();
16566 } else {
16567 break;
16568 }
16569 }
16570 }
16571 if iter.peek() == Some(&'.') {
16572 iter.next(); // consume the `.`
16573 // `.` precision: also accepts `*` per c:4796 same as width.
16574 if iter.peek() == Some(&'*') {
16575 iter.next();
16576 saw_prec_star = true;
16577 // c:Src/builtin.c:5275-5288 — the `*` precision arg is
16578 // math-evaluated identically to the `*` width arg
16579 // (`prec = (int)mathevali(metafy(*argp, …))`, error →
16580 // ret=1) — BUT only when the arg actually EXISTS
16581 // (c:5275 `if (*argp)`). If args have run out, `prec`
16582 // stays at its -1 init (c:5178) and NO precision is
16583 // emitted (c:5288 `if (prec >= 0) *d++ = '.', *d++ =
16584 // '*'`). A negative result is unset the same way.
16585 // Unlike width (init 0), a MISSING `*` precision must
16586 // leave the conversion at default precision, not force
16587 // `.0`: `printf '%.*d' ` prints `0` (default), not the
16588 // truncated-to-empty `%.0d`. Previously `unwrap_or(0)`
16589 // forced `.0` and dropped the digit.
16590 if let Some(a) = args.get(arg_i) {
16591 let p = parse_int_arg(a);
16592 arg_i += 1;
16593 if p >= 0 {
16594 spec.push('.');
16595 spec.push_str(&p.to_string());
16596 }
16597 }
16598 } else {
16599 // Literal precision: `.` then optional digits (an empty
16600 // digit run is precision 0, c:5283-5287 `else prec = 0`).
16601 spec.push('.');
16602 while let Some(&c) = iter.peek() {
16603 if c.is_ascii_digit() {
16604 spec.push(c);
16605 iter.next();
16606 } else {
16607 break;
16608 }
16609 }
16610 }
16611 }
16612 // c:Src/builtin.c:5290 — `/* ignore any size modifier */
16613 // if (*c == 'l' || *c == 'L' || *c == 'h') c++;`. zsh accepts
16614 // and discards a single length modifier (`%ld`, `%lu`, `%hd`)
16615 // — the conversion char that follows determines the type and
16616 // C rebuilds the libc format as `%l<conv>` itself. NOT pushed
16617 // into `spec`: it would corrupt the format passed to the
16618 // format_spec_* helpers. Only one modifier char is skipped,
16619 // matching C (so `%lld` then errors on the second `l`).
16620 if matches!(iter.peek(), Some('l') | Some('L') | Some('h')) {
16621 iter.next();
16622 }
16623 // c:Src/builtin.c:5215/5310 — a `%n$` positional spec sets
16624 // `curarg` directly; the conversion arms below read from
16625 // `arg_i`, so point it at the positional index for the
16626 // duration of the conversion, then restore. Restoring means
16627 // the positional reference does NOT advance the sequential
16628 // cursor (C only advances `argp` for non-positional args).
16629 let saved_argi = arg_i;
16630 let is_positional = positional.is_some();
16631 if let Some(p) = positional {
16632 arg_i = p;
16633 }
16634 match iter.next() {
16635 // c:Src/builtin.c:5414-5419 — a bare `%%` prints `%`, but a
16636 // `%` directive carrying any flag / width / precision / `*` is
16637 // an "invalid directive": zsh handles doubled `%%` in the
16638 // literal scan and only reaches the conversion switch (which
16639 // has no `%` case → default) once modifiers intervened. Here a
16640 // plain `%%` arrives with `spec == "%"`; anything else means
16641 // modifiers were consumed.
16642 Some('%') if spec != "%" || saw_prec_star => {
16643 let disp = if saw_prec_star && spec == "%" { "%.*" } else { spec.as_str() };
16644 return Err((out, format!("{disp}%: invalid directive")));
16645 }
16646 Some('%') => out.push('%'),
16647 Some('n') => {
16648 // c:5427-5428 — `if (curarg) setiparam(curarg, count -
16649 // rcount);`. The arg is the target variable's NAME; nothing
16650 // is emitted. `if (curarg)` tests that an arg SLOT exists,
16651 // not that it is non-empty — an empty or malformed name is
16652 // still handed to setiparam, which errors `not an
16653 // identifier`. Only a wholly ABSENT arg is the no-op.
16654 // `count - rcount` is bytes since the cycle start
16655 // (c:5168), i.e. out.len() - cycle_start here.
16656 if let Some(name) = args.get(arg_i) {
16657 n_targets.push((name.clone(), out.len() - cycle_start)); // c:5428
16658 }
16659 arg_i += 1;
16660 }
16661 Some('s') => {
16662 let a = args.get(arg_i).cloned().unwrap_or_default();
16663 spec.push('s');
16664 out.push_str(&format_spec_str(&spec, &a, false));
16665 arg_i += 1;
16666 }
16667 Some('d') | Some('i') => {
16668 let a = args.get(arg_i).cloned().unwrap_or_default();
16669 let n = parse_int_arg(&a);
16670 spec.push('d');
16671 out.push_str(&format_spec_int(&spec, n));
16672 arg_i += 1;
16673 }
16674 Some('u') => {
16675 let a = args.get(arg_i).cloned().unwrap_or_default();
16676 // c:Src/builtin.c:5511 — `if (!zstrtoul_underscore(
16677 // curarg, &zulongval)) zulongval = mathevali(...)`.
16678 // Unsigned conversions first try a direct unsigned
16679 // parse (so a full-range literal like
16680 // 18446744073709551615 is NOT truncated by the
16681 // signed-math path), then fall back to math eval
16682 // (which wraps a negative result into u64).
16683 let n = crate::ported::utils::zstrtoul_underscore(a.trim())
16684 .unwrap_or_else(|| parse_int_arg(&a) as u64);
16685 spec.push('u');
16686 out.push_str(&format_spec_uint(&spec, n));
16687 arg_i += 1;
16688 }
16689 Some('x') => {
16690 let a = args.get(arg_i).cloned().unwrap_or_default();
16691 // c:Src/builtin.c:5511 — `if (!zstrtoul_underscore(
16692 // curarg, &zulongval)) zulongval = mathevali(...)`.
16693 // Unsigned conversions first try a direct unsigned
16694 // parse (so a full-range literal like
16695 // 18446744073709551615 is NOT truncated by the
16696 // signed-math path), then fall back to math eval
16697 // (which wraps a negative result into u64).
16698 let n = crate::ported::utils::zstrtoul_underscore(a.trim())
16699 .unwrap_or_else(|| parse_int_arg(&a) as u64);
16700 spec.push('x');
16701 out.push_str(&format_spec_radix(&spec, n, 'x'));
16702 arg_i += 1;
16703 }
16704 Some('X') => {
16705 let a = args.get(arg_i).cloned().unwrap_or_default();
16706 // c:Src/builtin.c:5511 — `if (!zstrtoul_underscore(
16707 // curarg, &zulongval)) zulongval = mathevali(...)`.
16708 // Unsigned conversions first try a direct unsigned
16709 // parse (so a full-range literal like
16710 // 18446744073709551615 is NOT truncated by the
16711 // signed-math path), then fall back to math eval
16712 // (which wraps a negative result into u64).
16713 let n = crate::ported::utils::zstrtoul_underscore(a.trim())
16714 .unwrap_or_else(|| parse_int_arg(&a) as u64);
16715 spec.push('X');
16716 out.push_str(&format_spec_radix(&spec, n, 'X'));
16717 arg_i += 1;
16718 }
16719 Some('o') => {
16720 let a = args.get(arg_i).cloned().unwrap_or_default();
16721 // c:Src/builtin.c:5511 — `if (!zstrtoul_underscore(
16722 // curarg, &zulongval)) zulongval = mathevali(...)`.
16723 // Unsigned conversions first try a direct unsigned
16724 // parse (so a full-range literal like
16725 // 18446744073709551615 is NOT truncated by the
16726 // signed-math path), then fall back to math eval
16727 // (which wraps a negative result into u64).
16728 let n = crate::ported::utils::zstrtoul_underscore(a.trim())
16729 .unwrap_or_else(|| parse_int_arg(&a) as u64);
16730 spec.push('o');
16731 out.push_str(&format_spec_radix(&spec, n, 'o'));
16732 arg_i += 1;
16733 }
16734 Some(conv @ ('f' | 'F' | 'g' | 'G' | 'e' | 'E')) => {
16735 let a = args.get(arg_i).cloned().unwrap_or_default();
16736 // c:Src/builtin.c:5479-5488 — `doubleval = strtod(curarg,
16737 // &eptr); if (*eptr) mnumval = matheval(curarg)`. The arg
16738 // is parsed as a numeric constant first (handles "2.5",
16739 // "nan", "inf"); if that leaves leftover (a variable name
16740 // like `f`, or an expression like "1.5+1"), it is
16741 // evaluated as a math expression. The previous port did
16742 // only the constant parse, so `printf %g f` / `%f 1.5+1`
16743 // yielded 0.
16744 let n: f64 =
16745 if let Some(rest) = a.strip_prefix('\'').or_else(|| a.strip_prefix('"')) {
16746 // c:Src/builtin.c:5431-5447 — a leading `'`/`"` makes
16747 // the value the numeric char code of the next char,
16748 // for FLOAT conversions too (shared with int):
16749 // `printf '%.1E' \'B` → 66 → 6.6E+01. The int arm
16750 // (parse_int_arg) already did this; the float arm
16751 // skipped it, yielding 0.
16752 rest.chars().next().map(|c| c as i64 as f64).unwrap_or(0.0)
16753 } else {
16754 a.parse::<f64>().unwrap_or_else(|_| match matheval(&a) {
16755 Ok(m) if m.type_ == crate::ported::math::MN_FLOAT => m.d,
16756 Ok(m) => m.l as f64,
16757 // c:Src/builtin.c:5488 — a math-eval failure on
16758 // a FLOAT operand is a SOFT error, same as the
16759 // integer path: emit the diagnostic, clear the
16760 // errflag so later args still evaluate, and flag
16761 // ret=1. The float arm previously swallowed it,
16762 // so `printf '%g' '%d'` exited 0 where zsh exits
16763 // 1. (Empty/missing args math-eval to Ok(0), so
16764 // they do NOT trip this.)
16765 Err(msg) => {
16766 crate::ported::utils::zerr(&msg);
16767 crate::ported::utils::errflag.fetch_and(
16768 !crate::ported::utils::ERRFLAG_ERROR,
16769 std::sync::atomic::Ordering::Relaxed,
16770 );
16771 PRINTF_MATH_ERR.with(|c| c.set(true));
16772 0.0
16773 }
16774 })
16775 };
16776 // c:Src/builtin.c printf %g/%G uses libc snprintf
16777 // which strips trailing zeros; %e/%E uses scientific.
16778 out.push_str(&format_spec_float_conv(&spec, n, conv));
16779 arg_i += 1;
16780 }
16781 Some('c') => {
16782 // c:5300-5306 — `%c` outputs the arg's first char with
16783 // WIDTH/justify applied (`%3c`→" x", `%-3c`→"x ") but
16784 // precision IGNORED (`%.0c`→"x"). Strip precision from the
16785 // spec, then format as a one-char string. The previous port
16786 // pushed the char raw, dropping width.
16787 let a = args.get(arg_i).cloned().unwrap_or_default();
16788 // c:5300-5305 — `intval = curarg ? *curarg : 0`. An
16789 // empty or missing arg yields `*curarg == '\0'`, so
16790 // `%c` emits a NUL byte (not nothing). zsh:
16791 // `printf "%c" "" | od` → 00.
16792 let ch_str = a.chars().next().unwrap_or('\0').to_string();
16793 let mut cspec = spec.split('.').next().unwrap_or(spec.as_str()).to_string();
16794 cspec.push('s');
16795 out.push_str(&format_spec_str(&cspec, &ch_str, true));
16796 arg_i += 1;
16797 }
16798 // c:builtin.c:5403-5409 %q — shell-quote the arg using
16799 // QT_BACKSLASH_SHOWNULL (backslash-escape form), NOT
16800 // QT_QUOTEDZPUTS (single-quote form).
16801 //
16802 // c: stringval = quotestring(metafy(curarg, …),
16803 // QT_BACKSLASH_SHOWNULL);
16804 //
16805 // Symptom of the previous quotedzputs choice:
16806 // printf "%q\n" "a b c"
16807 // zshrs (before): 'a b c' zsh: a\ b\ c
16808 //
16809 // p10k uses `printf "%q "` for shell-quoting cached
16810 // command lines; the difference makes those caches
16811 // unreadable in zsh-syntax debuggers expecting the
16812 // backslash form.
16813 Some('q') => {
16814 // c:Src/builtin.c:5387-5391 — `stringval = curarg ?
16815 // quotestring(...) : &nullstr`. A MISSING argument (curarg
16816 // == NULL) is the empty nullstr and is NOT quoted, so
16817 // `printf '%q'` prints nothing — not `''`. A PRESENT arg,
16818 // even an empty string, IS quoted (→ `''`).
16819 let quoted = match args.get(arg_i) {
16820 Some(a) => crate::ported::utils::quotestring(
16821 a,
16822 crate::ported::zsh_h::QT_BACKSLASH_SHOWNULL,
16823 ),
16824 None => String::new(),
16825 };
16826 // c:Src/builtin.c:5405-5407 — `%q` sets `*d = 's'`
16827 // and runs the quoted value through the normal string
16828 // output (`print_val`), so width/precision/flags from
16829 // the spec apply (`printf "%-5q" a` → "a ").
16830 spec.push('s');
16831 out.push_str(&format_spec_str(&spec, "ed, true));
16832 arg_i += 1;
16833 }
16834 // c:builtin.c:5332-5336 — `%b` uses GETKEYS_PRINTF_ARG
16835 // (Src/zsh.h:3183 = GETKEY_BACKSLASH_C alone), NOT
16836 // GETKEYS_PRINT. The distinction:
16837 // GETKEYS_PRINT = OCTAL_ESC | EMACS | BACKSLASH_C
16838 // `\NNN` → octal, `\<c>` → drop backslash.
16839 // GETKEYS_PRINTF_ARG = BACKSLASH_C
16840 // `\NNN` → literal, `\0NNN` → octal, `\<c>` → keep both.
16841 // Bourne-style `printf '%b' '\141'` keeps `\141` literal
16842 // (only `\0NNN` is octal in the arg interpretation),
16843 // matching zsh. Previous Rust port used GETKEYS_PRINT,
16844 // so `\141` was octal-eval'd to `a` — diverged.
16845 Some('b') => {
16846 let a = args.get(arg_i).cloned().unwrap_or_default();
16847 let (s, _) =
16848 getkeystring_with(&a, crate::ported::zsh_h::GETKEYS_PRINTF_ARG as u32, None);
16849 // c:5380-5383 — a `\c` inside the `%b` arg truncates:
16850 // emit the expansion up to `\c`, then stop the whole
16851 // printf (no rest-of-format, no reuse). getkeystring_with
16852 // already cut `s` at the `\c` and set the TLS flag.
16853 let arg_truncated = crate::ported::utils::getkey_truncated_take();
16854 // c:5307-5360 — `%b` shares the `%s` width+precision
16855 // handling (`%5b`→" ab", `%3.1b`→" a"), applied to the
16856 // escape-expanded string. The previous port pushed the
16857 // expansion raw, dropping width/precision.
16858 spec.push('s');
16859 out.push_str(&format_spec_str(&spec, &s, false));
16860 arg_i += 1;
16861 if arg_truncated {
16862 fmttrunc = true; // c:5382
16863 break; // c:5383 — truncate the format here (stop this cycle)
16864 }
16865 }
16866 // c:builtin.c:5420 — `%n` consumes its arg but writes
16867 // nothing. C printf writes the byte-count-so-far to
16868 // the int pointer; zsh has no pointer to write to, so
16869 // it silently drops the directive. Previous Rust port
16870 // fell to the unknown-arm and emitted literal `%n`,
16871 // breaking `printf "%n" x; echo y` (zsh emits `y`,
16872 // zshrs emitted `%ny`).
16873 Some('n') => {
16874 arg_i += 1;
16875 }
16876 // c:builtin.c:5430-5443 — unknown directive in C is
16877 // a hard error: `zwarnnam(name, "%s: invalid
16878 // directive", start); ...; return 1;`. Bubble the
16879 // bad char up via Err; the caller emits the warning
16880 // and returns rc=1. Partial output already written
16881 // is preserved (matches C — the warning fires AFTER
16882 // earlier output bytes have already been emitted).
16883 Some(other) => {
16884 // c:5430-5436 — the message echoes `start`: the spec
16885 // text from `%` through the bad conversion char
16886 // inclusive (C null-terminates at `c[1]`). `spec`
16887 // already holds `%`+flags+width+`.prec`; append the
16888 // bad char. e.g. `%0$s` → "%0$: invalid directive".
16889 return Err((out, format!("{}{}: invalid directive", spec, other)));
16890 // c:5435
16891 }
16892 // c:Src/builtin.c:5430-5436 — a bare `%` with nothing
16893 // after it (end of format) is an invalid directive, same
16894 // as an unknown conversion char: emit any preceding output
16895 // then error (rc 1). The previous port pushed a literal
16896 // `%`, so `printf '%'` printed `%` with rc 0.
16897 None => return Err((out, format!("{}: invalid directive", spec))),
16898 }
16899 // c:Src/builtin.c — restore the sequential cursor after a
16900 // positional conversion (see the swap above).
16901 if is_positional {
16902 arg_i = saved_argi;
16903 }
16904 }
16905 // c:Src/builtin.c:5175-5178 — at the end of a cycle that used
16906 // positional specs, fold the highest positional into the base
16907 // offset (`first += maxarg`) and advance the sequential cursor
16908 // past it so the reapply check below sees forward progress.
16909 if cycle_maxarg > 0 {
16910 first_off += cycle_maxarg;
16911 if first_off > arg_i {
16912 arg_i = first_off;
16913 }
16914 }
16915 // c:Src/builtin.c:5527 — `} while (... && !fmttrunc ...)`. A `\c`
16916 // in the format or a `%b` arg stops the reuse loop.
16917 if fmttrunc || arg_i == prev || arg_i >= args.len() {
16918 break;
16919 }
16920 }
16921 Ok((out, bounds, n_targets))
16922}
16923
16924/// Apply a printf-style `%[-flag][width][.prec]s` spec to a string.
16925/// Mirrors C `printf "%-10s" str` formatting; the Rust `format!` macro
16926/// doesn't accept runtime-parsed specs so we hand-parse.
16927fn format_spec_str(spec: &str, s: &str, honor_zero: bool) -> String {
16928 // zsh's `%c` and `%q` route through `print_val`, which HONORS the `0` flag
16929 // (`printf %04c x` → `000x`, `%04q a` → `000a`), so those callers pass
16930 // honor_zero=true. `%s`/`%b` are handled inline with FORCED space padding
16931 // (Src/builtin.c:5375 `fprintf(fout, "%*c", …, ' ')` → `%04s ab` = " ab"),
16932 // so they pass honor_zero=false.
16933 let (left_align, zero_pad_flag, width, prec) = parse_flags_width_prec(spec);
16934 let truncated: &str = if let Some(p) = prec {
16935 let end: usize = s.chars().take(p).map(|c| c.len_utf8()).sum();
16936 &s[..end.min(s.len())]
16937 } else {
16938 s
16939 };
16940 let pad = width.saturating_sub(truncated.chars().count());
16941 if left_align {
16942 format!("{}{}", truncated, " ".repeat(pad))
16943 } else {
16944 let padch = if honor_zero && zero_pad_flag { '0' } else { ' ' };
16945 format!("{}{}", padch.to_string().repeat(pad), truncated)
16946 }
16947}
16948
16949/// c:Src/builtin.c — printf integer-argument parser. zsh accepts:
16950/// - Decimal (`42`, `-7`) — leading 0 stays decimal, NOT octal.
16951/// - Hex (`0x10`, `0X10`)
16952/// - Single character prefix `'A'` or `"A"` → ASCII code
16953/// - `BASE#NNN` radix literal
16954///
16955/// c:Src/builtin.c:5460-5464 — printf evaluates the `%d`/`%i` operand with
16956/// `mathevali`, which zerr's a "bad math expression" diagnostic AND sets
16957/// errflag on a parse error (`printf %d 12abc` → "operator expected at
16958/// `abc'"). C then clears ERRFLAG_ERROR and sets the builtin's `ret = 1`,
16959/// keeping the value at 0 but continuing to format the remaining args.
16960/// zshrs's `mathevali` returns the message in `Err` instead of emitting, so
16961/// surface it here (matching the `$((…))` path at subst.rs:2562), clear the
16962/// error so following args still evaluate, and record the soft failure in
16963/// `PRINTF_MATH_ERR` for bin_print to turn into exit status 1.
16964thread_local! {
16965 static PRINTF_MATH_ERR: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
16966}
16967
16968fn parse_int_arg(s: &str) -> i64 {
16969 // c:Src/builtin.c bin_print c:5447 — character-constant check
16970 // operates on the RAW arg, not after trim. `*curarg == '\''`
16971 // → take `curarg[1]` as a byte regardless of whitespace.
16972 // Otherwise `printf "%d" "' "` returns 0 instead of 32 because
16973 // the trailing space gets stripped before the leading-quote test.
16974 if let Some(rest) = s.strip_prefix('\'').or_else(|| s.strip_prefix('"')) {
16975 return rest.chars().next().map(|c| c as i64).unwrap_or(0);
16976 }
16977 // c:Src/builtin.c:5460 — `zlongval = mathevali(metafy(curarg, …))`.
16978 // printf evaluates the %d/%i operand as a math expression. matheval
16979 // natively handles decimal / 0x-hex / BASE#NNN / float-truncate and
16980 // overflow-truncation: the math lexer's zstrtol (Src/utils.c:2511)
16981 // emits the "number truncated after N digits" warning, so the
16982 // magnitude 9223372036854775808 truncates to 18 digits exactly as
16983 // zsh, and INT64_MIN (-9223372036854775808) round-trips without the
16984 // manual `-parsed` negate that panicked on i64::MIN.
16985 // c:5460-5464 — evaluate as math; on error emit the diagnostic (C's
16986 // mathevali does this internally), zero the value, and flag ret=1.
16987 match crate::ported::math::mathevali(s.trim()) {
16988 Ok(n) => n,
16989 Err(msg) => {
16990 crate::ported::utils::zerr(&msg);
16991 // c:5463 `errflag &= ~ERRFLAG_ERROR` — clear so the NEXT %d arg
16992 // still evaluates (matheval bails on a pre-set errflag,
16993 // math.rs:569); the soft failure is tracked separately.
16994 crate::ported::utils::errflag.fetch_and(
16995 !crate::ported::utils::ERRFLAG_ERROR,
16996 std::sync::atomic::Ordering::Relaxed,
16997 );
16998 PRINTF_MATH_ERR.with(|c| c.set(true)); // c:5464 ret = 1
16999 0
17000 }
17001 }
17002}
17003
17004fn format_spec_int(spec: &str, n: i64) -> String {
17005 let (left_align, zero_pad_flag, width, prec) = parse_flags_width_prec(spec);
17006 // c:libc printf %d — when precision is given, `0` flag is
17007 // ignored (precision controls the digit padding instead).
17008 let zero_pad = zero_pad_flag && !left_align && prec.is_none();
17009 // c:Src/builtin.c — `+` flag: prefix positive numbers with `+`.
17010 // ` ` flag: prefix positive numbers with a space (mutually
17011 // exclusive with `+` per POSIX; `+` wins when both set).
17012 let plus_flag = spec.contains('+');
17013 let space_flag = spec.contains(' ') && !plus_flag;
17014 let digits = n.unsigned_abs().to_string();
17015 // c:libc printf %d precision = minimum number of digits;
17016 // zero-pad the body up to that count BEFORE the sign + width. Converting
17017 // a zero value with precision 0 yields NO digits (`printf '%.0d' 0` → "").
17018 let digits = if let Some(p) = prec {
17019 if p == 0 && n == 0 {
17020 String::new()
17021 } else if digits.len() < p {
17022 format!("{}{}", "0".repeat(p - digits.len()), digits)
17023 } else {
17024 digits
17025 }
17026 } else {
17027 digits
17028 };
17029 // c:libc printf `'` (thousands-grouping) flag — C hands `'` to the
17030 // system printf; the Rust port groups the digit run itself per the
17031 // active locale's `thousands_sep` (localeconv, set from the env at
17032 // init.rs:1709). Empty separator (C/POSIX locale) → no grouping,
17033 // matching `printf "%'d" 1234567` = `1234567` under LC_ALL=C.
17034 // Grouping is suppressed when an explicit precision is given: glibc
17035 // does NOT group the precision zero-fill (`printf "%'.8d" 42` =
17036 // `00000042`, not `00,000,042`), so gate on `prec.is_none()`. This
17037 // matches glibc for every precision case where the value fits within
17038 // the precision digit count (the normal use of `%'.Nd`); the rare
17039 // degenerate combo of `'` + a precision SHORTER than a large value's
17040 // own digit count (`%'.8d` of 1234567 → glibc `1,234,567`) still
17041 // groups in glibc but is left ungrouped here — a documented edge.
17042 // Inlined rather than shared with format_spec_uint because the
17043 // port-purity gate forbids new fns under src/ported/.
17044 let digits = if spec.contains('\'') && prec.is_none() {
17045 let sep = unsafe {
17046 let lc = libc::localeconv();
17047 if lc.is_null() || (*lc).thousands_sep.is_null() {
17048 String::new()
17049 } else {
17050 std::ffi::CStr::from_ptr((*lc).thousands_sep)
17051 .to_string_lossy()
17052 .into_owned()
17053 }
17054 };
17055 if sep.is_empty() {
17056 digits
17057 } else {
17058 let chars: Vec<char> = digits.chars().collect();
17059 let len = chars.len();
17060 let mut grouped = String::with_capacity(len + len / 3 * sep.len());
17061 for (i, ch) in chars.iter().enumerate() {
17062 if i > 0 && (len - i) % 3 == 0 {
17063 grouped.push_str(&sep);
17064 }
17065 grouped.push(*ch);
17066 }
17067 grouped
17068 }
17069 } else {
17070 digits
17071 };
17072 let body = if n < 0 {
17073 format!("-{}", digits)
17074 } else if plus_flag {
17075 format!("+{}", digits)
17076 } else if space_flag {
17077 format!(" {}", digits)
17078 } else {
17079 digits
17080 };
17081 let pad = width.saturating_sub(body.chars().count());
17082 if pad == 0 {
17083 body
17084 } else if left_align {
17085 format!("{}{}", body, " ".repeat(pad))
17086 } else if zero_pad {
17087 // Zero-pad: sign/prefix char (`-`, `+`, ` `) stays at the
17088 // left, zeros pad between it and the digits.
17089 if let Some(rest) = body
17090 .strip_prefix('-')
17091 .or_else(|| body.strip_prefix('+'))
17092 .or_else(|| body.strip_prefix(' '))
17093 {
17094 let sign = body.chars().next().unwrap();
17095 format!("{}{}{}", sign, "0".repeat(pad), rest)
17096 } else {
17097 format!("{}{}", "0".repeat(pad), body)
17098 }
17099 } else {
17100 format!("{}{}", " ".repeat(pad), body)
17101 }
17102}
17103
17104/// printf %x / %X / %o with full flag support: `#` prefix, zero pad,
17105/// width, left-align. Matches libc printf semantics.
17106fn format_spec_radix(spec: &str, n: u64, conv: char) -> String {
17107 let (left_align, zero_pad_flag, width, prec) = parse_flags_width_prec(spec);
17108 // c:libc printf — when a precision is given the `0` flag is ignored;
17109 // precision controls the digit zero-fill instead.
17110 let zero_pad = zero_pad_flag && !left_align && prec.is_none();
17111 let hash_flag = spec.contains('#');
17112 let body = match conv {
17113 'x' => format!("{:x}", n),
17114 'X' => format!("{:X}", n),
17115 'o' => format!("{:o}", n),
17116 _ => n.to_string(),
17117 };
17118 // c:libc printf %o/%u/%x/%X — precision is the MINIMUM number of digits;
17119 // zero-pad the body up to it BEFORE the `#` prefix and width. Converting
17120 // a zero value with precision 0 yields NO digits.
17121 let body = if let Some(p) = prec {
17122 let len = body.chars().count();
17123 if p == 0 && n == 0 {
17124 String::new()
17125 } else if len < p {
17126 format!("{}{}", "0".repeat(p - len), body)
17127 } else {
17128 body
17129 }
17130 } else {
17131 body
17132 };
17133 // c:Src/builtin.c — `#` flag: prefix with `0x`/`0X` for hex (only
17134 // when value non-zero), `0` for octal (always, even zero, which
17135 // libc handles by emitting "0" anyway).
17136 let body = if hash_flag {
17137 match conv {
17138 'x' if n != 0 => format!("0x{}", body),
17139 'X' if n != 0 => format!("0X{}", body),
17140 'o' if !body.starts_with('0') => format!("0{}", body),
17141 _ => body,
17142 }
17143 } else {
17144 body
17145 };
17146 let pad = width.saturating_sub(body.chars().count());
17147 if pad == 0 {
17148 body
17149 } else if left_align {
17150 format!("{}{}", body, " ".repeat(pad))
17151 } else if zero_pad {
17152 // For `%#04x` with value 15: body = "0xf" (3 chars), width=4,
17153 // pad=1. Zero-pad after the `0x` prefix → "0x0f". Match libc.
17154 if let Some(rest) = body.strip_prefix("0x").or_else(|| body.strip_prefix("0X")) {
17155 let prefix = &body[..2];
17156 format!("{}{}{}", prefix, "0".repeat(pad), rest)
17157 } else {
17158 format!("{}{}", "0".repeat(pad), body)
17159 }
17160 } else {
17161 format!("{}{}", " ".repeat(pad), body)
17162 }
17163}
17164
17165fn format_spec_uint(spec: &str, n: u64) -> String {
17166 // c:Src/builtin.c — `%u` is rendered via libc `%lu`. Unsigned
17167 // conversions print NO sign: the `+` and ` ` flags are ignored
17168 // (they apply only to signed conversions), and a negative math
17169 // result has already wrapped into the u64 by the caller's cast
17170 // (`-42` → 18446744073709551574). The previous port delegated to
17171 // format_spec_int(n as i64), which reinterpreted the wrapped u64
17172 // back as a negative i64 and printed `-42`, and honored `+`/` `.
17173 let (left_align, zero_pad_flag, width, prec) = parse_flags_width_prec(spec);
17174 // libc: with an explicit precision the `0` flag is ignored.
17175 let zero_pad = zero_pad_flag && !left_align && prec.is_none();
17176 let digits = n.to_string();
17177 // Precision = minimum number of digits (zero-fill the body). A zero value
17178 // with precision 0 yields NO digits (c:libc printf).
17179 let body = match prec {
17180 Some(0) if n == 0 => String::new(),
17181 Some(p) if digits.len() < p => format!("{}{}", "0".repeat(p - digits.len()), digits),
17182 _ => digits,
17183 };
17184 // c:libc printf `'` thousands-grouping flag (see format_spec_int for
17185 // the rationale, the `prec.is_none()` gate, and the port-purity-gate
17186 // note on the inlined copy). Unsigned bodies carry no sign, so the
17187 // whole body groups.
17188 let body = if spec.contains('\'') && prec.is_none() {
17189 let sep = unsafe {
17190 let lc = libc::localeconv();
17191 if lc.is_null() || (*lc).thousands_sep.is_null() {
17192 String::new()
17193 } else {
17194 std::ffi::CStr::from_ptr((*lc).thousands_sep)
17195 .to_string_lossy()
17196 .into_owned()
17197 }
17198 };
17199 if sep.is_empty() {
17200 body
17201 } else {
17202 let chars: Vec<char> = body.chars().collect();
17203 let len = chars.len();
17204 let mut grouped = String::with_capacity(len + len / 3 * sep.len());
17205 for (i, ch) in chars.iter().enumerate() {
17206 if i > 0 && (len - i) % 3 == 0 {
17207 grouped.push_str(&sep);
17208 }
17209 grouped.push(*ch);
17210 }
17211 grouped
17212 }
17213 } else {
17214 body
17215 };
17216 let pad = width.saturating_sub(body.chars().count());
17217 if pad == 0 {
17218 body
17219 } else if left_align {
17220 format!("{}{}", body, " ".repeat(pad))
17221 } else if zero_pad {
17222 format!("{}{}", "0".repeat(pad), body)
17223 } else {
17224 format!("{}{}", " ".repeat(pad), body)
17225 }
17226}
17227
17228fn format_spec_float(spec: &str, n: f64) -> String {
17229 let (left_align, zero_pad, width, prec) = parse_flags_width_prec(spec);
17230 let p = prec.unwrap_or(6);
17231 let body = format!("{:.*}", p, n);
17232 let pad = width.saturating_sub(body.chars().count());
17233 if pad == 0 {
17234 body
17235 } else if left_align {
17236 format!("{}{}", body, " ".repeat(pad))
17237 } else if zero_pad {
17238 if let Some(rest) = body.strip_prefix('-') {
17239 format!("-{}{}", "0".repeat(pad), rest)
17240 } else {
17241 format!("{}{}", "0".repeat(pad), body)
17242 }
17243 } else {
17244 format!("{}{}", " ".repeat(pad), body)
17245 }
17246}
17247
17248/// printf %g / %G / %e / %E / %f / %F dispatch. Mirrors C printf
17249/// semantics (Src/builtin.c — libc snprintf): %g picks the shorter
17250/// of %e/%f and strips trailing zeros; %e/%E uses scientific notation;
17251/// %f/%F is decimal-fraction (no scientific). Default precision is 6.
17252// pub(crate) so zcalc can format through the SAME printf conversion it calls in
17253// zsh (zcalc:107 `printf "$_forms[_outform]\n" …`, _forms[1] = '%2$g'). The
17254// float-parameter formatter `convfloat` is NOT interchangeable: it re-appends a
17255// trailing `.` when %g yields no dot (c:params.c:5748-5749), which printf never
17256// does — `sqrt(2)*sqrt(2)` must print `2`, not `2.`.
17257pub(crate) fn format_spec_float_conv(spec: &str, n: f64, conv: char) -> String {
17258 // c:Src/builtin.c:5495-5499 — `/* force consistent form for Inf/NaN
17259 // output */ if (isnan(doubleval)) fputs("nan"); else if (isinf(doubleval))
17260 // fputs(doubleval < 0.0 ? "-inf" : "inf"); else print_val(...)`. zsh emits
17261 // the BARE lowercase string with no width/precision/flags/padding,
17262 // bypassing the normal formatting (platform-independent). The previous
17263 // Rust path ran nan/inf through `format!`, producing "NaN" and garbage
17264 // scientific ("NaNe+2147483647").
17265 if n.is_nan() {
17266 return "nan".to_string(); // c:5496-5497
17267 }
17268 if n.is_infinite() {
17269 return if n < 0.0 { "-inf" } else { "inf" }.to_string(); // c:5498-5499
17270 }
17271 let (left_align, zero_pad, width, prec) = parse_flags_width_prec(spec);
17272 let body = match conv {
17273 'f' | 'F' => {
17274 let p = prec.unwrap_or(6);
17275 format!("{:.*}", p, n)
17276 }
17277 'e' | 'E' => {
17278 // Use Rust's `{:.*e}` for the mantissa+exponent — it is EXACT
17279 // (correctly-rounded, like libc snprintf). The previous port
17280 // computed `mantissa = n / 10^exp` which lost precision: `37/10`
17281 // is not exactly 3.7, so `%.27e 37` printed the binary error
17282 // `3.70000…0177635683940e+01` instead of zsh's `3.7000…000e+01`.
17283 // Rust emits `<mant>e<exp>` with lowercase `e` and no exponent
17284 // padding/sign; reformat the exponent to libc's `e[+-]NN` (≥2 digits).
17285 let p = prec.unwrap_or(6);
17286 let raw = format!("{:.*e}", p, n);
17287 let (mant, exp_s) = raw.split_once('e').unwrap_or((raw.as_str(), "0"));
17288 let exp: i32 = exp_s.parse().unwrap_or(0);
17289 let e_char = if conv == 'E' { 'E' } else { 'e' };
17290 let exp_sign = if exp >= 0 { '+' } else { '-' };
17291 format!("{}{}{}{:02}", mant, e_char, exp_sign, exp.abs())
17292 }
17293 'g' | 'G' => {
17294 // c:libc printf %g: precision is # significant digits
17295 // (default 6). Use %e if exp < -4 OR exp >= precision,
17296 // else %f. Trailing zeros stripped unless `#` flag set
17297 // (zshrs doesn't track # — skip stripping suppression).
17298 let p_sig: i32 = prec.unwrap_or(6).max(1) as i32;
17299 let exp = if n == 0.0 {
17300 0i32
17301 } else {
17302 n.abs().log10().floor() as i32
17303 };
17304 let use_e = exp < -4 || exp >= p_sig;
17305 let body = if use_e {
17306 // Exact mantissa+exponent via Rust's `{:.*e}` (see the `%e`
17307 // arm) — `n / 10^exp` lost precision.
17308 let dec = (p_sig - 1).max(0) as usize;
17309 let raw = format!("{:.*e}", dec, n);
17310 let (m, exp_s) = raw.split_once('e').unwrap_or((raw.as_str(), "0"));
17311 let ex: i32 = exp_s.parse().unwrap_or(0);
17312 let e_char = if conv == 'G' { 'E' } else { 'e' };
17313 let exp_sign = if ex >= 0 { '+' } else { '-' };
17314 format!("{}{}{}{:02}", m, e_char, exp_sign, ex.abs())
17315 } else {
17316 // p_sig - 1 - exp digits after decimal point
17317 let dec = (p_sig - 1 - exp).max(0) as usize;
17318 format!("{:.*}", dec, n)
17319 };
17320 // Strip trailing zeros from the fractional part (but keep
17321 // at least one digit after `.` if `.` is present).
17322 // Only strip if no `#` flag was set in spec.
17323 // c:libc snprintf %g — trailing-zero strip done inline; no
17324 // separate helper in C source.
17325 if !spec.contains('#') {
17326 let stripped = if let Some(e_pos) = body.find(|c| c == 'e' || c == 'E') {
17327 let (mantissa, exp) = body.split_at(e_pos);
17328 let m = if mantissa.contains('.') {
17329 mantissa
17330 .trim_end_matches('0')
17331 .trim_end_matches('.')
17332 .to_string()
17333 } else {
17334 mantissa.to_string()
17335 };
17336 format!("{}{}", m, exp)
17337 } else if body.contains('.') {
17338 body.trim_end_matches('0').trim_end_matches('.').to_string()
17339 } else {
17340 body
17341 };
17342 stripped
17343 } else {
17344 body
17345 }
17346 }
17347 _ => format!("{}", n),
17348 };
17349 // c:libc printf `#` flag on a float conversion forces a decimal point to
17350 // appear even when precision 0 leaves no fractional digits: `%#.0f` 5 →
17351 // "5.", `%#.0e` 5 → "5.e+00", `%#.0g` 5 → "5.". (The %g/%G arm above also
17352 // uses `#` to suppress trailing-zero stripping.)
17353 let body = if spec.contains('#') && !body.contains('.') {
17354 match body.find(|c| c == 'e' || c == 'E') {
17355 Some(ep) => format!("{}.{}", &body[..ep], &body[ep..]),
17356 None => format!("{body}."),
17357 }
17358 } else {
17359 body
17360 };
17361 // c:libc printf flags `+` and ` ` — a non-negative float value gets
17362 // a forced leading `+` (with `+`) or a blank (with ` `); `+`
17363 // overrides ` `. parse_flags_width_prec only extracts `-`/`0`, so
17364 // these sign flags were dropped: `printf "%+.2f" 1.5` rendered
17365 // "1.50" instead of "+1.50". Negatives keep their own `-`. The
17366 // zero-pad arm below already expects an optional leading `+`.
17367 let body = if body.starts_with('-') {
17368 body
17369 } else if spec.contains('+') {
17370 format!("+{}", body)
17371 } else if spec.contains(' ') {
17372 format!(" {}", body)
17373 } else {
17374 body
17375 };
17376 let pad = width.saturating_sub(body.chars().count());
17377 if pad == 0 {
17378 body
17379 } else if left_align {
17380 // c:libc printf — `-` overrides `0`; pad on the right with spaces.
17381 format!("{}{}", body, " ".repeat(pad))
17382 } else if zero_pad {
17383 // c:libc printf — `0` flag without `-`: zero-pad between the
17384 // optional sign and digits. `printf "%05.2f" 1.5` → "01.50",
17385 // `printf "%05.2f" -1.5` → "-1.50" (no extra pad since sign +
17386 // body already fills width). Previously the helper discarded
17387 // zero_pad via parse_width_prec and always space-padded.
17388 if let Some(rest) = body.strip_prefix('-') {
17389 format!("-{}{}", "0".repeat(pad), rest)
17390 } else if let Some(rest) = body.strip_prefix('+') {
17391 format!("+{}{}", "0".repeat(pad), rest)
17392 } else if let Some(rest) = body.strip_prefix(' ') {
17393 // ` ` (space) flag + `0`: blank stays leftmost, zeros pad
17394 // between it and the digits. `printf "% 08.2f" 1.5` → " 0001.50".
17395 format!(" {}{}", "0".repeat(pad), rest)
17396 } else {
17397 format!("{}{}", "0".repeat(pad), body)
17398 }
17399 } else {
17400 format!("{}{}", " ".repeat(pad), body)
17401 }
17402}
17403
17404fn parse_width_prec(spec: &str) -> (bool, usize, Option<usize>) {
17405 let (left_align, _zero_pad, width, prec) = parse_flags_width_prec(spec);
17406 (left_align, width, prec)
17407}
17408
17409/// Parse printf-format flags `-`, `+`, ` `, `#`, `0`, width digits,
17410/// optional `.precision`. Returns (left_align, zero_pad, width, prec).
17411/// Mirrors C `parsefmt()` in Src/builtin.c — the `0` flag is only a
17412/// flag when it appears BEFORE any width digit; a `0` inside the
17413/// width number (e.g. `%10d`) is just part of the width.
17414fn parse_flags_width_prec(spec: &str) -> (bool, bool, usize, Option<usize>) {
17415 let s = spec.trim_start_matches('%');
17416 let mut i = 0;
17417 let bytes = s.as_bytes();
17418 let mut left_align = false;
17419 let mut zero_pad = false;
17420 // `'` (thousands-grouping) is a flag char too — skip it here so a
17421 // following width/precision (`%'12d`, `%'.8d`) is still parsed. The
17422 // grouping itself is applied by the caller (format_spec_int/uint).
17423 while i < bytes.len() && matches!(bytes[i], b'-' | b'+' | b' ' | b'#' | b'0' | b'\'') {
17424 match bytes[i] {
17425 b'-' => left_align = true,
17426 b'0' => zero_pad = true,
17427 _ => {}
17428 }
17429 i += 1;
17430 }
17431 let width_start = i;
17432 while i < bytes.len() && bytes[i].is_ascii_digit() {
17433 i += 1;
17434 }
17435 let width: usize = s[width_start..i].parse().unwrap_or(0);
17436 let mut prec: Option<usize> = None;
17437 if i < bytes.len() && bytes[i] == b'.' {
17438 i += 1;
17439 let p_start = i;
17440 while i < bytes.len() && bytes[i].is_ascii_digit() {
17441 i += 1;
17442 }
17443 prec = Some(s[p_start..i].parse().unwrap_or(0));
17444 }
17445 (left_align, zero_pad, width, prec)
17446}
17447
17448// `findcmd` (Src/exec.c:897) — moved to its canonical home at
17449// `crate::ported::exec::findcmd` per PORT.md Rule C (the C source
17450// lives in exec.c, so the Rust port belongs in exec.rs). Call sites
17451// import from the new path.
17452pub use crate::ported::exec::findcmd;
17453use crate::ported::signals_h::run_queued_signals;
17454
17455/// Port of `getsigidx(const char *s)` from `Src/jobs.c:3047`.
17456/// Local wrapper that delegates to the canonical
17457/// `crate::ported::jobs::getsigidx` (matching `Src/jobs.c` location).
17458/// Returns -1 for unknown so existing builtin.rs call sites (which
17459/// use the i32 sentinel) don't need to change.
17460fn getsigidx(name: &str) -> i32 {
17461 crate::ported::jobs::getsigidx(name).unwrap_or(-1)
17462}
17463
17464
17465
17466/// Port of `int pat_enables(const char *cmd, char **patp, int enable)`
17467/// from `Src/pattern.c:4171`. Local builtin.rs shim that delegates to
17468/// the canonical pattern.rs port. Static-link path: the actual
17469/// zpc_strings/zpc_disables manipulation lives in
17470/// `pat_enables`.
17471fn pat_enables(name: &str, argv: &[String], on: bool) -> i32 {
17472 // c:4171
17473 let patp: Vec<&str> = argv.iter().map(|s| s.as_str()).collect();
17474 crate::ported::pattern::pat_enables(name, &patp, on)
17475}
17476
17477// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
17478// ─── RUST-ONLY ACCESSORS ───
17479//
17480// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
17481// RwLock<T>>` globals declared above. C zsh uses direct global
17482// access; Rust needs these wrappers because `OnceLock::get_or_init`
17483// is the only way to lazily construct shared state. These ported sit
17484// here so the body of this file reads in C source order without
17485// the accessor wrappers interleaved between real port ported.
17486// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
17487
17488// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
17489// ─── RUST-ONLY ACCESSORS ───
17490//
17491// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
17492// RwLock<T>>` globals declared above. C zsh uses direct global
17493// access; Rust needs these wrappers because `OnceLock::get_or_init`
17494// is the only way to lazily construct shared state. These ported sit
17495// here so the body of this file reads in C source order without
17496// the accessor wrappers interleaved between real port ported.
17497// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
17498/// `traps_table` — see implementation.
17499pub fn traps_table() -> &'static Mutex<HashMap<String, String>> {
17500 TRAPS_INNER.get_or_init(|| Mutex::new(HashMap::new()))
17501}
17502
17503#[cfg(test)]
17504mod tests {
17505 use super::*;
17506
17507 /// `findcmd` with an existing ABSOLUTE path bypasses the PATH
17508 /// walk entirely (c:916 `if (arg0 == s || ...)` branch — `s` is
17509 /// the result of `strchr(arg0, '/')`; when the first char IS
17510 /// the slash, `arg0 == s` is true). The caller's `$PATH` is
17511 /// irrelevant for an absolute path. A regression that always
17512 /// walked $PATH would fail to find `/bin/sh` when $PATH was
17513 /// empty, breaking command-name resolution for cron/init contexts
17514 /// that explicitly pass absolute paths.
17515 #[test]
17516 fn findcmd_absolute_path_skips_path_walk() {
17517 let _g = crate::test_util::global_state_lock();
17518 // Empty $PATH to guarantee the walk would miss.
17519 setsparam("PATH", "");
17520 let resolved = findcmd("/bin/sh", 0, 0);
17521 unsetparam("PATH");
17522 assert_eq!(
17523 resolved.as_deref(),
17524 Some("/bin/sh"),
17525 "c:914-919 — absolute path that exists must resolve to itself \
17526 regardless of $PATH"
17527 );
17528 }
17529
17530 /// `findcmd` with `default_path != 0` MUST search the hardcoded
17531 /// `DEFAULT_PATH` (`/usr/bin:/bin:/usr/sbin:/sbin`), NOT the
17532 /// caller's `$PATH`. C body c:903-908. This is the `command -p`
17533 /// security contract: scripts that need to invoke a sanitized
17534 /// `awk`/`sed`/`grep` regardless of user-poisoned $PATH (e.g.
17535 /// `command -p sh -c '...'` in a setuid wrapper) rely on the
17536 /// fallback path. A regression that ignored `default_path` would
17537 /// re-introduce the very PATH-injection vulnerability that
17538 /// `command -p` exists to prevent.
17539 ///
17540 /// Pin: with $PATH set to a non-existent directory, `findcmd`
17541 /// for a binary that ONLY lives in /bin or /usr/bin (e.g. `sh`)
17542 /// must still resolve when `default_path=1`.
17543 #[test]
17544 fn findcmd_default_path_searches_hardcoded_dirs() {
17545 let _g = crate::test_util::global_state_lock();
17546 // Poison $PATH so the normal path-walk would miss.
17547 setsparam("PATH", "/nonexistent/zshrs-test-poison");
17548 // `sh` exists in /bin on every POSIX system.
17549 let resolved = findcmd("sh", 0, 1);
17550 unsetparam("PATH");
17551 assert!(
17552 resolved.is_some(),
17553 "c:903-908 — default_path must search DEFAULT_PATH regardless of $PATH"
17554 );
17555 let p = resolved.unwrap();
17556 assert!(
17557 DEFAULT_PATH.split(':').any(|d| p.starts_with(d)),
17558 "resolved path must be under one of DEFAULT_PATH's dirs; got {:?}",
17559 p
17560 );
17561 }
17562
17563 /// c:7399 — `trap - <undefined>` MUST report failure (non-zero
17564 /// exit) so scripts can detect the bad signal name. The previous
17565 /// Rust port returned 0 unconditionally from the clear path,
17566 /// silently masking errors. C returns `*argv != NULL` — non-zero
17567 /// when the loop broke on an undefined signal.
17568 #[test]
17569 fn bin_trap_clear_undefined_signal_returns_nonzero() {
17570 let _g = crate::test_util::global_state_lock();
17571 let empty = options {
17572 ind: [0u8; MAX_OPS],
17573 args: Vec::new(),
17574 argscount: 0,
17575 argsalloc: 0,
17576 };
17577 // `trap - BOGUS_NEVER_A_SIGNAL` → must return 1.
17578 let r = bin_trap(
17579 "trap",
17580 &["-".into(), "BOGUS_NEVER_A_SIGNAL".into()],
17581 &empty,
17582 0,
17583 );
17584 assert_ne!(
17585 r, 0,
17586 "trap - <undefined> must report error per c:7399 (got {})",
17587 r
17588 );
17589 }
17590
17591 /// c:7405-7409 — the trap body is parsed when the trap is INSTALLED
17592 /// (`parse_string(arg, 1)`), so an unparseable body fails right here
17593 /// with `couldn't parse trap command` and rc=1, and nothing is
17594 /// installed. zshrs previously stored the body text unexamined and
17595 /// returned 0, deferring the failure to signal delivery: `trap 'for'
17596 /// EXIT` reported success and then emitted a parse error at exit.
17597 ///
17598 /// The bodies here are the ones the oracle rejects. `while` is
17599 /// deliberately not among them — `zsh -fc 'while'` treats it as an
17600 /// incomplete construct and reads the remainder from stdin rather
17601 /// than erroring, so it is not an install-time parse failure.
17602 #[test]
17603 fn bin_trap_unparseable_body_rejected_at_install() {
17604 let _g = crate::test_util::global_state_lock();
17605 let empty = options {
17606 ind: [0u8; MAX_OPS],
17607 args: Vec::new(),
17608 argscount: 0,
17609 argsalloc: 0,
17610 };
17611 for body in ["for", "((", "fi", "done", "case", "if true", "print ok; for"] {
17612 let r = bin_trap("trap", &[body.into(), "USR1".into()], &empty, 0);
17613 assert_eq!(
17614 r, 1,
17615 "trap '{body}' USR1 must fail at install per c:7407 (got {r})"
17616 );
17617 // The rejected body must not have been installed (c:7408
17618 // returns before the install loop at c:7421).
17619 let installed = traps_table()
17620 .lock()
17621 .ok()
17622 .map(|t| t.contains_key("USR1"))
17623 .unwrap_or(false);
17624 assert!(
17625 !installed,
17626 "trap '{body}' USR1 was rejected but still installed a handler"
17627 );
17628 }
17629 // A parseable body still installs — the gate must not reject
17630 // everything.
17631 let r = bin_trap("trap", &["print ok".into(), "USR1".into()], &empty, 0);
17632 assert_eq!(r, 0, "parseable body must install (got {r})");
17633 if let Ok(mut t) = traps_table().lock() {
17634 assert!(t.contains_key("USR1"), "parseable body must be installed");
17635 t.remove("USR1");
17636 }
17637 }
17638
17639 /// c:7371 — `getpermtext(siglists[sig], NULL, 0)`. C keeps the trap
17640 /// body as a compiled Eprog and renders it back to source for the
17641 /// listing, so `trap` prints CANONICAL text rather than the string
17642 /// that was typed: separators become newlines. zshrs stores the body
17643 /// as raw text and previously echoed it verbatim, which is correct
17644 /// only for a single command — `trap 'print a; print b' EXIT` listed
17645 /// as `'print a; print b'` where zsh lists `$'print a\nprint b'`.
17646 ///
17647 /// Pins the deparse itself (parse_string → getpermtext) rather than
17648 /// bin_trap's stdout, so the assertion doesn't depend on capturing
17649 /// the builtin's print! output.
17650 #[test]
17651 fn trap_body_listing_deparses_to_canonical_text() {
17652 let _g = crate::test_util::global_state_lock();
17653 let prog = crate::ported::exec::parse_string("print a; print b", 1)
17654 .expect("body must parse for the listing path");
17655 let rendered = crate::ported::text::getpermtext(Box::new(prog), None, 0);
17656 assert_eq!(
17657 rendered, "print a\nprint b",
17658 "c:7371 getpermtext must canonicalise `;` separators to newlines (got {rendered:?})"
17659 );
17660 }
17661
17662 /// Src/options.c:537-549 — `emulate(zsh_name, ...)` dispatches
17663 /// on the FIRST char of the shell name, stripping a leading `r`
17664 /// (so `rcsh`/`rksh` work as restricted variants of their base
17665 /// shell). `bash` aliases to SH (the `'b'` branch of the case).
17666 /// Pin the bits assigned by `bin_emulate` for the canonical
17667 /// names + their first-char-overlap aliases.
17668 #[test]
17669 fn bin_emulate_dispatches_on_first_char_per_c537() {
17670 let _g = crate::test_util::global_state_lock();
17671 let empty = options {
17672 ind: [0u8; MAX_OPS],
17673 args: Vec::new(),
17674 argscount: 0,
17675 argsalloc: 0,
17676 };
17677 let saved = emulation.load(Relaxed);
17678
17679 // Each (name, expected_bits) — name covers the canonical
17680 // shell names AND their `r`-prefix / first-char variants.
17681 for (name, expected) in [
17682 ("csh", EMULATE_CSH),
17683 ("ksh", EMULATE_KSH),
17684 ("sh", EMULATE_SH),
17685 ("rcsh", EMULATE_CSH), // c:539-540
17686 ("rksh", EMULATE_KSH), // c:539-540
17687 ("bash", EMULATE_SH), // c:548 'b'
17688 ] {
17689 emulation.store(0, Relaxed);
17690 bin_emulate("emulate", &[name.into()], &empty, 0);
17691 let bits = emulation.load(Relaxed);
17692 assert_eq!(
17693 bits, expected,
17694 "emulate {} must set bits {:#x}, got {:#x}",
17695 name, expected, bits
17696 );
17697 }
17698 emulation.store(saved, Relaxed);
17699 }
17700
17701 /// c:7399 — `trap - SIGUSR1` (valid signal) MUST return 0, even
17702 /// when the trap was never set (remove is a no-op).
17703 #[test]
17704 fn bin_trap_clear_valid_signal_returns_zero() {
17705 let _g = crate::test_util::global_state_lock();
17706 let empty = options {
17707 ind: [0u8; MAX_OPS],
17708 args: Vec::new(),
17709 argscount: 0,
17710 argsalloc: 0,
17711 };
17712 let r = bin_trap("trap", &["-".into(), "USR1".into()], &empty, 0);
17713 assert_eq!(
17714 r, 0,
17715 "trap - USR1 must succeed even with no prior trap (got {})",
17716 r
17717 );
17718 }
17719
17720 #[test]
17721 fn registration_table_matches_c_count() {
17722 let _g = crate::test_util::global_state_lock();
17723 // Src/builtin.c:40-137 has 79 rows total (5 BIN_PREFIX + 71
17724 // BUILTIN + 3 debug-only BUILTIN). The Rust port bundles
17725 // additional builtins eagerly that C would load via zmodload:
17726 // zsh/rlimits (limit/ulimit/unlimit)
17727 // zsh/zle (bindkey/vared/zle)
17728 // zsh/cap (cap/getcap/setcap)
17729 // zsh/files (chmod/chown/ln/mkdir/rm/rmdir/sync)
17730 // zsh/complete (compadd/compset)
17731 // zsh/terminfo (echoti)
17732 // zsh/pcre (pcre_compile/pcre_match/pcre_study)
17733 // zsh/zutil (zformat/zgdbmpath)
17734 // zsh/sched (sched)
17735 // zsh/computil (comparguments/compdescribe/compfiles/
17736 // compgroups/compquote/comptags/comptry/compvalues)
17737 // zsh/system (syserror/sysread/syswrite/sysopen/sysseek/zsystem)
17738 // zsh/zselect (zselect)
17739 // zsh/socket (zsocket)
17740 // zsh/stat (stat)
17741 // zsh/watch (log)
17742 // zsh/zprof (zprof)
17743 // zsh/datetime (strftime)
17744 // zsh/zftp (zftp), zsh/zpty (zpty), zsh/curses (zcurses)
17745 // zsh/clone (clone), zsh/example (example)
17746 // zsh/param/private (private)
17747 // zsh/termcap (echotc)
17748 // zsh/compctl (compcall, compctl)
17749 // zsh/attr (zgetattr, zsetattr, zdelattr, zlistattr)
17750 // Tripwire pin on BUILTINS table length. The number drifts every
17751 // time the eagerly-loaded-module list above grows (new builtin
17752 // ported, new module wired). Bump it alongside the change so
17753 // accidental additions/removals still trip a review.
17754 assert_eq!(BUILTINS.len(), 159,
17755 "BUILTINS table size changed — bump count or update the eagerly-loaded-module list above");
17756 }
17757
17758 /// `Src/builtin.c:40-137` — every name in the canonical C builtin
17759 /// table must be present in the Rust port. Pins coverage of all
17760 /// 79 C builtins by name (ignores option-mask / handler details).
17761 /// Detects regressions where a builtin gets accidentally dropped
17762 /// from BUILTINS. Names extracted from upstream zsh `Src/builtin.c`.
17763 #[test]
17764 fn registration_table_contains_all_c_builtins() {
17765 let _g = crate::test_util::global_state_lock();
17766 // Canonical 79 names from Src/builtin.c:40-137 (verbatim).
17767 let c_names: &[&str] = &[
17768 "-",
17769 ".",
17770 ":",
17771 "[",
17772 "alias",
17773 "autoload",
17774 "bg",
17775 "break",
17776 "builtin",
17777 "bye",
17778 "cd",
17779 "chdir",
17780 "command",
17781 "continue",
17782 "declare",
17783 "dirs",
17784 "disable",
17785 "disown",
17786 "echo",
17787 "emulate",
17788 "enable",
17789 "eval",
17790 "exec",
17791 "exit",
17792 "export",
17793 "false",
17794 "fc",
17795 "fg",
17796 "float",
17797 "functions",
17798 "getln",
17799 "getopts",
17800 "hash",
17801 "hashinfo",
17802 "history",
17803 "integer",
17804 "jobs",
17805 "kill",
17806 "let",
17807 "local",
17808 "logout",
17809 "mem",
17810 "noglob",
17811 "patdebug",
17812 "popd",
17813 "print",
17814 "printf",
17815 "pushd",
17816 "pushln",
17817 "pwd",
17818 "r",
17819 "read",
17820 "readonly",
17821 "rehash",
17822 "return",
17823 "set",
17824 "setopt",
17825 "shift",
17826 "source",
17827 "suspend",
17828 "test",
17829 "times",
17830 "trap",
17831 "true",
17832 "ttyctl",
17833 "type",
17834 "typeset",
17835 "umask",
17836 "unalias",
17837 "unfunction",
17838 "unhash",
17839 "unset",
17840 "unsetopt",
17841 "wait",
17842 "whence",
17843 "where",
17844 "which",
17845 "zcompile",
17846 "zmodload",
17847 ];
17848 assert_eq!(
17849 c_names.len(),
17850 79,
17851 "C builtin.c row count is 79 — recount if changed"
17852 );
17853 let table_names: std::collections::HashSet<&str> =
17854 BUILTINS.iter().map(|b| b.node.nam.as_str()).collect();
17855 for c_name in c_names {
17856 assert!(
17857 table_names.contains(*c_name),
17858 "missing C builtin '{}' from BUILTINS table",
17859 c_name
17860 );
17861 }
17862 }
17863
17864 #[test]
17865 fn lookup_finds_known_builtins() {
17866 let _g = crate::test_util::global_state_lock();
17867 for name in [
17868 "cd", "echo", "print", "fg", "bg", "jobs", "wait", "typeset", "test", "[", ".",
17869 ] {
17870 assert!(
17871 createbuiltintable().get(name).copied().is_some(),
17872 "missing: {name}"
17873 );
17874 }
17875 }
17876
17877 #[test]
17878 fn lookup_misses_unknown() {
17879 let _g = crate::test_util::global_state_lock();
17880 assert!(createbuiltintable()
17881 .get("not-a-builtin-zZz")
17882 .copied()
17883 .is_none());
17884 }
17885
17886 #[test]
17887 fn prefix_entries_have_prefix_flag() {
17888 let _g = crate::test_util::global_state_lock();
17889 for name in ["-", "builtin", "command", "exec", "noglob"] {
17890 let b = createbuiltintable().get(name).copied().unwrap();
17891 assert!(
17892 b.node.flags as u32 & BINF_PREFIX != 0,
17893 "{name} missing BINF_PREFIX"
17894 );
17895 }
17896 }
17897
17898 #[test]
17899 fn fixdir_canonicalizes_absolute_paths() {
17900 let _g = crate::test_util::global_state_lock();
17901 // c:1297 — collapse `//`, drop `./`, pop `..`.
17902 assert_eq!(fixdir("/tmp/./foo"), "/tmp/foo");
17903 assert_eq!(fixdir("/tmp//foo"), "/tmp/foo");
17904 assert_eq!(fixdir("/tmp/bar/../foo"), "/tmp/foo");
17905 assert_eq!(fixdir("/tmp/bar/baz/../.."), "/tmp");
17906 }
17907
17908 #[test]
17909 fn fixdir_drops_dotdot_past_root() {
17910 let _g = crate::test_util::global_state_lock();
17911 // c:1372 — absolute path, `..` past `/` is dropped.
17912 assert_eq!(fixdir("/.."), "/");
17913 assert_eq!(fixdir("/../.."), "/");
17914 assert_eq!(fixdir("/foo/../../bar"), "/bar");
17915 }
17916
17917 #[test]
17918 fn fixdir_relative_keeps_leading_dotdot() {
17919 let _g = crate::test_util::global_state_lock();
17920 // c:1367 — relative path: `..` past start stays as `..`.
17921 assert_eq!(fixdir("../foo"), "../foo");
17922 assert_eq!(fixdir("../../foo"), "../../foo");
17923 assert_eq!(fixdir("foo/../bar"), "bar");
17924 }
17925
17926 #[test]
17927 fn fixdir_empty_collapses_to_dot() {
17928 let _g = crate::test_util::global_state_lock();
17929 // Relative path that collapses fully → "."
17930 assert_eq!(fixdir("./"), ".");
17931 assert_eq!(fixdir("foo/.."), ".");
17932 }
17933
17934 #[test]
17935 fn fixdir_empty_input_returns_empty() {
17936 let _g = crate::test_util::global_state_lock();
17937 assert_eq!(fixdir(""), "");
17938 }
17939
17940 #[test]
17941 fn fg_dispatch_id_distinguishes_aliases() {
17942 let _g = crate::test_util::global_state_lock();
17943 // bin_fg covers fg, bg, jobs, wait, disown — same handler,
17944 // different funcid. Mirrors Src/builtin.c:52,61,75,88,131.
17945 assert_eq!(
17946 createbuiltintable().get("fg").copied().unwrap().funcid,
17947 BIN_FG
17948 );
17949 assert_eq!(
17950 createbuiltintable().get("bg").copied().unwrap().funcid,
17951 BIN_BG
17952 );
17953 assert_eq!(
17954 createbuiltintable().get("jobs").copied().unwrap().funcid,
17955 BIN_JOBS
17956 );
17957 assert_eq!(
17958 createbuiltintable().get("wait").copied().unwrap().funcid,
17959 BIN_WAIT
17960 );
17961 assert_eq!(
17962 createbuiltintable().get("disown").copied().unwrap().funcid,
17963 BIN_DISOWN
17964 );
17965 }
17966
17967 /// c:1297 — `fixdir` is the lexical-canonicalisation for `cd`. The
17968 /// path `/a/b/../c` must resolve to `/a/c` BEFORE chdir(2) — the
17969 /// shell uses it to compute the logical PWD for $PWD/OLDPWD. A
17970 /// regression that drops the `..` consumption would make $PWD
17971 /// report `/a/b/../c` literally on `cd /a/b/../c`.
17972 #[test]
17973 fn fixdir_pops_dotdot_against_previous_component() {
17974 let _g = crate::test_util::global_state_lock();
17975 assert_eq!(fixdir("/a/b/../c"), "/a/c");
17976 assert_eq!(fixdir("/a/b/../../c"), "/c");
17977 assert_eq!(fixdir("/foo/.."), "/");
17978 }
17979
17980 /// c:1352 — `./` collapses to nothing. `/a/./b` must equal `/a/b`.
17981 #[test]
17982 fn fixdir_drops_dot_components() {
17983 let _g = crate::test_util::global_state_lock();
17984 assert_eq!(fixdir("/a/./b"), "/a/b");
17985 assert_eq!(fixdir("./a"), "a");
17986 assert_eq!(fixdir("./."), ".");
17987 }
17988
17989 /// c:1388 — `//` collapses to single `/` (no preservation of POSIX
17990 /// implementation-defined `//` semantics, which zsh doesn't honour).
17991 #[test]
17992 fn fixdir_collapses_consecutive_slashes() {
17993 let _g = crate::test_util::global_state_lock();
17994 assert_eq!(fixdir("/a//b"), "/a/b");
17995 assert_eq!(fixdir("/a///b/c"), "/a/b/c");
17996 }
17997
17998 /// c:1404 — absolute path: `..` past `/` silently drops. `/..`
17999 /// resolves to `/`. Catches a regression where the underflow
18000 /// emits `..` literally.
18001 #[test]
18002 fn fixdir_dotdot_past_root_clamps_to_root() {
18003 let _g = crate::test_util::global_state_lock();
18004 assert_eq!(fixdir("/.."), "/");
18005 assert_eq!(fixdir("/../../a"), "/a");
18006 }
18007
18008 /// c:1400 — RELATIVE path: leading `..` are preserved (no parent
18009 /// known until chdir time). This is critical for `cd ../../foo`
18010 /// which must NOT resolve `..` lexically.
18011 #[test]
18012 fn fixdir_relative_leading_dotdot_is_preserved() {
18013 let _g = crate::test_util::global_state_lock();
18014 assert_eq!(fixdir("../foo"), "../foo");
18015 assert_eq!(fixdir("../../foo"), "../../foo");
18016 }
18017
18018 /// c:1683 — `fcgetcomm` returns 0 for ambiguous numeric inputs
18019 /// only when the string actually starts with '0'. The atoi result
18020 /// alone (which is 0 for non-numeric) MUST NOT short-circuit —
18021 /// non-numeric input should fall through to hcomsearch instead.
18022 #[test]
18023 fn fcgetcomm_numeric_zero_only_for_literal_zero_prefix() {
18024 let _g = crate::test_util::global_state_lock();
18025 assert_eq!(fcgetcomm("0"), 0, "literal `0` is event 0");
18026 assert_eq!(fcgetcomm("42"), 42);
18027 // Non-numeric falls through to hcomsearch (no hist match → -1).
18028 assert_eq!(fcgetcomm("definitely_not_a_history_command_zshrs"), -1);
18029 }
18030
18031 /// c:1088-1093 — `cd_able_vars` requires CDABLEVARS to be set;
18032 /// otherwise returns None even when the head names a param. A
18033 /// regression that ignores the option flag would let `cd HOME`
18034 /// silently `cd $HOME` even when the user disabled CDABLEVARS.
18035 #[test]
18036 fn cd_able_vars_returns_none_without_cdablevars_option() {
18037 let _g = crate::test_util::global_state_lock();
18038 // CDABLEVARS is not set by default → must return None.
18039 // We don't fight the option state here; just verify the
18040 // off-state default short-circuits before paramtab lookup.
18041 // (If a future commit enables CDABLEVARS by default, this
18042 // test will fail loudly — that's the right canary.)
18043 let r = cd_able_vars("HOME/anything");
18044 // Without CDABLEVARS, must be None; with it, would be Some.
18045 // Accept either since the option default is the actual invariant.
18046 if !isset(CDABLEVARS) {
18047 assert!(r.is_none());
18048 }
18049 }
18050
18051 /// c:212 — `init_builtins` is idempotent: calling twice doesn't
18052 /// duplicate entries in the table. Regression that re-inserts on
18053 /// every call would balloon memory + break dispatch lookups.
18054 #[test]
18055 fn init_builtins_is_idempotent() {
18056 let _g = crate::test_util::global_state_lock();
18057 init_builtins();
18058 let count1 = createbuiltintable().len();
18059 init_builtins();
18060 let count2 = createbuiltintable().len();
18061 assert_eq!(count1, count2, "init_builtins must not duplicate entries");
18062 }
18063
18064 /// c:1708 — `fcsubs(sp, [(old, new), ...])` applies each
18065 /// substitution to the running string, returning the total
18066 /// replacement count. A regression returning 0 with substitutions
18067 /// applied would silently break `fc -s old=new`.
18068 #[test]
18069 fn fcsubs_applies_each_substitution_in_order() {
18070 let _g = crate::test_util::global_state_lock();
18071 let mut s = "echo foo bar foo".to_string();
18072 let n = fcsubs(&mut s, &[("foo".to_string(), "FOO".to_string())]);
18073 assert_eq!(s, "echo FOO bar FOO");
18074 assert_eq!(n, 2, "two `foo` matches replaced");
18075 }
18076
18077 /// c:1708 — empty `old` MUST skip (avoid infinite empty-match
18078 /// replacement loop). Regression treating "" as "match anywhere"
18079 /// would hang or silently corrupt every fc invocation.
18080 #[test]
18081 fn fcsubs_skips_empty_pattern() {
18082 let _g = crate::test_util::global_state_lock();
18083 let mut s = "anything".to_string();
18084 let n = fcsubs(&mut s, &[("".to_string(), "X".to_string())]);
18085 assert_eq!(s, "anything", "empty pattern must be skipped");
18086 assert_eq!(n, 0);
18087 }
18088
18089 /// c:1708 — chained substitutions apply left-to-right. After
18090 /// `a→b`, the next pair sees the post-substitution text. So
18091 /// `[(a→b), (b→c)]` over `a` yields `c`.
18092 #[test]
18093 fn fcsubs_chains_substitutions_left_to_right() {
18094 let _g = crate::test_util::global_state_lock();
18095 let mut s = "a".to_string();
18096 let n = fcsubs(
18097 &mut s,
18098 &[
18099 ("a".to_string(), "b".to_string()),
18100 ("b".to_string(), "c".to_string()),
18101 ],
18102 );
18103 assert_eq!(s, "c", "second sub sees post-first-sub text");
18104 assert_eq!(n, 2);
18105 }
18106
18107 /// c:1708 — substitution on no-match leaves string unchanged AND
18108 /// reports 0. Regression touching the string anyway would mangle
18109 /// fc output for events containing none of the requested patterns.
18110 #[test]
18111 fn fcsubs_no_match_returns_zero_unchanged() {
18112 let _g = crate::test_util::global_state_lock();
18113 let mut s = "hello world".to_string();
18114 let n = fcsubs(&mut s, &[("xyz".to_string(), "abc".to_string())]);
18115 assert_eq!(s, "hello world", "no match → unchanged");
18116 assert_eq!(n, 0);
18117 }
18118
18119 /// c:1297 — `fixdir` for plain relative path (no slashes, no
18120 /// dots) returns it unchanged. Most-common cd path; regression
18121 /// here would break `cd subdir`.
18122 #[test]
18123 fn fixdir_plain_relative_path_unchanged() {
18124 let _g = crate::test_util::global_state_lock();
18125 assert_eq!(fixdir("subdir"), "subdir");
18126 assert_eq!(fixdir("a/b/c"), "a/b/c");
18127 assert_eq!(fixdir("."), ".");
18128 }
18129
18130 /// Shared mutex for bin_let tests that toggle the global errflag.
18131 static BIN_LET_TEST_LOCK: Mutex<()> = Mutex::new(());
18132
18133 /// `Src/builtin.c:7469-7484` — `bin_let` semantics:
18134 /// 1. Returns 0 (success) when the LAST arg evaluates to non-zero.
18135 /// 2. Returns 1 (failure) when the LAST arg evaluates to zero.
18136 /// 3. Returns 2 AND CLEARS ERRFLAG_ERROR when any arg errors
18137 /// (let errors are non-fatal and local).
18138 #[test]
18139 fn bin_let_clears_errflag_on_math_error() {
18140 let _g = crate::test_util::global_state_lock();
18141 let _g = BIN_LET_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
18142 let saved = errflag.load(Relaxed);
18143 errflag.store(0, Relaxed);
18144
18145 // 1. Last arg evaluates to non-zero → return 0.
18146 let ops = options {
18147 ind: [0; MAX_OPS],
18148 args: Vec::new(),
18149 argscount: 0,
18150 argsalloc: 0,
18151 };
18152 let argv = vec!["1".to_string()];
18153 assert_eq!(
18154 bin_let("let", &argv, &ops, 0),
18155 0,
18156 "c:7482 — last expr non-zero → return 0 (success)"
18157 );
18158
18159 // 2. Last arg evaluates to zero → return 1.
18160 let argv = vec!["0".to_string()];
18161 assert_eq!(
18162 bin_let("let", &argv, &ops, 0),
18163 1,
18164 "c:7482 — last expr zero → return 1 (failure)"
18165 );
18166
18167 // 3. Bad-syntax arg → return 2 AND clear ERRFLAG_ERROR.
18168 // Pre-set errflag manually to simulate matheval failure side
18169 // effect (since exact bad-syntax behavior of the matheval port
18170 // is implementation-dependent — what we're pinning is the
18171 // bin_let response to a set errflag).
18172 errflag.store(ERRFLAG_ERROR, Relaxed);
18173 // Use a valid expression so matheval succeeds, but errflag
18174 // is already set from a prior step.
18175 let argv = vec!["1".to_string()];
18176 let rc = bin_let("let", &argv, &ops, 0);
18177 // c:7479 says `return 2;` but the installed zsh 5.9.1 returns
18178 // 1 (oracle: `zsh -fc 'let 1/0; echo $?'` → 1) and bin_let
18179 // matches the release binary — see the comment block at the
18180 // errflag branch in bin_let. This assertion pins the OBSERVED
18181 // status; it went stale when bin_let switched 2→1.
18182 assert_eq!(
18183 rc, 1,
18184 "c:7479 cleanup path — observed zsh 5.9.1 rc (release binary returns 1, not the c:7479 literal 2)"
18185 );
18186 // c:7478 — `errflag &= ~ERRFLAG_ERROR` must have run.
18187 assert_eq!(
18188 errflag.load(Relaxed) & ERRFLAG_ERROR,
18189 0,
18190 "c:7478 — ERRFLAG_ERROR must be CLEARED after let error"
18191 );
18192
18193 // Restore.
18194 errflag.store(saved, Relaxed);
18195 }
18196
18197 /// `Src/builtin.c:7474-7475` — C walks ALL argv via
18198 /// `while (*argv) val = matheval(*argv++);`. The LAST matheval
18199 /// result is what determines the return code. The previous Rust
18200 /// port broke on first error, skipping later args. Pin: a sequence
18201 /// of two non-zero exprs returns 0 even if both are evaluated.
18202 #[test]
18203 fn bin_let_walks_all_argv_last_wins() {
18204 let _g = crate::test_util::global_state_lock();
18205 let _g = BIN_LET_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
18206 errflag.store(0, Relaxed);
18207
18208 let ops = options {
18209 ind: [0; MAX_OPS],
18210 args: Vec::new(),
18211 argscount: 0,
18212 argsalloc: 0,
18213 };
18214 // c:7474 — `5; 0` (two args): last is 0 → return 1.
18215 let argv = vec!["5".to_string(), "0".to_string()];
18216 assert_eq!(
18217 bin_let("let", &argv, &ops, 0),
18218 1,
18219 "c:7474 — last arg wins (here: 0 → return 1)"
18220 );
18221
18222 // c:7474 — `0; 5` (two args): last is 5 → return 0.
18223 let argv = vec!["0".to_string(), "5".to_string()];
18224 assert_eq!(
18225 bin_let("let", &argv, &ops, 0),
18226 0,
18227 "c:7474 — last arg wins (here: 5 → return 0)"
18228 );
18229
18230 errflag.fetch_and(!ERRFLAG_ERROR, Relaxed);
18231 }
18232
18233 /// `Src/builtin.c:4799-4808` — `print -o` (sort) is CASE-SENSITIVE
18234 /// by default; `-i` flips to case-insensitive. The previous Rust
18235 /// port had this INVERTED: case-sensitive under `-i`,
18236 /// case-insensitive without. Pin the canonical semantic by direct
18237 /// reproduction of the sort step.
18238 ///
18239 /// `bin_print` itself is harder to test in isolation because it
18240 /// emits to stdout; instead we replicate the in-port sort logic
18241 /// to ensure the gate matches C semantics. If the port body's
18242 /// `if ignore_case` is ever re-inverted, the regression here
18243 /// surfaces immediately.
18244 #[test]
18245 fn bin_print_sort_matches_c_case_gate() {
18246 let _g = crate::test_util::global_state_lock();
18247 // Helper mirroring the in-port logic exactly.
18248 let sort_with = |items: &[&str], ignore_case: bool, backwards: bool| -> Vec<String> {
18249 let mut v: Vec<String> = items.iter().map(|s| s.to_string()).collect();
18250 if ignore_case {
18251 v.sort_by_key(|s| s.to_lowercase());
18252 } else {
18253 v.sort();
18254 }
18255 if backwards {
18256 v.reverse();
18257 }
18258 v
18259 };
18260 // `print -o foo Bar BAZ` (no `-i`): case-sensitive ASCII sort.
18261 // Uppercase ASCII < lowercase ASCII, so caps come first.
18262 let no_i = sort_with(&["foo", "Bar", "BAZ"], false, false);
18263 assert_eq!(
18264 no_i,
18265 vec!["BAZ", "Bar", "foo"],
18266 "c:4805 — without -i: case-sensitive sort (caps first by ASCII)"
18267 );
18268 // `print -oi foo Bar BAZ`: case-insensitive sort.
18269 // Lower-case comparison: "bar" < "baz" < "foo", so order is
18270 // Bar, BAZ, foo.
18271 let with_i = sort_with(&["foo", "Bar", "BAZ"], true, false);
18272 assert_eq!(
18273 with_i,
18274 vec!["Bar", "BAZ", "foo"],
18275 "c:4805 — with -i: case-insensitive sort"
18276 );
18277 // `print -O foo Bar BAZ` (no `-i`): case-sensitive descending.
18278 let big_o = sort_with(&["foo", "Bar", "BAZ"], false, true);
18279 assert_eq!(
18280 big_o,
18281 vec!["foo", "Bar", "BAZ"],
18282 "c:4806 — -O reverses after sort"
18283 );
18284 // Conjunction check: zsh-equivalent: print -O foo Bar BAZ
18285 // gives `foo Bar BAZ`. Pin so an inadvertent reverse-before-
18286 // sort regression fails.
18287 }
18288
18289 /// `Src/builtin.c:4854-4856 + 5564-5565` — `printf -z FMT ARGS...`
18290 /// captures formatted output then pushes to bufstack (same path
18291 /// as -z without -f).
18292 #[test]
18293 fn bin_print_printf_with_minus_z() {
18294 let _g = crate::test_util::global_state_lock();
18295 let mut ops = options {
18296 ind: [0u8; MAX_OPS],
18297 args: Vec::new(),
18298 argscount: 0,
18299 argsalloc: 0,
18300 };
18301 ops.ind[b'z' as usize] = 1;
18302 // -f set to "echo %s" (positional)
18303 ops.ind[b'f' as usize] = 1 | (1 << 2);
18304 ops.args = vec!["echo %s".to_string()];
18305 ops.argscount = 1;
18306 crate::ported::zle::zle_main::BUFSTACK
18307 .lock()
18308 .unwrap()
18309 .clear();
18310 let r = bin_print("printf", &["hello".to_string()], &ops, BIN_PRINTF);
18311 assert_eq!(r, 0);
18312 let buf = crate::ported::zle::zle_main::BUFSTACK.lock().unwrap();
18313 assert_eq!(
18314 buf.last().map(|s| s.as_str()),
18315 Some("echo hello"),
18316 "c:4854-4856 — printf -z must push formatted output to bufstack"
18317 );
18318 }
18319
18320 /// `Src/builtin.c:5564-5565` — `print -z WORDS...` pushes the
18321 /// joined string to the ZLE bufstack instead of stdout (consumed
18322 /// by the next zleread call so the string lands at the prompt).
18323 #[test]
18324 fn bin_print_minus_z_pushes_to_bufstack() {
18325 let _g = crate::test_util::global_state_lock();
18326 let mut ops = options {
18327 ind: [0u8; MAX_OPS],
18328 args: Vec::new(),
18329 argscount: 0,
18330 argsalloc: 0,
18331 };
18332 ops.ind[b'z' as usize] = 1;
18333 crate::ported::zle::zle_main::BUFSTACK
18334 .lock()
18335 .unwrap()
18336 .clear();
18337 let r = bin_print(
18338 "print",
18339 &["echo".to_string(), "foo".to_string()],
18340 &ops,
18341 BIN_PRINT,
18342 );
18343 assert_eq!(r, 0, "c:5565 — -z should succeed");
18344 let buf = crate::ported::zle::zle_main::BUFSTACK.lock().unwrap();
18345 assert_eq!(
18346 buf.last().map(|s| s.as_str()),
18347 Some("echo foo"),
18348 "c:5565 — bufstack must have `echo foo` as the top entry"
18349 );
18350 }
18351
18352 /// `Src/builtin.c:5569-5574` — `print -s WORDS...` pushes the
18353 /// joined string to the history table instead of stdout.
18354 #[test]
18355 fn bin_print_minus_s_pushes_to_history() {
18356 let _g = crate::test_util::global_state_lock();
18357 let mut ops = options {
18358 ind: [0u8; MAX_OPS],
18359 args: Vec::new(),
18360 argscount: 0,
18361 argsalloc: 0,
18362 };
18363 ops.ind[b's' as usize] = 1;
18364 // Clear histtab to a known state.
18365 crate::ported::hashtable::histtab_lock()
18366 .write()
18367 .unwrap()
18368 .clear();
18369 let r = bin_print(
18370 "print",
18371 &["hello".to_string(), "world".to_string()],
18372 &ops,
18373 BIN_PRINT,
18374 );
18375 assert_eq!(r, 0, "c:5574 — -s should succeed");
18376 // After -s, the joined "hello world" string must appear in
18377 // histtab (the in-process history lookup table).
18378 let tab = crate::ported::hashtable::histtab_lock().read().unwrap();
18379 assert!(
18380 tab.contains_key("hello world"),
18381 "c:5574 — addhistnode must record `hello world` in histtab"
18382 );
18383 }
18384
18385 /// `Src/builtin.c:4718-4741` — `print -m PATTERN args...` keeps
18386 /// only the args matching PATTERN. Pipe-roundtrip pin: pat=`foo*`,
18387 /// args=[foo1, bar, foo2] → expect `foo1 foo2\n` (NOT `bar`).
18388 #[test]
18389 fn bin_print_minus_m_glob_filter() {
18390 let _g = crate::test_util::global_state_lock();
18391 use std::io::Read as _;
18392 let mut fds: [libc::c_int; 2] = [0, 0];
18393 assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0);
18394 let (rfd, wfd) = (fds[0], fds[1]);
18395 let mut ops = options {
18396 ind: [0u8; MAX_OPS],
18397 args: Vec::new(),
18398 argscount: 0,
18399 argsalloc: 0,
18400 };
18401 ops.ind[b'u' as usize] = 1 | (1 << 2);
18402 ops.args = vec![wfd.to_string()];
18403 ops.argscount = 1;
18404 ops.ind[b'm' as usize] = 1; // -m
18405
18406 let r = bin_print(
18407 "print",
18408 &[
18409 "foo*".to_string(),
18410 "foo1".to_string(),
18411 "bar".to_string(),
18412 "foo2".to_string(),
18413 ],
18414 &ops,
18415 BIN_PRINT,
18416 );
18417 assert_eq!(r, 0);
18418 unsafe { libc::close(wfd) };
18419
18420 let mut buf = String::new();
18421 unsafe {
18422 use std::os::unix::io::FromRawFd;
18423 let mut f = fs::File::from_raw_fd(rfd);
18424 f.read_to_string(&mut buf).unwrap();
18425 }
18426 assert_eq!(
18427 buf, "foo1 foo2\n",
18428 "c:4718-4741 — -m filters to only `foo*`-matching args"
18429 );
18430 }
18431
18432 /// `Src/builtin.c:5126-5132` — `print -N a b` separates args with
18433 /// `\0` and terminates with `\0` (not `\n`). Pipe-roundtrip pin.
18434 #[test]
18435 fn bin_print_nul_separator_with_minus_N() {
18436 let _g = crate::test_util::global_state_lock();
18437 use std::io::Read as _;
18438 let mut fds: [libc::c_int; 2] = [0, 0];
18439 assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0);
18440 let (rfd, wfd) = (fds[0], fds[1]);
18441 let mut ops = options {
18442 ind: [0u8; MAX_OPS],
18443 args: Vec::new(),
18444 argscount: 0,
18445 argsalloc: 0,
18446 };
18447 // -u <wfd>
18448 ops.ind[b'u' as usize] = 1 | (1 << 2);
18449 ops.args = vec![wfd.to_string()];
18450 ops.argscount = 1;
18451 // -N (no arg)
18452 ops.ind[b'N' as usize] = 1;
18453
18454 let r = bin_print(
18455 "print",
18456 &["a".to_string(), "b".to_string(), "c".to_string()],
18457 &ops,
18458 BIN_PRINT,
18459 );
18460 assert_eq!(r, 0);
18461 unsafe { libc::close(wfd) };
18462
18463 let mut buf = Vec::new();
18464 unsafe {
18465 use std::os::unix::io::FromRawFd;
18466 let mut f = fs::File::from_raw_fd(rfd);
18467 f.read_to_end(&mut buf).unwrap();
18468 }
18469 assert_eq!(
18470 buf, b"a\0b\0c\0",
18471 "c:5126-5132 — -N: NUL separators + NUL terminator"
18472 );
18473 }
18474
18475 /// `Src/builtin.c:4815-4847` — `print -u FD` writes to the given
18476 /// file descriptor. Pin: write to a pipe via -u and read back.
18477 #[test]
18478 fn bin_print_writes_to_specified_fd() {
18479 let _g = crate::test_util::global_state_lock();
18480 use std::io::Read as _;
18481 // Open a pipe; print -u writes to write end, we read off read
18482 // end.
18483 let mut fds: [libc::c_int; 2] = [0, 0];
18484 assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0);
18485 let (rfd, wfd) = (fds[0], fds[1]);
18486
18487 // Build options with -u set to the write fd.
18488 let mut ops = options {
18489 ind: [0u8; MAX_OPS],
18490 args: Vec::new(),
18491 argscount: 0,
18492 argsalloc: 0,
18493 };
18494 ops.ind[b'u' as usize] = 1;
18495 ops.args = vec![wfd.to_string()];
18496 // OPT_ARG looks up via OPT_HASARG/argscount; the exact wiring
18497 // depends on the parseopts pre-call path. Use a minimal stub
18498 // so OPT_ARG('u') returns wfd's string.
18499 ops.argscount = 1;
18500
18501 // The OPT_ARG indexing path requires `ops.ind[b'u']` to encode
18502 // both the "is set" bit and an arg-index pointer. The default
18503 // parseopts wires this; for the unit test we synthesize the
18504 // wfd-as-string into args[0] AND set `ops.ind[b'u']` to point
18505 // at it via the same convention (`(ops.ind[c] >> 2) - 1`).
18506 ops.ind[b'u' as usize] = 1 | (1 << 2); // sense=1, arg_index=1 → args[0]
18507
18508 // Closing wfd in the caller after print so reader sees EOF.
18509 // We dup'd inside bin_print so closing wfd here is safe AFTER
18510 // bin_print returns.
18511 let r = bin_print("print", &["hello".to_string()], &ops, BIN_PRINT);
18512 assert_eq!(r, 0, "c:4847 — bin_print should return 0 on success");
18513 unsafe { libc::close(wfd) };
18514
18515 // Read from rfd.
18516 let mut buf = String::new();
18517 unsafe {
18518 use std::os::unix::io::FromRawFd;
18519 let mut f = fs::File::from_raw_fd(rfd);
18520 f.read_to_string(&mut buf).unwrap();
18521 }
18522 assert_eq!(buf, "hello\n", "c:4847 — write should land on -u FD");
18523 }
18524
18525 // ═══════════════════════════════════════════════════════════════════
18526 // fixdir — pure path-normalization helper (port of c:1297-1395).
18527 // Tests pin C-faithful collapsing of `.`, `..`, double slashes, and
18528 // sticky-`..` semantics for relative paths.
18529 // ═══════════════════════════════════════════════════════════════════
18530
18531 /// Empty input → empty output.
18532 #[test]
18533 fn fixdir_empty_returns_empty() {
18534 assert_eq!(fixdir(""), "");
18535 }
18536
18537 /// Root passes through.
18538 #[test]
18539 fn fixdir_root_passes_through() {
18540 assert_eq!(fixdir("/"), "/");
18541 }
18542
18543 /// `/.` → `/` (drop `.`).
18544 #[test]
18545 fn fixdir_root_dot_collapses_to_root() {
18546 assert_eq!(fixdir("/."), "/");
18547 }
18548
18549 /// `/a/./b` → `/a/b` (drop intermediate `.`).
18550 #[test]
18551 fn fixdir_strips_dot_components() {
18552 assert_eq!(fixdir("/a/./b"), "/a/b");
18553 }
18554
18555 /// `/a/b/..` → `/a` (`..` pops).
18556 #[test]
18557 fn fixdir_dot_dot_pops_previous_component() {
18558 assert_eq!(fixdir("/a/b/.."), "/a");
18559 }
18560
18561 /// `/a/b/../c` → `/a/c` (pop then append).
18562 #[test]
18563 fn fixdir_dot_dot_then_continue() {
18564 assert_eq!(fixdir("/a/b/../c"), "/a/c");
18565 }
18566
18567 /// `/..` → `/` (`..` past root silently drops).
18568 #[test]
18569 fn fixdir_dot_dot_past_root_drops() {
18570 assert_eq!(fixdir("/.."), "/");
18571 }
18572
18573 /// `/../..` → `/` (multiple `..` past root all drop).
18574 #[test]
18575 fn fixdir_multiple_dot_dot_past_root_drops() {
18576 assert_eq!(fixdir("/../.."), "/");
18577 }
18578
18579 /// `//a` → `/a` (collapse `//`).
18580 #[test]
18581 fn fixdir_collapses_double_slash() {
18582 assert_eq!(fixdir("//a"), "/a");
18583 }
18584
18585 /// `/a//b///c` → `/a/b/c` (collapse runs of slashes).
18586 #[test]
18587 fn fixdir_collapses_repeated_slashes() {
18588 assert_eq!(fixdir("/a//b///c"), "/a/b/c");
18589 }
18590
18591 // ── Relative paths ───────────────────────────────────────────────
18592 /// `a/b/c` → `a/b/c` (no change).
18593 #[test]
18594 fn fixdir_relative_no_dots_unchanged() {
18595 assert_eq!(fixdir("a/b/c"), "a/b/c");
18596 }
18597
18598 /// `a/./b` → `a/b` (drop `.`).
18599 #[test]
18600 fn fixdir_relative_drops_dot() {
18601 assert_eq!(fixdir("a/./b"), "a/b");
18602 }
18603
18604 /// `a/b/..` → `a` — `..` pops.
18605 #[test]
18606 fn fixdir_relative_dot_dot_pops() {
18607 assert_eq!(fixdir("a/b/.."), "a");
18608 }
18609
18610 /// `..` (leading) → `..` — relative path keeps leading `..`.
18611 #[test]
18612 fn fixdir_leading_dot_dot_preserved_in_relative() {
18613 assert_eq!(fixdir(".."), "..");
18614 }
18615
18616 /// `../..` (sticky `..`) — both preserved.
18617 #[test]
18618 fn fixdir_double_leading_dot_dot_both_preserved() {
18619 assert_eq!(fixdir("../.."), "../..");
18620 }
18621
18622 /// `../foo/..` → `..` (pop `foo`, leading `..` remains).
18623 #[test]
18624 fn fixdir_dot_dot_then_dir_then_dot_dot() {
18625 assert_eq!(fixdir("../foo/.."), "..");
18626 }
18627
18628 /// `.` alone → `.` (empty body → "." preserved for relative).
18629 #[test]
18630 fn fixdir_single_dot_returns_dot() {
18631 // No components, not absolute → returns "." per the c:1395 path.
18632 assert_eq!(fixdir("."), ".");
18633 }
18634
18635 /// Trailing slash dropped (output never has trailing `/`).
18636 #[test]
18637 fn fixdir_trailing_slash_dropped() {
18638 assert_eq!(fixdir("/a/b/"), "/a/b");
18639 assert_eq!(fixdir("a/b/"), "a/b");
18640 }
18641
18642 // ═══════════════════════════════════════════════════════════════════
18643 // cd_able_vars — CDABLEVARS option-gated lookup. Returns Some(val/tail)
18644 // if `s` head is a set parameter AND `cdablevars` is on. Else None.
18645 // ═══════════════════════════════════════════════════════════════════
18646
18647 use crate::ported::options::{opt_state_get, opt_state_set};
18648
18649 /// `cd_able_vars` returns None when CDABLEVARS option is OFF.
18650 #[test]
18651 fn cd_able_vars_returns_none_when_option_off() {
18652 let _g = crate::test_util::global_state_lock();
18653 let saved = opt_state_get("cdablevars").unwrap_or(false);
18654 opt_state_set("cdablevars", false);
18655 // Even if HOME is set, with cdablevars off cd_able_vars rejects.
18656 assert_eq!(cd_able_vars("HOME"), None);
18657 opt_state_set("cdablevars", saved);
18658 }
18659
18660 /// `cd_able_vars` looks up the named var when CDABLEVARS is ON.
18661 #[test]
18662 fn cd_able_vars_returns_value_when_option_on_and_var_set() {
18663 let _g = crate::test_util::global_state_lock();
18664 let saved_opt = opt_state_get("cdablevars").unwrap_or(false);
18665 opt_state_set("cdablevars", true);
18666 opt_state_set("exec", true);
18667 unsetparam("zshrs_cdav_proj");
18668 setsparam("zshrs_cdav_proj", "/tmp/myproject");
18669
18670 assert_eq!(
18671 cd_able_vars("zshrs_cdav_proj"),
18672 Some("/tmp/myproject".to_string())
18673 );
18674
18675 unsetparam("zshrs_cdav_proj");
18676 opt_state_set("cdablevars", saved_opt);
18677 }
18678
18679 /// With slash: head looked up, tail appended.
18680 /// `cd_able_vars("PROJ/src")` where PROJ=/home/user → "/home/user/src".
18681 #[test]
18682 fn cd_able_vars_appends_tail_after_head_substitution() {
18683 let _g = crate::test_util::global_state_lock();
18684 let saved_opt = opt_state_get("cdablevars").unwrap_or(false);
18685 opt_state_set("cdablevars", true);
18686 opt_state_set("exec", true);
18687 unsetparam("zshrs_cdav_PROJ");
18688 setsparam("zshrs_cdav_PROJ", "/home/user");
18689
18690 assert_eq!(
18691 cd_able_vars("zshrs_cdav_PROJ/src"),
18692 Some("/home/user/src".to_string())
18693 );
18694
18695 unsetparam("zshrs_cdav_PROJ");
18696 opt_state_set("cdablevars", saved_opt);
18697 }
18698
18699 /// Unknown head var → None (even with option on).
18700 #[test]
18701 fn cd_able_vars_unknown_var_returns_none() {
18702 let _g = crate::test_util::global_state_lock();
18703 let saved_opt = opt_state_get("cdablevars").unwrap_or(false);
18704 opt_state_set("cdablevars", true);
18705 unsetparam("zshrs_cdav_doesnt_exist");
18706
18707 assert_eq!(cd_able_vars("zshrs_cdav_doesnt_exist"), None);
18708
18709 opt_state_set("cdablevars", saved_opt);
18710 }
18711
18712 /// Empty head (e.g. "/some/path" starts with `/`) → None.
18713 #[test]
18714 fn cd_able_vars_empty_head_returns_none() {
18715 let _g = crate::test_util::global_state_lock();
18716 let saved_opt = opt_state_get("cdablevars").unwrap_or(false);
18717 opt_state_set("cdablevars", true);
18718 // Leading `/` → head split is empty.
18719 assert_eq!(cd_able_vars("/path/to/foo"), None);
18720 opt_state_set("cdablevars", saved_opt);
18721 }
18722
18723 // ─── zsh-corpus pins for fixed-return builtins ──────────────────
18724
18725 fn empty_opts_for_corpus() -> options {
18726 options {
18727 ind: [0u8; MAX_OPS],
18728 args: Vec::new(),
18729 argscount: 0,
18730 argsalloc: 0,
18731 }
18732 }
18733
18734 /// `Src/builtin.c:4550` — `bin_true` always returns 0 regardless of
18735 /// argv / opts / func. Matches `:` / `true` semantics.
18736 #[test]
18737 fn builtin_corpus_bin_true_always_zero() {
18738 let _g = crate::test_util::global_state_lock();
18739 let o = empty_opts_for_corpus();
18740 assert_eq!(bin_true("true", &[], &o, 0), 0, "bin_true no args = 0");
18741 assert_eq!(
18742 bin_true("true", &["x".into(), "y".into()], &o, 0),
18743 0,
18744 "bin_true with args = 0",
18745 );
18746 }
18747
18748 /// `Src/builtin.c:4559` — `bin_false` always returns 1.
18749 #[test]
18750 fn builtin_corpus_bin_false_always_one() {
18751 let _g = crate::test_util::global_state_lock();
18752 let o = empty_opts_for_corpus();
18753 assert_eq!(bin_false("false", &[], &o, 0), 1, "bin_false no args = 1");
18754 assert_eq!(
18755 bin_false("false", &["any".into()], &o, 0),
18756 1,
18757 "bin_false with args = 1",
18758 );
18759 }
18760
18761 /// `Src/builtin.c` — `bin_shift` with no positional params and no
18762 /// argv → 0 (zsh's POSIX-conforming "shift past end" semantics
18763 /// when no $@/argv to shift, plus no count argument).
18764 #[test]
18765 fn builtin_corpus_bin_shift_empty_positional_returns_zero_or_one() {
18766 let _g = crate::test_util::global_state_lock();
18767 let o = empty_opts_for_corpus();
18768 // No positional params, no arg → either zero (no-op) or one
18769 // (POSIX error). Both are acceptable per the spec language —
18770 // we pin only that it doesn't panic and returns 0 or 1.
18771 let r = bin_shift("shift", &[], &o, 0);
18772 assert!(r == 0 || r == 1, "shift on empty positional, got {r}");
18773 }
18774
18775 /// `Src/builtin.c` — `bin_let` with no math arg returns 1
18776 /// (no expression to evaluate).
18777 #[test]
18778 fn builtin_corpus_bin_let_no_args_returns_one() {
18779 let _g = crate::test_util::global_state_lock();
18780 let o = empty_opts_for_corpus();
18781 let r = bin_let("let", &[], &o, 0);
18782 assert_eq!(r, 1, "let with no args = 1");
18783 }
18784
18785 /// `bin_let "x=5"` evaluates math and assigns; success returns 0
18786 /// since the result (5) is non-zero. let returns 0 iff last expr
18787 /// was non-zero.
18788 #[test]
18789 fn builtin_corpus_bin_let_nonzero_expr_returns_zero() {
18790 let _g = crate::test_util::global_state_lock();
18791 let o = empty_opts_for_corpus();
18792 unsetparam("ZL_X");
18793 let r = bin_let("let", &["ZL_X=5".into()], &o, 0);
18794 assert_eq!(r, 0, "let 'x=5' assigns and returns 0 (nonzero result)");
18795 assert_eq!(getiparam("ZL_X"), 5);
18796 unsetparam("ZL_X");
18797 }
18798
18799 /// `bin_let "x=0"` returns 1 since last expression evaluates to 0.
18800 #[test]
18801 fn builtin_corpus_bin_let_zero_expr_returns_one() {
18802 let _g = crate::test_util::global_state_lock();
18803 let o = empty_opts_for_corpus();
18804 unsetparam("ZL_Y");
18805 let r = bin_let("let", &["ZL_Y=0".into()], &o, 0);
18806 assert_eq!(r, 1, "let 'x=0' returns 1 (zero result)");
18807 assert_eq!(getiparam("ZL_Y"), 0);
18808 unsetparam("ZL_Y");
18809 }
18810
18811 /// `bin_let` walks multiple expressions, exit status from last.
18812 #[test]
18813 fn builtin_corpus_bin_let_multi_expr_last_wins() {
18814 let _g = crate::test_util::global_state_lock();
18815 let o = empty_opts_for_corpus();
18816 unsetparam("ZL_A");
18817 unsetparam("ZL_B");
18818 let r = bin_let("let", &["ZL_A=1".into(), "ZL_B=7".into()], &o, 0);
18819 assert_eq!(r, 0, "last expr non-zero → 0");
18820 assert_eq!(getiparam("ZL_A"), 1);
18821 assert_eq!(getiparam("ZL_B"), 7);
18822 unsetparam("ZL_A");
18823 unsetparam("ZL_B");
18824 }
18825
18826 /// `bin_pwd` returns 0 on success — even without -P/-L, it should
18827 /// produce the current dir and return zero.
18828 #[test]
18829 fn builtin_corpus_bin_pwd_returns_zero() {
18830 let _g = crate::test_util::global_state_lock();
18831 let o = empty_opts_for_corpus();
18832 let r = bin_pwd("pwd", &[], &o, 0);
18833 assert_eq!(r, 0, "pwd returns 0 on success");
18834 }
18835
18836 // ═══════════════════════════════════════════════════════════════════
18837 // Additional C-parity tests for Src/builtin.c fixdir path normaliser.
18838 // ═══════════════════════════════════════════════════════════════════
18839
18840 /// c:1297 — `fixdir("")` returns empty string.
18841 #[test]
18842 fn fixdir_empty_returns_empty_pin() {
18843 let _g = crate::test_util::global_state_lock();
18844 assert_eq!(fixdir(""), "");
18845 }
18846
18847 /// c:1297 — `fixdir("/")` returns "/".
18848 #[test]
18849 fn fixdir_root_returns_root() {
18850 let _g = crate::test_util::global_state_lock();
18851 assert_eq!(fixdir("/"), "/", "root path preserved");
18852 }
18853
18854 /// c:1352 — `fixdir("/foo/./bar")` collapses `.` to `/foo/bar`.
18855 #[test]
18856 fn fixdir_drops_dot_segments() {
18857 let _g = crate::test_util::global_state_lock();
18858 assert_eq!(fixdir("/foo/./bar"), "/foo/bar");
18859 assert_eq!(fixdir("/./a"), "/a");
18860 assert_eq!(fixdir("/a/."), "/a");
18861 }
18862
18863 /// c:1339 — `fixdir("/foo//bar")` collapses `//` to `/foo/bar`.
18864 #[test]
18865 fn fixdir_collapses_double_slash_pin() {
18866 let _g = crate::test_util::global_state_lock();
18867 assert_eq!(fixdir("/foo//bar"), "/foo/bar");
18868 assert_eq!(fixdir("//foo//bar//"), "/foo/bar");
18869 }
18870
18871 /// c:1358-1372 — `fixdir("/foo/bar/..")` pops via `..` → `/foo`.
18872 #[test]
18873 fn fixdir_pops_via_dotdot() {
18874 let _g = crate::test_util::global_state_lock();
18875 assert_eq!(fixdir("/foo/bar/.."), "/foo");
18876 assert_eq!(fixdir("/a/b/c/../.."), "/a");
18877 assert_eq!(fixdir("/a/b/../c"), "/a/c");
18878 }
18879
18880 /// c:1358-1372 — `fixdir("/..")` past root → `/` (absolute paths
18881 /// silently drop `..` past `/`).
18882 #[test]
18883 fn fixdir_dotdot_past_root_clamps_to_root_pin() {
18884 let _g = crate::test_util::global_state_lock();
18885 assert_eq!(fixdir("/.."), "/", "absolute `..` past root → `/`");
18886 assert_eq!(fixdir("/../.."), "/");
18887 assert_eq!(fixdir("/a/../../.."), "/", "successive pops past root → /");
18888 }
18889
18890 /// c:1358-1372 — `fixdir("../foo")` on relative path KEEPS leading `..`.
18891 #[test]
18892 fn fixdir_relative_keeps_leading_dotdot_pin() {
18893 let _g = crate::test_util::global_state_lock();
18894 assert_eq!(fixdir("../foo"), "../foo");
18895 assert_eq!(fixdir("../../foo"), "../../foo");
18896 }
18897
18898 /// c:1297-1395 — `fixdir` is idempotent: fixdir(fixdir(x)) == fixdir(x).
18899 #[test]
18900 fn fixdir_is_idempotent() {
18901 let _g = crate::test_util::global_state_lock();
18902 for input in &[
18903 "/a/b/c",
18904 "/a/./b",
18905 "/a/b/..",
18906 "/../x",
18907 "../foo",
18908 "/foo//bar",
18909 ] {
18910 let once = fixdir(input);
18911 let twice = fixdir(&once);
18912 assert_eq!(once, twice, "fixdir must be idempotent on {:?}", input);
18913 }
18914 }
18915
18916 /// c:1297 — relative path of just `.` → `.` (not empty).
18917 #[test]
18918 fn fixdir_relative_dot_returns_dot() {
18919 let _g = crate::test_util::global_state_lock();
18920 assert_eq!(fixdir("."), ".", "lone `.` relative → `.`");
18921 }
18922
18923 /// c:1297 — `fixdir("foo")` (plain relative) is identity.
18924 #[test]
18925 fn fixdir_plain_relative_is_identity() {
18926 let _g = crate::test_util::global_state_lock();
18927 assert_eq!(fixdir("foo"), "foo");
18928 assert_eq!(fixdir("foo/bar"), "foo/bar");
18929 }
18930
18931 // ═══════════════════════════════════════════════════════════════════
18932 // Additional C-parity tests for Src/builtin.c bin_true/false +
18933 // bin_break + other simple builtins.
18934 // ═══════════════════════════════════════════════════════════════════
18935
18936 /// c:4550 — `bin_true` always returns 0 regardless of args.
18937 #[test]
18938 fn bin_true_always_returns_zero() {
18939 let _g = crate::test_util::global_state_lock();
18940 let ops = empty_opts_for_corpus();
18941 assert_eq!(bin_true("true", &[], &ops, 0), 0);
18942 assert_eq!(
18943 bin_true("true", &["arg1".into(), "arg2".into()], &ops, 0),
18944 0
18945 );
18946 }
18947
18948 /// c:4559 — `bin_false` always returns 1 regardless of args.
18949 #[test]
18950 fn bin_false_always_returns_one() {
18951 let _g = crate::test_util::global_state_lock();
18952 let ops = empty_opts_for_corpus();
18953 assert_eq!(bin_false("false", &[], &ops, 0), 1);
18954 assert_eq!(bin_false("false", &["a".into()], &ops, 0), 1);
18955 }
18956
18957 /// c:4550 — `bin_true` ignores name arg.
18958 #[test]
18959 fn bin_true_ignores_name() {
18960 let _g = crate::test_util::global_state_lock();
18961 let ops = empty_opts_for_corpus();
18962 // Any name returns 0 (name is unused per UNUSED(char *name)).
18963 assert_eq!(bin_true("anything", &[], &ops, 0), 0);
18964 assert_eq!(bin_true("", &[], &ops, 0), 0);
18965 }
18966
18967 /// c:4559 — `bin_false` ignores name arg.
18968 #[test]
18969 fn bin_false_ignores_name() {
18970 let _g = crate::test_util::global_state_lock();
18971 let ops = empty_opts_for_corpus();
18972 assert_eq!(bin_false("anything", &[], &ops, 0), 1);
18973 assert_eq!(bin_false("", &[], &ops, 0), 1);
18974 }
18975
18976 /// `bin_true`/`bin_false` are deterministic (pure functions).
18977 #[test]
18978 fn bin_true_false_deterministic() {
18979 let _g = crate::test_util::global_state_lock();
18980 let ops = empty_opts_for_corpus();
18981 for _ in 0..10 {
18982 assert_eq!(bin_true("true", &[], &ops, 0), 0);
18983 assert_eq!(bin_false("false", &[], &ops, 0), 1);
18984 }
18985 }
18986
18987 /// c:7872 — `realexit` would terminate process; pin that we DON'T
18988 /// accidentally call it. (no-op test — just documents the contract.)
18989 /// `_realexit` similarly forbidden in tests.
18990 #[test]
18991 fn realexit_contract_pin_no_test_actually_calls_it() {
18992 // This test exists to document that realexit / _realexit are
18993 // never called by test code (would terminate the test process).
18994 // No assertions — pin via not calling.
18995 }
18996
18997 // ═══════════════════════════════════════════════════════════════════
18998 // Additional C-parity tests for Src/builtin.c
18999 // c:131 createbuiltintable / c:232 init_builtins / c:1837 cd_able_vars
19000 // c:1999 fixdir / c:2490 fcgetcomm / c:2526 fcsubs / c:209 freebuiltinnode
19001 // ═══════════════════════════════════════════════════════════════════
19002
19003 /// c:131 — `createbuiltintable` returns &HashMap (compile-time pin).
19004 #[test]
19005 fn createbuiltintable_returns_hashmap_ref_type() {
19006 let _: &HashMap<String, &builtin> = createbuiltintable();
19007 }
19008
19009 /// c:131 — `createbuiltintable` is idempotent (returns same ref).
19010 #[test]
19011 fn createbuiltintable_returns_same_ref() {
19012 let a = createbuiltintable() as *const _;
19013 let b = createbuiltintable() as *const _;
19014 assert_eq!(a, b, "createbuiltintable must return same singleton");
19015 }
19016
19017 /// c:131 — table contains common shell builtins (sanity sweep).
19018 #[test]
19019 fn createbuiltintable_contains_canonical_builtins() {
19020 let t = createbuiltintable();
19021 for name in ["true", "false", "set", "cd", "exit", "echo"] {
19022 assert!(
19023 t.contains_key(name),
19024 "builtin table must contain {:?}",
19025 name
19026 );
19027 }
19028 }
19029
19030 /// c:232 — `init_builtins` is idempotent.
19031 #[test]
19032 fn init_builtins_idempotent() {
19033 let _g = crate::test_util::global_state_lock();
19034 for _ in 0..5 {
19035 init_builtins();
19036 }
19037 }
19038
19039 /// c:1837 — `cd_able_vars("")` empty returns None.
19040 #[test]
19041 fn cd_able_vars_empty_returns_none() {
19042 let _g = crate::test_util::global_state_lock();
19043 assert!(cd_able_vars("").is_none(), "empty → None");
19044 }
19045
19046 /// c:1837 — `cd_able_vars` returns Option<String> (type pin).
19047 #[test]
19048 fn cd_able_vars_returns_option_string_type() {
19049 let _g = crate::test_util::global_state_lock();
19050 let _: Option<String> = cd_able_vars("anything");
19051 }
19052
19053 /// c:1999 — `fixdir("")` empty returns String (type pin).
19054 #[test]
19055 fn fixdir_empty_returns_string_type() {
19056 let _: String = fixdir("");
19057 }
19058
19059 /// c:1999 — `fixdir` is pure.
19060 #[test]
19061 fn fixdir_is_pure() {
19062 for s in ["", "/abs", "rel", "./dot", "../parent", "a/b/c"] {
19063 let first = fixdir(s);
19064 for _ in 0..3 {
19065 assert_eq!(fixdir(s), first, "fixdir({:?}) must be pure", s);
19066 }
19067 }
19068 }
19069
19070 /// c:2490 — `fcgetcomm("")` empty returns i64 (type pin).
19071 #[test]
19072 fn fcgetcomm_empty_returns_i64_type() {
19073 let _g = crate::test_util::global_state_lock();
19074 let _: i64 = fcgetcomm("");
19075 }
19076
19077 /// c:2490 — `fcgetcomm` is deterministic for invalid input.
19078 #[test]
19079 fn fcgetcomm_invalid_is_deterministic() {
19080 let _g = crate::test_util::global_state_lock();
19081 for s in ["", "garbage", "not_a_number"] {
19082 let first = fcgetcomm(s);
19083 for _ in 0..3 {
19084 assert_eq!(
19085 fcgetcomm(s),
19086 first,
19087 "fcgetcomm({:?}) must be deterministic",
19088 s
19089 );
19090 }
19091 }
19092 }
19093
19094 /// c:2526 — `fcsubs(&mut s, &[])` empty sub list is no-op.
19095 #[test]
19096 fn fcsubs_empty_subs_returns_zero() {
19097 let mut s = "hello".to_string();
19098 let r = fcsubs(&mut s, &[]);
19099 assert_eq!(r, 0, "empty subs → 0 (no substitutions)");
19100 assert_eq!(s, "hello", "string unchanged");
19101 }
19102
19103 // ═══════════════════════════════════════════════════════════════════
19104 // Additional C-parity tests for Src/builtin.c
19105 // c:1324 bin_pwd / c:6810 bin_true / c:6823 bin_false / c:7247 bin_shift /
19106 // c:8444 bin_eval / c:8922 bin_test / c:9012 bin_times / c:9207 bin_ttyctl /
19107 // c:9295 bin_umask / c:9240 bin_let — type pins + edge cases
19108 // ═══════════════════════════════════════════════════════════════════
19109
19110 /// c:1324 — `bin_pwd` returns i32 (compile-time type pin).
19111 #[test]
19112 fn bin_pwd_returns_i32_type() {
19113 let _g = crate::test_util::global_state_lock();
19114 let o = empty_opts_for_corpus();
19115 let _: i32 = bin_pwd("pwd", &[], &o, 0);
19116 }
19117
19118 /// c:6810 — `bin_true` returns i32.
19119 #[test]
19120 fn bin_true_returns_i32_type() {
19121 let _g = crate::test_util::global_state_lock();
19122 let o = empty_opts_for_corpus();
19123 let _: i32 = bin_true("true", &[], &o, 0);
19124 }
19125
19126 /// c:6823 — `bin_false` returns i32.
19127 #[test]
19128 fn bin_false_returns_i32_type() {
19129 let _g = crate::test_util::global_state_lock();
19130 let o = empty_opts_for_corpus();
19131 let _: i32 = bin_false("false", &[], &o, 0);
19132 }
19133
19134 /// c:9240 — `bin_let` returns i32.
19135 #[test]
19136 fn bin_let_returns_i32_type() {
19137 let _g = crate::test_util::global_state_lock();
19138 let o = empty_opts_for_corpus();
19139 let _: i32 = bin_let("let", &[], &o, 0);
19140 }
19141
19142 /// c:7247 — `bin_shift` returns i32.
19143 #[test]
19144 fn bin_shift_returns_i32_type() {
19145 let _g = crate::test_util::global_state_lock();
19146 let o = empty_opts_for_corpus();
19147 let _: i32 = bin_shift("shift", &[], &o, 0);
19148 }
19149
19150 /// c:6810 + c:6823 — `bin_true` and `bin_false` are pure (always
19151 /// same value regardless of state).
19152 #[test]
19153 fn bin_true_false_are_pure() {
19154 let _g = crate::test_util::global_state_lock();
19155 let o = empty_opts_for_corpus();
19156 for _ in 0..5 {
19157 assert_eq!(bin_true("true", &[], &o, 0), 0);
19158 assert_eq!(bin_false("false", &[], &o, 0), 1);
19159 }
19160 }
19161
19162 /// c:1324 — `bin_pwd` exit code in u8 range.
19163 #[test]
19164 fn bin_pwd_return_in_u8_range() {
19165 let _g = crate::test_util::global_state_lock();
19166 let o = empty_opts_for_corpus();
19167 let r = bin_pwd("pwd", &[], &o, 0);
19168 assert!(
19169 (0..256).contains(&r),
19170 "bin_pwd exit code must fit u8, got {}",
19171 r
19172 );
19173 }
19174
19175 /// c:7247 — `bin_shift` with no args + empty positional returns
19176 /// in 0/1 range (POSIX-conforming).
19177 #[test]
19178 fn bin_shift_no_args_in_zero_one_range() {
19179 let _g = crate::test_util::global_state_lock();
19180 let o = empty_opts_for_corpus();
19181 let r = bin_shift("shift", &[], &o, 0);
19182 assert!(r == 0 || r == 1, "shift no args ∈ {{0,1}}, got {}", r);
19183 }
19184
19185 /// c:9240 — `bin_let` is deterministic for no-args.
19186 #[test]
19187 fn bin_let_no_args_is_deterministic() {
19188 let _g = crate::test_util::global_state_lock();
19189 let o = empty_opts_for_corpus();
19190 let first = bin_let("let", &[], &o, 0);
19191 for _ in 0..3 {
19192 assert_eq!(
19193 bin_let("let", &[], &o, 0),
19194 first,
19195 "bin_let no-args must be deterministic"
19196 );
19197 }
19198 }
19199
19200 /// c:8444 — `bin_eval(&[])` empty args returns i32 (type pin).
19201 #[test]
19202 fn bin_eval_returns_i32_type() {
19203 let _g = crate::test_util::global_state_lock();
19204 let o = empty_opts_for_corpus();
19205 let _: i32 = bin_eval("eval", &[], &o, 0);
19206 }
19207
19208 /// c:6810 — `bin_true` ignores everything (args, opts, func).
19209 #[test]
19210 fn bin_true_full_argument_immunity() {
19211 let _g = crate::test_util::global_state_lock();
19212 let o = empty_opts_for_corpus();
19213 // Various permutations all return 0.
19214 assert_eq!(bin_true("true", &[], &o, 0), 0);
19215 assert_eq!(bin_true("anything", &[], &o, 0), 0);
19216 assert_eq!(bin_true("true", &["unused".into()], &o, 99), 0);
19217 assert_eq!(
19218 bin_true("", &["a".into(), "b".into(), "c".into()], &o, -1),
19219 0
19220 );
19221 }
19222
19223 /// c:6823 — `bin_false` ignores everything (args, opts, func).
19224 #[test]
19225 fn bin_false_full_argument_immunity() {
19226 let _g = crate::test_util::global_state_lock();
19227 let o = empty_opts_for_corpus();
19228 assert_eq!(bin_false("false", &[], &o, 0), 1);
19229 assert_eq!(bin_false("anything", &[], &o, 0), 1);
19230 assert_eq!(bin_false("false", &["unused".into()], &o, 99), 1);
19231 assert_eq!(
19232 bin_false("", &["a".into(), "b".into(), "c".into()], &o, -1),
19233 1
19234 );
19235 }
19236}