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, interact, islogin, isset, mathfunc, nameddir,
80 options, param, shfunc, HandlerFunc, Meta, ALIAS_GLOBAL, ALIAS_SUFFIX, ASG_ARRAY, ASG_ARRAYP,
81 ASG_KEY_VALUE, ASG_VALUEP, AUTOPUSHD, BINF_ADDED, BINF_ASSIGN, BINF_BUILTIN, BINF_COMMAND,
82 BINF_DASH, BINF_DASHDASHVALID, BINF_EXEC, BINF_HANDLES_OPTS, BINF_KEEPNUM, BINF_MAGICEQUALS,
83 BINF_NOGLOB, BINF_PLUSOPTS, BINF_PREFIX, BINF_PRINTOPTS, BINF_PSPECIAL, BINF_SKIPDASH,
84 BINF_SKIPINVALID, BSDECHO, CDABLEVARS, CHASELINKS, CHECKJOBS, CHECKRUNNINGJOBS, DISABLED, EMULATE_CSH,
85 EMULATE_KSH, EMULATE_SH, EMULATE_ZSH, EMULATION, ERRFLAG_ERROR, FS_FUNC, FUNCTIONARGZERO,
86 GLOBALEXPORT, GLOBALRCS, HASHED, HFILE_APPEND, HFILE_NO_REWRITE, HFILE_SKIPOLD,
87 HFILE_USE_OPTIONS, HIST_FOREIGN, INTERACTIVE, KSHARRAYS, LOGINSHELL, MAX_OPS, MFF_STR,
88 MFF_USERFUNC, MONITOR, NULLBINCMD, OPT_ARG, OPT_HASARG, OPT_ISSET, OPT_MINUS, OPT_PLUS,
89 PATHDIRS, PAT_HEAPDUP, PAT_STATIC, PM_ABSPATH_USED, PM_ARRAY, PM_AUTOLOAD, PM_CUR_FPATH,
90 PM_DECLARED, PM_DEFAULTED, PM_EFLOAT, PM_EXPORTED, PM_FFLOAT, PM_HASHED, PM_HIDE, PM_HIDEVAL,
91 PM_INTEGER, PM_KSHSTORED, PM_LEFT, PM_LOADDIR, PM_LOCAL, PM_LOWER, PM_NAMEREF, PM_READONLY,
92 PM_RIGHT_B, PM_RIGHT_Z, PM_RO_BY_DESIGN, PM_SCALAR, PM_SPECIAL, PM_TAGGED, PM_TAGGED_LOCAL,
93 PM_TIED, PM_TYPE, PM_UNALIASED, PM_UNDEFINED, PM_UNIQUE, PM_UNSET, PM_UPPER, PM_WARNNESTED,
94 PM_ZSHSTORED, POSIXBUILTINS, POSIXCD, POSIXTRAPS, PRINT_INCLUDEVALUE, PRINT_LINE, PRINT_LIST,
95 PRINT_NAMEONLY, PRINT_POSIX_EXPORT, PRINT_POSIX_READONLY, PRINT_TYPE, PRINT_TYPESET,
96 PRINT_WHENCE_CSH, PRINT_WHENCE_FUNCDEF, PRINT_WHENCE_SIMPLE, PRINT_WHENCE_VERBOSE,
97 PRINT_WHENCE_WORD, PRINT_WITH_NAMESPACE, PUSHDIGNOREDUPS, PUSHDMINUS, PUSHDSILENT, PUSHDTOHOME,
98 RCQUOTES, RCS, SHINSTDIN, SORTIT_BACKWARDS, SORTIT_IGNORING_CASE, STAT_LOCKED, STAT_NOPRINT,
99 STAT_STOPPED, TRAP_STATE_FORCE_RETURN, TRAP_STATE_PRIMED, TYPESETSILENT, TYPESET_OPTSTR,
100 VERBOSE, XTRACE, ZEXIT_DEFERRED, ZEXIT_NORMAL, ZEXIT_SIGNAL, ZSIG_FUNC,
101};
102#[allow(unused_imports)]
103use crate::zwc::ZwcFile;
104
105// ---------------------------------------------------------------------------
106// BIN_* dispatch IDs.
107// Direct port of `Src/hashtable.h:34-70`. These are the integer
108// discriminators handlers use when one C function backs multiple
109// builtin names (e.g. `bin_fg` covers fg/bg/jobs/wait/disown).
110// ---------------------------------------------------------------------------
111
112// BIN_* constants moved to `crate::ported::hashtable_h` per the C
113// header layout (Src/hashtable.h:34-70). Re-exported here so existing
114// `crate::ported::builtin::BIN_X` paths keep resolving.
115pub use crate::ported::hashtable_h::{
116 BIN_BG, BIN_BRACKET, BIN_BREAK, BIN_CD, BIN_COMMAND, BIN_CONTINUE, BIN_DISABLE, BIN_DISOWN,
117 BIN_ECHO, BIN_ENABLE, BIN_EVAL, BIN_EXIT, BIN_EXPORT, BIN_FC, BIN_FG, BIN_JOBS, BIN_LOGOUT,
118 BIN_POPD, BIN_PRINT, BIN_PRINTF, BIN_PUSHD, BIN_PUSHLINE, BIN_R, BIN_READONLY, BIN_RETURN,
119 BIN_SCHED, BIN_SETOPT, BIN_TEST, BIN_TYPESET, BIN_UNALIAS, BIN_UNFUNCTION, BIN_UNHASH,
120 BIN_UNSET, BIN_UNSETOPT, BIN_WAIT,
121};
122
123/// Port of `mod_export int ineval;` from `Src/builtin.c:6389`.
124///
125/// `eval()` sets it at c:6155 — `ineval = !isset(EVALLINENO);` — and
126/// restores the saved value at c:6215. `Src/exec.c` reads it at three
127/// places to decide whether the wordcode's line number may overwrite
128/// the `lineno` global:
129/// c:1355 `if (!IN_EVAL_TRAP() && !ineval && code) lineno = code - 1;`
130/// c:1451 `if (!IN_EVAL_TRAP() && !ineval) { ... }`
131/// c:2056 `if (!IN_EVAL_TRAP() && !ineval && WC_PIPE_LINENO(pcode))`
132/// So under `NO_EVAL_LINENO` the eval body keeps the CALLER's
133/// `$LINENO` instead of counting lines inside the eval'd string.
134#[allow(non_upper_case_globals)]
135pub static ineval: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
136
137/// Construct the builtin lookup table.
138/// Port of `createbuiltintable()` from `Src/builtin.c:150`. The C
139/// version installs the hashtable function pointers (hash, addnode,
140/// printnode, etc.) and then calls `addbuiltins("zsh", builtins, ..)`.
141/// Here we just materialise the static `BUILTINS` slice into a
142/// `HashMap<String, &builtin>` — Rust's standard hashing replaces the
143/// C `hasher` callback and the `HashMap` itself replaces all the
144/// per-table function pointers (`addnode`/`getnode`/`removenode`/...).
145// Builtin Command Hash Table Functions // c:150
146/// `createbuiltintable` — see implementation.
147pub fn createbuiltintable() -> &'static HashMap<String, &'static builtin> {
148 // c:150
149 builtintab.get_or_init(|| {
150 let table: &'static Vec<builtin> = &*BUILTINS;
151 let watch_bintab: &'static Vec<builtin> = &*crate::ported::modules::watch::bintab;
152 let mut m: HashMap<String, &'static builtin> =
153 HashMap::with_capacity(table.len() + watch_bintab.len());
154 for b in table.iter() {
155 m.insert(b.node.nam.clone(), b);
156 }
157 // zshrs auto-loads all modules at startup. Fold each module's
158 // bintab into the core builtintab so `disable <name>` (and
159 // dispatch generally) finds module-provided builtins without
160 // an explicit `zmodload` step. Mirrors C's `addbuiltins(name,
161 // bintab, sizeof(bintab)/sizeof(*bintab))` call from each
162 // module's `boot_` hook (e.g. `Src/Modules/watch.c:694`).
163 for b in watch_bintab.iter() {
164 m.insert(b.node.nam.clone(), b);
165 }
166 // zshrs extension: fold `znative` (the plugin package manager,
167 // src/extensions/pkg) into the builtin table so it is a first-class
168 // builtin — `whence -w znative` reports `builtin`, `builtin znative` works,
169 // and it dispatches through execbuiltin like any other. No C
170 // counterpart (znative is zshrs-original).
171 let pkg_bintab: &'static Vec<builtin> = &*crate::pkg::builtin::bintab;
172 for b in pkg_bintab.iter() {
173 m.insert(b.node.nam.clone(), b);
174 }
175 // zshrs extension: fold the ztest/zassert unit-test framework
176 // (src/extensions/ztest.rs) into the builtin table so `zassert_eq`,
177 // `ztest_run`, … are first-class builtins in `zshrs -f` (visible to
178 // `whence -w`, `builtin`, `disable`, completion) — not just the
179 // fusevm command-dispatch arm. No C counterpart (zshrs-original).
180 let ztest_bintab: &'static Vec<builtin> = &*crate::extensions::ztest::bintab;
181 for b in ztest_bintab.iter() {
182 m.insert(b.node.nam.clone(), b);
183 }
184 m
185 })
186}
187
188// ===========================================================
189// Direct ports of static builtin helpers from Src/builtin.c not
190// yet covered above. The Rust executor wires builtins through
191// `crate::ported::builtins::*` per-builtin modules; these free-
192// fn entries satisfy ABI/name parity for the drift gate.
193// ===========================================================
194
195/// Port of `printbuiltinnode()` from `Src/builtin.c:174`.
196/// C decl: `printbuiltinnode(HashNode hn, int printflags)`
197///
198/// C body (c:174-194):
199/// ```c
200/// Builtin bn = (Builtin) hn;
201/// if (printflags & PRINT_WHENCE_WORD) {
202/// printf("%s: builtin\n", bn->node.nam); return;
203/// }
204/// if (printflags & PRINT_WHENCE_CSH) {
205/// printf("%s: shell built-in command\n", bn->node.nam); return;
206/// }
207/// if (printflags & PRINT_WHENCE_VERBOSE) {
208/// printf("%s is a shell builtin\n", bn->node.nam); return;
209/// }
210/// /* default is name only */
211/// printf("%s\n", bn->node.nam);
212/// ```
213pub fn printbuiltinnode(
214 hn: *mut hashnode, // c:174
215 printflags: i32,
216) {
217 if hn.is_null() {
218 return;
219 }
220 let bn = unsafe { &*hn }; // c:176
221 if (printflags & PRINT_WHENCE_WORD as i32) != 0 {
222 // c:178
223 println!("{}: builtin", bn.nam); // c:179
224 return; // c:180
225 }
226 if (printflags & PRINT_WHENCE_CSH as i32) != 0 {
227 // c:183
228 println!("{}: shell built-in command", bn.nam); // c:184
229 return; // c:185
230 }
231 if (printflags & PRINT_WHENCE_VERBOSE as i32) != 0 {
232 // c:188
233 // !!! DASH-FAITHFUL GATE (no C counterpart) !!! dash words POSIX special
234 // builtins as "a special shell builtin".
235 let kind = if crate::dash_mode::dash_faithful() && crate::dash_mode::dash_special_builtin(&bn.nam) {
236 "special shell builtin"
237 } else {
238 "shell builtin"
239 };
240 println!("{} is a {}", bn.nam, kind); // c:189
241 return; // c:190
242 }
243 // c:193 — `/* default is name only */`
244 println!("{}", bn.nam); // c:194
245}
246
247/// Port of `freebuiltinnode()` from `Src/builtin.c:199`.
248/// C decl: `freebuiltinnode(HashNode hn)`
249/// C: `static void freebuiltinnode(HashNode hn)` — free a builtin-table
250/// node only when BINF_ADDED is clear (i.e., dynamically added).
251pub fn freebuiltinnode(hn: *mut hashnode) {
252 // c:199
253 if hn.is_null() {
254 return;
255 }
256 let bn = unsafe { &*hn };
257 // c:204 — `if (!(bn->node.flags & BINF_ADDED))` then free.
258 if (bn.flags as u32 & BINF_ADDED) == 0 { // c:204
259 // Rust drop handles the actual free; nothing more to do.
260 }
261}
262
263/// Port of `init_builtins()` from Src/builtin.c:212.
264/// C: `void init_builtins(void)` — when not in EMULATE_ZSH, disable
265/// the `repeat` reserved word (compat for sh/ksh).
266///
267/// ```c
268/// if (!EMULATION(EMULATE_ZSH)) {
269/// HashNode hn = reswdtab->getnode2(reswdtab, "repeat");
270/// if (hn)
271/// reswdtab->disablenode(hn, 0);
272/// }
273/// ```
274pub fn init_builtins() {
275 // c:212
276 // c:214 — `if (!EMULATION(EMULATE_ZSH))`. EMULATION reads the
277 // canonical `emulation` global directly per zsh.h:2347.
278 if !EMULATION(EMULATE_ZSH) {
279 // c:214
280 // c:215-217 — `hn = reswdtab->getnode2(reswdtab,"repeat");
281 // if (hn) reswdtab->disablenode(hn, 0);`
282 if let Ok(mut tab) = reswdtab_lock().write() {
283 tab.disable("repeat");
284 }
285 }
286}
287
288/// Port of `OPT_ALLOC_CHUNK` from `Src/builtin.c:223`. Number of
289/// `ops->args[]` slots `new_optarg()` grows the array by when full.
290pub const OPT_ALLOC_CHUNK: i32 = 16; // c:227
291
292/// Port of `new_optarg()` from `Src/builtin.c:227`.
293/// C decl: `new_optarg(Options ops)`
294/// C: `static int new_optarg(Options ops)` — grow the `ops->args[]`
295/// array by `OPT_ALLOC_CHUNK` slots when full. Returns 1 on overflow
296/// (>=63 args), 0 on success.
297pub fn new_optarg(ops: &mut options) -> i32 {
298 // c:227
299 // c:227 — `if (ops->argscount == 63) return 1;`
300 if ops.argscount == 63 {
301 // c:231
302 return 1;
303 }
304 // c:232-241 — grow ops->args by OPT_ALLOC_CHUNK if argsalloc == argscount.
305 if ops.argsalloc == ops.argscount {
306 // c:232
307 ops.args
308 .resize((ops.argsalloc + OPT_ALLOC_CHUNK) as usize, String::new());
309 ops.argsalloc += OPT_ALLOC_CHUNK; // c:240
310 }
311 ops.argscount += 1; // c:243
312 0 // c:244
313}
314
315// ===========================================================
316// ksh_autoload_body moved from src/ported/vm_helper.
317// Mirrors the ksh-style autoload helper in Src/builtin.c
318// (bin_functions / load_function_def).
319// ===========================================================
320// (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.)
321
322bitflags::bitflags! {
323 /// Flags for autoloaded functions (autoload builtin -- Src/builtin.c bin_autoload).
324 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
325 pub struct AutoloadFlags: u32 {
326 const NO_ALIAS = 0b00000001; // -U: don't expand aliases
327 const ZSH_STYLE = 0b00000010; // -z: zsh-style autoload
328 const KSH_STYLE = 0b00000100; // -k: ksh-style autoload
329 const TRACE = 0b00001000; // -t: trace execution
330 const USE_CALLER_DIR = 0b00010000; // -d: use calling function's dir
331 const LOADED = 0b00100000; // function has been loaded
332 }
333}
334
335/// Port of `execbuiltin()` from `Src/builtin.c:250`.
336/// C decl: `execbuiltin(LinkList args, LinkList assigns, Builtin bn)`
337///
338/// C: `int execbuiltin(LinkList args, LinkList assigns, Builtin bn)` —
339/// execute a builtin handler function after parsing the arguments.
340///
341/// Walks `bn->optstr` against `args`, populating `ops.ind[c]` (`|= 1`
342/// for `-X`, `|= 2` for `+X`, `<< 2` arg-index for opts taking args
343/// per the `:`/`::`/`:%` suffix convention), then calls
344/// `bn->handlerfunc(name, argv, &ops, bn->funcid)`.
345///
346/// Signature note: C consumes the name via `ugetnode(args)` first
347/// (c:262); the Rust port receives `args` without the name and reads
348/// `bn->node.nam` directly. C's `LinkList assigns` ports to
349/// `Vec<asgment>` (closer to the C type than the earlier
350/// `Vec<(String, String)>` pair-tuple). `assignfunc` handler dispatch
351/// (c:495-502) — BINF_ASSIGN builtins taking two argument lists —
352/// isn't ported (no Rust-side caller passes a non-empty `assigns`),
353/// so XTRACE prints the structure but BINF_ASSIGN dispatch falls
354/// through to the plain handler.
355pub fn execbuiltin(
356 args: Vec<String>,
357 assigns: Vec<asgment>, // c:250
358 bn: *mut builtin,
359) -> i32 {
360 if bn.is_null() {
361 return 1;
362 }
363 let bn_ref = unsafe { &*bn };
364
365 // c:252-254 — locals.
366 let pp: Option<&str>; // c:252 char *pp
367 let name: String; // c:252 char *name
368 let mut optstr: Option<String>; // c:252 char *optstr
369 let mut flags: i32; // c:253 int flags
370 let mut argc: i32; // c:253 int argc
371 let mut execop: u8; // c:253 int execop
372 let xtr: bool = isset(XTRACE); // c:253 int xtr = isset(XTRACE)
373
374 // c:256-259 — `memset(ops.ind, 0, ...); ops.args = NULL; ops.argscount=ops.argsalloc=0;`
375 let mut ops = options {
376 ind: [0u8; MAX_OPS],
377 args: Vec::new(), // c:257
378 argscount: 0,
379 argsalloc: 0,
380 }; // c:258-259
381
382 // c:262 — `name = (char *) ugetnode(args);` — Rust reads bn.node.nam.
383 name = bn_ref.node.nam.clone(); // c:262
384
385 // c:264-268 — `if (!bn->handlerfunc)` early-exit.
386 if bn_ref.handlerfunc.is_none() {
387 // c:264
388 // c:265 — DPUTS(1, "Missing builtin detected too late")
389 DPUTS!(true, "Missing builtin detected too late"); // c:265
390 crate::ported::module::deletebuiltin(&name); // c:266
391 return 1; // c:267
392 }
393
394 // c:270-271 — `flags = bn->node.flags; optstr = bn->optstr;`
395 flags = bn_ref.node.flags; // c:270
396 optstr = bn_ref.optstr.clone(); // c:271
397
398 // c:275 — `argc = countlinknodes(args);` — total argv length.
399 argc = args.len() as i32; // c:275
400
401 // c:284-293 — `VARARR(char *, argarr, argc+1)` + copy args into argarr.
402 let argarr: Vec<String> = args; // c:284 argarr[]
403 let mut argv: usize = 0; // c:285 char **argv = argarr;
404
405 // c:296-411 — option parser body.
406 if let Some(ref os) = optstr.clone() {
407 // c:296
408 let optstr_local = os.clone();
409 let mut optstr_bytes: Vec<u8> = optstr_local.into_bytes();
410 // !!! DASH-STRICT GATE (no C counterpart) !!! dash's `echo` recognises only
411 // `-n`; `-e` / `-E` (and clusters like `-ne`) are printed as text.
412 if crate::dash_mode::dash_strict() && name == "echo" {
413 optstr_bytes = b"n".to_vec();
414 }
415 // !!! DASH-FAITHFUL GATE (no C counterpart) !!! dash's narrower option
416 // sets: `read` knows only `-r` (and `-p`), `local` takes no options, and
417 // `unalias` only `-a`; everything else is "Illegal option".
418 if crate::dash_mode::dash_faithful() {
419 if let Some(o) = crate::dash_mode::dash_builtin_optstr(&name) {
420 optstr_bytes = o.to_vec();
421 }
422 }
423 // !!! BASH-ONLY (no C counterpart) !!! bash's own option letters.
424 if crate::dash_mode::bash_mode() {
425 match name.as_str() {
426 "hash" => optstr_bytes = b"rdplt".to_vec(),
427 "enable" => optstr_bytes = b"anprs".to_vec(),
428 "type" => optstr_bytes.push(b'P'),
429 // bash `history`: -c clear, -d OFFSET delete, -a/-n/-r/-w/-s/-p.
430 "history" => optstr_bytes = b"acd:nprsw".to_vec(),
431 _ => {}
432 }
433 }
434 // !!! KSH93-ONLY (no C counterpart) !!! ksh93 `print -e` is accepted (escapes
435 // are already on; `-r` turns them off).
436 if crate::dash_mode::ksh93_mode() && name == "print" {
437 optstr_bytes.push(b'e');
438 }
439 let mut skipinvalid = (flags & BINF_SKIPINVALID as i32) != 0;
440 // c:297 — `char *arg = *argv;`
441 loop {
442 // c:300-303 — outer arg-by-arg loop guard:
443 // `arg && ((sense = (*arg == '-')) || ((flags & BINF_PLUSOPTS) && *arg == '+'))`.
444 let arg_str: String = match argarr.get(argv) {
445 Some(s) => s.clone(),
446 None => break,
447 };
448 let arg_bytes = arg_str.as_bytes();
449 if arg_bytes.is_empty() {
450 break;
451 }
452 let sense: i32 = if arg_bytes[0] == b'-' { 1 } else { 0 }; // c:302
453 if sense == 0
454 && !((flags & BINF_PLUSOPTS as i32) != 0 // c:303
455 && arg_bytes[0] == b'+')
456 {
457 break;
458 }
459 // c:305 — `if (!(flags & BINF_KEEPNUM) && idigit(arg[1])) break;`
460 if (flags & BINF_KEEPNUM as i32) == 0 // c:305
461 && arg_bytes.len() >= 2
462 && arg_bytes[1].is_ascii_digit()
463 {
464 break;
465 }
466 // c:308 — `if ((flags & BINF_SKIPDASH) && !arg[1]) break;`
467 if (flags & BINF_SKIPDASH as i32) != 0 && arg_bytes.len() == 1 {
468 // c:308
469 break;
470 }
471 // !!! DASH-STRICT GATE (no C counterpart) !!! dash's `echo` takes ONE
472 // option word and only if it is exactly `-n` (`echo -nn` and
473 // `echo -n -n a` print the later words as text).
474 if crate::dash_mode::dash_strict() && name == "echo" && (argv > 0 || arg_str != "-n") {
475 break;
476 }
477 // !!! BASH/KSH-MODE (no C counterpart) !!! bash/ksh `echo` has no lone-`-`
478 // option arg: `echo -` prints `-` (zsh's c:337-341 swallows it).
479 if (crate::dash_mode::bash_mode() || crate::dash_mode::korn_mode())
480 && name == "echo"
481 && arg_bytes.len() == 1 {
482 break;
483 }
484 // c:310-317 — `--` end-of-options if BINF_DASHDASHVALID.
485 if (flags & BINF_DASHDASHVALID as i32) != 0 && arg_str == "--" {
486 // c:310
487 argv += 1; // c:315
488 break; // c:316
489 }
490 // c:327-332 — `BINF_SKIPINVALID`: if any char in arg[1..] is
491 // not in optstr, the whole arg is treated as a positional.
492 if skipinvalid {
493 // c:327
494 let mut all_known = true;
495 for &c in &arg_bytes[1..] {
496 if !optstr_bytes.contains(&c) {
497 all_known = false;
498 break;
499 }
500 }
501 if !all_known {
502 break;
503 } // c:331
504 }
505 // c:335-336 — `if (arg[1] == '-') arg++;` — consume the
506 // second `-` of `--long-style`.
507 let mut k: usize = 1; // walks arg[k..]
508 if arg_bytes.len() >= 2 && arg_bytes[1] == b'-' {
509 // c:335
510 k = 2; // c:336
511 }
512 // c:337-341 — `if (!arg[1])` lone `-` / `+` indicator.
513 if arg_bytes.len() == k {
514 // c:337
515 ops.ind[b'-' as usize] = 1; // c:338
516 if sense == 0 {
517 // c:339
518 ops.ind[b'+' as usize] = 1; // c:340
519 }
520 }
521 // c:343-386 — inner loop over `*++arg` characters.
522 let mut bad_opt: Option<u8> = None;
523 while k < arg_bytes.len() {
524 // c:343
525 let c = arg_bytes[k];
526 execop = c; // c:345
527 let optptr = optstr_bytes.iter().position(|&b| b == c); // c:345 strchr(optstr,c)
528 if let Some(optidx) = optptr {
529 // c:345
530 ops.ind[c as usize] = if sense != 0 { 1 } else { 2 }; // c:346
531 // c:347 — `if (optptr[1] == ':')` — option takes arg.
532 if optidx + 1 < optstr_bytes.len() && optstr_bytes[optidx + 1] == b':' {
533 let mut argptr: Option<String> = None;
534 // c:349-352 — `if (optptr[2] == ':')` optional same-word.
535 if optidx + 2 < optstr_bytes.len() && optstr_bytes[optidx + 2] == b':' {
536 if k + 1 < arg_bytes.len() {
537 // c:350
538 argptr =
539 Some(String::from_utf8_lossy(&arg_bytes[k + 1..]).into_owned());
540 // c:351
541 }
542 } else if optidx + 2 < optstr_bytes.len()
543 && optstr_bytes[optidx + 2] == b'%'
544 {
545 // c:353-359 — `:%` numeric optional same or next word.
546 if k + 1 < arg_bytes.len() && arg_bytes[k + 1].is_ascii_digit() {
547 argptr =
548 Some(String::from_utf8_lossy(&arg_bytes[k + 1..]).into_owned());
549 } else if let Some(nxt) = argarr.get(argv + 1) {
550 if !nxt.is_empty() && nxt.as_bytes()[0].is_ascii_digit() {
551 argv += 1; // c:359 arg = *++argv
552 argptr = Some(nxt.clone());
553 }
554 }
555 } else {
556 // c:360-370 — plain `:` mandatory arg.
557 if k + 1 < arg_bytes.len() {
558 // c:362
559 argptr =
560 Some(String::from_utf8_lossy(&arg_bytes[k + 1..]).into_owned());
561 // c:363
562 } else if let Some(nxt) = argarr.get(argv + 1) {
563 argv += 1; // c:364 arg = *++argv
564 argptr = Some(nxt.clone()); // c:365
565 } else {
566 // c:366-370 — `argument expected: -%c`.
567 zwarnnam(&name, &format!("argument expected: -{}", execop as char)); // c:367-368
568 return 1; // c:369
569 }
570 }
571 if let Some(ap) = argptr {
572 // c:372
573 // c:373-377 — new_optarg overflow.
574 if new_optarg(&mut ops) != 0 {
575 // c:373
576 zwarnnam(&name, "too many option arguments"); // c:374-375
577 return 1; // c:376
578 }
579 // c:378 — `ops.ind[execop] |= ops.argscount << 2;`
580 ops.ind[execop as usize] |= (ops.argscount as u8) << 2;
581 // c:379 — `ops.args[ops.argscount-1] = argptr;`
582 ops.args[(ops.argscount - 1) as usize] = ap;
583 // c:380-381 — `while (arg[1]) arg++;` consume the rest.
584 k = arg_bytes.len();
585 }
586 }
587 k += 1;
588 } else {
589 bad_opt = Some(c); // c:385 break
590 break;
591 }
592 }
593 // c:389-394 — if we exited mid-arg on a bad char, emit "bad option".
594 if let Some(badc) = bad_opt {
595 // c:389
596 zwarnnam(
597 &name,
598 &format!(
599 "bad option: {}{}",
600 if sense != 0 { '-' } else { '+' },
601 badc as char
602 ),
603 ); // c:392
604 // ZSHRS-ONLY. POSIX: "If there is an error in a special
605 // built-in utility ... the shell shall abort." A bad
606 // option IS such an error, and ksh93u+m, mksh and dash
607 // all exit the script on it (`readonly -X; echo AFTER`
608 // never reaches the echo). zsh and bash do not, so this
609 // is gated to the POSIX-family drop-ins. The deferred
610 // EXIT_PENDING pair is the same one the POSIXBUILTINS
611 // path in fusevm_bridge arms.
612 if (flags & BINF_PSPECIAL as i32) != 0
613 && !crate::ported::zsh_h::isset(crate::ported::zsh_h::INTERACTIVE)
614 && crate::extensions::emulation_output::special_builtin_error_aborts()
615 {
616 use std::sync::atomic::Ordering;
617 EXIT_VAL.store(
618 crate::extensions::emulation_output::special_builtin_abort_status(true, 1),
619 Ordering::Relaxed,
620 );
621 EXIT_PENDING.store(1, Ordering::Relaxed);
622 }
623 // bash reports a builtin usage error with status 2, zsh
624 // with 1 (`readonly -X` → 2 in bash 5.3, 1 in zsh 5.9).
625 // The aborting shells never reach this return; their
626 // status comes from the deferred exit above.
627 if crate::extensions::emulation_startup::personality()
628 == crate::extensions::emulation_startup::Personality::Bash
629 {
630 return 2;
631 }
632 // dash's `sh_error("Illegal option")` status is 2 for every
633 // builtin, not zsh's 1.
634 if crate::dash_mode::dash_faithful() {
635 return 2;
636 }
637 return 1; // c:393
638 }
639 // c:395 — `arg = *++argv;`
640 argv += 1; // c:395
641 // c:398-402 — BINF_PRINTOPTS R-mode switch to "ne" optstr.
642 if (flags & BINF_PRINTOPTS as i32) != 0 // c:398
643 && ops.ind[b'R' as usize] != 0
644 && ops.ind[b'f' as usize] == 0
645 {
646 optstr_bytes = b"ne".to_vec(); // c:400
647 flags |= BINF_SKIPINVALID as i32; // c:401
648 skipinvalid = true;
649 }
650 // c:404-405 — `if (ops.ind['-']) break;` — `--` terminates.
651 if ops.ind[b'-' as usize] != 0 {
652 // c:404
653 break;
654 }
655 }
656 let _ = optstr_bytes;
657 } else if (flags & BINF_HANDLES_OPTS as i32) == 0 // c:407
658 && argarr.get(argv).map(|s| s == "--").unwrap_or(false)
659 {
660 // c:408
661 // c:409-410 — `ops.ind['-'] = 1; argv++;`
662 ops.ind[b'-' as usize] = 1; // c:409
663 argv += 1; // c:410
664 }
665 // Suppress optstr-unused warnings on the `else` path.
666 let _ = optstr.take();
667
668 // c:414-421 — apply `bn->defopts` defaults.
669 pp = bn_ref.defopts.as_deref(); // c:414
670 if let Some(pp_str) = pp {
671 // c:414
672 for &b in pp_str.as_bytes() {
673 // c:415
674 if ops.ind[b as usize] == 0 {
675 // c:417
676 ops.ind[b as usize] = 1; // c:418
677 }
678 }
679 }
680
681 // c:424 — `argc -= argv - argarr;` — subtract consumed flag args.
682 argc -= argv as i32; // c:424
683
684 // c:426-429 — errflag check. C zsh clears here because the
685 // *outer* execcmd_exec (Src/exec.c:3468) already bailed without
686 // calling the builtin when errflag was set after prefork; the
687 // clear at c:427 is paired with an earlier-arrived-at builtin
688 // entry (signal-driven, not prefork-driven).
689 //
690 // zshrs's bytecode flow always reaches the builtin call site
691 // even when arg expansion set errflag (the c:3468 pre-dispatch
692 // gate has no equivalent in the bytecode). Clearing errflag
693 // here would swallow the prefork error and let the post-command
694 // ERREXIT_CHECK pass — `set -u; echo $undefined; echo done`
695 // would run `echo done` instead of aborting.
696 //
697 // The flag is preserved for EVERY shell, not only scripts. Gating the
698 // clear on INTERACTIVE made a ZLE widget / completion function the one
699 // place a prefork error was still swallowed: `: $((1/0))` inside a widget
700 // kept executing its LIST, where C ends the list at c:Src/exec.c:1443
701 // (`while (… && !breaks && !retflag && !errflag)`). Observable on the
702 // default config with no ZLE function at all:
703 //
704 // setopt nounset; print pre; print $undefzz9; print DONEZZ
705 // zsh : pre / `undefzz9: parameter not set` (stops)
706 // zshrs: pre / `undefzz9: parameter not set` / DONEZZ (continues)
707 //
708 // C's REPL does not depend on this clear: c:Src/init.c:150 and c:161 set
709 // `errflag = 0` twice per prompt iteration, and both are ported
710 // (init.rs, `// c:139` and `// c:150`), so a surviving flag cannot wedge
711 // the line editor.
712 //
713 // KNOWN, DELIBERATE DEVIATION: `eval` is itself a builtin, so an `eval`
714 // ENTERED with errflag already set now returns 1 without running its body
715 // and keeps the flag, where C returns 1 and clears it. That is the price
716 // of not having C's c:Src/exec.c:3760-3763 pre-dispatch gate
717 // (`if (errflag) { lastval = 1; goto err; }`) on the bytecode path — the
718 // same trade already accepted for scripts. The strictly faithful fix is
719 // to add that pre-gate and restore C's unconditional clear.
720 let ef = errflag.load(Relaxed);
721 if (ef & ERRFLAG_ERROR) != 0 {
722 return 1; // c:428
723 }
724
725 // !!! DASH-FAITHFUL GATE (no C counterpart) !!! dash's `unset` with no
726 // names is a successful no-op.
727 if argc == 0 && name == "unset" && crate::dash_mode::dash_faithful() {
728 return 0;
729 }
730
731 // c:432-436 — argc bounds check.
732 if argc < bn_ref.minargs // c:432
733 || (argc > bn_ref.maxargs && bn_ref.maxargs != -1)
734 {
735 zwarnnam(
736 &name, // c:433
737 if argc < bn_ref.minargs {
738 "not enough arguments"
739 } else {
740 "too many arguments"
741 },
742 ); // c:434
743 // ksh93 `let` with no expression is a usage error, status 2.
744 if argc < bn_ref.minargs && name == "let" && crate::dash_mode::ksh93_mode() {
745 return 2;
746 }
747 // !!! DASH-FAITHFUL GATE (no C counterpart) !!! dash usage errors are status 2.
748 if crate::dash_mode::dash_faithful() {
749 return 2;
750 }
751 return 1; // c:435
752 }
753
754 // c:438-494 — display execution trace information, if required.
755 if xtr {
756 // c:439
757 // c:440-441 — `char **fullargv = argarr;` — use FULL argv
758 // (including consumed option words) so XTRACE shows what the
759 // user typed, not the option-stripped tail.
760 let fullargv = &argarr; // c:441
761 // All emits below are `fprintf(xtrerr, …)` / `fputc(…, xtrerr)` in
762 // C: append to the xtrerr line buffer and flush ONCE at the
763 // trailing `\n` (c:492-493), so the builtin's trace line reaches
764 // the shared stderr fd in a single write — a forked pipeline
765 // stage never interleaves with a concurrent one.
766 use crate::fusevm_bridge::{xtrerr_flush, xtrerr_fputs};
767 printprompt4(); // c:442 — buffers PS4 into xtrerr
768 // c:443 — `fprintf(xtrerr, "%s", name);`
769 xtrerr_fputs(&name); // c:443
770 // c:444-447 — `while (*fullargv) { fputc(' ',xtrerr); quotedzputs(...); }`
771 // C zsh's parser pre-splits `name=value` args for
772 // BINF_ASSIGN-flagged builtins (export/typeset/declare/local/
773 // readonly/integer/float) into asg{name,value} nodes, which
774 // the c:448-491 branch emits as
775 // `quotedzputs(name) + '=' + quotedzputs(value)` (c:453,487-488).
776 // zshrs's compiler doesn't populate `assigns`; args arrive
777 // here as the unsplit `"name=value"` whole-strings, so the
778 // c:443-446 path quotes the entire string and produces
779 // `export 'VAR=val'` instead of `export VAR=val`. Inline the
780 // same split here — the prefix is a legal scalar/array name
781 // (ident, optionally `[subscript]`) followed by `=` or
782 // `+=`, and the suffix is the value. Detection mirrors the
783 // C parser at Src/lex.c:2169 (gettokstr ASSIGN recognition)
784 // and is faithful to what the C path would have produced
785 // from a pre-split asg node.
786 let is_assign = (bn_ref.node.flags as u32 & BINF_ASSIGN) != 0;
787 for s in fullargv {
788 // c:444
789 xtrerr_fputs(" "); // c:445 fputc(' ', xtrerr)
790 let mut emitted = false;
791 if is_assign {
792 let sbytes = s.as_bytes();
793 // Walk the ident prefix.
794 let mut i = 0usize;
795 if !sbytes.is_empty() && (sbytes[0].is_ascii_alphabetic() || sbytes[0] == b'_') {
796 i = 1;
797 while i < sbytes.len()
798 && (sbytes[i].is_ascii_alphanumeric() || sbytes[i] == b'_')
799 {
800 i += 1;
801 }
802 // Optional `[subscript]` — bracket-balanced.
803 if i < sbytes.len() && sbytes[i] == b'[' {
804 let mut depth = 1i32;
805 i += 1;
806 while i < sbytes.len() && depth > 0 {
807 match sbytes[i] {
808 b'[' => depth += 1,
809 b']' => depth -= 1,
810 _ => {}
811 }
812 i += 1;
813 }
814 if depth != 0 {
815 i = 0; // unbalanced — bail to default path
816 }
817 }
818 if i > 0 {
819 // Match `=` or `+=` separator.
820 let (sep_len, sep) =
821 if sbytes.get(i) == Some(&b'+') && sbytes.get(i + 1) == Some(&b'=') {
822 (2usize, "+=")
823 } else if sbytes.get(i) == Some(&b'=') {
824 (1usize, "=")
825 } else {
826 (0, "")
827 };
828 let val = &s[i + sep_len..];
829 if sep_len != 0
830 && val.starts_with("(\u{1f}")
831 && val.ends_with("\u{1f})")
832 && val.len() >= 3
833 {
834 // c:454-485 — an ASG_ARRAY asgment: `=(`, then
835 // ` quoted(elem)` per element, then ` )`. The
836 // compiler packs `name=( … )` into one word with
837 // REJOIN_SEP around each element (fusevm_bridge
838 // BUILTIN_TYPESET_PAREN_PACK); unpack it the way
839 // bin_typeset does.
840 let mut elems: Vec<&str> =
841 val[1..val.len() - 1].split('\u{1f}').collect();
842 if elems.first().is_some_and(|e| e.is_empty()) {
843 elems.remove(0);
844 }
845 if elems.last().is_some_and(|e| e.is_empty()) {
846 elems.pop();
847 }
848 xtrerr_fputs(&s[..i]);
849 xtrerr_fputs(sep);
850 xtrerr_fputs("("); // c:455
851 for e in elems {
852 xtrerr_fputs(" "); // c:479
853 xtrerr_fputs("edzputs(e)); // c:480-481
854 }
855 xtrerr_fputs(" )"); // c:485
856 emitted = true;
857 } else if sep_len != 0 {
858 // c:453,487-488 — emit name + sep + quoted(value).
859 xtrerr_fputs(&format!(
860 "{}{}{}",
861 &s[..i],
862 sep,
863 quotedzputs(&s[i + sep_len..])
864 ));
865 emitted = true;
866 }
867 }
868 }
869 }
870 if !emitted {
871 xtrerr_fputs("edzputs(s)); // c:446
872 }
873 }
874 // c:448-491 — `if (assigns) { for (node = firstnode(assigns); ...) }`.
875 for asg in &assigns {
876 // c:450 firstnode/incnode
877 xtrerr_fputs(" "); // c:452 fputc(' ', xtrerr)
878 xtrerr_fputs("edzputs(&asg.name)); // c:453
879 if (asg.flags & ASG_ARRAY) != 0 {
880 // c:454
881 xtrerr_fputs("=("); // c:455
882 if let Some(ref list) = asg.array {
883 // c:456
884 if (asg.flags & ASG_KEY_VALUE) != 0 {
885 // c:457
886 // c:458-473 — `LinkNode keynode, valnode;` walk
887 // alternating key/value pairs, emitting
888 // `[key]=value` per pair. Uses the typed
889 // `LinkList<String>` accessors from
890 // `src/ported/linklist.rs` which port the
891 // `firstnode` / `nextnode` / `getdata` macros
892 // from `Src/zsh.h:576-588`.
893 let mut keynode = list.firstnode(); // c:459
894 loop {
895 // c:460
896 // c:461-462 — `if (!keynode) break;`
897 let kidx = match keynode {
898 // c:461
899 Some(i) => i,
900 None => break, // c:462
901 };
902 // c:463-465 — `valnode = nextnode(keynode); if (!valnode) break;`
903 let vidx = match list.nextnode(kidx) {
904 // c:463
905 Some(i) => i,
906 None => break, // c:465
907 };
908 // c:466-468 — `fputc('['); quotedzputs(getdata(keynode));`
909 xtrerr_fputs("["); // c:466
910 if let Some(k) = list.getdata(kidx) {
911 // c:467 getdata
912 xtrerr_fputs("edzputs(k));
913 // c:467
914 }
915 // c:469 — `fprintf(stderr, "]=");`
916 xtrerr_fputs("]="); // c:469
917 // c:470-471 — `quotedzputs(getdata(valnode));`
918 if let Some(v) = list.getdata(vidx) {
919 // c:470
920 xtrerr_fputs("edzputs(v));
921 // c:470
922 }
923 // c:472 — `keynode = nextnode(valnode);`
924 keynode = list.nextnode(vidx); // c:472
925 }
926 } else {
927 // c:474
928 // c:475-482 — plain array emit: walk every node
929 // and emit ` <quotedzputs(elem)>`.
930 let mut arrnode = list.firstnode(); // c:476
931 while let Some(idx) = arrnode {
932 // c:477
933 xtrerr_fputs(" "); // c:479 fputc(' ', xtrerr)
934 if let Some(elem) = list.getdata(idx) {
935 // c:480 getdata
936 xtrerr_fputs("edzputs(elem));
937 // c:480
938 }
939 arrnode = list.nextnode(idx); // c:478 incnode
940 }
941 }
942 }
943 xtrerr_fputs(" )"); // c:485
944 } else if let Some(ref scalar) = asg.scalar {
945 // c:486
946 xtrerr_fputs("="); // c:487 fputc('=', xtrerr)
947 xtrerr_fputs("edzputs(scalar)); // c:488
948 }
949 }
950 // c:492-493 — `fputc('\n', xtrerr); fflush(xtrerr);`
951 xtrerr_fputs("\n"); // c:492 fputc('\n', xtrerr)
952 xtrerr_flush(); // c:493 fflush(xtrerr) — one write for the whole line
953 }
954
955 // c:506 — `return (*(bn->handlerfunc))(name, argv, &ops, bn->funcid);`
956 let trimmed: Vec<String> = argarr[argv..].to_vec();
957 let handler = bn_ref.handlerfunc.expect("handlerfunc checked at c:264");
958 handler(&name, &trimmed, &ops, bn_ref.funcid) // c:506
959}
960
961/// Port of `bin_enable()` from `Src/builtin.c:517`.
962/// C decl: `bin_enable(char *name, char **argv, Options ops, int func)`
963/// C: `int bin_enable(char *name, char **argv, Options ops, int func)` —
964/// enable/disable hashtab entries (default builtins; `-f`/`-r`/`-s`/`-a`
965/// pick alternate tables); `-p` routes to pat_enables (pattern toggles).
966/// WARNING: param names don't match C — Rust=(name, argv, func) vs C=(name, argv, ops, func)
967pub fn bin_enable(
968 name: &str,
969 argv: &[String], // c:517
970 ops: &options,
971 func: i32,
972) -> i32 {
973 enum Tab {
974 Builtin,
975 Shfunc,
976 Reswd,
977 Alias,
978 SufAlias,
979 }
980 if crate::dash_mode::bash_mode()
981 && (argv.is_empty() || [b'n', b'a', b'p', b's'].iter().any(|f| OPT_ISSET(ops, *f)))
982 {
983 return crate::extensions::emulation_output::bash_enable(argv, ops, func);
984 }
985 let mut returnval = 0i32; // c:524
986 let mut match_count = 0i32; // c:524
987 // c:527-538 — `-p` early-out + table selection.
988 if OPT_ISSET(ops, b'p') {
989 // c:527
990 // c:528 — `return pat_enables(name, argv, func == BIN_ENABLE);`
991 return pat_enables(name, argv, func == BIN_ENABLE); // c:528
992 }
993 let tab = if OPT_ISSET(ops, b'f') {
994 Tab::Shfunc
995 }
996 // c:529
997 else if OPT_ISSET(ops, b'r') {
998 Tab::Reswd
999 }
1000 // c:531
1001 else if OPT_ISSET(ops, b's') {
1002 Tab::SufAlias
1003 }
1004 // c:533
1005 else if OPT_ISSET(ops, b'a') {
1006 Tab::Alias
1007 }
1008 // c:535
1009 else {
1010 Tab::Builtin
1011 }; // c:537
1012
1013 // c:540-547 — flags1/flags2 set based on enable vs disable direction.
1014 let enable = func == BIN_ENABLE;
1015 let (flags1, flags2) = if enable {
1016 // c:541
1017 (0u32, DISABLED as u32) // c:542
1018 } else {
1019 (DISABLED as u32, 0u32) // c:545
1020 };
1021
1022 // c:548 — `ht` is the live builtintab: core builtins plus those of
1023 // loaded (or autoload-stubbed) modules. zshrs's table is the static
1024 // union of every module and of its own extensions, so apply the gate
1025 // `$builtins` uses; under `--zsh` the zshrs-only extension tables
1026 // (znative, ztest, …) are not part of it either.
1027 let in_builtintab = |nm: &str| -> bool {
1028 createbuiltintable().contains_key(nm)
1029 && crate::extensions::ext_builtins::builtin_in_builtintab(nm)
1030 && (!crate::extensions::ext_builtins::hide_ext_builtins()
1031 || BUILTINS.iter().any(|b| b.node.nam == nm))
1032 };
1033
1034 // Helper closures over the chosen table.
1035 let toggle_one = |tab: &Tab, nm: &str, on: bool| -> bool {
1036 match tab {
1037 Tab::Alias => aliastab_lock()
1038 .write()
1039 .map(|mut t| if on { t.enable(nm) } else { t.disable(nm) })
1040 .unwrap_or(false),
1041 Tab::SufAlias => sufaliastab_lock()
1042 .write()
1043 .map(|mut t| if on { t.enable(nm) } else { t.disable(nm) })
1044 .unwrap_or(false),
1045 // c:541-547 — `enable`/`disable -r` toggles DISABLED on the
1046 // reswdtab entry; reswords resolve through getreswdnode in
1047 // the lexer so toggling here is enough to mask/unmask.
1048 Tab::Reswd => {
1049 let exists = reswdtab_lock()
1050 .read()
1051 .map(|t| t.get_including_disabled(nm).is_some())
1052 .unwrap_or(false);
1053 if !exists {
1054 return false;
1055 }
1056 reswdtab_lock()
1057 .write()
1058 .map(|mut t| if on { t.enable(nm) } else { t.disable(nm) })
1059 .unwrap_or(false)
1060 }
1061 // c:541-547 — `enable`/`disable -f` toggles DISABLED on the
1062 // shfunctab entry; ports to disableshfuncnode/enableshfuncnode
1063 // which also unsettrap/settrap TRAP* ported.
1064 Tab::Shfunc => {
1065 let exists = shfunctab_lock()
1066 .read()
1067 .map(|t| t.get_including_disabled(nm).is_some())
1068 .unwrap_or(false);
1069 if !exists {
1070 return false;
1071 }
1072 if on {
1073 crate::ported::hashtable::enableshfuncnode(nm);
1074 } else {
1075 crate::ported::hashtable::disableshfuncnode(nm);
1076 }
1077 true
1078 }
1079 // c:541-547 — `enable`/`disable` toggles DISABLED on the
1080 // builtin. The C struct `builtintab` stores DISABLED in
1081 // `node.flags`; Rust port keeps `builtintab` as an
1082 // immutable static lookup and tracks the disabled set in
1083 // BUILTINS_DISABLED so dispatch can mask the entry.
1084 Tab::Builtin => {
1085 // Host-registered native commands (`extensions/native_cmds.rs`
1086 // — `git`/`arb`/`stryke` in the fat build) dispatch as builtins
1087 // but have no `builtintab` entry, so the membership test alone
1088 // rejected them: `disable git` answered "no such hash table
1089 // element" for a name the same shell's `whence -w` called a
1090 // builtin. They read the same `BUILTINS_DISABLED` set that the
1091 // arm below writes, so accepting the name here is all that is
1092 // needed for `disable git` to fall the shell through to the
1093 // `git` on `PATH`, and `enable git` to take it back.
1094 if !in_builtintab(nm) && !crate::native_cmds::is_registered(nm)
1095 {
1096 return false;
1097 }
1098 if let Ok(mut set) = BUILTINS_DISABLED.lock() {
1099 if on {
1100 set.remove(nm);
1101 } else {
1102 set.insert(nm.to_string());
1103 }
1104 return true;
1105 }
1106 false
1107 }
1108 }
1109 };
1110 let collect_names = |tab: &Tab| -> Vec<String> {
1111 match tab {
1112 Tab::Alias => aliastab_lock()
1113 .read()
1114 .map(|t| t.iter().map(|(n, _)| n.clone()).collect())
1115 .unwrap_or_default(),
1116 Tab::SufAlias => sufaliastab_lock()
1117 .read()
1118 .map(|t| t.iter().map(|(n, _)| n.clone()).collect())
1119 .unwrap_or_default(),
1120 Tab::Reswd => reswdtab_lock()
1121 .read()
1122 .map(|t| t.iter().map(|(n, _)| n.clone()).collect())
1123 .unwrap_or_default(),
1124 Tab::Shfunc => shfunctab_lock()
1125 .read()
1126 .map(|t| t.iter().map(|(n, _)| n.clone()).collect())
1127 .unwrap_or_default(),
1128 Tab::Builtin => createbuiltintable()
1129 .keys()
1130 .filter(|n| in_builtintab(n))
1131 .cloned()
1132 .collect(),
1133 }
1134 };
1135
1136 // c:553-558 — no-args list.
1137 if argv.is_empty() {
1138 // c:553
1139 queue_signals(); // c:554
1140 // c:555 — `scanhashtable(ht, 1, flags1, flags2, ht->printnode, 0);`
1141 // Filter: print only entries where (flags & flags1) == flags1
1142 // && (flags & flags2) == 0. For enable/disable, flags1 and
1143 // flags2 are DISABLED-bit selectors that mask the listed set
1144 // to ONLY the kind being toggled (enable lists enabled,
1145 // disable lists disabled).
1146 let is_disabled = |nm: &str| -> bool {
1147 match tab {
1148 Tab::Alias => aliastab_lock()
1149 .read()
1150 .ok()
1151 .and_then(|t| {
1152 t.get_including_disabled(nm)
1153 .map(|a| (a.node.flags & DISABLED as i32) != 0)
1154 })
1155 .unwrap_or(false),
1156 Tab::SufAlias => sufaliastab_lock()
1157 .read()
1158 .ok()
1159 .and_then(|t| {
1160 t.get_including_disabled(nm)
1161 .map(|a| (a.node.flags & DISABLED as i32) != 0)
1162 })
1163 .unwrap_or(false),
1164 Tab::Reswd => reswdtab_lock()
1165 .read()
1166 .ok()
1167 .and_then(|t| {
1168 t.get_including_disabled(nm)
1169 .map(|r| (r.node.flags & DISABLED as i32) != 0)
1170 })
1171 .unwrap_or(false),
1172 Tab::Shfunc => shfunctab_lock()
1173 .read()
1174 .ok()
1175 .and_then(|t| {
1176 t.get_including_disabled(nm)
1177 .map(|f| (f.node.flags & DISABLED as i32) != 0)
1178 })
1179 .unwrap_or(false),
1180 Tab::Builtin => BUILTINS_DISABLED
1181 .lock()
1182 .map(|s| s.contains(nm))
1183 .unwrap_or(false),
1184 }
1185 };
1186 // c:Src/builtin.c — `scanhashtable(ht, 1, …)` walks the
1187 // table in sorted order (the first arg `sorted=1`). Mirror
1188 // by sorting the collected names before the per-name walk.
1189 // Without this, builtin/reswd listings came out in HashMap
1190 // iteration order, diverging from zsh.
1191 let mut all_names = collect_names(&tab);
1192 all_names.sort();
1193 for nm in all_names {
1194 let dis = is_disabled(&nm);
1195 let entry_flags = if dis { DISABLED as u32 } else { 0 };
1196 if (entry_flags & flags1) == flags1 && (entry_flags & flags2) == 0 {
1197 // c:Src/builtin.c — `ht->printnode` dispatches per
1198 // table type. For alias/sufalias, printaliasnode
1199 // (hashtable.rs:1477) prints `name=value`. For
1200 // builtin/reswd/shfunc, just the name. Previous
1201 // Rust port printed bare names for all kinds, so
1202 // `enable -a` lost the alias definitions. Mirror
1203 // C's printnode dispatch here.
1204 match tab {
1205 Tab::Alias => {
1206 let val = aliastab_lock()
1207 .read()
1208 .ok()
1209 .and_then(|t| t.get_including_disabled(&nm).map(|a| a.text.clone()));
1210 if let Some(v) = val {
1211 println!("{}={}", nm, quotedzputs(&v));
1212 } else {
1213 println!("{}", nm);
1214 }
1215 }
1216 Tab::SufAlias => {
1217 let val = sufaliastab_lock()
1218 .read()
1219 .ok()
1220 .and_then(|t| t.get_including_disabled(&nm).map(|a| a.text.clone()));
1221 if let Some(v) = val {
1222 println!("{}={}", nm, quotedzputs(&v));
1223 } else {
1224 println!("{}", nm);
1225 }
1226 }
1227 Tab::Shfunc => {
1228 // shfunctab->printnode is printshfuncnode
1229 // (c:Src/hashtable.c:914 via c:555 printflags 0):
1230 // the whole definition, not the bare name.
1231 let shf = shfunctab_lock()
1232 .read()
1233 .ok()
1234 .and_then(|t| t.get_including_disabled(&nm).cloned());
1235 match shf {
1236 Some(f) => crate::ported::hashtable::printshfuncnode(&f, 0),
1237 None => println!("{}", nm),
1238 }
1239 }
1240 _ => println!("{}", nm),
1241 }
1242 }
1243 }
1244 unqueue_signals(); // c:556
1245 return 0; // c:557
1246 }
1247
1248 // c:561-580 — `-m` glob branch.
1249 if OPT_ISSET(ops, b'm') {
1250 // c:561
1251 for arg in argv {
1252 // c:562
1253 queue_signals(); // c:563
1254 let pprog = patcompile(
1255 &{
1256 let mut __pat_tok = crate::pattern_data_escape::escape_data_backslashes(&arg); // a backslash tokenize leaves raw is DATA (c:Src/glob.c:3651)
1257 crate::ported::glob::tokenize(&mut __pat_tok);
1258 __pat_tok
1259 }, // c:566
1260 PAT_HEAPDUP,
1261 None,
1262 );
1263 if let Some(prog) = pprog {
1264 for nm in collect_names(&tab) {
1265 if pattry(&prog, &nm) {
1266 // c:567
1267 if toggle_one(&tab, &nm, enable) {
1268 match_count += 1; // c:567
1269 }
1270 }
1271 }
1272 } else {
1273 zwarnnam(name, &format!("bad pattern : {}", arg)); // c:572
1274 returnval = 1; // c:573
1275 }
1276 unqueue_signals(); // c:575
1277 }
1278 if match_count == 0 {
1279 // c:579
1280 returnval = 1; // c:580
1281 }
1282 return returnval; // c:581
1283 }
1284
1285 // c:585-594 — literal-name dispatch.
1286 queue_signals(); // c:585
1287 for arg in argv {
1288 // c:586
1289 if !toggle_one(&tab, arg, enable) {
1290 // c:587
1291 zwarnnam(name, &format!("no such hash table element: {}", arg)); // c:590
1292 returnval = 1; // c:591
1293 }
1294 }
1295 unqueue_signals(); // c:594
1296 returnval // c:595
1297}
1298
1299/// Port of `bin_set()` from `Src/builtin.c:601`.
1300/// C decl: `bin_set(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))`
1301/// C: `int bin_set(char *nam, char **args, UNUSED(Options ops),
1302/// UNUSED(int func))` — set shell options, declare arrays,
1303/// replace positional params, or display variables.
1304/// WARNING: param names don't match C — Rust=(nam, args, _func) vs C=(nam, args, ops, func)
1305pub fn bin_set(
1306 nam: &str,
1307 args: &[String], // c:601
1308 _ops: &options,
1309 _func: i32,
1310) -> i32 {
1311 // PFA-SMR aspect: emit setopt/unsetopt events for the POSIX
1312 // `set -o NAME` / `set +o NAME` form. This is the third option
1313 // syntax (alongside setopt NAME / unsetopt NAME); a recorder
1314 // user expects all three to surface in `zwhere -k setopt`.
1315 #[cfg(feature = "recorder")]
1316 if crate::recorder::is_enabled() && !args.is_empty() {
1317 let ctx = crate::recorder::recorder_ctx_global();
1318 let mut iter = args.iter().peekable();
1319 while let Some(a) = iter.next() {
1320 match a.as_str() {
1321 "-o" => {
1322 if let Some(name) = iter.next() {
1323 crate::recorder::emit_setopt(name, ctx.clone());
1324 }
1325 }
1326 "+o" => {
1327 if let Some(name) = iter.next() {
1328 crate::recorder::emit_unsetopt(name, ctx.clone());
1329 }
1330 }
1331 _ => {}
1332 }
1333 }
1334 }
1335
1336 let mut argv: Vec<String> = args.to_vec();
1337 let mut hadopt = false; // c:603
1338 let mut hadplus = false; // c:603
1339 let mut hadend = false; // c:603
1340 let mut sort: i32 = 0; // c:603
1341 let mut array: i32 = 0; // c:603
1342 let mut arrayname: Option<String> = None; // c:604
1343
1344 // c:608-614 — sh-compat: bare `set -` → +xv.
1345 if !EMULATION(EMULATE_ZSH) // c:608
1346 && !argv.is_empty() && argv[0] == "-"
1347 {
1348 // c:610-611 — `dosetopt(VERBOSE, 0, 0, opts); dosetopt(XTRACE, 0, 0, opts);`
1349 dosetopt(VERBOSE, 0, 0); // c:610
1350 dosetopt(XTRACE, 0, 0); // c:611
1351 if argv.len() == 1 {
1352 return 0;
1353 } // c:612-613
1354 argv.remove(0);
1355 }
1356
1357 // c:617-668 — top-level option-arg loop.
1358 let mut idx = 0usize;
1359 'outer: while idx < argv.len() // c:617
1360 && (argv[idx].starts_with('-') || argv[idx].starts_with('+'))
1361 {
1362 let arg = argv[idx].clone();
1363 let action = arg.starts_with('-'); // c:619
1364 if !action {
1365 hadplus = true;
1366 } // c:620
1367 // c:621-622 — bare `-` / `+` → "--"
1368 let body: String = if arg.len() == 1 {
1369 "--".to_string()
1370 } else {
1371 arg.clone()
1372 };
1373 // c:623 — `while (*++*args)`
1374 let chars: Vec<char> = body[1..].chars().collect();
1375 let mut ci = 0usize;
1376 while ci < chars.len() {
1377 // c:623
1378 let c = chars[ci];
1379 if c != '-' || action {
1380 hadopt = true;
1381 } // c:626
1382 // c:628-632 — `--` end-of-options.
1383 if c == '-' {
1384 // c:628
1385 hadend = true; // c:629
1386 idx += 1; // c:630 args++
1387 break 'outer;
1388 }
1389 // c:633-645 — `o` long-option name follows.
1390 if c == 'o' {
1391 // c:633
1392 let optname: String = if ci + 1 < chars.len() {
1393 chars[ci + 1..].iter().collect::<String>()
1394 } else {
1395 idx += 1;
1396 if idx >= argv.len() {
1397 // c:636-637 — `printoptionstates(hadplus);
1398 // inittyptab(); return 0;`. The Rust port
1399 // previously returned 0 without printing,
1400 // so `set -o` / `set +o` (no option name)
1401 // emitted nothing instead of the full
1402 // option-state listing (one per line).
1403 // hadplus is true when the user invoked
1404 // `set +o` (POSIX-output form); else `set -o`
1405 // (zsh-output form).
1406 crate::ported::options::printoptionstates(hadplus);
1407 return 0;
1408 }
1409 argv[idx].clone()
1410 };
1411 // !!! BASH-MODE GATE (no C counterpart) !!! bash accepts a
1412 // FIXED set of `set -o` names, six of which zsh has no option
1413 // for at all (`errtrace`, `functrace`, `history`, `keyword`,
1414 // `nolog`, `posix`) and two of which zsh's `dosetopt` refuses
1415 // to change after startup (`monitor`, `onecmd`; c:746). All
1416 // eight reached C's `no such option` / `can't change option`
1417 // here, so `bash -c 'set -o posix'` failed where real bash
1418 // returns 0. `bash_set_o` owns exactly those names in --bash
1419 // and returns None everywhere else, leaving the faithful C
1420 // path below untouched for --zsh and the other emulations.
1421 if let Some(st) = crate::dash_mode::bash_set_o(&optname, action) {
1422 if st != 0 {
1423 unqueue_signals();
1424 return st;
1425 }
1426 // c:645 — same `break` the successful C path takes, so a
1427 // trailing operand list (`set -o posix a b`) still becomes
1428 // the positional parameters.
1429 break;
1430 }
1431 let optno = optlookup(&optname); // c:642
1432 if optno == 0 {
1433 // c:642 — C: `zerrnam(nam, "no such option: %s", *args)`.
1434 // zwarnnam emits `zsh:<nam>:<lineno>:` prefix
1435 // (vs zerr's bare `zsh:<lineno>:`); use the
1436 // canonical form so `set +o BAD` matches zsh's
1437 // diagnostic format. Return 1 so $? reflects the
1438 // failed lookup (C's execbuiltin checks errflag
1439 // after the call; zshrs's bridge bases on the
1440 // return value).
1441 zerrnam(nam, &format!("no such option: {}", optname));
1442 unqueue_signals();
1443 return 1;
1444 } else if dosetopt(optno, if action { 1 } else { 0 }, 0) != 0
1445 // c:644
1446 {
1447 zerrnam(nam, &format!("can't change option: {}", optname));
1448 unqueue_signals();
1449 return 1;
1450 }
1451 break;
1452 }
1453 // c:646-657 — `A` array-mode (with optional name arg).
1454 if c == 'A' {
1455 // c:646
1456 array = if action { 1 } else { -1 }; // c:649
1457 let nameopt: Option<String> = if ci + 1 < chars.len() {
1458 Some(chars[ci + 1..].iter().collect::<String>())
1459 } else if idx + 1 < argv.len() {
1460 idx += 1;
1461 Some(argv[idx].clone())
1462 } else {
1463 None
1464 };
1465 arrayname = nameopt.clone();
1466 if arrayname.is_none() {
1467 // c:651
1468 idx += 1;
1469 break 'outer;
1470 }
1471 let ksharrays = isset(KSHARRAYS);
1472 if !ksharrays {
1473 // c:653
1474 idx += 1; // c:655 args++
1475 break 'outer; // c:656
1476 }
1477 break;
1478 }
1479 // c:659-660 — `s` sort flag.
1480 if c == 's' {
1481 // c:659
1482 sort = if action { 1 } else { -1 }; // c:660
1483 } else {
1484 // c:662-666 — short-option letter: optlookupc + dosetopt.
1485 // c:Src/builtin.c — zerrnam(nam, ...) prefixes the
1486 // builtin's name into the error tag ("zsh:set:1:" vs
1487 // bare "zsh:1:"). Use zerrnam to match zsh's exact
1488 // error format for `set -X 2>&1`.
1489 let optno = if crate::dash_mode::bash_set_letter(c, action) {
1490 ci += 1;
1491 continue;
1492 } else {
1493 crate::ported::options::optlookupc(c) // c:663
1494 };
1495 if optno == 0 {
1496 // c:663
1497 zerrnam(nam, &format!("bad option: -{}", c)); // c:663
1498 } else if dosetopt(optno, if action { 1 } else { 0 }, 0) != 0
1499 // c:664
1500 {
1501 zerrnam(nam, &format!("can't change option: -{}", c)); // c:664
1502 }
1503 }
1504 ci += 1;
1505 }
1506 idx += 1; // c:668
1507 }
1508 // c:Src/builtin.c:668 — `if (errflag) return 1;`. A bad / can't-change
1509 // option (or `no such option`) reported via zerrnam set ERRFLAG_ERROR;
1510 // `set -Z` must exit 1, not fall through to the (success) display path.
1511 if (crate::ported::utils::errflag.load(std::sync::atomic::Ordering::Relaxed)
1512 & crate::ported::utils::ERRFLAG_ERROR)
1513 != 0
1514 {
1515 return 1;
1516 }
1517 let _ = nam;
1518
1519 // c:676 — `queue_signals();`
1520 queue_signals();
1521 let remaining = &argv[idx..];
1522
1523 // c:678-694 — display path when no array/no args.
1524 if arrayname.is_none() {
1525 // c:678
1526 if !hadopt && remaining.is_empty() {
1527 // c:679
1528 // c:680-681 — `scanhashtable(paramtab, 1, 0, 0,
1529 // paramtab->printnode, hadplus ? PRINT_NAMEONLY : 0);`
1530 //
1531 // Every paramtab node goes through printnode (printparamnode),
1532 // sorted by name. printparamnode owns the whole format: arrays
1533 // as `name=( a b )`, assocs as `name=( [k]=v )`, PM_HIDEVAL
1534 // specials (`commands`, `functions`, ...) as the bare name, and
1535 // name quoting (`'$'`, `'#'`). The previous port printed every
1536 // node as a joined scalar `name='a b'`.
1537 //
1538 // Clone the nodes first: printparamnode reads paramtab (tied
1539 // peers, getfn dispatch) and must not run under the guard.
1540 let mut nodes: Vec<crate::ported::zsh_h::param> = {
1541 let tab = paramtab().read().unwrap();
1542 tab.values().map(|pm| (**pm).clone()).collect()
1543 };
1544 nodes.sort_by(|a, b| hnamcmp(&a.node.nam, &b.node.nam)); // c:680 sorted=1
1545 let printflags = if hadplus { PRINT_NAMEONLY } else { 0 }; // c:681
1546 for mut pm in nodes {
1547 printparamnode(&mut pm, printflags); // c:680 printnode
1548 }
1549 }
1550 if array != 0 {
1551 // c:684
1552 // c:685-687 — `scanhashtable(paramtab, 1, PM_ARRAY, 0,
1553 // paramtab->printnode, hadplus ? PRINT_NAMEONLY : 0)`.
1554 // scanhashtable keeps a node when `hn->flags & flags1`
1555 // (c:Src/hashtable.c:353), sorted by name; the
1556 // format is printparamnode's (`name=( a 'b c' )`).
1557 let mut nodes: Vec<crate::ported::zsh_h::param> = {
1558 let tab = paramtab().read().unwrap();
1559 tab.values()
1560 .filter(|pm| (pm.node.flags as u32 & PM_ARRAY) != 0)
1561 .map(|pm| (**pm).clone())
1562 .collect()
1563 };
1564 nodes.sort_by(|a, b| hnamcmp(&a.node.nam, &b.node.nam)); // c:685 sorted=1
1565 let printflags = if hadplus { PRINT_NAMEONLY } else { 0 }; // c:686
1566 for mut pm in nodes {
1567 printparamnode(&mut pm, printflags); // c:685 printnode
1568 }
1569 }
1570 if remaining.is_empty() && !hadend {
1571 // c:688
1572 unqueue_signals();
1573 return 0; // c:690
1574 }
1575 }
1576
1577 // c:693-695 — `set -s` sort.
1578 let sorted: Vec<String> = if sort != 0 {
1579 let mut v = remaining.to_vec();
1580 if sort < 0 {
1581 v.sort_by(|a, b| b.cmp(a));
1582 } else {
1583 v.sort();
1584 }
1585 v
1586 } else {
1587 remaining.to_vec()
1588 };
1589
1590 // c:696-708 — array assign or positional-param replace.
1591 if array != 0 {
1592 // c:696
1593 // c:697-708 — build array; `array < 0` appends to existing $name.
1594 let aname = arrayname.unwrap_or_default();
1595 let mut new_arr: Vec<String> = sorted;
1596 if array < 0 {
1597 // c:701
1598 // c:702-704 — `if ((a = getaparam(arrayname)) && arrlen_gt(a, len))`.
1599 // Read paramtab.u_arr directly; was using `:`-
1600 // split env value as a fake array.
1601 let existing: Vec<String> = {
1602 let tab = paramtab().read().unwrap();
1603 tab.get(&aname)
1604 .and_then(|pm| pm.u_arr.clone())
1605 .unwrap_or_default()
1606 };
1607 if existing.len() > new_arr.len() {
1608 // c:702
1609 new_arr.extend(existing.into_iter().skip(new_arr.len())); // c:703
1610 }
1611 }
1612 // c:709 — `setaparam(arrayname, x)`. In C, setaparam →
1613 // assignaparam → setarrvalue dispatches a PM_HASHED target to
1614 // arrhashsetfn, which converts the flat list to key/value pairs.
1615 // When the target is an associative array (`typeset -A h; set -A
1616 // h k v`), route through sethparam (the flat-kv-list assoc
1617 // setter) so the values land as pairs instead of being stored as
1618 // a plain array (which left the assoc empty). Plain arrays keep
1619 // setaparam.
1620 let target_is_hashed = {
1621 use PM_HASHED;
1622 paramtab()
1623 .read()
1624 .ok()
1625 .and_then(|tab| {
1626 tab.get(&aname)
1627 .map(|pm| (pm.node.flags as u32 & PM_HASHED) != 0)
1628 })
1629 .unwrap_or(false)
1630 };
1631 if target_is_hashed {
1632 crate::ported::params::sethparam(&aname, new_arr);
1633 } else {
1634 setaparam(&aname, new_arr);
1635 }
1636 } else {
1637 // c:711-712 — `freearray(pparams); pparams = zarrdup(args);`
1638 // PPARAMS is the single source of truth; fusevm reads via
1639 // `exec.pparams()`.
1640 if let Ok(mut pp) = PPARAMS.lock() {
1641 *pp = sorted; // c:712
1642 }
1643 // !!! EMULATION-ONLY (no C counterpart) !!! dash and ash reset the
1644 // `getopts` cursor whenever the positional parameters are replaced
1645 // (`setparam()` clears shellparam.optind/optoff), so `getopts "ab" o
1646 // -a; set -- -b -a; getopts "ab" o` yields `b` there and `a` in
1647 // bash / ksh93 / mksh / zsh. See
1648 // dash_mode::getopts_reset_on_set_positional — false in --zsh.
1649 if crate::dash_mode::getopts_reset_on_set_positional() {
1650 ZOPTIND.store(1, Relaxed);
1651 OPTCIND.store(0, Relaxed);
1652 crate::dash_mode::getopts_forget_reported();
1653 setiparam("OPTIND", 1);
1654 }
1655 }
1656 unqueue_signals(); // c:714
1657 0 // c:715
1658}
1659
1660/// Port of `bin_pwd()` from `Src/builtin.c:728`.
1661/// C decl: `bin_pwd(UNUSED(char *name), UNUSED(char **argv), Options ops, UNUSED(int func))`
1662/// C: `int bin_pwd(UNUSED(char *name), UNUSED(char **argv), Options ops,
1663/// UNUSED(int func))` — `-r`/`-P` or (CHASELINKS && !`-L`) →
1664/// print resolved cwd via zgetcwd; else print the cached `pwd`.
1665// pwd: display the name of the current directory // c:728
1666/// WARNING: param names don't match C — Rust=(_name, _argv, _func) vs C=(name, argv, ops, func)
1667pub fn bin_pwd(
1668 _name: &str,
1669 _argv: &[String], // c:728
1670 ops: &options,
1671 _func: i32,
1672) -> i32 {
1673 let chaselinks = isset(CHASELINKS);
1674 // c:730-731 — `if (OPT_ISSET(ops,'r') || OPT_ISSET(ops,'P') ||
1675 // (isset(CHASELINKS) && !OPT_ISSET(ops,'L')))`
1676 if OPT_ISSET(ops, b'r') || OPT_ISSET(ops, b'P') // c:730
1677 || (chaselinks && !OPT_ISSET(ops, b'L'))
1678 // c:731
1679 {
1680 // c:732 — `printf("%s\n", zgetcwd());`
1681 println!("{}", zgetcwd()); // c:732
1682 } else {
1683 // c:734 — `zputs(pwd, stdout); putchar('\n');`. C reads the
1684 // shell-internal `pwd` global (Src/params.c:108), NOT the
1685 // exported $PWD env var. The internal global is only mutated
1686 // by `cd`. zshrs lacks a separate internal pwd, so `$PWD`
1687 // doubles as both — and user code can spoof it (`PWD=/foo`).
1688 //
1689 // C-equivalent behavior in zshrs: trust `$PWD` ONLY when its
1690 // stat matches `getcwd()` (same dev+ino → same directory,
1691 // logical name preserved through symlinks). On mismatch the
1692 // user spoofed it; fall back to `zgetcwd()` so `pwd` reports
1693 // the actual current directory. Bug #441 — security-relevant:
1694 // defensive-coding patterns checking `pwd` output get fooled
1695 // by a `PWD=` assignment without this validation.
1696 let pwd_param = getsparam("PWD");
1697 let logical_pwd = pwd_param.as_deref().filter(|pwd| {
1698 // stat-compare $PWD against getcwd() — same inode means
1699 // $PWD is just a symlink-preserving alias for the real
1700 // cwd, safe to use.
1701 use std::os::unix::fs::MetadataExt;
1702 let cwd = zgetcwd();
1703 match (std::fs::metadata(pwd), std::fs::metadata(&cwd)) {
1704 (Ok(a), Ok(b)) => a.dev() == b.dev() && a.ino() == b.ino(),
1705 _ => false,
1706 }
1707 });
1708 println!("{}", logical_pwd.map(String::from).unwrap_or_else(zgetcwd)); // c:734
1709 }
1710 0 // c:737
1711}
1712
1713/// Port of `bin_dirs()` from `Src/builtin.c:749`.
1714/// C decl: `bin_dirs(UNUSED(char *name), char **argv, Options ops, UNUSED(int func))`
1715/// C: `int bin_dirs(UNUSED(char *name), char **argv, Options ops, ...)` —
1716/// list dirstack (default / -v / -p / -l) or replace it with argv.
1717// dirs: list the directory stack, or replace it with a provided list // c:749
1718/// WARNING: param names don't match C — Rust=(_name, argv, _func) vs C=(name, argv, ops, func)
1719pub fn bin_dirs(
1720 _name: &str,
1721 argv: &[String], // c:749
1722 ops: &options,
1723 _func: i32,
1724) -> i32 {
1725 queue_signals(); // c:753
1726 // c:755-756 — list mode: no args & no -c, OR -v / -p.
1727 if (argv.is_empty() && !OPT_ISSET(ops, b'c')) // c:755
1728 || OPT_ISSET(ops, b'v')
1729 || OPT_ISSET(ops, b'p')
1730 {
1731 let mut pos = 1; // c:760
1732 // c:763-769 — pick separator format.
1733 let fmt: &str = if OPT_ISSET(ops, b'v') {
1734 // c:763
1735 print!("0\t"); // c:764
1736 "\n{}\t" // c:765
1737 } else if OPT_ISSET(ops, b'p') {
1738 // c:767
1739 "\n"
1740 } else {
1741 " "
1742 };
1743 // c:771-774 — print pwd via fprintdir or zputs (`-l`).
1744 // Previous Rust port inlined a HOME-prefix replacement which
1745 // only abbreviated `$HOME/...` to `~/...` — missed every
1746 // user-defined nameddirtab entry (`hash -d proj=/big/path`).
1747 // Route through `utils::fprintdir` which calls `finddir`,
1748 // matching C's named-dir abbreviation.
1749 let pwd = getsparam("PWD").unwrap_or_else(|| zgetcwd());
1750 if OPT_ISSET(ops, b'l') {
1751 // c:771
1752 print!("{}", pwd); // c:772
1753 } else {
1754 print!("{}", fprintdir(&pwd)); // c:774
1755 }
1756 // c:775-781 — walk dirstack list.
1757 if let Ok(stack) = DIRSTACK.lock() {
1758 // c:775
1759 for entry in stack.iter() {
1760 if fmt == "\n{}\t" {
1761 print!("\n{}\t", pos);
1762 } else {
1763 print!("{}", fmt); // c:776
1764 }
1765 pos += 1; // c:776
1766 if OPT_ISSET(ops, b'l') {
1767 // c:777
1768 print!("{}", entry); // c:778
1769 } else {
1770 print!("{}", fprintdir(entry)); // c:780
1771 }
1772 }
1773 }
1774 unqueue_signals(); // c:783
1775 println!(); // c:784
1776 return 0; // c:785
1777 }
1778 // c:788-792 — replace dirstack with the supplied entries.
1779 if let Ok(mut stack) = DIRSTACK.lock() {
1780 stack.clear(); // c:790
1781 for arg in argv {
1782 stack.push(arg.clone()); // c:791
1783 }
1784 }
1785 unqueue_signals(); // c:793
1786 0 // c:794
1787}
1788
1789/// Port of `set_pwd_env()` from `Src/builtin.c:800`.
1790/// C decl: `set_pwd_env(void)`
1791/// Refreshes both `$PWD` and `$OLDPWD` shell
1792/// parameters from the shell-side `pwd`/`oldpwd` globals
1793/// (`assignsparam` at c:818-819), then `addenv`s each so child
1794/// processes inherit them (c:821-826). C also clears `PM_READONLY`
1795/// and unsets the param when a user retyped PWD/OLDPWD to a
1796/// non-scalar (c:806-816).
1797///
1798/// Rust port: the bin entry's analog of C's `pwd`/`oldpwd` globals
1799/// is the live OS env (validated against ispwd by ShellExecutor::new
1800/// per c:Src/init.c:1242-1259 — see the subshell-snapshot comment in
1801/// fusevm_bridge.rs treating `$PWD` env as the logical-pwd carrier).
1802/// So the data flow matches C: globals (env) → paramtab → env. The
1803/// previous revision read paramtab's own PWD and wrote it back — a
1804/// circular no-op that left the stale environ-snapshot import in
1805/// place when the inherited $PWD failed ispwd().
1806pub fn set_pwd_env() {
1807 // c:800
1808 // c:805-816 — a PWD/OLDPWD that is not a scalar loses PM_READONLY and
1809 // is unset so the assignments below recreate it as a scalar.
1810 for nm in ["PWD", "OLDPWD"] {
1811 let non_scalar = crate::ported::params::paramtab()
1812 .write()
1813 .ok()
1814 .and_then(|mut tab| {
1815 tab.get_mut(nm).map(|pm| {
1816 let ns = PM_TYPE(pm.node.flags as u32) != PM_SCALAR;
1817 if ns {
1818 pm.node.flags &= !(PM_READONLY as i32); // c:807
1819 }
1820 ns
1821 })
1822 })
1823 .unwrap_or(false);
1824 if non_scalar {
1825 crate::ported::params::unsetparam(nm); // c:808 unsetparam_pm(pm, 0, 1)
1826 }
1827 }
1828 // c:818 — `assignsparam("PWD", ztrdup(pwd), 0);`
1829 let pwd = env::var("PWD").unwrap_or_else(|_| {
1830 env::current_dir()
1831 .map(|p| p.to_string_lossy().into_owned())
1832 .unwrap_or_default()
1833 });
1834 setsparam("PWD", &pwd); // c:818
1835 // c:819 — `assignsparam("OLDPWD", ztrdup(oldpwd), 0);`
1836 if let Ok(oldpwd) = env::var("OLDPWD") {
1837 setsparam("OLDPWD", &oldpwd); // c:819
1838 }
1839 // c:821-826 — `pm = paramtab->getnode(paramtab, "PWD"); if
1840 // (!(pm->node.flags & PM_EXPORTED)) addenv(pm, pwd);` and the same
1841 // for OLDPWD. Writing the OS environment directly (the previous
1842 // `env::set_var`) skipped `addenv`'s `pm->flags |= PM_EXPORTED`
1843 // (params.c:5482-5484), so `${(t)PWD}` read `scalar` where zsh
1844 // reads `scalar-export`.
1845 for (name, value) in [
1846 ("PWD", &pwd),
1847 ("OLDPWD", &env::var("OLDPWD").unwrap_or_default()),
1848 ] {
1849 if value.is_empty() && name == "OLDPWD" {
1850 continue;
1851 }
1852 let exported = crate::ported::params::paramtab()
1853 .read()
1854 .ok()
1855 .and_then(|tab| {
1856 tab.get(name)
1857 .map(|pm| (pm.node.flags & PM_EXPORTED as i32) != 0)
1858 })
1859 .unwrap_or(false);
1860 if !exported {
1861 crate::ported::params::addenv(name, value);
1862 }
1863 }
1864}
1865
1866/// Port of `bin_cd()` from `Src/builtin.c:840`.
1867/// C decl: `bin_cd(char *nam, char **argv, Options ops, int func)`
1868/// C: `int bin_cd(char *nam, char **argv, Options ops, int func)`.
1869///
1870/// Body (verbatim translation per c:842-859):
1871/// ```c
1872/// doprintdir = (doprintdir == -1);
1873/// chasinglinks = OPT_ISSET(ops,'P') ||
1874/// (isset(CHASELINKS) && !OPT_ISSET(ops,'L'));
1875/// queue_signals();
1876/// zpushnode(dirstack, ztrdup(pwd));
1877/// if (!(dir = cd_get_dest(nam, argv, OPT_ISSET(ops,'s'), func))) {
1878/// zsfree(getlinknode(dirstack));
1879/// unqueue_signals();
1880/// return 1;
1881/// }
1882/// cd_new_pwd(func, dir, OPT_ISSET(ops, 'q'));
1883/// unqueue_signals();
1884/// return 0;
1885/// ```
1886// cd, chdir, pushd, popd // c:796
1887/// `bin_cd` — see implementation.
1888pub fn bin_cd(
1889 nam: &str,
1890 argv: &[String], // c:840
1891 ops: &options,
1892 func: i32,
1893) -> i32 {
1894 // c:844 — `doprintdir = (doprintdir == -1);`
1895 let prev = DOPRINTDIR.load(Relaxed);
1896 DOPRINTDIR.store(if prev == -1 { 1 } else { 0 }, Relaxed); // c:844
1897
1898 // c:846-847 — `chasinglinks = OPT_ISSET(ops,'P') ||
1899 // (isset(CHASELINKS) && !OPT_ISSET(ops,'L'));`
1900 let chase = OPT_ISSET(ops, b'P') // c:846
1901 || (isset(CHASELINKS)
1902 && !OPT_ISSET(ops, b'L'));
1903 CHASINGLINKS.store(chase as i32, Relaxed);
1904
1905 queue_signals(); // c:848
1906
1907 // c:849 — `zpushnode(dirstack, ztrdup(pwd));`. C uses the `pwd`
1908 // global (the in-shell logical cwd, kept in sync with
1909 // $PWD). Read from paramtab; fall back to getcwd if
1910 // unset. The C source pushes pre-cd pwd to the top of
1911 // dirstack here as a scratch slot used by cd_get_dest's
1912 // +N/-N resolver; cd_new_pwd's remnode logic relies on
1913 // this. Save the pre-cd path for the post-cd dirstack
1914 // maintenance below.
1915 let pre_pwd = getsparam("PWD").unwrap_or_else(|| zgetcwd());
1916
1917 // c:850-854 — `if (!(dir = cd_get_dest(...))) { pop; unqueue; return 1; }`
1918 let dest = cd_get_dest(nam, argv, OPT_ISSET(ops, b's'), func);
1919 if dest.is_none() {
1920 unqueue_signals(); // c:852
1921 return 1; // c:853
1922 }
1923 let dest_raw = dest.unwrap();
1924
1925 // `-s` (C's `hard`) is threaded down to `lchdir(path, NULL, hard)`
1926 // (c:Src/builtin.c:1173-1174), and it is `lchdir` that gives `-s` its
1927 // meaning: with `hard` set it walks the path one component at a time
1928 // and `lstat`s each one (c:Src/utils.c:7489), rejecting any component
1929 // that is not a directory AS THE LINK ITSELF —
1930 // c:7493-7496 — `if(!S_ISDIR(st1.st_mode)) { err = ENOTDIR; break; }`
1931 // — so a symlink (even one pointing AT a directory), a plain file and
1932 // a dangling link all fail the same way, with ENOTDIR. The error is
1933 // then reported by `cd_do_chdir` as `zwarnnam(cnam, "%e: %s", eno,
1934 // dest)` (c:1080), i.e. `not a directory: <dest>`.
1935 //
1936 // The previous code invented a message with no C counterpart —
1937 // `<dest>: symbolic link` — and mis-cited it as c:851 (which is just
1938 // the `cd_get_dest(nam, argv, OPT_ISSET(ops,'s'), func)` call, not a
1939 // symlink check). It also only tested `is_symlink`, so `cd -s
1940 // <plainfile>` fell through to the generic chdir and reported ENOENT
1941 // where zsh reports ENOTDIR.
1942 //
1943 // Ported here as the final-component check rather than inside a full
1944 // `lchdir` port: this reproduces C's outcome for the single-component
1945 // and absolute-path cases the `-s` option is used for. A path whose
1946 // FAILING component is an interior one still reports whatever the
1947 // plain chdir below reports, where C would report ENOTDIR for it too.
1948 if OPT_ISSET(ops, b's') {
1949 if let Ok(meta) = fs::symlink_metadata(&dest_raw) {
1950 // c:7489 `lstat(buf, &st1)` + c:7493 `!S_ISDIR(st1.st_mode)`
1951 if !meta.file_type().is_dir() {
1952 // c:7494 — `err = ENOTDIR;`, surfaced by c:1080 `"%e: %s"`.
1953 let mut msg = crate::ported::compat::strerror(libc::ENOTDIR);
1954 if let Some(c) = msg.chars().next() {
1955 msg = format!("{}{}", c.to_ascii_lowercase(), &msg[c.len_utf8()..]);
1956 }
1957 zwarnnam(nam, &format!("{}: {}", msg, dest_raw)); // c:1080
1958 unqueue_signals();
1959 return 1;
1960 }
1961 }
1962 }
1963
1964 // c:Src/builtin.c:855 — route the resolved arg through
1965 // cd_do_chdir so CDPATH walk, leading `~`/`.` handling, and
1966 // CDABLEVARS expansion fire. cd_do_chdir performs the actual
1967 // lchdir + returns the LOGICAL path (the one to write to PWD).
1968 // The previous Rust port called env::set_current_dir(dest_raw)
1969 // directly which skipped all of CDPATH, so `CDPATH=/foo cd bar`
1970 // would only resolve `./bar` even if `/foo/bar` existed.
1971 let dest_path = match cd_do_chdir(nam, &dest_raw, OPT_ISSET(ops, b's') as i32) {
1972 Some(p) => p,
1973 None => {
1974 unqueue_signals();
1975 // !!! DASH-FAMILY GATE !!! dash's failed `cd` is a `sh_error`: status 2.
1976 return crate::extensions::dash_mode::fatal_error_status().unwrap_or(1);
1977 }
1978 };
1979
1980 // c:1238 — `oldpwd = pwd;` snapshot pre-cd $PWD for $OLDPWD.
1981 // Read from paramtab (the canonical zsh-side `pwd`
1982 // global); was reading OS env which can lag behind.
1983 let old = getsparam("PWD");
1984 // c:1207-1245 — compute the new logical pwd (`new_pwd` in C's
1985 // cd_new_pwd) up front so the PUSHDIGNOREDUPS scan below and the
1986 // final `pwd =` write share one value. Hoisted from the old
1987 // write site; behaviour is identical (chase → resolved cwd,
1988 // absolute dest → as-is, relative dest → logical join).
1989 let chase = CHASINGLINKS.load(Relaxed) != 0; // c:1203
1990 let new_pwd_logical: String = if chase {
1991 match env::current_dir() {
1992 Ok(c) => c.to_string_lossy().into_owned(),
1993 Err(_) => dest_path.clone(),
1994 }
1995 } else if dest_path.starts_with('/') {
1996 dest_path.clone()
1997 } else {
1998 let mut segs: Vec<&str> = if pre_pwd.is_empty() || pre_pwd == "/" {
1999 Vec::new()
2000 } else {
2001 pre_pwd.trim_start_matches('/').split('/').collect()
2002 };
2003 for part in dest_path.split('/') {
2004 match part {
2005 "" | "." => {}
2006 ".." => {
2007 segs.pop();
2008 }
2009 _ => segs.push(part),
2010 }
2011 }
2012 if segs.is_empty() {
2013 "/".to_string()
2014 } else {
2015 format!("/{}", segs.join("/"))
2016 }
2017 };
2018 // c:1221-1235 — reconcile the LOGICAL path with the directory we are
2019 // actually standing in:
2020 // if (stat(unmeta(new_pwd), &st1) < 0) {
2021 // new_pwd = metafy(zgetcwd(), -1, META_DUP);
2022 // } else if (stat(".", &st2) < 0) {
2023 // if (chdir(unmeta(new_pwd)) < 0)
2024 // zwarn("unable to chdir(%s): %e", new_pwd, errno);
2025 // } else if (st1.st_ino != st2.st_ino || st1.st_dev != st2.st_dev) {
2026 // if (chasinglinks) new_pwd = metafy(zgetcwd(), -1, META_DUP);
2027 // else if (chdir(unmeta(new_pwd)) < 0)
2028 // zwarn("unable to chdir(%s): %e", new_pwd, errno);
2029 // }
2030 // The first arm is what makes `cd .` recover after the cwd was renamed
2031 // out from under the shell: the textual `$PWD/.` no longer stats, so
2032 // zsh falls back to `getcwd()` and $PWD becomes the NEW name. Without
2033 // it $PWD kept the stale path forever.
2034 let mut new_pwd_logical = new_pwd_logical;
2035 match std::fs::metadata(&new_pwd_logical) {
2036 Err(_) => {
2037 // c:1221-1224
2038 if let Ok(c) = env::current_dir() {
2039 new_pwd_logical = c.to_string_lossy().into_owned(); // c:1224
2040 }
2041 }
2042 Ok(st1) => {
2043 use std::os::unix::fs::MetadataExt as _;
2044 match std::fs::metadata(".") {
2045 // c:1225-1227 — the cwd itself is gone; move to new_pwd.
2046 Err(_) => {
2047 if env::set_current_dir(&new_pwd_logical).is_err() {
2048 zwarn(&format!(
2049 // c:1227
2050 "unable to chdir({}): {}",
2051 new_pwd_logical,
2052 crate::ported::utils::zsh_errno_msg(
2053 std::io::Error::last_os_error().raw_os_error().unwrap_or(0)
2054 )
2055 ));
2056 }
2057 }
2058 // c:1228 — the logical path names a DIFFERENT directory
2059 // than the one we are in.
2060 Ok(st2) => {
2061 if st1.ino() != st2.ino() || st1.dev() != st2.dev() {
2062 if chase {
2063 // c:1229-1232
2064 if let Ok(c) = env::current_dir() {
2065 new_pwd_logical = c.to_string_lossy().into_owned();
2066 // c:1232
2067 }
2068 } else if env::set_current_dir(&new_pwd_logical).is_err() {
2069 zwarn(&format!(
2070 // c:1234
2071 "unable to chdir({}): {}",
2072 new_pwd_logical,
2073 crate::ported::utils::zsh_errno_msg(
2074 std::io::Error::last_os_error().raw_os_error().unwrap_or(0)
2075 )
2076 ));
2077 }
2078 }
2079 }
2080 }
2081 }
2082 }
2083 // c:Src/builtin.c:849 + cd_new_pwd dirstack maintenance —
2084 // collapsed into a single post-cd update here since the Rust
2085 // cd_get_dest returns a String rather than a LinkNode that
2086 // could be pre-pushed onto dirstack:
2087 // * BIN_PUSHD: push pre-cd pwd to top so subsequent
2088 // `${dirstack[1]}` and `popd` see the previous directory.
2089 // * BIN_POPD: pop dirstack[0] (the directory we just left,
2090 // which cd_get_dest read from the stack to compute dest).
2091 // * BIN_CD: dirstack unchanged unless AUTO_PUSHD is set, in
2092 // which case the CD behaves like a pushd.
2093 let mut skip_popd_n_cd = false;
2094 {
2095 let autopushd = isset(AUTOPUSHD);
2096 // c:1187 cd_new_pwd — rolllist(dirstack, dir) for BIN_PUSHD then
2097 // remnode(dirstack, dir). For `pushd +N`/`-N`, the rotation moves
2098 // entries before the target to the END of dirstack rather than
2099 // simply inserting pre_pwd at the front. Bug #302 in docs/BUGS.md:
2100 // previous Rust port unconditionally inserted pre_pwd at position
2101 // 0, which is correct for `pushd <path>` but wrong for `pushd +N`
2102 // (left target entry still in DIRSTACK at its old position).
2103 let is_stack_rotate = argv.len() == 1
2104 && !isset(POSIXCD)
2105 && argv[0].len() > 1
2106 && (argv[0].starts_with('+') || argv[0].starts_with('-'))
2107 && argv[0][1..].chars().all(|c| c.is_ascii_digit());
2108 if let Ok(mut d) = DIRSTACK.lock() {
2109 if is_stack_rotate && func == BIN_PUSHD {
2110 // c:1190 — rolllist(dirstack, dir) + remnode. Rotate the
2111 // virtual full stack `[pre_pwd] ++ DIRSTACK_old` so the
2112 // target entry becomes firstnode (= new PWD); remaining
2113 // entries form new DIRSTACK in their rotated order.
2114 let dd: usize = argv[0][1..].parse().unwrap_or(0);
2115 let pushdminus = isset(PUSHDMINUS);
2116 let from_top = (argv[0].starts_with('+')) ^ pushdminus;
2117 let m = d.len();
2118 let n = m + 1;
2119 let k = if from_top { dd } else { n - 1 - dd };
2120 if k < n {
2121 let mut full: Vec<String> = Vec::with_capacity(n);
2122 full.push(pre_pwd.clone());
2123 full.extend(d.iter().cloned());
2124 let rotated: Vec<String> =
2125 full[k..].iter().chain(full[..k].iter()).cloned().collect();
2126 d.clear();
2127 d.extend(rotated.into_iter().skip(1));
2128 }
2129 } else if is_stack_rotate && func == BIN_CD {
2130 // c:1190-1199 — `cd +N`/`-N` never rolls the list (rolllist
2131 // is BIN_PUSHD only). On the C stack `[pwd] ++ DIRSTACK`
2132 // (pwd pushed at c:849) it does `remnode(dirstack, dir)` on
2133 // the target, then without AUTO_PUSHD
2134 // `zsfree(getlinknode(dirstack))` drops the new front node.
2135 // So with AUTO_PUSHD the old pwd stays on top of the rest in
2136 // order; without it the old pwd goes, and for `cd +0` (the
2137 // target WAS the pushed pwd) the front entry below it goes.
2138 let dd: usize = argv[0][1..].parse().unwrap_or(0);
2139 let pushdminus = isset(PUSHDMINUS);
2140 let from_top = (argv[0].starts_with('+')) ^ pushdminus;
2141 let m = d.len();
2142 let n = m + 1;
2143 let k = if from_top { dd } else { n - 1 - dd };
2144 if k == 0 {
2145 if !autopushd && !d.is_empty() {
2146 d.remove(0); // c:1199 getlinknode
2147 }
2148 } else if k - 1 < d.len() {
2149 d.remove(k - 1); // c:1195 remnode(dirstack, dir)
2150 if autopushd {
2151 d.insert(0, pre_pwd.clone()); // c:849 pushed pwd kept
2152 }
2153 }
2154 } else if is_stack_rotate && func == BIN_POPD {
2155 // c:Src/builtin.c:872-936 + c:1197-1199 — `popd +N` /
2156 // `popd -N`: when the target +N/-N is NOT firstnode
2157 // (i.e., N != 0 / N != top-of-stack), remove the
2158 // target entry from dirstack WITHOUT changing PWD or
2159 // pushing pre_pwd to the stack. C's logic:
2160 // target = dir (the +N resolved node);
2161 // if (dir != firstnode(dirstack)) return dir;
2162 // // … cd_do_chdir path runs only if target IS top
2163 //
2164 // The Rust dispatch reached here for the
2165 // "target-not-top" case after cd_get_dest already
2166 // resolved to dirstack[k-1]. Just remove that index
2167 // and skip the cd / pre_pwd-insert paths.
2168 // Bug #466 in docs/BUGS.md.
2169 let dd: usize = argv[0][1..].parse().unwrap_or(0);
2170 let pushdminus = isset(PUSHDMINUS);
2171 let from_top = (argv[0].starts_with('+')) ^ pushdminus;
2172 let m = d.len();
2173 let n = m + 1;
2174 // popd's stack-rotate-vs-top logic mirrors C: the
2175 // virtual stack is pre_pwd ++ DIRSTACK_old; position 0
2176 // is the CURRENT pwd, so "popd +0" / "popd +N where
2177 // N maps to top" pops the top (handled by the
2178 // existing BIN_POPD arm below). For N that resolves
2179 // to a non-top dirstack index, remove from dirstack
2180 // directly.
2181 let k = if from_top { dd } else { n - 1 - dd };
2182 if k == 0 {
2183 // c:934-940 + c:1197-1203 — the +N/-N resolved to
2184 // firstnode (the current PWD): this is a bare-`popd`
2185 // top removal. Pop the entry below PWD (DIRSTACK[0])
2186 // and let the normal cd path below move PWD into it
2187 // (cd_get_dest resolved dest to DIRSTACK[0]). Do NOT
2188 // set skip_popd_n_cd — PWD must change here.
2189 // Bug: prior port's `k >= 1` guard fell through this
2190 // else-if arm without popping, so the plain-POPD arm
2191 // was never reached and the top entry survived.
2192 if !d.is_empty() {
2193 d.remove(0);
2194 }
2195 } else if k - 1 < d.len() {
2196 d.remove(k - 1);
2197 // Override the cd path: keep PWD at pre_pwd so
2198 // the surrounding flow doesn't cd to the removed
2199 // entry. The destination passed to bin_cd was
2200 // the removed entry; we overwrite the post-cd
2201 // PWD write below to pre_pwd.
2202 skip_popd_n_cd = true;
2203 }
2204 } else if func == BIN_PUSHD || (func == BIN_CD && autopushd) {
2205 // c:849 — push pre-cd pwd.
2206 d.insert(0, pre_pwd.clone());
2207 } else if func == BIN_POPD && argv.len() == 1 {
2208 // c:908 + c:928-931 — `popd <non-index>`: the pushed node
2209 // is removed again and pwd stays on top, so the stack is
2210 // unchanged (see cd_get_dest).
2211 } else if func == BIN_POPD {
2212 // c:1197-1199 — pop top of stack (the dir we left).
2213 if !d.is_empty() {
2214 d.remove(0);
2215 }
2216 }
2217 // c:1214-1222 — PUSHDIGNOREDUPS runs inside cd_new_pwd for
2218 // EVERY func (plain cd, pushd, popd — the block is not
2219 // guarded by func), scanning the now-current dirstack for
2220 // the FIRST entry equal to `new_pwd` (the directory just
2221 // moved into) and removing it so a directory never appears
2222 // twice on the stack:
2223 // if (isset(PUSHDIGNOREDUPS)) {
2224 // for (n = firstnode(dirstack); n; incnode(n))
2225 // if (!strcmp(new_pwd, getdata(n))) {
2226 // zsfree(remnode(dirstack, n)); break; }
2227 // }
2228 // Prior port only checked pushd and compared d.first()
2229 // against pre_pwd (wrong target, wrong action), so a plain
2230 // `cd` into a dir already on the stack, or re-pushing such
2231 // a dir, left the stale duplicate in place. The non-top
2232 // `popd +N` path (skip_popd_n_cd) keeps PWD = pre_pwd, so
2233 // its dedup runs there against pre_pwd (see below).
2234 if !skip_popd_n_cd && isset(PUSHDIGNOREDUPS) {
2235 if let Some(pos) = d.iter().position(|s| *s == new_pwd_logical) {
2236 d.remove(pos);
2237 }
2238 }
2239 }
2240 }
2241 // c:Src/builtin.c:872-936 — `popd +N` (N != 0) removes the
2242 // target entry from dirstack WITHOUT changing PWD. cd_do_chdir
2243 // already chdir'd into the removed target's path; revert that
2244 // back to pre_pwd and skip the OLDPWD update + PWD write so the
2245 // post-popd CWD remains the pre-popd PWD. Bug #466 in
2246 // docs/BUGS.md.
2247 if skip_popd_n_cd {
2248 // Restore process cwd to pre_pwd. Ignore errors; the
2249 // dirstack mutation already happened.
2250 let _ = env::set_current_dir(&pre_pwd);
2251 // c:1242-1246 — cd_new_pwd runs `oldpwd = pwd; pwd = new_pwd`
2252 // for EVERY popd, including a non-top `popd +N`. There
2253 // new_pwd resolves back to the front-of-stack pre_pwd, so
2254 // PWD is unchanged but OLDPWD becomes the pre-popd PWD.
2255 // Prior port left OLDPWD stale (whatever an earlier cd/pushd
2256 // set), diverging on any `$OLDPWD` / `cd -` that followed.
2257 crate::ported::jobs::setjobpwd(); // c:1241 — before `pwd = new_pwd`
2258 crate::dash_mode::set_oldpwd_global(&pre_pwd); // c:1239
2259 setsparam("OLDPWD", &pre_pwd);
2260 env::set_var("OLDPWD", &pre_pwd);
2261 // c:Src/builtin.c:1245-1252 — print dirstack on POPD unless
2262 // quiet. Pass func=BIN_POPD so the dirstack-print branch
2263 // still fires for the listing (matches zsh's `popd +N` echo
2264 // of the resulting stack).
2265 cd_new_pwd(func, 0, OPT_ISSET(ops, b'q') as i32);
2266 unqueue_signals();
2267 return 0;
2268 }
2269 if let Some(o) = old {
2270 // c:1239 oldpwd = pwd
2271 // c:1239 + setsparam path: write OLDPWD to paramtab so
2272 // subsequent expansions of $OLDPWD see the new value
2273 // (the OS env write below is the export side; the
2274 // shell-side read must come from paramtab).
2275 crate::dash_mode::set_oldpwd_global(&o); // c:1239 oldpwd = pwd
2276 setsparam("OLDPWD", &o);
2277 env::set_var("OLDPWD", &o);
2278 }
2279 // c:1241 — `setjobpwd();` stamps every live job with the directory
2280 // it was started in, while $PWD still holds the pre-cd value.
2281 crate::ported::jobs::setjobpwd();
2282 // c:1241 — `pwd = new_pwd;` writes the LOGICAL path (the dest
2283 // argument as given to cd, not `getcwd()`). Computed once up front
2284 // (see `new_pwd_logical` above) so the PUSHDIGNOREDUPS scan and this
2285 // write agree. Symlink resolution only kicks in when `chasinglinks`
2286 // is set (c:1203-1208, c:1228-1231).
2287 let pwd: String = new_pwd_logical;
2288 // c:1242 — `setsparam("PWD", pwd);` + export side via env.
2289 setsparam("PWD", &pwd);
2290 env::set_var("PWD", &pwd);
2291 cd_new_pwd(func, 0, OPT_ISSET(ops, b'q') as i32); // c:856
2292
2293 unqueue_signals(); // c:858
2294 0 // c:859
2295}
2296
2297/// Port of `cd_get_dest()` from `Src/builtin.c:865`.
2298/// C decl: `cd_get_dest(char *nam, char **argv, int hard, int func)`
2299/// C: `static LinkNode cd_get_dest(char *nam, char **argv, int hard,
2300/// int func)` — resolve the `cd` argument (`-`, `+N`/`-N`,
2301/// bare → $HOME, two-arg substitution form) to a destination path.
2302/// Returns the resolved path on success, None on error (with the
2303/// appropriate zwarnnam already emitted).
2304/// WARNING: param names don't match C — Rust=() vs C=(nam, argv, hard, func)
2305pub fn cd_get_dest(nam: &str, argv: &[String], _hard: bool, func: i32) -> Option<String> {
2306 if argv.is_empty() {
2307 // c:872 — bare popd / pushd / cd (no args).
2308 // The Rust port doesn't pre-push pwd to dirstack inside bin_cd
2309 // (cd_get_dest's String return signature doesn't fit C's
2310 // pre-push pattern), so dirstack[0] here is the most-recent
2311 // pushed entry, not the temporary scratch slot the C source
2312 // sees. Adjust the indices accordingly: C reads index 1
2313 // (skipping the scratch), Rust reads index 0.
2314 if func == BIN_POPD {
2315 let depth = DIRSTACK.lock().map(|d| d.len()).unwrap_or(0);
2316 if depth < 1 {
2317 zwarnnam(nam, "directory stack empty");
2318 return None;
2319 }
2320 return DIRSTACK.lock().ok().and_then(|d| d.first().cloned());
2321 }
2322 if func == BIN_PUSHD {
2323 // c:876-879 — bare pushd without PUSHDTOHOME swaps top two
2324 // entries of dirstack. C represents dirstack with PWD at
2325 // index 0, so it walks `nextnode(firstnode(dirstack))` to
2326 // get index 1, then extracts index 0 via `getlinknode` and
2327 // reinserts after — net swap of [0]<->[1]. zshrs's DIRSTACK
2328 // omits PWD entirely (PWD lives in paramtab); index 0 here
2329 // = C's index 1.
2330 //
2331 // To achieve the same swap in this model: POP DIRSTACK[0]
2332 // (the saved target) and return it. cd_new_pwd at line ~1573
2333 // then re-pushes pre_pwd onto DIRSTACK[0]. Net result:
2334 // DIRSTACK[0] becomes pre_pwd, PWD becomes the popped value.
2335 //
2336 // Bug #67 in docs/BUGS.md: prior port read `d.first().cloned()`
2337 // (no pop), so cd_new_pwd's push added a 3rd entry instead
2338 // of swapping.
2339 //
2340 // c:881-885 — empty dirstack (only PWD in C's model, i.e.
2341 // DIRSTACK.is_empty() here) falls through to HOME below;
2342 // cd_new_pwd's push then leaves DIRSTACK = [pre_pwd].
2343 let pushdtohome = isset(PUSHDTOHOME);
2344 if !pushdtohome {
2345 let popped = DIRSTACK.lock().ok().and_then(|mut d| {
2346 if d.is_empty() {
2347 None
2348 } else {
2349 Some(d.remove(0))
2350 }
2351 });
2352 if let Some(target) = popped {
2353 return Some(target);
2354 }
2355 // Empty DIRSTACK → fall through to HOME below.
2356 }
2357 }
2358 // c:880-884 — fall through to $HOME.
2359 match getsparam("HOME") {
2360 Some(h) if !h.is_empty() => Some(h),
2361 _ => {
2362 zwarnnam(nam, "HOME not set");
2363 None
2364 }
2365 }
2366 } else if argv.len() == 1 {
2367 // c:887
2368 let arg = &argv[0];
2369 DOPRINTDIR.fetch_add(1, Relaxed); // c:891
2370 // c:892-908 — `+N`/`-N` numeric stack-index form.
2371 let posixcd = isset(POSIXCD);
2372 if !posixcd
2373 && arg.len() > 1
2374 && (arg.starts_with('+') || arg.starts_with('-'))
2375 && arg[1..].chars().all(|c| c.is_ascii_digit())
2376 {
2377 let dd: usize = arg[1..].parse().unwrap_or(0); // c:894
2378 let pushdminus = isset(PUSHDMINUS);
2379 let from_top = (arg.starts_with('+')) ^ pushdminus; // c:898
2380 // c:Src/builtin.c:904 — out-of-range stack index emits
2381 // "no such entry in dir stack". Previous Rust port
2382 // returned None silently and bin_cd's caller exited 1
2383 // with no stderr, breaking parity.
2384 // c:899-903 — index into the FULL dirstack: in zsh
2385 // `dirstack` is a LinkList with current PWD at firstnode,
2386 // so `+N` advances N nodes from firstnode (0 = current,
2387 // 1 = first non-current entry) and `-N` walks back N nodes
2388 // from lastnode (0 = last non-current entry). zshrs's
2389 // DIRSTACK omits PWD entirely (PWD lives in paramtab), so
2390 // build the virtual full stack `[PWD] ++ DIRSTACK` to index
2391 // against. Bug #302 in docs/BUGS.md: previous Rust port
2392 // indexed DIRSTACK directly, so `+1` returned DIRSTACK[1]
2393 // instead of DIRSTACK[0], and `+0`/`-len` errored instead of
2394 // returning PWD.
2395 let pwd_now = getsparam("PWD").unwrap_or_else(|| zgetcwd());
2396 let resolved = DIRSTACK.lock().ok().and_then(|d| {
2397 let m = d.len();
2398 let n = m + 1; // full virtual stack length (incl current)
2399 let k = if from_top {
2400 if dd >= n {
2401 return None;
2402 }
2403 dd
2404 } else {
2405 if dd >= n {
2406 return None;
2407 }
2408 n - 1 - dd
2409 };
2410 if k == 0 {
2411 // c:934-940 — for BIN_POPD, dir==firstnode(dirstack)
2412 // (the current PWD) is NOT removed in place: C does
2413 // `dir = nextnode(dir)` and cd_do_chdir's into the
2414 // entry BELOW PWD, then cd_new_pwd (c:1197-1199)
2415 // pops the front. So the chdir destination is
2416 // DIRSTACK[0] (the first non-current entry), not PWD.
2417 // For BIN_CD / BIN_PUSHD, +0 (or the swapped -last)
2418 // resolves to the current dir itself.
2419 // Bug: prior port returned pwd_now for every func, so
2420 // `popd +0` (and `popd +N`/`-N` mapping to firstnode
2421 // under PUSHD_MINUS) no-op'd instead of popping the top.
2422 if func == BIN_POPD {
2423 d.first().cloned().or_else(|| Some(pwd_now.clone()))
2424 } else {
2425 Some(pwd_now.clone())
2426 }
2427 } else {
2428 d.get(k - 1).cloned()
2429 }
2430 });
2431 if resolved.is_none() {
2432 zwarnnam(nam, "no such entry in dir stack");
2433 }
2434 return resolved;
2435 }
2436 // c:908-909 — `zpushnode(dirstack, ztrdup(strcmp(argv[0],"-")
2437 // ? (doprintdir--, argv[0]) : oldpwd))`. `strcmp(argv[0],"-")`
2438 // is non-zero for a normal dir, so a normal `cd dir` does
2439 // `doprintdir--` (cancelling the c:891 `doprintdir++` → no
2440 // print), while `cd -` keeps doprintdir set → prints $OLDPWD.
2441 // C reads the `oldpwd` global; route through `$OLDPWD`.
2442 let pushed = if arg == "-" {
2443 // c:909 — `: oldpwd` — no doprintdir--, so `cd -` prints.
2444 crate::dash_mode::oldpwd_cd_target()
2445 } else {
2446 // c:909 — `? (doprintdir--, argv[0])`.
2447 DOPRINTDIR.fetch_sub(1, Relaxed);
2448 Some(arg.clone())
2449 };
2450 // c:928-931 — `if (func == BIN_POPD) { if (!dir) { target = dir =
2451 // firstnode(dirstack); } ... dir = nextnode(dir); }`. For popd the
2452 // node just pushed at c:908 is only the target to remove; the
2453 // chdir goes to the next node, the pwd bin_cd pushed at c:849.
2454 // So `popd foo` / `popd -` stays in the current directory and
2455 // leaves the stack alone (cd_new_pwd c:1195-1199 removes the
2456 // pushed node, then takes pwd back off the front).
2457 if func == BIN_POPD {
2458 return Some(getsparam("PWD").unwrap_or_else(|| zgetcwd()));
2459 }
2460 pushed
2461 } else {
2462 // c:914-924 — two-arg substitution: cd OLDPATTERN NEWPATTERN.
2463 // C reads `pwd` global / `$PWD` param via getsparam;
2464 // fall back to getcwd if the param isn't populated.
2465 let pwd = getsparam("PWD").unwrap_or_else(|| zgetcwd());
2466 let pat = &argv[0];
2467 let new_pat = &argv[1];
2468 match pwd.find(pat.as_str()) {
2469 // c:917
2470 None => {
2471 zwarnnam(nam, &format!("string not in pwd: {}", pat)); // c:918
2472 None // c:919
2473 }
2474 Some(idx) => {
2475 // c:921-924 — splice: pwd[..idx] + new_pat + pwd[idx+pat.len()..]
2476 let mut out = String::new();
2477 out.push_str(&pwd[..idx]); // c:921
2478 out.push_str(new_pat); // c:922
2479 out.push_str(&pwd[idx + pat.len()..]); // c:923
2480 DOPRINTDIR.fetch_add(1, Relaxed);
2481 Some(out)
2482 }
2483 }
2484 }
2485}
2486
2487/// Port of `cd_do_chdir()` from `Src/builtin.c:967`.
2488/// C decl: `cd_do_chdir(char *cnam, char *dest, int hard)`
2489/// C: `static char *cd_do_chdir(char *cnam, char *dest, int hard)` —
2490/// resolve `dest` (handling cdpath, cdablevars, leading `~`/`.`),
2491/// chdir there, return the LOGICAL path used (not `getcwd`'d) or
2492/// NULL on error.
2493///
2494/// Per C `cd_try_chdir` (c:1116-1181), the return is `buf` — the
2495/// composed path the chdir was attempted against, after `fixdir()`
2496/// logical-normalisation (resolving `.`/`..` only, NOT symlinks).
2497/// Walks $cdpath when dest is relative and not `./` or `../`.
2498pub fn cd_do_chdir(cnam: &str, dest: &str, hard: i32) -> Option<String> {
2499 // c:967
2500 // c:996-998 — nocdpath = first segment is "." or ".."
2501 let nocdpath = dest.starts_with("./") || dest == "." || dest.starts_with("../") || dest == "..";
2502
2503 // c:1003-1008 — absolute path: try as-is, warn on failure.
2504 if dest.starts_with('/') {
2505 if let Some(ret) = cd_try_chdir("", dest, hard) {
2506 return Some(ret);
2507 }
2508 // c:Src/builtin.c — `zwarnnam(nam, "%e: %s", errno, dest)`.
2509 // C's %e printf format expands to strerror(errno) then
2510 // LOWERCASES the first letter unless the errno is EIO
2511 // (Src/utils.c:362-368 comment: "If the message is not
2512 // about I/O problems, it looks better if we uncapitalize
2513 // the first letter of the message"). Previous Rust port
2514 // used `io::Error::last_os_error()`'s Display impl, which
2515 // appends ` (os error N)` — leaked the Rust-internal
2516 // suffix into user-visible cd output. Bug #112 in
2517 // docs/BUGS.md. Route through compat::strerror to drop
2518 // the suffix AND apply the lowercase-first-letter rule so
2519 // the message matches C exactly.
2520 let errno = io::Error::last_os_error().raw_os_error().unwrap_or(0);
2521 let mut msg = crate::ported::compat::strerror(errno);
2522 if errno != libc::EIO {
2523 if let Some(c) = msg.chars().next() {
2524 msg = format!("{}{}", c.to_ascii_lowercase(), &msg[c.len_utf8()..]);
2525 }
2526 }
2527 zwarnnam(cnam, &format!("{}: {}", msg, dest));
2528 return None;
2529 }
2530
2531 // c:1015-1018 — check $cdpath for "." (presence flips hasdot).
2532 let posix_cd = isset(POSIXCD);
2533 let cdpath_str = getsparam("CDPATH").unwrap_or_default();
2534 let cdpath: Vec<&str> = if cdpath_str.is_empty() {
2535 Vec::new()
2536 } else {
2537 cdpath_str.split(':').collect()
2538 };
2539 let hasdot = !nocdpath && !posix_cd && cdpath.iter().any(|p| p.is_empty() || *p == ".");
2540 let mut eno = libc::ENOENT; // c:990 — `int hasdot = 0, eno = ENOENT;`
2541
2542 // c:1026-1031 — if no dot in cdpath (and !POSIXCD), try as-is first.
2543 if !hasdot && !posix_cd {
2544 if let Some(ret) = cd_try_chdir("", dest, hard) {
2545 return Some(ret);
2546 }
2547 // c:1030-1031 — `if (errno != ENOENT) eno = errno;`
2548 let e = io::Error::last_os_error().raw_os_error().unwrap_or(0);
2549 if e != libc::ENOENT {
2550 eno = e;
2551 }
2552 }
2553
2554 // c:1034-1043 — walk $cdpath unless nocdpath.
2555 if !nocdpath {
2556 for pp in cdpath.iter() {
2557 if let Some(ret) = cd_try_chdir(pp, dest, hard) {
2558 // c:1037-1050 — flag that the resolved directory should
2559 // be printed; the actual print happens later in
2560 // cd_new_pwd (c:1248-1251) gated on doprintdir. C does
2561 // NOT print here, it only bumps `doprintdir`.
2562 if isset(POSIXCD) {
2563 // c:1037-1045 — POSIX prints any time CDPATH was
2564 // used, except for an empty segment treated as ".".
2565 if !pp.is_empty() {
2566 DOPRINTDIR.fetch_add(1, Relaxed); // c:1045
2567 }
2568 } else if *pp != "." {
2569 // c:1046-1049 — non-POSIX: print only for a
2570 // non-"." segment.
2571 DOPRINTDIR.fetch_add(1, Relaxed); // c:1048
2572 }
2573 return Some(ret);
2574 }
2575 // c:1052-1053 — `if (errno != ENOENT) eno = errno;`
2576 let e = io::Error::last_os_error().raw_os_error().unwrap_or(0);
2577 if e != libc::ENOENT {
2578 eno = e;
2579 }
2580 }
2581 }
2582
2583 // c:1057-1063 — POSIXCD-mode last-resort: try dest as-is.
2584 if posix_cd {
2585 if let Some(ret) = cd_try_chdir("", dest, hard) {
2586 return Some(ret);
2587 }
2588 // c:1062-1063
2589 let e = io::Error::last_os_error().raw_os_error().unwrap_or(0);
2590 if e != libc::ENOENT {
2591 eno = e;
2592 }
2593 }
2594
2595 // c:1067-1075 — `cd_able_vars(dest)` — when CDABLEVARS is set,
2596 // look up the leading bareword as a parameter and try chdir to
2597 // its value (with any `/tail` appended). The helper at
2598 // `cd_able_vars` (this file, line 1881) already guards on
2599 // `isset(CDABLEVARS)` and returns None when the option is off
2600 // OR the head isn't a paramtab entry. Without this call
2601 // wired in, `setopt cdable_vars; mytmp=/tmp; cd mytmp` errored
2602 // with "no such file or directory" instead of cd'ing to /tmp.
2603 // Bug #217 in docs/BUGS.md.
2604 if let Some(expanded) = cd_able_vars(dest) {
2605 if let Some(ret) = cd_try_chdir("", &expanded, hard) {
2606 // c:1069 — `doprintdir++` so cd_new_pwd prints the
2607 // CDABLEVARS-resolved directory.
2608 DOPRINTDIR.fetch_add(1, Relaxed); // c:1069
2609 return Some(ret);
2610 }
2611 // c:1072-1073
2612 let e = io::Error::last_os_error().raw_os_error().unwrap_or(0);
2613 if e != libc::ENOENT {
2614 eno = e;
2615 }
2616 }
2617
2618 // c:1080 — `zwarnnam(cnam, "%e: %s", eno, dest);`. `%e` is
2619 // strerror of the first non-ENOENT failure seen (c:1030, 1052,
2620 // 1062, 1072), so `cd <file>` reports ENOTDIR and an unreadable
2621 // directory EACCES rather than a blanket ENOENT.
2622 zwarnnam(cnam, &format!("{}: {}", crate::ported::utils::zsh_errno_msg(eno), crate::ported::utils::nicedupstring(&dest))); // c:1080 `%s` → nicezputs (c:Src/utils.c:316)
2623 None
2624}
2625
2626/// Port of `cd_able_vars()` from `Src/builtin.c:1088`.
2627/// C decl: `cd_able_vars(char *s)`
2628/// C: `char *cd_able_vars(char *s)` — when CDABLEVARS is set, look up
2629/// the leading bareword as a parameter and return its expanded value
2630/// prefixed in front of any trailing `/...`. Returns NULL otherwise.
2631pub fn cd_able_vars(s: &str) -> Option<String> {
2632 // c:1088
2633 // c:1088 — `if (isset(CDABLEVARS)) { ... }`
2634 let cdablevars = isset(CDABLEVARS);
2635 if !cdablevars {
2636 // c:1093
2637 return None;
2638 }
2639 // c:1094-1110 — split on the first `/`, look up the head as $param.
2640 let (head, tail) = match s.find('/') {
2641 // c:1094
2642 Some(i) => (&s[..i], &s[i..]),
2643 None => (s, ""),
2644 };
2645 if head.is_empty() {
2646 return None;
2647 }
2648 // c:1097 — `s = getnameddir(s);`. The head is resolved as a NAMED
2649 // DIRECTORY, not as a raw parameter read: getnameddir
2650 // (c:Src/utils.c:1257-1260) only accepts a parameter whose value begins
2651 // with `/`, so `cdablevar1=tmpcd; cd cdablevar1` fails with "no such
2652 // file or directory" while `cdablevar2=$PWD/tmpcd` works.
2653 // c:1100-1101 — `if (s && *rest) s = dyncat(s, rest);`
2654 crate::ported::utils::getnameddir(head).map(|val| format!("{}{}", val, tail))
2655}
2656
2657/// Port of `cd_try_chdir()` from `Src/builtin.c:1116`.
2658/// C decl: `cd_try_chdir(char *pfix, char *dest, int hard)`
2659/// Compose `pfix/dest` (or `pwd/pfix/dest` for relative pfix, or
2660/// `pwd/dest` for empty pfix + relative dest), normalise via `fixdir`,
2661/// then attempt chdir. Falls back to `dest` alone when the full path
2662/// fails but `pfix` was present (cwd/parent may have been renamed).
2663pub fn cd_try_chdir(pfix: &str, dest: &str, hard: i32) -> Option<String> {
2664 // c:1116
2665 let pwd = getsparam("PWD").unwrap_or_default();
2666
2667 // c:1122-1158 — build buf from pfix/dest/pwd combinations.
2668 let mut buf = if !pfix.is_empty() {
2669 if pfix.starts_with('/') {
2670 // c:1123
2671 // c:1133 — buf = tricat(pfix, "/", dest); `/` + `tmp` stays `//tmp`
2672 // until fixdir (or the dash double-slash rule) decides.
2673 format!("{}/{}", pfix, dest)
2674 } else {
2675 // c:1135-1146 — pwd + "/" + pfix + "/" + dest
2676 let pwd_trim = if pwd == "/" { "" } else { pwd.as_str() };
2677 format!("{}/{}/{}", pwd_trim, pfix, dest)
2678 }
2679 } else if dest.starts_with('/') {
2680 // c:1148
2681 // c:1149 — buf = ztrdup(dest)
2682 dest.to_string()
2683 } else {
2684 // c:1150
2685 // c:1151-1157 — pwd + "/" + dest (trimming trailing slash off pwd)
2686 let pwd_trim = pwd.trim_end_matches('/');
2687 format!("{}/{}", pwd_trim, dest)
2688 };
2689
2690 // c:1161-1166 — fixdir normalisation, skipped if chasing symlinks.
2691 let mut dochaselinks = 0; // c:1120
2692 // !!! DASH-ONLY (no C counterpart) !!! POSIX lets a path with exactly two
2693 // leading slashes keep them; dash does (`CDPATH=/; cd tmp` -> `//tmp`,
2694 // `cd //tmp` -> `//tmp`, `cd ///tmp` -> `/tmp`) where fixdir collapses
2695 // them to one.
2696 let keep_double_slash =
2697 crate::dash_mode::dash_faithful() && buf.starts_with("//") && !buf.starts_with("///");
2698 if CHASINGLINKS.load(Relaxed) == 0 {
2699 dochaselinks = fixdir(&mut buf); // c:1165
2700 if keep_double_slash && !buf.starts_with("//") {
2701 buf.insert(0, '/');
2702 }
2703 }
2704
2705 // c:1169-1177 — "We try the full path first. If that fails, try the
2706 // argument to cd relatively. This is useful if the cwd or a parent
2707 // directory is renamed in the interim." The relative fallback is
2708 // skipped when a prefix was applied or `dest` is already absolute.
2709 // Either success returns `buf` (the logical full path) — c:1181.
2710 if lchdir(&buf, None, hard) != 0
2711 && (!pfix.is_empty() || dest.starts_with('/') || lchdir(dest, None, hard) != 0)
2712 {
2713 return None; // c:1175-1176
2714 }
2715 // c:1178-1180 — the chdir succeeded, so decide if we should force
2716 // links to be chased
2717 if dochaselinks != 0 {
2718 CHASINGLINKS.store(1, Relaxed);
2719 }
2720 Some(buf) // c:1181 — metafy(buf, ...)
2721}
2722
2723/// Port of `cd_new_pwd()` from `Src/builtin.c:1187`.
2724/// C decl: `cd_new_pwd(int func, LinkNode dir, int quiet)`
2725/// C: `static void cd_new_pwd(int func, LinkNode dir, int quiet)` —
2726/// commit a new PWD: rotate dirstack on `BIN_PUSHD`, pop on
2727/// `BIN_POPD`, then setparam(PWD/OLDPWD), fire chpwd hooks.
2728///
2729/// The PWD/OLDPWD write is now done by the caller (`bin_cd`) using
2730/// the logical `dest_path` from `cd_get_dest`. C's body at c:1238-1242
2731/// reads `new_pwd` off the dirstack — the Rust port's dirstack
2732/// plumbing isn't faithful enough to carry that path here, so the
2733/// caller writes PWD directly. This fn handles only the post-write
2734/// side effects (chpwd hooks, dirstack size cap).
2735/// WARNING: param names don't match C — Rust=(_func, _dir, _quiet) vs C=(func, dir, quiet)
2736pub fn cd_new_pwd(func: i32, _dir: usize, quiet: i32) {
2737 // c:1187 — post-cd side effects (print, hooks). Dirstack
2738 // maintenance was moved into `bin_cd` because the Rust port's
2739 // cd_get_dest returns a String (not a LinkNode pre-pushed on
2740 // dirstack), so C's remnode/rolllist sequence doesn't fit.
2741
2742 // c:1236-1242 — shift PWD → OLDPWD, set new PWD.
2743 //
2744 // The caller (`bin_cd` at builtin.rs:1379-1408) already wrote
2745 // OLDPWD (pre-cd $PWD) and PWD (the logical `dest_path`) into the
2746 // paramtab. Re-deriving them here would (a) double-shift OLDPWD
2747 // (overwriting the correct pre-cd value with the just-set PWD),
2748 // and (b) re-set PWD via `std::env::current_dir()` which always
2749 // returns the resolved physical path — clobbering the logical
2750 // path on systems where the destination is a symlink (e.g. macOS
2751 // /tmp → /private/tmp). C `cd_new_pwd` reads `new_pwd` off the
2752 // dirstack (the path the user typed); zshrs's dirstack plumbing
2753 // doesn't carry that path here, so the caller is the authoritative
2754 // PWD writer and this fn must NOT re-write either parameter.
2755
2756 // c:1244-1252 — print the new directory.
2757 // if (isset(INTERACTIVE) || isset(POSIXCD)) {
2758 // if (func != BIN_CD && isset(INTERACTIVE)) {
2759 // if (unset(PUSHDSILENT) && !quiet) printdirstack();
2760 // } else if (unset(CDSILENT) && doprintdir) {
2761 // fprintdir(pwd, stdout); putchar('\n');
2762 // }
2763 // }
2764 if isset(INTERACTIVE) || isset(POSIXCD) {
2765 if func != BIN_CD && isset(INTERACTIVE) {
2766 // c:1245-1247 — pushd/popd echo the dir stack.
2767 if !isset(PUSHDSILENT) && quiet == 0 {
2768 printdirstack(); // c:1247
2769 }
2770 } else if !isset(crate::ported::zsh_h::CDSILENT) && DOPRINTDIR.load(Relaxed) != 0 {
2771 // c:1248-1251 — cd echoes the resolved directory (set via
2772 // doprintdir by `cd -`, CDPATH, autocd, CDABLEVARS).
2773 let pwd = getsparam("PWD").unwrap_or_default(); // C global `pwd`
2774 println!("{}", fprintdir(&pwd)); // c:1249-1250
2775 }
2776 }
2777
2778 /* execute the chpwd function */
2779 // c:1255-1261 — every directory change that commits here (cd, chdir,
2780 // pushd, popd, however the command word was reached) runs the hook,
2781 // unless `-q` asked for quiet.
2782 let _ = std::io::stdout().flush(); // c:1256
2783 let _ = std::io::stderr().flush(); // c:1257
2784 if quiet == 0 {
2785 // c:1258
2786 crate::ported::utils::callhookfunc("chpwd", None, 1, std::ptr::null_mut()); // c:1259
2787 if crate::ported::init::zle_load_state.load(std::sync::atomic::Ordering::SeqCst) == 1 {
2788 // c:1260
2789 crate::ported::init::zleentry(crate::ported::zsh_h::ZLE_CMD_CHPWD); // c:1261
2790 }
2791 }
2792
2793 // c:1264-1271 — trim the dir stack to $DIRSTACKSIZE.
2794 // `dirstacksize = getiparam("DIRSTACKSIZE");
2795 // if (dirstacksize > 0) {
2796 // int remove = countlinknodes(dirstack) -
2797 // (dirstacksize < 2 ? 2 : dirstacksize);
2798 // while (remove-- >= 0)
2799 // zsfree(remnode(dirstack, lastnode(dirstack)));
2800 // }`
2801 let dirstacksize = getiparam("DIRSTACKSIZE"); // c:1264
2802 if dirstacksize > 0 {
2803 // c:1266
2804 if let Ok(mut d) = DIRSTACK.lock() {
2805 // c:1267-1268 — count minus the effective cap (min 2).
2806 let cap = if dirstacksize < 2 { 2 } else { dirstacksize };
2807 let mut remove = d.len() as i64 - cap;
2808 // c:1269-1270 — `while (remove-- >= 0)` pops the last node;
2809 // the post-decrement removes (remove+1) entries from the end.
2810 while remove >= 0 {
2811 remove -= 1;
2812 if d.pop().is_none() {
2813 break;
2814 }
2815 }
2816 }
2817 }
2818}
2819
2820/// Port of `printdirstack()` from Src/builtin.c:1277.
2821/// C: `static void printdirstack(void)` — fprintdir(pwd) followed by
2822/// space-separated entries from the dirstack list, ending in newline.
2823pub fn printdirstack() {
2824 // c:1277
2825 // c:1281 — `fprintdir(pwd, stdout);`. C uses the shell-side
2826 // `pwd` global (in-shell logical cwd), not getcwd. Read
2827 // $PWD from paramtab so the logical path (including
2828 // any unresolved symlinks) shows correctly. Route
2829 // through `utils::fprintdir` for the same `~` /
2830 // `~named` abbreviation real zsh emits.
2831 // Previous Rust port emitted raw paths, missing the
2832 // $HOME / nameddirtab abbreviation that makes pushd/popd output
2833 // legible. Same fix family as bin_dirs.
2834 let pwd = getsparam("PWD")
2835 .or_else(|| {
2836 env::current_dir()
2837 .ok()
2838 .and_then(|p| p.to_str().map(String::from))
2839 })
2840 .unwrap_or_default();
2841 print!("{}", fprintdir(&pwd)); // c:1281
2842 // c:1282-1286 — `for (node = firstnode(dirstack); ...)`
2843 if let Ok(d) = DIRSTACK.lock() {
2844 for entry in d.iter() {
2845 // c:1282
2846 print!(" {}", fprintdir(entry)); // c:1284
2847 }
2848 }
2849 println!(); // c:1287
2850}
2851
2852/// Port of `fixdir()` from `Src/builtin.c:1298`.
2853/// C decl: `int fixdir(char *src)`
2854///
2855/// Canonicalises `src` IN PLACE: collapses `//`, drops `.` segments and
2856/// removes each `foo/..` pair — but only after `stat()` shows `foo` is a
2857/// directory; when it is not, the rest of the path is left as written and
2858/// the result is 1. Under CHASE_DOTS a `..` is kept as an ordinary segment
2859/// and the result is 1 as well. A non-zero result tells cd_try_chdir that
2860/// the new directory must be found by chasing links (c:1165, c:1179-1180).
2861pub fn fixdir(src: &mut String) -> i32 {
2862 let s: Vec<u8> = src.as_bytes().to_vec();
2863 let at = |i: usize| s.get(i).copied().unwrap_or(0);
2864 let mut dest: Vec<u8> = Vec::with_capacity(s.len()); // c:1300 d0 = dest
2865 // c:1301-1319 — relative change with the cwd removed from under us
2866 // (`pwd` is "."): force links to be chased. Set to 2 so the lack of
2867 // CHASEDOTS is still obeyed after the first "../" is preserved.
2868 let pwd = getsparam("PWD").unwrap_or_default();
2869 let mut chasedots: i32 = if at(0) == b'.'
2870 && pwd == "."
2871 && (at(1) == b'/' || (at(1) == b'.' && at(2) == b'/'))
2872 {
2873 2 // c:1319-1320
2874 } else {
2875 0
2876 };
2877 let done = |dest: Vec<u8>, src: &mut String| {
2878 *src = String::from_utf8_lossy(&dest).into_owned();
2879 };
2880 let mut i = 0usize;
2881 loop {
2882 // c:1334-1342 — compress multiple /es into single
2883 if at(i) == b'/' {
2884 dest.push(b'/');
2885 i += 1;
2886 while at(i) == b'/' {
2887 i += 1;
2888 }
2889 }
2890 // c:1343-1350 — at the end of the input path, remove a trailing /
2891 // (if it exists), and return ct
2892 if at(i) == 0 {
2893 while dest.len() > 1 && dest.last() == Some(&b'/') {
2894 dest.pop();
2895 }
2896 done(dest, src);
2897 return chasedots; // c:1349
2898 }
2899 if at(i) == b'.' && at(i + 1) == b'.' && (at(i + 2) == 0 || at(i + 2) == b'/') {
2900 if isset(crate::ported::zsh_h::CHASEDOTS) || chasedots > 1 {
2901 // c:1356-1358 — and treat as normal segment
2902 chasedots = 1;
2903 } else {
2904 if dest.len() > 1 {
2905 // c:1360-1377 — remove a foo/.. combination:
2906 // first check foo exists, else return.
2907 let d0 = String::from_utf8_lossy(&dest).into_owned();
2908 if !std::fs::metadata(&d0).map(|m| m.is_dir()).unwrap_or(false) {
2909 dest.extend_from_slice(&s[i..]); // c:1370-1373
2910 done(dest, src);
2911 return 1; // c:1374
2912 }
2913 // c:1376-1378 — `for (dest--; dest > d0 + 1 &&
2914 // dest[-1] != '/'; dest--); if (dest[-1] != '/') dest--;`
2915 let mut d = dest.len() - 1;
2916 while d > 1 && dest[d - 1] != b'/' {
2917 d -= 1;
2918 }
2919 if dest[d - 1] != b'/' {
2920 d -= 1;
2921 }
2922 dest.truncate(d);
2923 }
2924 // c:1380-1382 — `src++; while (*++src == '/'); continue;`
2925 i += 2;
2926 while at(i) == b'/' {
2927 i += 1;
2928 }
2929 continue;
2930 }
2931 }
2932 if at(i) == b'.' && (at(i + 1) == b'/' || at(i + 1) == 0) {
2933 // c:1385-1387 — skip a . section
2934 i += 1;
2935 while at(i) == b'/' {
2936 i += 1;
2937 }
2938 } else {
2939 // c:1388-1392 — copy a normal segment into the output
2940 while at(i) != b'/' && at(i) != 0 {
2941 dest.push(s[i]);
2942 i += 1;
2943 }
2944 }
2945 }
2946}
2947
2948/// Port of `printqt()` from `Src/builtin.c:1399`.
2949/// C decl: `printqt(char *str)`
2950/// C: `mod_export void printqt(char *str)` — emit `str`, escaping any
2951/// `'` as `'\''` (or `''` if RCQUOTES is set).
2952pub fn printqt(str: &str) {
2953 // c:1399
2954 let rcquotes = isset(RCQUOTES); // c:1399 isset(RCQUOTES)
2955 for ch in str.chars() {
2956 // c:1403
2957 if ch == '\'' {
2958 // c:1404
2959 print!("{}", if rcquotes { "''" } else { "'\\''" }); // c:1405
2960 } else {
2961 print!("{}", ch); // c:1407
2962 }
2963 }
2964}
2965
2966/// Port of `printif()` from `Src/builtin.c:1411`.
2967/// C decl: `printif(char *str, int c)`
2968/// C: `mod_export void printif(char *str, int c)` — `printf(" -%c ", c)`
2969/// then `quotedzputs(str, stdout)`, only when `str != NULL`.
2970pub fn printif(str: Option<&str>, c: u8) {
2971 // c:1411
2972 if let Some(s) = str {
2973 // c:1399
2974 print!(" -{} ", c as char); // c:1399
2975 // c:1399 — quotedzputs(str, stdout); plain print preserves bytes
2976 // for the ASCII case; full quotedzputs lives in src/ported/utils.rs.
2977 print!("{}", s); // c:1399
2978 }
2979}
2980
2981/// Port of `bin_fc()` from `Src/builtin.c:1426`.
2982/// C decl: `bin_fc(char *nam, char **argv, Options ops, int func)`
2983/// C: `int bin_fc(char *nam, char **argv, Options ops, int func)`.
2984///
2985/// History/edit/list dispatcher: `-p` push hist stack, `-P` pop,
2986/// `-R` read, `-W` write, `-A` append, `-m` glob filter, `-l` list,
2987/// `-s` substitute, default: edit + re-execute. The C body is ~245
2988/// lines; the structural translation here covers the major options
2989/// and dispatches the underlying history-file ops to the existing
2990/// hist.rs accessors.
2991/// WARNING: param names don't match C — Rust=(nam, argv, func) vs C=(nam, argv, ops, func)
2992pub fn bin_fc(
2993 nam: &str,
2994 argv: &[String], // c:1426
2995 ops_in: &options,
2996 func: i32,
2997) -> i32 {
2998 // C `Options ops` is `struct options *` — mutable via `ops->ind['n']
2999 // = 1;` at c:1644. zshrs HandlerFunc takes `&options`, so we clone
3000 // to a fn-local `ops` mirror at the top. Mutation of the clone is
3001 // intra-fn only (`fclist` reads `ops` to format output and never
3002 // returns it), so behavior matches C.
3003 // !!! BASH-MODE (no C counterpart) !!! bash `history -c` empties the list;
3004 // `-d OFFSET` drops one entry; the file-sync flags have no store to sync.
3005 if nam == "history" && crate::dash_mode::bash_mode() {
3006 if OPT_ISSET(ops_in, b'c') {
3007 crate::ported::hist::hist_ring.lock().unwrap().clear();
3008 return 0;
3009 }
3010 if let Some(off) = OPT_ARG(ops_in, b'd') {
3011 let Ok(n) = off.parse::<i64>() else {
3012 zwarnnam(nam, &format!("{off}: history position out of range"));
3013 return 1;
3014 };
3015 let mut ring = crate::ported::hist::hist_ring.lock().unwrap();
3016 return match ring.iter().position(|e| e.histnum == n) {
3017 Some(i) => {
3018 ring.remove(i);
3019 0
3020 }
3021 None => {
3022 drop(ring);
3023 zwarnnam(nam, &format!("{off}: history position out of range"));
3024 1
3025 }
3026 };
3027 }
3028 if ["a", "n", "r", "w", "s", "p"].iter().any(|o| OPT_ISSET(ops_in, o.as_bytes()[0])) {
3029 return 0;
3030 }
3031 }
3032 let mut ops = ops_in.clone();
3033 let ops = &mut ops;
3034 let mut argv = argv.to_vec();
3035 let mut first: i64 = -1;
3036 let mut last: i64 = -1;
3037 let mut asgf: Vec<(String, String)> = Vec::new();
3038
3039 // c:1441-1481 — `-p` push history stack.
3040 if OPT_ISSET(ops, b'p') {
3041 // c:1441
3042 let mut hf = "".to_string();
3043 let mut hs: i64; // c:1443
3044 let mut shs: i64; // c:1444
3045 // c:1445 — `int level = OPT_ISSET(ops,'a') ? locallevel : -1;`
3046 let level: i32 = if OPT_ISSET(ops, b'a') {
3047 locallevel_param.load(Relaxed)
3048 } else {
3049 -1
3050 };
3051 hs = histsiz.load(Relaxed); // c:1442
3052 shs = savehistsiz.load(Relaxed);
3053 if !argv.is_empty() {
3054 // c:1445
3055 hf = argv.remove(0); // c:1446
3056 if !argv.is_empty() {
3057 // c:1447
3058 let s2 = argv.remove(0);
3059 match s2.parse::<i64>() {
3060 // c:1449 zstrtol
3061 Ok(n) => hs = n,
3062 Err(_) => {
3063 zwarnnam(
3064 "fc", // c:1452
3065 "HISTSIZE must be an integer",
3066 );
3067 return 1; // c:1453
3068 }
3069 }
3070 if !argv.is_empty() {
3071 // c:1455
3072 let s3 = argv.remove(0);
3073 match s3.parse::<i64>() {
3074 // c:1456
3075 Ok(n) => shs = n,
3076 Err(_) => {
3077 zwarnnam(
3078 "fc", // c:1459
3079 "SAVEHIST must be an integer",
3080 );
3081 return 1; // c:1460
3082 }
3083 }
3084 } else {
3085 shs = hs; // c:1464
3086 }
3087 if !argv.is_empty() {
3088 // c:1466
3089 zwarnnam(
3090 "fc", // c:1468
3091 "too many arguments",
3092 );
3093 return 1; // c:1469
3094 }
3095 }
3096 }
3097 // c:1473 — pushhiststack(hf, hs, shs, level); failure → return 1.
3098 pushhiststack(Some(&hf), hs, shs, level); // c:1473
3099 if !hf.is_empty() {
3100 // c:1475
3101 // c:1476-1480 — `if (stat(hf, &st) >= 0 || errno != ENOENT)
3102 // readhistfile(hf, 1, HFILE_USE_OPTIONS);`
3103 // Previous Rust port read `Error::last_os_error()` AFTER
3104 // checking `metadata().is_ok()` — racey: any intervening
3105 // syscall between the metadata call and last_os_error()
3106 // can stomp errno on some platforms. Capture the per-Err
3107 // raw_os_error directly so we read the SAME errno value
3108 // the stat call produced.
3109 let stat_result = fs::metadata(&hf);
3110 let should_read = match &stat_result {
3111 Ok(_) => true, // c:1477 stat >= 0
3112 Err(e) => e.raw_os_error() != Some(libc::ENOENT), // c:1477 errno != ENOENT
3113 };
3114 if should_read {
3115 // c:1477
3116 readhistfile(
3117 // c:1478
3118 Some(&hf),
3119 1,
3120 HFILE_USE_OPTIONS as i32,
3121 );
3122 }
3123 }
3124 return 0; // c:1483
3125 }
3126
3127 // c:1485-1491 — `-P` pop history stack.
3128 if OPT_ISSET(ops, b'P') {
3129 // c:1485
3130 if !argv.is_empty() {
3131 // c:1486
3132 zwarnnam("fc", "too many arguments"); // c:1487
3133 return 1; // c:1488
3134 }
3135 // c:1490 — `return !saveandpophiststack(-1, HFILE_USE_OPTIONS);`.
3136 let popped = saveandpophiststack(-1, HFILE_USE_OPTIONS as i32); // c:1490
3137 return if popped != 0 { 0 } else { 1 }; // c:1490 `!` flip
3138 }
3139
3140 // c:1494-1500 — `-m` pattern filter (compile first arg).
3141 let mut pprog: Option<crate::ported::pattern::PatProg> = None;
3142 let mut pprog_src: Option<String> = None;
3143 if !argv.is_empty() && OPT_ISSET(ops, b'm') {
3144 // c:1494
3145 let pat = argv.remove(0);
3146 // c:1495 — tokenize(*argv); — Rust `patcompile` handles tokenisation.
3147 match patcompile(
3148 &{
3149 let mut __pat_tok = crate::pattern_data_escape::escape_data_backslashes(&pat); // a backslash tokenize leaves raw is DATA (c:Src/glob.c:3651)
3150 crate::ported::glob::tokenize(&mut __pat_tok);
3151 __pat_tok
3152 }, // c:1496
3153 PAT_HEAPDUP,
3154 None,
3155 ) {
3156 Some(p) => {
3157 pprog = Some(p);
3158 pprog_src = Some(pat); // retain source string for fclist
3159 }
3160 None => {
3161 zwarnnam(nam, "invalid match pattern"); // c:1497
3162 return 1; // c:1498
3163 }
3164 }
3165 }
3166
3167 queue_signals(); // c:1502
3168
3169 // c:1503-1525 — `-R` read / `-W` write / `-A` append history file.
3170 if OPT_ISSET(ops, b'R') {
3171 // c:1503
3172 let path = argv.first().cloned();
3173 let flags = if OPT_ISSET(ops, b'I') {
3174 HFILE_SKIPOLD as i32
3175 } else {
3176 0
3177 };
3178 readhistfile(
3179 // c:1505
3180 path.as_deref(),
3181 1,
3182 flags,
3183 );
3184 unqueue_signals(); // c:1506
3185 return 0; // c:1507
3186 }
3187 if OPT_ISSET(ops, b'W') {
3188 // c:1509
3189 let path = argv.first().cloned();
3190 let flags = if OPT_ISSET(ops, b'I') {
3191 HFILE_SKIPOLD as i32
3192 } else {
3193 0
3194 };
3195 savehistfile(
3196 // c:1511
3197 path.as_deref(),
3198 flags,
3199 );
3200 unqueue_signals(); // c:1512
3201 return 0; // c:1513
3202 }
3203 if OPT_ISSET(ops, b'A') {
3204 // c:1515
3205 let path = argv.first().cloned();
3206 let mut flags = HFILE_APPEND as i32;
3207 if OPT_ISSET(ops, b'I') {
3208 flags |= HFILE_SKIPOLD as i32;
3209 } // c:1518
3210 savehistfile(
3211 // c:1517
3212 path.as_deref(),
3213 flags,
3214 );
3215 unqueue_signals(); // c:1519
3216 return 0; // c:1520
3217 }
3218
3219 // c:1523-1527 — refuse inside ZLE.
3220 if crate::ported::builtins::sched::zleactive.load(
3221 // c:1523
3222 Relaxed,
3223 ) != 0
3224 {
3225 unqueue_signals(); // c:1524
3226 zwarnnam(
3227 nam, // c:1525
3228 "no interactive history within ZLE",
3229 );
3230 return 1; // c:1526
3231 }
3232
3233 // c:1530-1547 — `name=value` substitution pairs.
3234 while !argv.is_empty() && argv[0].contains('=') {
3235 // c:1530
3236 let arg = argv.remove(0);
3237 if let Some(eq) = arg.find('=') {
3238 let n = &arg[..eq];
3239 let v = &arg[eq + 1..];
3240 if n.is_empty() {
3241 zwarnnam(nam, &format!("invalid replacement pattern: ={}", v)); // c:1534
3242 return 1;
3243 }
3244 asgf.push((n.to_string(), v.to_string())); // c:1546
3245 }
3246 }
3247
3248 // c:1550-1568 — first/last history specifiers via fcgetcomm.
3249 if !argv.is_empty() {
3250 // c:1550
3251 first = fcgetcomm(&argv.remove(0)); // c:1551
3252 if first == -1 {
3253 unqueue_signals();
3254 return 1; // c:1553
3255 }
3256 }
3257 if !argv.is_empty() {
3258 // c:1559
3259 last = fcgetcomm(&argv.remove(0)); // c:1560
3260 if last == -1 {
3261 unqueue_signals();
3262 return 1;
3263 }
3264 }
3265 if !argv.is_empty() {
3266 // c:1567
3267 unqueue_signals();
3268 zwarnnam("fc", "too many arguments"); // c:1569
3269 return 1;
3270 }
3271
3272 // c:1573-1610 — default ranges + listing/edit dispatch. C reads
3273 // the live `curhist` global at hist.rs directly. The
3274 // FQN here is forced — bare `curhist` would resolve
3275 // to the local `let curhist` we're declaring.
3276 let curhist: i64 = crate::ported::hist::curhist.load(Relaxed) as i64;
3277 if last == -1 {
3278 // c:1573
3279 if OPT_ISSET(ops, b'l') && first < curhist {
3280 // c:1574
3281 // c:1581-1583 — `last = (curline.histnum == curhist) ?
3282 // addhistnum(curhist,-1,0) : curhist;`
3283 // When the fc command itself occupies the current event
3284 // (the normal interactive case), the default range EXCLUDES
3285 // it — zsh's `fc -l 1` does not list the in-flight `fc -l 1`
3286 // line. The previous `last = curhist` included it.
3287 let curline_num = crate::ported::hist::curline
3288 .lock()
3289 .ok()
3290 .and_then(|c| c.as_ref().map(|e| e.histnum))
3291 .unwrap_or(0);
3292 last = if curline_num == curhist {
3293 crate::ported::hist::addhistnum(curhist, -1, 0) // c:1582
3294 } else {
3295 curhist // c:1583
3296 };
3297 // c:1584-1585 — `if (last < firsthist()) last = firsthist();`
3298 let fh = crate::ported::hist::firsthist();
3299 if last < fh {
3300 last = fh; // c:1585
3301 }
3302 } else {
3303 last = first; // c:1587
3304 }
3305 }
3306 if first == -1 {
3307 // c:1589
3308 let xflags = if OPT_ISSET(ops, b'L') {
3309 HIST_FOREIGN as i32
3310 } else {
3311 0
3312 }; // c:1602
3313 // c:1603-1604 — `first = OPT_ISSET(ops,'l')? addhistnum(curhist,-16,xflags)
3314 // : addhistnum(curline.histnum,-1,xflags);`
3315 let curline_num = crate::ported::hist::curline
3316 .lock()
3317 .ok()
3318 .and_then(|c| c.as_ref().map(|e| e.histnum))
3319 .unwrap_or(0);
3320 first = if OPT_ISSET(ops, b'l') {
3321 crate::ported::hist::addhistnum(curhist, -16, xflags)
3322 } else {
3323 crate::ported::hist::addhistnum(curline_num, -1, xflags)
3324 };
3325 if first < 1 {
3326 first = 1; // c:1605-1606
3327 }
3328 if last < first {
3329 last = first;
3330 } // c:1604
3331 }
3332
3333 let mut retval;
3334 if OPT_ISSET(ops, b'l') {
3335 // c:1606
3336 // c:1608 — `fclist(stdout, ops, first, last, asgf, pprog, 0);`
3337 retval = fclist(
3338 &mut io::stdout(),
3339 ops,
3340 first,
3341 last,
3342 &asgf,
3343 pprog_src.as_deref(),
3344 0,
3345 );
3346 unqueue_signals();
3347 } else {
3348 // c:1611-1668 — edit history range to a temp file, fcedit it,
3349 // then stuff() the result back as the next command.
3350 retval = 1; // c:1620
3351 // c:1620 — `gettempfile(NULL, 1, &fil)`: a NULL prefix makes
3352 // gettempfile fall back to `$TMPPREFIX` (c:2241-2242, default
3353 // /tmp/zsh), so the editor buffer is `/tmp/zshXXXXXX`. Passing a
3354 // bare "zshfc" prefix built a RELATIVE name instead, so `fc`
3355 // wrote `zshfc.<hex>` into the CURRENT DIRECTORY — visible as
3356 // stray untracked files in whatever repo you ran it from.
3357 let fil_opt = gettempfile(None); // c:1621 gettempfile
3358 match fil_opt {
3359 None => {
3360 // c:1623
3361 unqueue_signals(); // c:1624
3362 zwarnnam(
3363 "fc", // c:1625
3364 &format!("can't open temp file: {}", io::Error::last_os_error()),
3365 );
3366 }
3367 Some((fd, fil)) => {
3368 unsafe {
3369 libc::close(fd);
3370 } // c:1622 (file is reopened below)
3371 // c:1632 — `if (last >= curhist) { last = curhist - 1; ... }`
3372 if last >= curhist {
3373 // c:1632
3374 last = curhist - 1; // c:1633
3375 if first > last {
3376 // c:1634
3377 unqueue_signals(); // c:1635
3378 zwarnnam(
3379 "fc", // c:1636
3380 "current history line would recurse endlessly, aborted",
3381 );
3382 let _ = fs::remove_file(&fil); // c:1639 unlink
3383 return 1; // c:1640
3384 }
3385 }
3386 ops.ind[b'n' as usize] = 1; // c:1644 No line numbers
3387 let out = fs::OpenOptions::new()
3388 .create(true)
3389 .write(true)
3390 .truncate(true)
3391 .open(&fil)
3392 .ok();
3393 let listed = if let Some(mut f) = out {
3394 // c:1645 — thread pprog filter so `fc -m PAT` only
3395 // edits matching lines in this temp-file edit path.
3396 fclist(&mut f, ops, first, last, &asgf, pprog_src.as_deref(), 1)
3397 } else {
3398 1
3399 };
3400 if listed == 0 {
3401 // c:1645
3402 // c:1647-1656 — pick editor.
3403 let editor: String = if func == BIN_R || OPT_ISSET(ops, b's') {
3404 "-".to_string() // c:1648
3405 } else if OPT_HASARG(ops, b'e') {
3406 // c:1649
3407 OPT_ARG(ops, b'e').unwrap_or("").to_string() // c:1650
3408 } else {
3409 // c:1651-1654 — `getsparam("FCEDIT") ?:
3410 // getsparam("EDITOR") ?:
3411 // DEFAULT_FCEDIT`. paramtab read.
3412 getsparam("FCEDIT")
3413 .or_else(|| getsparam("EDITOR"))
3414 .unwrap_or_else(|| crate::ported::config_h::DEFAULT_FCEDIT.to_string())
3415 };
3416 unqueue_signals(); // c:1657
3417 if fcedit(&editor, &fil) != 0 {
3418 // c:1658
3419 if crate::ported::input::stuff(&fil) != 0 {
3420 // c:1659
3421 zwarnnam(
3422 "fc", // c:1660
3423 &format!("{}: {}", io::Error::last_os_error(), fil),
3424 );
3425 } else {
3426 // c:1663-1664 — `loop(0,1); retval = lastval;`.
3427 // `stuff` only PUSHES the edited text onto the
3428 // input stack (input.c:657); C then runs one
3429 // pass of the input loop to parse and execute
3430 // it. Without that call nothing consumed the
3431 // stuffed line, so the edited command never ran.
3432 crate::ported::init::r#loop(0, 1); // c:1663
3433 // C has ONE input source and one executor, so
3434 // loop(0,1) both consumes and RUNS what `stuff`
3435 // pushed. In zshrs the ported loop consumes the
3436 // pushed text (inbufct drops to 0) but its
3437 // AST-side execution is not the live path, so
3438 // outside an interactive shell — where the REPL
3439 // would have read and run the line itself —
3440 // `zsh -c 'fc -e ED 1'` echoed the edited
3441 // command and never ran it. Run it through the
3442 // same bridge `execstring` uses (exec.rs:1939).
3443 if !isset(SHINSTDIN) {
3444 if let Ok(edited) = fs::read_to_string(&fil) {
3445 crate::ported::exec::execstring(&edited, 1, 0, "fc");
3446 }
3447 }
3448 retval = LASTVAL.load(Relaxed); // c:1664
3449 }
3450 }
3451 } else {
3452 unqueue_signals(); // c:1667
3453 }
3454 let _ = fs::remove_file(&fil); // c:1671 unlink
3455 }
3456 }
3457 }
3458 let _ = pprog; // compiled form kept for parity; source threads through fclist.
3459 retval // c:1675
3460}
3461
3462/// Port of `fcgetcomm(char *s)` from Src/builtin.c:1683.
3463/// C: `static zlong fcgetcomm(char *s)` — match `s` against history
3464/// numbers (signed) or prefix; returns the matched event number.
3465/// Port of `fcgetcomm()` from `Src/builtin.c:1683`.
3466/// C decl: `fcgetcomm(char *s)`
3467/// Resolve an `fc` command-line argument to a
3468/// history event number. Numeric args become event numbers (negative
3469/// numbers count back from current via `addhistnum`); non-numeric
3470/// args go through `hcomsearch` (history prefix search). Emits
3471/// `zwarnnam("fc", "event not found: %s", s)` and returns -1 on
3472/// miss.
3473pub fn fcgetcomm(s: &str) -> i64 {
3474 // c:1683
3475 // c:1689 — `if ((cmd = atoi(s)) != 0 || *s == '0')` numeric arm.
3476 // atoi accepts leading whitespace + optional sign +
3477 // digits; trim+parse mirrors that.
3478 let trimmed = s.trim_start();
3479 let numeric = trimmed.parse::<i64>().ok();
3480 let is_zero_prefix = trimmed.starts_with('0');
3481 if let Some(mut cmd) = numeric {
3482 if cmd != 0 || is_zero_prefix {
3483 if cmd < 0 {
3484 // c:1693 — `cmd = addhistnum(curline.histnum, cmd, HIST_FOREIGN);`
3485 // Relative to the line being edited, not `curhist`: with no
3486 // edited line (`zsh -c`) curline.histnum is 0, so a negative
3487 // event finds nothing below it and resolves to 0.
3488 let curline_num = crate::ported::hist::curline
3489 .lock()
3490 .ok()
3491 .and_then(|c| c.as_ref().map(|e| e.histnum))
3492 .unwrap_or(0);
3493 cmd = addhistnum(curline_num, cmd as i32, HIST_FOREIGN as i32);
3494 }
3495 if cmd < 0 {
3496 // c:1695
3497 cmd = 0;
3498 }
3499 return cmd;
3500 }
3501 }
3502 // c:1700 — `cmd = hcomsearch(s); if (cmd == -1) zwarnnam(...);`
3503 match hcomsearch(s) {
3504 Some(n) => n,
3505 None => {
3506 zwarnnam("fc", &format!("event not found: {}", s));
3507 -1
3508 }
3509 }
3510}
3511
3512/// Port of `fcsubs()` from `Src/builtin.c:1708`.
3513/// C decl: `fcsubs(char **sp, struct asgment *sub)`
3514/// C: `static int fcsubs(char **sp, struct asgment *sub)` — apply the
3515/// linked-list of `old=new` substitutions to `*sp` in place; return
3516/// the count of substitutions made.
3517pub fn fcsubs(sp: &mut String, sub: &[(String, String)]) -> i32 {
3518 // c:1708
3519 // c:1708-1748 — for each (old, new), replace each occurrence in *sp.
3520 let mut subbed = 0i32; // c:1713
3521 for (old, new) in sub {
3522 // c:1716
3523 if old.is_empty() {
3524 continue;
3525 }
3526 let count = sp.matches(old.as_str()).count() as i32; // c:1722
3527 if count > 0 {
3528 *sp = sp.replace(old.as_str(), new); // c:1750
3529 subbed += count;
3530 }
3531 }
3532 subbed
3533}
3534
3535/// Direct port of `int fclist(FILE *f, Options ops, zlong first,
3536/// zlong last, struct asgment *subs, Patprog pprog, int is_command)`
3537/// from `Src/builtin.c:1750`. Walks the history event range
3538/// `first..=last`, applies the `subs` substitution chain to each
3539/// matching line (when `pprog` is set, only lines matching it),
3540/// then writes the result with optional timestamp prefix per
3541/// `-d/-f/-E/-i/-t`.
3542///
3543/// Rust signature: takes the output writer as a closure so callers
3544/// can route to stdout, a FILE*, or an in-memory buffer (the
3545/// `is_command` caller in `bin_fc` collects to a heredoc string).
3546#[allow(clippy::too_many_arguments)]
3547/// Port of `fclist()` from `Src/builtin.c:1750`. — C decl `fclist(FILE *f, Options ops, zlong first, zlong last,`
3548pub fn fclist(
3549 out: &mut dyn Write, // c:1750
3550 ops: &options,
3551 mut first: i64,
3552 mut last: i64,
3553 subs: &[(String, String)],
3554 pprog: Option<&str>,
3555 is_command: i32,
3556) -> i32 {
3557 use std::io::Write;
3558 let mut fclistdone = 0i32; // c:1754
3559 let mut xflags: u32 = 0; // c:1754
3560
3561 // c:1759-1763 — reverse range if required.
3562 if OPT_ISSET(ops, b'r') {
3563 std::mem::swap(&mut first, &mut last);
3564 }
3565 // c:1765-1770
3566 if is_command != 0 && first > last {
3567 zwarnnam("fc", "history events can't be executed backwards, aborted");
3568 return 1;
3569 }
3570
3571 // c:1772-1783 —
3572 // ent = gethistent(first, first < last? GETHIST_DOWNWARD : GETHIST_UPWARD);
3573 // if (!ent || (first < last? ent->histnum > last : ent->histnum < last)) {
3574 // if (first == last) zwarnnam("fc", "no such event: %s", buf);
3575 // else zwarnnam("fc", "no events in that range");
3576 // return 1;
3577 // }
3578 // gethistent's near match can land past the other end of the range
3579 // (`fc -l 5` with one event finds event 1, below `last`), which is
3580 // as much a miss as no event at all.
3581 let near = if first < last { 1 } else { -1 };
3582 // !!! BASH-MODE !!! bash lists an empty history as nothing, status 0.
3583 if crate::dash_mode::bash_mode() && is_command == 0 && gethistent(1, 1).is_none() {
3584 return 0;
3585 }
3586 let mut ev = match gethistent(first, near) {
3587 Some(e) if !(if first < last { e > last } else { e < last }) => e,
3588 _ => {
3589 zwarnnam(
3590 "fc",
3591 &if first == last {
3592 format!("no such event: {}", first)
3593 } else {
3594 "no events in that range".to_string()
3595 },
3596 );
3597 return 1;
3598 }
3599 };
3600
3601 // c:1785-1803 — time/date format for -d/-f/-E/-i/-t.
3602 let tdfmt: Option<String> = if OPT_ISSET(ops, b't') {
3603 Some(OPT_ARG(ops, b't').unwrap_or("").to_string()) // c:1789
3604 } else if OPT_ISSET(ops, b'i') {
3605 Some("%Y-%m-%d %H:%M".to_string()) // c:1791
3606 } else if OPT_ISSET(ops, b'E') {
3607 Some("%f.%-m.%Y %H:%M".to_string()) // c:1793
3608 } else if OPT_ISSET(ops, b'f') {
3609 Some("%-m/%f/%Y %H:%M".to_string()) // c:1795
3610 } else if OPT_ISSET(ops, b'd') {
3611 Some("%H:%M".to_string()) // c:1797
3612 } else {
3613 None // c:1801
3614 };
3615
3616 // c:1805-1811 — xflags exclude events.
3617 if OPT_ISSET(ops, b'L') {
3618 xflags |= crate::ported::zsh_h::HIST_FOREIGN;
3619 }
3620 if OPT_ISSET(ops, b'I') {
3621 xflags |= crate::ported::zsh_h::HIST_READ;
3622 }
3623
3624 // C pre-compiles the pattern once (bin_fc c:1493 patcompile).
3625 let prog = pprog.and_then(|pat| {
3626 let mut tok = crate::pattern_data_escape::escape_data_backslashes(pat); // a backslash tokenize leaves raw is DATA (c:Src/glob.c:3651)
3627 crate::ported::glob::tokenize(&mut tok);
3628 patcompile(&tok, 0, None)
3629 });
3630
3631 loop {
3632 // c:1813-1817
3633 let entry = match quietgethist(ev) {
3634 Some(e) => e,
3635 None => break,
3636 };
3637 let s: Option<String> = if (entry.node.flags as u32 & xflags) != 0 {
3638 None
3639 } else {
3640 Some(entry.node.nam.clone())
3641 };
3642 // c:1818 — `if (s && (!pprog || pattry(pprog, s)))`
3643 if let Some(mut s) = s.filter(|s| prog.as_ref().is_none_or(|p| pattry(p, s))) {
3644 // c:1820 — `fclistdone |= (subs ? fcsubs(&s, subs) : 1);`
3645 fclistdone |= if subs.is_empty() {
3646 1
3647 } else {
3648 i32::from(fcsubs(&mut s, subs) != 0)
3649 };
3650
3651 // c:1822-1828 — do numbering.
3652 if !OPT_ISSET(ops, b'n') {
3653 let foreign = (entry.node.flags as u32 & crate::ported::zsh_h::HIST_FOREIGN) != 0;
3654 let _ = write!(out, "{:>5}{} ", ev, if foreign { '*' } else { ' ' });
3655 }
3656 // c:1830-1839 — time (and possibly date) of execution.
3657 if let Some(ref fmt) = tdfmt {
3658 let t = std::time::UNIX_EPOCH
3659 + std::time::Duration::from_secs(entry.stim.max(0) as u64);
3660 let _ = write!(out, "{} ", crate::ported::utils::ztrftime(fmt, t, false));
3661 }
3662 // c:1841-1845 — time taken by the command.
3663 if OPT_ISSET(ops, b'D') {
3664 let diff = if entry.ftim != 0 { entry.ftim - entry.stim } else { 0 };
3665 let _ = write!(out, "{}:{:02} ", diff / 60, diff % 60);
3666 }
3667
3668 // c:1847-1856 — output the command:
3669 // if (f == stdout) { nicezputs(s, f); putc('\n', f); }
3670 // else { unmetafy(s, &len); fwrite(s, 1, len, f); putc('\n', f); }
3671 // A listing renders control bytes visibly (an embedded newline
3672 // from `print -s $'a\nb'` lists as `a\nb`); the temp file handed
3673 // to the editor gets the raw bytes so the edited command
3674 // round-trips.
3675 // !!! WARNING: Rust cannot compare a `&mut dyn Write` against
3676 // stdout, so the `f == stdout` test is read from `is_command`.
3677 // The two are equivalent at every C call site: c:1611 passes
3678 // (stdout, …, 0) and c:1643 passes (the temp file, …, 1).
3679 if is_command == 0 {
3680 crate::ported::utils::nicezputs(&s, out); // c:1849
3681 let _ = out.write_all(b"\n"); // c:1850
3682 } else {
3683 let _ = writeln!(out, "{}", s); // c:1854-1855
3684 }
3685 }
3686 // c:1859-1866 — move on to the next history line, or quit.
3687 let next = if first < last {
3688 crate::ported::hist::down_histent(ev).filter(|&n| n <= last)
3689 } else {
3690 crate::ported::hist::up_histent(ev).filter(|&n| n >= last)
3691 };
3692 match next {
3693 Some(n) => ev = n,
3694 None => break,
3695 }
3696 }
3697
3698 // c:1872-1878 — final processing.
3699 if fclistdone == 0 {
3700 if !subs.is_empty() {
3701 zwarnnam("fc", "no substitutions performed");
3702 } else if xflags != 0 || pprog.is_some() {
3703 zwarnnam("fc", "no matching events found");
3704 }
3705 return 1;
3706 }
3707 0 // c:1879
3708}
3709
3710/// Port of `fcedit()` from `Src/builtin.c:1885`.
3711/// C decl: `fcedit(char *ename, char *fn)`
3712/// C: `static int fcedit(char *ename, char *fn)` — invoke `$ename fn`,
3713/// returning the editor's exit status (0 if `ename == "-"`).
3714/// WARNING: param names don't match C — Rust=(ename, fn_) vs C=(ename, fn)
3715pub fn fcedit(ename: &str, fn_: &str) -> i32 {
3716 // c:1885
3717 // c:1885 — `if (!strcmp(ename, "-")) return 1;`
3718 if ename == "-" {
3719 // c:1888
3720 return 1; // c:1889
3721 }
3722 // c:1892-1896 — `s = tricat(ename, " ", fn); execstring(s, 1, 0,
3723 // "fc"); return !lastval;`. Two things the spawn form got wrong:
3724 // the editor is a COMMAND LINE run through the shell (so
3725 // `fc -e "print -r --"` or `FCEDIT="vim -u NONE"` work), and the
3726 // return is `!lastval` — 1 on SUCCESS. Returning the editor's exit
3727 // code inverted the caller's `if (fcedit(...))` test, so the edited
3728 // buffer was never stuffed back and `fc -e ED N` silently did
3729 // nothing and reported failure.
3730 let s = format!("{} {}", ename, fn_); // c:1892
3731 crate::ported::exec::execstring(&s, 1, 0, "fc"); // c:1893
3732 i32::from(LASTVAL.load(Relaxed) == 0) // c:1896 `return !lastval`
3733}
3734
3735/// Port of `getasg()` from `Src/builtin.c:1908`.
3736/// C decl: `getasg(char ***argvp, LinkList assigns)`
3737/// C: `static Asgment getasg(char ***argvp, LinkList assigns)` —
3738/// parse one assignment-form arg (`name=value` / `name`) from
3739/// `*argvp`. Returns NULL when exhausted.
3740/// ```c
3741/// static Asgment
3742/// getasg(char ***argvp, LinkList assigns)
3743/// {
3744/// char *s = **argvp;
3745/// static struct asgment asg;
3746/// if (!s) {
3747/// if (assigns) {
3748/// Asgment asgp = (Asgment)firstnode(assigns);
3749/// if (!asgp) return NULL;
3750/// (void)uremnode(assigns, &asgp->node);
3751/// return asgp;
3752/// }
3753/// return NULL;
3754/// }
3755/// if (*s == '=') { zerr("bad assignment"); return NULL; }
3756/// asg.name = s;
3757/// asg.flags = 0;
3758/// for (; *s && *s != '='; s++);
3759/// if (*s) { *s = '\0'; asg.value.scalar = s + 1; }
3760/// else asg.value.scalar = NULL;
3761/// (*argvp)++;
3762/// return &asg;
3763/// }
3764/// ```
3765pub fn getasg(
3766 argvp: &mut Vec<String>, // c:1908
3767 assigns: &mut Vec<(String, String)>,
3768) -> Option<(String, String)> {
3769 // c:1914-1923 — out-of-args path: drain from assigns list if non-empty.
3770 if argvp.is_empty() {
3771 // c:1914 !s
3772 if !assigns.is_empty() {
3773 // c:1915
3774 return Some(assigns.remove(0)); // c:1916-1920 firstnode + uremnode
3775 }
3776 return None; // c:1922
3777 }
3778
3779 let s = argvp.remove(0); // c:1944 (*argvp)++
3780
3781 // c:1926-1929 — empty-name guard: bare `=value` is an error.
3782 if s.starts_with('=') {
3783 // c:1926
3784 zerr("bad assignment"); // c:1927
3785 return None; // c:1928
3786 }
3787
3788 // c:1934-1943 — split on `=`. No `=` → name-only (scalar = NULL).
3789 match s.find('=') {
3790 // c:1934
3791 Some(i) => {
3792 // c:1938-1939 — `*s = '\0'; asg.value.scalar = s + 1;`
3793 Some((s[..i].to_string(), s[i + 1..].to_string())) // c:1939
3794 }
3795 None => {
3796 // c:1942 — `asg.value.scalar = NULL;` — name-only.
3797 Some((s, String::new())) // c:1942
3798 }
3799 }
3800}
3801
3802/// Port of `typeset_setbase()` from `Src/builtin.c:1961`.
3803/// C decl: `typeset_setbase(const char *name, Param pm, Options ops, int on, int always)`
3804/// C: `static int typeset_setbase(const char *name, Param pm, Options ops,
3805/// int on, int always)` — install numeric base on `pm`. For
3806/// `-i ARG`/`-E ARG`/`-F ARG`, parse ARG as base and validate
3807/// (must be 2..=36 for integer); error → return 1.
3808/// WARNING: param names don't match C — Rust=(name, pm, on, always) vs C=(name, pm, ops, on, always)
3809pub fn typeset_setbase(
3810 name: &str,
3811 pm: *mut param, // c:1961
3812 ops: &options,
3813 on: i32,
3814 always: i32,
3815) -> i32 {
3816 // c:1964 — `char *arg = NULL;`
3817 let mut arg: Option<&str> = None; // c:1964
3818 let on_u = on as u32;
3819 // c:1966-1971 — `if ((on & PM_INTEGER) && OPT_HASARG(ops,'i')) arg = OPT_ARG(ops,'i');`
3820 if (on_u & PM_INTEGER) != 0 && OPT_HASARG(ops, b'i') {
3821 // c:1966
3822 arg = OPT_ARG(ops, b'i'); // c:1967
3823 } else if (on_u & PM_EFLOAT) != 0 && OPT_HASARG(ops, b'E') {
3824 // c:1968
3825 arg = OPT_ARG(ops, b'E'); // c:1969
3826 } else if (on_u & PM_FFLOAT) != 0 && OPT_HASARG(ops, b'F') {
3827 // c:1970
3828 arg = OPT_ARG(ops, b'F'); // c:1971
3829 }
3830
3831 // c:1973 — `if (arg) {`
3832 if let Some(a) = arg {
3833 // c:1973
3834 // c:1976 — `int base = (int)zstrtol(arg, &eptr, 10);`
3835 let base = match a.trim().parse::<i32>() {
3836 Ok(b) => b,
3837 Err(_) => {
3838 // c:1977-1982
3839 if (on_u & PM_INTEGER) != 0 {
3840 zwarnnam(name, &format!("bad base value: {}", a)); // c:1979
3841 } else {
3842 zwarnnam(name, &format!("bad precision value: {}", a)); // c:1981
3843 }
3844 return 1; // c:1983
3845 }
3846 };
3847 // c:1985-1989 — integer base must be 2..=36 inclusive.
3848 if (on_u & PM_INTEGER) != 0 && (base < 2 || base > 36) {
3849 // c:1985
3850 zwarnnam(
3851 name,
3852 &format!("invalid base (must be 2 to 36 inclusive): {}", base),
3853 ); // c:1986-1987
3854 return 1; // c:1988
3855 }
3856 // c:1990 — `pm->base = base;`
3857 if !pm.is_null() {
3858 unsafe {
3859 (*pm).base = base;
3860 } // c:1990
3861 }
3862 } else if always != 0 {
3863 // c:1991
3864 // c:1997 — `pm->base = 0;`
3865 if !pm.is_null() {
3866 unsafe {
3867 (*pm).base = 0;
3868 } // c:1997
3869 }
3870 }
3871 0 // c:1997
3872}
3873
3874/// Port of `typeset_setwidth()` from `Src/builtin.c:1997`.
3875/// C decl: `typeset_setwidth(const char * name, Param pm, Options ops, int on, int always)`
3876/// C: `static int typeset_setwidth(const char *name, Param pm, Options ops,
3877/// int on, int always)` — install padding width via `-L/-R/-Z ARG`.
3878/// WARNING: param names don't match C — Rust=(name, pm, on, always) vs C=(name, pm, ops, on, always)
3879pub fn typeset_setwidth(
3880 name: &str,
3881 pm: *mut param, // c:1997
3882 ops: &options,
3883 on: i32,
3884 always: i32,
3885) -> i32 {
3886 // c:2000 — `char *arg = NULL;`
3887 let mut arg: Option<&str> = None; // c:2000
3888 let on_u = on as u32;
3889 // c:2002-2007
3890 if (on_u & PM_LEFT) != 0 && OPT_HASARG(ops, b'L') {
3891 // c:2002
3892 arg = OPT_ARG(ops, b'L'); // c:2003
3893 } else if (on_u & PM_RIGHT_B) != 0 && OPT_HASARG(ops, b'R') {
3894 // c:2004
3895 arg = OPT_ARG(ops, b'R'); // c:2005
3896 } else if (on_u & PM_RIGHT_Z) != 0 && OPT_HASARG(ops, b'Z') {
3897 // c:2006
3898 arg = OPT_ARG(ops, b'Z'); // c:2007
3899 }
3900
3901 // c:2009 — `if (arg) {`
3902 if let Some(a) = arg {
3903 // c:2009
3904 // c:2011 — `pm->width = (int)zstrtol(arg, &eptr, 10);`
3905 let width = match a.trim().parse::<i32>() {
3906 Ok(w) => w,
3907 Err(_) => {
3908 zwarnnam(name, &format!("bad width value: {}", a)); // c:2013
3909 return 1; // c:2014
3910 }
3911 };
3912 if !pm.is_null() {
3913 unsafe {
3914 (*pm).width = width;
3915 } // c:2011
3916 }
3917 } else if always != 0 {
3918 // c:2015
3919 // c:2016 — `pm->width = 0;`
3920 if !pm.is_null() {
3921 unsafe {
3922 (*pm).width = 0;
3923 } // c:2025
3924 }
3925 }
3926 0 // c:2025
3927}
3928
3929/// Port of `typeset_single()` from `Src/builtin.c:2025`.
3930/// C decl: `typeset_single(char *cname, char *pname, Param pm, int func,`
3931/// Port of `static Param typeset_single(char *cname, char *pname,
3932/// Param pm, int func, int on, int off, int roff, Asgment asg,
3933/// Param altpm, Options ops, int joinchar)` from `Src/builtin.c:2025`.
3934/// Per-name attribute resolver + assignment dispatcher invoked once
3935/// per arg from `bin_typeset`.
3936#[allow(clippy::too_many_arguments)]
3937pub fn typeset_single(
3938 cname: &str,
3939 pname: &str, // c:2025
3940 pm: *mut param,
3941 func: i32,
3942 mut on: i32,
3943 mut off: i32,
3944 _roff: i32,
3945 asg: *mut asgment,
3946 altpm: *mut param,
3947 ops: &options,
3948 _joinchar: i32,
3949) -> *mut param {
3950 let mut usepm: i32; // c:2029
3951 let mut tc: i32 = 0; // c:2029
3952 let _keeplocal: i32 = 0; // c:2029
3953 let mut newspecial: i32 = 0; /* NS_NONE */
3954 // c:2029
3955 let _readonly: i32 = 0; // c:2029
3956 let _dont_set: i32 = 0; // c:2029
3957 let mut pname_owned: String = pname.to_string(); // c:2030 subscript path
3958
3959 // c:2032-2050 — nameref resolution.
3960 let pm_ref = unsafe { pm.as_mut() };
3961 if let Some(pm_r) = &pm_ref {
3962 let pm_flags = pm_r.node.flags as u32;
3963 let locallevel_v = locallevel_param.load(Relaxed);
3964 if (pm_flags & PM_NAMEREF) != 0
3965 && ((off | on) as u32 & PM_NAMEREF) == 0
3966 && (pm_r.level == locallevel_v || (on as u32 & PM_LOCAL) == 0)
3967 {
3968 // c:2034 — pm = resolve_nameref(pm)
3969 // pname = pm->node.nam (when resolved)
3970 // The chain walk (`resolve_nameref_name`) and the `pname`
3971 // rewrite already ran in bin_typeset's per-arg loop before
3972 // this fn is entered, so only the c:2036-2048 checks remain.
3973 let unresolved_flags = pm_r.node.flags as u32;
3974 let extra_on_mask = !(PM_NAMEREF | PM_LOCAL | PM_READONLY) as i32;
3975 if (pm_flags & PM_NAMEREF) != 0
3976 && ((unresolved_flags & PM_UNSET) == 0 || (unresolved_flags & PM_DECLARED) != 0)
3977 && (on & extra_on_mask) != 0
3978 {
3979 // c:2042-2048 — error: can't change type of a nameref.
3980 if pm_r.width != 0 {
3981 // c:2041
3982 zwarnnam(
3983 cname, // c:2042
3984 &format!("{}: can't change type via subscript reference", pname),
3985 );
3986 } else {
3987 zwarnnam(
3988 cname, // c:2046
3989 &format!("{}: can't change type of a named reference", pname),
3990 );
3991 }
3992 return std::ptr::null_mut(); // c:2048
3993 }
3994 }
3995 }
3996
3997 // c:2062-2064 — `usepm = pm && (!(pm_flags & PM_UNSET) || OPT_ISSET(ops,'p') || ...)`
3998 let pm_flags = pm_ref.as_ref().map(|p| p.node.flags as u32).unwrap_or(0);
3999 usepm = if pm_ref.is_some()
4000 && ((pm_flags & PM_UNSET) == 0
4001 || OPT_ISSET(ops, b'p')
4002 || (isset(POSIXBUILTINS) && (pm_flags & (PM_READONLY | PM_EXPORTED)) != 0))
4003 {
4004 1
4005 } else {
4006 0
4007 };
4008
4009 // c:2070-2071 — preserve PM_UNSET for special params.
4010 if usepm == 0 && pm_ref.is_some() && (pm_flags & PM_SPECIAL) != 0 {
4011 usepm = 2; // c:2071
4012 }
4013
4014 // c:2078-2091 — don't reuse if local-level changed and PM_LOCAL set.
4015 let pm_level = pm_ref.as_ref().map(|p| p.level).unwrap_or(0);
4016 let locallevel_v = locallevel_param.load(Relaxed);
4017 if usepm != 0 && locallevel_v != pm_level && (on as u32 & PM_LOCAL) != 0 {
4018 // c:2078
4019 if (pm_flags & PM_SPECIAL) != 0 // c:2087
4020 && (on as u32 & PM_HIDE) == 0
4021 && (pm_flags & PM_HIDE & !off as u32) == 0
4022 {
4023 newspecial = 1; /* NS_NORMAL */ // c:2089
4024 }
4025 usepm = 0; // c:2090
4026 }
4027
4028 // c:2093-2116 — type-conversion / tied-colonarray detection.
4029 let asg_ref = unsafe { asg.as_ref() };
4030 tc = 0;
4031 if let Some(a) = asg_ref {
4032 if ASG_ARRAYP(a)
4033 && PM_TYPE(on as u32) == PM_SCALAR
4034 && !(usepm != 0 && (PM_TYPE(pm_flags) & (PM_ARRAY | PM_HASHED)) != 0)
4035 {
4036 on |= PM_ARRAY as i32; // c:2097
4037 }
4038 if usepm != 0 && ASG_ARRAYP(a) && newspecial == 0 // c:2098
4039 && PM_TYPE(pm_flags) != PM_ARRAY
4040 && PM_TYPE(pm_flags) != PM_HASHED
4041 {
4042 if (on as u32 & (PM_EFLOAT | PM_FFLOAT | PM_INTEGER)) != 0 {
4043 zerrnam(
4044 cname, // c:2102
4045 &format!("{}: can't assign array value to non-array", pname),
4046 );
4047 return std::ptr::null_mut();
4048 }
4049 if (pm_flags & PM_SPECIAL) != 0 {
4050 // c:2105
4051 zerrnam(
4052 cname, // c:2106
4053 &format!("{}: can't assign array value to non-array special", pname),
4054 );
4055 return std::ptr::null_mut();
4056 }
4057 tc = 1; // c:2109
4058 usepm = if OPT_MINUS(ops, b'p') {
4059 // c:2110
4060 (on as u32 & pm_flags) as i32
4061 } else if OPT_PLUS(ops, b'p') {
4062 // c:2112
4063 (off as u32 & pm_flags) as i32
4064 } else {
4065 0 // c:2115
4066 };
4067 }
4068 }
4069
4070 // c:2117-2199 — attribute-mask compatibility checks (chflags compute).
4071 if usepm != 0 || newspecial != 0 {
4072 let chflags = ((off as u32 & pm_flags) | (on as u32 & !pm_flags)) // c:2118
4073 & (PM_INTEGER
4074 | PM_EFLOAT
4075 | PM_FFLOAT
4076 | PM_HASHED | PM_ARRAY | PM_TIED | PM_AUTOLOAD);
4077 if chflags != 0 && chflags != (PM_EFLOAT | PM_FFLOAT) {
4078 tc = 1; // c:2122
4079 usepm = if OPT_MINUS(ops, b'p') {
4080 // c:2123
4081 (on as u32 & pm_flags) as i32
4082 } else if OPT_PLUS(ops, b'p') {
4083 // c:2125
4084 (off as u32 & pm_flags) as i32
4085 } else {
4086 // c:2127 — `else usepm = 0;`
4087 //
4088 // This `else` was missing, and it is what selects between the
4089 // two mutually exclusive paths. usepm != 0 takes the MERGE path
4090 // (c:2291: keep the existing flags, add `on`, drop `off`), while
4091 // usepm == 0 falls through to the type-conversion path (c:2355),
4092 // which deletes the parameter and recreates it carrying ONLY
4093 // READONLY|EXPORTED (c:2357). Leaving usepm set meant a base-type
4094 // change merged instead of recreating, so `typeset -Z 3 x=7;
4095 // typeset -i x=1` kept the right_zeros attribute (zsh: `integer`,
4096 // zshrs: `integer-right_zeros`).
4097 0
4098 };
4099 }
4100 }
4101
4102 // c:2202-2214 — readonly/exported preservation rules.
4103 if usepm != 0 || newspecial != 0 {
4104 if (on as u32 & (PM_READONLY | PM_EXPORTED)) != 0 // c:2202
4105 && (usepm == 0 || (pm_flags & PM_UNSET) != 0)
4106 && asg_ref.is_some_and(|a| !ASG_VALUEP(a))
4107 {
4108 on |= PM_UNSET as i32; // c:2205
4109 } else if usepm != 0 && (pm_flags & PM_READONLY) != 0 // c:2206
4110 && (on as u32 & PM_READONLY) == 0
4111 && func != BIN_EXPORT
4112 {
4113 zerr(&format!(
4114 // c:2208
4115 "read-only variable: {}",
4116 pm_ref.as_ref().unwrap().node.nam
4117 ));
4118 return std::ptr::null_mut();
4119 }
4120 }
4121
4122 // c:2226-2248 — reuse-existing-param fast paths.
4123 if usepm != 0 {
4124 let pm_r = pm_ref.as_ref().unwrap();
4125 if OPT_MINUS(ops, b'p')
4126 && on != 0
4127 && !((on as u32 & pm_flags) != 0 || ((on as u32 & PM_LOCAL) != 0 && pm_r.level != 0))
4128 {
4129 return std::ptr::null_mut(); // c:2229
4130 }
4131 if OPT_PLUS(ops, b'p') && off != 0 && (off as u32 & pm_flags) == 0 {
4132 return std::ptr::null_mut(); // c:2231
4133 }
4134 // c:2232-2238 — array/scalar consistency check
4135 if let Some(a) = asg_ref {
4136 let array_assign = (a.flags & ASG_ARRAY) != 0;
4137 let pm_is_arr = (PM_TYPE(pm_flags) & (PM_ARRAY | PM_HASHED)) != 0;
4138 if array_assign && !pm_is_arr {
4139 // c:2232
4140 zerrnam(
4141 cname, // c:2236
4142 &format!("{}: inconsistent type for assignment", pname),
4143 );
4144 return std::ptr::null_mut();
4145 }
4146 }
4147 }
4148
4149 // c:2240-2247 — print-only path: typeset -p / typeset name (no value).
4150 if usepm != 0 && on == 0 && _roff == 0 && asg_ref.is_some_and(|a| !ASG_VALUEP(a)) {
4151 // c:2241 — `int with_ns = OPT_ISSET(ops,'m') ? PRINT_WITH_NAMESPACE : 0;`
4152 let with_ns = if OPT_ISSET(ops, b'm') {
4153 // c:2241
4154 PRINT_WITH_NAMESPACE
4155 } else {
4156 0
4157 };
4158 // c:Src/builtin.c:2761-2765 — `typeset -p1 NAME` adds PRINT_LINE
4159 // (one array/assoc element per line). The listing path (~3805)
4160 // already parses this; the explicit-name print path here did not,
4161 // so `typeset -p1 myarray` printed the single-line `( a b c )`
4162 // form instead of zsh's multi-line `(\n a\n b\n c\n)`.
4163 let line_flag =
4164 if OPT_HASARG(ops, b'p') && OPT_ARG(ops, b'p').map(|a| a.trim()) == Some("1") {
4165 PRINT_LINE
4166 } else {
4167 0
4168 };
4169 if let Some(pm_r) = unsafe { pm.as_mut() } {
4170 if OPT_ISSET(ops, b'p') {
4171 // c:2242
4172 // c:2243 — `paramtab->printnode(&pm->node, PRINT_TYPESET|with_ns);`
4173 printparamnode(pm_r, PRINT_TYPESET | with_ns | line_flag);
4174 } else if !OPT_ISSET(ops, b'g') // c:2244
4175 && (!isset(TYPESETSILENT) || OPT_ISSET(ops, b'm'))
4176 // c:2245
4177 {
4178 // c:2246 — `paramtab->printnode(&pm->node, PRINT_INCLUDEVALUE|with_ns);`
4179 printparamnode(pm_r, PRINT_INCLUDEVALUE | with_ns);
4180 }
4181 }
4182 return pm; // c:2247
4183 }
4184
4185 // c:2355-2378 — tc (type-conversion) branch: recreate the param.
4186 if tc != 0 && !OPT_ISSET(ops, b'p') {
4187 on |= (!off as u32 & (PM_READONLY | PM_EXPORTED) & pm_flags) as i32; // c:2357
4188 if let Some(pm_r) = pm_ref {
4189 pm_r.node.flags &= !(PM_READONLY as i32); // c:2359
4190 }
4191 // c:2364 — keeplocal = pm->level (used by createparam path)
4192 // c:2372-2375 — carry scalar value across type change.
4193 // c:2378 — unsetparam_pm(pm, 0, 1)
4194 if let Some(pm_r) = unsafe { pm.as_mut() } {
4195 unsetparam_pm(pm_r, 0, 1);
4196 }
4197 pname_owned = pname.to_string(); // c:2377
4198 }
4199
4200 // c:2381-2467 — newspecial path: preserve special-param struct.
4201 // c:2469-2510 — createparam + assignment dispatch for new/converted.
4202 // c:2512-2453 — apply value via assignsparam/setaparam/sethparam.
4203 // These call into a 2-level helper chain (typeset_setwidth,
4204 // typeset_setbase, assignsparam, etc.) — the available Rust
4205 // ports drive single-attribute setters. The dispatcher entry
4206 // (bin_typeset at c:2655) walks the option matrix and invokes
4207 // those setters directly today.
4208 let _ = (altpm, pname_owned, _keeplocal, _dont_set, _readonly);
4209
4210 // c:2547 — `return pm;`
4211 pm
4212}
4213
4214// !!! WARNING: RUST-ONLY STATE (no C counterpart) !!!
4215// C's `-m` arm (Src/builtin.c:3084-3090) calls `typeset_single` directly on
4216// every matched Param. zshrs's typeset_single equivalent is bin_typeset's own
4217// per-arg loop, so that arm re-enters bin_typeset with the matched names as
4218// literal args; this flag makes the re-entry skip the `-m` arm instead of
4219// looping forever, while leaving `ops` (and hence `on`/`off`/`roff` and the
4220// c:2244 print gate) byte-identical to the outer call.
4221thread_local! {
4222 static TYPESET_M_APPLY: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
4223}
4224
4225/// Port of `bin_typeset()` from `Src/builtin.c:2655`.
4226/// C decl: `bin_typeset(char *name, char **argv, LinkList assigns, Options ops, int func)`
4227/// C: `int bin_typeset(char *name, char **argv, LinkList assigns,
4228/// Options ops, int func)`.
4229///
4230/// The C body (~500 lines) ports here in two layers: the option-flag
4231/// matrix + conflict-resolution / dispatch (faithfully translated)
4232/// and the per-arg param-setting loop (delegated to typeset_single
4233/// already ported above).
4234/// WARNING: param names don't match C — Rust=(name, argv, func) vs C=(name, argv, assigns, ops, func)
4235pub fn bin_typeset(
4236 name: &str,
4237 argv: &[String], // c:2655
4238 ops: &options,
4239 func: i32,
4240) -> i32 {
4241 // !!! DASH-FAITHFUL GATE (no C counterpart) !!! dash's `local` prints nothing
4242 // when given no names, and `local x` makes a local that INHERITS the
4243 // visible value (`x=5; f() { local x; echo $x; }` prints 5), where zsh's
4244 // starts it unset. Re-enter with the inherited value spelled out.
4245 if name == "local" && crate::dash_mode::dash_faithful() {
4246 // dash: `local` outside a function is a fatal "not in a function" (status 2).
4247 if crate::ported::params::locallevel.load(std::sync::atomic::Ordering::Relaxed) == 0 {
4248 zerrnam(name, "not in a function");
4249 return crate::dash_mode::fatal_error_status().unwrap_or(2);
4250 }
4251 if argv.is_empty() {
4252 return 0;
4253 }
4254 let inherited: Vec<String> = argv
4255 .iter()
4256 .map(|a| match crate::ported::params::getsparam(a) {
4257 Some(v) if !a.contains('=') => format!("{a}={v}"),
4258 _ => a.clone(),
4259 })
4260 .collect();
4261 if inherited != argv {
4262 return bin_typeset(name, &inherited, ops, func);
4263 }
4264 }
4265
4266 // !!! BASH-MODE (no C counterpart) !!! bash(1) `local -`: "the shell option
4267 // settings (set -/+) are restored on function return". zsh gets that from
4268 // LOCAL_OPTIONS, whose restore the function-exit path (exec.rs c:6129) runs.
4269 if name == "local"
4270 && crate::dash_mode::bash_mode()
4271 && crate::ported::params::locallevel.load(std::sync::atomic::Ordering::Relaxed) > 0
4272 && OPT_ISSET(ops, b'-')
4273 {
4274 // The option parser consumed the lone `-` (c:337-341) and left its
4275 // indicator in `ops.ind['-']`.
4276 crate::ported::options::opt_state_set("localoptions", true);
4277 if argv.is_empty() {
4278 return 0;
4279 }
4280 }
4281
4282 // !!! BASH-MODE (no C counterpart) !!! bash: `local` outside a function is
4283 // an error, status 1 (zsh accepts it as a plain typeset).
4284 if name == "local"
4285 && crate::dash_mode::bash_mode()
4286 && crate::ported::params::locallevel.load(std::sync::atomic::Ordering::Relaxed) == 0
4287 {
4288 zwarnnam(name, "can only be used in a function");
4289 return 1;
4290 }
4291
4292 // !!! BASH-MODE (no C counterpart) !!! bash's `declare -F` / `typeset -F`
4293 // is the function-NAME listing (zsh's -F is fixed-point float): bare
4294 // `declare -F` prints `declare -f NAME` per function; `declare -F NAME`
4295 // prints NAME and returns 1 when it is not a function.
4296 if (name == "declare" || name == "typeset")
4297 && crate::dash_mode::bash_mode()
4298 && OPT_ISSET(ops, b'F')
4299 && !OPT_ISSET(ops, b'f')
4300 {
4301 let names: Vec<String> = {
4302 let mut v: Vec<String> = shfunctab_lock()
4303 .read()
4304 .map(|t| t.iter().map(|(n, _)| n.clone()).collect())
4305 .unwrap_or_default();
4306 v.retain(|n| !n.starts_with("_zshrs_anon_"));
4307 v.sort();
4308 v
4309 };
4310 let mut rc = 0;
4311 let queries: Vec<&String> = argv.iter().filter(|a| !a.starts_with('-')).collect();
4312 if queries.is_empty() {
4313 for n in &names {
4314 println!("declare -f {}", n);
4315 }
4316 }
4317 for q in queries {
4318 if names.contains(q) {
4319 println!("{}", q);
4320 } else {
4321 rc = 1;
4322 }
4323 }
4324 return rc;
4325 }
4326
4327 // PFA-SMR aspect: bin_typeset is the C dispatch site for
4328 // typeset/declare/integer/float/local/export/readonly/private —
4329 // every one of those state-mutating builtins lands here with a
4330 // funcid (BIN_EXPORT/BIN_READONLY/BIN_TYPESET/...) discriminant.
4331 // Emit a per-name event per the recorder schema.
4332 #[cfg(feature = "recorder")]
4333 if crate::recorder::is_enabled() {
4334 let ctx = crate::recorder::recorder_ctx_global();
4335 // Collect option letters from argv `-x`/`+x` flag args AND
4336 // from the BUILTIN's defopts (the auto-set option chars C
4337 // applies before dispatching bin_typeset). Without the
4338 // defopts, `integer count=42` (defopts="i"), `float pi=1.0`
4339 // (defopts="E"), and `typeset -A h=(...)` whose `A` came
4340 // from a parent shape, lost their type bits and the
4341 // recorder emitted `[scalar]` for everything.
4342 let mut letters = String::new();
4343 let mut tied_mode = false;
4344 for a in argv {
4345 if a.starts_with('-') || a.starts_with('+') {
4346 let body = &a[1..];
4347 letters.push_str(body);
4348 if body.contains('T') {
4349 tied_mode = true;
4350 }
4351 }
4352 }
4353 // Add the option letters that defopts pre-set in `ops`.
4354 for ch in [b'i', b'E', b'F', b'A', b'a', b'r', b'x', b'g', b'l', b'u'] {
4355 if OPT_ISSET(&ops, ch) && !letters.contains(ch as char) {
4356 letters.push(ch as char);
4357 }
4358 }
4359 // Funcid-driven attr seeding: BIN_EXPORT seeds nothing
4360 // (recorder uses emit_export for those), BIN_READONLY seeds
4361 // SCALAR|READONLY. INTEGER/FLOAT/ASSOC/ARRAY now flow via
4362 // the defopts injection above into ParamAttrs::from_flag_chars.
4363 let mut attrs = crate::recorder::ParamAttrs::from_flag_chars(&letters);
4364 match func {
4365 crate::ported::builtin::BIN_READONLY => {
4366 attrs.set(crate::recorder::ParamAttrs::SCALAR);
4367 attrs.set(crate::recorder::ParamAttrs::READONLY);
4368 }
4369 _ => {}
4370 }
4371 // BIN_EXPORT routes to emit_export (different schema row).
4372 if func == crate::ported::builtin::BIN_EXPORT {
4373 for a in argv {
4374 if a == "-p" || a.starts_with('-') {
4375 continue;
4376 }
4377 if let Some((k, v)) = a.split_once('=') {
4378 crate::recorder::emit_export(k, Some(v), ctx.clone());
4379 } else {
4380 crate::recorder::emit_export(a, None, ctx.clone());
4381 }
4382 }
4383 } else {
4384 // Suppress the emit when invoked as `local`/`private` inside
4385 // a function — those scope to the frame and don't merit a
4386 // top-level state-mutation row. local_scope_depth is tracked
4387 // by the executor; defer to the global locallevel_param counter.
4388 let is_locallike = matches!(name, "local" | "private");
4389 let inside_function = locallevel_param.load(std::sync::atomic::Ordering::Relaxed) > 0;
4390 if !is_locallike || !inside_function {
4391 let mut tied_seen = 0usize;
4392 for a in argv {
4393 if a.starts_with('-') || a.starts_with('+') {
4394 continue;
4395 }
4396 if tied_mode {
4397 // For `typeset -T X Y [SEP]`, only X and Y are names.
4398 tied_seen += 1;
4399 if tied_seen > 2 {
4400 break;
4401 }
4402 }
4403 if let Some((k, v)) = a.split_once('=') {
4404 crate::recorder::emit_typeset_attrs(k, Some(v), attrs, ctx.clone());
4405 } else {
4406 crate::recorder::emit_typeset_attrs(a, None, attrs, ctx.clone());
4407 }
4408 }
4409 }
4410 }
4411 }
4412 let mut ops = ops.clone();
4413 let mut on: u32 = 0; // c:2661
4414 let mut off: u32 = 0; // c:2661
4415 let mut returnval: i32 = 0; // c:2664
4416 let mut printflags: i32 = PRINT_WITH_NAMESPACE; // c:2664
4417 let hasargs = !argv.is_empty(); // c:2665
4418
4419 // c:2668-2670 — POSIX bash/ksh ignore -p with args under
4420 // readonly/export.
4421 let posix = isset(POSIXBUILTINS);
4422 if (func == BIN_READONLY || func == BIN_EXPORT) && posix && hasargs {
4423 // c:2668
4424 ops.ind[b'p' as usize] = 0; // c:2670
4425 }
4426
4427 // c:2673 — `if (OPT_ISSET(ops,'f')) return bin_functions(...)`.
4428 if OPT_ISSET(&ops, b'f') {
4429 // c:2673
4430 return bin_functions(name, argv, &ops, func); // c:2673
4431 }
4432
4433 // c:2676 — POSIX readonly forces -g unless explicit +g.
4434 if func == BIN_READONLY && posix && !OPT_PLUS(&ops, b'g') {
4435 // c:2676
4436 ops.ind[b'g' as usize] = 1; // c:2677
4437 }
4438
4439 // c:2691-2706 — translate optstr letters into PM_* flag bits.
4440 let mut bit: u32 = PM_ARRAY; // c:2660
4441 for ch in TYPESET_OPTSTR.chars() {
4442 // c:2691
4443 let optval = ch as u8;
4444 if OPT_MINUS(&ops, optval) {
4445 on |= bit;
4446 }
4447 // c:2694-2695
4448 else if OPT_PLUS(&ops, optval) {
4449 off |= bit;
4450 }
4451 // c:2696-2697
4452 // c:2698-2706 — `-n` only allows readonly/upper/hideval/hide (55030).
4453 else {
4454 bit <<= 1;
4455 continue;
4456 }
4457 if OPT_MINUS(&ops, b'n') && (bit & !(PM_READONLY | PM_UPPER | PM_HIDEVAL | PM_HIDE)) != 0
4458 // c:2701
4459 {
4460 zwarnnam(name, &format!("-{} not allowed with -n", ch)); // c:2702
4461 }
4462 bit <<= 1;
4463 }
4464 // c:2708-2715 — -n / +n conflict resolution.
4465 if OPT_MINUS(&ops, b'n') {
4466 // c:2709-2711 — only readonly/upper/hideval/hide combine with -n;
4467 // anything else is a (silent) status-1 rejection (the zwarnnam
4468 // here is commented out in C — the per-bit loop above already
4469 // warned).
4470 if ((on | off) & !(PM_READONLY | PM_UPPER | PM_HIDEVAL | PM_HIDE)) != 0 {
4471 // c:2710
4472 return 1; // c:2711
4473 }
4474 on |= PM_NAMEREF; // c:2712
4475 } else if OPT_PLUS(&ops, b'n') {
4476 // c:2714
4477 off |= PM_NAMEREF; // c:2715
4478 }
4479 let roff = off; // c:2716
4480
4481 // c:2719-2740 — sanity checks: remove conflicting attrs.
4482 if (on & PM_FFLOAT) != 0 {
4483 // c:2719
4484 off |= PM_UPPER | PM_ARRAY | PM_HASHED | PM_INTEGER | PM_EFLOAT; // c:2720
4485 on &= !PM_EFLOAT; // c:2722
4486 }
4487 if (on & PM_EFLOAT) != 0 {
4488 // c:2724
4489 off |= PM_UPPER | PM_ARRAY | PM_HASHED | PM_INTEGER | PM_FFLOAT; // c:2725
4490 }
4491 if (on & PM_INTEGER) != 0 {
4492 // c:2726
4493 off |= PM_UPPER | PM_ARRAY | PM_HASHED | PM_EFLOAT | PM_FFLOAT; // c:2727
4494 }
4495 if (on & (PM_LEFT | PM_RIGHT_Z)) != 0 {
4496 // c:2731
4497 off |= PM_RIGHT_B; // c:2732
4498 }
4499 if (on & PM_RIGHT_B) != 0 {
4500 // c:2733
4501 off |= PM_LEFT | PM_RIGHT_Z; // c:2734
4502 }
4503 if (on & PM_UPPER) != 0 {
4504 off |= PM_LOWER;
4505 } // c:2735-2736
4506 if (on & PM_LOWER) != 0 {
4507 off |= PM_UPPER;
4508 } // c:2737-2738
4509 if (on & PM_HASHED) != 0 {
4510 off |= PM_ARRAY;
4511 } // c:2739-2740
4512 if (on & PM_TIED) != 0 {
4513 // c:2741
4514 off |= PM_INTEGER | PM_EFLOAT | PM_FFLOAT | PM_ARRAY | PM_HASHED; // c:2742
4515 }
4516 on &= !off; // c:2744
4517
4518 queue_signals(); // c:2746
4519
4520 // c:2748-2772 — `-p` print-mode: PRINT_POSIX_EXPORT / READONLY /
4521 // TYPESET, plus optional -p N for line-style.
4522 if OPT_ISSET(&ops, b'p') {
4523 // c:2748
4524 // c:2750 — C excludes ksh emulation from the POSIX form, which is
4525 // right for zsh's own `emulate ksh`. But the real Korn shells DO
4526 // print it: `ksh93u+m -c 'readonly RR=1; readonly -p'` answers
4527 // `readonly RR=1`, not zsh's `typeset -r RR=1`, and so does mksh.
4528 // So the DROP-INS take the POSIX branch; `--ksh --zsh` (zsh-style
4529 // emulation) keeps C's behaviour.
4530 let korn_dropin = matches!(
4531 crate::extensions::emulation_startup::personality(),
4532 crate::extensions::emulation_startup::Personality::Ksh93
4533 | crate::extensions::emulation_startup::Personality::Mksh
4534 | crate::extensions::emulation_startup::Personality::Pdksh
4535 );
4536 if posix && (!EMULATION(EMULATE_KSH) || korn_dropin) {
4537 // c:2750
4538 printflags |= match func {
4539 BIN_EXPORT => PRINT_POSIX_EXPORT, // c:2752
4540 BIN_READONLY => PRINT_POSIX_READONLY, // c:2754
4541 _ => PRINT_TYPESET, // c:2756
4542 };
4543 } else {
4544 printflags |= PRINT_TYPESET; // c:2758
4545 }
4546 if OPT_HASARG(&ops, b'p') {
4547 // c:2761
4548 let arg = OPT_ARG(&ops, b'p').unwrap_or("");
4549 match arg.trim().parse::<i32>() {
4550 // c:2763
4551 Ok(1) => printflags |= PRINT_LINE, // c:2765
4552 Ok(0) => {} // c:2770 -p0 == -p
4553 _ => {
4554 zwarnnam(name, &format!("bad argument to -p: {}", arg)); // c:2767
4555 unqueue_signals();
4556 return 1; // c:2769
4557 }
4558 }
4559 }
4560 }
4561
4562 // c:2775-2795 — no-args path: list whatever options select.
4563 if !hasargs {
4564 // c:2775
4565 if !OPT_ISSET(&ops, b'm') {
4566 // c:2779
4567 printflags &= !PRINT_WITH_NAMESPACE; // c:2780
4568 }
4569 // c:2776 — `int exclude = 0;`
4570 let mut exclude: u32 = 0; // c:2776
4571 if !OPT_ISSET(&ops, b'p') {
4572 // c:2782
4573 if (on | roff) == 0 {
4574 // c:2783
4575 printflags |= PRINT_TYPE; // c:2784
4576 }
4577 if roff != 0 || OPT_ISSET(&ops, b'+') {
4578 // c:2785
4579 printflags |= PRINT_NAMEONLY; // c:2786
4580 }
4581 } else if (printflags & (PRINT_POSIX_EXPORT | PRINT_POSIX_READONLY)) != 0 {
4582 // c:2787
4583 // c:2788-2791 —
4584 // /*
4585 // * For POSIX export/readonly, exclude non-scalars unless
4586 // * explicitly requested.
4587 // */
4588 // exclude = (PM_ARRAY|PM_HASHED) & ~(on|roff);
4589 exclude = (PM_ARRAY | PM_HASHED) & !((on as u32) | (roff as u32)); // c:2792
4590 }
4591 // !!! BASH-MODE (no C counterpart) !!! bash lists a bare attribute query
4592 // (`declare -x`, `export`, `readonly`, `declare -i|-a|-A|-r`) in the
4593 // reusable `declare -x NAME="value"` form that `-p` selects here.
4594 if crate::dash_mode::bash_mode()
4595 && !OPT_ISSET(&ops, b'p')
4596 && !OPT_ISSET(&ops, b'm')
4597 && roff == 0
4598 && ((on as u32) != 0 || func == BIN_EXPORT || func == BIN_READONLY)
4599 {
4600 printflags |= PRINT_TYPESET;
4601 }
4602 // c:2792 — `scanhashtable(paramtab, 1, on|roff, 0, paramtab->printnode,
4603 // printflags|(roff ? PRINT_NAMEONLY : 0));`
4604 //
4605 // Walk paramtab (sorted=1, alphabetical) filtering by on|roff
4606 // and dispatch printparamnode for each match. Previously inlined
4607 // a `println!("{}={}", k, v)` which:
4608 // - Ignored printflags (PRINT_TYPESET, PRINT_POSIX_EXPORT,
4609 // PRINT_POSIX_READONLY, PRINT_NAMEONLY) so `export -p` had
4610 // zero rows, `typeset -p` skipped attribute letters, and
4611 // `readonly -p` had no `readonly ` prefix.
4612 // - Read pm.u_str directly so PM_INTEGER / PM_*FLOAT /
4613 // PM_ARRAY / PM_HASHED values printed as empty.
4614 // printparamnode (params.c:6123) handles all of these.
4615 let printflags_final = printflags | if roff != 0 { PRINT_NAMEONLY } else { 0 }; // c:2792
4616 // c:2792 scanmatchtable flags1=on|roff, flags2=0.
4617 let on_roff = (on as u32) | (roff as u32);
4618 // PM_RO_BY_DESIGN expansion: zshrs's special-param
4619 // setup (vm_helper.rs:1054+) replaces the dropped
4620 // PM_READONLY bit with PM_RO_BY_DESIGN so internal
4621 // writes still pass assignstrvalue's PM_READONLY
4622 // guard. `typeset -r` listing must match on either
4623 // bit to surface those entries. Mirrors C zsh's
4624 // PM_READONLY_SPECIAL (= PM_SPECIAL | PM_READONLY |
4625 // PM_RO_BY_DESIGN) where the scanhashtable bit-mask
4626 // implicitly matches RO_BY_DESIGN too (both bits
4627 // set on the same params in C). #97 in docs/BUGS.md.
4628 let on_roff_expanded = if (on_roff & PM_READONLY) != 0 {
4629 on_roff | PM_RO_BY_DESIGN
4630 } else {
4631 on_roff
4632 };
4633 // c:Src/module.c:1218-1219 add_autoparam — in `zsh -f`, every
4634 // autoloadable module parameter (WATCH/watch, aliases…,
4635 // widgets/keymaps, termcap/terminfo, zsh_scheduled_events) is
4636 // a real paramtab stub: a scalar whose value is the module
4637 // name, flagged PM_AUTOLOAD. scanhashtable feeds the stubs to
4638 // printparamnode which prints `undefined NAME` (pmtypes row at
4639 // Src/params.c:6011) and suppresses them under -p
4640 // (Src/params.c:6150-6155). zshrs keeps always-functional
4641 // placeholder params instead, so synthesize the stub rows here
4642 // for every not-yet-loaded module and suppress the placeholder
4643 // rows for the same names (zsh -f shows `undefined WATCH`, not
4644 // a set `WATCH`).
4645 let mut stubs: Vec<(&'static str, &'static str)> = crate::vm_helper::autoload_param_stubs();
4646 // c:Src/module.c:1218-1223 add_autoparam — the stub IS the
4647 // paramtab node in C, so `typeset -h +g WATCH` inside a function
4648 // pushes a LOCAL over it (`pm->old = stub`, Src/params.c
4649 // createparam) and the stub becomes invisible: the scan sees only
4650 // the local, and once `unset` flags that local PM_UNSET
4651 // printparamnode drops it (c:Src/params.c:6136-6148). zshrs keeps
4652 // the stub OUT of paramtab and synthesises the row here, so the
4653 // synthetic row survived the shadowing and `typeset -h +g -m '*';
4654 // unset -m '*'; typeset` still listed `undefined WATCH` /
4655 // `undefined watch` (B02typeset.ztst:38). Drop the synthetic row
4656 // for any name the paramtab currently holds at a non-zero level —
4657 // that node is the local shadow and must be listed in its place.
4658 {
4659 let tab = paramtab().read().unwrap();
4660 // A node at a non-zero level IS the shadow; a node flagged
4661 // PM_UNSET inside a function is the shadow after `unset`
4662 // (c:Src/params.c:unsetparam_pm keeps the local node and
4663 // only raises PM_UNSET, so the stub underneath stays
4664 // covered until the scope ends).
4665 let in_func = locallevel.load(Ordering::Relaxed) != 0;
4666 stubs.retain(|(n, _)| {
4667 !tab.get(*n).is_some_and(|pm| {
4668 pm.level != 0 || (in_func && (pm.node.flags as u32 & PM_UNSET) != 0)
4669 })
4670 });
4671 }
4672 let names: Vec<String> = {
4673 let tab = paramtab().read().unwrap();
4674 let mut names: Vec<String> = tab
4675 .iter()
4676 .filter(|(k, pm)| {
4677 let f = pm.node.flags as u32;
4678 // c:2793 — scanhashtable passes PM_UNSET entries
4679 // through; printparamnode (c:6133-6144) prints
4680 // PM_DEFAULTED (declared-but-unset) ones name-only
4681 // and drops the rest. Keep DEFAULTED here so
4682 // `typeset -n` lists placeholder refs by name.
4683 // c:Src/params.c:6137-6146 — an unset parameter is still shown,
4684 // name only, by the POSIX `readonly -p` / `export -p` forms
4685 // when it carries that attribute.
4686 let posix_unset_shown = (printflags & (PRINT_POSIX_READONLY | PRINT_POSIX_EXPORT)) != 0
4687 && (f & (PM_READONLY | PM_EXPORTED)) != 0;
4688 if (f & PM_UNSET) != 0 && (f & PM_DEFAULTED) != PM_DEFAULTED && !posix_unset_shown {
4689 return false;
4690 }
4691 // Unloaded-module names print as autoload stubs
4692 // (merged below) — never as their placeholder row.
4693 if stubs.iter().any(|(n, _)| n == k) {
4694 return false;
4695 }
4696 // c:Src/builtin.c:2789 — `scanhashtable(paramtab, 1,
4697 // on|roff, exclude, paramtab->printnode, printflags)`.
4698 // The C scan has NO PM_HIDE exclusion: `addparamdef`
4699 // (Src/module.c:1060-1074) inserts every zsh/parameter
4700 // magic assoc into the REAL paramtab via createparam /
4701 // createspecialhash, so once the module has booted zsh
4702 // lists them (`association readonly parameters`,
4703 // `array readonly patchars`, …) — verified against
4704 // `zsh -f -c 'zmodload zsh/parameter; typeset -A'`.
4705 // A blanket PM_HIDE skip used to live here for bug
4706 // #371 (`typeset -A` listing magic-assocs in a shell
4707 // where the module was never loaded); that case is
4708 // already covered by the autoload-stub filter above,
4709 // which drops zshrs's paramtab placeholder for every
4710 // module that has not booted and prints the C
4711 // `undefined NAME` stub instead.
4712 // c:2794 — `scanhashtable(paramtab, 1, on|roff, exclude,
4713 // paramtab->printnode, printflags)`. `flags2` (exclude) is
4714 // scanhashtable's REJECT mask: c:Src/hashtable.c:394
4715 // `if (... (flags2 && (hn->flags & flags2))) continue;`.
4716 // c:Src/hashtable.c:394 tests the TABLE node's flags; for
4717 // a name whose private wrap_private lifted, that node is the
4718 // private in C (printprivatenode then prints the one
4719 // beneath, which is the node here).
4720 let f = crate::ported::modules::param_private::LIFTED_PRIVATES
4721 .lock()
4722 .ok()
4723 .and_then(|l| l.iter().rev().find(|(n, _)| n == *k).map(|(_, fl)| *fl as u32))
4724 .unwrap_or(f);
4725 // !!! DASH-FAITHFUL GATE (no C counterpart) !!! dash's `readonly` listing
4726 // holds only variables the script made readonly; zsh's read-only
4727 // specials (`$!`, `$#`, `$$`, `LINENO`, `PPID`, ...) are not dash variables.
4728 if func == BIN_READONLY
4729 && (f & PM_SPECIAL) != 0
4730 && crate::dash_mode::dash_faithful()
4731 {
4732 return false;
4733 }
4734 if exclude != 0 && (f & exclude) != 0 {
4735 return false; // c:394
4736 }
4737 on_roff_expanded == 0 || (f & on_roff_expanded) != 0
4738 })
4739 .map(|(k, _)| k.clone())
4740 .collect();
4741 // Stub flags are PM_SCALAR|PM_AUTOLOAD (module.c:1218-1219)
4742 // — apply the same scanmatchtable flags1 test (PM_AUTOLOAD
4743 // is never in on|roff, so any flag filter excludes them:
4744 // `typeset +x -r` / `typeset +i` list none, matching zsh).
4745 for (n, _) in &stubs {
4746 if on_roff_expanded == 0 || (PM_AUTOLOAD & on_roff_expanded) != 0 {
4747 names.push((*n).to_string());
4748 }
4749 }
4750 names.sort_by(|a, b| hnamcmp(a, b));
4751 names
4752 };
4753 // c:Src/params.c:6275 PM_TIED partner lookup — printparamnode
4754 // reads `paramtab` to fetch the tied peer (e.g. PATH→path).
4755 // Pre-clone the pm so the print runs WITHOUT holding any
4756 // paramtab lock; otherwise the partner-lookup inside
4757 // printparamnode would deadlock on the same write lock.
4758 // printparamnode mutates hn for the PM_TIED swap path (Bug
4759 // #410) — the mutation lands on the local clone only,
4760 // matching C's "throwaway-print-side" semantics.
4761 for k in names {
4762 // c:Src/module.c:1218-1219 — autoload stub: scalar param,
4763 // u.str = module name, PM_AUTOLOAD. printparamnode prints
4764 // `undefined NAME` (PRINT_TYPE) / `NAME` (PRINT_NAMEONLY)
4765 // and returns early under -p.
4766 if let Some((_, module)) = stubs.iter().find(|(n, _)| *n == k) {
4767 let mut stub_pm = crate::ported::zsh_h::param {
4768 node: crate::ported::zsh_h::hashnode {
4769 next: None,
4770 nam: k.clone(),
4771 flags: (PM_SCALAR | PM_AUTOLOAD) as i32, // c:1219
4772 },
4773 u_data: 0,
4774 u_tied: None,
4775 u_arr: None,
4776 u_str: Some((*module).to_string()), // c:1218 setsparam(pnam, module)
4777 u_val: 0,
4778 u_dval: 0.0,
4779 u_hash: None,
4780 gsu_s: None,
4781 gsu_i: None,
4782 gsu_f: None,
4783 gsu_a: None,
4784 gsu_h: None,
4785 base: 0,
4786 width: 0,
4787 env: None,
4788 ename: None,
4789 old: None,
4790 level: 0,
4791 };
4792 printparamnode(&mut stub_pm, printflags_final); // c:2792
4793 continue;
4794 }
4795 let mut pm_clone = match paramtab().read() {
4796 Ok(tab) => match tab.get(&k) {
4797 Some(pm) => pm.clone(),
4798 None => continue,
4799 },
4800 Err(_) => continue,
4801 };
4802 printparamnode(&mut pm_clone, printflags_final); // c:2792
4803 }
4804 unqueue_signals();
4805 return 0; // c:2794
4806 }
4807
4808 // c:2799-2810 — `local` (or +g) implies PM_LOCAL.
4809 let nm0 = name.chars().next().unwrap_or(' ');
4810 if nm0 == 'l' || OPT_PLUS(&ops, b'g') {
4811 // c:2799
4812 on |= PM_LOCAL; // c:2800
4813 } else if crate::dash_mode::in_ksh93_posix_function() && !OPT_ISSET(&ops, b'g') {
4814 // ksh93: `name() { typeset x; }` does not localize.
4815 ops.ind[b'g' as usize] = 1;
4816 } else if !OPT_ISSET(&ops, b'g') {
4817 // c:2801
4818 if OPT_MINUS(&ops, b'x') {
4819 // c:2802
4820 let globalexport = isset(GLOBALEXPORT);
4821 let ll_v = locallevel_param.load(Relaxed);
4822 if globalexport {
4823 // c:2803
4824 ops.ind[b'g' as usize] = 1; // c:2804
4825 } else if ll_v != 0 {
4826 // c:2805
4827 on |= PM_LOCAL; // c:2806
4828 }
4829 } else if !(OPT_ISSET(&ops, b'x') || OPT_ISSET(&ops, b'm')) {
4830 // c:2808
4831 on |= PM_LOCAL; // c:2809
4832 }
4833 }
4834
4835 // c:2813+ — -T tied vars + per-arg setting loop.
4836 // The full C body has dozens of paths (PM_TIED tie-pair setup at
4837 // c:2813-2900, glob -m walk at c:2905-2935, name=value assign
4838 // through typeset_single at c:2945+). The Rust port handles the
4839 // three high-frequency paths inline: assoc creation (`PM_HASHED`
4840 // + `name=(k v k v)`), array creation (`PM_ARRAY` + `name=(a b c)`),
4841 // and scalar assignment.
4842 let _ = off;
4843 let is_hashed = (on & PM_HASHED) != 0; // c:2655 `-A`
4844 let is_array = (on & PM_ARRAY) != 0; // c:2655 `-a`
4845 // c:Src/builtin.c typeset_single — when the array RHS comes from
4846 // an unquoted `$@` / `${arr[@]}` splat (e.g. `typeset -a opts=
4847 // ("$@")`), the upstream prefork has already split the value
4848 // into separate argv entries: `["opts=(a", "b", "c)"]`. C zsh's
4849 // parser captures the entire `name=(... )` shape as one ENVARRAY
4850 // token by walking paren depth at parse time so the splat fills
4851 // the array's element list. zshrs's compile path emits the
4852 // synthetic word `opts=("$@")` then runtime DQ-strip + splat
4853 // separates it. Reconstruct the single arg here: when one entry
4854 // starts with `NAME=(` and a later entry ends with `)`, rejoin
4855 // the run with spaces between elements.
4856 // c:Src/builtin.c — paren-init rejoin. C's parser hands the whole
4857 // `NAME=(e1 e2 …)` shape as one ENVARRAY token via parse-time paren-
4858 // walking so empty elements (`""` quoted to lexer Dnull-Dnull) stay
4859 // as separate list nodes. zshrs's compile path emits the args
4860 // pre-split through prefork — each `""` produces a true empty
4861 // entry in argv. The previous rejoin glued them with single-space
4862 // separators, then split_whitespace collapsed consecutive empties
4863 // → key/value swap on `typeset -A h=( "" val )`. Bug #93 in
4864 // docs/BUGS.md.
4865 //
4866 // Use `\u{1f}` (ASCII US — unit separator) as the rejoin
4867 // separator. The paren-init branch below splits on `\u{1f}` AND
4868 // whitespace, preserving consecutive `\u{1f}` as empty elements
4869 // (the original quoted-empty args) while still tolerating
4870 // single-arg-form paren-init like `a=(1 2 3)` where no rejoin
4871 // ran.
4872 const REJOIN_SEP: char = '\u{1f}';
4873 let argv: Vec<String> = {
4874 let mut out: Vec<String> = Vec::with_capacity(argv.len());
4875 let mut i = 0;
4876 while i < argv.len() {
4877 let arg = &argv[i];
4878 let open = arg.find("=(");
4879 // c:Src/lex.c:1228-1249 — whether `NAME=(` opens an array is a
4880 // LEXICAL decision, taken on the RAW character after `=`:
4881 // `e = hgetc(); if (e == '(') return ENVARRAY;`. A `(` that
4882 // arrived inside quotes never reaches that test, so
4883 // `local open='(' close=')'` is two ENVSTRINGs in C.
4884 // zshrs marks the syntactic form with REJOIN_SEP — parse.rs's
4885 // ENVARRAY synthetic word and compile_zsh's
4886 // BUILTIN_TYPESET_PAREN_PACK both emit it, and `is_paren_init`
4887 // (c:2095 `ASG_ARRAYP`) already keys on it. Requiring it here
4888 // too stops a dequoted `(` VALUE from swallowing the following
4889 // words: `local open='(' close=')'` (Completion/X/Command/
4890 // _setxkbmap sh:34) stored `open` as `(<US>close=)` and never
4891 // defined `close`, so `compquote open close` failed and
4892 // `setxkbmap -` lost its whole listing.
4893 let is_open = open.is_some()
4894 && arg.contains(REJOIN_SEP)
4895 && arg
4896 .as_bytes()
4897 .first()
4898 .is_some_and(|b| b.is_ascii_alphabetic() || *b == b'_')
4899 && !arg.ends_with(')');
4900 if is_open {
4901 // Find the matching `)` — scan forward through argv
4902 // tracking paren depth (the parser's `(` was just `(`
4903 // in the source). Each arg may have additional `(`
4904 // and `)` chars from quoted content.
4905 let mut depth: i32 = 0;
4906 for c in arg.chars() {
4907 if c == '(' {
4908 depth += 1;
4909 } else if c == ')' {
4910 depth -= 1;
4911 }
4912 }
4913 let mut buf = arg.clone();
4914 let mut j = i + 1;
4915 while depth > 0 && j < argv.len() {
4916 buf.push(REJOIN_SEP);
4917 buf.push_str(&argv[j]);
4918 for c in argv[j].chars() {
4919 if c == '(' {
4920 depth += 1;
4921 } else if c == ')' {
4922 depth -= 1;
4923 }
4924 }
4925 j += 1;
4926 }
4927 out.push(buf);
4928 i = j;
4929 } else {
4930 out.push(arg.clone());
4931 i += 1;
4932 }
4933 }
4934 out
4935 };
4936 let argv = argv.as_slice();
4937 // c:Src/builtin.c:2813-3030 — under PM_TIED (`-T scalar array [sep]`)
4938 // the third positional is the SEPARATOR character, not a param
4939 // name. The scalar and array become a tied pair: writing the
4940 // scalar splits the joined string back into the array; reading
4941 // the scalar joins the array on the separator. Direct port of the
4942 // C body's tied-pair setup at c:2813-2940 (validation) + the
4943 // typeset_single+tiedarr_gsu wiring at c:1956 + c:2945+.
4944 //
4945 // The Rust port handles the validation / scalar-array tie + value
4946 // installation as one block here rather than threading PM_TIED
4947 // through the generic per-arg loop. Beyond index 2 under -T is a
4948 // usage error in C zsh; the Rust port mirrors the same "too many
4949 // arguments" rejection.
4950 //
4951 // Bug #24 in docs/BUGS.md.
4952 // c:2807 — `if ((on & PM_TIED) && !OPT_ISSET(ops, 'p'))`: under -p the
4953 // names are printed (or reported missing) by the per-arg loop instead.
4954 let tied_mode = (on & PM_TIED) != 0 && !OPT_ISSET(&ops, b'p');
4955 if tied_mode {
4956 // c:2818-2822 — the FIRST thing the -T block does, ahead of the
4957 // argument-count checks:
4958 // if (OPT_ISSET(ops,'m')) {
4959 // zwarnnam(name, "incompatible options for -T");
4960 // unqueue_signals();
4961 // return 1;
4962 // }
4963 // `-m` takes its names as PATTERNS, which cannot express a tie; it was
4964 // silently accepted and did nothing.
4965 if OPT_ISSET(&ops, b'm') {
4966 zwarnnam(name, "incompatible options for -T"); // c:2819
4967 unqueue_signals(); // c:2820
4968 return 1; // c:2821
4969 }
4970 // c:2827-2830 — `if (nargs < 2)` reject.
4971 if argv.len() < 2 {
4972 zwarnnam(name, "-T requires names of scalar and array");
4973 unqueue_signals();
4974 return 1;
4975 }
4976 // c:2831-2833 — `if (nargs > 3)` reject.
4977 if argv.len() > 3 {
4978 zwarnnam(name, "too many arguments for -T");
4979 unqueue_signals();
4980 return 1;
4981 }
4982 // First arg: SCALAR name (with optional =value). c:2838-2840
4983 // `if (ASG_ARRAYP(&asg0))` — first arg can NOT be an array
4984 // assign; we accept either no-value or =scalar.
4985 let (sname, sval_opt): (&str, Option<String>) = match argv[0].find('=') {
4986 Some(i) => (&argv[0][..i], Some(argv[0][i + 1..].to_string())),
4987 None => (argv[0].as_str(), None),
4988 };
4989 // c:2838-2846 —
4990 // asg0 = *asg;
4991 // if (ASG_ARRAYP(&asg0)) {
4992 // zwarnnam(name, "first argument of tie must be scalar: %s",
4993 // asg0.name);
4994 // return 1;
4995 // }
4996 // ASG_ARRAYP is the parser's flag for an unquoted `NAME=(`. zshrs
4997 // carries it as the `\u{1f}` REJOIN_SEP that compile_zsh's
4998 // BUILTIN_TYPESET_PAREN_PACK / BUILTIN_TYPESET_PAREN_CLOSE put inside
4999 // the paren body (`S=(\u{1f}a\u{1f}b\u{1f})`, empty `S=(\u{1f})`);
5000 // a quoted `"S=(a b)"` is a scalar assignment of the text `(a b)` and
5001 // carries no marker (same test as `is_paren_init` in the per-arg loop).
5002 if let Some(v) = &sval_opt {
5003 if v.starts_with('(') && v.ends_with(')') && v.len() >= 2 && v.contains('\u{1f}') {
5004 unqueue_signals(); // c:2842
5005 zwarnnam(
5006 name,
5007 &format!("first argument of tie must be scalar: {}", sname),
5008 ); // c:2843
5009 return 1; // c:2845
5010 }
5011 }
5012 // Second arg: ARRAY name (with optional =(elements...) init).
5013 // Per c:2847-2854, second arg must be array-shape if it carries
5014 // a value. The Rust port accepts either `arr` or `arr=(a b c)`.
5015 let (aname, aval_opt): (&str, Option<Vec<String>>) = {
5016 let a = argv[1].as_str();
5017 if let Some(eq_idx) = a.find("=(") {
5018 let aname = &a[..eq_idx];
5019 // Strip `=(` prefix and `)` suffix. The parser emits
5020 // array elements separated by REJOIN_SEP (`\u{1f}`)
5021 // sentinels — see parse.rs:2444 commentary. Empty
5022 // strings between sentinels are intentional (zsh's
5023 // assoc paren-init relies on them, bug #93).
5024 let rest = &a[eq_idx + 2..];
5025 let inner = rest.trim_end_matches(')');
5026 let parts: Vec<String> = if inner.contains('\u{1f}') {
5027 // Only the boundary empties from the `=(` / `)` argv
5028 // fragments are dropped: a `[k]=` triad's empty VALUE
5029 // (c:Src/subst.c:77) is an element and must keep its
5030 // slot, as in the generic array-init path below.
5031 let mut v: Vec<String> = inner.split('\u{1f}').map(String::from).collect();
5032 if v.first().is_some_and(|s| s.is_empty()) {
5033 v.remove(0);
5034 }
5035 if v.last().is_some_and(|s| s.is_empty()) {
5036 v.pop();
5037 }
5038 v
5039 } else {
5040 // Fallback: whitespace split for synthetic args
5041 // that didn't pass through the REJOIN_SEP emitter.
5042 inner.split_whitespace().map(|s| s.to_string()).collect()
5043 };
5044 (aname, Some(parts))
5045 } else if let Some(eq_idx) = a.find('=') {
5046 // `arr=val` (scalar form for array slot) — reject per
5047 // c:2849-2854 "second argument of tie must be array".
5048 let _ = eq_idx;
5049 zwarnnam(
5050 name,
5051 &format!("second argument of tie must be array: {}", a),
5052 );
5053 unqueue_signals();
5054 return 1;
5055 } else {
5056 (a, None)
5057 }
5058 };
5059 // c:2857-2860 — `can't tie a variable to itself`.
5060 if sname == aname {
5061 zerrnam(name, &format!("can't tie a variable to itself: {}", sname));
5062 unqueue_signals();
5063 return 1;
5064 }
5065 // c:2861-2864 — `can't tie array elements` (subscripted name).
5066 if sname.contains('[') || aname.contains('[') {
5067 zerrnam(name, &format!("can't tie array elements: {}", sname));
5068 unqueue_signals();
5069 return 1;
5070 }
5071 // c:2866-2870 — `only one tied parameter can have value`.
5072 if sval_opt.is_some() && aval_opt.is_some() {
5073 zerrnam(
5074 name,
5075 &format!("only one tied parameter can have value: {}", sname),
5076 );
5077 unqueue_signals();
5078 return 1;
5079 }
5080 // c:2876-2898 — joinchar parse. Third argument, if given, is
5081 // character used to join the elements of the array in the
5082 // scalar.
5083 let joinchar: i32 = if argv.len() == 3 {
5084 // c:2884 `joinstr = nextasg->name;` — a metafied byte reaches
5085 // zshrs as the CHAR Meta followed by the char of `byte ^ 32`, so
5086 // the Meta test is on chars: the first UTF-8 byte of U+0083 is
5087 // 0xc2, never 0x83, and the old byte test could not fire.
5088 let joinstr = &argv[2];
5089 let mut cs = joinstr.chars();
5090 match (cs.next(), cs.next()) {
5091 (None, _) => 0, // c:2889-2890 `else if (!*joinstr) joinchar = 0;`
5092 (Some(m), Some(n)) if m as u32 == Meta as u32 => {
5093 ((n as u32) ^ 32) as i32 // c:2891-2892 `joinchar = joinstr[1] ^ 32;`
5094 }
5095 _ => joinstr.as_bytes()[0] as i32, // c:2893-2894 `joinchar = *joinstr;`
5096 }
5097 } else {
5098 ':' as i32 // c:2891-2892 `if (!joinstr) joinchar = ':';`
5099 };
5100 // c:Src/params.c:4320-4332 tiedarrsetfn — the separator is the join
5101 // character, metafied when it is imeta; the same bytes join the array
5102 // back in tiedarrgetfn (c:4303-4304 zjoin). NUL is imeta in C (`Meta
5103 // ' '`), but zshrs keeps NUL unmetafied inside values, so it stays raw.
5104 let joinsep: String = match joinchar as u8 {
5105 0 => "\0".to_string(),
5106 b if crate::ported::utils::imeta_byte(b) => [char::from(Meta), char::from(b ^ 32)].iter().collect(),
5107 b => char::from(b).to_string(),
5108 };
5109
5110 // c:Src/builtin.c:2940-2944 — when the scalar already exists
5111 // and has a value, save it for seeding the tied array:
5112 // `oldval = ztrdup(getsparam(asg0.name));`
5113 // The Rust port checks paramtab AND the environ inherited at
5114 // startup so `typeset -T PATH path :` over an env-imported
5115 // PATH seeds `path` from the live PATH instead of clobbering
5116 // it with empty.
5117 // c:2900-2901 — `pm = getnode(asg0.name); apm = getnode(asg->name);`
5118 // then c:2903-2943 decides between erroring, re-tying, and updating an
5119 // EXISTING tie. None of that block was ported: the code below simply
5120 // installed a fresh pair every time, which silently accepted three
5121 // cases C rejects and lost every attribute the new command line didn't
5122 // restate.
5123 let (pm_flags, pm_ename, pm_level) = paramtab()
5124 .read()
5125 .ok()
5126 .and_then(|t| {
5127 t.get(sname)
5128 .map(|p| (Some(p.node.flags as u32), p.ename.clone(), p.level))
5129 })
5130 .unwrap_or((None, None, 0));
5131 let (apm_flags, apm_ename) = paramtab()
5132 .read()
5133 .ok()
5134 .and_then(|t| {
5135 t.get(aname)
5136 .map(|p| (Some(p.node.flags as u32), p.ename.clone()))
5137 })
5138 .unwrap_or((None, None));
5139 let mut already_tied = false;
5140
5141 if let Some(sf) =
5142 pm_flags.filter(|f| (f & (PM_SPECIAL | PM_TIED)) == (PM_SPECIAL | PM_TIED))
5143 {
5144 let _ = sf;
5145 // c:2903-2918 — a SPECIAL tied scalar (PATH/path). Only a re-tie of
5146 // the identical special pair is allowed.
5147 let apm_special = apm_flags.map(|f| (f & PM_SPECIAL) != 0).unwrap_or(false);
5148 if pm_ename.as_deref() != Some(aname) || !apm_special {
5149 // c:2908-2911
5150 zwarnnam(
5151 name,
5152 &format!(
5153 "{} special parameter can only be tied to special parameter {}",
5154 sname,
5155 pm_ename.as_deref().unwrap_or("")
5156 ),
5157 );
5158 unqueue_signals();
5159 return 1;
5160 }
5161 if joinchar != ':' as i32 {
5162 // c:2913-2917
5163 zwarnnam(
5164 name,
5165 "cannot change the join character of special tied parameters",
5166 );
5167 unqueue_signals();
5168 return 1;
5169 }
5170 already_tied = true; // c:2918
5171 } else if let Some(af) =
5172 apm_flags.filter(|f| (f & (PM_SPECIAL | PM_TIED)) == (PM_SPECIAL | PM_TIED))
5173 {
5174 let _ = af;
5175 // c:2919-2929 — the ARRAY half is special+tied: catches tying it to
5176 // a different scalar, or to one that is no longer special.
5177 zwarnnam(
5178 name,
5179 &format!(
5180 "{} special parameter can only be tied to special parameter {}",
5181 aname,
5182 apm_ename.as_deref().unwrap_or("")
5183 ),
5184 );
5185 unqueue_signals();
5186 return 1;
5187 } else if let Some(sf) = pm_flags {
5188 // c:2930-2932 — only consider an existing scalar that is actually
5189 // live in this scope.
5190 if ((sf & PM_UNSET) == 0 || (sf & PM_DECLARED) != 0)
5191 && (locallevel_param.load(Relaxed) as i32 == pm_level
5192 || (on as u32 & PM_LOCAL) == 0)
5193 {
5194 if (sf & PM_TIED) != 0 {
5195 if PM_TYPE(sf) != PM_SCALAR {
5196 // c:2934-2937
5197 zwarnnam(name, &format!("already tied as non-scalar: {sname}"));
5198 unqueue_signals();
5199 return 1;
5200 } else if pm_ename.as_deref() == Some(aname) {
5201 already_tied = true; // c:2937
5202 } else {
5203 // c:2939-2942
5204 zwarnnam(name, &format!("can't tie already tied scalar: {sname}"));
5205 unqueue_signals();
5206 return 1;
5207 }
5208 }
5209 // else: not tied — only the export attribute is inherited
5210 // (c:2951), which `inherited_export` below already does.
5211 }
5212 }
5213
5214 if already_tied {
5215 // c:2308 — `tdp->joinchar != joinchar` compares against the
5216 // separator the pair was tied with; capture it before the
5217 // attribute update below overwrites it.
5218 let old_joinchar: Option<i32> = paramtab()
5219 .read()
5220 .ok()
5221 .and_then(|t| t.get(sname).and_then(|p| p.u_tied.as_deref().map(|td| td.joinchar)));
5222 // c:2957-2973 — C does NOT rebuild the pair here. It runs
5223 // typeset_single on each half "if only to update the attributes of
5224 // both, and of course to set the new value if one is provided",
5225 // then returns:
5226 // typeset_single(..., pm, on, off, ...)
5227 // typeset_single(..., apm, (on | PM_ARRAY) & ~PM_EXPORTED, off & ~PM_ARRAY, ...)
5228 // Applying on/off to the EXISTING entries is precisely what lets an
5229 // attribute the current command doesn't restate survive — so
5230 // `typeset -UT TS ts; typeset -T TS ts` keeps its uniqueness, and
5231 // likewise readonly. Rebuilding dropped everything but export.
5232 //
5233 // typeset_single itself is present in this file but is dead code
5234 // (nothing calls it; bin_typeset carries its own inline logic), so
5235 // the attribute update is applied directly here rather than routed
5236 // through it.
5237 if let Ok(mut tab) = paramtab().write() {
5238 if let Some(p) = tab.get_mut(sname) {
5239 p.node.flags = ((p.node.flags as u32 | on as u32) & !(off as u32)) as i32;
5240 // c:2967 — typeset_single takes `joinchar`, so a re-tie
5241 // RE-STATES the separator: "It is possible to apply -T to
5242 // two previously tied variables but with a different
5243 // separator character, in which case the variables remain
5244 // joined as before but the separator is changed."
5245 // (Doc/Zsh/builtins.yo, -T.) Updating flags alone left
5246 // `typeset -T TS ts; typeset -T TS ts ,` joining on ':'.
5247 if let Some(td) = p.u_tied.as_mut() {
5248 td.joinchar = joinchar; // c:2967
5249 }
5250 }
5251 if let Some(p) = tab.get_mut(aname) {
5252 let aon = (on as u32 | PM_ARRAY) & !PM_EXPORTED; // c:2970
5253 let aoff = off as u32 & !PM_ARRAY; // c:2971
5254 p.node.flags = ((p.node.flags as u32 | aon) & !aoff) as i32;
5255 }
5256 }
5257 // c:2254-2273 (typeset_single on the scalar half) — `-U` dedupes the
5258 // ARRAY peer in place:
5259 // if ((on & PM_UNIQUE) && !(pm->node.flags & PM_READONLY & ~off)) {
5260 // ... else if (PM_TYPE(pm->node.flags) == PM_SCALAR && pm->ename &&
5261 // (apm = paramtab->getnode(paramtab, pm->ename))) {
5262 // x = (*apm->gsu.a->getfn)(apm);
5263 // uniqarray(x);
5264 // Stamping the flag alone left `typeset -UT MANPATH manpath` at `/ /`.
5265 if (on as u32 & PM_UNIQUE) != 0 && (pm_flags.unwrap_or(0) & PM_READONLY & !(off as u32)) == 0 {
5266 if let Some(x) = crate::ported::exec::array(aname) {
5267 let x = crate::ported::params::uniqarray(x); // c:2270
5268 crate::ported::params::assignaparam(aname, x, 0);
5269 }
5270 }
5271 // c:2305-2317 — re-tying with a different join character and no
5272 // scalar value re-splits the scalar through the new separator:
5273 // if (tdp->joinchar != joinchar && !asg->value.scalar) {
5274 // tdp->joinchar = joinchar;
5275 // if (!(pm = assignsparam(pname, ztrdup(getsparam(pname)), 0)))
5276 // return NULL;
5277 // }
5278 // getsparam there is tiedarrgetfn, which joins the array with the
5279 // NEW character (c:Src/params.c:4303-4304), and tiedarrsetfn splits
5280 // on it again. Special pairs never get here with a new character
5281 // (c:2909-2913), and C skips them (`altpm && !(pm->node.flags &
5282 // PM_SPECIAL)`); they carry no tieddata, so `old_joinchar` is None.
5283 if old_joinchar.is_some_and(|o| o != joinchar) && sval_opt.is_none() {
5284 let joined = crate::ported::exec::array(aname).unwrap_or_default().join(&joinsep);
5285 crate::ported::params::assignsparam(sname, &joined, 0); // c:2314
5286 }
5287 // Values, if the command line carried any (c:2963 "and of course to
5288 // set the new value if one is provided for either of them").
5289 if let Some(sval) = sval_opt.as_deref() {
5290 crate::ported::params::assignsparam(sname, sval, 0);
5291 } else if let Some(arr) = aval_opt {
5292 // c:3019-3020 — `int flags = (asg->flags & ASG_KEY_VALUE) ?
5293 // ASSPM_KEY_VALUE : 0;`. The compiler marks the `[k]=v` form
5294 // with a Marker element (c:Src/subst.c:72).
5295 let flags = if arr.iter().any(|e| e.starts_with(crate::ported::zsh_h::Marker)) {
5296 crate::ported::zsh_h::ASSPM_KEY_VALUE
5297 } else {
5298 0
5299 };
5300 crate::ported::params::assignaparam(aname, arr, flags);
5301 }
5302 unqueue_signals();
5303 return 0;
5304 }
5305
5306 let existing_scalar: Option<String> = {
5307 let from_tab = paramtab()
5308 .read()
5309 .ok()
5310 .and_then(|t| t.get(sname).and_then(|p| p.u_str.clone()));
5311 from_tab.or_else(|| std::env::var(sname).ok())
5312 };
5313
5314 // Build the initial array value: prefer the array RHS, then
5315 // the scalar RHS (split on joinchar), then the existing
5316 // scalar's env value (split on joinchar). Mirrors C's sequence
5317 // at c:2960-3030 where typeset_single is called on both names;
5318 // if the scalar had a value it gets passed to tiedarrsetfn
5319 // which splits on the tieddata joinchar (c:4370-4381 sepsplit).
5320 let rhs_given = aval_opt.is_some() || sval_opt.is_some();
5321 let init_arr: Vec<String> = if let Some(arr) = aval_opt {
5322 arr
5323 } else if let Some(sval) = sval_opt.as_deref() {
5324 crate::ported::utils::sepsplit(sval, Some(&joinsep), true)
5325 } else if let Some(old) = existing_scalar
5326 .as_deref()
5327 // c:2930-2951 — `oldval = ztrdup(getsparam(asg0.name))` is taken
5328 // only for a scalar that is live (`!(PM_UNSET) || PM_DECLARED`)
5329 // in the scope the tie is declared in:
5330 // (locallevel == pm->level || !(on & PM_LOCAL))
5331 // and is neither tied already nor an array/hash. A declaration
5332 // that SHADOWS an outer binding starts empty
5333 // (`V=plain; f(){ local -T V v; print $V }` prints nothing), while
5334 // `f(){ local V=a:b; typeset -T V v }` keeps `a:b` because the
5335 // scalar already lives at this level.
5336 .filter(|_| {
5337 pm_flags.is_some_and(|sf| {
5338 ((sf & PM_UNSET) == 0 || (sf & PM_DECLARED) != 0)
5339 && (locallevel_param.load(Relaxed) as i32 == pm_level
5340 || (on as u32 & PM_LOCAL) == 0)
5341 && (sf & PM_TIED) == 0
5342 && (PM_TYPE(sf) & (PM_ARRAY | PM_HASHED)) == 0
5343 })
5344 })
5345 {
5346 if old.is_empty() {
5347 Vec::new()
5348 } else {
5349 crate::ported::utils::sepsplit(old, Some(&joinsep), true)
5350 }
5351 } else {
5352 Vec::new()
5353 };
5354 // c:2983-2989 — the array half is created with an EMPTY value
5355 // (`asg2.value.array = (LinkList)0`), and the initial value is only
5356 // stored afterwards through the normal assignment path, c:3018-3025:
5357 // if (asg->value.array)
5358 // assignaparam(asg->name, zlinklist2array(asg->value.array, 1), flags);
5359 // else if (asg0.value.scalar || oldval)
5360 // assignsparam(asg0.name, oldval, 0); /* via tiedarrsetfn */
5361 // That path is what applies the attributes the halves were just given,
5362 // most visibly PM_UNIQUE: `typeset -T -U P p=(a b a)` yields `a:b`.
5363 // Writing `init_arr` straight into both halves skipped it. The scalar
5364 // spellings are replayed as the array `init_arr` already split them
5365 // into, which is what tiedarrsetfn's sepsplit would produce.
5366 let assign_initial = rhs_given || !init_arr.is_empty();
5367
5368 // c:2984-2989 / c:2998-3005 — each half is made by typeset_single,
5369 // whose createparam arm takes a name only when
5370 // (isident(pname) || paramtab->getnode(paramtab, pname))
5371 // && (!idigit(*pname) || !strcmp(pname, "0"))
5372 // and otherwise zerrnam's "not an identifier" (leading digit) or
5373 // "not valid in this context" (c:2514-2547). The array half goes
5374 // first; when the scalar half then fails, C unsets the array it just
5375 // made (c:3001 `unsetparam_pm(apm, 1, 1)`).
5376 for (half, is_scalar) in [(aname, false), (sname, true)] {
5377 let exists = paramtab().read().map(|t| t.contains_key(half)).unwrap_or(false);
5378 let lead_digit = half.as_bytes().first().is_some_and(|b| b.is_ascii_digit());
5379 if !((isident(half) || exists) && (!lead_digit || half == "0")) {
5380 if lead_digit {
5381 zerrnam(name, &format!("not an identifier: {}", half)); // c:2544
5382 } else {
5383 zerrnam(name, &format!("not valid in this context: {}", half)); // c:2546
5384 }
5385 if is_scalar {
5386 unsetparam(aname); // c:3001
5387 }
5388 unqueue_signals();
5389 return 1; // c:2991 / c:3003
5390 }
5391 }
5392
5393 // Install the array side first (matching C c:2980 "Do it
5394 // first because we need the address"). Build a plain
5395 // PM_ARRAY|PM_TIED param.
5396 // c:Src/builtin.c:2986-2999 — the array side is created with
5397 // `(on | PM_ARRAY) & ~PM_EXPORTED` and the scalar side with the full
5398 // `on`. So `export -T FOO foo` / `readonly -T` carry those attribute
5399 // flags onto the pair (the array is NOT export-flagged; the scalar
5400 // is). Previously both were hardcoded to `…|PM_TIED`, so `export -T`
5401 // left FOO unexported — `${(t)FOO}` read `scalar-tied` and
5402 // `typeset -p FOO` printed `typeset -T` instead of `export -T`.
5403 //
5404 // c:Src/builtin.c:2953 — `on |= (pm->node.flags & ~roff) & PM_EXPORTED;`
5405 //
5406 // "Variable already exists in the current scope but is not tied.
5407 // We're preserving its value and export attribute but no other
5408 // attributes upon converting to 'tied'."
5409 //
5410 // So tying an ALREADY-EXPORTED scalar keeps it exported, even without
5411 // `-x`: `export E=a:b; typeset -T E e` leaves E in the environment
5412 // (${(t)E} == scalar-tied-export). zshrs built the tie attributes from
5413 // the command-line flags alone and silently dropped the export, which
5414 // takes the variable OUT of the environment of every later child.
5415 let inherited_export: u32 = paramtab()
5416 .read()
5417 .ok()
5418 .and_then(|t| t.get(sname).map(|p| p.node.flags as u32))
5419 .unwrap_or(0)
5420 & PM_EXPORTED
5421 & !(off as u32); // c:2953 `& ~roff` — an explicit +x still wins
5422 // c:2989 / c:3003 — C hands BOTH halves the full `on`:
5423 // apm = typeset_single(..., (on | PM_ARRAY) & ~PM_EXPORTED, ...)
5424 // pm = typeset_single(..., on, ...)
5425 // so every attribute the user asked for lands on the tie, not just the
5426 // export/readonly pair. Masking down to PM_EXPORTED|PM_READONLY silently
5427 // dropped the rest — most visibly PM_UNIQUE, so `typeset -UT B b` tied
5428 // the two halves but never deduped either (`${(t)b}` read `array-tied`
5429 // where zsh reads `array-tied-unique`), while the equivalent spelled as
5430 // two commands (`typeset -T B b; typeset -U b`) worked. Carrying `on`
5431 // whole is safe here: c:2742-2744 has already stripped the type bits
5432 // from it (`if (on & PM_TIED) off |= PM_INTEGER|…|PM_HASHED; on &= ~off`).
5433 let tie_attr: u32 = on as u32 | inherited_export;
5434 let mut apm = param::default();
5435 apm.node.nam = aname.to_string();
5436 apm.node.flags = ((PM_ARRAY | PM_TIED) | (tie_attr & !PM_EXPORTED)) as i32;
5437 apm.u_arr = Some(if assign_initial { Vec::new() } else { init_arr.clone() }); // c:2984
5438 apm.ename = Some(sname.to_string());
5439 apm.level = locallevel.load(Relaxed) as i32;
5440 // c:2982-2989 — `tdp = (Tieddata) zalloc(sizeof *tdp);
5441 // tdp->joinchar = joinchar;` — the tieddata rides on the
5442 // SCALAR side's pm->u.data (c:2566 `tdp->joinchar = joinchar`).
5443 let tdp = crate::ported::zsh_h::tieddata {
5444 arrptr: None,
5445 joinchar,
5446 };
5447
5448 // Install the scalar side with PM_TIED + gsu_s wired to the
5449 // tied-via-ename getters/setters so reads/writes propagate
5450 // through paramtab to apm.u_arr.
5451 let mut spm = param::default();
5452 spm.node.nam = sname.to_string();
5453 spm.node.flags = ((PM_SCALAR | PM_TIED) | tie_attr) as i32; // c:2999 scalar carries full `on`
5454 spm.ename = Some(aname.to_string());
5455 spm.u_str = Some(if assign_initial { String::new() } else { init_arr.join(&joinsep) });
5456 spm.u_tied = Some(Box::new(tdp));
5457 spm.level = locallevel.load(Relaxed) as i32;
5458 // c:Src/builtin.c:1956 — `static const struct gsu_scalar
5459 // tiedarr_gsu = { tiedarrgetfn, tiedarrsetfn, tiedarrunsetfn };`
5460 // The scalar side of a tied pair routes its reads/writes
5461 // through these so the partner array stays in sync. C's
5462 // tiedarrgetfn returns the joined string; tiedarrsetfn splits
5463 // and writes through `*dptr->arrptr`. The Rust port adapts
5464 // those to paramtab lookups via `pm.ename` (see params.rs
5465 // commentary). The C tiedarrgetfn returns `char*` (joined
5466 // scalar); the Rust signature returns `Vec<String>` because
5467 // the existing typing — adapt with a closure that joins.
5468 fn tied_scalar_getfn_shim(pm: ¶m) -> String {
5469 // c:4352 — `zjoin(*dptr->arrptr, (unsigned char)
5470 // dptr->joinchar, 1)`; joinchar lives on the tieddata
5471 // riding pm->u.data (Rust: typed u_tied view).
5472 // c:Src/params.c:4303-4304 + c:4320-4332 — the join bytes are the
5473 // metafied join character (raw NUL in zshrs, see bin_typeset above).
5474 let sep = match pm.u_tied.as_deref() {
5475 Some(td) => match td.joinchar as u8 {
5476 0 => "\0".to_string(),
5477 b if crate::ported::utils::imeta_byte(b) => [char::from(crate::ported::zsh_h::Meta), char::from(b ^ 32)].iter().collect(),
5478 b => char::from(b).to_string(),
5479 },
5480 None => ":".to_string(),
5481 };
5482 crate::ported::params::tiedarrgetfn(pm).join(&sep)
5483 }
5484 fn tied_scalar_setfn_shim(pm: &mut param, val: String) {
5485 crate::ported::params::tiedarrsetfn(pm, Some(val))
5486 }
5487 fn tied_scalar_unsetfn_shim(pm: &mut param, exp: i32) {
5488 crate::ported::params::tiedarrunsetfn(pm, exp)
5489 }
5490 spm.gsu_s = Some(Box::new(crate::ported::zsh_h::gsu_scalar {
5491 getfn: tied_scalar_getfn_shim,
5492 setfn: tied_scalar_setfn_shim,
5493 unsetfn: tied_scalar_unsetfn_shim,
5494 }));
5495
5496 if let Ok(mut tab) = paramtab().write() {
5497 // c:Src/params.c:1137 — when a declaration SHADOWS an existing
5498 // binding, `createparam` keeps the displaced one as `pm->old` so
5499 // `endparamscope` can put it back. This path builds both halves
5500 // from `param::default()` (`old: None`) and inserted them straight
5501 // over the top, so `local -T V v` over an existing `typeset -T V v`
5502 // DESTROYED the outer pair: after the function returned `$V` was
5503 // empty, `${(t)V}` reported nothing at all, and every later
5504 // `V=…` stopped updating `v` — the global tie was gone for the rest
5505 // of the shell's life. docs/BUGS.md #1039 C.
5506 //
5507 // Only chain when this declaration is actually creating a shadow at
5508 // a DEEPER level: re-tying at the same level legitimately replaces
5509 // the binding (and `already_tied` above handles the same-partner
5510 // case), so an unconditional chain would build a bogus restore
5511 // stack for plain global re-ties.
5512 let cur_ll = locallevel.load(Relaxed) as i32;
5513 let mut apm = apm;
5514 let mut spm = spm;
5515 if tab.get(aname).map(|p| p.level < cur_ll).unwrap_or(false) {
5516 apm.old = tab.remove(aname); // c:1137 `pm->old = oldpm`
5517 }
5518 if tab.get(sname).map(|p| p.level < cur_ll).unwrap_or(false) {
5519 spm.old = tab.remove(sname); // c:1137
5520 }
5521 tab.insert(aname.to_string(), Box::new(apm));
5522 tab.insert(sname.to_string(), Box::new(spm));
5523 }
5524 // c:3018-3025 — store the initial value now that both halves exist,
5525 // with c:3019's ASSPM_KEY_VALUE when the array value carries a
5526 // `[k]=v` Marker triad (c:Src/subst.c:72).
5527 if assign_initial {
5528 let flags = if init_arr.iter().any(|e| e.starts_with(crate::ported::zsh_h::Marker)) {
5529 crate::ported::zsh_h::ASSPM_KEY_VALUE
5530 } else {
5531 0
5532 };
5533 let _ = crate::ported::params::assignaparam(aname, init_arr, flags); // c:3020
5534 }
5535 unqueue_signals();
5536 return 0;
5537 }
5538
5539 // c:Src/builtin.c:3042-3098 — `-m PATTERN` (or `+m PATTERN`) treats
5540 // each arg as a glob, not a `name[=value]` pair.
5541 //
5542 // C splits into TWO subpaths:
5543 // +m: c:3068 `scanmatchtable(..., paramtab->printnode, printflags)`
5544 // — direct paramtab walk that prints PRINT_TYPE | PRINT_NAMEONLY
5545 // (built up at c:3051-3054 when on|roff == 0).
5546 // -m: c:3081-3094 build pmlist, then run `typeset_single` per
5547 // match. typeset_single's print path (c:2240-2247) uses
5548 // - PRINT_TYPESET with `-p`
5549 // - PRINT_INCLUDEVALUE otherwise, gated on
5550 // `!OPT_ISSET(ops,'g') && (unset(TYPESETSILENT) ||
5551 // OPT_ISSET(ops,'m'))`
5552 //
5553 // Earlier zshrs collapsed both into one `printparamnode(...,
5554 // PRINT_INCLUDEVALUE)` call which:
5555 // - Made `+m` emit `name=value` instead of `name` / `integer NAME`.
5556 // - Ignored `-g` print suppression so `typeset -gm 'A*'` spammed
5557 // every match (zsh is silent: `-g` blocks the print). Hit
5558 // during zinit's plugin loader which runs `typeset -gm`
5559 // against the plugin's exported-var set on every load.
5560 // Bug #48 in docs/BUGS.md.
5561 if OPT_ISSET(&ops, b'm') && !argv.is_empty() && !TYPESET_M_APPLY.with(|c| c.get()) {
5562 // c:3043-3055 — printflags for the +m direct-scan path.
5563 if !OPT_ISSET(&ops, b'p') {
5564 // c:3044-3050 — mass-changing types is fatal for namerefs.
5565 if (on & PM_NAMEREF) != 0 && OPT_MINUS(&ops, b'm') {
5566 zerrnam(name, "-m not allowed with -n"); // c:3048
5567 unqueue_signals();
5568 return 1; // c:3049
5569 }
5570 if (on | roff) == 0 {
5571 printflags |= PRINT_TYPE; // c:3052
5572 }
5573 if on == 0 {
5574 printflags |= PRINT_NAMEONLY; // c:3054
5575 }
5576 }
5577 // c:2241 — typeset_single's `-m` print path always sets
5578 // PRINT_WITH_NAMESPACE so dot-prefixed names (`.cd`) emit
5579 // instead of being filtered at printparamnode's namespace gate.
5580 let single_flags: i32 = if OPT_ISSET(&ops, b'p') {
5581 PRINT_TYPESET | PRINT_WITH_NAMESPACE
5582 } else {
5583 PRINT_INCLUDEVALUE | PRINT_WITH_NAMESPACE
5584 };
5585 // c:2244 — `else if (!OPT_ISSET(ops,'g') && (unset(TYPESETSILENT)
5586 // || OPT_ISSET(ops,'m')))` — typeset_single suppresses
5587 // PRINT_INCLUDEVALUE when `-g` is set. `-p` bypasses (c:2242).
5588 let do_minus_print = OPT_ISSET(&ops, b'p')
5589 || (!OPT_ISSET(&ops, b'g') && (!isset(TYPESETSILENT) || OPT_ISSET(&ops, b'm')));
5590 for pattern in argv.iter() {
5591 // c:3053 + c:3057 — `while ((asg = getasg(&argv, assigns)))` splits
5592 // each arg into `asg->name` / `asg->value` BEFORE the pattern is
5593 // tokenized (`tokenize(asg->name)`), so `typeset -m 'x*'=val`
5594 // globs `x*` and hands `val` to typeset_single for every match.
5595 // The whole arg used to be glob-compiled, so the `=val` half was
5596 // part of the pattern and matched nothing.
5597 let (pattern, m_value): (&str, Option<&str>) = match pattern.find('=') {
5598 Some(i) => (&pattern[..i], Some(&pattern[i + 1..])),
5599 None => (pattern.as_str(), None),
5600 };
5601 // c:3064 — `OPT_PLUS(ops,'m') && !ASG_VALUEP(asg)` is the gate for
5602 // the direct scanmatchtable print; everything else runs the
5603 // typeset_single loop.
5604 let is_plus_m = OPT_PLUS(&ops, b'm') && m_value.is_none();
5605 // c:3061 — `patcompile(asg->name, 0, NULL)` glob-compile.
5606 // Use the canonical pattern.rs port. On compile failure,
5607 // emit "bad pattern" and continue to the next arg.
5608 let pat = crate::ported::pattern::patcompile(
5609 &{
5610 let mut __pat_tok = crate::pattern_data_escape::escape_data_backslashes(&pattern); // a backslash tokenize leaves raw is DATA (c:Src/glob.c:3651)
5611 crate::ported::glob::tokenize(&mut __pat_tok);
5612 __pat_tok
5613 },
5614 crate::ported::zsh_h::PAT_HEAPDUP as i32,
5615 None,
5616 );
5617 let pat = match pat {
5618 Some(p) => p,
5619 None => {
5620 zwarnnam(name, &format!("bad pattern: {}", pattern));
5621 returnval = 1;
5622 continue;
5623 }
5624 };
5625 // c:3068 — `scanmatchtable(paramtab, pprog, 1, on|roff,
5626 // 0, paramtab->printnode, printflags);` — walk paramtab
5627 // entries whose name matches the pattern AND whose flag
5628 // bits intersect on|roff.
5629 //
5630 // c:3073-3080 — C collects the matches into `pmlist`
5631 // FIRST and only then walks it, because the per-entry work
5632 // can reorganise paramtab underneath the scan. zshrs needs
5633 // the same two-phase shape for a second, harder reason:
5634 // the table lock must be RELEASED before any entry is
5635 // touched. `printparamnode` re-enters `paramtab().read()`
5636 // on several paths —
5637 // params.rs:12622 PM_TIED peer lookup (the `+m` /
5638 // PRINT_TYPE arm),
5639 // params.rs:12303 + 12312, 12009, 12038 PM_SPECIAL /
5640 // empty-scalar / zero-integer value
5641 // probes via `getsparam` (→
5642 // params.rs:5353 and
5643 // `lookup_special_var` at 13494 /
5644 // 13547 / 13554 / 13640 / 13669),
5645 // params.rs:9365 `tiedarrgetfn`, reached through the
5646 // tied-scalar `gsu_s` getfn shim
5647 // (builtin.rs:4671).
5648 // `paramtab()` is a plain `std::sync::RwLock`, which is
5649 // NOT reentrant: acquiring the read half while this thread
5650 // already holds the write half parks the thread forever.
5651 // The previous code held `paramtab().write()` across
5652 // `printparamnode`, so `typeset -m PAT` hung the shell the
5653 // moment PAT matched any param that takes one of those
5654 // paths (`HISTCHARS`, `HOME`, `IFS`, `TERM`, `WORDCHARS`,
5655 // `USERNAME`, `SECONDS`, `TIMEFMT`, any tied scalar, and
5656 // every empty PM_SPECIAL scalar / zero-valued special
5657 // integer). It surfaced as a pattern-COUNT threshold
5658 // (`typeset -m` over ~19+ `${(k)parameters}` names hung
5659 // while 18 returned) only because the list is walked in
5660 // name order and the first such name sits around there;
5661 // the real trigger is the name, not the count.
5662 // Pre-cloning is the rule the `typeset -p NAME` path
5663 // already documents at builtin.rs:5156-5159.
5664 let matched: Vec<crate::ported::zsh_h::Param> = {
5665 let tab = paramtab().read().unwrap();
5666 let on_roff = (on as u32) | (roff as u32);
5667 let mut matched: Vec<crate::ported::zsh_h::Param> = tab
5668 .iter()
5669 .filter(|(k, pm)| {
5670 let f = pm.node.flags as u32;
5671 // c:3078-3079 — `if (pm->node.flags & PM_UNSET)
5672 // continue;` lives ONLY in the `-m` collection loop.
5673 // The `+m` path (c:3064-3067) is a bare
5674 // `scanmatchtable(..., paramtab->printnode, ...)` with
5675 // no such test, so a DECLARED-but-unset param still
5676 // lists there: under TYPESET_TO_UNSET zsh prints
5677 // `local hide scalar_test` for `private scalar_test;
5678 // typeset +m scalar_test` (V10private "basic scope
5679 // hiding"), where zshrs printed nothing.
5680 if !is_plus_m && (f & PM_UNSET) != 0 {
5681 return false;
5682 }
5683 // c:Src/builtin.c:3055-3094 / hashtable.c:373-440
5684 // — the `-m`/`+m` PATTERN path scans paramtab
5685 // via `scanmatchtable(paramtab, pprog, ...)`.
5686 // scanmatchtable matches ONLY on flags1/flags2
5687 // + the pattern; it does NOT exclude PM_HIDE.
5688 // An explicit pattern therefore REVEALS hidden
5689 // params — verified vs `/opt/homebrew/bin/zsh`:
5690 // `typeset +m 'a*'` → `undefined aliases` +
5691 // `array argv`; `typeset -m 'a*'` → `aliases`
5692 // + `argv=( )`. The magic-assoc placeholders
5693 // (PM_HIDE|PM_HIDEVAL, inserted by
5694 // init_partab_params) and `private` vars
5695 // (PM_HIDE, no PM_HIDEVAL — c:Modules/
5696 // param_private.c:174) must BOTH appear here,
5697 // unlike the no-args list path (c:2762-2792)
5698 // where scanhashtable's bare scan keeps them
5699 // hidden (bug #371). So NO PM_HIDE filter on
5700 // this PATTERN path — V10private: `private
5701 // x=5; typeset +m x` → `local x`.
5702 // c:3064-3066 — the `flags1 = on|roff` argument is
5703 // scanmatchtable's, i.e. the `+m` path ONLY. C's `-m`
5704 // collection loop (c:3075-3082) tests nothing but
5705 // `pattry`: those flags are what typeset_single is
5706 // about to APPLY, not a filter on what to match.
5707 // Filtering here made every MUTATING `-m` match
5708 // nothing — `typeset -h +g -m [[:alpha:]_]*`
5709 // (B02typeset's stress test) localized no parameter at
5710 // all, so the `unset -m [[:alpha:]_]*` that follows hit
5711 // the still-global readonly LINENO and aborted the
5712 // chunk before its harness marker was printed.
5713 if is_plus_m && on_roff != 0 && (f & on_roff) == 0 {
5714 return false;
5715 }
5716 crate::ported::pattern::pattry(&pat, k)
5717 })
5718 // c:3087 `addlinknode(pmlist, pm)` — the match list.
5719 .map(|(_, pm)| pm.clone())
5720 .collect();
5721 // c:3065-3066 — only `+m` goes through scanmatchtable with
5722 // `sorted = 1`; the `-m` loop (c:3083-3091) walks the
5723 // buckets, so its matches stay in hash-table order.
5724 if is_plus_m {
5725 matched.sort_by(|a, b| hnamcmp(&a.node.nam, &b.node.nam));
5726 }
5727 matched
5728 };
5729 // The table guard is dropped here, before any print.
5730 if is_plus_m {
5731 // c:3068-3070 — `+m`: direct print using the
5732 // PRINT_TYPE | PRINT_NAMEONLY flags built above.
5733 for mut pm in matched {
5734 printparamnode(&mut pm, printflags);
5735 }
5736 continue;
5737 }
5738 // c:3084-3090 — `-m`: run typeset_single on every match:
5739 // for (pmnode = firstnode(pmlist); pmnode; incnode(pmnode)) {
5740 // pm = (Param) getdata(pmnode);
5741 // if (!typeset_single(name, pm->node.nam, pm, func, on, off,
5742 // roff, asg, NULL, ops, 0))
5743 // returnval = 1;
5744 // }
5745 // zshrs's typeset_single equivalent is bin_typeset's OWN per-arg
5746 // loop (the typeset_single port in this file is unreachable — see
5747 // the note in the `already_tied` block), so the match list is fed
5748 // back through bin_typeset as LITERAL names. TYPESET_M_APPLY
5749 // suppresses this `-m` block on that re-entry so the very same
5750 // `ops` is re-derived into the very same `on`/`off`/`roff` and the
5751 // same c:2244 print gate. Only the pure-listing shape (`!on &&
5752 // !roff && !ASG_VALUEP(asg)`, c:2237) stays on the print path
5753 // below, which is where typeset_single returns early anyway.
5754 if (on | roff) != 0 || m_value.is_some() {
5755 // c:3090 — the typeset_single loop runs once per match; with
5756 // none it does nothing. Re-entering bin_typeset with an empty
5757 // name list would instead reach its no-argument LIST path and
5758 // print every parameter carrying the flags.
5759 if matched.is_empty() {
5760 continue;
5761 }
5762 let names: Vec<String> = matched
5763 .iter()
5764 .map(|pm| match m_value {
5765 Some(v) => format!("{}={}", pm.node.nam, v), // c:3086 asg
5766 None => pm.node.nam.clone(), // c:3085 pm->node.nam
5767 })
5768 .collect();
5769 let prev = TYPESET_M_APPLY.with(|c| c.replace(true));
5770 let rc = bin_typeset(name, &names, &ops, func); // c:3086 typeset_single
5771 TYPESET_M_APPLY.with(|c| c.set(prev));
5772 if rc != 0 {
5773 returnval = 1; // c:3089
5774 }
5775 continue;
5776 }
5777 if do_minus_print {
5778 // c:3090-3095 — walk the pre-built match list. Clones,
5779 // per the lock note above; printparamnode's writes into
5780 // `hn` (the u_str seed at params.rs:12312, the PM_TIED
5781 // value swap at 12639) are display-local scratch that C
5782 // performs on a reassigned local pointer, never on the
5783 // stored node, so printing the clone is also the more
5784 // faithful shape.
5785 for mut pm in matched {
5786 printparamnode(&mut pm, single_flags);
5787 }
5788 }
5789 }
5790 unqueue_signals();
5791 return returnval;
5792 }
5793
5794 // c:3035-3039 — `+T` is not a way to untie:
5795 // if (off & PM_TIED) {
5796 // unqueue_signals();
5797 // zerrnam(name, "use unset to remove tied variables");
5798 // return 1;
5799 // }
5800 // C puts this at the TOP LEVEL, so it precedes everything typeset_single
5801 // does — notably the special-parameter type-change rule, whose `chflags`
5802 // watches PM_TIED and would otherwise claim `typeset +T PATH path` and
5803 // report "can't change type of a special parameter" instead. The check was
5804 // missing entirely, so `+T` was silently accepted and did nothing.
5805 if (off as u32 & PM_TIED) != 0 {
5806 unqueue_signals(); // c:3036
5807 zerrnam(name, "use unset to remove tied variables"); // c:3037
5808 return 1; // c:3038
5809 }
5810
5811 let mut tied_name_count: usize = 0;
5812 for arg in argv {
5813 // c:Src/builtin.c typeset_single — when PM_LOCAL is in
5814 // flags, createparam first to install pm.old chain at
5815 // locallevel (createparam c:1132-1147). Applies uniformly
5816 // to all forms: `local x`, `local x=v`, `local arr=(...)`,
5817 // `local -A h`. endparamscope unwinds via Param.old.
5818 if tied_mode {
5819 tied_name_count += 1;
5820 if tied_name_count > 2 {
5821 // Separator / extra args — don't run name
5822 // validation or createparam against them. The scalar/array
5823 // pair itself was installed by the `tied_mode` block above.
5824 continue;
5825 }
5826 }
5827 let arg_name: &str = match arg.find('=') {
5828 Some(i) => &arg[..i],
5829 None => arg.as_str(),
5830 };
5831
5832 // c:3100-3113 — `-p` is decided FIRST for every assignment, value or
5833 // not, before typeset_single (and so before its name validation and
5834 // type-change logic) runs:
5835 // if (OPT_ISSET(ops,'p')) {
5836 // if (hn) paramtab->printnode(hn, printflags);
5837 // else { zwarnnam(name, "no such variable: %s", asg->name);
5838 // returnval = 1; }
5839 // continue;
5840 // }
5841 // `typeset -p s=1` prints (or reports) s and assigns nothing.
5842 if OPT_ISSET(&ops, b'p') {
5843 // !!! BASH-MODE GATE (no C counterpart) !!! `declare -p` of a bash
5844 // synthesized special array (PIPESTATUS / FUNCNAME / BASH_VERSINFO)
5845 // — these live outside paramtab, so the normal lookup below can't
5846 // find them. Emit the bash reusable form (BASH_VERSINFO is a
5847 // readonly array).
5848 if crate::dash_mode::bash_mode() {
5849 if let Some(vals) = crate::dash_mode::bash_special_array(arg_name) {
5850 let esc = |s: &str| -> String {
5851 let mut o = String::with_capacity(s.len());
5852 for c in s.chars() {
5853 if matches!(c, '"' | '\\' | '$' | '`') {
5854 o.push('\\');
5855 }
5856 o.push(c);
5857 }
5858 o
5859 };
5860 let flags = if arg_name == "BASH_VERSINFO" {
5861 "-ar"
5862 } else {
5863 "-a"
5864 };
5865 let body: String = vals
5866 .iter()
5867 .enumerate()
5868 .map(|(i, v)| format!("[{}]=\"{}\"", i, esc(v)))
5869 .collect::<Vec<_>>()
5870 .join(" ");
5871 println!("declare {} {}=({})", flags, arg_name, body);
5872 continue;
5873 }
5874 }
5875 // c:Src/Modules/param_private.c:678 — with zsh/param/private
5876 // loaded, `realparamtab->getnode` IS `getprivatenode`, so
5877 // c:Src/builtin.c:3109's `paramtab->getnode(paramtab, pname)`
5878 // returns NULL for a private declared in an OUTER scope. Without
5879 // the walk, `typeset -p array_test` inside a function called from
5880 // the declaring one found the private node, then printparamnode
5881 // dropped it silently at its PM_RO_BY_DESIGN level test
5882 // (c:Src/params.c:6157-6162) — no output AND no error, where zsh
5883 // prints `typeset: no such variable: array_test` and exits 1
5884 // (V10private.ztst:15). SAFETY: read-only `pm->old` walk under
5885 // the read guard held for the lookup.
5886 let existed = paramtab()
5887 .read()
5888 .map(|t| {
5889 t.get(arg_name).is_some_and(|pm| {
5890 !crate::ported::modules::param_private::getprivatenode(
5891 &**pm as *const crate::ported::zsh_h::param,
5892 )
5893 .is_null() // c:3109 getnode hook
5894 })
5895 })
5896 .unwrap_or(false);
5897 if existed {
5898 // c:Src/params.c:6275 — printparamnode looks up the
5899 // PM_TIED peer via paramtab; we must NOT hold the lock
5900 // when calling. Bug #410 — pre-clone pattern (mirrors
5901 // the bin_typeset `-m PAT` arm refactor).
5902 let mut pm_clone = match paramtab().read() {
5903 Ok(tab) => tab.get(arg_name).cloned(),
5904 Err(_) => None,
5905 };
5906 if let Some(ref mut pm) = pm_clone {
5907 // c:3104 — `paramtab->printnode(hn, printflags);` with
5908 // the flags bin_typeset accumulated: PRINT_WITH_NAMESPACE
5909 // (c:2664, kept because there are arguments), PRINT_TYPESET
5910 // or a POSIX variant (c:2750-2758), PRINT_LINE for -p1
5911 // (c:2765). With PRINT_WITH_NAMESPACE a `.ns.name` prints.
5912 printparamnode(pm, printflags);
5913 }
5914 } else if crate::extensions::dash_mode::korn_mode() {
5915 // ksh93 and mksh both list nothing for an unset name and exit 0
5916 // (`typeset -p nosuch; print $?` -> 0, nothing on stderr).
5917 } else {
5918 // c:Src/builtin.c:3110-3113 — when `typeset -p NAME`
5919 // and the param doesn't exist, emit
5920 // `typeset: no such variable: NAME` and set
5921 // returnval=1 so the builtin's exit status is 1.
5922 zwarnnam(name, &format!("no such variable: {}", arg_name));
5923 returnval = 1; // c:Src/builtin.c:3112
5924 }
5925 continue;
5926 }
5927
5928 // c:2117-2193 (inside typeset_single) — changing the TYPE of an
5929 // existing parameter is refused for specials, with SECONDS the one
5930 // documented exception.
5931 //
5932 // int chflags = ((off & pm->node.flags) | (on & ~pm->node.flags)) &
5933 // (PM_INTEGER|PM_EFLOAT|PM_FFLOAT|PM_HASHED|PM_ARRAY|PM_TIED|PM_AUTOLOAD);
5934 // /* keep the parameter if just switching between floating types */
5935 // if ((tc = chflags && chflags != (PM_EFLOAT|PM_FFLOAT))) { ... }
5936 // ...
5937 // if (... || tc) {
5938 // if (pm->node.flags & PM_SPECIAL) {
5939 // int err = 1;
5940 // if (!readonly && !strcmp(pname, "SECONDS")) {
5941 // ... else if (!setsecondstype(pm, on, off)) { ... err = 0; }
5942 // }
5943 // if (err) { zerrnam(cname, "%s: can't change type of a "
5944 // "special parameter", pname); return NULL; }
5945 //
5946 // None of this was ported, so a type change against a special was
5947 // silently ACCEPTED and then dropped: `typeset -F RANDOM`,
5948 // `typeset -F LINENO` and `typeset -F HISTSIZE` all returned 0 having
5949 // done nothing, where zsh fails. And the SECONDS exception — the
5950 // documented way to get sub-second timing — never took effect:
5951 // `typeset -F SECONDS` left it `integer-special` instead of
5952 // `float-special`.
5953 //
5954 // `chflags` is why this fires only on a REAL type change:
5955 // `typeset -i SECONDS` / `typeset -i LINENO` leave chflags == 0 (the
5956 // bit is already set) and are correctly no-ops, and -E↔-F is exempted
5957 // outright by the `!= (PM_EFLOAT|PM_FFLOAT)` clause. It also covers
5958 // `typeset -a PATH`, which is a PM_ARRAY type change on a special and
5959 // so earns the same message rather than an array-specific one.
5960 //
5961 // The `readonly` half of C's guard is not modelled here: it gates
5962 // turning readonly OFF, which reaches this branch by a different route.
5963 // setsecondstype (params.rs) is the real port and had NO callers.
5964 if !tied_mode {
5965 let pm_info = paramtab()
5966 .read()
5967 .ok()
5968 .and_then(|t| t.get(arg_name).map(|p| (p.node.flags as u32, p.level)));
5969 if let Some((pmf, pm_level)) = pm_info {
5970 let chflags = ((off as u32 & pmf) | (on as u32 & !pmf))
5971 & (PM_INTEGER
5972 | PM_EFLOAT
5973 | PM_FFLOAT
5974 | PM_HASHED
5975 | PM_ARRAY
5976 | PM_NAMEREF
5977 | PM_TIED
5978 | PM_AUTOLOAD); // c:2115-2117 (PM_NAMEREF since 53776)
5979 let tc = chflags != 0 && chflags != (PM_EFLOAT | PM_FFLOAT); // c:2122
5980 // c:2078-2091 — a `local` at a DEEPER scope than the existing
5981 // param does not CHANGE that param's type; it SHADOWS it with a
5982 // fresh local (createparam installs the pm.old chain, restored by
5983 // endparamscope). Only a SAME-scope re-typeset of a special is the
5984 // forbidden type change. The port applied the special type-change
5985 // error against the higher-scope param unconditionally, so
5986 // `local -a commands` (HASHED command-hash special → local ARRAY)
5987 // inside a function wrongly errored "can't change type of a
5988 // special parameter" and aborted — e.g. `_openssl_subcommands`'s
5989 // `local -a commands=(…)`, leaving `openssl <TAB>` empty. zsh
5990 // allows the shadow in a function (errors only at same scope /
5991 // top level). Mirror the reuse-decision guard used at c:2078-2091.
5992 let creating_local_shadow =
5993 (on as u32 & PM_LOCAL) != 0 && pm_level != locallevel_param.load(Relaxed);
5994 // c:2084-2086 — the shadow of a special stays special
5995 // (`newspecial = NS_NORMAL`) unless -h is given on either side,
5996 // and c:2114 then still compares types: `local -i HOME` is
5997 // refused, `local -h -i HOME` makes a plain local.
5998 let newspecial = creating_local_shadow
5999 && (pmf & PM_SPECIAL) != 0
6000 && (on as u32 & PM_HIDE) == 0
6001 && (pmf & PM_HIDE & !(off as u32)) == 0;
6002 if tc && (pmf & PM_SPECIAL) != 0 && (!creating_local_shadow || newspecial) {
6003 let mut err = true; // c:2144
6004 if arg_name == "SECONDS" {
6005 // c:2171 — `else if (!setsecondstype(pm, on, off))`.
6006 let ok = paramtab()
6007 .write()
6008 .ok()
6009 .and_then(|mut t| {
6010 t.get_mut(arg_name).map(|p| {
6011 crate::ported::params::setsecondstype(p, on as i32, off as i32)
6012 })
6013 })
6014 .unwrap_or(1);
6015 if ok == 0 {
6016 err = false; // c:2176
6017 // c:2172-2175 — `if (asg->value.scalar && !(pm =
6018 // assignsparam(pname, ..., 0))) return NULL;`
6019 if let Some(i) = arg.find('=') {
6020 crate::ported::params::assignsparam(arg_name, &arg[i + 1..], 0);
6021 }
6022 }
6023 }
6024 if err {
6025 // c:2181-2187
6026 if !OPT_ISSET(&ops, b'p') {
6027 zerrnam(
6028 name,
6029 &format!("{arg_name}: can't change type of a special parameter"),
6030 );
6031 }
6032 unqueue_signals();
6033 return 1;
6034 }
6035 // SECONDS: setsecondstype already installed the new type;
6036 // c:2169's `tc = 0` skips the normal conversion below.
6037 continue;
6038 }
6039 if tc && (pmf & PM_AUTOLOAD) != 0 && !creating_local_shadow {
6040 // c:2185-2190 — a parameter whose module is not loaded yet
6041 // may turn out special, so its type cannot change either.
6042 if !OPT_ISSET(&ops, b'p') {
6043 zerrnam(
6044 name,
6045 &format!("{arg_name}: can't change type of autoloaded parameter"),
6046 );
6047 }
6048 returnval = 1;
6049 continue; // c:2189 return NULL
6050 }
6051 }
6052 }
6053
6054 // c:2519-2552 (Src/builtin.c, inside typeset_single) — name
6055 // validation gate. Direct port:
6056 // else if ((isident(pname) || paramtab->getnode(paramtab, pname))
6057 // && (!idigit(*pname) || !strcmp(pname, "0"))) {
6058 // /* proceed */
6059 // } else {
6060 // if (idigit(*pname))
6061 // zerrnam(cname, "not an identifier: %s", pname);
6062 // else
6063 // zerrnam(cname, "not valid in this context: %s", pname);
6064 // return NULL;
6065 // }
6066 //
6067 // The C function returns NULL on failure; the outer bin_typeset
6068 // name loop continues to the next arg (errflag silences
6069 // subsequent zerr calls so we won't double-emit). Mirror that
6070 // here with `continue`.
6071 let pname_in_tab = paramtab()
6072 .read()
6073 .map(|t| t.get(arg_name).is_some())
6074 .unwrap_or(false);
6075 // c:2062-2064 — `usepm = pm && (!(pm->node.flags & PM_UNSET)
6076 // || ...)`: snapshot BEFORE any createparam this iteration
6077 // runs (the PM_LOCAL shadow block and the pre-assign type
6078 // stamps both create fresh pms). Only the reuse-existing-pm
6079 // branch of typeset_single carries the c:2336 `if (errflag)
6080 // return NULL` post-assign check; the fresh-param path
6081 // (c:2577+ createparam → c:2604 assignsparam) has NO errflag
6082 // check — `typeset -i x=3#8` zerrs the math but bin_typeset
6083 // still returns 0 (zsh 5.9 exit 0, lastval untouched).
6084 // 55020 adds `|| (pm->node.flags & PM_DECLARED)` (c:2058-2059): a
6085 // declared-but-unset parameter (TYPESET_TO_UNSET) is reused too.
6086 let usepm_existing = paramtab()
6087 .read()
6088 .map(|t| {
6089 t.get(arg_name).is_some_and(|pm| {
6090 (pm.node.flags as u32 & PM_UNSET) == 0
6091 || (pm.node.flags as u32 & PM_DECLARED) != 0
6092 })
6093 })
6094 .unwrap_or(false);
6095 // c:Src/builtin.c:2078 — snapshot the EXISTING param's local
6096 // level BEFORE this arg's createparam/assign runs. The bare-name
6097 // print decision (c:2244, inlined at ~5962) needs the pre-state
6098 // level to tell "re-declare same-level local" (usepm kept →
6099 // print) from "localize a higher-level/global var" (usepm=0 →
6100 // no print). Sentinel -1 when the param doesn't pre-exist.
6101 // c:Src/builtin.c:2070-2071 — `if (!usepm && pm && (pm->node.flags &
6102 // PM_SPECIAL)) usepm = 2;`: an UNSET special (a `private` declared
6103 // under TYPESET_TO_UNSET carries PM_SPECIAL from makeprivate,
6104 // Src/Modules/param_private.c:174) is still re-used for the type
6105 // comparison, so `private -a a; local a=s` is "inconsistent type".
6106 let special_unset_existing = paramtab()
6107 .read()
6108 .map(|t| {
6109 t.get(arg_name).is_some_and(|pm| {
6110 let f = pm.node.flags as u32;
6111 (f & PM_UNSET) != 0 && (f & PM_SPECIAL) != 0
6112 })
6113 })
6114 .unwrap_or(false);
6115 let pm_level_existing: i32 = paramtab()
6116 .read()
6117 .ok()
6118 .and_then(|t| t.get(arg_name).map(|pm| pm.level))
6119 .unwrap_or(-1);
6120 // c:2212-2222 (55030) — `-h` on a special or autoloadable parameter
6121 // that is reused at this level (not localized: c:2075-2088 drops
6122 // usepm then) is an error, where it used to be a silent no-op.
6123 {
6124 let cur_ll = locallevel.load(Relaxed) as i32;
6125 let pm_flags = paramtab()
6126 .read()
6127 .ok()
6128 .and_then(|t| t.get(arg_name).map(|pm| pm.node.flags as u32));
6129 let usepm_c = (usepm_existing || pm_flags.is_some_and(|f| (f & PM_SPECIAL) != 0)) // c:2058-2068
6130 && !(pm_level_existing != cur_ll && (on as u32 & PM_LOCAL) != 0); // c:2075-2088
6131 if usepm_c
6132 && (on as u32 & PM_HIDE) != 0
6133 && pm_flags.is_some_and(|f| (f & (PM_SPECIAL | PM_AUTOLOAD)) != 0)
6134 && pm_level_existing <= cur_ll
6135 {
6136 zerrnam(name, &format!("{}: can't change parameter attribute", arg_name)); // c:2219
6137 returnval = 1;
6138 continue; // c:2220 return NULL
6139 }
6140 }
6141 let first_is_digit = arg_name
6142 .as_bytes()
6143 .first()
6144 .is_some_and(|b| b.is_ascii_digit());
6145 let pname_valid =
6146 (isident(arg_name) || pname_in_tab) && (!first_is_digit || arg_name == "0");
6147 if !pname_valid {
6148 if first_is_digit {
6149 zerrnam(
6150 name, // c:2548
6151 &format!("not an identifier: {}", arg_name),
6152 );
6153 } else {
6154 zerrnam(
6155 name, // c:2550
6156 &format!("not valid in this context: {}", arg_name),
6157 );
6158 }
6159 // c:3153-3156 — `if (!typeset_single(...)) returnval = 1;`
6160 // — the NULL return from the validation gate makes
6161 // bin_typeset's per-arg loop record failure.
6162 returnval = 1;
6163 continue; // c:2551 return NULL
6164 }
6165
6166 // c:1961-1995 typeset_setbase — the `-i N` / `-E N` / `-F N`
6167 // argument is validated by every typeset_single arm that reaches it
6168 // (c:2294-2296 reuse, c:2444-2446 type change, c:2534-2538 new
6169 // param), before the value is assigned:
6170 // int base = (int)zstrtol(arg, &eptr, 10);
6171 // if (*eptr) { zwarnnam(name, "bad base value: %s" /
6172 // "bad precision value: %s", arg); return 1; }
6173 // if ((on & PM_INTEGER) && (base < 2 || base > 36)) {
6174 // zwarnnam(name, "invalid base (must be 2 to 36 inclusive): %d",
6175 // base); return 1; }
6176 // The check depends only on the options, so it runs once here, ahead
6177 // of the arms' mutations. On failure the reuse arm (usepm: the param
6178 // keeps its type) returns NULL with the old value intact; the type-
6179 // change arm has already deleted the old param (c:2378) and the new-
6180 // param arm calls `unsetparam_pm(pm, 0, 1)` (c:2536) — so outside
6181 // the reuse arm the name ends up unset.
6182 {
6183 let setbase_arg: Option<&str> = if (on as u32 & PM_INTEGER) != 0
6184 && OPT_HASARG(&ops, b'i')
6185 {
6186 OPT_ARG(&ops, b'i') // c:1966-1967
6187 } else if (on as u32 & PM_EFLOAT) != 0 && OPT_HASARG(&ops, b'E') {
6188 OPT_ARG(&ops, b'E') // c:1968-1969
6189 } else if (on as u32 & PM_FFLOAT) != 0 && OPT_HASARG(&ops, b'F') {
6190 OPT_ARG(&ops, b'F') // c:1970-1971
6191 } else {
6192 None
6193 };
6194 let setbase_err = setbase_arg.and_then(|a| {
6195 let (base, rest) = crate::ported::utils::zstrtol(a, 10); // c:1976
6196 if !rest.is_empty() {
6197 // c:1977-1983
6198 Some(if (on as u32 & PM_INTEGER) != 0 {
6199 format!("bad base value: {}", a)
6200 } else {
6201 format!("bad precision value: {}", a)
6202 })
6203 } else if (on as u32 & PM_INTEGER) != 0 && !(2..=36).contains(&(base as i32)) {
6204 // c:1984-1988
6205 Some(format!("invalid base (must be 2 to 36 inclusive): {}", base as i32))
6206 } else {
6207 None
6208 }
6209 });
6210 if let Some(msg) = setbase_err {
6211 zwarnnam(name, &msg);
6212 // c:2114-2118 — chflags/tc decide reuse vs recreate.
6213 let keeps_type = paramtab()
6214 .read()
6215 .ok()
6216 .and_then(|t| {
6217 t.get(arg_name).map(|pm| {
6218 let f = pm.node.flags as u32;
6219 let chflags = ((off as u32 & f) | (on as u32 & !f))
6220 & (PM_INTEGER
6221 | PM_EFLOAT
6222 | PM_FFLOAT
6223 | PM_HASHED
6224 | PM_ARRAY
6225 | PM_TIED
6226 | PM_AUTOLOAD);
6227 !(chflags != 0 && chflags != (PM_EFLOAT | PM_FFLOAT))
6228 })
6229 })
6230 .unwrap_or(false);
6231 if usepm_existing && keeps_type {
6232 // c:2285-2288 — the reuse arm has already merged the
6233 // attributes when typeset_setbase fails, so `float x=3;
6234 // typeset -F 1x x` leaves x a -F float.
6235 if let Ok(mut tab) = paramtab().write() {
6236 if let Some(pm) = tab.get_mut(arg_name) {
6237 pm.node.flags = (pm.node.flags | (on as i32 & !(PM_READONLY as i32)))
6238 & !(off as i32 | PM_UNSET as i32);
6239 }
6240 }
6241 } else if pname_in_tab {
6242 unsetparam(arg_name); // c:2378 / c:2536
6243 }
6244 returnval = 1; // c:3153-3156 typeset_single NULL
6245 continue;
6246 }
6247 }
6248
6249 // c:3117-3150 — `typeset -n NAME[=refname]` arm.
6250 if (on as u32 & PM_NAMEREF) != 0 {
6251 // c:Src/builtin.c:3117-3150 — the `-n` literal-name arm,
6252 // INLINE in C's bin_typeset (no separate C function; the
6253 // former vm_helper::typeset_nameref_arg helper relocated
6254 // here 2026-06-12 per the no-fake-fns-in-ported rule).
6255 // Combined with typeset_single's nameref pieces:
6256 // subscripted-name reject (c:2452-2456), read-only
6257 // reference guard (c:2249-2256), fresh-start unset
6258 // (c:3127-3141), scalar→nameref conversion carrying the
6259 // value as refname (c:3132-3135), creation + refname
6260 // assignment (assignsparam → PM_NAMEREF arm → setscope).
6261 let nameref_arm = |on: u32, off: u32| -> i32 {
6262 let value: Option<&str> = arg.find('=').map(|i| &arg[i + 1..]);
6263
6264 // c:2452-2456 — `typeset -n ptr[1]=...` is invalid.
6265 if arg_name.contains('[') {
6266 zerrnam(
6267 name,
6268 &format!("{}: reference variable cannot be an array", arg_name),
6269 );
6270 unqueue_signals();
6271 return 1;
6272 }
6273
6274 let cur_ll = locallevel_param.load(Relaxed) as i32;
6275 let existing = paramtab().read().ok().and_then(|t| {
6276 t.get(arg_name)
6277 .map(|pm| (pm.node.flags as u32, pm.level, pm.u_str.clone()))
6278 });
6279
6280 let mut carried_value: Option<String> = value.map(String::from);
6281 let mut reuse_existing = false;
6282 // c:2359-2363 — converting a readonly NON-reference to a
6283 // reference is a type change (`tc`, PM_NAMEREF is in chflags):
6284 // the readonly bit is carried over (`on |= ~off & PM_READONLY &
6285 // pm->node.flags`) and the old parameter recreated.
6286 let mut carry_readonly = false;
6287
6288 let existing_level: Option<i32> = existing.as_ref().map(|(_, l, _)| *l);
6289 if let Some((eflags, elevel, estr)) = existing {
6290 if (eflags & PM_READONLY) != 0 && (eflags & PM_NAMEREF) == 0 {
6291 carry_readonly = (off & PM_READONLY) == 0; // c:2361
6292 }
6293 // c:2249-2256 — read-only guard (typeset_single). Fires when
6294 // the existing pm is readonly, +r wasn't given, and either the
6295 // nameref-ness changes or a nameref gets a new value.
6296 // Only a reference reaches it: for a non-reference the -n is a
6297 // type change, which clears usepm (c:2119-2126) first.
6298 if (eflags & PM_READONLY) != 0
6299 && (eflags & PM_NAMEREF) != 0
6300 && (off & PM_READONLY) == 0
6301 && !OPT_ISSET(&ops, b'p')
6302 {
6303 let kind = if (eflags & PM_NAMEREF) != 0 {
6304 "reference"
6305 } else {
6306 "variable"
6307 };
6308 zerrnam(name, &format!("{}: read-only {}", arg_name, kind)); // c:2254
6309 unqueue_signals();
6310 return 1; // c:2256
6311 }
6312 // c:3127-3141 — namerefs always start over fresh.
6313 if elevel >= cur_ll || ((on & PM_LOCAL) == 0 && elevel < cur_ll) {
6314 // c:3132-3135 — converting a scalar: its value becomes
6315 // the refname.
6316 if carried_value.is_none()
6317 && PM_TYPE(eflags) == PM_SCALAR
6318 && estr.as_deref().map(|s| !s.is_empty()).unwrap_or(false)
6319 {
6320 carried_value = estr.clone();
6321 }
6322 // c:3136-3140 — `if (!(hn->flags & PM_READONLY)) {
6323 // unsetparam_pm(oldpm, 0, 1); hn = NULL; }` — only a
6324 // non-readonly ref starts over fresh; a readonly one is
6325 // KEPT and handed to typeset_single (c:3148-3149), where
6326 // `typeset +r -n ref[=val]` clears the flag in place and
6327 // any =val ASSIGNS THROUGH the surviving refname.
6328 if (eflags & (PM_READONLY | PM_NAMEREF)) == (PM_READONLY | PM_NAMEREF) {
6329 reuse_existing = true;
6330 } else if let Some(mut old) =
6331 paramtab().write().ok().and_then(|mut t| t.remove(arg_name))
6332 {
6333 // keep the shadowed outer binding for re-chaining
6334 if let Some(prev) = old.old.take() {
6335 if let Ok(mut tab) = paramtab().write() {
6336 tab.insert(arg_name.to_string(), prev);
6337 }
6338 }
6339 }
6340 } else if (eflags & (PM_READONLY | PM_NAMEREF)) == (PM_READONLY | PM_NAMEREF) {
6341 // c:3142-3149 — only a READONLY ref survives as the pm
6342 // handed to typeset_single (so `typeset -rn ref=var` can
6343 // error properly); everything else gets `hn = NULL` and
6344 // a fresh local SHADOW is created below (c:3148-3149).
6345 reuse_existing = true;
6346 }
6347 }
6348
6349 // (Re)create the nameref param (typeset_single c:2577+ createparam
6350 // with PM_NAMEREF type).
6351 if !reuse_existing {
6352 let shadowed = paramtab().write().ok().and_then(|mut t| {
6353 if (on & PM_LOCAL) != 0 && cur_ll > 0 {
6354 t.remove(arg_name)
6355 } else {
6356 None
6357 }
6358 });
6359 let mut flags = PM_NAMEREF as i32;
6360 // c:2544 — TYPESET_TO_UNSET: declared-but-unassigned.
6361 if carried_value.is_none() && isset(crate::ported::zsh_h::TYPESETTOUNSET) {
6362 flags |= crate::ported::zsh_h::PM_DEFAULTED as i32;
6363 }
6364 // crate::ported::zsh_h::PM_UPPER on a nameref marks the -u upscope variant (c:2698
6365 // allows -u with -n); crate::ported::zsh_h::PM_HIDEVAL likewise.
6366 // 55030 lets -h (PM_HIDE) through as well.
6367 flags |= (on
6368 & (crate::ported::zsh_h::PM_UPPER
6369 | crate::ported::zsh_h::PM_HIDEVAL
6370 | crate::ported::zsh_h::PM_HIDE))
6371 as i32;
6372 // c:1108-1132 — createparam REUSES the just-unset node at its
6373 // own level when !PM_LOCAL (`typeset -gn` rebind of a local
6374 // ref keeps the ref's level; it does NOT hoist to level 0).
6375 let level = if (on & PM_LOCAL) != 0 {
6376 cur_ll
6377 } else if let Some(elevel) = existing_level {
6378 elevel
6379 } else {
6380 0
6381 };
6382 let pm = Box::new(crate::ported::zsh_h::param {
6383 node: crate::ported::zsh_h::hashnode {
6384 next: None,
6385 nam: arg_name.to_string(),
6386 flags,
6387 },
6388 u_data: 0,
6389 u_tied: None,
6390 u_arr: None,
6391 u_str: if carried_value.is_none() && value.is_some() {
6392 Some(String::new()) // `typeset -n ptr=` placeholder
6393 } else {
6394 None
6395 },
6396 u_val: 0,
6397 u_dval: 0.0,
6398 u_hash: None,
6399 gsu_s: None,
6400 gsu_i: None,
6401 gsu_f: None,
6402 gsu_a: None,
6403 gsu_h: None,
6404 base: 0,
6405 width: 0,
6406 env: None,
6407 ename: None,
6408 old: shadowed,
6409 level,
6410 });
6411 if let Ok(mut tab) = paramtab().write() {
6412 tab.insert(arg_name.to_string(), pm);
6413 }
6414 } else {
6415 // reuse path (read-only refs surviving the fresh-start gate,
6416 // c:3148-3149): apply on/off bits in place. A readonly
6417 // SCALAR + `-n` converts to a nameref here — its current
6418 // value becomes the refname (c:2117+ type-conversion inside
6419 // typeset_single with the pm kept; the `typeset +r -n
6420 // ref=RW` shape: "assignment occurs after type change").
6421 if let Ok(mut tab) = paramtab().write() {
6422 if let Some(pm) = tab.get_mut(arg_name) {
6423 pm.node.flags |= (on
6424 & (crate::ported::zsh_h::PM_UPPER
6425 | crate::ported::zsh_h::PM_HIDEVAL
6426 | crate::ported::zsh_h::PM_HIDE))
6427 as i32;
6428 pm.node.flags |= PM_NAMEREF as i32;
6429 pm.node.flags &= !((off
6430 & (crate::ported::zsh_h::PM_UPPER
6431 | crate::ported::zsh_h::PM_HIDEVAL
6432 | crate::ported::zsh_h::PM_HIDE
6433 | PM_READONLY))
6434 as i32);
6435 }
6436 }
6437 }
6438
6439 // Assign the refname (typeset_single c:2326 assignsparam →
6440 // PM_NAMEREF assignstrvalue arm + valid_refname + setscope).
6441 let mut rc = 0;
6442 if reuse_existing {
6443 // c:2235 + c:2330 — the surviving (previously-readonly) ref
6444 // is assigned with `flags = (on & PM_NAMEREF) ?
6445 // ASSPM_NONAMEREF : 0`, so `typeset +r -n ref=RW` sets the
6446 // REFERENCE's refname (fetchvalue under SCANPM_NONAMEREF,
6447 // c:Src/params.c:3163), not the referent.
6448 if let Some(v) = value {
6449 let _no_deref = crate::ported::params::NamerefSuppressGuard::new();
6450 if crate::ported::params::setsparam(arg_name, v).is_none() {
6451 rc = 1;
6452 }
6453 }
6454 } else if let Some(v) = carried_value.as_deref() {
6455 if v.is_empty() {
6456 // `typeset -n ptr=` — empty placeholder, no setscope error.
6457 if let Ok(mut tab) = paramtab().write() {
6458 if let Some(pm) = tab.get_mut(arg_name) {
6459 pm.u_str = Some(String::new());
6460 pm.node.flags &= !(crate::ported::zsh_h::PM_DEFAULTED as i32);
6461 }
6462 }
6463 } else if crate::ported::params::setsparam(arg_name, v).is_none() {
6464 // c:2326 — typeset_single's assignsparam; the fresh
6465 // PM_NAMEREF pm routes to assignstrvalue's nameref arm
6466 // (c:2690-2720: valid_refname + SETREFNAME + setscope).
6467 rc = 1;
6468 }
6469 } else if !reuse_existing && crate::ported::params::is_nameref(arg_name) {
6470 // bare placeholder — still run setscope for parity (no-op).
6471 let _ = crate::ported::params::setscope_by_name(arg_name, None);
6472 }
6473
6474 // c:2618 — `pm->node.flags |= (on & PM_READONLY);` AFTER the
6475 // assignment so `typeset -rn ref=var` can set its initial value.
6476 if (on & PM_READONLY) != 0 || carry_readonly {
6477 if let Ok(mut tab) = paramtab().write() {
6478 if let Some(pm) = tab.get_mut(arg_name) {
6479 pm.node.flags |= PM_READONLY as i32;
6480 }
6481 }
6482 }
6483 rc
6484 };
6485 if nameref_arm(on as u32, off as u32) != 0 {
6486 returnval = 1; // c:3153-3156
6487 }
6488 continue;
6489 }
6490
6491 // c:2032-2050 — existing pm is a nameref and ±n was not
6492 // requested: resolve the chain and operate on the target.
6493 let mut nameref_rewrite: Option<String> = None;
6494 if (off as u32 & PM_NAMEREF) == 0
6495 && pname_in_tab
6496 && crate::ported::params::is_nameref(arg_name)
6497 {
6498 let cur_ll = locallevel.load(Relaxed) as i32;
6499 let (pm_level, pm_refname) = paramtab()
6500 .read()
6501 .ok()
6502 .and_then(|t| t.get(arg_name).map(|p| (p.level, p.u_str.clone())))
6503 .unwrap_or((0, None));
6504 if pm_level == cur_ll || (on as u32 & PM_LOCAL) == 0 {
6505 // c:2033
6506 let type_change = (on as u32 & !(PM_NAMEREF | PM_LOCAL | PM_READONLY)) != 0; // c:2038
6507 use crate::ported::params::nameref_resolution;
6508 match crate::ported::params::resolve_nameref_name(arg_name, None) {
6509 nameref_resolution::SelfRef | nameref_resolution::OutOfScope => {
6510 returnval = 1;
6511 continue;
6512 }
6513 nameref_resolution::Placeholder(last, _) => {
6514 // c:2033-2034 — `if ((pm = resolve_nameref(pm)))
6515 // pname = pm->node.nam;`
6516 // The chain endpoint becomes the name the rest of
6517 // typeset_single operates on, EVEN when that
6518 // endpoint is itself a reference.
6519 //
6520 // c:2036-2037 — the reject is additionally gated on
6521 // (!(pm->node.flags & PM_UNSET) ||
6522 // (pm->node.flags & PM_DECLARED))
6523 // so a chain ending on a reference that `unset -n`
6524 // left `PM_UNSET` with PM_DECLARED cleared (c:3798
6525 // in `unsetparam_pm`) does NOT error: it falls
6526 // through to createparam, whose c:1120-1123 arm
6527 // retargets `oldpm` to that endpoint and c:1176
6528 // overwrites its flags — `typeset -n ref1=ref0;
6529 // typeset -n ref0; unset -n ref0; typeset -i
6530 // ref1=42` yields an ordinary local integer.
6531 let endpoint_visible = paramtab()
6532 .read()
6533 .ok()
6534 .and_then(|t| {
6535 t.get(&last).map(|p| {
6536 let f = p.node.flags as u32;
6537 (f & PM_UNSET) == 0 || (f & PM_DECLARED) != 0
6538 })
6539 })
6540 .unwrap_or(true); // c:2037
6541 if !endpoint_visible {
6542 // c:2034 — `pname = pm->node.nam;`
6543 let tail = arg.find('=').map(|i| &arg[i..]).unwrap_or("");
6544 nameref_rewrite = Some(format!("{}{}", last, tail));
6545 } else if type_change {
6546 zwarnnam(
6547 name, // c:2046
6548 &format!("{}: can't change type of a named reference", last),
6549 );
6550 returnval = 1;
6551 continue; // c:2048 return NULL
6552 }
6553 // plain `typeset ref=value` on a placeholder
6554 // falls through — the scalar-assign arm routes
6555 // through assignsparam which writes the refname.
6556 }
6557 nameref_resolution::Target {
6558 name: t, subscript, ..
6559 } => {
6560 if subscript.is_some() && type_change {
6561 // c:2041-2044 — pm->width set: subscripted ref.
6562 zwarnnam(
6563 name,
6564 &format!(
6565 "{}: can't change type via subscript reference",
6566 pm_refname.as_deref().unwrap_or(arg_name)
6567 ),
6568 );
6569 returnval = 1;
6570 continue;
6571 }
6572 // c:2034-2035 — pname = pm->node.nam (resolved).
6573 let tail = arg.find('=').map(|i| &arg[i..]).unwrap_or("");
6574 let mut new_arg = t.clone();
6575 if let Some(sub) = &subscript {
6576 new_arg.push('[');
6577 new_arg.push_str(sub);
6578 new_arg.push(']');
6579 }
6580 new_arg.push_str(tail);
6581 nameref_rewrite = Some(new_arg);
6582 }
6583 nameref_resolution::NotRef => {}
6584 }
6585 }
6586 }
6587 let arg: &String = nameref_rewrite.as_ref().unwrap_or(arg);
6588 let arg_name: &str = match arg.find('=') {
6589 Some(i) => &arg[..i],
6590 None => arg.as_str(),
6591 };
6592 // Recompute the existing-pm snapshots for the rewritten name.
6593 let (pname_in_tab, usepm_existing) = if nameref_rewrite.is_some() {
6594 let tab = paramtab().read().unwrap();
6595 match tab.get(arg_name.split('[').next().unwrap_or(arg_name)) {
6596 Some(pm) => (
6597 true,
6598 (pm.node.flags as u32 & PM_UNSET) == 0
6599 || (pm.node.flags as u32 & PM_DECLARED) != 0, // c:2058-2059
6600 ),
6601 None => (false, false),
6602 }
6603 } else {
6604 (pname_in_tab, usepm_existing)
6605 };
6606
6607 // c:2930 — `else if (pm)` reuse decision for the bin_typeset
6608 // literal-name loop: `if ((!(pm->node.flags & PM_UNSET) ||
6609 // pm->node.flags & PM_DECLARED)
6610 // && (locallevel == pm->level || !(on & PM_LOCAL)))`.
6611 // Decides whether the existing pm is reusable in place or
6612 // shadowed by a new local. The Rust per-arg loop short-circuits
6613 // through `createparam`'s reuse arm (params.rs:1975) which
6614 // already encodes this rule, but the literal C predicate
6615 // belongs here so the parity is visible at the call site.
6616 let cur_locallevel = locallevel.load(Relaxed) as i32;
6617 let pm_reuse_local: bool = if pname_in_tab {
6618 let tab = paramtab().read().unwrap();
6619 let pm = tab.get(arg_name).unwrap();
6620 let f = pm.node.flags as u32;
6621 ((f & PM_UNSET) == 0 || (f & PM_DECLARED) != 0)
6622 && (cur_locallevel == pm.level || (on as u32 & PM_LOCAL) == 0) // c:2930
6623 } else {
6624 true
6625 };
6626 let _ = pm_reuse_local;
6627
6628 // c:3127-3132 — PM_NAMEREF literal-name branch. When
6629 // `(on & PM_NAMEREF)` and an existing `hn` is present:
6630 // `if (((Param)hn)->level >= locallevel ||
6631 // (!(on & PM_LOCAL) && ((Param)hn)->level < locallevel)) {
6632 // unsetparam_pm(oldpm, 0, 1); hn = NULL; }`.
6633 // Namerefs always start over fresh when redeclared.
6634 if (on as u32 & PM_NAMEREF) != 0 && pname_in_tab {
6635 let level_compare = paramtab()
6636 .read()
6637 .ok()
6638 .and_then(|t| t.get(arg_name).map(|pm| pm.level))
6639 .unwrap_or(0);
6640 if level_compare >= cur_locallevel
6641 || ((on as u32 & PM_LOCAL) == 0 && level_compare < cur_locallevel)
6642 // c:3130
6643 {
6644 // unsetparam_pm + hn = NULL would happen here. The
6645 // simplified PM_NAMEREF path leaves the reset to
6646 // typeset_single's name-resolution branch at
6647 // typeset_single c:2750.
6648 }
6649 }
6650
6651 // c:2469-2510 — `typeset_single` createparam dispatch for new
6652 // PM_LOCAL declarations. Inside a function scope (`local x` or
6653 // `typeset x` from a fn body), C calls createparam(name,
6654 // on|PM_LOCAL) which chains pm.old = oldpm at the current
6655 // locallevel — the c:2575 `pm->level = locallevel` stamp that
6656 // endparamscope unwinds. Without this, `local x=inside`
6657 // modifies the outer-scope x instead of installing a shadow.
6658 // c:Src/builtin.c:2469 — `if ((on & PM_LOCAL) && (!pm ||
6659 // pm->level < locallevel)) { ...createparam... }`. The
6660 // condition is: PM_LOCAL set AND (no existing pm OR existing
6661 // pm is at a LOWER scope than the current locallevel). At
6662 // TOP scope (locallevel=0), `pm->level < 0` is never true,
6663 // so C doesn't call createparam — it reuses the existing
6664 // param in place. The previous Rust port called createparam
6665 // unconditionally, which routed through the reuse arm at
6666 // params.rs:2062-2068 and then clobbered pm.node.flags via
6667 // params.rs:2132 (`pm->node.flags = flags & ~PM_LOCAL`).
6668 // That wiped PM_INTEGER, leaving u_val=42 with PM_SCALAR
6669 // flags → getsparam returned empty. Bug #326 in
6670 // docs/BUGS.md. Mirror the C guard so the createparam call
6671 // only fires when we genuinely need to allocate a new shadow.
6672 let needs_new_shadow = if pname_in_tab {
6673 paramtab()
6674 .read()
6675 .ok()
6676 .and_then(|t| t.get(arg_name).map(|pm| pm.level < cur_locallevel))
6677 .unwrap_or(true)
6678 } else {
6679 true
6680 };
6681 // c:Src/builtin.c:3085 — on the `-m` re-entry the loop is fed
6682 // MATCHED PARAMETER NAMES, never option words, so the `-`/`+`
6683 // prefix guard (which only exists to keep a stray flag word out
6684 // of createparam on the literal path) must not fire there: `-`
6685 // IS a real special parameter, and `typeset -h +g -m '*'` has
6686 // to localize it like any other or the `unset -m '*'` that
6687 // follows hits the still-global readonly `-` and aborts the
6688 // shell (E03posix.ztst:1, `fn:2: read-only variable: -`).
6689 let m_apply_names = TYPESET_M_APPLY.with(|c| c.get());
6690 // c:2448-2499 — a subscripted name (`g[2]=x`, `g[2,4]=(…)`) is settled
6691 // in typeset_single's `strchr(pname, '[')` arm by assignsparam /
6692 // assignaparam on the base parameter and never reaches createparam.
6693 // PM_LOCAL is set for a plain top-level `typeset` too (c:2799-2810), so
6694 // without this test the shadow block created a stray parameter
6695 // literally named `g[2]`.
6696 if (on as u32 & PM_LOCAL) != 0 // c:2469
6697 && !arg_name.is_empty()
6698 && !arg_name.contains('[')
6699 && (m_apply_names
6700 || (!arg_name.starts_with('-') && !arg_name.starts_with('+')))
6701 && needs_new_shadow
6702 {
6703 let kind = if is_hashed {
6704 PM_HASHED
6705 } else if is_array {
6706 PM_ARRAY
6707 } else {
6708 0
6709 };
6710 // c:2083-2085 — `newspecial = NS_NORMAL`:
6711 // if ((pm->node.flags & PM_SPECIAL)
6712 // && !(on & PM_HIDE) && !(pm->node.flags & PM_HIDE & ~off))
6713 // newspecial = NS_NORMAL;
6714 // The apply branch (c:2386-2425, "For specials, we keep the same
6715 // struct but zero everything") then re-uses the special's OWN
6716 // Param — accessors and all — so the local still reads through
6717 // the special's getfn. Note the `& ~off` term: a HIDDEN special
6718 // (`${(t)commands}` is `association-hide-hideval-special`) is
6719 // normally NOT preserved by `local -A commands`, but `+h` puts
6720 // PM_HIDE into `off`, which cancels the test and preserves it.
6721 // That is exactly what `_command_names` relies on:
6722 // local -a +h path
6723 // local -A +h commands
6724 // path=( $_saved_path )
6725 // Reference zsh reports `association-local-special` / 5531 keys
6726 // there; dropping the `& ~off` term made zshrs build a plain
6727 // empty local assoc (`association-local` / 0 keys), so
6728 // `compadd -k commands` had nothing to offer and every command
6729 // completion routed through that idiom produced no matches.
6730 //
6731 // The lookup is C's `paramtab->getnode2()` — "getnode2() to avoid
6732 // autoloading" (c:2460-2462). An untouched module-parameter stub
6733 // is PM_AUTOLOAD with none of the real node's type/special flags
6734 // (`loadparamnode`, Src/params.c:563-585, only swaps in the real
6735 // node once something FETCHES the name), so `local -A +h commands`
6736 // on a still-unloaded `zsh/parameter` stub creates a plain local
6737 // assoc — `${(t)commands}` reads `association-local` and
6738 // `$#commands` is 0. Touch `$commands` first and the very same
6739 // statement yields `association-local-special` / the live table.
6740 // zshrs installs the partab placeholders eagerly and models
6741 // PM_AUTOLOAD as the MATERIALIZED_MODULE_PARAMS side-set, so the
6742 // stub test has to be asked for explicitly here; without it,
6743 // command-position completion (`pr<TAB>` → `_autocd` →
6744 // `_command_names`, which never reads `$commands` beforehand)
6745 // preserved a special that reference zsh does not.
6746 let keep_special = !crate::vm_helper::module_param_is_autoload_stub(arg_name)
6747 && paramtab()
6748 .read()
6749 .ok()
6750 .and_then(|t| t.get(arg_name).map(|pm| pm.node.flags as u32))
6751 .is_some_and(|f| {
6752 (f & PM_SPECIAL) != 0 && (on & PM_HIDE) == 0 && (f & PM_HIDE & !off) == 0
6753 });
6754 // c:2475-2487 — C calls `assignsparam(pname, value, 0)`
6755 // which creates the pm via the assignsparam → createparam
6756 // path WITHOUT propagating PM_READONLY/PM_EXPORTED flags
6757 // (that path uses PM_SCALAR / PM_ARRAY / PM_HASHED only).
6758 // Post-assign attribute stamps add PM_READONLY/PM_EXPORTED
6759 // later. Mirror by passing ONLY the type-kind + PM_LOCAL
6760 // (not the full `on` mask) so the freshly-created pm
6761 // doesn't error on its own first assignment. PM_SPECIAL rides
6762 // along only for the newspecial case above — c:2425 stamps it
6763 // on the preserved struct (`… | on | PM_SPECIAL) & ~off`) and
6764 // createparam keys its accessor inheritance off it.
6765 // c:2410 — `copyparam(tpm, pm, 1)` saves the special's value
6766 // before the local takes over, and for a hash that is a COPY of
6767 // the table (c:Src/params.c:1288-1289 `tpm->u.hash =
6768 // copyparamtable(pm->gsu.h->getfn(pm), pm->node.nam)`).
6769 // scanendscope hands that copy back to the special's own setfn on
6770 // return (c:Src/params.c:5962 `pm->gsu.h->setfn(pm,
6771 // tpm->u.hash)`), which is how `local -A +h functions; unset
6772 // "functions[f]"` leaves `f` defined after the function returns.
6773 // Must be taken HERE: once createparam installs the shadow, the
6774 // scan below no longer reaches the special. PM_READONLY rows are
6775 // not copied because c:5945 skips their restore anyway.
6776 let mut special_hash_snapshot: Option<Vec<String>> = if keep_special {
6777 crate::ported::modules::parameter::PARTAB
6778 .iter()
6779 .find(|e| e.name == arg_name && (e.flags as u32 & PM_READONLY) == 0)
6780 .and_then(|e| {
6781 let keys = crate::vm_helper::partab_scan_keys(arg_name)?; // c:1289 getfn
6782 let mut flat = Vec::with_capacity(keys.len() * 2);
6783 for k in keys {
6784 // c:621 scancopyparams → c:609 copyparam per element
6785 let v = (e.getfn)(std::ptr::null_mut(), &k)
6786 .and_then(|p| p.u_str)
6787 .unwrap_or_default();
6788 flat.push(k);
6789 flat.push(v);
6790 }
6791 Some(flat)
6792 })
6793 } else {
6794 None
6795 };
6796 // c:2608-2609 — set below, inside the newspecial block, when the
6797 // declaration carries no value and the saved node is neither
6798 // PM_NORESTORE nor PM_READONLY; acted on once the paramtab lock
6799 // is released, because every special setfn re-enters paramtab.
6800 let mut empty_special_type: Option<u32> = None;
6801 let hash_row_writable = special_hash_snapshot.is_some();
6802 let _ = createparam(
6803 arg_name,
6804 kind as i32 | PM_LOCAL as i32 | if keep_special { PM_SPECIAL as i32 } else { 0 },
6805 );
6806 // c:2575 — `else if (on & PM_LOCAL) pm->level = locallevel;`
6807 // — stamp the just-created pm at the current scope so
6808 // endparamscope (params.c) unwinds the shadow when the
6809 // enclosing function returns. createparam at params.rs:2014
6810 // already sets `level: cur_locallevel` on the fresh pm;
6811 // re-stamp here against the post-createparam pm to mirror
6812 // C's explicit assignment, AND to catch the reuse-arm path
6813 // (params.rs:1975-1986) where the existing pm's level was
6814 // pre-set by a prior scope.
6815 if let Ok(mut tab) = paramtab().write() {
6816 if let Some(pm) = tab.get_mut(arg_name) {
6817 pm.level = cur_locallevel; // c:2575
6818 // c:2691 + c:4087 arrsetfn — flags that affect the
6819 // VALUE store (PM_UNIQUE dedup, PM_LEFT/RIGHT_B/Z
6820 // padding width) must land on pm.flags BEFORE
6821 // assignaparam → arrsetfn runs, else those
6822 // setfns see the un-flagged pm. C zsh applies
6823 // these as part of the pre-assignment stamp at
6824 // typeset_single c:2476-2479. Mirror by
6825 // pre-stamping the value-affecting subset of
6826 // `on`. The full attribute mask (PM_READONLY,
6827 // PM_EXPORTED, etc.) still lands in the
6828 // post-assign block below since those don't
6829 // change the value at write time.
6830 let pre_assign_mask: u32 =
6831 PM_UNIQUE | PM_LEFT | PM_RIGHT_B | PM_RIGHT_Z | PM_LOWER | PM_UPPER;
6832 pm.node.flags |= (on as u32 & pre_assign_mask) as i32;
6833 pm.node.flags &= !((off as u32 & pre_assign_mask) as i32);
6834 // c:Src/builtin.c:2544 — `if (isset(TYPESETTOUNSET))
6835 // pm->node.flags |= PM_DEFAULTED;`. Under
6836 // `setopt typeset_to_unset`, bare `typeset NAME`
6837 // (no `=`) creates a "declared but not assigned"
6838 // entry. Only fires when the arg has no `=`
6839 // (assignment paths get PM_UNSET cleared as a side
6840 // effect of the value write). Bug #280 in
6841 // docs/BUGS.md.
6842 if !arg.contains('=') && isset(crate::ported::zsh_h::TYPESETTOUNSET) {
6843 pm.node.flags |= PM_DEFAULTED as i32;
6844 }
6845 // c:2412-2413 + c:2426 — the newspecial preserve block.
6846 // C REUSES the special's own Param struct for the local
6847 // ("For specials, we keep the same struct but zero
6848 // everything", c:2385-2388): it copies the saved values
6849 // into `tpm` —
6850 // tpm->base = pm->base;
6851 // tpm->width = pm->width;
6852 // — and leaves `pm->base` / `pm->width` untouched, then
6853 // restores the readonly bit explicitly:
6854 // /*
6855 // * Readonlyness of special parameters must be preserved.
6856 // */
6857 // pm->node.flags |= tpm->node.flags & PM_READONLY;
6858 // zshrs allocates a FRESH node for the shadow, so both
6859 // have to be copied off `pm.old` (the node the special
6860 // was pushed down to). Without them `() { local status;
6861 // typeset -p status }` printed `typeset -i status=0`
6862 // instead of zsh's `typeset -i10 -r status=0`
6863 // (B02typeset.ztst:67) — base 0 suppressed the radix and
6864 // the missing PM_READONLY dropped the `-r` letter.
6865 if keep_special {
6866 if let Some(old) = pm.old.as_mut() {
6867 pm.base = old.base; // c:2412
6868 pm.width = old.width; // c:2413
6869 pm.node.flags |= (old.node.flags as u32 & PM_READONLY) as i32;
6870 // c:2426
6871 // !!! WARNING: RUST-ONLY REPRESENTATION !!!
6872 // C keeps the copied table in `tpm->u.hash`. zshrs's
6873 // `u_hash` is not populated for the magic rows, so the
6874 // snapshot rides on the saved node as a flat
6875 // key/value list in `u_arr`; endparamscope takes it
6876 // back out and replays it through the whole-hash
6877 // setfn.
6878 if special_hash_snapshot.is_some() {
6879 old.u_arr = special_hash_snapshot.take(); // c:1289
6880 }
6881 // c:2608-2609 — `else if (newspecial != NS_NONE &&
6882 // !(pm->old->node.flags & (PM_NORESTORE|PM_READONLY)))`,
6883 // the arm taken when `ASG_VALUEP(asg)` is false.
6884 if !arg.contains('=')
6885 && (old.node.flags as u32
6886 & (crate::ported::zsh_h::PM_NORESTORE | PM_READONLY))
6887 == 0
6888 {
6889 empty_special_type = Some(PM_TYPE(old.node.flags as u32));
6890 }
6891 }
6892 }
6893 }
6894 }
6895 // c:2610-2632 — "We need to use the special setting function to
6896 // re-initialise the special parameter to empty." The local keeps the
6897 // special's accessors, so each type is written through the
6898 // name-routed setter that reaches them: `local -a path` empties
6899 // $PATH as well, `local +h IFS` clears the field separators.
6900 match empty_special_type {
6901 Some(t) if t == PM_SCALAR || t == PM_NAMEREF => {
6902 let _ = setsparam(arg_name, ""); // c:2617 gsu.s->setfn(pm, "")
6903 }
6904 Some(t) if t == PM_INTEGER => {
6905 let _ = setiparam(arg_name, 0); // c:2620 gsu.i->setfn(pm, 0)
6906 }
6907 Some(t) if t == PM_EFLOAT || t == PM_FFLOAT => {
6908 // c:2624 gsu.f->setfn(pm, 0.0)
6909 let zero = mnumber {
6910 l: 0,
6911 d: 0.0,
6912 type_: crate::ported::zsh_h::MN_FLOAT,
6913 };
6914 let _ = crate::ported::params::setnparam(arg_name, zero);
6915 }
6916 Some(t) if t == PM_ARRAY => {
6917 let _ = setaparam(arg_name, Vec::new()); // c:2627 mkarray(NULL)
6918 }
6919 // c:2630 gsu.h->setfn(pm, newparamtable(17, ...)) — an empty table
6920 // through the whole-hash setfn (setpmraliases flushes the class,
6921 // setpmfunctions adds nothing). Read-only rows never get here in
6922 // C (c:2609); zshrs's seeded rows do not all carry PM_READONLY,
6923 // so gate on the same writable-row test the snapshot uses.
6924 Some(t) if t == PM_HASHED && hash_row_writable => {
6925 let _ = crate::ported::params::assignaparam(arg_name, Vec::new(), 0);
6926 }
6927 _ => {}
6928 }
6929 }
6930
6931 // c:2462-2467 — subscripted-name PM_LOCAL guard: `else if
6932 // ((on & PM_LOCAL) && locallevel) { ... if (!pm || pm->level
6933 // != locallevel) zerrnam("can't create local array elements") }`.
6934 // Refuses to create a NEW local for `local arr[N]=val` when
6935 // the outer-scope pm at a different level exists. The Rust
6936 // per-arg loop treats subscripted names as the eq-branch's
6937 // `name[key]=val` shape inside assignsparam; the guard fires
6938 // here BEFORE the assignment so we emit the C error message.
6939 if let Some(br) = arg_name.find('[') {
6940 let base = &arg_name[..br];
6941 if (on as u32 & PM_LOCAL) != 0 && cur_locallevel != 0 {
6942 // c:2462
6943 let pm_level = paramtab()
6944 .read()
6945 .ok()
6946 .and_then(|t| t.get(base).map(|pm| pm.level));
6947 if pm_level.is_none() || pm_level != Some(cur_locallevel) {
6948 // c:2466
6949 // c:2463-2467 — C blanks the `[` only for the getnode
6950 // lookup (`*subscript = 0; … *subscript = '['`), so the
6951 // message carries the FULL `pname` INCLUDING the
6952 // subscript: `array[2]: can't create local array
6953 // elements`. Passing the bare base name dropped `[2]`.
6954 zerrnam(
6955 name,
6956 &format!("{}: can't create local array elements", arg_name), // c:2466
6957 );
6958 continue; // c:2467
6959 }
6960 }
6961 // c:Src/builtin.c:2500-2502 — `else { zerrnam(cname,
6962 // "%s: inconsistent array element or slice assignment",
6963 // pname); return NULL; }`. C's typeset_single rejects
6964 // type-flag attributes on subscripted names: when the
6965 // name has a `[…]` subscript AND the requested `on` mask
6966 // carries a non-scalar PM_TYPE bit (PM_INTEGER, PM_EFLOAT,
6967 // PM_FFLOAT, PM_ARRAY, PM_HASHED), it's
6968 // `typeset -i h[k]` / `-A h[k]` / etc. — attribute
6969 // applies to whole array, not element. zshrs's
6970 // typeset path silently accepted these and applied
6971 // the attribute to the element. Bug #219 in
6972 // docs/BUGS.md. Plain `h[k]=value` (no `-` flag) has
6973 // PM_TYPE(on) == PM_SCALAR and stays valid.
6974 let pm_type_bits =
6975 on as u32 & (PM_INTEGER | PM_EFLOAT | PM_FFLOAT | PM_ARRAY | PM_HASHED);
6976 if pm_type_bits != 0 {
6977 zerrnam(
6978 name,
6979 &format!(
6980 "{}: inconsistent array element or slice assignment",
6981 arg_name
6982 ),
6983 );
6984 continue;
6985 }
6986 }
6987
6988 if let Some(eq) = arg.find('=') {
6989 let n = &arg[..eq];
6990 // c:Src/builtin.c:2289 — `pm->node.flags = ... & ~off` must
6991 // clear `off` bits in value-affecting flags BEFORE the
6992 // assignment runs (params.rs assignaparam:5526 snapshots
6993 // PM_UNIQUE and dedups in setfn). The PM_LOCAL pre_assign
6994 // block below only fires when on & PM_LOCAL; top-level
6995 // `typeset +U arr=(...)` has on=0 and would otherwise
6996 // leave the stale PM_UNIQUE in place. Bug #234.
6997 let pre_assign_off_mask =
6998 (PM_UNIQUE | PM_LEFT | PM_RIGHT_B | PM_RIGHT_Z | PM_LOWER | PM_UPPER) as i32;
6999 let off_in_pre_mask = (off as i32) & pre_assign_off_mask;
7000 if off_in_pre_mask != 0 {
7001 if let Ok(mut tab) = paramtab().write() {
7002 if let Some(pm) = tab.get_mut(n) {
7003 pm.node.flags &= !off_in_pre_mask;
7004 }
7005 }
7006 }
7007 let raw_v = &arg[eq + 1..];
7008 // c:2945-3050 — `=(elem elem ...)` array-init syntax.
7009 // The parser hands the whole `(...)` body in as one arg
7010 // when typeset's BINF_MAGICEQUALS is set; the `(` / `)` are
7011 // literal first/last bytes. Strip them and split on
7012 // whitespace to recover the element list.
7013 //
7014 // c:Src/parse.c par_simple/intypeset — the array-vs-scalar
7015 // distinction is SYNTACTIC (the `asg->flags & ASG_ARRAY`
7016 // bit), set when the `(` is an UNQUOTED paren at parse time,
7017 // NOT a heuristic on the expanded value. zshrs encodes that
7018 // bit as the `\u{1f}` REJOIN_SEP marker inserted by
7019 // compile_zsh's BUILTIN_TYPESET_PAREN_PACK, which only fires
7020 // for syntactic (unquoted) paren-inits. A double-quoted
7021 // value that happens to expand to `(...)` —
7022 // `typeset out="(${x:gs/%/%%})"` — reaches here as a plain
7023 // dequoted scalar with NO marker and must stay a SCALAR
7024 // (zsh: `out=(ruby-3.2%%@1.0)`). Keying on `starts_with('(')`
7025 // alone mis-stored it as an array. The `()`-empty and
7026 // `=(\u{1f}elem…)` pack forms always carry the marker.
7027 let is_paren_init = raw_v.starts_with('(')
7028 && raw_v.ends_with(')')
7029 && raw_v.len() >= 2
7030 && raw_v.contains('\u{1f}');
7031 // c:Src/builtin.c:2095-2097 — `if (ASG_ARRAYP(asg) &&
7032 // PM_TYPE(on) == PM_SCALAR && ...) on |= PM_ARRAY;`. Auto-
7033 // promote to array when the assignment shape is array but
7034 // no explicit type was requested. Required so plain
7035 // `local arr=(a b c)` (which carries on=PM_LOCAL with
7036 // no PM_TYPE bits) reaches the array-init path. Without
7037 // it the c:2342 inconsistency check below errors on every
7038 // bare array assign.
7039 let mut on = on;
7040 // c:Src/builtin.c:2091-2093 — the promotion is skipped when the
7041 // parameter is being reused and is ALREADY an array or hash:
7042 // if (ASG_ARRAYP(asg) && PM_TYPE(on) == PM_SCALAR &&
7043 // !(usepm && (PM_TYPE(pm->node.flags) & (PM_ARRAY|PM_HASHED))))
7044 // on |= PM_ARRAY;
7045 // The reuse arm then hands the list to assignaparam (c:2324-2329),
7046 // which keeps a hashed pm hashed (c:Src/params.c:3341 resets only
7047 // non-array types), so `typeset -A h=(a 1); typeset h=(b 2)` is
7048 // still an assoc. usepm is cleared first for a new local level
7049 // (c:2078-2086). The inline paths below key on `on`'s type, so
7050 // the reused pm's type stands in for the untouched PM_SCALAR.
7051 let reused_type = paramtab().read().ok().and_then(|t| {
7052 t.get(arg_name).and_then(|pm| {
7053 let f = pm.node.flags as u32;
7054 let same_scope = (on as u32 & PM_LOCAL) == 0
7055 || pm.level == locallevel_param.load(Relaxed) as i32;
7056 let typ = crate::ported::zsh_h::PM_TYPE(f);
7057 ((f & PM_UNSET) == 0 && same_scope && (typ == PM_ARRAY || typ == PM_HASHED))
7058 .then_some(typ)
7059 })
7060 });
7061 if is_paren_init && crate::ported::zsh_h::PM_TYPE(on) == PM_SCALAR {
7062 on |= reused_type.unwrap_or(PM_ARRAY);
7063 }
7064 let is_hashed = (on & PM_HASHED) != 0;
7065 let is_array = (on & PM_ARRAY) != 0;
7066 // c:Src/builtin.c:2342-2347 — `inconsistent type for
7067 // assignment`: when the user types an array RHS
7068 // (`x=(...)`) but the resolved type flags don't include
7069 // PM_ARRAY|PM_HASHED, error out. C source:
7070 // ```
7071 // if ((asg->flags & ASG_ARRAY) ?
7072 // !(on & (PM_ARRAY|PM_HASHED)) :
7073 // (asg->value.scalar && (on & (PM_ARRAY|PM_HASHED)))) {
7074 // zerrnam(cname, "%s: inconsistent type for assignment", pname);
7075 // return NULL;
7076 // }
7077 // ```
7078 // The C conflict-resolution pass at c:2718-2742 clears
7079 // PM_ARRAY/PM_HASHED from `on` when a conflicting type
7080 // (`-i` / `-E` / `-F`) is requested (`on & PM_INTEGER →
7081 // off |= PM_ARRAY; on &= ~off`), so `-ia x=(1 2 3)` lands
7082 // here with PM_INTEGER set + PM_ARRAY cleared. The
7083 // c:2095 auto-promote above doesn't fire either (PM_TYPE
7084 // is non-zero from PM_INTEGER), so the inconsistency
7085 // error fires correctly. Bug #250.
7086 if is_paren_init && (on & (PM_ARRAY | PM_HASHED)) == 0 {
7087 zerrnam(
7088 name, // cname
7089 &format!("{}: inconsistent type for assignment", n),
7090 );
7091 unqueue_signals();
7092 return 1;
7093 }
7094 if is_paren_init {
7095 let inner = &raw_v[1..raw_v.len() - 1]; // c:2950
7096 // c:Src/builtin.c:2555-2556 globlist — C glob-expands each
7097 // element working on the TOKENIZED value where quoted
7098 // metachars keep their Bnull escape, so a quoted `(` / `*`
7099 // is inert. zshrs's compile path (compile_zsh's
7100 // BUILTIN_TYPESET_PAREN_PACK) already runs the per-element
7101 // glob via compile_word_str — identical to the bare
7102 // `arr=(…)` path (compile_zsh.rs ~3294) — and hands
7103 // bin_typeset the FULLY-EXPANDED, DEQUOTED elements via the
7104 // `\u{1f}` sentinel form. Re-globbing those dequoted values
7105 // here is a double-glob that loses the original quoting:
7106 // `typeset -a p=("*.txt")` globbed the quoted star, and
7107 // `typeset -a p=("a=\$((1+2))")` parsed the literal `((` as
7108 // a bareglobqual ("unknown file attribute: ("). Skip the
7109 // re-glob for the pack (`\u{1f}`) form; only the legacy
7110 // whitespace form (elements not pre-expanded) needs it.
7111 let from_pack = inner.contains('\u{1f}');
7112 // c:2952 globlist — each list node is one element. When
7113 // the multi-arg rejoin loop above ran, elements are
7114 // separated by the `\u{1f}` REJOIN_SEP sentinel
7115 // (consecutive sentinels preserve quoted-empty args).
7116 // Otherwise (single-arg paren-init form like `a=(1 2 3)`)
7117 // the inner is plain-whitespace separated.
7118 // Bug #93 in docs/BUGS.md.
7119 let split_elems: Vec<String> = if inner.contains('\u{1f}') {
7120 // c:Src/builtin.c:2952 globlist — each REJOIN_SEP
7121 // segment is ONE already-parsed array element (the
7122 // parser split argv and quote-removed each element),
7123 // so split EXACTLY on the sentinel. Do NOT trim or
7124 // drop all-whitespace segments — that would corrupt
7125 // quoted leading/trailing whitespace, e.g.
7126 // `local a=( " name: x" )` (the ` name:` prefix is
7127 // significant and must survive for `${a[(i) name: …]}`
7128 // to match). Boundary empties from the `=(` / `)` argv
7129 // fragments are removed by the first/last-empty trim
7130 // immediately below.
7131 inner.split('\u{1f}').map(String::from).collect::<Vec<_>>()
7132 } else {
7133 inner.split_whitespace().map(String::from).collect()
7134 };
7135 let split_elems = if inner.contains('\u{1f}') {
7136 let mut v: Vec<String> = split_elems;
7137 // Trim only the FIRST leading empty (from the open
7138 // `=(` arg's tail after stripping `(`) and the LAST
7139 // trailing empty (from the close `)` arg's head),
7140 // preserving any genuine empties in the middle.
7141 if v.first().is_some_and(|s| s.is_empty()) {
7142 v.remove(0);
7143 }
7144 if v.last().is_some_and(|s| s.is_empty()) {
7145 v.pop();
7146 }
7147 v
7148 } else {
7149 split_elems
7150 };
7151 // c:Src/subst.c:2558-2571 — `$=var` IFS-split operator.
7152 // The full paramsubst path in subst.rs doesn't yet
7153 // handle the `=` flag (spbreak=2 in C) when invoked
7154 // outside the canonical addvars→prefork→stringsubst
7155 // chain. typeset's array-init reaches bin_typeset
7156 // AFTER the unified prefork has already run (against
7157 // the entire `arr=( ... )` string, treating the parens
7158 // as literal), so any `$=name` inside the parens
7159 // stayed untouched.
7160 //
7161 // Mirror the C `spbreak=2` semantics inline: for each
7162 // element of the form `$=NAME`, look up NAME, split on
7163 // IFS, and substitute the resulting fields in place.
7164 // Other elements (already paramsubst'd by the outer
7165 // prefork, or literal) pass through unchanged.
7166 let raw_elems: Vec<String> = {
7167 // c:Src/params.c IFS — the canonical IFS source is
7168 // the IFS shell param (PM_SPECIAL gsu reads the
7169 // live $IFS in the current scope). Reading the
7170 // OS env var would miss `local IFS=` overrides and
7171 // catch stale values left behind by earlier scopes.
7172 let ifs_chars: Vec<char> = crate::ported::params::getsparam("IFS")
7173 .unwrap_or_else(|| " \t\n".to_string())
7174 .chars()
7175 .collect();
7176 let mut out: Vec<String> = Vec::new();
7177 for se in split_elems {
7178 if let Some(stripped) = se.strip_prefix("$=") {
7179 // c:2562 — `s++; spbreak = 2; nojoin =
7180 // !(ifs && *ifs);`. Look up the variable;
7181 // if unset, treat as empty string and
7182 // produce no fields.
7183 let val =
7184 crate::ported::params::getsparam(stripped).unwrap_or_default();
7185 if ifs_chars.is_empty() {
7186 if !val.is_empty() {
7187 out.push(val);
7188 }
7189 } else {
7190 for field in val
7191 .split(|c: char| ifs_chars.contains(&c))
7192 .filter(|s| !s.is_empty())
7193 {
7194 out.push(field.to_string());
7195 }
7196 }
7197 } else {
7198 out.push(se);
7199 }
7200 }
7201 out
7202 };
7203 // c:2555-2556 — `globlist(vl, prefork_ret)` glob-expands
7204 // each element when it contains wildcards. Skipped for the
7205 // pre-expanded pack form (see `from_pack` above): those
7206 // elements were already glob-expanded (quote-aware) at
7207 // compile time, so re-globbing the dequoted values is wrong.
7208 let mut elems: Vec<String> = Vec::with_capacity(raw_elems.len());
7209 for re in raw_elems {
7210 if from_pack {
7211 elems.push(re);
7212 continue;
7213 }
7214 // c:Src/glob.c:1230 zglob — `if (... !haswilds(ostr)
7215 // ...) return;` short-circuits before patcompile.
7216 // In C, lexer/prefork preserves Bnull-escape on
7217 // quoted metachars (`'('` becomes `Bnull (`), so
7218 // haswilds returns false on a quoted-paren key.
7219 // zshrs's upstream prefork strips Bnull before
7220 // typeset args land here (see TRACE_TS_RAWV =
7221 // `[40, 31, 40, 31, ...]` — bare `(` without the
7222 // Bnull escape), so haswilds spuriously returns
7223 // true on a literal `(` from `T=( '(' v )`. Guard
7224 // by pre-compiling: if patcompile fails, the
7225 // element can't be a real glob — keep it literal
7226 // (matches the C zglob no-wild short-circuit and
7227 // suppresses the spurious "bad pattern" zerr).
7228 // haswilds scans TOKENIZED strings — tokenize a
7229 // local copy of the untokenized element first
7230 // (c:Src/glob.c:3548), as C does for runtime-built
7231 // strings (compcore.c:2231); patcompile consumes
7232 // the same tokenized form.
7233 let mut re_tok = re.to_string();
7234 crate::ported::glob::tokenize(&mut re_tok);
7235 if crate::ported::pattern::haswilds(&re_tok) {
7236 let compilable = crate::ported::pattern::patcompile(
7237 &re_tok,
7238 crate::ported::zsh_h::PAT_HEAPDUP as i32,
7239 None,
7240 )
7241 .is_some();
7242 if !compilable {
7243 elems.push(re);
7244 continue;
7245 }
7246 let expanded = crate::ported::glob::glob_path(&re);
7247 if expanded.is_empty() || (expanded.len() == 1 && expanded[0] == re) {
7248 elems.push(re);
7249 } else {
7250 elems.extend(expanded);
7251 }
7252 } else {
7253 elems.push(re);
7254 }
7255 }
7256 // c:2448 + c:2486-2494 — a subscripted name with an array value is
7257 // a slice assignment on the existing parameter:
7258 // } else if (PM_TYPE(on) == PM_ARRAY && ASG_ARRAYP(asg)) {
7259 // int flags = (asg->flags & ASG_KEY_VALUE) ? ASSPM_KEY_VALUE : 0;
7260 // if (!(pm = assignaparam(pname, asg->value.array ?
7261 // zlinklist2array(asg->value.array, 1) : mkarray(NULL), flags)))
7262 // return NULL;
7263 // dont_set = 1;
7264 // The array-init path below stored the element list under the
7265 // literal name `array[2,4]` and left `array` untouched.
7266 if n.contains('[') {
7267 let flags = if elems.iter().any(|e| e.starts_with(crate::ported::zsh_h::Marker)) {
7268 crate::ported::zsh_h::ASSPM_KEY_VALUE
7269 } else {
7270 0
7271 };
7272 if crate::ported::params::assignaparam(n, elems, flags).is_none() {
7273 returnval = 1; // c:2491 return NULL
7274 }
7275 continue;
7276 }
7277 // c:Src/builtin.c:2355-2378 — tc (type-conversion)
7278 // branch: the requested type differs from the
7279 // existing param's type. zsh keeps readonly/exported
7280 // status across the conversion (`on |= ~off &
7281 // (PM_READONLY|PM_EXPORTED) & pm->node.flags`) but
7282 // turns PM_READONLY OFF on the old pm so the delete/
7283 // recreate succeeds — `typeset -r h2=(); typeset -A
7284 // h2=(k v)` is legal in zsh. The Rust assignment
7285 // funnel (sethparam/setaparam) performs the type
7286 // rewrite in place, so clearing PM_READONLY here is
7287 // the c:2359 step and the post-assign stamp below
7288 // restores it from `on` (c:2357 carry).
7289 {
7290 let requested = if is_hashed { PM_HASHED } else { PM_ARRAY };
7291 let existing_flags = paramtab()
7292 .read()
7293 .ok()
7294 .and_then(|t| t.get(n).map(|pm| pm.node.flags as u32));
7295 if let Some(f) = existing_flags {
7296 if (f & PM_UNSET) == 0 && crate::ported::zsh_h::PM_TYPE(f) != requested {
7297 // c:2357 — carry readonly/exported into `on`.
7298 on |= !off & (PM_READONLY | PM_EXPORTED) & f;
7299 // c:2359 — `pm->node.flags &= ~PM_READONLY;`
7300 if let Ok(mut tab) = paramtab().write() {
7301 if let Some(pm) = tab.get_mut(n) {
7302 pm.node.flags &= !(PM_READONLY as i32);
7303 }
7304 }
7305 }
7306 }
7307 }
7308 // c:Src/builtin.c:2476-2479 + c:2691 + c:4087 arrsetfn —
7309 // pre-stamp value-affecting flags on the pm BEFORE the
7310 // array-init setarrvalue runs, so PM_UNIQUE drives dedup
7311 // inside assignaparam (params.rs:5571 reads pm.flags &
7312 // PM_UNIQUE to gate simple_arrayuniq). Without this,
7313 // `typeset -aUx u=(/bin /usr/bin /bin)` stamped PM_UNIQUE
7314 // only AFTER setarrvalue ran, so dedup never fired.
7315 // `typeset -aU u=(...)` happened to work due to a side
7316 // path; this makes both consistent. Bug #272.
7317 if is_array {
7318 let pre_assign_mask: u32 = PM_UNIQUE;
7319 if (on as u32 & pre_assign_mask) != 0 {
7320 let exists = paramtab()
7321 .read()
7322 .map(|t| t.contains_key(n))
7323 .unwrap_or(false);
7324 if !exists {
7325 let _ = createparam(n, PM_ARRAY as i32);
7326 }
7327 if let Ok(mut tab) = paramtab().write() {
7328 if let Some(pm) = tab.get_mut(n) {
7329 pm.node.flags |= (on as u32 & pre_assign_mask) as i32;
7330 }
7331 }
7332 }
7333 }
7334 // c:2357 — a scalar → array/hashed conversion is a TYPE change,
7335 // which recreates the param carrying only READONLY|EXPORTED, so
7336 // the old scalar padding flags (-L/-R/-Z) must NOT survive it:
7337 // `typeset -Z 3 x=7; typeset -a x=(1 2)` is `array`, not
7338 // `array-right_zeros`. Capture whether the param was ALREADY an
7339 // array/hashed (in which case it is NOT a type change and the
7340 // padding is preserved by the merge stamp below).
7341 let prior_was_arraylike = paramtab().read().ok().and_then(|t| {
7342 t.get(n).map(|pm| {
7343 let typ = PM_TYPE(pm.node.flags as u32);
7344 typ == PM_ARRAY || typ == PM_HASHED
7345 })
7346 }) == Some(true);
7347 if is_hashed && elems.iter().any(|e| e.starts_with(crate::ported::zsh_h::Marker)) {
7348 // c:Src/params.c:3485-3502 — once the compiler marked a
7349 // `[key]=value` element (c:Src/subst.c:49-79), EVERY element
7350 // must be a Marker / key / value triad.
7351 if elems.len() % 3 != 0
7352 || elems
7353 .chunks(3)
7354 .any(|t| !t[0].starts_with(crate::ported::zsh_h::Marker))
7355 {
7356 zerr("bad [key]=value syntax for associative array"); // c:3499
7357 // c:Src/params.c:3500 `return NULL` -> typeset_single c:2326-2329 returns
7358 // NULL -> bin_typeset c:3153-3156 `returnval = 1`; no attribute stamps.
7359 returnval = 1;
7360 continue;
7361 } else {
7362 // c:Src/params.c:4094-4117 arrhashsetfn — a `Marker +` triad
7363 // appends onto the key's current value.
7364 let mut map: IndexMap<String, String> = IndexMap::new();
7365 for t in elems.chunks(3) {
7366 if t[0].ends_with('+') {
7367 map.entry(t[1].clone()).or_default().push_str(&t[2]);
7368 } else {
7369 map.insert(t[1].clone(), t[2].clone());
7370 }
7371 }
7372 crate::ported::exec::set_assoc(n, map);
7373 }
7374 } else if is_hashed {
7375 // c:2960-2975 — `setdataparam(..., PM_HASHED, …)`.
7376 // Two assoc-init shapes accepted by zsh:
7377 // 1. flat alternating k/v: `m=(k1 v1 k2 v2)`
7378 // 2. per-element [K]=V: `m=([k1]=v1 [k2]=v2)`
7379 // The parser hands all elements as one `(…)` body,
7380 // so we detect shape 2 when every element starts
7381 // with `[` and contains `]=`. Otherwise fall back
7382 // to alternating pairs.
7383 let bracket_shape = !elems.is_empty()
7384 && elems.iter().all(|e| e.starts_with('[') && e.contains("]="));
7385 // c:Src/params.c:4076-4085 arrhashsetfn — the element list is
7386 // counted first and an ODD total is refused outright:
7387 // for (aptr = val; *aptr; ++aptr)
7388 // if (**aptr != Marker) ++alen;
7389 // if (alen % 2) {
7390 // freearray(val);
7391 // zerr("bad set of key/value pairs for associative array");
7392 // return;
7393 // }
7394 // The flat `m=(k1 v1 k2 v2)` form IS that list, so a trailing
7395 // key with no value is an error and the parameter is left
7396 // untouched. The pair walk below invented an empty value with
7397 // `it.next().unwrap_or_default()`, so `typeset -A a=(k)`
7398 // silently defined `a[k]=""` where zsh refuses the assignment.
7399 // In C's `[k]=v` form every element carries a Marker, so its
7400 // `alen` is always even and the gate cannot fire — which is why
7401 // the test is scoped to the flat shape here.
7402 let odd_pairs = !bracket_shape && elems.len() % 2 != 0; // c:4081
7403 // c:Src/params.c:3485-3502 — when the parser marked ANY
7404 // element as a `[key]=value` triad, EVERY element must be
7405 // one:
7406 // /*
7407 // * We strictly enforce [key]=value syntax for associative
7408 // * arrays. Marker can only indicate a Marker / key / value
7409 // * triad; it cannot be there by accident.
7410 // */
7411 // for (aptr = val; *aptr; aptr += 3)
7412 // if (**aptr != Marker) {
7413 // ... zerr("bad [key]=value syntax for associative array");
7414 // return NULL;
7415 // }
7416 // So a MIXED list (`local -A h=(1 one [2]=two 3 three)`)
7417 // fails with THAT message, not with arrhashsetfn's
7418 // odd-element-count one (B02typeset.ztst:63). zshrs's
7419 // parser hands the elements down as plain strings, so the
7420 // `[k]=v` shape is recognised exactly the way
7421 // `bracket_shape` above recognises it.
7422 let any_bracket = elems.iter().any(|e| e.starts_with('[') && e.contains("]=")); // c:3495
7423 if any_bracket && !bracket_shape {
7424 zerr("bad [key]=value syntax for associative array"); // c:3499
7425 // c:Src/params.c:3500 `return NULL` -> typeset_single c:2326-2329 returns
7426 // NULL -> bin_typeset c:3153-3156 `returnval = 1`; no attribute stamps.
7427 returnval = 1;
7428 continue;
7429 // c:3500 `return NULL;` — the parameter is NOT set.
7430 } else if odd_pairs {
7431 // c:4083 — `zerr(...)`; zshrs's zerr raises ERRFLAG_ERROR
7432 // itself (c:Src/utils.c:194), so the shell aborts the rest
7433 // of the input exactly as C does.
7434 zerr("bad set of key/value pairs for associative array");
7435 // c:4083
7436 // c:4084 `return;` — the parameter is NOT set.
7437 } else {
7438 let mut map: IndexMap<String, String> = IndexMap::new();
7439 if bracket_shape {
7440 for e in &elems {
7441 let close = e.find("]=").unwrap();
7442 let k = e[1..close].to_string();
7443 let v = e[close + 2..].to_string();
7444 map.insert(k, v);
7445 }
7446 } else {
7447 let mut it = elems.into_iter(); // c:2960 pair walk
7448 while let Some(k) = it.next() {
7449 let v = it.next().unwrap_or_default();
7450 map.insert(k, v); // c:2964 hashtab insert
7451 }
7452 }
7453 crate::ported::exec::set_assoc(n, map.clone());
7454 }
7455 } else if crate::dash_mode::bash_mode()
7456 && !elems.is_empty()
7457 && elems.iter().all(|e| e.starts_with('[') && e.contains("]="))
7458 && elems.iter().all(|e| {
7459 e.find("]=")
7460 .map(|c| e[1..c].trim().parse::<usize>().is_ok())
7461 .unwrap_or(false)
7462 })
7463 {
7464 // !!! BASH-MODE GATE !!! bash `declare -a a=([5]=x [10]=y)`
7465 // — explicit 0-based indices in an indexed-array literal
7466 // (the assoc branch above handles the `-A` form). Place each
7467 // value at its index and record the un-indexed slots as
7468 // sparse holes, mirroring the plain `a=([i]=v)` path.
7469 let mut pairs: Vec<(usize, String)> = Vec::new();
7470 for e in &elems {
7471 let close = e.find("]=").unwrap();
7472 if let Ok(idx) = e[1..close].trim().parse::<usize>() {
7473 pairs.push((idx, e[close + 2..].to_string()));
7474 }
7475 }
7476 let len = pairs.iter().map(|(i, _)| *i + 1).max().unwrap_or(0);
7477 let mut dense = vec![String::new(); len];
7478 let mut explicit: std::collections::BTreeSet<usize> =
7479 std::collections::BTreeSet::new();
7480 for (i, v) in pairs {
7481 if i < dense.len() {
7482 dense[i] = v;
7483 explicit.insert(i);
7484 }
7485 }
7486 crate::ported::exec::set_array(n, dense.clone());
7487 crate::bash_arrays::clear(n);
7488 crate::bash_arrays::note_holes_outside(n, &explicit, dense.len());
7489 } else if elems.iter().any(|e| e.starts_with(crate::ported::zsh_h::Marker)) {
7490 // c:3019-3020 — the compiler emitted `[k]=v` elements as
7491 // Marker triads (c:Src/subst.c:49-79), so the value goes to
7492 // assignaparam with ASSPM_KEY_VALUE, which places each one
7493 // by index (c:Src/params.c:3436-3541).
7494 let _ = crate::ported::params::assignaparam(
7495 n,
7496 elems.clone(),
7497 crate::ported::zsh_h::ASSPM_KEY_VALUE,
7498 );
7499 // !!! BASH-MODE GATE !!! and then the same sparse marking
7500 // the two literal forms above do. This is the shape the
7501 // compiler actually produces for `declare -a a=([5]=x
7502 // [10]=y)` — the bracket-form branch above never sees it,
7503 // because by then the elements are Marker triads and not
7504 // `[5]=x` strings — so the declare form came out DENSE:
7505 // eleven elements with nine empties where bash reports two.
7506 // Triad layout is [Marker, index, value] (c:Src/subst.c:49-79),
7507 // matching the `a=(…)` path in fusevm_bridge.
7508 if crate::dash_mode::sparse_arrays() {
7509 let marker = crate::ported::zsh_h::Marker;
7510 let pure_indexed = !elems.is_empty()
7511 && elems.len() % 3 == 0
7512 && elems.chunks(3).all(|ch| ch[0].starts_with(marker));
7513 if pure_indexed {
7514 let mut explicit: std::collections::BTreeSet<usize> =
7515 std::collections::BTreeSet::new();
7516 for ch in elems.chunks(3) {
7517 if let Ok(i) = ch[1].trim().parse::<usize>() {
7518 explicit.insert(i);
7519 }
7520 }
7521 let len = crate::ported::params::getaparam(n)
7522 .map(|a| a.len())
7523 .unwrap_or(0);
7524 crate::bash_arrays::clear(n);
7525 crate::bash_arrays::note_holes_outside(n, &explicit, len);
7526 }
7527 }
7528 } else {
7529 // c:2980-2995 — plain array.
7530 crate::ported::exec::set_array(n, elems.clone());
7531 }
7532 // c:2357 — on a genuine type change (was NOT array/hashed), drop
7533 // the old scalar padding flags; re-add only what THIS command's
7534 // `on` requests (so `typeset -aZ 3 arr=(...)` still zero-pads).
7535 if !prior_was_arraylike {
7536 let pad = (PM_LEFT | PM_RIGHT_B | PM_RIGHT_Z) as i32;
7537 if let Ok(mut tab) = paramtab().write() {
7538 if let Some(pm) = tab.get_mut(n) {
7539 pm.node.flags = (pm.node.flags & !pad) | (on as i32 & pad);
7540 }
7541 }
7542 }
7543 // c:2330-2337 (typeset_single) — `if (!(pm =
7544 // assignaparam(pname, ..., flags))) return NULL;
7545 // ... if (errflag) return NULL;` A readonly
7546 // rejection inside setarrvalue (c:Src/params.c:2900)
7547 // sets errflag and refuses the value; typeset_single
7548 // returns NULL and bin_typeset records `returnval = 1`
7549 // (c:3153-3156). Skip the attribute stamps — C never
7550 // reaches them on this path. Gated on usepm: the
7551 // c:2336 check lives ONLY in the reuse-existing-pm
7552 // branch.
7553 if usepm_existing && (errflag.load(Relaxed) & ERRFLAG_ERROR) != 0 {
7554 returnval = 1; // c:3156
7555 continue; // c:2337 return NULL
7556 }
7557 // c:2510-2520 — `on = pm->node.flags;` then stamp the
7558 // attribute bits on the just-assigned param. The
7559 // scalar-assign arm below does the same; the array /
7560 // assoc `=(...)` init path was missing this, so
7561 // `typeset -ax ARR=(a b)` left PM_EXPORTED unset on
7562 // the paramtab entry. `(t)ARR` then read `array`
7563 // instead of `array-export`, and `typeset -p ARR`
7564 // emitted `typeset -a ARR=...` instead of `-ax`.
7565 let post_assign_mask = (PM_READONLY
7566 | PM_EXPORTED
7567 | PM_LEFT
7568 | PM_RIGHT_B
7569 | PM_RIGHT_Z
7570 | PM_TAGGED
7571 | PM_HIDE
7572 | PM_HIDEVAL
7573 | PM_UNIQUE) as i32;
7574 let post_assign_to_set = (on
7575 & (PM_READONLY
7576 | PM_EXPORTED
7577 | PM_LEFT
7578 | PM_RIGHT_B
7579 | PM_RIGHT_Z
7580 | PM_TAGGED
7581 | PM_HIDE
7582 | PM_HIDEVAL
7583 | PM_UNIQUE)) as i32;
7584 if post_assign_to_set != 0 {
7585 if let Ok(mut tab) = paramtab().write() {
7586 if let Some(pm) = tab.get_mut(n) {
7587 // c:2289 MERGE — preserve untouched attributes.
7588 pm.node.flags |= post_assign_to_set;
7589 }
7590 }
7591 }
7592 } else {
7593 // c:2232-2238 (typeset_single) — a SCALAR assignment to an
7594 // existing ARRAY/HASHED param WITHOUT a type flag to convert it
7595 // is inconsistent: `typeset -A x=(k v); typeset x=scalar` errors.
7596 // (An explicit type flag — `typeset -i x=9` — is a type change
7597 // handled above and does NOT reach here.)
7598 let target_is_arraylike = paramtab().read().ok().and_then(|t| {
7599 t.get(n).map(|pm| {
7600 let typ = PM_TYPE(pm.node.flags as u32);
7601 typ == PM_ARRAY || typ == PM_HASHED
7602 })
7603 }) == Some(true);
7604 let requesting_type =
7605 (on as u32 & (PM_INTEGER | PM_EFLOAT | PM_FFLOAT | PM_ARRAY | PM_HASHED)) != 0;
7606 // c:2115-2126 — `chflags = ((off & pm->node.flags) | (on &
7607 // ~pm->node.flags)) & (PM_INTEGER|...|PM_AUTOLOAD)`; a type
7608 // change (`typeset +a arr=s` removes PM_ARRAY) sets tc and
7609 // clears usepm, so the c:2233 inconsistency test never runs.
7610 let removing_type = paramtab().read().ok().and_then(|t| {
7611 t.get(n).map(|pm| {
7612 let pmf = pm.node.flags as u32;
7613 let chflags = ((off as u32 & pmf) | (on as u32 & !pmf))
7614 & (PM_INTEGER
7615 | PM_EFLOAT
7616 | PM_FFLOAT
7617 | PM_HASHED
7618 | PM_ARRAY
7619 | PM_TIED
7620 | PM_AUTOLOAD);
7621 chflags != 0 && chflags != (PM_EFLOAT | PM_FFLOAT) // c:2119
7622 })
7623 }) == Some(true);
7624 let requesting_type = requesting_type || removing_type;
7625 // c:2216 — the whole c:2233-2237 inconsistency test lives INSIDE
7626 // `if (usepm) {`, i.e. it only applies when typeset_single is
7627 // RE-USING the existing pm. `usepm` is seeded at c:2062-2064
7628 // (`pm && !(pm->node.flags & PM_UNSET) || ...`) and then CLEARED
7629 // at c:2078-2090 when we are localizing a parameter that lives
7630 // at an outer scope:
7631 // if (usepm && locallevel != pm->level && (on & PM_LOCAL)) {
7632 // if ((pm->node.flags & PM_SPECIAL) && !(on & PM_HIDE)
7633 // && !(pm->node.flags & PM_HIDE & ~off))
7634 // newspecial = NS_NORMAL;
7635 // usepm = 0;
7636 // }
7637 // So `local path=/a/b` in a function NEVER reaches c:2236 in C:
7638 // `path` is PM_ARRAY|PM_TIED|PM_SPECIAL at level 0 while
7639 // locallevel is 1, usepm drops to 0, and control falls through
7640 // to the fresh-parameter path at c:2340, which compares the
7641 // value shape only against the REQUESTED `on` flags (handled by
7642 // the c:2345 test just below). zshrs evaluated c:2236
7643 // unconditionally against whatever was in paramtab, so EVERY
7644 // `local <tied-or-array-special>=<scalar>` was rejected —
7645 // `path`, `cdpath`, `fignore`, `manpath`, `fpath`, ... Bug #1110.
7646 // `usepm_existing` / `pm_level_existing` are the PRE-createparam
7647 // snapshots taken at the top of this arg's iteration, which is
7648 // exactly the state c:2062-2090 reads.
7649 let usepm = usepm_existing
7650 && !(pm_level_existing != cur_locallevel && (on as u32 & PM_LOCAL) != 0); // c:2078
7651 // c:2355-2378 — `if (tc && !OPT_ISSET(ops,'p'))`: removing
7652 // the array/hash type (`typeset +a arr=s`) keeps only
7653 // READONLY|EXPORTED (`on |= ~off & (PM_READONLY|PM_EXPORTED)
7654 // & pm->node.flags`, c:2357) and `unsetparam_pm(pm, 0, 1)`
7655 // (c:2378); the scalar path below then creates it afresh.
7656 // A numeric type REMOVED with no numeric type requested
7657 // (`typeset +i n=s`) is the same tc; a numeric type requested
7658 // is converted by the pre-assign arm below.
7659 let off_removes_type = paramtab().read().ok().and_then(|t| {
7660 t.get(n).map(|pm| {
7661 (off as u32 & pm.node.flags as u32 & (PM_INTEGER | PM_EFLOAT | PM_FFLOAT))
7662 != 0
7663 && (on as u32 & (PM_INTEGER | PM_EFLOAT | PM_FFLOAT)) == 0
7664 })
7665 }) == Some(true);
7666 let mut on = if usepm
7667 && removing_type
7668 && (target_is_arraylike || off_removes_type)
7669 && !OPT_ISSET(&ops, b'p')
7670 {
7671 let pmf = paramtab()
7672 .read()
7673 .ok()
7674 .and_then(|t| t.get(n).map(|pm| pm.node.flags as u32))
7675 .unwrap_or(0);
7676 let carried = !(off as u32) & (PM_READONLY | PM_EXPORTED) & pmf; // c:2357
7677 if let Ok(mut tab) = paramtab().write() {
7678 if let Some(pm) = tab.get_mut(n) {
7679 pm.node.flags &= !(PM_READONLY as i32); // c:2359
7680 }
7681 }
7682 crate::ported::params::unsetparam(n); // c:2378
7683 // c:2521 — `createparam(pname, on & ~PM_READONLY)` re-types the
7684 // struct a kept local left behind (Src/params.c:1132 `pm = oldpm`).
7685 let _ = createparam(n, ((on | carried) & !(PM_READONLY | PM_LOCAL)) as i32);
7686 on | carried
7687 } else {
7688 on
7689 };
7690 // c:2071 `usepm = 2` joins the c:2233 test (same c:2078 level rule).
7691 let usepm_cmp = usepm
7692 || (special_unset_existing
7693 && !(pm_level_existing != cur_locallevel && (on as u32 & PM_LOCAL) != 0));
7694 if usepm_cmp && target_is_arraylike && !requesting_type {
7695 zerrnam(name, &format!("{}: inconsistent type for assignment", n)); // c:2236
7696 returnval = 1;
7697 continue;
7698 }
7699 // c:2062-2064 — `usepm = pm && (!(pm->node.flags & PM_UNSET) || ...)`:
7700 // an UNSET non-special pm (e.g. `typeset -a g` under
7701 // TYPESET_TO_UNSET, c:2540-2541) is not reused, so control reaches
7702 // the fresh-parameter path whose c:2521 `createparam(pname, on &
7703 // ~PM_READONLY)` takes over the unset struct (Src/params.c:1132
7704 // `pm = oldpm`) and re-types it from `on` alone — `typeset g=s`
7705 // leaves a SCALAR, not the old declared array.
7706 // The same holds for every type, not just arrays: `integer y; unset
7707 // y; typeset -u y=abc` re-types the struct from `-u` alone, and
7708 // assignsparam (whose fetchvalue treats an unset, undeclared node
7709 // as absent, c:Src/params.c:2264-2265) then finds it set. Only the
7710 // REUSE arm of createparam (c:Src/params.c:1130 `oldpm->level ==
7711 // locallevel || !(flags & PM_LOCAL)`) is modelled here; a shadow
7712 // of an outer-scope name was already created above.
7713 if !usepm {
7714 let unset_plain = paramtab().read().ok().and_then(|t| {
7715 t.get(n).map(|pm| {
7716 (pm.node.flags as u32 & PM_UNSET) != 0
7717 && (pm.node.flags as u32 & PM_SPECIAL) == 0
7718 && (pm.level == cur_locallevel || (on as u32 & PM_LOCAL) == 0)
7719 })
7720 }) == Some(true);
7721 if unset_plain {
7722 let _ = createparam(n, (on as u32 & !(PM_READONLY | PM_LOCAL)) as i32); // c:2521
7723 }
7724 }
7725 // c:Src/builtin.c:2342-2345 (second half of the inconsistency
7726 // test) — a SCALAR value assigned to an explicitly-REQUESTED
7727 // array/hashed declaration (`typeset -a g=1`, `typeset -A h=1`,
7728 // `typeset -aU u=1`) is inconsistent. The check above only
7729 // caught a scalar assigned to a pre-EXISTING array with no type
7730 // flag; this catches the `-a`/`-A` decl form (paren-RHS to a
7731 // non-array is the mirror half at builtin.rs:5687). zsh errors
7732 // and leaves the param UNSET (a pre-existing value dropped too).
7733 // Bug #1028.
7734 if (on as u32 & (PM_ARRAY | PM_HASHED)) != 0 {
7735 zerrnam(name, &format!("{}: inconsistent type for assignment", n)); // c:2345
7736 crate::ported::params::unsetparam(n);
7737 returnval = 1;
7738 continue;
7739 }
7740 // c:3010-3030 — `name=value` scalar assign. C-canonical
7741 // `setsparam` (Src/params.c:3350) writes paramtab; the
7742 // env mirror at `Src/params.c:3024 addenv` follows.
7743 // PM_LOWER / PM_UPPER do NOT fold at assignment. `setstrvalue`
7744 // (c:Src/params.c:2677) has no case arm at all — the only two
7745 // fold sites in the C source are `getstrvalue`'s VALFLAG_SUBST
7746 // tail (c:2497-2506, i.e. READ during substitution) and
7747 // `copyenvstr` (c:5434-5442, the env mirror). The stored value
7748 // stays verbatim, which is why removing the attribute restores
7749 // the original text and why `typeset -p` shows it unfolded:
7750 // typeset -l v=ABC; typeset -p v → typeset -l v=ABC
7751 // typeset -l v=ABC; typeset +l v; print $v → ABC
7752 // Only the env mirror below folds, per copyenvstr.
7753 let env_val: String = if (on & PM_LOWER) != 0 {
7754 // c:5439-5440 — `if (flags & PM_LOWER) *s = tulower(*s)`.
7755 raw_v.to_ascii_lowercase()
7756 } else if (on & PM_UPPER) != 0 {
7757 // c:5441-5442 — `else if (flags & PM_UPPER) *s = tuupper(*s)`.
7758 raw_v.to_ascii_uppercase()
7759 } else {
7760 raw_v.to_string()
7761 };
7762 // c:typeset_single — createparam with the type flag
7763 // BEFORE assignsparam, so assignstrvalue's PM_TYPE
7764 // dispatch (params.c:2748) routes the value through
7765 // the correct setfn:
7766 // - PM_INTEGER → intsetfn (mathevali → u_val)
7767 // - PM_EFLOAT/PM_FFLOAT → floatsetfn (parsefloat → u_dval)
7768 // - PM_SCALAR → strsetfn (u_str)
7769 // The previous Rust ordering (setsparam first, then
7770 // flip flags) wrote "5" to u_str then changed PM_TYPE
7771 // to PM_INTEGER without migrating u_str → u_val, so
7772 // getsparam(n) read u_val=0 instead of 5.
7773 // c:2748-2784 — pre-assign type flags only (PM_INTEGER
7774 // etc. — affect storage / setfn dispatch). Post-assign
7775 // attributes (PM_READONLY / PM_EXPORTED / justification
7776 // bits) are stamped AFTER setsparam since the C path
7777 // (c:2475 `assignsparam(pname, val, 0)` → c:2510
7778 // `on = pm->node.flags`) sets PM_READONLY only after
7779 // the value lands. Mixing them pre-assign caused
7780 // `readonly y=hello` to error "read-only variable: y"
7781 // — the freshly-created pm had PM_READONLY which
7782 // blocked its OWN initial assign.
7783 let pre_assign_mask =
7784 (PM_INTEGER | PM_EFLOAT | PM_FFLOAT | PM_LOWER | PM_UPPER | PM_NAMEREF) as i32;
7785 let pre_assign_to_set = (on
7786 & (PM_INTEGER | PM_EFLOAT | PM_FFLOAT | PM_LOWER | PM_UPPER | PM_NAMEREF))
7787 as i32;
7788 if pre_assign_to_set != 0 {
7789 let pname_in_tab = paramtab()
7790 .read()
7791 .map(|t| t.contains_key(n))
7792 .unwrap_or(false);
7793 if !pname_in_tab {
7794 // c:1132+ createparam(name, type_flags) — fresh.
7795 let _ = createparam(n, pre_assign_to_set);
7796 } else {
7797 // c:2355-2378 tc (type-conversion) — flip the
7798 // PM_TYPE bits on the existing param BEFORE
7799 // re-assigning so assignstrvalue routes through
7800 // the new type's setfn.
7801 //
7802 // c:2357 — a TYPE change deletes and recreates the param
7803 // carrying ONLY READONLY|EXPORTED, so the old scalar
7804 // padding flags (-L/-R/-Z) do NOT survive it: after
7805 // `typeset -Z 3 x=7; typeset -i x=1` the type is plain
7806 // `integer`, not `integer-right_zeros`. When the new type
7807 // is numeric (INTEGER/EFLOAT/FFLOAT), clear the padding
7808 // flags from the old param too. (A single `typeset -iZ`
7809 // command re-adds RIGHT_Z via the post-assign stamp since
7810 // it is in `on`.)
7811 let is_numeric_type =
7812 (pre_assign_to_set as u32 & (PM_INTEGER | PM_EFLOAT | PM_FFLOAT)) != 0;
7813 // On a numeric type change also drop the scalar padding
7814 // flags AND — if the old param was an array/hashed —
7815 // PM_ARRAY|PM_HASHED plus its stored data, since C deletes
7816 // and recreates: `typeset -A x=(k v); typeset -F x=1.5`
7817 // yields a plain `float`, not a hashed remnant.
7818 let old_type = paramtab()
7819 .read()
7820 .ok()
7821 .and_then(|t| t.get(n).map(|pm| PM_TYPE(pm.node.flags as u32)))
7822 .unwrap_or(PM_SCALAR);
7823 let was_arraylike = old_type == PM_ARRAY || old_type == PM_HASHED;
7824 // c:2114-2118 chflags/tc, then c:2351-2359 — a type
7825 // conversion carries readonly/exported into `on` and
7826 // turns the old param's readonly off, so the value
7827 // assignment below is not refused and the post-assign
7828 // stamp restores the bit: `typeset -r x=ro;
7829 // typeset -F x=1.5` is a readonly float in zsh. Same
7830 // step the array-init arm above takes.
7831 let old_flags = paramtab()
7832 .read()
7833 .ok()
7834 .and_then(|t| t.get(n).map(|pm| pm.node.flags as u32))
7835 .unwrap_or(0);
7836 let chflags = ((off as u32 & old_flags) | (on as u32 & !old_flags))
7837 & (PM_INTEGER
7838 | PM_EFLOAT
7839 | PM_FFLOAT
7840 | PM_HASHED
7841 | PM_ARRAY
7842 | PM_TIED
7843 | PM_AUTOLOAD); // c:2114-2116
7844 let tc = chflags != 0 && chflags != (PM_EFLOAT | PM_FFLOAT); // c:2118
7845 if tc && !OPT_ISSET(&ops, b'p') && (old_flags & PM_READONLY) != 0 {
7846 on |= !off & (PM_READONLY | PM_EXPORTED) & old_flags; // c:2357
7847 if let Ok(mut tab) = paramtab().write() {
7848 if let Some(pm) = tab.get_mut(n) {
7849 pm.node.flags &= !(PM_READONLY as i32); // c:2359
7850 }
7851 }
7852 }
7853 let extra_clear = if is_numeric_type {
7854 let mut c = (PM_LEFT | PM_RIGHT_B | PM_RIGHT_Z) as i32;
7855 if was_arraylike {
7856 c |= (PM_ARRAY | PM_HASHED) as i32;
7857 }
7858 c
7859 } else {
7860 0
7861 };
7862 if is_numeric_type && was_arraylike {
7863 // c:2355-2378 — array/hashed → numeric is a full type
7864 // conversion: delete the old aggregate (storage +
7865 // paramtab entry) and recreate as the numeric type, so
7866 // the value migrates through the new setfn and the
7867 // result is a clean `integer`/`float`. Modifying flags
7868 // in place left the hashed storage's setfn dispatch
7869 // active, so the value stayed a scalar.
7870 crate::ported::exec::unset_array(n);
7871 crate::ported::exec::unset_assoc(n);
7872 if let Ok(mut tab) = paramtab().write() {
7873 tab.remove(n);
7874 }
7875 let _ = createparam(n, pre_assign_to_set);
7876 } else if let Ok(mut tab) = paramtab().write() {
7877 if let Some(pm) = tab.get_mut(n) {
7878 pm.node.flags = (pm.node.flags & !(pre_assign_mask | extra_clear))
7879 | pre_assign_to_set;
7880 }
7881 }
7882 }
7883 }
7884 // c:2289 — `pm->node.flags = (pm->node.flags | (on &
7885 // ~PM_READONLY)) & ~(off | PM_UNSET);` — `+n` / `+r`
7886 // WITH a value clear the bits BEFORE the assignment
7887 // runs (`typeset +rn ref=RW`: type change first, so
7888 // the = writes the now-plain scalar, not through the
7889 // old refname).
7890 let off_pre = (off as u32 & (PM_NAMEREF | PM_READONLY)) as i32;
7891 if off_pre != 0 {
7892 if let Ok(mut tab) = paramtab().write() {
7893 if let Some(pm) = tab.get_mut(n) {
7894 pm.node.flags &= !off_pre;
7895 if (off as u32 & PM_NAMEREF) != 0 {
7896 pm.width = 0;
7897 pm.base = 0;
7898 }
7899 }
7900 }
7901 }
7902 // c:2322 — typeset assigns via `assignsparam(pname, ..., 0)`:
7903 // flags=0, NOT setsparam's ASSPM_WARN (params.c:3294), so an
7904 // explicit `typeset -g NAME=val` inside a function never
7905 // trips WARN_CREATE_GLOBAL. Routing through setsparam here
7906 // made zshrs warn where zsh is silent (f-sy-h's
7907 // `typeset -g _ZSH_HIGHLIGHT_PRIOR_BUFFER=...`).
7908 // c:2577-2604 — the `if (ASG_VALUEP(asg) && !dont_set)` tail of
7909 // typeset_single dispatches on the TYPE OF THE PARAM WE ENDED UP
7910 // WITH, not on the shape of the value:
7911 // if (pm->node.flags & (PM_ARRAY|PM_HASHED)) { ...array... }
7912 // else { ... assignsparam(pname, ztrdup(asg->value.scalar), 0); }
7913 // `local path=/a/b` arrives here holding an ARRAY pm — the
7914 // newspecial block (c:2381-2425) preserved `path`'s PM_TYPE
7915 // through `pm->node.flags = (PM_TYPE(pm->node.flags) | on |
7916 // PM_SPECIAL) & ~off` — so C takes the array arm. Recomputed
7917 // here rather than reusing the c:2236 snapshot because the
7918 // pre-assign type-flag block above can convert an array param
7919 // to a numeric scalar (`typeset -i path=5`).
7920 let pm_is_arraylike = paramtab().read().ok().and_then(|t| {
7921 t.get(n).map(|pm| {
7922 let typ = PM_TYPE(pm.node.flags as u32);
7923 typ == PM_ARRAY || typ == PM_HASHED
7924 })
7925 }) == Some(true);
7926 if pm_is_arraylike {
7927 // c:2564-2576 — verbatim from the C:
7928 // /*
7929 // * Attempt to assign a scalar value to an array.
7930 // * This can happen if the array is special.
7931 // * We'll be lenient and guess what the user meant.
7932 // * This is how normal assignment works.
7933 // */
7934 // if (*asg->value.scalar) {
7935 // /* Array with one value */
7936 // arrayval = mkarray(ztrdup(asg->value.scalar));
7937 // } else {
7938 // /* Empty array */
7939 // arrayval = mkarray(NULL);
7940 // }
7941 let arrayval: Vec<String> = if raw_v.is_empty() {
7942 Vec::new() // c:2575 mkarray(NULL)
7943 } else {
7944 vec![raw_v.to_string()] // c:2572 mkarray(ztrdup(...))
7945 };
7946 crate::ported::exec::set_array(n, arrayval); // c:2578 assignaparam
7947 } else {
7948 crate::ported::params::assignsparam(n, raw_v, 0); // c:2322
7949 }
7950 // c:2326-2328 + c:2336-2337 (typeset_single) —
7951 // `if (asg->value.scalar && !(pm = assignsparam(
7952 // pname, ztrdup(asg->value.scalar), 0)))
7953 // return NULL;
7954 // ... if (errflag) return NULL;`
7955 // A readonly rejection inside assignstrvalue
7956 // (c:Src/params.c:2697) sets errflag and the value is
7957 // refused; typeset_single returns NULL and bin_typeset
7958 // records `returnval = 1` (c:3153-3156). Skip the
7959 // attribute stamps — C never reaches them on this path.
7960 // Gated on usepm: the c:2336 check lives ONLY in the
7961 // reuse-existing-pm branch.
7962 if usepm_existing && (errflag.load(Relaxed) & ERRFLAG_ERROR) != 0 {
7963 returnval = 1; // c:3156
7964 continue; // c:2337 return NULL
7965 }
7966 // c:2510-2520 — `on = pm->node.flags;` then stamp the
7967 // attribute bits on the just-assigned param.
7968 let post_assign_mask = (PM_READONLY
7969 | PM_EXPORTED
7970 | PM_LEFT
7971 | PM_RIGHT_B
7972 | PM_RIGHT_Z
7973 | PM_TAGGED
7974 | PM_HIDE
7975 | PM_HIDEVAL
7976 | PM_UNIQUE) as i32;
7977 let post_assign_to_set = (on
7978 & (PM_READONLY
7979 | PM_EXPORTED
7980 | PM_LEFT
7981 | PM_RIGHT_B
7982 | PM_RIGHT_Z
7983 | PM_TAGGED
7984 | PM_HIDE
7985 | PM_HIDEVAL
7986 | PM_UNIQUE)) as i32;
7987 if post_assign_to_set != 0 {
7988 // c:2289 — `pm->node.flags = (pm->node.flags | (on & ...)) & ~off`.
7989 // This is a MERGE: OR in the requested attributes and clear
7990 // the ones explicitly turned off, but PRESERVE existing
7991 // attributes the user did not touch. The previous
7992 // `(flags & !post_assign_mask) | to_set` cleared EVERY mask
7993 // bit, so `typeset -Z 3 x=7; typeset -r x=ro` lost the -Z
7994 // (RIGHT_Z is in the mask but not in `on`), yielding
7995 // `scalar-readonly` where zsh keeps `scalar-right_zeros-readonly`.
7996 let post_assign_to_clear = (off as u32 & post_assign_mask as u32) as i32;
7997 if let Ok(mut tab) = paramtab().write() {
7998 if let Some(pm) = tab.get_mut(n) {
7999 pm.node.flags =
8000 (pm.node.flags | post_assign_to_set) & !post_assign_to_clear;
8001 }
8002 }
8003 }
8004 // c:1973-1989 (Src/builtin.c, inside typeset_single)
8005 // — `if (arg) { int base = zstrtol(arg, ...) ;
8006 // pm->base = base; }`. The precision arg from `-i N`,
8007 // `-E N`, `-F N` (parsed by execbuiltin as
8008 // ops.args[<F-arg-slot>]) lands on the param's `base`
8009 // field, which convfloat reads as the format-digit
8010 // count. Without this stamp, `typeset -F 2 x=3.14`
8011 // ignored the `2` and printed at the default
8012 // 10-digit precision.
8013 {
8014 let prec_arg: Option<&str> = if (on & PM_INTEGER) != 0 && OPT_HASARG(&ops, b'i')
8015 {
8016 OPT_ARG(&ops, b'i')
8017 } else if (on & PM_EFLOAT) != 0 && OPT_HASARG(&ops, b'E') {
8018 OPT_ARG(&ops, b'E')
8019 } else if (on & PM_FFLOAT) != 0 && OPT_HASARG(&ops, b'F') {
8020 OPT_ARG(&ops, b'F')
8021 } else {
8022 None
8023 };
8024 if let Some(s) = prec_arg {
8025 if let Ok(b) = s.trim().parse::<i32>() {
8026 if let Ok(mut tab) = paramtab().write() {
8027 if let Some(pm) = tab.get_mut(n) {
8028 pm.base = b;
8029 }
8030 }
8031 }
8032 }
8033 }
8034 // c:2009-2014 typeset_setwidth — `-L N` / `-R N` / `-Z N`
8035 // install pm.width = N. The auto-fallback in assignsparam
8036 // (PM_INTEGER block at params.rs:3619) stamps width to
8037 // s.len() when PM_RIGHT_Z is set but width==0; that lands
8038 // BEFORE the post-assign PM_RIGHT_Z stamp here, so the
8039 // user's explicit `-Z 6` was being overwritten by the
8040 // value's char count. Set width AFTER setsparam so the
8041 // explicit option arg wins over the auto-fallback.
8042 {
8043 let width_arg: Option<&str> =
8044 if (on as u32 & PM_LEFT) != 0 && OPT_HASARG(&ops, b'L') {
8045 OPT_ARG(&ops, b'L')
8046 } else if (on as u32 & PM_RIGHT_B) != 0 && OPT_HASARG(&ops, b'R') {
8047 OPT_ARG(&ops, b'R')
8048 } else if (on as u32 & PM_RIGHT_Z) != 0 && OPT_HASARG(&ops, b'Z') {
8049 OPT_ARG(&ops, b'Z')
8050 } else {
8051 None
8052 };
8053 if let Some(s) = width_arg {
8054 if let Ok(w) = s.trim().parse::<i32>() {
8055 if let Ok(mut tab) = paramtab().write() {
8056 if let Some(pm) = tab.get_mut(n) {
8057 pm.width = w;
8058 }
8059 }
8060 }
8061 }
8062 }
8063 // c:Src/params.c:3024 addenv — only mirror to OS env
8064 // when PM_EXPORTED is in flags or already-exported.
8065 // c:2306 — `if (!(pm->node.flags & (PM_ARRAY|PM_HASHED)))`
8066 // guards the whole addenv/delenv block: an array/hashed param
8067 // never gets a scalar env entry of its own (a TIED array
8068 // reaches the environment through its scalar partner via
8069 // `arrfixenv`, Src/params.c). The lenient scalar-to-array arm
8070 // above must not write `path=/a/b` into the environment
8071 // alongside the tied `PATH`.
8072 let already_exported = env::var_os(n).is_some();
8073 if !pm_is_arraylike && ((on & PM_EXPORTED) != 0 || already_exported) {
8074 env::set_var(n, &env_val); // c:3024 addenv (value via copyenvstr, c:5434)
8075 }
8076 }
8077 } else if is_hashed || is_array {
8078 // c:Src/builtin.c:2117-2119 — `chflags = ((off & pm->node.flags) |
8079 // (on & ~pm->node.flags)) & (…|PM_HASHED|PM_ARRAY|…); tc = chflags
8080 // && chflags != (PM_EFLOAT|PM_FFLOAT);`. Re-declaring an existing
8081 // association with `-a` turns PM_ARRAY on where the node has it
8082 // off, so `tc` is set and c:2351-2374 runs `unsetparam_pm(pm, 0,
8083 // 1)` on the old parameter before the new one is built. That
8084 // reaches `stdunsetfn`'s PM_HASHED arm (c:Src/params.c:3922-3925),
8085 // whose `pm->gsu.h->setfn(pm, NULL)` is `hashsetfn`
8086 // (c:Src/params.c:4045-4049) — `deleteparamtable(pm->u.hash)`.
8087 // Re-declaring with `-A` leaves `on & ~pm->node.flags` empty, so
8088 // `tc` stays 0 and the pairs survive; that is why only the
8089 // away-from-association direction discards.
8090 //
8091 // !!! WARNING: RUST-ONLY HAZARD (the structures do NOT match) !!!
8092 // In C the pairs live INSIDE the Param (`pm->u.hash`), so dropping
8093 // the Param inherently drops them. zshrs keeps them in the
8094 // parallel `paramtab_hashed_storage` map keyed by NAME, with no
8095 // type and no scope dimension, so flipping the node's type bits
8096 // left the row in place and every SUBSCRIPTED read kept resolving
8097 // through it:
8098 // typeset -A H; H[x]=1; typeset -a H; H=(p q)
8099 // answered `${H[1]}` / `${H[-1]}` / `${H[1,2]}` empty and
8100 // `${(k)H}` / `${(v)H}` with the dead `x` / `1` pair, while
8101 // `${(t)H}`, `${#H}`, `${H[@]}` and plain `$H` were already
8102 // correct — the array was stored, it just could not be read back
8103 // by index. The row therefore has to be discarded by hand here.
8104 if is_array && !is_hashed {
8105 let was_hashed = paramtab()
8106 .read()
8107 .ok()
8108 .and_then(|t| t.get(arg).map(|pm| PM_TYPE(pm.node.flags as u32) == PM_HASHED))
8109 .unwrap_or(false);
8110 if was_hashed {
8111 // c:Src/params.c:4048 deleteparamtable(pm->u.hash)
8112 crate::ported::exec::unset_assoc(arg);
8113 if let Ok(mut m) = crate::ported::params::paramtab_hashed_storage().lock() {
8114 m.remove(arg);
8115 }
8116 }
8117 }
8118 // c:3060-3070 — bare name + `-A`/`-a` declares an empty
8119 // assoc/array.
8120 if is_hashed {
8121 // c:2521 — C just calls `createparam(pname, on & ~PM_READONLY)`
8122 // here; it never installs an empty hash of its own. For the
8123 // newspecial shadow (c:2386-2425) the preserved Param keeps
8124 // pointing at the SPECIAL's table, so `local -A +h commands`
8125 // still enumerates the live command hash. zshrs's magic module
8126 // hashes (Src/Modules/parameter.c partab[]) are reached by
8127 // FALLING THROUGH `paramtab_hashed_storage` to the row's
8128 // scanfn (params.rs gethkparam), so materialising an empty bag
8129 // for one of them is not "an empty local assoc", it is a
8130 // blanked special: `$#commands` read 0 inside `_command_names`
8131 // and `compadd -k commands` offered nothing.
8132 let preserved_special_hash = paramtab()
8133 .read()
8134 .ok()
8135 .and_then(|t| {
8136 t.get(arg)
8137 .map(|pm| (pm.node.flags as u32 & PM_SPECIAL) != 0)
8138 })
8139 .unwrap_or(false)
8140 && crate::ported::modules::parameter::PARTAB
8141 .iter()
8142 .any(|e| e.name == arg);
8143 if !preserved_special_hash && crate::ported::exec::assoc(arg).is_none() {
8144 // !!! RUST-ONLY: the empty bag is a declaration, not an
8145 // assignment. set_assoc routes through sethparam, whose
8146 // `flags &= ~PM_DEFAULTED` (c:Src/params.c:3590) would turn
8147 // a TYPESET_TO_UNSET `typeset -A h` into a set, empty hash;
8148 // C's createparam (c:2521) never assigns. Carry the
8149 // declaration's PM_DEFAULTED bits across the store.
8150 let defaulted = paramtab()
8151 .read()
8152 .ok()
8153 .and_then(|t| t.get(arg).map(|pm| pm.node.flags & PM_DEFAULTED as i32))
8154 .unwrap_or(0);
8155 crate::ported::exec::set_assoc(arg, IndexMap::new());
8156 if defaulted != 0 {
8157 if let Ok(mut t) = paramtab().write() {
8158 if let Some(pm) = t.get_mut(arg) {
8159 pm.node.flags |= defaulted;
8160 }
8161 }
8162 }
8163 }
8164 } else if crate::ported::exec::array(arg).is_none() {
8165 crate::ported::exec::set_array(arg, Vec::new());
8166 }
8167 // c:Src/params.c:4087 arrsetfn — when PM_UNIQUE is set on
8168 // an existing array, the canonical setfn applies
8169 // `uniqarray()` to the current contents. `typeset -aU arr`
8170 // on an existing arr must dedupe in place; without this,
8171 // the flag stamp lands on pm.flags but the value stays
8172 // un-deduped until the next assignment.
8173 if is_array && (on as u32 & PM_UNIQUE) != 0 {
8174 let current = crate::ported::exec::array(arg).unwrap_or_default();
8175 // simple_arrayuniq is the in-place dedupe used by
8176 // params.rs arrsetfn (PM_UNIQUE path).
8177 let deduped = {
8178 let mut seen = std::collections::HashSet::new();
8179 current
8180 .into_iter()
8181 .filter(|x| seen.insert(x.clone()))
8182 .collect::<Vec<_>>()
8183 };
8184 crate::ported::exec::set_array(arg, deduped);
8185 }
8186 // Stamp attribute bits on paramtab entry — same set as
8187 // the `name=value` post-assign mask.
8188 let post_assign_mask = (PM_READONLY
8189 | PM_EXPORTED
8190 | PM_LEFT
8191 | PM_RIGHT_B
8192 | PM_RIGHT_Z
8193 | PM_TAGGED
8194 | PM_HIDE
8195 | PM_HIDEVAL
8196 | PM_UNIQUE
8197 | PM_HASHED
8198 | PM_ARRAY) as i32;
8199 let post_assign_to_set = (on
8200 & (PM_READONLY
8201 | PM_EXPORTED
8202 | PM_LEFT
8203 | PM_RIGHT_B
8204 | PM_RIGHT_Z
8205 | PM_TAGGED
8206 | PM_HIDE
8207 | PM_HIDEVAL
8208 | PM_UNIQUE
8209 | PM_HASHED
8210 | PM_ARRAY)) as i32;
8211 if post_assign_to_set != 0 {
8212 if let Ok(mut tab) = paramtab().write() {
8213 if let Some(pm) = tab.get_mut(arg_name) {
8214 // c:2289 MERGE — preserve attributes the user did not touch
8215 // (e.g. `typeset -L 4 x=hi; typeset -r x` keeps -L). `off` bits
8216 // are cleared separately below.
8217 pm.node.flags |= post_assign_to_set;
8218 }
8219 }
8220 }
8221 // c:Src/builtin.c:2289 — `pm->node.flags = ... & ~off`.
8222 // `off` bits in post_assign_mask must clear regardless of
8223 // `on` (e.g. `typeset +U arr=(...)` where on=0,
8224 // off=PM_UNIQUE). The post_assign_to_set gate above
8225 // short-circuits the on=0 case. Bug #234 in docs/BUGS.md.
8226 let off_in_mask = (off as i32) & post_assign_mask;
8227 if off_in_mask != 0 {
8228 if let Ok(mut tab) = paramtab().write() {
8229 if let Some(pm) = tab.get_mut(arg_name) {
8230 pm.node.flags &= !off_in_mask;
8231 }
8232 }
8233 }
8234 } else {
8235 // c:2254-2272 — `if ((on & PM_UNIQUE) && !(pm->node.flags &
8236 // PM_READONLY & ~off)) { … } else if (PM_TYPE(pm->node.flags)
8237 // == PM_SCALAR && pm->ename &&
8238 // (apm = (Param) paramtab->getnode(paramtab, pm->ename))) {
8239 // x = (*apm->gsu.a->getfn)(apm);
8240 // uniqarray(x);
8241 // if (x) arrfixenv(pm->node.nam, x);
8242 // }`
8243 // The PM_ARRAY half of that block is handled in the `is_array` arm
8244 // above; this is the TIED-SCALAR half. `typeset -T SCALAR=l:o:c:a:l
8245 // array; typeset -U SCALAR` must dedupe the partner ARRAY in place
8246 // (zsh: `SCALAR=l:o:c:a`), which zshrs never did because the flag
8247 // stamp landed on the scalar and nothing touched the peer.
8248 if (on as u32 & PM_UNIQUE) != 0 {
8249 let peer: Option<(String, Vec<String>)> = paramtab()
8250 .read()
8251 .ok()
8252 .and_then(|t| t.get(arg.as_str()).cloned())
8253 .filter(|pm| {
8254 // c:2254 — `!(pm->node.flags & PM_READONLY & ~off)`
8255 (pm.node.flags as u32 & PM_READONLY & !(off as u32)) == 0
8256 // c:2263 — `PM_TYPE(pm->node.flags) == PM_SCALAR`
8257 && PM_TYPE(pm.node.flags as u32) == PM_SCALAR
8258 })
8259 .and_then(|pm| pm.ename.clone()) // c:2263 `pm->ename`
8260 .and_then(|ename| {
8261 paramtab() // c:2265 getnode(paramtab, pm->ename)
8262 .read()
8263 .ok()
8264 .and_then(|t| {
8265 t.get(&ename).map(|apm| (ename.clone(), apm.u_arr.clone()))
8266 })
8267 .and_then(|(n, a)| a.map(|a| (n, a))) // c:2266 getfn
8268 });
8269 if let Some((ename, x)) = peer {
8270 let x = crate::ported::params::uniqarray(x); // c:2267
8271 if let Ok(mut tab) = paramtab().write() {
8272 if let Some(apm) = tab.get_mut(&ename) {
8273 apm.u_arr = Some(x.clone());
8274 }
8275 // The tied scalar caches the joined string in u_str;
8276 // refresh it so `$SCALAR` reads the deduped join
8277 // without waiting for the next write.
8278 if let Some(spm) = tab.get_mut(arg.as_str()) {
8279 let sep = match spm.u_tied.as_deref() {
8280 Some(td) if td.joinchar == 0 => "\0".to_string(),
8281 Some(td) => ((td.joinchar as u8) as char).to_string(),
8282 None => ":".to_string(),
8283 };
8284 spm.u_str = Some(x.join(&sep));
8285 }
8286 }
8287 crate::ported::params::arrfixenv(arg.as_str(), Some(&x)); // c:2269
8288 }
8289 }
8290 // c:2355-2378 (typeset_single tc branch) — bare `typeset -i n`
8291 // / `-F n` / `-E n` / `-l n` / `-u n` / `-r n` / `export N`
8292 // / `readonly N` converts/stamps the existing param. Split
8293 // into pre-assign (type conversion) and post-assign
8294 // (attribute stamp) the same way the `name=value` arm does.
8295 let pre_assign_mask =
8296 (PM_INTEGER | PM_EFLOAT | PM_FFLOAT | PM_LOWER | PM_UPPER | PM_NAMEREF) as i32;
8297 let pre_assign_to_set = (on
8298 & (PM_INTEGER | PM_EFLOAT | PM_FFLOAT | PM_LOWER | PM_UPPER | PM_NAMEREF))
8299 as i32;
8300 let post_assign_mask = (PM_READONLY
8301 | PM_EXPORTED
8302 | PM_LEFT
8303 | PM_RIGHT_B
8304 | PM_RIGHT_Z
8305 | PM_TAGGED
8306 | PM_HIDE
8307 | PM_HIDEVAL
8308 | PM_UNIQUE) as i32;
8309 let post_assign_to_set = (on
8310 & (PM_READONLY
8311 | PM_EXPORTED
8312 | PM_LEFT
8313 | PM_RIGHT_B
8314 | PM_RIGHT_Z
8315 | PM_TAGGED
8316 | PM_HIDE
8317 | PM_HIDEVAL
8318 | PM_UNIQUE)) as i32;
8319 // c:2374 — `s = ztrdup(getsparam(pname));`. Capture the
8320 // pre-conversion scalar value so the re-assignment after
8321 // type flip preserves it through the new setfn.
8322 // `typeset +n ref`: the stored REFNAME (u_str raw) becomes
8323 // the scalar value — getsparam would deref through the
8324 // chain and return the target's value instead (c:2374 via
8325 // the +n type-conversion arm; K01 "remove nameref
8326 // attribute" expects `typeset ptr=var`).
8327 // c:Src/builtin.c:2062-2064 — C computes `usepm` at the TOP of
8328 // typeset_single, BEFORE the parameter is touched, and the bare-name
8329 // print at c:2246 lives inside that branch:
8330 // usepm = pm && (!(pm->node.flags & PM_UNSET) || OPT_ISSET(ops,'p') ||
8331 // (isset(POSIXBUILTINS) &&
8332 // (pm->node.flags & (PM_READONLY|PM_EXPORTED))));
8333 // "Here we just avoid using it for the present tests if it's unset."
8334 // So `typeset NAME` on a declared-but-UNSET param prints NOTHING.
8335 // The snapshot MUST be taken here: later in this same iteration the
8336 // re-declaration clears PM_UNSET, so reading the flag at the print
8337 // site always saw it already cleared. TYPESET_TO_UNSET produces
8338 // exactly this state (PM_DEFAULTED = PM_DECLARED|PM_UNSET,
8339 // zsh.h:1934). Bug #1056.
8340 let usepm_at_entry = paramtab()
8341 .read()
8342 .ok()
8343 .and_then(|t| t.get(arg.as_str()).map(|p| p.node.flags as u32))
8344 .map(|f| {
8345 (f & PM_UNSET) == 0
8346 || (f & PM_DECLARED) != 0 // c:2059 (55020)
8347 || (isset(crate::ported::zsh_h::POSIXBUILTINS)
8348 && (f & (PM_READONLY | PM_EXPORTED)) != 0)
8349 })
8350 .unwrap_or(true);
8351 // c:2194-2206 — POSIXBUILTINS applies stricter readonly rules:
8352 // if ((on & (PM_READONLY|PM_EXPORTED)) &&
8353 // (!usepm || (pm->node.flags & PM_UNSET)) && !ASG_VALUEP(asg))
8354 // on |= PM_UNSET;
8355 // else if (usepm && (pm->node.flags & PM_READONLY) &&
8356 // !(on & PM_READONLY) && func != BIN_EXPORT) {
8357 // zerr("read-only variable: %s", pm->node.nam);
8358 // return NULL;
8359 // }
8360 // So `readonly NAME` leaves NAME unset, and `typeset +r NAME` on it
8361 // is refused.
8362 let entry_flags: Option<u32> = paramtab()
8363 .read()
8364 .ok()
8365 .and_then(|t| t.get(arg.as_str()).map(|p| p.node.flags as u32));
8366 let posix_strict = isset(POSIXBUILTINS) && !OPT_ISSET(&ops, b'p'); // c:2194
8367 let usepm_entry = entry_flags.is_some() && usepm_at_entry;
8368 let posix_keep_unset = posix_strict
8369 && (on as u32 & (PM_READONLY | PM_EXPORTED)) != 0
8370 && (!usepm_entry || entry_flags.is_some_and(|f| (f & PM_UNSET) != 0)); // c:2198-2201
8371 // c:2294-2296 (55020) — `off |= PM_UNSET` is skipped for a
8372 // PM_DECLARED parameter, so changing only the flags of a
8373 // TYPESET_TO_UNSET declaration (`typeset -a h; typeset -U h`)
8374 // leaves it unset.
8375 let keep_declared_unset = usepm_entry
8376 && entry_flags
8377 .is_some_and(|f| (f & (PM_DECLARED | PM_UNSET)) == (PM_DECLARED | PM_UNSET));
8378 if posix_strict
8379 && !posix_keep_unset
8380 && usepm_entry
8381 && entry_flags.is_some_and(|f| (f & PM_READONLY) != 0)
8382 && (on as u32 & PM_READONLY) == 0
8383 && func != BIN_EXPORT
8384 {
8385 zerr(&format!("read-only variable: {}", arg)); // c:2204
8386 returnval = 1;
8387 continue; // c:2205 return NULL
8388 }
8389 // c:3104-3106 — bin_typeset fetches the node with getnode2 "to
8390 // avoid autoloading", so a PM_AUTOLOAD stub is reused as-is
8391 // (its u.str is the module name) and its module stays unloaded.
8392 let autoload_stub = entry_flags.is_some_and(|f| (f & PM_AUTOLOAD) != 0);
8393 let saved_val =
8394 if autoload_stub
8395 || ((off as u32 & PM_NAMEREF) != 0 && crate::ported::params::is_nameref(arg))
8396 {
8397 paramtab()
8398 .read()
8399 .ok()
8400 .and_then(|t| t.get(arg.as_str()).and_then(|p| p.u_str.clone()))
8401 } else {
8402 getsparam(arg)
8403 };
8404 // c:Src/builtin.c:3072 — `if (!getsparam(pname))
8405 // setsparam(pname, "");`. C zsh's getsparam returns the
8406 // join-of-values for PM_HASHED and PM_ARRAY so this gate
8407 // fires only for truly-undeclared names. zshrs's getsparam
8408 // returns None for assoc/array (it routes through the
8409 // PM_SCALAR / PM_INTEGER / PM_*FLOAT arms only), so this
8410 // branch falsely fired for an existing hash AND triggered
8411 // assignsparam's PM_HASHED→PM_SCALAR coerce at params.rs:
8412 // 5368-5374 which removes the entry from
8413 // paramtab_hashed_storage. Bug #218 in docs/BUGS.md.
8414 //
8415 // Gate on the paramtab flag instead: if the param exists
8416 // and is PM_HASHED/PM_ARRAY, treat it as already-declared
8417 // even though getsparam returned None.
8418 let already_typed = paramtab()
8419 .read()
8420 .ok()
8421 .and_then(|t| {
8422 t.get(arg).map(|pm| {
8423 let f = pm.node.flags as u32;
8424 let typ = PM_TYPE(f);
8425 typ == PM_HASHED || typ == PM_ARRAY
8426 })
8427 })
8428 .unwrap_or(false);
8429 // Capture the param's PRIOR numeric class (before the flag
8430 // change below) so the base-stamp arm can reset pm.base when
8431 // switching BETWEEN integer and float — the base field means
8432 // radix for integers but precision for floats, so a leftover
8433 // value leaks (float precision 3 → integer base 3 = "3#10";
8434 // int base 16 → float 16-digit precision). E↔F and same-type
8435 // re-declares keep it (both float, base=precision). zsh 5.9.1
8436 // verified.
8437 let (prior_is_integer, prior_is_float) = paramtab()
8438 .read()
8439 .ok()
8440 .and_then(|t| {
8441 t.get(arg).map(|pm| {
8442 let f = pm.node.flags as u32;
8443 ((f & PM_INTEGER) != 0, (f & (PM_EFLOAT | PM_FFLOAT)) != 0)
8444 })
8445 })
8446 .unwrap_or((false, false));
8447 // c:2058-2060 — under POSIXBUILTINS an UNSET readonly/exported
8448 // parameter is still reused (`usepm`), and the reuse arm never
8449 // assigns; creating it here would trip its own readonly bit.
8450 let was_fresh = saved_val.is_none()
8451 && !already_typed
8452 && !(posix_keep_unset && usepm_entry)
8453 && !keep_declared_unset; // c:2059 (55020): a declared pm is reused, not assigned
8454 if was_fresh {
8455 // c:3072 — `if (!getsparam(pname)) setsparam(pname, "")`.
8456 // flags=0: a typeset-driven create never trips
8457 // WARN_CREATE_GLOBAL (typeset_single assigns with
8458 // `assignsparam(pname, ..., 0)`, c:2322).
8459 crate::ported::params::assignsparam(arg, "", 0); // c:3074
8460 // c:Src/builtin.c:2544 — `if (isset(TYPESETTOUNSET))
8461 // pm->node.flags |= PM_DEFAULTED;`. Under
8462 // `setopt typeset_to_unset`, bare `typeset NAME` (no
8463 // `=`) creates a "declared but not assigned" entry:
8464 // PM_DECLARED + PM_UNSET (= PM_DEFAULTED). `${+NAME}`
8465 // and `[[ -v NAME ]]` then return false until an
8466 // explicit assignment clears PM_UNSET. Bug #280 in
8467 // docs/BUGS.md.
8468 if isset(crate::ported::zsh_h::TYPESETTOUNSET) {
8469 if let Ok(mut tab) = paramtab().write() {
8470 if let Some(pm) = tab.get_mut(arg) {
8471 pm.node.flags |= PM_DEFAULTED as i32;
8472 }
8473 }
8474 }
8475 let _ = was_fresh;
8476 }
8477 // c:Src/builtin.c:2117-2131 — `typeset +a`/`+A` removes the
8478 // array/hashed attribute, which is a TYPE CHANGE (`chflags` picks
8479 // up `off & PM_ARRAY`, `tc=1`, `usepm=0`). UNLIKE the +i/+E/+l
8480 // conversions below — which migrate the stored value back to a
8481 // scalar — an array/assoc has no scalar representation, so the
8482 // recreated scalar is EMPTY. `typeset -a a=(1 2 3); typeset +a a`
8483 // → `a` is a scalar with value "". Only fires when the param is
8484 // ACTUALLY array/hashed (a no-op `+a` on a scalar leaves it be).
8485 // Bug #1029.
8486 if (off as u32 & (PM_ARRAY | PM_HASHED)) != 0 {
8487 let is_arraylike = paramtab().read().ok().and_then(|t| {
8488 t.get(arg).map(|pm| {
8489 let typ = PM_TYPE(pm.node.flags as u32);
8490 typ == PM_ARRAY || typ == PM_HASHED
8491 })
8492 }) == Some(true);
8493 if is_arraylike {
8494 // The assoc backing row goes with it. This used to be
8495 // open-coded here ("unsetparam alone can leave the
8496 // paramtab_hashed_storage entry, so a later scalar deref
8497 // still saw the joined values") because `stdunsetfn`'s
8498 // PM_HASHED arm only cleared `pm.u_hash`, which nothing
8499 // populates. That arm now does what C's does — c:3922-3925
8500 // → `hashsetfn` c:4047 `deleteparamtable(pm->u.hash)` — so
8501 // `unsetparam` drops the row on the way through, the same
8502 // way C's does, and the hand-rolled copy is gone.
8503 crate::ported::params::unsetparam(arg);
8504 crate::ported::params::assignsparam(arg, "", 0); // empty scalar
8505 }
8506 }
8507 // c:Src/builtin.c::typeset_single c:2374-2378 — the `+i`
8508 // / `+E` / `+F` / `+l` / `+u` / `+r` / `+n` paths
8509 // REMOVE the corresponding PM_* flag and migrate the
8510 // stored value back to u_str. C captures
8511 // `s = ztrdup(getsparam(pname))` BEFORE the type-conversion
8512 // unset and restores it after. Without this branch,
8513 // `typeset +i n` (where n was an integer with value 42)
8514 // cleared u_str without copying the integer value back —
8515 // result was `typeset n=''`. Bug #326 in docs/BUGS.md.
8516 //
8517 // ONLY a real type change migrates the value. C decides that with
8518 // `chflags` (c:2117-2119):
8519 // chflags = ((off & pm->flags) | (on & ~pm->flags)) &
8520 // (PM_INTEGER|PM_EFLOAT|PM_FFLOAT|PM_HASHED|
8521 // PM_ARRAY|PM_TIED|PM_AUTOLOAD);
8522 // tc = chflags && chflags != (PM_EFLOAT|PM_FFLOAT);
8523 // PM_LOWER / PM_UPPER are absent from that mask, so `typeset -l v`
8524 // / `typeset +l v` leave `tc == 0` and fall into the `usepm` arm at
8525 // c:2280, which is a pure flag flip:
8526 // pm->node.flags = (pm->node.flags | (on & ~PM_READONLY)) & ~off;
8527 // The stored string is never re-read and never rewritten. Migrating
8528 // it here re-stored the case-FOLDED read (getsparam applies the
8529 // substitution-time fold), so removing the attribute could not
8530 // restore the original text.
8531 //
8532 // The `& pm->flags` / `& ~pm->flags` halves matter as much as the
8533 // mask: only bits that actually CHANGE count. `typeset +i v` on a
8534 // param that was never an integer leaves `off & pm->flags == 0`,
8535 // so `tc == 0` and the value is left alone. PM_NAMEREF is a
8536 // zshrs-local addition to the mask (the `+n` conversion at
8537 // builtin.rs restores the stored refname); C keeps namerefs out of
8538 // chflags, so this stays flagged as a deviation.
8539 const TYPE_CONV_BITS: u32 = PM_INTEGER | PM_EFLOAT | PM_FFLOAT | PM_NAMEREF; // c:2117
8540 let cur_flags = paramtab()
8541 .read()
8542 .ok()
8543 .and_then(|t| t.get(arg.as_str()).map(|p| p.node.flags as u32))
8544 .unwrap_or(0);
8545 // c:2117-2119 — chflags, then `tc = chflags && chflags != (PM_EFLOAT|PM_FFLOAT)`
8546 // (a plain E↔F swap keeps the parameter, so it is not a conversion).
8547 let chflags = ((off & cur_flags) | (on & !cur_flags)) & TYPE_CONV_BITS;
8548 let tc = chflags != 0 && chflags != (PM_EFLOAT | PM_FFLOAT); // c:2118
8549 // c:2362-2369 — "Try to carry over a value, but not when changing
8550 // from, to, or between non-scalar types":
8551 // if (!ASG_VALUEP(asg) && !((pm->node.flags|on) & (PM_ARRAY|PM_HASHED)))
8552 // asg->value.scalar = dupstring(getsparam(pname));
8553 // `typeset -a a=(1 2); typeset -i a` therefore starts the integer at
8554 // 0. getsparam here joins an array, so re-storing it unconditionally
8555 // fed `1 2` to the integer setfn: "bad math expression".
8556 // `pm->node.flags` is the type at ENTRY: by this point the `+a`/`+A`
8557 // arm above has already replaced an array with an empty scalar
8558 // (`-i` puts PM_ARRAY in `off`), so `cur_flags` no longer shows it.
8559 // `already_typed` was read before that arm ran.
8560 let carry = !already_typed && (on & (PM_ARRAY | PM_HASHED)) == 0;
8561 // c:2351-2359 — a type conversion recreates the parameter:
8562 // /* Maintain existing readonly/exported status... */
8563 // on |= ~off & (PM_READONLY|PM_EXPORTED) & pm->node.flags;
8564 // /* ...but turn off existing readonly so we can delete it */
8565 // pm->node.flags &= ~PM_READONLY;
8566 // and createparam(pname, on & ~PM_READONLY) (c:2518) gets the
8567 // carried value before the readonly bit returns. Without the
8568 // clear, the value migration below hit the readonly guard:
8569 // `typeset -r x=ro; typeset -F x` said "read-only variable: x"
8570 // where zsh converts it to a readonly float.
8571 let tc_keeps_readonly = tc
8572 && !OPT_ISSET(&ops, b'p')
8573 && (cur_flags & PM_READONLY) != 0
8574 && (off & PM_READONLY) == 0; // c:2357
8575 if tc && !OPT_ISSET(&ops, b'p') && (cur_flags & PM_READONLY) != 0 {
8576 if let Ok(mut tab) = paramtab().write() {
8577 if let Some(pm) = tab.get_mut(arg.as_str()) {
8578 pm.node.flags &= !(PM_READONLY as i32); // c:2359
8579 }
8580 }
8581 }
8582 let pre_assign_to_clear = (off
8583 & (PM_INTEGER | PM_EFLOAT | PM_FFLOAT | PM_LOWER | PM_UPPER | PM_NAMEREF))
8584 as i32;
8585 if pre_assign_to_clear != 0 {
8586 if let Ok(mut tab) = paramtab().write() {
8587 if let Some(pm) = tab.get_mut(arg.as_str()) {
8588 pm.node.flags &= !pre_assign_to_clear;
8589 // `+n` — drop the nameref scope/subscript info
8590 // along with the flag (c:2374-2378 conversion).
8591 if (off as u32 & PM_NAMEREF) != 0 {
8592 pm.width = 0;
8593 pm.base = 0;
8594 }
8595 }
8596 }
8597 // Restore the captured scalar so the cleared-type
8598 // param has its value as a string (PM_SCALAR
8599 // semantics now apply). flags=0 — typeset-internal
8600 // restore, never WARN_CREATE_GLOBAL (c:2322).
8601 if tc && carry {
8602 if let Some(ref val) = saved_val {
8603 crate::ported::params::assignsparam(arg, val, 0);
8604 }
8605 }
8606 }
8607 if pre_assign_to_set != 0 {
8608 if let Ok(mut tab) = paramtab().write() {
8609 if let Some(pm) = tab.get_mut(arg) {
8610 // c:2280 — a pure case-attribute change keeps every
8611 // other flag; only a type change rewrites the type
8612 // bits. Masking the whole pre-assign set on `-l`/`-u`
8613 // would silently drop PM_INTEGER from an integer param.
8614 let clear_mask = if tc {
8615 pre_assign_mask
8616 } else {
8617 (PM_LOWER | PM_UPPER) as i32
8618 };
8619 pm.node.flags = (pm.node.flags & !clear_mask) | pre_assign_to_set;
8620 }
8621 }
8622 // c:2372-2378 — re-assign saved value through new type's
8623 // setfn so u_val (for PM_INTEGER) or u_dval (for PM_*FLOAT)
8624 // catches the value migration from u_str.
8625 if tc && carry {
8626 if let Some(ref val) = saved_val {
8627 setsparam(arg, val);
8628 }
8629 }
8630 }
8631 if tc_keeps_readonly {
8632 // c:2357 — the kept readonly bit is part of `on` for the
8633 // recreated parameter, applied once its value is in.
8634 if let Ok(mut tab) = paramtab().write() {
8635 if let Some(pm) = tab.get_mut(arg.as_str()) {
8636 pm.node.flags |= PM_READONLY as i32;
8637 }
8638 }
8639 }
8640 // c:1973-1989 (Src/builtin.c, inside typeset_single):
8641 // if (arg) {
8642 // int base = zstrtol(arg, ..., 10);
8643 // pm->base = base;
8644 // }
8645 // The precision arg from `-i N`, `-E N`, `-F N` (parsed
8646 // by execbuiltin as `ops.args[<F-arg-slot>]`) lands on
8647 // the param's `base` field, which `convfloat` (c:5689 in
8648 // params.c) reads as the format-digit count. The
8649 // `name=value` arm above (c:2009-2014 typeset_setwidth
8650 // companion path) already stamps `pm.base`; this bare-
8651 // declare arm was missing it, so `typeset -F 4 f` left
8652 // pm.base=0 and `convfloat` rendered with the default
8653 // 10-digit precision (parity bug #29). With this stamp
8654 // a subsequent `(( f = ... ))` re-assignment preserves
8655 // the `pm.base = 4` set here through `assignnparam`'s
8656 // re-assign path (params.rs:5340-5374, c:2874-2878).
8657 {
8658 let prec_arg: Option<&str> = if (on & PM_INTEGER) != 0 && OPT_HASARG(&ops, b'i') {
8659 OPT_ARG(&ops, b'i') // c:1974 -i N
8660 } else if (on & PM_EFLOAT) != 0 && OPT_HASARG(&ops, b'E') {
8661 OPT_ARG(&ops, b'E') // c:1977 -E N
8662 } else if (on & PM_FFLOAT) != 0 && OPT_HASARG(&ops, b'F') {
8663 OPT_ARG(&ops, b'F') // c:1980 -F N
8664 } else {
8665 None
8666 };
8667 if let Some(s) = prec_arg {
8668 if let Ok(b) = s.trim().parse::<i32>() {
8669 // c:1985 zstrtol
8670 if let Ok(mut tab) = paramtab().write() {
8671 if let Some(pm) = tab.get_mut(arg) {
8672 pm.base = b; // c:1987 pm->base = base
8673 }
8674 }
8675 }
8676 } else {
8677 // No explicit base/precision arg. When an EXISTING
8678 // numeric param switches BETWEEN integer and float,
8679 // reset pm.base to the default (0) — the shared base
8680 // field would otherwise leak the old type's value
8681 // (createparam's base default is 0, c:Src/params.c:1155;
8682 // a numeric type change re-creates the representation).
8683 // Gated so E↔F and same-type re-declares keep their
8684 // base/precision (`typeset -i16 f; typeset -i f` stays
8685 // base 16). zsh 5.9.1 verified.
8686 let switching_int_float = ((on & PM_INTEGER) != 0 && prior_is_float)
8687 || ((on & (PM_EFLOAT | PM_FFLOAT)) != 0 && prior_is_integer);
8688 if !was_fresh && switching_int_float {
8689 if let Ok(mut tab) = paramtab().write() {
8690 if let Some(pm) = tab.get_mut(arg) {
8691 pm.base = 0;
8692 }
8693 }
8694 }
8695 }
8696 }
8697 // c:2528-2533 — the createparam (valueless declare) branch
8698 // must ALSO stamp the `-L`/`-R`/`-Z` field width
8699 // (typeset_setwidth, c:2002-2011). The base block above
8700 // handled `-i`/`-E`/`-F` precision but the width was dropped,
8701 // so `typeset -L 10 f` (declare, then assign later) left
8702 // pm.width=0 and the value never left/right-justified — and
8703 // `typeset -p` rendered `-FL` with no number. The inline-value
8704 // arm (the `name=value` path) already stamps width; only this
8705 // bare-declare arm was missing it.
8706 {
8707 let width_arg: Option<&str> = if (on & PM_LEFT) != 0 && OPT_HASARG(&ops, b'L') {
8708 OPT_ARG(&ops, b'L') // c:2003
8709 } else if (on & PM_RIGHT_B) != 0 && OPT_HASARG(&ops, b'R') {
8710 OPT_ARG(&ops, b'R') // c:2005
8711 } else if (on & PM_RIGHT_Z) != 0 && OPT_HASARG(&ops, b'Z') {
8712 OPT_ARG(&ops, b'Z') // c:2007
8713 } else {
8714 None
8715 };
8716 if let Some(s) = width_arg {
8717 if let Ok(w) = s.trim().parse::<i32>() {
8718 // c:2011 zstrtol
8719 if let Ok(mut tab) = paramtab().write() {
8720 if let Some(pm) = tab.get_mut(arg) {
8721 pm.width = w; // c:2011 pm->width = width
8722 }
8723 }
8724 }
8725 }
8726 }
8727 // c:2510+ — stamp post-assign attributes (PM_EXPORTED,
8728 // PM_READONLY, etc.) on the (possibly newly-created) pm.
8729 if post_assign_to_set != 0 {
8730 if let Ok(mut tab) = paramtab().write() {
8731 if let Some(pm) = tab.get_mut(arg) {
8732 // c:2289 MERGE — preserve attributes the user did not touch
8733 // (e.g. `typeset -L 4 x=hi; typeset -r x` keeps -L). `off` bits
8734 // are cleared separately below.
8735 pm.node.flags |= post_assign_to_set;
8736 }
8737 }
8738 // c:Src/params.c — when PM_UNIQUE is freshly stamped on
8739 // an EXISTING array via bare `typeset -U arr` (no value
8740 // assignment), zsh dedups the current contents in-place
8741 // (verified vs /opt/homebrew/bin/zsh: `arr=(a b a c b);
8742 // typeset -U arr; echo "$arr[@]"` → "a b c"). Apply the
8743 // same dedup here.
8744 // A declared-but-unset array (TYPESET_TO_UNSET) has nothing to
8745 // dedup; C's uniqarray on its empty getfn result is a no-op,
8746 // whereas setaparam here would re-create it as a set array.
8747 let declared_unset = paramtab().read().ok().and_then(|t| {
8748 t.get(arg).map(|pm| (pm.node.flags as u32 & PM_UNSET) != 0)
8749 }) == Some(true);
8750 if (post_assign_to_set as u32 & PM_UNIQUE) != 0 && !declared_unset {
8751 let existing = crate::ported::params::getaparam(arg);
8752 if let Some(arr) = existing {
8753 let mut seen: std::collections::HashSet<String> =
8754 std::collections::HashSet::new();
8755 let deduped: Vec<String> =
8756 arr.into_iter().filter(|e| seen.insert(e.clone())).collect();
8757 crate::ported::params::setaparam(arg, deduped);
8758 }
8759 }
8760 // c:Src/builtin.c:2302-2307 — `if (!(pm->node.flags &
8761 // (PM_ARRAY|PM_HASHED))) { ... addenv(pm, getsparam(pname)); }`.
8762 // Arrays and associative arrays are NOT mirrored to the
8763 // OS env even when PM_EXPORTED is set — env strings are
8764 // single name=value pairs and have no representation
8765 // for indexed or hashed data. zsh silently no-ops the
8766 // export request for these types; zshrs's prior code
8767 // wrote a malformed empty `h=` entry for `export h`
8768 // where h is an assoc. Bug #349.
8769 if (on as u32 & PM_EXPORTED) != 0 {
8770 let is_array_or_hashed = paramtab()
8771 .read()
8772 .ok()
8773 .and_then(|t| t.get(arg).map(|pm| pm.node.flags as u32))
8774 .map_or(false, |f| (f & (PM_ARRAY | PM_HASHED)) != 0)
8775 || crate::ported::params::paramtab_hashed_storage()
8776 .lock()
8777 .ok()
8778 .map_or(false, |s| s.contains_key(arg))
8779 || crate::ported::exec::array(arg).is_some()
8780 || crate::ported::exec::assoc(arg).is_some();
8781 if !is_array_or_hashed {
8782 // c:Src/builtin.c:2299-2301 — a bare `export NAME` of a parameter that
8783 // was never assigned (PM_UNSET) does not `addenv`; the environment
8784 // entry appears with the first assignment.
8785 if let Some(val) = saved_val.as_deref().or((!was_fresh).then_some("")) {
8786 // c:Src/builtin.c typeset_single -> export_param
8787 // (c:Src/params.c:2670) -> zputenv (c:5325), which
8788 // hands the value to `setenv` as a NUL-TERMINATED C
8789 // string: an embedded NUL simply ends it.
8790 // `env::set_var` PANICS on one instead, and the
8791 // panic is reachable from a bare `export IFS` —
8792 // zsh's default $IFS is " \t\n\0" (c:Src/params.c
8793 // ifs default), so the NUL is not an exotic value,
8794 // it is the stock one. Route through the ported
8795 // zputenv so this site inherits C's truncation
8796 // instead of re-deriving it.
8797 crate::ported::params::zputenv(&format!("{arg}={val}"));
8798 }
8799 }
8800 }
8801 }
8802 // c:Src/builtin.c::typeset_single — `typeset +x VAR`
8803 // clears PM_EXPORTED via the `off` mask. C zsh calls
8804 // removeenv on the param, which strips the var from the
8805 // OS environment so child processes no longer see it.
8806 // Must fire INDEPENDENTLY of post_assign_to_set since `+x`
8807 // alone sets `off` (PM_EXPORTED) but `on` is 0, so the
8808 // post_assign_to_set block above doesn't run. Also clear
8809 // the flag bit on the paramtab entry so `(t)V` no longer
8810 // reports `-export`. Bug #201 in docs/BUGS.md.
8811 if (off as u32 & PM_EXPORTED) != 0 {
8812 env::remove_var(arg);
8813 if let Ok(mut tab) = paramtab().write() {
8814 if let Some(pm) = tab.get_mut(arg) {
8815 pm.node.flags &= !(PM_EXPORTED as i32);
8816 }
8817 }
8818 }
8819 // c:Src/builtin.c:2289 — `pm->node.flags = ... & ~off`.
8820 // `off` bits in post_assign_mask must clear regardless of
8821 // `on` (e.g. `typeset +U arr` where on=0, off=PM_UNIQUE).
8822 // The post_assign_to_set gate above short-circuits the
8823 // on=0 case. Same family as `+x` block immediately above.
8824 // Bug #234 in docs/BUGS.md.
8825 let off_in_mask = (off as i32) & post_assign_mask;
8826 if off_in_mask != 0 {
8827 if let Ok(mut tab) = paramtab().write() {
8828 if let Some(pm) = tab.get_mut(arg) {
8829 pm.node.flags &= !off_in_mask;
8830 }
8831 }
8832 }
8833 // c:2201 `on |= PM_UNSET` / c:2283-2286 "Keep unset if using readonly
8834 // in POSIX mode" — the valueless declaration leaves the parameter
8835 // unset after its attributes are stamped.
8836 if posix_keep_unset || keep_declared_unset {
8837 if let Ok(mut tab) = paramtab().write() {
8838 if let Some(pm) = tab.get_mut(arg) {
8839 pm.node.flags |= PM_UNSET as i32;
8840 }
8841 }
8842 }
8843 // c:Src/builtin.c::typeset_single c:2241-2246 — when no
8844 // type flags and no value assignment, `typeset NAME` is a
8845 // display request: print the existing param in the
8846 // canonical form. C uses
8847 // `printparamnode(&pm->node, PRINT_INCLUDEVALUE|with_ns)`.
8848 // Without this, `typeset h` (where h is an assoc) emitted
8849 // nothing. The earlier per-arg print at typeset_single
8850 // line 3181 isn't called from bin_typeset's per-arg loop
8851 // — the loop does its own inline work. Bug #218 in
8852 // docs/BUGS.md.
8853 // c:Src/builtin.c::typeset_single c:2241-2246 — bare
8854 // `typeset NAME` (no on/off flags except PM_LOCAL which
8855 // bin_typeset auto-applies at c:2808) prints the param.
8856 // PM_LOCAL is added implicitly when not -g / -x / -m so
8857 // mask it out when checking "are any user flags set".
8858 //
8859 // c:Src/builtin.c:2078-2090 — the bare form prints UNLESS it
8860 // is localizing a variable from an outer scope. zsh clears
8861 // `usepm` (→ no print) only when `locallevel != pm->level &&
8862 // (on & PM_LOCAL)` — i.e. the existing param lives at a
8863 // different (outer/global) level and we're making a local.
8864 // Re-declaring a param that ALREADY exists at the current
8865 // level (`f(){ typeset x=1; typeset x }`) keeps usepm and
8866 // DOES print `x=1`. The previous `at_top_scope` gate wrongly
8867 // suppressed that in-function re-declaration case.
8868 let user_on = (on as u32) & !PM_LOCAL;
8869 let cur_ll = locallevel.load(Relaxed);
8870 let not_localizing = pm_level_existing == cur_ll || (on as u32 & PM_LOCAL) == 0;
8871 // c:Src/builtin.c:2244 — `else if (!OPT_ISSET(ops,'g') &&
8872 // (unset(TYPESETSILENT) || OPT_ISSET(ops,'m')))`. The `-g`
8873 // (global-scope) flag SUPPRESSES the bare-name print so
8874 // `typeset -g ZPFX` is a silent declaration / no-op rather
8875 // than a list. zinit's plugin loader runs `typeset -g VAR`
8876 // for already-environment-exported names on every load;
8877 // without this gate zshrs spammed `VAR=value` for each on
8878 // startup.
8879 // c:Src/builtin.c:2058-2061 — the print at c:2249-2256 sits INSIDE
8880 // the `usepm` branch:
8881 // usepm = pm && (!(pm->node.flags & PM_UNSET) ||
8882 // (pm->node.flags & PM_DECLARED) || OPT_ISSET(ops,'p') ||
8883 // (isset(POSIXBUILTINS) &&
8884 // (pm->node.flags & (PM_READONLY|PM_EXPORTED))));
8885 // Since 55020 a TYPESET_TO_UNSET declaration (PM_DEFAULTED =
8886 // PM_DECLARED|PM_UNSET) is reused, so `typeset NAME` on it prints
8887 // the bare name (printparamnode c:Src/params.c:6158-6167); an
8888 // unset, undeclared parameter still prints nothing.
8889 // The `-p` term of usepm is already covered by the `!OPT_ISSET(p)`
8890 // conjunct below. Bug #1056.
8891 if user_on == 0
8892 && off == 0
8893 && not_localizing
8894 && !OPT_ISSET(&ops, b'p')
8895 && !OPT_ISSET(&ops, b'g')
8896 && (!isset(TYPESETSILENT) || OPT_ISSET(&ops, b'm'))
8897 && pname_in_tab
8898 && usepm_at_entry
8899 {
8900 let with_ns = if OPT_ISSET(&ops, b'm') {
8901 PRINT_WITH_NAMESPACE
8902 } else {
8903 0
8904 };
8905 let _ = with_ns;
8906 // c:Src/params.c:6158-6167 — printparamnode shows a
8907 // PM_DEFAULTED (declared, still unset) parameter by NAME
8908 // only; since 55020 it reaches this print (usepm is set for
8909 // PM_DECLARED, c:2059).
8910 let defaulted_unset = paramtab().read().ok().and_then(|t| {
8911 t.get(arg).map(|pm| {
8912 (pm.node.flags as u32 & PM_DEFAULTED) == PM_DEFAULTED
8913 })
8914 }) == Some(true);
8915 if defaulted_unset {
8916 println!("{}", arg);
8917 continue;
8918 }
8919 // c:Src/params.c:6286-6288 + Src/module.c:1218-1223 — a zsh/parameter
8920 // table nothing has loaded yet is a PM_AUTOLOAD stub, which
8921 // printparamnode shows by NAME only (`typeset aliases`).
8922 if crate::vm_helper::module_param_is_autoload_stub(arg) {
8923 println!("{}", arg);
8924 continue;
8925 }
8926 // The paramtab entry for assoc/array shapes set via
8927 // direct assignment (`a=(1 2 3)` / `h[k]=v`) doesn't
8928 // always have PM_ARRAY/PM_HASHED set on flags — the
8929 // values live in the executor's arrays/assoc_arrays
8930 // storage, separate from paramtab. Check the canonical
8931 // storage paths and emit the right form directly,
8932 // mirroring printparamvalue's PM_ARRAY/PM_HASHED arms
8933 // (params.rs:8819+).
8934 //
8935 // Order matters: assoc lookup falls back to
8936 // crate::ported::exec::assoc which reads the executor's
8937 // assoc_arrays storage. typeset -A populated via
8938 // `typeset -A h=(a 1 b 2)` lives there, not in
8939 // paramtab_hashed_storage. Bug #218 in docs/BUGS.md.
8940 // c:Src/params.c:6301-6312 — printparamvalue walks a PM_HASHED
8941 // parameter through its own scan; a SPECIALPMDEF module hash
8942 // (`aliases`, `options`, …) has no stored map, the scanfn IS the
8943 // contents. PM_HIDEVAL hides the value only without
8944 // PRINT_INCLUDEVALUE, which `typeset NAME` passes.
8945 let special_hash = paramtab().read().ok().is_some_and(|t| {
8946 t.get(arg).is_some_and(|pm| {
8947 pm.node.flags as u32 & (PM_SPECIAL | PM_HASHED) == (PM_SPECIAL | PM_HASHED)
8948 })
8949 });
8950 if special_hash {
8951 if let (Some(keys), Some(vals)) = (
8952 crate::ported::params::gethkparam(arg),
8953 crate::ported::params::gethparam(arg),
8954 ) {
8955 let body: String = keys
8956 .iter()
8957 .zip(vals.iter())
8958 .map(|(k, v)| {
8959 format!(
8960 "[{}]={} ",
8961 crate::ported::utils::quotedzputs(k),
8962 crate::ported::utils::quotedzputs(v)
8963 )
8964 })
8965 .collect();
8966 println!("{}=( {})", arg, body);
8967 continue;
8968 }
8969 }
8970 let assoc = crate::ported::params::paramtab_hashed_storage()
8971 .lock()
8972 .ok()
8973 .and_then(|s| s.get(arg).cloned())
8974 .or_else(|| crate::ported::exec::assoc(arg));
8975 if let Some(map) = assoc {
8976 let mut entries: Vec<(&String, &String)> = map.iter().collect();
8977 entries.sort_by(|(a, _), (b, _)| a.cmp(b));
8978 let mut s = format!("{}=( ", arg);
8979 let mut first = true;
8980 for (k, v) in entries {
8981 if !first {
8982 s.push(' ');
8983 }
8984 first = false;
8985 s.push_str(&format!(
8986 "[{}]={}",
8987 crate::ported::utils::quotedzputs(k), // c:Src/params.c:6301
8988 crate::ported::utils::quotedzputs(v)
8989 ));
8990 }
8991 s.push_str(" )");
8992 println!("{}", s);
8993 } else if let Some(arr) = crate::ported::params::getaparam(arg)
8994 .or_else(|| crate::ported::exec::array(arg))
8995 .or_else(|| {
8996 // Fallback: paramtab entry's u_arr may be set
8997 // even when PM_TYPE doesn't include PM_ARRAY
8998 // (direct `a=(1 2 3)` assignment doesn't always
8999 // update the flag).
9000 paramtab()
9001 .read()
9002 .ok()
9003 .and_then(|t| t.get(arg).and_then(|pm| pm.u_arr.clone()))
9004 .filter(|v| !v.is_empty())
9005 })
9006 {
9007 let parts: Vec<String> = arr
9008 .iter()
9009 .map(|v| crate::ported::utils::quotedzputs(v))
9010 .collect();
9011 println!("{}=( {} )", arg, parts.join(" "));
9012 } else if let Some(val) = getsparam(arg) {
9013 println!("{}={}", arg, crate::ported::utils::quotedzputs(&val));
9014 }
9015 }
9016 }
9017 }
9018 unqueue_signals();
9019 returnval
9020}
9021
9022/// Port of `eval_autoload()` from `Src/builtin.c:3166`.
9023/// C decl: `eval_autoload(Shfunc shf, char *name, Options ops, int func)`
9024/// C: `int eval_autoload(Shfunc shf, char *name, Options ops, int func)`.
9025/// PM_UNDEFINED guard; -X spawns the eval-trampoline, otherwise loadautofn
9026/// resolves and installs the body.
9027/// WARNING: param names don't match C — Rust=(shf, name, func) vs C=(shf, name, ops, func)
9028pub fn eval_autoload(
9029 shf: *mut shfunc,
9030 name: &str, // c:3166
9031 ops: &options,
9032 func: i32,
9033) -> i32 {
9034 if shf.is_null() {
9035 return 1;
9036 }
9037 let shf_mut = unsafe { &mut *shf };
9038 // c:3168-3169 — `if (!(shf->node.flags & PM_UNDEFINED)) return 1;`
9039 if (shf_mut.node.flags as u32 & PM_UNDEFINED) == 0 {
9040 // c:3168
9041 return 1; // c:3169
9042 }
9043 // c:3171-3174 — `if (shf->funcdef) { freeeprog(shf->funcdef); shf->funcdef = &dummy_eprog; }`
9044 if shf_mut.funcdef.is_some() {
9045 // c:3171
9046 shf_mut.funcdef = None; // c:3173 freeeprog + dummy
9047 }
9048 // c:3175-3181 — `-X` spawns the autoload trampoline via bin_eval.
9049 if OPT_MINUS(ops, b'X') {
9050 // c:3175
9051 // c:3177 — `fargv[0] = quotestring(name, QT_SINGLE_OPTIONAL); fargv[1] = "\"$@\"";`
9052 let fargv = vec![
9053 // c:3177-3179
9054 quotedzputs(name),
9055 "\"$@\"".to_string(),
9056 ];
9057 // c:3180 — `shf->funcdef = mkautofn(shf);`
9058 let p = mkautofn(shf); // c:3180
9059 let _ = p; // funcdef writeback handled inside mkautofn at c:3801
9060 return bin_eval(name, &fargv, ops, func); // c:3181
9061 }
9062 // c:3184-3186 — `return !loadautofn(shf, (OPT_ISSET('k') ? 2 :
9063 // (OPT_ISSET('z') ? 0 : 1)), 1,
9064 // OPT_ISSET('d'));`
9065 let mode = if OPT_ISSET(ops, b'k') {
9066 2
9067 }
9068 // c:3184
9069 else if OPT_ISSET(ops, b'z') {
9070 0
9071 }
9072 // c:3185
9073 else {
9074 1
9075 };
9076 let _d = OPT_ISSET(ops, b'd');
9077 // c:3184 — `return !loadautofn(shf, ...)`. C's loadautofn returns
9078 // `Shfunc` (pointer): NULL = failure, non-NULL = success. C's
9079 // `!loadautofn` yields 1 (true) when the pointer is NULL —
9080 // i.e. eval_autoload returns 1 on failure, 0 on success.
9081 //
9082 // The Rust port's loadautofn returns an i32 with the OPPOSITE
9083 // sign convention: 0 = success, non-zero = failure (the
9084 // canonical Rust ShellExecutor-status convention used throughout
9085 // this file). The previous Rust port literally translated `!`
9086 // as `if r == 0 { 1 } else { 0 }` — but that double-inverts the
9087 // already-flipped convention, so file-not-found (Rust r=1)
9088 // returned eval_autoload=0 (zsh-success), and the broken
9089 // `autoload +X totally_fake` silently exited 0 instead of 1.
9090 // The C-faithful return for the Rust convention is just `r`.
9091 // Bug #107 in docs/BUGS.md.
9092 loadautofn(shf, mode, 1, _d as i32) // c:3184
9093}
9094
9095/// Port of `check_autoload()` from `Src/builtin.c:3193`.
9096/// C decl: `check_autoload(Shfunc shf, char *name, Options ops, int func)`
9097/// C: `static int check_autoload(Shfunc shf, char *name, Options ops,
9098/// int func)` — `OPT_ISSET(ops,'X')` ? eval_autoload : 0.
9099/// WARNING: param names don't match C — Rust=(shf, name, func) vs C=(shf, name, ops, func)
9100pub fn check_autoload(
9101 shf: *mut shfunc,
9102 name: &str, // c:3193
9103 ops: &options,
9104 func: i32,
9105) -> i32 {
9106 // c:3196-3199 — `if (OPT_ISSET(ops,'X')) return eval_autoload(...);`
9107 if OPT_ISSET(ops, b'X') {
9108 // c:3196
9109 return eval_autoload(shf, name, ops, func); // c:3197
9110 }
9111 // c:3200-3242 — -r / -R re-resolve: walk fpath for the function file.
9112 let want_r = OPT_ISSET(ops, b'r');
9113 let want_R = OPT_ISSET(ops, b'R');
9114 if (want_r || want_R) && !shf.is_null() {
9115 // c:3200
9116 let shf_mut = unsafe { &mut *shf };
9117 if (shf_mut.node.flags as u32 & PM_UNDEFINED) == 0 {
9118 return 0;
9119 }
9120 // c:3202-3216 — already has filename + PM_LOADDIR: try the cached
9121 // dir first via spec_path[].
9122 if (shf_mut.node.flags as u32 & PM_LOADDIR) != 0 && shf_mut.filename.is_some() {
9123 let spec = vec![shf_mut.filename.clone().unwrap_or_default()];
9124 if getfpfunc(
9125 &shf_mut.node.nam,
9126 &mut None, // c:3206
9127 Some(&spec),
9128 1,
9129 &mut None, // c:3206 `&ksh` — unused on the test-only path
9130 )
9131 .is_some()
9132 {
9133 return 0; // c:3209
9134 }
9135 // c:3211-3217 — `-d` not set: bail (with -R = error, with -r = silent).
9136 if !OPT_ISSET(ops, b'd') {
9137 // c:3211
9138 if want_R {
9139 // c:3212
9140 zerr(&format!(
9141 "{}: function definition file not found",
9142 shf_mut.node.nam
9143 )); // c:3213
9144 return 1; // c:3215
9145 }
9146 return 0; // c:3216
9147 }
9148 }
9149 // c:3219-3231 — fpath walk via getfpfunc + dircache_set install.
9150 let mut dir_path: Option<String> = None;
9151 if getfpfunc(&shf_mut.node.nam, &mut dir_path, None, 1, &mut None).is_some() // c:3219
9152 && dir_path.is_some()
9153 {
9154 // c:3220-3228 — dircache_set + relative-path absolutize.
9155 let mut old_slot = shf_mut.filename.take();
9156 dircache_set(&mut old_slot, None); // c:3220
9157 let dp = dir_path.unwrap();
9158 let mut new_slot: Option<String> = None;
9159 dircache_set(&mut new_slot, Some(&dp)); // c:3228
9160 shf_mut.filename = new_slot;
9161 shf_mut.node.flags |= PM_LOADDIR as i32; // c:3229
9162 return 0; // c:3230
9163 }
9164 // c:3233-3239 — -R: error; -r: silent.
9165 if want_R {
9166 // c:3233
9167 zerr(&format!(
9168 "{}: function definition file not found",
9169 shf_mut.node.nam
9170 )); // c:3243
9171 return 1; // c:3243
9172 }
9173 }
9174 0 // c:3243
9175}
9176
9177/// Port of `listusermathfunc()` from `Src/builtin.c:3243`.
9178/// C decl: `listusermathfunc(MathFunc p)`
9179/// C: `static void listusermathfunc(MathFunc p)` — emit a `functions -M`
9180/// row for one user math function with arg counts and module name.
9181pub fn listusermathfunc(p: &mathfunc) {
9182 // c:3243
9183 // c:3247-3257 — pick `showargs` 0..3 based on module/min/max presence.
9184 let mut showargs: i32 = if p.module.is_some() {
9185 // c:3249
9186 3
9187 } else if p.maxargs != if p.minargs != 0 { p.minargs } else { -1 } {
9188 // c:3251
9189 2
9190 } else if p.minargs != 0 {
9191 // c:3253
9192 1
9193 } else {
9194 0 // c:3256
9195 };
9196
9197 // c:3259 — `printf("functions -M%s %s", (p->flags & MFF_STR) ? "s" : "", p->name);`
9198 let s_suffix = if (p.flags & MFF_STR) != 0 { "s" } else { "" }; // c:3259
9199 print!("functions -M{} {}", s_suffix, p.name); // c:3259
9200 if showargs != 0 {
9201 // c:3260
9202 print!(" {}", p.minargs); // c:3261
9203 showargs -= 1; // c:3262
9204 }
9205 if showargs != 0 {
9206 // c:3264
9207 print!(" {}", p.maxargs); // c:3265
9208 showargs -= 1; // c:3266
9209 }
9210 if showargs != 0 {
9211 // c:3268
9212 // c:3269-3274 — function names are not required to be ident chars,
9213 // so the module name goes through quotedzputs for safe printing.
9214 print!(" "); // c:3273
9215 print!("{}", quotedzputs(p.module.as_deref().unwrap_or(""))); // c:3274
9216 showargs -= 1; // c:3275
9217 }
9218 println!(); // c:3277
9219}
9220
9221/// Port of `add_autoload_function()` from `Src/builtin.c:3278`.
9222/// C decl: `add_autoload_function(Shfunc shf, char *funcname)`
9223/// C: `static void add_autoload_function(Shfunc shf, char *funcname)` —
9224/// two branches:
9225/// (a) funcname is absolute & shf is PM_UNDEFINED → split `/dir/nam`,
9226/// dircache_set(&shf->filename, dir), set PM_LOADDIR|PM_ABSPATH_USED,
9227/// shfunctab->addnode(nam, shf).
9228/// (b) otherwise → walk funcstack to find calling function; if it has
9229/// PM_LOADDIR|PM_ABSPATH_USED, build `"<calling-dir>/funcname"` and
9230/// access(R_OK); on success copy the dir into shf and set
9231/// PM_LOADDIR|PM_ABSPATH_USED. Then shfunctab->addnode(funcname, shf).
9232/// WARNING: param names don't match C — Rust=(shf) vs C=(shf, funcname)
9233pub fn add_autoload_function(
9234 shf: *mut shfunc, // c:3278
9235 funcname: &str,
9236) {
9237 if shf.is_null() || funcname.is_empty() {
9238 return;
9239 }
9240 let shf_ref = unsafe { &mut *shf };
9241
9242 let is_abs_path = funcname.starts_with('/') // c:3282
9243 && funcname.len() > 1
9244 && funcname[1..].contains('/')
9245 && (shf_ref.node.flags as u32 & PM_UNDEFINED) != 0;
9246
9247 if is_abs_path {
9248 // c:3287 — `nam = strrchr(funcname, '/');`
9249 let nam_idx = funcname.rfind('/').unwrap(); // c:3287
9250 let (dir, nam) = if nam_idx == 0 {
9251 // c:3289
9252 ("/".to_string(), funcname[1..].to_string()) // c:3290
9253 } else {
9254 (
9255 funcname[..nam_idx].to_string(), // c:3293
9256 funcname[nam_idx + 1..].to_string(),
9257 )
9258 };
9259 // c:3296 — `dircache_set(&shf->filename, NULL); dircache_set(..., dir);`
9260 let mut old_slot = shf_ref.filename.take();
9261 dircache_set(&mut old_slot, None); // c:3296
9262 let mut new_slot: Option<String> = None;
9263 dircache_set(&mut new_slot, Some(&dir)); // c:3297
9264 shf_ref.filename = new_slot;
9265 // c:3298-3299 — `shf->node.flags |= PM_LOADDIR | PM_ABSPATH_USED;`
9266 shf_ref.node.flags |= (PM_LOADDIR | PM_ABSPATH_USED) as i32; // c:3298
9267 // c:3300 — `shfunctab->addnode(shfunctab, ztrdup(nam), shf);` — the
9268 // node is keyed on the BASENAME (`nam`), not the full path. zshrs's
9269 // addnode keys on `shf.node.nam`, so rename the node before adding;
9270 // otherwise `autoload -Uz /dir/.hist.format` registered a function
9271 // literally named `/dir/.hist.format` (plugins that autoload by full
9272 // path — zsh-hist's `autoload -Uz $fdir/.hist.*` — diverged from
9273 // zsh, which names it `.hist.format`).
9274 shf_ref.node.nam = nam;
9275 if let Ok(mut t) = shfunctab_lock().write() {
9276 t.addnode(shf); // c:3300
9277 }
9278 } else {
9279 // c:3304-3327 — walk funcstack, look up calling fn in shfunctab, if
9280 // it has PM_LOADDIR|PM_ABSPATH_USED build "<dir>/<funcname>" and
9281 // access(R_OK), inherit the dir on hit.
9282 let calling_f: Option<String> = {
9283 let stack = FUNCSTACK.lock().map(|s| s.clone()).unwrap_or_default();
9284 // c:3306 — `for (fs = funcstack; fs; fs = fs->prev)`
9285 stack
9286 .iter()
9287 .rev()
9288 .find(|fs| {
9289 // c:3306
9290 // c:3307 — `if (fs->tp == FS_FUNC && fs->name &&
9291 // (!shf->node.nam || strcmp(fs->name, shf->node.nam)))`
9292 FS_FUNC != 0 // mirror struct doesn't expose tp directly;
9293 && !fs.name.is_empty()
9294 && (shf_ref.node.nam.is_empty() || fs.name != shf_ref.node.nam)
9295 })
9296 .map(|fs| fs.name.clone()) // c:3308
9297 };
9298 if let Some(cf) = calling_f {
9299 // c:3315
9300 // c:3316 — `shf2 = shfunctab->getnode2(shfunctab, calling_f);`
9301 let shf2_ptr = shfunctab_lock()
9302 .read()
9303 .map(|t| t.getnode2(&cf))
9304 .unwrap_or(std::ptr::null_mut());
9305 if !shf2_ptr.is_null() {
9306 let shf2 = unsafe { &*shf2_ptr };
9307 // c:3317-3318
9308 let needs = (PM_LOADDIR | PM_ABSPATH_USED) as i32;
9309 if (shf2.node.flags & needs) == needs {
9310 // c:3317
9311 if let Some(dir2) = &shf2.filename {
9312 // c:3318
9313 // c:3320 — `snprintf(buf, PATH_MAX, "%s/%s", dir2, funcname);`
9314 let buf = format!("{}/{}", dir2, funcname); // c:3320
9315 if buf.len() <= libc::PATH_MAX as usize {
9316 // c:3320
9317 // c:3324 — `if (!access(buf, R_OK))`
9318 let buf_c = std::ffi::CString::new(buf.clone()).ok();
9319 if let Some(bc) = buf_c {
9320 if unsafe { libc::access(bc.as_ptr(), libc::R_OK) } == 0 {
9321 // c:3324
9322 let mut old_slot = shf_ref.filename.take();
9323 dircache_set(&mut old_slot, None); // c:3325
9324 let dir2c = dir2.clone();
9325 let mut new_slot: Option<String> = None;
9326 dircache_set(&mut new_slot, Some(&dir2c)); // c:3326
9327 shf_ref.filename = new_slot;
9328 shf_ref.node.flags |= (PM_LOADDIR | PM_ABSPATH_USED) as i32;
9329 // c:3327
9330 }
9331 }
9332 }
9333 }
9334 }
9335 }
9336 }
9337 // c:3334 — `shfunctab->addnode(shfunctab, ztrdup(funcname), shf);`
9338 // addnode keys by `shf->node.nam`; if the caller picked a different
9339 // funcname here, re-tag the node name first so the keyed insert
9340 // matches the C contract.
9341 unsafe {
9342 if !shf.is_null() {
9343 (*shf).node.nam = funcname.to_string();
9344 }
9345 }
9346 if let Ok(mut t) = shfunctab_lock().write() {
9347 t.addnode(shf); // c:3334
9348 }
9349 }
9350}
9351
9352/// Port of `bin_functions()` from `Src/builtin.c:3342`.
9353/// C decl: `bin_functions(char *name, char **argv, Options ops, int func)`
9354/// C: `int bin_functions(char *name, char **argv, Options ops, int func)`.
9355/// This is the canonical free-function port matching the C signature so
9356/// the dispatcher can call it. The earlier `ShellExecutor::bin_functions`
9357/// inherent method is an ad-hoc Rust-side helper kept for the existing
9358/// in-process executor; both should converge on this function.
9359/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
9360pub fn bin_functions(
9361 name: &str,
9362 argv: &[String], // c:3342
9363 ops: &options,
9364 _func: i32,
9365) -> i32 {
9366 // c:3346-3347 — `int returnval = 0; int on = 0, off = 0, pflags = 0,
9367 // roff, expand = 0;`
9368 let mut returnval: i32 = 0; // c:3346
9369 let mut on: u32 = 0; // c:3347
9370 let mut off: u32 = 0; // c:3347
9371 let _pflags: i32 = 0; // c:3347
9372 let _expand: i32 = 0; // c:3347
9373
9374 // c:3350-3351 — `if (OPT_PLUS(ops,'u')) off |= PM_UNDEFINED; else if
9375 // (OPT_MINUS(ops,'u') || OPT_ISSET(ops,'X')) on |= PM_UNDEFINED;`
9376 if OPT_PLUS(ops, b'u') {
9377 // c:3350
9378 off |= PM_UNDEFINED; // c:3351
9379 } else if OPT_MINUS(ops, b'u') || OPT_ISSET(ops, b'X') {
9380 // c:3352
9381 on |= PM_UNDEFINED; // c:3353
9382 }
9383 // c:3354-3357 — -U / +U toggle PM_UNALIASED|PM_UNDEFINED.
9384 if OPT_MINUS(ops, b'U') {
9385 // c:3354
9386 on |= PM_UNALIASED | PM_UNDEFINED; // c:3355
9387 } else if OPT_PLUS(ops, b'U') {
9388 // c:3356
9389 off |= PM_UNALIASED; // c:3357
9390 }
9391 // c:3358-3361 — -t / +t toggle PM_TAGGED.
9392 if OPT_MINUS(ops, b't') {
9393 // c:3358
9394 on |= PM_TAGGED; // c:3359
9395 } else if OPT_PLUS(ops, b't') {
9396 // c:3360
9397 off |= PM_TAGGED; // c:3361
9398 }
9399 // c:3362-3365 — -T / +T toggle PM_TAGGED_LOCAL.
9400 if OPT_MINUS(ops, b'T') {
9401 // c:3362
9402 on |= PM_TAGGED_LOCAL; // c:3363
9403 } else if OPT_PLUS(ops, b'T') {
9404 // c:3364
9405 off |= PM_TAGGED_LOCAL; // c:3365
9406 }
9407 // c:3366-3369 — -W / +W toggle PM_WARNNESTED.
9408 if OPT_MINUS(ops, b'W') {
9409 // c:3366
9410 on |= PM_WARNNESTED; // c:3367
9411 } else if OPT_PLUS(ops, b'W') {
9412 // c:3368
9413 off |= PM_WARNNESTED; // c:3369
9414 }
9415 // c:3370 — `roff = off;`
9416 let mut roff = off; // c:3370
9417 // c:3371-3377 — -z / +z PM_ZSHSTORED|PM_KSHSTORED interaction.
9418 if OPT_MINUS(ops, b'z') {
9419 // c:3371
9420 on |= PM_ZSHSTORED; // c:3372
9421 off |= PM_KSHSTORED; // c:3373
9422 } else if OPT_PLUS(ops, b'z') {
9423 // c:3374
9424 off |= PM_ZSHSTORED; // c:3375
9425 roff |= PM_ZSHSTORED; // c:3376
9426 }
9427 // c:3379-3385 — -k / +k PM_KSHSTORED|PM_ZSHSTORED interaction.
9428 if OPT_MINUS(ops, b'k') {
9429 // c:3379
9430 on |= PM_KSHSTORED; // c:3380
9431 off |= PM_ZSHSTORED; // c:3381
9432 } else if OPT_PLUS(ops, b'k') {
9433 // c:3382
9434 off |= PM_KSHSTORED; // c:3383
9435 roff |= PM_KSHSTORED; // c:3384
9436 }
9437 // c:3386-3392 — -d / +d PM_CUR_FPATH toggle.
9438 if OPT_MINUS(ops, b'd') {
9439 // c:3386
9440 on |= PM_CUR_FPATH; // c:3387
9441 off |= PM_CUR_FPATH; // c:3388
9442 } else if OPT_PLUS(ops, b'd') {
9443 // c:3389
9444 off |= PM_CUR_FPATH; // c:3390
9445 roff |= PM_CUR_FPATH; // c:3391
9446 }
9447
9448 // c:3394-3400 — early-error validation: invalid flag combinations.
9449 // C: `(OPT_MINUS(ops,'X') && (OPT_ISSET(ops,'m') || !scriptname))` —
9450 // \`-X\` is only valid in a script context (autoload-from-fpath
9451 // dispatch). Previous Rust port dropped the \`|| !scriptname\` half
9452 // so \`functions -X foo\` from interactive shell silently
9453 // succeeded — divergent.
9454 let scriptname_missing = scriptname_get().is_none();
9455 if (off & PM_UNDEFINED) != 0 // c:3394
9456 || (OPT_ISSET(ops, b'k') && OPT_ISSET(ops, b'z')) // c:3394
9457 || (OPT_ISSET(ops, b'x') && !OPT_HASARG(ops, b'x')) // c:3395
9458 || (OPT_MINUS(ops, b'X') // c:3396
9459 && (OPT_ISSET(ops, b'm') || scriptname_missing)) // c:3396 !scriptname
9460 || (OPT_ISSET(ops, b'c')
9461 && (OPT_ISSET(ops, b'x') || OPT_ISSET(ops, b'X') || OPT_ISSET(ops, b'm')))
9462 {
9463 zwarnnam(name, "bad option(s)"); // c:3399
9464 return 1; // c:3400
9465 }
9466
9467 // c:3402-3452 — `-c` (clone) branch: copy named function under a new
9468 // name, optionally registering it as a TRAP* signal trap.
9469 if OPT_ISSET(ops, b'c') {
9470 // c:3402
9471 if argv.len() < 2 || argv.len() > 2 {
9472 // c:3405
9473 zwarnnam(name, "-c: requires two arguments"); // c:3406
9474 return 1;
9475 }
9476 let src_name = &argv[0];
9477 let dst_name = &argv[1];
9478 // c:3409 — `shf = shfunctab->getnode(shfunctab, *argv);`
9479 let src_ptr = shfunctab_lock()
9480 .read()
9481 .map(|t| t.getnode(src_name.as_str()))
9482 .unwrap_or(std::ptr::null_mut());
9483 if src_ptr.is_null() {
9484 // c:3410
9485 zwarnnam(name, &format!("no such function: {}", src_name)); // c:3411
9486 return 1;
9487 }
9488 // c:3414-3421 — autoload-trampoline expansion if PM_UNDEFINED.
9489 // C body: `if (shf->flags & PM_UNDEFINED) { freeeprog;
9490 // funcdef=dummy; shf = loadautofn(shf,1,0,0); if (!shf) return 1; }`.
9491 // Rust port routes through the local loadautofn helper at
9492 // builtin.rs:883 which walks $fpath via getfpfunc, reads the
9493 // file, stores the body text on the Rust-side ShFunc, and
9494 // clears PM_UNDEFINED.
9495 if (unsafe { (*src_ptr).node.flags } as u32 & PM_UNDEFINED) != 0 {
9496 // c:3415-3418 — `freeeprog(shf->funcdef); shf->funcdef =
9497 // &dummy_eprog;` clear out any stale autoload stub before
9498 // re-loading. Rust port: drop the Option<Eprog>.
9499 unsafe {
9500 (*src_ptr).funcdef = None;
9501 }
9502 // c:3419 — `loadautofn(shf, 1, 0, 0)`.
9503 if loadautofn(src_ptr, 1, 0, 0) != 0 {
9504 // c:3420-3421 — autoload failed.
9505 return 1;
9506 }
9507 }
9508 // c:3422-3430 — `newsh = zalloc + memcpy + filename rebuild`.
9509 let src_ref = unsafe { &*src_ptr };
9510 let new_filename =
9511 if (src_ref.node.flags as u32 & PM_UNDEFINED) == 0 && src_ref.filename.is_some() {
9512 src_ref.filename.clone() // c:3429
9513 } else {
9514 None
9515 };
9516 let _ = new_filename; // wired into shfunctab[dst_name] below
9517 // c:3437-3447 — TRAP* prefix detection + signal trap registration.
9518 if dst_name.starts_with("TRAP") {
9519 // c:3437
9520 // c:3438 — `int sigidx = getsigidx(s + 4);`
9521 let sigidx = getsigidx(&dst_name[4..]); // c:3438
9522 if sigidx != -1 {
9523 // c:3439
9524 // c:3440 — `if (settrap(sigidx, NULL, ZSIG_FUNC))`.
9525 if settrap(sigidx, None, ZSIG_FUNC) != 0 {
9526 // c:3440
9527 // freeeprog(newsh->funcdef) — funcdef Drop covers it.
9528 // dircache_set(&newsh->filename, NULL);
9529 // zfree(newsh, sizeof(*newsh));
9530 return 1; // c:3445
9531 }
9532 // c:3447 — `removetrapnode(sigidx);` — clear any prior trap.
9533 removetrapnode(sigidx); // c:3447
9534 }
9535 }
9536 // c:3422-3430 — C does `newsh = zalloc + memcpy(*newsh, *shf)` so
9537 // src and dst become independent copies. Box-clone the source body
9538 // (rather than aliasing src_ptr) so subsequent mutation through
9539 // `getnode(dst_name)` doesn't bleed into src.
9540 // c:3450 — `shfunctab->addnode(shfunctab, ztrdup(s), &newsh->node);`
9541 let newsh = unsafe {
9542 let mut copy = (*src_ptr).clone();
9543 copy.node.nam = dst_name.clone();
9544 // c:3424-3428 — `if (newsh->node.flags & PM_LOADDIR) {
9545 // newsh->node.flags &= ~PM_LOADDIR;
9546 // newsh->filename = tricat(shf->filename, "/", shf->node.nam);
9547 // } else newsh->filename = ztrdup(shf->filename);`
9548 // A PM_LOADDIR filename is the DIRECTORY the file came from, and
9549 // the file is found by appending the function's own name. The
9550 // copy has a different name, so the full path is fixed here from
9551 // the ORIGINAL name; keeping PM_LOADDIR made `%x` in `cp` report
9552 // `dir/cp` instead of `dir/af`.
9553 if (copy.node.flags as u32 & PM_LOADDIR) != 0 {
9554 copy.node.flags &= !(PM_LOADDIR as i32); // c:3425
9555 copy.filename = (*src_ptr)
9556 .filename
9557 .as_ref()
9558 .map(|dir| format!("{}/{}", dir, (*src_ptr).node.nam)); // c:3426
9559 }
9560 Box::into_raw(Box::new(copy))
9561 };
9562 if let Ok(mut t) = shfunctab_lock().write() {
9563 t.addnode(newsh); // c:3450
9564 }
9565 return 0; // c:3451
9566 }
9567
9568 // c:3454-3463 — `-x N` indent override for printing.
9569 let mut expand: i32 = 0; // c:3454 (also c:3347)
9570 if OPT_ISSET(ops, b'x') {
9571 // c:3454
9572 let arg = OPT_ARG(ops, b'x').unwrap_or("");
9573 match arg.trim().parse::<i32>() {
9574 // c:3456
9575 Ok(n) => {
9576 expand = n; // c:3456
9577 if expand == 0 {
9578 expand = -1;
9579 } // c:3461-3462
9580 }
9581 Err(_) => {
9582 zwarnnam(name, "number expected after -x"); // c:3458
9583 return 1; // c:3459
9584 }
9585 }
9586 }
9587
9588 // c:3465-3466 — `+f` / roff / `+` enables PRINT_NAMEONLY.
9589 let mut pflags: i32 = 0;
9590 if OPT_PLUS(ops, b'f') || roff != 0 || OPT_ISSET(ops, b'+') {
9591 // c:3465
9592 pflags |= PRINT_NAMEONLY; // c:3466
9593 }
9594
9595 // c:3468-3530 — `-M`/`+M` add/remove/list math function path.
9596 if OPT_MINUS(ops, b'M') || OPT_PLUS(ops, b'M') {
9597 // c:3468
9598 // c:3473-3477 — refuse incompatible flag combos.
9599 if on != 0
9600 || off != 0
9601 || pflags != 0
9602 || OPT_ISSET(ops, b'X')
9603 || OPT_ISSET(ops, b'u')
9604 || OPT_ISSET(ops, b'U')
9605 || OPT_ISSET(ops, b'w')
9606 {
9607 zwarnnam(name, "bad option(s)"); // c:3475
9608 return 1; // c:3476
9609 }
9610 if argv.is_empty() {
9611 // c:3478
9612 // c:3479-3484 — list user math ported.
9613 queue_signals(); // c:3480
9614 if let Ok(table) = MATHFUNCS.lock() {
9615 // c:3481
9616 for p in table.iter() {
9617 // c:3481
9618 if (p.flags & MFF_USERFUNC) != 0 {
9619 // c:3482
9620 listusermathfunc(p); // c:3483
9621 }
9622 }
9623 }
9624 unqueue_signals(); // c:3484
9625 return returnval;
9626 } else if OPT_ISSET(ops, b'm') {
9627 // c:3485
9628 // c:3486-3515 — list/delete matching math ported by pattern.
9629 for arg in argv.iter() {
9630 queue_signals(); // c:3488
9631 // c:3489 — `tokenize(*argv)`; Rust patcompile handles it.
9632 if let Some(pprog) = patcompile(
9633 &{
9634 let mut __pat_tok = crate::pattern_data_escape::escape_data_backslashes(&arg); // a backslash tokenize leaves raw is DATA (c:Src/glob.c:3651)
9635 crate::ported::glob::tokenize(&mut __pat_tok);
9636 __pat_tok
9637 },
9638 PAT_STATIC,
9639 None,
9640 ) {
9641 // c:3490
9642 if OPT_PLUS(ops, b'M') {
9643 // c:3497
9644 // Delete matching user ported.
9645 if let Ok(mut table) = MATHFUNCS.lock() {
9646 table.retain(|p| {
9647 !((p.flags & MFF_USERFUNC) != 0 && pattry(&pprog, &p.name))
9648 });
9649 }
9650 } else {
9651 // c:3502 — listusermathfunc for matches.
9652 if let Ok(table) = MATHFUNCS.lock() {
9653 for p in table.iter() {
9654 if (p.flags & MFF_USERFUNC) != 0 && pattry(&pprog, &p.name) {
9655 listusermathfunc(p);
9656 }
9657 }
9658 }
9659 }
9660 } else {
9661 // c:3509
9662 // c:3510-3512 — bad pattern.
9663 zwarnnam(
9664 name, // c:3511
9665 &format!("bad pattern : {}", arg),
9666 );
9667 returnval = 1; // c:3512
9668 }
9669 unqueue_signals(); // c:3514
9670 }
9671 return returnval;
9672 } else if OPT_PLUS(ops, b'M') {
9673 // c:3516
9674 // c:3517-3533 — `+M name…` delete by exact name.
9675 for arg in argv.iter() {
9676 queue_signals(); // c:3519
9677 if let Ok(mut table) = MATHFUNCS.lock() {
9678 let idx = table.iter().position(|p| p.name == *arg); // c:3520-3521
9679 if let Some(i) = idx {
9680 if (table[i].flags & MFF_USERFUNC) == 0 {
9681 // c:3522-3527 — library function, refuse.
9682 zwarnnam(
9683 name, // c:3523
9684 &format!("+M {}: is a library function", arg),
9685 );
9686 returnval = 1; // c:3525
9687 } else {
9688 table.remove(i); // c:3528
9689 }
9690 }
9691 }
9692 unqueue_signals(); // c:3532
9693 }
9694 return returnval;
9695 } else {
9696 // c:3535-3611 — `-M name [min [max [mod]]]` add a user math fn.
9697 let mut argv_iter = argv.iter();
9698 let funcname = argv_iter.next().unwrap(); // c:3537
9699 let mut minargs: i32;
9700 let mut maxargs: i32;
9701 if OPT_ISSET(ops, b's') {
9702 // c:3541
9703 minargs = 1; // c:3542
9704 maxargs = 1; // c:3542
9705 } else {
9706 minargs = 0; // c:3544
9707 maxargs = -1; // c:3545
9708 }
9709 // c:3548-3552 — bad math function name check.
9710 let bytes = funcname.as_bytes();
9711 let first_bad = bytes.is_empty()
9712 || (bytes[0] as char).is_ascii_digit()
9713 || !bytes
9714 .iter()
9715 .all(|&c| c.is_ascii_alphanumeric() || c == b'_');
9716 if first_bad {
9717 // c:3549
9718 zwarnnam(
9719 name, // c:3550
9720 &format!("-M {}: bad math function name", funcname),
9721 );
9722 return 1; // c:3551
9723 }
9724 if let Some(arg) = argv_iter.next() {
9725 // c:3554
9726 match arg.parse::<i32>() {
9727 // c:3555 zstrtol
9728 Ok(n) if n >= 0 => minargs = n, // c:3556
9729 _ => {
9730 zwarnnam(
9731 name, // c:3557
9732 &format!("-M: invalid min number of arguments: {}", arg),
9733 );
9734 return 1; // c:3559
9735 }
9736 }
9737 if OPT_ISSET(ops, b's') && minargs != 1 {
9738 // c:3561
9739 zwarnnam(
9740 name, // c:3562
9741 "-Ms: must take a single string argument",
9742 );
9743 return 1; // c:3563
9744 }
9745 maxargs = minargs; // c:3565
9746 }
9747 if let Some(arg) = argv_iter.next() {
9748 // c:3568
9749 match arg.parse::<i32>() {
9750 // c:3569
9751 Ok(n) if n >= -1 && (n == -1 || n >= minargs) => maxargs = n,
9752 _ => {
9753 zwarnnam(
9754 name, // c:3573
9755 &format!("-M: invalid max number of arguments: {}", arg),
9756 );
9757 return 1; // c:3576
9758 }
9759 }
9760 if OPT_ISSET(ops, b's') && maxargs != 1 {
9761 // c:3578
9762 zwarnnam(
9763 name, // c:3579
9764 "-Ms: must take a single string argument",
9765 );
9766 return 1; // c:3580
9767 }
9768 }
9769 let modname = argv_iter.next().cloned(); // c:3584-3585
9770 if argv_iter.next().is_some() {
9771 // c:3586
9772 zwarnnam(name, "-M: too many arguments"); // c:3587
9773 return 1; // c:3588
9774 }
9775 // c:3591-3598 — alloc and populate mathfunc.
9776 let mut flags = MFF_USERFUNC; // c:3593
9777 if OPT_ISSET(ops, b's') {
9778 // c:3594
9779 flags |= MFF_STR; // c:3595
9780 }
9781 let new_fn = mathfunc {
9782 next: None, // c:3608 chain via Vec
9783 name: funcname.clone(), // c:3592
9784 flags, // c:3593
9785 nfunc: None,
9786 sfunc: None,
9787 module: modname, // c:3596
9788 minargs, // c:3597
9789 maxargs, // c:3598
9790 funcid: 0,
9791 };
9792 queue_signals(); // c:3600
9793 if let Ok(mut table) = MATHFUNCS.lock() {
9794 // c:3601-3606 — remove existing user entry with same name.
9795 if let Some(i) = table.iter().position(|p| p.name == new_fn.name) {
9796 table.remove(i); // c:3603
9797 }
9798 // c:3608-3609 — prepend to mathfuncs head.
9799 table.insert(0, new_fn);
9800 }
9801 unqueue_signals(); // c:3610
9802 return returnval;
9803 }
9804 }
9805
9806 // c:3616-3655 — `-X` re-autoload from inside a function.
9807 if OPT_MINUS(ops, b'X') {
9808 // c:3616
9809 if argv.len() > 1 {
9810 // c:3620
9811 zwarnnam(name, "-X: too many arguments"); // c:3621
9812 return 1; // c:3622
9813 }
9814 queue_signals(); // c:3624
9815 // c:3625-3633 — walk funcstack to find the enclosing FS_FUNC frame.
9816 let funcname: Option<String> = {
9817 let stack = FUNCSTACK.lock().map(|s| s.clone()).unwrap_or_default();
9818 stack
9819 .iter()
9820 .rev()
9821 // c:3623 — `if (fs->tp == FS_FUNC)`. C skips FS_EVAL and
9822 // FS_SOURCE frames and takes the innermost real FUNCTION.
9823 // Matching on "name is non-empty" instead picked the `(eval)`
9824 // frame for `f() { eval autoload -X }` (the zinit /
9825 // C04funcdef:29 idiom), so the load was attempted for a
9826 // function literally named `(eval)`.
9827 .find(|fs| fs.tp == crate::ported::zsh_h::FS_FUNC && !fs.name.is_empty()) // c:3623
9828 .map(|fs| fs.name.clone()) // c:3628
9829 };
9830 let ret;
9831 if funcname.is_none() {
9832 // c:3635
9833 // c:3637 — `zerrnam(name, "bad autoload");`. zerrnam, NOT zwarnnam:
9834 // in C the `zerr*` family sets `errflag`, which aborts the enclosing
9835 // command and the script, while `zwarn*` only prints. The comment
9836 // here already cited zerrnam but the call was the warning form, so
9837 // `autoload -X` outside a function printed the diagnostic and then
9838 // kept going — `autoload -X; echo AFTER` ran AFTER where zsh stops.
9839 // (A subshell still continues in both, because only the subshell
9840 // dies; inside a function `-X` takes the real autoload path and
9841 // never reaches here.)
9842 zerrnam(name, "bad autoload"); // c:3637
9843 ret = 1; // c:3638
9844 } else {
9845 let fname = funcname.unwrap();
9846 // c:3640-3647 — getnode(shfunctab, funcname) || addnode(new shf).
9847 let mut shf_ptr = shfunctab_lock()
9848 .read()
9849 .map(|t| t.getnode(fname.as_str()))
9850 .unwrap_or(std::ptr::null_mut());
9851 if !shf_ptr.is_null() { // c:3640
9852 // exists already
9853 } else {
9854 // c:3645 — `shf = zshcalloc(sizeof *shf);`
9855 // `shfunctab->addnode(shfunctab, ztrdup(funcname), shf);`
9856 let new_shf = Box::into_raw(Box::new(shfunc {
9857 node: hashnode {
9858 next: None,
9859 nam: fname.clone(),
9860 flags: 0,
9861 },
9862 filename: None,
9863 lineno: 0,
9864 funcdef: None,
9865 redir: None,
9866 sticky: None,
9867 body: None,
9868 redir_text: None,
9869 }));
9870 if let Ok(mut t) = shfunctab_lock().write() {
9871 t.addnode(new_shf); // c:3646
9872 }
9873 shf_ptr = new_shf;
9874 }
9875 if !argv.is_empty() {
9876 // c:3648
9877 if !shf_ptr.is_null() {
9878 let shf_mut = unsafe { &mut *shf_ptr };
9879 let mut old_slot = shf_mut.filename.take();
9880 dircache_set(&mut old_slot, None); // c:3649
9881 let mut new_slot: Option<String> = None;
9882 dircache_set(&mut new_slot, Some(&argv[0])); // c:3650
9883 shf_mut.filename = new_slot;
9884 on |= PM_LOADDIR; // c:3294 — `shf->node.flags |= PM_LOADDIR;`
9885 }
9886 }
9887 // c:3653 — `shf->node.flags = on;` — `on` carries
9888 // PM_UNDEFINED (set for -X at c:3352 / builtin.rs:6142).
9889 // This write was MISSING: eval_autoload's first gate is
9890 // `if (!(shf->node.flags & PM_UNDEFINED)) return 1;`
9891 // (c:3168), so zinit's NEW_AUTOLOAD=1 stubs —
9892 // functions[add-zsh-hook]="local -a fpath; fpath=(…);
9893 // builtin autoload -X -U -z"
9894 // — silently returned 1 and every wrapped plugin autoload
9895 // (add-zsh-hook, regexp-replace, add-zle-hook-widget)
9896 // reported `function definition file not found` at first
9897 // call in the interactive session.
9898 if !shf_ptr.is_null() {
9899 unsafe {
9900 (*shf_ptr).node.flags = on as i32; // c:3653
9901 }
9902 }
9903 // RUST-ONLY: drop the executor's compiled chunk for the
9904 // CURRENT (stub) definition. C's eval_autoload -X arm
9905 // replaces shf->funcdef with mkautofn (c:3180) so the
9906 // bin_eval re-call loads the real file; zshrs's dispatch
9907 // keeps a parallel functions_compiled chunk whose
9908 // presence skips the autoload prelude — re-dispatch ran
9909 // the STALE stub (zinit NEW_AUTOLOAD=1: `builtin
9910 // autoload -X` inside the stub) in an infinite mutual
9911 // recursion → stack overflow in the interactive session.
9912 let _ = crate::ported::exec::unregister_function(&fname);
9913 // c:3654 — `ret = eval_autoload(shf, funcname, ops, func);`
9914 ret = eval_autoload(shf_ptr, &fname, ops, _func); // c:3654
9915 }
9916 unqueue_signals(); // c:3656
9917 return ret;
9918 }
9919
9920 // c:3658-3669 — no-arg listing path: print all (non-DISABLED) shfuncs
9921 // matching `on|off` mask through scanshfunc + printnode.
9922 if argv.is_empty() {
9923 // c:3658
9924 queue_signals(); // c:3663
9925 if OPT_ISSET(ops, b'U') && !OPT_ISSET(ops, b'u') {
9926 // c:3664
9927 on &= !PM_UNDEFINED; // c:3665
9928 }
9929 // c:3666 — `scanshfunc(1, on|off, DISABLED, shfunctab->printnode,
9930 // pflags, expand);` — C's scanhashtable takes a
9931 // `flags1` filter mask: only entries where `(flags & flags1) ==
9932 // flags1` are emitted. Rust's `scanshfunc` doesn't accept the
9933 // filter, so apply it inside the closure. Without this gate
9934 // `functions -t` (no args) walked every function and printed
9935 // its body — should be empty / name-only restricted to
9936 // PM_TAGGED entries. Bug #468.
9937 let filter_mask = on | off;
9938 scanshfunc(|_nm, entry| {
9939 let f = entry.node.flags as u32;
9940 if filter_mask != 0 && (f & filter_mask) != filter_mask {
9941 return;
9942 }
9943 // !!! RUST-ONLY GATE (no C counterpart) !!!
9944 // C's anonymous functions (`() { … } args`) are never entered into
9945 // shfunctab at all — execfuncdef runs the Eprog and frees it
9946 // (Src/exec.c). zshrs desugars them into a real shfunc named
9947 // `_zshrs_anon_N` (parse.rs:9528/9585) and deletes the entry after
9948 // the single call (fusevm_bridge.rs:14746), so the only window in
9949 // which one is visible is from INSIDE its own body — which is
9950 // exactly where B02typeset's `typeset -f` runs, since the ztst
9951 // harness evaluates every chunk inside `() { … }`. Skip them so
9952 // the listing matches zsh.
9953 if entry.node.nam.starts_with("_zshrs_anon_") {
9954 return;
9955 }
9956 printshfuncexpand(entry, pflags, expand);
9957 });
9958 unqueue_signals(); // c:3668
9959 return returnval;
9960 }
9961
9962 // c:3672-3708 — `-m` glob: treat each arg as a pattern, scan-and-print
9963 // matching shfuncs (no on/off → list) or apply on/off mask.
9964 if OPT_ISSET(ops, b'm') {
9965 // c:3673
9966 on &= !PM_UNDEFINED; // c:3674
9967 let mut returnval = returnval;
9968 for pat in argv {
9969 // c:3675
9970 queue_signals(); // c:3676
9971 // c:3678 — `tokenize(*argv)` + `patcompile(...)`
9972 let pprog = patcompile(
9973 &{
9974 let mut __pat_tok = crate::pattern_data_escape::escape_data_backslashes(&pat); // a backslash tokenize leaves raw is DATA (c:Src/glob.c:3651)
9975 crate::ported::glob::tokenize(&mut __pat_tok);
9976 __pat_tok
9977 }, // c:3680
9978 PAT_HEAPDUP,
9979 None,
9980 );
9981 if let Some(prog) = pprog {
9982 // c:3680-3683 — scan-and-print matching shfuncs.
9983 if (on | off) == 0 && !OPT_ISSET(ops, b'X') {
9984 // c:3682
9985 // c:3682-3683 — `scanmatchshfunc(pprog, 1, 0,
9986 // DISABLED, shfunctab->printnode, pflags, expand)`.
9987 // Walk shfunctab via the hashtable.rs port and emit
9988 // each match through `printshfuncexpand` so autoload
9989 // stubs come out as `name () { # undefined; builtin
9990 // autoload -XU }` and loaded funcs print their body.
9991 scanmatchshfunc(Some(pat), |_nm, entry| {
9992 printshfuncexpand(entry, pflags, expand);
9993 });
9994 } else {
9995 // c:3686-3699 — walk shfunctab, apply (on, off) and
9996 // re-eval autoload for each matching shf.
9997 let names: Vec<String> = shfunctab_lock()
9998 .read()
9999 .map(|t| t.iter().map(|(k, _)| k.clone()).collect())
10000 .unwrap_or_default();
10001 for nm in &names {
10002 if !pattry(&prog, nm) {
10003 // c:3690
10004 continue;
10005 }
10006 let shf_ptr = shfunctab_lock()
10007 .read()
10008 .map(|t| t.getnode(nm.as_str()))
10009 .unwrap_or(std::ptr::null_mut());
10010 if shf_ptr.is_null() {
10011 continue;
10012 }
10013 let shf_mut = unsafe { &mut *shf_ptr };
10014 // c:3691 — `shf->node.flags = (... | (on & ~PM_UNDEFINED)) & ~off;`
10015 shf_mut.node.flags =
10016 (shf_mut.node.flags | ((on & !PM_UNDEFINED) as i32)) & !(off as i32); // c:3691
10017 if check_autoload(shf_ptr, &shf_mut.node.nam, ops, _func) != 0 {
10018 // c:3693
10019 returnval = 1; // c:3695
10020 }
10021 }
10022 }
10023 } else {
10024 // c:3700-3702 — `untokenize + zwarnnam(name, "bad pattern")`.
10025 zwarnnam(name, &format!("bad pattern : {}", pat)); // c:3701
10026 returnval = 1; // c:3702
10027 }
10028 unqueue_signals(); // c:3704
10029 }
10030 return returnval;
10031 }
10032
10033 // c:3710-3735 — literal name list, no globbing.
10034 let mut returnval = returnval;
10035 queue_signals(); // c:3711
10036 for fname in argv {
10037 // c:3712
10038 // c:3713-3714 — `returnval = dump_autoload(name, *argv, on, ops, func);`
10039 // A missing dump is diagnosed by `load_dump_header` (parse.rs); a valid
10040 // one registers its functions for autoload.
10041 if OPT_ISSET(ops, b'w') {
10042 // c:3713
10043 returnval = crate::ported::parse::dump_autoload(name, fname, on as i32, ops, _func); // c:3714
10044 continue;
10045 }
10046 // c:3715 — `shf = shfunctab->getnode(shfunctab, *argv);`
10047 let shf_ptr = shfunctab_lock()
10048 .read()
10049 .map(|t| t.getnode(fname.as_str()))
10050 .unwrap_or(std::ptr::null_mut());
10051 if !shf_ptr.is_null() {
10052 // c:3715
10053 let shf_mut = unsafe { &mut *shf_ptr };
10054 if (on | off) != 0 {
10055 // c:3717
10056 // c:3719 — apply on/off mask, then check_autoload.
10057 shf_mut.node.flags =
10058 (shf_mut.node.flags | ((on & !PM_UNDEFINED) as i32)) & !(off as i32); // c:3719
10059 if check_autoload(shf_ptr, &shf_mut.node.nam, ops, _func) != 0 {
10060 // c:3720
10061 returnval = 1; // c:3721
10062 }
10063 } else {
10064 // c:3723 — `printshfuncexpand(&shf->node, pflags, expand);`
10065 // C prints the function via shfunctab.printnode honoring
10066 // pflags (PRINT_NAMEONLY / verbose). The previous Rust
10067 // port just printed the name — `functions f` skipped
10068 // the `f () { ... body ... }` body listing entirely.
10069 printshfuncexpand(shf_mut, pflags, expand); // c:3723
10070 }
10071 } else if (on & PM_UNDEFINED) != 0 {
10072 // c:3725
10073 // c:3726-3782 — autoload-define path: TRAP* + abs-path + new shf.
10074 let mut sigidx: i32 = -1;
10075 let mut ok = true;
10076 // c:3728-3735 — TRAP* prefix → removetrapnode(sigidx).
10077 if fname.starts_with("TRAP") {
10078 // c:3728
10079 // c:3729 — `if ((sigidx = getsigidx(*argv + 4)) != -1)`
10080 sigidx = getsigidx(&fname[4..]); // c:3729
10081 if sigidx != -1 {
10082 // c:3729
10083 // c:3733 — `removetrapnode(sigidx);`
10084 removetrapnode(sigidx); // c:3733
10085 }
10086 }
10087 // c:3737-3759 — absolute path /dir/base form: install dir on
10088 // existing matching base name with PM_UNDEFINED set.
10089 if fname.starts_with('/') {
10090 // c:3737
10091 let base = fname.rsplit('/').next().unwrap_or("");
10092 if !base.is_empty() {
10093 let base_ptr = shfunctab_lock()
10094 .read()
10095 .map(|t| t.getnode(base))
10096 .unwrap_or(std::ptr::null_mut());
10097 if !base_ptr.is_null() {
10098 let bs = unsafe { &mut *base_ptr };
10099 // c:3742 — apply flag mask.
10100 bs.node.flags =
10101 (bs.node.flags | ((on & !PM_UNDEFINED) as i32)) & !(off as i32); // c:3742
10102 if (bs.node.flags as u32 & PM_UNDEFINED) != 0 {
10103 // c:3744
10104 let dir = if fname.len() > 1 && base.len() == fname.len() - 1 {
10105 "/".to_string() // c:3747
10106 } else {
10107 fname[..fname.len() - base.len() - 1].to_string()
10108 // c:3749-3751
10109 };
10110 let mut old_slot = bs.filename.take();
10111 dircache_set(&mut old_slot, None); // c:3753
10112 let mut new_slot: Option<String> = None;
10113 dircache_set(&mut new_slot, Some(&dir)); // c:3754
10114 bs.filename = new_slot;
10115 }
10116 if check_autoload(base_ptr, &bs.node.nam, ops, _func) != 0 {
10117 // c:3756
10118 returnval = 1;
10119 }
10120 continue; // c:3758
10121 }
10122 }
10123 }
10124 // c:3763-3766 — new undefined shf, mkautofn, add_autoload_function.
10125 let new_shf = Box::new(shfunc {
10126 node: hashnode {
10127 next: None,
10128 nam: fname.clone(),
10129 flags: on as i32, // c:3764
10130 },
10131 filename: None,
10132 lineno: 0,
10133 funcdef: None,
10134 redir: None,
10135 sticky: None,
10136 body: None,
10137 redir_text: None,
10138 });
10139 let new_shf_ptr = Box::into_raw(new_shf);
10140 let _ = mkautofn(new_shf_ptr); // c:3765
10141 // c:3766 — `shfunc_set_sticky(shf);` stamps the pending
10142 // `emulate … -c` snapshot so the loaded body runs (and is
10143 // parsed) under that emulation (doshfunc c:5978).
10144 crate::ported::exec::shfunc_set_sticky(unsafe { &mut *new_shf_ptr });
10145 add_autoload_function(new_shf_ptr, fname); // c:3767
10146 // PFA-SMR: an `autoload NAME` registers a function stub
10147 // that fires on first call. Record it as a `function`
10148 // kind so replay re-registers the autoload. C zsh has
10149 // no recorder hook here; emit per Rust-only schema.
10150 #[cfg(feature = "recorder")]
10151 if crate::recorder::is_enabled() {
10152 let ctx = crate::recorder::recorder_ctx_global();
10153 crate::recorder::emit_function(fname, None, ctx);
10154 }
10155 if sigidx != -1 {
10156 // c:3769
10157 // c:3770 — `if (settrap(sigidx, NULL, ZSIG_FUNC)) { ... }`
10158 if settrap(sigidx, None, ZSIG_FUNC) != 0 {
10159 // c:3770
10160 // c:3771 — `shfunctab->removenode(shfunctab, *argv);`
10161 if let Ok(mut t) = shfunctab_lock().write() {
10162 t.remove(fname);
10163 }
10164 // c:3772 — `shfunctab->freenode(&shf->node);` Drop covers it.
10165 returnval = 1; // c:3773
10166 ok = false; // c:3774
10167 }
10168 }
10169 if ok && check_autoload(new_shf_ptr, &fname, ops, _func) != 0 {
10170 // c:3779
10171 returnval = 1; // c:3780
10172 }
10173 } else {
10174 // c:3783 — `returnval = 1;` (named function not found,
10175 // no autoload requested).
10176 returnval = 1; // c:3783
10177 }
10178 }
10179 unqueue_signals(); // c:3785
10180 let _ = (expand, pflags);
10181 returnval
10182}
10183
10184/// Port of `mkautofn()` from `Src/builtin.c:3790`.
10185/// C decl: `mkautofn(Shfunc shf)`
10186/// C: `Eprog mkautofn(Shfunc shf)` — synthesize a 5-wordcode body that
10187/// re-fires the autoload mechanism when first called.
10188pub fn mkautofn(shf: *mut shfunc) -> *mut eprog {
10189 // c:3790
10190 // c:3793-3810 — alloc Eprog with 5 wordcode slots, set p->shf, p->npats=0,
10191 // p->nref=1 (permanent). Static-link path: synthesize a Box<eprog> that
10192 // satisfies the autoload trampoline contract.
10193 let p = Box::new(eprog {
10194 len: 5 * size_of::<u32>() as i32, // c:3796
10195 prog: Vec::new(), // c:3797
10196 strs: None, // c:3798
10197 shf: if shf.is_null() {
10198 None
10199 }
10200 // c:3799
10201 else {
10202 Some(unsafe { Box::from_raw(shf) })
10203 },
10204 npats: 0, // c:3800
10205 nref: 1, // c:3801
10206 flags: 0,
10207 pats: Vec::new(),
10208 dump: None,
10209 strs_metafied: false, // no pool (strs: None) — flag unused
10210 });
10211 Box::into_raw(p)
10212}
10213
10214/// Port of `bin_unset()` from `Src/builtin.c:3818`.
10215/// C decl: `bin_unset(char *name, char **argv, Options ops, int func)`
10216/// C: `int bin_unset(char *name, char **argv, Options ops, int func)` —
10217/// `-f` delegates to `bin_unhash`; `-m` glob deletes matching params;
10218/// default literal-name unset with subscript handling.
10219/// WARNING: param names don't match C — Rust=(name, argv, func) vs C=(name, argv, ops, func)
10220pub fn bin_unset(
10221 name: &str,
10222 argv: &[String], // c:3818
10223 ops: &options,
10224 func: i32,
10225) -> i32 {
10226 let mut returnval = 0i32; // c:3823
10227 let mut match_count = 0i32; // c:3823
10228 // c:3884-3886 — every arm below acts on the node `getnode2` returns,
10229 // which is the VISIBLE binding. When `local -A commands` (or
10230 // `local -a aliases`) shadows a zsh/parameter row, that node is the
10231 // plain local createparam made at c:Src/params.c:1147-1156 with
10232 // `pm->old` holding the special, so neither the autoload-stub removal
10233 // (c:3874 applies to the GLOBAL PM_AUTOLOAD stub only) nor the magic
10234 // element dispatch (the local's gsu is stdhash_gsu, not the module's)
10235 // may run. zshrs keys both of those on the NAME (the stub side-set and
10236 // PARTAB), so ask what the visible node is. A `local -A +h commands`
10237 // kept the special struct (PM_SPECIAL, c:Src/builtin.c:2386-2425) and
10238 // still dispatches magically.
10239 let plain_local_shadow = |nm: &str| -> bool {
10240 paramtab().read().ok().is_some_and(|t| {
10241 t.get(nm)
10242 .is_some_and(|pm| pm.level > 0 && (pm.node.flags as u32 & PM_SPECIAL) == 0)
10243 })
10244 };
10245
10246 // PFA-SMR aspect: emit unset events for each named param. The
10247 // recorder tracks state-mutations across the shell session for
10248 // the zshrs-recorder binary's replay/inspect tooling.
10249 #[cfg(feature = "recorder")]
10250 if crate::recorder::is_enabled() {
10251 let ctx = crate::recorder::recorder_ctx_global();
10252 for a in argv {
10253 if a.starts_with('-') || a == "--" {
10254 continue;
10255 }
10256 crate::recorder::emit_unset(a, ctx.clone());
10257 }
10258 }
10259
10260 // c:3826 — `if (OPT_ISSET(ops,'f')) return bin_unhash(name, argv, ops, func);`
10261 if OPT_ISSET(ops, b'f') {
10262 // c:3826
10263 return bin_unhash(name, argv, ops, func); // c:3827
10264 }
10265
10266 // c:3830-3862 — `-m` glob.
10267 if OPT_ISSET(ops, b'm') {
10268 // c:3831
10269 for s in argv {
10270 // c:3832
10271 queue_signals(); // c:3833
10272 let pprog = patcompile(
10273 &{
10274 let mut __pat_tok = crate::pattern_data_escape::escape_data_backslashes(&s); // a backslash tokenize leaves raw is DATA (c:Src/glob.c:3651)
10275 crate::ported::glob::tokenize(&mut __pat_tok);
10276 __pat_tok
10277 }, // c:3836
10278 PAT_HEAPDUP,
10279 None,
10280 );
10281 if let Some(prog) = pprog {
10282 // c:3838-3851 — walk paramtab (NOT env::vars), unset via
10283 // unsetparam (which respects PM_NAMEREF + readonly guards).
10284 //
10285 // The previous Rust port walked `std::env::vars()` — the
10286 // OS environment. This is a different name set:
10287 // - Shell-internal vars (not exported) would survive
10288 // `unset -m 'PATTERN'` even though they match.
10289 // - Env vars not in paramtab would be removed without
10290 // the PM_READONLY guard in unsetparam_pm.
10291 //
10292 // Same family of bug as the env::var vs paramtab fixes
10293 // earlier in the series.
10294 let names: Vec<String> = {
10295 let tab = paramtab().read().unwrap();
10296 tab.keys().cloned().collect()
10297 };
10298 for nm in &names {
10299 if pattry(&prog, nm) {
10300 // c:3842
10301 // c:3846 — `unsetparam_pm(pm, 0, 1)` runs on the
10302 // node the walk found. For a magic assoc/array
10303 // nothing has read yet that node is still the
10304 // PM_AUTOLOAD stub (Src/module.c:1218-1223): a
10305 // plain PM_SCALAR with no PM_READONLY, so it is
10306 // removed silently (c:3874). zshrs seeds the real
10307 // special instead, whose PM_READONLY would reject
10308 // the glob — `unset -m 'func*'` died on
10309 // `functrace` where zsh unsets the lot. Same
10310 // stub-state branch as the literal-name arm below.
10311 if crate::vm_helper::module_param_is_autoload_stub(nm)
10312 && !plain_local_shadow(nm)
10313 && (crate::ported::modules::parameter::PARTAB
10314 .iter()
10315 .any(|e| e.name == nm.as_str())
10316 || crate::ported::modules::parameter::PARTAB_ARRAY
10317 .iter()
10318 .any(|e| e.name == nm.as_str()))
10319 {
10320 let _ = paramtab().write().ok().map(|mut t| t.remove(nm)); // c:3874
10321 let _ = crate::ported::params::paramtab_hashed_storage()
10322 .lock()
10323 .ok()
10324 .as_deref_mut()
10325 .map(|m| m.remove(nm.as_str()));
10326 match_count += 1; // c:3848
10327 continue;
10328 }
10329 // c:3843-3846 — `if (OPT_ISSET(ops,'n') || ((pm =
10330 // resolve_nameref(pm)) && !(pm->node.flags &
10331 // PM_NAMEREF))) unsetparam_pm(pm, 0, 1);` — the
10332 // same test the literal-name arm applies at
10333 // c:3949-3951. unsetparam() skips namerefs, so
10334 // route a matched ref through that arm (the `-m`
10335 // bit cleared, `-n` kept) instead of dropping it.
10336 if crate::ported::params::is_nameref(nm) {
10337 let mut lit_ops = ops.clone();
10338 lit_ops.ind[b'm' as usize] = 0;
10339 bin_unset(name, std::slice::from_ref(nm), &lit_ops, func);
10340 match_count += 1; // c:3848
10341 continue;
10342 }
10343 unsetparam(nm); // c:3847 (with guards)
10344 match_count += 1; // c:3848
10345 }
10346 }
10347 } else {
10348 zwarnnam(name, &format!("bad pattern : {}", s)); // c:3854
10349 returnval = 1; // c:3855
10350 }
10351 unqueue_signals(); // c:3857
10352 }
10353 if match_count == 0 {
10354 // c:3861
10355 returnval = 1; // c:3862
10356 }
10357 return returnval; // c:3863
10358 }
10359
10360 // c:3866-3915 — literal-name unset with optional subscript.
10361 queue_signals(); // c:3867
10362 for s in argv {
10363 // c:3868
10364 // c:3869-3878 — extract `name[subscript]` shape.
10365 let (nm, subscript) = match s.find('[') {
10366 // c:3869
10367 Some(start) if s.ends_with(']') => {
10368 // c:3873
10369 (&s[..start], Some(&s[start + 1..s.len() - 1])) // c:3875
10370 }
10371 Some(_) => {
10372 // c:3873-3879 — bracket without `]` close → invalid.
10373 zerrnam(name, &format!("{}: invalid parameter name", crate::ported::utils::nicedupstring(&s))); // c:3876 — `%s` is nicezputs (c:Src/utils.c:316)
10374 returnval = 1; // c:3877
10375 continue; // c:3878
10376 }
10377 None => (s.as_str(), None),
10378 };
10379 // c:3873 — `if ((ss && !subscript) || !isident(s))`. isident
10380 // (c:Src/params.c:1309) takes an all-digit name (`unset 1` is a
10381 // no-op, as no such parameter exists) and namespaced `.a.b`. The
10382 // diagnostic is zerrnam: errflag is set and the rest of the
10383 // command list is abandoned.
10384 if !isident(nm) {
10385 zerrnam(name, &format!("{}: invalid parameter name", crate::ported::utils::nicedupstring(&s))); // c:3876 — `%s` is nicezputs (c:Src/utils.c:316)
10386 returnval = 1; // c:3877
10387 continue; // c:3878
10388 }
10389 // c:3886-3905 — `if (!pm) continue;` then unset.
10390 // C `unsetparam_pm` dispatches on `pm->gsu` (the gsu_*
10391 // accessor for the param's type): assoc gets
10392 // `gsu_a->unset(pm, subscript)`, array gets
10393 // `gsu_arr->unset(pm, subscript)`, scalar gets `unsetparam`.
10394 match subscript {
10395 // c:3886
10396 Some(key) => {
10397 // c:Src/Modules/mapfile.c:126 unsetpmmapfile — unsetting
10398 // an element of the special $mapfile assoc UNLINKS the
10399 // file named by the key. It is not a regular assoc, so
10400 // route here before the generic assoc/array delete.
10401 if nm == "mapfile" {
10402 crate::ported::modules::mapfile::unsetpmmapfile(key, false);
10403 continue;
10404 }
10405 // c:3891-3895 — `if (PM_TYPE(pm->node.flags) == PM_HASHED) {
10406 // HashTable tht = paramtab;
10407 // if ((paramtab = pm->gsu.h->getfn(pm))) unsetparam(subscript);
10408 // paramtab = tht; }`
10409 //
10410 // For the zsh/parameter magic associations the hash that
10411 // `gsu.h->getfn` hands back is NOT a value store: its
10412 // `getnode` is `getpmfunction` / `getpmralias` / … (each
10413 // synthesising a child Param whose scalar gsu carries the
10414 // element `unsetfn`), and that `unsetfn` mutates the REAL
10415 // shell table — `shfunctab`, `aliastab`, `cmdnamtab`,
10416 // `nameddirtab`, the option flags. Deleting a key from a
10417 // value snapshot and writing the snapshot back (the generic
10418 // assoc path below) cannot express that: the whole-hash
10419 // setfns (`setpmfunctions`, c:344-365) only ADD entries, so
10420 // `unset "functions[m]"` left the shell function defined.
10421 //
10422 // Mirror of the element-SET dispatch in `assignsparam`
10423 // (`src/ported/params.rs:6455`), which routes
10424 // `functions[m]=body` to `setpmfunction` for the same reason.
10425 if !plain_local_shadow(nm) {
10426 use crate::ported::modules::parameter as pmod;
10427 use crate::ported::zsh_h::Param;
10428 // c:3884-3886 — `pm = (paramtab == realparamtab ?
10429 // paramtab->getnode2(paramtab, s) : paramtab->getnode(...))`
10430 // — the comment on that line is `getnode2() to avoid
10431 // autoloading`. A magic assoc that has never been touched
10432 // is still the PM_AUTOLOAD stub `zsh/parameter` planted
10433 // (c:Src/module.c:1218-1219), and getnode2 does NOT run
10434 // loadparamnode, so `PM_TYPE(pm->node.flags)` is PM_SCALAR,
10435 // not PM_HASHED: control falls into the scalar arm at
10436 // c:3896-3919, where `getindex` rejects the string
10437 // subscript with `assignment to invalid subscript range`
10438 // and the function/alias is left alone. Verified against
10439 // zsh 5.9.2: `f(){ :; }; unset "functions[f]"; whence f`
10440 // still prints `f`, while the same unset AFTER any read of
10441 // `$functions` removes it.
10442 if crate::vm_helper::module_param_is_autoload_stub(nm) {
10443 // c:Src/params.c getindex — `zerr("%s: assignment to
10444 // invalid subscript range", ...)`; bin_unset then does
10445 // `returnval = errflag; errflag &= ~ERRFLAG_ERROR;`
10446 // (c:3917-3918), so the status is 1 but the script is
10447 // NOT aborted.
10448 zwarn(&format!("{}: assignment to invalid subscript range", nm));
10449 returnval = 1; // c:3917
10450 continue;
10451 }
10452 // c:3893 `paramtab->getnode(paramtab, subscript)` (via
10453 // unsetparam, c:3763-3768) paired with the element gsu's
10454 // `unsetfn`. `None` for the unsetfn is C's NULL slot in
10455 // `nullsetscalar_gsu` (`Src/params.c:168-169`), used by
10456 // every PM_READONLY_SPECIAL hash — unreachable because
10457 // `unsetparam_pm` rejects on PM_READONLY first (c:3785).
10458 type ElemUnset = fn(Param, i32);
10459 let nul: *mut crate::ported::zsh_h::HashTable = std::ptr::null_mut();
10460 let hashed: Option<(Option<Param>, Option<ElemUnset>)> = match nm {
10461 // c:2281 SPECIALPMDEF("functions", …, getpmfunction),
10462 // c:394-395 pmfunction_gsu { …, unsetpmfunction }.
10463 "functions" => Some((
10464 pmod::getpmfunction(nul, key),
10465 Some(pmod::unsetpmfunction as ElemUnset),
10466 )),
10467 // c:2261-2262 + c:396-397 pmdisfunction_gsu shares
10468 // `unsetpmfunction`.
10469 "dis_functions" => Some((
10470 pmod::getpmdisfunction(nul, key),
10471 Some(pmod::unsetpmfunction as ElemUnset),
10472 )),
10473 // c:2251 + c:1867-1868 pmralias_gsu { …, unsetpmalias }.
10474 "aliases" => Some((
10475 pmod::getpmralias(nul, key),
10476 Some(pmod::unsetpmalias as ElemUnset),
10477 )),
10478 // c:2287-2288 + c:1869-1870 pmgalias_gsu.
10479 "galiases" => Some((
10480 pmod::getpmgalias(nul, key),
10481 Some(pmod::unsetpmalias as ElemUnset),
10482 )),
10483 // c:2258-2259 + c:1873-1874 pmdisralias_gsu.
10484 "dis_aliases" => Some((
10485 pmod::getpmdisralias(nul, key),
10486 Some(pmod::unsetpmalias as ElemUnset),
10487 )),
10488 // c:2266-2267 + c:1875-1876 pmdisgalias_gsu.
10489 "dis_galiases" => Some((
10490 pmod::getpmdisgalias(nul, key),
10491 Some(pmod::unsetpmalias as ElemUnset),
10492 )),
10493 // c:2312-2313 + c:1871-1872 pmsalias_gsu { …,
10494 // unsetpmsalias } (sufaliastab, not aliastab).
10495 "saliases" => Some((
10496 pmod::getpmsalias(nul, key),
10497 Some(pmod::unsetpmsalias as ElemUnset),
10498 )),
10499 // c:2272-2273 + c:1877-1878 pmdissalias_gsu.
10500 "dis_saliases" => Some((
10501 pmod::getpmdissalias(nul, key),
10502 Some(pmod::unsetpmsalias as ElemUnset),
10503 )),
10504 // c:2256 + c:209-210 pmcommand_gsu { …, unsetpmcommand }.
10505 "commands" => Some((
10506 pmod::getpmcommand(nul, key),
10507 Some(pmod::unsetpmcommand as ElemUnset),
10508 )),
10509 // c:2301-2302 + c:1607-1608 pmnamedir_gsu.
10510 "nameddirs" => Some((
10511 pmod::getpmnameddir(nul, key),
10512 Some(pmod::unsetpmnameddir as ElemUnset),
10513 )),
10514 // c:2303-2304 + c:997-998 pmoption_gsu { …,
10515 // unsetpmoption } — turns the option OFF.
10516 "options" => Some((
10517 pmod::getpmoption(nul, key),
10518 Some(pmod::unsetpmoption as ElemUnset),
10519 )),
10520 // PM_READONLY_SPECIAL hashes (c:2255, 2259-2260,
10521 // 2264-2265, 2269-2270, 2290-2291, 2296-2300,
10522 // 2305-2306, 2314-2317): their `getpm*` stamps
10523 // PM_SCALAR|PM_READONLY, so the element unset is
10524 // rejected by unsetparam_pm's readonly guard.
10525 "parameters" => Some((pmod::getpmparameter(nul, key), None)),
10526 "builtins" => Some((pmod::getpmbuiltin(nul, key), None)),
10527 "dis_builtins" => Some((pmod::getpmdisbuiltin(nul, key), None)),
10528 "functions_source" => Some((pmod::getpmfunction_source(nul, key), None)),
10529 "dis_functions_source" => {
10530 Some((pmod::getpmdisfunction_source(nul, key), None))
10531 }
10532 // c:1051-1056 — getpmmodule starts with
10533 // `m = modulestab->getnode2(modulestab, name);
10534 // if (!m) return NULL;`, so a name that is not a
10535 // known module yields NO node and `unset
10536 // "modules[bogus]"` is a silent no-op (rc 0), while a
10537 // KNOWN module hits the PM_READONLY rejection. zshrs's
10538 // getpmmodule port never returns None — it folds the
10539 // unknown-name case into the "no type string" arm
10540 // (c:1068-1069, PM_UNSET|PM_SPECIAL), which is pinned
10541 // by its unit test — so re-apply C's `!m` gate here:
10542 // an empty type string only means NULL-in-C when the
10543 // module is absent from modulestab.
10544 "modules" => Some((
10545 pmod::getpmmodule(nul, key).filter(|p| {
10546 (p.node.flags as u32 & PM_UNSET) == 0
10547 || crate::ported::module::MODULESTAB
10548 .lock()
10549 .map(|t| t.modules.contains_key(key))
10550 .unwrap_or(false)
10551 }),
10552 None,
10553 )),
10554 "history" => Some((pmod::getpmhistory(nul, key), None)),
10555 "jobdirs" => Some((pmod::getpmjobdir(nul, key), None)),
10556 "jobstates" => Some((pmod::getpmjobstate(nul, key), None)),
10557 "jobtexts" => Some((pmod::getpmjobtext(nul, key), None)),
10558 "userdirs" => Some((pmod::getpmuserdir(nul, key), None)),
10559 "usergroups" => Some((pmod::getpmusergroups(nul, key), None)),
10560 _ => None,
10561 };
10562 if let Some((node, unsetfn)) = hashed {
10563 // c:3765-3768 — `if ((pm = paramtab->getnode(...)))
10564 // unsetparam_pm(pm, 0, 1);` — a NULL node (unknown
10565 // module) is silently ignored.
10566 if let Some(pm) = node {
10567 let f = pm.node.flags as u32;
10568 if (f & PM_READONLY) != 0 {
10569 // c:3785-3790 — `zerr("read-only %s: %s", …)`.
10570 zerr(&format!("read-only variable: {}", pm.node.nam));
10571 } else if (f & PM_UNSET) == 0 {
10572 // c:3805-3806 — `if (!(pm->node.flags &
10573 // PM_UNSET) || (pm->node.flags &
10574 // PM_REMOVABLE)) pm->gsu.s->unsetfn(pm, exp);`
10575 // A missing key comes back PM_UNSET (e.g.
10576 // getfunction c:437-440), so the unsetfn is
10577 // skipped and `unset "functions[nope]"` is a
10578 // silent no-op, as in zsh.
10579 if let Some(uf) = unsetfn {
10580 uf(pm, 1); // c:3806 exp=1 from unsetparam
10581 }
10582 }
10583 }
10584 // c:3895 `paramtab = tht;` — nothing else to do for
10585 // this argument; the generic assoc/array delete below
10586 // must not also run.
10587 continue;
10588 }
10589 }
10590 // c:3893 assoc subscript: `m[key]` delete.
10591 if let Some(mut map) = crate::ported::exec::assoc(nm) {
10592 map.shift_remove(key); // c:3893
10593 crate::ported::exec::set_assoc(nm, map);
10594 } else if let Some(mut arr) = crate::ported::exec::array(nm) {
10595 // c:3895 — array subscript: `arr[N]` sets element
10596 // N to empty; `arr[N,M]` clears the inclusive
10597 // range. The previous Rust port only handled the
10598 // single-index form; `unset arr[2,3]` was a no-op.
10599 if !crate::dash_mode::sparse_arrays() {
10600 // c:3896-3916 — `vbuf.scanflags = SCANPM_ARRONLY; vbuf.start
10601 // = 0; vbuf.end = -1; … if (getindex(&ss, &vbuf,
10602 // SCANPM_ASSIGNING) == 0 && vbuf.pm && !(PM_UNSET))`: the
10603 // subscript goes through getindex, so KSHARRAYS /
10604 // KSHZEROSUBSCRIPT, arithmetic (`a[i]`, `a[1+1]`), ranges
10605 // with negative ends and the (i)/(r) flags all resolve as
10606 // in an assignment. The selected slice is then replaced
10607 // by one empty element (setarrvalue, Src/params.c:2933-2990).
10608 let pm = paramtab().read().ok().and_then(|t| t.get(nm).cloned());
10609 let mut vbuf = crate::ported::zsh_h::value {
10610 pm,
10611 arr: Vec::new(),
10612 scanflags: crate::ported::zsh_h::SCANPM_ARRONLY as i32, // c:3898
10613 valflags: 0, // c:3900
10614 start: 0, // c:3901
10615 end: -1, // c:3902
10616 };
10617 // c:1736-1760 — on an array, getarg's (r)/(R) (and
10618 // (k)/(K), c:1401/1406) return the matched INDEX, exactly
10619 // what (i)/(I) return; this port's getindex hands the
10620 // matched element back in `v->arr` instead, so ask for
10621 // the index form.
10622 let search_as_index = key
10623 .strip_prefix('(')
10624 .and_then(|r| r.split_once(')'))
10625 .is_some_and(|(f, _)| f.contains(['r', 'R', 'k', 'K']) && !f.contains(['i', 'I']));
10626 let key: std::borrow::Cow<str> = match key.strip_prefix('(').and_then(|r| r.split_once(')')) {
10627 Some((flags, rest)) if !flags.contains(['i', 'I']) => format!(
10628 "({}){}",
10629 flags.replace(['r', 'k'], "i").replace(['R', 'K'], "I"),
10630 rest
10631 )
10632 .into(),
10633 _ => key.into(),
10634 };
10635 let bracketed = format!("[{}]", key); // c:3904 `*ss = '['`
10636 let mut sp: &str = &bracketed;
10637 if crate::ported::params::getindex(
10638 &mut sp,
10639 &mut vbuf,
10640 crate::ported::zsh_h::SCANPM_ASSIGNING as i32,
10641 ) == 0
10642 && vbuf
10643 .pm
10644 .as_ref()
10645 .is_some_and(|p| (p.node.flags as u32 & PM_UNSET) == 0)
10646 {
10647 if search_as_index && vbuf.start == 0 && (vbuf.valflags & crate::ported::zsh_h::VALFLAG_INV) != 0 {
10648 // c:1758-1760 — a reverse (R) search that misses
10649 // returns index 0. Without `ind` that reaches
10650 // getindex's range tail as `start == end == 0`
10651 // (c:2126-2150), not the (I) inverse arm.
10652 vbuf.valflags &= !crate::ported::zsh_h::VALFLAG_INV;
10653 if isset(crate::ported::zsh_h::KSHZEROSUBSCRIPT) {
10654 vbuf.end = 1; // c:2140 `end = startnextlen`
10655 } else {
10656 vbuf.valflags |= crate::ported::zsh_h::VALFLAG_EMPTY; // c:2147
10657 vbuf.start = -1; // c:2148
10658 vbuf.end = 0;
10659 }
10660 }
10661 let inv = (vbuf.valflags & crate::ported::zsh_h::VALFLAG_INV) != 0;
10662 // c:3912-3914 — "start is after the element for
10663 // reverse index"; only an existing element is unset.
10664 let start = vbuf.start - inv as i32;
10665 // `arrlen_gt(arr, start)` takes the bound UNSIGNED and
10666 // tests `arrlen >= 1 + bound` (Src/utils.c:2384-2387),
10667 // so -1 wraps to 0 (true: `unset 'a[-1]'` clears the
10668 // last element) while every other negative start is
10669 // a huge bound (false: `unset 'a[-2]'` is a no-op).
10670 let in_range = start == -1 || (start >= 0 && arr.len() > start as usize);
10671 if in_range && (vbuf.valflags & crate::ported::zsh_h::VALFLAG_EMPTY) != 0 {
10672 // c:2913-2917 (setarrvalue) — the strict zero
10673 // subscript (getindex c:2141-2150). zerr is a
10674 // no-op once an error is already flagged
10675 // (`a[1/0]` reported its own).
10676 if (errflag.load(Relaxed) & ERRFLAG_ERROR) == 0 {
10677 zerr(&format!("{}: assignment to invalid subscript range", nm));
10678 }
10679 } else if in_range {
10680 // c:2938-2955 (setarrvalue) — bounds normalisation.
10681 let oldlen = arr.len() as i32;
10682 let (mut s, mut e) = (vbuf.start, vbuf.end);
10683 if inv && !isset(crate::ported::zsh_h::KSHARRAYS) {
10684 if s > 0 {
10685 s -= 1;
10686 }
10687 e -= 1;
10688 }
10689 if s < 0 {
10690 s = (s + oldlen).max(0);
10691 }
10692 if e < 0 {
10693 e = (e + oldlen + 1).max(0);
10694 }
10695 if e < s || s > oldlen {
10696 e = s;
10697 } else if e > oldlen {
10698 e = oldlen;
10699 }
10700 // c:2957-2990 — splice `("")` over [s, e).
10701 arr.splice(s as usize..e as usize, [String::new()]);
10702 crate::ported::exec::set_array(nm, arr);
10703 }
10704 }
10705 // c:3917-3918 — `returnval = errflag; errflag &=
10706 // ~ERRFLAG_ERROR;`
10707 if (errflag.load(Relaxed) & ERRFLAG_ERROR) != 0 {
10708 returnval = 1;
10709 }
10710 errflag.fetch_and(!ERRFLAG_ERROR, Relaxed);
10711 } else {
10712 // !!! BASH-MODE GATE !!! bash's own index rules, unchanged.
10713 let len_i = arr.len() as i32;
10714 let resolve = |raw: i32| -> Option<usize> {
10715 let pos = if raw < 0 { len_i + raw + 1 } else { raw };
10716 if pos >= 1 && pos as usize <= arr.len() {
10717 Some((pos - 1) as usize)
10718 } else {
10719 None
10720 }
10721 };
10722 if let Some((s, e)) = key.split_once(',') {
10723 // c:Src/params.c — `unset arr[N,M]` replaces
10724 // the N..M range with ONE empty element. For
10725 // (a b c d), `unset arr[2,3]` → (a "" d).
10726 // Negative bounds aren't supported by zsh's
10727 // unset (it leaves the array unchanged), so
10728 // restrict to positive forms.
10729 let s_i = s.trim().parse::<i32>().ok();
10730 let e_i = e.trim().parse::<i32>().ok();
10731 if let (Some(start), Some(end)) = (s_i, e_i) {
10732 if start >= 1 && end >= 1 {
10733 let from = (start - 1) as usize;
10734 let to = (end - 1) as usize;
10735 if from < arr.len() && from <= to {
10736 let cap_to = to.min(arr.len() - 1);
10737 let mut new_arr: Vec<String> = Vec::with_capacity(arr.len());
10738 new_arr.extend(arr[..from].iter().cloned());
10739 new_arr.push(String::new());
10740 new_arr.extend(arr[cap_to + 1..].iter().cloned());
10741 crate::ported::exec::set_array(nm, new_arr);
10742 }
10743 }
10744 }
10745 } else if crate::dash_mode::sparse_arrays() {
10746 // bash sparse arrays: subscripts are 0-based and
10747 // `unset a[i]` leaves a HOLE (index gap) rather than
10748 // shifting or clearing to a dense empty. Negative
10749 // indexes count from the end. The dense Vec keeps a
10750 // "" placeholder; bash_arrays tracks the hole so
10751 // `${a[@]}`/`${#a[@]}`/`${!a[@]}` skip it.
10752 if let Ok(i) = key.parse::<i32>() {
10753 let len_i = arr.len() as i32;
10754 let idx0 = if i < 0 { len_i + i } else { i };
10755 if idx0 >= 0 && (idx0 as usize) < arr.len() {
10756 let idx = idx0 as usize;
10757 arr[idx] = String::new();
10758 crate::bash_arrays::note_unset(nm, idx);
10759 crate::ported::exec::set_array(nm, arr);
10760 }
10761 }
10762 } else if let Ok(i) = key.parse::<i32>() {
10763 // c:Src/params.c — single-index unset only
10764 // accepts positive subscripts and the special
10765 // -1 form (clear last element). Other negative
10766 // values are a no-op in zsh — verified
10767 // empirically: `unset arr[-2]`, `arr[-3]` etc.
10768 // leave the array unchanged.
10769 // `unset arr[0]` is invalid (unless
10770 // KSHZEROSUBSCRIPT) and emits a diagnostic.
10771 if i == 0 {
10772 zwarn(&format!("{}: assignment to invalid subscript range", nm));
10773 returnval = 1;
10774 } else if i == -1 {
10775 if !arr.is_empty() {
10776 let idx = arr.len() - 1;
10777 arr[idx] = String::new();
10778 crate::ported::exec::set_array(nm, arr);
10779 }
10780 } else if i > 0 {
10781 if let Some(idx) = resolve(i) {
10782 arr[idx] = String::new();
10783 crate::ported::exec::set_array(nm, arr);
10784 }
10785 }
10786 // Other negative values: no-op (zsh behavior).
10787 }
10788 }
10789 // c:Src/params.c:2922 — a subscript unset of a tied
10790 // colon-array (`unset path[2]`) re-derives the scalar
10791 // side, just like the assignment paths in params.rs.
10792 if let Some((_, scalar)) = crate::ported::params::TIED_COLON_ARRAYS
10793 .iter()
10794 .find(|(a, _)| *a == nm)
10795 {
10796 let cur = crate::ported::exec::array(nm).unwrap_or_default();
10797 crate::ported::params::assignsparam(scalar, &cur.join(":"), 0);
10798 }
10799 } else {
10800 // c:3897-3901 — `if ((pm->node.flags & PM_NAMEREF) &&
10801 // !(pm = resolve_nameref(pm))) continue;` — the element
10802 // unset acts on the referent.
10803 let (target, tflags) = if crate::ported::params::is_nameref(nm) {
10804 use crate::ported::params::nameref_resolution;
10805 match crate::ported::params::resolve_nameref_name(nm, None) {
10806 nameref_resolution::Target {
10807 name: t,
10808 subscript: None,
10809 pm: Some(p),
10810 ..
10811 } => {
10812 let f = p.node.flags as u32;
10813 (t, f)
10814 }
10815 // c:params.c:6341-6342 — a placeholder chain
10816 // resolves to the (PM_NAMEREF) ref itself, which
10817 // falls through to the c:3919 type check below.
10818 nameref_resolution::Placeholder(last, _) => {
10819 let f = paramtab()
10820 .read()
10821 .ok()
10822 .and_then(|t| t.get(&last).map(|p| p.node.flags as u32))
10823 .unwrap_or(PM_NAMEREF);
10824 (last, f)
10825 }
10826 _ => continue,
10827 }
10828 } else {
10829 // c:3886-3890 — `getnode2`; `if (!pm) continue;`
10830 match paramtab()
10831 .read()
10832 .ok()
10833 .and_then(|t| t.get(nm).map(|pm| pm.node.flags as u32))
10834 {
10835 Some(f) => (nm.to_string(), f),
10836 None => continue,
10837 }
10838 };
10839 if PM_TYPE(tflags) == PM_SCALAR {
10840 // c:3902-3915 — `vbuf.pm = pm; vbuf.start = 0;
10841 // vbuf.end = -1; … if (getindex(&ss, &vbuf,
10842 // SCANPM_ASSIGNING) == 0 && vbuf.pm &&
10843 // !(vbuf.pm->node.flags & PM_UNSET))
10844 // setstrvalue(&vbuf, ztrdup(""));` — the subscripted
10845 // character range is spliced out (`var=value;
10846 // unset "var[2,3]"` → vue). C's vbuf aliases the
10847 // table node; the Rust value owns a clone, so the
10848 // mutated node is written back.
10849 let pm = paramtab().read().ok().and_then(|t| t.get(&target).cloned());
10850 let mut vbuf = crate::ported::zsh_h::value {
10851 pm,
10852 arr: Vec::new(),
10853 scanflags: 0, // c:3903-3904 (PM_SCALAR)
10854 valflags: 0, // c:3906
10855 start: 0, // c:3907
10856 end: -1, // c:3908
10857 };
10858 let bracketed = format!("[{}]", key); // c:3910 `*ss = '['`
10859 let mut sp: &str = &bracketed;
10860 if crate::ported::params::getindex(
10861 &mut sp,
10862 &mut vbuf,
10863 crate::ported::zsh_h::SCANPM_ASSIGNING as i32,
10864 ) == 0
10865 && vbuf
10866 .pm
10867 .as_ref()
10868 .is_some_and(|p| (p.node.flags as u32 & PM_UNSET) == 0)
10869 {
10870 crate::ported::params::setstrvalue(Some(&mut vbuf), ""); // c:3914
10871 if let Some(pm_back) = vbuf.pm {
10872 if let Ok(mut tab) = paramtab().write() {
10873 tab.insert(target.clone(), pm_back);
10874 }
10875 }
10876 }
10877 // c:3917-3918 — `returnval = errflag; errflag &=
10878 // ~ERRFLAG_ERROR;`
10879 if (errflag.load(Relaxed) & ERRFLAG_ERROR) != 0 {
10880 returnval = 1;
10881 }
10882 errflag.fetch_and(!ERRFLAG_ERROR, Relaxed);
10883 } else if PM_TYPE(tflags) != PM_ARRAY && PM_TYPE(tflags) != PM_HASHED {
10884 // c:3919-3921
10885 zerrnam(name, &format!("{}: invalid element for unset", nm));
10886 returnval = 1;
10887 }
10888 }
10889 }
10890 None => {
10891 // c:3915-3919 (54722) — nameref handling. Without -n the
10892 // ref resolves and the TARGET is unset with unsetparam_pm,
10893 // whatever scope it lives in. With -n the ref itself is
10894 // removed.
10895 let mut nm: &str = nm;
10896 let mut resolved_target: Option<String> = None; // non-ref target to unset
10897 let mut ref_removal: Option<String> = None; // ref entry to remove
10898 let mut handled = false;
10899 if crate::ported::params::is_nameref(nm) {
10900 if OPT_ISSET(ops, b'n') {
10901 ref_removal = Some(nm.to_string()); // unset -n: the ref itself
10902 } else {
10903 use crate::ported::params::nameref_resolution;
10904 match crate::ported::params::resolve_nameref_name(nm, None) {
10905 // c:3942-3943 — `if (!(pm = resolve_nameref(pm))) continue;`
10906 nameref_resolution::SelfRef | nameref_resolution::OutOfScope => {
10907 continue
10908 }
10909 nameref_resolution::Placeholder(..) => {
10910 // c:3949-3951 — `(pm = resolve_nameref(pm))
10911 // && !(pm->node.flags & PM_NAMEREF)`: a chain
10912 // ending at a placeholder ref resolves to that
10913 // ref, which fails the PM_NAMEREF test, so
10914 // nothing is unset.
10915 continue;
10916 }
10917 nameref_resolution::Target {
10918 name: t,
10919 subscript,
10920 pm,
10921 level,
10922 } => {
10923 if subscript.is_some() {
10924 // c:3897 — subscripted ref: resolve_nameref
10925 // early-exits (pm->width); the REF itself
10926 // is the unset object.
10927 ref_removal = Some(nm.to_string());
10928 } else if pm.is_none() {
10929 // dangling — nothing to unset (c:3942).
10930 continue;
10931 } else {
10932 // c:3952 — `unsetparam_pm(pm, 0, 1)` on
10933 // the RESOLVED struct. When that struct
10934 // is the visible binding, the ordinary
10935 // unsetparam path below is exactly it.
10936 // When it is hidden by a local of the
10937 // same name (upscope picked an `old`
10938 // node), unset that node in place.
10939 let visible = paramtab()
10940 .read()
10941 .ok()
10942 .and_then(|tab| tab.get(&t).map(|p| p.level == level))
10943 .unwrap_or(true);
10944 if visible {
10945 resolved_target = Some(t);
10946 } else {
10947 // paramtab is keyed by name with the
10948 // chain owned by the visible node, so
10949 // take it out, walk to `level`, and
10950 // unset that node.
10951 let top = paramtab().write().ok().and_then(|mut tab| tab.remove(&t));
10952 if let Some(mut top) = top {
10953 let mut defer_global = false;
10954 let mut cur = top.old.as_deref_mut();
10955 while let Some(hpm) = cur {
10956 if hpm.level == level {
10957 if crate::ported::params::unsetparam_pm(hpm, 0, 1) != 0 {
10958 returnval = 1; // c:3953
10959 } else if hpm.level == 0 {
10960 // c:3859-3869 — a hidden
10961 // global cannot be removed
10962 // yet: `zpushnode(scoperefs[0],
10963 // pm)`; endparamscope deletes
10964 // it once uncovered. A local
10965 // keeps its node (c:3851-3853).
10966 defer_global = true;
10967 }
10968 break;
10969 }
10970 cur = hpm.old.as_deref_mut();
10971 }
10972 if let Ok(mut tab) = paramtab().write() {
10973 tab.insert(t.clone(), top);
10974 }
10975 if defer_global {
10976 crate::ported::params::SCOPEREFS.with(|sr| {
10977 let mut sr = sr.borrow_mut();
10978 if sr.is_empty() {
10979 sr.resize(8, Vec::new()); // c:3865
10980 }
10981 sr[0].insert(0, t.clone()); // c:3868
10982 });
10983 }
10984 }
10985 handled = true;
10986 }
10987 }
10988 }
10989 nameref_resolution::NotRef => {}
10990 }
10991 }
10992 if handled {
10993 continue;
10994 }
10995 if let Some(refnam) = ref_removal {
10996 // unsetparam() deliberately skips namerefs, so
10997 // do the table surgery here with the readonly
10998 // guard (c:3850 via unsetparam_pm).
10999 let ro = paramtab()
11000 .read()
11001 .ok()
11002 .and_then(|t| {
11003 t.get(&refnam)
11004 .map(|p| (p.node.flags as u32 & PM_READONLY) != 0)
11005 })
11006 .unwrap_or(false);
11007 if ro {
11008 zerr(&format!("read-only variable: {}", refnam)); // c:3850
11009 returnval = 1;
11010 continue;
11011 }
11012 // c:3911-3913 — a LOCAL ref is kept in the
11013 // table marked PM_UNSET (scope-end removes it);
11014 // only globals/level-0 refs drop out entirely.
11015 let cur_ll = locallevel.load(Relaxed) as i32;
11016 let keep_local = paramtab()
11017 .read()
11018 .ok()
11019 .and_then(|t| t.get(&refnam).map(|p| p.level > 0 && cur_ll >= p.level))
11020 .unwrap_or(false);
11021 if keep_local {
11022 if let Ok(mut tab) = paramtab().write() {
11023 if let Some(p) = tab.get_mut(&refnam) {
11024 // c:3858 + c:3870 stdunsetfn
11025 p.node.flags &= !(PM_DECLARED as i32);
11026 p.node.flags |= PM_UNSET as i32;
11027 p.u_str = None;
11028 p.base = 0;
11029 p.width = 0;
11030 }
11031 }
11032 } else if let Some(mut pm) =
11033 paramtab().write().ok().and_then(|mut t| t.remove(&refnam))
11034 {
11035 if let Some(prev) = pm.old.take() {
11036 if let Ok(mut tab) = paramtab().write() {
11037 tab.insert(refnam.clone(), prev);
11038 }
11039 }
11040 }
11041 continue;
11042 }
11043 }
11044 let target_buf;
11045 if let Some(t) = resolved_target {
11046 target_buf = t;
11047 nm = &target_buf;
11048 }
11049 // c:3884-3886 — `pm = (paramtab == realparamtab ?
11050 // paramtab->getnode2(paramtab, s) : paramtab->getnode(
11051 // paramtab, s))` — the comment on that line is
11052 // `getnode2() to avoid autoloading`. A magic assoc /
11053 // array that nothing has READ yet is still the
11054 // `PM_AUTOLOAD` stub `add_autoparam` planted
11055 // (c:Src/module.c:1218-1223 `setsparam(pnam, module);
11056 // pm->node.flags |= PM_AUTOLOAD`), i.e. a PLAIN
11057 // `PM_SCALAR` node at level 0 whose value is the owning
11058 // module's name — NOT the `SPECIALPMDEF` row. So
11059 // `unsetparam_pm` on it:
11060 // * skips the readonly guard (c:3786) — the stub has
11061 // no PM_READONLY, which is why `unset parameters`
11062 // succeeds in zsh where the materialized special
11063 // would be rejected;
11064 // * fails c:3851-3852's keep-the-node test
11065 // `(flags & (PM_SPECIAL|PM_REMOVABLE)) == PM_SPECIAL`
11066 // and falls through to c:3874
11067 // `paramtab->removenode(paramtab, pm->node.nam)`.
11068 // The name is then unbound: `${#functions}` is 0,
11069 // `functions[k]=v` hits c:2700's "assignment to invalid
11070 // subscript range", and `functions=(a b)` makes an
11071 // ORDINARY array.
11072 //
11073 // zshrs seeds the real special eagerly
11074 // (`vm_helper::init_partab_params`) instead of planting
11075 // a stub, so reproduce C by consulting the same
11076 // stub-state predicate the subscript arm above uses and
11077 // dropping the node directly. Materialized names fall
11078 // through to `unsetparam` below, which is C's other
11079 // branch: node kept, `PM_UNSET` stamped, readonly rows
11080 // rejected.
11081 if crate::vm_helper::module_param_is_autoload_stub(nm)
11082 && !plain_local_shadow(nm)
11083 && (crate::ported::modules::parameter::PARTAB
11084 .iter()
11085 .any(|e| e.name == nm)
11086 || crate::ported::modules::parameter::PARTAB_ARRAY
11087 .iter()
11088 .any(|e| e.name == nm))
11089 {
11090 crate::ported::params::paramtab()
11091 .write()
11092 .ok()
11093 .map(|mut t| t.remove(nm)); // c:3874
11094 let _ = crate::ported::params::paramtab_hashed_storage()
11095 .lock()
11096 .ok()
11097 .as_deref_mut()
11098 .map(|m| m.remove(nm));
11099 continue; // c:3893 next argument
11100 }
11101 // c:3900-3905 — whole-param unset.
11102 // Route through `unsetparam` (params.rs) so the
11103 // canonical readonly-guard + pm.old uncover restore
11104 // fires. Without this, `local x=inner; unset x`
11105 // would erase the OUTER binding too (the local pm's
11106 // pm.old chain dropped on the floor).
11107 //
11108 // c:Src/builtin.c:3952-3953 — `if (unsetparam_pm(pm,
11109 // 0, 1)) returnval = 1;` (readonly rejection). On
11110 // rejection the param is untouched, so the env delenv
11111 // (which unsetparam_pm only runs on its success path,
11112 // c:Src/params.c:3872) must not fire either.
11113 //
11114 // The assoc backing row used to be wiped here too, with the
11115 // note "NOT touched by params.rs::unsetparam so we wipe them
11116 // directly here". It is now, because `stdunsetfn`'s PM_HASHED
11117 // arm does what C's does (c:3922-3925 → `hashsetfn` c:4047
11118 // `deleteparamtable(pm->u.hash)`) instead of clearing a
11119 // `u_hash` slot nothing populates. The readonly gate is
11120 // preserved for free: `unsetparam_pm` takes its rejection at
11121 // c:3852 BEFORE it reaches the unsetfn, so a rejected unset
11122 // still drops no row.
11123 if unsetparam(nm) != 0 {
11124 returnval = 1; // c:3953
11125 } else {
11126 env::remove_var(nm); // c:3905 delenv
11127 }
11128 }
11129 }
11130 }
11131 unqueue_signals(); // c:3914
11132 returnval // c:3915
11133}
11134
11135/// Port of `fetchcmdnamnode()` from `Src/builtin.c:3967`.
11136/// C decl: `fetchcmdnamnode(HashNode hn, UNUSED(int printflags))`
11137/// C: `static void fetchcmdnamnode(HashNode hn, UNUSED(int printflags))` →
11138/// `addlinknode(matchednodes, cn->node.nam);`
11139/// C body (2 lines):
11140/// `Cmdnam cn = (Cmdnam) hn;
11141/// addlinknode(matchednodes, cn->node.nam);`
11142/// (C source does not null-check hn — callers guarantee non-null.)
11143/// WARNING: param names don't match C — Rust=(hn) vs C=(hn, printflags)
11144pub fn fetchcmdnamnode(
11145 hn: *mut hashnode, // c:3967
11146 _printflags: i32,
11147) {
11148 let nam = unsafe { (*hn).nam.clone() }; // c:3969 cast + read
11149 if let Ok(mut m) = MATCHEDNODES.lock() {
11150 m.push(nam);
11151 } // c:3971
11152}
11153
11154/// Port of `bin_whence()` from `Src/builtin.c:3975`.
11155/// C decl: `bin_whence(char *nam, char **argv, Options ops, int func)`
11156/// C: `int bin_whence(char *nam, char **argv, Options ops, int func)`.
11157///
11158/// `whence`/`type`/`which`/`where`/`command` dispatcher. `-c` csh,
11159/// `-v` verbose, `-a` all-matches, `-w` word-form, `-x` indent
11160/// override, `-m` glob-args, `-p` path-only, `-f` print funcdef,
11161/// `-s/-S` follow symlink. The C body walks alias/reswd/shfunc/
11162/// builtin/cmdnam tabs in order; this port preserves the structure
11163/// and dispatch logic, deferring the per-tab scanmatch walks to the
11164/// existing tab accessors.
11165/// WARNING: param names don't match C — Rust=(nam, argv, func) vs C=(nam, argv, ops, func)
11166pub fn bin_whence(
11167 nam: &str,
11168 argv: &[String], // c:3975
11169 ops: &options,
11170 func: i32,
11171) -> i32 {
11172 let mut returnval: i32 = 0;
11173 let mut printflags: i32 = 0;
11174 let mut informed: i32 = 0;
11175 let mut expand: i32 = 0;
11176
11177 // c:3989-3993 — flags.
11178 let csh = OPT_ISSET(ops, b'c'); // c:3989
11179 let v = OPT_ISSET(ops, b'v'); // c:3990
11180 let all = OPT_ISSET(ops, b'a'); // c:3991
11181 let wd = OPT_ISSET(ops, b'w'); // c:3992
11182
11183 // c:3995-4002 — `-x N` indent override.
11184 if OPT_ISSET(ops, b'x') {
11185 // c:3995
11186 let arg = OPT_ARG(ops, b'x').unwrap_or("");
11187 match arg.trim().parse::<i32>() {
11188 // c:3997
11189 Ok(n) => {
11190 expand = n;
11191 if expand == 0 {
11192 expand = -1;
11193 } // c:4001
11194 }
11195 Err(_) => {
11196 zwarnnam(nam, "number expected after -x"); // c:3998
11197 return 1;
11198 }
11199 }
11200 }
11201
11202 // !!! BASH-MODE GATE (no C counterpart) !!! bash `type -t NAME` prints a
11203 // single word naming what NAME resolves to — `alias` / `keyword` /
11204 // `function` / `builtin` / `file` — or nothing (exit 1) if unknown. zsh's
11205 // `type` has no `-t`, so this is gated to --bash. Precedence matches bash:
11206 // alias, keyword, function, builtin, file. Uses a closure (not a free fn)
11207 // to satisfy the port-purity build gate on src/ported/.
11208 // Outside bash/ksh93 the registered `-t` is not zsh's: `type`'s optstr is
11209 // "ampfsSw" (c:Src/builtin.c:123), so zsh answers `bad option: -t`, status 1.
11210 if !crate::dash_mode::bash_mode() && !crate::dash_mode::ksh93_mode() && OPT_ISSET(ops, b't') {
11211 zwarnnam(nam, "bad option: -t");
11212 return 1;
11213 }
11214 if crate::dash_mode::bash_mode() && OPT_ISSET(ops, b't') {
11215 let type_of = |name: &str| -> Option<&'static str> {
11216 if crate::dash_mode::aliases_visible()
11217 && aliastab_lock()
11218 .read()
11219 .ok()
11220 .map_or(false, |t| t.get(name).is_some())
11221 {
11222 return Some("alias");
11223 }
11224 if reswdtab_lock()
11225 .read()
11226 .ok()
11227 .map_or(false, |t| t.get(name).is_some())
11228 {
11229 return Some("keyword");
11230 }
11231 if getshfunc(name).is_some() {
11232 return Some("function");
11233 }
11234 let disabled = BUILTINS_DISABLED.lock().map_or(false, |s| s.contains(name));
11235 if !disabled && createbuiltintable().get(name).is_some() {
11236 return Some("builtin");
11237 }
11238 if crate::ported::exec::findcmd(name, 0, 0).is_some() {
11239 return Some("file");
11240 }
11241 None
11242 };
11243 let mut rc = 0;
11244 for name in argv {
11245 match type_of(name) {
11246 Some(t) => println!("{}", t),
11247 None => rc = 1,
11248 }
11249 }
11250 return rc;
11251 }
11252
11253 // !!! BASH-MODE GATE (no C counterpart) !!! `type -P NAME` forces a $PATH
11254 // search and prints the file, whatever else NAME is.
11255 if crate::dash_mode::bash_mode() && OPT_ISSET(ops, b'P') {
11256 let mut rc = 0;
11257 for name in argv {
11258 match crate::ported::exec::findcmd(name, 0, 0) {
11259 Some(path) => println!("{}", path),
11260 None => rc = 1,
11261 }
11262 }
11263 return rc;
11264 }
11265
11266 // c:4004-4012 — printflags from -w/-c/-v/(default simple)/-f.
11267 if OPT_ISSET(ops, b'w') {
11268 printflags |= PRINT_WHENCE_WORD;
11269 }
11270 // c:4004
11271 else if OPT_ISSET(ops, b'c') {
11272 printflags |= PRINT_WHENCE_CSH;
11273 }
11274 // c:4006
11275 else if OPT_ISSET(ops, b'v') {
11276 printflags |= PRINT_WHENCE_VERBOSE;
11277 }
11278 // c:4008
11279 else {
11280 printflags |= PRINT_WHENCE_SIMPLE;
11281 } // c:4010
11282 if OPT_ISSET(ops, b'f') {
11283 printflags |= PRINT_WHENCE_FUNCDEF;
11284 } // c:4012
11285
11286 // c:4015-4024 — BIN_COMMAND -V or -V-equivalent flag wrangling.
11287 // C body:
11288 // if (func == BIN_COMMAND)
11289 // if (OPT_ISSET(ops,'V')) { printflags = aliasflags = PRINT_WHENCE_VERBOSE; v = 1; }
11290 // else { aliasflags = PRINT_LIST; printflags = PRINT_WHENCE_SIMPLE; v = 0; }
11291 // else aliasflags = printflags;
11292 // Previous Rust port omitted the `v = 0` reset in the non-V
11293 // command branch, so `command foo` with a stray user -v leaked
11294 // verbose mode. Mirror C: force v unconditionally under
11295 // BIN_COMMAND.
11296 let mut v = v;
11297 let aliasflags = if func == BIN_COMMAND {
11298 // c:4015
11299 if OPT_ISSET(ops, b'V') {
11300 // c:4016
11301 printflags = PRINT_WHENCE_VERBOSE; // c:4017
11302 v = true; // c:4018
11303 PRINT_WHENCE_VERBOSE
11304 } else {
11305 printflags = PRINT_WHENCE_SIMPLE; // c:4021
11306 v = false; // c:4022
11307 PRINT_LIST // c:4020
11308 }
11309 } else {
11310 printflags // c:4024
11311 };
11312
11313 // c:4026-4119 — `-m` glob branch: each arg is a pattern; walk every
11314 // hashtab in turn (alias/reswd/shfunc/builtin/cmdnam) and emit a
11315 // print row per matching node. C uses scanmatchtable + a per-tab
11316 // print callback; the Rust port iterates each tab's accessor and
11317 // emits the print directly.
11318 if OPT_ISSET(ops, b'm') {
11319 // c:4028 — `cmdnamtab->filltable(cmdnamtab);` populates every
11320 // $PATH entry into cmdnamtab so the c:4070 scan below sees
11321 // every executable (not just hashed ones). C calls this once
11322 // per `-m` invocation; Rust mirrors with a single fillcmdnamtable
11323 // against the shell-side $PATH array.
11324 if let Some(path) = getsparam("PATH") {
11325 let path_arr: Vec<String> = path.split(':').map(|s| s.to_string()).collect();
11326 fillcmdnamtable(&path_arr);
11327 }
11328 // c:4030-4033 — `if (all) { pushheap(); matchednodes = newlinklist(); }`.
11329 // MATCHEDNODES is the Rust analog of `matchednodes`; pushheap
11330 // is a Rust no-op (no heap allocator).
11331 if all {
11332 // c:4030
11333 if let Ok(mut m) = MATCHEDNODES.lock() {
11334 m.clear();
11335 }
11336 }
11337 queue_signals(); // c:4034
11338 for pat in argv {
11339 // c:4035
11340 // c:4037 — `tokenize(*argv);` (Rust patcompile handles the
11341 // tokenize step internally; explicit call is a no-op here).
11342 let pprog = patcompile(
11343 &{
11344 let mut __pat_tok = crate::pattern_data_escape::escape_data_backslashes(&pat); // a backslash tokenize leaves raw is DATA (c:Src/glob.c:3651)
11345 crate::ported::glob::tokenize(&mut __pat_tok);
11346 __pat_tok
11347 }, // c:4038
11348 PAT_HEAPDUP,
11349 None,
11350 );
11351 match pprog {
11352 None => {
11353 // c:4039
11354 zwarnnam(nam, &format!("bad pattern : {}", pat)); // c:4040
11355 returnval = 1; // c:4041
11356 continue;
11357 }
11358 Some(prog) => {
11359 if !OPT_ISSET(ops, b'p') {
11360 // c:4044 — !`-p` path-only.
11361 // c:4049-4051 — `scanmatchtable(aliastab, pprog,
11362 // 1, 0, DISABLED, aliastab->printnode, printflags);`.
11363 // Route through the canonical printnode callback.
11364 let alias_matches: Vec<alias> = aliastab_lock()
11365 .read()
11366 .map(|t| {
11367 t.iter()
11368 .filter(|(n, _)| pattry(&prog, n))
11369 .map(|(_, a)| a.clone())
11370 .collect()
11371 })
11372 .unwrap_or_default();
11373 for a in &alias_matches {
11374 printaliasnode(a, printflags); // c:4051
11375 informed += 1; // c:4049
11376 }
11377 // c:4054-4056 — `scanmatchtable(reswdtab, pprog,
11378 // 1, 0, DISABLED, reswdtab->printnode, printflags);`.
11379 // reswdtab->printnode is `printreswdnode`
11380 // (Src/hashtable.c:1147), which honours the
11381 // -w/-c/-v printflags (`for: reserved`). The
11382 // DISABLED mask skips `disable -r`ed words.
11383 // c:Src/hashtable.c:402 sorts by hnamcmp (ztrcmp).
11384 let mut rw_matches: Vec<crate::ported::zsh_h::reswd> = reswdtab_lock()
11385 .read()
11386 .map(|t| {
11387 t.iter()
11388 .filter(|(k, _)| pattry(&prog, k))
11389 .filter(|(_, rw)| (rw.node.flags & DISABLED as i32) == 0)
11390 .map(|(_, rw)| rw.clone())
11391 .collect()
11392 })
11393 .unwrap_or_default();
11394 rw_matches.sort_by(|a, b| crate::ported::utils::ztrcmp(&a.node.nam, &b.node.nam));
11395 for rw in &rw_matches {
11396 crate::ported::hashtable::printreswdnode(rw, printflags); // c:4056
11397 informed += 1; // c:4054
11398 }
11399 // c:4059-4061 — `scanmatchshfunc(pprog, 1, 0,
11400 // DISABLED, shfunctab->printnode, printflags,
11401 // expand);`. Route through canonical
11402 // printshfuncexpand with `expand`.
11403 let func_matches: Vec<shfunc> = shfunctab_lock()
11404 .read()
11405 .map(|t| {
11406 t.iter()
11407 .filter(|(n, _)| pattry(&prog, n))
11408 .map(|(_, f)| f.clone())
11409 .collect()
11410 })
11411 .unwrap_or_default();
11412 for f in &func_matches {
11413 printshfuncexpand(f, printflags, expand); // c:4061
11414 informed += 1; // c:4059
11415 }
11416 // c:4064-4066 — `scanmatchtable(builtintab, pprog,
11417 // 1, 0, DISABLED, builtintab->printnode,
11418 // printflags);`. C's scanmatchtable walks the
11419 // hashtable in a deterministic order that, for
11420 // the matched-against name set, happens to be
11421 // sorted alphabetically — that's what makes
11422 // `whence -m "echo*"` emit echo, echotc,
11423 // echoti. zshrs's BUILTINS Vec is in
11424 // declaration order (echo, echoti, echotc) so
11425 // the unsorted walk produced a different
11426 // ordering. Collect+sort to match.
11427 // c:4065 — the `DISABLED` mask arg to scanmatchtable
11428 // makes the walk SKIP nodes carrying DISABLED, so a
11429 // `disable`d builtin never appears in `whence -m`.
11430 let disabled_snapshot: std::collections::HashSet<String> =
11431 BUILTINS_DISABLED
11432 .lock()
11433 .map(|s| s.clone())
11434 .unwrap_or_default();
11435 // c:4064 — the walk is over `builtintab`, one node per
11436 // name. The raw BUILTINS vec repeats module rows
11437 // (`kill`, `log`, …) that createbuiltintable folds, so
11438 // walking it printed `kill` two or three times.
11439 let mut bn_matches: Vec<&builtin> = createbuiltintable()
11440 .values()
11441 .copied()
11442 .filter(|b| pattry(&prog, &b.node.nam))
11443 .filter(|b| !disabled_snapshot.contains(b.node.nam.as_str()))
11444 // Same module gate as the name lookup below: a
11445 // builtin of an unloaded module (`zstat`) is not in
11446 // C's builtintab until `zmodload` adds it.
11447 .filter(|b| crate::extensions::ext_builtins::module_builtin_available(&b.node.nam))
11448 .collect();
11449 // c:Src/hashtable.c:402 `qsort(…, hnamcmp)` — ztrcmp.
11450 bn_matches.sort_by(|a, b| crate::ported::utils::ztrcmp(&a.node.nam, &b.node.nam));
11451 for b in bn_matches {
11452 printbuiltinnode(
11453 &b.node as *const hashnode as *mut hashnode,
11454 printflags,
11455 ); // c:4066
11456 informed += 1; // c:4064
11457 }
11458 }
11459 // c:4070-4073 — `scanmatchtable(cmdnamtab, pprog,
11460 // 1, 0, 0, (all ? fetchcmdnamnode :
11461 // cmdnamtab->printnode), printflags);`. After
11462 // fillcmdnamtable above, cmdnamtab has every
11463 // PATH-resident command name. Walk the canonical
11464 // table (not std::fs::read_dir) so HASHED/non-
11465 // HASHED distinction is preserved.
11466 // The `1` in that call is scanmatchtable's `sorted`
11467 // argument, and c:Src/hashtable.c:402 sorts with
11468 // `qsort(hnsorttab, ct, sizeof(HashNode), hnamcmp)`
11469 // where `hnamcmp` (c:341-346) is
11470 // `ztrcmp(a->nam, b->nam)` — the NODE NAME. The
11471 // printed token (the full path) is not what orders
11472 // the walk; it only happens to agree when the
11473 // matches all live in one directory.
11474 //
11475 // This sorted on the printed path instead, above a
11476 // comment asserting that was C's order. Measured
11477 // against real zsh 5.9 with `gls`/`ls`/`rls` in
11478 // three different directories:
11479 //
11480 // % zsh -f -c 'setopt extendedglob
11481 // whence -m "(#a1)lls"'
11482 // /opt/homebrew/bin/gls
11483 // /bin/ls
11484 // /Users/…/.cargo/bin/rls
11485 //
11486 // i.e. gls < ls < rls by NAME, while the paths run
11487 // /Users < /bin < /opt. zshrs answered in path
11488 // order (rls, ls, gls) — every `whence -m`,
11489 // `where`, `which -m` with matches spanning
11490 // directories was mis-ordered, and
11491 // `_correct_filename '=lls'` inherited it.
11492 let mut cmd_matches: Vec<(String, cmdnam)> = cmdnamtab_lock()
11493 .read()
11494 .map(|t| {
11495 t.iter()
11496 .filter(|(n, _)| pattry(&prog, n))
11497 .map(|(n, c)| (n.clone(), c.clone()))
11498 .collect()
11499 })
11500 .unwrap_or_default();
11501 // c:341-346 `hnamcmp` — `ztrcmp(a->nam, b->nam)`.
11502 cmd_matches.sort_by(|a, b| crate::ported::utils::ztrcmp(&a.1.node.nam, &b.1.node.nam));
11503 for (n, c) in &cmd_matches {
11504 if all {
11505 // c:4072 fetchcmdnamnode — accumulates
11506 // matching node names into matchednodes.
11507 if let Ok(mut m) = MATCHEDNODES.lock() {
11508 m.push(n.clone());
11509 }
11510 } else {
11511 // c:4072 cmdnamtab->printnode — emits per
11512 // PRINT_WHENCE_WORD/CSH/VERBOSE branches.
11513 printcmdnamnode(c, printflags);
11514 }
11515 informed += 1; // c:4070
11516 }
11517 }
11518 }
11519 run_queued_signals(); // c:4079
11520 }
11521 unqueue_signals(); // c:4081
11522 if !all {
11523 // c:4082-4084 — `return returnval || !informed;` (early-out
11524 // when not in `-a` accumulator mode).
11525 return if returnval != 0 || informed == 0 {
11526 1
11527 } else {
11528 0
11529 }; // c:4082
11530 }
11531 }
11532
11533 // c:4121-4205 — literal-name dispatch per arg.
11534 queue_signals();
11535 // C source uses MATCHEDNODES only when `-m` (glob-args) is set;
11536 // plain `-a` keeps the literal argv. Without this gate, `whence
11537 // -a true` consulted an empty MATCHEDNODES and skipped every
11538 // print.
11539 let argv_vec: Vec<String> = if OPT_ISSET(ops, b'm') {
11540 MATCHEDNODES.lock().map(|m| m.clone()).unwrap_or_default()
11541 } else {
11542 argv.to_vec()
11543 };
11544 // c:4076 — `allmatched` is set exactly when -m ran in -a mode (the
11545 // non -a case returned at c:4082); those names came from cmdnamtab and
11546 // the internal tables were already scanned, so c:4091 skips them.
11547 let allmatched = OPT_ISSET(ops, b'm');
11548 for arg in &argv_vec {
11549 // c:4121
11550 // c:4088 — `informed = 0;` reset per iteration so the per-arg
11551 // not-found path can fire correctly.
11552 informed = 0; // c:4088
11553 // c:4090 `char *cnam` is the findcmd return in C; in Rust it
11554 // is bound inline at the findcmd call site below.
11555 // c:4089-4137 — !`-p` and !`-a` matched-from-prior-`-m` arm.
11556 if !OPT_ISSET(ops, b'p') && !allmatched {
11557 // c:4093-4097 — alias check. C: `aliastab->printnode(hn, aliasflags)`.
11558 let alias_text = aliastab_lock()
11559 .read()
11560 .ok()
11561 .filter(|_| crate::dash_mode::aliases_visible())
11562 .and_then(|t| t.get(arg).map(|a| a.clone()));
11563 if let Some(a) = alias_text {
11564 printaliasnode(&a, aliasflags); // c:4094
11565 informed = 1; // c:4095
11566 if !all {
11567 continue;
11568 } // c:4097
11569 }
11570 // c:4099 /* Look for suffix alias */
11571 // c:4100-4102 — `if ((suf = strrchr(*argv, '.')) && suf[1] &&
11572 // suf > *argv && suf[-1] != Meta &&
11573 // (hn = sufaliastab->getnode(sufaliastab, suf+1)))`.
11574 // The order is load-bearing: C short-circuits, so `suf[-1]`
11575 // (the byte BEFORE `.`) is read only after `suf > *argv`
11576 // confirms `.` is not at position 0.
11577 if let Some(idx) = arg.rfind('.') {
11578 // c:4100 — suf = strrchr(*argv, '.'); suf[1] (char after `.`
11579 // is non-NUL) AND c:4101 suf > *argv (`.` not at start)
11580 // AND suf[-1] != Meta (char before `.` is not Meta-escape).
11581 // Chained into one `if` so Rust short-circuits identically
11582 // to C; reading `arg.as_bytes()[idx - 1]` eagerly with
11583 // idx==0 panics with subtract-overflow.
11584 if idx + 1 < arg.len() && idx > 0 && arg.as_bytes()[idx - 1] as u8 != Meta {
11585 let suf = &arg[idx + 1..]; // c:4102 suf+1
11586 let suf_alias = sufaliastab_lock()
11587 .read()
11588 .ok()
11589 .and_then(|t| t.get(suf).map(|a| a.clone()));
11590 if let Some(a) = suf_alias {
11591 printaliasnode(&a, printflags); // c:4103
11592 informed = 1; // c:4104
11593 if !all {
11594 continue;
11595 } // c:4106
11596 }
11597 }
11598 }
11599 // c:4109-4114 — `if ((hn = reswdtab->getnode(reswdtab, *argv)))
11600 // reswdtab->printnode(hn, printflags);`. Reads canonical
11601 // reswdtab instead of a drift-prone literal array.
11602 let is_reswd = reswdtab_lock()
11603 .read()
11604 .map(|t| t.get(arg).is_some())
11605 .unwrap_or(false);
11606 if is_reswd {
11607 // c:4109
11608 if (printflags & PRINT_WHENCE_WORD as i32) != 0 {
11609 println!("{}: reserved", arg);
11610 } else if (printflags & PRINT_WHENCE_CSH as i32) != 0 {
11611 println!("{}: shell reserved word", arg);
11612 } else if (printflags & PRINT_WHENCE_VERBOSE as i32) != 0 {
11613 println!(
11614 "{} is a {}",
11615 arg,
11616 if crate::dash_mode::dash_faithful() {
11617 "shell keyword"
11618 } else if crate::dash_mode::ksh93_mode() {
11619 "keyword"
11620 } else {
11621 "reserved word"
11622 }
11623 );
11624 } else {
11625 println!("{}", arg); // c:4110
11626 }
11627 informed = 1; // c:4111
11628 if !all {
11629 continue;
11630 } // c:4112
11631 }
11632 // c:4116-4121 — shell function check. C:
11633 // `printshfuncexpand(hn, printflags, expand)`.
11634 // Inline match-on-printflags reimplementation deleted —
11635 // route through the canonical port at hashtable.rs:1407,
11636 // which threads `expand` (for `-x N` indent override) and
11637 // handles PRINT_WHENCE_FUNCDEF/PRINT_WHENCE_WORD/_CSH/
11638 // _VERBOSE branches per Src/hashtable.c:1340-1404.
11639 let shfunc_node = getshfunc(arg);
11640 if let Some(ref f) = shfunc_node {
11641 // !!! EMULATION-ONLY (no C counterpart) !!! zsh's verbose
11642 // form names the file the function came from — for a `-c`
11643 // script that is the shell itself, so `type f` ends in
11644 // "is a shell function from zsh". No real Bourne shell says
11645 // that: bash / ksh93 / mksh print "f is a function" and
11646 // dash / ash print "f is a shell function", and bash alone
11647 // follows the header with the definition. Measured on the
11648 // real binaries; see dash_mode::function_whence_verbose.
11649 // Only the VERBOSE form (`type` / `whence -v` /
11650 // `command -V`) is affected — `-w`, `-c` and the plain
11651 // simple form already match.
11652 let verbose_only = (printflags & PRINT_WHENCE_VERBOSE) != 0
11653 && (printflags & PRINT_WHENCE_FUNCDEF) == 0;
11654 match crate::dash_mode::function_whence_verbose(arg).filter(|_| verbose_only) {
11655 Some(line) => {
11656 println!("{}", line);
11657 if crate::dash_mode::function_whence_prints_body() {
11658 // Same rendering bin_trap uses for a trap body:
11659 // the stored source re-parsed and deparsed, one
11660 // statement per line.
11661 let rendered = f
11662 .body
11663 .as_deref()
11664 .filter(|b| !b.is_empty())
11665 .and_then(|b| crate::ported::exec::parse_string(b, 1))
11666 .map(|prog| {
11667 crate::ported::text::getpermtext(Box::new(prog), None, 0)
11668 })
11669 .unwrap_or_default();
11670 print!("{}", crate::dash_mode::bash_function_body(arg, &rendered));
11671 }
11672 }
11673 None => printshfuncexpand(f, printflags, expand), // c:4117
11674 }
11675 informed = 1; // c:4118
11676 if !all {
11677 continue;
11678 } // c:4120
11679 }
11680 // c:4123-4128 — builtin check. C: `builtintab->printnode(
11681 // hn, printflags)` → printbuiltinnode at Src/builtin.c:174.
11682 // Inline match-on-(wd|csh|v) reimplementation deleted —
11683 // route through the canonical port at builtin.rs:139.
11684 // c:Src/Modules/files.c:806-824 — bare `mkdir`/`rm`/`mv`/`ln`/
11685 // `chmod`/... are bound by `zsh/files`, so without an explicit
11686 // `zmodload zsh/files` the name is not in builtintab at all and
11687 // `type rm` must fall through to the cmdnamtab/$PATH path below
11688 // (docs/BUGS.md #28). That is one instance of the general rule:
11689 // One predicate for every consumer — see
11690 // `ext_builtins::module_builtin_available`. The previous code
11691 // here was a hand-maintained list of gated names (zsh/files,
11692 // zsh/stat, zsh/zselect, zsh/zpty, zsh/net/tcp, zsh/zftp,
11693 // zsh/system) extended one bug at a time, so every module NOT on
11694 // it leaked through: `whence -w strftime`, `pcre_compile`,
11695 // `clone`, `zcurses`, `ztie`, `zgdbmpath`, `cap`, `getcap`,
11696 // `zgetattr`, `zdelattr`, `sysopen`, `zsocket`, `zprof` and
11697 // `example` all reported `builtin` where zsh -f reports `none`,
11698 // and where this very shell's own `${+builtins[...]}` said 0.
11699 let is_module_bound_gated =
11700 !crate::extensions::ext_builtins::module_builtin_available(arg);
11701 // c:Src/builtin.c:4123 — `builtintab->getnode(name)` returns NULL
11702 // for a builtin carrying the DISABLED flag (set by `disable NAME`),
11703 // so whence/type do NOT report a disabled builtin as a builtin and
11704 // instead fall through to the hashed/PATH external command.
11705 let is_disabled_builtin = BUILTINS_DISABLED
11706 .lock()
11707 .map(|s| s.contains(arg.as_str()))
11708 .unwrap_or(false);
11709 let builtin_node: Option<*mut hashnode> =
11710 if is_module_bound_gated || is_disabled_builtin {
11711 None
11712 } else {
11713 BUILTINS
11714 .iter()
11715 .find(|b| b.node.nam == *arg)
11716 .map(|b| &b.node as *const hashnode as *mut hashnode)
11717 };
11718 if let Some(hn) = builtin_node {
11719 printbuiltinnode(hn, printflags); // c:4124
11720 informed = 1; // c:4125
11721 if !all {
11722 continue;
11723 } // c:4127
11724 }
11725 // zshrs extension builtins (daemon z* family: zd, zcache,
11726 // zjob, …) dispatch by name via try_dispatch instead of
11727 // living in BUILTINS — but they ARE builtins and whence/
11728 // type must classify them as such (`whence -w zd` reported
11729 // the external /opt/homebrew/bin/zd instead).
11730 // Under `--zsh` / ZSHRS_HIDE_EXT_BUILTINS these zshrs-only
11731 // names are NOT in the builtin namespace — `${(k)builtins}`
11732 // omits them (Src/Modules/parameter.c:823 mirror at
11733 // modules/parameter.rs:2046) — so whence must not report
11734 // them either. One predicate for every consumer:
11735 // `ext_builtins::hide_ext_builtins`. Dispatch is untouched.
11736 if builtin_node.is_none()
11737 && !is_disabled_builtin
11738 && !crate::ext_builtins::hide_ext_builtins()
11739 && crate::daemon::builtins::is_zshrs_builtin(arg)
11740 {
11741 let mut ext_node = hashnode {
11742 next: None,
11743 nam: arg.clone(),
11744 flags: 0,
11745 };
11746 printbuiltinnode(&mut ext_node as *mut hashnode, printflags);
11747 informed = 1;
11748 if !all {
11749 continue;
11750 }
11751 }
11752 // zshrs extension builtins folded into builtintab (znative package
11753 // manager, ztest/zassert framework, watch) — first-class builtins
11754 // absent from the static BUILTINS table; classify like the daemon
11755 // z* family above so `whence -w zassert_eq` reports `builtin`.
11756 // Same `hide_ext_builtins()` gate as the daemon arm above:
11757 // `zsh -f` has no `cat`/`head`/`seq`/`tail` builtin, so
11758 // `which cat` must print `/bin/cat`, not `cat: shell
11759 // built-in command` (E01options.ztst:17 — the %prep line
11760 // `catpath=$(which cat)` feeds the EQUALS expectation).
11761 if builtin_node.is_none()
11762 && !is_disabled_builtin
11763 && !crate::ext_builtins::hide_ext_builtins()
11764 && !crate::daemon::builtins::is_zshrs_builtin(arg)
11765 && crate::extensions::ext_builtins::is_extension_builtin(arg)
11766 {
11767 let mut ext_node = hashnode {
11768 next: None,
11769 nam: arg.clone(),
11770 flags: 0,
11771 };
11772 printbuiltinnode(&mut ext_node as *mut hashnode, printflags);
11773 informed = 1;
11774 if !all {
11775 continue;
11776 }
11777 }
11778 // c:4167-4173 — cmdnamtab HASHED check (commands installed
11779 // via `hash NAME=PATH`). Read the canonical cmdnamtab
11780 // directly. Was a fake env-var bridge under invented
11781 // `__zshrs_hash_NAME` keys; cmdnamtab is bucket-2-
11782 // consolidated now.
11783 // c:4115 — `if ((hn = cmdnamtab->getnode(cmdnamtab, *argv)) &&
11784 // (hn->flags & HASHED))`.
11785 let hashed_node: Option<cmdnam> = {
11786 match cmdnamtab_lock().read() {
11787 Ok(tab) => tab
11788 .get(arg)
11789 .filter(|cn| (cn.node.flags & HASHED as i32) != 0)
11790 .cloned(),
11791 Err(_) => None,
11792 }
11793 };
11794 if let Some(cn) = hashed_node {
11795 // c:4116 — `cmdnamtab->printnode(hn, printflags)`, i.e.
11796 // `printcmdnamnode`, which dispatches on the PRINT_WHENCE_*
11797 // bits. This site open-coded `println!("{}", p)` plus a
11798 // PRINT_LIST special case, so every other form was wrong:
11799 // `type foo` (PRINT_WHENCE_VERBOSE) printed the bare path
11800 // instead of `foo is hashed to /usr/bin/awk`, and
11801 // `whence -w foo` (PRINT_WHENCE_WORD) lost `foo: hashed`.
11802 printcmdnamnode(&cn, printflags);
11803 informed = 1; // c:4117
11804 if !all {
11805 continue;
11806 } // c:4118-4119
11807 }
11808 }
11809 // c:4141-4172 — `-a` all-paths search. C iterates the
11810 // shell-side `path` array (the tied $path/$PATH global,
11811 // Src/parse.c). Rust reads $PATH via getsparam — same source.
11812 if all && !arg.starts_with('/') {
11813 // c:4141
11814 if let Some(path) = getsparam("PATH") {
11815 for dir in path.split(':') {
11816 // c:4145 — `if (**pp) buf = zhtricat(*pp, "/", *argv);
11817 // else buf = dupstring(*argv);`.
11818 // Empty path entry means CWD per POSIX, but C still
11819 // joins with "/" if non-empty; Rust matches the
11820 // !empty arm.
11821 if dir.is_empty() {
11822 continue;
11823 }
11824 let full = format!("{}/{}", dir, arg); // c:4147
11825 // c:4150 — `iscom(buf)`: access(X_OK)==0 &&
11826 // S_ISREG(stat). Was `Path::is_file()` which omits
11827 // the X_OK check — would have flagged non-executable
11828 // files as matches.
11829 if iscom(&full) {
11830 // c:4150
11831 if wd {
11832 // c:4151
11833 println!("{}: command", arg); // c:4152
11834 } else {
11835 if v && !csh {
11836 // c:4154
11837 print!("{} is ", arg); // c:4155
11838 print!("{}", quotedzputs(&full)); // c:4156
11839 } else {
11840 print!("{}", full); // c:4158
11841 }
11842 // c:4159-4160 — `if (OPT_ISSET(ops,'s') ||
11843 // OPT_ISSET(ops,'S')) print_if_link(buf,
11844 // OPT_ISSET(ops,'S'));`. -s prints just
11845 // the final realpath; -S prints the whole
11846 // chain.
11847 if OPT_ISSET(ops, b's') || OPT_ISSET(ops, b'S') {
11848 print_if_link(&full, OPT_ISSET(ops, b'S')); // c:4160
11849 }
11850 println!(); // c:4161 fputc('\n', stdout)
11851 }
11852 informed = 1; // c:4163
11853 } else {
11854 }
11855 }
11856 }
11857 // c:4166-4171 — `if (!informed && (wd || v || csh))`. C:
11858 // zputs(*argv, stdout); puts(wd ? ": none" : " not found");
11859 // Was `if !informed != 0 && ...` which is broken Rust — the
11860 // `!` is bitwise NOT on the i32, so the condition was true
11861 // when informed != 0 (inverted). Fix: explicit `informed == 0`.
11862 if informed == 0 && (wd || v || csh) {
11863 // c:4166
11864 // dash: `NAME: not found` (see the end-of-function status note).
11865 if (crate::dash_mode::bash_mode() || crate::dash_mode::ksh93_mode()) && !wd {
11866 // bash and ksh93 report a missing name on stderr: `type: NAME: not found`.
11867 zwarnnam(nam, &format!("{arg}: not found"));
11868 } else {
11869 println!("{}{}", arg, if wd { ": none" } else if crate::dash_mode::dash_strict() { ": not found" } else { " not found" }); // c:4168-4169
11870 }
11871 returnval = 1; // c:4170
11872 }
11873 continue;
11874 }
11875 // c:4157-4164 — `} else if (func == BIN_COMMAND &&
11876 // OPT_ISSET(ops,'p') && (hn = builtintab->getnode(builtintab,
11877 // *argv))) {`
11878 // /*
11879 // * Special case for "command -p[vV]" which needs to
11880 // * show a builtin in preference to an external command.
11881 // */
11882 // builtintab->printnode(hn, printflags);
11883 // informed = 1;
11884 if func == BIN_COMMAND && OPT_ISSET(ops, b'p') {
11885 // c:4157
11886 // c:4158 is `builtintab->getnode(builtintab, *argv)`, the SAME
11887 // lookup as c:4123 above — so it answers NULL for exactly the
11888 // same two reasons, and this arm has to respect both. A raw
11889 // `BUILTINS.iter().find()` here reported `chown`, `mkdir`, `rm`
11890 // and every other `zsh/files` name as a builtin under
11891 // `command -pv` while `whence -w` on the same name correctly
11892 // said `none`, because the module-bound gate and the `disable`
11893 // set were only applied at c:4123. The divergence was hidden as
11894 // long as `-p` never reached `ops` at all.
11895 let gated = !crate::extensions::ext_builtins::module_builtin_available(arg)
11896 || BUILTINS_DISABLED
11897 .lock()
11898 .map(|s| s.contains(arg.as_str()))
11899 .unwrap_or(false);
11900 if let Some(b) = BUILTINS
11901 .iter()
11902 .find(|b| b.node.nam == *arg)
11903 .filter(|_| !gated)
11904 {
11905 // c:4163 — `builtintab->printnode(hn, printflags)`, NOT a
11906 // hardcoded `NAME: builtin` line: `printflags` here is
11907 // PRINT_WHENCE_SIMPLE for `-pv` (prints just `echo`) and
11908 // PRINT_WHENCE_VERBOSE for `-p -V` (prints `echo is a shell
11909 // builtin`). The `NAME: builtin` spelling is the
11910 // PRINT_WHENCE_WORD form and belongs to `whence -w`.
11911 printbuiltinnode(&b.node as *const hashnode as *mut hashnode, printflags); // c:4163
11912 informed = 1; // c:4164
11913 continue;
11914 }
11915 }
11916 // c:4181-4197 — external-command fallback via findcmd.
11917 // C: `if ((cnam = findcmd(*argv, 1, func == BIN_COMMAND &&
11918 // OPT_ISSET(ops, 'p'))))`. Single call site — the previous
11919 // Rust port had two near-duplicate findcmd blocks which
11920 // doubled the lookup. Collapsed into one to match C.
11921 if let Some(cnam) = findcmd(
11922 arg,
11923 1, // c:4181 docmd
11924 (func == BIN_COMMAND && OPT_ISSET(ops, b'p')) as i32, // c:4182-4183
11925 ) {
11926 // c:4181
11927 if wd {
11928 // c:4184
11929 println!("{}: command", arg); // c:4186
11930 } else {
11931 if v && !csh {
11932 // c:4188
11933 print!("{} is ", arg); // c:4189
11934 print!("{}", quotedzputs(&cnam)); // c:4190
11935 } else {
11936 print!("{}", cnam); // c:4192
11937 }
11938 // c:4193-4194 — `-s`/`-S` symlink follow.
11939 if OPT_ISSET(ops, b's') || OPT_ISSET(ops, b'S') {
11940 print_if_link(&cnam, OPT_ISSET(ops, b'S')); // c:4194
11941 }
11942 println!(); // c:4195 fputc('\n', stdout)
11943 }
11944 informed = 1; // c:4197
11945 continue;
11946 }
11947 // c:4201-4205 — not found at all.
11948 if v || csh || wd {
11949 // c:4202
11950 if (crate::dash_mode::bash_mode() || crate::dash_mode::ksh93_mode()) && !wd {
11951 zwarnnam(nam, &format!("{arg}: not found"));
11952 } else {
11953 println!("{}{}", arg, if wd { ": none" } else if crate::dash_mode::dash_strict() { ": not found" } else { " not found" }); // c:4203
11954 }
11955 }
11956 returnval = 1; // c:4204
11957 }
11958 unqueue_signals();
11959 let status = returnval | (informed == 0) as i32; // c:4209
11960 // !!! DASH-STRICT GATE (no C counterpart) !!! dash's `type` / `command -v`
11961 // / `command -V` answer 127 (not zsh's 1) when a name is not found.
11962 if crate::dash_mode::dash_strict() && status != 0 { 127 } else { status }
11963}
11964
11965/// Port of `bin_hash()` from `Src/builtin.c:4234`.
11966/// C decl: `bin_hash(char *name, char **argv, Options ops, UNUSED(int func))`
11967/// C: `int bin_hash(char *name, char **argv, Options ops, ...)` —
11968/// manage `cmdnamtab` (default) or `nameddirtab` (`-d`); `-r` empties,
11969/// `-f` fills, `-L` sets PRINT_LIST, `-m` is a glob.
11970/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
11971pub fn bin_hash(
11972 name: &str,
11973 argv: &[String], // c:4234
11974 ops: &options,
11975 _func: i32,
11976) -> i32 {
11977 if crate::dash_mode::bash_mode() {
11978 return crate::extensions::emulation_output::bash_hash(argv, ops);
11979 }
11980 let mut returnval = 0i32; // c:4239
11981 let mut printflags = 0i32; // c:4240
11982 let dir_mode = OPT_ISSET(ops, b'd'); // c:4242
11983
11984 // PFA-SMR aspect: only `hash -d NAME=PATH` mutates the named-dir
11985 // table; the default `hash CMD=PATH` form populates a runtime
11986 // command cache that the recorder doesn't re-apply.
11987 #[cfg(feature = "recorder")]
11988 if crate::recorder::is_enabled() && dir_mode {
11989 let ctx = crate::recorder::recorder_ctx_global();
11990 for a in argv {
11991 if a.starts_with('-') {
11992 continue;
11993 }
11994 if let Some((k, v)) = a.split_once('=') {
11995 crate::recorder::emit_hash_d(k, v, ctx.clone());
11996 }
11997 }
11998 }
11999
12000 // c:4247-4263 — `-r` empty / `-f` fill (no other args).
12001 if OPT_ISSET(ops, b'r') || OPT_ISSET(ops, b'f') {
12002 // c:4247
12003 if !argv.is_empty() {
12004 // c:4249
12005 zwarnnam("hash", "too many arguments"); // c:4250
12006 return 1; // c:4251
12007 }
12008 if OPT_ISSET(ops, b'r') {
12009 // c:4255
12010 // c:4256 — `emptyhashtable(cmdnamtab)` /
12011 // `emptynameddirtable()`.
12012 if dir_mode {
12013 emptynameddirtable();
12014 } else {
12015 emptycmdnamtable();
12016 }
12017 }
12018 if OPT_ISSET(ops, b'f') {
12019 // c:4259
12020 // c:4260 — `fillcmdnamtable(cmdnamtab)` /
12021 // `fillnameddirtable()`. cmdnamtab fill = walk every
12022 // PATH entry and hashdir() it.
12023 if dir_mode {
12024 fillnameddirtable();
12025 } else {
12026 // Read $path (the lowercase array form) from env.
12027 // c:4260 — fill cmdnamtab from $path. Read shell-side
12028 // $PATH so changes via `path=(...)` flow in.
12029 let path_str = getsparam("PATH").unwrap_or_default();
12030 let path_arr: Vec<String> = path_str.split(':').map(|s| s.to_string()).collect();
12031 fillcmdnamtable(&path_arr);
12032 }
12033 }
12034 return 0; // c:4262
12035 }
12036
12037 // c:4265 — `-L` enables PRINT_LIST.
12038 if OPT_ISSET(ops, b'L') {
12039 printflags |= PRINT_LIST;
12040 } // c:4265
12041
12042 // c:4268-4273 — no args: list table.
12043 if argv.is_empty() {
12044 // c:4268
12045 queue_signals(); // c:4269
12046 // c:4270 — `scanhashtable(ht, 1, 0, 0, ht->printnode, printflags)`.
12047 // Walk the selected table (cmdnamtab default, nameddirtab when
12048 // `-d`). Previous Rust port only walked nameddirtab — `hash`
12049 // with no args (the typical user-visible form) silently printed
12050 // nothing on cmdnamtab.
12051 // c:4270 — `scanhashtable(ht, 1, 0, 0, ht->printnode, printflags)`.
12052 // The second arg `1` is `sorted` — the table is listed in
12053 // alphabetical (meta-aware hnamcmp) order, not hash/insertion
12054 // order. The previous port walked `t.iter()` raw, so `hash -d`
12055 // printed entries in an arbitrary order (`two`, `one` instead of
12056 // `one`, `two`).
12057 if dir_mode {
12058 if let Ok(t) = nameddirtab().lock() {
12059 let mut entries: Vec<_> = t.iter().collect();
12060 entries.sort_by(|a, b| hnamcmp(a.0, b.0)); // c:4270 sorted=1
12061 for (_n, nd) in entries {
12062 printnameddirnode(nd, printflags);
12063 }
12064 }
12065 } else {
12066 // c:4270 — cmdnamtab walk (the default `ht`). PATH lookup
12067 // arr is empty in the printnode call site because per-node
12068 // hashed entries carry their own resolved path.
12069 if let Ok(t) = cmdnamtab_lock().read() {
12070 let mut entries: Vec<_> = t.iter().collect();
12071 entries.sort_by(|a, b| hnamcmp(a.0, b.0)); // c:4270 sorted=1
12072 // ZSHRS-ONLY: bash heads its `hash` listing with a
12073 // `hits<TAB>command` column header. No other shell here
12074 // prints one, and zsh's port has no concept of it.
12075 if !entries.is_empty() {
12076 if let Some(h) = crate::extensions::emulation_output::hash_header() {
12077 println!("{h}");
12078 }
12079 }
12080 for (_n, cn) in entries {
12081 // c:4270 — `scanhashtable(cmdnamtab, ..., printcmdnamnode, ...)`
12082 printcmdnamnode(cn, printflags);
12083 }
12084 }
12085 }
12086 unqueue_signals(); // c:4271
12087 return 0; // c:4272
12088 }
12089
12090 // c:4276-4329 — name-list dispatch, both literal and -m glob.
12091 queue_signals(); // c:4276
12092 let mut idx = 0;
12093 while idx < argv.len() {
12094 // c:4277
12095 let arg = &argv[idx];
12096 idx += 1;
12097 if OPT_ISSET(ops, b'm') {
12098 // c:4279
12099 // c:4280-4290 — glob-match path.
12100 let pprog = patcompile(
12101 &{
12102 let mut __pat_tok = crate::pattern_data_escape::escape_data_backslashes(&arg); // a backslash tokenize leaves raw is DATA (c:Src/glob.c:3651)
12103 crate::ported::glob::tokenize(&mut __pat_tok);
12104 __pat_tok
12105 }, // c:4282
12106 PAT_HEAPDUP,
12107 None,
12108 );
12109 if let Some(prog) = pprog {
12110 if dir_mode {
12111 if let Ok(t) = nameddirtab().lock() {
12112 let mut entries: Vec<_> = t.iter().collect();
12113 entries.sort_by(|a, b| hnamcmp(a.0, b.0)); // c:4286 sorted=1
12114 for (n, nd) in entries {
12115 if pattry(&prog, n) {
12116 // c:4286
12117 printnameddirnode(nd, printflags);
12118 }
12119 }
12120 }
12121 } else {
12122 // c:4286 — `scanmatchtable(ht, pprog, 1, 0, 0, ht->printnode,
12123 // printflags)`. `ht` is cmdnamtab unless `-d` selected
12124 // nameddirtab, and this arm was missing entirely: `hash -m
12125 // 'aw*'` matched nothing and exited 0, whatever the command
12126 // table held. It went unnoticed while the table was always
12127 // empty.
12128 if let Ok(t) = cmdnamtab_lock().read() {
12129 let mut entries: Vec<_> = t.iter().collect();
12130 entries.sort_by(|a, b| hnamcmp(a.0, b.0)); // c:4286 sorted=1
12131 for (n, cn) in entries {
12132 if pattry(&prog, n) {
12133 // c:4286
12134 printcmdnamnode(cn, printflags);
12135 }
12136 }
12137 }
12138 }
12139 } else {
12140 zwarnnam(name, &format!("bad pattern : {}", arg)); // c:4292
12141 returnval = 1; // c:4293
12142 }
12143 continue;
12144 }
12145 // c:4297-4317 — literal name=value or name-only.
12146 let (n, val) = match arg.find('=') {
12147 Some(eq) => (&arg[..eq], Some(&arg[eq + 1..])),
12148 None => (arg.as_str(), None),
12149 };
12150 if let Some(v) = val {
12151 // c:4302
12152 // Define entry.
12153 if dir_mode {
12154 // c:4302
12155 // c:4303-4310 — `itype_end(asg->name, IUSER, 0)` validates;
12156 // dir name must be all-IUSER chars. IUSER (Src/utils.c:
12157 // 4173-4191) = digits + alpha + `_` + `-` + `.` (plus
12158 // non-ASCII). The previous port allowed only alphanumeric
12159 // + `_`, so `hash -d t-t=/foo` was rejected with "invalid
12160 // character", which then broke `~t-t` named-dir expansion.
12161 if !n
12162 .chars()
12163 .all(|c| c.is_alphanumeric() || c == '_' || c == '-' || c == '.')
12164 {
12165 // c:4305
12166 zwarnnam(name, &format!("invalid character in directory name: {}", n)); // c:4306
12167 returnval = 1; // c:4308
12168 continue; // c:4309
12169 }
12170 // c:Src/builtin.c:80 — `BUILTIN("hash", BINF_MAGICEQUALS,
12171 // …)`. The MAGICEQUALS flag causes the C parser to
12172 // apply tilde/equals expansion to `name=value` argv
12173 // entries via Src/utils.c::quotestring's MAGICEQUALSUBST
12174 // arm at c:6304. By the time argv reaches bin_hash, the
12175 // value has already been tilde-expanded.
12176 //
12177 // The Rust pipeline doesn't apply MAGICEQUALS at the
12178 // parser/compile level for `hash -d` args, so the
12179 // literal `~` reaches here untouched. Bug #32 in
12180 // docs/BUGS.md: `hash -d zh=~` stored `'~'` instead
12181 // of $HOME. Apply tilde expansion locally by routing
12182 // the value through filesubstr — re-tokenize a
12183 // leading `~` to the Tilde TOKEN (\u{e198}) which
12184 // filesubstr keys on per its strict-token
12185 // contract (subst.rs:1761).
12186 let expanded_v: String = if v.starts_with('~') {
12187 let rest = &v[1..];
12188 let tokenized = format!("\u{e198}{}", rest);
12189 crate::ported::subst::filesubstr(&tokenized, false)
12190 .unwrap_or_else(|| v.to_string())
12191 } else {
12192 v.to_string()
12193 };
12194 let nd = nameddir {
12195 node: hashnode {
12196 next: None,
12197 nam: n.to_string(),
12198 flags: 0,
12199 },
12200 dir: expanded_v,
12201 diff: 0,
12202 };
12203 addnameddirnode(n, nd); // c:4314
12204 } else {
12205 // c:4313-4318 — `Cmdnam cn = zshcalloc(sizeof *cn);
12206 // cn->node.flags = HASHED;
12207 // cn->u.cmd = ztrdup(asg->value.scalar);
12208 // ht->addnode(ht, ztrdup(asg->name), hn);`
12209 // Insert into cmdnamtab so `hash myc` lookup hits it
12210 // (was storing in `__zshrs_hash_*` env var — fakery
12211 // that the user-facing `hash myc` query never read).
12212 let cn = cmdnam {
12213 node: hashnode {
12214 next: None,
12215 nam: n.to_string(),
12216 flags: HASHED as i32, // c:4316
12217 },
12218 name: None,
12219 cmd: Some(v.to_string()), // c:4316
12220 };
12221 if let Ok(mut tab) = cmdnamtab_lock().write() {
12222 tab.add(cn); // c:4318 addnode
12223 }
12224 }
12225 if OPT_ISSET(ops, b'v') {
12226 // c:4321
12227 if dir_mode {
12228 if let Ok(t) = nameddirtab().lock() {
12229 if let Some(nd) = t.get(n) {
12230 // c:4322
12231 printnameddirnode(nd, 0);
12232 }
12233 }
12234 } else if let Some(cn) =
12235 cmdnamtab_lock().read().ok().and_then(|t| t.get(n).cloned())
12236 {
12237 // c:4301-4302 — `if (OPT_ISSET(ops, 'v')) ht->printnode(hn, 0);`
12238 // cmdnamtab's printnode is printcmdnamnode (hashtable.c:739).
12239 crate::ported::hashtable::printcmdnamnode(&cn, 0);
12240 }
12241 }
12242 } else {
12243 // c:4323-4334 — display existing entry / look up.
12244 if dir_mode {
12245 let snapshot = nameddirtab().lock().ok().and_then(|t| t.get(n).cloned());
12246 match snapshot {
12247 Some(nd) => {
12248 if OPT_ISSET(ops, b'v') {
12249 // c:4337
12250 printnameddirnode(&nd, 0);
12251 }
12252 }
12253 None => {
12254 zwarnnam(name, &format!("no such directory name: {}", n)); // c:4327
12255 returnval = 1; // c:4328
12256 }
12257 }
12258 } else {
12259 // c:4319-4334 — `else if (!(hn = ht->getnode2(ht,
12260 // asg->name))) { ... if (!hashcmd(asg->name, path))
12261 // zwarnnam("no such command"); }`. C path: first
12262 // check cmdnamtab for an existing entry; only fall
12263 // back to hashcmd's PATH walk when not present. The
12264 // previous Rust port skipped the cmdnamtab check, so
12265 // a prior `hash myc=/path` insert was invisible to
12266 // the matching `hash myc` query.
12267 let in_cmdnamtab = cmdnamtab_lock()
12268 .read()
12269 .map(|t| t.get(n).is_some())
12270 .unwrap_or(false);
12271 if !in_cmdnamtab {
12272 // c:4319 hn == NULL → try hashcmd.
12273 let path: Vec<String> = getsparam("PATH")
12274 .map(|p| p.split(':').map(String::from).collect())
12275 .unwrap_or_default();
12276 if crate::ported::exec::hashcmd(n, &path).is_none() {
12277 // c:4332
12278 zwarnnam(name, &format!("no such command: {}", n)); // c:4333
12279 returnval = 1; // c:4334
12280 }
12281 }
12282 // c:4317-4321 — `if(OPT_ISSET(ops,'v') && (hn = ht->getnode2(ht,
12283 // asg->name))) ht->printnode(hn, 0); } else if(OPT_ISSET(ops,'v'))
12284 // ht->printnode(hn, 0);` — both the freshly-hashed (hashcmd added
12285 // the node, exec.rs hashcmd c:1036) and the already-present arm print.
12286 if OPT_ISSET(ops, b'v') {
12287 if let Some(cn) = cmdnamtab_lock().read().ok().and_then(|t| t.get(n).cloned())
12288 {
12289 crate::ported::hashtable::printcmdnamnode(&cn, 0);
12290 }
12291 }
12292 }
12293 }
12294 }
12295 unqueue_signals(); // c:4346
12296 returnval // c:4346
12297}
12298
12299/// Port of `bin_unhash()` from `Src/builtin.c:4346`.
12300/// C decl: `bin_unhash(char *name, char **argv, Options ops, int func)`
12301/// C: `int bin_unhash(char *name, char **argv, Options ops, int func)` —
12302/// remove entries from cmdnamtab/aliastab/sufaliastab/nameddirtab/
12303/// shfunctab. `-a` clears all, `-m` is a glob.
12304/// WARNING: param names don't match C — Rust=(name, argv, func) vs C=(name, argv, ops, func)
12305pub fn bin_unhash(
12306 name: &str,
12307 argv: &[String], // c:4346
12308 ops: &options,
12309 func: i32,
12310) -> i32 {
12311 let mut returnval = 0i32; // c:4351
12312 let mut all = 0i32; // c:4351
12313 let mut match_count = 0i32; // c:4351
12314
12315 // PFA-SMR aspect: when invoked as `unalias`, record the un-alias
12316 // events so the replay can suppress earlier `alias` calls.
12317 #[cfg(feature = "recorder")]
12318 if crate::recorder::is_enabled() && func == crate::ported::builtin::BIN_UNALIAS {
12319 let ctx = crate::recorder::recorder_ctx_global();
12320 for a in argv {
12321 if a.starts_with('-') && a != "-" {
12322 continue;
12323 }
12324 crate::recorder::emit_unalias(a, ctx.clone());
12325 }
12326 }
12327
12328 // c:4355-4373 — table-pick dispatch.
12329 enum Tab {
12330 CmdNam,
12331 NamedDir,
12332 Shfunc,
12333 Alias,
12334 SufAlias,
12335 }
12336 let tab: Tab;
12337 if func == BIN_UNALIAS {
12338 // c:4356
12339 tab = if OPT_ISSET(ops, b's') {
12340 Tab::SufAlias
12341 } else {
12342 Tab::Alias
12343 }; // c:4357
12344 if OPT_ISSET(ops, b'a') {
12345 // c:4361
12346 if !argv.is_empty() {
12347 // c:4362
12348 zwarnnam(name, "-a: too many arguments"); // c:4363
12349 return 1; // c:4364
12350 }
12351 all = 1; // c:4366
12352 } else if argv.is_empty() {
12353 // c:4367
12354 zwarnnam(name, "not enough arguments"); // c:4368
12355 return 1; // c:4369
12356 }
12357 } else if OPT_ISSET(ops, b'd') {
12358 tab = Tab::NamedDir; // c:4370
12359 } else if OPT_ISSET(ops, b'f') {
12360 tab = Tab::Shfunc; // c:4372
12361 } else if OPT_ISSET(ops, b's') {
12362 tab = Tab::SufAlias; // c:4374
12363 } else if func == BIN_UNHASH && OPT_ISSET(ops, b'a') {
12364 tab = Tab::Alias; // c:4376
12365 } else {
12366 tab = Tab::CmdNam;
12367 } // c:4378
12368
12369 // Helper: clear entire table.
12370 let clear_all = |t: &Tab| match t {
12371 Tab::Alias => {
12372 let _ = aliastab_lock().write().map(|mut g| g.clear());
12373 }
12374 Tab::SufAlias => {
12375 let _ = sufaliastab_lock().write().map(|mut g| g.clear());
12376 }
12377 Tab::NamedDir => {
12378 emptynameddirtable();
12379 }
12380 Tab::Shfunc => {
12381 // c:4388 — empty whole shfunctab (`unhash -af` etc.). C uses
12382 // `emptyhashtable(shfunctab)` GSU; Rust port iterates names
12383 // and removes each (no `clear` method on shfunc_table).
12384 if let Ok(mut t) = shfunctab_lock().write() {
12385 let names: Vec<String> = t.iter().map(|(k, _)| k.clone()).collect();
12386 for nm in names {
12387 let _ = t.remove(&nm);
12388 }
12389 }
12390 }
12391 Tab::CmdNam => {
12392 emptycmdnamtable();
12393 } // c:4389
12394 };
12395 let remove_one = |t: &Tab, nm: &str| -> bool {
12396 match t {
12397 Tab::Alias => aliastab_lock()
12398 .write()
12399 .map(|mut g| g.remove(nm).is_some())
12400 .unwrap_or(false),
12401 Tab::SufAlias => sufaliastab_lock()
12402 .write()
12403 .map(|mut g| g.remove(nm).is_some())
12404 .unwrap_or(false),
12405 Tab::NamedDir => crate::ported::hashnameddir::removenameddirnode(nm).is_some(),
12406 Tab::Shfunc => {
12407 // Lineage tap: the removal is the last op on the
12408 // function's chain, the way `unset` is on a parameter's.
12409 if crate::provenance::active() {
12410 crate::provenance::on_func_unset(nm);
12411 }
12412 // c:4405 — `if ((hn = ht->removenode(ht, *argv)))`. For
12413 // shfunctab `removenode` IS `removeshfuncnode`
12414 // (c:Src/hashtable.c:825 `shfunctab->removenode =
12415 // removeshfuncnode;`), which routes a `TRAP<SIG>` name
12416 // through `removetrap` so the LOCAL_TRAPS save/restore and
12417 // the sigtrapped[] teardown happen. Calling the raw table
12418 // remove here skipped both: `unfunction TRAPZERR` inside a
12419 // `setopt localtraps` function left the trap armed with a
12420 // dangling function and never restored the outer handler
12421 // (C03traps:13,14).
12422 let from_tab = crate::ported::hashtable::removeshfuncnode(nm).is_some();
12423 // Also remove from the executor's compiled-function /
12424 // source maps. Without this, `unset -f f` cleared
12425 // shfunctab but dispatch_function_call still found the
12426 // compiled chunk and ran the old body. Routed via the
12427 // exec accessors unregister_function fn-ptr installed by
12428 // fusevm_bridge at startup (no ShellExecutor reach-in
12429 // from src/ported/).
12430 let from_exec = crate::ported::exec::unregister_function(nm);
12431 from_tab || from_exec
12432 }
12433 // c:4405 — `if ((hn = ht->removenode(ht, *argv)))`.
12434 // Removal returns truthy only when the entry actually
12435 // existed. Previous Rust port hardcoded `true` after a
12436 // void-return `freecmdnamnode` call, so `unhash badname`
12437 // silently succeeded instead of emitting the canonical
12438 // "no such hash table element" error.
12439 Tab::CmdNam => cmdnamtab_lock()
12440 .write()
12441 .map(|mut g| g.remove(nm).is_some())
12442 .unwrap_or(false),
12443 }
12444 };
12445
12446 if all != 0 {
12447 // c:4382
12448 queue_signals(); // c:4383
12449 clear_all(&tab); // c:4384-4389
12450 unqueue_signals(); // c:4390
12451 return 0; // c:4391
12452 }
12453
12454 // c:4395-4421 — `-m` glob branch.
12455 if OPT_ISSET(ops, b'm') {
12456 // c:4395
12457 for arg in argv {
12458 // c:4396
12459 queue_signals(); // c:4397
12460 let pprog = patcompile(
12461 &{
12462 let mut __pat_tok = crate::pattern_data_escape::escape_data_backslashes(&arg); // a backslash tokenize leaves raw is DATA (c:Src/glob.c:3651)
12463 crate::ported::glob::tokenize(&mut __pat_tok);
12464 __pat_tok
12465 }, // c:4400
12466 PAT_HEAPDUP,
12467 None,
12468 );
12469 if let Some(prog) = pprog {
12470 // Collect names then remove (avoid iterator/mutation conflict).
12471 // c:4408 — `scanmatchtable(ht, pprog, ...)` walks every
12472 // entry in the selected table. Previous Rust port left
12473 // Tab::CmdNam returning an empty Vec, so `unhash -m PAT`
12474 // (default cmd-hash table) silently matched zero entries.
12475 let names: Vec<String> = match &tab {
12476 Tab::Alias => aliastab_lock()
12477 .read()
12478 .map(|t| t.iter().map(|(n, _)| n.clone()).collect())
12479 .unwrap_or_default(),
12480 Tab::SufAlias => sufaliastab_lock()
12481 .read()
12482 .map(|t| t.iter().map(|(n, _)| n.clone()).collect())
12483 .unwrap_or_default(),
12484 Tab::NamedDir => nameddirtab()
12485 .lock()
12486 .map(|t| t.keys().cloned().collect())
12487 .unwrap_or_default(),
12488 Tab::Shfunc => shfunctab_lock()
12489 .read()
12490 .map(|t| t.iter().map(|(k, _)| k.clone()).collect())
12491 .unwrap_or_default(),
12492 // c:4408 — cmdnamtab walk via `cmdnamtab_lock().iter()`.
12493 Tab::CmdNam => cmdnamtab_lock()
12494 .read()
12495 .map(|t| t.iter().map(|(n, _)| n.clone()).collect())
12496 .unwrap_or_default(),
12497 };
12498 for nm in &names {
12499 if pattry(&prog, nm) {
12500 // c:4408
12501 if remove_one(&tab, nm) {
12502 match_count += 1; // c:4410
12503 }
12504 }
12505 }
12506 } else {
12507 zwarnnam(name, &format!("bad pattern : {}", arg)); // c:4416
12508 returnval = 1; // c:4417
12509 }
12510 unqueue_signals(); // c:4419
12511 }
12512 if match_count == 0 {
12513 // c:4424
12514 returnval = 1; // c:4425
12515 }
12516 return returnval; // c:4426
12517 }
12518
12519 // c:4429-4439 — literal-name removals.
12520 queue_signals(); // c:4430
12521 for arg in argv {
12522 // c:4431
12523 if remove_one(&tab, arg) { // c:4432
12524 // freed
12525 } else if func == BIN_UNSET && isset(POSIXBUILTINS) {
12526 // c:4434 — POSIX: unset of nonexistent isn't an error.
12527 returnval = 0; // c:4435
12528 } else {
12529 zwarnnam(name, &format!("no such hash table element: {}", arg)); // c:4437
12530 returnval = 1; // c:4450
12531 }
12532 }
12533 unqueue_signals(); // c:4450
12534 returnval // c:4450
12535}
12536
12537/// Port of `bin_alias()` from `Src/builtin.c:4450`.
12538/// C decl: `bin_alias(char *name, char **argv, Options ops, UNUSED(int func))`
12539/// C: `int bin_alias(char *name, char **argv, Options ops, ...)` — list,
12540/// define, glob-list, or display aliases. `-r`/`-g`/`-s` filter type;
12541/// `-L` prints definitions; `-m` treats args as patterns.
12542/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
12543pub fn bin_alias(
12544 name: &str,
12545 argv: &[String], // c:4450
12546 ops: &options,
12547 _func: i32,
12548) -> i32 {
12549 let mut returnval = 0i32; // c:4455
12550 let mut flags1 = 0u32; // c:4456
12551 let mut flags2 = DISABLED as u32; // c:4456
12552 let mut printflags = 0i32; // c:4457
12553 let mut use_suffix = false; // tracks ht switch
12554
12555 // ZSHRS-ONLY. `alias -t` lists the Korn shells' TRACKED aliases,
12556 // which are the command hash table — mksh defines its `hash` alias as
12557 // `\builtin alias -t`, so without this `hash` in `--mksh` failed with
12558 // "bad option: -t". zsh has no `-t`, so every other personality keeps
12559 // rejecting it exactly as before.
12560 if OPT_ISSET(ops, b't') {
12561 use crate::extensions::emulation_startup::{personality, Personality};
12562 if !matches!(
12563 personality(),
12564 Personality::Ksh93 | Personality::Mksh | Personality::Pdksh
12565 ) {
12566 zwarnnam(name, "bad option: -t");
12567 return 1;
12568 }
12569 // With NAME operands `alias -t` tracks those commands, which is
12570 // exactly `hash NAME` — mksh's `hash` alias relies on both halves
12571 // (`hash ls` becomes `alias -t ls`, bare `hash` becomes
12572 // `alias -t`). Delegate rather than re-implement the PATH search.
12573 if !argv.is_empty() {
12574 let hash_ops = crate::ported::zsh_h::options {
12575 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
12576 args: Vec::new(),
12577 argscount: 0,
12578 argsalloc: 0,
12579 };
12580 return bin_hash(name, argv, &hash_ops, 0);
12581 }
12582 queue_signals();
12583 if let Ok(t) = cmdnamtab_lock().read() {
12584 let mut entries: Vec<_> = t.iter().collect();
12585 entries.sort_by(|a, b| hnamcmp(a.0, b.0));
12586 for (_n, cn) in entries {
12587 printcmdnamnode(cn, 0);
12588 }
12589 }
12590 unqueue_signals();
12591 return 0;
12592 }
12593
12594 // c:4461-4485 — type-flag parsing.
12595 let type_opts = (OPT_ISSET(ops, b'r') as i32) // c:4461
12596 + (OPT_ISSET(ops, b'g') as i32)
12597 + (OPT_ISSET(ops, b's') as i32);
12598 if type_opts != 0 {
12599 // c:4464
12600 if type_opts > 1 {
12601 // c:4465
12602 zwarnnam(name, "invalid combination of options"); // c:4466
12603 return 1; // c:4467
12604 }
12605 if OPT_ISSET(ops, b'g') {
12606 // c:4469
12607 flags1 |= ALIAS_GLOBAL as u32; // c:4470
12608 } else {
12609 flags2 |= ALIAS_GLOBAL as u32; // c:4472
12610 }
12611 if OPT_ISSET(ops, b's') {
12612 // c:4473
12613 flags1 |= ALIAS_SUFFIX as u32; // c:4480
12614 use_suffix = true; // c:4481
12615 } else {
12616 flags2 |= ALIAS_SUFFIX as u32; // c:4483
12617 }
12618 }
12619
12620 // c:4486-4490 — printflags from -L / + suffix.
12621 if OPT_ISSET(ops, b'L') {
12622 // c:4486
12623 printflags |= PRINT_LIST; // c:4487
12624 } else if OPT_PLUS(ops, b'g')
12625 || OPT_PLUS(ops, b'r')
12626 || OPT_PLUS(ops, b's')
12627 || OPT_PLUS(ops, b'm')
12628 || OPT_ISSET(ops, b'+')
12629 // c:4488
12630 {
12631 printflags |= PRINT_NAMEONLY; // c:4490
12632 }
12633
12634 // C bin_alias dispatches printing via `ht->printnode` (set to
12635 // `printaliasnode` at hashtable.c:1208) — `scanhashtable`,
12636 // `scanmatchtable`, and the single-name branch all call
12637 // `ht->printnode(&a->node, printflags)`. The Rust port routes
12638 // through the canonical `printaliasnode` (hashtable.rs:1477) for
12639 // the same dispatch. No local closure.
12640
12641 // c:4495-4500 — no args: list all (filtered by flags).
12642 if argv.is_empty() {
12643 // c:4495
12644 queue_signals(); // c:4496
12645 let lock = if use_suffix {
12646 sufaliastab_lock()
12647 } else {
12648 aliastab_lock()
12649 };
12650 if let Ok(t) = lock.read() {
12651 // c:Src/builtin.c:4496 — `scanhashtable(aliastab, 1, ...)`
12652 // sets the SORT flag (1). The C source walks the table
12653 // sorted by `hnamcmp` (byte-wise ASCII compare); the
12654 // Rust port iterated in arbitrary hash order, so
12655 // `alias` output sequence diverged from zsh's
12656 // (uppercase-before-lowercase) ASCII ordering.
12657 let mut entries: Vec<_> = t.iter().collect();
12658 entries.sort_by(|(a, _), (b, _)| a.cmp(b));
12659 for (_n, a) in entries {
12660 // c:4497
12661 if (a.node.flags & flags1 as i32) == flags1 as i32
12662 && (a.node.flags & flags2 as i32) == 0
12663 {
12664 printaliasnode(a, printflags);
12665 }
12666 }
12667 }
12668 unqueue_signals(); // c:4498
12669 return 0; // c:4499
12670 }
12671
12672 // c:4503-4519 — `-m` glob branch.
12673 if OPT_ISSET(ops, b'm') {
12674 // c:4503
12675 for pat in argv {
12676 // c:4504
12677 queue_signals(); // c:4505
12678 // c:4506 — `tokenize + patcompile`.
12679 let pprog = patcompile(
12680 &{
12681 let mut __pat_tok = crate::pattern_data_escape::escape_data_backslashes(&pat); // a backslash tokenize leaves raw is DATA (c:Src/glob.c:3651)
12682 crate::ported::glob::tokenize(&mut __pat_tok);
12683 __pat_tok
12684 }, // c:4507
12685 PAT_HEAPDUP,
12686 None,
12687 );
12688 if let Some(prog) = pprog {
12689 let lock = if use_suffix {
12690 sufaliastab_lock()
12691 } else {
12692 aliastab_lock()
12693 };
12694 if let Ok(t) = lock.read() {
12695 for (_n, a) in t.iter() {
12696 // c:4509
12697 if (a.node.flags & flags1 as i32) == flags1 as i32
12698 && (a.node.flags & flags2 as i32) == 0
12699 && pattry(&prog, &a.node.nam)
12700 {
12701 printaliasnode(a, printflags);
12702 }
12703 }
12704 }
12705 } else {
12706 zwarnnam(name, &format!("bad pattern : {}", pat)); // c:4514
12707 returnval = 1; // c:4515
12708 }
12709 unqueue_signals(); // c:4517
12710 }
12711 return returnval; // c:4518
12712 }
12713
12714 // c:4521-4540 — literal args: define `name=value` or display a single name.
12715 queue_signals(); // c:4522
12716 // PFA-SMR: capture per-definition for replay. Dispatch the
12717 // right subkind (galias/salias/alias) based on the parsed
12718 // flag bits set above. Same per-name loop the C code walks
12719 // — one record per `name=value` argv slot. Without this,
12720 // the recorder harness saw zero `alias` captures.
12721 #[cfg(feature = "recorder")]
12722 let recorder_active = crate::recorder::is_enabled();
12723 #[cfg(feature = "recorder")]
12724 let recorder_ctx = if recorder_active {
12725 Some(crate::recorder::recorder_ctx_global())
12726 } else {
12727 None
12728 };
12729 let mut idx = 0;
12730 while idx < argv.len() {
12731 // c:4523
12732 let arg = &argv[idx];
12733 idx += 1;
12734 // c:Src/builtin.c:4523 `while ((asg = getasg(&argv, NULL)))`
12735 // — empty-name assignment makes getasg call `zerr("bad
12736 // assignment")` (c:1927, sets errflag) and return NULL. The
12737 // C while-loop exits without setting returnval, so bin_alias
12738 // returns 0. /bin/zsh's outer execution checks errflag and
12739 // aborts subsequent commands but keeps lastval=0.
12740 //
12741 // Rust port divergence: the canonical `zerr` here would set
12742 // ERRFLAG_ERROR, which zshrs's execcmd_exec promotes to
12743 // lastval=1 (different shape from C's "abort but keep
12744 // lastval"). Route through `zwarnnam` so the diagnostic
12745 // emits without flipping errflag; returnval stays 0 and the
12746 // shell rc matches /bin/zsh's observed behavior.
12747 if arg.starts_with('=') {
12748 zwarnnam(name, "bad assignment");
12749 break;
12750 }
12751 if let Some(eq) = arg.find('=') {
12752 // c:4524 (asg->value.scalar)
12753 if !OPT_ISSET(ops, b'L') {
12754 // c:4524
12755 let n = &arg[..eq];
12756 let v = &arg[eq + 1..];
12757 let lock = if use_suffix {
12758 sufaliastab_lock()
12759 } else {
12760 aliastab_lock()
12761 };
12762 if let Ok(mut t) = lock.write() {
12763 let a = createaliasnode(n, v, flags1); // c:4527
12764 t.add(a);
12765 }
12766 #[cfg(feature = "recorder")]
12767 if let Some(ref ctx) = recorder_ctx {
12768 if (flags1 & ALIAS_GLOBAL as u32) != 0 {
12769 crate::recorder::emit_galias(n, Some(v), ctx.clone());
12770 } else if (flags1 & ALIAS_SUFFIX as u32) != 0 {
12771 crate::recorder::emit_salias(n, Some(v), ctx.clone());
12772 } else {
12773 crate::recorder::emit_alias(n, Some(v), ctx.clone());
12774 }
12775 }
12776 continue;
12777 }
12778 }
12779 let n = if let Some(eq) = arg.find('=') {
12780 &arg[..eq]
12781 } else {
12782 arg.as_str()
12783 };
12784 let lock = if use_suffix {
12785 sufaliastab_lock()
12786 } else {
12787 aliastab_lock()
12788 };
12789 // c:4530 — `(a = (Alias) ht->getnode(ht, asg->name))`. C
12790 // `getnode` is `gethashnode` (Src/hashtable.c:231) which
12791 // returns NULL for DISABLED entries; skipping disabled at
12792 // lookup time is what makes `alias FOO` print nothing after
12793 // `disable -a FOO` (bug #227). Must use `.get()` not
12794 // `.get_including_disabled()` here.
12795 let found = lock.read().ok().and_then(|t| {
12796 t.get(n)
12797 .map(|a| (a.node.nam.clone(), a.node.flags as u32, a.text.clone()))
12798 });
12799 match found {
12800 Some((nm, fl, txt)) => {
12801 // c:4530
12802 // c:4532-4537 — type-filter check.
12803 let show = type_opts == 0
12804 || use_suffix
12805 || (OPT_ISSET(ops, b'r') && (fl & (ALIAS_GLOBAL | ALIAS_SUFFIX) as u32) == 0)
12806 || (OPT_ISSET(ops, b'g') && (fl & ALIAS_GLOBAL as u32) != 0);
12807 if show {
12808 let a = createaliasnode(&nm, &txt, fl);
12809 printaliasnode(&a, printflags);
12810 }
12811 }
12812 None => {
12813 // c:4538
12814 returnval = 1; // c:4539
12815 }
12816 }
12817 }
12818 unqueue_signals(); // c:4541
12819 returnval // c:4542
12820}
12821
12822/// Port of `bin_true()` from `Src/builtin.c:4550`.
12823/// C decl: `bin_true(UNUSED(char *name), UNUSED(char **argv), UNUSED(Options ops), UNUSED(int func))`
12824/// C: `int bin_true(UNUSED(char *name), UNUSED(char **argv),
12825/// UNUSED(Options ops), UNUSED(int func))` → `return 0;`
12826/// WARNING: param names don't match C — Rust=(_name, _argv, _func) vs C=(name, argv, ops, func)
12827pub fn bin_true(
12828 _name: &str,
12829 _argv: &[String], // c:4550
12830 _ops: &options,
12831 _func: i32,
12832) -> i32 {
12833 0 // c:4559
12834}
12835
12836/// Port of `bin_false()` from `Src/builtin.c:4559`.
12837/// C decl: `bin_false(UNUSED(char *name), UNUSED(char **argv), UNUSED(Options ops), UNUSED(int func))`
12838/// C: `int bin_false(UNUSED(char *name), UNUSED(char **argv),
12839/// UNUSED(Options ops), UNUSED(int func))` → `return 1;`
12840/// WARNING: param names don't match C — Rust=(_name, _argv, _func) vs C=(name, argv, ops, func)
12841pub fn bin_false(
12842 _name: &str,
12843 _argv: &[String], // c:4559
12844 _ops: &options,
12845 _func: i32,
12846) -> i32 {
12847 1 // c:4562
12848}
12849
12850/// Port of `bin_print()` from `Src/builtin.c:4587`.
12851/// C decl: `bin_print(char *name, char **args, Options ops, int func)`
12852/// C: `int bin_print(char *name, char **args, Options ops, int func)`.
12853///
12854/// The C body is ~1000 lines: `print` / `echo` / `printf` / `pushln`
12855/// dispatcher with -n/-N/-c/-r/-R/-l/-D/-i/-f/-v/-s/-S/-z/-e/-E etc.
12856/// The structural port handles the script-friendly subset that the
12857/// daily-driver hits: print/echo plain emission with -n, -l (one per
12858/// line), -r raw, -E newline-only, -- end-of-options. The full -f
12859/// printf format-spec engine and ZLE/history wireups defer to the
12860/// expand_printf_escapes helpers.
12861/// WARNING: param names don't match C — Rust=(name, args, func) vs C=(name, args, ops, func)
12862pub fn bin_print(
12863 name: &str,
12864 args: &[String], // c:4587
12865 ops: &options,
12866 func: i32,
12867) -> i32 {
12868 // c:4659-4684 — "Error check option combinations and option arguments".
12869 // The FIRST thing bin_print does, before any argument work:
12870 // if (OPT_ISSET(ops,'z') + OPT_ISSET(ops,'s') + OPT_ISSET(ops,'S') +
12871 // OPT_ISSET(ops,'v') > 1) {
12872 // zwarnnam(name, "only one of -s, -S, -v, or -z allowed"); return 1; }
12873 // if ((OPT_ISSET(ops,'z') | OPT_ISSET(ops,'s') | OPT_ISSET(ops,'S')) +
12874 // (OPT_ISSET(ops,'c') | OPT_ISSET(ops,'C')) > 1) {
12875 // zwarnnam(name, "-c or -C not allowed with -s, -S, or -z"); return 1; }
12876 // if ((OPT_ISSET(ops,'z') | OPT_ISSET(ops,'v') | OPT_ISSET(ops,'s') |
12877 // OPT_ISSET(ops,'S')) + (OPT_ISSET(ops,'p') | OPT_ISSET(ops,'u')) > 1) {
12878 // zwarnnam(name, "-p or -u not allowed with -s, -S, -v, or -z"); return 1; }
12879 //
12880 // None of the three were ported, so every conflicting combination was
12881 // silently accepted: `print -s -v v a b`, `print -C 2 -s a b` and
12882 // `print -u1 -v v a b` all returned 0 having done something other than what
12883 // zsh does. Note the mixed operators are deliberate — `+` counts DISTINCT
12884 // options in the first test (so `-s -S` is two), while `|` collapses each
12885 // GROUP in the other two before counting, so the test is "one from each
12886 // group", not "two options total".
12887 //
12888 // C's fourth check ("-f not allowed with -c, -C, or -S", c:4679-4683) is
12889 // COMMENTED OUT and must NOT be reinstated: `print -f %s -c a b` is legal
12890 // and prints, as verified against the oracle. The string exists in the
12891 // source but the code is dead — the reason to read the C rather than grep
12892 // its message table.
12893 //
12894 // These precede the `-C` argument validation below, matching C, so
12895 // `print -C 0 -s` reports the conflict rather than the bad column count.
12896 {
12897 let n_sv = i32::from(OPT_ISSET(ops, b'z'))
12898 + i32::from(OPT_ISSET(ops, b's'))
12899 + i32::from(OPT_ISSET(ops, b'S'))
12900 + i32::from(OPT_ISSET(ops, b'v'));
12901 if n_sv > 1 {
12902 zwarnnam(name, "only one of -s, -S, -v, or -z allowed"); // c:4664
12903 return 1; // c:4665
12904 }
12905 let g_zsS = OPT_ISSET(ops, b'z') || OPT_ISSET(ops, b's') || OPT_ISSET(ops, b'S');
12906 let g_cC = OPT_ISSET(ops, b'c') || OPT_ISSET(ops, b'C');
12907 if i32::from(g_zsS) + i32::from(g_cC) > 1 {
12908 zwarnnam(name, "-c or -C not allowed with -s, -S, or -z"); // c:4670
12909 return 1; // c:4671
12910 }
12911 let g_zvsS = OPT_ISSET(ops, b'z')
12912 || OPT_ISSET(ops, b'v')
12913 || OPT_ISSET(ops, b's')
12914 || OPT_ISSET(ops, b'S');
12915 let g_pu = OPT_ISSET(ops, b'p') || OPT_ISSET(ops, b'u');
12916 if i32::from(g_zvsS) + i32::from(g_pu) > 1 {
12917 zwarnnam(name, "-p or -u not allowed with -s, -S, -v, or -z"); // c:4677
12918 return 1; // c:4678
12919 }
12920 }
12921 let nonewline = OPT_ISSET(ops, b'n'); // c:4595
12922 let raw = OPT_ISSET(ops, b'r') || OPT_ISSET(ops, b'R'); // c:4596
12923 // c:5126 — `-l` puts one arg per line; `-c`/`-C` columns are
12924 // handled by the `columnate` block below.
12925 let one_per_line = OPT_ISSET(ops, b'l');
12926 let nul_sep = OPT_ISSET(ops, b'N'); // c:5114/5127/5132 — NUL separator
12927 let dirify_d = OPT_ISSET(ops, b'D');
12928 let _printf_mode = func == BIN_PRINTF || OPT_HASARG(ops, b'f'); // c:4604
12929 let echo_mode = func == BIN_ECHO;
12930 let _ = (name, raw);
12931
12932 // c:Src/builtin.c:5095-5106 — `-x N` (and `-X N`) require the
12933 // argument to be a positive integer (zstrtol parse with no
12934 // trailing garbage AND value > 0). zsh emits
12935 // `positive integer expected after -x: <arg>` and returns 1
12936 // when either gate fails. The previous Rust port silently
12937 // accepted any -x argument and proceeded to print the rest of
12938 // argv. Bug #525.
12939 if OPT_HASARG(ops, b'x') || OPT_HASARG(ops, b'X') {
12940 let which = if OPT_HASARG(ops, b'X') { b'X' } else { b'x' };
12941 let xarg = OPT_ARG(ops, which).unwrap_or("");
12942 // c:Src/builtin.c:5101 — `expand = zstrtol(xarg, &eptr, 10);
12943 // if (*eptr || expand <= 0) zwarnnam(...positive integer
12944 // expected...); return 1;`. zstrtol parses optional
12945 // leading sign + digits and stops at the first non-digit.
12946 // Reject when eptr isn't end-of-string (trailing garbage)
12947 // OR the signed result is <= 0 (zero / negative).
12948 let (expand, eptr) = crate::ported::utils::zstrtol(xarg, 10); // c:5085
12949 let valid = eptr.is_empty() && expand > 0; // c:5086
12950 if !valid {
12951 zwarnnam(
12952 name,
12953 &format!(
12954 "positive integer expected after -{}: {}",
12955 'x', xarg // c:5087 — C always names 'x', even for -X
12956 ),
12957 );
12958 return 1;
12959 }
12960 }
12961
12962 // c:4633-4685 — destination dispatch. -u FD writes to fd, -s pushes
12963 // to history, -z to ZLE buffer, -v VAR assigns to scalar.
12964 let dest_var: Option<String> = if OPT_HASARG(ops, b'v') {
12965 OPT_ARG(ops, b'v').map(String::from)
12966 } else {
12967 None
12968 };
12969 // c:Src/builtin.c:4827-4828 — `-p` writes to the coprocess fd
12970 // (the canonical `coprocout` global; Src/exec.c:430). When a
12971 // coproc is live, write to that fd; otherwise emit the same
12972 // diagnostic + rc=1 zsh does (Src/builtin.c:5050
12973 // `zwarnnam(name, "-p: no coprocess")`). Bug #388.
12974 let print_dash_p_fd: Option<fs::File> = if OPT_ISSET(ops, b'p') {
12975 let coprocout =
12976 crate::ported::modules::clone::coprocout.load(std::sync::atomic::Ordering::Relaxed);
12977 if coprocout < 0 {
12978 zwarnnam(name, "-p: no coprocess");
12979 return 1;
12980 }
12981 // c:4843 — dup for an owned writer that close-on-drop
12982 // doesn't close the user's original fd.
12983 let dup_fd = unsafe { libc::dup(coprocout) };
12984 if dup_fd < 0 {
12985 zwarnnam(name, "-p: no coprocess");
12986 return 1;
12987 }
12988 use std::os::unix::io::FromRawFd;
12989 Some(unsafe { fs::File::from_raw_fd(dup_fd) })
12990 } else {
12991 None
12992 };
12993 // c:4815-4851 — `-u FD` (and `-p` coprocess) dispatch. Parses FD,
12994 // dup's it for an owned descriptor, opens as a File for writes.
12995 // The previous Rust port silently dropped `-u`, so `print -u 2
12996 // hello` went to stdout instead of stderr.
12997 let dest_fd: Option<fs::File> = if OPT_HASARG(ops, b'u') {
12998 // c:4826
12999 let argptr = OPT_ARG(ops, b'u').unwrap_or("");
13000 // c:4827-4835 — undocumented `-up` aliases to coprocout.
13001 let parsed: Result<i32, ()> = if argptr == "p" {
13002 let cout =
13003 crate::ported::modules::clone::coprocout.load(std::sync::atomic::Ordering::Relaxed);
13004 if cout < 0 {
13005 zwarnnam(name, "-p: no coprocess"); // c:4830
13006 return 1; // c:4831
13007 }
13008 Ok(cout) // c:4828
13009 } else {
13010 argptr.parse::<i32>().map_err(|_| ())
13011 };
13012 match parsed {
13013 // c:4835 zstrtol
13014 Ok(fdarg) => {
13015 // DELIBERATE DEVIATION from c:4843-4851, which has no
13016 // fdtable check here: it just `dup()`s and `fdopen()`s,
13017 // and reports "bad file number" only because the `dup`
13018 // of a descriptor zsh does not have fails with EBADF.
13019 //
13020 // That is incidental safety. It holds in C because the
13021 // descriptors zsh keeps for itself above 9 are read-only
13022 // (its `/dev/null`, its sourced script), so `-u` on one
13023 // fails at the `fdopen(fd, "w")` a line later instead —
13024 // "bad mode on fd N". zshrs keeps something C never
13025 // keeps: a long-lived READ-WRITE SQLite handle to the
13026 // user's history database, plus an append handle to the
13027 // log. `print -u 11` therefore returned 0 and wrote into
13028 // the history database — measured, it overwrote the
13029 // SQLite header of a 665 MB file, and `pragma
13030 // quick_check` then reported it was not a database.
13031 //
13032 // So the fdtable is consulted here. This can only make
13033 // zshrs stricter, never looser, and it moves toward zsh
13034 // rather than away from it: the descriptors it refuses
13035 // are ones a real zsh does not have open at all, where
13036 // zsh answers `bad file number: N` with status 1 — which
13037 // is exactly what is produced below. A descriptor the
13038 // SCRIPT owns is `FDT_EXTERNAL` (c:Src/exec.c:2409) or
13039 // one of 0/1/2 (c:Src/init.c:1900) and is unaffected, and
13040 // anything past `max_zsh_fd` is left alone per
13041 // c:Src/exec.c:3886-3891.
13042 let shell_owned = argptr != "p" && fdarg > 9 && {
13043 let max_fd = crate::ported::utils::MAX_ZSH_FD
13044 .load(std::sync::atomic::Ordering::Relaxed);
13045 fdarg <= max_fd && {
13046 let kind = crate::ported::utils::fdtable_get(fdarg)
13047 & crate::ported::zsh_h::FDT_TYPE_MASK;
13048 kind != crate::ported::zsh_h::FDT_UNUSED
13049 && kind != crate::ported::zsh_h::FDT_EXTERNAL
13050 }
13051 };
13052 if shell_owned {
13053 zwarnnam(name, &format!("bad file number: {}", fdarg)); // c:4844
13054 return 1; // c:4845
13055 }
13056 // c:4843 — `dup(fdarg)` for an owned writer that
13057 // close-on-drop doesn't close the user's original fd.
13058 let dup_fd = unsafe { libc::dup(fdarg) };
13059 if dup_fd < 0 {
13060 zwarnnam(name, &format!("bad file number: {}", fdarg)); // c:4844
13061 return 1; // c:4845
13062 }
13063 use std::os::unix::io::FromRawFd;
13064 Some(unsafe { fs::File::from_raw_fd(dup_fd) }) // c:4847
13065 }
13066 Err(_) => {
13067 zwarnnam(name, &format!("number expected after -u: {}", argptr)); // c:4837
13068 return 1; // c:4838
13069 }
13070 }
13071 } else {
13072 None
13073 };
13074
13075 // c:4604-4612 — printf format-string handling.
13076 if _printf_mode {
13077 // c:Src/builtin.c:4701-4706 — `if (func == BIN_PRINTF) { if
13078 // (!strcmp(*args, "--") && !*++args) { ...not enough args...
13079 // } fmt = *args++; }`. Consume a leading `--` end-of-options
13080 // marker (printf is BINF_SKIPDASH but NOT BINF_DASHDASHVALID,
13081 // so the generic flag-parser doesn't strip `--` — bin_print
13082 // does the strip itself for BIN_PRINTF). Without this,
13083 // `printf -- "%s\n" hi` used `--` as the format string. Bug
13084 // #284.
13085 let args_owned: Vec<String>;
13086 let args: &[String] = if func == BIN_PRINTF
13087 && !OPT_HASARG(ops, b'f')
13088 && !args.is_empty()
13089 && args[0] == "--"
13090 {
13091 // Slice past the `--` for the rest of this arm only.
13092 if args.len() == 1 {
13093 // c:4703 `not enough arguments`.
13094 zwarnnam(name, "not enough arguments");
13095 return 1;
13096 }
13097 args_owned = args[1..].to_vec();
13098 &args_owned
13099 } else {
13100 args
13101 };
13102 let fmt = if let Some(f) = OPT_ARG(ops, b'f') {
13103 f.to_string()
13104 } else if !args.is_empty() {
13105 args[0].clone()
13106 } else {
13107 return 0;
13108 };
13109 let rest: &[String] = if OPT_HASARG(ops, b'f') {
13110 args
13111 } else {
13112 &args[1..]
13113 };
13114 // c:4712-4741 — the -m filter runs BEFORE format rendering:
13115 // first remaining arg is the pattern, later args are kept iff
13116 // pattry matches, and `if (fmt && !*args) return 0;` short-
13117 // circuits an empty post-filter list. The fmt fast-branch
13118 // here previously skipped the filter entirely, so
13119 // `print -m -f '%s|' 'a*' apple banana` rendered every arg.
13120 let m_filtered: Vec<String>;
13121 let rest: &[String] = if OPT_ISSET(ops, b'm') {
13122 if rest.is_empty() {
13123 zwarnnam(name, "no pattern specified"); // c:4723
13124 return 1;
13125 }
13126 let pat = &rest[0];
13127 let mut pat_tok = crate::pattern_data_escape::escape_data_backslashes(pat); // a backslash tokenize leaves raw is DATA (c:Src/glob.c:3651)
13128 crate::ported::glob::tokenize(&mut pat_tok); // c:4727
13129 let Some(pprog) = crate::ported::pattern::patcompile(&pat_tok, PAT_STATIC, None) else {
13130 zwarnnam(name, &format!("bad pattern: {}", pat)); // c:4730
13131 return 1;
13132 };
13133 m_filtered = rest[1..]
13134 .iter()
13135 .filter(|a| crate::ported::pattern::pattry(&pprog, a)) // c:4735-4737
13136 .cloned()
13137 .collect();
13138 if m_filtered.is_empty() {
13139 return 0; // c:4741 `if (fmt && !*args) return 0;`
13140 }
13141 &m_filtered
13142 } else {
13143 rest
13144 };
13145 // c:4744-4764 — the per-argument interpretation loop runs over the
13146 // FORMAT ARGUMENTS (C consumed `fmt` at c:4692/4694, so `first`/
13147 // `args` no longer contain it) after the `-m` filter and before the
13148 // `-o`/`-O` sort. Only the `-P` arm matters here; `-D` has no
13149 // fmt-specific behaviour and the `\`-escape arm is handled by
13150 // printf_format. This `-f` fast-branch returned before bin_print's
13151 // shared `-P` loop below, so `print -P -f '%s' %W` emitted the
13152 // literal `%W` instead of the date.
13153 let p_expanded: Vec<String>;
13154 let rest: &[String] = if OPT_ISSET(ops, b'P') {
13155 // c:4745-4746 — `if (OPT_ISSET(ops, 'P'))
13156 // txtunknownattrs = TXT_ATTR_ALL;`
13157 crate::ported::prompt::txtunknownattrs.store(
13158 crate::ported::zsh_h::TXT_ATTR_ALL,
13159 std::sync::atomic::Ordering::SeqCst,
13160 );
13161 p_expanded = rest
13162 .iter()
13163 .map(|a| {
13164 // c:4757-4759 — `promptexpand(metafy(args[n], …), 0, …)`.
13165 let mut s = crate::ported::prompt::expand_prompt(a);
13166 // c:Src/prompt.c:236-247 — `if (!ns) { … chuck(Inpar/
13167 // Outpar/Nularg); }`: the ns=0 caller must not leak the
13168 // SGR-wrapping markers. See the same strip in the non-fmt
13169 // `-P` loop below.
13170 s.retain(|c| c != '\x01' && c != '\x02');
13171 s
13172 })
13173 .collect();
13174 &p_expanded
13175 } else {
13176 rest
13177 };
13178 // c:4799-4807 — the `-o`/`-O`/`-i` argument sort runs BEFORE
13179 // format rendering. The non-fmt path sorts at the tail of
13180 // bin_print (strmetasort), but this `-f` fast-branch returns
13181 // first, so `print -of '%s\n' foo bar baz` rendered the args
13182 // unsorted. Apply the same strmetasort to the format args here.
13183 let rest_sorted: Vec<String>;
13184 let rest: &[String] = if OPT_ISSET(ops, b'o') || OPT_ISSET(ops, b'O') {
13185 let mut v = rest.to_vec();
13186 let mut sflags: u32 = if OPT_ISSET(ops, b'i') {
13187 SORTIT_IGNORING_CASE as u32 // c:4805
13188 } else {
13189 0
13190 };
13191 if OPT_ISSET(ops, b'O') {
13192 sflags |= SORTIT_BACKWARDS as u32; // c:4806
13193 }
13194 strmetasort(&mut v, sflags, None); // c:4807
13195 rest_sorted = v;
13196 &rest_sorted
13197 } else {
13198 rest
13199 };
13200 // c:5464 — reset the per-run `%d`/`%i` math-error flag; a bad
13201 // math operand during formatting sets it (parse_int_arg) and turns
13202 // the builtin's exit status into 1 without aborting output.
13203 PRINTF_MATH_ERR.with(|c| c.set(false));
13204 // c:builtin.c:5430-5443 — printf returns 1 on unknown
13205 // directive after `zwarnnam(name, "%s: invalid directive",
13206 // start)`. The partial output produced before the bad
13207 // directive is still printed (C emits via fwrite/fprintf
13208 // throughout the format walk).
13209 let (out, bounds, n_targets) = match printf_format(&fmt, rest) {
13210 Ok(s) => s,
13211 Err((partial, msg)) => {
13212 print!("{}", partial); // c: partial output already in fout
13213 use std::io::Write;
13214 let _ = std::io::stdout().flush();
13215 // c:5435 invalid directive / c:5204 argument specifier
13216 // out of range — `msg` carries the full zwarnnam text.
13217 crate::ported::utils::zwarnnam(name, &msg);
13218 // dash's printf reports a bad directive as status 2.
13219 if crate::dash_mode::dash_faithful() {
13220 return 2;
13221 }
13222 return 1; // c:5443
13223 }
13224 };
13225 // c:Src/builtin.c:5427-5429 — apply the `%n` stores. C runs setiparam
13226 // inside the format loop; printf_format has no shell handle, so it
13227 // collected (name, byte_count) and the assignment happens here. Done
13228 // before the -z/-s output branches because in C the variable is set
13229 // during formatting regardless of where the text is routed.
13230 for (nv, cnt) in &n_targets {
13231 crate::ported::params::setiparam(nv, *cnt as i64); // c:5428
13232 }
13233 // c:4854-4856 — `if (OPT_ISSET(ops, 'v') || (fmt && (OPT_ISSET
13234 // (ops, 'z') || OPT_ISSET(ops, 's')))) ASSIGN_MSTREAM(...)`.
13235 // For -f combined with -z or -s, capture output then route
13236 // through the same dispatch as the non-fmt path.
13237 if OPT_ISSET(ops, b'z') {
13238 // c:5548-5549 — `zpushnode(bufstack, stringval)`. zpushnode
13239 // inserts at the list HEAD (`zsh.h:591`) and the consumers —
13240 // `zleread`'s restore (zle_main.c:1297), `read -z`
13241 // (builtin.c:6762) and `get-line` (zle_hist.c:901) — all take
13242 // the head back off. Appending made the stack FIFO, so two
13243 // queued lines came back in the wrong order.
13244 crate::ported::zle::zle_main::BUFSTACK
13245 .lock()
13246 .unwrap()
13247 .insert(0, out);
13248 return 0;
13249 }
13250 if OPT_ISSET(ops, b's') {
13251 // c:5569-5574 — push captured output as a history entry.
13252 let event_id = crate::ported::hist::prepnexthistent();
13253 crate::ported::hashtable::addhistnode(&out, event_id as i32);
13254 // c:hist.c:1399-1410 — C's `prepnexthistent` ALLOCATES the entry
13255 // and links it into `hist_ring` itself. zshrs's only evicts and
13256 // bumps `curhist`, so every caller has to do the link — which
13257 // `hist.rs:2282` and the non-fmt `-s` arm below both do, and this
13258 // arm did not. `addhistnode` alone populates `histtab` but leaves
13259 // the ring empty, and `fc` reads the RING, so `print -f '%s\n' -s X`
13260 // stored an entry that `fc -ln -1` could not see. It is the same
13261 // omission the non-fmt arm's own comment describes.
13262 let ent = crate::ported::hist::make_histent(event_id, out.clone());
13263 if let Ok(mut ring) = crate::ported::hist::hist_ring.lock() {
13264 ring.insert(0, ent);
13265 crate::ported::hist::histlinect.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
13266 }
13267 return 0;
13268 }
13269 if let Some(ref v) = dest_var {
13270 // c:builtin.c:5162 — `visarr = v && PM_TYPE(...) == PM_ARRAY`.
13271 // When the -v target is an existing array, the output is
13272 // split into one element per format-reuse cycle (c:5552-5560
13273 // `if (visarr && splits) { ... setaparam(...) }`); otherwise
13274 // the whole output is one scalar (c:5567 `setsparam(...)`).
13275 let visarr = {
13276 use crate::ported::zsh_h::{PM_ARRAY, PM_TYPE};
13277 paramtab()
13278 .read()
13279 .ok()
13280 .and_then(|tab| {
13281 tab.get(v)
13282 .map(|pm| PM_TYPE(pm.node.flags as u32) == PM_ARRAY)
13283 })
13284 .unwrap_or(false)
13285 };
13286 // c:5169-5171 — `splits` is only allocated on the SECOND and
13287 // later cycles (`argp > args`); a single-cycle format leaves
13288 // `splits` NULL, so `if (visarr && splits)` (c:5552) is false
13289 // and the whole output becomes one scalar. `bounds.len() > 1`
13290 // mirrors `splits != NULL`.
13291 if visarr && bounds.len() > 1 {
13292 // c:5553-5559 — slice `out` at each cycle boundary; one
13293 // array element per format application. `bounds` holds the
13294 // byte offset where each cycle began (the first is 0).
13295 let mut arrayval: Vec<String> = Vec::with_capacity(bounds.len());
13296 for (i, &start) in bounds.iter().enumerate() {
13297 let end = bounds.get(i + 1).copied().unwrap_or(out.len());
13298 arrayval.push(out[start..end].to_string());
13299 }
13300 setaparam(v, arrayval);
13301 } else {
13302 setsparam(v, &out);
13303 }
13304 } else {
13305 // c:Src/builtin.c — C printf goes through libc fwrite to
13306 // stdout, unbuffered when fd 1 is the redirect target.
13307 // Rust's `print!` is line-buffered; without a trailing
13308 // newline the bytes stay in the buffer past the redirect
13309 // restore and land on the original stdout. Route through
13310 // stdout().write_all + flush so the bytes hit fd 1 (the
13311 // redirect target) immediately. Bug #397.
13312 use std::io::Write as _;
13313 let stdout = io::stdout();
13314 let mut lk = stdout.lock();
13315 // c:Src/builtin.c:5408 — `print_val(unmetafy(stringval,
13316 // &curlen))`: printf's values are unmetafied before the
13317 // write. Decode Meta-char pairs to raw bytes. Bug #127.
13318 crate::stdout_ferror::fwrite_stdout(&mut lk, &[&crate::ported::utils::unmetafy_str(&out)]);
13319 }
13320 // c:5464 — a `%d`/`%i` math-operand error makes printf exit 1
13321 // (output already emitted).
13322 return if PRINTF_MATH_ERR.with(|c| c.get()) {
13323 1
13324 } else {
13325 0
13326 };
13327 }
13328
13329 // c:4718-4741 — `-m PATTERN args...` glob-filter. First arg is
13330 // the pattern; remaining args are kept iff `pattry(pat, arg)`.
13331 // Previously absent — `print -m 'foo*' foo1 bar foo2` emitted
13332 // all four args instead of just foo1/foo2.
13333 let mut processed_args: Vec<String> = if OPT_ISSET(ops, b'm') {
13334 // c:4718
13335 if args.is_empty() {
13336 // c:4722
13337 zwarnnam(name, "no pattern specified"); // c:4723
13338 return 1; // c:4724
13339 }
13340 // c:4728 — `patcompile(*args, PAT_STATIC, NULL)`.
13341 let pat = &args[0];
13342 let pprog = patcompile(
13343 &{
13344 let mut __pat_tok = crate::pattern_data_escape::escape_data_backslashes(&pat); // a backslash tokenize leaves raw is DATA (c:Src/glob.c:3651)
13345 crate::ported::glob::tokenize(&mut __pat_tok);
13346 __pat_tok
13347 },
13348 PAT_STATIC,
13349 None,
13350 );
13351 match pprog {
13352 None => {
13353 zwarnnam(name, &format!("bad pattern: {}", pat)); // c:4730
13354 return 1; // c:4732
13355 }
13356 Some(prog) => {
13357 // c:4734-4737 — `for (t = p = ++args; *p; p++) if
13358 // (pattry(pprog, *p)) *t++ = *p;`. Keep matching args.
13359 args[1..]
13360 .iter()
13361 .filter(|a| pattry(&prog, a))
13362 .cloned()
13363 .collect()
13364 }
13365 }
13366 } else {
13367 args.to_vec()
13368 };
13369
13370 // c:4860+ — main print loop.
13371 // c:5126-5127 — separator priority: `-l` ('\n') > `-N` ('\0') > ' '.
13372 let sep = if one_per_line {
13373 "\n"
13374 } else if nul_sep {
13375 "\0"
13376 } else {
13377 " "
13378 };
13379 // c:4731-4781 — per-argument interpretation runs in C order: backslash
13380 // escapes (getkeystring), then `-P`, then `-D`; the `-o`/`-O` sort
13381 // (c:4784-4792) comes after all three, so it orders the INTERPRETED
13382 // strings (`print -lo 'a\tb' 'a\nc'` sorts on the TAB/LF bytes).
13383 // c:Src/builtin.c:4866-4886 — when `-r` is NOT set, each arg goes
13384 // through `getkeystring` to interpret backslash escapes (`\n`,
13385 // `\t`, `\\`, escaped space `\ `, etc.). `echo` follows the same
13386 // path when `BSD_ECHO`/`SH_OPTION_LETTERS`-style isn't in effect;
13387 // BIN_ECHO with `-E` keeps escapes literal. Without this, `print
13388 // -- ${(q)a}` for `a="he llo"` emitted `he\ llo` instead of zsh's
13389 // `he llo` (the (q) flag's backslash gets consumed by print).
13390 // c:builtin.c:4747-4767 — escape interpretation dispatch.
13391 // - `fmt` (printf format already chosen via -f) or
13392 // `(!-e && (-R || -r || -E))` → unmetafy only, NO escape
13393 // interpretation (raw passthrough).
13394 // - Otherwise pick `escape_how` per c:4754-4760:
13395 // `-b` → GETKEYS_BINDKEY (bindkey
13396 // escapes: `\C-`/`\M-`/`^X`)
13397 // func != BIN_ECHO && !`-e` → GETKEYS_PRINT (with EMACS:
13398 // unknown `\<c>` → `<c>`)
13399 // else (BIN_ECHO or `-e`) → GETKEYS_ECHO (preserves
13400 // unknown `\<c>` as `\<c>`)
13401 //
13402 // Previous Rust port unconditionally used GETKEYS_PRINT for both
13403 // `echo` and `print` — `echo "${(qq)s}"` for `s="a'b"` stripped
13404 // the `\` from the `(qq)`-emitted `'a'\''b'` because GETKEYS_PRINT
13405 // includes GETKEY_EMACS. zsh keeps the `\` (echo uses GETKEYS_ECHO,
13406 // no EMACS).
13407 let dash_e = OPT_ISSET(ops, b'e');
13408 // c:Src/builtin.c:4754 — BSD_ECHO option flips echo's default:
13409 // escape processing is OFF unless `-e` is explicitly passed.
13410 // Without bsd_echo (the SysV default), escapes process unless
13411 // `-E`/`-R`/`-r` is set.
13412 // !!! DASH-STRICT GATE (no C counterpart) !!! dash's `echo` is the
13413 // XSI/SysV variant: it interprets `\t`, `\n`, `\c`, … by default.
13414 // EMULATE_SH sets BSDECHO (escapes OFF without -e), the opposite of
13415 // dash, so force bsd_echo inactive under dash_strict → escapes process
13416 // by default, matching `/bin/dash`. (Note: dash's echo also does not
13417 // consume -e/-E as flags; that flag-parsing nuance is not handled here
13418 // — only the default escape behavior is aligned.)
13419 // !!! POSIX-FAITHFUL GATE (no C counterpart) !!! The same divergence one
13420 // level up: `zshrs --sh` must match the REAL `/bin/sh`, not zsh's
13421 // approximation of it. Every POSIX `sh` this parity matrix references
13422 // interprets the escapes — macOS `/bin/sh` (bash built with
13423 // xpg_echo-by-default) and Linux `/bin/sh` (dash) both print a TAB for
13424 // `echo "a\tb"` — while zsh's `emulate sh` sets BSDECHO and prints the
13425 // two characters literally. `--bash` is excluded: bash's own `echo`
13426 // requires `-e`, which is what BSDECHO already gives it. The `--X --zsh`
13427 // zsh-STYLE legs clear posix_faithful, so they keep zsh's BSDECHO
13428 // behavior and stay byte-identical to `zsh -c 'emulate sh; …'`.
13429 let posix_faithful_echo = crate::dash_mode::posix_faithful() && !crate::dash_mode::bash_mode();
13430 let bsd_echo_active =
13431 echo_mode && isset(BSDECHO) && !crate::dash_mode::dash_strict() && !posix_faithful_echo;
13432 let suppress_escapes = OPT_ISSET(ops, b'R')
13433 || OPT_ISSET(ops, b'r')
13434 || (echo_mode && OPT_ISSET(ops, b'E'))
13435 || (bsd_echo_active && !dash_e);
13436 let mut backslash_c_truncated = false;
13437 if !suppress_escapes || dash_e {
13438 // c:builtin.c:4754-4760 — `-b` (bindkey escapes) takes precedence,
13439 // then GETKEYS_PRINT for print (EMACS), else GETKEYS_ECHO.
13440 let escape_how: u32 = if OPT_ISSET(ops, b'b') {
13441 GETKEYS_BINDKEY // c:4755
13442 } else if !echo_mode && !dash_e {
13443 GETKEYS_PRINT // c:4758
13444 } else if crate::dash_mode::dash_faithful() {
13445 // !!! DASH-FAITHFUL GATE (no C counterpart) !!! dash's echo
13446 // (conv_escape_str) takes `\NNN` as well as `\0NNN`; zsh's ECHO
13447 // set only knows the `\0` introducer.
13448 GETKEYS_ECHO | crate::ported::zsh_h::GETKEY_OCTAL_ESC as u32
13449 } else {
13450 GETKEYS_ECHO // c:4760
13451 };
13452 // Clear any stale TLS flag before the loop.
13453 let _ = crate::ported::utils::getkey_truncated_take();
13454 let mut new_args: Vec<String> = Vec::with_capacity(processed_args.len());
13455 for a in processed_args.iter() {
13456 // !!! KSH93-ONLY (no C counterpart) !!! ksh93 has its own, narrower
13457 // escape set (dash_mode::ksh93_print_escapes); `-b` keeps bindkey escapes.
13458 if crate::dash_mode::ksh93_mode() && !OPT_ISSET(ops, b'b') {
13459 let (s, truncated) = crate::dash_mode::ksh93_print_escapes(a);
13460 new_args.push(s);
13461 if truncated {
13462 backslash_c_truncated = true;
13463 break;
13464 }
13465 continue;
13466 }
13467 let a = &if echo_mode && crate::dash_mode::dash_faithful() {
13468 crate::dash_mode::dash_echo_octal_normalize(a)
13469 } else {
13470 a.clone()
13471 };
13472 let (s, _) = getkeystring_with(a, escape_how, None);
13473 new_args.push(s);
13474 if crate::ported::utils::getkey_truncated_take() {
13475 // c:utils.c:7045 — `\c` truncated; drop remaining
13476 // args entirely AND suppress trailing newline.
13477 backslash_c_truncated = true;
13478 break;
13479 }
13480 }
13481 processed_args = new_args;
13482 }
13483 // c:4598-4600 — `-P` prompt-style percent expansion (`%n`, `%d`,
13484 // `%?`, `%h`, `%%`, etc.). Routes through `expand_prompt`
13485 // (canonical port of `Src/prompt.c:182 promptexpand`).
13486 if OPT_ISSET(ops, b'P') {
13487 // c:Src/builtin.c:4745-4746 — `if (OPT_ISSET(ops, 'P'))
13488 // txtunknownattrs = TXT_ATTR_ALL;`. Marks every attribute
13489 // as initially "unknown" so the first `%b`/`%u`/`%s`/`%f`/`%k`
13490 // off-escape in this print invocation emits its terminfo cap
13491 // (via tunsetattrs at prompt.c:1758 — `txtcurrentattrs |=
13492 // newattrs & txtunknownattrs`). Without this seed, applying
13493 // an off-escape against zero current attrs produced no diff
13494 // and zshrs emitted nothing — `%b` / `%u` / `%s` looked like
13495 // no-ops in fresh prompts. Bug #38 in docs/BUGS.md.
13496 crate::ported::prompt::txtunknownattrs.store(
13497 crate::ported::zsh_h::TXT_ATTR_ALL,
13498 std::sync::atomic::Ordering::SeqCst,
13499 );
13500 // c:4598-4600 — `-P` prompt-style percent expansion.
13501 for a in processed_args.iter_mut() {
13502 *a = crate::ported::prompt::expand_prompt(a); // c:Src/prompt.c:182
13503 // c:Src/prompt.c:236-247 — `if (!ns) { ... chuck(Inpar/
13504 // Outpar/Nularg); }`. When `ns=0` (non-stripping flag
13505 // off), zsh REMOVES the Inpar/Outpar/Nularg marker bytes
13506 // from the output. `print -P` calls promptexpand with
13507 // `ns=0` per Src/builtin.c:4598, so the SGR-wrapping
13508 // markers MUST NOT leak into stdout. The Rust port's
13509 // expand_prompt uses ad-hoc `\x01`/`\x02` (readline
13510 // RL_PROMPT_*_IGNORE) markers instead of canonical
13511 // Inpar/Outpar, but the strip rule applies identically:
13512 // for non-prompt-render callers, scrub them. Parity bug
13513 // #17 — without this, `print -P "%F{red}red%f"` emitted
13514 // `\x01\E[31m\x02red\x01\E[39m\x02` instead of zsh's
13515 // `\E[31mred\E[39m`.
13516 a.retain(|c| c != '\x01' && c != '\x02');
13517 }
13518 }
13519 // c:Src/builtin.c:4783-4795 — `-D`: interpret each arg as a
13520 // directory and abbreviate via `finddir(args[n])` — longest-prefix
13521 // match against the full named-dir table ($HOME → `~`, `hash -d`
13522 // entries → `~name`, plus the `zsh_directory_name` hook). C rewrites
13523 // the matched prefix to `~name` and keeps the trailing path. The
13524 // canonical `finddir` port already returns the `~name/rest` form.
13525 if dirify_d {
13526 for a in processed_args.iter_mut() {
13527 if let Some(abbrev) = crate::ported::utils::finddir(a) {
13528 *a = abbrev; // c:4791 — `~%s%s`
13529 }
13530 }
13531 }
13532 // c:4799-4808 — `-o` / `-O` / `-i` sort flags.
13533 //
13534 // C body:
13535 // ```c
13536 // if (OPT_ISSET(ops,'o') || OPT_ISSET(ops,'O')) {
13537 // flags = OPT_ISSET(ops,'i') ? SORTIT_IGNORING_CASE : 0;
13538 // if (OPT_ISSET(ops,'O'))
13539 // flags |= SORTIT_BACKWARDS;
13540 // strmetasort(args, flags, len);
13541 // }
13542 // ```
13543 //
13544 // Meaning: `-i` sets `SORTIT_IGNORING_CASE` (case-INSENSITIVE).
13545 // Without `-i`, sort is case-SENSITIVE.
13546 //
13547 // The previous Rust port had this INVERTED — it bound
13548 // `case_sensitive = OPT_ISSET(ops, b'i')`, then case-sensitive-
13549 // sorted under `-i` and case-insensitive-sorted without `-i`.
13550 // The doc-comment for the block claimed "-o → case-insensitive
13551 // ascending" which is also wrong. Result: `print -o foo Bar BAZ`
13552 // emitted `BAZ Bar foo` (case-insensitive) when zsh emits
13553 // `BAZ Bar foo` only WITH `-i`; without it, zsh emits
13554 // `BAZ Bar foo` ordered by ASCII (caps first).
13555 if OPT_ISSET(ops, b'o') || OPT_ISSET(ops, b'O') {
13556 // c:4800
13557 // c:4801-4807 — `flags = OPT_ISSET(ops,'i') ? SORTIT_IGNORING_CASE
13558 // : 0; if (OPT_ISSET(ops,'O')) flags |= SORTIT_BACKWARDS;
13559 // strmetasort(args, flags, len);`. The previous Rust port used an
13560 // ad-hoc `processed_args.sort()` (byte/ordinal order) plus a
13561 // `.reverse()`, bypassing the faithful comparator. zsh's eltpcmp
13562 // sorts via `strcoll(as, bs)` (sort.c:134) — locale collation,
13563 // case-insensitive in UTF-8 locales — and SORTIT_BACKWARDS flips
13564 // `sortdir` inside the comparator (not a post-sort reverse).
13565 let mut flags: u32 = if OPT_ISSET(ops, b'i') {
13566 SORTIT_IGNORING_CASE as u32 // c:4805
13567 } else {
13568 0
13569 };
13570 if OPT_ISSET(ops, b'O') {
13571 flags |= SORTIT_BACKWARDS as u32; // c:4806
13572 }
13573 // c:4792 — `strmetasort(args, flags, len);`. C passes the REAL
13574 // per-argument length array, never NULL (contrast c:694, the
13575 // `typeset` sort, which does pass NULL). That is what selects
13576 // `eltpcmp`'s embedded-NUL branch (`ae->len != -1`), so arguments
13577 // holding NUL bytes compare over their FULL byte extent instead of
13578 // being truncated at the first NUL by `strcoll`. Passing `None`
13579 // here made every `$'a\0...'` argument compare equal to plain
13580 // `a`, so `print -lO $'a' $'a\0' $'a\0b' …` came back in input
13581 // order instead of sorted (Test/B03print.ztst "sorting with
13582 // embedded nulls"). Rust `String` stores the NULs faithfully, so
13583 // the length is simply each argument's byte length.
13584 let mut lens: Vec<usize> = processed_args.iter().map(|a| a.len()).collect(); // c:4792 len
13585 strmetasort(&mut processed_args, flags, Some(&mut lens)); // c:4792
13586 }
13587 // c:4842-4990 — "-c -- output in columns". `-c` sizes the grid from the
13588 // terminal width, `-C N` fixes the column count. Each ROW is written
13589 // with its own terminator (c:4989 `fputc(OPT_ISSET(ops,'N') ? '\0' :
13590 // '\n', fout)`) and the block returns straight after, so `-n` does not
13591 // apply and `-v` captures the final row's newline too.
13592 let columnate = !_printf_mode && (OPT_ISSET(ops, b'c') || OPT_ISSET(ops, b'C'));
13593 let body = if columnate {
13594 let mut nc: i32 = 0;
13595 if OPT_HASARG(ops, b'C') {
13596 // c:4687-4698 — `-C` validates its argument two ways and FAILS;
13597 // it does not fall back:
13598 // nc = (int)zstrtol(argptr, &eptr, 10);
13599 // if (*eptr) {
13600 // zwarnnam(name, "number expected after -%c: %s", 'C', argptr);
13601 // return 1;
13602 // }
13603 // if (nc <= 0) {
13604 // zwarnnam(name, "invalid number of columns: %s", argptr);
13605 // return 1;
13606 // }
13607 let argptr = OPT_ARG(ops, b'C').unwrap_or("");
13608 let (nc_l, rest) = crate::ported::utils::zstrtol_underscore(argptr, 10, false); // c:4689
13609 if !rest.is_empty() {
13610 zwarnnam(name, &format!("number expected after -C: {argptr}")); // c:4691
13611 return 1; // c:4692
13612 }
13613 if nc_l <= 0 {
13614 zwarnnam(name, &format!("invalid number of columns: {argptr}")); // c:4695
13615 return 1; // c:4696
13616 }
13617 nc = nc_l as i32;
13618 }
13619 let args = &processed_args;
13620 let argc = args.len() as i32;
13621 // c:4846-4903 — "We need the character widths to align output in
13622 // columns." Under MULTIBYTE each arg's width is the sum of its
13623 // printable WCWIDTHs; otherwise the byte length (`widths = len`).
13624 let widths: Vec<i32> = args
13625 .iter()
13626 .map(|a| {
13627 if !isset(crate::ported::zsh_h::MULTIBYTE) {
13628 return a.len() as i32; // c:4901 widths = len
13629 }
13630 let mut width = 0i32;
13631 let mut it = a.chars();
13632 while let Some(c) = it.next() {
13633 // c:4870-4883 — "Prevent misaligned columns due to
13634 // escape sequences by skipping over them. Octals \033
13635 // and \233 are the possible escape characters
13636 // recognized by ANSI." Skip through the first
13637 // alphabetic character.
13638 if c == '\u{1b}' || c == '\u{9b}' {
13639 for d in it.by_ref() {
13640 if d.is_ascii_alphabetic() {
13641 break;
13642 }
13643 }
13644 continue;
13645 }
13646 let wcw = crate::ported::zsh_h::WCWIDTH(c); // c:4892
13647 // c:4893-4895 — "treat unprintable as 0"
13648 if wcw > 0 {
13649 width += wcw;
13650 }
13651 }
13652 width
13653 })
13654 .collect();
13655 let across = OPT_ISSET(ops, b'a');
13656 let (nr, sc);
13657 if OPT_ISSET(ops, b'C') {
13658 // c:4907-4934 — n: number of elements, nc: number of columns,
13659 // nr: number of rows. "Ignore lengths in last column since
13660 // they don't affect the separation."
13661 let n = argc;
13662 nr = (n + nc - 1) / nc;
13663 let mut l = 0i32;
13664 for i in 0..argc {
13665 if across {
13666 if (i % nc) == nc - 1 {
13667 continue; // c:4925
13668 }
13669 } else if i >= nr * (nc - 1) {
13670 break; // c:4928
13671 }
13672 if l < widths[i as usize] {
13673 l = widths[i as usize];
13674 }
13675 }
13676 sc = l + 2; // c:4933
13677 } else {
13678 // c:4937-4961 — l: maximum length seen; sc: column width;
13679 // nc: number of columns (at least one).
13680 let l = widths.iter().copied().max().unwrap_or(0);
13681 sc = l + 2; // c:4953
13682 nc = (crate::ported::utils::ZTERM_COLUMNS.load(Ordering::SeqCst) + 1) / sc; // c:4954
13683 if nc == 0 {
13684 nc = 1; // c:4956
13685 }
13686 nr = (argc + nc - 1) / nc; // c:4957
13687 }
13688 let mut out = String::new();
13689 let mut n: i32 = 0; // c:4960 — "print across, i.e. columns first"
13690 for i in 0..nr {
13691 if across {
13692 // c:4962-4972
13693 let mut ic = 0;
13694 while ic < nc && n < argc {
13695 out.push_str(&args[n as usize]);
13696 let mut l = widths[n as usize];
13697 if n < argc && ic < nc - 1 {
13698 while l < sc {
13699 out.push(' ');
13700 l += 1;
13701 }
13702 }
13703 ic += 1;
13704 n += 1;
13705 }
13706 } else {
13707 // c:4974-4983
13708 n = i;
13709 loop {
13710 out.push_str(&args[n as usize]);
13711 let mut l = widths[n as usize];
13712 let mut t = nr;
13713 while t != 0 && n < argc {
13714 t -= 1;
13715 n += 1;
13716 }
13717 if n < argc {
13718 while l < sc {
13719 out.push(' ');
13720 l += 1;
13721 }
13722 }
13723 if n >= argc {
13724 break;
13725 }
13726 }
13727 }
13728 out.push(if nul_sep { '\0' } else { '\n' }); // c:4985
13729 }
13730 out
13731 } else if OPT_HASARG(ops, b'x') || OPT_HASARG(ops, b'X') {
13732 // c:Src/builtin.c:5095-5125 — `print -x N` / `-X N` expand tabs
13733 // to spaces at tab stops of width N (`-x` only leading tabs,
13734 // `-X` all tabs), threading the column position across args and
13735 // resetting it on the `-l` newline. zexpandtabs (utils.c:5975)
13736 // was ported but never wired into print output.
13737 let all = OPT_HASARG(ops, b'X');
13738 let which = if all { b'X' } else { b'x' };
13739 let width: i32 = OPT_ARG(ops, which)
13740 .map(|a| crate::ported::utils::zstrtol(a, 10).0 as i32) // c:5085
13741 .unwrap_or(8);
13742 let n = processed_args.len();
13743 let mut out = String::new();
13744 let mut startpos = 0i32;
13745 for (i, arg) in processed_args.iter().enumerate() {
13746 startpos = crate::ported::utils::zexpandtabs(arg, width, startpos, all, &mut out);
13747 if i + 1 < n {
13748 if OPT_ISSET(ops, b'l') {
13749 out.push('\n');
13750 startpos = 0;
13751 } else if OPT_ISSET(ops, b'N') {
13752 out.push('\0');
13753 } else {
13754 out.push(' ');
13755 startpos += 1;
13756 }
13757 }
13758 }
13759 out
13760 } else {
13761 processed_args.join(sep)
13762 };
13763 // c:5564-5575 — destination dispatch order:
13764 // -z → zpushnode(bufstack, stringval)
13765 // -v → setsparam(VAR, stringval)
13766 // -s → prepnexthistent() + addhistnode(histtab, stringval)
13767 // else → fwrite to fout
13768 if OPT_ISSET(ops, b'z') {
13769 // c:5564-5565 — `zpushnode(bufstack, stringval)`. The ZLE
13770 // bufstack is consumed by the next zleread call so the
13771 // string is presented at the prompt — `print -z 'echo foo'`
13772 // queues `echo foo` for the user to press Enter on.
13773 // c:5026 — `zpushnode(bufstack, sepjoin(args, NULL, 0))`, which
13774 // inserts at the list HEAD (`zsh.h:591`); every consumer pops the
13775 // head, so appending reversed the order of two queued lines.
13776 crate::ported::zle::zle_main::BUFSTACK
13777 .lock()
13778 .unwrap()
13779 .insert(0, body); // c:5026
13780 return 0;
13781 }
13782 if OPT_ISSET(ops, b's') || OPT_ISSET(ops, b'S') {
13783 // c:5047-5093 — `-s` / `-S` push captured output to the history
13784 // list. `-S` requires exactly one arg (c:5058-5062
13785 // `if (nwords > 1) zwarnnam(name, "option -S takes a single
13786 // argument"); return 1`); the words array is then populated by
13787 // `histsplitwords` (c:5065). `-s` joins all args with spaces
13788 // and stores them as a single history entry. Both paths call
13789 // `prepnexthistent` + `addhistnode(histtab, ent->node.nam, ent)`.
13790 if OPT_ISSET(ops, b'S') && processed_args.len() > 1 {
13791 // c:5059
13792 zwarnnam(name, "option -S takes a single argument"); // c:5059
13793 return 1; // c:5061
13794 }
13795 let event_id = crate::ported::hist::prepnexthistent(); // c:5066/5072
13796 crate::ported::hashtable::addhistnode(&body, event_id as i32); // c:5090
13797 // c:Src/builtin.c — C's `addhistnode(histtab, …)` uses the
13798 // ent ALREADY linked into hist_ring by prepnexthistent. zshrs's
13799 // prepnexthistent only bumps curhist and doesn't insert into
13800 // the ring, so `fc -l` finds no events. Push the ent into the
13801 // ring here so the gethistent lookup in fc/history sees it.
13802 // Without this, `print -s X; fc -l` reported "no such event: 1"
13803 // even though the histtab had the entry.
13804 let mut ent = crate::ported::hist::make_histent(event_id, body.clone());
13805 // c:5032-5058 — record the word boundaries so `!!`, `!$` and `!:N`
13806 // can pick words out of the entry. `-S` splits its single argument
13807 // with `histsplitwords`; `-s` takes each argument as one word.
13808 let spans: Vec<(usize, usize)> = if OPT_ISSET(ops, b'S') && !processed_args.is_empty() {
13809 crate::ported::hist::histsplitwords(&processed_args[0], true) // c:5049
13810 } else {
13811 let mut nlen = 0usize;
13812 processed_args
13813 .iter()
13814 .map(|arg| {
13815 let span = (nlen, nlen + arg.len()); // c:5060-5062
13816 nlen += arg.len() + 1;
13817 span
13818 })
13819 .collect()
13820 };
13821 ent.words = spans.iter().flat_map(|&(b, e)| [b as i16, e as i16]).collect();
13822 ent.nwords = spans.len() as i32; // c:5054, c:5058
13823 if let Ok(mut ring) = crate::ported::hist::hist_ring.lock() {
13824 ring.insert(0, ent);
13825 crate::ported::hist::histlinect.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
13826 }
13827 return 0;
13828 }
13829 if let Some(ref v) = dest_var {
13830 // c:Src/builtin.c:544-546 — the trailing separator is part of the
13831 // captured `-v` value too: it is written to the same stream and read
13832 // back into `buf`. It is suppressed only when `-n`, a `\c` escape
13833 // (`nnl`), OR `-v` WITHOUT `-l` apply. So `print -v x -l a b c` stores
13834 // `a\nb\nc\n` (with the trailing newline) where `print -v x a b c`
13835 // stores `a b c` (none). The port previously suppressed it for every
13836 // `-v`, dropping the `-l` trailing newline.
13837 // c:4990-4999 — the columnate block already wrote every row's
13838 // terminator into the stream, and READ_MSTREAM captures all of it.
13839 let suppress_term =
13840 columnate || nonewline || backslash_c_truncated || !OPT_ISSET(ops, b'l');
13841 let mut val = body.clone();
13842 if !suppress_term {
13843 val.push(if nul_sep { '\0' } else { '\n' }); // c:546
13844 }
13845 setsparam(v, &val);
13846 } else {
13847 // c:5130-5132 — final terminator: `-n` suppresses; `-N` emits
13848 // NUL instead of newline; else newline. `\c` truncation
13849 // (c:utils.c:7045) also suppresses — matches zsh's
13850 // `echo "a\cb"; echo END` → `aEND`.
13851 //
13852 // c:Src/builtin.c:4982-5025 — the columnate (`-c`/`-C`) block emits its
13853 // newline PER ROW (already in `body`) and then `return ret`s, never
13854 // reaching this shared terminator: `-n` does not suppress it, and with
13855 // ZERO args there are zero rows, so `print -c` outputs nothing.
13856 let final_term: &[u8] = if columnate || nonewline || backslash_c_truncated {
13857 b""
13858 } else if nul_sep {
13859 b"\0"
13860 } else {
13861 b"\n"
13862 };
13863 // c:Bug #388 — `-p` and `-u` are mutually exclusive but both
13864 // route to the same write-to-fd path. Prefer the `-p`-resolved
13865 // coprocout (when present) over the `-u`-resolved fd. If
13866 // neither, fall through to stdout below.
13867 let dest_fd_active = print_dash_p_fd.or(dest_fd);
13868 // c:Src/builtin.c:4752 — `unmetafy(args[n], &len[n])`: print's
13869 // args are unmetafied before the fwrite. zshrs keeps values
13870 // metafied (Meta-char pairs, vm_helper::meta_encode_byte)
13871 // until the byte boundary, so decode here — `print $'\xff'`
13872 // writes the single raw byte 0xff. Bug #127.
13873 let body_bytes = crate::ported::utils::unmetafy_str(&body);
13874 if let Some(mut f) = dest_fd_active {
13875 // c:4847 — write to dup'd file descriptor.
13876 use std::io::Write as _;
13877 let _ = f.write_all(&body_bytes); // c:5124 fwrite
13878 let _ = f.write_all(final_term); // c:5132
13879 // f closes on drop (close(dup_fd)) — user's original fd
13880 // remains open per c:4843 dup semantics.
13881 } else {
13882 // stdout path. -N writes NUL via raw stdout; print!/println!
13883 // would mangle a NUL inside a String literal via format
13884 // machinery, so route through stdout().write_all directly.
13885 use std::io::Write as _;
13886 let stdout = io::stdout();
13887 let mut lk = stdout.lock();
13888 crate::stdout_ferror::fwrite_stdout(&mut lk, &[&body_bytes, final_term]); // c:5124, c:5132
13889 // c:Src/builtin.c — C printf goes through libc fwrite to
13890 // stdout (fd 1) which is unbuffered when the builtin runs
13891 // under a redirect (because dup2 to fd 1 puts the file
13892 // there). Rust's `io::stdout()` is LINE-buffered, so
13893 // `printf "abcde"` (no `\n`) leaves bytes pending in the
13894 // userspace buffer; when the surrounding `> file` redirect
13895 // closes the dup'd fd, those bytes flush AFTER the restore
13896 // — landing on the original stdout (terminal) AND missing
13897 // the file. Flush here so the bytes hit fd 1 (the redirect
13898 // target) before bin_print returns. Bug #397.
13899 let _ = lk.flush();
13900 }
13901 }
13902 0
13903}
13904
13905/// Port of `bin_shift()` from `Src/builtin.c:5593`.
13906/// C decl: `bin_shift(char *name, char **argv, Options ops, UNUSED(int func))`
13907/// C: `int bin_shift(char *name, char **argv, Options ops, UNUSED(int func))`
13908/// — shift positional params (or named arrays) by `num` positions; `-p`
13909/// pops from the right end.
13910/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
13911pub fn bin_shift(
13912 name: &str,
13913 argv: &[String], // c:5593
13914 ops: &options,
13915 _func: i32,
13916) -> i32 {
13917 let mut num: i32 = 1; // c:5595
13918 let mut ret: i32 = 0; // c:5595
13919 let mut idx = 0usize;
13920 queue_signals(); // c:5599
13921 // c:5600-5605 — first arg parsed as math expr unless it's an array name.
13922 if !argv.is_empty() {
13923 // !!! DASH-FAITHFUL GATE (no C counterpart) !!! dash's shift takes only a
13924 // plain number (`number()`); anything else is a fatal "Illegal number".
13925 if crate::dash_mode::dash_faithful() {
13926 match crate::dash_mode::dash_number(&argv[0]) {
13927 Some(n) => num = n,
13928 None => {
13929 unqueue_signals();
13930 zerrnam(name, &format!("Illegal number: {}", argv[0]));
13931 return crate::dash_mode::fatal_error_status().unwrap_or(2);
13932 }
13933 }
13934 }
13935 // c:5600
13936 let first = &argv[0];
13937 // c:5600 — `if (!getaparam(*argv))` decides whether the arg is
13938 // a numeric shift-count vs an array name. getaparam
13939 // dispatches `argv` to the live positional vector.
13940 let is_array = crate::ported::params::getaparam(first).is_some();
13941 if !is_array {
13942 // c:5600
13943 // c:5601 — `num = mathevali(*argv++);`. The previous Rust port
13944 // used `parse::<i32>()` which rejects any non-trivial
13945 // arithmetic: `shift 1+2` would silently return ret=1
13946 // instead of shifting by 3. Route through mathevali.
13947 num = mathevali(first).unwrap_or_else(|_| {
13948 ret = 1;
13949 0
13950 }) as i32; // c:5601
13951 idx = 1;
13952 // c:5602-5605 — `if (errflag) return 1;`.
13953 if ret != 0 || errflag.load(Relaxed) != 0 {
13954 unqueue_signals(); // c:5604
13955 return 1;
13956 }
13957 }
13958 }
13959
13960 // c:5608-5611 — `if (num < 0)` reject.
13961 if num < 0 {
13962 // c:5608
13963 unqueue_signals(); // c:5609
13964 zwarnnam(name, "argument to shift must be non-negative"); // c:5610
13965 return 1; // c:5611
13966 }
13967
13968 // c:5614-5635 — named-array shift loop.
13969 if idx < argv.len() {
13970 // c:5614
13971 for arr_name in &argv[idx..] {
13972 // c:5615
13973 // c:5616 — `if ((s = getaparam(*argv)))` else silent skip.
13974 let s: Vec<String> = match crate::ported::params::getaparam(arr_name) {
13975 Some(arr) => arr,
13976 None => continue,
13977 };
13978 // c:5617-5621 — arrlen_lt check.
13979 if (s.len() as i32) < num {
13980 // c:5617
13981 zwarnnam(name, &format!("shift count must be <= ${{#{}}}", arr_name)); // c:5602
13982 ret += 1; // c:5619
13983 continue; // c:5620
13984 }
13985 // c:5622-5634 — -p shifts off the right end, otherwise the left.
13986 let s2: Vec<String> = if OPT_ISSET(ops, b'p') {
13987 // c:5622
13988 s[..s.len() - num as usize].to_vec() // c:5625-5628
13989 } else {
13990 s[num as usize..].to_vec() // c:5631
13991 };
13992 // c:5633 — `setaparam(*argv, s);`. Write the shifted array
13993 // back to paramtab as a proper PM_ARRAY. Was a
13994 // fake: `env::set_var` + colon-joined fake-array
13995 // which neither carries array structure nor
13996 // reaches subsequent `${arr_name[@]}` expansions.
13997 setaparam(arr_name, s2);
13998 }
13999 } else {
14000 // c:5636-5654 — shift positional parameters ($1..$N).
14001 // Static-link path: positional params live in src/ported/vm_helper;
14002 // expose via PPARAMS Mutex<Vec<String>>.
14003 let mut pp = PPARAMS.lock().unwrap_or_else(|e| {
14004 PPARAMS.clear_poison();
14005 e.into_inner()
14006 });
14007 let l = pp.len() as i32;
14008 if num > l {
14009 // c:5636
14010 zwarnnam(name, "shift count must be <= $#"); // c:5637
14011 // ZSHRS-ONLY. `shift` is a POSIX special builtin and shifting
14012 // past `$#` is an ERROR, so ksh93u+m, mksh and dash all abort
14013 // the script on it (`shift 99; echo AFTER` never echoes).
14014 // Not a usage error, so ksh93 exits with the builtin's own
14015 // status (1) while dash still uses 2 — see
14016 // `special_builtin_abort_status`.
14017 if !crate::ported::zsh_h::isset(crate::ported::zsh_h::INTERACTIVE)
14018 && crate::extensions::emulation_output::special_builtin_error_aborts()
14019 {
14020 use std::sync::atomic::Ordering;
14021 EXIT_VAL.store(
14022 crate::extensions::emulation_output::special_builtin_abort_status(false, 1),
14023 Ordering::Relaxed,
14024 );
14025 EXIT_PENDING.store(1, Ordering::Relaxed);
14026 }
14027 ret = 1; // c:5638
14028 } else if OPT_ISSET(ops, b'p') {
14029 // c:5641
14030 pp.truncate((l - num) as usize); // c:5642-5644
14031 } else {
14032 pp.drain(..num as usize); // c:5646-5650
14033 }
14034 // PPARAMS is the single source of truth. fusevm-side reads
14035 // route through exec.pparams() which reads PPARAMS, so the
14036 // shift is immediately visible — no exec.positional_params
14037 // mirror needed.
14038 drop(pp);
14039 }
14040 unqueue_signals(); // c:5658
14041 ret // c:5659
14042}
14043
14044/// Port of `bin_getopts()` from `Src/builtin.c:5672`.
14045/// C decl: `bin_getopts(UNUSED(char *name), char **argv, UNUSED(Options ops), UNUSED(int func))`
14046/// C: `int bin_getopts(UNUSED(char *name), char **argv, UNUSED(Options ops),
14047/// UNUSED(int func))`.
14048///
14049/// POSIX getopts. Maintains state in $OPTIND (zoptind) and an internal
14050/// per-arg cursor (optcind). Reads from the script's positional params
14051/// when no extra args supplied, otherwise from the trailing argv.
14052/// WARNING: param names don't match C — Rust=(_name, argv, _func) vs C=(name, argv, ops, func)
14053pub fn bin_getopts(
14054 _name: &str,
14055 argv: &[String], // c:5672
14056 ops: &options,
14057 _func: i32,
14058) -> i32 {
14059 if argv.len() < 2 {
14060 // !!! DASH-FAITHFUL GATE (no C counterpart) !!! dash: usage error, status 2.
14061 if crate::dash_mode::dash_faithful() {
14062 zwarnnam(_name, "Usage: getopts optstring var [arg...]");
14063 return 2;
14064 }
14065 return 1;
14066 }
14067 // c:5675 — `char *optstr = unmetafy(*argv++, &lenoptstr); char *var = *argv++;`
14068 let optstr_full = argv[0].clone();
14069 let var = argv[1].clone();
14070 // c:5676 — `char **args = (*argv) ? argv : pparams;`
14071 let argv_rest: Vec<String> = argv[2..].to_vec();
14072 let args: Vec<String> = if !argv_rest.is_empty() {
14073 argv_rest
14074 } else {
14075 PPARAMS.lock().map(|p| p.clone()).unwrap_or_default()
14076 };
14077
14078 // c:Src/builtin.c:5680 — at entry, re-sync the internal zoptind
14079 // tracker against the user-visible $OPTIND param. zsh exposes
14080 // OPTIND as a writable param so scripts can reset it to 1 to
14081 // re-parse positionals; the Rust port had `ZOPTIND` as an atomic
14082 // that was the AUTHORITY (writes to $OPTIND didn't propagate
14083 // back), so `OPTIND=1` between two getopts loops left zoptind at
14084 // the post-loop value and the second pass returned immediately.
14085 // !!! EMULATION-ONLY (no C counterpart) !!! In the real-shell drop-in
14086 // modes the value written to $OPTIND carries a bias over zsh's internal
14087 // `zoptind` (see extensions::dash_mode::getopts_optind_report); undo it
14088 // here so the argument index below is zsh's own. Identity in --zsh and
14089 // in native zshrs, and a script's own `OPTIND=1` still passes through.
14090 let raw_optind = getiparam("OPTIND");
14091 let paramtab_oi = crate::dash_mode::getopts_optind_internal(raw_optind);
14092 // c:Src/params.c:370 — `IPDEF6("OPTIND", &zoptind, varinteger_gsu)`:
14093 // `zoptind` IS $OPTIND's value slot, so a script's `OPTIND=0` (or a
14094 // fresh `local OPTIND`, which reads 0) is what the reset below sees.
14095 // `unset OPTIND` only flags the special PM_UNSET (stdunsetfn leaves a
14096 // special's storage alone), so the counter carries on in ZOPTIND, and
14097 // getopts' writes to `zoptind` never make $OPTIND set again.
14098 let optind_set = paramtab()
14099 .read()
14100 .unwrap()
14101 .get("OPTIND")
14102 .is_some_and(|pm| (pm.node.flags as u32 & PM_UNSET) == 0);
14103 let write_optind = |v: i64| {
14104 if optind_set {
14105 setiparam("OPTIND", v);
14106 }
14107 };
14108 let mut zoptind = if optind_set {
14109 paramtab_oi as i32
14110 } else {
14111 ZOPTIND.load(Relaxed)
14112 };
14113 // c:5681-5685 — `if (zoptind < 1) { zoptind = 1; optcind = 0; }`
14114 if zoptind < 1 {
14115 // c:5681
14116 zoptind = 1;
14117 OPTCIND.store(0, Relaxed);
14118 // c:5683 — the reset lands in $OPTIND itself, so it is visible
14119 // even when the no-more-options return (c:5686) follows.
14120 write_optind(1);
14121 }
14122 // c:Src/builtin.c — when $OPTIND was just reset to 1 (i.e. the
14123 // user-visible param disagrees with the previous internal
14124 // pointer), reset optcind so the new pass starts at byte 0 of
14125 // the first option arg.
14126 // !!! EMULATION-ONLY second disjunct (no C counterpart) !!! bash, ksh93,
14127 // mksh and /bin/sh rewind the within-argument cursor on ANY script write
14128 // to $OPTIND, not just on a write of 1 from elsewhere; dash and ash never
14129 // do. See dash_mode::getopts_optind_user_reset — false in --zsh, so the
14130 // c:5681-5685 rule on the left stands alone there.
14131 let mut optcind = if (optind_set && paramtab_oi == 1 && ZOPTIND.load(Relaxed) != 1)
14132 || crate::dash_mode::getopts_optind_user_reset(raw_optind)
14133 {
14134 0
14135 } else {
14136 OPTCIND.load(Relaxed)
14137 };
14138
14139 // c:5686-5688 — `if (arrlen_lt(args, zoptind)) return 1;`
14140 if (args.len() as i32) < zoptind {
14141 // c:5686
14142 ZOPTIND.store(zoptind, Relaxed);
14143 // !!! EMULATION-ONLY (no C counterpart) !!! XCU getopts requires
14144 // `name` to be set to `?` (and, in bash/ksh93/mksh, $OPTARG to be
14145 // cleared) on the end-of-options return; zsh leaves both alone.
14146 // See dash_mode::getopts_end_of_options. No-op in --zsh.
14147 crate::dash_mode::getopts_end_of_options(&var);
14148 return 1;
14149 }
14150
14151 // c:5691-5693 — `quiet = *optstr == ':'; optstr += quiet; lenoptstr -= quiet;`
14152 // c:5648 (54471) — `posix = isset(POSIXBUILTINS) || OPT_ISSET(ops, 'p');`
14153 let posix = isset(POSIXBUILTINS) || OPT_ISSET(ops, b'p');
14154
14155 let (quiet, optstr) = if optstr_full.starts_with(':') {
14156 // c:5691
14157 (true, &optstr_full[1..])
14158 } else {
14159 (false, optstr_full.as_str())
14160 };
14161
14162 // c:5696 — `str = unmetafy(dupstring(args[zoptind - 1]), &lenstr);`
14163 let mut str_buf = args[(zoptind - 1) as usize].clone();
14164 let mut lenstr = str_buf.len() as i32;
14165 if lenstr == 0 {
14166 // !!! EMULATION-ONLY (no C counterpart) !!! XCU getopts requires
14167 // `name` to be set to `?` (and, in bash/ksh93/mksh, $OPTARG to be
14168 // cleared) on the end-of-options return; zsh leaves both alone.
14169 // See dash_mode::getopts_end_of_options. No-op in --zsh.
14170 crate::dash_mode::getopts_end_of_options(&var);
14171 return 1;
14172 } // c:5697
14173
14174 // c:5699-5703 — bump to next arg if optcind exhausted current.
14175 if optcind >= lenstr {
14176 // c:5699
14177 optcind = 0;
14178 zoptind += 1;
14179 if zoptind as usize > args.len() {
14180 // c:5701
14181 ZOPTIND.store(zoptind, Relaxed);
14182 OPTCIND.store(optcind, Relaxed);
14183 // !!! EMULATION-ONLY (no C counterpart) !!! XCU getopts requires
14184 // `name` to be set to `?` (and, in bash/ksh93/mksh, $OPTARG to be
14185 // cleared) on the end-of-options return; zsh leaves both alone.
14186 // See dash_mode::getopts_end_of_options. No-op in --zsh.
14187 crate::dash_mode::getopts_end_of_options(&var);
14188 write_optind(crate::dash_mode::getopts_optind_report(zoptind, optcind, lenstr) as i64); // c:5702
14189 return 1;
14190 }
14191 str_buf = args[(zoptind - 1) as usize].clone();
14192 lenstr = str_buf.len() as i32;
14193 }
14194
14195 // c:5705-5712 — first option char checks: not `-`/`+` → done; `--` → done.
14196 if optcind == 0 {
14197 // c:5705
14198 // c:5677 (54471) — `+` introduces options only without posix.
14199 if lenstr < 2 || (!str_buf.starts_with('-') && (posix || !str_buf.starts_with('+'))) {
14200 ZOPTIND.store(zoptind, Relaxed);
14201 OPTCIND.store(optcind, Relaxed);
14202 // c:5707 — mirror to $OPTIND so callers see the post-loop
14203 // pointer. Previous Rust port skipped this write on the
14204 // "no more options" exit; OPTIND stayed at the last
14205 // option arg index (-b) instead of advancing past it.
14206 write_optind(crate::dash_mode::getopts_optind_report(zoptind, optcind, lenstr) as i64);
14207 // !!! EMULATION-ONLY (no C counterpart) !!! XCU getopts requires
14208 // `name` to be set to `?` (and, in bash/ksh93/mksh, $OPTARG to be
14209 // cleared) on the end-of-options return; zsh leaves both alone.
14210 // See dash_mode::getopts_end_of_options. No-op in --zsh.
14211 crate::dash_mode::getopts_end_of_options(&var);
14212 return 1;
14213 }
14214 if lenstr == 2 && &str_buf[..2] == "--" {
14215 // c:5708
14216 zoptind += 1;
14217 ZOPTIND.store(zoptind, Relaxed);
14218 OPTCIND.store(0, Relaxed);
14219 // !!! EMULATION-ONLY (no C counterpart) !!! XCU getopts requires
14220 // `name` to be set to `?` (and, in bash/ksh93/mksh, $OPTARG to be
14221 // cleared) on the end-of-options return; zsh leaves both alone.
14222 // See dash_mode::getopts_end_of_options. No-op in --zsh.
14223 crate::dash_mode::getopts_end_of_options(&var);
14224 write_optind(crate::dash_mode::getopts_optind_report(zoptind, optcind, lenstr) as i64); // c:5711
14225 return 1;
14226 }
14227 optcind += 1;
14228 }
14229 // c:5685-5687 (54711) — `opch = str[optcind]; optcind += MB_CHARLENCONV(str + optcind, …, &wopch);`
14230 let opch = str_buf.as_bytes()[optcind as usize];
14231 let wopch = str_buf
14232 .get(optcind as usize..)
14233 .and_then(|s| s.chars().next())
14234 .unwrap_or(opch as char);
14235 optcind += wopch.len_utf8() as i32;
14236
14237 // c:5716-5721 — `lenoptbuf = (str[0] == '+') ? 2 : 1; optbuf[lenoptbuf-1] = opch;`
14238 let plus = str_buf.starts_with('+');
14239 let optbuf: String = if plus {
14240 format!("+{}", opch as char)
14241 } else {
14242 format!("{}", opch as char)
14243 };
14244
14245 // c:5724-5740 — illegal option: `?` reply, OPTIND fixed under posix.
14246 let found = optstr.bytes().position(|b| b == opch);
14247 if opch == b':' || found.is_none() {
14248 // c:5724
14249 if posix {
14250 // c:5728
14251 optcind = 0;
14252 zoptind += 1;
14253 }
14254 // c:5731 — `setsparam(var, ztrdup(p));` where p = "?"
14255 setsparam(&var, "?");
14256 if quiet {
14257 // c:5733
14258 setsparam("OPTARG", &optbuf); // c:5734
14259 } else {
14260 let prefix = if plus { "+" } else { "-" };
14261 zwarn(&format!("bad option: {}{}", prefix, wopch)); // c:5736
14262 setsparam("OPTARG", "");
14263 }
14264 ZOPTIND.store(zoptind, Relaxed);
14265 OPTCIND.store(optcind, Relaxed);
14266 // Sync OPTIND env var so callers can read.
14267 write_optind(crate::dash_mode::getopts_optind_report(zoptind, optcind, lenstr) as i64);
14268 return 0;
14269 }
14270
14271 // c:5744 — `if (p[1] == ':')` — required argument.
14272 let p = found.unwrap();
14273 let optstr_bytes = optstr.as_bytes();
14274 if p + 1 < optstr_bytes.len() && optstr_bytes[p + 1] == b':' {
14275 // c:5744
14276 if optcind == lenstr {
14277 // c:5745
14278 // c:5746 — argument in next arg.
14279 if zoptind as usize >= args.len() {
14280 // c:5747
14281 if posix {
14282 optcind = 0;
14283 zoptind += 1;
14284 }
14285 if quiet {
14286 // c:5754
14287 setsparam(&var, ":");
14288 setsparam("OPTARG", &optbuf);
14289 } else {
14290 setsparam(&var, "?");
14291 setsparam("OPTARG", "");
14292 let prefix = if plus { "+" } else { "-" };
14293 zwarn(&format!(
14294 "argument expected after {}{} option",
14295 prefix, wopch
14296 )); // c:5760
14297 }
14298 ZOPTIND.store(zoptind, Relaxed);
14299 OPTCIND.store(optcind, Relaxed);
14300 write_optind(crate::dash_mode::getopts_optind_report(zoptind, optcind, lenstr) as i64);
14301 return 0;
14302 }
14303 // c:5763 — `p = ztrdup(args[zoptind++]);` — read args[zoptind]
14304 // then post-increment. zoptind now points one past the
14305 // arg-bearing flag's index.
14306 let p_arg = args[zoptind as usize].clone(); // c:5763
14307 zoptind += 1; // c:5763 post-increment
14308 setsparam("OPTARG", &p_arg); // c:5765
14309 // c:5771 — `optcind = 0; zoptind++;` — bump past the
14310 // consumed value arg too, so the NEXT getopts call sees
14311 // the arg AFTER the value. Previous Rust port skipped this
14312 // second increment, so the next iter re-read the consumed
14313 // value as if it were a new flag.
14314 optcind = 0; // c:5771
14315 zoptind += 1; // c:5772
14316 } else {
14317 // c:5774 — `p = metafy(str+optcind, lenstr-optcind, META_DUP);`
14318 let p_arg = str_buf[(optcind as usize)..].to_string();
14319 setsparam("OPTARG", &p_arg);
14320 optcind = 0;
14321 zoptind += 1;
14322 }
14323 } else {
14324 // c:5784 — `zsfree(zoptarg); zoptarg = ztrdup("");`
14325 setsparam("OPTARG", "");
14326 }
14327
14328 // c:5788 — `setsparam(var, metafy(optbuf, lenoptbuf, META_DUP));`
14329 setsparam(&var, &optbuf);
14330 ZOPTIND.store(zoptind, Relaxed);
14331 OPTCIND.store(optcind, Relaxed);
14332 write_optind(crate::dash_mode::getopts_optind_report(zoptind, optcind, lenstr) as i64);
14333 0 // c:5790
14334}
14335
14336/// Port of `bin_break()` from `Src/builtin.c:5809`.
14337/// C decl: `bin_break(char *name, char **argv, UNUSED(Options ops), int func)`
14338/// C: `int bin_break(char *name, char **argv, UNUSED(Options ops), int func)`
14339/// — handles BIN_BREAK / BIN_CONTINUE / BIN_RETURN / BIN_LOGOUT / BIN_EXIT.
14340/// WARNING: param names don't match C — Rust=(name, argv, func) vs C=(name, argv, ops, func)
14341pub fn bin_break(
14342 name: &str,
14343 argv: &[String], // c:5809
14344 _ops: &options,
14345 func: i32,
14346) -> i32 {
14347 // BIN_BREAK/CONTINUE/RETURN/EXIT/LOGOUT live at the top of this file
14348 // (c:5707-5712 in Src/builtin.c via the BUILTIN(...) table).
14349 // c:5811 — `int num = lastval, nump = 0, implicit;`
14350 let mut num: i32 = LASTVAL.load(Relaxed); // c:5811
14351 let mut nump = 0i32; // c:5811
14352 let implicit = argv.is_empty(); // c:5814
14353 // !!! DASH-ONLY (no C counterpart) !!! dash's `number()` takes a plain
14354 // `[+]digits` that fits an int; anything else (`abc`, `-1`, `1+1`, `0x10`,
14355 // empty) is `sh_error("Illegal number: %s")`, which unwinds the whole
14356 // command list with status 2 (see dash_mode::fatal_error_status). zsh
14357 // instead evaluates the argument as arithmetic.
14358 let mut dash_num: Option<i32> = None;
14359 if !implicit && crate::dash_mode::dash_faithful() {
14360 dash_num = crate::dash_mode::dash_number(&argv[0]);
14361 if dash_num.is_none() {
14362 zerrnam(name, &format!("Illegal number: {}", argv[0]));
14363 return crate::dash_mode::fatal_error_status().unwrap_or(2);
14364 }
14365 }
14366 // c:5815-5818 — first arg parsed as math expr.
14367 if let Some(n) = dash_num {
14368 num = n;
14369 nump = 1;
14370 } else if !implicit {
14371 // c:5815
14372 // c:5816 — mathevali reports a bad expression itself (zerr, which
14373 // sets errflag), so the `argument is not positive` zerrnam below is
14374 // then suppressed; the Rust mathevali hands the message back instead.
14375 num = match mathevali(&argv[0]) {
14376 Ok(n) => n as i32,
14377 Err(msg) => {
14378 zerr(&msg);
14379 0
14380 }
14381 }; // c:5816
14382 nump = 1; // c:5817
14383 }
14384 // !!! EMULATION-ONLY (no C counterpart) !!! bash and the pdksh line keep a
14385 // function's status in 8 bits (`f() { return 300; }; f; echo $?` -> 44,
14386 // `return -1` -> 255); zsh and ksh93 hand back the full value.
14387 if func == BIN_RETURN && (crate::dash_mode::bash_mode() || crate::dash_mode::pdksh_family()) {
14388 num &= 0xff;
14389 }
14390
14391 let loops = LOOPS.load(Relaxed);
14392 match func {
14393 // c:5793-5808 (55061) — BIN_CONTINUE / BIN_BREAK share one arm.
14394 // Both errors are `zerrnam`, which sets errflag so the calling
14395 // function or script stops after the command (Bug #616).
14396 x if x == BIN_CONTINUE || x == BIN_BREAK => {
14397 let num = if nump != 0 { num } else { 1 }; // c:5795
14398 // !!! DASH-FAITHFUL GATE (no C counterpart) !!! dash's breakcmd clamps the
14399 // count to the loop nest and returns 0: `break` / `continue` outside a
14400 // loop (or with count 0) is a silent no-op, not an error.
14401 if crate::dash_mode::dash_faithful() && (num == 0 || loops == 0) {
14402 return 0;
14403 }
14404 if num <= 0 {
14405 // c:5796
14406 zerrnam(name, &format!("argument is not positive: {}", num)); // c:5797
14407 return 1; // c:5798
14408 }
14409 if loops == 0 {
14410 // c:5800 — break/continue only permitted in loops
14411 zerrnam(
14412 name,
14413 if ANCESTOR_LOOPS.load(Relaxed) != 0 {
14414 "not in same subshell as first enclosing loop" // c:5802
14415 } else {
14416 "not in while, until, select, or repeat loop" // c:5803 (5.9.2; the dev tree adds "for")
14417 },
14418 );
14419 return 1; // c:5804
14420 }
14421 CONTFLAG.store((func == BIN_CONTINUE) as i32, Relaxed); // c:5806
14422 BREAKS.store(num.min(loops), Relaxed); // c:5807
14423 }
14424 // c:5839-5860 — BIN_RETURN.
14425 x if x == BIN_RETURN => {
14426 let interactive = isset(INTERACTIVE);
14427 let shinstdin = isset(SHINSTDIN);
14428 let ll_v = locallevel_param.load(Relaxed);
14429 let sourcelevel = crate::ported::init::sourcelevel.load(Relaxed);
14430 // c:5840-5841 — `if ((interactive && shinstdin) || locallevel || sourcelevel)`
14431 if (interactive && shinstdin) || ll_v != 0 || sourcelevel != 0 {
14432 // c:5840
14433 RETFLAG.store(1, Relaxed); // c:5842
14434 BREAKS.store(loops, Relaxed); // c:5843
14435 LASTVAL.store(num, Relaxed); // c:5844
14436 // c:5845-5854 — inside a primed trap with the sentinel
14437 // `trap_return == -2`, promote to TRAP_STATE_FORCE_RETURN
14438 // and carry `lastval`. POSIXTRAPS + `implicit` opts out:
14439 // POSIX semantics keep $? from before the trap fired.
14440 let posixtraps = isset(POSIXTRAPS);
14441 let cur_state = TRAP_STATE.load(Relaxed);
14442 let cur_return = TRAP_RETURN.load(Relaxed);
14443 if cur_state == TRAP_STATE_PRIMED // c:5845
14444 && cur_return == -2 // c:5845
14445 && !(posixtraps && implicit)
14446 // c:5851
14447 {
14448 TRAP_STATE.store(
14449 // c:5852
14450 TRAP_STATE_FORCE_RETURN,
14451 Relaxed,
14452 );
14453 TRAP_RETURN.store(num, Relaxed);
14454 // c:5853
14455 }
14456 return num; // c:5855
14457 }
14458 // !!! BASH-MODE (no C counterpart) !!! bash refuses `return` outside
14459 // a function / sourced script (status 2) and keeps running.
14460 if crate::dash_mode::bash_mode() {
14461 zwarnnam(name, "can only `return\u{27} from a function or sourced script");
14462 return 2;
14463 }
14464 // c:5858 — fallthrough: treat as logout/exit.
14465 zexit(num, ZEXIT_NORMAL); // c:5858
14466 }
14467 // c:5864-5869 — BIN_LOGOUT: refuse if not LOGINSHELL, then
14468 // FALLTHROUGH into the BIN_EXIT body. The previous Rust port
14469 // called \`zexit(num, ZEXIT_NORMAL)\` directly instead of
14470 // entering the BIN_EXIT defer-guard, so \`logout\` from inside
14471 // a function would skip EXIT traps + function unwind +
14472 // \"you have running jobs\" warning — same gap as the prior
14473 // BIN_EXIT fix.
14474 x if x == BIN_LOGOUT => {
14475 // c:5865 — `if (unset(LOGINSHELL))`. The previous Rust port
14476 // called `optlookup("login")` — but "login" is the
14477 // SHELL-LETTER-FLAG name (zshletters table letter 'l'),
14478 // not an option name. Option name canonicalization maps
14479 // LOGINSHELL → "loginshell" (Src/options.c index_to_name
14480 // at line 1682 in Rust port).
14481 //
14482 // \`optlookup(\"login\")\` returns OPT_INVALID (0), so
14483 // \`isset(0)\` always returns false — bin_logout always
14484 // saw \"not login shell\" and rejected with that error
14485 // regardless of whether the shell was actually started
14486 // with \`-l\`.
14487 let loginshell = isset(LOGINSHELL);
14488 if !loginshell {
14489 // c:5865
14490 // c:Src/builtin.c:5861 — `zerrnam(name, "not login
14491 // shell");`. C uses zerrnam (sets ERRFLAG_ERROR via
14492 // utils.c:203), NOT zwarnnam. The errflag set aborts
14493 // the remainder of the current command list — `logout
14494 // 2>&1; print ex:$?` in a non-login shell prints only
14495 // the error message because the `;` separator's next
14496 // command is gated on errflag-clear. Previous Rust
14497 // port used zwarnnam (no errflag) so `print` still
14498 // ran. Pinned by logout_builtin_stderr parity probe.
14499 zerrnam(name, "not login shell"); // c:5861
14500 return 1; // c:5862
14501 }
14502 // c:5869 — `/*FALLTHROUGH*/` into BIN_EXIT body.
14503 // Reusing the BIN_EXIT branch below by setting `func` to
14504 // BIN_EXIT isn't possible mid-match; inline the same
14505 // guard logic here.
14506 let cur_locallevel = locallevel_param.load(Relaxed);
14507 let forklevel = FORKLEVEL.load(Relaxed);
14508 let shell_exiting = SHELL_EXITING.load(Relaxed);
14509 if cur_locallevel > forklevel && shell_exiting != -1 {
14510 // c:5871
14511 if STOPMSG.load(Relaxed) == 0 {
14512 zexit(0, ZEXIT_DEFERRED); // c:5884
14513 }
14514 if STOPMSG.load(Relaxed) == 0 {
14515 // c:5884
14516 let trap_state = TRAP_STATE.load(Relaxed);
14517 if trap_state != 0 {
14518 // c:5885
14519 TRAP_STATE.store(
14520 // c:5886
14521 TRAP_STATE_FORCE_RETURN,
14522 Relaxed,
14523 );
14524 }
14525 RETFLAG.store(1, Relaxed); // c:5887
14526 BREAKS.store(
14527 LOOPS.load(Relaxed), // c:5888
14528 Relaxed,
14529 );
14530 EXIT_PENDING.store(1, Relaxed); // c:5889
14531 EXIT_LEVEL.store(cur_locallevel, Relaxed); // c:5890 — exit_level = locallevel;
14532 EXIT_VAL.store(num, Relaxed); // c:5891
14533 }
14534 } else {
14535 zexit(num, ZEXIT_NORMAL); // c:5894
14536 }
14537 }
14538 // c:5870-5894 — BIN_EXIT: function-context guard. C body:
14539 // if (locallevel > forklevel && shell_exiting != -1) {
14540 // if (stopmsg || (zexit(0, ZEXIT_DEFERRED), !stopmsg)) {
14541 // if (trap_state) trap_state = TRAP_STATE_FORCE_RETURN;
14542 // retflag = 1; breaks = loops;
14543 // exit_pending = 1; exit_level = locallevel; exit_val = num;
14544 // }
14545 // } else zexit(num, ZEXIT_NORMAL);
14546 //
14547 // Inside a function (locallevel > forklevel) the shell can't
14548 // exit directly — EXIT traps still need to run. The probe
14549 // path zexit(0, ZEXIT_DEFERRED) calls checkjobs; if no
14550 // stopmsg triggered, we defer: set retflag + breaks +
14551 // exit_pending so the function unwind takes us out.
14552 //
14553 // The previous Rust port skipped this entire guard, always
14554 // calling zexit(num, ZEXIT_NORMAL) directly. `exit` inside
14555 // a function would terminate without running EXIT traps or
14556 // unwinding the function stack.
14557 x if x == BIN_EXIT => {
14558 let cur_locallevel = locallevel_param.load(Relaxed);
14559 let forklevel = FORKLEVEL.load(Relaxed);
14560 let shell_exiting = SHELL_EXITING.load(Relaxed);
14561 if cur_locallevel > forklevel && shell_exiting != -1 {
14562 // c:5871
14563 // Probe via ZEXIT_DEFERRED — may set stopmsg.
14564 if STOPMSG.load(Relaxed) == 0 {
14565 zexit(0, ZEXIT_DEFERRED); // c:5884
14566 }
14567 if STOPMSG.load(Relaxed) == 0 {
14568 // c:5884 still no stopmsg → defer
14569 let trap_state = TRAP_STATE.load(Relaxed);
14570 if trap_state != 0 {
14571 // c:5885
14572 TRAP_STATE.store(
14573 // c:5886
14574 TRAP_STATE_FORCE_RETURN,
14575 Relaxed,
14576 );
14577 }
14578 RETFLAG.store(1, Relaxed); // c:5887
14579 BREAKS.store(
14580 LOOPS.load(Relaxed), // c:5888
14581 Relaxed,
14582 );
14583 EXIT_PENDING.store(1, Relaxed); // c:5889
14584 EXIT_LEVEL.store(cur_locallevel, Relaxed); // c:5890 — exit_level = locallevel;
14585 EXIT_VAL.store(num, Relaxed); // c:5891
14586 }
14587 } else {
14588 zexit(num, ZEXIT_NORMAL); // c:5894
14589 }
14590 }
14591 _ => {}
14592 }
14593 0
14594}
14595
14596/// Port of `checkjobs()` from Src/builtin.c:5899.
14597/// C: `static void checkjobs(void)` — walk `jobtab[1..maxjob]`; for each
14598/// non-current job that's STAT_LOCKED, not STAT_NOPRINT, and either
14599/// running (when CHECKRUNNINGJOBS is set) or STAT_STOPPED, emit
14600/// "you have running/stopped jobs" + set `stopmsg = 1`.
14601pub fn checkjobs() {
14602 // c:5899
14603 let checkrunning = isset(CHECKRUNNINGJOBS);
14604 // c:5901 — read the canonical jobs.rs THISJOB/MAXJOB globals.
14605 // The previous builtin.rs duplicate AtomicI32s for both never
14606 // synced with the jobs.rs Mutex<i32> values that the spawn /
14607 // wait paths actually update — checkjobs would see stale 0s
14608 // regardless of how many jobs were active.
14609 let thisjob: i32 = *crate::ported::jobs::THISJOB
14610 .get_or_init(|| Mutex::new(-1_i32))
14611 .lock()
14612 .expect("THISJOB poisoned");
14613 // jobs::MAXJOB is stored as `Mutex<usize>` (Rust adaptation for
14614 // Vec-index semantics); cast to i32 for comparison with `thisjob`.
14615 let maxjob: i32 = *crate::ported::jobs::MAXJOB
14616 .get_or_init(|| Mutex::new(0_usize))
14617 .lock()
14618 .expect("MAXJOB poisoned") as i32;
14619
14620 // c:5903 — `for (i = 1; i <= maxjob; i++)`
14621 let mut found: Option<i32> = None;
14622 let mut found_stat: i32 = 0;
14623 for i in 1..=maxjob {
14624 // c:5903
14625 // c:5904 `jobtab[i].stat` — the canonical table. The JOBSTATS
14626 // mirror this used to read is never written, so every job looked
14627 // unused and `exit` never warned about running or stopped jobs.
14628 let stat = crate::ported::jobs::JOBTAB
14629 .get_or_init(|| Mutex::new(Vec::new()))
14630 .lock()
14631 .ok()
14632 .and_then(|t| t.get(i as usize).map(|j| j.stat))
14633 .unwrap_or(0);
14634 // c:5904-5906 — `i != thisjob && (stat & STAT_LOCKED) &&
14635 // !(stat & STAT_NOPRINT) &&
14636 // (CHECKRUNNINGJOBS || stat & STAT_STOPPED)`
14637 if i != thisjob // c:5904
14638 && (stat & STAT_LOCKED) != 0 // c:5904
14639 && (stat & STAT_NOPRINT) == 0 // c:5905
14640 && (checkrunning || (stat & STAT_STOPPED) != 0)
14641 // c:5906
14642 {
14643 found = Some(i); // c:5907
14644 found_stat = stat;
14645 break;
14646 }
14647 }
14648 // c:5908 — `if (i <= maxjob)`
14649 if found.is_some() {
14650 // c:5908
14651 if (found_stat & STAT_STOPPED) != 0 {
14652 // c:5909
14653 // c:5903-5908 — USE_SUSPENDED is always defined (config_h.rs).
14654 zerr("you have suspended jobs."); // c:5904
14655 } else {
14656 // c:5917 — `zerr("you have running jobs.");`
14657 zerr("you have running jobs."); // c:5917
14658 }
14659 STOPMSG.store(1, Relaxed); // c:5919
14660 }
14661}
14662
14663/// Port of `realexit()` from Src/builtin.c:5953.
14664/// C body (single statement):
14665/// `exit((shell_exiting || exit_pending) ? exit_val : lastval);`
14666pub fn realexit() -> ! {
14667 // c:5953
14668 std::process::exit(
14669 if SHELL_EXITING.load(Relaxed) != 0 || EXIT_PENDING.load(Relaxed) != 0 {
14670 EXIT_VAL.load(Relaxed)
14671 } else {
14672 LASTVAL.load(Relaxed)
14673 },
14674 );
14675}
14676
14677/// Port of `_realexit()` from Src/builtin.c:5962.
14678/// C body (single statement):
14679/// `_exit((shell_exiting || exit_pending) ? exit_val : lastval);`
14680pub fn _realexit() -> ! {
14681 // c:5962
14682 unsafe {
14683 libc::_exit(
14684 if SHELL_EXITING.load(Relaxed) != 0 || EXIT_PENDING.load(Relaxed) != 0 {
14685 EXIT_VAL.load(Relaxed)
14686 } else {
14687 LASTVAL.load(Relaxed)
14688 },
14689 )
14690 }
14691}
14692
14693/// Port of `zexit()` from `Src/builtin.c:5977`.
14694/// C decl: `zexit(int val, enum zexit_t from_where)`
14695/// C: `void zexit(int val, enum zexit_t from_where)` — record exit
14696/// value, fire EXIT trap unless already exiting, then realexit.
14697#[allow(unused_variables)]
14698pub fn zexit(val: i32, from_where: i32) {
14699 // c:5977
14700 // c:5989 — `exit_val = val;`
14701 EXIT_VAL.store(val, Relaxed); // c:5989
14702 // c:5990 — `if (shell_exiting == -1) { retflag = 1; breaks = loops; return; }`
14703 if SHELL_EXITING.load(Relaxed) == -1 {
14704 // c:5990
14705 RETFLAG.store(1, Relaxed); // c:5991
14706 BREAKS.store(LOOPS.load(Relaxed), Relaxed); // c:5992
14707 return; // c:5993
14708 }
14709
14710 // c:5996-6004 — `if (isset(MONITOR) && !stopmsg && from_where != ZEXIT_SIGNAL)`:
14711 // run scanjobs + checkjobs; if stopmsg got set (running jobs warned),
14712 // mark stopmsg=2 and DEFER the exit. The previous Rust port skipped
14713 // this entire block, so `exit` with running jobs would terminate
14714 // immediately rather than emitting the standard
14715 // \"zsh: you have running jobs\" + waiting for a confirmation exit.
14716 if isset(MONITOR) // c:5996
14717 && STOPMSG.load(Relaxed) == 0
14718 && from_where != ZEXIT_SIGNAL
14719 {
14720 // c:5997 — `scanjobs(); /* check if jobs need printing */`
14721 if let Some(jt) = crate::ported::jobs::JOBTAB.get() {
14722 let mut guard = jt.lock().unwrap();
14723 crate::ported::jobs::scanjobs(&mut guard); // c:5997
14724 }
14725 // c:5998 — `if (isset(CHECKJOBS))`. Without the gate
14726 // `setopt no_check_jobs` still refused the first `exit` with
14727 // "you have running jobs." (W02jobs.ztst "running job with
14728 // no_check_jobs").
14729 if isset(CHECKJOBS) {
14730 checkjobs(); // c:5999
14731 }
14732 if STOPMSG.load(Relaxed) != 0 {
14733 // c:6000
14734 STOPMSG.store(2, Relaxed); // c:6001
14735 return; // c:6002 defer
14736 }
14737 }
14738 // c:6006-6008 — `if (from_where == ZEXIT_DEFERRED || (shell_exiting++
14739 // && from_where != ZEXIT_NORMAL)) return;`. Probe path:
14740 // ZEXIT_DEFERRED callers only want the checkjobs gate to fire; if
14741 // it didn't trip, return without actually exiting.
14742 if from_where == ZEXIT_DEFERRED {
14743 // c:6006
14744 return;
14745 }
14746 let prev_exiting = SHELL_EXITING.fetch_add(1, Relaxed);
14747 if prev_exiting != 0 && from_where != ZEXIT_NORMAL {
14748 // c:6007
14749 return;
14750 }
14751 // c:6014 — `shell_exiting = -1;`
14752 SHELL_EXITING.store(-1, Relaxed); // c:6014
14753 // c:6019 — `errflag = 0;`
14754 errflag.store(0, Relaxed); // c:6019
14755 // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
14756 // Exit is committed from here, so drain the buffered autoload
14757 // bytecode entries. `preprompt()` is the batch boundary for an
14758 // interactive shell; a script never reaches a prompt, so without
14759 // this its compiles would never reach the cache at all.
14760 crate::autoload_cache::try_flush_pending();
14761 crate::deparse_cache::try_flush_pending();
14762 // c:6021-6024 — MONITOR → killrunjobs.
14763 if isset(MONITOR) {
14764 // c:6021
14765 crate::ported::signals::killrunjobs(if from_where == ZEXIT_SIGNAL { 1 } else { 0 });
14766 // c:6023
14767 }
14768 // !!! RUST-ONLY GATE: see SUBSHELL_DEPTH declaration above for
14769 // rationale. C zsh's realexit at c:5953 unconditionally calls
14770 // process::exit because the subshell was forked; in zshrs the
14771 // subshell runs in-process, so process::exit would kill the
14772 // whole shell. Defer: set EXIT_PENDING + EXIT_VAL + reset
14773 // SHELL_EXITING so the subshell_end unwind in fusevm_bridge
14774 // catches and propagates the status to the parent.
14775 if SUBSHELL_DEPTH.load(Relaxed) > 0 {
14776 SHELL_EXITING.store(0, Relaxed);
14777 EXIT_VAL.store(val, Relaxed);
14778 EXIT_PENDING.store(1, Relaxed);
14779 RETFLAG.store(1, Relaxed);
14780 BREAKS.store(LOOPS.load(Relaxed), Relaxed);
14781 return;
14782 }
14783 // c:6012 — `cleanfilelists();` — delete per-job temp-file lists
14784 // before exit. jobs.rs::cleanfilelists is a Rust-only-signature
14785 // adapter over the C global jobtab (see its WARNING block).
14786 // C runs it unconditionally. zshrs allocates JOBTAB lazily (a `-c`
14787 // command that never started a job has none), and skipping the call
14788 // then also skipped the `=(cmd)` temp files cleanfilelists drains from
14789 // the VM's pending list: `() { exit } =(true)` left the file behind
14790 // (D03procsubst.ztst "exit in shell function cleans up tempfiles").
14791 match crate::ported::jobs::JOBTAB.get() {
14792 Some(tab) => {
14793 let mut tab = tab.lock().unwrap_or_else(|e| e.into_inner());
14794 crate::ported::jobs::cleanfilelists(&mut tab); // c:6012
14795 }
14796 None => crate::ported::jobs::cleanfilelists(&mut []), // c:6012
14797 }
14798 // ZSHRS-ONLY, no C counterpart — the selected drop-in's logout file
14799 // (`~/.bash_logout` for `--bash`, `~/.logout` + `/etc/csh.logout` for
14800 // `--csh`; the Korn and Bourne shells document none).
14801 //
14802 // NOT gated on `interact()` the way zsh's `.zlogout` below is: bash
14803 // reads `~/.bash_logout` when "an interactive login shell exits, or a
14804 // non-interactive login shell executes the exit builtin" (bash(1)
14805 // INVOCATION) — verified against bash 5.3.15, where both `bash -l -i`
14806 // on exit and `bash -l -c exit` source it, while `bash -l -c true`
14807 // (fall-through, no exit builtin) does not. zshrs reaches this
14808 // function only through the exit path, matching that distinction.
14809 // History saving below still runs for every drop-in; only the
14810 // `.zlogout` sourcing is theirs to replace.
14811 if crate::extensions::emulation_startup::overrides_zsh_startup() {
14812 crate::extensions::emulation_startup::run_logout_scripts();
14813 }
14814 // c:6013-6028 — `if (isset(RCS) && interact)`: save the history
14815 // file and run logout scripts. This is THE write-at-exit path for
14816 // interactive history (`-f`/NO_RCS shells intentionally skip it,
14817 // matching C).
14818 if isset(RCS) && interact() {
14819 // c:6013
14820 // c:6014-6020 — `if (!nohistsave) { ... }`.
14821 if crate::ported::exec::nohistsave.load(Relaxed) == 0 {
14822 let mut writeflags = HFILE_USE_OPTIONS as i32; // c:6015
14823 if from_where == ZEXIT_SIGNAL {
14824 // c:6016
14825 writeflags |= HFILE_NO_REWRITE as i32; // c:6017
14826 }
14827 saveandpophiststack(1, writeflags); // c:6018
14828 savehistfile(None, writeflags); // c:6019
14829 }
14830 // c:6021-6027 — `if (islogin && !subsh) { sourcehome(".zlogout");
14831 // ... source(GLOBAL_ZLOGOUT); }`. The C `subsh` check is covered
14832 // by the RUST-ONLY SUBSHELL_DEPTH gate above (in-process
14833 // subshells returned before reaching here).
14834 // `&& !overrides_zsh_startup()`: only a zsh shell has a
14835 // `.zlogout`; every drop-in's logout file was sourced above.
14836 if islogin() && !crate::extensions::emulation_startup::overrides_zsh_startup() {
14837 // c:6021
14838 crate::ported::init::sourcehome(".zlogout"); // c:6022
14839 if isset(RCS) && isset(GLOBALRCS) {
14840 // c:6024
14841 let _ = crate::ported::init::source(&crate::extensions::global_rc::global_rc_path(
14842 crate::ported::config_h::GLOBAL_ZLOGOUT,
14843 ));
14844 // c:6025
14845 }
14846 }
14847 }
14848 // c:Src/builtin.c:6075-6079 — fire EXIT trap (SIGEXIT) before
14849 // calling realexit. The trap body sees $? = val (carried via
14850 // LASTVAL below) and runs in the shell process. Remove the
14851 // entry from traps_table first so the trap body's own commands
14852 // don't re-trigger it recursively.
14853 //
14854 // Two trap-storage paths: (a) `trap 'cmd' EXIT` stores raw
14855 // body text in `traps_table` (no settrap), handled by the
14856 // direct execute_script below; (b) `TRAPEXIT() { ... }`
14857 // function-named form goes through settrap(SIGEXIT, None,
14858 // ZSIG_FUNC) at funcdef time and lives in the
14859 // shfunctab+sigtrapped pair, dispatched via `dotrap(SIGEXIT)`.
14860 // C zsh's dotrap handles BOTH paths internally; Rust splits
14861 // them. Call dotrap to cover the function-named form. Bug
14862 // #157 in docs/BUGS.md.
14863 let exit_trap = traps_table().lock().ok().and_then(|mut t| t.remove("EXIT"));
14864 LASTVAL.store(val, Relaxed); // c:6076 — $? inside trap = val
14865 crate::ported::signals::in_exit_trap.store(1, Relaxed);
14866 if let Some(body) = exit_trap {
14867 // Set LASTVAL to the requested exit value so `$?` inside
14868 // the trap body sees the right number (matches `(exit 7)`
14869 // → trap body reads $?=7).
14870 let _ = crate::ported::exec::execute_script(&body);
14871 }
14872 // c:6043-6044 — `if (sigtrapped[SIGEXIT]) dotrap(SIGEXIT);`.
14873 //
14874 // dotrap DOES reach its ZSIG_FUNC arm here, but the body cannot run: by
14875 // this point we are outside the VM, and dotrapargs' doshfunc path has no
14876 // executor to run the compiled function chunk in, so it returns silently.
14877 // The natural end-of-script path already knows this and invokes the
14878 // function through the script pipeline instead (vm_helper.rs:2850-2854);
14879 // the explicit `exit` path did not, so
14880 // TRAPEXIT() { print BYE }; exit 3
14881 // printed nothing where zsh prints BYE — while the string form
14882 // (`trap "print S" EXIT; exit 3`) worked, because that one is dispatched
14883 // from traps_table just above.
14884 let exit_fn_trapped = crate::ported::signals::sigtrapped
14885 .lock()
14886 .ok()
14887 .and_then(|g| g.get(crate::signals_h::SIGEXIT as usize).copied())
14888 .unwrap_or(0)
14889 & crate::ported::zsh_h::ZSIG_FUNC as i32
14890 != 0;
14891 if exit_fn_trapped {
14892 let _ = crate::ported::exec::execute_script("TRAPEXIT");
14893 } else {
14894 let _ = crate::ported::signals::dotrap(crate::signals_h::SIGEXIT);
14895 }
14896 // c:Src/init.c::zexit — `callhookfunc("zshexit", NULL, 1, NULL)`.
14897 // The hook fires both a `zshexit` function (if defined) AND
14898 // walks the `zshexit_functions` array. Distinct from SIGEXIT
14899 // trap dispatch above — zshexit is a zsh-specific shell-event
14900 // hook that operates parallel to trap. Bug #215 in docs/BUGS.md.
14901 let _ = crate::ported::utils::callhookfunc("zshexit", None, 1, std::ptr::null_mut());
14902 crate::ported::signals::in_exit_trap.store(0, Relaxed);
14903 realexit(); // c:6082
14904}
14905
14906/// Port of `bin_dot()` from `Src/builtin.c:6060`.
14907/// C decl: `bin_dot(char *name, char **argv, UNUSED(Options ops), UNUSED(int func))`
14908/// C: `int bin_dot(char *name, char **argv, ...)` — `.` / `source`
14909/// builtin: locate script (cwd → first `/`-bearing path → $path search)
14910/// and execute it; positional params shift to argv[1..].
14911/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
14912pub fn bin_dot(
14913 name: &str,
14914 argv: &[String], // c:6060
14915 _ops: &options,
14916 _func: i32,
14917) -> i32 {
14918 if argv.is_empty() {
14919 // c:6068
14920 return 0; // c:6069
14921 }
14922
14923 // PFA-SMR aspect: record the source path so the replay tool can
14924 // re-apply the same source/dot at the same call site.
14925 #[cfg(feature = "recorder")]
14926 if crate::recorder::is_enabled() && !argv[0].is_empty() {
14927 let ctx = crate::recorder::recorder_ctx_global();
14928 crate::recorder::emit_source(&argv[0], ctx);
14929 }
14930 // c:6071-6074 — save pparams, install argv[1..] as new pparams.
14931 // !!! DASH-ONLY (no C counterpart) !!! dash's `.` takes no positional
14932 // arguments: `. file a b` runs file with the CALLER's $1.. intact.
14933 let saved_pparams: Option<Vec<String>> = if argv.len() > 1 && !crate::dash_mode::dash_faithful() {
14934 // c:6072
14935 let mut pp = PPARAMS.lock().unwrap_or_else(|e| {
14936 PPARAMS.clear_poison();
14937 e.into_inner()
14938 });
14939 let saved = pp.clone();
14940 *pp = argv[1..].to_vec(); // c:6073
14941 Some(saved)
14942 } else {
14943 None
14944 };
14945
14946 let arg0 = argv[0].clone(); // c:6076
14947 let _enam = arg0.clone(); // c:6076
14948 // c:6077-6080 — `if (isset(FUNCTIONARGZERO)) { old0 = argzero;
14949 // argzero = ztrdup(arg0); }`.
14950 // Save the prior argzero so it can be restored at the end of
14951 // bin_dot; under FUNCTIONARGZERO, the sourced file becomes the
14952 // active $0 for the duration of the source.
14953 let saved_argzero: Option<Option<String>> = if isset(FUNCTIONARGZERO) {
14954 let prev = argzero();
14955 set_argzero(Some(arg0.clone()));
14956 Some(prev)
14957 } else {
14958 None
14959 };
14960 let mut diddot = 0i32; // c:6064
14961 let mut dotdot = 0i32; // c:6064
14962
14963 // c:6087-6093 — for `source`, try cwd first.
14964 let mut found_path: Option<String> = None;
14965 if !name.starts_with('.') {
14966 // c:6087
14967 let p = Path::new(&arg0);
14968 if p.exists() && !p.is_dir() {
14969 // c:6088-6089
14970 diddot = 1; // c:6090
14971 found_path = Some(arg0.clone()); // c:6091 (effective)
14972 }
14973 }
14974
14975 // c:6094-6101 — try literal path with `/` in it.
14976 if found_path.is_none() && arg0.contains('/') {
14977 // c:6096
14978 if arg0.starts_with("./") {
14979 diddot += 1;
14980 }
14981 // c:6097
14982 else if arg0.starts_with("../") {
14983 dotdot += 1;
14984 } // c:6098
14985 let p = Path::new(&arg0);
14986 if p.exists() && !p.is_dir() {
14987 found_path = Some(arg0.clone()); // c:6100
14988 } else if Path::new(&format!("{}.zwc", arg0)).is_file() {
14989 // c:6100 — C calls `source(arg0)` UNCONDITIONALLY for a
14990 // slash path; `source()` (init.c:1566) then loads the
14991 // sibling `<arg0>.zwc` via try_source_file even when the
14992 // plain file is gone. zshrs's pre-resolved found_path
14993 // gate must therefore accept the compiled form too —
14994 // the try_source_file hook at the read site below
14995 // supplies the body.
14996 found_path = Some(arg0.clone());
14997 }
14998 }
14999
15000 // c:6102-6121 — $path search (with PATHDIRS guard).
15001 let pathdirs = isset(PATHDIRS);
15002 if found_path.is_none() && (!arg0.contains('/') || (pathdirs && diddot < 2 && dotdot == 0)) {
15003 // c:6102
15004 // c:6103 — `for (pp = path; *pp; pp++)`. C walks the `path[]`
15005 // array (the shell-side $path), not the colon-joined
15006 // $PATH env. Read $PATH from paramtab (the shell
15007 // string view); the colon-split below mirrors the C
15008 // path[] iteration.
15009 let path_env = getsparam("PATH").unwrap_or_default();
15010 for dir in path_env.split(':') {
15011 // c:6107
15012 let buf = if dir.is_empty() || dir == "." {
15013 // c:6108
15014 if diddot != 0 {
15015 continue;
15016 }
15017 diddot = 1; // c:6111
15018 arg0.clone() // c:6112
15019 } else {
15020 format!("{}/{}", dir, arg0) // c:6114
15021 };
15022 let p = Path::new(&buf);
15023 if p.exists() && !p.is_dir() {
15024 // c:6117-6118
15025 found_path = Some(buf); // c:6119
15026 break;
15027 }
15028 }
15029 }
15030
15031 // c:6125-6128 — restore pparams. C does this AFTER source() runs
15032 // so the sourced script sees the new positional args; bug #459 in
15033 // docs/BUGS.md — the previous Rust port restored here BEFORE the
15034 // file body executed, so `source script.zsh hello world` had the
15035 // script see $#=0 / $@="". Defer the restore until after
15036 // execute_script returns (the success path below + the error path
15037 // for file-not-found).
15038
15039 // c:6130-6137 — error path. C: `if (ret == SOURCE_NOT_FOUND)`
15040 // emits via zerrnam (POSIX) / zwarnnam (default). The Rust port
15041 // uses zwarnnam unconditionally because the POSIX hard-error
15042 // path also calls zerrnam which behaves identically here (both
15043 // route through zwarning); the only difference C makes is
15044 // promoting errflag to ERRFLAG_ERROR which already happens
15045 // inside zwarnnam.
15046 let path = match found_path {
15047 Some(p) => p,
15048 None => {
15049 // c:6149 — restore argzero on the error path BEFORE
15050 // emitting the diagnostic and returning. C's structure is
15051 // `source(); if (isset(FUNCTIONARGZERO)) restore`; for the
15052 // file-not-found early-return, the restore still has to
15053 // happen so the outer shell's $0 doesn't keep the dropped
15054 // arg0. Bug #103 in docs/BUGS.md: the previous port did
15055 // this unconditional pre-check restore which ran BEFORE
15056 // `execute_script` on the file-found branch — clobbering
15057 // the just-set arg0 so the sourced script saw the shell
15058 // binary as $0 instead of its own path.
15059 if let Some(prev) = saved_argzero.clone() {
15060 set_argzero(prev);
15061 }
15062 // c:6126-6128 — also restore pparams on the not-found path,
15063 // matching C's c:6125 unconditional cleanup before return.
15064 if let Some(saved) = saved_pparams.clone() {
15065 let mut pp = PPARAMS.lock().unwrap_or_else(|e| {
15066 PPARAMS.clear_poison();
15067 e.into_inner()
15068 });
15069 *pp = saved;
15070 }
15071 // c:6130-6137 — `if (isset(POSIXBUILTINS)) zerrnam(...)
15072 // else zwarnnam(...)`. zerrnam sets ERRFLAG_ERROR ("hard
15073 // error in POSIX (we'll exit later)" — the exec.c done:
15074 // gate at c:4378 turns it into exit(1) for the PSPECIAL
15075 // `.`); zwarnnam does NOT touch errflag, so without
15076 // POSIX_BUILTINS the script continues.
15077 let msg = format!("{}: {}", "no such file or directory", arg0); // c:6135
15078 // !!! BASH-MODE (no C counterpart) !!! non-posix bash reports the
15079 // missing file, returns 1 and keeps running (only `set -o posix`
15080 // makes it fatal); zsh's POSIX_BUILTINS aborts and returns 127.
15081 if crate::dash_mode::bash_mode() {
15082 zwarnnam(name, &msg);
15083 return 1;
15084 }
15085 if isset(crate::ported::zsh_h::POSIXBUILTINS) {
15086 crate::ported::utils::zerrnam(name, &msg); // c:6133
15087 } else {
15088 zwarnnam(name, &msg); // c:6135
15089 }
15090 // c:6143 — `return ret == SOURCE_OK ? lastval : 128 - ret`.
15091 // SOURCE_NOT_FOUND = 1 (Src/zsh.h:2214) → 128 - 1 = 127.
15092 return 128 - 1;
15093 }
15094 };
15095
15096 // c:6140 — `ret = source(enam = buf);`
15097 // C `source()` lives at Src/init.c:1550. It opens the file, sets
15098 // up sourcelevel + scriptname + funcstack, parses + executes via
15099 // the wordcode walker, then unwinds. Rust port reads the file
15100 // and routes the body through fusevm's `execute_script` — the
15101 // VM's parse + compile + run loop is the analog of C's
15102 // loop/execlist tree walk. Errors during execution propagate
15103 // through `lastval`; missing read returns SOURCE_ERROR (128-2 =
15104 // 126) per c:6143.
15105 //
15106 // crate::ported::init::sourcelevel bump (Src/init.c:1606 `sourcelevel++;` /
15107 // c:1644 `sourcelevel--;`) is REQUIRED for `return` inside the
15108 // sourced file to unwind correctly. bin_break (Src/builtin.c:5840)
15109 // checks `(interactive && shinstdin) || locallevel || sourcelevel`
15110 // — without the bump, `return N` falls through to `zexit(num,
15111 // ZEXIT_NORMAL)` (c:5858) and kills the entire shell instead of
15112 // unwinding to the source caller. Also clear RETFLAG after the
15113 // sourced script returns so the unwind doesn't propagate to the
15114 // outer compile unit.
15115 // c:Src/init.c:1557-1558 — `char *old_scriptname = scriptname;
15116 // char *old_scriptfilename = scriptfilename;`
15117 // c:1591-1592 — `scriptname = s; scriptfilename = s;`
15118 // For the duration of source execution, both scriptname and
15119 // scriptfilename point to the sourced file path. zerr/zwarnnam
15120 // diagnostics emitted from inside the sourced file then use this
15121 // path as the prefix (`/path/to/foo:N: ...` instead of `zsh:1:`).
15122 // Bug #139 in docs/BUGS.md. The `path` here is the resolved
15123 // absolute path; C uses the user-supplied `s` (the bin_dot
15124 // argv[0]) but `arg0` (the user-supplied name) is what zsh
15125 // actually shows in diagnostics, so use arg0.
15126 // c:Src/init.c:1566 — `source()` tries the compiled form FIRST:
15127 // `!(prog = try_source_file((us = unmeta(s)))) && (tempfd =
15128 // open(us, ...)) == -1` — a sibling `<file>.zwc` newer than the
15129 // file (or `s` itself being a `.zwc`) short-circuits the plain
15130 // read. zshrs executes through the fusevm text pipeline, so the
15131 // dump's wordcode bridges via getpermtext (same bridge as the
15132 // `.zwc` autoload path in exec.rs::loadautofn).
15133 // It runs BEFORE `scriptname = s` (c:1591), so a load diagnostic
15134 // such as parse.c:3746 check_dump_file's "invalid description" is
15135 // prefixed with the CALLER's scriptname (`zsh:21:`), not the file's.
15136 let zwc_src = crate::ported::parse::try_source_file(&path)
15137 .map(|prog| crate::ported::text::getpermtext(Box::new(prog), None, 0));
15138 let old_scriptname = crate::ported::utils::scriptname_get(); // c:1557
15139 let old_scriptfilename = crate::ported::utils::scriptfilename_get(); // c:1558
15140 crate::ported::utils::set_scriptname(Some(arg0.clone())); // c:1591
15141 // c:1592 — `scriptfilename = s;` — PS4 `%x` reads this; without
15142 // the store the bottom of an `xtrace` line stack stays "zsh"
15143 // (the executor seed) even inside the sourced file. Bug shared
15144 // with source_from_memory in bins/zshrs.rs.
15145 crate::ported::utils::set_scriptfilename(Some(arg0.clone())); // c:1592
15146
15147 // c:Src/init.c:1609 — `oloops = loops;` — and c:1622 `loops = 0;`.
15148 // `source` is a full loop-scope FIREWALL: the sourced file starts
15149 // with a zero nesting count no matter how deep the caller's loops
15150 // are, and the caller's count is put back at c:1694 below.
15151 //
15152 // Both halves are observable:
15153 // * `loops = 0` on entry — `for i in 1 2; do source f; done` where
15154 // `f` does a top-level `return` must still run BOTH iterations.
15155 // `bin_break`'s BIN_RETURN arm sets `breaks = loops` (c:5835),
15156 // so without the zeroing the return would set `breaks = 1` and
15157 // the caller's `for` would drain it and stop after iteration 1.
15158 // It is also why a bare `break` at a sourced file's top level
15159 // errors with "not in while, until, select, or repeat loop"
15160 // even when the `source` itself sits inside a loop.
15161 // * `loops = oloops` on exit — the caller's own `break` keeps
15162 // working after the source returns.
15163 let oloops = LOOPS.load(Relaxed); // c:1609
15164 LOOPS.store(0, Relaxed); // c:1622
15165
15166 // c:Src/init.c:1627-1630 — `if (isset(SOURCETRACE)) { printprompt4();
15167 // fprintf(xtrerr ? xtrerr : stderr, "<sourcetrace>\n"); }`
15168 if isset(crate::ported::zsh_h::SOURCETRACE) {
15169 // c:1621 — `lineno = 1;` precedes the trace, so PS4 `%i` reads 1.
15170 // The file's own statements set it again as they run; put the
15171 // caller's value back for the frame push below.
15172 let oldlineno = crate::ported::params::getsparam("LINENO")
15173 .and_then(|s| s.parse::<i64>().ok())
15174 .unwrap_or(0);
15175 crate::fusevm_bridge::set_lineno_impl(1); // c:1621
15176 crate::ported::utils::printprompt4(); // c:1628
15177 crate::fusevm_bridge::xtrerr_fputs("<sourcetrace>\n"); // c:1629
15178 crate::fusevm_bridge::xtrerr_flush();
15179 crate::fusevm_bridge::set_lineno_impl(oldlineno);
15180 }
15181
15182 crate::ported::init::sourcelevel.fetch_add(1, Relaxed); // c:1606
15183
15184 // c:Src/init.c:1608-1616 — push a funcstack frame with
15185 // `tp = FS_SOURCE` so prompt %x / xtrace / `$funcfiletrace`
15186 // resolve to the sourced file, not the calling function's
15187 // defining file. Without this push, sourcing a script from
15188 // inside a function left the funcstack TOP as the function's
15189 // own FS_FUNC frame; the prompt %x handler then reported the
15190 // function's `filename` (where the function was DEFINED) as
15191 // the current source file. Repro: powerlevel10k.zsh-theme's
15192 // `__p9k_root_dir=${${(%):-%x}:A:h}` resolved to BIN_DIR
15193 // (zinit.zsh's dir) instead of the theme's plugin dir, so the
15194 // theme then sourced `$BIN_DIR/internal/p10k.zsh` (no such
15195 // file) instead of `$THEME_DIR/internal/p10k.zsh`.
15196 // c:Src/init.c:1612 — `fstack.lineno = oldlineno;` where `oldlineno`
15197 // was captured from the ONE `lineno` global (c:1608). zshrs mirrors
15198 // that global in several places; the counter `BUILTIN_SET_LINENO`
15199 // actually drives per statement is `lex::LEX_LINENO` — the same one
15200 // `init::source` (init.rs:1614) and `doshfunc` (exec.rs:6938) read for
15201 // their frames. Reading `input::lineno` here instead picked up a stale
15202 // mirror, so `$functrace` inside a sourced file reported the caller as
15203 // `file:1` instead of the line the `.` was on.
15204 let prev_funcstack_lineno = crate::ported::lex::lineno() as i64;
15205 let pushed_frame = {
15206 let mut stack = crate::ported::modules::parameter::FUNCSTACK
15207 .lock()
15208 .unwrap_or_else(|e| {
15209 crate::ported::modules::parameter::FUNCSTACK.clear_poison();
15210 e.into_inner()
15211 });
15212 let prev_caller = stack
15213 .last()
15214 .map(|fs| fs.name.clone())
15215 .or_else(|| old_scriptfilename.clone())
15216 .or_else(|| Some("zsh".to_string()));
15217 let frame = crate::ported::zsh_h::funcstack {
15218 prev: None,
15219 name: arg0.clone(), // c:1608 fstack.name = scriptfilename
15220 filename: Some(arg0.clone()), // c:1613 fstack.filename = scriptfilename
15221 caller: prev_caller, // c:1609
15222 flineno: 0, // c:1611
15223 lineno: prev_funcstack_lineno, // c:1612
15224 tp: crate::ported::zsh_h::FS_SOURCE, // c:1615
15225 };
15226 stack.push(frame);
15227 true
15228 };
15229
15230 // c:Src/init.c:1620 — `errflag &= ~ERRFLAG_ERROR;` before
15231 // executing the sourced body, so a stale flag from the caller's
15232 // context can't abort the file's first list.
15233 crate::ported::utils::errflag.fetch_and(!crate::ported::zsh_h::ERRFLAG_ERROR, Relaxed);
15234 // Recursion backstop — c:Src/jobs.c:1878-1884. zsh runs the sourced
15235 // list through execpline, whose per-pipeline `initjob()` caps recursion
15236 // at MAX_MAXJOBS: `zerr("job table full or recursion limit exceeded")`
15237 // then bails. The fusevm pipeline path (execute_script below) doesn't
15238 // allocate a job per pipeline, so without this guard runaway
15239 // `. self`-style recursion — invisible to FUNCNEST, which counts only
15240 // FS_FUNC frames (doshfunc, exec.rs:5684) — grew the FS_SOURCE stack
15241 // unbounded and overflowed the 256 MB main-thread stack → uncatchable
15242 // SIGBUS. Total FUNCSTACK depth (incl. the FS_SOURCE frame just pushed)
15243 // is the proxy for zsh's concurrently-held job slots; at/over the
15244 // ceiling, raise the zsh-identical error and refuse the deeper body
15245 // (SOURCE_ERROR → 126, as zsh's bail returns). The frame is popped below.
15246 let over_limit = crate::ported::modules::parameter::FUNCSTACK
15247 .lock()
15248 .map(|s| s.len())
15249 .unwrap_or(0)
15250 >= crate::ported::jobs::MAX_MAXJOBS;
15251 // c:Src/init.c:220 — `execode(prog, 0, 0, toplevel ? "toplevel" : "file");`
15252 // A SOURCED file is the `"file"` arm, so its body runs with `file`
15253 // appended to `zsh_eval_context`: `toplevel:file` at top level,
15254 // `toplevel:shfunc:file` inside a function, `toplevel:eval:file` inside an
15255 // eval. zshrs pushed nothing here, so the component was missing at every
15256 // depth. funcstack was already correct (the sourced path pushes its own
15257 // FS_SOURCE frame just below) — only the eval-context entry was absent.
15258 // Popped on every return path by the guard. Bug #1067.
15259 let sync_eval_ctx = |stack: &[String]| {
15260 crate::ported::exec::EvalContextFrame::sync(stack);
15261 };
15262 if let Ok(mut ctx) = crate::ported::exec::zsh_eval_context.lock() {
15263 ctx.push("file".to_string());
15264 sync_eval_ctx(&ctx);
15265 }
15266 struct SourceCtxGuard<F: Fn(&[String])>(F);
15267 impl<F: Fn(&[String])> Drop for SourceCtxGuard<F> {
15268 fn drop(&mut self) {
15269 if let Ok(mut ctx) = crate::ported::exec::zsh_eval_context.lock() {
15270 ctx.pop();
15271 (self.0)(&ctx);
15272 }
15273 }
15274 }
15275 let _source_ctx_guard = SourceCtxGuard(sync_eval_ctx);
15276 let mut result = if over_limit {
15277 crate::ported::utils::zerr("job table full or recursion limit exceeded");
15278 128 - 2 // c:6143 — SOURCE_ERROR = 2 → 126
15279 } else {
15280 // c:Src/init.c:1618-1641 — source() has TWO arms and they are not
15281 // interchangeable:
15282 // * `if (prog) { … execode(prog, 1, 0, "filecode"); … }` (c:1618-1622)
15283 // — the file was ALREADY COMPILED (`try_source_file` found a
15284 // `.zwc` the user made with `zcompile`), so there is nothing left
15285 // to lex and the whole program runs at once.
15286 // * `else { /* loop through the file to be sourced */ switch
15287 // (loop(0, 0)) … }` (c:1625-1641) — a plain text file is parsed
15288 // and executed ONE EVENT AT A TIME.
15289 // The second arm is load-bearing: `loop()` re-enters the lexer after
15290 // every command, so an `alias` (or `setopt rcquotes`, c:Src/lex.c:1326)
15291 // that a line INSTALLS is in force when the NEXT line is lexed.
15292 // Compiling the whole file first — which is what `execute_script`
15293 // does — lexes every line with the state the file started with, so
15294 // `alias greet=…` on line 1 followed by `greet` on line 2 reported
15295 // `command not found`. Bug #1087.
15296 match zwc_src {
15297 // c:1621 prog path — `execode(prog, 1, 0, "filecode")`. The
15298 // deparse is re-lexed, so it goes through the zwc entry point
15299 // that pins the lexer to the spelling `untokenize` produced.
15300 Some(src) => crate::fusevm_bridge::execute_zwc_program(&src).unwrap_or(1),
15301 // c:1626 — the sourced file is read as RAW BYTES; a non-UTF-8
15302 // byte in it is metafied (Src/utils.c:4856), never an error.
15303 // `read_to_string` failed the whole read instead, and this arm
15304 // turned that into a SILENT `return 126` — a completion file
15305 // with one legacy byte sourced to nothing at all.
15306 None => match crate::script_bytes::read_script_file(&path) {
15307 // c:1626-1627 — `switch (loop(0, 0))`
15308 Ok(src) => crate::fusevm_bridge::source_file_per_command(&src).unwrap_or(1),
15309 // c:6143 — SOURCE_ERROR = 2 (Src/zsh.h:2216) → 128 - 2 = 126.
15310 Err(_) => 128 - 2,
15311 },
15312 }
15313 };
15314 // c:Src/init.c:1623-1624 — `if (errflag) ret = SOURCE_ERROR;`
15315 // c:Src/init.c:1663 — `errflag &= ~ERRFLAG_ERROR;` on the restore
15316 // path. source is a CONTAINMENT boundary: an errflag abort inside
15317 // the sourced file breaks ITS lists, then source() clears the
15318 // flag, bin_dot returns 128 - SOURCE_ERROR = 126 (c:Src/builtin.c
15319 // :6143), and the CALLER continues. zsh 5.9: parent script
15320 // sourcing a file that hits `readonly` reassign prints its next
15321 // line with $? = 126.
15322 if (crate::ported::utils::errflag.load(Relaxed) & crate::ported::zsh_h::ERRFLAG_ERROR) != 0 {
15323 crate::ported::utils::errflag.fetch_and(!crate::ported::zsh_h::ERRFLAG_ERROR, Relaxed);
15324 result = 128 - 2; // c:6143 — 128 - SOURCE_ERROR
15325 }
15326 if pushed_frame {
15327 // c:1643 — `funcstack = funcstack->prev;`
15328 let mut stack = crate::ported::modules::parameter::FUNCSTACK
15329 .lock()
15330 .unwrap_or_else(|e| {
15331 crate::ported::modules::parameter::FUNCSTACK.clear_poison();
15332 e.into_inner()
15333 });
15334 stack.pop();
15335 }
15336 crate::ported::init::sourcelevel.fetch_sub(1, Relaxed); // c:1644
15337 // c:1666 — `scriptname = old_scriptname;` and matching
15338 // scriptfilename restore from the c:1667 line.
15339 crate::ported::utils::set_scriptname(old_scriptname);
15340 crate::ported::utils::set_scriptfilename(old_scriptfilename);
15341 // c:Src/init.c:1694 — `loops = oloops;` — put the caller's loop
15342 // nesting count back (see the c:1609/1622 note at the entry half).
15343 LOOPS.store(oloops, Relaxed); // c:1694
15344
15345 // c:Src/init.c:1697-1698 — `if (!exit_pending) retflag = 0;`
15346 // RETFLAG is set by bin_break's BIN_RETURN arm (c:5834). Once the
15347 // sourced file's execute_script unwinds, the return has been
15348 // serviced; clear the flag so the outer compile unit's main loop
15349 // (init.rs:1252's `if retflag break` guard) doesn't see a stale
15350 // request and abort `echo done` after `source foo`.
15351 //
15352 // The `exit_pending` guard is C's, not an approximation: `exit`
15353 // inside a function sets retflag ALONG WITH exit_pending
15354 // (c:5874-5878) precisely so the unwind keeps propagating past
15355 // every enclosing scope until the deferred exit is honoured.
15356 // Clearing retflag unconditionally would strand that exit at the
15357 // `source` boundary.
15358 if EXIT_PENDING.load(Relaxed) == 0 {
15359 // c:1697
15360 RETFLAG.store(0, Relaxed); // c:1698
15361 }
15362
15363 // c:6149 again — restore argzero on the success path as well.
15364 if let Some(prev) = saved_argzero {
15365 set_argzero(prev);
15366 }
15367 // c:6125-6128 — restore pparams on the success path AFTER the
15368 // source body has finished executing. Bug #459.
15369 if let Some(saved) = saved_pparams {
15370 let mut pp = PPARAMS.lock().unwrap_or_else(|e| {
15371 PPARAMS.clear_poison();
15372 e.into_inner()
15373 });
15374 *pp = saved;
15375 }
15376 result
15377}
15378
15379/// Port of `eval()` from `Src/builtin.c:6151`.
15380/// C decl: `eval(char **argv)`
15381pub fn eval(argv: &[String]) -> i32 {
15382 // c:6151
15383 // c:6153 — `Eprog prog;` (declared inline below)
15384 // c:6154 — `char *oscriptname = scriptname;`
15385 let oscriptname: Option<String> = scriptname_get();
15386 // c:6155 — `int oineval = ineval, fpushed;`
15387 let oineval: i32 = INEVAL.load(Relaxed);
15388 let fpushed: bool;
15389 // c:6156 — `struct funcstack fstack;`
15390
15391 // c:6163 — `ineval = !isset(EVALLINENO);`
15392 INEVAL.store(
15393 if !isset(crate::ported::zsh_h::EVALLINENO) {
15394 1
15395 } else {
15396 0
15397 },
15398 Relaxed,
15399 );
15400 let ineval_now = INEVAL.load(Relaxed) != 0;
15401
15402 if !ineval_now {
15403 // c:6164
15404 // c:6165 — `scriptname = "(eval)";`
15405 crate::ported::utils::set_scriptname(Some("(eval)".to_string()));
15406 // c:6166-6196 — funcstack push: build a fstack frame describing
15407 // this eval, link it to the head of FUNCSTACK.
15408 let prev_frame = {
15409 let stack = FUNCSTACK.lock().unwrap_or_else(|e| e.into_inner());
15410 stack.last().cloned()
15411 };
15412 let lineno_now = crate::ported::input::lineno.with(|c| c.get()) as i64;
15413 let caller = match &prev_frame {
15414 Some(p) => Some(p.name.clone()),
15415 None => argzero(), // c:6168 dupstring(argzero)
15416 };
15417
15418 // c:6182-6196 — flineno/filename derivation. Three cases:
15419 // 1. no prev frame OR prev tp == FS_SOURCE: flineno=lineno,
15420 // filename=caller (the source name)
15421 // 2. prev tp == FS_EVAL: flineno = prev.flineno + lineno - 1
15422 // 3. otherwise (function): flineno = prev.flineno + lineno,
15423 // filename = prev.filename or ""
15424 let (flineno, filename): (i64, Option<String>) = match &prev_frame {
15425 None => (lineno_now, caller.clone()), // c:6183-6184
15426 Some(p) if p.tp == crate::ported::zsh_h::FS_SOURCE => {
15427 (lineno_now, caller.clone()) // c:6183-6184
15428 }
15429 Some(p) => {
15430 let mut fl = p.flineno + lineno_now; // c:6186
15431 if p.tp == crate::ported::zsh_h::FS_EVAL {
15432 // c:6191
15433 fl -= 1; // c:6192
15434 }
15435 let fname = p.filename.clone().or_else(|| Some(String::new())); // c:6193-6195
15436 (fl, fname)
15437 }
15438 };
15439 let frame = crate::ported::zsh_h::funcstack {
15440 prev: None, // c:1349 — linked via FUNCSTACK vec
15441 name: "(eval)".to_string(), // c:6167
15442 filename, // c:6184 / c:6193
15443 caller, // c:6168
15444 flineno, // c:6183 / c:6186
15445 lineno: lineno_now, // c:6169
15446 tp: crate::ported::zsh_h::FS_EVAL, // c:6170
15447 };
15448 {
15449 let mut stack = FUNCSTACK.lock().unwrap_or_else(|e| e.into_inner());
15450 stack.push(frame); // c:6197 funcstack = &fstack
15451 }
15452 fpushed = true; // c:6199
15453 } else {
15454 fpushed = false; // c:6201
15455 }
15456
15457 // c:6203 — `prog = parse_string(zjoin(argv, ' ', 1), 1);`
15458 let joined = crate::ported::utils::zjoin(argv, ' ');
15459 let prog = crate::ported::exec::parse_string(&joined, 1);
15460
15461 if let Some(prog) = prog {
15462 // c:6205 — `if (wc_code(*prog->prog) != WC_LIST)`
15463 let head = prog.prog.first().copied().unwrap_or(0);
15464 if crate::ported::zsh_h::wc_code(head) != crate::ported::zsh_h::WC_LIST as u32 {
15465 /* No code to execute */
15466 // c:6206
15467 LASTVAL.store(0, Relaxed); // c:6207
15468 } else {
15469 // c:6209 — `execode(prog, 1, 0, "eval");`. Routes through
15470 // the executor; in-process equivalent.
15471 //
15472 // PREVIOUSLY called run_command_substitution which captures
15473 // stdout into a String and returns it — bin_eval threw the
15474 // capture away, so `eval 'echo hi'` produced no output.
15475 // execute_script_zsh_pipeline runs the script with stdout
15476 // flowing to the caller (no capture) which is what eval
15477 // wants. Same routing the eval-via-execstring path uses
15478 // (vm_helper.rs:1518 EXIT-trap fire).
15479 //
15480 // Recursion backstop — c:Src/jobs.c:1878-1884. zsh's per-pipeline
15481 // `initjob()` caps eval recursion at MAX_MAXJOBS ("job table full
15482 // or recursion limit exceeded"); the fusevm pipeline path doesn't
15483 // allocate a job per pipeline, so runaway `eval`-string recursion
15484 // (invisible to FUNCNEST, which counts FS_FUNC frames only, AND to
15485 // the FS_EVAL frame count — nested evals suppress the funcstack
15486 // push via INEVAL, c:6164, exactly like zsh) is otherwise unbounded
15487 // → main-thread stack overflow → SIGBUS. Track eval re-entry depth
15488 // directly (the Rust proxy for zsh's concurrently-held job slots,
15489 // one per nested eval pipeline) and cap at the same MAX_MAXJOBS
15490 // ceiling. Requested as a parity fix by the maintainer.
15491 let eval_depth = crate::vm_helper::EVAL_RECURSION_DEPTH.with(|d| {
15492 let v = d.get() + 1;
15493 d.set(v);
15494 v
15495 });
15496 if eval_depth >= crate::ported::jobs::MAX_MAXJOBS {
15497 crate::ported::utils::zerr("job table full or recursion limit exceeded");
15498 LASTVAL.store(1, Relaxed);
15499 } else {
15500 // c:6209 — `execode(prog, 1, 0, "eval")`. The `"eval"`
15501 // frame is pushed by the live BUILTIN_EVAL path
15502 // (fusevm_bridge.rs, `EvalContextFrame::push("eval")`)
15503 // that `execute_script_zsh_pipeline` routes through;
15504 // pushing it here as well would double it.
15505 let _ = crate::ported::exec::execute_script_zsh_pipeline(&joined);
15506 }
15507 crate::vm_helper::EVAL_RECURSION_DEPTH.with(|d| d.set(d.get().saturating_sub(1)));
15508 // c:6211-6212 — `if (errflag && !lastval) lastval = errflag;`
15509 let ef = errflag.load(Relaxed);
15510 let lv = LASTVAL.load(Relaxed);
15511 if ef != 0 && lv == 0 {
15512 LASTVAL.store(ef, Relaxed);
15513 }
15514 }
15515 } else {
15516 LASTVAL.store(1, Relaxed); // c:6215
15517 }
15518
15519 if fpushed {
15520 // c:6218
15521 // c:6219 — `funcstack = funcstack->prev;`
15522 let mut stack = FUNCSTACK.lock().unwrap_or_else(|e| e.into_inner());
15523 stack.pop();
15524 }
15525
15526 // c:6221 — `errflag &= ~ERRFLAG_ERROR;`
15527 errflag.fetch_and(!ERRFLAG_ERROR, Relaxed);
15528 // The executor's NOMATCH cell is the Rust half of that error state
15529 // (set beside `zerr` for c:Src/glob.c:1877); clear it with the bit, or
15530 // an `x=( *nomatch )` in the body skips the caller's NEXT command.
15531 let _ = crate::fusevm_bridge::try_with_executor(|exec| exec.current_command_glob_failed.set(false));
15532 // c:6222 — `scriptname = oscriptname;`
15533 crate::ported::utils::set_scriptname(oscriptname);
15534 // c:6223 — `ineval = oineval;`
15535 INEVAL.store(oineval, Relaxed);
15536
15537 LASTVAL.load(Relaxed) // c:6225
15538}
15539
15540/// Port of `bin_emulate()` from `Src/builtin.c:6232`.
15541/// C decl: `bin_emulate(char *nam, char **argv, Options ops, UNUSED(int func))`
15542/// C: `int bin_emulate(char *nam, char **argv, Options ops, ...)` —
15543/// no-args print current emulation; single-arg switch emulation;
15544/// `-l` list, `-L` set LOCAL*, `-R` reset to defaults.
15545/// WARNING: param names don't match C — Rust=(nam, argv, _func) vs C=(nam, argv, ops, func)
15546pub fn bin_emulate(
15547 nam: &str,
15548 argv: &[String], // c:6232
15549 ops: &options,
15550 _func: i32,
15551) -> i32 {
15552 let opt_l = OPT_ISSET(ops, b'l'); // c:6236
15553 let opt_l_arg = OPT_ISSET(ops, b'L'); // c:6234
15554 let opt_r = OPT_ISSET(ops, b'R'); // c:6235
15555
15556 // c:6249-6275 — no args: print current emulation name.
15557 if argv.is_empty() {
15558 // c:6249
15559 if opt_l_arg || opt_r {
15560 // c:6250
15561 zwarnnam(nam, "not enough arguments"); // c:6251
15562 return 1; // c:6252
15563 }
15564 // c:6255-6271 — `switch(SHELL_EMULATION())` → name dispatch.
15565 let bits = emulation.load(Relaxed) as i32;
15566 let shname = if (bits & EMULATE_CSH) != 0 {
15567 "csh"
15568 }
15569 // c:6255
15570 else if (bits & EMULATE_KSH) != 0 {
15571 "ksh"
15572 }
15573 // c:6259
15574 else if (bits & EMULATE_SH) != 0 {
15575 "sh"
15576 }
15577 // c:6263
15578 else {
15579 "zsh"
15580 }; // c:6268
15581 println!("{}", shname); // c:6273
15582 return 0; // c:6274
15583 }
15584
15585 // c:6278-6295 — single-arg form: `emulate <shname>`.
15586 let shname = &argv[0];
15587 if argv.len() == 1 {
15588 // c:6278
15589 // c:6280-6285 — `if (opt_l) cmdopts = zhalloc(...); else cmdopts = opts;`
15590 // In our static-link port, the live option table IS the
15591 // "real opts"; under -l we build a snapshot HashMap and
15592 // mutate THAT instead of touching global state. Under
15593 // !-l we apply emulate semantics to the live table.
15594 // c:537-549 — C `emulate(zsh_name, ...)` reads ONLY the first
15595 // char (after stripping a leading `r` for rcsh/rksh): 'c'
15596 // → CSH, 'k' → KSH, 's'/'b' → SH (so `bash` aliases to sh),
15597 // else ZSH. Previous Rust port did full-string equality so
15598 // `emulate rcsh` / `emulate bash` silently fell back to ZSH.
15599 let bytes = shname.as_bytes();
15600 let mut ch = if !bytes.is_empty() { bytes[0] } else { 0 };
15601 if ch == b'r' && bytes.len() >= 2 {
15602 // c:539
15603 ch = bytes[1]; // c:540
15604 }
15605 let bits = match ch {
15606 // c:543
15607 b'c' => EMULATE_CSH, // c:544
15608 b'k' => EMULATE_KSH, // c:546
15609 b's' | b'b' => EMULATE_SH, // c:548
15610 _ => EMULATE_ZSH, // c:550
15611 };
15612 // c:6286 — `emulate(shname, opt_R, &emulation, cmdopts)`.
15613 emulation.store(bits, Relaxed);
15614
15615 // c:6270-6276 — `if (opt_l) { cmdopts = zhalloc(OPT_SIZE);
15616 // memcpy(cmdopts, opts, OPT_SIZE); } else cmdopts = opts;`. Only -l
15617 // works on a COPY; every other form applies the emulation to the live
15618 // `opts[]` and never reads a copy back. The copy below (two walks of
15619 // every option, one before and one after emulate) is therefore built
15620 // only for -l. Building it unconditionally made each `emulate -L zsh`
15621 // — the first line of compdef, run once per `#compdef` file by
15622 // compinit — pay two full option-table walks for nothing.
15623 if !opt_l {
15624 crate::ported::options::emulate(shname.as_str(), opt_r); // c:6277
15625 if opt_l_arg {
15626 // c:6278-6280 — `cmdopts[LOCALOPTIONS] = cmdopts[LOCALTRAPS] =
15627 // cmdopts[LOCALPATTERNS] = 1;` on the live array.
15628 for nm in ["localoptions", "localtraps", "localpatterns"] {
15629 crate::ported::options::opt_state_set(nm, true);
15630 }
15631 }
15632 crate::ported::pattern::clearpatterndisables(); // c:6285
15633 return 0; // c:6286
15634 }
15635
15636 // Build the cmdopts view that c:6286-6292 manipulates (-l only).
15637 let mut cmdopts: HashMap<String, bool> = HashMap::new();
15638 for n in ZSH_OPTIONS_SET.iter() {
15639 cmdopts.insert(
15640 n.to_string(),
15641 crate::ported::options::opt_state_get(n).unwrap_or(false),
15642 );
15643 }
15644 // c:6286 — `emulate(shname, opt_R, &emulation, cmdopts)` ALSO
15645 // populates `cmdopts` (when !opt_l, `cmdopts` aliases the live
15646 // `opts[]`). The Rust port has a per-emulation defaults walker
15647 // at options::emulate() that writes through to OPTS_LIVE — call
15648 // it here so `emulate -L sh` flips KSH_ARRAYS / SH_NULLCMD /
15649 // POSIX_ALIASES / etc. to their target-emulation defaults.
15650 //
15651 // Bug #26 in docs/BUGS.md — without this, `emulate -L sh`
15652 // changed the emulation tag but left every option at its zsh
15653 // default, so `a=(1 2 3); echo ${a[0]}` stayed 1-indexed
15654 // (empty result) instead of switching to KSH_ARRAYS 0-indexed.
15655 // c:6285 — `emulate(shname, opt_R, &emulation, cmdopts);` runs
15656 // UNCONDITIONALLY. C can do that because it passes the target array in:
15657 // under -l cmdopts is a COPY of opts (c:6281-6282) and the emulation is
15658 // applied to the copy; otherwise cmdopts aliases the live opts[].
15659 //
15660 // options::emulate() has no such parameter — it writes through to
15661 // OPTS_LIVE — so the -l case is expressed by applying it to the live
15662 // state, snapshotting the result, and restoring. Same effect as C's
15663 // copy: `emulate -l sh` reports what the options WOULD be, and the
15664 // shell is left untouched.
15665 //
15666 // Skipping the call under -l (as this did) meant cmdopts stayed the
15667 // CURRENT options, so `emulate -l sh` listed zsh's defaults, not sh's —
15668 // ~38 options wrong. It looked right only for `emulate -l zsh`, where
15669 // the current options ARE the answer.
15670 let saved_opts = if opt_l {
15671 Some(crate::ported::options::opt_state_snapshot())
15672 } else {
15673 None
15674 };
15675 crate::ported::options::emulate(shname.as_str(), opt_r); // c:6285
15676 // Re-sync cmdopts from OPTS_LIVE so it reflects the emulation's
15677 // defaults rather than the pre-emulate state captured above.
15678 for n in ZSH_OPTIONS_SET.iter() {
15679 cmdopts.insert(
15680 n.to_string(),
15681 crate::ported::options::opt_state_get(n).unwrap_or(false),
15682 );
15683 }
15684 if let Some(saved) = saved_opts {
15685 // Undo the live apply — C never touched opts[] in the -l path.
15686 crate::ported::options::opt_state_restore(saved);
15687 }
15688
15689 // c:6287-6289 — opt_L: set LOCALOPTIONS/LOCALTRAPS/LOCALPATTERNS=1
15690 // in cmdopts. In the !opt_l live-apply case we also set them in
15691 // OPTS_LIVE; in the opt_l snapshot case we only set them in
15692 // cmdopts (the snapshot the list call walks).
15693 if opt_l_arg {
15694 // c:6287
15695 for nm in ["localoptions", "localtraps", "localpatterns"] {
15696 cmdopts.insert(nm.to_string(), true);
15697 if !opt_l {
15698 crate::ported::options::opt_state_set(nm, true);
15699 }
15700 }
15701 }
15702 if opt_l {
15703 // c:6290
15704 // c:6291 — `list_emulate_options(cmdopts, opt_R);`
15705 crate::ported::options::list_emulate_options(&cmdopts, opt_r);
15706 return 0; // c:6292
15707 }
15708 // c:6294 — `clearpatterndisables();` resets the per-pattern
15709 // disabled-feature bitset that a previous emulation may have
15710 // left in place.
15711 crate::ported::pattern::clearpatterndisables();
15712 return 0; // c:6295
15713 }
15714
15715 // c:6297-6300 — too many args under -l.
15716 if opt_l {
15717 // c:6297
15718 zwarnnam(nam, "too many arguments for -l"); // c:6298
15719 return 1; // c:6299
15720 }
15721
15722 // c:6302-6342 — `emulate <shname> [-o OPT|+o OPT|-LR ...]` per-command
15723 // form. C: `argv++; emulate(shname, opt_R, ...); parseopts(...);` —
15724 // switch emulation, then parse the remaining tokens as long-form
15725 // options that apply ON TOP of the emulation defaults. zinit's
15726 // +zi-log (zinit.zsh:2178) calls
15727 // `emulate -LR zsh -o extendedglob`
15728 // which requires extendedglob to be set after the zsh-emulation
15729 // reset, so the (#b)/(#m) flags in subsequent pattern subst calls
15730 // are recognized. The previous Rust port returned 0 without
15731 // applying ANY of the trailing options, so extendedglob stayed off
15732 // and `${msg//(#b)…/…}` errored with `bad pattern: …`.
15733 //
15734 let _ = opt_r;
15735 // c:6303 — `memcpy(saveopts, opts, sizeof(opts));` + c:6326
15736 // `saveemulation = emulation;` — snapshot the live option table
15737 // and both emulation cells BEFORE applying the new emulation.
15738 // C builds the new state in a LOCAL `new_opts` and only commits
15739 // it at c:6328; the Rust port writes the live table directly, so
15740 // this snapshot is the rollback for the `-c` eval (restore2 at
15741 // c:6377-6379) and the parse-error path (goto restore, c:6311).
15742 // Without the restore, `emulate sh -c 'f() { ...; }'` left the
15743 // shell PERMANENTLY in sh-emulation option state — a later
15744 // `bad substitution` zerr then aborted the whole non-interactive
15745 // shell (D04parameter chunk 159 poisoned chunk 225 into a
15746 // shell-exit).
15747 let saveopts = crate::ported::options::opt_state_snapshot(); // c:6303
15748 let saveemulation = emulation.load(Relaxed); // c:6326
15749 let saveemulation_live = crate::ported::options::EMULATION.load(Relaxed); // c:6326 (port keeps 2 cells)
15750 let savehackchar = *crate::ported::params::keyboardhack_lock().lock().unwrap(); // c:6305
15751 // c:6381-6382 `restore:` — `keyboardhackchar = savehackchar; inittyptab();`
15752 let restore_hackchar = || {
15753 *crate::ported::params::keyboardhack_lock().lock().unwrap() = savehackchar; // c:6381
15754 crate::ported::utils::inittyptab(); // c:6382 restore banghist
15755 };
15756 // c:6306 — `emulate(shname, opt_R, &new_emulation, new_opts);` —
15757 // apply emulation defaults to the live opts table.
15758 crate::ported::options::emulate(shname.as_str(), opt_r);
15759
15760 // c:6308 — `parseopts(nam, &argv, new_opts, &cmd, optlist, 0)`.
15761 // C body at Src/init.c:390+:
15762 // while (!optionbreak && *argv && (**argv == '-' || **argv == '+')) {
15763 // action = (**argv == '-');
15764 // while (*++*argv) {
15765 // if (**argv == '-') { /* `--` ends options; `--NAME` long form */ }
15766 // else if (**argv == 'c') { *cmdp = *argv; }
15767 // else if (**argv == 'o') {
15768 // if (!*++*argv) argv++; // `-o NAME` separate-arg
15769 // if (!*argv) { WARN("string expected after -o"); return 1; }
15770 // longoptions: optno = optlookup(*argv);
15771 // if (!optno) { WARN("no such option: %s", *argv); return 1; }
15772 // dosetopt(optno, action, ...);
15773 // break;
15774 // }
15775 // else { /* single-char option letter, e.g. 'L', 'R' */
15776 // optno = optlookupc(**argv);
15777 // dosetopt(optno, action, ...);
15778 // }
15779 // }
15780 // argv++;
15781 // }
15782 //
15783 // Faithful port: walk each arg, parse `-`/`+` action sign, then
15784 // walk char-by-char. `--` ends options; `-o NAME` / `+o NAME`
15785 // long-form (NAME inline-attached or separate arg); single-char
15786 // option letters set/unset via `dosetopt` (Rust:
15787 // `opt_state_set`). `-c command` records the cmd-eval mode (not
15788 // wired through here — bin_emulate uses opt_L for localoptions
15789 // only).
15790 let mut i = 1; // skip shname (argv[0])
15791 let mut optionbreak = false;
15792 let mut cmd_body: Option<String> = None; // c:6310 — `-c command` capture.
15793 // c:6308 `optlist` — parseopts records each explicitly-set option
15794 // (as an index into new_opts) so the `-c` arm can build the
15795 // sticky struct's on_opts/off_opts at c:6347-6373. Rust tracks
15796 // (canonical-name, on?) pairs.
15797 let mut optlist: Vec<(String, bool)> = Vec::new();
15798 // c:6309-6311 — `if (parseopts(...)) { ret = 1; goto restore; }`. C's
15799 // parseopts wrote only the local new_opts; this port writes the live
15800 // table, so the failure path rolls the snapshot back.
15801 let parse_fail = || -> i32 {
15802 emulation.store(saveemulation, Relaxed);
15803 crate::ported::options::EMULATION.store(saveemulation_live, Relaxed);
15804 crate::ported::options::opt_state_restore(saveopts.clone());
15805 restore_hackchar();
15806 1
15807 };
15808 // !!! RUST-ONLY ADAPTER !!! — C's `optletters` (c:Src/options.c:287)
15809 // tests SHOPTIONLETTERS in the LIVE opts, which parseopts never writes
15810 // (it fills new_opts); this port has already applied the emulation to
15811 // the live table, so letters are looked up with the value it had
15812 // before: `emulate sh -L` resolves `L` through zsh's letters, not sh's.
15813 let pre_sol = saveopts.get("shoptionletters").copied().unwrap_or(false);
15814 let optlookupc_pre = |c: char| -> i32 {
15815 let now = crate::ported::zsh_h::isset(crate::ported::zsh_h::SHOPTIONLETTERS);
15816 crate::ported::options::opt_state_set("shoptionletters", pre_sol);
15817 let r = crate::ported::options::optlookupc(c);
15818 crate::ported::options::opt_state_set("shoptionletters", now);
15819 r
15820 };
15821 while !optionbreak && i < argv.len() {
15822 let arg = &argv[i];
15823 // c:Src/init.c:418 — only `-` / `+` start an option arg.
15824 let first = arg.chars().next().unwrap_or('\0');
15825 if first != '-' && first != '+' {
15826 break;
15827 }
15828 let action = first == '-'; // c:420
15829 // c:421-422 — bare `-` / `+` treated as `--` (end-of-options
15830 // marker preserved as-is for caller).
15831 if arg.len() == 1 {
15832 i += 1;
15833 break;
15834 }
15835 // Walk chars after the leading +/-.
15836 let bytes: Vec<char> = arg.chars().collect();
15837 let mut j = 1;
15838 let mut consumed_next_arg = false;
15839 while j < bytes.len() {
15840 let ch = bytes[j];
15841 if ch == '-' {
15842 // c:Src/init.c:424-429 — "The pseudo-option `--' signifies
15843 // the end of options."
15844 if j == 1 && bytes.len() == 2 {
15845 optionbreak = true; // the `i += 1` after this walk steps past it
15846 break;
15847 }
15848 // c:Src/init.c:430-431 — `if (!toplevel || …) goto
15849 // badoptionstring;`: GNU-style long options belong to the
15850 // shell's own command line only.
15851 zwarnnam(nam, &format!("bad option string: '{}'", arg)); // c:520
15852 return parse_fail(); // c:521 `return 1` → c:6311 `goto restore`
15853 }
15854 if ch == 'b' && !pre_sol {
15855 // c:Src/init.c:480-486 — "-b ends options at the end of this
15856 // argument".
15857 optionbreak = true;
15858 j += 1;
15859 continue;
15860 }
15861 if ch == 'c' {
15862 // c:6310 — `-c command`. Capture the command body from
15863 // either the rest of the current arg (e.g. `-c'body'`)
15864 // or the next argv slot. C's sticky-emulation evaluator
15865 // (c:6332-6373) wraps the cmd in saved/restored
15866 // emulation state; we mirror by running the captured
15867 // body through eval after the option-parse loop exits.
15868 let body = if j + 1 < bytes.len() {
15869 bytes[j + 1..].iter().collect::<String>()
15870 } else {
15871 if i + 1 >= argv.len() {
15872 zwarnnam(nam, "string expected after -c");
15873 return parse_fail();
15874 }
15875 consumed_next_arg = true;
15876 argv[i + 1].clone()
15877 };
15878 cmd_body = Some(body);
15879 break;
15880 }
15881 if ch == 'o' {
15882 // c:Src/init.c:496-501 — `-o NAME` / `+o NAME`: NAME is the
15883 // rest of this word, else the next word.
15884 let name = if j + 1 < bytes.len() {
15885 bytes[j + 1..].iter().collect::<String>()
15886 } else {
15887 if i + 1 >= argv.len() {
15888 zwarnnam(nam, "string expected after -o"); // c:499
15889 return parse_fail(); // c:500
15890 }
15891 consumed_next_arg = true;
15892 argv[i + 1].clone()
15893 };
15894 // c:506-518. optlookup resolves the `no`/`no_` negation
15895 // prefix (a NEGATIVE optno) and dosetopt inverts it and runs
15896 // the option's side effects.
15897 let optno = crate::ported::options::optlookup(&name);
15898 if optno == 0 {
15899 zwarnnam(nam, &format!("no such option: {}", name)); // c:507
15900 return parse_fail(); // c:508
15901 } else if optno == crate::ported::zsh_h::EMACSMODE || optno == crate::ported::zsh_h::VIMODE {
15902 zwarnnam(nam, &format!("can't change option: {}", name)); // c:510
15903 } else if crate::ported::options::dosetopt(optno, action as i32, 0) != 0 {
15904 zwarnnam(nam, &format!("can't change option: {}", name)); // c:514
15905 } else {
15906 // c:515-516 — parseopts_insert(optlist, new_opts, optno)
15907 let canon = crate::ported::zsh_h::opt_name(optno.abs()).to_string();
15908 let effective_on = if optno < 0 { !action } else { action };
15909 optlist.push((canon, effective_on));
15910 }
15911 break; // c:519
15912 }
15913 if ch.is_ascii_whitespace() {
15914 // c:Src/init.c:520-527 — trailing blanks are allowed, any
15915 // other character after one is a bad option string.
15916 if bytes[j + 1..].iter().any(|c| !c.is_ascii_whitespace()) {
15917 zwarnnam(nam, &format!("bad option string: '{}'", arg)); // c:525
15918 return parse_fail(); // c:526
15919 }
15920 break; // c:528
15921 }
15922 // c:Src/init.c:529-545 — a single option letter.
15923 let optno = optlookupc_pre(ch);
15924 if optno == 0 {
15925 zwarnnam(nam, &format!("bad option: -{}", ch)); // c:535
15926 return parse_fail(); // c:536
15927 } else if optno == crate::ported::zsh_h::EMACSMODE || optno == crate::ported::zsh_h::VIMODE {
15928 let rest: String = bytes[j..].iter().collect();
15929 zwarnnam(nam, &format!("can't change option: {}", rest)); // c:539
15930 } else if crate::ported::options::dosetopt(optno, action as i32, 0) != 0 {
15931 zwarnnam(nam, &format!("can't change option: -{}", ch)); // c:543
15932 } else {
15933 // c:544-545 — parseopts_insert(optlist, new_opts, optno)
15934 let canon = crate::ported::zsh_h::opt_name(optno.abs()).to_string();
15935 let effective_on = if optno < 0 { !action } else { action };
15936 optlist.push((canon, effective_on));
15937 }
15938 j += 1;
15939 }
15940 i += 1;
15941 if consumed_next_arg {
15942 i += 1;
15943 }
15944 }
15945 // c:6332-6377 — `-c command` evaluator: save patterns, install
15946 // the pending `sticky` struct (functions defined in the body get
15947 // stamped via shfunc_set_sticky at c:5402), eval the body, then
15948 // restore sticky + emulation + opts + patterns (restore2: at
15949 // c:6377-6379).
15950 if let Some(body) = cmd_body {
15951 if opt_l_arg {
15952 // c:6333-6336 — `-L` is incompatible with `-c`.
15953 zwarnnam(nam, "option -L incompatible with -c");
15954 // c:6336 `goto restore2` — undo the emulation switch.
15955 emulation.store(saveemulation, Relaxed);
15956 crate::ported::options::EMULATION.store(saveemulation_live, Relaxed);
15957 crate::ported::options::opt_state_restore(saveopts);
15958 restore_hackchar();
15959 return 1;
15960 }
15961 // c:6319 — `savepatterns = savepatterndisables();`
15962 let savepatterns = crate::ported::pattern::savepatterndisables();
15963 // c:6324 — `clearpatterndisables();` — every emulation starts
15964 // with an empty pattern-disable set.
15965 crate::ported::pattern::clearpatterndisables();
15966 // c:6344-6373 — `save_sticky = sticky; sticky = hcalloc(...);`
15967 // Build the sticky struct: target emulation + the on/off
15968 // option indices the optlist recorded.
15969 let save_sticky = crate::ported::options::sticky
15970 .lock()
15971 .unwrap_or_else(|e| e.into_inner())
15972 .take(); // c:6344
15973 {
15974 let mut on_opts: Vec<crate::ported::zsh_h::OptIndex> = Vec::new();
15975 let mut off_opts: Vec<crate::ported::zsh_h::OptIndex> = Vec::new();
15976 for (name, on) in &optlist {
15977 // c:6366-6372 — Data is index into new_opts.
15978 let optno = optlookup(name);
15979 if optno > 0 {
15980 if *on {
15981 on_opts.push(optno as crate::ported::zsh_h::OptIndex);
15982 } else {
15983 off_opts.push(optno as crate::ported::zsh_h::OptIndex);
15984 }
15985 }
15986 }
15987 *crate::ported::options::sticky
15988 .lock()
15989 .unwrap_or_else(|e| e.into_inner()) =
15990 Some(Box::new(crate::ported::zsh_h::emulation_options {
15991 // c:6346 — `sticky->emulation = emulation;`. C's
15992 // `emulation` global carries EMULATE_FULLY (set by
15993 // `emulate()` at Src/options.c:551 when `-R` is
15994 // given), and doshfunc feeds the whole value back
15995 // to `installemulation` (c:5993), which is what
15996 // makes `emulate -R sh -c 'f(){...}'` re-apply the
15997 // FULL emulation (banghist off) inside `f`. The
15998 // Rust port keeps that bit in the separate
15999 // FULLY_EMULATING cell, so OR it back in here —
16000 // otherwise the sticky snapshot silently degrades
16001 // `-R` to a plain `emulate sh`.
16002 emulation: crate::ported::options::EMULATION.load(Relaxed)
16003 | if crate::ported::options::FULLY_EMULATING.load(Relaxed) {
16004 crate::ported::zsh_h::EMULATE_FULLY
16005 } else {
16006 0
16007 }, // c:6346
16008 n_on_opts: on_opts.len() as i32, // c:6351
16009 n_off_opts: off_opts.len() as i32, // c:6353
16010 on_opts, // c:6356-6358
16011 off_opts, // c:6360-6362
16012 }));
16013 }
16014 let r = eval(&[body]); // c:6374 — `ret = eval(argv);`
16015 // c:6375 — `sticky = save_sticky;`
16016 *crate::ported::options::sticky
16017 .lock()
16018 .unwrap_or_else(|e| e.into_inner()) = save_sticky;
16019 // c:6377-6379 restore2: — emulation + opts + patterns.
16020 emulation.store(saveemulation, Relaxed); // c:6377
16021 crate::ported::options::EMULATION.store(saveemulation_live, Relaxed); // c:6377
16022 crate::ported::options::opt_state_restore(saveopts); // c:6378
16023 crate::ported::pattern::restorepatterndisables(savepatterns); // c:6379
16024 restore_hackchar(); // c:6381-6382 restore:
16025 return r;
16026 }
16027
16028 // c:6314-6317 — `if (*argv) zwarnnam(nam, "unknown argument %s",
16029 // *argv);`. Anything left after the option-parse loop is unknown.
16030 // zinit's `emulate -LR zsh -o extendedglob` exhausts argv, so this
16031 // arm doesn't fire — but mirror the C behavior for other callers.
16032 if i < argv.len() {
16033 zwarnnam(nam, &format!("unknown argument {}", argv[i])); // c:6314
16034 // c:6315 `goto restore` — C's LIVE opts were never touched on
16035 // this path (parseopts wrote the local new_opts); the Rust
16036 // loop wrote live state, so roll back the snapshot to match.
16037 emulation.store(saveemulation, Relaxed);
16038 crate::ported::options::EMULATION.store(saveemulation_live, Relaxed);
16039 crate::ported::options::opt_state_restore(saveopts);
16040 restore_hackchar();
16041 return 1;
16042 }
16043
16044 // c:6339-6340 — `if (opt_L) opts[LOCALOPTIONS] = opts[LOCALTRAPS] =
16045 // opts[LOCALPATTERNS] = 1;`.
16046 if opt_l_arg {
16047 for nm in ["localoptions", "localtraps", "localpatterns"] {
16048 crate::ported::options::opt_state_set(nm, true);
16049 }
16050 }
16051 0
16052}
16053
16054/// Port of `bin_eval()` from `Src/builtin.c:6393`.
16055/// C decl: `bin_eval(UNUSED(char *nam), char **argv, UNUSED(Options ops), UNUSED(int func))`
16056/// C: `int bin_eval(UNUSED args)` → `return eval(argv);`
16057/// WARNING: param names don't match C — Rust=(_name, argv, _func) vs C=(nam, argv, ops, func)
16058pub fn bin_eval(
16059 _name: &str,
16060 argv: &[String], // c:6393
16061 _ops: &options,
16062 _func: i32,
16063) -> i32 {
16064 // c:Src/builtin.c:407-411 — generic `--` end-of-options strip
16065 // applied by `execbuiltin` for builtins that:
16066 // 1. Have NULL optstr (so the `if (optstr) { option parse }`
16067 // loop didn't run), AND
16068 // 2. Don't have BINF_HANDLES_OPTS in their flags.
16069 // ```c
16070 // } else if (!(flags & BINF_HANDLES_OPTS) && *argv &&
16071 // !strcmp(*argv, "--")) {
16072 // ops.ind['-'] = 1;
16073 // argv++;
16074 // }
16075 // ```
16076 // `eval` has optstr=NULL (Src/builtin.c:65) and no
16077 // BINF_HANDLES_OPTS, so the strip applies. The Rust dispatch
16078 // path bypasses execbuiltin, so we need to do the strip
16079 // explicitly here. Without it `eval -- "echo hi"` joined to
16080 // `-- echo hi` and tried to dispatch `--` as a command. Bug
16081 // #319 in docs/BUGS.md.
16082 let stripped: Vec<String>;
16083 let argv: &[String] = if let Some(first) = argv.first() {
16084 if first == "--" {
16085 stripped = argv[1..].to_vec();
16086 &stripped
16087 } else {
16088 argv
16089 }
16090 } else {
16091 argv
16092 };
16093 eval(argv) // c:6396
16094}
16095
16096/// Port of `bin_read()` from `Src/builtin.c:6412`.
16097/// C decl: `bin_read(char *name, char **args, Options ops, UNUSED(int func))`
16098/// C: `int bin_read(char *name, char **args, Options ops, UNUSED(int func))`.
16099///
16100/// The C body is ~720 lines covering the whole `read` builtin matrix:
16101/// `-A` array, `-k N` raw chars, `-q` yes/no, `-r` raw, `-s` silent,
16102/// `-t TIMEOUT`, `-u FD` input FD, `-p` coproc, `-d DELIM` delimiter,
16103/// `-e` echo, `-E` echo-stdout-only, `-l`/`-c` compctl. The structural
16104/// port below handles the script-friendly subset: VAR= default,
16105/// `read -p PROMPT VAR`, `read -t TIMEOUT VAR`, `read -A ARRAY`,
16106/// `read -k N VAR`. Terminal-mode (-q/-s/-e) and ZLE plumbing defer
16107/// to the existing zle/io accessors.
16108/// WARNING: param names don't match C — Rust=(name, args, _func) vs C=(name, args, ops, func)
16109pub fn bin_read(
16110 name: &str,
16111 args: &[String], // c:6412
16112 ops: &options,
16113 _func: i32,
16114) -> i32 {
16115 // !!! DASH-ONLY (no C counterpart) !!! dash has no implicit REPLY: a
16116 // `read` naming no variable is `sh_error("arg count")` (status 2, input
16117 // not consumed, REPLY untouched). zsh reads into $REPLY. `read` is a
16118 // regular builtin in dash, so the error does not abort the script.
16119 if args.is_empty() && crate::dash_mode::dash_faithful() {
16120 zwarnnam(name, "arg count");
16121 return 2;
16122 }
16123 let mut args = args.to_vec();
16124 let mut nchars: i32 = 1; // c:6415
16125 let mut partial_eof = false;
16126
16127 // c:6432-6438 — `-k N` raw-char count.
16128 if OPT_HASARG(ops, b'k') {
16129 // c:6432
16130 let optarg = OPT_ARG(ops, b'k').unwrap_or("");
16131 match optarg.trim().parse::<i32>() {
16132 Ok(n) => nchars = n,
16133 Err(_) => {
16134 zwarnnam(name, &format!("number expected after -k: {}", optarg)); // c:6437
16135 return 1;
16136 }
16137 }
16138 }
16139
16140 // !!! BASH-MODE GATE (no C counterpart) !!! bash `read -n N` / `read -N N`
16141 // read chars from the input (not a terminal): `-n` reads AT MOST N,
16142 // stopping early at the delimiter (newline); `-N` reads EXACTLY N, ignoring
16143 // the delimiter. zsh's optstr treats both as boolean no-ops, so the count N
16144 // is left as the first positional; pull it out and route through the
16145 // char-read path (also skips the tty requirement).
16146 // ksh93 spells the same two reads `-n N` / `-N N` (ksh93u+m `read -n 2 a <<< abcd`
16147 // -> `ab`), so the gate covers it too.
16148 let read_counts = crate::dash_mode::bash_mode() || crate::dash_mode::ksh93_mode();
16149 // !!! SHARED-OPTSTR GUARD (no C counterpart) !!! the registered optstr
16150 // carries bash's `-a` / `-N`; zsh's own is "cd:ek:%lnpqrst:%zu:AE"
16151 // (c:Src/builtin.c:109), where both are `bad option` (status 1).
16152 if !read_counts {
16153 for (flag, c) in [(b'a', 'a'), (b'N', 'N')] {
16154 if OPT_ISSET(ops, flag) {
16155 zwarnnam(name, &format!("bad option: -{c}"));
16156 return 1;
16157 }
16158 }
16159 }
16160 let bash_n = read_counts && OPT_ISSET(ops, b'n');
16161 let bash_bign = read_counts && OPT_ISSET(ops, b'N');
16162 let bash_stop_at_nl = bash_n && !bash_bign; // -N ignores the delimiter
16163 let bash_nchars = (bash_n || bash_bign)
16164 && args
16165 .first()
16166 .map(|a| a.trim().parse::<i32>().is_ok())
16167 .unwrap_or(false);
16168 if bash_nchars {
16169 nchars = args.remove(0).trim().parse::<i32>().unwrap_or(1);
16170 }
16171
16172 // c:6445-6446 — `firstarg = (*args && **args == '?' ? *args++ :
16173 // *args); reply = *args ? *args++ : ...`. When the first arg
16174 // starts with `?` it is a prompt-only arg (consumed; the NEXT arg
16175 // is the reply name). Otherwise firstarg ALIASES the reply name —
16176 // and the prompt block at c:6534-6543 scans it for an embedded
16177 // `?`, prints the tail as the prompt, and truncates the name in
16178 // place (`readpmpt[-1] = '\0'`). So `read v?prompt: ` reads into
16179 // `v` with prompt `prompt: `. Bug #387 follow-up: the leading-`?`
16180 // form was handled but the `var?prompt` split was missing, so
16181 // zshrs errored "not an identifier: v?prompt".
16182 let mut argi = 0usize;
16183 let mut prompt: Option<String> = None;
16184 if argi < args.len() && args[argi].starts_with('?') {
16185 // c:6445 — prompt-only first arg.
16186 prompt = Some(args[argi][1..].to_string());
16187 argi += 1;
16188 }
16189 // `-A` is the zsh/ksh array-read flag; bash uses `-a`. Honor `-a` as an
16190 // array read only in bash mode so `read -a arr <<< "x y z"` works like
16191 // /bin/bash (in other modes `-a` parses but is inert, matching nothing).
16192 let want_array =
16193 OPT_ISSET(ops, b'A') || (read_counts && OPT_ISSET(ops, b'a'));
16194 let reply = if argi < args.len() {
16195 let mut r = args[argi].clone();
16196 argi += 1;
16197 // c:6534-6543 — firstarg (== this arg when no leading-`?` arg
16198 // was consumed) splits at its first `?`: name before, prompt
16199 // after.
16200 if prompt.is_none() {
16201 if let Some(q) = r.find('?') {
16202 prompt = Some(r[q + 1..].to_string());
16203 r.truncate(q); // c:6542 readpmpt[-1] = '\0'
16204 }
16205 }
16206 r
16207 } else if want_array {
16208 "reply".to_string() // c:6446
16209 } else {
16210 "REPLY".to_string() // c:6446
16211 };
16212
16213 if want_array && argi < args.len() {
16214 // c:6448
16215 zwarnnam(name, "only one array argument allowed"); // c:6449
16216 return 1;
16217 }
16218
16219 // c:6408-6413 — `haso` (true if /dev/tty has been opened specially),
16220 // `oshout`, `keys`, `resettty`, `saveti`, `readchar`, `izle`.
16221 let mut haso = false;
16222 let mut oshout: usize = 0;
16223 let mut keys = false;
16224 let mut resettty = false;
16225 let mut saveti: Option<libc::termios> = None;
16226 let readchar = std::cell::Cell::new(-1i32);
16227 let mut izle = crate::ported::builtins::sched::zleactive.load(Relaxed) != 0;
16228 let isem = getsparam("TERM").as_deref() == Some("emacs"); // c:6407
16229 let mut tty_readfd: Option<i32> = None; // C's `readfd = SHTTY`
16230 if (OPT_ISSET(ops, b'k') || OPT_ISSET(ops, b'q'))
16231 && !bash_nchars
16232 && !OPT_ISSET(ops, b'u')
16233 && !OPT_ISSET(ops, b'p')
16234 {
16235 // c:6457
16236 if !izle {
16237 // c:6458
16238 let shtty = crate::ported::init::SHTTY.load(Relaxed);
16239 if shtty == -1 {
16240 // c:6459 — need to open /dev/tty specially.
16241 let fd = unsafe {
16242 libc::open(
16243 c"/dev/tty".as_ptr(),
16244 libc::O_RDWR | libc::O_NOCTTY,
16245 )
16246 }; // c:6460
16247 crate::ported::init::SHTTY.store(fd, Relaxed);
16248 if fd != -1 {
16249 haso = true; // c:6461
16250 oshout = *crate::ported::init::shout.lock().unwrap(); // c:6462
16251 crate::ported::init::init_shout(); // c:6463
16252 }
16253 } else if *crate::ported::init::shout.lock().unwrap() == 0 {
16254 // c:6465 — we need an output FILE* on the tty.
16255 crate::ported::init::init_shout(); // c:6467
16256 }
16257 // c:6469 — we should have a SHTTY opened by now.
16258 if crate::ported::init::SHTTY.load(Relaxed) == -1 {
16259 // c:6470 — unfortunately, we didn't.
16260 eprintln!("not interactive and can't open terminal"); // c:6471
16261 return 1; // c:6473
16262 }
16263 if crate::ported::zsh_h::unset(INTERACTIVE) {
16264 // c:6475
16265 if let Some(ti) = crate::ported::utils::gettyinfo() {
16266 // c:6476 gettyinfo(&shttyinfo)
16267 *crate::ported::utils::SHTTYINFO.lock().unwrap() = Some(ti);
16268 }
16269 }
16270 // c:6478 — attach to the tty.
16271 crate::ported::utils::attachtty(
16272 crate::ported::modules::clone::mypgrp.load(Relaxed),
16273 );
16274 if !isem {
16275 crate::ported::utils::setcbreak(); // c:6480
16276 }
16277 tty_readfd = Some(crate::ported::init::SHTTY.load(Relaxed)); // c:6481
16278 }
16279 keys = true; // c:6483
16280 } else {
16281 izle = false; // c:6507/c:6514/c:6515 `izle = 0` for -u, -p, or neither
16282 }
16283
16284 // c:Src/builtin.c:6510-6515 — `else if (OPT_ISSET(ops,'p')) {
16285 // readfd = coprocin; if (readfd < 0) { zwarnnam(name, "-p: no
16286 // coprocess"); return 1; } izle = 0; }`. C emits this BEFORE
16287 // identifier validation of the reply name, so a bare
16288 // `read -p "var name" REPLY` (no coproc started) errors with
16289 // "-p: no coprocess" rather than "not an identifier". Bug #387.
16290 // The `-u`-with-arg path falls through and shares this same gate
16291 // when its arg is "p" (c:6494) — also handled.
16292 if OPT_ISSET(ops, b'p') || (OPT_HASARG(ops, b'u') && OPT_ARG(ops, b'u').as_deref() == Some("p"))
16293 {
16294 let coprocin =
16295 crate::ported::modules::clone::coprocin.load(std::sync::atomic::Ordering::Relaxed);
16296 if coprocin < 0 {
16297 zwarnnam(name, "-p: no coprocess");
16298 return 1;
16299 }
16300 }
16301
16302 // c:6453-6455 — `return compctlreadptr(name, args, ops, reply)`.
16303 // The compctlreadptr function pointer defaults to
16304 // `fallback_compctlread` (init.c:1834) and gets reassigned to the
16305 // real `compctlread` when zsh/compctl loads via the module's
16306 // boot_ hook. zshrs auto-loads zsh/compctl in default mode so the
16307 // direct call to `compctlread` is correct there. In --zsh parity
16308 // mode zsh/compctl isn't loaded until explicit `zmodload`, so
16309 // route through `fallback_compctlread` instead — matching
16310 // `zsh -fc 'getln -c x'` which emits "no loaded module provides
16311 // read for completion context".
16312 if OPT_ISSET(ops, b'l') || OPT_ISSET(ops, b'c') {
16313 // c:6453
16314 let zsh_mode = crate::IS_ZSH_MODE.load(std::sync::atomic::Ordering::Relaxed);
16315 // In --zsh mode, regardless of MODULESTAB's "loaded" flag
16316 // (which we set true at startup for QoL), route through
16317 // fallback_compctlread because zsh -fc's reported behavior
16318 // for `getln -c` is the fallback message. The MODULESTAB-
16319 // load-flag check below would defeat parity: zshrs marks
16320 // zsh/compctl as default-loaded so its compctl/compcall
16321 // builtins work without explicit zmodload. zsh -fc has only
16322 // zsh/main loaded and only auto-loads compctl when the
16323 // compctl/compcall builtins themselves are called — `read -c`
16324 // does NOT trigger that auto-load (zsh calls
16325 // `compctlreadptr` which defaults to `fallback_compctlread`
16326 // until something explicitly zmodload's zsh/compctl).
16327 //
16328 // The simplest C-faithful parity: in --zsh mode, always emit
16329 // the fallback message. Default zshrs mode keeps calling the
16330 // real compctlread.
16331 if zsh_mode {
16332 return crate::ported::init::fallback_compctlread(name);
16333 }
16334 return crate::ported::zle::compctl::compctlread(name, &args[argi..], ops, &reply);
16335 }
16336
16337 // Optional explicit input FD via -u, or coprocin via -p. When
16338 // unspecified, fall back to fd 0 (stdin). All read paths below
16339 // route bytes through `read_byte_from_fd` so `read -u 3 var`
16340 // after `exec 3< file` correctly pulls from the user fd; with
16341 // `-p`, the same path reads from coprocin (set by the BUILTIN_
16342 // RUN_COPROC handler at fusevm_bridge.rs around the
16343 // `coproc CMD` launch). Bug #388.
16344 let ufd: i32 = if OPT_ISSET(ops, b'p') {
16345 crate::ported::modules::clone::coprocin.load(std::sync::atomic::Ordering::Relaxed)
16346 } else if OPT_HASARG(ops, b'u') {
16347 // c:Src/builtin.c:6494-6500 — `fdarg = (int)zstrtol(argptr,
16348 // &eptr, 10); if (*eptr) { zwarnnam(name, "number expected
16349 // after -u: %s", argptr); return 1; }`. zshrs previously
16350 // `unwrap_or(0)`d, silently dropping `read -u abc v` errors.
16351 let argptr = OPT_ARG(ops, b'u').unwrap_or("");
16352 match argptr.parse::<i32>() {
16353 Ok(n) => {
16354 // Same deviation, and the same reason, as the `-u` arm of
16355 // `print` above: c:Src/builtin.c:6494-6500 has no fdtable
16356 // check, because in C a descriptor the shell keeps for
16357 // itself above 9 is not readable by the script — there is
16358 // nothing there to read. In zshrs there is: `read -u 11
16359 // line` returned 0 and put `SQLite format 3` — the header
16360 // of the user's history database — into a shell variable.
16361 //
16362 // Refusing yields status 1 and no diagnostic, which is
16363 // byte-for-byte what zsh 5.9.2 does for a descriptor it
16364 // does not have open. Script-owned descriptors are
16365 // `FDT_EXTERNAL` (c:Src/exec.c:2409) or 0/1/2
16366 // (c:Src/init.c:1900) and are untouched; anything past
16367 // `max_zsh_fd` is left alone per c:Src/exec.c:3886-3891.
16368 let shell_owned = n > 9 && {
16369 let max_fd = crate::ported::utils::MAX_ZSH_FD
16370 .load(std::sync::atomic::Ordering::Relaxed);
16371 n <= max_fd && {
16372 let kind = crate::ported::utils::fdtable_get(n)
16373 & crate::ported::zsh_h::FDT_TYPE_MASK;
16374 kind != crate::ported::zsh_h::FDT_UNUSED
16375 && kind != crate::ported::zsh_h::FDT_EXTERNAL
16376 }
16377 };
16378 if shell_owned {
16379 return 1;
16380 }
16381 n
16382 }
16383 Err(_) => {
16384 zwarnnam(name, &format!("number expected after -u: {}", argptr));
16385 return 1;
16386 }
16387 }
16388 } else {
16389 0
16390 };
16391 let ufd = tty_readfd.unwrap_or(ufd); // c:6481 `readfd = SHTTY`
16392 readfd.store(ufd, Relaxed);
16393
16394 // c:6511-6522 — `-s`: turn echo off on a tty input fd.
16395 if OPT_ISSET(ops, b's') && unsafe { libc::isatty(ufd) } != 0 {
16396 if let Ok(ti) = crate::ported::utils::fdgettyinfo(ufd) {
16397 saveti = Some(ti); // c:6516
16398 resettty = true;
16399 let mut ti = ti;
16400 ti.c_lflag &= !libc::ECHO; // c:6519
16401 let _ = crate::ported::utils::fdsettyinfo(ufd, &ti); // c:6523
16402 }
16403 }
16404
16405 // c:Src/builtin.c:6418 — single-byte reader bound to `ufd`.
16406 // libc::read with len=1 keeps the file position advancing across
16407 // successive calls (matches zsh's per-byte read loop). Returns
16408 // Some(byte) on success, None on EOF, error sentinel on syscall
16409 // failure (caller maps to return 2).
16410 let izle_timeout_cell = std::cell::Cell::new(0i64); // c:6416 izle_timeout
16411 let read_byte = |fd: i32| -> io::Result<Option<u8>> {
16412 // c:6638-6660 — ZLE input and the byte `read_poll` already consumed.
16413 if izle || readchar.get() >= 0 {
16414 let mut rc = readchar.replace(-1);
16415 let c = zread(izle as i32, &mut rc, izle_timeout_cell.get());
16416 return Ok(if c < 0 { None } else { Some(c as u8) });
16417 }
16418 let mut b = [0u8; 1];
16419 loop {
16420 let n = unsafe { libc::read(fd, b.as_mut_ptr() as *mut libc::c_void, 1) };
16421 // c:6671-6674 — `while ((val = read(readfd, bptr, nchars)) < 0) {
16422 // if (errno != EINTR || errflag || …) break; }`. errno must be
16423 // taken straight after the read: a diagnostic `Path::exists()`
16424 // used to sit between the two, and its failing stat(2) replaced
16425 // EINTR with ENOENT, so any signal landing during the read (a
16426 // SIGCHLD, a trapped USR1) ended it as a read error with status 1.
16427 let err = io::Error::last_os_error();
16428 match n {
16429 1 => return Ok(Some(b[0])),
16430 0 => return Ok(None),
16431 -1 => {
16432 if err.kind() == io::ErrorKind::Interrupted {
16433 continue;
16434 }
16435 return Err(err);
16436 }
16437 _ => return Ok(None),
16438 }
16439 }
16440 };
16441
16442 // c:6534-6543 — `read "?prompt"`: the prompt is written only when the
16443 // input is a tty (`keys || isatty(0)`), to `shout` when it is open.
16444 if let Some(ref p) = prompt {
16445 if keys || unsafe { libc::isatty(0) } != 0 {
16446 let shout_fd = *crate::ported::init::shout.lock().unwrap();
16447 if shout_fd > 0 {
16448 let bytes = p.as_bytes();
16449 unsafe {
16450 libc::write(
16451 shout_fd as i32 - 1,
16452 bytes.as_ptr() as *const libc::c_void,
16453 bytes.len(),
16454 )
16455 };
16456 } else {
16457 eprint!("{}", p);
16458 let _ = Write::flush(&mut io::stderr());
16459 }
16460 }
16461 }
16462
16463 // c:6560-6577 — `-d DELIM` on a tty: leave canonical mode so the
16464 // delimiter can end the read.
16465 if OPT_HASARG(ops, b'd') && unsafe { libc::isatty(ufd) } != 0 {
16466 if let Ok(ti) = crate::ported::utils::fdgettyinfo(ufd) {
16467 if !resettty {
16468 saveti = Some(ti); // c:6566
16469 resettty = true;
16470 }
16471 let mut ti = ti;
16472 ti.c_lflag &= !libc::ICANON; // c:6572
16473 ti.c_cc[libc::VMIN] = 1;
16474 ti.c_cc[libc::VTIME] = 0;
16475 let _ = crate::ported::utils::fdsettyinfo(ufd, &ti); // c:6577
16476 }
16477 }
16478
16479 // c:6579-6617 — `-t TIMEOUT`.
16480 if OPT_ISSET(ops, b't') {
16481 let mut timeout: i64 = 0; // c:6580
16482 if OPT_HASARG(ops, b't') {
16483 // c:6581
16484 let mn = match crate::ported::math::matheval(OPT_ARG(ops, b't').unwrap_or("")) {
16485 Ok(mn) => mn,
16486 Err(_) => return 1, // c:6584 `if (errflag) return 1;`
16487 };
16488 if errflag.load(Relaxed) != 0 {
16489 return 1;
16490 }
16491 if (mn.type_ & crate::ported::zsh_h::MN_FLOAT) != 0 {
16492 timeout = (mn.d * 1e6) as i64; // c:6587-6588
16493 } else {
16494 timeout = mn.l * 1_000_000; // c:6590
16495 }
16496 }
16497 if izle {
16498 // c:6593 — timeout is in 100ths of a second rather than us.
16499 izle_timeout_cell.set(-(timeout / 10000 + 1)); // c:6598
16500 } else {
16501 let mut rc = readchar.get();
16502 let polled = ufd != -1
16503 && crate::ported::utils::read_poll(ufd, &mut rc, keys && !izle, timeout);
16504 readchar.set(rc);
16505 if !polled {
16506 // c:6606
16507 if keys && !izle && !isem {
16508 if let Some(ti) = *crate::ported::utils::SHTTYINFO.lock().unwrap() {
16509 crate::ported::utils::settyinfo(&ti); // c:6608
16510 }
16511 } else if resettty {
16512 if let (Some(ti), true) = (saveti.as_ref(), ufd >= 0) {
16513 let _ = crate::ported::utils::fdsettyinfo(ufd, ti); // c:6610
16514 }
16515 }
16516 if haso {
16517 *crate::ported::init::shout.lock().unwrap() = oshout; // c:6614
16518 crate::ported::init::SHTTY.store(-1, Relaxed); // c:6615
16519 }
16520 return if OPT_ISSET(ops, b'q') { 2 } else { 1 }; // c:6617
16521 }
16522 }
16523 }
16524
16525 // c:Src/builtin.c:6769-6770 — `-z` pops the front of the ZLE
16526 // bufstack into `zbuf` and reads from THAT instead of the file
16527 // descriptor. When the bufstack is empty the popped value is the
16528 // empty string, which makes `zread` return EOF immediately;
16529 // c:7110-7115 then returns 1 after the reply assignment.
16530 //
16531 // zbuforig = zbuf = (!OPT_ISSET(ops,'z')) ? NULL :
16532 // (nonempty(bufstack)) ? (char *)getlinknode(bufstack) :
16533 // ztrdup("");
16534 // ...
16535 // if (zbuforig) { char first = *zbuforig; ... if (!first) return 1; }
16536 //
16537 // The default delim handling (newline-terminated, raw or
16538 // backslash-cooked) still applies — read up to the first newline
16539 // in the popped buffer or to its end if no newline.
16540 let mut buf = String::new();
16541 if OPT_ISSET(ops, b'z') {
16542 let popped: Option<String> = {
16543 let mut stack = crate::ported::zle::zle_main::BUFSTACK.lock().unwrap();
16544 if stack.is_empty() {
16545 None
16546 } else {
16547 Some(stack.remove(0))
16548 }
16549 };
16550 let zbuf_text = popped.clone().unwrap_or_default();
16551 let raw_mode = OPT_ISSET(ops, b'r') || OPT_ISSET(ops, b'R');
16552 if OPT_HASARG(ops, b'd') {
16553 let arg = OPT_ARG(ops, b'd').unwrap_or("");
16554 let delim = arg.as_bytes().first().copied().unwrap_or(b'\0');
16555 let mut out = Vec::<u8>::new();
16556 for &b in zbuf_text.as_bytes() {
16557 if b == delim {
16558 break;
16559 }
16560 out.push(b);
16561 }
16562 buf = crate::script_bytes::decode_script_bytes(&out);
16563 } else {
16564 let mut out = Vec::<u8>::new();
16565 let bytes = zbuf_text.as_bytes();
16566 let mut i = 0;
16567 while i < bytes.len() {
16568 let b = bytes[i];
16569 if !raw_mode && b == b'\\' && i + 1 < bytes.len() {
16570 let nx = bytes[i + 1];
16571 if nx == b'\n' {
16572 i += 2;
16573 continue;
16574 }
16575 out.push(nx);
16576 i += 2;
16577 continue;
16578 }
16579 if b == b'\n' {
16580 break;
16581 }
16582 out.push(b);
16583 i += 1;
16584 }
16585 buf = crate::script_bytes::decode_script_bytes(&out);
16586 }
16587 // c:7102-7109 — -e/-E echo, -e suppresses assignment.
16588 let opt_e = OPT_ISSET(ops, b'e');
16589 let opt_big_e = OPT_ISSET(ops, b'E');
16590 if opt_e || opt_big_e {
16591 println!("{}", buf);
16592 }
16593 if !opt_e {
16594 setsparam(&reply, &buf);
16595 }
16596 // c:7110-7115 — empty bufstack pop → return 1 even after assign.
16597 return if popped.is_none() || popped.as_deref() == Some("") {
16598 1
16599 } else {
16600 0
16601 };
16602 }
16603 if OPT_ISSET(ops, b'k') || OPT_ISSET(ops, b'q') || bash_nchars {
16604 // c:Src/builtin.c:6630 — `if (OPT_ISSET(ops,'k') ||
16605 // OPT_ISSET(ops,'q'))` — both read raw chars (no line/IFS
16606 // processing). `-q` reads exactly one char (nchars defaults to
16607 // 1) for the yes/no decision applied below. The previous port
16608 // gated only on `-k`, so `-q` fell through to the line reader
16609 // and never applied the y/Y check (always returned 0).
16610 // c:Src/builtin.c — `-k 0` (zero chars requested) is a no-op
16611 // read that zsh treats as failure (returns 1) because no
16612 // bytes can be consumed. Mirror so `read -k 0` exits 1
16613 // instead of "succeeding" with an empty buffer.
16614 // bash `-n N` (bash_nchars) joins this raw-char path but STOPS at the
16615 // delimiter (newline): `read -n 10 <<< "ab"` gets "ab", not 10 bytes.
16616 if nchars <= 0 {
16617 return 1;
16618 }
16619 // c:6633-6634 — `bptr = buf = zalloc(nchars*MB_CUR_MAX+1)`: `nchars`
16620 // counts CHARACTERS, so the byte budget is nchars * MB_CUR_MAX.
16621 // c:6683-6705 — `mbrlen` drives the countdown: MB_INCOMPLETE keeps
16622 // reading without decrementing, MB_INVALID is "treat as single
16623 // byte" (c:6690-6693), and only a completed character does
16624 // `nchars--`. The old port decremented per BYTE, so `read -k2`
16625 // over `«»` returned the two bytes of `«` alone.
16626 let mb_on = crate::ported::options::opt_state_get("multibyte").unwrap_or(true);
16627 let mut got = Vec::<u8>::with_capacity(nchars as usize * 4);
16628 let mut nleft = nchars as usize; // c:6715 `while (nchars > 0)`
16629 let mut pending = Vec::<u8>::new();
16630 while nleft > 0 {
16631 match read_byte(ufd) {
16632 Ok(Some(b)) => {
16633 if bash_stop_at_nl && b == b'\n' {
16634 break; // bash -n stops early at the delimiter
16635 }
16636 if !mb_on {
16637 // c:6656-6659 — byte mode: one byte is one character.
16638 got.push(b);
16639 nleft -= 1;
16640 continue;
16641 }
16642 pending.push(b);
16643 match std::str::from_utf8(&pending) {
16644 // c:6701 — a complete character: `nchars--`.
16645 Ok(_) => {
16646 got.extend_from_slice(&pending);
16647 pending.clear();
16648 nleft -= 1;
16649 }
16650 // c:6686-6688 — MB_INCOMPLETE: keep reading.
16651 Err(e) if e.error_len().is_none() && pending.len() < 4 => {}
16652 // c:6690-6693 — MB_INVALID: treat as a single byte.
16653 Err(_) => {
16654 got.extend_from_slice(&pending);
16655 pending.clear();
16656 nleft -= 1;
16657 }
16658 }
16659 }
16660 // c:6674-6677 — `if (val <= 0) { eof = 1; break; }`.
16661 _ => {
16662 partial_eof = true;
16663 break;
16664 }
16665 }
16666 }
16667 // EOF mid-character: keep the debris, as c:6892-6898 does.
16668 got.extend_from_slice(&pending);
16669 // "Keep the debris" is only true if the conversion keeps it:
16670 // `String::from_utf8_lossy` collapsed every MB_INVALID byte the loop
16671 // above deliberately preserved into a single U+FFFD, which is the
16672 // opposite of what c:6690-6693 asks for. `read -k` on a latin-1
16673 // stream now yields the byte itself.
16674 buf = crate::script_bytes::decode_script_bytes(&got);
16675 } else if OPT_HASARG(ops, b'd') {
16676 // c:Src/builtin.c:6538 — `-d DELIM`: read until the first
16677 // CHARACTER of DELIM. EOF mid-record returns what was read so
16678 // far, exit 1 like the default path.
16679 let arg = OPT_ARG(ops, b'd').unwrap_or("");
16680 // c:6538-6555 — the delimiter is ONE CHARACTER, not one byte:
16681 // if (isset(MULTIBYTE)) { mb_charinit();
16682 // (void)mb_metacharlenconv(delimstr, &wi); }
16683 // else wi = WEOF;
16684 // if (wi != WEOF) delim = (wchar_t)wi;
16685 // else { delim = (wchar_t)(unsigned char)delimstr[0]; rawbyte = 1; }
16686 // The old port compared the FIRST BYTE, so `read -d £` (0xc2 0xa3)
16687 // stopped at the 0xc2 of any other Latin-1 character (`¤`, `«`) and
16688 // left the trailing byte in the next record.
16689 let dbytes = crate::ported::utils::unmetafy_str(arg);
16690 // c:6543 — the wide decode only happens under MULTIBYTE.
16691 let mb_on = crate::ported::options::opt_state_get("multibyte").unwrap_or(true);
16692 let (delim_wc, rawbyte) = if dbytes.is_empty() {
16693 // c:Src/builtin.c:6418 — empty `-d` arg means NUL delimiter.
16694 // zsh's `delimstr[0]` reads the arg buffer's NUL terminator.
16695 (0u32, true)
16696 } else if mb_on {
16697 match crate::ported::utils::mb_metacharlenconv(&dbytes) {
16698 (_, Some(wc), _) => (wc as u32, false), // c:6549-6550
16699 _ => (dbytes[0] as u32, true), // c:6551-6554 (WEOF)
16700 }
16701 } else {
16702 (dbytes[0] as u32, true) // c:6547-6548 `wi = WEOF`
16703 };
16704 let mut buf_bytes = Vec::<u8>::new();
16705 // c:6983-6998 — one byte at a time into `bptr`, then
16706 // `ret = mbrtowc(&wc, bptr, 1, &mbs)`: MB_INCOMPLETE keeps reading,
16707 // MB_INVALID resets the state and treats THIS byte as the character
16708 // (the incomplete bytes already staged stay in the buffer).
16709 let mut pending = Vec::<u8>::new();
16710 // c:7045 — the loop breaks on `c == EOF || (c == delim && !zbuf)`, and
16711 // c:7116 keys the exit status off WHICH of the two ended it. "Did any
16712 // byte arrive" can't tell an EOF-terminated record from a
16713 // delimiter-terminated one, so track the delimiter itself.
16714 let mut saw_delim = false;
16715 // c:7055 — `bslash = c == '\\' && !bslash && !OPT_ISSET(ops,'r')`.
16716 // In C there is ONE read loop and `-d` only swaps the `delim` variable,
16717 // so the backslash rules apply to `-d` exactly as they do to a plain
16718 // `read`. zshrs split the loop in two and the `-d` copy dropped them,
16719 // so `read -d ' ' x <<< 'esc\ttab'` kept the backslash (`esc\ttab`)
16720 // where zsh yields `escttab`.
16721 let raw_mode = OPT_ISSET(ops, b'r') || OPT_ISSET(ops, b'R');
16722 let mut bslash = false;
16723 loop {
16724 match read_byte(ufd) {
16725 Ok(Some(b)) => {
16726 // c:6984-6998 — assemble ONE character before any of the
16727 // delimiter / backslash tests run. `rawbyte` (c:6554) and
16728 // NO_MULTIBYTE both short-circuit to the byte itself.
16729 let (wc, unit): (u32, Vec<u8>) = if rawbyte || !mb_on {
16730 (b as u32, vec![b]) // c:6988-6991
16731 } else {
16732 pending.push(b);
16733 match std::str::from_utf8(&pending) {
16734 // c:6992 — `ret != MB_INCOMPLETE`, a whole character.
16735 Ok(v) => {
16736 let c = v.chars().next().unwrap_or('\0');
16737 (c as u32, std::mem::take(&mut pending))
16738 }
16739 // c:6792 — `MB_INCOMPLETE`: keep reading bytes.
16740 Err(e) if e.error_len().is_none() && pending.len() < 4 => {
16741 continue;
16742 }
16743 // c:6993-6997 — `MB_INVALID`: reset the shift state
16744 // and treat this as a single character.
16745 Err(_) => (b as u32, std::mem::take(&mut pending)),
16746 }
16747 };
16748 // c:7003-7005 — a backslash-escaped DELIMITER is a line
16749 // continuation: both are dropped and the record keeps
16750 // going. `read -d t x <<< 'esc\ttab'` therefore yields `esc`
16751 // (the escaped `t` vanishes; the NEXT `t` terminates).
16752 if bslash && wc == delim_wc {
16753 bslash = false; // c:7004
16754 continue; // c:7005
16755 }
16756 if wc == delim_wc {
16757 saw_delim = true; // c:7007 (`wc == delim`)
16758 break;
16759 }
16760 // c:7017-7019 — an unescaped backslash is consumed and
16761 // escapes the next character (unless `-r`).
16762 let was_escaped = bslash; // this char follows a backslash
16763 bslash = wc == u32::from(b'\\') && !bslash && !raw_mode; // c:7017
16764 if bslash {
16765 continue; // c:7019
16766 }
16767 // Mark a backslash-escaped byte literal so IFS splitting
16768 // skips it (see the default-line loop above for the rule).
16769 if was_escaped {
16770 buf_bytes.extend_from_slice("\u{e199}".as_bytes());
16771 }
16772 buf_bytes.extend_from_slice(&unit); // c:7021-7027
16773 }
16774 Ok(None) => break,
16775 // c:Src/builtin.c:7162-7188 zread — a read(2) error
16776 // (EBADF from `read foo <&-`, EIO, ...) returns EOF,
16777 // not a distinct error status; bin_read then reports
16778 // plain eof (exit 1), with no diagnostic.
16779 Err(_) => break,
16780 }
16781 }
16782 // c:6892-6898 — "We can only get here if there is an EOF in the
16783 // middle of a character... safest to keep the debris, I suppose."
16784 buf_bytes.extend_from_slice(&pending);
16785 // Lossless, for the same reason as the `-k` arm: the bytes this loop
16786 // preserved on purpose must survive the String conversion.
16787 buf = crate::script_bytes::decode_script_bytes(&buf_bytes);
16788 // c:Src/builtin.c:6418 — `read -d ''` (NUL delimiter) strips
16789 // trailing newlines from the captured content. This matches
16790 // the `find -print0 | while read -d ''` idiom which expects
16791 // path entries without trailing whitespace. zsh's read body
16792 // applies this trim only for the empty-delim case; non-empty
16793 // delimiters keep the raw bytes.
16794 if arg.is_empty() {
16795 while buf.ends_with('\n') {
16796 buf.pop();
16797 }
16798 }
16799 // c:7107 + c:7116-7122 — C assigns the parameter FIRST (`setsparam(reply,
16800 // buf)`) and only then returns 1 if the loop ended at EOF. So an
16801 // unterminated record still lands in the variable but reports failure:
16802 // `read -d : x <<< 'abc'` sets x=abc and returns 1, which is what makes
16803 // `while read -d : x` terminate on a final field with no trailing
16804 // delimiter. The old early `return 1` fired only when NOTHING was read,
16805 // so every EOF-terminated record wrongly reported success (rc=0) — and
16806 // it also skipped the assignment entirely, leaving the variable at its
16807 // previous value instead of clearing it.
16808 //
16809 // `partial_eof` is the same carrier the default line branch below uses;
16810 // the shared `if partial_eof { return 1 }` after the assignment block
16811 // applies the status.
16812 partial_eof = !saw_delim; // c:7116 (`else if (c == EOF)`)
16813 } else {
16814 // Read a line (default behaviour). c:Src/builtin.c:6505
16815 // — without `-r`, backslash-X eats the backslash and keeps
16816 // the literal X (backslash-newline is line continuation).
16817 let raw_mode = OPT_ISSET(ops, b'r') || OPT_ISSET(ops, b'R');
16818 let mut buf_bytes = Vec::<u8>::new();
16819 let mut got_any = false;
16820 let mut saw_newline = false;
16821 loop {
16822 match read_byte(ufd) {
16823 Ok(Some(b)) => {
16824 got_any = true;
16825 if !raw_mode && b == b'\\' {
16826 match read_byte(ufd) {
16827 Ok(Some(nx)) => {
16828 if nx == b'\n' {
16829 // Line continuation — drop both.
16830 continue;
16831 }
16832 // A backslash-escaped char is literal and must
16833 // NOT act as an IFS separator: dash/ksh/bash/zsh
16834 // all read `a\ b` as the single field `a b`.
16835 // Prefix it with the Bnull mark (\u{e199}, UTF-8
16836 // C2 99) so the IFS split below skips it; the
16837 // mark is stripped before assignment.
16838 buf_bytes.extend_from_slice("\u{e199}".as_bytes());
16839 buf_bytes.push(nx);
16840 continue;
16841 }
16842 Ok(None) => {
16843 buf_bytes.push(b'\\');
16844 break;
16845 }
16846 // c:7162-7188 zread — read error == EOF.
16847 Err(_) => break,
16848 }
16849 }
16850 if b == b'\n' {
16851 saw_newline = true;
16852 break;
16853 }
16854 buf_bytes.push(b);
16855 }
16856 Ok(None) => break,
16857 // c:7162-7188 zread — read error == EOF (exit 1, no
16858 // message): `exec 3<&-; read foo <&-` exits 1 in zsh.
16859 Err(_) => break,
16860 }
16861 }
16862 if !got_any {
16863 // c:Src/builtin.c — immediate EOF (no bytes read at all).
16864 // zsh clears the target variable(s) BEFORE returning 1
16865 // so `while read line; do …; done < file` leaves `\$line`
16866 // empty after the loop ends (not the last value).
16867 // Previously zshrs returned 1 without touching the var,
16868 // so the loop body's final iteration value persisted.
16869 buf = String::new();
16870 partial_eof = true;
16871 // Fall through to the assignment block which will write
16872 // "" to reply / vars / array. The trailing `if
16873 // partial_eof { return 1; }` (line 8628) preserves the
16874 // EOF status code.
16875 } else {
16876 // `read line < latin1-file` handed the variable a U+FFFD where
16877 // zsh assigns the raw byte; lossless decode keeps them equal.
16878 buf = crate::script_bytes::decode_script_bytes(&buf_bytes);
16879 partial_eof = !saw_newline;
16880 }
16881 }
16882
16883 // c:6718-6730 — dispose of the tty state the `-k`/`-q` read set up.
16884 if (OPT_ISSET(ops, b'k') || OPT_ISSET(ops, b'q'))
16885 && !izle
16886 && !OPT_ISSET(ops, b'u')
16887 && !OPT_ISSET(ops, b'p')
16888 {
16889 if isem {
16890 // c:6720 — `while (val > 0 && read(SHTTY, &d, 1) == 1 && d != '\n');`
16891 let mut d = 0u8;
16892 while !partial_eof
16893 && unsafe {
16894 libc::read(
16895 crate::ported::init::SHTTY.load(Relaxed),
16896 &mut d as *mut u8 as *mut libc::c_void,
16897 1,
16898 )
16899 } == 1
16900 && d != b'\n'
16901 {}
16902 } else if resettty {
16903 if let Some(ti) = saveti.as_ref() {
16904 let _ = crate::ported::utils::fdsettyinfo(ufd, ti); // c:6722
16905 }
16906 resettty = false; // c:6723
16907 }
16908 if haso {
16909 // c:6725
16910 *crate::ported::init::shout.lock().unwrap() = oshout; // c:6727
16911 crate::ported::init::SHTTY.store(-1, Relaxed); // c:6728
16912 }
16913 }
16914 // c:6751/c:6962/c:7091 — `if (resettty) fdsettyinfo(readfd, &saveti);`
16915 if resettty {
16916 if let Some(ti) = saveti.as_ref() {
16917 let _ = crate::ported::utils::fdsettyinfo(ufd, ti);
16918 }
16919 }
16920
16921 // c:Src/builtin.c:6730-6742 — `-q` (read yes/no): the single char
16922 // read above is "yes" iff it is exactly 'y' or 'Y'; the reply var is
16923 // set to "y"/"n" and the exit status is 0 (yes) / 1 (no). (A timeout is status 2, returned by the `-t` poll above.) This must run BEFORE the
16924 // IFS/array assignment dispatch — `-q` never does line splitting.
16925 if OPT_ISSET(ops, b'q') {
16926 // c:6737-6740 — "Keep eof as status but status is now whether we read
16927 // 'y' or 'Y'. If we timed out, status is 2."
16928 // if (eof) eof = 2;
16929 // else eof = (bptr - buf != 1 || (buf[0] != 'y' && buf[0] != 'Y'));
16930 let eof = if partial_eof { 2 } else if buf == "y" || buf == "Y" { 0 } else { 1 };
16931 setsparam(&reply, if eof == 0 { "y" } else { "n" }); // c:6742
16932 return eof;
16933 }
16934
16935 // Backslash-escaped chars were tagged with the Bnull mark (\u{e199}) by
16936 // the read loop so the IFS splitting below treats them as literal.
16937 // `rmark` is that tag; `unmark` strips it (keeping the escaped char),
16938 // applied to every field/value just before assignment. Both are no-ops
16939 // on the common markerless line (fast `contains` guard). No C
16940 // counterpart — C's read splits inline during the char loop, so it
16941 // never needs a post-hoc mark; the port splits in a second pass.
16942 let rmark = '\u{e199}';
16943 let unmark = |s: &str| -> String {
16944 if !s.contains(rmark) {
16945 return s.to_string();
16946 }
16947 let mut out = String::with_capacity(s.len());
16948 let mut m = false;
16949 for c in s.chars() {
16950 if !m && c == rmark {
16951 m = true;
16952 continue;
16953 }
16954 m = false;
16955 out.push(c);
16956 }
16957 out
16958 };
16959
16960 // c:6855 / c:7039 — `isep(c)` / `iwsep(c)` classify each input
16961 // character against the typtab `inittyptab` builds from $IFS
16962 // (c:Src/utils.c:4216-4230). With MULTIBYTE unset a raw byte >= 0x80
16963 // reaches this walk as zshrs's character-level Meta pair (U+0083 +
16964 // `B ^ 32`, the form `$IFS` holds too), and it must be classified as the
16965 // ONE byte C sees. Testing the pair's two chars against the IFS string
16966 // split at the Meta char and kept the second half:
16967 // `unsetopt multibyte; IFS=$'\xe9'; read -r x y <<< $'p\xe9q'` gave
16968 // `p` / `Éq` where zsh gives `p` / `q`. Every other unit goes through
16969 // `wcsitype(c, ISEP)` (c:Src/utils.c:4321), which reads the typtab for
16970 // ASCII and `ifs_wide` for a multibyte char; only a single byte can be
16971 // IWSEP (c:Src/utils.c:3686).
16972 let read_multibyte = isset(crate::ported::zsh_h::MULTIBYTE);
16973 // Byte length of the unit at the front of `s`: a Meta pair is one unit.
16974 let unit_len = |s: &str| -> usize {
16975 let mut it = s.chars();
16976 match it.next() {
16977 None => 0,
16978 Some(c) if !read_multibyte && c == '\u{83}' => match it.next() {
16979 Some(n) if (0x80..=0xff).contains(&(n as u32)) => c.len_utf8() + n.len_utf8(),
16980 _ => c.len_utf8(),
16981 },
16982 Some(c) => c.len_utf8(),
16983 }
16984 };
16985 // (is separator, is whitespace separator) for one unit.
16986 let unit_sep = |u: &str| -> (bool, bool) {
16987 let mut it = u.chars();
16988 let c = it.next().unwrap_or('\0');
16989 if !read_multibyte && c == '\u{83}' {
16990 if let Some(n) = it.next() {
16991 let b = (n as u32 as u8) ^ 32; // c:4217 `*s == Meta ? *++s ^ 32`
16992 return (
16993 crate::ported::ztype_h::isep(b), // c:6855
16994 crate::ported::ztype_h::iwsep(b), // c:6856
16995 );
16996 }
16997 }
16998 let s = crate::ported::utils::wcsitype(c, crate::ported::ztype_h::ISEP as u32); // c:7039
16999 (s, s && (c as u32) < 128 && crate::ported::ztype_h::iwsep(c as u8)) // c:3686
17000 };
17001 // Byte length of the leading whitespace-IFS run of `s`.
17002 let ws_prefix_len = |s: &str| -> usize {
17003 let mut i = 0;
17004 while i < s.len() {
17005 let l = unit_len(&s[i..]);
17006 if !unit_sep(&s[i..i + l]).1 {
17007 break;
17008 }
17009 i += l;
17010 }
17011 i
17012 };
17013 // Byte offset where the trailing whitespace-IFS run of `s` starts.
17014 let ws_suffix_start = |s: &str| -> usize {
17015 let mut i = 0;
17016 let mut end = 0;
17017 while i < s.len() {
17018 let l = unit_len(&s[i..]);
17019 let ws = unit_sep(&s[i..i + l]).1;
17020 i += l;
17021 if !ws {
17022 end = i;
17023 }
17024 }
17025 end
17026 };
17027 // c:Src/builtin.c:6772-6830 — read one word starting at byte `*cur`,
17028 // updating C's `first` (a non-whitespace separator was seen, so empty
17029 // fields count) and `gotnl` (the delimiter or EOF was reached). The
17030 // returned word keeps its Bnull marks; callers unmark before assigning.
17031 let read_word = |cur: &mut usize, first: &mut bool, gotnl: &mut bool| -> String {
17032 let mut word = String::new();
17033 loop {
17034 if *cur >= buf.len() {
17035 *gotnl = true; // c:6898 `if (wc == delim) gotnl = 1` / EOF
17036 break;
17037 }
17038 let l = unit_len(&buf[*cur..]);
17039 let unit = &buf[*cur..*cur + l];
17040 *cur += l;
17041 // c:6823-6826 — a backslash-escaped unit is stored literally and
17042 // clears `first`; it is never a separator.
17043 if unit.starts_with(rmark) {
17044 word.push_str(unit);
17045 if *cur < buf.len() {
17046 let nl = unit_len(&buf[*cur..]);
17047 word.push_str(&buf[*cur..*cur + nl]);
17048 *cur += nl;
17049 }
17050 *first = false;
17051 continue;
17052 }
17053 let (sep, ws) = unit_sep(unit);
17054 if sep {
17055 // c:6815 `bptr != buf` — with MULTIBYTE set, zread hands
17056 // mbrtowc one byte at a time and every leading byte of a
17057 // multibyte character is stored before it decodes (c:6827),
17058 // so a multibyte separator always ends the word, even an
17059 // empty one; c:6887 `*laststart = '\0'` then drops those bytes.
17060 // `IFS=" 日"; read -rA a <<< " a 日 b"` is (a "" b).
17061 let bptr_ne_buf = !word.is_empty() || (read_multibyte && l > 1);
17062 if bptr_ne_buf || (!ws && *first) {
17063 *first |= !ws; // c:6817
17064 break;
17065 }
17066 *first |= !ws; // c:6820
17067 continue;
17068 }
17069 *first = false; // c:6826
17070 word.push_str(unit);
17071 }
17072 word
17073 };
17074
17075 // Assign to scalar reply, multi-var split, or array.
17076 // c:6685-6735 — `read x y z` splits buf by IFS, fills the first
17077 // N-1 vars with one IFS-separated field each, and stores the
17078 // REST of the line (including embedded IFS chars) into the last
17079 // var. zsh's read is stable on `print "a b c d" | read x y z`:
17080 // x="a", y="b", z="c d".
17081 if want_array {
17082 // c:Src/builtin.c:6685-6735 — `read -A arr` splits on $IFS
17083 // (whitespace-IFS coalesces; non-whitespace-IFS each acts as
17084 // a single delimiter). The previous port hardcoded
17085 // split_whitespace(), which ignored custom IFS like `:` and
17086 // produced a single-element array for `IFS=: read -A arr
17087 // <<< "a:b:c"`. Mirror the multi-var path's IFS handling.
17088 // c:6863-6869 — the FIRST word's char loop skips leading
17089 // whitespace-IFS (bptr==buf && iwsep → continue), so leading
17090 // whitespace produces no empty element.
17091 // c:Src/builtin.c:6764 — `while (*args || (OPT_ISSET(ops,'A') && !gotnl))`:
17092 // one word per pass until the delimiter (or EOF) sets gotnl.
17093 let mut parts: Vec<String> = Vec::new();
17094 let mut cur = 0usize;
17095 let mut first = true; // c:6763
17096 let mut gotnl = false;
17097 while !gotnl {
17098 let word = read_word(&mut cur, &mut first, &mut gotnl);
17099 // c:6929 — `if (!OPT_ISSET(ops,'e') && (*buf || first || gotnl))`
17100 if !word.is_empty() || first || gotnl {
17101 parts.push(unmark(&word)); // c:6931 addlinknode
17102 }
17103 }
17104 // c:Src/builtin.c:6910-6961 — `-A` with `-e`/`-E`. The word is
17105 // echoed (one per line) when `-e` (main per-word loop, c:6910)
17106 // OR `-E` (array-display loop, c:6949). The array is SET only
17107 // when `-e` is NOT set (c:6921 collects words, c:6945 allocates
17108 // p only when !-e). So `read -AE` echoes AND assigns; `read -Ae`
17109 // echoes but leaves the array untouched. The previous port
17110 // always assigned and never echoed.
17111 let opt_e = OPT_ISSET(ops, b'e');
17112 if opt_e || OPT_ISSET(ops, b'E') {
17113 for w in &parts {
17114 println!("{}", w);
17115 }
17116 }
17117 if !opt_e {
17118 setaparam(&reply, parts); // c:6960 setaparam
17119 }
17120 // c:6964 — `return c == EOF;`: the array arm returns before the
17121 // c:7121 `return errflag` tail, so a failed assignment (odd pairs
17122 // into an assoc) still exits 0 unless the line hit EOF.
17123 return partial_eof as i32;
17124 } else if argi < args.len() {
17125 // Multi-var: `read x y [z]`. First var = reply (already
17126 // consumed); rest are args[argi..]. Split with at most
17127 // `vars.len()` chunks using IFS.
17128 let mut vars: Vec<String> = Vec::with_capacity(args.len() - argi + 1);
17129 vars.push(reply);
17130 for n in &args[argi..] {
17131 vars.push(n.clone());
17132 }
17133 // Trim leading IFS-whitespace per zsh's read semantics
17134 // (`a b c` → x=a, y="b c", not x="" y=…).
17135 // c:Src/builtin.c — `-E` echoes each field to stdout as it is read,
17136 // one per line (c:6957 in the -A path, and the equivalent per-word
17137 // display on the multi-var path); `-e` does the same but ASSIGNS
17138 // NOTHING (c:7106 `if (!OPT_ISSET(ops,'e')) setsparam(...)`). This path
17139 // did neither, so `read -E a b` split correctly but printed no echo
17140 // (the whole-line -E echo worked only for a single variable).
17141 let opt_e = OPT_ISSET(ops, b'e');
17142 let opt_echo = opt_e || OPT_ISSET(ops, b'E');
17143 // Strip escape marks just before assignment so `read x y` on `a\ b`
17144 // assigns x="a b" (one field), matching dash/ksh/bash/zsh.
17145 let emit = |var: &str, val: &str| {
17146 let val = unmark(val);
17147 if opt_echo {
17148 println!("{val}"); // c:6958 zputs + putchar('\n')
17149 }
17150 if !opt_e {
17151 setsparam(var, &val); // c:7106
17152 }
17153 };
17154 let mut cur = 0usize;
17155 let mut first = true; // c:6763
17156 let mut gotnl = false;
17157 let (last, leading) = vars.split_last().expect("vars holds reply");
17158 // c:6764 — every variable but the last takes one word.
17159 for var in leading {
17160 let word = read_word(&mut cur, &mut first, &mut gotnl);
17161 // c:6929 — `(*buf || first || gotnl)`; otherwise the word is
17162 // dropped and the variable left untouched.
17163 if !word.is_empty() || first || gotnl {
17164 emit(var, &word);
17165 } else if opt_echo {
17166 println!("{}", unmark(&word)); // c:6926 echo precedes the test
17167 }
17168 }
17169 // c:6993-7060 — any remaining part of the line goes into the last
17170 // parameter. Only a separator met while nothing is stored yet
17171 // (`bptr == buf`, c:7009) is skipped: whitespace always, one
17172 // non-whitespace separator when the word loop did not end on one.
17173 let mut rest = String::new();
17174 if !gotnl {
17175 while cur < buf.len() {
17176 let l = unit_len(&buf[cur..]);
17177 let unit = &buf[cur..cur + l];
17178 cur += l;
17179 if unit.starts_with(rmark) {
17180 rest.push_str(unit);
17181 if cur < buf.len() {
17182 let nl = unit_len(&buf[cur..]);
17183 rest.push_str(&buf[cur..cur + nl]);
17184 cur += nl;
17185 }
17186 continue;
17187 }
17188 // c:7009 — a multibyte character's leading bytes are already
17189 // stored by the time it decodes, so `bptr == buf` is false.
17190 let bptr_is_buf = rest.is_empty() && !(read_multibyte && l > 1);
17191 let (sep, ws) = unit_sep(unit);
17192 if bptr_is_buf && sep {
17193 if ws {
17194 continue; // c:7011
17195 } else if !first {
17196 first = true; // c:7013
17197 continue;
17198 }
17199 }
17200 rest.push_str(unit);
17201 }
17202 }
17203 // c:7078-7091 — strip trailing IFS whitespace.
17204 emit(last, &rest[..ws_suffix_start(&rest)]);
17205 } else {
17206 // c:Src/builtin.c:6843 — `read VAR` single-var path strips
17207 // leading/trailing IFS-whitespace exactly like the multi-var
17208 // last-var fallback above. C's read loop discards leading
17209 // whitespace-IFS via `if (c == sep && IS_SPACE(c)) ... loop`
17210 // (c:6843) before building each word, then on word-end it
17211 // either emits the word or, on the last var, keeps building
17212 // until newline — at which point the trailing whitespace-IFS
17213 // gets discarded by the same skip. Without this, `read line`
17214 // on ` hello ` set `line` to ` hello ` (with spaces)
17215 // instead of `hello` — bug #247.
17216 let lead = &buf[ws_prefix_len(&buf)..];
17217 let trimmed = &lead[..ws_suffix_start(lead)];
17218 // Strip backslash-escape marks: `read x` on `a\ b` → x="a b".
17219 let trimmed = unmark(trimmed);
17220 // c:Src/builtin.c:7102-7109 — `-e` / `-E` flags. Both echo
17221 // the read content to stdout (`zputs(buf, stdout); putchar
17222 // ('\n')`); `-e` ALSO skips the setsparam (echo-only, no
17223 // assign), while `-E` echoes AND assigns. Bug #434.
17224 let opt_e = OPT_ISSET(ops, b'e');
17225 let opt_big_e = OPT_ISSET(ops, b'E');
17226 if opt_e || opt_big_e {
17227 println!("{}", trimmed);
17228 }
17229 if !opt_e {
17230 setsparam(&reply, &trimmed);
17231 }
17232 }
17233 // c:Src/builtin.c:6534 — partial-EOF post-assign exit.
17234 if partial_eof {
17235 return 1;
17236 }
17237 // c:Src/builtin.c:7116-7121 — "The following is to ensure a failure to
17238 // set the parameter causes a non-zero status return."
17239 // return errflag;
17240 // A readonly target makes setsparam zerr (errflag set); returning 0
17241 // left `(typeset -r foo; read foo) <<<bar` exiting 0.
17242 errflag.load(Relaxed)
17243}
17244
17245/// Port of `static char *zbuf;` from `Src/builtin.c:6397` — the buffer
17246/// `read -z` takes its input from (`None` is C's NULL). Unmetafied bytes
17247/// are not stored: the buffer keeps C's metafied text, and `zread` decodes
17248/// the `Meta` escapes as C does.
17249pub static zbuf: Mutex<Option<(Vec<u8>, usize)>> = Mutex::new(None); // c:6397
17250
17251/// Port of `static int readfd;` from `Src/builtin.c:6398` — the descriptor
17252/// `zread` reads from.
17253pub static readfd: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:6398
17254
17255/// Port of `zread()` from `Src/builtin.c:7134`.
17256/// C decl: `zread(int izle, int *readchar, long izle_timeout)`
17257/// C: `static int zread(int izle, int *readchar, long izle_timeout)` —
17258/// read one byte from ZLE, `zbuf` or `readfd`, honouring a pushed-back
17259/// `readchar`. Returns `EOF` (-1) at end of input.
17260pub fn zread(izle: i32, readchar: &mut i32, izle_timeout: i64) -> i32 {
17261 // c:7134
17262 let mut retry = false; // c:7136 `int retry = 0;`
17263
17264 if izle != 0 {
17265 // c:7141
17266 let mut c: i32 = 0; // c:7142
17267 let mut timeout_unused: i32 = 0; // c:7143 NULL
17268 crate::ported::zle::zle_main::zle_main_entry(
17269 crate::ported::zsh_h::ZLE_CMD_GET_KEY,
17270 &mut crate::ported::zle::zle_main::zle_main_entry_args::GetKey {
17271 do_keytmout: izle_timeout,
17272 timeout: &mut timeout_unused,
17273 chrp: &mut c,
17274 },
17275 ); // c:7143
17276 return if c < 0 { -1 } else { c }; // c:7145
17277 }
17278 // c:7147 — use zbuf if possible.
17279 if let Some((buf, pos)) = zbuf.lock().unwrap().as_mut() {
17280 // c:7151-7154 — `*zbuf == Meta` → next byte ^ 32.
17281 if buf.get(*pos).copied() == Some(Meta as u8) {
17282 *pos += 1;
17283 let b = buf.get(*pos).copied().unwrap_or(0) ^ 32;
17284 *pos += 1;
17285 return b as i32; // c:7152
17286 }
17287 return match buf.get(*pos).copied() {
17288 Some(b) if b != 0 => {
17289 *pos += 1;
17290 b as i32 // c:7154
17291 }
17292 _ => -1, // c:7154 EOF
17293 };
17294 }
17295 if *readchar >= 0 {
17296 // c:7156
17297 let cc = *readchar as u8; // c:7157
17298 *readchar = -1; // c:7158
17299 return cc as i32; // c:7159
17300 }
17301 loop {
17302 // c:7161 — read a character from readfd.
17303 let fd = readfd.load(Relaxed);
17304 let mut cc = [0u8; 1];
17305 let ret = unsafe { libc::read(fd, cc.as_mut_ptr() as *mut libc::c_void, 1) }; // c:7163
17306 match ret {
17307 1 => return cc[0] as i32, // c:7166
17308 -1 => {
17309 let e = io::Error::last_os_error().raw_os_error().unwrap_or(0);
17310 if !retry
17311 && fd == 0
17312 && (e == libc::EAGAIN || e == libc::EWOULDBLOCK)
17313 && crate::ported::utils::setblock_stdin() != 0
17314 {
17315 // c:7170-7181
17316 retry = true;
17317 continue;
17318 } else if e == libc::EINTR
17319 && !(errflag.load(Relaxed) != 0
17320 || RETFLAG.load(Relaxed) != 0
17321 || BREAKS.load(Relaxed) != 0
17322 || CONTFLAG.load(Relaxed) != 0)
17323 {
17324 continue; // c:7183
17325 }
17326 }
17327 _ => {}
17328 }
17329 return -1; // c:7187 EOF
17330 }
17331}
17332
17333/// Port of `testlex()` from Src/builtin.c:7200.
17334/// C: `void testlex(void)` — advance the test-builtin lexer one token
17335/// from `testargs` into `tok`/`tokstr`. Maps `-o`→DBAR, `-a`→DAMPER,
17336/// `!`→BANG, `(`→INPAR, `)`→OUTPAR, `<`→INANG, `>`→OUTANG, otherwise STRING.
17337/// `parse_cond` reaches it through the `condlex` pointer (parse.rs
17338/// `CONDLEX_TESTLEX`).
17339pub fn testlex() {
17340 use crate::ported::lex::{set_tok, set_tokstr, tok};
17341 use crate::ported::zsh_h::{
17342 BANG_TOK, DAMPER, DBAR, INANG_TOK, INPAR_TOK, LEXERR, NULLTOK, OUTANG_TOK, OUTPAR_TOK,
17343 STRING_LEX,
17344 };
17345 // c:7203
17346 if tok() == LEXERR {
17347 return;
17348 }
17349
17350 TESTARGS.with_borrow(|testargs| {
17351 let i = TESTARGS_IDX.get();
17352 CURTESTARG.set(i); // c:7205 — `curtestarg = testargs`
17353 set_tokstr(testargs.get(i).cloned()); // c:7205 — `tokstr = *curtestarg`
17354 let Some(arg) = testargs.get(i) else {
17355 // c:7206 — `if (!*testargs)`
17356 // c:7208 — if tok is already zero, reading past the end: error
17357 set_tok(if tok() != NULLTOK { NULLTOK } else { LEXERR });
17358 return;
17359 };
17360 set_tok(match arg.as_str() {
17361 "-o" => DBAR, // c:7211
17362 "-a" => DAMPER, // c:7213
17363 "!" => BANG_TOK, // c:7215
17364 "(" => INPAR_TOK, // c:7217
17365 ")" => OUTPAR_TOK, // c:7219
17366 "<" => INANG_TOK, // c:7221
17367 ">" => OUTANG_TOK, // c:7223
17368 _ => STRING_LEX, // c:7225
17369 });
17370 TESTARGS_IDX.set(i + 1); // c:7226 — `testargs++`
17371 });
17372}
17373
17374/// Port of `bin_test()` from `Src/builtin.c:7231`.
17375/// C decl: `bin_test(char *name, char **argv, UNUSED(Options ops), int func)`
17376/// C: `int bin_test(char *name, char **argv, UNUSED(Options ops), int func)`
17377/// — the `test` / `[` builtin: when invoked as `[`, requires a trailing
17378/// `]`; XSI-extension paren-stripping for 3/4-arg forms; the argv is
17379/// compiled to wordcode by `parse_cond` and run through `evalcond`,
17380/// returning 0/1/2.
17381/// WARNING: param names don't match C — Rust=(name, argv, func) vs C=(name, argv, ops, func)
17382pub fn bin_test(
17383 name: &str,
17384 argv: &[String], // c:7231
17385 _ops: &options,
17386 func: i32,
17387) -> i32 {
17388 use crate::ported::lex::tok;
17389 use crate::ported::parse::{parse_cond, CONDLEX_TESTLEX};
17390 use crate::ported::zsh_h::{estate, LEXERR, NULLTOK};
17391 // !!! DASH-FAITHFUL GATE (no C counterpart) !!! dash runs its own
17392 // `testcmd` grammar (src/bltin/test.c), not zsh's par_cond.
17393 if crate::dash_mode::dash_faithful() {
17394 return crate::dash_mode::dash_test(name, argv);
17395 }
17396 let mut argv = argv.to_vec();
17397 let mut sense = 0i32; // c:7236
17398
17399 // c:7239-7247 — `[` requires trailing `]`.
17400 if func == BIN_BRACKET {
17401 // c:7239
17402 if argv.is_empty() || argv.last().map(|s| s.as_str()) != Some("]") {
17403 // c:7241
17404 zwarnnam(name, "']' expected"); // c:7243
17405 return 2; // c:7244
17406 }
17407 argv.pop(); // c:7246 (s[-1] = NULL)
17408 }
17409
17410 // c:7249-7250 — empty argv → false (1).
17411 if argv.is_empty() {
17412 // c:7249
17413 return 1; // c:7250
17414 }
17415
17416 // !!! BASH/KSH-MODE (no C counterpart) !!! bash/ksh two-argument `test -a FILE`
17417 // (exists, same as `-e`) and `test -o OPTION` (option is on). In zsh's
17418 // grammar (c:7276+) both `-a` and `-o` are the binary AND / OR, so a lone
17419 // operand is "too many arguments".
17420 if (crate::dash_mode::bash_mode() || crate::dash_mode::korn_mode()) && argv.len() == 2 {
17421 if argv[0] == "-a" {
17422 argv[0] = "-e".to_string();
17423 } else if argv[0] == "-o" {
17424 let r = crate::ported::cond::optison(Some(name), &argv[1]);
17425 return if r == 0 { 0 } else { 1 };
17426 }
17427 }
17428
17429 // c:7257-7274 — XSI 3/4-arg parens + 4-arg `!` extension.
17430 let nargs = argv.len(); // c:7257
17431 if nargs == 3 || nargs == 4 {
17432 // c:7258
17433 // c:7264-7269 — strip `(` ... `)` parens unless the 3-arg middle
17434 // would be a binary op (which takes priority).
17435 if argv[0] == "(" && argv[nargs - 1] == ")" // c:7264
17436 && (nargs != 3 || crate::ported::text::is_cond_binary_op(&argv[1]) == 0)
17437 // c:7265
17438 {
17439 argv.pop(); // c:7266
17440 argv.remove(0); // c:7267
17441 }
17442 if nargs == 4 && argv[0] == "!" {
17443 // c:7270 — the `!` negation short-circuit fires ONLY at FOUR args
17444 // (`[ ! a = b ]` → negate the 3-arg binary test). At THREE args the
17445 // leading `!` is NOT stripped: it falls through to the grammar,
17446 // which applies the POSIX rule "if $2 is a binary operator, test $1
17447 // op $3" — so `[ ! = x ]` is the string comparison "!" = "x".
17448 sense = 1; // c:7272
17449 argv.remove(0); // c:7273
17450 }
17451 }
17452
17453 // c:7276-7281 — `zcontext_save(); testargs = argv; tok = NULLTOK;
17454 // condlex = testlex; testlex(); prog = parse_cond();
17455 // condlex = zshlex;`
17456 crate::ported::context::zcontext_save(); // c:7276
17457 let nargv = argv.len();
17458 TESTARGS.with_borrow_mut(|a| *a = argv); // c:7277 — testargs = argv
17459 TESTARGS_IDX.set(0);
17460 crate::ported::lex::set_tok(NULLTOK); // c:7278
17461 CONDLEX_TESTLEX.set(true); // c:7279 — condlex = testlex
17462 testlex(); // c:7280
17463 let prog = parse_cond(); // c:7281
17464 CONDLEX_TESTLEX.set(false); // c:7282 — condlex = zshlex
17465
17466 // c:7284-7288 — a COND_ERROR already printed its diagnostic.
17467 if errflag.load(Relaxed) != 0 {
17468 // c:7284
17469 errflag.fetch_and(!ERRFLAG_ERROR, Relaxed); // c:7285
17470 crate::ported::context::zcontext_restore(); // c:7286
17471 return 2; // c:7287
17472 }
17473 // c:7290-7294 — a bare YYERROR prints here. `tokstr` is NULL exactly when
17474 // the lexer ran off the end of the argument list.
17475 if prog.is_none() || tok() == LEXERR {
17476 // c:7290
17477 zwarnnam(
17478 name,
17479 if crate::ported::lex::tokstr().is_some() {
17480 "parse error"
17481 } else {
17482 "argument expected"
17483 },
17484 ); // c:7291
17485 crate::ported::context::zcontext_restore(); // c:7292
17486 return 2; // c:7293
17487 }
17488 crate::ported::context::zcontext_restore(); // c:7295
17489
17490 // c:7297-7300 — `if (*curtestarg)`: the grammar stopped before consuming
17491 // every argument.
17492 if CURTESTARG.get() < nargv {
17493 // c:7297
17494 zwarnnam(name, "too many arguments"); // c:7298
17495 return 2; // c:7299
17496 }
17497
17498 // c:7302-7308 — syntax is OK, so evaluate.
17499 let prog = prog.unwrap();
17500 let strs = prog.strs.clone();
17501 let mut state = estate {
17502 // c:7303 — `state.prog = prog; state.pc = prog->prog;`
17503 prog: Box::new(prog),
17504 pc: 0,
17505 strs, // c:7305 — `state.strs = prog->strs;`
17506 strs_offset: 0,
17507 };
17508
17509 // c:7307 — `ret = evalcond(&state, name);`. `name` is C's `fromtest`
17510 // signal — non-NULL means "called from test/[", which enables the strict
17511 // integer-expression error path (c:Src/cond.c:236-251). Bug #411.
17512 let mut ret = crate::ported::cond::evalcond(&mut state, Some(name)); // c:7307
17513
17514 // c:7308-7309 — `if (ret < 2 && sense) ret = !ret;`
17515 if ret < 2 && sense != 0 {
17516 // c:7308
17517 ret = if ret == 0 { 1 } else { 0 }; // c:7309
17518 }
17519 ret // c:7310
17520}
17521
17522/// Port of `bin_times()` from `Src/builtin.c:7324`.
17523/// C decl: `bin_times(UNUSED(char *name), UNUSED(char **argv), UNUSED(Options ops), UNUSED(int func))`
17524/// C: `int bin_times(UNUSED args)` — `times(&buf)`; print user/system
17525/// for self then for children, separated by spaces and newlines.
17526/// WARNING: param names don't match C — Rust=(_name, _argv, _func) vs C=(name, argv, ops, func)
17527pub fn bin_times(
17528 _name: &str,
17529 _argv: &[String], // c:7328
17530 _ops: &options,
17531 _func: i32,
17532) -> i32 {
17533 let mut buf: libc::tms = unsafe { std::mem::zeroed() }; // c:7331
17534 // c:7332 — `long clktck = get_clktck();`. The previous Rust port
17535 // inlined a `sysconf(_SC_CLK_TCK)` call here. Route through the
17536 // canonical `get_clktck()` port at jobs.rs:567 so any future
17537 // hardening (caching, error fallback) propagates to every caller.
17538 let clktck = crate::ported::jobs::get_clktck() as f64; // c:7332
17539 let clktck = if clktck <= 0.0 { 100.0 } else { clktck };
17540 // c:7335 — `if (times(&buf) == -1) return 1;`
17541 if unsafe { libc::times(&mut buf) } == (-1i64) as libc::clock_t {
17542 // c:7335
17543 return 1; // c:7336
17544 }
17545 let pttime = |t: libc::clock_t| {
17546 // c:Src/builtin.c:7315-7318 — `printf("%ldm%ld.%02lds", X/(60*clktck),
17547 // X/clktck%clktck, X*100/clktck%100)`. Integer-arithmetic
17548 // breakdown: minutes (no zero-pad), seconds (no zero-pad), and
17549 // centiseconds (2-digit zero-pad). Bug #499 — previous Rust
17550 // port used floating-point `%.3f` with 3-decimal precision,
17551 // diverging from zsh's `0m0.00s`.
17552 // ZSHRS-ONLY dispatch: the fraction width and seconds padding
17553 // differ per shell — bash prints milliseconds, dash microseconds,
17554 // mksh zero-pads the seconds — so a drop-in cannot use zsh's
17555 // centisecond form. `emulation_output::times_field` carries the
17556 // captured table; the zsh arm there is this c:7315-7318 formula.
17557 print!(
17558 "{}",
17559 crate::extensions::emulation_output::times_field(t as i64, clktck as i64)
17560 );
17561 };
17562 pttime(buf.tms_utime); // c:7332
17563 print!(" "); // c:7333
17564 pttime(buf.tms_stime); // c:7334
17565 println!(); // c:7335
17566 pttime(buf.tms_cutime); // c:7336
17567 print!(" "); // c:7337
17568 pttime(buf.tms_cstime); // c:7338
17569 println!(); // c:7339
17570 0 // c:7340
17571}
17572
17573/// Port of `bin_trap()` from `Src/builtin.c:7347`.
17574/// C decl: `bin_trap(char *name, char **argv, UNUSED(Options ops), UNUSED(int func))`
17575/// C: `int bin_trap(char *name, char **argv, ...)` — list, clear, or
17576/// set signal traps.
17577/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
17578pub fn bin_trap(
17579 name: &str,
17580 argv: &[String], // c:7347
17581 _ops: &options,
17582 _func: i32,
17583) -> i32 {
17584 // PFA-SMR aspect: record `trap HANDLER SIG...` calls. Skip
17585 // listing-only forms (`trap`, `trap -l`, `trap -p`) — those don't
17586 // mutate state.
17587 #[cfg(feature = "recorder")]
17588 if crate::recorder::is_enabled() {
17589 let listing = argv.is_empty() || (argv.len() == 1 && (argv[0] == "-l" || argv[0] == "-p"));
17590 if !listing && argv.len() >= 2 {
17591 let ctx = crate::recorder::recorder_ctx_global();
17592 let handler = &argv[0];
17593 for sig in &argv[1..] {
17594 crate::recorder::emit_trap(sig, handler, ctx.clone());
17595 }
17596 }
17597 }
17598
17599 if crate::dash_mode::bash_mode() {
17600 if let Some(rc) = crate::extensions::emulation_output::bash_trap_return(argv) {
17601 return rc;
17602 }
17603 }
17604
17605 // !!! PDKSH-FAMILY-ONLY (no C counterpart) !!! mksh's `trap` takes no options
17606 // at all (`--` excepted), and `trap` is a POSIX special builtin, so a usage
17607 // error aborts a non-interactive shell:
17608 // mksh -c 'trap -x; echo after' -> "trap: -x: unknown option", rc 1, no `after`
17609 // (also `-l`, `-p`, clusters like `-ZZ`, which report the first letter).
17610 if crate::dash_mode::posix_faithful() && crate::dash_mode::pdksh_family() {
17611 if let Some(bad) = argv
17612 .first()
17613 .filter(|a| a.len() > 1 && a.starts_with('-') && *a != "--")
17614 {
17615 zerrnam(name, &format!("-{}: unknown option", bad.chars().nth(1).unwrap_or('-')));
17616 return 1;
17617 }
17618 }
17619
17620 // !!! DASH-FAITHFUL GATE (no C counterpart) !!! dash's trap takes no
17621 // options (`trap -p`/`-x` -> "Illegal option", status 2) and a lone `-` is
17622 // an action with no signal ("bad trap", status 1).
17623 if crate::dash_mode::dash_faithful() {
17624 match argv.first().map(String::as_str) {
17625 Some("-") if argv.len() == 1 => {
17626 zwarnnam(name, "-: bad trap");
17627 return 1;
17628 }
17629 Some(a) if a.len() >= 2 && a.starts_with('-') && a != "--" => {
17630 zwarnnam(name, &format!("Illegal option {}", &a[..2]));
17631 // `trap` is a POSIX special builtin: the error ends a script.
17632 if !crate::ported::zsh_h::isset(crate::ported::zsh_h::INTERACTIVE) {
17633 EXIT_VAL.store(2, std::sync::atomic::Ordering::Relaxed);
17634 EXIT_PENDING.store(1, std::sync::atomic::Ordering::Relaxed);
17635 }
17636 return 2;
17637 }
17638 _ => {}
17639 }
17640 }
17641
17642 // ZSHRS-ONLY. `trap -l` is bash's spelling of `kill -l` and lists the
17643 // signals; the Korn and Bourne shells REJECT it ("trap: -l: unknown
17644 // option", exit 2) and zsh treats `-l` as a trap body with no signal
17645 // (c:Src/builtin.c:7382-7388: `signal expected`, rc 1). zshrs matched
17646 // zsh in every mode, so `--bash` printed nothing where bash prints
17647 // the signal table, and the POSIX drop-ins succeeded where they
17648 // should fail.
17649 if argv.len() == 1 && argv[0] == "-l" {
17650 use crate::extensions::emulation_startup::{personality, Personality};
17651 match personality() {
17652 Personality::Bash => {
17653 let sigs = crate::extensions::emulation_output::kill_l_signals();
17654 if let Some(rendered) = crate::extensions::emulation_output::kill_list(&sigs) {
17655 print!("{rendered}");
17656 }
17657 return 0;
17658 }
17659 // Both Korn lines reject it; they differ only in the status
17660 // (ksh93u+m exits 2, mksh exits 1).
17661 Personality::Ksh93 => {
17662 zwarnnam(name, "-l: unknown option");
17663 return 2;
17664 }
17665 Personality::Mksh | Personality::Pdksh => {
17666 zwarnnam(name, "-l: unknown option");
17667 return 1;
17668 }
17669 Personality::Dash | Personality::Sh => {
17670 zwarnnam(name, "Illegal option -l");
17671 return 2;
17672 }
17673 // zsh and csh fall through to C's `signal expected`.
17674 _ => {}
17675 }
17676 }
17677
17678 // !!! EMULATION-ONLY (no C counterpart) !!! ksh93 and bash spell the trap
17679 // listing `trap -p`; zsh has no such option (it reaches the bare-body path
17680 // below). Measured: `ksh -c 'trap "print a" INT; trap -p'` lists the trap.
17681 // mksh has no listing option at all: `mksh -c 'trap -p'` -> "trap: -p: unknown option", 1.
17682 if argv.len() == 1
17683 && argv[0] == "-p"
17684 && matches!(
17685 crate::extensions::emulation_startup::personality(),
17686 crate::extensions::emulation_startup::Personality::Mksh
17687 | crate::extensions::emulation_startup::Personality::Pdksh
17688 )
17689 {
17690 zwarnnam(name, "-p: unknown option");
17691 return 1;
17692 }
17693 // !!! EMULATION-ONLY (no C counterpart) !!! ksh93 `trap -p SIG...` prints just the
17694 // ACTION text of each named signal's trap, one per line (nothing for an unset one):
17695 // ksh -c 'trap "print a" INT; trap -p INT' -> print a
17696 if argv.len() > 1
17697 && argv[0] == "-p"
17698 && crate::extensions::emulation_startup::personality()
17699 == crate::extensions::emulation_startup::Personality::Ksh93
17700 {
17701 let traps = traps_table().lock().map(|t| t.clone()).unwrap_or_default();
17702 for sig in &argv[1..] {
17703 let idx = getsigidx(sig);
17704 if let Some((_, body)) = traps.iter().find(|(k, _)| getsigidx(k) == idx) {
17705 println!("{body}");
17706 }
17707 }
17708 return 0;
17709 }
17710 let list_via_p = argv.len() == 1
17711 && argv[0] == "-p"
17712 && matches!(
17713 crate::extensions::emulation_startup::personality(),
17714 crate::extensions::emulation_startup::Personality::Ksh93
17715 | crate::extensions::emulation_startup::Personality::Bash
17716 );
17717 // bash `trap -p SIG...` lists only the named signals' traps.
17718 let p_filter: Option<Vec<i32>> = (argv.len() > 1
17719 && argv[0] == "-p"
17720 && matches!(
17721 crate::extensions::emulation_startup::personality(),
17722 crate::extensions::emulation_startup::Personality::Bash
17723 ))
17724 .then(|| {
17725 argv[1..]
17726 .iter()
17727 .map(|s| if s == "RETURN" { i32::MAX } else { getsigidx(s) })
17728 .collect()
17729 });
17730 let argv: &[String] = if list_via_p || p_filter.is_some() {
17731 &[]
17732 } else {
17733 argv
17734 };
17735 let mut argv = argv.to_vec();
17736 // c:7353 — `if (*argv && !strcmp(*argv, "--")) argv++;`
17737 if !argv.is_empty() && argv[0] == "--" {
17738 // c:7353
17739 argv.remove(0); // c:7354
17740 }
17741
17742 // c:7357-7380 — no args: list current traps.
17743 if argv.is_empty() {
17744 // c:7357
17745 // Local enum for the merged function-form / string-form
17746 // listing walker below. RUST-ONLY scoping helper for the
17747 // c:Src/builtin.c:7359-7375 if/else-if structure.
17748 enum TrapEntry {
17749 Func(String),
17750 Str(String, String),
17751 }
17752 queue_signals(); // c:7358
17753 let traps = traps_table().lock().map(|t| t.clone()).unwrap_or_default();
17754 // c:Src/builtin.c:7359-7375 — C walks `sigtrapped[]` by signal
17755 // number index, so the listing is naturally sorted by sig num
17756 // (HUP=1, INT=2, …, USR1=30, …). zshrs stores traps in a
17757 // HashMap<name, body> whose iteration order is non-deterministic
17758 // — bug #68 in docs/BUGS.md: `trap` output came out in random
17759 // order vs zsh's stable signum-sorted view. Sort by getsigidx
17760 // before printing so the iteration matches C's array walk.
17761 //
17762 // c:Src/builtin.c:7360-7365 — `if (sigtrapped[sig] & ZSIG_FUNC)
17763 // { hn = gettrapnode(sig, 0); shfunctab->printnode(hn, 0); }`.
17764 // Walk shfunctab for `TRAP<signame>` functions and emit their
17765 // full definition alongside the string-form traps so the
17766 // listing matches zsh. Bug #461 in docs/BUGS.md.
17767 let trap_funcs: Vec<(String, i32, String)> = {
17768 let mut acc: Vec<(String, i32, String)> = Vec::new();
17769 if let Ok(tab) = crate::ported::hashtable::shfunctab_lock().read() {
17770 for (fname, _) in tab.iter() {
17771 if let Some(sig_name) = fname.strip_prefix("TRAP") {
17772 let idx = getsigidx(sig_name);
17773 if idx != -1 {
17774 acc.push((fname.clone(), idx, sig_name.to_string()));
17775 }
17776 }
17777 }
17778 }
17779 acc
17780 };
17781 // Merge function-form + string-form, sort by sig idx
17782 // (function-form FIRST per zsh ordering — c:Src/builtin.c:7359
17783 // walks each signal once; ZSIG_FUNC takes precedence within a
17784 // single signum slot since the C code uses `else if` between
17785 // the two arms).
17786 let mut combined: Vec<(i32, TrapEntry)> = Vec::new();
17787 for (fname, idx, _sig) in &trap_funcs {
17788 combined.push((*idx, TrapEntry::Func(fname.clone())));
17789 }
17790 for (sig, body) in traps.iter() {
17791 let idx = getsigidx(sig);
17792 // Skip if a function-form already exists for this sig
17793 // (C's `else if` semantics — function-form wins).
17794 if trap_funcs.iter().any(|(_, i, _)| *i == idx) {
17795 continue;
17796 }
17797 combined.push((
17798 if idx == -1 { i32::MAX } else { idx },
17799 TrapEntry::Str(sig.clone(), body.clone()),
17800 ));
17801 }
17802 // c:7358-7361 — `} else if (sigtrapped[sig]) { if (!siglists[sig])
17803 // printf("trap -- '' %s\n", name); ... }`
17804 // C walks the sigtrapped[] ARRAY, so a signal whose trap state is
17805 // set but which has NO body still lists, as `trap -- '' SIG`. The
17806 // canonical case is an INHERITED SIG_IGN recorded at startup
17807 // (c:Src/init.c:1444-1445 sets sigtrapped[SIGQUIT] = ZSIG_IGNORED
17808 // under nohup / a `trap '' QUIT` parent). zshrs builds this listing
17809 // from the name->body table plus shfunctab, neither of which holds a
17810 // body-less entry, so those signals were invisible: `nohup zshrs -fc
17811 // trap` printed nothing where zsh printed `trap -- '' QUIT`.
17812 {
17813 let states: Vec<(usize, i32)> = match crate::ported::signals::sigtrapped.lock() {
17814 Ok(g) => g.iter().copied().enumerate().collect(),
17815 Err(_) => Vec::new(),
17816 };
17817 for (sig, state) in states {
17818 // c:7358 `else if (sigtrapped[sig])`
17819 if state == 0 || (state & crate::ported::zsh_h::ZSIG_FUNC) != 0 {
17820 continue;
17821 }
17822 let idx = sig as i32;
17823 // Already covered by a function-form or a stored body.
17824 if combined.iter().any(|(i, _)| *i == idx) {
17825 continue;
17826 }
17827 let name = crate::ported::jobs::getsigname(idx); // c:7359
17828 if name.is_empty() {
17829 continue;
17830 }
17831 if traps.contains_key(&name) {
17832 continue;
17833 }
17834 // c:7360-7361 — no siglists[sig] body → empty-body entry.
17835 combined.push((idx, TrapEntry::Str(name, String::new())));
17836 }
17837 }
17838 if let Some(f) = &p_filter {
17839 combined.retain(|(i, _)| f.contains(i));
17840 }
17841 combined.sort_by_key(|(idx, _)| *idx);
17842 // !!! EMULATION-ONLY (no C counterpart) !!! ksh93 walks the trap
17843 // table downwards; c:7358 and every other shell walk it upwards.
17844 // See dash_mode::trap_listing_descending (false in --zsh).
17845 if crate::dash_mode::trap_listing_descending() {
17846 combined.reverse();
17847 }
17848 for (_idx, entry) in &combined {
17849 match entry {
17850 TrapEntry::Func(fname) => {
17851 if let Ok(tab) = crate::ported::hashtable::shfunctab_lock().read() {
17852 if let Some(shf) = tab.get(fname) {
17853 crate::ported::hashtable::printshfuncnode(shf, 0);
17854 }
17855 }
17856 }
17857 TrapEntry::Str(sig, body) => {
17858 // !!! EMULATION-ONLY (no C counterpart) !!! bash, dash,
17859 // ash, /bin/sh, ksh93 and mksh all echo the RAW trap body
17860 // instead of re-rendering the compiled Eprog, quote it
17861 // unconditionally (except the Korn pair), and — bash only
17862 // — print the signal name with a `SIG` prefix. Measured on
17863 // the real binaries; see dash_mode::trap_listing_line.
17864 if let Some(line) = crate::dash_mode::trap_listing_line(sig, body) {
17865 println!("{}", line);
17866 continue;
17867 }
17868 // c:7371 — `s = getpermtext(siglists[sig], NULL, 0);`. C
17869 // holds the body as a compiled Eprog and renders it back
17870 // to source for the listing, so what prints is CANONICAL
17871 // text rather than the string the user typed:
17872 // `trap 'print a; print b' EXIT` lists as
17873 // `trap -- $'print a\nprint b' EXIT` (separators become
17874 // newlines, `if`/`for`/`(` bodies get re-indented).
17875 // zshrs stores the body as raw text, so deparse it here
17876 // the same way. Rendering only at listing time keeps the
17877 // stored text authoritative for execution — a getpermtext
17878 // rendering bug then costs a wrong listing, not a wrong
17879 // trap body.
17880 //
17881 // An empty body takes C's `!siglists[sig]` branch
17882 // (c:7368-7369, prints `trap -- '' SIG`) and is never
17883 // rendered. parse_string is the wordcode parser, whose
17884 // coverage is narrower than the AST parser that executes
17885 // the body (e.g. `(pat)` case arms — see the pin test in
17886 // text.rs), so fall back to the raw text on a parse
17887 // failure rather than dropping the entry.
17888 let rendered = if body.is_empty() {
17889 body.clone()
17890 } else {
17891 crate::ported::exec::parse_string(body, 1)
17892 .map(|p| crate::ported::text::getpermtext(Box::new(p), None, 0))
17893 .unwrap_or_else(|| body.clone())
17894 };
17895 // c:7370-7375 — `printf("trap -- "); quotedzputs(...); printf(" %s\n", name);`
17896 print!("trap -- "); // c:7372
17897 print!("{}", quotedzputs(&rendered)); // c:7373
17898 println!(" {}", sig); // c:7374
17899 }
17900 }
17901 }
17902 unqueue_signals(); // c:7378
17903 return 0; // c:7379
17904 }
17905
17906 // c:7384-7400 — first arg is signal number / single `-` → clear.
17907 let first = &argv[0];
17908 if getsigidx(first) != -1 || first == "-" {
17909 // c:7384
17910 let start = if first == "-" { 1 } else { 0 }; // c:7385
17911 // c:7399 — `return *argv != NULL;`. After a successful loop
17912 // *argv is the trailing NULL (Rust: idx == len after the
17913 // walk); on `break` due to an undefined signal *argv is the
17914 // bad arg (idx < len). Previous Rust port hardcoded
17915 // `return 0`, so `trap - INVALID` would silently report
17916 // success and downstream scripts couldn't detect the bad
17917 // signal name.
17918 let mut had_error = 0i32;
17919 // c:7386-7398 — build the signal list, then run ONE
17920 // `unsettrap` body over it. `trap -` clears every slot
17921 // (c:7387-7388: `for (sig = 0; sig < TRAPCOUNT; sig++)
17922 // unsettrap(sig);`); `trap - SIG…` clears the named ones and
17923 // stops at the first undefined name (c:7390-7398), leaving
17924 // the already-walked signals cleared exactly as C does.
17925 let mut to_clear: Vec<i32> = Vec::new();
17926 if start >= argv.len() {
17927 // c:7386
17928 to_clear.extend(0..crate::ported::signals_h::TRAPCOUNT); // c:7387
17929 } else {
17930 for arg in &argv[start..] {
17931 // c:7390
17932 let sig = getsigidx(arg);
17933 if sig == -1 {
17934 // c:7392
17935 zwarnnam(name, &format!("undefined signal: {}", arg)); // c:7393
17936 had_error = 1; // c:7399 *argv non-NULL on break
17937 break; // c:7394
17938 }
17939 to_clear.push(sig);
17940 }
17941 }
17942 for sig in to_clear {
17943 // c:7388 / c:7397 — `unsettrap(sig)`. C's unsettrap
17944 // (c:Src/signals.c:759) → removetrap (c:Src/signals.c:772)
17945 // does three things the previous zshrs clear path skipped
17946 // by only doing `traps_table().remove()`:
17947 // - c:Src/signals.c:800 `sigtrapped[sig] = 0`, so
17948 // handletrap stops claiming the signal;
17949 // - c:Src/signals.c:815 `signal_default(sig)`, which
17950 // RESTORES THE DEFAULT DISPOSITION — this is why
17951 // `trap 'print caught' USR2; kill -USR2 $$; trap - USR2;
17952 // kill -USR2 $$` kills the shell in zsh instead of
17953 // silently ignoring the second signal;
17954 // - c:Src/signals.c:836-843 `removehashnode(shfunctab,
17955 // node->nam)`, dropping the `TRAP<sig>` function-form
17956 // trap (freed by unsettrap at c:765).
17957 // unsettrap MUST run FIRST: removetrap → dosavetrap (c:774)
17958 // snapshots the CURRENT body for the LOCAL_TRAPS restore.
17959 crate::ported::signals::unsettrap(sig);
17960 // c:Src/signals.c:846 `siglists[sig] = NULL` — the body
17961 // drop. zshrs keeps trap bodies in `traps_table` keyed by
17962 // canonical signal NAME (C indexes siglists[] by number),
17963 // so the number has to be mapped back to the name bin_trap
17964 // installed it under (`0` → `EXIT`).
17965 let key = if sig == 0 {
17966 "EXIT".to_string()
17967 } else {
17968 crate::ported::signals_h::sigs_name(sig)
17969 .map(|s| s.to_string())
17970 .unwrap_or_else(|| sig.to_string())
17971 };
17972 if let Ok(mut t) = traps_table().lock() {
17973 t.remove(&key);
17974 }
17975 // c:Src/signals.c:836-843 — ZSIG_FUNC arm.
17976 if let Ok(mut tab) = crate::ported::hashtable::shfunctab_lock().write() {
17977 tab.remove(&format!("TRAP{}", key));
17978 }
17979 }
17980 return had_error; // c:7399
17981 }
17982
17983 // c:7404-7411 — first arg is the trap body.
17984 let arg = argv.remove(0); // c:7404
17985 // c:7405-7409 — `if (!*arg) prog = &dummy_eprog; else if (!(prog =
17986 // parse_string(arg, 1))) { zwarnnam(name, "couldn't parse trap
17987 // command"); return 1; }`. The body is parsed when the trap is
17988 // installed, so an unparseable body is rejected here rather than
17989 // deferred to signal delivery. An empty body (`trap '' SIG`) takes
17990 // C's dummy_eprog branch and is never parsed. This gate precedes
17991 // the signal checks below, so `trap 'for' BOGUS` reports the parse
17992 // failure, not "undefined signal".
17993 //
17994 // C reaches its parser through parse_string (ported at
17995 // exec.rs:parse_string over the ported lexer). The trap body is
17996 // stored as text and later run through parse_isolated by
17997 // execute_script_zsh_pipeline (vm_helper.rs:2249), so validation
17998 // goes through the same parser that will execute it — otherwise a
17999 // body could pass here and still fail at delivery. Save/restore
18000 // errflag around the probe exactly as vm_helper.rs:2240-2251 does,
18001 // so a rejected body doesn't leave the error flag set and abort the
18002 // enclosing shell. The parser emits its own `parse error` via zerr
18003 // first; this adds C's second diagnostic.
18004 if !arg.is_empty() {
18005 let saved_errflag = errflag.load(Ordering::Relaxed);
18006 errflag.fetch_and(!crate::ported::zsh_h::ERRFLAG_ERROR, Ordering::Relaxed);
18007 let _ = crate::vm_helper::parse_isolated(&arg);
18008 let parse_failed =
18009 (errflag.load(Ordering::Relaxed) & crate::ported::zsh_h::ERRFLAG_ERROR) != 0;
18010 errflag.store(saved_errflag, Ordering::Relaxed);
18011 if parse_failed {
18012 zwarnnam(name, "couldn't parse trap command"); // c:7407
18013 return 1; // c:7408
18014 }
18015 }
18016 if argv.is_empty() {
18017 // c:7382-7388 (upstream a3547fd4c1, 54013) — a body with no
18018 // signal after it is an error: `undefined signal: ARG` when
18019 // the lone arg looks like a signal (numeric or SIG-prefixed),
18020 // `signal expected` otherwise. zsh 5.9.2 accepted the bare
18021 // body silently; master rejects it (`trap -l`, `trap -p` and
18022 // `trap cmd` all take this branch).
18023 if arg.starts_with("SIG") || arg.chars().next().is_some_and(|c| c.is_ascii_digit()) {
18024 zwarnnam(name, &format!("undefined signal: {}", arg)); // c:7384
18025 } else {
18026 zwarnnam(name, "signal expected"); // c:7386
18027 }
18028 return 1; // c:7387
18029 }
18030
18031 // c:7421-7448 — install trap on each named signal.
18032 let mut trap_install_error = 0i32;
18033 for sigarg in &argv {
18034 // c:7421
18035 let sig = getsigidx(sigarg);
18036 if sig == -1 {
18037 // c:7426
18038 zwarnnam(name, &format!("undefined signal: {}", sigarg)); // c:7427
18039 trap_install_error = 1; // c:7445 *argv non-NULL on break
18040 break; // c:7428
18041 }
18042 // !!! DASH-STRICT GATE (no C counterpart) !!! dash's trap table is the
18043 // real signals plus EXIT/0: the zsh pseudo-signals ERR/ZERR/DEBUG and the
18044 // `SIG`-prefixed spellings are "bad trap" (status 1, rest of the list skipped).
18045 if crate::dash_mode::dash_strict()
18046 && (sig == crate::ported::signals_h::SIGZERR
18047 || sig == crate::ported::signals_h::SIGDEBUG
18048 || sigarg.to_ascii_uppercase().starts_with("SIG"))
18049 {
18050 zwarnnam(name, &format!("{}: bad trap", sigarg));
18051 trap_install_error = 1;
18052 break;
18053 }
18054 // c:Src/signals.c — C zsh stores traps in a fixed array
18055 // indexed by signal number. Aliases (`0`, `EXIT`, `SIGEXIT`)
18056 // all resolve to index 0 and share the same slot. The Rust
18057 // port stores by name string, so we must normalize the key
18058 // to the canonical signal name (or "EXIT" for the 0 alias)
18059 // — otherwise `trap 'echo bye' 0` lands in a `"0"` slot
18060 // that nothing else looks up.
18061 let canonical = if sig == 0 {
18062 "EXIT".to_string()
18063 } else if let Some(name) = crate::ported::signals_h::sigs_name(sig) {
18064 // c:Src/builtin.c — when sigarg is numeric (`trap "" 2`),
18065 // the listing path must still emit the SYMBOLIC name
18066 // (`INT`) so the canonical name lookup downstream works.
18067 // Previously zshrs's `to_uppercase()` of "2" left the key
18068 // as "2", so `trap "" 2; trap` listed `trap -- '' 2`
18069 // instead of `trap -- '' INT`. Resolve to the canonical
18070 // signal name via sigs_name(idx).
18071 //
18072 // c:Src/jobs.c:2740 alt_sigs + jobs.c:3089 getsigname — a trap
18073 // set via an ALIAS name lists under the ALIAS, not the
18074 // canonical sigs[] name (C flags it ZSIG_ALIAS). The only
18075 // unconditional alt_sigs entry is `{ "ERR", SIGZERR }`, so
18076 // `trap … ERR; trap` must print `… ERR`, not `… ZERR`. The
18077 // Rust port stores traps by name string; preserve the alias
18078 // the user typed as the key. The dotrap dispatch already
18079 // resolves SIGZERR through both "ZERR" and "ERR"
18080 // (signals.rs:1505), so firing is unaffected.
18081 let normalized = sigarg
18082 .strip_prefix("SIG")
18083 .or_else(|| sigarg.strip_prefix("sig"))
18084 .unwrap_or(sigarg.as_str())
18085 .to_uppercase();
18086 if sig == crate::ported::signals_h::SIGZERR && normalized == "ERR" {
18087 "ERR".to_string()
18088 } else {
18089 name.to_string()
18090 }
18091 } else {
18092 // Strip SIG/sig prefix and uppercase so `SIGINT` / `int`
18093 // / `INT` all map to the same key.
18094 sigarg
18095 .strip_prefix("SIG")
18096 .or_else(|| sigarg.strip_prefix("sig"))
18097 .unwrap_or(sigarg.as_str())
18098 .to_uppercase()
18099 };
18100 // c:Src/signals.c settrap — register both the libc signal
18101 // handler AND the sigtrapped[idx] flag. Without setting
18102 // sigtrapped, handletrap() early-returns 0 (sees the slot
18103 // as "not trapped") and the dotrap dispatch never fires.
18104 // The traps_table entry alone isn't enough — handletrap
18105 // gates on sigtrapped[idx] != 0.
18106 //
18107 // c:Src/signals.c:693 settrap flag semantics:
18108 // ZSIG_FUNC → trap body is a shell function (TRAPSIG)
18109 // no flag → trap body is an Eprog in siglists[sig]
18110 // zshrs stores the body STRING in `traps_table` instead of
18111 // either, but the runtime's `endtrapscope`/`dotrap` dispatch
18112 // (signals.rs:1249) requires the !ZSIG_FUNC arm to fire so
18113 // the traps_table lookup happens. Passing ZSIG_FUNC sent it
18114 // down the shfunc-dispatch arm which fails silently when no
18115 // TRAPSIG function exists — bug #80 in docs/BUGS.md.
18116 //
18117 // SIGEXIT (sig=0) must also reach settrap so locallevel
18118 // tagging lands and `endtrapscope` fires the function-local
18119 // EXIT trap at function exit (the previous `sig > 0` gate
18120 // skipped it entirely).
18121 //
18122 // CRITICAL ORDER: settrap calls unsettrap → dosavetrap which
18123 // SNAPSHOTS the current traps_table body for the save-stack.
18124 // settrap MUST run BEFORE inserting the new body so the saved
18125 // entry captures the OUTER scope's body, not the one we're
18126 // about to install. (Reversed order led to the save loop
18127 // restoring the just-installed body back into traps_table
18128 // after the function exit, causing the script-end trap fire
18129 // to re-run the inner body.)
18130 // c:7429 — C calls `settrap(sig, prog, 0)` for EVERY valid trap
18131 // index, pseudo-signals included: `sig < TRAPCOUNT` is the only
18132 // bound (c:7421-7448). The `sig <= SIGCOUNT` cap here excluded
18133 // ZERR (SIGCOUNT+1) and DEBUG (SIGCOUNT+2), so those two never
18134 // reached settrap → unsettrap → removetrap → dosavetrap and were
18135 // therefore never saved/restored by the LOCAL_TRAPS scope stack:
18136 // `f() { setopt localtraps; trap '…' DEBUG }; f` left the DEBUG
18137 // trap armed for the rest of the shell (C05debug:1,5,6).
18138 // settrap itself already gates install_handler/signal_ignore on
18139 // `sig != 0 && sig <= SIGCOUNT` (c:714-736), so widening the
18140 // bound here touches no libc disposition.
18141 if sig >= 0 && sig < crate::ported::signals_h::TRAPCOUNT && sig != libc::SIGCHLD as i32 {
18142 // c:Src/signals.c:712 — `if (!(flags & ZSIG_FUNC) && empty_eprog(l))`.
18143 // settrap treats an `l == None` (or empty Eprog) body as
18144 // `trap '' SIG` — i.e. ZSIG_IGNORED + signal_ignore(sig).
18145 // That suppresses the signal entirely AND skips
18146 // install_handler, so a non-empty `trap "echo TRAP" SIG`
18147 // would never reach handletrap → dotrap. C-zsh's bin_trap
18148 // parses the body string into an Eprog via parse_string()
18149 // before passing to settrap. The zshrs port stores the
18150 // body as plain text in `traps_table` and dispatches via
18151 // execute_script (the Eprog parser isn't on the
18152 // critical path), so we need a placeholder Eprog whose
18153 // only purpose is to make empty_eprog() return false and
18154 // steer settrap down the ZSIG_TRAPPED + install_handler
18155 // branch. The placeholder body is never executed — dotrap
18156 // reads the dispatch text from traps_table.
18157 //
18158 // Bug #104 in docs/BUGS.md: signal sent from inside a
18159 // function via `kill -X $$` was lost because settrap had
18160 // routed USR1 to signal_ignore at trap-install time, so
18161 // when the signal hit while doshfunc had queue_signals()
18162 // active, nothing in the queue → nothing to dispatch on
18163 // unqueue. Empty-body path (`trap '' SIG`) still passes
18164 // None as before so the ZSIG_IGNORED branch fires.
18165 let body_eprog: Option<crate::ported::zsh_h::Eprog> = if arg.is_empty() {
18166 None
18167 } else {
18168 let mut prog = crate::ported::zsh_h::eprog::default();
18169 // c:Src/parse.c:584 — empty_eprog returns true on
18170 // `prog[0] == WCB_END`; push any non-WCB_END value
18171 // (1 is arbitrary, matches the existing
18172 // `empty_eprog_non_empty_non_end_returns_false`
18173 // pin test in parse.rs).
18174 prog.prog.push(1);
18175 Some(Box::new(prog))
18176 };
18177 settrap(sig, body_eprog, 0);
18178 }
18179 // c:Src/signals.c:707 — settrap → unsettrap → removetrap
18180 // (c:836-843 removeshfuncnode) drops any pre-existing
18181 // FUNCTION-form trap for this signal BEFORE the new (string-form)
18182 // body is installed. zshrs's removetrap does not touch shfunctab,
18183 // so a leftover `TRAPZERR() { … }` would shadow a later `trap
18184 // 'body' ERR` — the dispatch prefers the function form, so the OLD
18185 // function fired instead of the new string body. Remove it here,
18186 // mirroring the `trap -` unset path above. Keyed by the CANONICAL
18187 // signal name (sigs_name), so both `ERR` and `ZERR` spellings drop
18188 // the same `TRAPZERR` node. Runs for VIRTUAL signals too (ZERR /
18189 // DEBUG / EXIT sit beyond SIGCOUNT, so the settrap block above is
18190 // skipped for them — the earlier placement inside that block never
18191 // fired for ZERR).
18192 let func_key = if sig == 0 {
18193 "EXIT".to_string()
18194 } else {
18195 crate::ported::signals_h::sigs_name(sig)
18196 .map(|s| s.to_string())
18197 .unwrap_or_else(|| sig.to_string())
18198 };
18199 if let Ok(mut tab) = crate::ported::hashtable::shfunctab_lock().write() {
18200 tab.remove(&format!("TRAP{}", func_key));
18201 }
18202 if let Ok(mut t) = traps_table().lock() {
18203 t.insert(canonical.clone(), arg.clone()); // c:7448 (effective)
18204 }
18205 // c:Src/signals.c::settrap — when a string-form trap
18206 // replaces an existing trap, settrap calls unsettrap which
18207 // clears the C `sigfuncs[sig]` shfunc slot. The zshrs port
18208 // dispatches function-form TRAPxxx through shfunctab
18209 // independently of sigfuncs, so a pre-existing TRAPxxx
18210 // function survives the string-form install and BOTH fire
18211 // on signal delivery. Remove any matching TRAPxxx shfunc
18212 // here so dotrap dispatches the just-installed string body
18213 // only. Bug #541 in docs/BUGS.md.
18214 let trap_fn_name = format!("TRAP{}", canonical);
18215 if let Ok(mut tab) = crate::ported::hashtable::shfunctab_lock().write() {
18216 tab.remove(&trap_fn_name);
18217 }
18218 }
18219 trap_install_error
18220}
18221
18222/// Port of `bin_ttyctl()` from `Src/builtin.c:7454`.
18223/// C decl: `bin_ttyctl(UNUSED(char *name), UNUSED(char **argv), Options ops, UNUSED(int func))`
18224/// C: `int bin_ttyctl(UNUSED args, Options ops, ...)` — `-f` freezes the
18225/// tty, `-u` unfreezes; otherwise emit `"tty is [not ]frozen"`.
18226/// WARNING: param names don't match C — Rust=(_name, _argv, _func) vs C=(name, argv, ops, func)
18227pub fn bin_ttyctl(
18228 _name: &str,
18229 _argv: &[String], // c:7454
18230 ops: &options,
18231 _func: i32,
18232) -> i32 {
18233 use std::sync::Mutex;
18234 // c:7456-7461 — route through the canonical jobs::TTYFROZEN
18235 // global. The previous builtin.rs duplicate AtomicI32 NEVER synced
18236 // with jobs.rs's Mutex<i32> store; `ttyctl -f` set the local
18237 // Atomic but didn't freeze the tty from the perspective of the
18238 // job-control wait path that reads jobs::TTYFROZEN.
18239 let cell = crate::ported::jobs::TTYFROZEN.get_or_init(|| Mutex::new(0_i32));
18240 if OPT_ISSET(ops, b'f') {
18241 // c:7456
18242 *cell.lock().expect("TTYFROZEN poisoned") = 1; // c:7457
18243 } else if OPT_ISSET(ops, b'u') {
18244 // c:7458
18245 *cell.lock().expect("TTYFROZEN poisoned") = 0; // c:7459
18246 } else {
18247 let f = *cell.lock().expect("TTYFROZEN poisoned");
18248 // c:7461 — `printf("tty is %sfrozen\n", ttyfrozen ? "" : "not ");`
18249 println!("tty is {}frozen", if f != 0 { "" } else { "not " }); // c:7461
18250 }
18251 0 // c:7463
18252}
18253
18254/// Port of `bin_let()` from `Src/builtin.c:7469`.
18255/// C decl: `bin_let(UNUSED(char *name), char **argv, UNUSED(Options ops), UNUSED(int func))`
18256/// C: `int bin_let(UNUSED(char *name), char **argv, UNUSED(Options ops),
18257/// UNUSED(int func))` — evaluate each arg as a math expression;
18258/// return 1 if the final value is zero (success/false), 0 if non-zero
18259/// (true), 2 on math error.
18260/// WARNING: param names don't match C — Rust=(_name, argv, _func) vs C=(name, argv, ops, func)
18261pub fn bin_let(
18262 _name: &str,
18263 argv: &[String], // c:7469
18264 _ops: &options,
18265 _func: i32,
18266) -> i32 {
18267 use crate::ported::utils::{errflag, ERRFLAG_ERROR};
18268 use std::sync::atomic::Ordering;
18269
18270 // c:7472 — `mnumber val = zero_mnumber;`
18271 let mut val: mnumber = mnumber {
18272 l: 0,
18273 d: 0.0,
18274 type_: MN_INTEGER,
18275 }; // c:7472
18276 // c:7474-7475 — `while (*argv) val = matheval(*argv++);` — DO walk
18277 // every arg even if one fails. C doesn't break on error mid-loop;
18278 // it just lets errflag accumulate. Previously the Rust port broke
18279 // on first failure, leaving later args unevaluated.
18280 for expr in argv {
18281 // c:7474
18282 match matheval(expr) {
18283 Ok(v) => {
18284 // c:7475 — `val = matheval(*argv++)`; matheval returns zero when
18285 // the evaluation raised an error (math.c: `if (errflag) { xyval.type
18286 // = MN_INTEGER; xyval.u.l = 0; }`), e.g. an unset parameter under
18287 // NO_UNSET still yields a value from the expression walker here.
18288 val = if (errflag.load(Relaxed) & ERRFLAG_ERROR) != 0 {
18289 mnumber { l: 0, d: 0.0, type_: MN_INTEGER }
18290 } else {
18291 v
18292 };
18293 }
18294 Err(msg) => {
18295 // c:Src/math.c:checkunary zerr side-effect — the C
18296 // path writes the parse-error string to stderr via
18297 // mathevali → checkunary → zerr. Rust's matheval
18298 // captures the message in Err and bin_let was
18299 // discarding it via `if let Ok(...)`. Surface it.
18300 zerr(&msg);
18301 // c:7475 — a failed matheval yields zero_mnumber.
18302 val = mnumber { l: 0, d: 0.0, type_: MN_INTEGER };
18303 }
18304 }
18305 }
18306 // c:7476-7478 (5.9.2) — `/* Errors in math evaluation in let are
18307 // non-fatal. */ errflag &= ~ERRFLAG_ERROR;` then the status comes from
18308 // the value below; the dev tree's upstream 54285 returns 2 here, 5.9.2
18309 // returns 1 because the failed evaluation yields zero.
18310 errflag.fetch_and(!ERRFLAG_ERROR, Relaxed); // c:7478
18311 // c:7482 — `return (val.type == MN_INTEGER) ? val.u.l == 0 : val.u.d == 0.0;`
18312 if val.type_ == MN_INTEGER {
18313 // c:7482
18314 (val.l == 0) as i32
18315 } else {
18316 (val.d == 0.0) as i32
18317 }
18318}
18319
18320/// Port of `bin_umask()` from `Src/builtin.c:7491`.
18321/// C decl: `bin_umask(char *nam, char **args, Options ops, UNUSED(int func))`
18322/// C: `int bin_umask(char *nam, char **args, Options ops, ...)` —
18323/// set/show file-creation mask. No args → show; numeric arg → octal
18324/// parse; symbolic `[ugoa]+[+-=][rwx]+,...` → walk and apply.
18325/// WARNING: param names don't match C — Rust=(nam, args, _func) vs C=(nam, args, ops, func)
18326pub fn bin_umask(
18327 nam: &str,
18328 args: &[String], // c:7491
18329 ops: &options,
18330 _func: i32,
18331) -> i32 {
18332 // c:7497-7500 — read current umask.
18333 queue_signals(); // c:7497
18334 let mut um: u32 = unsafe { libc::umask(0o777) } as u32; // c:7498
18335 unsafe {
18336 libc::umask(um as libc::mode_t);
18337 } // c:7499
18338 unqueue_signals(); // c:7500
18339
18340 // c:7503-7521 — no args: display.
18341 if args.is_empty() {
18342 // c:7503
18343 if OPT_ISSET(ops, b'S') {
18344 // c:7504
18345 let who_chars = ['u', 'g', 'o']; // c:7505
18346 for (i, who) in who_chars.iter().enumerate() {
18347 // c:7507
18348 print!("{}=", who); // c:7510
18349 let mut what_iter = ['r', 'w', 'x'].iter(); // c:7511
18350 while let Some(w) = what_iter.next() {
18351 // c:7512
18352 if (um & 0o400) == 0 {
18353 // c:7513
18354 print!("{}", w); // c:7514
18355 }
18356 um <<= 1; // c:7515
18357 }
18358 if i < 2 {
18359 print!(",");
18360 } else {
18361 println!();
18362 } // c:7518
18363 }
18364 } else if crate::dash_mode::umask_four_digit() {
18365 // !!! EMULATION-ONLY (no C counterpart) !!! bash/ksh93/dash/ash
18366 // and /bin/sh print a FIXED four-digit mask; zsh's conditional
18367 // leading zero below is a zsh-ism. mksh keeps zsh's form, which
18368 // is why `umask_four_digit` excludes the pdksh line. Measured on
18369 // the real binaries — see extensions::dash_mode::umask_four_digit.
18370 println!("{:04o}", um);
18371 } else {
18372 // c:7522-7524 — `if (um & 0700) putchar('0'); printf("%03o\n", um);`
18373 if (um & 0o700) != 0 {
18374 // c:7522
18375 print!("0"); // c:7523
18376 }
18377 println!("{:03o}", um); // c:7524
18378 }
18379 return 0; // c:7526
18380 }
18381
18382 // c:7528 — `if (idigit(*s))` numeric form.
18383 let s = &args[0];
18384 if s.chars().next().is_some_and(|c| c.is_ascii_digit()) {
18385 // c:7528
18386 // c:7530 — `um = zstrtol(s, &s, 8);`
18387 match u32::from_str_radix(s, 8) {
18388 // c:7530
18389 Ok(n) => um = n, // c:7530
18390 Err(_) => {
18391 zwarnnam(nam, "bad umask"); // c:7532
18392 return 1; // c:7533
18393 }
18394 }
18395 } else {
18396 // c:7536-7585 — symbolic notation walker.
18397 let bytes = s.as_bytes();
18398 let mut i = 0;
18399 loop {
18400 // c:7544 — `whomask = 0;`
18401 let mut whomask: u32 = 0; // c:7544
18402 // c:7545-7553 — collect ugoa.
18403 while i < bytes.len() {
18404 // c:7545
18405 match bytes[i] {
18406 b'u' => {
18407 whomask |= 0o700;
18408 i += 1;
18409 } // c:7547
18410 b'g' => {
18411 whomask |= 0o070;
18412 i += 1;
18413 } // c:7549
18414 b'o' => {
18415 whomask |= 0o007;
18416 i += 1;
18417 } // c:7551
18418 b'a' => {
18419 whomask |= 0o777;
18420 i += 1;
18421 } // c:7553
18422 _ => break,
18423 }
18424 }
18425 // c:7555 — default whomask = 0777.
18426 if whomask == 0 {
18427 whomask = 0o777;
18428 } // c:7555
18429 // c:7557-7565 — op +/-/=.
18430 let umaskop = if i < bytes.len() { bytes[i] } else { 0 }; // c:7557
18431 if !(umaskop == b'+' || umaskop == b'-' || umaskop == b'=') {
18432 // c:7558
18433 if umaskop != 0 {
18434 // c:7559
18435 zwarnnam(
18436 nam,
18437 &format!("bad symbolic mode operator: {}", umaskop as char),
18438 ); // c:7560
18439 } else {
18440 zwarnnam(nam, "bad umask"); // c:7562
18441 }
18442 return 1; // c:7564
18443 }
18444 i += 1;
18445 // c:7567-7577 — collect rwx.
18446 let mut mask: u32 = 0; // c:7567
18447 while i < bytes.len() && bytes[i] != b',' {
18448 // c:7568
18449 match bytes[i] {
18450 b'r' => mask |= 0o444 & whomask, // c:7570
18451 b'w' => mask |= 0o222 & whomask, // c:7572
18452 b'x' => mask |= 0o111 & whomask, // c:7574
18453 other => {
18454 zwarnnam(
18455 nam,
18456 &format!("bad symbolic mode permission: {}", other as char),
18457 ); // c:7576
18458 return 1; // c:7577
18459 }
18460 }
18461 i += 1;
18462 }
18463 // c:7580-7585 — apply.
18464 match umaskop {
18465 b'+' => um &= !mask, // c:7581
18466 b'-' => um |= mask, // c:7583
18467 _ => um = (um | whomask) & !mask, // c:7585 (=)
18468 }
18469 if i < bytes.len() && bytes[i] == b',' {
18470 // c:7586
18471 i += 1; // c:7587
18472 } else {
18473 break; // c:7589
18474 }
18475 }
18476 if i < bytes.len() {
18477 // c:7591
18478 zwarnnam(
18479 nam,
18480 &format!("bad character in symbolic mode: {}", bytes[i] as char),
18481 ); // c:7592
18482 return 1; // c:7593
18483 }
18484 }
18485 // c:7598 — `umask(um);`
18486 unsafe {
18487 libc::umask(um as libc::mode_t);
18488 } // c:7598
18489 0 // c:7599
18490}
18491
18492/// Port of `bin_notavail()` from `Src/builtin.c:7604`.
18493/// C decl: `bin_notavail(char *nam, UNUSED(char **argv), UNUSED(Options ops), UNUSED(int func))`
18494/// C: `int bin_notavail(char *nam, UNUSED(char **argv),
18495/// UNUSED(Options ops), UNUSED(int func))`
18496/// → `zwarnnam(nam, "not available on this system"); return 1;`
18497/// WARNING: param names don't match C — Rust=(nam, _argv, _func) vs C=(nam, argv, ops, func)
18498pub fn bin_notavail(
18499 nam: &str,
18500 _argv: &[String], // c:7604
18501 _ops: &options,
18502 _func: i32,
18503) -> i32 {
18504 zwarnnam(nam, "not available on this system"); // c:7607
18505 1 // c:7608
18506}
18507// ---------------------------------------------------------------------------
18508// Builtin descriptor.
18509// Port of `struct builtin` from `Src/zsh.h` (the one expanded by the
18510// `BUILTIN` / `BIN_PREFIX` macros at line 1452 of zsh.h).
18511// ---------------------------------------------------------------------------
18512// ---------------------------------------------------------------------------
18513// The master registration table.
18514//
18515// Direct, line-for-line port of `static struct builtin builtins[]`
18516// at `Src/builtin.c:40-137`. Entries appear in the same order so
18517// any diff against the C source stays trivial. The `handler_name`
18518// column points at the canonical Rust port that the dispatcher in
18519// `Executor::register_builtins` (`src/ported/vm_helper`) wires up.
18520// ---------------------------------------------------------------------------
18521/// `BUILTINS` static.
18522pub static BUILTINS: std::sync::LazyLock<Vec<builtin>> = std::sync::LazyLock::new(|| {
18523 vec![
18524 BIN_PREFIX("-", BINF_DASH),
18525 BIN_PREFIX("builtin", BINF_BUILTIN),
18526 BIN_PREFIX("command", BINF_COMMAND),
18527 BIN_PREFIX("exec", BINF_EXEC),
18528 BIN_PREFIX("noglob", BINF_NOGLOB),
18529 BUILTIN(
18530 "[",
18531 BINF_HANDLES_OPTS,
18532 Some(bin_test as HandlerFunc),
18533 0,
18534 -1,
18535 BIN_BRACKET,
18536 None,
18537 None,
18538 ),
18539 BUILTIN(
18540 ".",
18541 BINF_PSPECIAL,
18542 Some(bin_dot as HandlerFunc),
18543 1,
18544 -1,
18545 0,
18546 None,
18547 None,
18548 ),
18549 BUILTIN(
18550 ":",
18551 BINF_PSPECIAL,
18552 Some(bin_true as HandlerFunc),
18553 0,
18554 -1,
18555 0,
18556 None,
18557 None,
18558 ),
18559 // !!! RUST-ONLY — NO C COUNTERPART !!! Inline Rust FFI: the
18560 // `rust { ... }` desugar (src/rust_ffi.rs) emits `__rust_compile
18561 // '<base64>' <line>`. min=1/max=2: the base64 body plus the optional
18562 // source line.
18563 BUILTIN(
18564 "__rust_compile",
18565 0,
18566 Some(crate::rust_ffi::bin_rust_compile as HandlerFunc),
18567 1,
18568 2,
18569 0,
18570 None,
18571 None,
18572 ),
18573 BUILTIN(
18574 "alias",
18575 BINF_MAGICEQUALS | BINF_PLUSOPTS,
18576 Some(bin_alias as HandlerFunc),
18577 0,
18578 -1,
18579 0,
18580 // `t` is ZSHRS-ONLY: the Korn shells' tracked-alias listing
18581 // (`alias -t`), which is how mksh's own `hash` alias is
18582 // defined. zsh has no such option, so bin_alias rejects it
18583 // outside the Korn drop-ins with zsh's own diagnostic.
18584 Some("Lgmrst"),
18585 None,
18586 ),
18587 BUILTIN(
18588 "autoload",
18589 BINF_PLUSOPTS,
18590 Some(bin_functions as HandlerFunc),
18591 0,
18592 -1,
18593 0,
18594 Some("dmktrRTUwWXz"),
18595 Some("u"),
18596 ),
18597 BUILTIN(
18598 "bg",
18599 0,
18600 Some(bin_fg as HandlerFunc),
18601 0,
18602 -1,
18603 BIN_BG,
18604 None,
18605 None,
18606 ),
18607 BUILTIN(
18608 "break",
18609 BINF_PSPECIAL,
18610 Some(bin_break as HandlerFunc),
18611 0,
18612 1,
18613 BIN_BREAK,
18614 None,
18615 None,
18616 ),
18617 BUILTIN(
18618 "bye",
18619 0,
18620 Some(bin_break as HandlerFunc),
18621 0,
18622 1,
18623 BIN_EXIT,
18624 None,
18625 None,
18626 ),
18627 BUILTIN(
18628 "cd",
18629 BINF_SKIPINVALID | BINF_SKIPDASH | BINF_DASHDASHVALID,
18630 Some(bin_cd as HandlerFunc),
18631 0,
18632 2,
18633 BIN_CD,
18634 Some("qsPL"),
18635 None,
18636 ),
18637 BUILTIN(
18638 "chdir",
18639 BINF_SKIPINVALID | BINF_SKIPDASH | BINF_DASHDASHVALID,
18640 Some(bin_cd as HandlerFunc),
18641 0,
18642 2,
18643 BIN_CD,
18644 Some("qsPL"),
18645 None,
18646 ),
18647 BUILTIN(
18648 "continue",
18649 BINF_PSPECIAL,
18650 Some(bin_break as HandlerFunc),
18651 0,
18652 1,
18653 BIN_CONTINUE,
18654 None,
18655 None,
18656 ),
18657 BUILTIN(
18658 "declare",
18659 BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN,
18660 Some(bin_typeset as HandlerFunc),
18661 0,
18662 -1,
18663 0,
18664 Some("AE:%F:%HL:%R:%TUZ:%afghi:%klmnp:%rtuxz"),
18665 None,
18666 ),
18667 BUILTIN(
18668 "dirs",
18669 0,
18670 Some(bin_dirs as HandlerFunc),
18671 0,
18672 -1,
18673 0,
18674 Some("clpv"),
18675 None,
18676 ),
18677 BUILTIN(
18678 "disable",
18679 0,
18680 Some(bin_enable as HandlerFunc),
18681 0,
18682 -1,
18683 BIN_DISABLE,
18684 Some("afmprs"),
18685 None,
18686 ),
18687 BUILTIN(
18688 "disown",
18689 0,
18690 Some(bin_fg as HandlerFunc),
18691 0,
18692 -1,
18693 BIN_DISOWN,
18694 Some("a"),
18695 None,
18696 ),
18697 BUILTIN(
18698 "echo",
18699 BINF_SKIPINVALID,
18700 Some(bin_print as HandlerFunc),
18701 0,
18702 -1,
18703 BIN_ECHO,
18704 Some("neE"),
18705 Some("-"),
18706 ),
18707 BUILTIN(
18708 "emulate",
18709 0,
18710 Some(bin_emulate as HandlerFunc),
18711 0,
18712 -1,
18713 0,
18714 Some("lLR"),
18715 None,
18716 ),
18717 BUILTIN(
18718 "enable",
18719 0,
18720 Some(bin_enable as HandlerFunc),
18721 0,
18722 -1,
18723 BIN_ENABLE,
18724 Some("afmprs"),
18725 None,
18726 ),
18727 BUILTIN(
18728 "eval",
18729 BINF_PSPECIAL,
18730 Some(bin_eval as HandlerFunc),
18731 0,
18732 -1,
18733 BIN_EVAL,
18734 None,
18735 None,
18736 ),
18737 BUILTIN(
18738 "exit",
18739 BINF_PSPECIAL,
18740 Some(bin_break as HandlerFunc),
18741 0,
18742 1,
18743 BIN_EXIT,
18744 None,
18745 None,
18746 ),
18747 BUILTIN(
18748 "export",
18749 BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN,
18750 Some(bin_typeset as HandlerFunc),
18751 0,
18752 -1,
18753 BIN_EXPORT,
18754 Some("E:%F:%HL:%R:%TUZ:%afhi:%lp:%rtu"),
18755 Some("xg"),
18756 ),
18757 BUILTIN(
18758 "false",
18759 0,
18760 Some(bin_false as HandlerFunc),
18761 0,
18762 -1,
18763 0,
18764 None,
18765 None,
18766 ),
18767 // C source (Src/builtin.c:69-73): the argument to -e used to be
18768 // optional; making it required is more consistent.
18769 BUILTIN(
18770 "fc",
18771 0,
18772 Some(bin_fc as HandlerFunc),
18773 0,
18774 -1,
18775 BIN_FC,
18776 Some("aAdDe:EfiIlLmnpPrRst:W"),
18777 None,
18778 ),
18779 BUILTIN(
18780 "fg",
18781 0,
18782 Some(bin_fg as HandlerFunc),
18783 0,
18784 -1,
18785 BIN_FG,
18786 None,
18787 None,
18788 ),
18789 BUILTIN(
18790 "float",
18791 BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN,
18792 Some(bin_typeset as HandlerFunc),
18793 0,
18794 -1,
18795 0,
18796 Some("E:%F:%HL:%R:%Z:%ghlp:%rtux"),
18797 Some("E"),
18798 ),
18799 BUILTIN(
18800 "functions",
18801 BINF_PLUSOPTS,
18802 Some(bin_functions as HandlerFunc),
18803 0,
18804 -1,
18805 0,
18806 Some("ckmMstTuUWx:z"),
18807 None,
18808 ),
18809 BUILTIN(
18810 "getln",
18811 0,
18812 Some(bin_read as HandlerFunc),
18813 0,
18814 -1,
18815 0,
18816 Some("ecnAlE"),
18817 Some("zr"),
18818 ),
18819 BUILTIN(
18820 "getopts",
18821 0,
18822 Some(bin_getopts as HandlerFunc),
18823 2,
18824 -1,
18825 0,
18826 Some("p"), // c:79 (54471)
18827 None,
18828 ),
18829 BUILTIN(
18830 "hash",
18831 BINF_MAGICEQUALS,
18832 Some(bin_hash as HandlerFunc),
18833 0,
18834 -1,
18835 0,
18836 Some("Ldfmrv"),
18837 None,
18838 ),
18839 // Src/builtin.c — `#ifdef ZSH_HASH_DEBUG`
18840 // BUILTIN("hashinfo", 0, bin_hashinfo, 0, 0, 0, NULL, NULL)
18841 BUILTIN(
18842 "hashinfo",
18843 0,
18844 Some(crate::ported::hashtable::bin_hashinfo as HandlerFunc),
18845 0,
18846 0,
18847 0,
18848 None,
18849 None,
18850 ),
18851 BUILTIN(
18852 "history",
18853 0,
18854 Some(bin_fc as HandlerFunc),
18855 0,
18856 -1,
18857 BIN_FC,
18858 Some("adDEfiLmnpPrt:"),
18859 Some("l"),
18860 ),
18861 BUILTIN(
18862 "integer",
18863 BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN,
18864 Some(bin_typeset as HandlerFunc),
18865 0,
18866 -1,
18867 0,
18868 Some("HL:%R:%Z:%ghi:%lp:%rtux"),
18869 Some("i"),
18870 ),
18871 BUILTIN(
18872 "jobs",
18873 0,
18874 Some(bin_fg as HandlerFunc),
18875 0,
18876 -1,
18877 BIN_JOBS,
18878 Some("dlpZrs"),
18879 None,
18880 ),
18881 BUILTIN(
18882 "kill",
18883 BINF_HANDLES_OPTS,
18884 Some(crate::ported::jobs::bin_kill as HandlerFunc),
18885 0,
18886 -1,
18887 0,
18888 None,
18889 None,
18890 ),
18891 BUILTIN("let", 0, Some(bin_let as HandlerFunc), 1, -1, 0, None, None),
18892 BUILTIN(
18893 "local",
18894 BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN,
18895 Some(bin_typeset as HandlerFunc),
18896 0,
18897 -1,
18898 0,
18899 Some("AE:%F:%HL:%R:%TUZ:%ahi:%lnp:%rtux"),
18900 None,
18901 ),
18902 BUILTIN(
18903 "logout",
18904 0,
18905 Some(bin_break as HandlerFunc),
18906 0,
18907 1,
18908 BIN_LOGOUT,
18909 None,
18910 None,
18911 ),
18912 // Src/builtin.c — `#if defined(ZSH_MEM) & defined(ZSH_MEM_DEBUG)`
18913 // BUILTIN("mem", 0, bin_mem, 0, 0, 0, "v", NULL)
18914 BUILTIN(
18915 "mem",
18916 0,
18917 Some(crate::ported::mem::bin_mem as HandlerFunc),
18918 0,
18919 0,
18920 0,
18921 Some("v"),
18922 None,
18923 ),
18924 BUILTIN(
18925 "popd",
18926 BINF_SKIPINVALID | BINF_SKIPDASH | BINF_DASHDASHVALID,
18927 Some(bin_cd as HandlerFunc),
18928 0,
18929 1,
18930 BIN_POPD,
18931 Some("q"),
18932 None,
18933 ),
18934 // Src/builtin.c — `#if defined(ZSH_PAT_DEBUG)`
18935 // BUILTIN("patdebug", 0, bin_patdebug, 1, -1, 0, "p", NULL)
18936 BUILTIN("patdebug", 0, None, 1, -1, 0, Some("p"), None),
18937 BUILTIN(
18938 "print",
18939 BINF_PRINTOPTS,
18940 Some(bin_print as HandlerFunc),
18941 0,
18942 -1,
18943 BIN_PRINT,
18944 Some("abcC:Df:ilmnNoOpPrRsSu:v:x:X:z-"),
18945 None,
18946 ),
18947 BUILTIN(
18948 "printf",
18949 BINF_SKIPINVALID | BINF_SKIPDASH,
18950 Some(bin_print as HandlerFunc),
18951 1,
18952 -1,
18953 BIN_PRINTF,
18954 Some("v:"),
18955 None,
18956 ),
18957 BUILTIN(
18958 "pushd",
18959 BINF_SKIPINVALID | BINF_SKIPDASH | BINF_DASHDASHVALID,
18960 Some(bin_cd as HandlerFunc),
18961 0,
18962 2,
18963 BIN_PUSHD,
18964 Some("qsPL"),
18965 None,
18966 ),
18967 BUILTIN(
18968 "pushln",
18969 0,
18970 Some(bin_print as HandlerFunc),
18971 0,
18972 -1,
18973 BIN_PRINT,
18974 None,
18975 Some("-nz"),
18976 ),
18977 BUILTIN(
18978 "pwd",
18979 0,
18980 Some(bin_pwd as HandlerFunc),
18981 0,
18982 0,
18983 0,
18984 Some("rLP"),
18985 None,
18986 ),
18987 BUILTIN(
18988 "r",
18989 0,
18990 Some(bin_fc as HandlerFunc),
18991 0,
18992 -1,
18993 BIN_R,
18994 Some("IlLnr"),
18995 None,
18996 ),
18997 BUILTIN(
18998 "read",
18999 0,
19000 Some(bin_read as HandlerFunc),
19001 0,
19002 -1,
19003 0,
19004 // `a` is bash's array-read flag (zsh/ksh use `A`); accepted here
19005 // so `read -a arr` parses, and treated as array-read only in
19006 // bash mode (see bin_read's want_array).
19007 Some("acd:ek:%lnNpqrst:%zu:AE"),
19008 None,
19009 ),
19010 BUILTIN(
19011 "readonly",
19012 BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN,
19013 Some(bin_typeset as HandlerFunc),
19014 0,
19015 -1,
19016 BIN_READONLY,
19017 Some("AE:%F:%HL:%R:%TUZ:%afghi:%lptux"),
19018 Some("r"),
19019 ),
19020 BUILTIN(
19021 "rehash",
19022 0,
19023 Some(bin_hash as HandlerFunc),
19024 0,
19025 0,
19026 0,
19027 Some("df"),
19028 Some("r"),
19029 ),
19030 BUILTIN(
19031 "return",
19032 BINF_PSPECIAL,
19033 Some(bin_break as HandlerFunc),
19034 0,
19035 1,
19036 BIN_RETURN,
19037 None,
19038 None,
19039 ),
19040 BUILTIN(
19041 "set",
19042 BINF_PSPECIAL | BINF_HANDLES_OPTS,
19043 Some(bin_set as HandlerFunc),
19044 0,
19045 -1,
19046 0,
19047 None,
19048 None,
19049 ),
19050 BUILTIN(
19051 "setopt",
19052 0,
19053 Some(crate::ported::options::bin_setopt as HandlerFunc),
19054 0,
19055 -1,
19056 BIN_SETOPT,
19057 None,
19058 None,
19059 ),
19060 // c:Src/Builtins/sched.c:375 — sched is a Builtins module
19061 // builtin (zsh/sched). bintab has only one entry. The Rust
19062 // port at builtins/sched.rs::bin_sched (325 lines) was not
19063 // registered, so `sched 09:00 echo morning` returned
19064 // "command not found".
19065 BUILTIN(
19066 "sched",
19067 0,
19068 Some(crate::ported::builtins::sched::bin_sched as HandlerFunc),
19069 0,
19070 -1,
19071 0,
19072 None,
19073 None,
19074 ),
19075 BUILTIN(
19076 "shift",
19077 BINF_PSPECIAL,
19078 Some(bin_shift as HandlerFunc),
19079 0,
19080 -1,
19081 0,
19082 Some("p"),
19083 None,
19084 ),
19085 BUILTIN(
19086 "source",
19087 BINF_PSPECIAL,
19088 Some(bin_dot as HandlerFunc),
19089 1,
19090 -1,
19091 0,
19092 None,
19093 None,
19094 ),
19095 BUILTIN(
19096 "suspend",
19097 0,
19098 Some(crate::ported::jobs::bin_suspend as HandlerFunc),
19099 0,
19100 0,
19101 0,
19102 Some("f"),
19103 None,
19104 ),
19105 BUILTIN(
19106 "test",
19107 BINF_HANDLES_OPTS,
19108 Some(bin_test as HandlerFunc),
19109 0,
19110 -1,
19111 BIN_TEST,
19112 None,
19113 None,
19114 ),
19115 BUILTIN(
19116 "ttyctl",
19117 0,
19118 Some(bin_ttyctl as HandlerFunc),
19119 0,
19120 0,
19121 0,
19122 Some("fu"),
19123 None,
19124 ),
19125 // c:Src/Builtins/rlimits.c:868-870 — limit/ulimit/unlimit are
19126 // declared in the rlimits Builtins-module's bintab. zshrs has the
19127 // free-fn ports at src/ported/builtins/rlimits.rs but never
19128 // registered them; the BUILTIN_NAMES derivation missed them and
19129 // `type limit` etc. returned empty.
19130 BUILTIN(
19131 "limit",
19132 0,
19133 Some(crate::ported::builtins::rlimits::bin_limit as HandlerFunc),
19134 0,
19135 -1,
19136 0,
19137 Some("sh"),
19138 None,
19139 ), // c:rlimits.c:868
19140 BUILTIN(
19141 "ulimit",
19142 0,
19143 Some(crate::ported::builtins::rlimits::bin_ulimit as HandlerFunc),
19144 0,
19145 -1,
19146 0,
19147 None,
19148 None,
19149 ), // c:rlimits.c:869
19150 BUILTIN(
19151 "unlimit",
19152 0,
19153 Some(crate::ported::builtins::rlimits::bin_unlimit as HandlerFunc),
19154 0,
19155 -1,
19156 0,
19157 Some("hs"),
19158 None,
19159 ), // c:rlimits.c:870
19160 BUILTIN(
19161 "times",
19162 BINF_PSPECIAL,
19163 Some(bin_times as HandlerFunc),
19164 0,
19165 0,
19166 0,
19167 None,
19168 None,
19169 ),
19170 BUILTIN(
19171 "trap",
19172 BINF_PSPECIAL | BINF_HANDLES_OPTS,
19173 Some(bin_trap as HandlerFunc),
19174 0,
19175 -1,
19176 0,
19177 None,
19178 None,
19179 ),
19180 BUILTIN(
19181 "true",
19182 0,
19183 Some(bin_true as HandlerFunc),
19184 0,
19185 -1,
19186 0,
19187 None,
19188 None,
19189 ),
19190 BUILTIN(
19191 "type",
19192 0,
19193 Some(bin_whence as HandlerFunc),
19194 0,
19195 -1,
19196 0,
19197 // `t` (bash `type -t`) accepted only meaningfully in --bash;
19198 // bin_whence gates the behavior on bash_mode().
19199 Some("ampfsStw"),
19200 Some("v"),
19201 ),
19202 BUILTIN(
19203 "typeset",
19204 BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN,
19205 Some(bin_typeset as HandlerFunc),
19206 0,
19207 -1,
19208 0,
19209 Some("AE:%F:%HL:%R:%TUZ:%afghi:%klp:%rtuxmnz"),
19210 None,
19211 ),
19212 // `nameref` — zsh/ksh93 module's named-reference declaration
19213 // builtin. Same handler as `typeset` with a tighter opt-string
19214 // (`-g`/`-p`/`-r`/`-u`) and the assign-style invocation. The
19215 // canonical Src/Modules/ksh93.c:bintab entry:
19216 // BUILTIN("nameref", BINF_ASSIGN, bin_typeset, 0, -1, 0,
19217 // "gpru", "n")
19218 // The 9th arg ("n") is the default-flag — typeset_flags inherits
19219 // PM_NAMEREF when the builtin is invoked under this name. zshrs's
19220 // bin_typeset wrapper doesn't read the 9th field yet, but the
19221 // registration still surfaces `nameref` in the tool window /
19222 // completion / hover docs.
19223 BUILTIN(
19224 "nameref",
19225 BINF_ASSIGN,
19226 Some(bin_typeset as HandlerFunc),
19227 0,
19228 -1,
19229 0,
19230 Some("gpru"),
19231 Some("n"),
19232 ),
19233 BUILTIN(
19234 "umask",
19235 0,
19236 Some(bin_umask as HandlerFunc),
19237 0,
19238 1,
19239 0,
19240 Some("S"),
19241 None,
19242 ),
19243 BUILTIN(
19244 "unalias",
19245 0,
19246 Some(bin_unhash as HandlerFunc),
19247 0,
19248 -1,
19249 BIN_UNALIAS,
19250 Some("ams"),
19251 None,
19252 ),
19253 BUILTIN(
19254 "unfunction",
19255 0,
19256 Some(bin_unhash as HandlerFunc),
19257 1,
19258 -1,
19259 BIN_UNFUNCTION,
19260 Some("m"),
19261 Some("f"),
19262 ),
19263 BUILTIN(
19264 "unhash",
19265 0,
19266 Some(bin_unhash as HandlerFunc),
19267 1,
19268 -1,
19269 BIN_UNHASH,
19270 Some("adfms"),
19271 None,
19272 ),
19273 BUILTIN(
19274 "unset",
19275 BINF_PSPECIAL,
19276 Some(bin_unset as HandlerFunc),
19277 1,
19278 -1,
19279 BIN_UNSET,
19280 Some("fmvn"),
19281 None,
19282 ),
19283 BUILTIN(
19284 "unsetopt",
19285 0,
19286 Some(crate::ported::options::bin_setopt as HandlerFunc),
19287 0,
19288 -1,
19289 BIN_UNSETOPT,
19290 None,
19291 None,
19292 ),
19293 BUILTIN(
19294 "wait",
19295 0,
19296 Some(bin_fg as HandlerFunc),
19297 0,
19298 -1,
19299 BIN_WAIT,
19300 None,
19301 None,
19302 ),
19303 BUILTIN(
19304 "whence",
19305 0,
19306 Some(bin_whence as HandlerFunc),
19307 0,
19308 -1,
19309 0,
19310 Some("acmpvfsSwx:"),
19311 None,
19312 ),
19313 BUILTIN(
19314 "where",
19315 0,
19316 Some(bin_whence as HandlerFunc),
19317 0,
19318 -1,
19319 0,
19320 Some("pmsSwx:"),
19321 Some("ca"),
19322 ),
19323 BUILTIN(
19324 "which",
19325 0,
19326 Some(bin_whence as HandlerFunc),
19327 0,
19328 -1,
19329 0,
19330 Some("ampsSwx:"),
19331 Some("c"),
19332 ),
19333 BUILTIN(
19334 "zmodload",
19335 0,
19336 Some(crate::ported::module::bin_zmodload as HandlerFunc),
19337 0,
19338 -1,
19339 0,
19340 Some("AFRILP:abcfdilmpsue"),
19341 None,
19342 ),
19343 BUILTIN(
19344 "zcompile",
19345 0,
19346 Some(crate::ported::parse::bin_zcompile as HandlerFunc),
19347 0,
19348 -1,
19349 0,
19350 Some("tUMRcmzka"),
19351 None,
19352 ),
19353 // Module builtins (zsh/zutil, zsh/cap, zsh/pcre, etc.) — these
19354 // live in src/ported/modules/* and src/ported/zle/* but their
19355 // canonical pub fn signatures match HandlerFunc, so they can be
19356 // dispatched via execbuiltin alongside the main builtins.
19357 BUILTIN(
19358 "zstyle",
19359 0,
19360 Some(crate::ported::modules::zutil::bin_zstyle as HandlerFunc),
19361 0,
19362 -1,
19363 0,
19364 // c:Src/Modules/zutil.c:2139 — `BUILTIN("zstyle", 0,
19365 // bin_zstyle, 0, -1, 0, NULL, NULL)`. NULL optstr: bin_zstyle
19366 // parses args[0] directly (see c:493-512 + c:587-600) so the
19367 // diagnostic format for unknown flags matches zsh's
19368 // "invalid option: -X" (the execbuiltin generic parser would
19369 // have emitted "bad option: -X" which diverges from zsh).
19370 // Bug surfaced via test_zstyle_unknown_flag_errors.
19371 None,
19372 None,
19373 ),
19374 BUILTIN(
19375 "zformat",
19376 0,
19377 Some(crate::ported::modules::zutil::bin_zformat as HandlerFunc),
19378 2,
19379 -1,
19380 0,
19381 // c:Src/Modules/zutil.c:2151 — `BUILTIN("zformat", 0,
19382 // bin_zformat, 2, -1, 0, "afFqQ", NULL)`.
19383 Some("afFqQ"),
19384 None,
19385 ),
19386 BUILTIN(
19387 "zparseopts",
19388 0,
19389 Some(crate::ported::modules::zutil::bin_zparseopts as HandlerFunc),
19390 0,
19391 -1,
19392 0,
19393 // c:src/zsh/Src/Modules/zutil.c:2137 —
19394 // BUILTIN("zparseopts", 0, bin_zparseopts, 1, -1, 0, NULL, NULL)
19395 // NULL optstring: in zsh 5.9.2 the generic parser must NOT eat
19396 // zparseopts's leading `-` words, because a word it does not
19397 // recognise is an option DESCRIPTION, not an error. Handing it
19398 // 5.9.999's optstring "a:A:DEFGKMn:v:" (c:2150, upstream commit
19399 // 88d51a2400) turns `zparseopts -D -E -move=opt_move` into
19400 // "bad option: -m" at Src/builtin.c:385-390 — the regression that
19401 // broke ~/.zinit/bin/zinit-install.zsh:1528. zshrs reports
19402 // $ZSH_VERSION=5.9.2, so it takes the 5.9.2 rule; the switch is
19403 // `LONG_SPEC_NEEDS_GUARD` in src/ported/modules/zutil.rs, and
19404 // retargeting means flipping it AND restoring the optstring here.
19405 // bin_zparseopts scans the flag words itself (stacking and cuddled
19406 // optargs included) and reads them back with OPT_ISSET/OPT_ARG.
19407 //
19408 // minargs stays 0, NOT c:2137's 1: bare `zparseopts` must reach
19409 // the builtin to report "missing option descriptions"
19410 // (c:1885-1888), and `zparseopts -a ''` must reach it to report
19411 // "missing array name for -a" (c:1844).
19412 None,
19413 None,
19414 ),
19415 BUILTIN(
19416 "zregexparse",
19417 0,
19418 Some(crate::ported::modules::zutil::bin_zregexparse as HandlerFunc),
19419 0,
19420 -1,
19421 0,
19422 Some("c"),
19423 None,
19424 ),
19425 BUILTIN(
19426 "cap",
19427 0,
19428 Some(crate::ported::modules::cap::bin_cap as HandlerFunc),
19429 0,
19430 1,
19431 0,
19432 None,
19433 None,
19434 ),
19435 BUILTIN(
19436 "getcap",
19437 0,
19438 Some(crate::ported::modules::cap::bin_getcap as HandlerFunc),
19439 1,
19440 -1,
19441 0,
19442 None,
19443 None,
19444 ),
19445 BUILTIN(
19446 "setcap",
19447 0,
19448 Some(crate::ported::modules::cap::bin_setcap as HandlerFunc),
19449 1,
19450 -1,
19451 0,
19452 None,
19453 None,
19454 ),
19455 BUILTIN(
19456 "pcre_compile",
19457 0,
19458 Some(crate::ported::modules::pcre::bin_pcre_compile as HandlerFunc),
19459 1,
19460 1,
19461 0,
19462 // c:Src/Modules/pcre.c:521 — `"aimxs"`. The `s` (PCRE2_DOTALL,
19463 // handled at pcre.rs:109) was missing, so `pcre_compile -s`
19464 // errored "bad option: -s".
19465 Some("aimxs"),
19466 None,
19467 ),
19468 BUILTIN(
19469 "pcre_study",
19470 0,
19471 Some(crate::ported::modules::pcre::bin_pcre_study as HandlerFunc),
19472 0,
19473 0,
19474 0,
19475 None,
19476 None,
19477 ),
19478 BUILTIN(
19479 "pcre_match",
19480 0,
19481 Some(crate::ported::modules::pcre::bin_pcre_match as HandlerFunc),
19482 1,
19483 -1,
19484 0,
19485 // c:Src/Modules/pcre.c:522 — `"A:a:v:n:bd"`. `A`(named assoc),
19486 // `a`(capture array), `v`(match var), `n`(start offset) take
19487 // args; `b`(offset pair) and `d`(dfa) are flags. The prior
19488 // "ab:nv:" made `-a` a flag (so `-a arr` ate the subject),
19489 // `-b` arg-taking, and dropped `-A`/`-n:`/`-d` entirely.
19490 Some("A:a:v:n:bd"),
19491 None,
19492 ),
19493 BUILTIN(
19494 "ztcp",
19495 0,
19496 Some(crate::ported::modules::tcp::bin_ztcp as HandlerFunc),
19497 0,
19498 -1,
19499 0,
19500 // c:Src/Modules/tcp.c — `"acd:flLtv"`. `-d` (target fd) takes
19501 // an argument; zshrs had "acdflLtv" (d a flag), so `ztcp -d 9`
19502 // treated 9 as a positional and OPT_ARG(d) was empty.
19503 Some("acd:flLtv"),
19504 None,
19505 ),
19506 BUILTIN(
19507 "ztie",
19508 0,
19509 Some(crate::ported::modules::db_gdbm::bin_ztie as HandlerFunc),
19510 0,
19511 -1,
19512 0,
19513 Some("d:f:r"),
19514 None,
19515 ),
19516 BUILTIN(
19517 "zuntie",
19518 0,
19519 Some(crate::ported::modules::db_gdbm::bin_zuntie as HandlerFunc),
19520 1,
19521 -1,
19522 0,
19523 Some("u"),
19524 None,
19525 ),
19526 BUILTIN(
19527 "zgdbmpath",
19528 0,
19529 Some(crate::ported::modules::db_gdbm::bin_zgdbmpath as HandlerFunc),
19530 1,
19531 1,
19532 0,
19533 None,
19534 None,
19535 ),
19536 BUILTIN(
19537 "echoti",
19538 0,
19539 Some(crate::ported::modules::terminfo::bin_echoti as HandlerFunc),
19540 1,
19541 -1,
19542 0,
19543 None,
19544 None,
19545 ),
19546 BUILTIN(
19547 "fg",
19548 0,
19549 Some(bin_fg as HandlerFunc),
19550 0,
19551 -1,
19552 BIN_FG,
19553 None,
19554 None,
19555 ),
19556 BUILTIN(
19557 "kill",
19558 BINF_HANDLES_OPTS,
19559 Some(crate::ported::jobs::bin_kill as HandlerFunc),
19560 0,
19561 -1,
19562 0,
19563 None,
19564 None,
19565 ),
19566 BUILTIN(
19567 "suspend",
19568 0,
19569 Some(crate::ported::jobs::bin_suspend as HandlerFunc),
19570 0,
19571 0,
19572 0,
19573 Some("f"),
19574 None,
19575 ),
19576 BUILTIN(
19577 "bindkey",
19578 0,
19579 Some(crate::ported::zle::zle_keymap::bin_bindkey as HandlerFunc),
19580 0,
19581 -1,
19582 0,
19583 // c:Src/Zle/zle_main.c:2209 — `"evaM:ldDANmrsLRp"`. The
19584 // `:` after `M` marks it as taking a required arg
19585 // (`-M <keymap>`); without it the parser consumes the
19586 // keymap name as a positional and `-M` falls back to
19587 // "main" → wrong keymap returned for every `-M X` call.
19588 Some("evaM:ldDANmrsLRp"),
19589 None,
19590 ),
19591 BUILTIN(
19592 "vared",
19593 0,
19594 Some(crate::ported::zle::zle_main::bin_vared as HandlerFunc),
19595 1,
19596 1,
19597 0,
19598 Some("aAcef:ghi:M:m:p:r:t:"),
19599 None,
19600 ),
19601 // Registration ORDER note (zshrs-only): C adds these two through
19602 // compctl.mdd's `autofeatures="b:compctl b:compcall"`, which the
19603 // generated `Src/bltinmods.list` walks in MODULE order — and
19604 // `Src/Zle/compctl.mdd` sorts before `complete.mdd` / `computil.mdd`,
19605 // so zsh/compctl reaches `builtintab` BEFORE zsh/complete and
19606 // zsh/computil do. `BUILTINTAB_NODES` front-inserts
19607 // (c:Src/hashtable.c:217-218), so this order decides the chain order
19608 // `${(k)builtins}` prints: with the block left below the computil
19609 // batch, bucket 29 came out `compctl comptags compdescribe` where
19610 // `zsh -f` prints `comptags compdescribe compctl`.
19611 // c:Src/Zle/compctl.c:4000-4001 — zsh/compctl module.
19612 BUILTIN(
19613 "compcall",
19614 0,
19615 Some(crate::ported::zle::compctl::bin_compcall as HandlerFunc),
19616 0,
19617 0,
19618 0,
19619 Some("TD"),
19620 None,
19621 ), // c:4000
19622 BUILTIN(
19623 "compctl",
19624 0,
19625 Some(crate::ported::zle::compctl::bin_compctl as HandlerFunc),
19626 0,
19627 -1,
19628 0,
19629 None,
19630 None,
19631 ), // c:4001
19632 // C: `BUILTIN("compadd", BINF_HANDLES_OPTS, bin_compadd, 0, -1, 0,
19633 // NULL, NULL)` (complete.c:1693). compadd parses ALL its own
19634 // options (the char-by-char loop in bin_compadd_body). The earlier
19635 // registration gave it an incomplete getopt spec ("J:V:1X:fnqQF:Wsi")
19636 // WITHOUT BINF_HANDLES_OPTS, so the dispatcher pre-parsed and
19637 // STRIPPED -J/-V/-X/… into `ops` and handed bin_compadd only the
19638 // positional words — every `compadd -J group -X expl …` lost its
19639 // group name and explanation (second `compadd -J` groups vanished,
19640 // no group headers). Must be HANDLES_OPTS + no optspec so argv
19641 // reaches bin_compadd raw.
19642 BUILTIN(
19643 "compadd",
19644 BINF_HANDLES_OPTS,
19645 Some(crate::ported::zle::complete::bin_compadd as HandlerFunc),
19646 0,
19647 -1,
19648 0,
19649 None,
19650 None,
19651 ),
19652 // C: `BUILTIN("compset", 0, bin_compset, 1, 3, 0, NULL, NULL)`
19653 // (complete.c:1694). bin_compset parses `argv[0]` (-n/-p/-q/-P/-S)
19654 // ITSELF; the earlier Rust registration added a getopt spec
19655 // ("npqPS:") that C does NOT have, so the dispatcher pre-parsed and
19656 // STRIPPED those options from argv before bin_compset ran —
19657 // `argv[0].starts_with('-')` then failed and every shell-invoked
19658 // `compset -p/-q/-n …` no-opped (word-splitting completers broke).
19659 // Must be NULL optspec (+ max 3) so argv reaches bin_compset raw.
19660 BUILTIN(
19661 "compset",
19662 0,
19663 Some(crate::ported::zle::complete::bin_compset as HandlerFunc),
19664 1,
19665 3,
19666 0,
19667 None,
19668 None,
19669 ),
19670 // c:Src/Zle/computil.c:5103-5110 — zsh/computil module's 8
19671 // builtins drive compsys (the canonical completion system).
19672 // All have HandlerFunc-compatible signatures already; just
19673 // need BUILTINS-table registration. Without these,
19674 // _describe / _arguments / _values / _files / _groups / etc.
19675 // (compsys's primary entry points) silently no-op.
19676 BUILTIN(
19677 "comparguments",
19678 0,
19679 Some(crate::ported::zle::computil::bin_comparguments as HandlerFunc),
19680 1,
19681 -1,
19682 0,
19683 None,
19684 None,
19685 ), // c:5103
19686 BUILTIN(
19687 "compdescribe",
19688 0,
19689 Some(crate::ported::zle::computil::bin_compdescribe as HandlerFunc),
19690 3,
19691 -1,
19692 0,
19693 None,
19694 None,
19695 ), // c:5104
19696 BUILTIN(
19697 "compfiles",
19698 0,
19699 Some(crate::ported::zle::computil::bin_compfiles as HandlerFunc),
19700 1,
19701 -1,
19702 0,
19703 None,
19704 None,
19705 ), // c:5105
19706 BUILTIN(
19707 "compgroups",
19708 0,
19709 Some(crate::ported::zle::computil::bin_compgroups as HandlerFunc),
19710 1,
19711 -1,
19712 0,
19713 None,
19714 None,
19715 ), // c:5106
19716 BUILTIN(
19717 "compquote",
19718 0,
19719 Some(crate::ported::zle::computil::bin_compquote as HandlerFunc),
19720 1,
19721 -1,
19722 0,
19723 Some("p"),
19724 None,
19725 ), // c:5107
19726 BUILTIN(
19727 "comptags",
19728 0,
19729 Some(crate::ported::zle::computil::bin_comptags as HandlerFunc),
19730 1,
19731 -1,
19732 0,
19733 None,
19734 None,
19735 ), // c:5108
19736 BUILTIN(
19737 "comptry",
19738 0,
19739 Some(crate::ported::zle::computil::bin_comptry as HandlerFunc),
19740 0,
19741 -1,
19742 0,
19743 None,
19744 None,
19745 ), // c:5109
19746 BUILTIN(
19747 "compvalues",
19748 0,
19749 Some(crate::ported::zle::computil::bin_compvalues as HandlerFunc),
19750 1,
19751 -1,
19752 0,
19753 None,
19754 None,
19755 ), // c:5110
19756 // c:Src/Modules/system.c:819-824 — zsh/system module builtins.
19757 BUILTIN(
19758 "syserror",
19759 0,
19760 Some(crate::ported::modules::system::bin_syserror as HandlerFunc),
19761 0,
19762 1,
19763 0,
19764 Some("e:p:"),
19765 None,
19766 ), // c:819
19767 BUILTIN(
19768 "sysread",
19769 0,
19770 Some(crate::ported::modules::system::bin_sysread as HandlerFunc),
19771 0,
19772 1,
19773 0,
19774 Some("c:i:o:s:t:"),
19775 None,
19776 ), // c:820
19777 BUILTIN(
19778 "syswrite",
19779 0,
19780 Some(crate::ported::modules::system::bin_syswrite as HandlerFunc),
19781 1,
19782 1,
19783 0,
19784 Some("c:o:"),
19785 None,
19786 ), // c:821
19787 BUILTIN(
19788 "sysopen",
19789 0,
19790 Some(crate::ported::modules::system::bin_sysopen as HandlerFunc),
19791 1,
19792 1,
19793 0,
19794 Some("rwau:o:m:"),
19795 None,
19796 ), // c:822
19797 BUILTIN(
19798 "sysseek",
19799 0,
19800 Some(crate::ported::modules::system::bin_sysseek as HandlerFunc),
19801 1,
19802 1,
19803 0,
19804 Some("u:w:"),
19805 None,
19806 ), // c:823
19807 BUILTIN(
19808 "zsystem",
19809 0,
19810 Some(crate::ported::modules::system::bin_zsystem as HandlerFunc),
19811 1,
19812 -1,
19813 0,
19814 None,
19815 None,
19816 ), // c:824
19817 // c:Src/Modules/zselect.c:272 — zsh/zselect module.
19818 BUILTIN(
19819 "zselect",
19820 0,
19821 Some(crate::ported::modules::zselect::bin_zselect as HandlerFunc),
19822 0,
19823 -1,
19824 0,
19825 None,
19826 None,
19827 ), // c:272
19828 // c:Src/Modules/socket.c:276 — zsh/socket module.
19829 BUILTIN(
19830 "zsocket",
19831 0,
19832 Some(crate::ported::modules::socket::bin_zsocket as HandlerFunc),
19833 0,
19834 3,
19835 0,
19836 Some("ad:ltv"),
19837 None,
19838 ), // c:276
19839 // c:Src/Modules/stat.c:637 — zsh/stat module.
19840 BUILTIN(
19841 "stat",
19842 0,
19843 Some(crate::ported::modules::stat::bin_stat as HandlerFunc),
19844 0,
19845 -1,
19846 0,
19847 None,
19848 None,
19849 ), // c:637
19850 // c:Src/Modules/stat.c:638 — `zstat` alias, same handler.
19851 BUILTIN(
19852 "zstat",
19853 0,
19854 Some(crate::ported::modules::stat::bin_stat as HandlerFunc),
19855 0,
19856 -1,
19857 0,
19858 None,
19859 None,
19860 ), // c:638
19861 // c:Src/Modules/watch.c:694 — zsh/watch module's `log`.
19862 BUILTIN(
19863 "log",
19864 0,
19865 Some(crate::ported::modules::watch::bin_log as HandlerFunc),
19866 0,
19867 0,
19868 0,
19869 None,
19870 None,
19871 ), // c:694
19872 // c:Src/Modules/zprof.c:315 — zsh/zprof module.
19873 BUILTIN(
19874 "zprof",
19875 0,
19876 Some(crate::ported::modules::zprof::bin_zprof as HandlerFunc),
19877 0,
19878 0,
19879 0,
19880 Some("c"),
19881 None,
19882 ), // c:315
19883 // c:Src/Modules/datetime.c:239 — zsh/datetime module.
19884 BUILTIN(
19885 "strftime",
19886 0,
19887 Some(crate::ported::modules::datetime::bin_strftime as HandlerFunc),
19888 1,
19889 3,
19890 0,
19891 Some("nqrs:"),
19892 None,
19893 ), // c:239
19894 // c:Src/Modules/zftp.c:189 — zsh/zftp module.
19895 BUILTIN(
19896 "zftp",
19897 0,
19898 Some(crate::ported::modules::zftp::bin_zftp as HandlerFunc),
19899 1,
19900 -1,
19901 0,
19902 None,
19903 None,
19904 ), // c:189
19905 // c:Src/Modules/zpty.c:882 — zsh/zpty module.
19906 BUILTIN(
19907 "zpty",
19908 0,
19909 Some(crate::ported::modules::zpty::bin_zpty as HandlerFunc),
19910 0,
19911 -1,
19912 0,
19913 Some("ebdmrwLnt"),
19914 None,
19915 ), // c:882
19916 // c:Src/Modules/curses.c:1632 — zsh/curses module.
19917 BUILTIN(
19918 "zcurses",
19919 0,
19920 Some(crate::ported::modules::curses::bin_zcurses as HandlerFunc),
19921 1,
19922 -1,
19923 0,
19924 Some(""),
19925 None,
19926 ), // c:1632
19927 // c:Src/Modules/clone.c:110 — zsh/clone module (Linux only;
19928 // bin_clone on non-Linux is the "not available" stub).
19929 BUILTIN(
19930 "clone",
19931 0,
19932 Some(crate::ported::modules::clone::bin_clone as HandlerFunc),
19933 1,
19934 1,
19935 0,
19936 None,
19937 None,
19938 ), // c:110
19939 // c:Src/Modules/example.c — zsh/example module (template).
19940 BUILTIN(
19941 "example",
19942 0,
19943 Some(crate::ported::modules::example::bin_example as HandlerFunc),
19944 0,
19945 -1,
19946 0,
19947 Some("flags"),
19948 None,
19949 ),
19950 // c:Src/Modules/param_private.c:652 — zsh/param/private module.
19951 // BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN.
19952 // The option string is zsh 5.9.2's `...%ahi:%lmprtux`
19953 // (param_private.c:581 in the 5.9.1/5.9.2 sources), which accepts
19954 // `private -p NAME`; the dev tree's `%lnmrtux` dropped `p` when it
19955 // added `n`. `n` stays because zshrs carries named references.
19956 BUILTIN(
19957 "private",
19958 BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN,
19959 Some(crate::ported::modules::param_private::bin_private as HandlerFunc),
19960 0,
19961 -1,
19962 0,
19963 Some("AE:%F:%HL:%PR:%TUZ:%ahi:%lmnprtux"),
19964 Some("P"),
19965 ), // c:652
19966 // c:Src/Modules/termcap.c:139 — zsh/termcap module.
19967 BUILTIN(
19968 "echotc",
19969 0,
19970 Some(crate::ported::modules::termcap::bin_echotc as HandlerFunc),
19971 1,
19972 -1,
19973 0,
19974 None,
19975 None,
19976 ), // c:139
19977 // c:Src/Modules/attr.c:220-223 — zsh/attr module (4 builtins).
19978 BUILTIN(
19979 "zgetattr",
19980 0,
19981 Some(crate::ported::modules::attr::bin_getattr as HandlerFunc),
19982 2,
19983 3,
19984 0,
19985 Some("h"),
19986 None,
19987 ), // c:220
19988 BUILTIN(
19989 "zsetattr",
19990 0,
19991 Some(crate::ported::modules::attr::bin_setattr as HandlerFunc),
19992 3,
19993 3,
19994 0,
19995 Some("h"),
19996 None,
19997 ), // c:221
19998 BUILTIN(
19999 "zdelattr",
20000 0,
20001 Some(crate::ported::modules::attr::bin_delattr as HandlerFunc),
20002 2,
20003 -1,
20004 0,
20005 Some("h"),
20006 None,
20007 ), // c:222
20008 BUILTIN(
20009 "zlistattr",
20010 0,
20011 Some(crate::ported::modules::attr::bin_listattr as HandlerFunc),
20012 1,
20013 2,
20014 0,
20015 Some("h"),
20016 None,
20017 ), // c:223
20018 BUILTIN(
20019 "zle",
20020 0,
20021 Some(crate::ported::zle::zle_thingy::bin_zle as HandlerFunc),
20022 0,
20023 -1,
20024 0,
20025 // c:Src/Zle/zle_main.c:2212 — exact C optstr. The previous
20026 // string dropped g/G/r/w, so `zle -w -F fd handler`
20027 // (zzcomplete's zz-deploy-code:27 widget-mode fd watcher)
20028 // died with "bad option: -w" before bin_zle ever ran.
20029 Some("aAcCDfFgGIKlLmMNrRTUw"),
20030 None,
20031 ),
20032 // zsh/files module — file-manipulation builtins. All have
20033 // HandlerFunc-compatible signatures already.
20034 BUILTIN(
20035 "mkdir",
20036 0,
20037 Some(crate::ported::modules::files::bin_mkdir as HandlerFunc),
20038 1,
20039 -1,
20040 0,
20041 Some("pm:"),
20042 None,
20043 ),
20044 BUILTIN(
20045 "rmdir",
20046 0,
20047 Some(crate::ported::modules::files::bin_rmdir as HandlerFunc),
20048 1,
20049 -1,
20050 0,
20051 None,
20052 None,
20053 ),
20054 BUILTIN(
20055 "ln",
20056 0,
20057 Some(crate::ported::modules::files::bin_ln as HandlerFunc),
20058 1,
20059 -1,
20060 0,
20061 Some("dfins"),
20062 None,
20063 ),
20064 // `mv` — zsh/files. Same handler as `ln` with BIN_MV dispatch.
20065 // c:Src/Modules/files.c — `BUILTIN("mv", 0, bin_ln, 2, -1, BIN_MV, "fi", NULL)`.
20066 BUILTIN(
20067 "mv",
20068 0,
20069 Some(crate::ported::modules::files::bin_ln as HandlerFunc),
20070 2,
20071 -1,
20072 crate::ported::modules::files::BIN_MV,
20073 Some("fi"),
20074 None,
20075 ),
20076 BUILTIN(
20077 "rm",
20078 0,
20079 Some(crate::ported::modules::files::bin_rm as HandlerFunc),
20080 1,
20081 -1,
20082 0,
20083 Some("dfiRrs"),
20084 None,
20085 ),
20086 BUILTIN(
20087 "chmod",
20088 0,
20089 Some(crate::ported::modules::files::bin_chmod as HandlerFunc),
20090 2,
20091 -1,
20092 0,
20093 Some("Rs"),
20094 None,
20095 ),
20096 // c:Src/Modules/files.c:806 — BUILTIN("chgrp", 0, bin_chown, 2, -1, BIN_CHGRP, "hRs", NULL)
20097 BUILTIN(
20098 "chgrp",
20099 0,
20100 Some(crate::ported::modules::files::bin_chown as HandlerFunc),
20101 2,
20102 -1,
20103 crate::ported::modules::files::BIN_CHGRP,
20104 Some("hRs"),
20105 None,
20106 ),
20107 // c:Src/Modules/files.c:808 — BUILTIN("chown", 0, bin_chown, 2, -1, BIN_CHOWN, "hRs", NULL)
20108 BUILTIN(
20109 "chown",
20110 0,
20111 Some(crate::ported::modules::files::bin_chown as HandlerFunc),
20112 2,
20113 -1,
20114 crate::ported::modules::files::BIN_CHOWN,
20115 Some("hRs"),
20116 None,
20117 ),
20118 BUILTIN(
20119 "sync",
20120 0,
20121 Some(crate::ported::modules::files::bin_sync as HandlerFunc),
20122 0,
20123 0,
20124 0,
20125 None,
20126 None,
20127 ),
20128 // c:Src/Modules/files.c:816-824 — zf_* aliases. Same handlers as
20129 // chmod/chown/ln/mkdir/rm/rmdir/sync but separate BUILTIN entries
20130 // so `autoload -U zf_*` resolves and `zsh -f` sees them all.
20131 BUILTIN(
20132 "zf_chgrp",
20133 0,
20134 Some(crate::ported::modules::files::bin_chown as HandlerFunc),
20135 2,
20136 -1,
20137 crate::ported::modules::files::BIN_CHGRP,
20138 Some("hRs"),
20139 None,
20140 ), // c:816
20141 BUILTIN(
20142 "zf_chmod",
20143 0,
20144 Some(crate::ported::modules::files::bin_chmod as HandlerFunc),
20145 2,
20146 -1,
20147 0,
20148 Some("Rs"),
20149 None,
20150 ), // c:817
20151 BUILTIN(
20152 "zf_chown",
20153 0,
20154 Some(crate::ported::modules::files::bin_chown as HandlerFunc),
20155 2,
20156 -1,
20157 crate::ported::modules::files::BIN_CHOWN,
20158 Some("hRs"),
20159 None,
20160 ), // c:818
20161 BUILTIN(
20162 "zf_ln",
20163 0,
20164 Some(crate::ported::modules::files::bin_ln as HandlerFunc),
20165 1,
20166 -1,
20167 crate::ported::modules::files::BIN_LN,
20168 Some("dfins"),
20169 None,
20170 ), // c:819
20171 BUILTIN(
20172 "zf_mkdir",
20173 0,
20174 Some(crate::ported::modules::files::bin_mkdir as HandlerFunc),
20175 1,
20176 -1,
20177 0,
20178 Some("pm:"),
20179 None,
20180 ), // c:820
20181 BUILTIN(
20182 "zf_mv",
20183 0,
20184 Some(crate::ported::modules::files::bin_ln as HandlerFunc),
20185 2,
20186 -1,
20187 crate::ported::modules::files::BIN_MV,
20188 Some("fi"),
20189 None,
20190 ), // c:821
20191 BUILTIN(
20192 "zf_rm",
20193 0,
20194 Some(crate::ported::modules::files::bin_rm as HandlerFunc),
20195 1,
20196 -1,
20197 0,
20198 Some("dfiRrs"),
20199 None,
20200 ), // c:822
20201 BUILTIN(
20202 "zf_rmdir",
20203 0,
20204 Some(crate::ported::modules::files::bin_rmdir as HandlerFunc),
20205 1,
20206 -1,
20207 0,
20208 None,
20209 None,
20210 ), // c:823
20211 BUILTIN(
20212 "zf_sync",
20213 0,
20214 Some(crate::ported::modules::files::bin_sync as HandlerFunc),
20215 0,
20216 0,
20217 0,
20218 None,
20219 None,
20220 ), // c:824
20221 ]
20222});
20223// hash table containing builtin commands // c:143
20224/// Process-wide builtin lookup table. Filled lazily the first time
20225/// `builtintab()` is called; mirrors the C `mod_export HashTable
20226/// builtintab` exposed at `Src/builtin.c:146`.
20227static builtintab: OnceLock<HashMap<String, &'static builtin>> = OnceLock::new();
20228
20229/// The BUCKET-ARRAY form of `builtintab` — the storage C actually uses, kept
20230/// alongside the `HashMap` above (which stays the lookup fast path).
20231///
20232/// c:Src/builtin.c:152 `builtintab = newhashtable(85, "builtintab", NULL);`
20233/// then c:167 `addbuiltins("zsh", builtins, sizeof(builtins)/sizeof(*builtins))`,
20234/// whose loop (c:Src/module.c:547-557) walks the `builtins[]` array in
20235/// DECLARATION order calling `addbuiltin` → `builtintab->addnode` (= the
20236/// `addhashnode` / `addhashnode2` front-insert at c:Src/hashtable.c:168-222).
20237///
20238/// Enumeration order matters beyond cosmetics: `scanhashtable` walks buckets
20239/// 0..hsize-1, each chain head→tail (c:Src/hashtable.c:420-434), and that is
20240/// the order `${(k)builtins}` prints AND the order `compadd -k builtins` feeds
20241/// `join_clines`, whose ambiguous-prefix fold is not commutative. A `HashMap`
20242/// (or a sorted list) produces a different, wrong sequence.
20243///
20244/// 85 buckets vs 103 nodes never trips the c:183/219 `ct >= hsize * 2`
20245/// expansion, so the table keeps its original `hsize`.
20246/// Iterate it with `.iter()` to get C's `scanhashtable` traversal — bucket
20247/// 0..hsize-1, each chain head→tail (c:Src/hashtable.c:420-434).
20248///
20249/// Only entries that are actually IN `builtintab` are added: zshrs's
20250/// `BUILTINS` slice is the static union of every statically-linked module's
20251/// bintab, while C's table holds the core `builtins[]` plus whatever the
20252/// currently-loaded modules added. `builtin_in_builtintab` is the existing
20253/// predicate for that membership; feeding the same set through C's hash makes
20254/// the bucket walk match `zsh -f` name for name.
20255pub static BUILTINTAB_NODES: std::sync::LazyLock<
20256 crate::ported::hashtable::hashtable_nodes<&'static builtin>, // c:152
20257> = std::sync::LazyLock::new(|| {
20258 // c:152 — `newhashtable(85, "builtintab", NULL)`.
20259 let mut ht = crate::ported::hashtable::hashtable_nodes::newhashtable(85);
20260 // c:167 → c:Src/module.c:547 `for (n = 0; n < size; n++)`, i.e. the
20261 // `builtins[]` DECLARATION order. A duplicate name re-adds in place
20262 // (c:Src/hashtable.c:189-204 `replacing:`), leaving it at the same chain
20263 // position C's `BINF_ADDED` skip would.
20264 for b in BUILTINS.iter() {
20265 if !crate::ext_builtins::builtin_in_builtintab(&b.node.nam) {
20266 continue;
20267 }
20268 ht.addhashnode2(&b.node.nam, b); // c:Src/module.c:551 addbuiltin
20269 }
20270 ht
20271});
20272
20273/// Names whose `node.flags & DISABLED` is set in C. The Rust port's
20274/// `builtintab` is an immutable static, so the disabled bit lives
20275/// in this parallel set; `bin_enable` toggles it via builtin.c:587.
20276/// Dispatch sites check `is_builtin_disabled(name)` before calling
20277/// `handlerfunc` to mirror C's "skip nodes with DISABLED set" walk.
20278pub static BUILTINS_DISABLED: std::sync::LazyLock<
20279 // c:587 (Src/builtin.c)
20280 Mutex<std::collections::HashSet<String>>,
20281> = std::sync::LazyLock::new(|| Mutex::new(std::collections::HashSet::new()));
20282
20283// `shfunctab` is the canonical singleton at `hashtable::shfunctab_lock()`
20284// (RwLock<shfunc_table>); the prior parallel `shfunctab_table()` /
20285// `SHFUNCTAB_INNER` (usize-pointer Mutex<HashMap>) was deleted so the
20286// `bin_functions` C-port and the bytecode function-def path both write
20287// through one table. C-faithful access via `addnode`/`getnode`/`getnode2`
20288// methods (Src/zsh.h:281+ HashTable GSU pointers).
20289
20290// `matchednodes` global from Src/builtin.c:4550.
20291/// `MATCHEDNODES` static.
20292pub static MATCHEDNODES: Mutex<Vec<String>> = Mutex::new(Vec::new());
20293
20294// `stopmsg` global from Src/jobs.c — non-zero when checkjobs() printed.
20295/// `STOPMSG` static.
20296pub static STOPMSG: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
20297// `sfcontext` global from Src/exec.c:239 — current shell-function
20298// dispatch context (SFC_NONE / SFC_BUILTIN / SFC_FUNC / SFC_SUBST...).
20299/// `SFCONTEXT` static.
20300pub static SFCONTEXT: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:exec.c:239
20301 // `maxjob` / `thisjob` globals from Src/jobs.c:62/63 — canonical
20302 // storage lives in jobs.rs (`OnceLock<Mutex<i32>>`). The previous
20303 // builtin.rs duplicate `AtomicI32` stores NEVER synced with the
20304 // jobs.rs Mutex<i32> values that the spawn/wait paths actually
20305 // update; `checkjobs` (line 5092) read stale 0s no matter how many
20306 // jobs were active. Callers route through jobs::MAXJOB / jobs::THISJOB
20307 // directly now.
20308 // `jobstats` mirror — flat per-slot stat bits (STAT_*). Real jobtab
20309 // lives in src/ported/jobs.rs's JobTable; this mirror is updated by
20310 // the spawn/wait paths that already touch STOPMSG. Empty → no jobs,
20311 // matching the post-init state of `jobtab[]`.
20312/// `JOBSTATS` static.
20313pub static JOBSTATS: Mutex<Vec<i32>> = Mutex::new(Vec::new());
20314
20315// File-static globals for [_]realexit/zexit — c:5945+, init.c, signals.c.
20316/// `SHELL_EXITING` static.
20317pub static SHELL_EXITING: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
20318/// `EXIT_PENDING` static.
20319pub static EXIT_PENDING: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
20320/// `EXIT_VAL` static.
20321pub static EXIT_VAL: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
20322/// Port of `mod_export volatile int exit_level;` from `Src/builtin.c:5796`.
20323///
20324/// Records the `locallevel` at the moment a deferred `exit` was issued
20325/// inside a function. The `exec.c:6141` gate
20326/// `if (exit_pending && exit_level >= locallevel+1 && !in_exit_trap)`
20327/// fires only when the unwind has reached a scope at or above the
20328/// deferral point — preventing premature exit while the deferred
20329/// status walks back through nested function frames. C uses
20330/// `volatile int` because the value is read from a signal-touching
20331/// context; Rust's AtomicI32 with Relaxed ordering matches the same
20332/// no-fence read shape.
20333pub static EXIT_LEVEL: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
20334// ====================================================================
20335// !!! WARNING: RUST-ONLY ATOMIC — NO DIRECT C COUNTERPART !!!
20336// ====================================================================
20337// C zsh forks for `(...)` subshells; the child runs to completion
20338// then exits via process::exit, and the parent (post-fork) continues.
20339// zshrs runs subshells in-process via fusevm_bridge::subshell_begin/
20340// subshell_end (no fork), so `exit N` inside a subshell would call
20341// realexit() → process::exit(N) and terminate the WHOLE shell —
20342// breaking `(exit 7); echo $?` (expected: `7\n`, observed: shell
20343// dies with code 7).
20344//
20345// This counter is bumped by subshell_begin / decremented by
20346// subshell_end. zexit() at c:5977 checks it before realexit and,
20347// when > 0, sets EXIT_VAL + EXIT_PENDING and returns — letting the
20348// subshell unwinder catch the deferred exit at its boundary
20349// (mirroring what the deferred-exit path at c:5871-5891 does for
20350// function-scope exit).
20351//
20352// In C zsh, equivalent state is `forklevel` AT EXACTLY the subshell
20353// depth that fork would create — but the C check `locallevel >
20354// forklevel` is FALSE at subshell-top precisely so the fork can
20355// exit the child via realexit. Without fork, we need this extra
20356// gate.
20357/// `SUBSHELL_DEPTH` static.
20358pub static SUBSHELL_DEPTH: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
20359/// `LASTVAL` static.
20360pub static LASTVAL: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
20361
20362// `char **testargs, **curtestarg` from Src/builtin.c:7186 — the argv cursor
20363// `bin_test` seeds and `testlex()` advances. C holds pointers into the argv
20364// array; here `TESTARGS` owns the argv and `TESTARGS_IDX` / `CURTESTARG` are
20365// the offsets of `testargs` / `curtestarg` into it. Thread-local because the
20366// lexer state `testlex` writes (`tok`, `tokstr`) is thread-local too.
20367thread_local! {
20368 /// The argv `testargs` points into.
20369 pub static TESTARGS: std::cell::RefCell<Vec<String>> = const { std::cell::RefCell::new(Vec::new()) };
20370 /// Offset of `testargs` into `TESTARGS`.
20371 pub static TESTARGS_IDX: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
20372 /// Offset of `curtestarg` into `TESTARGS`.
20373 pub static CURTESTARG: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
20374}
20375
20376// int doprintdir = 0; set in exec.c (for autocd, cdpath, etc.) // c:722
20377// `doprintdir` from Src/exec.c — set when an autocd'd command should
20378// echo the new directory before executing.
20379/// `DOPRINTDIR` static.
20380pub static DOPRINTDIR: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
20381// set if we are resolving links to their true paths // c:829
20382// `chasinglinks` from Src/exec.c — non-zero when CHASELINKS / -P
20383// resolution is active.
20384/// `CHASINGLINKS` static.
20385pub static CHASINGLINKS: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
20386
20387// `pparams` global from Src/init.c — positional parameters $1..$N.
20388/// `PPARAMS` static.
20389pub static PPARAMS: Mutex<Vec<String>> = Mutex::new(Vec::new());
20390
20391// `zoptind` (Src/builtin.c:5667) and `optcind` (c:5670) — the two
20392// pieces of getopts state. zoptind backs the user-visible $OPTIND.
20393/// `ZOPTIND` static.
20394pub static ZOPTIND: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(1);
20395/// `OPTCIND` static.
20396pub static OPTCIND: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
20397
20398// `ttyfrozen` global lives canonically in jobs.rs (`OnceLock<Mutex<i32>>`
20399// at jobs.rs:2625). The previous AtomicI32 duplicate here NEVER
20400// synced with the jobs.rs store — same desync hazard as the prior
20401// MAXJOB / THISJOB fix. Callers route through jobs::TTYFROZEN.
20402
20403/// Port of `mod_export int ineval` from `Src/builtin.c:6389`. Set
20404/// while `eval` is dispatching its body (incremented before
20405/// `execode(prog, 1, 0, "eval")`, decremented after). Tested by
20406/// `IN_EVAL_TRAP()` in zsh.h:2962 to determine trap-context state.
20407pub static INEVAL: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:6389
20408
20409// `loops` / `breaks` / `contflag` / `retflag` / `locallevel` / `sourcelevel`
20410// globals from Src/loop.c + Src/init.c — control-flow state consulted by
20411// the bin_break dispatcher.
20412/// Port of `int ancestor_loops;` from `Src/loop.c:36` (55061) — the
20413/// number of nested loops started in ANCESTOR subshells; `entersubsh`
20414/// moves `loops` here (c:Src/exec.c:1263-1264) so `break`/`continue`
20415/// can tell "not in a loop" from "loop is in a parent process".
20416pub static ANCESTOR_LOOPS: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
20417/// `LOOPS` static — `int loops;` (`Src/loop.c:41`), loops started in the
20418/// current subshell.
20419pub static LOOPS: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
20420/// `BREAKS` static.
20421pub static BREAKS: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
20422/// `CONTFLAG` static.
20423pub static CONTFLAG: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
20424/// `RETFLAG` static.
20425pub static RETFLAG: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
20426// Same single-storage rationale as locallevel_param above — C zsh has
20427// only ONE `int sourcelevel;` global (Src/init.c:60). The canonical
20428// Rust port is `sourcelevel_init` (lowercase,
20429// matches C name). Re-export that single storage so the bin_break
20430// reader and the bin_dot bumps address the same atomic; without
20431// this, `bin_dot` could increment one global while `bin_break`
20432// inspected the other and `return` inside a sourced file would
20433// fall through to `zexit` (Src/builtin.c:5858).
20434// `locallevel_param` was previously a SEPARATE AtomicI32 here, but C
20435// zsh has only ONE `int locallevel;` global (Src/params.c:54).
20436// The canonical Rust port is `locallevel_param`
20437// (lowercase, matches C name). Re-export that single storage so
20438// every reader and writer addresses the same atomic — without
20439// this, `locallevel_param.store(0)` in zle/computil.rs would zero one
20440// global while `params::locallevel.fetch_add(1)` in vm_helper
20441// incremented a DIFFERENT global, leaving the two views out of
20442// sync indefinitely.
20443// `ZEXIT_NORMAL` re-exported from canonical zsh_h.rs (port of the
20444// `enum { ZEXIT_NORMAL, ZEXIT_SIGNAL, ZEXIT_DEFERRED }` in Src/zsh.h).
20445// Same single-source-of-truth pattern as TERM_UNKNOWN / HISTFLAG_*
20446// / etc — duplicate const declarations are a drift hazard.
20447
20448// Local builders that construct C-shape `builtin` rows for the
20449// static registration table below. They mirror the
20450// `BUILTIN(...)` / `BIN_PREFIX(...)` macros in `Src/zsh.h:1450-1452`,
20451// taking `u32` flag bitsets (BINF_*) and a `&str` handler-name
20452// column used only for documentation/wiring lookup — handler
20453// function pointers themselves are wired up later in
20454// `Executor::register_builtins` (`src/ported/vm_helper`).
20455//
20456// The `handler` arg was previously a `_handler_name: &'static str` that
20457// was discarded — `handlerfunc` always ended up `NULLBINCMD`, so
20458// `execbuiltin`'s c:506 `(*handlerfunc)(...)` dispatch was unreachable.
20459// Now the descriptor carries the actual port-side `HandlerFunc` so
20460// `execbuiltin` can parse flags and call through to the real builtin.
20461/// `BUILTIN` — see implementation.
20462#[allow(non_snake_case)]
20463/// Port of `BUILTIN()` from `Src/zsh.h:1450`. — C decl `BUILTIN(name, flags, handler, min, max, funcid, optstr, defopts)`
20464pub fn BUILTIN(
20465 name: &str,
20466 flags: u32,
20467 handler: Option<HandlerFunc>,
20468 min: i32,
20469 max: i32,
20470 funcid: i32,
20471 optstr: Option<&str>,
20472 defopts: Option<&str>,
20473) -> builtin {
20474 builtin {
20475 node: hashnode {
20476 next: None,
20477 nam: name.to_string(),
20478 flags: flags as i32,
20479 },
20480 handlerfunc: handler,
20481 minargs: min,
20482 maxargs: max,
20483 funcid,
20484 optstr: optstr.map(|s| s.to_string()),
20485 defopts: defopts.map(|s| s.to_string()),
20486 }
20487}
20488
20489// `traps` mirror — sig name → body. Real `sigtrapped[]`/`siglists[]`
20490// arrays live in src/ported/signals.rs; this Mutex is the static-link
20491// shim that bin_trap reads/writes.
20492static TRAPS_INNER: OnceLock<Mutex<HashMap<String, String>>> = OnceLock::new();
20493
20494#[allow(non_snake_case)]
20495/// Port of `BIN_PREFIX()` from `Src/zsh.h:1452`. — C decl `BIN_PREFIX(name, flags)`
20496fn BIN_PREFIX(name: &str, flags: u32) -> builtin {
20497 BUILTIN(name, flags | BINF_PREFIX, None, 0, 0, 0, None, None)
20498}
20499
20500/// Printf format engine used by bin_print's -f/printf mode: the spec walk of
20501/// Src/builtin.c:4691-5500 (flags, width, precision, `*`, `%n$` positional
20502/// specifiers, `%b`, `%q`, `%n`, `\c` truncation and format reuse).
20503///
20504/// Returns `Ok(output)` on success, or `Err((output_so_far, bad_char))`
20505/// when an unknown `%X` directive is hit. The C source emits
20506/// `zwarnnam(name, "%s: invalid directive", start)` at builtin.c:5435
20507/// then `return 1` — partial output already written stays written. The
20508/// Rust caller (bin_print, c:4854+) mirrors that: print the partial
20509/// output, emit `zwarnnam`, return 1.
20510/// Returns the formatted output plus the byte offset at which each
20511/// format-reuse CYCLE began (c:Src/builtin.c `splits`). `printf -v` to
20512/// an array assigns one element per cycle, so the caller slices `out`
20513/// at these boundaries.
20514/// !!! WARNING: RUST-ONLY HELPER !!!
20515///
20516/// No upstream C counterpart: this name does not exist anywhere in
20517/// `Src/`. C has no printf-spec helper functions at all: `bin_print`
20518/// (`Src/builtin.c:4587`) walks the format string inline and hands the
20519/// reconstructed spec to libc `fprintf` through the `print_val` macro
20520/// (`Src/builtin.c:4571`). Rust has no runtime format-spec engine, so
20521/// that walk is split into these hand-written pieces.
20522/// Deliberately left uncited so the port audit keeps reporting it.
20523fn printf_format(
20524 fmt: &str,
20525 args: &[String],
20526) -> Result<(String, Vec<usize>, Vec<(String, usize)>), (String, String)> {
20527 // c:Src/builtin.c:4711 — `fmt = getkeystring(fmt, &flen, ...,
20528 // GETKEYS_PRINTF_FMT, ...);`. The format string is first run
20529 // through getkeystring to interpret backslash escapes (`\n`,
20530 // `\t`, `\xNN`, etc.) before %-format substitution. The shim
20531 // `getkeystring(s)` defaults flags to 0, which (a) drops the
20532 // backslash on `\'`/`\"` (wrong for printf — zsh keeps unknown
20533 // `\<c>` literal because PRINTF_FMT excludes GETKEY_EMACS) and
20534 // (b) treats `\c` as a control-char escape rather than the
20535 // truncation marker. Route through `_with(GETKEYS_PRINTF_FMT)`
20536 // to match C exactly. `_with` takes a u32 `how` mask; the
20537 // canonical i32 const lives in `zsh_h` (Src/zsh.h:3180-3181).
20538 // !!! DASH-STRICT GATE (no C counterpart) !!! dash's printf format has no
20539 // `\c` escape: the two characters are printed as written.
20540 let fmt = if crate::dash_mode::dash_strict() {
20541 crate::dash_mode::dash_printf_keep_backslash_c(fmt)
20542 } else {
20543 fmt.to_string()
20544 };
20545 let fmt = fmt.as_str();
20546 let (fmt, _) = getkeystring_with(fmt, crate::ported::zsh_h::GETKEYS_PRINTF_FMT as u32, None); // c:builtin.c:4711
20547 // c:Src/builtin.c:4696/5382/5527 — a `\c` in the FORMAT (or, below, in
20548 // a `%b` arg) sets `fmttrunc`, which (a) truncates output here and
20549 // (b) stops the format-reuse loop entirely (no reapplication over the
20550 // remaining args). getkeystring_with already cut `fmt` at the `\c`;
20551 // take the TLS flag it set so the reuse loop knows to stop.
20552 let mut fmttrunc = crate::ported::utils::getkey_truncated_take();
20553 let mut out = String::new();
20554 let mut arg_i: usize = 0;
20555 // c:Src/builtin.c:5166/5176 — `first` is the base of the current
20556 // format-reuse cycle's argument window. `%n$` positional specs index
20557 // `first + n - 1`; at the start of each new cycle `first += maxarg`
20558 // (the highest positional used in the prior cycle). `first_off` is
20559 // the Rust analog (absolute offset into `args`).
20560 let mut first_off: usize = 0;
20561 // c:Src/builtin.c — `splits`: byte offset where each format-reuse
20562 // cycle starts (the first is 0). Used by `printf -v ARRAY`.
20563 let mut bounds: Vec<usize> = Vec::new();
20564 // c:Src/builtin.c:5427-5429 — `case 'n': if (curarg) setiparam(curarg,
20565 // count - rcount);`. `%n` consumes an argument (the variable NAME) and
20566 // stores the number of bytes emitted so far into it, printing nothing.
20567 // printf_format is a pure formatter with no shell handle, so it records
20568 // (name, byte_count_at_this_point) and the caller does the setiparam —
20569 // out.len() here is exactly C's `count - rcount`.
20570 let mut n_targets: Vec<(String, usize)> = Vec::new();
20571 // c:Src/builtin.c:4914-4923 — printf reapplies the format string
20572 // until ALL args are consumed. `printf '%s,' a b c` → `a,b,c,`,
20573 // not `a,`. The outer loop reapplies; the inner do-while body
20574 // mirrors C's per-arg conversion loop directly.
20575 loop {
20576 bounds.push(out.len());
20577 // c:Src/builtin.c:5168 `rcount = count;` reset the `%n` byte count
20578 // at the start of each format-reuse cycle, so `%n` reports bytes
20579 // emitted SINCE this cycle began, not cumulatively. `printf 'x%n' a b`
20580 // stores 1 in both a and b, not 1 and 2.
20581 let cycle_start = out.len();
20582 let prev = arg_i;
20583 // c:Src/builtin.c:5175-5178 — `maxarg` is the highest positional
20584 // (`%n$`) referenced in THIS cycle; reset per cycle and folded
20585 // into `first_off` at cycle end.
20586 let mut cycle_maxarg: usize = 0;
20587 let mut iter = fmt.chars().peekable();
20588 while let Some(c) = iter.next() {
20589 if c != '%' {
20590 out.push(c);
20591 continue;
20592 }
20593 // c:Src/builtin.c:4791+ — parse width/precision/flag chars
20594 // between `%` and the conversion. Capture them so `printf
20595 // "%-10s" hi` and `printf "%.3f" 3.14159` render correctly.
20596 let mut spec = String::from("%");
20597 // c:Src/builtin.c:5170 — `start = c++;`. `start` is the RAW
20598 // format text from the `%` onward; the invalid-directive
20599 // diagnostic (c:5414-5419) prints it after planting a temporary
20600 // NUL at `c[1]`, i.e. "% .. <bad conversion char>". `spec` is a
20601 // REWRITTEN spec (`*` width already substituted by its numeric
20602 // value), so it cannot serve as `start`; accumulate the raw text
20603 // separately as each character is consumed.
20604 let mut raw = String::from("%");
20605 // c:Src/builtin.c:5199-5219 — `%n$` positional argument
20606 // specifier. A leading 1-9 digit run followed by `$` selects
20607 // the n-th argument (1-based) for this conversion instead of
20608 // the next sequential one. A leading digit run NOT followed
20609 // by `$` is an ordinary field width (handled below), so peek
20610 // the whole run before deciding. (`0` can't start a
20611 // positional — it's a flag — so C tests `'1'..'9'`.)
20612 let mut positional: Option<usize> = None;
20613 if matches!(iter.peek(), Some(&d) if ('1'..='9').contains(&d)) {
20614 let mut digits = String::new();
20615 while let Some(&d) = iter.peek() {
20616 if d.is_ascii_digit() {
20617 digits.push(d);
20618 raw.push(d);
20619 iter.next();
20620 } else {
20621 break;
20622 }
20623 }
20624 if iter.peek() == Some(&'$') {
20625 iter.next(); // c:5202 — consume `$`
20626 raw.push('$');
20627 let narg: usize = digits.parse().unwrap_or(0);
20628 // c:5203-5212 — out-of-range positional is a hard
20629 // error (`zwarnnam(...); return 1`). `argc` in C is
20630 // the per-cycle remaining count (total minus the
20631 // already-consumed `first_off`).
20632 if narg == 0 || first_off + narg > args.len() {
20633 return Err((
20634 out,
20635 format!("{}: argument specifier out of range", narg), // c:5204
20636 ));
20637 }
20638 // c:5214-5216 — `if (narg > maxarg) maxarg = narg;`
20639 // `curarg = *(first + narg - 1);`
20640 if narg > cycle_maxarg {
20641 cycle_maxarg = narg;
20642 }
20643 positional = Some(first_off + narg - 1);
20644 } else {
20645 // Not a positional: the digits are an ordinary field
20646 // width — emit them into the spec and fall through.
20647 spec.push_str(&digits);
20648 }
20649 }
20650 loop {
20651 match iter.peek() {
20652 // c:Src/builtin.c:4791+ — printf flag chars. The `'`
20653 // (thousands-grouping) flag is passed straight to libc
20654 // by C zsh; the Rust port groups the digit run itself
20655 // in format_spec_int/uint per localeconv. Accept it here
20656 // so it isn't mistaken for the conversion char (which
20657 // produced "%': invalid directive").
20658 Some(&c) if matches!(c, '-' | '+' | ' ' | '#' | '0' | '\'') => {
20659 spec.push(c);
20660 raw.push(c);
20661 iter.next();
20662 }
20663 _ => break,
20664 }
20665 }
20666 // c:Src/builtin.c:5205-5215 — width can be either a
20667 // digit literal (`width = strtoul(c, &endptr, 0); c = endptr;`)
20668 // or `*` (consume an arg as the width).
20669 // Without `*` handling, `printf '%*d' 4 7` rendered the
20670 // literal `%*d` because the iter saw `*` and aborted the
20671 // spec walk before reaching the conversion char.
20672 if iter.peek() == Some(&'*') {
20673 iter.next(); // c:5207 — consume the `*` marker
20674 raw.push('*');
20675 // c:5208-5231 — `if (idigit(*++c))`: a `*` width may itself
20676 // carry a positional specifier (`%*n$d`), naming which
20677 // argument supplies the width. Unlike the leading `%n$`
20678 // (c:5183, which tests `'1'..'9'`), `idigit` here accepts
20679 // `0` too, so `%*0$d` reaches the range check and errors.
20680 if matches!(iter.peek(), Some(&d) if d.is_ascii_digit()) {
20681 let raw_before_digits = raw.len();
20682 let mut digits = String::new();
20683 while let Some(&d) = iter.peek() {
20684 if d.is_ascii_digit() {
20685 digits.push(d);
20686 raw.push(d);
20687 iter.next();
20688 } else {
20689 break;
20690 }
20691 }
20692 if iter.peek() == Some(&'$') {
20693 iter.next(); // c:5211 — `c = endptr + 1;`
20694 raw.push('$');
20695 let narg: usize = digits.parse().unwrap_or(0);
20696 // c:5212-5224 — `if (narg > argc || narg <= 0)`.
20697 // `argc` is the remaining window (total minus the
20698 // already-folded `first_off`).
20699 if narg == 0 || first_off + narg > args.len() {
20700 return Err((
20701 out,
20702 format!("{}: argument specifier out of range", narg), // c:5214
20703 ));
20704 }
20705 // c:5227-5228 — `if (narg > maxarg) maxarg = narg;`
20706 // `argp = first + narg - 1;`
20707 // NOTE: unlike the leading `%n$` (which only sets
20708 // `curarg`), a `*n$` width MOVES the sequential
20709 // cursor `argp`, and the `argp++` below leaves it
20710 // past the width argument.
20711 if narg > cycle_maxarg {
20712 cycle_maxarg = narg;
20713 }
20714 arg_i = first_off + narg - 1;
20715 } else {
20716 // c:5208-5210 — digits NOT followed by `$` are left
20717 // unconsumed by C (`c` still points at the first of
20718 // them). They therefore fall through the `.`/size
20719 // -modifier tests and land on the conversion switch,
20720 // where a digit is an unknown directive. Peekable
20721 // cannot push the run back, so raise the identical
20722 // diagnostic here: `start` truncated after the first
20723 // digit (c:5416-5418 plants the NUL at `c[1]`).
20724 let mut msg = raw[..raw_before_digits].to_string();
20725 msg.push(digits.as_bytes()[0] as char);
20726 return Err((out, format!("{}: invalid directive", msg)));
20727 // c:5419
20728 }
20729 }
20730 // c:5232-5238 — `if (*argp) { width = mathevali(...); argp++;
20731 // ... }`. The `*` width arg is MATH-EVALUATED, not a plain
20732 // integer parse: `0x1f`->31, ` 4`->4, `'A`->65, `2+3`->5.
20733 // parse_int_arg is the shared getnum->mathevali path (the same
20734 // one the %d/%i arm uses), and on a math error it zeroes the
20735 // value and flags ret=1 (c:5235 `errflag -> ret = 1`).
20736 // When the args have run out `width` stays at its 0 init
20737 // (c:5180) and `argp` is NOT advanced.
20738 if let Some(a) = args.get(arg_i) {
20739 let w: i64 = parse_int_arg(a);
20740 arg_i += 1; // c:5234
20741 spec.push_str(&w.to_string());
20742 }
20743 } else {
20744 while let Some(&c) = iter.peek() {
20745 if c.is_ascii_digit() {
20746 spec.push(c);
20747 raw.push(c);
20748 iter.next();
20749 } else {
20750 break;
20751 }
20752 }
20753 }
20754 if iter.peek() == Some(&'.') {
20755 iter.next(); // c:5248 — consume the `.`
20756 raw.push('.');
20757 // c:5249 — `if (*++c == '*')`: precision accepts `*` exactly
20758 // like width, positional specifier included.
20759 if iter.peek() == Some(&'*') {
20760 iter.next();
20761 raw.push('*');
20762 // c:5250-5273 — `if (idigit(*++c))`: `%.*n$d` names the
20763 // argument that supplies the precision.
20764 if matches!(iter.peek(), Some(&d) if d.is_ascii_digit()) {
20765 let raw_before_digits = raw.len();
20766 let mut digits = String::new();
20767 while let Some(&d) = iter.peek() {
20768 if d.is_ascii_digit() {
20769 digits.push(d);
20770 raw.push(d);
20771 iter.next();
20772 } else {
20773 break;
20774 }
20775 }
20776 if iter.peek() == Some(&'$') {
20777 iter.next(); // c:5253 — `c = endptr + 1;`
20778 raw.push('$');
20779 let narg: usize = digits.parse().unwrap_or(0);
20780 // c:5254-5266 — `if (narg > argc || narg <= 0)`
20781 if narg == 0 || first_off + narg > args.len() {
20782 return Err((
20783 out,
20784 format!("{}: argument specifier out of range", narg), // c:5256
20785 ));
20786 }
20787 // c:5269-5270 — `if (narg > maxarg) maxarg = narg;`
20788 // `argp = first + narg - 1;`
20789 if narg > cycle_maxarg {
20790 cycle_maxarg = narg;
20791 }
20792 arg_i = first_off + narg - 1;
20793 } else {
20794 // c:5250-5252 — see the width case above: an
20795 // un-`$`-terminated digit run is left for the
20796 // conversion switch, i.e. an invalid directive.
20797 let mut msg = raw[..raw_before_digits].to_string();
20798 msg.push(digits.as_bytes()[0] as char);
20799 return Err((out, format!("{}: invalid directive", msg)));
20800 // c:5419
20801 }
20802 }
20803 // c:Src/builtin.c:5275-5288 — the `*` precision arg is
20804 // math-evaluated identically to the `*` width arg
20805 // (`prec = (int)mathevali(metafy(*argp, …))`, error →
20806 // ret=1) — BUT only when the arg actually EXISTS
20807 // (c:5275 `if (*argp)`). If args have run out, `prec`
20808 // stays at its -1 init (c:5178) and NO precision is
20809 // emitted (c:5288 `if (prec >= 0) *d++ = '.', *d++ =
20810 // '*'`). A negative result is unset the same way.
20811 // Unlike width (init 0), a MISSING `*` precision must
20812 // leave the conversion at default precision, not force
20813 // `.0`: `printf '%.*d' ` prints `0` (default), not the
20814 // truncated-to-empty `%.0d`. Previously `unwrap_or(0)`
20815 // forced `.0` and dropped the digit.
20816 if let Some(a) = args.get(arg_i) {
20817 let p = parse_int_arg(a);
20818 arg_i += 1;
20819 if p >= 0 {
20820 spec.push('.');
20821 spec.push_str(&p.to_string());
20822 }
20823 }
20824 } else {
20825 // Literal precision: `.` then optional digits (an empty
20826 // digit run is precision 0, c:5283-5287 `else prec = 0`).
20827 spec.push('.');
20828 while let Some(&c) = iter.peek() {
20829 if c.is_ascii_digit() {
20830 spec.push(c);
20831 raw.push(c);
20832 iter.next();
20833 } else {
20834 break;
20835 }
20836 }
20837 }
20838 }
20839 // c:Src/builtin.c:5290 — `/* ignore any size modifier */
20840 // if (*c == 'l' || *c == 'L' || *c == 'h') c++;`. zsh accepts
20841 // and discards a single length modifier (`%ld`, `%lu`, `%hd`)
20842 // — the conversion char that follows determines the type and
20843 // C rebuilds the libc format as `%l<conv>` itself. NOT pushed
20844 // into `spec`: it would corrupt the format passed to the
20845 // format_spec_* helpers. Only one modifier char is skipped,
20846 // matching C (so `%lld` then errors on the second `l`).
20847 if let Some(&m) = iter.peek() {
20848 if matches!(m, 'l' | 'L' | 'h') && !crate::dash_mode::dash_strict() {
20849 raw.push(m); // c:5290 — `c++` past the modifier; `start` still covers it
20850 iter.next();
20851 }
20852 }
20853 // c:Src/builtin.c:5215/5310 — a `%n$` positional spec sets
20854 // `curarg` directly; the conversion arms below read from
20855 // `arg_i`, so point it at the positional index for the
20856 // duration of the conversion, then restore. Restoring means
20857 // the positional reference does NOT advance the sequential
20858 // cursor (C only advances `argp` for non-positional args).
20859 let saved_argi = arg_i;
20860 let is_positional = positional.is_some();
20861 if let Some(p) = positional {
20862 arg_i = p;
20863 }
20864 match iter.next() {
20865 // c:Src/builtin.c:5414-5419 — a bare `%%` prints `%`, but a
20866 // `%` directive carrying any flag / width / precision / `*` is
20867 // an "invalid directive": zsh handles doubled `%%` in the
20868 // literal scan (c:5172) and only reaches the conversion switch
20869 // (which has no `%` case → default) once modifiers intervened.
20870 // The test is on the SOURCE text (`raw`, C's `start`), not on
20871 // the rebuilt `spec`: a `*` width whose argument list is
20872 // exhausted adds nothing to `spec` (c:5232 `if (*argp)`), yet
20873 // `%*%` is still a directive with a modifier, and the message
20874 // prints `start` verbatim (`%*%`, not the substituted `%3%`).
20875 Some('%') if raw != "%" => {
20876 return Err((out, format!("{raw}%: invalid directive"))); // c:5419
20877 }
20878 Some('%') => out.push('%'),
20879 Some('n') => {
20880 // c:5427-5428 — `if (curarg) setiparam(curarg, count -
20881 // rcount);`. The arg is the target variable's NAME; nothing
20882 // is emitted. `if (curarg)` tests that an arg SLOT exists,
20883 // not that it is non-empty — an empty or malformed name is
20884 // still handed to setiparam, which errors `not an
20885 // identifier`. Only a wholly ABSENT arg is the no-op.
20886 // `count - rcount` is bytes since the cycle start
20887 // (c:5168), i.e. out.len() - cycle_start here.
20888 if let Some(name) = args.get(arg_i) {
20889 n_targets.push((name.clone(), out.len() - cycle_start));
20890 // c:5428
20891 }
20892 arg_i += 1;
20893 }
20894 Some('s') => {
20895 let a = args.get(arg_i).cloned().unwrap_or_default();
20896 spec.push('s');
20897 out.push_str(&format_spec_str(&spec, &a, false));
20898 arg_i += 1;
20899 }
20900 // bash/ksh `printf '%(FMT)T' TS` — strftime(FMT, TS). The
20901 // timestamp is epoch seconds; a NEGATIVE value or a MISSING
20902 // arg means "now" (bash also defines -2 = shell start, treated
20903 // as now here). zsh's printf has NO such directive (it uses the
20904 // `strftime` builtin), so it is gated to bash / ksh emulation;
20905 // under --zsh it stays an "invalid directive" like real zsh.
20906 Some('(') => {
20907 // korn_mode() (a bare --ksh/--mksh/--pdksh), not EMULATION(KSH): zsh itself
20908 // rejects the directive under `emulate ksh` (5.9.2: "%(: invalid directive").
20909 let allow = crate::dash_mode::bash_mode() || crate::dash_mode::korn_mode();
20910 // Collect the strftime format up to the matching ')'.
20911 let mut tfmt = String::new();
20912 let mut closed = false;
20913 for c in iter.by_ref() {
20914 if c == ')' {
20915 closed = true;
20916 break;
20917 }
20918 tfmt.push(c);
20919 }
20920 // The conversion char after ')' must be 'T'.
20921 let conv_t = iter.next() == Some('T');
20922 if !allow || !closed || !conv_t {
20923 return Err((out, format!("{}(: invalid directive", spec)));
20924 }
20925 let now = crate::ported::modules::datetime::getcurrenttime()
20926 .first()
20927 .and_then(|s| s.parse::<i64>().ok())
20928 .unwrap_or(0);
20929 let ts: i64 = match args.get(arg_i) {
20930 Some(a) => {
20931 arg_i += 1;
20932 let v = parse_int_arg(a);
20933 if v < 0 {
20934 now
20935 } else {
20936 v
20937 }
20938 }
20939 None => now,
20940 };
20941 let formatted = (|| -> Option<String> {
20942 let mut tm: libc::tm = unsafe { std::mem::zeroed() };
20943 let t = ts as libc::time_t;
20944 let cfmt = std::ffi::CString::new(tfmt.as_str()).ok()?;
20945 unsafe {
20946 if libc::localtime_r(&t, &mut tm).is_null() {
20947 return None;
20948 }
20949 let mut buf = vec![0u8; 256];
20950 let n = libc::strftime(
20951 buf.as_mut_ptr() as *mut libc::c_char,
20952 buf.len(),
20953 cfmt.as_ptr(),
20954 &tm,
20955 );
20956 buf.truncate(n);
20957 String::from_utf8(buf).ok()
20958 }
20959 })()
20960 .unwrap_or_default();
20961 // Apply any field width/flags to the rendered string, the
20962 // same way `%s` does.
20963 spec.push('s');
20964 out.push_str(&format_spec_str(&spec, &formatted, false));
20965 }
20966 Some('d') | Some('i') => {
20967 if crate::extensions::dash_mode::bash_printf_empty_numeric_error(
20968 args.get(arg_i),
20969 ) {
20970 PRINTF_MATH_ERR.with(|c| c.set(true));
20971 }
20972 let a = args.get(arg_i).cloned().unwrap_or_default();
20973 let n = parse_int_arg(&a);
20974 spec.push('d');
20975 out.push_str(&format_spec_int(&spec, n));
20976 arg_i += 1;
20977 }
20978 Some('u') => {
20979 if crate::extensions::dash_mode::bash_printf_empty_numeric_error(
20980 args.get(arg_i),
20981 ) {
20982 PRINTF_MATH_ERR.with(|c| c.set(true));
20983 }
20984 let a = args.get(arg_i).cloned().unwrap_or_default();
20985 // c:Src/builtin.c:5511 — `if (!zstrtoul_underscore(
20986 // curarg, &zulongval)) zulongval = mathevali(...)`.
20987 // Unsigned conversions first try a direct unsigned
20988 // parse (so a full-range literal like
20989 // 18446744073709551615 is NOT truncated by the
20990 // signed-math path), then fall back to math eval
20991 // (which wraps a negative result into u64).
20992 let n = crate::ported::utils::zstrtoul_underscore(a.trim())
20993 .unwrap_or_else(|| parse_int_arg(&a) as u64);
20994 spec.push('u');
20995 out.push_str(&format_spec_uint(&spec, n));
20996 arg_i += 1;
20997 }
20998 Some('x') => {
20999 if crate::extensions::dash_mode::bash_printf_empty_numeric_error(
21000 args.get(arg_i),
21001 ) {
21002 PRINTF_MATH_ERR.with(|c| c.set(true));
21003 }
21004 let a = args.get(arg_i).cloned().unwrap_or_default();
21005 // c:Src/builtin.c:5511 — `if (!zstrtoul_underscore(
21006 // curarg, &zulongval)) zulongval = mathevali(...)`.
21007 // Unsigned conversions first try a direct unsigned
21008 // parse (so a full-range literal like
21009 // 18446744073709551615 is NOT truncated by the
21010 // signed-math path), then fall back to math eval
21011 // (which wraps a negative result into u64).
21012 let n = crate::ported::utils::zstrtoul_underscore(a.trim())
21013 .unwrap_or_else(|| parse_int_arg(&a) as u64);
21014 spec.push('x');
21015 out.push_str(&format_spec_radix(&spec, n, 'x'));
21016 arg_i += 1;
21017 }
21018 Some('X') => {
21019 if crate::extensions::dash_mode::bash_printf_empty_numeric_error(
21020 args.get(arg_i),
21021 ) {
21022 PRINTF_MATH_ERR.with(|c| c.set(true));
21023 }
21024 let a = args.get(arg_i).cloned().unwrap_or_default();
21025 // c:Src/builtin.c:5511 — `if (!zstrtoul_underscore(
21026 // curarg, &zulongval)) zulongval = mathevali(...)`.
21027 // Unsigned conversions first try a direct unsigned
21028 // parse (so a full-range literal like
21029 // 18446744073709551615 is NOT truncated by the
21030 // signed-math path), then fall back to math eval
21031 // (which wraps a negative result into u64).
21032 let n = crate::ported::utils::zstrtoul_underscore(a.trim())
21033 .unwrap_or_else(|| parse_int_arg(&a) as u64);
21034 spec.push('X');
21035 out.push_str(&format_spec_radix(&spec, n, 'X'));
21036 arg_i += 1;
21037 }
21038 Some('o') => {
21039 if crate::extensions::dash_mode::bash_printf_empty_numeric_error(
21040 args.get(arg_i),
21041 ) {
21042 PRINTF_MATH_ERR.with(|c| c.set(true));
21043 }
21044 let a = args.get(arg_i).cloned().unwrap_or_default();
21045 // c:Src/builtin.c:5511 — `if (!zstrtoul_underscore(
21046 // curarg, &zulongval)) zulongval = mathevali(...)`.
21047 // Unsigned conversions first try a direct unsigned
21048 // parse (so a full-range literal like
21049 // 18446744073709551615 is NOT truncated by the
21050 // signed-math path), then fall back to math eval
21051 // (which wraps a negative result into u64).
21052 let n = crate::ported::utils::zstrtoul_underscore(a.trim())
21053 .unwrap_or_else(|| parse_int_arg(&a) as u64);
21054 spec.push('o');
21055 out.push_str(&format_spec_radix(&spec, n, 'o'));
21056 arg_i += 1;
21057 }
21058 // c:Src/builtin.c:5398-5403 — the float `type=2` case list is
21059 // exactly `e E f g G`; `%F` is NOT in it and falls to the
21060 // c:5413 default arm ("%F: invalid directive", rc 1). bash and
21061 // POSIX `sh` DO accept `%F` (verified: `/bin/sh -c "printf
21062 // '%F\n' 1"` → 1.000000), so keep it valid in those modes.
21063 Some(conv @ ('f' | 'F' | 'g' | 'G' | 'e' | 'E'))
21064 if conv != 'F'
21065 || crate::dash_mode::bash_mode()
21066 || crate::dash_mode::posix_faithful() =>
21067 {
21068 let a = args.get(arg_i).cloned().unwrap_or_default();
21069 // c:Src/builtin.c:5479-5488 — `doubleval = strtod(curarg,
21070 // &eptr); if (*eptr) mnumval = matheval(curarg)`. The arg
21071 // is parsed as a numeric constant first (handles "2.5",
21072 // "nan", "inf"); if that leaves leftover (a variable name
21073 // like `f`, or an expression like "1.5+1"), it is
21074 // evaluated as a math expression. The previous port did
21075 // only the constant parse, so `printf %g f` / `%f 1.5+1`
21076 // yielded 0.
21077 let n: f64 =
21078 if let Some(rest) = a.strip_prefix('\'').or_else(|| a.strip_prefix('"')) {
21079 // c:Src/builtin.c:5431-5447 — a leading `'`/`"` makes
21080 // the value the numeric char code of the next char,
21081 // for FLOAT conversions too (shared with int):
21082 // `printf '%.1E' \'B` → 66 → 6.6E+01. The int arm
21083 // (parse_int_arg) already did this; the float arm
21084 // skipped it, yielding 0.
21085 // c:5433-5442 — decode one character of the unmetafied
21086 // arg; on WEOF use the raw byte (as parse_int_arg).
21087 {
21088 let raw = crate::ported::utils::unmetafy_str(rest);
21089 if raw.is_empty() {
21090 0.0
21091 } else {
21092 match crate::ported::utils::mb_metacharlenconv(&raw).1 {
21093 Some(c) => c as i64 as f64,
21094 None => raw[0] as f64,
21095 }
21096 }
21097 }
21098 } else {
21099 a.parse::<f64>().unwrap_or_else(|_| match matheval(&a) {
21100 Ok(m) if m.type_ == crate::ported::math::MN_FLOAT => m.d,
21101 Ok(m) => m.l as f64,
21102 // c:Src/builtin.c:5488 — a math-eval failure on
21103 // a FLOAT operand is a SOFT error, same as the
21104 // integer path: emit the diagnostic, clear the
21105 // errflag so later args still evaluate, and flag
21106 // ret=1. The float arm previously swallowed it,
21107 // so `printf '%g' '%d'` exited 0 where zsh exits
21108 // 1. (Empty/missing args math-eval to Ok(0), so
21109 // they do NOT trip this.)
21110 Err(msg) => {
21111 crate::ported::utils::zerr(&msg);
21112 crate::ported::utils::errflag.fetch_and(
21113 !crate::ported::utils::ERRFLAG_ERROR,
21114 std::sync::atomic::Ordering::Relaxed,
21115 );
21116 PRINTF_MATH_ERR.with(|c| c.set(true));
21117 0.0
21118 }
21119 })
21120 };
21121 // c:Src/builtin.c printf %g/%G uses libc snprintf
21122 // which strips trailing zeros; %e/%E uses scientific.
21123 out.push_str(&format_spec_float_conv(&spec, n, conv));
21124 arg_i += 1;
21125 }
21126 Some('c') => {
21127 // c:5300-5306 — `%c` outputs the arg's first char with
21128 // WIDTH/justify applied (`%3c`→" x", `%-3c`→"x ") but
21129 // precision IGNORED (`%.0c`→"x"). Strip precision from the
21130 // spec, then format as a one-char string. The previous port
21131 // pushed the char raw, dropping width.
21132 let a = args.get(arg_i).cloned().unwrap_or_default();
21133 // c:5300-5305 — `intval = curarg ? *curarg : 0`. An
21134 // empty or missing arg yields `*curarg == '\0'`, so
21135 // `%c` emits a NUL byte (not nothing). zsh:
21136 // `printf "%c" "" | od` → 00.
21137 // `*curarg` is the first BYTE of the unmetafied arg, not
21138 // its first character: `printf %c é` writes the lone
21139 // lead byte 0xc3. Keep it in the port's metafied form
21140 // (Meta + byte^32 for 0x80..) so the final unmetafy
21141 // writes that one raw byte; libc's `%*c` then pads by
21142 // one byte.
21143 let byte = crate::ported::utils::unmetafy_str(&a)
21144 .first()
21145 .copied()
21146 .unwrap_or(0);
21147 let mut ch_str = String::new();
21148 if byte < 0x80 {
21149 ch_str.push(byte as char);
21150 } else {
21151 ch_str.push('\u{83}');
21152 ch_str.push(char::from(byte ^ 32));
21153 }
21154 let (left_align, zero_pad, width, _) = parse_flags_width_prec(&spec);
21155 let zero_pad = zero_pad && crate::host_libc::printf_zero_flag_pads_strings();
21156 let pad = width.saturating_sub(1);
21157 if left_align {
21158 out.push_str(&ch_str);
21159 out.push_str(&" ".repeat(pad));
21160 } else {
21161 out.push_str(&(if zero_pad { "0" } else { " " }).repeat(pad));
21162 out.push_str(&ch_str);
21163 }
21164 arg_i += 1;
21165 }
21166 // c:builtin.c:5403-5409 %q — shell-quote the arg using
21167 // QT_BACKSLASH_SHOWNULL (backslash-escape form), NOT
21168 // QT_QUOTEDZPUTS (single-quote form).
21169 //
21170 // c: stringval = quotestring(metafy(curarg, …),
21171 // QT_BACKSLASH_SHOWNULL);
21172 //
21173 // Symptom of the previous quotedzputs choice:
21174 // printf "%q\n" "a b c"
21175 // zshrs (before): 'a b c' zsh: a\ b\ c
21176 //
21177 // p10k uses `printf "%q "` for shell-quoting cached
21178 // command lines; the difference makes those caches
21179 // unreadable in zsh-syntax debuggers expecting the
21180 // backslash form.
21181 // !!! DASH-STRICT GATE (no C counterpart) !!! dash's printf
21182 // has no `%q` directive; it errors "invalid directive". Under
21183 // dash_strict, skip this arm so `%q` falls to the `Some(other)`
21184 // arm below, which produces exactly that error like /bin/dash.
21185 Some('q') if !crate::dash_mode::dash_strict() => {
21186 // c:Src/builtin.c:5387-5391 — `stringval = curarg ?
21187 // quotestring(...) : &nullstr`. A MISSING argument (curarg
21188 // == NULL) is the empty nullstr and is NOT quoted, so
21189 // `printf '%q'` prints nothing — not `''`. A PRESENT arg,
21190 // even an empty string, IS quoted (→ `''`).
21191 let quoted = match args.get(arg_i) {
21192 Some(a) => if crate::dash_mode::ksh93_mode() {
21193 crate::dash_mode::ksh93_printf_q(a)
21194 } else if crate::dash_mode::bash_mode()
21195 && crate::dash_mode::bash_ansic_shouldquote(a)
21196 {
21197 // bash: a value with control characters prints as one `$'…'`.
21198 crate::dash_mode::bash_ansic_quote(a)
21199 } else {
21200 crate::ported::utils::quotestring(
21201 a,
21202 crate::ported::zsh_h::QT_BACKSLASH_SHOWNULL,
21203 )
21204 },
21205 None => String::new(),
21206 };
21207 // c:Src/builtin.c:5405-5407 — `%q` sets `*d = 's'`
21208 // and runs the quoted value through the normal string
21209 // output (`print_val`), so width/precision/flags from
21210 // the spec apply (`printf "%-5q" a` → "a ").
21211 spec.push('s');
21212 out.push_str(&format_spec_str(&spec, "ed, true));
21213 arg_i += 1;
21214 }
21215 // c:builtin.c:5332-5336 — `%b` uses GETKEYS_PRINTF_ARG
21216 // (Src/zsh.h:3183 = GETKEY_BACKSLASH_C alone), NOT
21217 // GETKEYS_PRINT. The distinction:
21218 // GETKEYS_PRINT = OCTAL_ESC | EMACS | BACKSLASH_C
21219 // `\NNN` → octal, `\<c>` → drop backslash.
21220 // GETKEYS_PRINTF_ARG = BACKSLASH_C
21221 // `\NNN` → literal, `\0NNN` → octal, `\<c>` → keep both.
21222 // Bourne-style `printf '%b' '\141'` keeps `\141` literal
21223 // (only `\0NNN` is octal in the arg interpretation),
21224 // matching zsh. Previous Rust port used GETKEYS_PRINT,
21225 // so `\141` was octal-eval'd to `a` — diverged.
21226 Some('b') => {
21227 let a = args.get(arg_i).cloned().unwrap_or_default();
21228 // !!! BASH-MODE (no C counterpart) !!! bash `%b` also reads a
21229 // bare `\NNN` (up to 3 digits) as octal: `printf %b '\101'` -> A,
21230 // '\1' -> ^A. Respell each as zsh's `\0NNN` introducer form (which
21231 // GETKEYS_PRINTF_ARG already decodes) so `\0101`, `\x41` and
21232 // `\\` keep their getkeystring meaning.
21233 let a = if crate::dash_mode::bash_mode() {
21234 const BS: char = '\u{5c}';
21235 let cs: Vec<char> = a.chars().collect();
21236 let mut o = String::with_capacity(a.len());
21237 let mut i = 0;
21238 while i < cs.len() {
21239 if cs[i] != BS || i + 1 >= cs.len() {
21240 o.push(cs[i]);
21241 i += 1;
21242 } else if cs[i + 1] == '0' || !cs[i + 1].is_digit(8) {
21243 // `\0NNN`, `\\`, `\x..`, `\n` ...: pass the pair through.
21244 o.push(BS);
21245 o.push(cs[i + 1]);
21246 i += 2;
21247 } else {
21248 let digits: String =
21249 cs[i + 1..].iter().take(3).take_while(|c| c.is_digit(8)).collect();
21250 o.push(BS);
21251 o.push('0');
21252 o.push_str(&digits);
21253 i += 1 + digits.len();
21254 }
21255 }
21256 o
21257 } else {
21258 a
21259 };
21260 // !!! KSH93-ONLY (no C counterpart) !!! ksh93 `%b` decodes the same
21261 // narrower escape set as its `print` (dash_mode::ksh93_print_escapes).
21262 let (s, arg_truncated) = if crate::dash_mode::ksh93_mode() {
21263 crate::dash_mode::ksh93_print_escapes(&a)
21264 } else {
21265 let (s, _) = getkeystring_with(
21266 &a,
21267 crate::ported::zsh_h::GETKEYS_PRINTF_ARG as u32,
21268 None,
21269 );
21270 (s, crate::ported::utils::getkey_truncated_take())
21271 };
21272 // c:5380-5383 — a `\c` inside the `%b` arg truncates:
21273 // emit the expansion up to `\c`, then stop the whole
21274 // printf (no rest-of-format, no reuse). getkeystring_with
21275 // already cut `s` at the `\c` and set the TLS flag.
21276 // c:5307-5360 — `%b` shares the `%s` width+precision
21277 // handling (`%5b`→" ab", `%3.1b`→" a"), applied to the
21278 // escape-expanded string. The previous port pushed the
21279 // expansion raw, dropping width/precision.
21280 spec.push('s');
21281 out.push_str(&format_spec_str(&spec, &s, false));
21282 arg_i += 1;
21283 if arg_truncated {
21284 fmttrunc = true; // c:5382
21285 break; // c:5383 — truncate the format here (stop this cycle)
21286 }
21287 }
21288 // c:builtin.c:5420 — `%n` consumes its arg but writes
21289 // nothing. C printf writes the byte-count-so-far to
21290 // the int pointer; zsh has no pointer to write to, so
21291 // it silently drops the directive. Previous Rust port
21292 // fell to the unknown-arm and emitted literal `%n`,
21293 // breaking `printf "%n" x; echo y` (zsh emits `y`,
21294 // zshrs emitted `%ny`).
21295 Some('n') => {
21296 arg_i += 1;
21297 }
21298 // c:builtin.c:5430-5443 — unknown directive in C is
21299 // a hard error: `zwarnnam(name, "%s: invalid
21300 // directive", start); ...; return 1;`. Bubble the
21301 // bad char up via Err; the caller emits the warning
21302 // and returns rc=1. Partial output already written
21303 // is preserved (matches C — the warning fires AFTER
21304 // earlier output bytes have already been emitted).
21305 Some(other) => {
21306 // c:5414-5419 — the message echoes `start`: the RAW
21307 // format text from `%` through the bad conversion char
21308 // inclusive.
21309 // c:5415-5417 — `if (*c) { save = c[1]; c[1] = '\0'; }`
21310 // i.e. the temporary NUL lands AFTER the bad char, but
21311 // only when the bad char is itself non-NUL. A conversion
21312 // char that IS a NUL (`printf $'%\0'`) leaves `start`
21313 // terminated at the `%` itself, so zsh prints just "%".
21314 // `start` is a C string either way, so it also stops at
21315 // any earlier embedded NUL.
21316 let mut msg = raw.clone();
21317 if other != '\0' {
21318 msg.push(other); // c:5416
21319 }
21320 if let Some(nul) = msg.find('\0') {
21321 msg.truncate(nul);
21322 }
21323 return Err((out, format!("{}: invalid directive", msg)));
21324 // c:5419
21325 }
21326 // c:Src/builtin.c:5430-5436 — a bare `%` with nothing
21327 // after it (end of format) is an invalid directive, same
21328 // as an unknown conversion char: emit any preceding output
21329 // then error (rc 1). The previous port pushed a literal
21330 // `%`, so `printf '%'` printed `%` with rc 0.
21331 None => return Err((out, format!("{}: invalid directive", spec))),
21332 }
21333 // c:Src/builtin.c — restore the sequential cursor after a
21334 // positional conversion (see the swap above).
21335 if is_positional {
21336 arg_i = saved_argi;
21337 }
21338 }
21339 // c:Src/builtin.c:5521-5522 — `if (maxarg && (argp - first > maxarg))
21340 // maxarg = argp - first;`
21341 // C runs this at the tail of every conversion; `argp` only ever
21342 // grows within a cycle and `maxarg` is not read again until the
21343 // cycle ends, so evaluating it once here is equivalent.
21344 if cycle_maxarg > 0 && arg_i.saturating_sub(first_off) > cycle_maxarg {
21345 cycle_maxarg = arg_i - first_off;
21346 }
21347 // c:Src/builtin.c:5525 — `if (maxarg) argp = first + maxarg;`
21348 if cycle_maxarg > 0 {
21349 arg_i = first_off + cycle_maxarg;
21350 }
21351 let _ = prev;
21352 // c:Src/builtin.c:5527 — `} while (*argp && argp != first &&
21353 // !fmttrunc && !OPT_ISSET(ops,'r'));`. A `\c` in the format or
21354 // in a `%b` arg stops the reuse loop. `argp != first` is C's
21355 // "did this cycle consume anything" guard — note `first` only ever
21356 // advances by `maxarg`, so with no positional specs it stays at
21357 // args[0] and the test is simply "argp moved".
21358 if fmttrunc || arg_i >= args.len() || arg_i == first_off {
21359 break;
21360 }
21361 // c:Src/builtin.c:5158-5162 (top of the next iteration) —
21362 // `if (maxarg) { first += maxarg; argc -= maxarg; maxarg = 0; }`
21363 first_off += cycle_maxarg;
21364 }
21365 Ok((out, bounds, n_targets))
21366}
21367
21368/// Apply a printf-style `%[-flag][width][.prec]s` spec to a string.
21369/// Mirrors C `printf "%-10s" str` formatting; the Rust `format!` macro
21370/// doesn't accept runtime-parsed specs so we hand-parse.
21371/// !!! WARNING: RUST-ONLY HELPER !!!
21372///
21373/// No upstream C counterpart: this name does not exist anywhere in
21374/// `Src/`. C has no printf-spec helper functions at all: `bin_print`
21375/// (`Src/builtin.c:4587`) walks the format string inline and hands the
21376/// reconstructed spec to libc `fprintf` through the `print_val` macro
21377/// (`Src/builtin.c:4571`). Rust has no runtime format-spec engine, so
21378/// that walk is split into these hand-written pieces.
21379/// Deliberately left uncited so the port audit keeps reporting it.
21380fn format_spec_str(spec: &str, s: &str, honor_zero: bool) -> String {
21381 // zsh's `%c` and `%q` route through `print_val`, which HONORS the `0` flag
21382 // (`printf %04c x` → `000x`, `%04q a` → `000a`), so those callers pass
21383 // honor_zero=true. `%s`/`%b` are handled inline with FORCED space padding
21384 // (Src/builtin.c:5375 `fprintf(fout, "%*c", …, ' ')` → `%04s ab` = " ab"),
21385 // so they pass honor_zero=false.
21386 let (left_align, zero_pad_flag, width, prec) = parse_flags_width_prec(spec);
21387 let honor_zero = honor_zero && crate::host_libc::printf_zero_flag_pads_strings();
21388 if !honor_zero {
21389 // c:Src/builtin.c:5335-5372 — the inline `%s`/`%b` arm counts the
21390 // argument's characters with `mbrlen` over its raw bytes (an invalid
21391 // or incomplete sequence counts as one character, c:5346-5360; with
21392 // MULTIBYTE off every byte is one, c:5363), truncates at `prec`
21393 // characters (c:5343-5346) and pads by that character count
21394 // (c:5368-5372). Counting the port's metafied `char`s made an
21395 // undecodable byte two columns wide and cut a `%.1s` between the
21396 // `Meta` byte and its payload.
21397 let _ = *crate::ported::utils::MB_LOCALE_READY;
21398 let b = crate::ported::utils::unmetafy_str(s);
21399 let multibyte = crate::ported::options::opt_state_get("multibyte").unwrap_or(true);
21400 let mut mbs: crate::ported::utils::MbStateBuf = crate::ported::utils::MBSTATE_ZERO; // c:5338
21401 let mut lbytes = b.len();
21402 let mut lchars = 0usize;
21403 let mut ptr = 0usize;
21404 while ptr < b.len() {
21405 // c:5341
21406 if Some(lchars) == prec {
21407 lbytes = ptr; // c:5343-5346
21408 break;
21409 }
21410 let chars = if multibyte {
21411 // c:5349 `chars = mbrlen(ptr, lleft, &mbs)`; mbrlen is
21412 // mbrtowc with a NULL destination.
21413 let r = unsafe {
21414 crate::ported::utils::mbrtowc(
21415 std::ptr::null_mut(),
21416 b[ptr..].as_ptr() as *const libc::c_char,
21417 b.len() - ptr,
21418 &mut mbs as *mut _ as *mut libc::c_void,
21419 )
21420 };
21421 if r == 0
21422 || r == crate::ported::utils::MB_INVALID
21423 || r == crate::ported::utils::MB_INCOMPLETE
21424 {
21425 1 // c:5360-5361 `if (chars <= 0) chars = 1;`
21426 } else {
21427 r
21428 }
21429 } else {
21430 1 // c:5364
21431 };
21432 ptr += chars; // c:5366
21433 lchars += 1;
21434 }
21435 // The printed prefix, back in the port's String form: the
21436 // `mb_metacharlenconv` units of the kept bytes (c:5370 `fwrite(b, 1,
21437 // lbytes, fout)`).
21438 let mut kept = String::with_capacity(lbytes);
21439 let mut i = 0usize;
21440 while i < lbytes {
21441 let (n, _, unit) = crate::ported::utils::mb_metacharlenconv(&b[i..lbytes]);
21442 kept.push_str(&unit);
21443 i += n.max(1);
21444 }
21445 let pad = " ".repeat(width.saturating_sub(lchars)); // c:5368-5372
21446 return if left_align {
21447 format!("{}{}", kept, pad)
21448 } else {
21449 format!("{}{}", pad, kept)
21450 };
21451 }
21452 let truncated: &str = if let Some(p) = prec {
21453 let end: usize = s.chars().take(p).map(|c| c.len_utf8()).sum();
21454 &s[..end.min(s.len())]
21455 } else {
21456 s
21457 };
21458 let pad = width.saturating_sub(truncated.chars().count());
21459 if left_align {
21460 format!("{}{}", truncated, " ".repeat(pad))
21461 } else {
21462 let padch = if honor_zero && zero_pad_flag {
21463 '0'
21464 } else {
21465 ' '
21466 };
21467 format!("{}{}", padch.to_string().repeat(pad), truncated)
21468 }
21469}
21470
21471/// c:Src/builtin.c — printf integer-argument parser. zsh accepts:
21472/// - Decimal (`42`, `-7`) — leading 0 stays decimal, NOT octal.
21473/// - Hex (`0x10`, `0X10`)
21474/// - Single character prefix `'A'` or `"A"` → ASCII code
21475/// - `BASE#NNN` radix literal
21476///
21477/// c:Src/builtin.c:5460-5464 — printf evaluates the `%d`/`%i` operand with
21478/// `mathevali`, which zerr's a "bad math expression" diagnostic AND sets
21479/// errflag on a parse error (`printf %d 12abc` → "operator expected at
21480/// `abc'"). C then clears ERRFLAG_ERROR and sets the builtin's `ret = 1`,
21481/// keeping the value at 0 but continuing to format the remaining args.
21482/// zshrs's `mathevali` returns the message in `Err` instead of emitting, so
21483/// surface it here (matching the `$((…))` path at subst.rs:2562), clear the
21484/// error so following args still evaluate, and record the soft failure in
21485/// `PRINTF_MATH_ERR` for bin_print to turn into exit status 1.
21486thread_local! {
21487 static PRINTF_MATH_ERR: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
21488}
21489
21490/// !!! WARNING: RUST-ONLY HELPER !!!
21491///
21492/// No upstream C counterpart: this name does not exist anywhere in
21493/// `Src/`. C has no printf-spec helper functions at all: `bin_print`
21494/// (`Src/builtin.c:4587`) walks the format string inline and hands the
21495/// reconstructed spec to libc `fprintf` through the `print_val` macro
21496/// (`Src/builtin.c:4571`). Rust has no runtime format-spec engine, so
21497/// that walk is split into these hand-written pieces.
21498/// Deliberately left uncited so the port audit keeps reporting it.
21499fn parse_int_arg(s: &str) -> i64 {
21500 // c:Src/builtin.c bin_print c:5447 — character-constant check
21501 // operates on the RAW arg, not after trim. `*curarg == '\''`
21502 // → take `curarg[1]` as a byte regardless of whitespace.
21503 // Otherwise `printf "%d" "' "` returns 0 instead of 32 because
21504 // the trailing space gets stripped before the leading-quote test.
21505 if let Some(rest) = s.strip_prefix('\'').or_else(|| s.strip_prefix('"')) {
21506 // c:5433-5442 — `mb_metacharlenconv(metafy(curarg+1, …), &cc)`; on
21507 // WEOF, `cc = (unsigned char) curarg[1]`. The arg is metafied, so the
21508 // first char of `rest` is the Meta byte for any high byte: `printf %x
21509 // "'"$'\xf0'` printed 83 where zsh prints f0.
21510 let raw = crate::ported::utils::unmetafy_str(rest);
21511 if raw.is_empty() {
21512 return 0; // c:5442 `(curlen > 1) ? … : 0`
21513 }
21514 return match crate::ported::utils::mb_metacharlenconv(&raw).1 {
21515 Some(c) => c as i64,
21516 None => raw[0] as i64, // c:5441-5442
21517 };
21518 }
21519 // !!! POSIX-FAITHFUL GATE (no C counterpart) !!!
21520 // zsh's printf `%d` math-EVALUATES its operand, so `printf %d A` treats
21521 // `A` as a math variable (→ 0, exit 0) and `printf %d 1+1` → 2. dash /
21522 // POSIX sh instead do `strtoimax` NUMERIC parsing: they use the leading
21523 // parsed value and exit non-zero on any non-numeric junk (`A` → 0 exit 1,
21524 // `1+1` → 1 exit 1, `12x` → 12 exit 1, ` 7 ` → 7 exit 1), while an
21525 // empty operand is a clean 0. Under `zshrs --sh`/`--dash`
21526 // (posix_faithful + EMULATE_SH) parse numerically to match the real
21527 // shell exactly. `--... --zsh` and runtime `emulate sh` leave
21528 // posix_faithful() false and keep zsh's math-eval semantics.
21529 if crate::dash_mode::posix_faithful()
21530 && crate::ported::zsh_h::EMULATION(crate::ported::zsh_h::EMULATE_SH)
21531 {
21532 if s.is_empty() {
21533 return 0; // dash: empty operand → 0, exit 0 (no error)
21534 }
21535 // strtoimax: skip leading whitespace, optional sign, base prefix.
21536 let t = s.trim_start_matches([' ', '\t', '\n']);
21537 let (neg, body) = match t.strip_prefix('-') {
21538 Some(r) => (true, r),
21539 None => (false, t.strip_prefix('+').unwrap_or(t)),
21540 };
21541 let (radix, digits) =
21542 if let Some(h) = body.strip_prefix("0x").or_else(|| body.strip_prefix("0X")) {
21543 (16u32, h)
21544 } else if body.starts_with('0') {
21545 (8u32, body) // leading 0 → octal ("0" itself is a clean 0)
21546 } else {
21547 (10u32, body)
21548 };
21549 let ndig = digits.chars().take_while(|c| c.is_digit(radix)).count();
21550 // Digit chars are ASCII, so char count == byte length of the run.
21551 let mut val = i64::from_str_radix(&digits[..ndig], radix).unwrap_or(0);
21552 if neg {
21553 val = -val;
21554 }
21555 // POSIX: any leftover after the digit run — or no digits at all —
21556 // is a diagnostic (exit 1); the leading value is still used.
21557 if ndig == 0 || ndig != digits.chars().count() {
21558 PRINTF_MATH_ERR.with(|c| c.set(true));
21559 }
21560 return val;
21561 }
21562 // c:Src/builtin.c:5460 — `zlongval = mathevali(metafy(curarg, …))`.
21563 // printf evaluates the %d/%i operand as a math expression. matheval
21564 // natively handles decimal / 0x-hex / BASE#NNN / float-truncate and
21565 // overflow-truncation: the math lexer's zstrtol (Src/utils.c:2511)
21566 // emits the "number truncated after N digits" warning, so the
21567 // magnitude 9223372036854775808 truncates to 18 digits exactly as
21568 // zsh, and INT64_MIN (-9223372036854775808) round-trips without the
21569 // manual `-parsed` negate that panicked on i64::MIN.
21570 // c:5460-5464 — evaluate as math; on error emit the diagnostic (C's
21571 // mathevali does this internally), zero the value, and flag ret=1.
21572 match crate::ported::math::mathevali(s.trim()) {
21573 Ok(n) => n,
21574 Err(msg) => {
21575 crate::ported::utils::zerr(&msg);
21576 // c:5463 `errflag &= ~ERRFLAG_ERROR` — clear so the NEXT %d arg
21577 // still evaluates (matheval bails on a pre-set errflag,
21578 // math.rs:569); the soft failure is tracked separately.
21579 crate::ported::utils::errflag.fetch_and(
21580 !crate::ported::utils::ERRFLAG_ERROR,
21581 std::sync::atomic::Ordering::Relaxed,
21582 );
21583 PRINTF_MATH_ERR.with(|c| c.set(true)); // c:5464 ret = 1
21584 0
21585 }
21586 }
21587}
21588
21589/// !!! WARNING: RUST-ONLY HELPER !!!
21590///
21591/// No upstream C counterpart: this name does not exist anywhere in
21592/// `Src/`. C has no printf-spec helper functions at all: `bin_print`
21593/// (`Src/builtin.c:4587`) walks the format string inline and hands the
21594/// reconstructed spec to libc `fprintf` through the `print_val` macro
21595/// (`Src/builtin.c:4571`). Rust has no runtime format-spec engine, so
21596/// that walk is split into these hand-written pieces.
21597/// Deliberately left uncited so the port audit keeps reporting it.
21598fn format_spec_int(spec: &str, n: i64) -> String {
21599 let (left_align, zero_pad_flag, width, prec) = parse_flags_width_prec(spec);
21600 // c:libc printf %d — when precision is given, `0` flag is
21601 // ignored (precision controls the digit padding instead).
21602 let zero_pad = zero_pad_flag && !left_align && prec.is_none();
21603 // c:Src/builtin.c — `+` flag: prefix positive numbers with `+`.
21604 // ` ` flag: prefix positive numbers with a space (mutually
21605 // exclusive with `+` per POSIX; `+` wins when both set).
21606 let plus_flag = spec.contains('+');
21607 let space_flag = spec.contains(' ') && !plus_flag;
21608 let digits = n.unsigned_abs().to_string();
21609 // c:libc printf %d precision = minimum number of digits;
21610 // zero-pad the body up to that count BEFORE the sign + width. Converting
21611 // a zero value with precision 0 yields NO digits (`printf '%.0d' 0` → "").
21612 let digits = if let Some(p) = prec {
21613 if p == 0 && n == 0 {
21614 String::new()
21615 } else if digits.len() < p {
21616 format!("{}{}", "0".repeat(p - digits.len()), digits)
21617 } else {
21618 digits
21619 }
21620 } else {
21621 digits
21622 };
21623 // c:libc printf `'` (thousands-grouping) flag — C hands `'` to the
21624 // system printf; the Rust port groups the digit run itself per the
21625 // active locale's `thousands_sep` (localeconv, set from the env at
21626 // init.rs:1709). Empty separator (C/POSIX locale) → no grouping,
21627 // matching `printf "%'d" 1234567` = `1234567` under LC_ALL=C.
21628 // Grouping is suppressed when an explicit precision is given: glibc
21629 // does NOT group the precision zero-fill (`printf "%'.8d" 42` =
21630 // `00000042`, not `00,000,042`), so gate on `prec.is_none()`. This
21631 // matches glibc for every precision case where the value fits within
21632 // the precision digit count (the normal use of `%'.Nd`); the rare
21633 // degenerate combo of `'` + a precision SHORTER than a large value's
21634 // own digit count (`%'.8d` of 1234567 → glibc `1,234,567`) still
21635 // groups in glibc but is left ungrouped here — a documented edge.
21636 // Inlined rather than shared with format_spec_uint because the
21637 // port-purity gate forbids new fns under src/ported/.
21638 let digits = if spec.contains('\'') && prec.is_none() {
21639 let sep = unsafe {
21640 let lc = libc::localeconv();
21641 if lc.is_null() || (*lc).thousands_sep.is_null() {
21642 String::new()
21643 } else {
21644 std::ffi::CStr::from_ptr((*lc).thousands_sep)
21645 .to_string_lossy()
21646 .into_owned()
21647 }
21648 };
21649 if sep.is_empty() {
21650 digits
21651 } else {
21652 let chars: Vec<char> = digits.chars().collect();
21653 let len = chars.len();
21654 let mut grouped = String::with_capacity(len + len / 3 * sep.len());
21655 for (i, ch) in chars.iter().enumerate() {
21656 if i > 0 && (len - i) % 3 == 0 {
21657 grouped.push_str(&sep);
21658 }
21659 grouped.push(*ch);
21660 }
21661 grouped
21662 }
21663 } else {
21664 digits
21665 };
21666 let body = if n < 0 {
21667 format!("-{}", digits)
21668 } else if plus_flag {
21669 format!("+{}", digits)
21670 } else if space_flag {
21671 format!(" {}", digits)
21672 } else {
21673 digits
21674 };
21675 let pad = width.saturating_sub(body.chars().count());
21676 if pad == 0 {
21677 body
21678 } else if left_align {
21679 format!("{}{}", body, " ".repeat(pad))
21680 } else if zero_pad {
21681 // Zero-pad: sign/prefix char (`-`, `+`, ` `) stays at the
21682 // left, zeros pad between it and the digits.
21683 if let Some(rest) = body
21684 .strip_prefix('-')
21685 .or_else(|| body.strip_prefix('+'))
21686 .or_else(|| body.strip_prefix(' '))
21687 {
21688 let sign = body.chars().next().unwrap();
21689 format!("{}{}{}", sign, "0".repeat(pad), rest)
21690 } else {
21691 format!("{}{}", "0".repeat(pad), body)
21692 }
21693 } else {
21694 format!("{}{}", " ".repeat(pad), body)
21695 }
21696}
21697
21698/// printf %x / %X / %o with full flag support: `#` prefix, zero pad,
21699/// width, left-align. Matches libc printf semantics.
21700/// !!! WARNING: RUST-ONLY HELPER !!!
21701///
21702/// No upstream C counterpart: this name does not exist anywhere in
21703/// `Src/`. C has no printf-spec helper functions at all: `bin_print`
21704/// (`Src/builtin.c:4587`) walks the format string inline and hands the
21705/// reconstructed spec to libc `fprintf` through the `print_val` macro
21706/// (`Src/builtin.c:4571`). Rust has no runtime format-spec engine, so
21707/// that walk is split into these hand-written pieces.
21708/// Deliberately left uncited so the port audit keeps reporting it.
21709fn format_spec_radix(spec: &str, n: u64, conv: char) -> String {
21710 let (left_align, zero_pad_flag, width, prec) = parse_flags_width_prec(spec);
21711 // c:libc printf — when a precision is given the `0` flag is ignored;
21712 // precision controls the digit zero-fill instead.
21713 let zero_pad = zero_pad_flag && !left_align && prec.is_none();
21714 let hash_flag = spec.contains('#');
21715 let body = match conv {
21716 'x' => format!("{:x}", n),
21717 'X' => format!("{:X}", n),
21718 'o' => format!("{:o}", n),
21719 _ => n.to_string(),
21720 };
21721 // c:libc printf %o/%u/%x/%X — precision is the MINIMUM number of digits;
21722 // zero-pad the body up to it BEFORE the `#` prefix and width. Converting
21723 // a zero value with precision 0 yields NO digits.
21724 let body = if let Some(p) = prec {
21725 let len = body.chars().count();
21726 if p == 0 && n == 0 {
21727 String::new()
21728 } else if len < p {
21729 format!("{}{}", "0".repeat(p - len), body)
21730 } else {
21731 body
21732 }
21733 } else {
21734 body
21735 };
21736 // c:Src/builtin.c — `#` flag: prefix with `0x`/`0X` for hex (only
21737 // when value non-zero), `0` for octal (always, even zero, which
21738 // libc handles by emitting "0" anyway).
21739 let body = if hash_flag {
21740 match conv {
21741 'x' if n != 0 => format!("0x{}", body),
21742 'X' if n != 0 => format!("0X{}", body),
21743 'o' if !body.starts_with('0') => format!("0{}", body),
21744 _ => body,
21745 }
21746 } else {
21747 body
21748 };
21749 let pad = width.saturating_sub(body.chars().count());
21750 if pad == 0 {
21751 body
21752 } else if left_align {
21753 format!("{}{}", body, " ".repeat(pad))
21754 } else if zero_pad {
21755 // For `%#04x` with value 15: body = "0xf" (3 chars), width=4,
21756 // pad=1. Zero-pad after the `0x` prefix → "0x0f". Match libc.
21757 if let Some(rest) = body.strip_prefix("0x").or_else(|| body.strip_prefix("0X")) {
21758 let prefix = &body[..2];
21759 format!("{}{}{}", prefix, "0".repeat(pad), rest)
21760 } else {
21761 format!("{}{}", "0".repeat(pad), body)
21762 }
21763 } else {
21764 format!("{}{}", " ".repeat(pad), body)
21765 }
21766}
21767
21768/// !!! WARNING: RUST-ONLY HELPER !!!
21769///
21770/// No upstream C counterpart: this name does not exist anywhere in
21771/// `Src/`. C has no printf-spec helper functions at all: `bin_print`
21772/// (`Src/builtin.c:4587`) walks the format string inline and hands the
21773/// reconstructed spec to libc `fprintf` through the `print_val` macro
21774/// (`Src/builtin.c:4571`). Rust has no runtime format-spec engine, so
21775/// that walk is split into these hand-written pieces.
21776/// Deliberately left uncited so the port audit keeps reporting it.
21777fn format_spec_uint(spec: &str, n: u64) -> String {
21778 // c:Src/builtin.c — `%u` is rendered via libc `%lu`. Unsigned
21779 // conversions print NO sign: the `+` and ` ` flags are ignored
21780 // (they apply only to signed conversions), and a negative math
21781 // result has already wrapped into the u64 by the caller's cast
21782 // (`-42` → 18446744073709551574). The previous port delegated to
21783 // format_spec_int(n as i64), which reinterpreted the wrapped u64
21784 // back as a negative i64 and printed `-42`, and honored `+`/` `.
21785 let (left_align, zero_pad_flag, width, prec) = parse_flags_width_prec(spec);
21786 // libc: with an explicit precision the `0` flag is ignored.
21787 let zero_pad = zero_pad_flag && !left_align && prec.is_none();
21788 let digits = n.to_string();
21789 // Precision = minimum number of digits (zero-fill the body). A zero value
21790 // with precision 0 yields NO digits (c:libc printf).
21791 let body = match prec {
21792 Some(0) if n == 0 => String::new(),
21793 Some(p) if digits.len() < p => format!("{}{}", "0".repeat(p - digits.len()), digits),
21794 _ => digits,
21795 };
21796 // c:libc printf `'` thousands-grouping flag (see format_spec_int for
21797 // the rationale, the `prec.is_none()` gate, and the port-purity-gate
21798 // note on the inlined copy). Unsigned bodies carry no sign, so the
21799 // whole body groups.
21800 let body = if spec.contains('\'') && prec.is_none() {
21801 let sep = unsafe {
21802 let lc = libc::localeconv();
21803 if lc.is_null() || (*lc).thousands_sep.is_null() {
21804 String::new()
21805 } else {
21806 std::ffi::CStr::from_ptr((*lc).thousands_sep)
21807 .to_string_lossy()
21808 .into_owned()
21809 }
21810 };
21811 if sep.is_empty() {
21812 body
21813 } else {
21814 let chars: Vec<char> = body.chars().collect();
21815 let len = chars.len();
21816 let mut grouped = String::with_capacity(len + len / 3 * sep.len());
21817 for (i, ch) in chars.iter().enumerate() {
21818 if i > 0 && (len - i) % 3 == 0 {
21819 grouped.push_str(&sep);
21820 }
21821 grouped.push(*ch);
21822 }
21823 grouped
21824 }
21825 } else {
21826 body
21827 };
21828 let pad = width.saturating_sub(body.chars().count());
21829 if pad == 0 {
21830 body
21831 } else if left_align {
21832 format!("{}{}", body, " ".repeat(pad))
21833 } else if zero_pad {
21834 format!("{}{}", "0".repeat(pad), body)
21835 } else {
21836 format!("{}{}", " ".repeat(pad), body)
21837 }
21838}
21839
21840/// !!! WARNING: RUST-ONLY HELPER !!!
21841///
21842/// No upstream C counterpart: this name does not exist anywhere in
21843/// `Src/`. C has no printf-spec helper functions at all: `bin_print`
21844/// (`Src/builtin.c:4587`) walks the format string inline and hands the
21845/// reconstructed spec to libc `fprintf` through the `print_val` macro
21846/// (`Src/builtin.c:4571`). Rust has no runtime format-spec engine, so
21847/// that walk is split into these hand-written pieces.
21848/// Deliberately left uncited so the port audit keeps reporting it.
21849fn format_spec_float(spec: &str, n: f64) -> String {
21850 let (left_align, zero_pad, width, prec) = parse_flags_width_prec(spec);
21851 let p = prec.unwrap_or(6);
21852 let body = format!("{:.*}", p, n);
21853 let pad = width.saturating_sub(body.chars().count());
21854 if pad == 0 {
21855 body
21856 } else if left_align {
21857 format!("{}{}", body, " ".repeat(pad))
21858 } else if zero_pad {
21859 if let Some(rest) = body.strip_prefix('-') {
21860 format!("-{}{}", "0".repeat(pad), rest)
21861 } else {
21862 format!("{}{}", "0".repeat(pad), body)
21863 }
21864 } else {
21865 format!("{}{}", " ".repeat(pad), body)
21866 }
21867}
21868
21869/// printf %g / %G / %e / %E / %f / %F dispatch. Mirrors C printf
21870/// semantics (Src/builtin.c — libc snprintf): %g picks the shorter
21871/// of %e/%f and strips trailing zeros; %e/%E uses scientific notation;
21872/// %f/%F is decimal-fraction (no scientific). Default precision is 6.
21873// pub(crate) so zcalc can format through the SAME printf conversion it calls in
21874// zsh (zcalc:107 `printf "$_forms[_outform]\n" …`, _forms[1] = '%2$g'). The
21875// float-parameter formatter `convfloat` is NOT interchangeable: it re-appends a
21876// trailing `.` when %g yields no dot (c:params.c:5748-5749), which printf never
21877// does — `sqrt(2)*sqrt(2)` must print `2`, not `2.`.
21878/// !!! WARNING: RUST-ONLY HELPER !!!
21879///
21880/// No upstream C counterpart: this name does not exist anywhere in
21881/// `Src/`. C has no printf-spec helper functions at all: `bin_print`
21882/// (`Src/builtin.c:4587`) walks the format string inline and hands the
21883/// reconstructed spec to libc `fprintf` through the `print_val` macro
21884/// (`Src/builtin.c:4571`). Rust has no runtime format-spec engine, so
21885/// that walk is split into these hand-written pieces.
21886/// Deliberately left uncited so the port audit keeps reporting it.
21887pub(crate) fn format_spec_float_conv(spec: &str, n: f64, conv: char) -> String {
21888 // c:Src/builtin.c:5495-5499 — `/* force consistent form for Inf/NaN
21889 // output */ if (isnan(doubleval)) fputs("nan"); else if (isinf(doubleval))
21890 // fputs(doubleval < 0.0 ? "-inf" : "inf"); else print_val(...)`. zsh emits
21891 // the BARE lowercase string with no width/precision/flags/padding,
21892 // bypassing the normal formatting (platform-independent). The previous
21893 // Rust path ran nan/inf through `format!`, producing "NaN" and garbage
21894 // scientific ("NaNe+2147483647").
21895 if n.is_nan() {
21896 return "nan".to_string(); // c:5496-5497
21897 }
21898 if n.is_infinite() {
21899 return if n < 0.0 { "-inf" } else { "inf" }.to_string(); // c:5498-5499
21900 }
21901 let (left_align, zero_pad, width, prec) = parse_flags_width_prec(spec);
21902 let n = crate::dash_mode::ksh93_round_ties_away(n, conv, prec);
21903 let body = match conv {
21904 'f' | 'F' => {
21905 let p = prec.unwrap_or(6);
21906 format!("{:.*}", p, n)
21907 }
21908 'e' | 'E' => {
21909 // Use Rust's `{:.*e}` for the mantissa+exponent — it is EXACT
21910 // (correctly-rounded, like libc snprintf). The previous port
21911 // computed `mantissa = n / 10^exp` which lost precision: `37/10`
21912 // is not exactly 3.7, so `%.27e 37` printed the binary error
21913 // `3.70000…0177635683940e+01` instead of zsh's `3.7000…000e+01`.
21914 // Rust emits `<mant>e<exp>` with lowercase `e` and no exponent
21915 // padding/sign; reformat the exponent to libc's `e[+-]NN` (≥2 digits).
21916 let p = prec.unwrap_or(6);
21917 let raw = format!("{:.*e}", p, n);
21918 let (mant, exp_s) = raw.split_once('e').unwrap_or((raw.as_str(), "0"));
21919 let exp: i32 = exp_s.parse().unwrap_or(0);
21920 let e_char = if conv == 'E' { 'E' } else { 'e' };
21921 let exp_sign = if exp >= 0 { '+' } else { '-' };
21922 format!("{}{}{}{:02}", mant, e_char, exp_sign, exp.abs())
21923 }
21924 'g' | 'G' => {
21925 // c:libc printf %g: precision is # significant digits
21926 // (default 6). Use %e if exp < -4 OR exp >= precision,
21927 // else %f. Trailing zeros stripped unless `#` flag set
21928 // (zshrs doesn't track # — skip stripping suppression).
21929 let p_sig: i32 = prec.unwrap_or(6).max(1) as i32;
21930 let exp = if n == 0.0 {
21931 0i32
21932 } else {
21933 n.abs().log10().floor() as i32
21934 };
21935 let use_e = exp < -4 || exp >= p_sig;
21936 let body = if use_e {
21937 // Exact mantissa+exponent via Rust's `{:.*e}` (see the `%e`
21938 // arm) — `n / 10^exp` lost precision.
21939 let dec = (p_sig - 1).max(0) as usize;
21940 let raw = format!("{:.*e}", dec, n);
21941 let (m, exp_s) = raw.split_once('e').unwrap_or((raw.as_str(), "0"));
21942 let ex: i32 = exp_s.parse().unwrap_or(0);
21943 let e_char = if conv == 'G' { 'E' } else { 'e' };
21944 let exp_sign = if ex >= 0 { '+' } else { '-' };
21945 format!("{}{}{}{:02}", m, e_char, exp_sign, ex.abs())
21946 } else {
21947 // p_sig - 1 - exp digits after decimal point
21948 let dec = (p_sig - 1 - exp).max(0) as usize;
21949 format!("{:.*}", dec, n)
21950 };
21951 // Strip trailing zeros from the fractional part (but keep
21952 // at least one digit after `.` if `.` is present).
21953 // Only strip if no `#` flag was set in spec.
21954 // c:libc snprintf %g — trailing-zero strip done inline; no
21955 // separate helper in C source.
21956 if !spec.contains('#') {
21957 let stripped = if let Some(e_pos) = body.find(|c| c == 'e' || c == 'E') {
21958 let (mantissa, exp) = body.split_at(e_pos);
21959 let m = if mantissa.contains('.') {
21960 mantissa
21961 .trim_end_matches('0')
21962 .trim_end_matches('.')
21963 .to_string()
21964 } else {
21965 mantissa.to_string()
21966 };
21967 format!("{}{}", m, exp)
21968 } else if body.contains('.') {
21969 body.trim_end_matches('0').trim_end_matches('.').to_string()
21970 } else {
21971 body
21972 };
21973 stripped
21974 } else {
21975 body
21976 }
21977 }
21978 _ => format!("{}", n),
21979 };
21980 // c:libc printf `#` flag on a float conversion forces a decimal point to
21981 // appear even when precision 0 leaves no fractional digits: `%#.0f` 5 →
21982 // "5.", `%#.0e` 5 → "5.e+00", `%#.0g` 5 → "5.". (The %g/%G arm above also
21983 // uses `#` to suppress trailing-zero stripping.)
21984 let body = if spec.contains('#') && !body.contains('.') {
21985 match body.find(|c| c == 'e' || c == 'E') {
21986 Some(ep) => format!("{}.{}", &body[..ep], &body[ep..]),
21987 None => format!("{body}."),
21988 }
21989 } else {
21990 body
21991 };
21992 // c:libc printf flags `+` and ` ` — a non-negative float value gets
21993 // a forced leading `+` (with `+`) or a blank (with ` `); `+`
21994 // overrides ` `. parse_flags_width_prec only extracts `-`/`0`, so
21995 // these sign flags were dropped: `printf "%+.2f" 1.5` rendered
21996 // "1.50" instead of "+1.50". Negatives keep their own `-`. The
21997 // zero-pad arm below already expects an optional leading `+`.
21998 let body = if body.starts_with('-') {
21999 body
22000 } else if spec.contains('+') {
22001 format!("+{}", body)
22002 } else if spec.contains(' ') {
22003 format!(" {}", body)
22004 } else {
22005 body
22006 };
22007 let pad = width.saturating_sub(body.chars().count());
22008 if pad == 0 {
22009 body
22010 } else if left_align {
22011 // c:libc printf — `-` overrides `0`; pad on the right with spaces.
22012 format!("{}{}", body, " ".repeat(pad))
22013 } else if zero_pad {
22014 // c:libc printf — `0` flag without `-`: zero-pad between the
22015 // optional sign and digits. `printf "%05.2f" 1.5` → "01.50",
22016 // `printf "%05.2f" -1.5` → "-1.50" (no extra pad since sign +
22017 // body already fills width). Previously the helper discarded
22018 // zero_pad via parse_width_prec and always space-padded.
22019 if let Some(rest) = body.strip_prefix('-') {
22020 format!("-{}{}", "0".repeat(pad), rest)
22021 } else if let Some(rest) = body.strip_prefix('+') {
22022 format!("+{}{}", "0".repeat(pad), rest)
22023 } else if let Some(rest) = body.strip_prefix(' ') {
22024 // ` ` (space) flag + `0`: blank stays leftmost, zeros pad
22025 // between it and the digits. `printf "% 08.2f" 1.5` → " 0001.50".
22026 format!(" {}{}", "0".repeat(pad), rest)
22027 } else {
22028 format!("{}{}", "0".repeat(pad), body)
22029 }
22030 } else {
22031 format!("{}{}", " ".repeat(pad), body)
22032 }
22033}
22034
22035/// !!! WARNING: RUST-ONLY HELPER !!!
22036///
22037/// No upstream C counterpart: this name does not exist anywhere in
22038/// `Src/`. C has no printf-spec helper functions at all: `bin_print`
22039/// (`Src/builtin.c:4587`) walks the format string inline and hands the
22040/// reconstructed spec to libc `fprintf` through the `print_val` macro
22041/// (`Src/builtin.c:4571`). Rust has no runtime format-spec engine, so
22042/// that walk is split into these hand-written pieces.
22043/// Deliberately left uncited so the port audit keeps reporting it.
22044fn parse_width_prec(spec: &str) -> (bool, usize, Option<usize>) {
22045 let (left_align, _zero_pad, width, prec) = parse_flags_width_prec(spec);
22046 (left_align, width, prec)
22047}
22048
22049/// Parse printf-format flags `-`, `+`, ` `, `#`, `0`, width digits,
22050/// optional `.precision`. Returns (left_align, zero_pad, width, prec).
22051/// Mirrors C `parsefmt()` in Src/builtin.c — the `0` flag is only a
22052/// flag when it appears BEFORE any width digit; a `0` inside the
22053/// width number (e.g. `%10d`) is just part of the width.
22054/// !!! WARNING: RUST-ONLY HELPER !!!
22055///
22056/// No upstream C counterpart: this name does not exist anywhere in
22057/// `Src/`. C has no printf-spec helper functions at all: `bin_print`
22058/// (`Src/builtin.c:4587`) walks the format string inline and hands the
22059/// reconstructed spec to libc `fprintf` through the `print_val` macro
22060/// (`Src/builtin.c:4571`). Rust has no runtime format-spec engine, so
22061/// that walk is split into these hand-written pieces.
22062/// Deliberately left uncited so the port audit keeps reporting it.
22063fn parse_flags_width_prec(spec: &str) -> (bool, bool, usize, Option<usize>) {
22064 let s = spec.trim_start_matches('%');
22065 let mut i = 0;
22066 let bytes = s.as_bytes();
22067 let mut left_align = false;
22068 let mut zero_pad = false;
22069 // `'` (thousands-grouping) is a flag char too — skip it here so a
22070 // following width/precision (`%'12d`, `%'.8d`) is still parsed. The
22071 // grouping itself is applied by the caller (format_spec_int/uint).
22072 while i < bytes.len() && matches!(bytes[i], b'-' | b'+' | b' ' | b'#' | b'0' | b'\'') {
22073 match bytes[i] {
22074 b'-' => left_align = true,
22075 b'0' => zero_pad = true,
22076 _ => {}
22077 }
22078 i += 1;
22079 }
22080 let width_start = i;
22081 while i < bytes.len() && bytes[i].is_ascii_digit() {
22082 i += 1;
22083 }
22084 let width: usize = s[width_start..i].parse().unwrap_or(0);
22085 let mut prec: Option<usize> = None;
22086 if i < bytes.len() && bytes[i] == b'.' {
22087 i += 1;
22088 let p_start = i;
22089 while i < bytes.len() && bytes[i].is_ascii_digit() {
22090 i += 1;
22091 }
22092 prec = Some(s[p_start..i].parse().unwrap_or(0));
22093 }
22094 (left_align, zero_pad, width, prec)
22095}
22096
22097// `findcmd` (Src/exec.c:897) — moved to its canonical home at
22098// `crate::ported::exec::findcmd` per PORT.md Rule C (the C source
22099// lives in exec.c, so the Rust port belongs in exec.rs). Call sites
22100// import from the new path.
22101pub use crate::ported::exec::findcmd;
22102use crate::ported::signals_h::run_queued_signals;
22103
22104/// Port of `getsigidx()` from `Src/jobs.c:3047`.
22105/// C decl: `getsigidx(const char *s)`
22106/// Local wrapper that delegates to the canonical
22107/// `crate::ported::jobs::getsigidx` (matching `Src/jobs.c` location).
22108/// Returns -1 for unknown so existing builtin.rs call sites (which
22109/// use the i32 sentinel) don't need to change.
22110fn getsigidx(name: &str) -> i32 {
22111 crate::ported::jobs::getsigidx(name).unwrap_or(-1)
22112}
22113
22114/// Port of `int pat_enables(const char *cmd, char **patp, int enable)`
22115/// from `Src/pattern.c:4171`. Local builtin.rs shim that delegates to
22116/// the canonical pattern.rs port. Static-link path: the actual
22117/// zpc_strings/zpc_disables manipulation lives in
22118/// `pat_enables`.
22119/// Port of `pat_enables()` from `Src/pattern.c:4171`. — C decl `pat_enables(const char *cmd, char **patp, int enable)`
22120fn pat_enables(name: &str, argv: &[String], on: bool) -> i32 {
22121 // c:4171
22122 let patp: Vec<&str> = argv.iter().map(|s| s.as_str()).collect();
22123 crate::ported::pattern::pat_enables(name, &patp, on)
22124}
22125
22126// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
22127// ─── RUST-ONLY ACCESSORS ───
22128//
22129// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
22130// RwLock<T>>` globals declared above. C zsh uses direct global
22131// access; Rust needs these wrappers because `OnceLock::get_or_init`
22132// is the only way to lazily construct shared state. These ported sit
22133// here so the body of this file reads in C source order without
22134// the accessor wrappers interleaved between real port ported.
22135// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
22136
22137// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
22138// ─── RUST-ONLY ACCESSORS ───
22139//
22140// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
22141// RwLock<T>>` globals declared above. C zsh uses direct global
22142// access; Rust needs these wrappers because `OnceLock::get_or_init`
22143// is the only way to lazily construct shared state. These ported sit
22144// here so the body of this file reads in C source order without
22145// the accessor wrappers interleaved between real port ported.
22146// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
22147/// `traps_table` — see implementation.
22148/// !!! WARNING: RUST-ONLY HELPER !!!
22149///
22150/// No upstream C counterpart. C keeps trap state in the
22151/// `int *sigtrapped` (`Src/signals.c:39`) and `Eprog *siglists`
22152/// (`Src/signals.c:53`) arrays and touches them directly; this is the
22153/// `OnceLock` accessor for the builtin.rs-side `TRAPS_INNER` mirror
22154/// that `bin_trap` reads/writes.
22155/// Deliberately left uncited so the port audit keeps reporting it.
22156pub fn traps_table() -> &'static Mutex<HashMap<String, String>> {
22157 TRAPS_INNER.get_or_init(|| Mutex::new(HashMap::new()))
22158}
22159
22160#[cfg(test)]
22161mod tests {
22162 use super::*;
22163
22164 /// `findcmd` with an existing ABSOLUTE path bypasses the PATH
22165 /// walk entirely (c:916 `if (arg0 == s || ...)` branch — `s` is
22166 /// the result of `strchr(arg0, '/')`; when the first char IS
22167 /// the slash, `arg0 == s` is true). The caller's `$PATH` is
22168 /// irrelevant for an absolute path. A regression that always
22169 /// walked $PATH would fail to find `/bin/sh` when $PATH was
22170 /// empty, breaking command-name resolution for cron/init contexts
22171 /// that explicitly pass absolute paths.
22172 #[test]
22173 fn findcmd_absolute_path_skips_path_walk() {
22174 let _g = crate::test_util::global_state_lock();
22175 // `PATH` is an exported special: writing it also rewrites the
22176 // PROCESS environment, and `unsetparam` does not put the old
22177 // value back. Leaving it empty broke every later test that
22178 // spawns a helper by name (`Command::new("zsh")` → ENOENT), so
22179 // snapshot and restore it here.
22180 let saved_path = getsparam("PATH");
22181 // Empty $PATH to guarantee the walk would miss.
22182 setsparam("PATH", "");
22183 let resolved = findcmd("/bin/sh", 0, 0);
22184 match saved_path {
22185 Some(p) => {
22186 setsparam("PATH", &p);
22187 }
22188 None => {
22189 unsetparam("PATH");
22190 }
22191 }
22192 assert_eq!(
22193 resolved.as_deref(),
22194 Some("/bin/sh"),
22195 "c:914-919 — absolute path that exists must resolve to itself \
22196 regardless of $PATH"
22197 );
22198 }
22199
22200 /// `findcmd` with `default_path != 0` MUST search the hardcoded
22201 /// `DEFAULT_PATH` (`/usr/bin:/bin:/usr/sbin:/sbin`), NOT the
22202 /// caller's `$PATH`. C body c:903-908. This is the `command -p`
22203 /// security contract: scripts that need to invoke a sanitized
22204 /// `awk`/`sed`/`grep` regardless of user-poisoned $PATH (e.g.
22205 /// `command -p sh -c '...'` in a setuid wrapper) rely on the
22206 /// fallback path. A regression that ignored `default_path` would
22207 /// re-introduce the very PATH-injection vulnerability that
22208 /// `command -p` exists to prevent.
22209 ///
22210 /// Pin: with $PATH set to a non-existent directory, `findcmd`
22211 /// for a binary that ONLY lives in /bin or /usr/bin (e.g. `sh`)
22212 /// must still resolve when `default_path=1`.
22213 #[test]
22214 fn findcmd_default_path_searches_hardcoded_dirs() {
22215 let _g = crate::test_util::global_state_lock();
22216 // Snapshot/restore — see findcmd_absolute_path_skips_path_walk.
22217 let saved_path = getsparam("PATH");
22218 // Poison $PATH so the normal path-walk would miss.
22219 setsparam("PATH", "/nonexistent/zshrs-test-poison");
22220 // `sh` exists in /bin on every POSIX system.
22221 let resolved = findcmd("sh", 0, 1);
22222 match saved_path {
22223 Some(p) => {
22224 setsparam("PATH", &p);
22225 }
22226 None => {
22227 unsetparam("PATH");
22228 }
22229 }
22230 assert!(
22231 resolved.is_some(),
22232 "c:903-908 — default_path must search DEFAULT_PATH regardless of $PATH"
22233 );
22234 let p = resolved.unwrap();
22235 assert!(
22236 DEFAULT_PATH.split(':').any(|d| p.starts_with(d)),
22237 "resolved path must be under one of DEFAULT_PATH's dirs; got {:?}",
22238 p
22239 );
22240 }
22241
22242 /// c:7399 — `trap - <undefined>` MUST report failure (non-zero
22243 /// exit) so scripts can detect the bad signal name. The previous
22244 /// Rust port returned 0 unconditionally from the clear path,
22245 /// silently masking errors. C returns `*argv != NULL` — non-zero
22246 /// when the loop broke on an undefined signal.
22247 #[test]
22248 fn bin_trap_clear_undefined_signal_returns_nonzero() {
22249 let _g = crate::test_util::global_state_lock();
22250 let empty = options {
22251 ind: [0u8; MAX_OPS],
22252 args: Vec::new(),
22253 argscount: 0,
22254 argsalloc: 0,
22255 };
22256 // `trap - BOGUS_NEVER_A_SIGNAL` → must return 1.
22257 let r = bin_trap(
22258 "trap",
22259 &["-".into(), "BOGUS_NEVER_A_SIGNAL".into()],
22260 &empty,
22261 0,
22262 );
22263 assert_ne!(
22264 r, 0,
22265 "trap - <undefined> must report error per c:7399 (got {})",
22266 r
22267 );
22268 }
22269
22270 /// c:7405-7409 — the trap body is parsed when the trap is INSTALLED
22271 /// (`parse_string(arg, 1)`), so an unparseable body fails right here
22272 /// with `couldn't parse trap command` and rc=1, and nothing is
22273 /// installed. zshrs previously stored the body text unexamined and
22274 /// returned 0, deferring the failure to signal delivery: `trap 'for'
22275 /// EXIT` reported success and then emitted a parse error at exit.
22276 ///
22277 /// The bodies here are the ones the oracle rejects. `while` is
22278 /// deliberately not among them — `zsh -fc 'while'` treats it as an
22279 /// incomplete construct and reads the remainder from stdin rather
22280 /// than erroring, so it is not an install-time parse failure.
22281 #[test]
22282 fn bin_trap_unparseable_body_rejected_at_install() {
22283 let _g = crate::test_util::global_state_lock();
22284 let empty = options {
22285 ind: [0u8; MAX_OPS],
22286 args: Vec::new(),
22287 argscount: 0,
22288 argsalloc: 0,
22289 };
22290 for body in [
22291 "for",
22292 "((",
22293 "fi",
22294 "done",
22295 "case",
22296 "if true",
22297 "print ok; for",
22298 ] {
22299 let r = bin_trap("trap", &[body.into(), "USR1".into()], &empty, 0);
22300 assert_eq!(
22301 r, 1,
22302 "trap '{body}' USR1 must fail at install per c:7407 (got {r})"
22303 );
22304 // The rejected body must not have been installed (c:7408
22305 // returns before the install loop at c:7421).
22306 let installed = traps_table()
22307 .lock()
22308 .ok()
22309 .map(|t| t.contains_key("USR1"))
22310 .unwrap_or(false);
22311 assert!(
22312 !installed,
22313 "trap '{body}' USR1 was rejected but still installed a handler"
22314 );
22315 }
22316 // A parseable body still installs — the gate must not reject
22317 // everything.
22318 let r = bin_trap("trap", &["print ok".into(), "USR1".into()], &empty, 0);
22319 assert_eq!(r, 0, "parseable body must install (got {r})");
22320 if let Ok(mut t) = traps_table().lock() {
22321 assert!(t.contains_key("USR1"), "parseable body must be installed");
22322 t.remove("USR1");
22323 }
22324 }
22325
22326 /// c:7371 — `getpermtext(siglists[sig], NULL, 0)`. C keeps the trap
22327 /// body as a compiled Eprog and renders it back to source for the
22328 /// listing, so `trap` prints CANONICAL text rather than the string
22329 /// that was typed: separators become newlines. zshrs stores the body
22330 /// as raw text and previously echoed it verbatim, which is correct
22331 /// only for a single command — `trap 'print a; print b' EXIT` listed
22332 /// as `'print a; print b'` where zsh lists `$'print a\nprint b'`.
22333 ///
22334 /// Pins the deparse itself (parse_string → getpermtext) rather than
22335 /// bin_trap's stdout, so the assertion doesn't depend on capturing
22336 /// the builtin's print! output.
22337 #[test]
22338 fn trap_body_listing_deparses_to_canonical_text() {
22339 let _g = crate::test_util::global_state_lock();
22340 let prog = crate::ported::exec::parse_string("print a; print b", 1)
22341 .expect("body must parse for the listing path");
22342 let rendered = crate::ported::text::getpermtext(Box::new(prog), None, 0);
22343 assert_eq!(
22344 rendered, "print a\nprint b",
22345 "c:7371 getpermtext must canonicalise `;` separators to newlines (got {rendered:?})"
22346 );
22347 }
22348
22349 /// Src/options.c:537-549 — `emulate(zsh_name, ...)` dispatches
22350 /// on the FIRST char of the shell name, stripping a leading `r`
22351 /// (so `rcsh`/`rksh` work as restricted variants of their base
22352 /// shell). `bash` aliases to SH (the `'b'` branch of the case).
22353 /// Pin the bits assigned by `bin_emulate` for the canonical
22354 /// names + their first-char-overlap aliases.
22355 #[test]
22356 fn bin_emulate_dispatches_on_first_char_per_c537() {
22357 let _g = crate::test_util::global_state_lock();
22358 let empty = options {
22359 ind: [0u8; MAX_OPS],
22360 args: Vec::new(),
22361 argscount: 0,
22362 argsalloc: 0,
22363 };
22364 let saved = emulation.load(Relaxed);
22365 // `emulate` rewrites the WHOLE option table (installemulation,
22366 // c:551-572), not just the emulation bits — `emulate csh` turns
22367 // CPRECEDENCES on, `emulate sh` turns SHWORDSPLIT on, and so on.
22368 // Restoring only `emulation` left those options set for every
22369 // later test in the process (a leaked CPRECEDENCES made
22370 // `4 - - 3 * 7 << 1 & 7 ^ 1 | 16 ** 2` evaluate to C's 259
22371 // instead of zsh's 1591). Snapshot and restore the table too.
22372 let saved_opts = crate::ported::options::opt_state_snapshot();
22373
22374 // Each (name, expected_bits) — name covers the canonical
22375 // shell names AND their `r`-prefix / first-char variants.
22376 for (name, expected) in [
22377 ("csh", EMULATE_CSH),
22378 ("ksh", EMULATE_KSH),
22379 ("sh", EMULATE_SH),
22380 ("rcsh", EMULATE_CSH), // c:539-540
22381 ("rksh", EMULATE_KSH), // c:539-540
22382 ("bash", EMULATE_SH), // c:548 'b'
22383 ] {
22384 emulation.store(0, Relaxed);
22385 bin_emulate("emulate", &[name.into()], &empty, 0);
22386 let bits = emulation.load(Relaxed);
22387 assert_eq!(
22388 bits, expected,
22389 "emulate {} must set bits {:#x}, got {:#x}",
22390 name, expected, bits
22391 );
22392 }
22393 emulation.store(saved, Relaxed);
22394 crate::ported::options::opt_state_restore(saved_opts);
22395 }
22396
22397 /// c:7399 — `trap - SIGUSR1` (valid signal) MUST return 0, even
22398 /// when the trap was never set (remove is a no-op).
22399 #[test]
22400 fn bin_trap_clear_valid_signal_returns_zero() {
22401 let _g = crate::test_util::global_state_lock();
22402 let empty = options {
22403 ind: [0u8; MAX_OPS],
22404 args: Vec::new(),
22405 argscount: 0,
22406 argsalloc: 0,
22407 };
22408 let r = bin_trap("trap", &["-".into(), "USR1".into()], &empty, 0);
22409 assert_eq!(
22410 r, 0,
22411 "trap - USR1 must succeed even with no prior trap (got {})",
22412 r
22413 );
22414 }
22415
22416 #[test]
22417 fn registration_table_matches_c_count() {
22418 let _g = crate::test_util::global_state_lock();
22419 // Src/builtin.c:40-137 has 79 rows total (5 BIN_PREFIX + 71
22420 // BUILTIN + 3 debug-only BUILTIN). The Rust port bundles
22421 // additional builtins eagerly that C would load via zmodload:
22422 // zsh/rlimits (limit/ulimit/unlimit)
22423 // zsh/zle (bindkey/vared/zle)
22424 // zsh/cap (cap/getcap/setcap)
22425 // zsh/files (chmod/chown/ln/mkdir/rm/rmdir/sync)
22426 // zsh/complete (compadd/compset)
22427 // zsh/terminfo (echoti)
22428 // zsh/pcre (pcre_compile/pcre_match/pcre_study)
22429 // zsh/zutil (zformat/zgdbmpath)
22430 // zsh/sched (sched)
22431 // zsh/computil (comparguments/compdescribe/compfiles/
22432 // compgroups/compquote/comptags/comptry/compvalues)
22433 // zsh/system (syserror/sysread/syswrite/sysopen/sysseek/zsystem)
22434 // zsh/zselect (zselect)
22435 // zsh/socket (zsocket)
22436 // zsh/stat (stat)
22437 // zsh/watch (log)
22438 // zsh/zprof (zprof)
22439 // zsh/datetime (strftime)
22440 // zsh/zftp (zftp), zsh/zpty (zpty), zsh/curses (zcurses)
22441 // zsh/clone (clone), zsh/example (example)
22442 // zsh/param/private (private)
22443 // zsh/termcap (echotc)
22444 // zsh/compctl (compcall, compctl)
22445 // zsh/attr (zgetattr, zsetattr, zdelattr, zlistattr)
22446 // Tripwire pin on BUILTINS table length. The number drifts every
22447 // time the eagerly-loaded-module list above grows (new builtin
22448 // ported, new module wired). Bump it alongside the change so
22449 // accidental additions/removals still trip a review.
22450 // 159 ported zsh builtins + 1 zshrs-native (`__rust_compile`, the
22451 // inline Rust FFI desugar target — no C counterpart) = 160.
22452 assert_eq!(BUILTINS.len(), 160,
22453 "BUILTINS table size changed — bump count or update the eagerly-loaded-module list above");
22454 }
22455
22456 /// `Src/builtin.c:40-137` — every name in the canonical C builtin
22457 /// table must be present in the Rust port. Pins coverage of all
22458 /// 79 C builtins by name (ignores option-mask / handler details).
22459 /// Detects regressions where a builtin gets accidentally dropped
22460 /// from BUILTINS. Names extracted from upstream zsh `Src/builtin.c`.
22461 #[test]
22462 fn registration_table_contains_all_c_builtins() {
22463 let _g = crate::test_util::global_state_lock();
22464 // Canonical 79 names from Src/builtin.c:40-137 (verbatim).
22465 let c_names: &[&str] = &[
22466 "-",
22467 ".",
22468 ":",
22469 "[",
22470 "alias",
22471 "autoload",
22472 "bg",
22473 "break",
22474 "builtin",
22475 "bye",
22476 "cd",
22477 "chdir",
22478 "command",
22479 "continue",
22480 "declare",
22481 "dirs",
22482 "disable",
22483 "disown",
22484 "echo",
22485 "emulate",
22486 "enable",
22487 "eval",
22488 "exec",
22489 "exit",
22490 "export",
22491 "false",
22492 "fc",
22493 "fg",
22494 "float",
22495 "functions",
22496 "getln",
22497 "getopts",
22498 "hash",
22499 "hashinfo",
22500 "history",
22501 "integer",
22502 "jobs",
22503 "kill",
22504 "let",
22505 "local",
22506 "logout",
22507 "mem",
22508 "noglob",
22509 "patdebug",
22510 "popd",
22511 "print",
22512 "printf",
22513 "pushd",
22514 "pushln",
22515 "pwd",
22516 "r",
22517 "read",
22518 "readonly",
22519 "rehash",
22520 "return",
22521 "set",
22522 "setopt",
22523 "shift",
22524 "source",
22525 "suspend",
22526 "test",
22527 "times",
22528 "trap",
22529 "true",
22530 "ttyctl",
22531 "type",
22532 "typeset",
22533 "umask",
22534 "unalias",
22535 "unfunction",
22536 "unhash",
22537 "unset",
22538 "unsetopt",
22539 "wait",
22540 "whence",
22541 "where",
22542 "which",
22543 "zcompile",
22544 "zmodload",
22545 ];
22546 assert_eq!(
22547 c_names.len(),
22548 79,
22549 "C builtin.c row count is 79 — recount if changed"
22550 );
22551 let table_names: std::collections::HashSet<&str> =
22552 BUILTINS.iter().map(|b| b.node.nam.as_str()).collect();
22553 for c_name in c_names {
22554 assert!(
22555 table_names.contains(*c_name),
22556 "missing C builtin '{}' from BUILTINS table",
22557 c_name
22558 );
22559 }
22560 }
22561
22562 #[test]
22563 fn lookup_finds_known_builtins() {
22564 let _g = crate::test_util::global_state_lock();
22565 for name in [
22566 "cd", "echo", "print", "fg", "bg", "jobs", "wait", "typeset", "test", "[", ".",
22567 ] {
22568 assert!(
22569 createbuiltintable().get(name).copied().is_some(),
22570 "missing: {name}"
22571 );
22572 }
22573 }
22574
22575 #[test]
22576 fn lookup_misses_unknown() {
22577 let _g = crate::test_util::global_state_lock();
22578 assert!(createbuiltintable()
22579 .get("not-a-builtin-zZz")
22580 .copied()
22581 .is_none());
22582 }
22583
22584 #[test]
22585 fn prefix_entries_have_prefix_flag() {
22586 let _g = crate::test_util::global_state_lock();
22587 for name in ["-", "builtin", "command", "exec", "noglob"] {
22588 let b = createbuiltintable().get(name).copied().unwrap();
22589 assert!(
22590 b.node.flags as u32 & BINF_PREFIX != 0,
22591 "{name} missing BINF_PREFIX"
22592 );
22593 }
22594 }
22595
22596 /// Run `fixdir` (c:Src/builtin.c:1298) on `p`; the rewritten path and
22597 /// the int it returns.
22598 fn fixdir_of(p: &str) -> (String, i32) {
22599 let mut s = p.to_string();
22600 let r = fixdir(&mut s);
22601 (s, r)
22602 }
22603
22604 #[test]
22605 fn fixdir_canonicalizes_absolute_paths() {
22606 let _g = crate::test_util::global_state_lock();
22607 // c:1334-1392 — collapse `//`, drop `./`, pop `foo/..` for a
22608 // directory `foo`.
22609 let d = std::env::temp_dir().join(format!("fixdir-{}", std::process::id()));
22610 std::fs::create_dir_all(d.join("bar/baz")).unwrap();
22611 let d = d.to_string_lossy().into_owned();
22612 assert_eq!(fixdir_of(&format!("{d}/./bar")), (format!("{d}/bar"), 0));
22613 assert_eq!(fixdir_of(&format!("{d}//bar//")), (format!("{d}/bar"), 0));
22614 assert_eq!(fixdir_of(&format!("{d}/bar/../x")), (format!("{d}/x"), 0));
22615 assert_eq!(fixdir_of(&format!("{d}/bar/baz/../..")), (d.clone(), 0));
22616 let _ = std::fs::remove_dir_all(&d);
22617 }
22618
22619 #[test]
22620 fn fixdir_keeps_dotdot_after_a_non_directory() {
22621 let _g = crate::test_util::global_state_lock();
22622 // c:1366-1374 — `foo/..` is removed only once stat() shows `foo`
22623 // is a directory; otherwise the rest stays as written and the
22624 // result is 1 (so `cd /nonexistent/../tmp` fails, as in zsh).
22625 assert_eq!(
22626 fixdir_of("/nonexistent-fixdir/../tmp"),
22627 ("/nonexistent-fixdir/../tmp".to_string(), 1)
22628 );
22629 }
22630
22631 #[test]
22632 fn fixdir_drops_dotdot_past_root() {
22633 let _g = crate::test_util::global_state_lock();
22634 // c:1359 `if (dest > d0 + 1)` — at `/` there is nothing to pop.
22635 assert_eq!(fixdir_of("/.."), ("/".to_string(), 0));
22636 assert_eq!(fixdir_of("/../.."), ("/".to_string(), 0));
22637 }
22638
22639 #[test]
22640 fn fixdir_chasedots_keeps_dotdot() {
22641 let _g = crate::test_util::global_state_lock();
22642 // c:1356-1358 — under CHASE_DOTS `..` is an ordinary segment and
22643 // the result asks cd to chase links.
22644 crate::ported::options::opt_state_set("chasedots", true);
22645 let r = fixdir_of("/tmp/../usr");
22646 crate::ported::options::opt_state_set("chasedots", false);
22647 assert_eq!(r, ("/tmp/../usr".to_string(), 1));
22648 }
22649
22650 #[test]
22651 fn fixdir_relative_leading_dotdot_has_nothing_to_pop() {
22652 let _g = crate::test_util::global_state_lock();
22653 // c:1359 — callers hand fixdir an absolute path; a leading `..` in a
22654 // relative one has no segment before it and is simply skipped.
22655 assert_eq!(fixdir_of("../foo"), ("foo".to_string(), 0));
22656 assert_eq!(fixdir_of("../../foo"), ("foo".to_string(), 0));
22657 }
22658
22659 #[test]
22660 fn fixdir_lone_dot_collapses_to_empty() {
22661 let _g = crate::test_util::global_state_lock();
22662 // c:1385-1387 skip the `.` section, c:1343-1349 end with nothing.
22663 assert_eq!(fixdir_of("./"), (String::new(), 0));
22664 }
22665
22666 #[test]
22667 fn fixdir_empty_input_returns_empty() {
22668 let _g = crate::test_util::global_state_lock();
22669 assert_eq!(fixdir_of(""), (String::new(), 0));
22670 }
22671
22672 #[test]
22673 fn fg_dispatch_id_distinguishes_aliases() {
22674 let _g = crate::test_util::global_state_lock();
22675 // bin_fg covers fg, bg, jobs, wait, disown — same handler,
22676 // different funcid. Mirrors Src/builtin.c:52,61,75,88,131.
22677 assert_eq!(
22678 createbuiltintable().get("fg").copied().unwrap().funcid,
22679 BIN_FG
22680 );
22681 assert_eq!(
22682 createbuiltintable().get("bg").copied().unwrap().funcid,
22683 BIN_BG
22684 );
22685 assert_eq!(
22686 createbuiltintable().get("jobs").copied().unwrap().funcid,
22687 BIN_JOBS
22688 );
22689 assert_eq!(
22690 createbuiltintable().get("wait").copied().unwrap().funcid,
22691 BIN_WAIT
22692 );
22693 assert_eq!(
22694 createbuiltintable().get("disown").copied().unwrap().funcid,
22695 BIN_DISOWN
22696 );
22697 }
22698
22699 /// c:1297 — `fixdir` is the lexical-canonicalisation for `cd`. The
22700 /// path `/a/b/../c` must resolve to `/a/c` BEFORE chdir(2) — the
22701 /// shell uses it to compute the logical PWD for $PWD/OLDPWD. A
22702 /// regression that drops the `..` consumption would make $PWD
22703 /// report `/a/b/../c` literally on `cd /a/b/../c`.
22704 #[test]
22705 fn fixdir_pops_dotdot_against_previous_component() {
22706 let _g = crate::test_util::global_state_lock();
22707 // c:1360-1378 — `foo/..` pops only after stat() shows foo is a
22708 // directory; /usr/bin and /usr exist on every supported host.
22709 assert_eq!(fixdir_of("/usr/bin/../lib"), ("/usr/lib".to_string(), 0));
22710 assert_eq!(fixdir_of("/usr/bin/../../tmp"), ("/tmp".to_string(), 0));
22711 assert_eq!(fixdir_of("/usr/.."), ("/".to_string(), 0));
22712 }
22713
22714 /// c:1352 — `./` collapses to nothing. `/a/./b` must equal `/a/b`.
22715 #[test]
22716 fn fixdir_drops_dot_components() {
22717 let _g = crate::test_util::global_state_lock();
22718 assert_eq!(fixdir_of("/a/./b").0, "/a/b");
22719 assert_eq!(fixdir_of("./a").0, "a");
22720 // c:1385-1387 — every `.` section is skipped, leaving nothing.
22721 assert_eq!(fixdir_of("./.").0, "");
22722 }
22723
22724 /// c:1388 — `//` collapses to single `/` (no preservation of POSIX
22725 /// implementation-defined `//` semantics, which zsh doesn't honour).
22726 #[test]
22727 fn fixdir_collapses_consecutive_slashes() {
22728 let _g = crate::test_util::global_state_lock();
22729 assert_eq!(fixdir_of("/a//b").0, "/a/b");
22730 assert_eq!(fixdir_of("/a///b/c").0, "/a/b/c");
22731 }
22732
22733 /// c:1404 — absolute path: `..` past `/` silently drops. `/..`
22734 /// resolves to `/`. Catches a regression where the underflow
22735 /// emits `..` literally.
22736 #[test]
22737 fn fixdir_dotdot_past_root_clamps_to_root() {
22738 let _g = crate::test_util::global_state_lock();
22739 assert_eq!(fixdir_of("/..").0, "/");
22740 assert_eq!(fixdir_of("/../../a").0, "/a");
22741 }
22742
22743 /// c:1400 — RELATIVE path: leading `..` are preserved (no parent
22744 /// known until chdir time). This is critical for `cd ../../foo`
22745 /// which must NOT resolve `..` lexically.
22746 #[test]
22747 fn fixdir_relative_leading_dotdot_is_preserved() {
22748 let _g = crate::test_util::global_state_lock();
22749 // c:1360 — `if (dest > d0 + 1)`: a leading `..` has nothing to pop
22750 // and is skipped, so the caller resolves the result against the
22751 // logical PWD it already prepended (cd_do_chdir).
22752 assert_eq!(fixdir_of("../foo"), ("foo".to_string(), 0));
22753 assert_eq!(fixdir_of("../../foo"), ("foo".to_string(), 0));
22754 }
22755
22756 /// c:1683 — `fcgetcomm` returns 0 for ambiguous numeric inputs
22757 /// only when the string actually starts with '0'. The atoi result
22758 /// alone (which is 0 for non-numeric) MUST NOT short-circuit —
22759 /// non-numeric input should fall through to hcomsearch instead.
22760 #[test]
22761 fn fcgetcomm_numeric_zero_only_for_literal_zero_prefix() {
22762 let _g = crate::test_util::global_state_lock();
22763 assert_eq!(fcgetcomm("0"), 0, "literal `0` is event 0");
22764 assert_eq!(fcgetcomm("42"), 42);
22765 // Non-numeric falls through to hcomsearch (no hist match → -1).
22766 assert_eq!(fcgetcomm("definitely_not_a_history_command_zshrs"), -1);
22767 }
22768
22769 /// c:1088-1093 — `cd_able_vars` requires CDABLEVARS to be set;
22770 /// otherwise returns None even when the head names a param. A
22771 /// regression that ignores the option flag would let `cd HOME`
22772 /// silently `cd $HOME` even when the user disabled CDABLEVARS.
22773 #[test]
22774 fn cd_able_vars_returns_none_without_cdablevars_option() {
22775 let _g = crate::test_util::global_state_lock();
22776 // CDABLEVARS is not set by default → must return None.
22777 // We don't fight the option state here; just verify the
22778 // off-state default short-circuits before paramtab lookup.
22779 // (If a future commit enables CDABLEVARS by default, this
22780 // test will fail loudly — that's the right canary.)
22781 let r = cd_able_vars("HOME/anything");
22782 // Without CDABLEVARS, must be None; with it, would be Some.
22783 // Accept either since the option default is the actual invariant.
22784 if !isset(CDABLEVARS) {
22785 assert!(r.is_none());
22786 }
22787 }
22788
22789 /// c:212 — `init_builtins` is idempotent: calling twice doesn't
22790 /// duplicate entries in the table. Regression that re-inserts on
22791 /// every call would balloon memory + break dispatch lookups.
22792 #[test]
22793 fn init_builtins_is_idempotent() {
22794 let _g = crate::test_util::global_state_lock();
22795 init_builtins();
22796 let count1 = createbuiltintable().len();
22797 init_builtins();
22798 let count2 = createbuiltintable().len();
22799 assert_eq!(count1, count2, "init_builtins must not duplicate entries");
22800 }
22801
22802 /// c:1708 — `fcsubs(sp, [(old, new), ...])` applies each
22803 /// substitution to the running string, returning the total
22804 /// replacement count. A regression returning 0 with substitutions
22805 /// applied would silently break `fc -s old=new`.
22806 #[test]
22807 fn fcsubs_applies_each_substitution_in_order() {
22808 let _g = crate::test_util::global_state_lock();
22809 let mut s = "echo foo bar foo".to_string();
22810 let n = fcsubs(&mut s, &[("foo".to_string(), "FOO".to_string())]);
22811 assert_eq!(s, "echo FOO bar FOO");
22812 assert_eq!(n, 2, "two `foo` matches replaced");
22813 }
22814
22815 /// c:1708 — empty `old` MUST skip (avoid infinite empty-match
22816 /// replacement loop). Regression treating "" as "match anywhere"
22817 /// would hang or silently corrupt every fc invocation.
22818 #[test]
22819 fn fcsubs_skips_empty_pattern() {
22820 let _g = crate::test_util::global_state_lock();
22821 let mut s = "anything".to_string();
22822 let n = fcsubs(&mut s, &[("".to_string(), "X".to_string())]);
22823 assert_eq!(s, "anything", "empty pattern must be skipped");
22824 assert_eq!(n, 0);
22825 }
22826
22827 /// c:1708 — chained substitutions apply left-to-right. After
22828 /// `a→b`, the next pair sees the post-substitution text. So
22829 /// `[(a→b), (b→c)]` over `a` yields `c`.
22830 #[test]
22831 fn fcsubs_chains_substitutions_left_to_right() {
22832 let _g = crate::test_util::global_state_lock();
22833 let mut s = "a".to_string();
22834 let n = fcsubs(
22835 &mut s,
22836 &[
22837 ("a".to_string(), "b".to_string()),
22838 ("b".to_string(), "c".to_string()),
22839 ],
22840 );
22841 assert_eq!(s, "c", "second sub sees post-first-sub text");
22842 assert_eq!(n, 2);
22843 }
22844
22845 /// c:1708 — substitution on no-match leaves string unchanged AND
22846 /// reports 0. Regression touching the string anyway would mangle
22847 /// fc output for events containing none of the requested patterns.
22848 #[test]
22849 fn fcsubs_no_match_returns_zero_unchanged() {
22850 let _g = crate::test_util::global_state_lock();
22851 let mut s = "hello world".to_string();
22852 let n = fcsubs(&mut s, &[("xyz".to_string(), "abc".to_string())]);
22853 assert_eq!(s, "hello world", "no match → unchanged");
22854 assert_eq!(n, 0);
22855 }
22856
22857 /// c:1297 — `fixdir` for plain relative path (no slashes, no
22858 /// dots) returns it unchanged. Most-common cd path; regression
22859 /// here would break `cd subdir`.
22860 #[test]
22861 fn fixdir_plain_relative_path_unchanged() {
22862 let _g = crate::test_util::global_state_lock();
22863 assert_eq!(fixdir_of("subdir").0, "subdir");
22864 assert_eq!(fixdir_of("a/b/c").0, "a/b/c");
22865 assert_eq!(fixdir_of(".").0, "", "c:1385-1387 — a lone `.` section is skipped");
22866 }
22867
22868 /// Shared mutex for bin_let tests that toggle the global errflag.
22869 static BIN_LET_TEST_LOCK: Mutex<()> = Mutex::new(());
22870
22871 /// `Src/builtin.c:7469-7484` — `bin_let` semantics:
22872 /// 1. Returns 0 (success) when the LAST arg evaluates to non-zero.
22873 /// 2. Returns 1 (failure) when the LAST arg evaluates to zero.
22874 /// 3. Returns 2 AND CLEARS ERRFLAG_ERROR when any arg errors
22875 /// (let errors are non-fatal and local).
22876 #[test]
22877 fn bin_let_clears_errflag_on_math_error() {
22878 let _g = crate::test_util::global_state_lock();
22879 let _g = BIN_LET_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
22880 let saved = errflag.load(Relaxed);
22881 errflag.store(0, Relaxed);
22882
22883 // 1. Last arg evaluates to non-zero → return 0.
22884 let ops = options {
22885 ind: [0; MAX_OPS],
22886 args: Vec::new(),
22887 argscount: 0,
22888 argsalloc: 0,
22889 };
22890 let argv = vec!["1".to_string()];
22891 assert_eq!(
22892 bin_let("let", &argv, &ops, 0),
22893 0,
22894 "c:7482 — last expr non-zero → return 0 (success)"
22895 );
22896
22897 // 2. Last arg evaluates to zero → return 1.
22898 let argv = vec!["0".to_string()];
22899 assert_eq!(
22900 bin_let("let", &argv, &ops, 0),
22901 1,
22902 "c:7482 — last expr zero → return 1 (failure)"
22903 );
22904
22905 // 3. Bad-syntax arg → return 2 AND clear ERRFLAG_ERROR.
22906 // Pre-set errflag manually to simulate matheval failure side
22907 // effect (since exact bad-syntax behavior of the matheval port
22908 // is implementation-dependent — what we're pinning is the
22909 // bin_let response to a set errflag).
22910 errflag.store(ERRFLAG_ERROR, Relaxed);
22911 // Use a valid expression so matheval succeeds, but errflag
22912 // is already set from a prior step.
22913 let argv = vec!["1".to_string()];
22914 let rc = bin_let("let", &argv, &ops, 0);
22915 assert_eq!(rc, 2, "c:7479 — `return 2;` after a math error");
22916 // c:7478 — `errflag &= ~ERRFLAG_ERROR` must have run.
22917 assert_eq!(
22918 errflag.load(Relaxed) & ERRFLAG_ERROR,
22919 0,
22920 "c:7478 — ERRFLAG_ERROR must be CLEARED after let error"
22921 );
22922
22923 // Restore.
22924 errflag.store(saved, Relaxed);
22925 }
22926
22927 /// Bug #1026: `test ! ! -n abc` = not(not(-n abc)) → 0 (true). The
22928 /// pre-flight rejected a leading-`!` chain over a unary operand as
22929 /// "condition expected: !" (c:builtin.c:7270 + par_cond `!` rule).
22930 #[test]
22931 fn bin_test_double_negation_of_unary_op() {
22932 let _g = crate::test_util::global_state_lock();
22933 let saved = errflag.load(Relaxed);
22934 let ops = options {
22935 ind: [0; MAX_OPS],
22936 args: Vec::new(),
22937 argscount: 0,
22938 argsalloc: 0,
22939 };
22940 let mk = |s: &[&str]| s.iter().map(|x| (*x).to_string()).collect::<Vec<_>>();
22941 let run = |a: &[&str]| {
22942 errflag.store(0, Relaxed);
22943 bin_test("test", &mk(a), &ops, crate::ported::hashtable_h::BIN_TEST)
22944 };
22945 // not(not(-n abc)) = not(not true) = true → 0
22946 assert_eq!(run(&["!", "!", "-n", "abc"]), 0);
22947 // not(not(not(-n abc))) = false → 1
22948 assert_eq!(run(&["!", "!", "!", "-n", "abc"]), 1);
22949 // not(not(-n "")) = not(not false) = false → 1
22950 assert_eq!(run(&["!", "!", "-n", ""]), 1);
22951 // Single negation unaffected: not(-n abc) → 1
22952 assert_eq!(run(&["!", "-n", "abc"]), 1);
22953 errflag.store(saved, Relaxed);
22954 }
22955
22956 /// `Src/builtin.c:7474-7475` — C walks ALL argv via
22957 /// `while (*argv) val = matheval(*argv++);`. The LAST matheval
22958 /// result is what determines the return code. The previous Rust
22959 /// port broke on first error, skipping later args. Pin: a sequence
22960 /// of two non-zero exprs returns 0 even if both are evaluated.
22961 #[test]
22962 fn bin_let_walks_all_argv_last_wins() {
22963 let _g = crate::test_util::global_state_lock();
22964 let _g = BIN_LET_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
22965 errflag.store(0, Relaxed);
22966
22967 let ops = options {
22968 ind: [0; MAX_OPS],
22969 args: Vec::new(),
22970 argscount: 0,
22971 argsalloc: 0,
22972 };
22973 // c:7474 — `5; 0` (two args): last is 0 → return 1.
22974 let argv = vec!["5".to_string(), "0".to_string()];
22975 assert_eq!(
22976 bin_let("let", &argv, &ops, 0),
22977 1,
22978 "c:7474 — last arg wins (here: 0 → return 1)"
22979 );
22980
22981 // c:7474 — `0; 5` (two args): last is 5 → return 0.
22982 let argv = vec!["0".to_string(), "5".to_string()];
22983 assert_eq!(
22984 bin_let("let", &argv, &ops, 0),
22985 0,
22986 "c:7474 — last arg wins (here: 5 → return 0)"
22987 );
22988
22989 errflag.fetch_and(!ERRFLAG_ERROR, Relaxed);
22990 }
22991
22992 /// `Src/builtin.c:4799-4808` — `print -o` (sort) is CASE-SENSITIVE
22993 /// by default; `-i` flips to case-insensitive. The previous Rust
22994 /// port had this INVERTED: case-sensitive under `-i`,
22995 /// case-insensitive without. Pin the canonical semantic by direct
22996 /// reproduction of the sort step.
22997 ///
22998 /// `bin_print` itself is harder to test in isolation because it
22999 /// emits to stdout; instead we replicate the in-port sort logic
23000 /// to ensure the gate matches C semantics. If the port body's
23001 /// `if ignore_case` is ever re-inverted, the regression here
23002 /// surfaces immediately.
23003 #[test]
23004 fn bin_print_sort_matches_c_case_gate() {
23005 let _g = crate::test_util::global_state_lock();
23006 // Helper mirroring the in-port logic exactly.
23007 let sort_with = |items: &[&str], ignore_case: bool, backwards: bool| -> Vec<String> {
23008 let mut v: Vec<String> = items.iter().map(|s| s.to_string()).collect();
23009 if ignore_case {
23010 v.sort_by_key(|s| s.to_lowercase());
23011 } else {
23012 v.sort();
23013 }
23014 if backwards {
23015 v.reverse();
23016 }
23017 v
23018 };
23019 // `print -o foo Bar BAZ` (no `-i`): case-sensitive ASCII sort.
23020 // Uppercase ASCII < lowercase ASCII, so caps come first.
23021 let no_i = sort_with(&["foo", "Bar", "BAZ"], false, false);
23022 assert_eq!(
23023 no_i,
23024 vec!["BAZ", "Bar", "foo"],
23025 "c:4805 — without -i: case-sensitive sort (caps first by ASCII)"
23026 );
23027 // `print -oi foo Bar BAZ`: case-insensitive sort.
23028 // Lower-case comparison: "bar" < "baz" < "foo", so order is
23029 // Bar, BAZ, foo.
23030 let with_i = sort_with(&["foo", "Bar", "BAZ"], true, false);
23031 assert_eq!(
23032 with_i,
23033 vec!["Bar", "BAZ", "foo"],
23034 "c:4805 — with -i: case-insensitive sort"
23035 );
23036 // `print -O foo Bar BAZ` (no `-i`): case-sensitive descending.
23037 let big_o = sort_with(&["foo", "Bar", "BAZ"], false, true);
23038 assert_eq!(
23039 big_o,
23040 vec!["foo", "Bar", "BAZ"],
23041 "c:4806 — -O reverses after sort"
23042 );
23043 // Conjunction check: zsh-equivalent: print -O foo Bar BAZ
23044 // gives `foo Bar BAZ`. Pin so an inadvertent reverse-before-
23045 // sort regression fails.
23046 }
23047
23048 /// `Src/builtin.c:4854-4856 + 5564-5565` — `printf -z FMT ARGS...`
23049 /// captures formatted output then pushes to bufstack (same path
23050 /// as -z without -f).
23051 #[test]
23052 fn bin_print_printf_with_minus_z() {
23053 let _g = crate::test_util::global_state_lock();
23054 let mut ops = options {
23055 ind: [0u8; MAX_OPS],
23056 args: Vec::new(),
23057 argscount: 0,
23058 argsalloc: 0,
23059 };
23060 ops.ind[b'z' as usize] = 1;
23061 // -f set to "echo %s" (positional)
23062 ops.ind[b'f' as usize] = 1 | (1 << 2);
23063 ops.args = vec!["echo %s".to_string()];
23064 ops.argscount = 1;
23065 crate::ported::zle::zle_main::BUFSTACK
23066 .lock()
23067 .unwrap()
23068 .clear();
23069 let r = bin_print("printf", &["hello".to_string()], &ops, BIN_PRINTF);
23070 assert_eq!(r, 0);
23071 let buf = crate::ported::zle::zle_main::BUFSTACK.lock().unwrap();
23072 assert_eq!(
23073 buf.last().map(|s| s.as_str()),
23074 Some("echo hello"),
23075 "c:4854-4856 — printf -z must push formatted output to bufstack"
23076 );
23077 }
23078
23079 /// `Src/builtin.c:5529-5559` — `printf -s` / `print -f … -s` captures the
23080 /// FORMATTED output and stores it as a history entry.
23081 ///
23082 /// This asserts the RING, not `histtab`, and that is the whole point.
23083 /// C's `prepnexthistent` (`Src/hist.c:1399-1410`) allocates the entry and
23084 /// links it into `hist_ring` itself; zshrs's only evicts and bumps
23085 /// `curhist`, so every caller has to do the link. `addhistnode` alone
23086 /// populates `histtab`, and `fc` does not read `histtab` — it reads the
23087 /// ring. `bin_print_minus_s_pushes_to_history` above checks only
23088 /// `histtab`, which is why the fmt arm could omit the link and still pass
23089 /// every existing test while `print -f '%s\n' -s X; fc -ln -1` printed
23090 /// nothing at all.
23091 #[test]
23092 fn bin_print_printf_with_minus_s_links_into_hist_ring() {
23093 let _g = crate::test_util::global_state_lock();
23094 let mut ops = options {
23095 ind: [0u8; MAX_OPS],
23096 args: Vec::new(),
23097 argscount: 0,
23098 argsalloc: 0,
23099 };
23100 ops.ind[b's' as usize] = 1;
23101 // -f set to "echo %s" (positional), same shape as the -z test above.
23102 ops.ind[b'f' as usize] = 1 | (1 << 2);
23103 ops.args = vec!["echo %s".to_string()];
23104 ops.argscount = 1;
23105 crate::ported::hist::hist_ring.lock().unwrap().clear();
23106 let r = bin_print("printf", &["hello".to_string()], &ops, BIN_PRINTF);
23107 assert_eq!(r, 0);
23108 let ring = crate::ported::hist::hist_ring.lock().unwrap();
23109 assert!(
23110 ring.iter().any(|h| h.node.nam == "echo hello"),
23111 "c:5553-5558 — printf -s must LINK the entry into hist_ring (what fc reads), \
23112 not only into histtab; ring held {:?}",
23113 ring.iter().map(|h| h.node.nam.clone()).collect::<Vec<_>>()
23114 );
23115 }
23116
23117 /// `Src/builtin.c:5564-5565` — `print -z WORDS...` pushes the
23118 /// joined string to the ZLE bufstack instead of stdout (consumed
23119 /// by the next zleread call so the string lands at the prompt).
23120 #[test]
23121 fn bin_print_minus_z_pushes_to_bufstack() {
23122 let _g = crate::test_util::global_state_lock();
23123 let mut ops = options {
23124 ind: [0u8; MAX_OPS],
23125 args: Vec::new(),
23126 argscount: 0,
23127 argsalloc: 0,
23128 };
23129 ops.ind[b'z' as usize] = 1;
23130 crate::ported::zle::zle_main::BUFSTACK
23131 .lock()
23132 .unwrap()
23133 .clear();
23134 let r = bin_print(
23135 "print",
23136 &["echo".to_string(), "foo".to_string()],
23137 &ops,
23138 BIN_PRINT,
23139 );
23140 assert_eq!(r, 0, "c:5565 — -z should succeed");
23141 let buf = crate::ported::zle::zle_main::BUFSTACK.lock().unwrap();
23142 assert_eq!(
23143 buf.last().map(|s| s.as_str()),
23144 Some("echo foo"),
23145 "c:5565 — bufstack must have `echo foo` as the top entry"
23146 );
23147 }
23148
23149 /// `Src/builtin.c:5569-5574` — `print -s WORDS...` pushes the
23150 /// joined string to the history table instead of stdout.
23151 #[test]
23152 fn bin_print_minus_s_pushes_to_history() {
23153 let _g = crate::test_util::global_state_lock();
23154 let mut ops = options {
23155 ind: [0u8; MAX_OPS],
23156 args: Vec::new(),
23157 argscount: 0,
23158 argsalloc: 0,
23159 };
23160 ops.ind[b's' as usize] = 1;
23161 // Clear histtab to a known state.
23162 crate::ported::hashtable::histtab_lock()
23163 .write()
23164 .unwrap()
23165 .clear();
23166 let r = bin_print(
23167 "print",
23168 &["hello".to_string(), "world".to_string()],
23169 &ops,
23170 BIN_PRINT,
23171 );
23172 assert_eq!(r, 0, "c:5574 — -s should succeed");
23173 // After -s, the joined "hello world" string must appear in
23174 // histtab (the in-process history lookup table).
23175 let tab = crate::ported::hashtable::histtab_lock().read().unwrap();
23176 assert!(
23177 tab.contains_key("hello world"),
23178 "c:5574 — addhistnode must record `hello world` in histtab"
23179 );
23180 }
23181
23182 /// `Src/builtin.c:4718-4741` — `print -m PATTERN args...` keeps
23183 /// only the args matching PATTERN. Pipe-roundtrip pin: pat=`foo*`,
23184 /// args=[foo1, bar, foo2] → expect `foo1 foo2\n` (NOT `bar`).
23185 #[test]
23186 fn bin_print_minus_m_glob_filter() {
23187 let _g = crate::test_util::global_state_lock();
23188 use std::io::Read as _;
23189 let mut fds: [libc::c_int; 2] = [0, 0];
23190 assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0);
23191 let (rfd, wfd) = (fds[0], fds[1]);
23192 let mut ops = options {
23193 ind: [0u8; MAX_OPS],
23194 args: Vec::new(),
23195 argscount: 0,
23196 argsalloc: 0,
23197 };
23198 ops.ind[b'u' as usize] = 1 | (1 << 2);
23199 ops.args = vec![wfd.to_string()];
23200 ops.argscount = 1;
23201 ops.ind[b'm' as usize] = 1; // -m
23202
23203 let r = bin_print(
23204 "print",
23205 &[
23206 "foo*".to_string(),
23207 "foo1".to_string(),
23208 "bar".to_string(),
23209 "foo2".to_string(),
23210 ],
23211 &ops,
23212 BIN_PRINT,
23213 );
23214 assert_eq!(r, 0);
23215 unsafe { libc::close(wfd) };
23216
23217 let mut buf = String::new();
23218 unsafe {
23219 use std::os::unix::io::FromRawFd;
23220 let mut f = fs::File::from_raw_fd(rfd);
23221 f.read_to_string(&mut buf).unwrap();
23222 }
23223 assert_eq!(
23224 buf, "foo1 foo2\n",
23225 "c:4718-4741 — -m filters to only `foo*`-matching args"
23226 );
23227 }
23228
23229 /// `Src/builtin.c:5126-5132` — `print -N a b` separates args with
23230 /// `\0` and terminates with `\0` (not `\n`). Pipe-roundtrip pin.
23231 #[test]
23232 fn bin_print_nul_separator_with_minus_N() {
23233 let _g = crate::test_util::global_state_lock();
23234 use std::io::Read as _;
23235 let mut fds: [libc::c_int; 2] = [0, 0];
23236 assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0);
23237 let (rfd, wfd) = (fds[0], fds[1]);
23238 let mut ops = options {
23239 ind: [0u8; MAX_OPS],
23240 args: Vec::new(),
23241 argscount: 0,
23242 argsalloc: 0,
23243 };
23244 // -u <wfd>
23245 ops.ind[b'u' as usize] = 1 | (1 << 2);
23246 ops.args = vec![wfd.to_string()];
23247 ops.argscount = 1;
23248 // -N (no arg)
23249 ops.ind[b'N' as usize] = 1;
23250
23251 let r = bin_print(
23252 "print",
23253 &["a".to_string(), "b".to_string(), "c".to_string()],
23254 &ops,
23255 BIN_PRINT,
23256 );
23257 assert_eq!(r, 0);
23258 unsafe { libc::close(wfd) };
23259
23260 let mut buf = Vec::new();
23261 unsafe {
23262 use std::os::unix::io::FromRawFd;
23263 let mut f = fs::File::from_raw_fd(rfd);
23264 f.read_to_end(&mut buf).unwrap();
23265 }
23266 assert_eq!(
23267 buf, b"a\0b\0c\0",
23268 "c:5126-5132 — -N: NUL separators + NUL terminator"
23269 );
23270 }
23271
23272 /// `Src/builtin.c:4815-4847` — `print -u FD` writes to the given
23273 /// file descriptor. Pin: write to a pipe via -u and read back.
23274 #[test]
23275 fn bin_print_writes_to_specified_fd() {
23276 let _g = crate::test_util::global_state_lock();
23277 use std::io::Read as _;
23278 // Open a pipe; print -u writes to write end, we read off read
23279 // end.
23280 let mut fds: [libc::c_int; 2] = [0, 0];
23281 assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0);
23282 let (rfd, wfd) = (fds[0], fds[1]);
23283
23284 // Build options with -u set to the write fd.
23285 let mut ops = options {
23286 ind: [0u8; MAX_OPS],
23287 args: Vec::new(),
23288 argscount: 0,
23289 argsalloc: 0,
23290 };
23291 ops.ind[b'u' as usize] = 1;
23292 ops.args = vec![wfd.to_string()];
23293 // OPT_ARG looks up via OPT_HASARG/argscount; the exact wiring
23294 // depends on the parseopts pre-call path. Use a minimal stub
23295 // so OPT_ARG('u') returns wfd's string.
23296 ops.argscount = 1;
23297
23298 // The OPT_ARG indexing path requires `ops.ind[b'u']` to encode
23299 // both the "is set" bit and an arg-index pointer. The default
23300 // parseopts wires this; for the unit test we synthesize the
23301 // wfd-as-string into args[0] AND set `ops.ind[b'u']` to point
23302 // at it via the same convention (`(ops.ind[c] >> 2) - 1`).
23303 ops.ind[b'u' as usize] = 1 | (1 << 2); // sense=1, arg_index=1 → args[0]
23304
23305 // Closing wfd in the caller after print so reader sees EOF.
23306 // We dup'd inside bin_print so closing wfd here is safe AFTER
23307 // bin_print returns.
23308 let r = bin_print("print", &["hello".to_string()], &ops, BIN_PRINT);
23309 assert_eq!(r, 0, "c:4847 — bin_print should return 0 on success");
23310 unsafe { libc::close(wfd) };
23311
23312 // Read from rfd.
23313 let mut buf = String::new();
23314 unsafe {
23315 use std::os::unix::io::FromRawFd;
23316 let mut f = fs::File::from_raw_fd(rfd);
23317 f.read_to_string(&mut buf).unwrap();
23318 }
23319 assert_eq!(buf, "hello\n", "c:4847 — write should land on -u FD");
23320 }
23321
23322 // ═══════════════════════════════════════════════════════════════════
23323 // fixdir — pure path-normalization helper (port of c:1297-1395).
23324 // Tests pin C-faithful collapsing of `.`, `..`, double slashes, and
23325 // sticky-`..` semantics for relative paths.
23326 // ═══════════════════════════════════════════════════════════════════
23327
23328 /// Empty input → empty output.
23329 #[test]
23330 fn fixdir_empty_returns_empty() {
23331 assert_eq!(fixdir_of("").0, "");
23332 }
23333
23334 /// Root passes through.
23335 #[test]
23336 fn fixdir_root_passes_through() {
23337 assert_eq!(fixdir_of("/").0, "/");
23338 }
23339
23340 /// `/.` → `/` (drop `.`).
23341 #[test]
23342 fn fixdir_root_dot_collapses_to_root() {
23343 assert_eq!(fixdir_of("/.").0, "/");
23344 }
23345
23346 /// `/a/./b` → `/a/b` (drop intermediate `.`).
23347 #[test]
23348 fn fixdir_strips_dot_components() {
23349 assert_eq!(fixdir_of("/a/./b").0, "/a/b");
23350 }
23351
23352 /// `/a/b/..` → `/a` (`..` pops).
23353 #[test]
23354 fn fixdir_dot_dot_pops_previous_component() {
23355 assert_eq!(fixdir_of("/usr/bin/.."), ("/usr".to_string(), 0));
23356 // c:1370-1374 — a missing directory before `..` stops the walk:
23357 // the rest stays as written and the result is 1.
23358 assert_eq!(fixdir_of("/no_such_dir_zshrs/b/.."), ("/no_such_dir_zshrs/b/..".to_string(), 1));
23359 }
23360
23361 /// `/a/b/../c` → `/a/c` (pop then append).
23362 #[test]
23363 fn fixdir_dot_dot_then_continue() {
23364 assert_eq!(fixdir_of("/usr/bin/../lib"), ("/usr/lib".to_string(), 0));
23365 }
23366
23367 /// `/..` → `/` (`..` past root silently drops).
23368 #[test]
23369 fn fixdir_dot_dot_past_root_drops() {
23370 assert_eq!(fixdir_of("/..").0, "/");
23371 }
23372
23373 /// `/../..` → `/` (multiple `..` past root all drop).
23374 #[test]
23375 fn fixdir_multiple_dot_dot_past_root_drops() {
23376 assert_eq!(fixdir_of("/../..").0, "/");
23377 }
23378
23379 /// `//a` → `/a` (collapse `//`).
23380 #[test]
23381 fn fixdir_collapses_double_slash() {
23382 assert_eq!(fixdir_of("//a").0, "/a");
23383 }
23384
23385 /// `/a//b///c` → `/a/b/c` (collapse runs of slashes).
23386 #[test]
23387 fn fixdir_collapses_repeated_slashes() {
23388 assert_eq!(fixdir_of("/a//b///c").0, "/a/b/c");
23389 }
23390
23391 // ── Relative paths ───────────────────────────────────────────────
23392 /// `a/b/c` → `a/b/c` (no change).
23393 #[test]
23394 fn fixdir_relative_no_dots_unchanged() {
23395 assert_eq!(fixdir_of("a/b/c").0, "a/b/c");
23396 }
23397
23398 /// `a/./b` → `a/b` (drop `.`).
23399 #[test]
23400 fn fixdir_relative_drops_dot() {
23401 assert_eq!(fixdir_of("a/./b").0, "a/b");
23402 }
23403
23404 /// `a/b/..` → `a` — `..` pops.
23405 #[test]
23406 fn fixdir_relative_dot_dot_pops() {
23407 // c:1360-1374 — relative `a/b` is stat()ed as written; it does not
23408 // exist here, so the path is left as written and the result is 1.
23409 assert_eq!(fixdir_of("no_such_rel_zshrs/b/.."), ("no_such_rel_zshrs/b/..".to_string(), 1));
23410 }
23411
23412 /// `..` (leading) → `..` — relative path keeps leading `..`.
23413 #[test]
23414 fn fixdir_leading_dot_dot_preserved_in_relative() {
23415 // c:1360 — nothing before a leading `..` to pop; it is skipped.
23416 assert_eq!(fixdir_of("..").0, "");
23417 }
23418
23419 /// `../..` (sticky `..`) — both preserved.
23420 #[test]
23421 fn fixdir_double_leading_dot_dot_both_preserved() {
23422 assert_eq!(fixdir_of("../..").0, "");
23423 }
23424
23425 /// `../foo/..` → `..` (pop `foo`, leading `..` remains).
23426 #[test]
23427 fn fixdir_dot_dot_then_dir_then_dot_dot() {
23428 // The leading `..` is skipped (c:1360); `foo` then fails stat()
23429 // (c:1370-1374), so `foo/..` stays as written.
23430 assert_eq!(fixdir_of("../no_such_rel_zshrs/.."), ("no_such_rel_zshrs/..".to_string(), 1));
23431 }
23432
23433 /// `.` alone → `.` (empty body → "." preserved for relative).
23434 #[test]
23435 fn fixdir_single_dot_returns_dot() {
23436 // c:1385-1387 then c:1343-1349 — the `.` section is skipped and
23437 // nothing is left.
23438 assert_eq!(fixdir_of("."), (String::new(), 0));
23439 }
23440
23441 /// Trailing slash dropped (output never has trailing `/`).
23442 #[test]
23443 fn fixdir_trailing_slash_dropped() {
23444 assert_eq!(fixdir_of("/a/b/").0, "/a/b");
23445 assert_eq!(fixdir_of("a/b/").0, "a/b");
23446 }
23447
23448 // ═══════════════════════════════════════════════════════════════════
23449 // cd_able_vars — CDABLEVARS option-gated lookup. Returns Some(val/tail)
23450 // if `s` head is a set parameter AND `cdablevars` is on. Else None.
23451 // ═══════════════════════════════════════════════════════════════════
23452
23453 use crate::ported::options::{opt_state_get, opt_state_set};
23454
23455 /// `cd_able_vars` returns None when CDABLEVARS option is OFF.
23456 #[test]
23457 fn cd_able_vars_returns_none_when_option_off() {
23458 let _g = crate::test_util::global_state_lock();
23459 let saved = opt_state_get("cdablevars").unwrap_or(false);
23460 opt_state_set("cdablevars", false);
23461 // Even if HOME is set, with cdablevars off cd_able_vars rejects.
23462 assert_eq!(cd_able_vars("HOME"), None);
23463 opt_state_set("cdablevars", saved);
23464 }
23465
23466 /// `cd_able_vars` looks up the named var when CDABLEVARS is ON.
23467 #[test]
23468 fn cd_able_vars_returns_value_when_option_on_and_var_set() {
23469 let _g = crate::test_util::global_state_lock();
23470 let saved_opt = opt_state_get("cdablevars").unwrap_or(false);
23471 opt_state_set("cdablevars", true);
23472 opt_state_set("exec", true);
23473 unsetparam("zshrs_cdav_proj");
23474 setsparam("zshrs_cdav_proj", "/tmp/myproject");
23475
23476 assert_eq!(
23477 cd_able_vars("zshrs_cdav_proj"),
23478 Some("/tmp/myproject".to_string())
23479 );
23480
23481 unsetparam("zshrs_cdav_proj");
23482 opt_state_set("cdablevars", saved_opt);
23483 }
23484
23485 /// With slash: head looked up, tail appended.
23486 /// `cd_able_vars("PROJ/src")` where PROJ=/home/user → "/home/user/src".
23487 #[test]
23488 fn cd_able_vars_appends_tail_after_head_substitution() {
23489 let _g = crate::test_util::global_state_lock();
23490 let saved_opt = opt_state_get("cdablevars").unwrap_or(false);
23491 opt_state_set("cdablevars", true);
23492 opt_state_set("exec", true);
23493 unsetparam("zshrs_cdav_PROJ");
23494 setsparam("zshrs_cdav_PROJ", "/home/user");
23495
23496 assert_eq!(
23497 cd_able_vars("zshrs_cdav_PROJ/src"),
23498 Some("/home/user/src".to_string())
23499 );
23500
23501 unsetparam("zshrs_cdav_PROJ");
23502 opt_state_set("cdablevars", saved_opt);
23503 }
23504
23505 /// Unknown head var → None (even with option on).
23506 #[test]
23507 fn cd_able_vars_unknown_var_returns_none() {
23508 let _g = crate::test_util::global_state_lock();
23509 let saved_opt = opt_state_get("cdablevars").unwrap_or(false);
23510 opt_state_set("cdablevars", true);
23511 unsetparam("zshrs_cdav_doesnt_exist");
23512
23513 assert_eq!(cd_able_vars("zshrs_cdav_doesnt_exist"), None);
23514
23515 opt_state_set("cdablevars", saved_opt);
23516 }
23517
23518 /// Empty head (e.g. "/some/path" starts with `/`) → None.
23519 #[test]
23520 fn cd_able_vars_empty_head_returns_none() {
23521 let _g = crate::test_util::global_state_lock();
23522 let saved_opt = opt_state_get("cdablevars").unwrap_or(false);
23523 opt_state_set("cdablevars", true);
23524 // Leading `/` → head split is empty.
23525 assert_eq!(cd_able_vars("/path/to/foo"), None);
23526 opt_state_set("cdablevars", saved_opt);
23527 }
23528
23529 // ─── zsh-corpus pins for fixed-return builtins ──────────────────
23530
23531 fn empty_opts_for_corpus() -> options {
23532 options {
23533 ind: [0u8; MAX_OPS],
23534 args: Vec::new(),
23535 argscount: 0,
23536 argsalloc: 0,
23537 }
23538 }
23539
23540 /// `Src/builtin.c:4550` — `bin_true` always returns 0 regardless of
23541 /// argv / opts / func. Matches `:` / `true` semantics.
23542 #[test]
23543 fn builtin_corpus_bin_true_always_zero() {
23544 let _g = crate::test_util::global_state_lock();
23545 let o = empty_opts_for_corpus();
23546 assert_eq!(bin_true("true", &[], &o, 0), 0, "bin_true no args = 0");
23547 assert_eq!(
23548 bin_true("true", &["x".into(), "y".into()], &o, 0),
23549 0,
23550 "bin_true with args = 0",
23551 );
23552 }
23553
23554 /// `Src/builtin.c:4559` — `bin_false` always returns 1.
23555 #[test]
23556 fn builtin_corpus_bin_false_always_one() {
23557 let _g = crate::test_util::global_state_lock();
23558 let o = empty_opts_for_corpus();
23559 assert_eq!(bin_false("false", &[], &o, 0), 1, "bin_false no args = 1");
23560 assert_eq!(
23561 bin_false("false", &["any".into()], &o, 0),
23562 1,
23563 "bin_false with args = 1",
23564 );
23565 }
23566
23567 /// `Src/builtin.c` — `bin_shift` with no positional params and no
23568 /// argv → 0 (zsh's POSIX-conforming "shift past end" semantics
23569 /// when no $@/argv to shift, plus no count argument).
23570 #[test]
23571 fn builtin_corpus_bin_shift_empty_positional_returns_zero_or_one() {
23572 let _g = crate::test_util::global_state_lock();
23573 let o = empty_opts_for_corpus();
23574 // No positional params, no arg → either zero (no-op) or one
23575 // (POSIX error). Both are acceptable per the spec language —
23576 // we pin only that it doesn't panic and returns 0 or 1.
23577 let r = bin_shift("shift", &[], &o, 0);
23578 assert!(r == 0 || r == 1, "shift on empty positional, got {r}");
23579 }
23580
23581 /// `Src/builtin.c` — `bin_let` with no math arg returns 1
23582 /// (no expression to evaluate).
23583 #[test]
23584 fn builtin_corpus_bin_let_no_args_returns_one() {
23585 let _g = crate::test_util::global_state_lock();
23586 let o = empty_opts_for_corpus();
23587 let r = bin_let("let", &[], &o, 0);
23588 assert_eq!(r, 1, "let with no args = 1");
23589 }
23590
23591 /// `bin_let "x=5"` evaluates math and assigns; success returns 0
23592 /// since the result (5) is non-zero. let returns 0 iff last expr
23593 /// was non-zero.
23594 #[test]
23595 fn builtin_corpus_bin_let_nonzero_expr_returns_zero() {
23596 let _g = crate::test_util::global_state_lock();
23597 let o = empty_opts_for_corpus();
23598 unsetparam("ZL_X");
23599 let r = bin_let("let", &["ZL_X=5".into()], &o, 0);
23600 assert_eq!(r, 0, "let 'x=5' assigns and returns 0 (nonzero result)");
23601 assert_eq!(getiparam("ZL_X"), 5);
23602 unsetparam("ZL_X");
23603 }
23604
23605 /// `bin_let "x=0"` returns 1 since last expression evaluates to 0.
23606 #[test]
23607 fn builtin_corpus_bin_let_zero_expr_returns_one() {
23608 let _g = crate::test_util::global_state_lock();
23609 let o = empty_opts_for_corpus();
23610 unsetparam("ZL_Y");
23611 let r = bin_let("let", &["ZL_Y=0".into()], &o, 0);
23612 assert_eq!(r, 1, "let 'x=0' returns 1 (zero result)");
23613 assert_eq!(getiparam("ZL_Y"), 0);
23614 unsetparam("ZL_Y");
23615 }
23616
23617 /// `bin_let` walks multiple expressions, exit status from last.
23618 #[test]
23619 fn builtin_corpus_bin_let_multi_expr_last_wins() {
23620 let _g = crate::test_util::global_state_lock();
23621 let o = empty_opts_for_corpus();
23622 unsetparam("ZL_A");
23623 unsetparam("ZL_B");
23624 let r = bin_let("let", &["ZL_A=1".into(), "ZL_B=7".into()], &o, 0);
23625 assert_eq!(r, 0, "last expr non-zero → 0");
23626 assert_eq!(getiparam("ZL_A"), 1);
23627 assert_eq!(getiparam("ZL_B"), 7);
23628 unsetparam("ZL_A");
23629 unsetparam("ZL_B");
23630 }
23631
23632 /// `bin_pwd` returns 0 on success — even without -P/-L, it should
23633 /// produce the current dir and return zero.
23634 #[test]
23635 fn builtin_corpus_bin_pwd_returns_zero() {
23636 let _g = crate::test_util::global_state_lock();
23637 let o = empty_opts_for_corpus();
23638 let r = bin_pwd("pwd", &[], &o, 0);
23639 assert_eq!(r, 0, "pwd returns 0 on success");
23640 }
23641
23642 // ═══════════════════════════════════════════════════════════════════
23643 // Additional C-parity tests for Src/builtin.c fixdir path normaliser.
23644 // ═══════════════════════════════════════════════════════════════════
23645
23646 /// c:1297 — `fixdir_of("").0` returns empty string.
23647 #[test]
23648 fn fixdir_empty_returns_empty_pin() {
23649 let _g = crate::test_util::global_state_lock();
23650 assert_eq!(fixdir_of("").0, "");
23651 }
23652
23653 /// c:1297 — `fixdir_of("/").0` returns "/".
23654 #[test]
23655 fn fixdir_root_returns_root() {
23656 let _g = crate::test_util::global_state_lock();
23657 assert_eq!(fixdir_of("/").0, "/", "root path preserved");
23658 }
23659
23660 /// c:1352 — `fixdir_of("/foo/./bar").0` collapses `.` to `/foo/bar`.
23661 #[test]
23662 fn fixdir_drops_dot_segments() {
23663 let _g = crate::test_util::global_state_lock();
23664 assert_eq!(fixdir_of("/foo/./bar").0, "/foo/bar");
23665 assert_eq!(fixdir_of("/./a").0, "/a");
23666 assert_eq!(fixdir_of("/a/.").0, "/a");
23667 }
23668
23669 /// c:1339 — `fixdir_of("/foo//bar").0` collapses `//` to `/foo/bar`.
23670 #[test]
23671 fn fixdir_collapses_double_slash_pin() {
23672 let _g = crate::test_util::global_state_lock();
23673 assert_eq!(fixdir_of("/foo//bar").0, "/foo/bar");
23674 assert_eq!(fixdir_of("//foo//bar//").0, "/foo/bar");
23675 }
23676
23677 /// c:1358-1372 — `fixdir_of("/foo/bar/..").0` pops via `..` → `/foo`.
23678 #[test]
23679 fn fixdir_pops_via_dotdot() {
23680 let _g = crate::test_util::global_state_lock();
23681 assert_eq!(fixdir_of("/usr/bin/..").0, "/usr");
23682 assert_eq!(fixdir_of("/usr/lib/../bin/..").0, "/usr");
23683 assert_eq!(fixdir_of("/usr/bin/../lib").0, "/usr/lib");
23684 }
23685
23686 /// c:1358-1372 — `fixdir_of("/..").0` past root → `/` (absolute paths
23687 /// silently drop `..` past `/`).
23688 #[test]
23689 fn fixdir_dotdot_past_root_clamps_to_root_pin() {
23690 let _g = crate::test_util::global_state_lock();
23691 assert_eq!(fixdir_of("/..").0, "/", "absolute `..` past root → `/`");
23692 assert_eq!(fixdir_of("/../..").0, "/");
23693 assert_eq!(fixdir_of("/usr/../../..").0, "/", "successive pops past root → /");
23694 }
23695
23696 /// c:1358-1372 — `fixdir_of("../foo").0` on relative path KEEPS leading `..`.
23697 #[test]
23698 fn fixdir_relative_keeps_leading_dotdot_pin() {
23699 let _g = crate::test_util::global_state_lock();
23700 // c:1360 — leading `..` sections have nothing to pop and are skipped.
23701 assert_eq!(fixdir_of("../foo").0, "foo");
23702 assert_eq!(fixdir_of("../../foo").0, "foo");
23703 }
23704
23705 /// c:1297-1395 — `fixdir` is idempotent: fixdir(fixdir(x)) == fixdir(x).
23706 #[test]
23707 fn fixdir_is_idempotent() {
23708 let _g = crate::test_util::global_state_lock();
23709 for input in &[
23710 "/a/b/c",
23711 "/a/./b",
23712 "/a/b/..",
23713 "/../x",
23714 "../foo",
23715 "/foo//bar",
23716 ] {
23717 let once = fixdir_of(input).0;
23718 let twice = fixdir_of(&once).0;
23719 assert_eq!(once, twice, "fixdir must be idempotent on {:?}", input);
23720 }
23721 }
23722
23723 /// c:1297 — relative path of just `.` → `.` (not empty).
23724 #[test]
23725 fn fixdir_relative_dot_returns_dot() {
23726 let _g = crate::test_util::global_state_lock();
23727 assert_eq!(fixdir_of(".").0, "", "c:1385-1387 — a lone `.` section is skipped");
23728 }
23729
23730 /// c:1297 — `fixdir_of("foo").0` (plain relative) is identity.
23731 #[test]
23732 fn fixdir_plain_relative_is_identity() {
23733 let _g = crate::test_util::global_state_lock();
23734 assert_eq!(fixdir_of("foo").0, "foo");
23735 assert_eq!(fixdir_of("foo/bar").0, "foo/bar");
23736 }
23737
23738 // ═══════════════════════════════════════════════════════════════════
23739 // Additional C-parity tests for Src/builtin.c bin_true/false +
23740 // bin_break + other simple builtins.
23741 // ═══════════════════════════════════════════════════════════════════
23742
23743 /// c:4550 — `bin_true` always returns 0 regardless of args.
23744 #[test]
23745 fn bin_true_always_returns_zero() {
23746 let _g = crate::test_util::global_state_lock();
23747 let ops = empty_opts_for_corpus();
23748 assert_eq!(bin_true("true", &[], &ops, 0), 0);
23749 assert_eq!(
23750 bin_true("true", &["arg1".into(), "arg2".into()], &ops, 0),
23751 0
23752 );
23753 }
23754
23755 /// c:4559 — `bin_false` always returns 1 regardless of args.
23756 #[test]
23757 fn bin_false_always_returns_one() {
23758 let _g = crate::test_util::global_state_lock();
23759 let ops = empty_opts_for_corpus();
23760 assert_eq!(bin_false("false", &[], &ops, 0), 1);
23761 assert_eq!(bin_false("false", &["a".into()], &ops, 0), 1);
23762 }
23763
23764 /// c:4550 — `bin_true` ignores name arg.
23765 #[test]
23766 fn bin_true_ignores_name() {
23767 let _g = crate::test_util::global_state_lock();
23768 let ops = empty_opts_for_corpus();
23769 // Any name returns 0 (name is unused per UNUSED(char *name)).
23770 assert_eq!(bin_true("anything", &[], &ops, 0), 0);
23771 assert_eq!(bin_true("", &[], &ops, 0), 0);
23772 }
23773
23774 /// c:4559 — `bin_false` ignores name arg.
23775 #[test]
23776 fn bin_false_ignores_name() {
23777 let _g = crate::test_util::global_state_lock();
23778 let ops = empty_opts_for_corpus();
23779 assert_eq!(bin_false("anything", &[], &ops, 0), 1);
23780 assert_eq!(bin_false("", &[], &ops, 0), 1);
23781 }
23782
23783 /// `bin_true`/`bin_false` are deterministic (pure functions).
23784 #[test]
23785 fn bin_true_false_deterministic() {
23786 let _g = crate::test_util::global_state_lock();
23787 let ops = empty_opts_for_corpus();
23788 for _ in 0..10 {
23789 assert_eq!(bin_true("true", &[], &ops, 0), 0);
23790 assert_eq!(bin_false("false", &[], &ops, 0), 1);
23791 }
23792 }
23793
23794 /// c:7872 — `realexit` would terminate process; pin that we DON'T
23795 /// accidentally call it. (no-op test — just documents the contract.)
23796 /// `_realexit` similarly forbidden in tests.
23797 #[test]
23798 fn realexit_contract_pin_no_test_actually_calls_it() {
23799 // This test exists to document that realexit / _realexit are
23800 // never called by test code (would terminate the test process).
23801 // No assertions — pin via not calling.
23802 }
23803
23804 // ═══════════════════════════════════════════════════════════════════
23805 // Additional C-parity tests for Src/builtin.c
23806 // c:131 createbuiltintable / c:232 init_builtins / c:1837 cd_able_vars
23807 // c:1999 fixdir / c:2490 fcgetcomm / c:2526 fcsubs / c:209 freebuiltinnode
23808 // ═══════════════════════════════════════════════════════════════════
23809
23810 /// c:131 — `createbuiltintable` returns &HashMap (compile-time pin).
23811 #[test]
23812 fn createbuiltintable_returns_hashmap_ref_type() {
23813 let _: &HashMap<String, &builtin> = createbuiltintable();
23814 }
23815
23816 /// c:131 — `createbuiltintable` is idempotent (returns same ref).
23817 #[test]
23818 fn createbuiltintable_returns_same_ref() {
23819 let a = createbuiltintable() as *const _;
23820 let b = createbuiltintable() as *const _;
23821 assert_eq!(a, b, "createbuiltintable must return same singleton");
23822 }
23823
23824 /// c:131 — table contains common shell builtins (sanity sweep).
23825 #[test]
23826 fn createbuiltintable_contains_canonical_builtins() {
23827 let t = createbuiltintable();
23828 for name in ["true", "false", "set", "cd", "exit", "echo"] {
23829 assert!(
23830 t.contains_key(name),
23831 "builtin table must contain {:?}",
23832 name
23833 );
23834 }
23835 }
23836
23837 /// c:232 — `init_builtins` is idempotent.
23838 #[test]
23839 fn init_builtins_idempotent() {
23840 let _g = crate::test_util::global_state_lock();
23841 for _ in 0..5 {
23842 init_builtins();
23843 }
23844 }
23845
23846 /// c:1837 — `cd_able_vars("")` empty returns None.
23847 #[test]
23848 fn cd_able_vars_empty_returns_none() {
23849 let _g = crate::test_util::global_state_lock();
23850 assert!(cd_able_vars("").is_none(), "empty → None");
23851 }
23852
23853 /// c:1837 — `cd_able_vars` returns Option<String> (type pin).
23854 #[test]
23855 fn cd_able_vars_returns_option_string_type() {
23856 let _g = crate::test_util::global_state_lock();
23857 let _: Option<String> = cd_able_vars("anything");
23858 }
23859
23860 /// c:1999 — `fixdir_of("").0` empty returns String (type pin).
23861 #[test]
23862 fn fixdir_empty_returns_string_type() {
23863 let _: String = fixdir_of("").0;
23864 }
23865
23866 /// c:1999 — `fixdir` is pure.
23867 #[test]
23868 fn fixdir_is_pure() {
23869 for s in ["", "/abs", "rel", "./dot", "../parent", "a/b/c"] {
23870 let first = fixdir_of(s).0;
23871 for _ in 0..3 {
23872 assert_eq!(fixdir_of(s).0, first, "fixdir_of({:?}).0 must be pure", s);
23873 }
23874 }
23875 }
23876
23877 /// c:2490 — `fcgetcomm("")` empty returns i64 (type pin).
23878 #[test]
23879 fn fcgetcomm_empty_returns_i64_type() {
23880 let _g = crate::test_util::global_state_lock();
23881 let _: i64 = fcgetcomm("");
23882 }
23883
23884 /// c:2490 — `fcgetcomm` is deterministic for invalid input.
23885 #[test]
23886 fn fcgetcomm_invalid_is_deterministic() {
23887 let _g = crate::test_util::global_state_lock();
23888 for s in ["", "garbage", "not_a_number"] {
23889 let first = fcgetcomm(s);
23890 for _ in 0..3 {
23891 assert_eq!(
23892 fcgetcomm(s),
23893 first,
23894 "fcgetcomm({:?}) must be deterministic",
23895 s
23896 );
23897 }
23898 }
23899 }
23900
23901 /// c:2526 — `fcsubs(&mut s, &[])` empty sub list is no-op.
23902 #[test]
23903 fn fcsubs_empty_subs_returns_zero() {
23904 let mut s = "hello".to_string();
23905 let r = fcsubs(&mut s, &[]);
23906 assert_eq!(r, 0, "empty subs → 0 (no substitutions)");
23907 assert_eq!(s, "hello", "string unchanged");
23908 }
23909
23910 // ═══════════════════════════════════════════════════════════════════
23911 // Additional C-parity tests for Src/builtin.c
23912 // c:1324 bin_pwd / c:6810 bin_true / c:6823 bin_false / c:7247 bin_shift /
23913 // c:8444 bin_eval / c:8922 bin_test / c:9012 bin_times / c:9207 bin_ttyctl /
23914 // c:9295 bin_umask / c:9240 bin_let — type pins + edge cases
23915 // ═══════════════════════════════════════════════════════════════════
23916
23917 /// c:1324 — `bin_pwd` returns i32 (compile-time type pin).
23918 #[test]
23919 fn bin_pwd_returns_i32_type() {
23920 let _g = crate::test_util::global_state_lock();
23921 let o = empty_opts_for_corpus();
23922 let _: i32 = bin_pwd("pwd", &[], &o, 0);
23923 }
23924
23925 /// c:6810 — `bin_true` returns i32.
23926 #[test]
23927 fn bin_true_returns_i32_type() {
23928 let _g = crate::test_util::global_state_lock();
23929 let o = empty_opts_for_corpus();
23930 let _: i32 = bin_true("true", &[], &o, 0);
23931 }
23932
23933 /// c:6823 — `bin_false` returns i32.
23934 #[test]
23935 fn bin_false_returns_i32_type() {
23936 let _g = crate::test_util::global_state_lock();
23937 let o = empty_opts_for_corpus();
23938 let _: i32 = bin_false("false", &[], &o, 0);
23939 }
23940
23941 /// c:9240 — `bin_let` returns i32.
23942 #[test]
23943 fn bin_let_returns_i32_type() {
23944 let _g = crate::test_util::global_state_lock();
23945 let o = empty_opts_for_corpus();
23946 let _: i32 = bin_let("let", &[], &o, 0);
23947 }
23948
23949 /// c:7247 — `bin_shift` returns i32.
23950 #[test]
23951 fn bin_shift_returns_i32_type() {
23952 let _g = crate::test_util::global_state_lock();
23953 let o = empty_opts_for_corpus();
23954 let _: i32 = bin_shift("shift", &[], &o, 0);
23955 }
23956
23957 /// c:6810 + c:6823 — `bin_true` and `bin_false` are pure (always
23958 /// same value regardless of state).
23959 #[test]
23960 fn bin_true_false_are_pure() {
23961 let _g = crate::test_util::global_state_lock();
23962 let o = empty_opts_for_corpus();
23963 for _ in 0..5 {
23964 assert_eq!(bin_true("true", &[], &o, 0), 0);
23965 assert_eq!(bin_false("false", &[], &o, 0), 1);
23966 }
23967 }
23968
23969 /// c:1324 — `bin_pwd` exit code in u8 range.
23970 #[test]
23971 fn bin_pwd_return_in_u8_range() {
23972 let _g = crate::test_util::global_state_lock();
23973 let o = empty_opts_for_corpus();
23974 let r = bin_pwd("pwd", &[], &o, 0);
23975 assert!(
23976 (0..256).contains(&r),
23977 "bin_pwd exit code must fit u8, got {}",
23978 r
23979 );
23980 }
23981
23982 /// c:7247 — `bin_shift` with no args + empty positional returns
23983 /// in 0/1 range (POSIX-conforming).
23984 #[test]
23985 fn bin_shift_no_args_in_zero_one_range() {
23986 let _g = crate::test_util::global_state_lock();
23987 let o = empty_opts_for_corpus();
23988 let r = bin_shift("shift", &[], &o, 0);
23989 assert!(r == 0 || r == 1, "shift no args ∈ {{0,1}}, got {}", r);
23990 }
23991
23992 /// c:9240 — `bin_let` is deterministic for no-args.
23993 #[test]
23994 fn bin_let_no_args_is_deterministic() {
23995 let _g = crate::test_util::global_state_lock();
23996 let o = empty_opts_for_corpus();
23997 let first = bin_let("let", &[], &o, 0);
23998 for _ in 0..3 {
23999 assert_eq!(
24000 bin_let("let", &[], &o, 0),
24001 first,
24002 "bin_let no-args must be deterministic"
24003 );
24004 }
24005 }
24006
24007 /// c:8444 — `bin_eval(&[])` empty args returns i32 (type pin).
24008 #[test]
24009 fn bin_eval_returns_i32_type() {
24010 let _g = crate::test_util::global_state_lock();
24011 let o = empty_opts_for_corpus();
24012 let _: i32 = bin_eval("eval", &[], &o, 0);
24013 }
24014
24015 /// c:6810 — `bin_true` ignores everything (args, opts, func).
24016 #[test]
24017 fn bin_true_full_argument_immunity() {
24018 let _g = crate::test_util::global_state_lock();
24019 let o = empty_opts_for_corpus();
24020 // Various permutations all return 0.
24021 assert_eq!(bin_true("true", &[], &o, 0), 0);
24022 assert_eq!(bin_true("anything", &[], &o, 0), 0);
24023 assert_eq!(bin_true("true", &["unused".into()], &o, 99), 0);
24024 assert_eq!(
24025 bin_true("", &["a".into(), "b".into(), "c".into()], &o, -1),
24026 0
24027 );
24028 }
24029
24030 /// c:6823 — `bin_false` ignores everything (args, opts, func).
24031 #[test]
24032 fn bin_false_full_argument_immunity() {
24033 let _g = crate::test_util::global_state_lock();
24034 let o = empty_opts_for_corpus();
24035 assert_eq!(bin_false("false", &[], &o, 0), 1);
24036 assert_eq!(bin_false("anything", &[], &o, 0), 1);
24037 assert_eq!(bin_false("false", &["unused".into()], &o, 99), 1);
24038 assert_eq!(
24039 bin_false("", &["a".into(), "b".into(), "c".into()], &o, -1),
24040 1
24041 );
24042 }
24043}