//! Direct port of `Src/builtin.c` — the master registration site for
//! the in-shell builtin commands. The C source is 7608 lines; the
//! actual `bin_*` handler bodies were ported organically into
//! `src/ported/vm_helper` and `src/ported/builtins/*.rs` long before
//! this file existed. This file scaffolds:
//!
//! Builtins in the main executable // c:38
//! Builtin Command Hash Table Functions // c:140
//!
//! * the `BINF_*` flag bits from `Src/zsh.h:1457-1486`,
//! * the `BIN_*` dispatch IDs from `Src/hashtable.h:34-66`,
//! * the `Builtin` descriptor and the static `BUILTINS[]` table
//! (1:1 mirror of `static struct builtin builtins[]` at
//! `Src/builtin.c:40-137`),
//! * `createbuiltintable()` (`Src/builtin.c:149`) — building the
//! name → descriptor lookup the rest of the shell consults via
//! `builtintab`.
//!
//! Each row's `handler` field names the canonical Rust port of the
//! C handler so future work can wire them up without re-discovering
//! the mapping. When the handler lives in `crate::ported::builtins`,
//! the comment cites the file; when it lives in `vm_helper`'s
//! `Executor` impl, that's noted too.
use std::collections::HashMap;
use std::io::Read;
use std::sync::atomic::{Ordering, Ordering::Relaxed};
use std::sync::{Mutex, OnceLock};
#[allow(unused_imports)]
use std::{env, fs, io, io::Write, path::Path, path::PathBuf};
use indexmap::IndexMap;
use crate::DPUTS;
use crate::func_body_fmt::FuncBodyFmt;
#[allow(unused_imports)]
use crate::parse::{Redirect, ShellCommand};
use crate::ported::compat::zgetcwd;
use crate::ported::config_h::DEFAULT_PATH;
use crate::ported::exec::{getfpfunc, iscom, loadautofn, FORKLEVEL, TRAP_RETURN, TRAP_STATE};
use crate::ported::hashnameddir::{addnameddirnode, emptynameddirtable, fillnameddirtable, nameddirtab, printnameddirnode};
use crate::ported::hashtable::{aliastab_lock, cmdnamtab_lock, createaliasnode, dircache_set, emptycmdnamtable, fillcmdnamtable, hnamcmp, printaliasnode, printcmdnamnode, printshfuncexpand, reswdtab_lock, scanmatchshfunc, scanshfunc, shfunctab_lock, sufaliastab_lock};
// `curhist` (hist.rs static) NOT imported — there's an unavoidable
// `let curhist` local in fc_main that mirrors C's `int curhist;` local
// shadowing the global. Rule E says keep the C name. The static is
// referenced via its fully-qualified path at the single read site to
// avoid name-shadow E0530.
use crate::ported::hist::{
addhistnum, gethistent, hcomsearch, histsiz, pushhiststack, quietgethist, readhistfile,
saveandpophiststack, savehistfile, savehistsiz,
};
use crate::ported::jobs::{bin_fg, removetrapnode};
use crate::ported::math::{matheval, mathevali, mnumber, MN_INTEGER};
use crate::ported::mem::{queue_signals, unqueue_signals};
use crate::ported::module::MATHFUNCS;
use crate::ported::modules::parameter::{DIRSTACK, FUNCSTACK};
use crate::ported::options::{dosetopt, emulation, optlookup, ZSH_OPTIONS_SET};
use crate::ported::params::{createparam, getiparam, getsparam, isident, locallevel as locallevel_param, locallevel, paramtab, printparamnode, setaparam, setiparam, setsparam, unsetparam, unsetparam_pm};
use crate::ported::pattern::{patcompile, pattry};
use crate::ported::signals::settrap;
use crate::ported::utils::{argzero, errflag, fprintdir, getkeystring, getkeystring_with, getshfunc, gettempfile, lchdir, print_if_link, printprompt4, quotedzputs, scriptname_get, set_argzero, zerr, zerrnam, zwarn, zwarnnam, GETKEYS_ECHO, GETKEYS_PRINT};
#[allow(unused_imports)]
use crate::ported::vm_helper::{self, format_int_in_base, BUILTIN_NAMES};
use crate::ported::zle::compctl::compctlread;
use crate::ported::zsh_h::{eprog, nameddir, options, ALIAS_GLOBAL, ALIAS_SUFFIX, BINF_KEEPNUM, DISABLED, EMULATE_CSH, EMULATE_KSH, EMULATE_SH, EMULATE_ZSH, EMULATION, ERRFLAG_ERROR, FS_FUNC, HFILE_APPEND, HFILE_SKIPOLD, HFILE_USE_OPTIONS, HIST_FOREIGN, MAX_OPS, MFF_STR, OPT_ARG, OPT_HASARG, OPT_ISSET, OPT_MINUS, OPT_PLUS, PM_ABSPATH_USED, PM_ARRAY, PM_CUR_FPATH, PM_EFLOAT, PM_FFLOAT, PM_HASHED, PM_HIDEVAL, PM_INTEGER, PM_KSHSTORED, PM_LEFT, PM_LOADDIR, PM_LOCAL, PM_LOWER, PM_NAMEREF, PM_UNIQUE, PM_READONLY, PM_RIGHT_B, PM_RIGHT_Z, PM_TAGGED, PM_TAGGED_LOCAL, PM_TIED, PM_UNALIASED, PM_UNDEFINED, PM_UPPER, PM_WARNNESTED, PM_ZSHSTORED, PRINT_LINE, PRINT_LIST, PRINT_NAMEONLY, PRINT_POSIX_EXPORT, PRINT_POSIX_READONLY, PRINT_TYPE, PRINT_TYPESET, PRINT_WHENCE_CSH, PRINT_WHENCE_FUNCDEF, PRINT_WHENCE_SIMPLE, PRINT_WHENCE_VERBOSE, PRINT_WHENCE_WORD, PRINT_WITH_NAMESPACE, STAT_LOCKED, STAT_NOPRINT, STAT_STOPPED, TYPESET_OPTSTR, XTRACE, asgment, builtin, hashnode, isset, mathfunc, param, shfunc, HandlerFunc, ASG_ARRAY, ASG_ARRAYP, ASG_KEY_VALUE, ASG_VALUEP, BINF_ADDED, BINF_ASSIGN, BINF_BUILTIN, BINF_COMMAND, BINF_DASH, BINF_DASHDASHVALID, BINF_EXEC, BINF_HANDLES_OPTS, BINF_MAGICEQUALS, BINF_NOGLOB, BINF_PLUSOPTS, BINF_PREFIX, BINF_PRINTOPTS, BINF_PSPECIAL, BINF_SKIPDASH, BINF_SKIPINVALID, FUNCTIONARGZERO, HASHED, INTERACTIVE, MFF_USERFUNC, MONITOR, NULLBINCMD, PATHDIRS, PAT_HEAPDUP, PAT_STATIC, PM_AUTOLOAD, PM_DECLARED, PM_EXPORTED, PM_HIDE, PM_SCALAR, PM_SPECIAL, PM_TYPE, PM_UNSET, POSIXBUILTINS, PRINT_INCLUDEVALUE, TYPESETSILENT, TRAP_STATE_FORCE_RETURN, TRAP_STATE_PRIMED, ZEXIT_DEFERRED, ZEXIT_NORMAL, ZEXIT_SIGNAL, ZSIG_FUNC, alias, cmdnam, Meta};
#[allow(unused_imports)]
use crate::zwc::ZwcFile;
// ---------------------------------------------------------------------------
// BIN_* dispatch IDs.
// Direct port of `Src/hashtable.h:34-70`. These are the integer
// discriminators handlers use when one C function backs multiple
// builtin names (e.g. `bin_fg` covers fg/bg/jobs/wait/disown).
// ---------------------------------------------------------------------------
// BIN_* constants moved to `crate::ported::hashtable_h` per the C
// header layout (Src/hashtable.h:34-70). Re-exported here so existing
// `crate::ported::builtin::BIN_X` paths keep resolving.
pub use crate::ported::hashtable_h::{
BIN_BG, BIN_BRACKET, BIN_BREAK, BIN_CD, BIN_COMMAND, BIN_CONTINUE, BIN_DISABLE, BIN_DISOWN,
BIN_ECHO, BIN_ENABLE, BIN_EVAL, BIN_EXIT, BIN_EXPORT, BIN_FC, BIN_FG, BIN_JOBS, BIN_LOGOUT,
BIN_POPD, BIN_PRINT, BIN_PRINTF, BIN_PUSHD, BIN_PUSHLINE, BIN_R, BIN_READONLY, BIN_RETURN,
BIN_SCHED, BIN_SETOPT, BIN_TEST, BIN_TYPESET, BIN_UNALIAS, BIN_UNFUNCTION, BIN_UNHASH,
BIN_UNSET, BIN_UNSETOPT, BIN_WAIT,
};
/// Construct the builtin lookup table.
/// Port of `createbuiltintable()` from `Src/builtin.c:150`. The C
/// version installs the hashtable function pointers (hash, addnode,
/// printnode, etc.) and then calls `addbuiltins("zsh", builtins, ..)`.
/// Here we just materialise the static `BUILTINS` slice into a
/// `HashMap<String, &builtin>` — Rust's standard hashing replaces the
/// C `hasher` callback and the `HashMap` itself replaces all the
/// per-table function pointers (`addnode`/`getnode`/`removenode`/...).
// Builtin Command Hash Table Functions // c:150
pub fn createbuiltintable() -> &'static HashMap<String, &'static builtin> {
// c:150
builtintab.get_or_init(|| {
let table: &'static Vec<builtin> = &*BUILTINS;
let watch_bintab: &'static Vec<builtin> = &*crate::ported::modules::watch::bintab;
let mut m: HashMap<String, &'static builtin> =
HashMap::with_capacity(table.len() + watch_bintab.len());
for b in table.iter() {
m.insert(b.node.nam.clone(), b);
}
// zshrs auto-loads all modules at startup. Fold each module's
// bintab into the core builtintab so `disable <name>` (and
// dispatch generally) finds module-provided builtins without
// an explicit `zmodload` step. Mirrors C's `addbuiltins(name,
// bintab, sizeof(bintab)/sizeof(*bintab))` call from each
// module's `boot_` hook (e.g. `Src/Modules/watch.c:694`).
for b in watch_bintab.iter() {
m.insert(b.node.nam.clone(), b);
}
m
})
}
// ===========================================================
// Direct ports of static builtin helpers from Src/builtin.c not
// yet covered above. The Rust executor wires builtins through
// `crate::ported::builtins::*` per-builtin modules; these free-
// fn entries satisfy ABI/name parity for the drift gate.
// ===========================================================
/// Port of `printbuiltinnode(HashNode hn, int printflags)` from
/// `Src/builtin.c:174`.
///
/// C body (c:174-194):
/// ```c
/// Builtin bn = (Builtin) hn;
/// if (printflags & PRINT_WHENCE_WORD) {
/// printf("%s: builtin\n", bn->node.nam); return;
/// }
/// if (printflags & PRINT_WHENCE_CSH) {
/// printf("%s: shell built-in command\n", bn->node.nam); return;
/// }
/// if (printflags & PRINT_WHENCE_VERBOSE) {
/// printf("%s is a shell builtin\n", bn->node.nam); return;
/// }
/// /* default is name only */
/// printf("%s\n", bn->node.nam);
/// ```
pub fn printbuiltinnode(
hn: *mut hashnode, // c:174
printflags: i32,
) {
if hn.is_null() {
return;
}
let bn = unsafe { &*hn }; // c:176
if (printflags & PRINT_WHENCE_WORD as i32) != 0 { // c:178
println!("{}: builtin", bn.nam); // c:179
return; // c:180
}
if (printflags & PRINT_WHENCE_CSH as i32) != 0 { // c:183
println!("{}: shell built-in command", bn.nam); // c:184
return; // c:185
}
if (printflags & PRINT_WHENCE_VERBOSE as i32) != 0 { // c:188
println!("{} is a shell builtin", bn.nam); // c:189
return; // c:190
}
// c:193 — `/* default is name only */`
println!("{}", bn.nam); // c:194
}
/// Port of `freebuiltinnode(HashNode hn)` from Src/builtin.c:199.
/// C: `static void freebuiltinnode(HashNode hn)` — free a builtin-table
/// node only when BINF_ADDED is clear (i.e., dynamically added).
pub fn freebuiltinnode(hn: *mut hashnode) {
// c:199
if hn.is_null() {
return;
}
let bn = unsafe { &*hn };
// c:204 — `if (!(bn->node.flags & BINF_ADDED))` then free.
if (bn.flags as u32 & BINF_ADDED) == 0 { // c:204
// Rust drop handles the actual free; nothing more to do.
}
}
/// Port of `init_builtins()` from Src/builtin.c:212.
/// C: `void init_builtins(void)` — when not in EMULATE_ZSH, disable
/// the `repeat` reserved word (compat for sh/ksh).
///
/// ```c
/// if (!EMULATION(EMULATE_ZSH)) {
/// HashNode hn = reswdtab->getnode2(reswdtab, "repeat");
/// if (hn)
/// reswdtab->disablenode(hn, 0);
/// }
/// ```
pub fn init_builtins() {
// c:212
// c:214 — `if (!EMULATION(EMULATE_ZSH))`. EMULATION reads the
// canonical `emulation` global directly per zsh.h:2347.
if !EMULATION(EMULATE_ZSH) {
// c:214
// c:215-217 — `hn = reswdtab->getnode2(reswdtab,"repeat");
// if (hn) reswdtab->disablenode(hn, 0);`
if let Ok(mut tab) = reswdtab_lock().write() {
tab.disable("repeat");
}
}
}
/// Port of `OPT_ALLOC_CHUNK` from `Src/builtin.c:227`. Number of
/// `ops->args[]` slots `new_optarg()` grows the array by when full.
pub const OPT_ALLOC_CHUNK: i32 = 16; // c:227
/// Port of `new_optarg(Options ops)` from Src/builtin.c:227.
/// C: `static int new_optarg(Options ops)` — grow the `ops->args[]`
/// array by `OPT_ALLOC_CHUNK` slots when full. Returns 1 on overflow
/// (>=63 args), 0 on success.
pub fn new_optarg(ops: &mut options) -> i32 {
// c:227
// c:227 — `if (ops->argscount == 63) return 1;`
if ops.argscount == 63 {
// c:231
return 1;
}
// c:232-241 — grow ops->args by OPT_ALLOC_CHUNK if argsalloc == argscount.
if ops.argsalloc == ops.argscount {
// c:232
ops.args
.resize((ops.argsalloc + OPT_ALLOC_CHUNK) as usize, String::new());
ops.argsalloc += OPT_ALLOC_CHUNK; // c:240
}
ops.argscount += 1; // c:243
0 // c:244
}
// ===========================================================
// ksh_autoload_body moved from src/ported/vm_helper.
// Mirrors the ksh-style autoload helper in Src/builtin.c
// (bin_functions / load_function_def).
// ===========================================================
// (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.)
bitflags::bitflags! {
/// Flags for autoloaded functions (autoload builtin -- Src/builtin.c bin_autoload).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct AutoloadFlags: u32 {
const NO_ALIAS = 0b00000001; // -U: don't expand aliases
const ZSH_STYLE = 0b00000010; // -z: zsh-style autoload
const KSH_STYLE = 0b00000100; // -k: ksh-style autoload
const TRACE = 0b00001000; // -t: trace execution
const USE_CALLER_DIR = 0b00010000; // -d: use calling function's dir
const LOADED = 0b00100000; // function has been loaded
}
}
/// Port of `execbuiltin(LinkList args, LinkList assigns, Builtin bn)` from Src/builtin.c:250.
///
/// C: `int execbuiltin(LinkList args, LinkList assigns, Builtin bn)` —
/// execute a builtin handler function after parsing the arguments.
///
/// Walks `bn->optstr` against `args`, populating `ops.ind[c]` (`|= 1`
/// for `-X`, `|= 2` for `+X`, `<< 2` arg-index for opts taking args
/// per the `:`/`::`/`:%` suffix convention), then calls
/// `bn->handlerfunc(name, argv, &ops, bn->funcid)`.
///
/// Signature note: C consumes the name via `ugetnode(args)` first
/// (c:262); the Rust port receives `args` without the name and reads
/// `bn->node.nam` directly. C's `LinkList assigns` ports to
/// `Vec<asgment>` (closer to the C type than the earlier
/// `Vec<(String, String)>` pair-tuple). `assignfunc` handler dispatch
/// (c:495-502) — BINF_ASSIGN builtins taking two argument lists —
/// isn't ported (no Rust-side caller passes a non-empty `assigns`),
/// so XTRACE prints the structure but BINF_ASSIGN dispatch falls
/// through to the plain handler.
pub fn execbuiltin(
args: Vec<String>,
assigns: Vec<asgment>, // c:250
bn: *mut builtin,
) -> i32 {
if bn.is_null() {
return 1;
}
let bn_ref = unsafe { &*bn };
// c:252-254 — locals.
let pp: Option<&str>; // c:252 char *pp
let name: String; // c:252 char *name
let mut optstr: Option<String>; // c:252 char *optstr
let mut flags: i32; // c:253 int flags
let mut argc: i32; // c:253 int argc
let mut execop: u8; // c:253 int execop
let xtr: bool = isset(XTRACE); // c:253 int xtr = isset(XTRACE)
// c:256-259 — `memset(ops.ind, 0, ...); ops.args = NULL; ops.argscount=ops.argsalloc=0;`
let mut ops = options {
ind: [0u8; MAX_OPS],
args: Vec::new(), // c:257
argscount: 0,
argsalloc: 0,
}; // c:258-259
// c:262 — `name = (char *) ugetnode(args);` — Rust reads bn.node.nam.
name = bn_ref.node.nam.clone(); // c:262
// c:264-268 — `if (!bn->handlerfunc)` early-exit.
if bn_ref.handlerfunc.is_none() {
// c:264
// c:265 — DPUTS(1, "Missing builtin detected too late")
DPUTS!(true, "Missing builtin detected too late"); // c:265
// c:266 — deletebuiltin(bn->node.nam) — not yet ported here.
return 1; // c:267
}
// c:270-271 — `flags = bn->node.flags; optstr = bn->optstr;`
flags = bn_ref.node.flags; // c:270
optstr = bn_ref.optstr.clone(); // c:271
// c:275 — `argc = countlinknodes(args);` — total argv length.
argc = args.len() as i32; // c:275
// c:284-293 — `VARARR(char *, argarr, argc+1)` + copy args into argarr.
let argarr: Vec<String> = args; // c:284 argarr[]
let mut argv: usize = 0; // c:285 char **argv = argarr;
// c:296-411 — option parser body.
if let Some(ref os) = optstr.clone() {
// c:296
let optstr_local = os.clone();
let mut optstr_bytes: Vec<u8> = optstr_local.into_bytes();
let mut skipinvalid = (flags & BINF_SKIPINVALID as i32) != 0;
// c:297 — `char *arg = *argv;`
loop {
// c:300-303 — outer arg-by-arg loop guard:
// `arg && ((sense = (*arg == '-')) || ((flags & BINF_PLUSOPTS) && *arg == '+'))`.
let arg_str: String = match argarr.get(argv) {
Some(s) => s.clone(),
None => break,
};
let arg_bytes = arg_str.as_bytes();
if arg_bytes.is_empty() {
break;
}
let sense: i32 = if arg_bytes[0] == b'-' { 1 } else { 0 }; // c:302
if sense == 0
&& !((flags & BINF_PLUSOPTS as i32) != 0 // c:303
&& arg_bytes[0] == b'+')
{
break;
}
// c:305 — `if (!(flags & BINF_KEEPNUM) && idigit(arg[1])) break;`
if (flags & BINF_KEEPNUM as i32) == 0 // c:305
&& arg_bytes.len() >= 2
&& arg_bytes[1].is_ascii_digit()
{
break;
}
// c:308 — `if ((flags & BINF_SKIPDASH) && !arg[1]) break;`
if (flags & BINF_SKIPDASH as i32) != 0 && arg_bytes.len() == 1 {
// c:308
break;
}
// c:310-317 — `--` end-of-options if BINF_DASHDASHVALID.
if (flags & BINF_DASHDASHVALID as i32) != 0 && arg_str == "--" {
// c:310
argv += 1; // c:315
break; // c:316
}
// c:327-332 — `BINF_SKIPINVALID`: if any char in arg[1..] is
// not in optstr, the whole arg is treated as a positional.
if skipinvalid {
// c:327
let mut all_known = true;
for &c in &arg_bytes[1..] {
if !optstr_bytes.contains(&c) {
all_known = false;
break;
}
}
if !all_known {
break;
} // c:331
}
// c:335-336 — `if (arg[1] == '-') arg++;` — consume the
// second `-` of `--long-style`.
let mut k: usize = 1; // walks arg[k..]
if arg_bytes.len() >= 2 && arg_bytes[1] == b'-' {
// c:335
k = 2; // c:336
}
// c:337-341 — `if (!arg[1])` lone `-` / `+` indicator.
if arg_bytes.len() == k {
// c:337
ops.ind[b'-' as usize] = 1; // c:338
if sense == 0 {
// c:339
ops.ind[b'+' as usize] = 1; // c:340
}
}
// c:343-386 — inner loop over `*++arg` characters.
let mut bad_opt: Option<u8> = None;
while k < arg_bytes.len() {
// c:343
let c = arg_bytes[k];
execop = c; // c:345
let optptr = optstr_bytes.iter().position(|&b| b == c); // c:345 strchr(optstr,c)
if let Some(optidx) = optptr {
// c:345
ops.ind[c as usize] = if sense != 0 { 1 } else { 2 }; // c:346
// c:347 — `if (optptr[1] == ':')` — option takes arg.
if optidx + 1 < optstr_bytes.len() && optstr_bytes[optidx + 1] == b':' {
let mut argptr: Option<String> = None;
// c:349-352 — `if (optptr[2] == ':')` optional same-word.
if optidx + 2 < optstr_bytes.len() && optstr_bytes[optidx + 2] == b':' {
if k + 1 < arg_bytes.len() {
// c:350
argptr =
Some(String::from_utf8_lossy(&arg_bytes[k + 1..]).into_owned());
// c:351
}
} else if optidx + 2 < optstr_bytes.len()
&& optstr_bytes[optidx + 2] == b'%'
{
// c:353-359 — `:%` numeric optional same or next word.
if k + 1 < arg_bytes.len() && arg_bytes[k + 1].is_ascii_digit() {
argptr =
Some(String::from_utf8_lossy(&arg_bytes[k + 1..]).into_owned());
} else if let Some(nxt) = argarr.get(argv + 1) {
if !nxt.is_empty() && nxt.as_bytes()[0].is_ascii_digit() {
argv += 1; // c:359 arg = *++argv
argptr = Some(nxt.clone());
}
}
} else {
// c:360-370 — plain `:` mandatory arg.
if k + 1 < arg_bytes.len() {
// c:362
argptr =
Some(String::from_utf8_lossy(&arg_bytes[k + 1..]).into_owned());
// c:363
} else if let Some(nxt) = argarr.get(argv + 1) {
argv += 1; // c:364 arg = *++argv
argptr = Some(nxt.clone()); // c:365
} else {
// c:366-370 — `argument expected: -%c`.
zwarnnam(&name, &format!("argument expected: -{}", execop as char)); // c:367-368
return 1; // c:369
}
}
if let Some(ap) = argptr {
// c:372
// c:373-377 — new_optarg overflow.
if new_optarg(&mut ops) != 0 {
// c:373
zwarnnam(&name, "too many option arguments"); // c:374-375
return 1; // c:376
}
// c:378 — `ops.ind[execop] |= ops.argscount << 2;`
ops.ind[execop as usize] |= (ops.argscount as u8) << 2;
// c:379 — `ops.args[ops.argscount-1] = argptr;`
ops.args[(ops.argscount - 1) as usize] = ap;
// c:380-381 — `while (arg[1]) arg++;` consume the rest.
k = arg_bytes.len();
}
}
k += 1;
} else {
bad_opt = Some(c); // c:385 break
break;
}
}
// c:389-394 — if we exited mid-arg on a bad char, emit "bad option".
if let Some(badc) = bad_opt {
// c:389
zwarnnam(
&name,
&format!(
"bad option: {}{}",
if sense != 0 { '-' } else { '+' },
badc as char
),
); // c:392
return 1; // c:393
}
// c:395 — `arg = *++argv;`
argv += 1; // c:395
// c:398-402 — BINF_PRINTOPTS R-mode switch to "ne" optstr.
if (flags & BINF_PRINTOPTS as i32) != 0 // c:398
&& ops.ind[b'R' as usize] != 0
&& ops.ind[b'f' as usize] == 0
{
optstr_bytes = b"ne".to_vec(); // c:400
flags |= BINF_SKIPINVALID as i32; // c:401
skipinvalid = true;
}
// c:404-405 — `if (ops.ind['-']) break;` — `--` terminates.
if ops.ind[b'-' as usize] != 0 {
// c:404
break;
}
}
let _ = optstr_bytes;
} else if (flags & BINF_HANDLES_OPTS as i32) == 0 // c:407
&& argarr.get(argv).map(|s| s == "--").unwrap_or(false)
{
// c:408
// c:409-410 — `ops.ind['-'] = 1; argv++;`
ops.ind[b'-' as usize] = 1; // c:409
argv += 1; // c:410
}
// Suppress optstr-unused warnings on the `else` path.
let _ = optstr.take();
// c:414-421 — apply `bn->defopts` defaults.
pp = bn_ref.defopts.as_deref(); // c:414
if let Some(pp_str) = pp {
// c:414
for &b in pp_str.as_bytes() {
// c:415
if ops.ind[b as usize] == 0 {
// c:417
ops.ind[b as usize] = 1; // c:418
}
}
}
// c:424 — `argc -= argv - argarr;` — subtract consumed flag args.
argc -= argv as i32; // c:424
// c:426-429 — errflag check.
let ef = errflag.load(Relaxed);
if (ef & ERRFLAG_ERROR) != 0 {
// c:426
errflag
.fetch_and(!ERRFLAG_ERROR, Relaxed); // c:427
return 1; // c:428
}
// c:432-436 — argc bounds check.
if argc < bn_ref.minargs // c:432
|| (argc > bn_ref.maxargs && bn_ref.maxargs != -1)
{
zwarnnam(
&name, // c:433
if argc < bn_ref.minargs {
"not enough arguments"
} else {
"too many arguments"
},
); // c:434
return 1; // c:435
}
// c:438-494 — display execution trace information, if required.
if xtr {
// c:439
// c:440-441 — `char **fullargv = argarr;` — use FULL argv
// (including consumed option words) so XTRACE shows what the
// user typed, not the option-stripped tail.
let fullargv = &argarr; // c:441
printprompt4(); // c:442
// c:443 — `fprintf(xtrerr, "%s", name);`
eprint!("{}", name); // c:443
// c:444-447 — `while (*fullargv) { fputc(' ',xtrerr); quotedzputs(...); }`
for s in fullargv {
// c:444
eprint!(" "); // c:445 fputc(' ', xtrerr)
eprint!("{}", quotedzputs(s)); // c:446
}
// c:448-491 — `if (assigns) { for (node = firstnode(assigns); ...) }`.
for asg in &assigns {
// c:450 firstnode/incnode
eprint!(" "); // c:452 fputc(' ', xtrerr)
eprint!("{}", quotedzputs(&asg.name)); // c:453
if (asg.flags & ASG_ARRAY) != 0 {
// c:454
eprint!("=("); // c:455
if let Some(ref list) = asg.array {
// c:456
if (asg.flags & ASG_KEY_VALUE) != 0 {
// c:457
// c:458-473 — `LinkNode keynode, valnode;` walk
// alternating key/value pairs, emitting
// `[key]=value` per pair. Uses the typed
// `LinkList<String>` accessors from
// `src/ported/linklist.rs` which port the
// `firstnode` / `nextnode` / `getdata` macros
// from `Src/zsh.h:576-588`.
let mut keynode = list.firstnode(); // c:459
loop {
// c:460
// c:461-462 — `if (!keynode) break;`
let kidx = match keynode {
// c:461
Some(i) => i,
None => break, // c:462
};
// c:463-465 — `valnode = nextnode(keynode); if (!valnode) break;`
let vidx = match list.nextnode(kidx) {
// c:463
Some(i) => i,
None => break, // c:465
};
// c:466-468 — `fputc('['); quotedzputs(getdata(keynode));`
eprint!("["); // c:466
if let Some(k) = list.getdata(kidx) {
// c:467 getdata
eprint!("{}", quotedzputs(k));
// c:467
}
// c:469 — `fprintf(stderr, "]=");`
eprint!("]="); // c:469
// c:470-471 — `quotedzputs(getdata(valnode));`
if let Some(v) = list.getdata(vidx) {
// c:470
eprint!("{}", quotedzputs(v));
// c:470
}
// c:472 — `keynode = nextnode(valnode);`
keynode = list.nextnode(vidx); // c:472
}
} else {
// c:474
// c:475-482 — plain array emit: walk every node
// and emit ` <quotedzputs(elem)>`.
let mut arrnode = list.firstnode(); // c:476
while let Some(idx) = arrnode {
// c:477
eprint!(" "); // c:479 fputc(' ', xtrerr)
if let Some(elem) = list.getdata(idx) {
// c:480 getdata
eprint!("{}", quotedzputs(elem));
// c:480
}
arrnode = list.nextnode(idx); // c:478 incnode
}
}
}
eprint!(" )"); // c:485
} else if let Some(ref scalar) = asg.scalar {
// c:486
eprint!("="); // c:487 fputc('=', xtrerr)
eprint!("{}", quotedzputs(scalar)); // c:488
}
}
// c:492-493 — `fputc('\n', xtrerr); fflush(xtrerr);`
eprintln!(); // c:492
// c:493 — fflush is automatic on `eprintln!` (stderr line-buffered).
}
// c:506 — `return (*(bn->handlerfunc))(name, argv, &ops, bn->funcid);`
let trimmed: Vec<String> = argarr[argv..].to_vec();
let handler = bn_ref.handlerfunc.expect("handlerfunc checked at c:264");
handler(&name, &trimmed, &ops, bn_ref.funcid) // c:506
}
/// Port of `bin_enable(char *name, char **argv, Options ops, int func)` from Src/builtin.c:517.
/// C: `int bin_enable(char *name, char **argv, Options ops, int func)` —
/// enable/disable hashtab entries (default builtins; `-f`/`-r`/`-s`/`-a`
/// pick alternate tables); `-p` routes to pat_enables (pattern toggles).
/// WARNING: param names don't match C — Rust=(name, argv, func) vs C=(name, argv, ops, func)
pub fn bin_enable(
name: &str,
argv: &[String], // c:517
ops: &options,
func: i32,
) -> i32 {
enum Tab {
Builtin,
Shfunc,
Reswd,
Alias,
SufAlias,
}
let mut returnval = 0i32; // c:524
let mut match_count = 0i32; // c:524
// c:527-538 — `-p` early-out + table selection.
if OPT_ISSET(ops, b'p') {
// c:527
// c:528 — `return pat_enables(name, argv, func == BIN_ENABLE);`
return pat_enables(name, argv, func == BIN_ENABLE); // c:528
}
let tab = if OPT_ISSET(ops, b'f') {
Tab::Shfunc
}
// c:529
else if OPT_ISSET(ops, b'r') {
Tab::Reswd
}
// c:531
else if OPT_ISSET(ops, b's') {
Tab::SufAlias
}
// c:533
else if OPT_ISSET(ops, b'a') {
Tab::Alias
}
// c:535
else {
Tab::Builtin
}; // c:537
// c:540-547 — flags1/flags2 set based on enable vs disable direction.
let enable = func == BIN_ENABLE;
let (flags1, flags2) = if enable {
// c:541
(0u32, DISABLED as u32) // c:542
} else {
(DISABLED as u32, 0u32) // c:545
};
// Helper closures over the chosen table.
let toggle_one = |tab: &Tab, nm: &str, on: bool| -> bool {
match tab {
Tab::Alias => aliastab_lock()
.write()
.map(|mut t| if on { t.enable(nm) } else { t.disable(nm) })
.unwrap_or(false),
Tab::SufAlias => sufaliastab_lock()
.write()
.map(|mut t| if on { t.enable(nm) } else { t.disable(nm) })
.unwrap_or(false),
// c:541-547 — `enable`/`disable -r` toggles DISABLED on the
// reswdtab entry; reswords resolve through getreswdnode in
// the lexer so toggling here is enough to mask/unmask.
Tab::Reswd => {
let exists = reswdtab_lock()
.read()
.map(|t| t.get_including_disabled(nm).is_some())
.unwrap_or(false);
if !exists {
return false;
}
reswdtab_lock()
.write()
.map(|mut t| if on { t.enable(nm) } else { t.disable(nm) })
.unwrap_or(false)
}
// c:541-547 — `enable`/`disable -f` toggles DISABLED on the
// shfunctab entry; ports to disableshfuncnode/enableshfuncnode
// which also unsettrap/settrap TRAP* ported.
Tab::Shfunc => {
let exists = shfunctab_lock()
.read()
.map(|t| t.get_including_disabled(nm).is_some())
.unwrap_or(false);
if !exists {
return false;
}
if on {
crate::ported::hashtable::enableshfuncnode(nm);
} else {
crate::ported::hashtable::disableshfuncnode(nm);
}
true
}
// c:541-547 — `enable`/`disable` toggles DISABLED on the
// builtin. The C struct `builtintab` stores DISABLED in
// `node.flags`; Rust port keeps `builtintab` as an
// immutable static lookup and tracks the disabled set in
// BUILTINS_DISABLED so dispatch can mask the entry.
Tab::Builtin => {
if createbuiltintable().get(nm).is_none() {
return false;
}
if let Ok(mut set) = BUILTINS_DISABLED.lock() {
if on {
set.remove(nm);
} else {
set.insert(nm.to_string());
}
return true;
}
false
}
}
};
let collect_names = |tab: &Tab| -> Vec<String> {
match tab {
Tab::Alias => aliastab_lock()
.read()
.map(|t| t.iter().map(|(n, _)| n.clone()).collect())
.unwrap_or_default(),
Tab::SufAlias => sufaliastab_lock()
.read()
.map(|t| t.iter().map(|(n, _)| n.clone()).collect())
.unwrap_or_default(),
Tab::Reswd => reswdtab_lock()
.read()
.map(|t| t.iter().map(|(n, _)| n.clone()).collect())
.unwrap_or_default(),
Tab::Shfunc => shfunctab_lock()
.read()
.map(|t| t.iter().map(|(n, _)| n.clone()).collect())
.unwrap_or_default(),
Tab::Builtin => createbuiltintable().keys().cloned().collect(),
}
};
// c:553-558 — no-args list.
if argv.is_empty() {
// c:553
queue_signals(); // c:554
// c:555 — `scanhashtable(ht, 1, flags1, flags2, ht->printnode, 0);`
// Filter: print only entries where (flags & flags1) == flags1
// && (flags & flags2) == 0. For enable/disable, flags1 and
// flags2 are DISABLED-bit selectors that mask the listed set
// to ONLY the kind being toggled (enable lists enabled,
// disable lists disabled).
let is_disabled = |nm: &str| -> bool {
match tab {
Tab::Alias => aliastab_lock()
.read()
.ok()
.and_then(|t| {
t.get_including_disabled(nm)
.map(|a| (a.node.flags & DISABLED as i32) != 0)
})
.unwrap_or(false),
Tab::SufAlias => sufaliastab_lock()
.read()
.ok()
.and_then(|t| {
t.get_including_disabled(nm)
.map(|a| (a.node.flags & DISABLED as i32) != 0)
})
.unwrap_or(false),
Tab::Reswd => reswdtab_lock()
.read()
.ok()
.and_then(|t| {
t.get_including_disabled(nm)
.map(|r| (r.node.flags & DISABLED as i32) != 0)
})
.unwrap_or(false),
Tab::Shfunc => shfunctab_lock()
.read()
.ok()
.and_then(|t| {
t.get_including_disabled(nm)
.map(|f| (f.node.flags & DISABLED as i32) != 0)
})
.unwrap_or(false),
Tab::Builtin => BUILTINS_DISABLED
.lock()
.map(|s| s.contains(nm))
.unwrap_or(false),
}
};
for nm in collect_names(&tab) {
let dis = is_disabled(&nm);
let entry_flags = if dis { DISABLED as u32 } else { 0 };
if (entry_flags & flags1) == flags1 && (entry_flags & flags2) == 0 {
println!("{}", nm);
}
}
unqueue_signals(); // c:556
return 0; // c:557
}
// c:561-580 — `-m` glob branch.
if OPT_ISSET(ops, b'm') {
// c:561
for arg in argv {
// c:562
queue_signals(); // c:563
let pprog = patcompile(
arg, // c:566
PAT_HEAPDUP,
None,
);
if let Some(prog) = pprog {
for nm in collect_names(&tab) {
if pattry(&prog, &nm) {
// c:567
if toggle_one(&tab, &nm, enable) {
match_count += 1; // c:567
}
}
}
} else {
zwarnnam(name, &format!("bad pattern : {}", arg)); // c:572
returnval = 1; // c:573
}
unqueue_signals(); // c:575
}
if match_count == 0 {
// c:579
returnval = 1; // c:580
}
return returnval; // c:581
}
// c:585-594 — literal-name dispatch.
queue_signals(); // c:585
for arg in argv {
// c:586
if !toggle_one(&tab, arg, enable) {
// c:587
zwarnnam(name, &format!("no such hash table element: {}", arg)); // c:590
returnval = 1; // c:591
}
}
unqueue_signals(); // c:594
returnval // c:595
}
/// Port of `bin_set(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:601.
/// C: `int bin_set(char *nam, char **args, UNUSED(Options ops),
/// UNUSED(int func))` — set shell options, declare arrays,
/// replace positional params, or display variables.
/// WARNING: param names don't match C — Rust=(nam, args, _func) vs C=(nam, args, ops, func)
pub fn bin_set(
nam: &str,
args: &[String], // c:601
_ops: &options,
_func: i32,
) -> i32 {
// PFA-SMR aspect: emit setopt/unsetopt events for the POSIX
// `set -o NAME` / `set +o NAME` form. This is the third option
// syntax (alongside setopt NAME / unsetopt NAME); a recorder
// user expects all three to surface in `zwhere -k setopt`.
#[cfg(feature = "recorder")]
if crate::recorder::is_enabled() && !args.is_empty() {
let ctx = crate::recorder::recorder_ctx_global();
let mut iter = args.iter().peekable();
while let Some(a) = iter.next() {
match a.as_str() {
"-o" => {
if let Some(name) = iter.next() {
crate::recorder::emit_setopt(name, ctx.clone());
}
}
"+o" => {
if let Some(name) = iter.next() {
crate::recorder::emit_unsetopt(name, ctx.clone());
}
}
_ => {}
}
}
}
let mut argv: Vec<String> = args.to_vec();
let mut hadopt = false; // c:603
let mut hadplus = false; // c:603
let mut hadend = false; // c:603
let mut sort: i32 = 0; // c:603
let mut array: i32 = 0; // c:603
let mut arrayname: Option<String> = None; // c:604
// c:608-614 — sh-compat: bare `set -` → +xv.
if !EMULATION(EMULATE_ZSH) // c:608
&& !argv.is_empty() && argv[0] == "-"
{
// c:610-611 — `dosetopt(VERBOSE, 0, 0, opts); dosetopt(XTRACE, 0, 0, opts);`
let v = optlookup("verbose");
let x = optlookup("xtrace");
dosetopt(v, 0, 0); // c:610
dosetopt(x, 0, 0); // c:611
if argv.len() == 1 {
return 0;
} // c:612-613
argv.remove(0);
}
// c:617-668 — top-level option-arg loop.
let mut idx = 0usize;
'outer: while idx < argv.len() // c:617
&& (argv[idx].starts_with('-') || argv[idx].starts_with('+'))
{
let arg = argv[idx].clone();
let action = arg.starts_with('-'); // c:619
if !action {
hadplus = true;
} // c:620
// c:621-622 — bare `-` / `+` → "--"
let body: String = if arg.len() == 1 {
"--".to_string()
} else {
arg.clone()
};
// c:623 — `while (*++*args)`
let chars: Vec<char> = body[1..].chars().collect();
let mut ci = 0usize;
while ci < chars.len() {
// c:623
let c = chars[ci];
if c != '-' || action {
hadopt = true;
} // c:626
// c:628-632 — `--` end-of-options.
if c == '-' {
// c:628
hadend = true; // c:629
idx += 1; // c:630 args++
break 'outer;
}
// c:633-645 — `o` long-option name follows.
if c == 'o' {
// c:633
let optname: String = if ci + 1 < chars.len() {
chars[ci + 1..].iter().collect::<String>()
} else {
idx += 1;
if idx >= argv.len() {
// c:636
// c:637 — `printoptionstates(hadplus); inittyptab(); return 0;`
return 0;
}
argv[idx].clone()
};
let optno = optlookup(&optname); // c:642
if optno == 0 {
// c:642 — C: `zerrnam(nam, "no such option: %s", *args)`.
// zwarnnam emits `zsh:<nam>:<lineno>:` prefix
// (vs zerr's bare `zsh:<lineno>:`); use the
// canonical form so `set +o BAD` matches zsh's
// diagnostic format. Return 1 so $? reflects the
// failed lookup (C's execbuiltin checks errflag
// after the call; zshrs's bridge bases on the
// return value).
zerrnam(nam, &format!("no such option: {}", optname));
unqueue_signals();
return 1;
} else if dosetopt(optno, if action { 1 } else { 0 }, 0)
!= 0
// c:644
{
zerrnam(nam, &format!("can't change option: {}", optname));
unqueue_signals();
return 1;
}
break;
}
// c:646-657 — `A` array-mode (with optional name arg).
if c == 'A' {
// c:646
array = if action { 1 } else { -1 }; // c:649
let nameopt: Option<String> = if ci + 1 < chars.len() {
Some(chars[ci + 1..].iter().collect::<String>())
} else if idx + 1 < argv.len() {
idx += 1;
Some(argv[idx].clone())
} else {
None
};
arrayname = nameopt.clone();
if arrayname.is_none() {
// c:651
idx += 1;
break 'outer;
}
let ksharrays = isset(optlookup("ksharrays"));
if !ksharrays {
// c:653
idx += 1; // c:655 args++
break 'outer; // c:656
}
break;
}
// c:659-660 — `s` sort flag.
if c == 's' {
// c:659
sort = if action { 1 } else { -1 }; // c:660
} else {
// c:662-666 — short-option letter: optlookupc + dosetopt.
let optno = crate::ported::options::optlookupc(c); // c:663
if optno == 0 {
// c:663
zerr(&format!("bad option: -{}", c)); // c:663
} else if dosetopt(optno, if action { 1 } else { 0 }, 0)
!= 0
// c:664
{
zerr(&format!("can't change option: -{}", c)); // c:664
}
}
ci += 1;
}
idx += 1; // c:668
}
let _ = nam;
// c:676 — `queue_signals();`
queue_signals();
let remaining = &argv[idx..];
// c:678-694 — display path when no array/no args.
if arrayname.is_none() {
// c:678
if !hadopt && remaining.is_empty() {
// c:679
// c:680-681 — `scanhashtable(paramtab, 1, 0, 0,
// paramtab->printnode, hadplus ? PRINT_NAMEONLY : 0);`
//
// C walks the paramtab (sorted=1 → alphabetical). The previous
// Rust port walked `std::env::vars()` — the OS environment.
// Shell-internal vars (not exported to env) would never appear
// in the `set` listing, diverging from C where ALL paramtab
// entries are emitted.
//
// Same family of bug as the prior bin_unset -m fix.
let mut entries: Vec<(String, String)> = {
let tab = paramtab().read().unwrap();
tab.iter()
.filter(|(_, pm)| {
// c:scanhashtable filter: skip PM_UNSET. C also
// skips entries with flags2=0 (none extra filtered).
(pm.node.flags as u32 & PM_UNSET) == 0
})
.map(|(k, pm)| {
let v = pm.u_str.clone().unwrap_or_default();
(k.clone(), v)
})
.collect()
};
// c:680 sorted=1 → meta-aware sort via hnamcmp (already fixed
// to use ztrcmp earlier in the series).
entries.sort_by(|a, b| hnamcmp(&a.0, &b.0));
for (k, v) in entries {
if hadplus {
// c:681 PRINT_NAMEONLY
println!("{}", k);
} else {
println!("{}={}", k, quotedzputs(&v));
}
}
}
if array != 0 {
// c:684
// c:685-687 — `scanhashtable(paramtab, 1, PM_ARRAY, 0,
// paramtab->printnode, hadplus ? PRINT_NAMEONLY : 0)`.
// Walk paramtab filtering by PM_ARRAY and emit each as
// `name=(elem1 elem2 ...)`. Previous Rust port stubbed
// this body with a "nothing to enumerate" comment — but
// paramtab does store arrays in `u_arr`, so `set -A` (no
// name) MUST list every PM_ARRAY entry. Sorted via
// hnamcmp (meta-aware compare) per `sorted=1` in the C
// scanhashtable call.
let mut arr_entries: Vec<(String, Vec<String>)> = {
use {PM_ARRAY, PM_TYPE};
let tab = paramtab().read().unwrap();
tab.iter()
.filter(|(_, pm)| {
PM_TYPE(pm.node.flags as u32) == PM_ARRAY
&& (pm.node.flags as u32 & PM_UNSET) == 0
})
.map(|(k, pm)| (k.clone(), pm.u_arr.clone().unwrap_or_default()))
.collect()
};
arr_entries.sort_by(|a, b| hnamcmp(&a.0, &b.0)); // c:685 sorted=1
for (k, arr) in arr_entries {
if hadplus {
// c:686 PRINT_NAMEONLY
println!("{}", k);
} else {
let quoted: Vec<String> = arr
.iter()
.map(|v| quotedzputs(v))
.collect();
println!("{}=({})", k, quoted.join(" "));
}
}
}
if remaining.is_empty() && !hadend {
// c:688
unqueue_signals();
return 0; // c:690
}
}
// c:693-695 — `set -s` sort.
let sorted: Vec<String> = if sort != 0 {
let mut v = remaining.to_vec();
if sort < 0 {
v.sort_by(|a, b| b.cmp(a));
} else {
v.sort();
}
v
} else {
remaining.to_vec()
};
// c:696-708 — array assign or positional-param replace.
if array != 0 {
// c:696
// c:697-708 — build array; `array < 0` appends to existing $name.
let aname = arrayname.unwrap_or_default();
let mut new_arr: Vec<String> = sorted;
if array < 0 {
// c:701
// c:702-704 — `if ((a = getaparam(arrayname)) && arrlen_gt(a, len))`.
// Read paramtab.u_arr directly; was using `:`-
// split env value as a fake array.
let existing: Vec<String> = {
let tab = paramtab().read().unwrap();
tab.get(&aname)
.and_then(|pm| pm.u_arr.clone())
.unwrap_or_default()
};
if existing.len() > new_arr.len() {
// c:702
new_arr.extend(existing.into_iter().skip(new_arr.len())); // c:703
}
}
// c:709 — `setaparam(arrayname, x);`. Use setaparam (array
// setter) so the value lands as a proper PM_ARRAY,
// not a colon-joined scalar.
setaparam(&aname, new_arr);
} else {
// c:711-712 — `freearray(pparams); pparams = zarrdup(args);`
// PPARAMS is the single source of truth; fusevm reads via
// `exec.pparams()`.
if let Ok(mut pp) = PPARAMS.lock() {
*pp = sorted; // c:712
}
}
unqueue_signals(); // c:714
0 // c:715
}
/// Port of `bin_pwd(UNUSED(char *name), UNUSED(char **argv), Options ops, UNUSED(int func))` from Src/builtin.c:728.
/// C: `int bin_pwd(UNUSED(char *name), UNUSED(char **argv), Options ops,
/// UNUSED(int func))` — `-r`/`-P` or (CHASELINKS && !`-L`) →
/// print resolved cwd via zgetcwd; else print the cached `pwd`.
// pwd: display the name of the current directory // c:728
/// WARNING: param names don't match C — Rust=(_name, _argv, _func) vs C=(name, argv, ops, func)
pub fn bin_pwd(
_name: &str,
_argv: &[String], // c:728
ops: &options,
_func: i32,
) -> i32 {
let chaselinks = isset(optlookup("chaselinks"));
// c:730-731 — `if (OPT_ISSET(ops,'r') || OPT_ISSET(ops,'P') ||
// (isset(CHASELINKS) && !OPT_ISSET(ops,'L')))`
if OPT_ISSET(ops, b'r') || OPT_ISSET(ops, b'P') // c:730
|| (chaselinks && !OPT_ISSET(ops, b'L'))
// c:731
{
// c:732 — `printf("%s\n", zgetcwd());`
println!("{}", zgetcwd()); // c:732
} else {
// c:734 — `zputs(pwd, stdout); putchar('\n');`. C reads the
// shell-internal `pwd` global (Src/params.c:108). The
// canonical Rust accessor is `getsparam("PWD")` which reads
// from the paramtab (the source-of-truth backing for PWD).
//
// Previously this used `std::env::var("PWD")` which reads
// the OS environment — divergent. The OS env var is only
// sync'd to the paramtab on export; the paramtab can hold
// a more recent value, and `unset PWD; cd /foo; pwd` would
// print the wrong thing under the env-var path (env was
// already unset, so the read fell through to zgetcwd
// bypassing the just-set paramtab PWD).
let pwd = getsparam("PWD").unwrap_or_else(|| zgetcwd());
println!("{}", pwd); // c:734
}
0 // c:737
}
/// Port of `bin_dirs(UNUSED(char *name), char **argv, Options ops, UNUSED(int func))` from Src/builtin.c:749.
/// C: `int bin_dirs(UNUSED(char *name), char **argv, Options ops, ...)` —
/// list dirstack (default / -v / -p / -l) or replace it with argv.
// dirs: list the directory stack, or replace it with a provided list // c:749
/// WARNING: param names don't match C — Rust=(_name, argv, _func) vs C=(name, argv, ops, func)
pub fn bin_dirs(
_name: &str,
argv: &[String], // c:749
ops: &options,
_func: i32,
) -> i32 {
queue_signals(); // c:753
// c:755-756 — list mode: no args & no -c, OR -v / -p.
if (argv.is_empty() && !OPT_ISSET(ops, b'c')) // c:755
|| OPT_ISSET(ops, b'v')
|| OPT_ISSET(ops, b'p')
{
let mut pos = 1; // c:760
// c:763-769 — pick separator format.
let fmt: &str = if OPT_ISSET(ops, b'v') {
// c:763
print!("0\t"); // c:764
"\n{}\t" // c:765
} else if OPT_ISSET(ops, b'p') {
// c:767
"\n"
} else {
" "
};
// c:771-774 — print pwd via fprintdir or zputs (`-l`).
// Previous Rust port inlined a HOME-prefix replacement which
// only abbreviated `$HOME/...` to `~/...` — missed every
// user-defined nameddirtab entry (`hash -d proj=/big/path`).
// Route through `utils::fprintdir` which calls `finddir`,
// matching C's named-dir abbreviation.
let pwd = getsparam("PWD").unwrap_or_else(|| zgetcwd());
if OPT_ISSET(ops, b'l') {
// c:771
print!("{}", pwd); // c:772
} else {
print!("{}", fprintdir(&pwd)); // c:774
}
// c:775-781 — walk dirstack list.
if let Ok(stack) = DIRSTACK.lock() {
// c:775
for entry in stack.iter() {
if fmt == "\n{}\t" {
print!("\n{}\t", pos);
} else {
print!("{}", fmt); // c:776
}
pos += 1; // c:776
if OPT_ISSET(ops, b'l') {
// c:777
print!("{}", entry); // c:778
} else {
print!("{}", fprintdir(entry)); // c:780
}
}
}
unqueue_signals(); // c:783
println!(); // c:784
return 0; // c:785
}
// c:788-792 — replace dirstack with the supplied entries.
if let Ok(mut stack) = DIRSTACK.lock() {
stack.clear(); // c:790
for arg in argv {
stack.push(arg.clone()); // c:791
}
}
unqueue_signals(); // c:793
0 // c:794
}
/// Direct port of `void set_pwd_env(void)` from
/// `Src/builtin.c:800`. Refreshes both `$PWD` and `$OLDPWD` to mirror
/// the shell-side `pwd`/`oldpwd` globals. C clears `PM_READONLY` on
/// each if it's currently typed as scalar (paranoid guard for users
/// who did `typeset -r PWD`), then writes via `setsparam`.
///
/// Rust port reads `$PWD`/`$OLDPWD` from paramtab (the shell-side
/// truth), then writes them back via `setsparam` plus an OS-env
/// mirror so child processes inherit the values. Was a fake that
/// only wrote `getcwd()` into the OS env, bypassing paramtab and
/// silently dropping `$OLDPWD`.
pub fn set_pwd_env() {
// c:800
// c:805-810 — `if ((pm = paramtab->getnode("PWD")) && ...) pm->node.flags &= ~PM_READONLY;`
// The PM_READONLY clear isn't ported (no PM_READONLY
// consumer breaks downstream); the canonical
// refresh goes through setsparam which handles the
// flag set.
// c:813 — `setsparam("PWD", pwd);`. Read paramtab's PWD if set;
// fall back to getcwd so a fresh shell starts with PWD
// populated.
let pwd = getsparam("PWD").or_else(|| {
env::current_dir()
.ok()
.map(|p| p.to_string_lossy().into_owned())
});
if let Some(s) = pwd {
setsparam("PWD", &s); // c:813
env::set_var("PWD", &s);
}
// c:818 — `setsparam("OLDPWD", oldpwd);` mirror; only fires when
// oldpwd is set (initially NULL on first shell).
if let Some(s) = getsparam("OLDPWD") {
env::set_var("OLDPWD", &s);
}
}
/// Port of `bin_cd(char *nam, char **argv, Options ops, int func)` from Src/builtin.c:840.
/// C: `int bin_cd(char *nam, char **argv, Options ops, int func)`.
///
/// Body (verbatim translation per c:842-859):
/// ```c
/// doprintdir = (doprintdir == -1);
/// chasinglinks = OPT_ISSET(ops,'P') ||
/// (isset(CHASELINKS) && !OPT_ISSET(ops,'L'));
/// queue_signals();
/// zpushnode(dirstack, ztrdup(pwd));
/// if (!(dir = cd_get_dest(nam, argv, OPT_ISSET(ops,'s'), func))) {
/// zsfree(getlinknode(dirstack));
/// unqueue_signals();
/// return 1;
/// }
/// cd_new_pwd(func, dir, OPT_ISSET(ops, 'q'));
/// unqueue_signals();
/// return 0;
/// ```
// cd, chdir, pushd, popd // c:796
pub fn bin_cd(
nam: &str,
argv: &[String], // c:840
ops: &options,
func: i32,
) -> i32 {
// c:844 — `doprintdir = (doprintdir == -1);`
let prev = DOPRINTDIR.load(Relaxed);
DOPRINTDIR.store(if prev == -1 { 1 } else { 0 }, Relaxed); // c:844
// c:846-847 — `chasinglinks = OPT_ISSET(ops,'P') ||
// (isset(CHASELINKS) && !OPT_ISSET(ops,'L'));`
let chase = OPT_ISSET(ops, b'P') // c:846
|| (isset(optlookup("chaselinks"))
&& !OPT_ISSET(ops, b'L'));
CHASINGLINKS.store(chase as i32, Relaxed);
queue_signals(); // c:848
// c:849 — `zpushnode(dirstack, ztrdup(pwd));`. C uses the `pwd`
// global (the in-shell logical cwd, kept in sync with
// $PWD). Read from paramtab; fall back to getcwd if
// unset. The C source pushes pre-cd pwd to the top of
// dirstack here as a scratch slot used by cd_get_dest's
// +N/-N resolver; cd_new_pwd's remnode logic relies on
// this. Save the pre-cd path for the post-cd dirstack
// maintenance below.
let pre_pwd = getsparam("PWD").unwrap_or_else(|| zgetcwd());
// c:850-854 — `if (!(dir = cd_get_dest(...))) { pop; unqueue; return 1; }`
let dest = cd_get_dest(nam, argv, OPT_ISSET(ops, b's'), func);
if dest.is_none() {
unqueue_signals(); // c:852
return 1; // c:853
}
let dest_raw = dest.unwrap();
// c:Src/builtin.c:851 — `-s` safe mode: refuse to chdir into a
// symlink. Check via fs::symlink_metadata so we see the link
// itself, not its target.
if OPT_ISSET(ops, b's') {
if let Ok(meta) = fs::symlink_metadata(&dest_raw) {
if meta.file_type().is_symlink() {
zwarnnam(nam, &format!("{}: symbolic link", dest_raw));
unqueue_signals();
return 1;
}
}
}
// c:Src/builtin.c:855 — route the resolved arg through
// cd_do_chdir so CDPATH walk, leading `~`/`.` handling, and
// CDABLEVARS expansion fire. cd_do_chdir performs the actual
// lchdir + returns the LOGICAL path (the one to write to PWD).
// The previous Rust port called env::set_current_dir(dest_raw)
// directly which skipped all of CDPATH, so `CDPATH=/foo cd bar`
// would only resolve `./bar` even if `/foo/bar` existed.
let dest_path = match cd_do_chdir(nam, &dest_raw, OPT_ISSET(ops, b's') as i32) {
Some(p) => p,
None => {
unqueue_signals();
return 1;
}
};
// c:1238 — `oldpwd = pwd;` snapshot pre-cd $PWD for $OLDPWD.
// Read from paramtab (the canonical zsh-side `pwd`
// global); was reading OS env which can lag behind.
let old = getsparam("PWD");
// c:Src/builtin.c:849 + cd_new_pwd dirstack maintenance —
// collapsed into a single post-cd update here since the Rust
// cd_get_dest returns a String rather than a LinkNode that
// could be pre-pushed onto dirstack:
// * BIN_PUSHD: push pre-cd pwd to top so subsequent
// `${dirstack[1]}` and `popd` see the previous directory.
// * BIN_POPD: pop dirstack[0] (the directory we just left,
// which cd_get_dest read from the stack to compute dest).
// * BIN_CD: dirstack unchanged unless AUTO_PUSHD is set, in
// which case the CD behaves like a pushd.
{
let autopushd = isset(optlookup("autopushd"));
if let Ok(mut d) = DIRSTACK.lock() {
if func == BIN_PUSHD || (func == BIN_CD && autopushd) {
// c:849 — push pre-cd pwd.
// c:1210-1218 — PUSHDIGNOREDUPS: skip duplicate of
// the new (current) pwd.
let dup_skip = isset(optlookup("pushdignoredups"))
&& d.first().map(|s| *s == pre_pwd).unwrap_or(false);
if !dup_skip {
d.insert(0, pre_pwd.clone());
}
} else if func == BIN_POPD {
// c:1197-1199 — pop top of stack (the dir we left).
if !d.is_empty() {
d.remove(0);
}
}
}
}
if let Some(o) = old {
// c:1239 oldpwd = pwd
// c:1239 + setsparam path: write OLDPWD to paramtab so
// subsequent expansions of $OLDPWD see the new value
// (the OS env write below is the export side; the
// shell-side read must come from paramtab).
setsparam("OLDPWD", &o);
env::set_var("OLDPWD", &o);
}
// c:1241 — `pwd = new_pwd;` writes the LOGICAL path (the dest
// argument as given to cd, not `getcwd()`). Symlink resolution
// only kicks in when `chasinglinks` is set (c:1203-1208,
// c:1228-1231) — both fall back to `findpwd()`/`zgetcwd()`.
// Earlier port called `std::env::current_dir()` (= `getcwd(3)`),
// which always resolves symlinks (e.g. /tmp → /private/tmp on
// macOS), breaking logical-PWD parity with zsh.
let chase = CHASINGLINKS.load(Relaxed) != 0; // c:1203
let pwd: String = if chase {
// c:1203
// c:1204 — `s = findpwd(new_pwd);` — resolved cwd.
match env::current_dir() {
Ok(c) => c.to_string_lossy().into_owned(),
Err(_) => dest_path.clone(),
}
} else if dest_path.starts_with('/') {
// c:1241 — absolute path. pwd = new_pwd.
dest_path.clone()
} else {
// c:1240 — relative path. zsh resolves logically: walk the
// dest segments against pre-cd pwd, collapsing `.` and `..`
// without dereferencing symlinks. Without this, `pushd ..`
// from /tmp left $PWD = ".." literally.
let mut segs: Vec<&str> = if pre_pwd.is_empty() || pre_pwd == "/" {
Vec::new()
} else {
pre_pwd.trim_start_matches('/').split('/').collect()
};
for part in dest_path.split('/') {
match part {
"" | "." => {}
".." => {
segs.pop();
}
_ => segs.push(part),
}
}
if segs.is_empty() {
"/".to_string()
} else {
format!("/{}", segs.join("/"))
}
};
// c:1242 — `setsparam("PWD", pwd);` + export side via env.
setsparam("PWD", &pwd);
env::set_var("PWD", &pwd);
cd_new_pwd(func, 0, OPT_ISSET(ops, b'q') as i32); // c:856
unqueue_signals(); // c:858
0 // c:859
}
/// Port of `cd_get_dest(char *nam, char **argv, int hard, int func)` from Src/builtin.c:865.
/// C: `static LinkNode cd_get_dest(char *nam, char **argv, int hard,
/// int func)` — resolve the `cd` argument (`-`, `+N`/`-N`,
/// bare → $HOME, two-arg substitution form) to a destination path.
/// Returns the resolved path on success, None on error (with the
/// appropriate zwarnnam already emitted).
/// WARNING: param names don't match C — Rust=() vs C=(nam, argv, hard, func)
pub fn cd_get_dest(nam: &str, argv: &[String], _hard: bool, func: i32) -> Option<String> {
if argv.is_empty() {
// c:872 — bare popd / pushd / cd (no args).
// The Rust port doesn't pre-push pwd to dirstack inside bin_cd
// (cd_get_dest's String return signature doesn't fit C's
// pre-push pattern), so dirstack[0] here is the most-recent
// pushed entry, not the temporary scratch slot the C source
// sees. Adjust the indices accordingly: C reads index 1
// (skipping the scratch), Rust reads index 0.
if func == BIN_POPD {
let depth = DIRSTACK.lock().map(|d| d.len()).unwrap_or(0);
if depth < 1 {
zwarnnam(nam, "directory stack empty");
return None;
}
return DIRSTACK.lock().ok().and_then(|d| d.first().cloned());
}
if func == BIN_PUSHD {
// c:877 — bare pushd without PUSHDTOHOME swaps top two.
// In Rust's pre-push-free model that's just dirstack[0].
let pushdtohome = isset(optlookup("pushdtohome"));
if !pushdtohome {
return DIRSTACK.lock().ok().and_then(|d| d.first().cloned());
}
}
// c:880-884 — fall through to $HOME.
match getsparam("HOME") {
Some(h) if !h.is_empty() => Some(h),
_ => {
zwarnnam(nam, "HOME not set");
None
}
}
} else if argv.len() == 1 {
// c:887
let arg = &argv[0];
DOPRINTDIR.fetch_add(1, Relaxed); // c:891
// c:892-908 — `+N`/`-N` numeric stack-index form.
let posixcd = isset(optlookup("posixcd"));
if !posixcd
&& arg.len() > 1
&& (arg.starts_with('+') || arg.starts_with('-'))
&& arg[1..].chars().all(|c| c.is_ascii_digit())
{
let dd: usize = arg[1..].parse().unwrap_or(0); // c:894
let pushdminus = isset(optlookup("pushdminus"));
let from_top = (arg.starts_with('+')) ^ pushdminus; // c:898
return DIRSTACK.lock().ok().and_then(|d| {
if from_top {
d.get(dd).cloned()
} else if d.len() > dd {
d.get(d.len() - 1 - dd).cloned()
} else {
None
}
});
}
// c:910-911 — `-` alias for $OLDPWD; else literal arg.
// C reads `oldpwd` global / `$OLDPWD` param;
// route through paramtab via getsparam.
if arg == "-" {
// c:911
DOPRINTDIR.fetch_sub(1, Relaxed);
getsparam("OLDPWD")
} else {
Some(arg.clone()) // c:911
}
} else {
// c:914-924 — two-arg substitution: cd OLDPATTERN NEWPATTERN.
// C reads `pwd` global / `$PWD` param via getsparam;
// fall back to getcwd if the param isn't populated.
let pwd = getsparam("PWD").unwrap_or_else(|| zgetcwd());
let pat = &argv[0];
let new_pat = &argv[1];
match pwd.find(pat.as_str()) {
// c:917
None => {
zwarnnam(nam, &format!("string not in pwd: {}", pat)); // c:918
None // c:919
}
Some(idx) => {
// c:921-924 — splice: pwd[..idx] + new_pat + pwd[idx+pat.len()..]
let mut out = String::new();
out.push_str(&pwd[..idx]); // c:921
out.push_str(new_pat); // c:922
out.push_str(&pwd[idx + pat.len()..]); // c:923
DOPRINTDIR.fetch_add(1, Relaxed);
Some(out)
}
}
}
}
/// Port of `cd_do_chdir(char *cnam, char *dest, int hard)` from Src/builtin.c:967.
/// C: `static char *cd_do_chdir(char *cnam, char *dest, int hard)` —
/// resolve `dest` (handling cdpath, cdablevars, leading `~`/`.`),
/// chdir there, return the LOGICAL path used (not `getcwd`'d) or
/// NULL on error.
///
/// Per C `cd_try_chdir` (c:1116-1181), the return is `buf` — the
/// composed path the chdir was attempted against, after `fixdir()`
/// logical-normalisation (resolving `.`/`..` only, NOT symlinks).
/// Walks $cdpath when dest is relative and not `./` or `../`.
pub fn cd_do_chdir(cnam: &str, dest: &str, hard: i32) -> Option<String> {
// c:967
// c:996-998 — nocdpath = first segment is "." or ".."
let nocdpath = dest.starts_with("./") || dest == "." || dest.starts_with("../") || dest == "..";
// c:1003-1008 — absolute path: try as-is, warn on failure.
if dest.starts_with('/') {
if let Some(ret) = cd_try_chdir("", dest, hard) {
return Some(ret);
}
zwarnnam(
cnam,
&format!("{}: {}", io::Error::last_os_error(), dest),
);
return None;
}
// c:1015-1018 — check $cdpath for "." (presence flips hasdot).
let posix_cd = isset(optlookup("posixcd"));
let cdpath_str = getsparam("CDPATH").unwrap_or_default();
let cdpath: Vec<&str> = if cdpath_str.is_empty() {
Vec::new()
} else {
cdpath_str.split(':').collect()
};
let hasdot = !nocdpath && !posix_cd && cdpath.iter().any(|p| p.is_empty() || *p == ".");
// c:1026-1031 — if no dot in cdpath (and !POSIXCD), try as-is first.
if !hasdot && !posix_cd {
if let Some(ret) = cd_try_chdir("", dest, hard) {
return Some(ret);
}
}
// c:1034-1043 — walk $cdpath unless nocdpath.
if !nocdpath {
for pp in cdpath.iter() {
if let Some(ret) = cd_try_chdir(pp, dest, hard) {
// c:1037-1040 — print resolved path when from CDPATH
// (non-"."), gated on DOPRINTDIR > 0. zsh only prints
// the resolved path in interactive mode or when the
// shell explicitly set the flag (e.g. `cd -P`). Non-
// interactive `-fc` scripts skip the print.
if !pp.is_empty()
&& *pp != "."
&& DOPRINTDIR.load(Relaxed) > 0
&& isset(INTERACTIVE)
{
println!("{}", ret);
}
return Some(ret);
}
}
}
// c:1057-1063 — POSIXCD-mode last-resort: try dest as-is.
if posix_cd {
if let Some(ret) = cd_try_chdir("", dest, hard) {
return Some(ret);
}
}
// c:1071 — failure warning.
zwarnnam(cnam, &format!("no such file or directory: {}", dest));
None
}
/// Port of `cd_able_vars(char *s)` from Src/builtin.c:1088.
/// C: `char *cd_able_vars(char *s)` — when CDABLEVARS is set, look up
/// the leading bareword as a parameter and return its expanded value
/// prefixed in front of any trailing `/...`. Returns NULL otherwise.
pub fn cd_able_vars(s: &str) -> Option<String> {
// c:1088
// c:1088 — `if (isset(CDABLEVARS)) { ... }`
let cdablevars = isset(optlookup("cdablevars"));
if !cdablevars {
// c:1093
return None;
}
// c:1094-1110 — split on the first `/`, look up the head as $param.
let (head, tail) = match s.find('/') {
// c:1094
Some(i) => (&s[..i], &s[i..]),
None => (s, ""),
};
if head.is_empty() {
return None;
}
// c:1116 — `if ((val = getsparam(s))) { ret = tricat(val, tail, "") }`.
// C reads $head from paramtab; was reading OS env, missing
// CDABLEVARS-style assignments like `proj=$HOME/src`.
getsparam(head).map(|val| format!("{}{}", val, tail))
}
/// Port of `cd_try_chdir(char *pfix, char *dest, int hard)` from `Src/builtin.c:1116`.
/// Compose `pfix/dest` (or `pwd/pfix/dest` for relative pfix, or
/// `pwd/dest` for empty pfix + relative dest), normalise via `fixdir`,
/// then attempt chdir. Falls back to `dest` alone when the full path
/// fails but `pfix` was present (cwd/parent may have been renamed).
pub fn cd_try_chdir(pfix: &str, dest: &str, hard: i32) -> Option<String> {
// c:1116
let pwd = getsparam("PWD").unwrap_or_default();
// c:1122-1158 — build buf from pfix/dest/pwd combinations.
let mut buf = if !pfix.is_empty() {
if pfix.starts_with('/') {
// c:1123
// c:1133 — buf = tricat(pfix, "/", dest)
if pfix.ends_with('/') {
format!("{}{}", pfix, dest)
} else {
format!("{}/{}", pfix, dest)
}
} else {
// c:1135-1146 — pwd + "/" + pfix + "/" + dest
let pwd_trim = if pwd == "/" { "" } else { pwd.as_str() };
format!("{}/{}/{}", pwd_trim, pfix, dest)
}
} else if dest.starts_with('/') {
// c:1148
// c:1149 — buf = ztrdup(dest)
dest.to_string()
} else {
// c:1150
// c:1151-1157 — pwd + "/" + dest (trimming trailing slash off pwd)
let pwd_trim = pwd.trim_end_matches('/');
format!("{}/{}", pwd_trim, dest)
};
// c:1163-1166 — fixdir normalisation, skipped if chasing symlinks.
if CHASINGLINKS.load(Relaxed) == 0 {
buf = fixdir(&buf); // c:1164
}
// c:1172-1183 — try lchdir(buf); on failure and (pfix || dest abs) was
// not the input shape that allows fallback, give up.
let _ = hard;
if lchdir(&buf).is_ok() {
return Some(buf);
}
// c:1173 — fallback: try `dest` alone when pfix was non-empty
// and dest isn't already absolute.
if !pfix.is_empty() && !dest.starts_with('/') {
if lchdir(dest).is_ok() {
return Some(dest.to_string());
}
}
None // c:1185
}
/// Port of `cd_new_pwd(int func, LinkNode dir, int quiet)` from Src/builtin.c:1187.
/// C: `static void cd_new_pwd(int func, LinkNode dir, int quiet)` —
/// commit a new PWD: rotate dirstack on `BIN_PUSHD`, pop on
/// `BIN_POPD`, then setparam(PWD/OLDPWD), fire chpwd hooks.
///
/// The PWD/OLDPWD write is now done by the caller (`bin_cd`) using
/// the logical `dest_path` from `cd_get_dest`. C's body at c:1238-1242
/// reads `new_pwd` off the dirstack — the Rust port's dirstack
/// plumbing isn't faithful enough to carry that path here, so the
/// caller writes PWD directly. This fn handles only the post-write
/// side effects (chpwd hooks, dirstack size cap).
/// WARNING: param names don't match C — Rust=(_func, _dir, _quiet) vs C=(func, dir, quiet)
pub fn cd_new_pwd(func: i32, _dir: usize, quiet: i32) {
// c:1187 — post-cd side effects (print, hooks). Dirstack
// maintenance was moved into `bin_cd` because the Rust port's
// cd_get_dest returns a String (not a LinkNode pre-pushed on
// dirstack), so C's remnode/rolllist sequence doesn't fit.
// c:1236-1242 — shift PWD → OLDPWD, set new PWD.
//
// The caller (`bin_cd` at builtin.rs:1379-1408) already wrote
// OLDPWD (pre-cd $PWD) and PWD (the logical `dest_path`) into the
// paramtab. Re-deriving them here would (a) double-shift OLDPWD
// (overwriting the correct pre-cd value with the just-set PWD),
// and (b) re-set PWD via `std::env::current_dir()` which always
// returns the resolved physical path — clobbering the logical
// path on systems where the destination is a symlink (e.g. macOS
// /tmp → /private/tmp). C `cd_new_pwd` reads `new_pwd` off the
// dirstack (the path the user typed); zshrs's dirstack plumbing
// doesn't carry that path here, so the caller is the authoritative
// PWD writer and this fn must NOT re-write either parameter.
// c:1245-1252 — print dirstack on PUSHD/POPD (unless silent/quiet).
if quiet == 0 && func != BIN_CD && isset(INTERACTIVE) && !isset(optlookup("pushdsilent")) {
printdirstack();
}
// c:1264 — runhookdef(GETCOLORATTR/chpwd) — fire chpwd hooks.
// Hook table integration not surfaced here; chpwd_functions array
// is populated via $hook_functions and read by the executor wrapper.
}
/// Port of `printdirstack()` from Src/builtin.c:1277.
/// C: `static void printdirstack(void)` — fprintdir(pwd) followed by
/// space-separated entries from the dirstack list, ending in newline.
pub fn printdirstack() {
// c:1277
// c:1281 — `fprintdir(pwd, stdout);`. C uses the shell-side
// `pwd` global (in-shell logical cwd), not getcwd. Read
// $PWD from paramtab so the logical path (including
// any unresolved symlinks) shows correctly. Route
// through `utils::fprintdir` for the same `~` /
// `~named` abbreviation real zsh emits.
// Previous Rust port emitted raw paths, missing the
// $HOME / nameddirtab abbreviation that makes pushd/popd output
// legible. Same fix family as bin_dirs.
let pwd = getsparam("PWD")
.or_else(|| {
env::current_dir()
.ok()
.and_then(|p| p.to_str().map(String::from))
})
.unwrap_or_default();
print!("{}", fprintdir(&pwd)); // c:1281
// c:1282-1286 — `for (node = firstnode(dirstack); ...)`
if let Ok(d) = DIRSTACK.lock() {
for entry in d.iter() {
// c:1282
print!(" {}", fprintdir(entry)); // c:1284
}
}
println!(); // c:1287
}
/// Direct port of `int fixdir(char *src)` from
/// `Src/builtin.c:1297`. Lexically canonicalises a path in-place
/// (no symlink follow): collapses `//`, drops `./` segments, and
/// removes `..` along with their preceding segment. Returns 1 if
/// fully canonicalised, 0 if a `..` could not be popped (e.g. at
/// the root or with `..` as the first segment under CHASEDOTS=0).
///
/// Rust port takes ownership of `src` and returns the canonical
/// form; was a 1-line stub returning empty string.
pub fn fixdir(src: &str) -> String {
// c:1297
if src.is_empty() {
return String::new();
}
// c:1320-1325 — `chasedots` flag for the cdpath `../` edge case.
// Skipped here — only fires under the pwd=="." rare
// state. Lexical canonicalisation is what callers
// rely on.
let abs = src.starts_with('/');
let mut components: Vec<&str> = Vec::new();
// c:1339-1395 — walk slash-separated segments.
for seg in src.split('/') {
match seg {
"" => continue, // collapse `//`
"." => continue, // c:1352 drop `./`
".." => {
// c:1358-1372 — pop previous segment if present and not
// also `..` (sticky-`..` for relative
// paths past their start).
if let Some(last) = components.last() {
if *last == ".." {
components.push("..");
} else {
components.pop();
}
} else if !abs {
// Relative path: keep the leading `..`.
components.push("..");
}
// Absolute path: silently drop `..` past `/`.
}
other => components.push(other),
}
}
let body = components.join("/");
if abs {
format!("/{}", body)
} else if body.is_empty() {
".".to_string()
} else {
body
}
}
/// Port of `printqt(char *str)` from Src/builtin.c:1399.
/// C: `mod_export void printqt(char *str)` — emit `str`, escaping any
/// `'` as `'\''` (or `''` if RCQUOTES is set).
pub fn printqt(str: &str) {
// c:1399
let rcquotes = isset(optlookup("rcquotes")); // c:1399 isset(RCQUOTES)
for ch in str.chars() {
// c:1403
if ch == '\'' {
// c:1404
print!("{}", if rcquotes { "''" } else { "'\\''" }); // c:1405
} else {
print!("{}", ch); // c:1407
}
}
}
/// Port of `printif(char *str, int c)` from Src/builtin.c:1411.
/// C: `mod_export void printif(char *str, int c)` — `printf(" -%c ", c)`
/// then `quotedzputs(str, stdout)`, only when `str != NULL`.
pub fn printif(str: Option<&str>, c: u8) {
// c:1411
if let Some(s) = str {
// c:1399
print!(" -{} ", c as char); // c:1399
// c:1399 — quotedzputs(str, stdout); plain print preserves bytes
// for the ASCII case; full quotedzputs lives in src/ported/utils.rs.
print!("{}", s); // c:1399
}
}
/// Port of `bin_fc(char *nam, char **argv, Options ops, int func)` from Src/builtin.c:1426.
/// C: `int bin_fc(char *nam, char **argv, Options ops, int func)`.
///
/// History/edit/list dispatcher: `-p` push hist stack, `-P` pop,
/// `-R` read, `-W` write, `-A` append, `-m` glob filter, `-l` list,
/// `-s` substitute, default: edit + re-execute. The C body is ~245
/// lines; the structural translation here covers the major options
/// and dispatches the underlying history-file ops to the existing
/// hist.rs accessors.
/// WARNING: param names don't match C — Rust=(nam, argv, func) vs C=(nam, argv, ops, func)
pub fn bin_fc(
nam: &str,
argv: &[String], // c:1426
ops_in: &options,
func: i32,
) -> i32 {
// C `Options ops` is `struct options *` — mutable via `ops->ind['n']
// = 1;` at c:1644. zshrs HandlerFunc takes `&options`, so we clone
// to a fn-local `ops` mirror at the top. Mutation of the clone is
// intra-fn only (`fclist` reads `ops` to format output and never
// returns it), so behavior matches C.
let mut ops = ops_in.clone();
let ops = &mut ops;
let mut argv = argv.to_vec();
let mut first: i64 = -1;
let mut last: i64 = -1;
let mut asgf: Vec<(String, String)> = Vec::new();
// c:1441-1481 — `-p` push history stack.
if OPT_ISSET(ops, b'p') {
// c:1441
let mut hf = "".to_string();
let mut hs: i64; // c:1443
let mut shs: i64; // c:1444
// c:1445 — `int level = OPT_ISSET(ops,'a') ? locallevel : -1;`
let level: i32 = if OPT_ISSET(ops, b'a') {
locallevel_param.load(Relaxed)
} else {
-1
};
hs = histsiz.load(Relaxed); // c:1442
shs = savehistsiz.load(Relaxed);
if !argv.is_empty() {
// c:1445
hf = argv.remove(0); // c:1446
if !argv.is_empty() {
// c:1447
let s2 = argv.remove(0);
match s2.parse::<i64>() {
// c:1449 zstrtol
Ok(n) => hs = n,
Err(_) => {
zwarnnam(
"fc", // c:1452
"HISTSIZE must be an integer",
);
return 1; // c:1453
}
}
if !argv.is_empty() {
// c:1455
let s3 = argv.remove(0);
match s3.parse::<i64>() {
// c:1456
Ok(n) => shs = n,
Err(_) => {
zwarnnam(
"fc", // c:1459
"SAVEHIST must be an integer",
);
return 1; // c:1460
}
}
} else {
shs = hs; // c:1464
}
if !argv.is_empty() {
// c:1466
zwarnnam(
"fc", // c:1468
"too many arguments",
);
return 1; // c:1469
}
}
}
// c:1473 — pushhiststack(hf, hs, shs, level); failure → return 1.
pushhiststack(Some(&hf), hs, shs, level); // c:1473
if !hf.is_empty() {
// c:1475
// c:1476-1480 — `if (stat(hf, &st) >= 0 || errno != ENOENT)
// readhistfile(hf, 1, HFILE_USE_OPTIONS);`
// Previous Rust port read `Error::last_os_error()` AFTER
// checking `metadata().is_ok()` — racey: any intervening
// syscall between the metadata call and last_os_error()
// can stomp errno on some platforms. Capture the per-Err
// raw_os_error directly so we read the SAME errno value
// the stat call produced.
let stat_result = fs::metadata(&hf);
let should_read = match &stat_result {
Ok(_) => true, // c:1477 stat >= 0
Err(e) => e.raw_os_error() != Some(libc::ENOENT), // c:1477 errno != ENOENT
};
if should_read {
// c:1477
readhistfile(
// c:1478
Some(&hf),
1,
HFILE_USE_OPTIONS as i32,
);
}
}
return 0; // c:1483
}
// c:1485-1491 — `-P` pop history stack.
if OPT_ISSET(ops, b'P') {
// c:1485
if !argv.is_empty() {
// c:1486
zwarnnam("fc", "too many arguments"); // c:1487
return 1; // c:1488
}
// c:1490 — `return !saveandpophiststack(-1, HFILE_USE_OPTIONS);`.
let popped = saveandpophiststack(-1, HFILE_USE_OPTIONS as i32); // c:1490
return if popped != 0 { 0 } else { 1 }; // c:1490 `!` flip
}
// c:1494-1500 — `-m` pattern filter (compile first arg).
let mut pprog: Option<crate::ported::pattern::PatProg> = None;
let mut pprog_src: Option<String> = None;
if !argv.is_empty() && OPT_ISSET(ops, b'm') {
// c:1494
let pat = argv.remove(0);
// c:1495 — tokenize(*argv); — Rust `patcompile` handles tokenisation.
match patcompile(
&pat, // c:1496
PAT_HEAPDUP,
None,
) {
Some(p) => {
pprog = Some(p);
pprog_src = Some(pat); // retain source string for fclist
}
None => {
zwarnnam(nam, "invalid match pattern"); // c:1497
return 1; // c:1498
}
}
}
queue_signals(); // c:1502
// c:1503-1525 — `-R` read / `-W` write / `-A` append history file.
if OPT_ISSET(ops, b'R') {
// c:1503
let path = argv.first().cloned();
let flags = if OPT_ISSET(ops, b'I') {
HFILE_SKIPOLD as i32
} else {
0
};
readhistfile(
// c:1505
path.as_deref(),
1,
flags,
);
unqueue_signals(); // c:1506
return 0; // c:1507
}
if OPT_ISSET(ops, b'W') {
// c:1509
let path = argv.first().cloned();
let flags = if OPT_ISSET(ops, b'I') {
HFILE_SKIPOLD as i32
} else {
0
};
savehistfile(
// c:1511
path.as_deref(),
flags,
);
unqueue_signals(); // c:1512
return 0; // c:1513
}
if OPT_ISSET(ops, b'A') {
// c:1515
let path = argv.first().cloned();
let mut flags = HFILE_APPEND as i32;
if OPT_ISSET(ops, b'I') {
flags |= HFILE_SKIPOLD as i32;
} // c:1518
savehistfile(
// c:1517
path.as_deref(),
flags,
);
unqueue_signals(); // c:1519
return 0; // c:1520
}
// c:1523-1527 — refuse inside ZLE.
if crate::ported::builtins::sched::zleactive.load(
// c:1523
Relaxed,
) != 0
{
unqueue_signals(); // c:1524
zwarnnam(
nam, // c:1525
"no interactive history within ZLE",
);
return 1; // c:1526
}
// c:1530-1547 — `name=value` substitution pairs.
while !argv.is_empty() && argv[0].contains('=') {
// c:1530
let arg = argv.remove(0);
if let Some(eq) = arg.find('=') {
let n = &arg[..eq];
let v = &arg[eq + 1..];
if n.is_empty() {
zwarnnam(nam, &format!("invalid replacement pattern: ={}", v)); // c:1534
return 1;
}
asgf.push((n.to_string(), v.to_string())); // c:1546
}
}
// c:1550-1568 — first/last history specifiers via fcgetcomm.
if !argv.is_empty() {
// c:1550
first = fcgetcomm(&argv.remove(0)); // c:1551
if first == -1 {
unqueue_signals();
return 1; // c:1553
}
}
if !argv.is_empty() {
// c:1559
last = fcgetcomm(&argv.remove(0)); // c:1560
if last == -1 {
unqueue_signals();
return 1;
}
}
if !argv.is_empty() {
// c:1567
unqueue_signals();
zwarnnam("fc", "too many arguments"); // c:1569
return 1;
}
// c:1573-1610 — default ranges + listing/edit dispatch. C reads
// the live `curhist` global at hist.rs directly. The
// FQN here is forced — bare `curhist` would resolve
// to the local `let curhist` we're declaring.
let curhist: i64 = crate::ported::hist::curhist.load(Relaxed) as i64;
if last == -1 {
// c:1573
if OPT_ISSET(ops, b'l') && first < curhist {
// c:1574
last = curhist; // c:1583
if last < 1 {
last = 1;
} // c:1585
} else {
last = first; // c:1587
}
}
if first == -1 {
// c:1589
let _xflags = if OPT_ISSET(ops, b'L') {
HIST_FOREIGN
} else {
0
}; // c:1597
first = if OPT_ISSET(ops, b'l') {
(curhist - 16).max(1)
}
// c:1598
else {
(curhist - 1).max(1)
};
if last < first {
last = first;
} // c:1604
}
let mut retval;
if OPT_ISSET(ops, b'l') {
// c:1606
// c:1608 — `fclist(stdout, ops, first, last, asgf, pprog, 0);`
retval = fclist(
&mut io::stdout(),
ops,
first,
last,
&asgf,
pprog_src.as_deref(),
0,
);
unqueue_signals();
} else {
// c:1611-1668 — edit history range to a temp file, fcedit it,
// then stuff() the result back as the next command.
retval = 1; // c:1620
let fil_opt = gettempfile(Some("zshfc")); // c:1621 gettempfile
match fil_opt {
None => {
// c:1623
unqueue_signals(); // c:1624
zwarnnam(
"fc", // c:1625
&format!("can't open temp file: {}", io::Error::last_os_error()),
);
}
Some((fd, fil)) => {
unsafe {
libc::close(fd);
} // c:1622 (file is reopened below)
// c:1632 — `if (last >= curhist) { last = curhist - 1; ... }`
if last >= curhist {
// c:1632
last = curhist - 1; // c:1633
if first > last {
// c:1634
unqueue_signals(); // c:1635
zwarnnam(
"fc", // c:1636
"current history line would recurse endlessly, aborted",
);
let _ = fs::remove_file(&fil); // c:1639 unlink
return 1; // c:1640
}
}
ops.ind[b'n' as usize] = 1; // c:1644 No line numbers
let out = fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&fil)
.ok();
let listed = if let Some(mut f) = out {
// c:1645 — thread pprog filter so `fc -m PAT` only
// edits matching lines in this temp-file edit path.
fclist(&mut f, ops, first, last, &asgf, pprog_src.as_deref(), 1)
} else {
1
};
if listed == 0 {
// c:1645
// c:1647-1656 — pick editor.
let editor: String = if func == BIN_R || OPT_ISSET(ops, b's') {
"-".to_string() // c:1648
} else if OPT_HASARG(ops, b'e') {
// c:1649
OPT_ARG(ops, b'e').unwrap_or("").to_string() // c:1650
} else {
// c:1651-1654 — `getsparam("FCEDIT") ?:
// getsparam("EDITOR") ?:
// DEFAULT_FCEDIT`. paramtab read.
getsparam("FCEDIT")
.or_else(|| getsparam("EDITOR"))
.unwrap_or_else(|| crate::ported::config_h::DEFAULT_FCEDIT.to_string())
};
unqueue_signals(); // c:1657
if fcedit(&editor, &fil) != 0 {
// c:1658
if crate::ported::input::stuff(&fil) != 0 {
// c:1659
zwarnnam(
"fc", // c:1660
&format!("{}: {}", io::Error::last_os_error(), fil),
);
} else {
// c:1663-1664 — `loop(0,1); retval = lastval;`
// The interactive loop drives the next stuffed
// line through the parser. Static-link path:
// the executor's input source picks it up on
// the next read; lastval reflects that result.
retval = LASTVAL.load(
// c:1664
Relaxed,
);
}
}
} else {
unqueue_signals(); // c:1667
}
let _ = fs::remove_file(&fil); // c:1671 unlink
}
}
}
let _ = pprog; // compiled form kept for parity; source threads through fclist.
retval // c:1675
}
/// Port of `fcgetcomm(char *s)` from Src/builtin.c:1683.
/// C: `static zlong fcgetcomm(char *s)` — match `s` against history
/// numbers (signed) or prefix; returns the matched event number.
/// Direct port of `zlong fcgetcomm(char *s)` from
/// `Src/builtin.c:1683`. Resolve an `fc` command-line argument to a
/// history event number. Numeric args become event numbers (negative
/// numbers count back from current via `addhistnum`); non-numeric
/// args go through `hcomsearch` (history prefix search). Emits
/// `zwarnnam("fc", "event not found: %s", s)` and returns -1 on
/// miss.
pub fn fcgetcomm(s: &str) -> i64 {
// c:1683
// c:1689 — `if ((cmd = atoi(s)) != 0 || *s == '0')` numeric arm.
// atoi accepts leading whitespace + optional sign +
// digits; trim+parse mirrors that.
let trimmed = s.trim_start();
let numeric = trimmed.parse::<i64>().ok();
let is_zero_prefix = trimmed.starts_with('0');
if let Some(mut cmd) = numeric {
if cmd != 0 || is_zero_prefix {
if cmd < 0 {
// c:1693 — `cmd = addhistnum(curline.histnum, cmd, HIST_FOREIGN);`
let curh = crate::ported::hist::curhist.load(Relaxed);
cmd = addhistnum(curh, cmd as i32, 1);
}
if cmd < 0 {
// c:1695
cmd = 0;
}
return cmd;
}
}
// c:1700 — `cmd = hcomsearch(s); if (cmd == -1) zwarnnam(...);`
match hcomsearch(s) {
Some(n) => n,
None => {
zwarnnam("fc", &format!("event not found: {}", s));
-1
}
}
}
/// Port of `fcsubs(char **sp, struct asgment *sub)` from Src/builtin.c:1708.
/// C: `static int fcsubs(char **sp, struct asgment *sub)` — apply the
/// linked-list of `old=new` substitutions to `*sp` in place; return
/// the count of substitutions made.
pub fn fcsubs(sp: &mut String, sub: &[(String, String)]) -> i32 {
// c:1708
// c:1708-1748 — for each (old, new), replace each occurrence in *sp.
let mut subbed = 0i32; // c:1713
for (old, new) in sub {
// c:1716
if old.is_empty() {
continue;
}
let count = sp.matches(old.as_str()).count() as i32; // c:1722
if count > 0 {
*sp = sp.replace(old.as_str(), new); // c:1750
subbed += count;
}
}
subbed
}
/// Direct port of `int fclist(FILE *f, Options ops, zlong first,
/// zlong last, struct asgment *subs, Patprog pprog, int is_command)`
/// from `Src/builtin.c:1750`. Walks the history event range
/// `first..=last`, applies the `subs` substitution chain to each
/// matching line (when `pprog` is set, only lines matching it),
/// then writes the result with optional timestamp prefix per
/// `-d/-f/-E/-i/-t`.
///
/// Rust signature: takes the output writer as a closure so callers
/// can route to stdout, a FILE*, or an in-memory buffer (the
/// `is_command` caller in `bin_fc` collects to a heredoc string).
/// Was a 5-line stub returning 0; now actually emits the range.
#[allow(clippy::too_many_arguments)]
pub fn fclist(
out: &mut dyn Write, // c:1750
ops: &options,
mut first: i64,
mut last: i64,
subs: &[(String, String)],
pprog: Option<&str>,
is_command: i32,
) -> i32 {
use std::io::Write;
// c:1762-1766 — `if (OPT_ISSET(ops,'r')) swap(first, last);`
if OPT_ISSET(ops, b'r') {
std::mem::swap(&mut first, &mut last);
}
// c:1768-1773 — `if (is_command && first > last) zwarnnam(...)`.
if is_command != 0 && first > last {
zwarnnam("fc", "history events can't be executed backwards, aborted");
return 1;
}
// c:1776-1790 — `gethistent(first, ...)` with bidirectional fallback.
let near = if first < last { 1 } else { -1 };
let start_ev = match gethistent(first, near) {
Some(e) => e,
None => {
// c:Src/builtin.c — `no such event: <N>` carries the
// requested event number so the user can see which
// index missed. zsh appends the failing event id;
// the bare `no such event` message diverged.
zwarnnam(
"fc",
&if first == last {
format!("no such event: {}", first)
} else {
"no events in that range".to_string()
},
);
return 1;
}
};
// c:1792-1817 — timestamp format setup.
let want_time = OPT_ISSET(ops, b'd')
|| OPT_ISSET(ops, b'f')
|| OPT_ISSET(ops, b'E')
|| OPT_ISSET(ops, b'i')
|| OPT_ISSET(ops, b't');
let tdfmt: Option<&'static str> = if !want_time {
None
} else if OPT_ISSET(ops, b't') {
Some("%H:%M") // -t expects user-supplied fmt; without OPT_ARG access default to %H:%M
} else if OPT_ISSET(ops, b'i') {
Some("%Y-%m-%d %H:%M")
} else if OPT_ISSET(ops, b'E') {
Some("%d.%m.%Y %H:%M")
} else if OPT_ISSET(ops, b'f') {
Some("%m/%d/%Y %H:%M")
} else {
Some("%H:%M")
};
// c:1820-1880 — walk events from start_ev toward `last`. Each entry:
// apply pprog filter, apply subs chain, emit (with
// event num + timestamp unless -n or is_command).
let mut ev = start_ev;
let step: i64 = if first < last { 1 } else { -1 };
loop {
// c:1830 — `ent = quietgethist(ev);` — fetch entry by event #.
let entry = match quietgethist(ev) {
Some(e) => e,
None => break,
};
let line = entry.node.nam.clone();
// c:1833 — pprog pattern filter. C pre-compiles a Patprog;
// Rust compiles per-call. Most fc -l calls have no
// pattern so the gate is cheap.
if let Some(pat) = pprog {
let prog = patcompile(pat, 0, None);
let matched = prog.as_ref().map(|p| pattry(p, &line)).unwrap_or(true);
if !matched {
if ev == last {
break;
}
ev += step;
continue;
}
}
// c:1841-1855 — apply subs chain (asgment list of `old=new`
// pairs that get substituted in order).
let mut text = line;
for (old, new) in subs.iter() {
if old.is_empty() {
continue;
}
text = text.replace(old.as_str(), new.as_str());
}
// c:1860-1870 — emit prefix: event number (unless -n / -h),
// then optional timestamp.
if is_command == 0 {
if !OPT_ISSET(ops, b'n') {
let _ = write!(out, "{:>5}", ev);
if OPT_ISSET(ops, b'D') {
let _ = write!(out, "{:>10}", entry.stim - entry.ftim);
}
if let Some(fmt) = tdfmt {
// c:1817 — `strftime(timebuf, 256, tdfmt,
// localtime(&ent->stim))`.
// Use libc directly so locale-aware
// format specifiers (%Y %m %d %H %M %S
// %p etc.) all work without a hand-rolled
// strftime port.
let formatted: Option<String> = (|| {
let mut tm: libc::tm = unsafe { std::mem::zeroed() };
let t: libc::time_t = entry.stim as libc::time_t;
let cfmt = std::ffi::CString::new(fmt).ok()?;
unsafe {
if libc::localtime_r(&t, &mut tm).is_null() {
return None;
}
let mut buf = vec![0u8; 256];
let n = libc::strftime(
buf.as_mut_ptr() as *mut libc::c_char,
buf.len(),
cfmt.as_ptr(),
&tm,
);
if n == 0 {
return None;
}
buf.truncate(n);
String::from_utf8(buf).ok()
}
})();
if let Some(s) = formatted {
let _ = write!(out, " {}", s);
} else {
// strftime failed (locale issue / format bug);
// fall back to raw epoch matching C's
// pre-strftime print behavior.
let _ = write!(out, " {}", entry.stim);
}
}
let _ = write!(out, " ");
}
}
// c:1875 — write the line.
let _ = writeln!(out, "{}", text);
if ev == last {
break;
}
ev += step;
if ev < 0 {
break;
}
}
0 // c:1880
}
/// Port of `fcedit(char *ename, char *fn)` from Src/builtin.c:1885.
/// C: `static int fcedit(char *ename, char *fn)` — invoke `$ename fn`,
/// returning the editor's exit status (0 if `ename == "-"`).
/// WARNING: param names don't match C — Rust=(ename, fn_) vs C=(ename, fn)
pub fn fcedit(ename: &str, fn_: &str) -> i32 {
// c:1885
// c:1885 — `if (!strcmp(ename, "-")) return 1;`
if ename == "-" {
// c:1888
return 1; // c:1889
}
// c:1891-1900 — execlp(ename, ename, fn, NULL) wrapped in fork/wait.
let status = std::process::Command::new(ename) // c:1895
.arg(fn_)
.status();
match status {
Ok(s) => s.code().unwrap_or(1),
Err(_) => 1,
}
}
/// Port of `getasg(char ***argvp, LinkList assigns)` from Src/builtin.c:1908.
/// C: `static Asgment getasg(char ***argvp, LinkList assigns)` —
/// parse one assignment-form arg (`name=value` / `name`) from
/// `*argvp`. Returns NULL when exhausted.
/// ```c
/// static Asgment
/// getasg(char ***argvp, LinkList assigns)
/// {
/// char *s = **argvp;
/// static struct asgment asg;
/// if (!s) {
/// if (assigns) {
/// Asgment asgp = (Asgment)firstnode(assigns);
/// if (!asgp) return NULL;
/// (void)uremnode(assigns, &asgp->node);
/// return asgp;
/// }
/// return NULL;
/// }
/// if (*s == '=') { zerr("bad assignment"); return NULL; }
/// asg.name = s;
/// asg.flags = 0;
/// for (; *s && *s != '='; s++);
/// if (*s) { *s = '\0'; asg.value.scalar = s + 1; }
/// else asg.value.scalar = NULL;
/// (*argvp)++;
/// return &asg;
/// }
/// ```
pub fn getasg(
argvp: &mut Vec<String>, // c:1908
assigns: &mut Vec<(String, String)>,
) -> Option<(String, String)> {
// c:1914-1923 — out-of-args path: drain from assigns list if non-empty.
if argvp.is_empty() {
// c:1914 !s
if !assigns.is_empty() {
// c:1915
return Some(assigns.remove(0)); // c:1916-1920 firstnode + uremnode
}
return None; // c:1922
}
let s = argvp.remove(0); // c:1944 (*argvp)++
// c:1926-1929 — empty-name guard: bare `=value` is an error.
if s.starts_with('=') {
// c:1926
zerr("bad assignment"); // c:1927
return None; // c:1928
}
// c:1934-1943 — split on `=`. No `=` → name-only (scalar = NULL).
match s.find('=') {
// c:1934
Some(i) => {
// c:1938-1939 — `*s = '\0'; asg.value.scalar = s + 1;`
Some((s[..i].to_string(), s[i + 1..].to_string())) // c:1939
}
None => {
// c:1942 — `asg.value.scalar = NULL;` — name-only.
Some((s, String::new())) // c:1942
}
}
}
/// Port of `typeset_setbase(const char *name, Param pm, Options ops, int on, int always)` from Src/builtin.c:1961.
/// C: `static int typeset_setbase(const char *name, Param pm, Options ops,
/// int on, int always)` — install numeric base on `pm`. For
/// `-i ARG`/`-E ARG`/`-F ARG`, parse ARG as base and validate
/// (must be 2..=36 for integer); error → return 1.
/// WARNING: param names don't match C — Rust=(name, pm, on, always) vs C=(name, pm, ops, on, always)
pub fn typeset_setbase(
name: &str,
pm: *mut param, // c:1961
ops: &options,
on: i32,
always: i32,
) -> i32 {
// c:1964 — `char *arg = NULL;`
let mut arg: Option<&str> = None; // c:1964
let on_u = on as u32;
// c:1966-1971 — `if ((on & PM_INTEGER) && OPT_HASARG(ops,'i')) arg = OPT_ARG(ops,'i');`
if (on_u & PM_INTEGER) != 0 && OPT_HASARG(ops, b'i') {
// c:1966
arg = OPT_ARG(ops, b'i'); // c:1967
} else if (on_u & PM_EFLOAT) != 0 && OPT_HASARG(ops, b'E') {
// c:1968
arg = OPT_ARG(ops, b'E'); // c:1969
} else if (on_u & PM_FFLOAT) != 0 && OPT_HASARG(ops, b'F') {
// c:1970
arg = OPT_ARG(ops, b'F'); // c:1971
}
// c:1973 — `if (arg) {`
if let Some(a) = arg {
// c:1973
// c:1976 — `int base = (int)zstrtol(arg, &eptr, 10);`
let base = match a.trim().parse::<i32>() {
Ok(b) => b,
Err(_) => {
// c:1977-1982
if (on_u & PM_INTEGER) != 0 {
zwarnnam(name, &format!("bad base value: {}", a)); // c:1979
} else {
zwarnnam(name, &format!("bad precision value: {}", a)); // c:1981
}
return 1; // c:1983
}
};
// c:1985-1989 — integer base must be 2..=36 inclusive.
if (on_u & PM_INTEGER) != 0 && (base < 2 || base > 36) {
// c:1985
zwarnnam(
name,
&format!("invalid base (must be 2 to 36 inclusive): {}", base),
); // c:1986-1987
return 1; // c:1988
}
// c:1990 — `pm->base = base;`
if !pm.is_null() {
unsafe {
(*pm).base = base;
} // c:1990
}
} else if always != 0 {
// c:1991
// c:1997 — `pm->base = 0;`
if !pm.is_null() {
unsafe {
(*pm).base = 0;
} // c:1997
}
}
0 // c:1997
}
/// Port of `typeset_setwidth(const char * name, Param pm, Options ops, int on, int always)` from Src/builtin.c:1997.
/// C: `static int typeset_setwidth(const char *name, Param pm, Options ops,
/// int on, int always)` — install padding width via `-L/-R/-Z ARG`.
/// WARNING: param names don't match C — Rust=(name, pm, on, always) vs C=(name, pm, ops, on, always)
pub fn typeset_setwidth(
name: &str,
pm: *mut param, // c:1997
ops: &options,
on: i32,
always: i32,
) -> i32 {
// c:2000 — `char *arg = NULL;`
let mut arg: Option<&str> = None; // c:2000
let on_u = on as u32;
// c:2002-2007
if (on_u & PM_LEFT) != 0 && OPT_HASARG(ops, b'L') {
// c:2002
arg = OPT_ARG(ops, b'L'); // c:2003
} else if (on_u & PM_RIGHT_B) != 0 && OPT_HASARG(ops, b'R') {
// c:2004
arg = OPT_ARG(ops, b'R'); // c:2005
} else if (on_u & PM_RIGHT_Z) != 0 && OPT_HASARG(ops, b'Z') {
// c:2006
arg = OPT_ARG(ops, b'Z'); // c:2007
}
// c:2009 — `if (arg) {`
if let Some(a) = arg {
// c:2009
// c:2011 — `pm->width = (int)zstrtol(arg, &eptr, 10);`
let width = match a.trim().parse::<i32>() {
Ok(w) => w,
Err(_) => {
zwarnnam(name, &format!("bad width value: {}", a)); // c:2013
return 1; // c:2014
}
};
if !pm.is_null() {
unsafe {
(*pm).width = width;
} // c:2011
}
} else if always != 0 {
// c:2015
// c:2016 — `pm->width = 0;`
if !pm.is_null() {
unsafe {
(*pm).width = 0;
} // c:2025
}
}
0 // c:2025
}
/// Port of `typeset_single(char *cname, char *pname, Param pm, int func, int on, int off, int roff, Asgment asg, Param altpm, Options ops, int joinchar)` from Src/builtin.c:2025.
/// Port of `static Param typeset_single(char *cname, char *pname,
/// Param pm, int func, int on, int off, int roff, Asgment asg,
/// Param altpm, Options ops, int joinchar)` from `Src/builtin.c:2025`.
/// Per-name attribute resolver + assignment dispatcher invoked once
/// per arg from `bin_typeset`.
#[allow(clippy::too_many_arguments)]
pub fn typeset_single(
cname: &str,
pname: &str, // c:2025
pm: *mut param,
func: i32,
mut on: i32,
mut off: i32,
_roff: i32,
asg: *mut asgment,
altpm: *mut param,
ops: &options,
_joinchar: i32,
) -> *mut param {
let mut usepm: i32; // c:2029
let mut tc: i32 = 0; // c:2029
let _keeplocal: i32 = 0; // c:2029
let mut newspecial: i32 = 0; /* NS_NONE */
// c:2029
let _readonly: i32 = 0; // c:2029
let _dont_set: i32 = 0; // c:2029
let mut pname_owned: String = pname.to_string(); // c:2030 subscript path
// c:2032-2050 — nameref resolution.
let pm_ref = unsafe { pm.as_mut() };
if let Some(pm_r) = &pm_ref {
let pm_flags = pm_r.node.flags as u32;
let locallevel_v =
locallevel_param.load(Relaxed);
if (pm_flags & PM_NAMEREF) != 0
&& ((off | on) as u32 & PM_NAMEREF) == 0
&& (pm_r.level == locallevel_v || (on as u32 & PM_LOCAL) == 0)
{
// c:2034 — pm = resolve_nameref(pm)
// pname = pm->node.nam (when resolved)
// resolve_nameref not yet ported; skip the rewrite.
let unresolved_flags = pm_r.node.flags as u32;
let extra_on_mask = !(PM_NAMEREF | PM_LOCAL | PM_READONLY) as i32;
if (pm_flags & PM_NAMEREF) != 0
&& ((unresolved_flags & PM_UNSET) == 0 || (unresolved_flags & PM_DECLARED) != 0)
&& (on & extra_on_mask) != 0
{
// c:2042-2048 — error: can't change type of a nameref.
if pm_r.width != 0 {
// c:2041
zwarnnam(
cname, // c:2042
&format!("{}: can't change type via subscript reference", pname),
);
} else {
zwarnnam(
cname, // c:2046
&format!("{}: can't change type of a named reference", pname),
);
}
return std::ptr::null_mut(); // c:2048
}
}
}
// c:2062-2064 — `usepm = pm && (!(pm_flags & PM_UNSET) || OPT_ISSET(ops,'p') || ...)`
let pm_flags = pm_ref.as_ref().map(|p| p.node.flags as u32).unwrap_or(0);
usepm = if pm_ref.is_some()
&& ((pm_flags & PM_UNSET) == 0
|| OPT_ISSET(ops, b'p')
|| (isset(POSIXBUILTINS) && (pm_flags & (PM_READONLY | PM_EXPORTED)) != 0))
{
1
} else {
0
};
// c:2070-2071 — preserve PM_UNSET for special params.
if usepm == 0 && pm_ref.is_some() && (pm_flags & PM_SPECIAL) != 0 {
usepm = 2; // c:2071
}
// c:2078-2091 — don't reuse if local-level changed and PM_LOCAL set.
let pm_level = pm_ref.as_ref().map(|p| p.level).unwrap_or(0);
let locallevel_v = locallevel_param.load(Relaxed);
if usepm != 0 && locallevel_v != pm_level && (on as u32 & PM_LOCAL) != 0 {
// c:2078
if (pm_flags & PM_SPECIAL) != 0 // c:2087
&& (on as u32 & PM_HIDE) == 0
&& (pm_flags & PM_HIDE & !off as u32) == 0
{
newspecial = 1; /* NS_NORMAL */ // c:2089
}
usepm = 0; // c:2090
}
// c:2093-2116 — type-conversion / tied-colonarray detection.
let asg_ref = unsafe { asg.as_ref() };
tc = 0;
if let Some(a) = asg_ref {
if ASG_ARRAYP(a)
&& PM_TYPE(on as u32) == PM_SCALAR
&& !(usepm != 0 && (PM_TYPE(pm_flags) & (PM_ARRAY | PM_HASHED)) != 0)
{
on |= PM_ARRAY as i32; // c:2097
}
if usepm != 0 && ASG_ARRAYP(a) && newspecial == 0 // c:2098
&& PM_TYPE(pm_flags) != PM_ARRAY
&& PM_TYPE(pm_flags) != PM_HASHED
{
if (on as u32 & (PM_EFLOAT | PM_FFLOAT | PM_INTEGER)) != 0 {
zerrnam(
cname, // c:2102
&format!("{}: can't assign array value to non-array", pname),
);
return std::ptr::null_mut();
}
if (pm_flags & PM_SPECIAL) != 0 {
// c:2105
zerrnam(
cname, // c:2106
&format!("{}: can't assign array value to non-array special", pname),
);
return std::ptr::null_mut();
}
tc = 1; // c:2109
usepm = if OPT_MINUS(ops, b'p') {
// c:2110
(on as u32 & pm_flags) as i32
} else if OPT_PLUS(ops, b'p') {
// c:2112
(off as u32 & pm_flags) as i32
} else {
0 // c:2115
};
}
}
// c:2117-2199 — attribute-mask compatibility checks (chflags compute).
if usepm != 0 || newspecial != 0 {
let chflags = ((off as u32 & pm_flags) | (on as u32 & !pm_flags)) // c:2118
& (PM_INTEGER
| PM_EFLOAT
| PM_FFLOAT
| PM_HASHED | PM_ARRAY | PM_TIED | PM_AUTOLOAD);
if chflags != 0 && chflags != (PM_EFLOAT | PM_FFLOAT) {
tc = 1; // c:2122
if OPT_MINUS(ops, b'p') {
// c:2123
usepm = (on as u32 & pm_flags) as i32;
} else if OPT_PLUS(ops, b'p') {
usepm = (off as u32 & pm_flags) as i32;
}
}
}
// c:2202-2214 — readonly/exported preservation rules.
if usepm != 0 || newspecial != 0 {
if (on as u32 & (PM_READONLY | PM_EXPORTED)) != 0 // c:2202
&& (usepm == 0 || (pm_flags & PM_UNSET) != 0)
&& asg_ref.is_some_and(|a| !ASG_VALUEP(a))
{
on |= PM_UNSET as i32; // c:2205
} else if usepm != 0 && (pm_flags & PM_READONLY) != 0 // c:2206
&& (on as u32 & PM_READONLY) == 0
&& func != BIN_EXPORT
{
zerr(&format!(
// c:2208
"read-only variable: {}",
pm_ref.as_ref().unwrap().node.nam
));
return std::ptr::null_mut();
}
}
// c:2226-2248 — reuse-existing-param fast paths.
if usepm != 0 {
let pm_r = pm_ref.as_ref().unwrap();
if OPT_MINUS(ops, b'p')
&& on != 0
&& !((on as u32 & pm_flags) != 0 || ((on as u32 & PM_LOCAL) != 0 && pm_r.level != 0))
{
return std::ptr::null_mut(); // c:2229
}
if OPT_PLUS(ops, b'p') && off != 0 && (off as u32 & pm_flags) == 0 {
return std::ptr::null_mut(); // c:2231
}
// c:2232-2238 — array/scalar consistency check
if let Some(a) = asg_ref {
let array_assign = (a.flags & ASG_ARRAY) != 0;
let pm_is_arr = (PM_TYPE(pm_flags) & (PM_ARRAY | PM_HASHED)) != 0;
if array_assign && !pm_is_arr {
// c:2232
zerrnam(
cname, // c:2236
&format!("{}: inconsistent type for assignment", pname),
);
return std::ptr::null_mut();
}
}
}
// c:2240-2247 — print-only path: typeset -p / typeset name (no value).
if usepm != 0 && on == 0 && _roff == 0 && asg_ref.is_some_and(|a| !ASG_VALUEP(a)) {
// c:2241 — `int with_ns = OPT_ISSET(ops,'m') ? PRINT_WITH_NAMESPACE : 0;`
let with_ns = if OPT_ISSET(ops, b'm') { // c:2241
PRINT_WITH_NAMESPACE
} else {
0
};
if let Some(pm_r) = unsafe { pm.as_mut() } {
if OPT_ISSET(ops, b'p') { // c:2242
// c:2243 — `paramtab->printnode(&pm->node, PRINT_TYPESET|with_ns);`
printparamnode(
pm_r,
PRINT_TYPESET | with_ns,
);
} else if !OPT_ISSET(ops, b'g') // c:2244
&& (!isset(TYPESETSILENT) || OPT_ISSET(ops, b'm')) // c:2245
{
// c:2246 — `paramtab->printnode(&pm->node, PRINT_INCLUDEVALUE|with_ns);`
printparamnode(
pm_r,
PRINT_INCLUDEVALUE | with_ns,
);
}
}
return pm; // c:2247
}
// c:2355-2378 — tc (type-conversion) branch: recreate the param.
if tc != 0 && !OPT_ISSET(ops, b'p') {
on |= (!off as u32 & (PM_READONLY | PM_EXPORTED) & pm_flags) as i32; // c:2357
if let Some(pm_r) = pm_ref {
pm_r.node.flags &= !(PM_READONLY as i32); // c:2359
}
// c:2364 — keeplocal = pm->level (used by createparam path)
// c:2372-2375 — carry scalar value across type change.
// c:2378 — unsetparam_pm(pm, 0, 1)
if let Some(pm_r) = unsafe { pm.as_mut() } {
unsetparam_pm(pm_r, 0, 1);
}
pname_owned = pname.to_string(); // c:2377
}
// c:2381-2467 — newspecial path: preserve special-param struct.
// c:2469-2510 — createparam + assignment dispatch for new/converted.
// c:2512-2453 — apply value via assignsparam/setaparam/sethparam.
// These call into a 2-level helper chain (typeset_setwidth,
// typeset_setbase, assignsparam, etc.) — the available Rust
// ports drive single-attribute setters. The dispatcher entry
// (bin_typeset at c:2655) walks the option matrix and invokes
// those setters directly today.
let _ = (altpm, pname_owned, _keeplocal, _dont_set, _readonly);
// c:2547 — `return pm;`
pm
}
/// Port of `bin_typeset(char *name, char **argv, LinkList assigns, Options ops, int func)` from Src/builtin.c:2655.
/// C: `int bin_typeset(char *name, char **argv, LinkList assigns,
/// Options ops, int func)`.
///
/// The C body (~500 lines) ports here in two layers: the option-flag
/// matrix + conflict-resolution / dispatch (faithfully translated)
/// and the per-arg param-setting loop (delegated to typeset_single
/// already ported above).
/// WARNING: param names don't match C — Rust=(name, argv, func) vs C=(name, argv, assigns, ops, func)
pub fn bin_typeset(
name: &str,
argv: &[String], // c:2655
ops: &options,
func: i32,
) -> i32 {
// PFA-SMR aspect: bin_typeset is the C dispatch site for
// typeset/declare/integer/float/local/export/readonly/private —
// every one of those state-mutating builtins lands here with a
// funcid (BIN_EXPORT/BIN_READONLY/BIN_TYPESET/...) discriminant.
// Emit a per-name event per the recorder schema.
#[cfg(feature = "recorder")]
if crate::recorder::is_enabled() {
let ctx = crate::recorder::recorder_ctx_global();
// Collect option letters (`-x`/`+x` body) so ParamAttrs reflects
// the typeset flag set the C source sees in `on`.
let mut letters = String::new();
let mut tied_mode = false;
for a in argv {
if a.starts_with('-') || a.starts_with('+') {
let body = &a[1..];
letters.push_str(body);
if body.contains('T') {
tied_mode = true;
}
}
}
// Funcid-driven attr seeding: BIN_EXPORT seeds nothing
// (recorder uses emit_export for those), BIN_READONLY seeds
// SCALAR|READONLY, BIN_FLOAT seeds FLOAT, BIN_INTEGER seeds
// INTEGER. Otherwise pass the letter set through
// ParamAttrs::from_flag_chars verbatim.
let mut attrs = crate::recorder::ParamAttrs::from_flag_chars(&letters);
match func {
crate::ported::builtin::BIN_READONLY => {
attrs.set(crate::recorder::ParamAttrs::SCALAR);
attrs.set(crate::recorder::ParamAttrs::READONLY);
}
_ => {}
}
// BIN_EXPORT routes to emit_export (different schema row).
if func == crate::ported::builtin::BIN_EXPORT {
for a in argv {
if a == "-p" || a.starts_with('-') {
continue;
}
if let Some((k, v)) = a.split_once('=') {
crate::recorder::emit_export(k, Some(v), ctx.clone());
} else {
crate::recorder::emit_export(a, None, ctx.clone());
}
}
} else {
// Suppress the emit when invoked as `local`/`private` inside
// a function — those scope to the frame and don't merit a
// top-level state-mutation row. local_scope_depth is tracked
// by the executor; defer to the global locallevel_param counter.
let is_locallike = matches!(name, "local" | "private");
let inside_function = locallevel_param.load(std::sync::atomic::Ordering::Relaxed) > 0;
if !is_locallike || !inside_function {
let mut tied_seen = 0usize;
for a in argv {
if a.starts_with('-') || a.starts_with('+') {
continue;
}
if tied_mode {
// For `typeset -T X Y [SEP]`, only X and Y are names.
tied_seen += 1;
if tied_seen > 2 {
break;
}
}
if let Some((k, v)) = a.split_once('=') {
crate::recorder::emit_typeset_attrs(k, Some(v), attrs, ctx.clone());
} else {
crate::recorder::emit_typeset_attrs(a, None, attrs, ctx.clone());
}
}
}
}
}
let mut ops = ops.clone();
let mut on: u32 = 0; // c:2661
let mut off: u32 = 0; // c:2661
let returnval: i32 = 0; // c:2664
let mut printflags: i32 = PRINT_WITH_NAMESPACE; // c:2664
let hasargs = !argv.is_empty(); // c:2665
// c:2668-2670 — POSIX bash/ksh ignore -p with args under
// readonly/export.
let posix = isset(optlookup("posixbuiltins"));
if (func == BIN_READONLY || func == BIN_EXPORT) && posix && hasargs {
// c:2668
ops.ind[b'p' as usize] = 0; // c:2670
}
// c:2673 — `if (OPT_ISSET(ops,'f')) return bin_functions(...)`.
if OPT_ISSET(&ops, b'f') {
// c:2673
return bin_functions(name, argv, &ops, func); // c:2673
}
// c:2676 — POSIX readonly forces -g unless explicit +g.
if func == BIN_READONLY && posix && !OPT_PLUS(&ops, b'g') {
// c:2676
ops.ind[b'g' as usize] = 1; // c:2677
}
// c:2691-2706 — translate optstr letters into PM_* flag bits.
let mut bit: u32 = PM_ARRAY; // c:2660
for ch in TYPESET_OPTSTR.chars() {
// c:2691
let optval = ch as u8;
if OPT_MINUS(&ops, optval) {
on |= bit;
}
// c:2694-2695
else if OPT_PLUS(&ops, optval) {
off |= bit;
}
// c:2696-2697
// c:2698-2706 — `-n` only allows readonly/upper/hideval.
else {
bit <<= 1;
continue;
}
if OPT_MINUS(&ops, b'n') && (bit & !(PM_READONLY | PM_UPPER | PM_HIDEVAL)) != 0
// c:2701
{
zwarnnam(name, &format!("-{} not allowed with -n", ch)); // c:2702
}
bit <<= 1;
}
// c:2708-2715 — -n / +n conflict resolution.
if OPT_MINUS(&ops, b'n') {
// c:Src/builtin.c — zsh -fc rejects `typeset -n` as
// "bad option: -n" because PM_NAMEREF support requires
// a specific build flag / option mode that the default
// `-fc` non-interactive shell doesn't enable. The Rust
// port had partial nameref support that set PM_NAMEREF
// without the dereference machinery, so `typeset -n REF=
// TARGET; echo $REF` printed "TARGET" instead of erroring.
// Reject -n for typeset entirely to match zsh -fc; use
// zwarnnam (not zerrnam) so this is a per-command warning
// that doesn't set errflag — zsh's script loop continues
// past bad-option diagnostics in non-interactive mode.
zwarnnam(name, "bad option: -n");
return 1;
} else if OPT_PLUS(&ops, b'n') {
// c:2714
off |= PM_NAMEREF; // c:2715
}
let roff = off; // c:2716
// c:2719-2740 — sanity checks: remove conflicting attrs.
if (on & PM_FFLOAT) != 0 {
// c:2719
off |= PM_UPPER | PM_ARRAY | PM_HASHED | PM_INTEGER | PM_EFLOAT; // c:2720
on &= !PM_EFLOAT; // c:2722
}
if (on & PM_EFLOAT) != 0 {
// c:2724
off |= PM_UPPER | PM_ARRAY | PM_HASHED | PM_INTEGER | PM_FFLOAT; // c:2725
}
if (on & PM_INTEGER) != 0 {
// c:2726
off |= PM_UPPER | PM_ARRAY | PM_HASHED | PM_EFLOAT | PM_FFLOAT; // c:2727
}
if (on & (PM_LEFT | PM_RIGHT_Z)) != 0 {
// c:2731
off |= PM_RIGHT_B; // c:2732
}
if (on & PM_RIGHT_B) != 0 {
// c:2733
off |= PM_LEFT | PM_RIGHT_Z; // c:2734
}
if (on & PM_UPPER) != 0 {
off |= PM_LOWER;
} // c:2735-2736
if (on & PM_LOWER) != 0 {
off |= PM_UPPER;
} // c:2737-2738
if (on & PM_HASHED) != 0 {
off |= PM_ARRAY;
} // c:2739-2740
if (on & PM_TIED) != 0 {
// c:2741
off |= PM_INTEGER | PM_EFLOAT | PM_FFLOAT | PM_ARRAY | PM_HASHED; // c:2742
}
on &= !off; // c:2744
queue_signals(); // c:2746
// c:2748-2772 — `-p` print-mode: PRINT_POSIX_EXPORT / READONLY /
// TYPESET, plus optional -p N for line-style.
if OPT_ISSET(&ops, b'p') {
// c:2748
if posix && !EMULATION(EMULATE_KSH) {
// c:2750
printflags |= match func {
BIN_EXPORT => PRINT_POSIX_EXPORT, // c:2752
BIN_READONLY => PRINT_POSIX_READONLY, // c:2754
_ => PRINT_TYPESET, // c:2756
};
} else {
printflags |= PRINT_TYPESET; // c:2758
}
if OPT_HASARG(&ops, b'p') {
// c:2761
let arg = OPT_ARG(&ops, b'p').unwrap_or("");
match arg.trim().parse::<i32>() {
// c:2763
Ok(1) => printflags |= PRINT_LINE, // c:2765
Ok(0) => {} // c:2770 -p0 == -p
_ => {
zwarnnam(name, &format!("bad argument to -p: {}", arg)); // c:2767
unqueue_signals();
return 1; // c:2769
}
}
}
}
// c:2775-2795 — no-args path: list whatever options select.
if !hasargs {
// c:2775
if !OPT_ISSET(&ops, b'm') {
// c:2779
printflags &= !PRINT_WITH_NAMESPACE; // c:2780
}
if !OPT_ISSET(&ops, b'p') {
// c:2782
if (on | roff) == 0 {
// c:2783
printflags |= PRINT_TYPE; // c:2784
}
if roff != 0 || OPT_ISSET(&ops, b'+') {
// c:2785
printflags |= PRINT_NAMEONLY; // c:2786
}
}
// c:2792 — `scanhashtable(paramtab, 1, on|roff, 0, paramtab->printnode,
// printflags|(roff ? PRINT_NAMEONLY : 0));`
//
// Walk paramtab (sorted=1, alphabetical) filtering by on|roff
// and dispatch printparamnode for each match. Previously inlined
// a `println!("{}={}", k, v)` which:
// - Ignored printflags (PRINT_TYPESET, PRINT_POSIX_EXPORT,
// PRINT_POSIX_READONLY, PRINT_NAMEONLY) so `export -p` had
// zero rows, `typeset -p` skipped attribute letters, and
// `readonly -p` had no `readonly ` prefix.
// - Read pm.u_str directly so PM_INTEGER / PM_*FLOAT /
// PM_ARRAY / PM_HASHED values printed as empty.
// printparamnode (params.c:6123) handles all of these.
let printflags_final = printflags | if roff != 0 { PRINT_NAMEONLY } else { 0 }; // c:2792
let names: Vec<String> = {
let tab = paramtab().read().unwrap();
let mut names: Vec<String> = tab
.iter()
.filter(|(_, pm)| {
let f = pm.node.flags as u32;
if (f & PM_UNSET) != 0 {
return false;
}
// c:2792 scanmatchtable flags1=on|roff, flags2=0.
let on_roff = (on as u32) | (roff as u32);
on_roff == 0 || (f & on_roff) != 0
})
.map(|(k, _)| k.clone())
.collect();
names.sort_by(|a, b| hnamcmp(a, b));
names
};
for k in names {
if let Ok(mut tab) = paramtab().write() {
if let Some(pm) = tab.get_mut(&k) {
printparamnode(pm, printflags_final); // c:2792
}
}
}
unqueue_signals();
return 0; // c:2794
}
// c:2799-2810 — `local` (or +g) implies PM_LOCAL.
let nm0 = name.chars().next().unwrap_or(' ');
if nm0 == 'l' || OPT_PLUS(&ops, b'g') {
// c:2799
on |= PM_LOCAL; // c:2800
} else if !OPT_ISSET(&ops, b'g') {
// c:2801
if OPT_MINUS(&ops, b'x') {
// c:2802
let globalexport = isset(optlookup("globalexport"));
let ll_v = locallevel_param.load(Relaxed);
if globalexport {
// c:2803
ops.ind[b'g' as usize] = 1; // c:2804
} else if ll_v != 0 {
// c:2805
on |= PM_LOCAL; // c:2806
}
} else if !(OPT_ISSET(&ops, b'x') || OPT_ISSET(&ops, b'm')) {
// c:2808
on |= PM_LOCAL; // c:2809
}
}
// c:2813+ — -T tied vars + per-arg setting loop.
// The full C body has dozens of paths (PM_TIED tie-pair setup at
// c:2813-2900, glob -m walk at c:2905-2935, name=value assign
// through typeset_single at c:2945+). The Rust port handles the
// three high-frequency paths inline: assoc creation (`PM_HASHED`
// + `name=(k v k v)`), array creation (`PM_ARRAY` + `name=(a b c)`),
// and scalar assignment.
let _ = (off, returnval);
let is_hashed = (on & PM_HASHED) != 0; // c:2655 `-A`
let is_array = (on & PM_ARRAY) != 0; // c:2655 `-a`
// c:Src/builtin.c typeset_single — when the array RHS comes from
// an unquoted `$@` / `${arr[@]}` splat (e.g. `typeset -a opts=
// ("$@")`), the upstream prefork has already split the value
// into separate argv entries: `["opts=(a", "b", "c)"]`. C zsh's
// parser captures the entire `name=(... )` shape as one ENVARRAY
// token by walking paren depth at parse time so the splat fills
// the array's element list. zshrs's compile path emits the
// synthetic word `opts=("$@")` then runtime DQ-strip + splat
// separates it. Reconstruct the single arg here: when one entry
// starts with `NAME=(` and a later entry ends with `)`, rejoin
// the run with spaces between elements.
let argv: Vec<String> = {
let mut out: Vec<String> = Vec::with_capacity(argv.len());
let mut i = 0;
while i < argv.len() {
let arg = &argv[i];
let open = arg.find("=(");
let is_open = open.is_some()
&& arg.as_bytes().first().is_some_and(|b| {
b.is_ascii_alphabetic() || *b == b'_'
})
&& !arg.ends_with(')');
if is_open {
// Find the matching `)` — scan forward through argv
// tracking paren depth (the parser's `(` was just `(`
// in the source). Each arg may have additional `(`
// and `)` chars from quoted content.
let mut depth: i32 = 0;
for c in arg.chars() {
if c == '(' {
depth += 1;
} else if c == ')' {
depth -= 1;
}
}
let mut buf = arg.clone();
let mut j = i + 1;
while depth > 0 && j < argv.len() {
buf.push(' ');
buf.push_str(&argv[j]);
for c in argv[j].chars() {
if c == '(' {
depth += 1;
} else if c == ')' {
depth -= 1;
}
}
j += 1;
}
out.push(buf);
i = j;
} else {
out.push(arg.clone());
i += 1;
}
}
out
};
let argv = argv.as_slice();
for arg in argv {
// c:Src/builtin.c typeset_single — when PM_LOCAL is in
// flags, createparam first to install pm.old chain at
// locallevel (createparam c:1132-1147). Applies uniformly
// to all forms: `local x`, `local x=v`, `local arr=(...)`,
// `local -A h`. endparamscope unwinds via Param.old.
let arg_name: &str = match arg.find('=') {
Some(i) => &arg[..i],
None => arg.as_str(),
};
// c:2519-2552 (Src/builtin.c, inside typeset_single) — name
// validation gate. Direct port:
// else if ((isident(pname) || paramtab->getnode(paramtab, pname))
// && (!idigit(*pname) || !strcmp(pname, "0"))) {
// /* proceed */
// } else {
// if (idigit(*pname))
// zerrnam(cname, "not an identifier: %s", pname);
// else
// zerrnam(cname, "not valid in this context: %s", pname);
// return NULL;
// }
//
// The C function returns NULL on failure; the outer bin_typeset
// name loop continues to the next arg (errflag silences
// subsequent zerr calls so we won't double-emit). Mirror that
// here with `continue`.
let pname_in_tab = paramtab()
.read()
.map(|t| t.get(arg_name).is_some())
.unwrap_or(false);
let first_is_digit = arg_name
.as_bytes()
.first()
.is_some_and(|b| b.is_ascii_digit());
let pname_valid = (isident(arg_name) || pname_in_tab)
&& (!first_is_digit || arg_name == "0");
if !pname_valid {
if first_is_digit {
zerrnam(
name, // c:2548
&format!("not an identifier: {}", arg_name),
);
} else {
zerrnam(
name, // c:2550
&format!("not valid in this context: {}", arg_name),
);
}
continue; // c:2551 return NULL
}
// c:2241-2247 — `-p` print-mode for an existing param (no `=`,
// no value). C `typeset_single` lands here when `usepm` is set
// and `!ASG_VALUEP(asg)`, BEFORE createparam runs (c:2218 →
// c:2244 early return). The Rust loop must also dispatch the
// print branch first; otherwise the createparam call below
// overwrites pm.node.flags on the reuse-arm (c:2018), clobbering
// typeset-attribute bits set by an earlier `typeset -i n` call.
if !arg.contains('=') && OPT_ISSET(&ops, b'p') {
let with_ns = if OPT_ISSET(&ops, b'm') { // c:2241
PRINT_WITH_NAMESPACE
} else {
0
};
let existed = paramtab()
.read()
.map(|t| t.contains_key(arg_name))
.unwrap_or(false);
if existed {
if let Ok(mut tab) = paramtab().write() {
if let Some(pm) = tab.get_mut(arg_name) {
// c:2243 — `paramtab->printnode(&pm->node,
// PRINT_TYPESET|with_ns);`
printparamnode(
pm,
PRINT_TYPESET | with_ns,
);
}
}
}
continue;
}
// c:2930 — `else if (pm)` reuse decision for the bin_typeset
// literal-name loop: `if ((!(pm->node.flags & PM_UNSET) ||
// pm->node.flags & PM_DECLARED)
// && (locallevel == pm->level || !(on & PM_LOCAL)))`.
// Decides whether the existing pm is reusable in place or
// shadowed by a new local. The Rust per-arg loop short-circuits
// through `createparam`'s reuse arm (params.rs:1975) which
// already encodes this rule, but the literal C predicate
// belongs here so the parity is visible at the call site.
let cur_locallevel =
locallevel.load(Relaxed) as i32;
let pm_reuse_local: bool = if pname_in_tab {
let tab = paramtab().read().unwrap();
let pm = tab.get(arg_name).unwrap();
let f = pm.node.flags as u32;
((f & PM_UNSET) == 0 || (f & PM_DECLARED) != 0)
&& (cur_locallevel == pm.level || (on as u32 & PM_LOCAL) == 0) // c:2930
} else {
true
};
let _ = pm_reuse_local;
// c:3127-3132 — PM_NAMEREF literal-name branch. When
// `(on & PM_NAMEREF)` and an existing `hn` is present:
// `if (((Param)hn)->level >= locallevel ||
// (!(on & PM_LOCAL) && ((Param)hn)->level < locallevel)) {
// unsetparam_pm(oldpm, 0, 1); hn = NULL; }`.
// Namerefs always start over fresh when redeclared.
if (on as u32 & PM_NAMEREF) != 0 && pname_in_tab {
let level_compare = paramtab()
.read()
.ok()
.and_then(|t| t.get(arg_name).map(|pm| pm.level))
.unwrap_or(0);
if level_compare >= cur_locallevel
|| ((on as u32 & PM_LOCAL) == 0 && level_compare < cur_locallevel) // c:3130
{
// unsetparam_pm + hn = NULL would happen here. The
// simplified PM_NAMEREF path leaves the reset to
// typeset_single's name-resolution branch at
// typeset_single c:2750.
}
}
// c:2469-2510 — `typeset_single` createparam dispatch for new
// PM_LOCAL declarations. Inside a function scope (`local x` or
// `typeset x` from a fn body), C calls createparam(name,
// on|PM_LOCAL) which chains pm.old = oldpm at the current
// locallevel — the c:2575 `pm->level = locallevel` stamp that
// endparamscope unwinds. Without this, `local x=inside`
// modifies the outer-scope x instead of installing a shadow.
if (on as u32 & PM_LOCAL) != 0 // c:2469
&& !arg_name.is_empty()
&& !arg_name.starts_with('-')
&& !arg_name.starts_with('+')
{
let kind = if is_hashed {
PM_HASHED
} else if is_array {
PM_ARRAY
} else {
0
};
// c:2475-2487 — C calls `assignsparam(pname, value, 0)`
// which creates the pm via the assignsparam → createparam
// path WITHOUT propagating PM_READONLY/PM_EXPORTED flags
// (that path uses PM_SCALAR / PM_ARRAY / PM_HASHED only).
// Post-assign attribute stamps add PM_READONLY/PM_EXPORTED
// later. Mirror by passing ONLY the type-kind + PM_LOCAL
// (not the full `on` mask) so the freshly-created pm
// doesn't error on its own first assignment.
let _ = createparam(arg_name, kind as i32 | PM_LOCAL as i32);
// c:2575 — `else if (on & PM_LOCAL) pm->level = locallevel;`
// — stamp the just-created pm at the current scope so
// endparamscope (params.c) unwinds the shadow when the
// enclosing function returns. createparam at params.rs:2014
// already sets `level: cur_locallevel` on the fresh pm;
// re-stamp here against the post-createparam pm to mirror
// C's explicit assignment, AND to catch the reuse-arm path
// (params.rs:1975-1986) where the existing pm's level was
// pre-set by a prior scope.
if let Ok(mut tab) = paramtab().write() {
if let Some(pm) = tab.get_mut(arg_name) {
pm.level = cur_locallevel; // c:2575
// c:2691 + c:4087 arrsetfn — flags that affect the
// VALUE store (PM_UNIQUE dedup, PM_LEFT/RIGHT_B/Z
// padding width) must land on pm.flags BEFORE
// assignaparam → arrsetfn runs, else those
// setfns see the un-flagged pm. C zsh applies
// these as part of the pre-assignment stamp at
// typeset_single c:2476-2479. Mirror by
// pre-stamping the value-affecting subset of
// `on`. The full attribute mask (PM_READONLY,
// PM_EXPORTED, etc.) still lands in the
// post-assign block below since those don't
// change the value at write time.
let pre_assign_mask: u32 = PM_UNIQUE | PM_LEFT | PM_RIGHT_B
| PM_RIGHT_Z | PM_LOWER | PM_UPPER;
pm.node.flags |= (on as u32 & pre_assign_mask) as i32;
pm.node.flags &= !((off as u32 & pre_assign_mask) as i32);
}
}
}
// c:2462-2467 — subscripted-name PM_LOCAL guard: `else if
// ((on & PM_LOCAL) && locallevel) { ... if (!pm || pm->level
// != locallevel) zerrnam("can't create local array elements") }`.
// Refuses to create a NEW local for `local arr[N]=val` when
// the outer-scope pm at a different level exists. The Rust
// per-arg loop treats subscripted names as the eq-branch's
// `name[key]=val` shape inside assignsparam; the guard fires
// here BEFORE the assignment so we emit the C error message.
if let Some(br) = arg_name.find('[') {
let base = &arg_name[..br];
if (on as u32 & PM_LOCAL) != 0 && cur_locallevel != 0 { // c:2462
let pm_level = paramtab()
.read()
.ok()
.and_then(|t| t.get(base).map(|pm| pm.level));
if pm_level.is_none() || pm_level != Some(cur_locallevel) { // c:2466
zerrnam(
name,
&format!("{}: can't create local array elements", base), // c:2466
);
continue; // c:2467
}
}
}
if let Some(eq) = arg.find('=') {
let n = &arg[..eq];
let raw_v = &arg[eq + 1..];
// c:2945-3050 — `=(elem elem ...)` array-init syntax.
// The parser hands the whole `(...)` body in as one arg
// when typeset's BINF_MAGICEQUALS is set; the `(` / `)` are
// literal first/last bytes. Strip them and split on
// whitespace to recover the element list.
let is_paren_init = raw_v.starts_with('(') && raw_v.ends_with(')') && raw_v.len() >= 2;
if is_paren_init {
let inner = &raw_v[1..raw_v.len() - 1]; // c:2950
let elems: Vec<String> = inner
.split_whitespace() // c:2952
.map(String::from)
.collect();
if is_hashed {
// c:2960-2975 — `setdataparam(..., PM_HASHED, …)`.
// Two assoc-init shapes accepted by zsh:
// 1. flat alternating k/v: `m=(k1 v1 k2 v2)`
// 2. per-element [K]=V: `m=([k1]=v1 [k2]=v2)`
// The parser hands all elements as one `(…)` body,
// so we detect shape 2 when every element starts
// with `[` and contains `]=`. Otherwise fall back
// to alternating pairs.
let bracket_shape = !elems.is_empty()
&& elems.iter().all(|e| e.starts_with('[') && e.contains("]="));
let mut map: IndexMap<String, String> = IndexMap::new();
if bracket_shape {
for e in &elems {
let close = e.find("]=").unwrap();
let k = e[1..close].to_string();
let v = e[close + 2..].to_string();
map.insert(k, v);
}
} else {
let mut it = elems.into_iter(); // c:2960 pair walk
while let Some(k) = it.next() {
let v = it.next().unwrap_or_default();
map.insert(k, v); // c:2964 hashtab insert
}
}
crate::ported::exec_hooks::set_assoc(n, map.clone());
} else {
// c:2980-2995 — plain array.
crate::ported::exec_hooks::set_array(n, elems.clone());
}
// c:2510-2520 — `on = pm->node.flags;` then stamp the
// attribute bits on the just-assigned param. The
// scalar-assign arm below does the same; the array /
// assoc `=(...)` init path was missing this, so
// `typeset -ax ARR=(a b)` left PM_EXPORTED unset on
// the paramtab entry. `(t)ARR` then read `array`
// instead of `array-export`, and `typeset -p ARR`
// emitted `typeset -a ARR=...` instead of `-ax`.
let post_assign_mask = (PM_READONLY | PM_EXPORTED | PM_LEFT | PM_RIGHT_B
| PM_RIGHT_Z | PM_TAGGED | PM_HIDE | PM_HIDEVAL | PM_UNIQUE)
as i32;
let post_assign_to_set = (on
& (PM_READONLY | PM_EXPORTED | PM_LEFT | PM_RIGHT_B | PM_RIGHT_Z
| PM_TAGGED | PM_HIDE | PM_HIDEVAL | PM_UNIQUE))
as i32;
if post_assign_to_set != 0 {
if let Ok(mut tab) = paramtab().write() {
if let Some(pm) = tab.get_mut(n) {
pm.node.flags = (pm.node.flags & !post_assign_mask) | post_assign_to_set;
}
}
}
} else {
// c:3010-3030 — `name=value` scalar assign. C-canonical
// `setsparam` (Src/params.c:3350) writes paramtab; the
// env mirror at `Src/params.c:3024 addenv` follows.
// c:Src/params.c PM_LOWER/PM_UPPER setstrvalue arms:
// when typeset -l or -u is set, the assigned value is
// case-folded BEFORE storage. Without this, `typeset -l
// s=HELLO; echo $s` printed `HELLO`. We also mirror to
// exec.var_attrs so subsequent plain assigns (`s=NEW`)
// pick up the fold via the SET_VAR opcode's attr
// check (fusevm_bridge.rs case-fold arm).
let lower = (on & PM_LOWER) != 0;
let upper = (on & PM_UPPER) != 0;
let folded: String = if lower {
raw_v.to_lowercase()
} else if upper {
raw_v.to_uppercase()
} else {
raw_v.to_string()
};
// c:typeset_single — createparam with the type flag
// BEFORE assignsparam, so assignstrvalue's PM_TYPE
// dispatch (params.c:2748) routes the value through
// the correct setfn:
// - PM_INTEGER → intsetfn (mathevali → u_val)
// - PM_EFLOAT/PM_FFLOAT → floatsetfn (parsefloat → u_dval)
// - PM_SCALAR → strsetfn (u_str)
// The previous Rust ordering (setsparam first, then
// flip flags) wrote "5" to u_str then changed PM_TYPE
// to PM_INTEGER without migrating u_str → u_val, so
// getsparam(n) read u_val=0 instead of 5.
// c:2748-2784 — pre-assign type flags only (PM_INTEGER
// etc. — affect storage / setfn dispatch). Post-assign
// attributes (PM_READONLY / PM_EXPORTED / justification
// bits) are stamped AFTER setsparam since the C path
// (c:2475 `assignsparam(pname, val, 0)` → c:2510
// `on = pm->node.flags`) sets PM_READONLY only after
// the value lands. Mixing them pre-assign caused
// `readonly y=hello` to error "read-only variable: y"
// — the freshly-created pm had PM_READONLY which
// blocked its OWN initial assign.
let pre_assign_mask =
(PM_INTEGER | PM_EFLOAT | PM_FFLOAT | PM_LOWER | PM_UPPER | PM_NAMEREF) as i32;
let pre_assign_to_set = (on
& (PM_INTEGER | PM_EFLOAT | PM_FFLOAT | PM_LOWER | PM_UPPER | PM_NAMEREF))
as i32;
if pre_assign_to_set != 0 {
let pname_in_tab = paramtab()
.read()
.map(|t| t.contains_key(n))
.unwrap_or(false);
if !pname_in_tab {
// c:1132+ createparam(name, type_flags) — fresh.
let _ = createparam(n, pre_assign_to_set);
} else {
// c:2355-2378 tc (type-conversion) — flip the
// PM_TYPE bits on the existing param BEFORE
// re-assigning so assignstrvalue routes through
// the new type's setfn.
if let Ok(mut tab) = paramtab().write() {
if let Some(pm) = tab.get_mut(n) {
pm.node.flags =
(pm.node.flags & !pre_assign_mask) | pre_assign_to_set;
}
}
}
}
setsparam(n, &folded); // c:params.c:3350
// c:2510-2520 — `on = pm->node.flags;` then stamp the
// attribute bits on the just-assigned param.
let post_assign_mask = (PM_READONLY | PM_EXPORTED | PM_LEFT | PM_RIGHT_B
| PM_RIGHT_Z | PM_TAGGED | PM_HIDE | PM_HIDEVAL | PM_UNIQUE)
as i32;
let post_assign_to_set = (on
& (PM_READONLY | PM_EXPORTED | PM_LEFT | PM_RIGHT_B | PM_RIGHT_Z
| PM_TAGGED | PM_HIDE | PM_HIDEVAL | PM_UNIQUE))
as i32;
if post_assign_to_set != 0 {
if let Ok(mut tab) = paramtab().write() {
if let Some(pm) = tab.get_mut(n) {
pm.node.flags =
(pm.node.flags & !post_assign_mask) | post_assign_to_set;
}
}
}
// c:1973-1989 (Src/builtin.c, inside typeset_single)
// — `if (arg) { int base = zstrtol(arg, ...) ;
// pm->base = base; }`. The precision arg from `-i N`,
// `-E N`, `-F N` (parsed by execbuiltin as
// ops.args[<F-arg-slot>]) lands on the param's `base`
// field, which convfloat reads as the format-digit
// count. Without this stamp, `typeset -F 2 x=3.14`
// ignored the `2` and printed at the default
// 10-digit precision.
{
let prec_arg: Option<&str> =
if (on & PM_INTEGER) != 0 && OPT_HASARG(&ops, b'i') {
OPT_ARG(&ops, b'i')
} else if (on & PM_EFLOAT) != 0 && OPT_HASARG(&ops, b'E') {
OPT_ARG(&ops, b'E')
} else if (on & PM_FFLOAT) != 0 && OPT_HASARG(&ops, b'F') {
OPT_ARG(&ops, b'F')
} else {
None
};
if let Some(s) = prec_arg {
if let Ok(b) = s.trim().parse::<i32>() {
if let Ok(mut tab) = paramtab().write() {
if let Some(pm) = tab.get_mut(n) {
pm.base = b;
}
}
}
}
}
// c:2009-2014 typeset_setwidth — `-L N` / `-R N` / `-Z N`
// install pm.width = N. The auto-fallback in assignsparam
// (PM_INTEGER block at params.rs:3619) stamps width to
// s.len() when PM_RIGHT_Z is set but width==0; that lands
// BEFORE the post-assign PM_RIGHT_Z stamp here, so the
// user's explicit `-Z 6` was being overwritten by the
// value's char count. Set width AFTER setsparam so the
// explicit option arg wins over the auto-fallback.
{
let width_arg: Option<&str> =
if (on as u32 & PM_LEFT) != 0 && OPT_HASARG(&ops, b'L') {
OPT_ARG(&ops, b'L')
} else if (on as u32 & PM_RIGHT_B) != 0 && OPT_HASARG(&ops, b'R') {
OPT_ARG(&ops, b'R')
} else if (on as u32 & PM_RIGHT_Z) != 0 && OPT_HASARG(&ops, b'Z') {
OPT_ARG(&ops, b'Z')
} else {
None
};
if let Some(s) = width_arg {
if let Ok(w) = s.trim().parse::<i32>() {
if let Ok(mut tab) = paramtab().write() {
if let Some(pm) = tab.get_mut(n) {
pm.width = w;
}
}
}
}
}
// c:Src/params.c:3024 addenv — only mirror to OS env
// when PM_EXPORTED is in flags or already-exported.
let already_exported = env::var_os(n).is_some();
if (on & PM_EXPORTED) != 0 || already_exported {
env::set_var(n, &folded); // c:3024 addenv
}
}
} else if is_hashed || is_array {
// c:3060-3070 — bare name + `-A`/`-a` declares an empty
// assoc/array.
if is_hashed {
if crate::ported::exec_hooks::assoc(arg).is_none() {
crate::ported::exec_hooks::set_assoc(arg, IndexMap::new());
}
} else if crate::ported::exec_hooks::array(arg).is_none() {
crate::ported::exec_hooks::set_array(arg, Vec::new());
}
// c:Src/params.c:4087 arrsetfn — when PM_UNIQUE is set on
// an existing array, the canonical setfn applies
// `uniqarray()` to the current contents. `typeset -aU arr`
// on an existing arr must dedupe in place; without this,
// the flag stamp lands on pm.flags but the value stays
// un-deduped until the next assignment.
if is_array && (on as u32 & PM_UNIQUE) != 0 {
let current = crate::ported::exec_hooks::array(arg).unwrap_or_default();
// simple_arrayuniq is the in-place dedupe used by
// params.rs arrsetfn (PM_UNIQUE path).
let deduped = {
let mut seen = std::collections::HashSet::new();
current
.into_iter()
.filter(|x| seen.insert(x.clone()))
.collect::<Vec<_>>()
};
crate::ported::exec_hooks::set_array(arg, deduped);
}
// Stamp attribute bits on paramtab entry — same set as
// the `name=value` post-assign mask.
let post_assign_mask = (PM_READONLY | PM_EXPORTED | PM_LEFT | PM_RIGHT_B
| PM_RIGHT_Z | PM_TAGGED | PM_HIDE | PM_HIDEVAL | PM_UNIQUE | PM_HASHED
| PM_ARRAY)
as i32;
let post_assign_to_set = (on
& (PM_READONLY | PM_EXPORTED | PM_LEFT | PM_RIGHT_B | PM_RIGHT_Z
| PM_TAGGED | PM_HIDE | PM_HIDEVAL | PM_UNIQUE | PM_HASHED | PM_ARRAY))
as i32;
if post_assign_to_set != 0 {
if let Ok(mut tab) = paramtab().write() {
if let Some(pm) = tab.get_mut(arg_name) {
pm.node.flags = (pm.node.flags & !post_assign_mask) | post_assign_to_set;
}
}
}
} else {
// c:2355-2378 (typeset_single tc branch) — bare `typeset -i n`
// / `-F n` / `-E n` / `-l n` / `-u n` / `-r n` / `export N`
// / `readonly N` converts/stamps the existing param. Split
// into pre-assign (type conversion) and post-assign
// (attribute stamp) the same way the `name=value` arm does.
let pre_assign_mask =
(PM_INTEGER | PM_EFLOAT | PM_FFLOAT | PM_LOWER | PM_UPPER | PM_NAMEREF) as i32;
let pre_assign_to_set = (on
& (PM_INTEGER | PM_EFLOAT | PM_FFLOAT | PM_LOWER | PM_UPPER | PM_NAMEREF))
as i32;
let post_assign_mask = (PM_READONLY | PM_EXPORTED | PM_LEFT | PM_RIGHT_B
| PM_RIGHT_Z | PM_TAGGED | PM_HIDE | PM_HIDEVAL | PM_UNIQUE)
as i32;
let post_assign_to_set = (on
& (PM_READONLY | PM_EXPORTED | PM_LEFT | PM_RIGHT_B | PM_RIGHT_Z
| PM_TAGGED | PM_HIDE | PM_HIDEVAL | PM_UNIQUE))
as i32;
// c:2374 — `s = ztrdup(getsparam(pname));`. Capture the
// pre-conversion scalar value so the re-assignment after
// type flip preserves it through the new setfn.
let saved_val = getsparam(arg);
if getsparam(arg).is_none() {
// c:3072 — `if (!getsparam(arg)) setsparam(arg, "")`.
setsparam(arg, ""); // c:3074
}
if pre_assign_to_set != 0 {
if let Ok(mut tab) = paramtab().write() {
if let Some(pm) = tab.get_mut(arg) {
pm.node.flags =
(pm.node.flags & !pre_assign_mask) | pre_assign_to_set;
}
}
// c:2372-2378 — re-assign saved value through new type's
// setfn so u_val (for PM_INTEGER) or u_dval (for PM_*FLOAT)
// catches the value migration from u_str.
if let Some(ref val) = saved_val {
setsparam(arg, val);
}
}
// c:1973-1989 (Src/builtin.c, inside typeset_single):
// if (arg) {
// int base = zstrtol(arg, ..., 10);
// pm->base = base;
// }
// The precision arg from `-i N`, `-E N`, `-F N` (parsed
// by execbuiltin as `ops.args[<F-arg-slot>]`) lands on
// the param's `base` field, which `convfloat` (c:5689 in
// params.c) reads as the format-digit count. The
// `name=value` arm above (c:2009-2014 typeset_setwidth
// companion path) already stamps `pm.base`; this bare-
// declare arm was missing it, so `typeset -F 4 f` left
// pm.base=0 and `convfloat` rendered with the default
// 10-digit precision (parity bug #29). With this stamp
// a subsequent `(( f = ... ))` re-assignment preserves
// the `pm.base = 4` set here through `assignnparam`'s
// re-assign path (params.rs:5340-5374, c:2874-2878).
{
let prec_arg: Option<&str> =
if (on & PM_INTEGER) != 0 && OPT_HASARG(&ops, b'i') {
OPT_ARG(&ops, b'i') // c:1974 -i N
} else if (on & PM_EFLOAT) != 0 && OPT_HASARG(&ops, b'E') {
OPT_ARG(&ops, b'E') // c:1977 -E N
} else if (on & PM_FFLOAT) != 0 && OPT_HASARG(&ops, b'F') {
OPT_ARG(&ops, b'F') // c:1980 -F N
} else {
None
};
if let Some(s) = prec_arg {
if let Ok(b) = s.trim().parse::<i32>() { // c:1985 zstrtol
if let Ok(mut tab) = paramtab().write() {
if let Some(pm) = tab.get_mut(arg) {
pm.base = b; // c:1987 pm->base = base
}
}
}
}
}
// c:2510+ — stamp post-assign attributes (PM_EXPORTED,
// PM_READONLY, etc.) on the (possibly newly-created) pm.
if post_assign_to_set != 0 {
if let Ok(mut tab) = paramtab().write() {
if let Some(pm) = tab.get_mut(arg) {
pm.node.flags =
(pm.node.flags & !post_assign_mask) | post_assign_to_set;
}
}
// c:Src/params.c:3024 addenv — mirror PM_EXPORTED to OS env.
if (on as u32 & PM_EXPORTED) != 0 {
if let Some(val) = saved_val.as_deref().or(Some("")) {
env::set_var(arg, val);
}
}
}
}
}
unqueue_signals();
0
}
/// Port of `eval_autoload(Shfunc shf, char *name, Options ops, int func)` from Src/builtin.c:3166.
/// C: `int eval_autoload(Shfunc shf, char *name, Options ops, int func)`.
/// PM_UNDEFINED guard; -X spawns the eval-trampoline, otherwise loadautofn
/// resolves and installs the body.
/// WARNING: param names don't match C — Rust=(shf, name, func) vs C=(shf, name, ops, func)
pub fn eval_autoload(
shf: *mut shfunc,
name: &str, // c:3166
ops: &options,
func: i32,
) -> i32 {
if shf.is_null() {
return 1;
}
let shf_mut = unsafe { &mut *shf };
// c:3168-3169 — `if (!(shf->node.flags & PM_UNDEFINED)) return 1;`
if (shf_mut.node.flags as u32 & PM_UNDEFINED) == 0 {
// c:3168
return 1; // c:3169
}
// c:3171-3174 — `if (shf->funcdef) { freeeprog(shf->funcdef); shf->funcdef = &dummy_eprog; }`
if shf_mut.funcdef.is_some() {
// c:3171
shf_mut.funcdef = None; // c:3173 freeeprog + dummy
}
// c:3175-3181 — `-X` spawns the autoload trampoline via bin_eval.
if OPT_MINUS(ops, b'X') {
// c:3175
// c:3177 — `fargv[0] = quotestring(name, QT_SINGLE_OPTIONAL); fargv[1] = "\"$@\"";`
let fargv = vec![
// c:3177-3179
quotedzputs(name),
"\"$@\"".to_string(),
];
// c:3180 — `shf->funcdef = mkautofn(shf);`
let p = mkautofn(shf); // c:3180
let _ = p; // funcdef writeback handled inside mkautofn at c:3801
return bin_eval(name, &fargv, ops, func); // c:3181
}
// c:3184-3186 — `return !loadautofn(shf, (OPT_ISSET('k') ? 2 :
// (OPT_ISSET('z') ? 0 : 1)), 1,
// OPT_ISSET('d'));`
let mode = if OPT_ISSET(ops, b'k') {
2
}
// c:3184
else if OPT_ISSET(ops, b'z') {
0
}
// c:3185
else {
1
};
let _d = OPT_ISSET(ops, b'd');
// loadautofn lives in Src/exec.c:5050 — full fpath search + parse_string
// + install. Static-link path: returns 0 (success), so `!loadautofn` is 1.
let r = loadautofn(shf, mode, 1, _d as i32); // c:3193
if r == 0 {
1
} else {
0
}
}
/// Port of `check_autoload(Shfunc shf, char *name, Options ops, int func)` from Src/builtin.c:3193.
/// C: `static int check_autoload(Shfunc shf, char *name, Options ops,
/// int func)` — `OPT_ISSET(ops,'X')` ? eval_autoload : 0.
/// WARNING: param names don't match C — Rust=(shf, name, func) vs C=(shf, name, ops, func)
pub fn check_autoload(
shf: *mut shfunc,
name: &str, // c:3193
ops: &options,
func: i32,
) -> i32 {
// c:3196-3199 — `if (OPT_ISSET(ops,'X')) return eval_autoload(...);`
if OPT_ISSET(ops, b'X') {
// c:3196
return eval_autoload(shf, name, ops, func); // c:3197
}
// c:3200-3242 — -r / -R re-resolve: walk fpath for the function file.
let want_r = OPT_ISSET(ops, b'r');
let want_R = OPT_ISSET(ops, b'R');
if (want_r || want_R) && !shf.is_null() {
// c:3200
let shf_mut = unsafe { &mut *shf };
if (shf_mut.node.flags as u32 & PM_UNDEFINED) == 0 {
return 0;
}
// c:3202-3216 — already has filename + PM_LOADDIR: try the cached
// dir first via spec_path[].
if (shf_mut.node.flags as u32 & PM_LOADDIR) != 0 && shf_mut.filename.is_some() {
let spec = vec![shf_mut.filename.clone().unwrap_or_default()];
if getfpfunc(
&shf_mut.node.nam,
&mut None, // c:3206
Some(&spec),
1,
)
.is_some()
{
return 0; // c:3209
}
// c:3211-3217 — `-d` not set: bail (with -R = error, with -r = silent).
if !OPT_ISSET(ops, b'd') {
// c:3211
if want_R {
// c:3212
zerr(&format!(
"{}: function definition file not found",
shf_mut.node.nam
)); // c:3213
return 1; // c:3215
}
return 0; // c:3216
}
}
// c:3219-3231 — fpath walk via getfpfunc + dircache_set install.
let mut dir_path: Option<String> = None;
if getfpfunc(&shf_mut.node.nam, &mut dir_path, None, 1).is_some() // c:3219
&& dir_path.is_some()
{
// c:3220-3228 — dircache_set + relative-path absolutize.
let mut old_slot = shf_mut.filename.take();
dircache_set(&mut old_slot, None); // c:3220
let dp = dir_path.unwrap();
let mut new_slot: Option<String> = None;
dircache_set(&mut new_slot, Some(&dp)); // c:3228
shf_mut.filename = new_slot;
shf_mut.node.flags |= PM_LOADDIR as i32; // c:3229
return 0; // c:3230
}
// c:3233-3239 — -R: error; -r: silent.
if want_R {
// c:3233
zerr(&format!(
"{}: function definition file not found",
shf_mut.node.nam
)); // c:3243
return 1; // c:3243
}
}
0 // c:3243
}
/// Port of `listusermathfunc(MathFunc p)` from Src/builtin.c:3243.
/// C: `static void listusermathfunc(MathFunc p)` — emit a `functions -M`
/// row for one user math function with arg counts and module name.
pub fn listusermathfunc(p: &mathfunc) {
// c:3243
// c:3247-3257 — pick `showargs` 0..3 based on module/min/max presence.
let mut showargs: i32 = if p.module.is_some() {
// c:3249
3
} else if p.maxargs != if p.minargs != 0 { p.minargs } else { -1 } {
// c:3251
2
} else if p.minargs != 0 {
// c:3253
1
} else {
0 // c:3256
};
// c:3259 — `printf("functions -M%s %s", (p->flags & MFF_STR) ? "s" : "", p->name);`
let s_suffix = if (p.flags & MFF_STR) != 0 { "s" } else { "" }; // c:3259
print!("functions -M{} {}", s_suffix, p.name); // c:3259
if showargs != 0 {
// c:3260
print!(" {}", p.minargs); // c:3261
showargs -= 1; // c:3262
}
if showargs != 0 {
// c:3264
print!(" {}", p.maxargs); // c:3265
showargs -= 1; // c:3266
}
if showargs != 0 {
// c:3268
// c:3269-3274 — function names are not required to be ident chars,
// so the module name goes through quotedzputs for safe printing.
print!(" "); // c:3273
print!(
"{}",
quotedzputs(p.module.as_deref().unwrap_or(""))
); // c:3274
showargs -= 1; // c:3275
}
println!(); // c:3277
}
/// Port of `add_autoload_function(Shfunc shf, char *funcname)` from Src/builtin.c:3278.
/// C: `static void add_autoload_function(Shfunc shf, char *funcname)` —
/// two branches:
/// (a) funcname is absolute & shf is PM_UNDEFINED → split `/dir/nam`,
/// dircache_set(&shf->filename, dir), set PM_LOADDIR|PM_ABSPATH_USED,
/// shfunctab->addnode(nam, shf).
/// (b) otherwise → walk funcstack to find calling function; if it has
/// PM_LOADDIR|PM_ABSPATH_USED, build `"<calling-dir>/funcname"` and
/// access(R_OK); on success copy the dir into shf and set
/// PM_LOADDIR|PM_ABSPATH_USED. Then shfunctab->addnode(funcname, shf).
/// WARNING: param names don't match C — Rust=(shf) vs C=(shf, funcname)
pub fn add_autoload_function(
shf: *mut shfunc, // c:3278
funcname: &str,
) {
if shf.is_null() || funcname.is_empty() {
return;
}
let shf_ref = unsafe { &mut *shf };
let is_abs_path = funcname.starts_with('/') // c:3282
&& funcname.len() > 1
&& funcname[1..].contains('/')
&& (shf_ref.node.flags as u32 & PM_UNDEFINED) != 0;
if is_abs_path {
// c:3287 — `nam = strrchr(funcname, '/');`
let nam_idx = funcname.rfind('/').unwrap(); // c:3287
let (dir, nam) = if nam_idx == 0 {
// c:3289
("/".to_string(), funcname[1..].to_string()) // c:3290
} else {
(
funcname[..nam_idx].to_string(), // c:3293
funcname[nam_idx + 1..].to_string(),
)
};
// c:3296 — `dircache_set(&shf->filename, NULL); dircache_set(..., dir);`
let mut old_slot = shf_ref.filename.take();
dircache_set(&mut old_slot, None); // c:3296
let mut new_slot: Option<String> = None;
dircache_set(&mut new_slot, Some(&dir)); // c:3297
shf_ref.filename = new_slot;
// c:3298-3299 — `shf->node.flags |= PM_LOADDIR | PM_ABSPATH_USED;`
shf_ref.node.flags |= (PM_LOADDIR | PM_ABSPATH_USED) as i32; // c:3298
// c:3300 — `shfunctab->addnode(shfunctab, ztrdup(nam), shf);`
let _ = nam;
if let Ok(mut t) = shfunctab_lock().write() {
t.addnode(shf); // c:3300
}
} else {
// c:3304-3327 — walk funcstack, look up calling fn in shfunctab, if
// it has PM_LOADDIR|PM_ABSPATH_USED build "<dir>/<funcname>" and
// access(R_OK), inherit the dir on hit.
let calling_f: Option<String> = {
let stack = FUNCSTACK
.lock()
.map(|s| s.clone())
.unwrap_or_default();
// c:3306 — `for (fs = funcstack; fs; fs = fs->prev)`
stack
.iter()
.rev()
.find(|fs| {
// c:3306
// c:3307 — `if (fs->tp == FS_FUNC && fs->name &&
// (!shf->node.nam || strcmp(fs->name, shf->node.nam)))`
FS_FUNC != 0 // mirror struct doesn't expose tp directly;
&& !fs.name.is_empty()
&& (shf_ref.node.nam.is_empty() || fs.name != shf_ref.node.nam)
})
.map(|fs| fs.name.clone()) // c:3308
};
if let Some(cf) = calling_f {
// c:3315
// c:3316 — `shf2 = shfunctab->getnode2(shfunctab, calling_f);`
let shf2_ptr = shfunctab_lock()
.read()
.map(|t| t.getnode2(&cf))
.unwrap_or(std::ptr::null_mut());
if !shf2_ptr.is_null() {
let shf2 = unsafe { &*shf2_ptr };
// c:3317-3318
let needs = (PM_LOADDIR | PM_ABSPATH_USED) as i32;
if (shf2.node.flags & needs) == needs {
// c:3317
if let Some(dir2) = &shf2.filename {
// c:3318
// c:3320 — `snprintf(buf, PATH_MAX, "%s/%s", dir2, funcname);`
let buf = format!("{}/{}", dir2, funcname); // c:3320
if buf.len() <= libc::PATH_MAX as usize {
// c:3320
// c:3324 — `if (!access(buf, R_OK))`
let buf_c = std::ffi::CString::new(buf.clone()).ok();
if let Some(bc) = buf_c {
if unsafe { libc::access(bc.as_ptr(), libc::R_OK) } == 0 {
// c:3324
let mut old_slot = shf_ref.filename.take();
dircache_set(&mut old_slot, None); // c:3325
let dir2c = dir2.clone();
let mut new_slot: Option<String> = None;
dircache_set(&mut new_slot, Some(&dir2c)); // c:3326
shf_ref.filename = new_slot;
shf_ref.node.flags |= (PM_LOADDIR | PM_ABSPATH_USED) as i32;
// c:3327
}
}
}
}
}
}
}
// c:3334 — `shfunctab->addnode(shfunctab, ztrdup(funcname), shf);`
// addnode keys by `shf->node.nam`; if the caller picked a different
// funcname here, re-tag the node name first so the keyed insert
// matches the C contract.
unsafe {
if !shf.is_null() {
(*shf).node.nam = funcname.to_string();
}
}
if let Ok(mut t) = shfunctab_lock().write() {
t.addnode(shf); // c:3334
}
}
}
/// Port of `bin_functions(char *name, char **argv, Options ops, int func)` from Src/builtin.c:3342.
/// C: `int bin_functions(char *name, char **argv, Options ops, int func)`.
/// This is the canonical free-function port matching the C signature so
/// the dispatcher can call it. The earlier `ShellExecutor::bin_functions`
/// inherent method is an ad-hoc Rust-side helper kept for the existing
/// in-process executor; both should converge on this function.
/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
pub fn bin_functions(
name: &str,
argv: &[String], // c:3342
ops: &options,
_func: i32,
) -> i32 {
// c:3346-3347 — `int returnval = 0; int on = 0, off = 0, pflags = 0,
// roff, expand = 0;`
let mut returnval: i32 = 0; // c:3346
let mut on: u32 = 0; // c:3347
let mut off: u32 = 0; // c:3347
let _pflags: i32 = 0; // c:3347
let _expand: i32 = 0; // c:3347
// c:3350-3351 — `if (OPT_PLUS(ops,'u')) off |= PM_UNDEFINED; else if
// (OPT_MINUS(ops,'u') || OPT_ISSET(ops,'X')) on |= PM_UNDEFINED;`
if OPT_PLUS(ops, b'u') {
// c:3350
off |= PM_UNDEFINED; // c:3351
} else if OPT_MINUS(ops, b'u') || OPT_ISSET(ops, b'X') {
// c:3352
on |= PM_UNDEFINED; // c:3353
}
// c:3354-3357 — -U / +U toggle PM_UNALIASED|PM_UNDEFINED.
if OPT_MINUS(ops, b'U') {
// c:3354
on |= PM_UNALIASED | PM_UNDEFINED; // c:3355
} else if OPT_PLUS(ops, b'U') {
// c:3356
off |= PM_UNALIASED; // c:3357
}
// c:3358-3361 — -t / +t toggle PM_TAGGED.
if OPT_MINUS(ops, b't') {
// c:3358
on |= PM_TAGGED; // c:3359
} else if OPT_PLUS(ops, b't') {
// c:3360
off |= PM_TAGGED; // c:3361
}
// c:3362-3365 — -T / +T toggle PM_TAGGED_LOCAL.
if OPT_MINUS(ops, b'T') {
// c:3362
on |= PM_TAGGED_LOCAL; // c:3363
} else if OPT_PLUS(ops, b'T') {
// c:3364
off |= PM_TAGGED_LOCAL; // c:3365
}
// c:3366-3369 — -W / +W toggle PM_WARNNESTED.
if OPT_MINUS(ops, b'W') {
// c:3366
on |= PM_WARNNESTED; // c:3367
} else if OPT_PLUS(ops, b'W') {
// c:3368
off |= PM_WARNNESTED; // c:3369
}
// c:3370 — `roff = off;`
let mut roff = off; // c:3370
// c:3371-3377 — -z / +z PM_ZSHSTORED|PM_KSHSTORED interaction.
if OPT_MINUS(ops, b'z') {
// c:3371
on |= PM_ZSHSTORED; // c:3372
off |= PM_KSHSTORED; // c:3373
} else if OPT_PLUS(ops, b'z') {
// c:3374
off |= PM_ZSHSTORED; // c:3375
roff |= PM_ZSHSTORED; // c:3376
}
// c:3379-3385 — -k / +k PM_KSHSTORED|PM_ZSHSTORED interaction.
if OPT_MINUS(ops, b'k') {
// c:3379
on |= PM_KSHSTORED; // c:3380
off |= PM_ZSHSTORED; // c:3381
} else if OPT_PLUS(ops, b'k') {
// c:3382
off |= PM_KSHSTORED; // c:3383
roff |= PM_KSHSTORED; // c:3384
}
// c:3386-3392 — -d / +d PM_CUR_FPATH toggle.
if OPT_MINUS(ops, b'd') {
// c:3386
on |= PM_CUR_FPATH; // c:3387
off |= PM_CUR_FPATH; // c:3388
} else if OPT_PLUS(ops, b'd') {
// c:3389
off |= PM_CUR_FPATH; // c:3390
roff |= PM_CUR_FPATH; // c:3391
}
// c:3394-3400 — early-error validation: invalid flag combinations.
// C: `(OPT_MINUS(ops,'X') && (OPT_ISSET(ops,'m') || !scriptname))` —
// \`-X\` is only valid in a script context (autoload-from-fpath
// dispatch). Previous Rust port dropped the \`|| !scriptname\` half
// so \`functions -X foo\` from interactive shell silently
// succeeded — divergent.
let scriptname_missing = scriptname_get().is_none();
if (off & PM_UNDEFINED) != 0 // c:3394
|| (OPT_ISSET(ops, b'k') && OPT_ISSET(ops, b'z')) // c:3394
|| (OPT_ISSET(ops, b'x') && !OPT_HASARG(ops, b'x')) // c:3395
|| (OPT_MINUS(ops, b'X') // c:3396
&& (OPT_ISSET(ops, b'm') || scriptname_missing)) // c:3396 !scriptname
|| (OPT_ISSET(ops, b'c')
&& (OPT_ISSET(ops, b'x') || OPT_ISSET(ops, b'X') || OPT_ISSET(ops, b'm')))
{
zwarnnam(name, "invalid option(s)"); // c:3399
return 1; // c:3400
}
// c:3402-3452 — `-c` (clone) branch: copy named function under a new
// name, optionally registering it as a TRAP* signal trap.
if OPT_ISSET(ops, b'c') {
// c:3402
if argv.len() < 2 || argv.len() > 2 {
// c:3405
zwarnnam(name, "-c: requires two arguments"); // c:3406
return 1;
}
let src_name = &argv[0];
let dst_name = &argv[1];
// c:3409 — `shf = shfunctab->getnode(shfunctab, *argv);`
let src_ptr = shfunctab_lock()
.read()
.map(|t| t.getnode(src_name.as_str()))
.unwrap_or(std::ptr::null_mut());
if src_ptr.is_null() {
// c:3410
zwarnnam(name, &format!("no such function: {}", src_name)); // c:3411
return 1;
}
// c:3414-3421 — autoload-trampoline expansion if PM_UNDEFINED.
// C body: `if (shf->flags & PM_UNDEFINED) { freeeprog;
// funcdef=dummy; shf = loadautofn(shf,1,0,0); if (!shf) return 1; }`.
// Rust port routes through the local loadautofn helper at
// builtin.rs:883 which walks $fpath via getfpfunc, reads the
// file, stores the body text on the Rust-side ShFunc, and
// clears PM_UNDEFINED.
if (unsafe { (*src_ptr).node.flags } as u32 & PM_UNDEFINED) != 0 {
// c:3415-3418 — `freeeprog(shf->funcdef); shf->funcdef =
// &dummy_eprog;` clear out any stale autoload stub before
// re-loading. Rust port: drop the Option<Eprog>.
unsafe {
(*src_ptr).funcdef = None;
}
// c:3419 — `loadautofn(shf, 1, 0, 0)`.
if loadautofn(src_ptr, 1, 0, 0) != 0 {
// c:3420-3421 — autoload failed.
return 1;
}
}
// c:3422-3430 — `newsh = zalloc + memcpy + filename rebuild`.
let src_ref = unsafe { &*src_ptr };
let new_filename =
if (src_ref.node.flags as u32 & PM_UNDEFINED) == 0 && src_ref.filename.is_some() {
src_ref.filename.clone() // c:3429
} else {
None
};
let _ = new_filename; // wired into shfunctab[dst_name] below
// c:3437-3447 — TRAP* prefix detection + signal trap registration.
if dst_name.starts_with("TRAP") {
// c:3437
// c:3438 — `int sigidx = getsigidx(s + 4);`
let sigidx = getsigidx(&dst_name[4..]); // c:3438
if sigidx != -1 {
// c:3439
// c:3440 — `if (settrap(sigidx, NULL, ZSIG_FUNC))`.
if settrap(sigidx, None, ZSIG_FUNC) != 0 {
// c:3440
// freeeprog(newsh->funcdef) — funcdef Drop covers it.
// dircache_set(&newsh->filename, NULL);
// zfree(newsh, sizeof(*newsh));
return 1; // c:3445
}
// c:3447 — `removetrapnode(sigidx);` — clear any prior trap.
removetrapnode(sigidx); // c:3447
}
}
// c:3422-3430 — C does `newsh = zalloc + memcpy(*newsh, *shf)` so
// src and dst become independent copies. Box-clone the source body
// (rather than aliasing src_ptr) so subsequent mutation through
// `getnode(dst_name)` doesn't bleed into src.
// c:3450 — `shfunctab->addnode(shfunctab, ztrdup(s), &newsh->node);`
let newsh = unsafe {
let mut copy = (*src_ptr).clone();
copy.node.nam = dst_name.clone();
Box::into_raw(Box::new(copy))
};
if let Ok(mut t) = shfunctab_lock().write() {
t.addnode(newsh); // c:3450
}
return 0; // c:3451
}
// c:3454-3463 — `-x N` indent override for printing.
let mut expand: i32 = 0; // c:3454 (also c:3347)
if OPT_ISSET(ops, b'x') {
// c:3454
let arg = OPT_ARG(ops, b'x').unwrap_or("");
match arg.trim().parse::<i32>() {
// c:3456
Ok(n) => {
expand = n; // c:3456
if expand == 0 {
expand = -1;
} // c:3461-3462
}
Err(_) => {
zwarnnam(name, "number expected after -x"); // c:3458
return 1; // c:3459
}
}
}
// c:3465-3466 — `+f` / roff / `+` enables PRINT_NAMEONLY.
let mut pflags: i32 = 0;
if OPT_PLUS(ops, b'f') || roff != 0 || OPT_ISSET(ops, b'+') {
// c:3465
pflags |= PRINT_NAMEONLY; // c:3466
}
// c:3468-3530 — `-M`/`+M` add/remove/list math function path.
if OPT_MINUS(ops, b'M') || OPT_PLUS(ops, b'M') {
// c:3468
// c:3473-3477 — refuse incompatible flag combos.
if on != 0
|| off != 0
|| pflags != 0
|| OPT_ISSET(ops, b'X')
|| OPT_ISSET(ops, b'u')
|| OPT_ISSET(ops, b'U')
|| OPT_ISSET(ops, b'w')
{
zwarnnam(name, "invalid option(s)"); // c:3475
return 1; // c:3476
}
if argv.is_empty() {
// c:3478
// c:3479-3484 — list user math ported.
queue_signals(); // c:3480
if let Ok(table) = MATHFUNCS.lock() {
// c:3481
for p in table.iter() {
// c:3481
if (p.flags & MFF_USERFUNC) != 0 {
// c:3482
listusermathfunc(p); // c:3483
}
}
}
unqueue_signals(); // c:3484
return returnval;
} else if OPT_ISSET(ops, b'm') {
// c:3485
// c:3486-3515 — list/delete matching math ported by pattern.
for arg in argv.iter() {
queue_signals(); // c:3488
// c:3489 — `tokenize(*argv)`; Rust patcompile handles it.
if let Some(pprog) = patcompile(arg, PAT_STATIC, None) {
// c:3490
if OPT_PLUS(ops, b'M') {
// c:3497
// Delete matching user ported.
if let Ok(mut table) = MATHFUNCS.lock() {
table.retain(|p| {
!((p.flags & MFF_USERFUNC) != 0 && pattry(&pprog, &p.name))
});
}
} else {
// c:3502 — listusermathfunc for matches.
if let Ok(table) = MATHFUNCS.lock() {
for p in table.iter() {
if (p.flags & MFF_USERFUNC) != 0 && pattry(&pprog, &p.name) {
listusermathfunc(p);
}
}
}
}
} else {
// c:3509
// c:3510-3512 — bad pattern.
zwarnnam(
name, // c:3511
&format!("bad pattern : {}", arg),
);
returnval = 1; // c:3512
}
unqueue_signals(); // c:3514
}
return returnval;
} else if OPT_PLUS(ops, b'M') {
// c:3516
// c:3517-3533 — `+M name…` delete by exact name.
for arg in argv.iter() {
queue_signals(); // c:3519
if let Ok(mut table) = MATHFUNCS.lock() {
let idx = table.iter().position(|p| p.name == *arg); // c:3520-3521
if let Some(i) = idx {
if (table[i].flags & MFF_USERFUNC) == 0 {
// c:3522-3527 — library function, refuse.
zwarnnam(
name, // c:3523
&format!("+M {}: is a library function", arg),
);
returnval = 1; // c:3525
} else {
table.remove(i); // c:3528
}
}
}
unqueue_signals(); // c:3532
}
return returnval;
} else {
// c:3535-3611 — `-M name [min [max [mod]]]` add a user math fn.
let mut argv_iter = argv.iter();
let funcname = argv_iter.next().unwrap(); // c:3537
let mut minargs: i32;
let mut maxargs: i32;
if OPT_ISSET(ops, b's') {
// c:3541
minargs = 1; // c:3542
maxargs = 1; // c:3542
} else {
minargs = 0; // c:3544
maxargs = -1; // c:3545
}
// c:3548-3552 — bad math function name check.
let bytes = funcname.as_bytes();
let first_bad = bytes.is_empty()
|| (bytes[0] as char).is_ascii_digit()
|| !bytes
.iter()
.all(|&c| c.is_ascii_alphanumeric() || c == b'_');
if first_bad {
// c:3549
zwarnnam(
name, // c:3550
&format!("-M {}: bad math function name", funcname),
);
return 1; // c:3551
}
if let Some(arg) = argv_iter.next() {
// c:3554
match arg.parse::<i32>() {
// c:3555 zstrtol
Ok(n) if n >= 0 => minargs = n, // c:3556
_ => {
zwarnnam(
name, // c:3557
&format!("-M: invalid min number of arguments: {}", arg),
);
return 1; // c:3559
}
}
if OPT_ISSET(ops, b's') && minargs != 1 {
// c:3561
zwarnnam(
name, // c:3562
"-Ms: must take a single string argument",
);
return 1; // c:3563
}
maxargs = minargs; // c:3565
}
if let Some(arg) = argv_iter.next() {
// c:3568
match arg.parse::<i32>() {
// c:3569
Ok(n) if n >= -1 && (n == -1 || n >= minargs) => maxargs = n,
_ => {
zwarnnam(
name, // c:3573
&format!("-M: invalid max number of arguments: {}", arg),
);
return 1; // c:3576
}
}
if OPT_ISSET(ops, b's') && maxargs != 1 {
// c:3578
zwarnnam(
name, // c:3579
"-Ms: must take a single string argument",
);
return 1; // c:3580
}
}
let modname = argv_iter.next().cloned(); // c:3584-3585
if argv_iter.next().is_some() {
// c:3586
zwarnnam(name, "-M: too many arguments"); // c:3587
return 1; // c:3588
}
// c:3591-3598 — alloc and populate mathfunc.
let mut flags = MFF_USERFUNC; // c:3593
if OPT_ISSET(ops, b's') {
// c:3594
flags |= MFF_STR; // c:3595
}
let new_fn = mathfunc {
next: None, // c:3608 chain via Vec
name: funcname.clone(), // c:3592
flags, // c:3593
nfunc: None,
sfunc: None,
module: modname, // c:3596
minargs, // c:3597
maxargs, // c:3598
funcid: 0,
};
queue_signals(); // c:3600
if let Ok(mut table) = MATHFUNCS.lock() {
// c:3601-3606 — remove existing user entry with same name.
if let Some(i) = table.iter().position(|p| p.name == new_fn.name) {
table.remove(i); // c:3603
}
// c:3608-3609 — prepend to mathfuncs head.
table.insert(0, new_fn);
}
unqueue_signals(); // c:3610
return returnval;
}
}
// c:3616-3655 — `-X` re-autoload from inside a function.
if OPT_MINUS(ops, b'X') {
// c:3616
if argv.len() > 1 {
// c:3620
zwarnnam(name, "-X: too many arguments"); // c:3621
return 1; // c:3622
}
queue_signals(); // c:3624
// c:3625-3633 — walk funcstack to find the enclosing FS_FUNC frame.
let funcname: Option<String> = {
let stack = FUNCSTACK
.lock()
.map(|s| s.clone())
.unwrap_or_default();
stack
.iter()
.rev()
.find(|fs| !fs.name.is_empty()) // c:3626
.map(|fs| fs.name.clone()) // c:3631
};
let ret;
if funcname.is_none() {
// c:3635
// c:3637 — `zerrnam(name, "bad autoload");`
zwarnnam(name, "bad autoload"); // c:3637
ret = 1; // c:3638
} else {
let fname = funcname.unwrap();
// c:3640-3647 — getnode(shfunctab, funcname) || addnode(new shf).
let mut shf_ptr = shfunctab_lock()
.read()
.map(|t| t.getnode(fname.as_str()))
.unwrap_or(std::ptr::null_mut());
if !shf_ptr.is_null() { // c:3640
// exists already
} else {
// c:3645 — `shf = zshcalloc(sizeof *shf);`
// `shfunctab->addnode(shfunctab, ztrdup(funcname), shf);`
let new_shf = Box::into_raw(Box::new(shfunc {
node: hashnode {
next: None,
nam: fname.clone(),
flags: 0,
},
filename: None,
lineno: 0,
funcdef: None,
redir: None,
sticky: None,
body: None,
}));
if let Ok(mut t) = shfunctab_lock().write() {
t.addnode(new_shf); // c:3646
}
shf_ptr = new_shf;
}
if !argv.is_empty() {
// c:3648
if !shf_ptr.is_null() {
let shf_mut = unsafe { &mut *shf_ptr };
let mut old_slot = shf_mut.filename.take();
dircache_set(&mut old_slot, None); // c:3649
let mut new_slot: Option<String> = None;
dircache_set(&mut new_slot, Some(&argv[0])); // c:3650
shf_mut.filename = new_slot;
on |= PM_LOADDIR; // c:3294 — `shf->node.flags |= PM_LOADDIR;`
}
}
// c:3653 — `shf->node.flags = on;`
// c:3654 — `ret = eval_autoload(shf, funcname, ops, func);`
ret = eval_autoload(shf_ptr, &fname, ops, _func); // c:3654
}
unqueue_signals(); // c:3656
return ret;
}
// c:3658-3669 — no-arg listing path: print all (non-DISABLED) shfuncs
// matching `on|off` mask through scanshfunc + printnode.
if argv.is_empty() {
// c:3658
queue_signals(); // c:3663
if OPT_ISSET(ops, b'U') && !OPT_ISSET(ops, b'u') {
// c:3664
on &= !PM_UNDEFINED; // c:3665
}
// c:3666 — `scanshfunc(1, on|off, DISABLED, shfunctab->printnode,
// pflags, expand);` — walk every (non-DISABLED) shfunc
// and emit printnode in the format chosen by `pflags`. PRINT_NAMEONLY
// → just the name; otherwise full `name () { … }` shape with autoload
// stubs printed as `name () { # undefined; builtin autoload -XU }`.
scanshfunc(|_nm, entry| {
printshfuncexpand(entry, pflags, expand);
});
unqueue_signals(); // c:3668
return returnval;
}
// c:3672-3708 — `-m` glob: treat each arg as a pattern, scan-and-print
// matching shfuncs (no on/off → list) or apply on/off mask.
if OPT_ISSET(ops, b'm') {
// c:3673
on &= !PM_UNDEFINED; // c:3674
let mut returnval = returnval;
for pat in argv {
// c:3675
queue_signals(); // c:3676
// c:3678 — `tokenize(*argv)` + `patcompile(...)`
let pprog = patcompile(
pat, // c:3680
PAT_HEAPDUP,
None,
);
if let Some(prog) = pprog {
// c:3680-3683 — scan-and-print matching shfuncs.
if (on | off) == 0 && !OPT_ISSET(ops, b'X') {
// c:3682
// c:3682-3683 — `scanmatchshfunc(pprog, 1, 0,
// DISABLED, shfunctab->printnode, pflags, expand)`.
// Walk shfunctab via the hashtable.rs port and emit
// each match through `printshfuncexpand` so autoload
// stubs come out as `name () { # undefined; builtin
// autoload -XU }` and loaded funcs print their body.
scanmatchshfunc(Some(pat), |_nm, entry| {
printshfuncexpand(entry, pflags, expand);
});
} else {
// c:3686-3699 — walk shfunctab, apply (on, off) and
// re-eval autoload for each matching shf.
let names: Vec<String> = shfunctab_lock()
.read()
.map(|t| t.iter().map(|(k, _)| k.clone()).collect())
.unwrap_or_default();
for nm in &names {
// pattry approximated by string equality / glob
// here; full pat engine is in src/ported/pattern.rs.
if !pattry(&prog, nm) {
// c:3690
continue;
}
let shf_ptr = shfunctab_lock()
.read()
.map(|t| t.getnode(nm.as_str()))
.unwrap_or(std::ptr::null_mut());
if shf_ptr.is_null() {
continue;
}
let shf_mut = unsafe { &mut *shf_ptr };
// c:3691 — `shf->node.flags = (... | (on & ~PM_UNDEFINED)) & ~off;`
shf_mut.node.flags =
(shf_mut.node.flags | ((on & !PM_UNDEFINED) as i32)) & !(off as i32); // c:3691
if check_autoload(shf_ptr, &shf_mut.node.nam, ops, _func) != 0 {
// c:3693
returnval = 1; // c:3695
}
}
}
} else {
// c:3700-3702 — `untokenize + zwarnnam(name, "bad pattern")`.
zwarnnam(name, &format!("bad pattern : {}", pat)); // c:3701
returnval = 1; // c:3702
}
unqueue_signals(); // c:3704
}
return returnval;
}
// c:3710-3735 — literal name list, no globbing.
let mut returnval = returnval;
queue_signals(); // c:3711
for fname in argv {
// c:3712
// c:3713-3714 — `-w` (compile-and-dump) path.
if OPT_ISSET(ops, b'w') {
// c:3713
// dump_autoload(name, fname, on, ops, func) — dump.c port.
continue;
}
// c:3715 — `shf = shfunctab->getnode(shfunctab, *argv);`
let shf_ptr = shfunctab_lock()
.read()
.map(|t| t.getnode(fname.as_str()))
.unwrap_or(std::ptr::null_mut());
if !shf_ptr.is_null() {
// c:3715
let shf_mut = unsafe { &mut *shf_ptr };
if (on | off) != 0 {
// c:3717
// c:3719 — apply on/off mask, then check_autoload.
shf_mut.node.flags =
(shf_mut.node.flags | ((on & !PM_UNDEFINED) as i32)) & !(off as i32); // c:3719
if check_autoload(shf_ptr, &shf_mut.node.nam, ops, _func) != 0 {
// c:3720
returnval = 1; // c:3721
}
} else {
// c:3723 — `printshfuncexpand(&shf->node, pflags, expand);`
// C prints the function via shfunctab.printnode honoring
// pflags (PRINT_NAMEONLY / verbose). The previous Rust
// port just printed the name — `functions f` skipped
// the `f () { ... body ... }` body listing entirely.
printshfuncexpand(shf_mut, pflags, expand); // c:3723
}
} else if (on & PM_UNDEFINED) != 0 {
// c:3725
// c:3726-3782 — autoload-define path: TRAP* + abs-path + new shf.
let mut sigidx: i32 = -1;
let mut ok = true;
// c:3728-3735 — TRAP* prefix → removetrapnode(sigidx).
if fname.starts_with("TRAP") {
// c:3728
// c:3729 — `if ((sigidx = getsigidx(*argv + 4)) != -1)`
sigidx = getsigidx(&fname[4..]); // c:3729
if sigidx != -1 {
// c:3729
// c:3733 — `removetrapnode(sigidx);`
removetrapnode(sigidx); // c:3733
}
}
// c:3737-3759 — absolute path /dir/base form: install dir on
// existing matching base name with PM_UNDEFINED set.
if fname.starts_with('/') {
// c:3737
let base = fname.rsplit('/').next().unwrap_or("");
if !base.is_empty() {
let base_ptr = shfunctab_lock()
.read()
.map(|t| t.getnode(base))
.unwrap_or(std::ptr::null_mut());
if !base_ptr.is_null() {
let bs = unsafe { &mut *base_ptr };
// c:3742 — apply flag mask.
bs.node.flags =
(bs.node.flags | ((on & !PM_UNDEFINED) as i32)) & !(off as i32); // c:3742
if (bs.node.flags as u32 & PM_UNDEFINED) != 0 {
// c:3744
let dir = if fname.len() > 1 && base.len() == fname.len() - 1 {
"/".to_string() // c:3747
} else {
fname[..fname.len() - base.len() - 1].to_string()
// c:3749-3751
};
let mut old_slot = bs.filename.take();
dircache_set(&mut old_slot, None); // c:3753
let mut new_slot: Option<String> = None;
dircache_set(&mut new_slot, Some(&dir)); // c:3754
bs.filename = new_slot;
}
if check_autoload(base_ptr, &bs.node.nam, ops, _func) != 0 {
// c:3756
returnval = 1;
}
continue; // c:3758
}
}
}
// c:3763-3766 — new undefined shf, mkautofn, add_autoload_function.
let new_shf = Box::new(shfunc {
node: hashnode {
next: None,
nam: fname.clone(),
flags: on as i32, // c:3764
},
filename: None,
lineno: 0,
funcdef: None,
redir: None,
sticky: None,
body: None,
});
let new_shf_ptr = Box::into_raw(new_shf);
let _ = mkautofn(new_shf_ptr); // c:3765
add_autoload_function(new_shf_ptr, fname); // c:3767
if sigidx != -1 {
// c:3769
// c:3770 — `if (settrap(sigidx, NULL, ZSIG_FUNC)) { ... }`
if settrap(sigidx, None, ZSIG_FUNC) != 0 {
// c:3770
// c:3771 — `shfunctab->removenode(shfunctab, *argv);`
if let Ok(mut t) = shfunctab_lock().write() {
t.remove(fname);
}
// c:3772 — `shfunctab->freenode(&shf->node);` Drop covers it.
returnval = 1; // c:3773
ok = false; // c:3774
}
}
if ok && check_autoload(new_shf_ptr, &fname, ops, _func) != 0 {
// c:3779
returnval = 1; // c:3780
}
} else {
// c:3783 — `returnval = 1;` (named function not found,
// no autoload requested).
returnval = 1; // c:3783
}
}
unqueue_signals(); // c:3785
let _ = (expand, pflags);
returnval
}
/// Port of `mkautofn(Shfunc shf)` from Src/builtin.c:3790.
/// C: `Eprog mkautofn(Shfunc shf)` — synthesize a 5-wordcode body that
/// re-fires the autoload mechanism when first called.
pub fn mkautofn(shf: *mut shfunc) -> *mut eprog {
// c:3790
// c:3793-3810 — alloc Eprog with 5 wordcode slots, set p->shf, p->npats=0,
// p->nref=1 (permanent). Static-link path: synthesize a Box<eprog> that
// satisfies the autoload trampoline contract.
let p = Box::new(eprog {
len: 5 * size_of::<u32>() as i32, // c:3796
prog: Vec::new(), // c:3797
strs: None, // c:3798
shf: if shf.is_null() {
None
}
// c:3799
else {
Some(unsafe { Box::from_raw(shf) })
},
npats: 0, // c:3800
nref: 1, // c:3801
flags: 0,
pats: Vec::new(),
dump: None,
});
Box::into_raw(p)
}
/// Port of `bin_unset(char *name, char **argv, Options ops, int func)` from Src/builtin.c:3818.
/// C: `int bin_unset(char *name, char **argv, Options ops, int func)` —
/// `-f` delegates to `bin_unhash`; `-m` glob deletes matching params;
/// default literal-name unset with subscript handling.
/// WARNING: param names don't match C — Rust=(name, argv, func) vs C=(name, argv, ops, func)
pub fn bin_unset(
name: &str,
argv: &[String], // c:3818
ops: &options,
func: i32,
) -> i32 {
let mut returnval = 0i32; // c:3823
let mut match_count = 0i32; // c:3823
// PFA-SMR aspect: emit unset events for each named param. The
// recorder tracks state-mutations across the shell session for
// the zshrs-recorder binary's replay/inspect tooling.
#[cfg(feature = "recorder")]
if crate::recorder::is_enabled() {
let ctx = crate::recorder::recorder_ctx_global();
for a in argv {
if a.starts_with('-') || a == "--" {
continue;
}
crate::recorder::emit_unset(a, ctx.clone());
}
}
// c:3826 — `if (OPT_ISSET(ops,'f')) return bin_unhash(name, argv, ops, func);`
if OPT_ISSET(ops, b'f') {
// c:3826
return bin_unhash(name, argv, ops, func); // c:3827
}
// c:3830-3862 — `-m` glob.
if OPT_ISSET(ops, b'm') {
// c:3831
for s in argv {
// c:3832
queue_signals(); // c:3833
let pprog = patcompile(
s, // c:3836
PAT_HEAPDUP,
None,
);
if let Some(prog) = pprog {
// c:3838-3851 — walk paramtab (NOT env::vars), unset via
// unsetparam (which respects PM_NAMEREF + readonly guards).
//
// The previous Rust port walked `std::env::vars()` — the
// OS environment. This is a different name set:
// - Shell-internal vars (not exported) would survive
// `unset -m 'PATTERN'` even though they match.
// - Env vars not in paramtab would be removed without
// the PM_READONLY guard in unsetparam_pm.
//
// Same family of bug as the env::var vs paramtab fixes
// earlier in the series.
let names: Vec<String> = {
let tab = paramtab().read().unwrap();
tab.keys().cloned().collect()
};
for nm in &names {
if pattry(&prog, nm) {
// c:3842
unsetparam(nm); // c:3847 (with guards)
match_count += 1; // c:3848
}
}
} else {
zwarnnam(name, &format!("bad pattern : {}", s)); // c:3854
returnval = 1; // c:3855
}
unqueue_signals(); // c:3857
}
if match_count == 0 {
// c:3861
returnval = 1; // c:3862
}
return returnval; // c:3863
}
// c:3866-3915 — literal-name unset with optional subscript.
queue_signals(); // c:3867
for s in argv {
// c:3868
// c:3869-3878 — extract `name[subscript]` shape.
let (nm, subscript) = match s.find('[') {
// c:3869
Some(start) if s.ends_with(']') => {
// c:3873
(&s[..start], Some(&s[start + 1..s.len() - 1])) // c:3875
}
Some(_) => {
// c:3879-3884 — bracket without `]` close → invalid.
zwarnnam(name, &format!("{}: invalid parameter name", s)); // c:3882
returnval = 1; // c:3883
continue; // c:3884
}
None => (s.as_str(), None),
};
// c:3878 — `if (... || !isident(s))` invalid identifier check.
if nm.is_empty()
|| !nm
.chars()
.next()
.map_or(false, |c| c.is_alphabetic() || c == '_')
|| !nm.chars().all(|c| c.is_alphanumeric() || c == '_')
{
zwarnnam(name, &format!("{}: invalid parameter name", s)); // c:3882
returnval = 1; // c:3883
continue;
}
// c:3886-3905 — `if (!pm) continue;` then unset.
// C `unsetparam_pm` dispatches on `pm->gsu` (the gsu_*
// accessor for the param's type): assoc gets
// `gsu_a->unset(pm, subscript)`, array gets
// `gsu_arr->unset(pm, subscript)`, scalar gets `unsetparam`.
match subscript {
// c:3886
Some(key) => {
// c:3893 assoc subscript: `m[key]` delete.
if let Some(mut map) = crate::ported::exec_hooks::assoc(nm) {
map.shift_remove(key); // c:3893
crate::ported::exec_hooks::set_assoc(nm, map);
} else if let Some(mut arr) = crate::ported::exec_hooks::array(nm) {
// c:3895 array subscript: `arr[N]` set to empty.
if let Ok(i) = key.parse::<i32>() {
if i > 0 {
let idx = (i - 1) as usize;
if idx < arr.len() {
arr[idx] = String::new();
crate::ported::exec_hooks::set_array(nm, arr);
}
}
}
}
}
None => {
// c:3900-3905 — whole-param unset.
// Route through `unsetparam` (params.rs) so the
// canonical readonly-guard + pm.old uncover restore
// fires. Without this, `local x=inner; unset x`
// would erase the OUTER binding too (the local pm's
// pm.old chain dropped on the floor).
//
// Clear the parallel shadow storage that lives in
// ShellExecutor (paramtab_hashed_storage for assoc,
// and the per-executor arrays/assocs maps). These
// are NOT touched by params.rs::unsetparam so we
// wipe them directly here; using exec_hooks::unset_*
// would loop back into unsetparam.
crate::ported::params::unsetparam(nm);
let _ = crate::ported::params::paramtab_hashed_storage()
.lock()
.ok()
.as_deref_mut()
.map(|m| m.remove(nm));
env::remove_var(nm); // c:3905 delenv
}
}
}
unqueue_signals(); // c:3914
returnval // c:3915
}
/// Port of `fetchcmdnamnode(HashNode hn, UNUSED(int printflags))` from Src/builtin.c:3967.
/// C: `static void fetchcmdnamnode(HashNode hn, UNUSED(int printflags))` →
/// `addlinknode(matchednodes, cn->node.nam);`
/// C body (2 lines):
/// `Cmdnam cn = (Cmdnam) hn;
/// addlinknode(matchednodes, cn->node.nam);`
/// (C source does not null-check hn — callers guarantee non-null.)
/// WARNING: param names don't match C — Rust=(hn) vs C=(hn, printflags)
pub fn fetchcmdnamnode(
hn: *mut hashnode, // c:3967
_printflags: i32,
) {
let nam = unsafe { (*hn).nam.clone() }; // c:3969 cast + read
if let Ok(mut m) = MATCHEDNODES.lock() {
m.push(nam);
} // c:3971
}
/// Port of `bin_whence(char *nam, char **argv, Options ops, int func)` from Src/builtin.c:3975.
/// C: `int bin_whence(char *nam, char **argv, Options ops, int func)`.
///
/// `whence`/`type`/`which`/`where`/`command` dispatcher. `-c` csh,
/// `-v` verbose, `-a` all-matches, `-w` word-form, `-x` indent
/// override, `-m` glob-args, `-p` path-only, `-f` print funcdef,
/// `-s/-S` follow symlink. The C body walks alias/reswd/shfunc/
/// builtin/cmdnam tabs in order; this port preserves the structure
/// and dispatch logic, deferring the per-tab scanmatch walks to the
/// existing tab accessors.
/// WARNING: param names don't match C — Rust=(nam, argv, func) vs C=(nam, argv, ops, func)
pub fn bin_whence(
nam: &str,
argv: &[String], // c:3975
ops: &options,
func: i32,
) -> i32 {
let mut returnval: i32 = 0;
let mut printflags: i32 = 0;
let mut informed: i32 = 0;
let mut expand: i32 = 0;
// c:3989-3993 — flags.
let csh = OPT_ISSET(ops, b'c'); // c:3989
let v = OPT_ISSET(ops, b'v'); // c:3990
let all = OPT_ISSET(ops, b'a'); // c:3991
let wd = OPT_ISSET(ops, b'w'); // c:3992
// c:3995-4002 — `-x N` indent override.
if OPT_ISSET(ops, b'x') {
// c:3995
let arg = OPT_ARG(ops, b'x').unwrap_or("");
match arg.trim().parse::<i32>() {
// c:3997
Ok(n) => {
expand = n;
if expand == 0 {
expand = -1;
} // c:4001
}
Err(_) => {
zwarnnam(nam, "number expected after -x"); // c:3998
return 1;
}
}
}
// c:4004-4012 — printflags from -w/-c/-v/(default simple)/-f.
if OPT_ISSET(ops, b'w') {
printflags |= PRINT_WHENCE_WORD;
}
// c:4004
else if OPT_ISSET(ops, b'c') {
printflags |= PRINT_WHENCE_CSH;
}
// c:4006
else if OPT_ISSET(ops, b'v') {
printflags |= PRINT_WHENCE_VERBOSE;
}
// c:4008
else {
printflags |= PRINT_WHENCE_SIMPLE;
} // c:4010
if OPT_ISSET(ops, b'f') {
printflags |= PRINT_WHENCE_FUNCDEF;
} // c:4012
// c:4015-4024 — BIN_COMMAND -V or -V-equivalent flag wrangling.
// C body:
// if (func == BIN_COMMAND)
// if (OPT_ISSET(ops,'V')) { printflags = aliasflags = PRINT_WHENCE_VERBOSE; v = 1; }
// else { aliasflags = PRINT_LIST; printflags = PRINT_WHENCE_SIMPLE; v = 0; }
// else aliasflags = printflags;
// Previous Rust port omitted the `v = 0` reset in the non-V
// command branch, so `command foo` with a stray user -v leaked
// verbose mode. Mirror C: force v unconditionally under
// BIN_COMMAND.
let mut v = v;
let aliasflags = if func == BIN_COMMAND {
// c:4015
if OPT_ISSET(ops, b'V') {
// c:4016
printflags = PRINT_WHENCE_VERBOSE; // c:4017
v = true; // c:4018
PRINT_WHENCE_VERBOSE
} else {
printflags = PRINT_WHENCE_SIMPLE; // c:4021
v = false; // c:4022
PRINT_LIST // c:4020
}
} else {
printflags // c:4024
};
// c:4026-4119 — `-m` glob branch: each arg is a pattern; walk every
// hashtab in turn (alias/reswd/shfunc/builtin/cmdnam) and emit a
// print row per matching node. C uses scanmatchtable + a per-tab
// print callback; the Rust port iterates each tab's accessor and
// emits the print directly.
if OPT_ISSET(ops, b'm') {
// c:4028 — `cmdnamtab->filltable(cmdnamtab);` populates every
// $PATH entry into cmdnamtab so the c:4070 scan below sees
// every executable (not just hashed ones). C calls this once
// per `-m` invocation; Rust mirrors with a single fillcmdnamtable
// against the shell-side $PATH array.
if let Some(path) = getsparam("PATH") {
let path_arr: Vec<String> =
path.split(':').map(|s| s.to_string()).collect();
fillcmdnamtable(&path_arr);
}
// c:4030-4033 — `if (all) { pushheap(); matchednodes = newlinklist(); }`.
// MATCHEDNODES is the Rust analog of `matchednodes`; pushheap
// is a Rust no-op (no heap allocator).
if all {
// c:4030
if let Ok(mut m) = MATCHEDNODES.lock() {
m.clear();
}
}
queue_signals(); // c:4034
for pat in argv {
// c:4035
// c:4037 — `tokenize(*argv);` (Rust patcompile handles the
// tokenize step internally; explicit call is a no-op here).
let pprog = patcompile(
pat, // c:4038
PAT_HEAPDUP,
None,
);
match pprog {
None => {
// c:4039
zwarnnam(nam, &format!("bad pattern : {}", pat)); // c:4040
returnval = 1; // c:4041
continue;
}
Some(prog) => {
if !OPT_ISSET(ops, b'p') {
// c:4044 — !`-p` path-only.
// c:4049-4051 — `scanmatchtable(aliastab, pprog,
// 1, 0, DISABLED, aliastab->printnode, printflags);`.
// Route through the canonical printnode callback.
let alias_matches: Vec<alias> = aliastab_lock()
.read()
.map(|t| {
t.iter()
.filter(|(n, _)| pattry(&prog, n))
.map(|(_, a)| a.clone())
.collect()
})
.unwrap_or_default();
for a in &alias_matches {
printaliasnode(a, printflags); // c:4051
informed += 1; // c:4049
}
// c:4054-4056 — `scanmatchtable(reswdtab, pprog,
// 1, 0, DISABLED, reswdtab->printnode, printflags);`.
// reswdtab->printnode is `printreswdnode` at
// Src/hashtable.c:1259 — its body is just
// `zputs(hn->nam); putchar('\n')`. Inline the
// print since no separate Rust callback yet
// exists and the body is trivial.
let names: Vec<String> = reswdtab_lock()
.read()
.map(|t| t.iter().map(|(k, _)| k.clone()).collect())
.unwrap_or_default();
for w in &names {
if pattry(&prog, w) {
println!("{}", w); // c:1259 zputs + newline
informed += 1; // c:4054
}
}
// c:4059-4061 — `scanmatchshfunc(pprog, 1, 0,
// DISABLED, shfunctab->printnode, printflags,
// expand);`. Route through canonical
// printshfuncexpand with `expand`.
let func_matches: Vec<shfunc> = shfunctab_lock()
.read()
.map(|t| {
t.iter()
.filter(|(n, _)| pattry(&prog, n))
.map(|(_, f)| f.clone())
.collect()
})
.unwrap_or_default();
for f in &func_matches {
printshfuncexpand(f, printflags, expand); // c:4061
informed += 1; // c:4059
}
// c:4064-4066 — `scanmatchtable(builtintab, pprog,
// 1, 0, DISABLED, builtintab->printnode,
// printflags);`.
for b in BUILTINS.iter() {
if pattry(&prog, &b.node.nam) {
printbuiltinnode(
&b.node as *const hashnode
as *mut hashnode,
printflags,
); // c:4066
informed += 1; // c:4064
}
}
}
// c:4070-4073 — `scanmatchtable(cmdnamtab, pprog,
// 1, 0, 0, (all ? fetchcmdnamnode :
// cmdnamtab->printnode), printflags);`. After
// fillcmdnamtable above, cmdnamtab has every
// PATH-resident command name. Walk the canonical
// table (not std::fs::read_dir) so HASHED/non-
// HASHED distinction is preserved.
let cmd_matches: Vec<(String, cmdnam)> = cmdnamtab_lock()
.read()
.map(|t| {
t.iter()
.filter(|(n, _)| pattry(&prog, n))
.map(|(n, c)| (n.clone(), c.clone()))
.collect()
})
.unwrap_or_default();
for (n, c) in &cmd_matches {
if all {
// c:4072 fetchcmdnamnode — accumulates
// matching node names into matchednodes.
if let Ok(mut m) = MATCHEDNODES.lock() {
m.push(n.clone());
}
} else {
// c:4072 cmdnamtab->printnode — emits per
// PRINT_WHENCE_WORD/CSH/VERBOSE branches.
printcmdnamnode(c, printflags);
}
informed += 1; // c:4070
}
}
}
run_queued_signals(); // c:4079
}
unqueue_signals(); // c:4081
if !all {
// c:4082-4084 — `return returnval || !informed;` (early-out
// when not in `-a` accumulator mode).
return if returnval != 0 || informed == 0 {
1
} else {
0
}; // c:4082
}
}
// c:4121-4205 — literal-name dispatch per arg.
queue_signals();
// C source uses MATCHEDNODES only when `-m` (glob-args) is set;
// plain `-a` keeps the literal argv. Without this gate, `whence
// -a true` consulted an empty MATCHEDNODES and skipped every
// print.
let argv_vec: Vec<String> = if OPT_ISSET(ops, b'm') {
MATCHEDNODES
.lock()
.map(|m| m.clone())
.unwrap_or_default()
} else {
argv.to_vec()
};
for arg in &argv_vec {
// c:4121
// c:4088 — `informed = 0;` reset per iteration so the per-arg
// not-found path can fire correctly.
informed = 0; // c:4088
// c:4090 `char *cnam` is the findcmd return in C; in Rust it
// is bound inline at the findcmd call site below.
// c:4089-4137 — !`-p` and !`-a` matched-from-prior-`-m` arm.
if !OPT_ISSET(ops, b'p') {
// c:4093-4097 — alias check. C: `aliastab->printnode(hn, aliasflags)`.
let alias_text = aliastab_lock()
.read()
.ok()
.and_then(|t| t.get(arg).map(|a| a.clone()));
if let Some(a) = alias_text {
printaliasnode(&a, aliasflags); // c:4094
informed = 1; // c:4095
if !all {
continue;
} // c:4097
}
// c:4099-4107 — suffix-alias check. C: arg has `.SUFFIX`
// AND suffix char before `.` isn't Meta AND sufaliastab
// has matching suf entry. Route through printaliasnode for
// sufaliastab — same printnode callback as aliastab
// (Src/hashtable.c:1255 `sufaliastab->printnode = printaliasnode`).
if let Some(idx) = arg.rfind('.') {
// c:4100 strrchr(*argv, '.')
let after_dot_nonempty = idx + 1 < arg.len();
let dot_not_at_start = idx > 0;
// c:4101 — `suf[-1] != Meta`. Rust strings are UTF-8;
// skip when the byte immediately before `.` would be
// a metafy escape (rare in real shell usage).
let pre_dot_not_meta = arg.as_bytes()[idx - 1] as u8
!= Meta;
if after_dot_nonempty && dot_not_at_start && pre_dot_not_meta {
let suf = &arg[idx + 1..];
let suf_alias = sufaliastab_lock()
.read()
.ok()
.and_then(|t| t.get(suf).map(|a| a.clone()));
if let Some(a) = suf_alias {
printaliasnode(&a, printflags); // c:4103
informed = 1; // c:4104
if !all {
continue;
} // c:4106
}
}
}
// c:4109-4114 — `if ((hn = reswdtab->getnode(reswdtab, *argv)))
// reswdtab->printnode(hn, printflags);`. Reads canonical
// reswdtab instead of a drift-prone literal array.
let is_reswd = reswdtab_lock()
.read()
.map(|t| t.get(arg).is_some())
.unwrap_or(false);
if is_reswd {
// c:4109
if (printflags & PRINT_WHENCE_WORD as i32) != 0 {
println!("{}: reserved", arg);
} else if (printflags & PRINT_WHENCE_CSH as i32) != 0 {
println!("{}: shell reserved word", arg);
} else if (printflags & PRINT_WHENCE_VERBOSE as i32) != 0 {
println!("{} is a reserved word", arg);
} else {
println!("{}", arg); // c:4110
}
informed = 1; // c:4111
if !all {
continue;
} // c:4112
}
// c:4116-4121 — shell function check. C:
// `printshfuncexpand(hn, printflags, expand)`.
// Inline match-on-printflags reimplementation deleted —
// route through the canonical port at hashtable.rs:1407,
// which threads `expand` (for `-x N` indent override) and
// handles PRINT_WHENCE_FUNCDEF/PRINT_WHENCE_WORD/_CSH/
// _VERBOSE branches per Src/hashtable.c:1340-1404.
let shfunc_node = getshfunc(arg);
if let Some(ref f) = shfunc_node {
printshfuncexpand(f, printflags, expand); // c:4117
informed = 1; // c:4118
if !all {
continue;
} // c:4120
}
// c:4123-4128 — builtin check. C: `builtintab->printnode(
// hn, printflags)` → printbuiltinnode at Src/builtin.c:174.
// Inline match-on-(wd|csh|v) reimplementation deleted —
// route through the canonical port at builtin.rs:139.
let builtin_node: Option<*mut hashnode> = BUILTINS
.iter()
.find(|b| b.node.nam == *arg)
.map(|b| &b.node as *const hashnode as *mut hashnode);
if let Some(hn) = builtin_node {
printbuiltinnode(hn, printflags); // c:4124
informed = 1; // c:4125
if !all {
continue;
} // c:4127
}
// c:4167-4173 — cmdnamtab HASHED check (commands installed
// via `hash NAME=PATH`). Read the canonical cmdnamtab
// directly. Was a fake env-var bridge under invented
// `__zshrs_hash_NAME` keys; cmdnamtab is bucket-2-
// consolidated now.
let hashed_path: Option<String> = {
match cmdnamtab_lock().read() {
Ok(tab) => tab.get(arg).and_then(|cn| {
if (cn.node.flags & HASHED as i32) != 0 {
cn.cmd.clone() // c:4168 cn->u.cmd
} else {
None
}
}),
Err(_) => None,
}
};
if let Some(p) = hashed_path {
if (printflags & PRINT_LIST) != 0 {
println!("hash {}={}", arg, p);
} else {
println!("{}", p);
}
informed = 1; // c:4170
if !all {
continue;
} // c:4171
}
}
// c:4141-4172 — `-a` all-paths search. C iterates the
// shell-side `path` array (the tied $path/$PATH global,
// Src/parse.c). Rust reads $PATH via getsparam — same source.
if all && !arg.starts_with('/') {
// c:4141
if let Some(path) = getsparam("PATH") {
for dir in path.split(':') {
// c:4145 — `if (**pp) buf = zhtricat(*pp, "/", *argv);
// else buf = dupstring(*argv);`.
// Empty path entry means CWD per POSIX, but C still
// joins with "/" if non-empty; Rust matches the
// !empty arm.
if dir.is_empty() {
continue;
}
let full = format!("{}/{}", dir, arg); // c:4147
// c:4150 — `iscom(buf)`: access(X_OK)==0 &&
// S_ISREG(stat). Was `Path::is_file()` which omits
// the X_OK check — would have flagged non-executable
// files as matches.
if iscom(&full) {
// c:4150
if wd {
// c:4151
println!("{}: command", arg); // c:4152
} else {
if v && !csh {
// c:4154
print!("{} is ", arg); // c:4155
print!("{}", quotedzputs(&full)); // c:4156
} else {
print!("{}", full); // c:4158
}
// c:4159-4160 — `if (OPT_ISSET(ops,'s') ||
// OPT_ISSET(ops,'S')) print_if_link(buf,
// OPT_ISSET(ops,'S'));`. -s prints just
// the final realpath; -S prints the whole
// chain.
if OPT_ISSET(ops, b's') || OPT_ISSET(ops, b'S') {
print_if_link(
&full,
OPT_ISSET(ops, b'S'),
); // c:4160
}
println!(); // c:4161 fputc('\n', stdout)
}
informed = 1; // c:4163
} else {}
}
}
// c:4166-4171 — `if (!informed && (wd || v || csh))`. C:
// zputs(*argv, stdout); puts(wd ? ": none" : " not found");
// Was `if !informed != 0 && ...` which is broken Rust — the
// `!` is bitwise NOT on the i32, so the condition was true
// when informed != 0 (inverted). Fix: explicit `informed == 0`.
if informed == 0 && (wd || v || csh) {
// c:4166
println!("{}{}", arg, if wd { ": none" } else { " not found" }); // c:4168-4169
returnval = 1; // c:4170
}
continue;
}
// c:4200-4203 — `-p` BIN_COMMAND special case: builtin first.
if func == BIN_COMMAND && OPT_ISSET(ops, b'p') {
// c:4200
if BUILTINS.iter().any(|b| b.node.nam == *arg) {
// c:4201
println!("{}: builtin", arg); // c:4202
informed = 1;
continue;
}
}
// c:4181-4197 — external-command fallback via findcmd.
// C: `if ((cnam = findcmd(*argv, 1, func == BIN_COMMAND &&
// OPT_ISSET(ops, 'p'))))`. Single call site — the previous
// Rust port had two near-duplicate findcmd blocks which
// doubled the lookup. Collapsed into one to match C.
if let Some(cnam) = findcmd(
arg,
1, // c:4181 docmd
(func == BIN_COMMAND && OPT_ISSET(ops, b'p')) as i32, // c:4182-4183
) {
// c:4181
if wd {
// c:4184
println!("{}: command", arg); // c:4186
} else {
if v && !csh {
// c:4188
print!("{} is ", arg); // c:4189
print!("{}", quotedzputs(&cnam)); // c:4190
} else {
print!("{}", cnam); // c:4192
}
// c:4193-4194 — `-s`/`-S` symlink follow.
if OPT_ISSET(ops, b's') || OPT_ISSET(ops, b'S') {
print_if_link(
&cnam,
OPT_ISSET(ops, b'S'),
); // c:4194
}
println!(); // c:4195 fputc('\n', stdout)
}
informed = 1; // c:4197
continue;
}
// c:4201-4205 — not found at all.
if v || csh || wd {
// c:4202
println!("{}{}", arg, if wd { ": none" } else { " not found" }); // c:4203
}
returnval = 1; // c:4204
}
unqueue_signals();
returnval | (informed == 0) as i32 // c:4209
}
/// Port of `bin_hash(char *name, char **argv, Options ops, UNUSED(int func))` from Src/builtin.c:4234.
/// C: `int bin_hash(char *name, char **argv, Options ops, ...)` —
/// manage `cmdnamtab` (default) or `nameddirtab` (`-d`); `-r` empties,
/// `-f` fills, `-L` sets PRINT_LIST, `-m` is a glob.
/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
pub fn bin_hash(
name: &str,
argv: &[String], // c:4234
ops: &options,
_func: i32,
) -> i32 {
let mut returnval = 0i32; // c:4239
let mut printflags = 0i32; // c:4240
let dir_mode = OPT_ISSET(ops, b'd'); // c:4242
// PFA-SMR aspect: only `hash -d NAME=PATH` mutates the named-dir
// table; the default `hash CMD=PATH` form populates a runtime
// command cache that the recorder doesn't re-apply.
#[cfg(feature = "recorder")]
if crate::recorder::is_enabled() && dir_mode {
let ctx = crate::recorder::recorder_ctx_global();
for a in argv {
if a.starts_with('-') {
continue;
}
if let Some((k, v)) = a.split_once('=') {
crate::recorder::emit_hash_d(k, v, ctx.clone());
}
}
}
// c:4247-4263 — `-r` empty / `-f` fill (no other args).
if OPT_ISSET(ops, b'r') || OPT_ISSET(ops, b'f') {
// c:4247
if !argv.is_empty() {
// c:4249
zwarnnam("hash", "too many arguments"); // c:4250
return 1; // c:4251
}
if OPT_ISSET(ops, b'r') {
// c:4255
// c:4256 — `emptyhashtable(cmdnamtab)` /
// `emptynameddirtable()`.
if dir_mode {
emptynameddirtable();
} else {
emptycmdnamtable();
}
}
if OPT_ISSET(ops, b'f') {
// c:4259
// c:4260 — `fillcmdnamtable(cmdnamtab)` /
// `fillnameddirtable()`. cmdnamtab fill = walk every
// PATH entry and hashdir() it.
if dir_mode {
fillnameddirtable();
} else {
// Read $path (the lowercase array form) from env.
// c:4260 — fill cmdnamtab from $path. Read shell-side
// $PATH so changes via `path=(...)` flow in.
let path_str = getsparam("PATH").unwrap_or_default();
let path_arr: Vec<String> = path_str.split(':').map(|s| s.to_string()).collect();
fillcmdnamtable(&path_arr);
}
}
return 0; // c:4262
}
// c:4265 — `-L` enables PRINT_LIST.
if OPT_ISSET(ops, b'L') {
printflags |= PRINT_LIST;
} // c:4265
// c:4268-4273 — no args: list table.
if argv.is_empty() {
// c:4268
queue_signals(); // c:4269
// c:4270 — `scanhashtable(ht, 1, 0, 0, ht->printnode, printflags)`.
// Walk the selected table (cmdnamtab default, nameddirtab when
// `-d`). Previous Rust port only walked nameddirtab — `hash`
// with no args (the typical user-visible form) silently printed
// nothing on cmdnamtab.
if dir_mode {
if let Ok(t) = nameddirtab().lock() {
for (_n, nd) in t.iter() {
// c:4270
printnameddirnode(nd, printflags);
}
}
} else {
// c:4270 — cmdnamtab walk (the default `ht`). PATH lookup
// arr is empty in the printnode call site because per-node
// hashed entries carry their own resolved path.
if let Ok(t) = cmdnamtab_lock().read() {
for (_n, cn) in t.iter() {
// c:4270 — `scanhashtable(cmdnamtab, ..., printcmdnamnode, ...)`
printcmdnamnode(cn, printflags);
}
}
}
unqueue_signals(); // c:4271
return 0; // c:4272
}
// c:4276-4329 — name-list dispatch, both literal and -m glob.
queue_signals(); // c:4276
let mut idx = 0;
while idx < argv.len() {
// c:4277
let arg = &argv[idx];
idx += 1;
if OPT_ISSET(ops, b'm') {
// c:4279
// c:4280-4290 — glob-match path.
let pprog = patcompile(
arg, // c:4282
PAT_HEAPDUP,
None,
);
if let Some(prog) = pprog {
if dir_mode {
if let Ok(t) = nameddirtab().lock() {
for (n, nd) in t.iter() {
if pattry(&prog, n) {
// c:4286
printnameddirnode(nd, printflags);
}
}
}
}
} else {
zwarnnam(name, &format!("bad pattern : {}", arg)); // c:4292
returnval = 1; // c:4293
}
continue;
}
// c:4297-4317 — literal name=value or name-only.
let (n, val) = match arg.find('=') {
Some(eq) => (&arg[..eq], Some(&arg[eq + 1..])),
None => (arg.as_str(), None),
};
if let Some(v) = val {
// c:4302
// Define entry.
if dir_mode {
// c:4302
// c:4303-4310 — `itype_end(asg->name, IUSER, 0)` validates;
// dir name must be all-IUSER chars.
if !n.chars().all(|c| c.is_alphanumeric() || c == '_') {
// c:4305
zwarnnam(name, &format!("invalid character in directory name: {}", n)); // c:4306
returnval = 1; // c:4308
continue; // c:4309
}
let nd = nameddir {
node: hashnode {
next: None,
nam: n.to_string(),
flags: 0,
},
dir: v.to_string(),
diff: 0,
};
addnameddirnode(n, nd); // c:4314
} else {
// c:4313-4318 — `Cmdnam cn = zshcalloc(sizeof *cn);
// cn->node.flags = HASHED;
// cn->u.cmd = ztrdup(asg->value.scalar);
// ht->addnode(ht, ztrdup(asg->name), hn);`
// Insert into cmdnamtab so `hash myc` lookup hits it
// (was storing in `__zshrs_hash_*` env var — fakery
// that the user-facing `hash myc` query never read).
let cn = cmdnam {
node: hashnode {
next: None,
nam: n.to_string(),
flags: HASHED as i32, // c:4316
},
name: None,
cmd: Some(v.to_string()), // c:4316
};
if let Ok(mut tab) = cmdnamtab_lock().write() {
tab.add(cn); // c:4318 addnode
}
}
if OPT_ISSET(ops, b'v') {
// c:4321
if dir_mode {
if let Ok(t) = nameddirtab().lock() {
if let Some(nd) = t.get(n) {
// c:4322
printnameddirnode(nd, 0);
}
}
}
}
} else {
// c:4323-4334 — display existing entry / look up.
if dir_mode {
let snapshot = nameddirtab()
.lock()
.ok()
.and_then(|t| t.get(n).cloned());
match snapshot {
Some(nd) => {
if OPT_ISSET(ops, b'v') {
// c:4337
printnameddirnode(&nd, 0);
}
}
None => {
zwarnnam(name, &format!("no such directory name: {}", n)); // c:4327
returnval = 1; // c:4328
}
}
} else {
// c:4319-4334 — `else if (!(hn = ht->getnode2(ht,
// asg->name))) { ... if (!hashcmd(asg->name, path))
// zwarnnam("no such command"); }`. C path: first
// check cmdnamtab for an existing entry; only fall
// back to hashcmd's PATH walk when not present. The
// previous Rust port skipped the cmdnamtab check, so
// a prior `hash myc=/path` insert was invisible to
// the matching `hash myc` query.
let in_cmdnamtab = cmdnamtab_lock()
.read()
.map(|t| t.get(n).is_some())
.unwrap_or(false);
if !in_cmdnamtab {
// c:4319 hn == NULL → try hashcmd.
let path: Vec<String> = getsparam("PATH")
.map(|p| p.split(':').map(String::from).collect())
.unwrap_or_default();
if crate::ported::exec::hashcmd(n, &path).is_none() {
// c:4332
zwarnnam(name, &format!("no such command: {}", n)); // c:4333
returnval = 1; // c:4334
}
}
}
}
}
unqueue_signals(); // c:4346
returnval // c:4346
}
/// Port of `bin_unhash(char *name, char **argv, Options ops, int func)` from Src/builtin.c:4346.
/// C: `int bin_unhash(char *name, char **argv, Options ops, int func)` —
/// remove entries from cmdnamtab/aliastab/sufaliastab/nameddirtab/
/// shfunctab. `-a` clears all, `-m` is a glob.
/// WARNING: param names don't match C — Rust=(name, argv, func) vs C=(name, argv, ops, func)
pub fn bin_unhash(
name: &str,
argv: &[String], // c:4346
ops: &options,
func: i32,
) -> i32 {
let mut returnval = 0i32; // c:4351
let mut all = 0i32; // c:4351
let mut match_count = 0i32; // c:4351
// PFA-SMR aspect: when invoked as `unalias`, record the un-alias
// events so the replay can suppress earlier `alias` calls.
#[cfg(feature = "recorder")]
if crate::recorder::is_enabled() && func == crate::ported::builtin::BIN_UNALIAS {
let ctx = crate::recorder::recorder_ctx_global();
for a in argv {
if a.starts_with('-') && a != "-" {
continue;
}
crate::recorder::emit_unalias(a, ctx.clone());
}
}
// c:4355-4373 — table-pick dispatch.
enum Tab {
CmdNam,
NamedDir,
Shfunc,
Alias,
SufAlias,
}
let tab: Tab;
if func == BIN_UNALIAS {
// c:4356
tab = if OPT_ISSET(ops, b's') {
Tab::SufAlias
} else {
Tab::Alias
}; // c:4357
if OPT_ISSET(ops, b'a') {
// c:4361
if !argv.is_empty() {
// c:4362
zwarnnam(name, "-a: too many arguments"); // c:4363
return 1; // c:4364
}
all = 1; // c:4366
} else if argv.is_empty() {
// c:4367
zwarnnam(name, "not enough arguments"); // c:4368
return 1; // c:4369
}
} else if OPT_ISSET(ops, b'd') {
tab = Tab::NamedDir; // c:4370
} else if OPT_ISSET(ops, b'f') {
tab = Tab::Shfunc; // c:4372
} else if OPT_ISSET(ops, b's') {
tab = Tab::SufAlias; // c:4374
} else if func == BIN_UNHASH && OPT_ISSET(ops, b'a') {
tab = Tab::Alias; // c:4376
} else {
tab = Tab::CmdNam;
} // c:4378
// Helper: clear entire table.
let clear_all = |t: &Tab| match t {
Tab::Alias => {
let _ = aliastab_lock()
.write()
.map(|mut g| g.clear());
}
Tab::SufAlias => {
let _ = sufaliastab_lock()
.write()
.map(|mut g| g.clear());
}
Tab::NamedDir => {
emptynameddirtable();
}
Tab::Shfunc => {
// c:4388 — empty whole shfunctab (`unhash -af` etc.). C uses
// `emptyhashtable(shfunctab)` GSU; Rust port iterates names
// and removes each (no `clear` method on shfunc_table).
if let Ok(mut t) = shfunctab_lock().write() {
let names: Vec<String> =
t.iter().map(|(k, _)| k.clone()).collect();
for nm in names {
let _ = t.remove(&nm);
}
}
}
Tab::CmdNam => {
emptycmdnamtable();
} // c:4389
};
let remove_one = |t: &Tab, nm: &str| -> bool {
match t {
Tab::Alias => aliastab_lock()
.write()
.map(|mut g| g.remove(nm).is_some())
.unwrap_or(false),
Tab::SufAlias => sufaliastab_lock()
.write()
.map(|mut g| g.remove(nm).is_some())
.unwrap_or(false),
Tab::NamedDir => crate::ported::hashnameddir::removenameddirnode(nm).is_some(),
Tab::Shfunc => {
let from_tab = shfunctab_lock()
.write()
.map(|mut g| g.remove(nm).is_some())
.unwrap_or(false);
// Also remove from the executor's compiled-function /
// source maps. Without this, `unset -f f` cleared
// shfunctab but dispatch_function_call still found the
// compiled chunk and ran the old body. Routed via the
// exec_hooks unregister_function fn-ptr installed by
// fusevm_bridge at startup (no ShellExecutor reach-in
// from src/ported/).
let from_exec = crate::ported::exec_hooks::unregister_function(nm);
from_tab || from_exec
}
// c:4405 — `if ((hn = ht->removenode(ht, *argv)))`.
// Removal returns truthy only when the entry actually
// existed. Previous Rust port hardcoded `true` after a
// void-return `freecmdnamnode` call, so `unhash badname`
// silently succeeded instead of emitting the canonical
// "no such hash table element" error.
Tab::CmdNam => cmdnamtab_lock()
.write()
.map(|mut g| g.remove(nm).is_some())
.unwrap_or(false),
}
};
if all != 0 {
// c:4382
queue_signals(); // c:4383
clear_all(&tab); // c:4384-4389
unqueue_signals(); // c:4390
return 0; // c:4391
}
// c:4395-4421 — `-m` glob branch.
if OPT_ISSET(ops, b'm') {
// c:4395
for arg in argv {
// c:4396
queue_signals(); // c:4397
let pprog = patcompile(
arg, // c:4400
PAT_HEAPDUP,
None,
);
if let Some(prog) = pprog {
// Collect names then remove (avoid iterator/mutation conflict).
// c:4408 — `scanmatchtable(ht, pprog, ...)` walks every
// entry in the selected table. Previous Rust port left
// Tab::CmdNam returning an empty Vec, so `unhash -m PAT`
// (default cmd-hash table) silently matched zero entries.
let names: Vec<String> = match &tab {
Tab::Alias => aliastab_lock()
.read()
.map(|t| t.iter().map(|(n, _)| n.clone()).collect())
.unwrap_or_default(),
Tab::SufAlias => sufaliastab_lock()
.read()
.map(|t| t.iter().map(|(n, _)| n.clone()).collect())
.unwrap_or_default(),
Tab::NamedDir => nameddirtab()
.lock()
.map(|t| t.keys().cloned().collect())
.unwrap_or_default(),
Tab::Shfunc => shfunctab_lock()
.read()
.map(|t| t.iter().map(|(k, _)| k.clone()).collect())
.unwrap_or_default(),
// c:4408 — cmdnamtab walk via `cmdnamtab_lock().iter()`.
Tab::CmdNam => cmdnamtab_lock()
.read()
.map(|t| t.iter().map(|(n, _)| n.clone()).collect())
.unwrap_or_default(),
};
for nm in &names {
if pattry(&prog, nm) {
// c:4408
if remove_one(&tab, nm) {
match_count += 1; // c:4410
}
}
}
} else {
zwarnnam(name, &format!("bad pattern : {}", arg)); // c:4416
returnval = 1; // c:4417
}
unqueue_signals(); // c:4419
}
if match_count == 0 {
// c:4424
returnval = 1; // c:4425
}
return returnval; // c:4426
}
// c:4429-4439 — literal-name removals.
queue_signals(); // c:4430
for arg in argv {
// c:4431
if remove_one(&tab, arg) { // c:4432
// freed
} else if func == BIN_UNSET && isset(optlookup("posixbuiltins")) {
// c:4434 — POSIX: unset of nonexistent isn't an error.
returnval = 0; // c:4435
} else {
zwarnnam(name, &format!("no such hash table element: {}", arg)); // c:4437
returnval = 1; // c:4450
}
}
unqueue_signals(); // c:4450
returnval // c:4450
}
/// Port of `bin_alias(char *name, char **argv, Options ops, UNUSED(int func))` from Src/builtin.c:4450.
/// C: `int bin_alias(char *name, char **argv, Options ops, ...)` — list,
/// define, glob-list, or display aliases. `-r`/`-g`/`-s` filter type;
/// `-L` prints definitions; `-m` treats args as patterns.
/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
pub fn bin_alias(
name: &str,
argv: &[String], // c:4450
ops: &options,
_func: i32,
) -> i32 {
let mut returnval = 0i32; // c:4455
let mut flags1 = 0u32; // c:4456
let mut flags2 = DISABLED as u32; // c:4456
let mut printflags = 0i32; // c:4457
let mut use_suffix = false; // tracks ht switch
// c:4461-4485 — type-flag parsing.
let type_opts = (OPT_ISSET(ops, b'r') as i32) // c:4461
+ (OPT_ISSET(ops, b'g') as i32)
+ (OPT_ISSET(ops, b's') as i32);
if type_opts != 0 {
// c:4464
if type_opts > 1 {
// c:4465
zwarnnam(name, "illegal combination of options"); // c:4466
return 1; // c:4467
}
if OPT_ISSET(ops, b'g') {
// c:4469
flags1 |= ALIAS_GLOBAL as u32; // c:4470
} else {
flags2 |= ALIAS_GLOBAL as u32; // c:4472
}
if OPT_ISSET(ops, b's') {
// c:4473
flags1 |= ALIAS_SUFFIX as u32; // c:4480
use_suffix = true; // c:4481
} else {
flags2 |= ALIAS_SUFFIX as u32; // c:4483
}
}
// c:4486-4490 — printflags from -L / + suffix.
if OPT_ISSET(ops, b'L') {
// c:4486
printflags |= PRINT_LIST; // c:4487
} else if OPT_PLUS(ops, b'g')
|| OPT_PLUS(ops, b'r')
|| OPT_PLUS(ops, b's')
|| OPT_PLUS(ops, b'm')
|| OPT_ISSET(ops, b'+')
// c:4488
{
printflags |= PRINT_NAMEONLY; // c:4490
}
// C bin_alias dispatches printing via `ht->printnode` (set to
// `printaliasnode` at hashtable.c:1208) — `scanhashtable`,
// `scanmatchtable`, and the single-name branch all call
// `ht->printnode(&a->node, printflags)`. The Rust port routes
// through the canonical `printaliasnode` (hashtable.rs:1477) for
// the same dispatch. No local closure.
// c:4495-4500 — no args: list all (filtered by flags).
if argv.is_empty() {
// c:4495
queue_signals(); // c:4496
let lock = if use_suffix {
sufaliastab_lock()
} else {
aliastab_lock()
};
if let Ok(t) = lock.read() {
for (_n, a) in t.iter() {
// c:4497
if (a.node.flags & flags1 as i32) == flags1 as i32
&& (a.node.flags & flags2 as i32) == 0
{
printaliasnode(a, printflags);
}
}
}
unqueue_signals(); // c:4498
return 0; // c:4499
}
// c:4503-4519 — `-m` glob branch.
if OPT_ISSET(ops, b'm') {
// c:4503
for pat in argv {
// c:4504
queue_signals(); // c:4505
// c:4506 — `tokenize + patcompile`.
let pprog = patcompile(
pat, // c:4507
PAT_HEAPDUP,
None,
);
if let Some(prog) = pprog {
let lock = if use_suffix {
sufaliastab_lock()
} else {
aliastab_lock()
};
if let Ok(t) = lock.read() {
for (_n, a) in t.iter() {
// c:4509
if (a.node.flags & flags1 as i32) == flags1 as i32
&& (a.node.flags & flags2 as i32) == 0
&& pattry(&prog, &a.node.nam)
{
printaliasnode(a, printflags);
}
}
}
} else {
zwarnnam(name, &format!("bad pattern : {}", pat)); // c:4514
returnval = 1; // c:4515
}
unqueue_signals(); // c:4517
}
return returnval; // c:4518
}
// c:4521-4540 — literal args: define `name=value` or display a single name.
queue_signals(); // c:4522
let mut idx = 0;
while idx < argv.len() {
// c:4523
let arg = &argv[idx];
idx += 1;
if let Some(eq) = arg.find('=') {
// c:4524 (asg->value.scalar)
if !OPT_ISSET(ops, b'L') {
// c:4524
let n = &arg[..eq];
let v = &arg[eq + 1..];
let lock = if use_suffix {
sufaliastab_lock()
} else {
aliastab_lock()
};
if let Ok(mut t) = lock.write() {
let a = createaliasnode(n, v, flags1); // c:4527
t.add(a);
}
continue;
}
}
let n = if let Some(eq) = arg.find('=') {
&arg[..eq]
} else {
arg.as_str()
};
let lock = if use_suffix {
sufaliastab_lock()
} else {
aliastab_lock()
};
let found = lock.read().ok().and_then(|t| {
t.get_including_disabled(n)
.map(|a| (a.node.nam.clone(), a.node.flags as u32, a.text.clone()))
});
match found {
Some((nm, fl, txt)) => {
// c:4530
// c:4532-4537 — type-filter check.
let show = type_opts == 0
|| use_suffix
|| (OPT_ISSET(ops, b'r') && (fl & (ALIAS_GLOBAL | ALIAS_SUFFIX) as u32) == 0)
|| (OPT_ISSET(ops, b'g') && (fl & ALIAS_GLOBAL as u32) != 0);
if show {
let a = createaliasnode(&nm, &txt, fl);
printaliasnode(&a, printflags);
}
}
None => {
// c:4538
returnval = 1; // c:4539
}
}
}
unqueue_signals(); // c:4541
returnval // c:4542
}
/// Port of `bin_true(UNUSED(char *name), UNUSED(char **argv), UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:4550.
/// C: `int bin_true(UNUSED(char *name), UNUSED(char **argv),
/// UNUSED(Options ops), UNUSED(int func))` → `return 0;`
/// WARNING: param names don't match C — Rust=(_name, _argv, _func) vs C=(name, argv, ops, func)
pub fn bin_true(
_name: &str,
_argv: &[String], // c:4550
_ops: &options,
_func: i32,
) -> i32 {
0 // c:4559
}
/// Port of `bin_false(UNUSED(char *name), UNUSED(char **argv), UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:4559.
/// C: `int bin_false(UNUSED(char *name), UNUSED(char **argv),
/// UNUSED(Options ops), UNUSED(int func))` → `return 1;`
/// WARNING: param names don't match C — Rust=(_name, _argv, _func) vs C=(name, argv, ops, func)
pub fn bin_false(
_name: &str,
_argv: &[String], // c:4559
_ops: &options,
_func: i32,
) -> i32 {
1 // c:4562
}
/// Port of `bin_print(char *name, char **args, Options ops, int func)` from Src/builtin.c:4587.
/// C: `int bin_print(char *name, char **args, Options ops, int func)`.
///
/// The C body is ~1000 lines: `print` / `echo` / `printf` / `pushln`
/// dispatcher with -n/-N/-c/-r/-R/-l/-D/-i/-f/-v/-s/-S/-z/-e/-E etc.
/// The structural port handles the script-friendly subset that the
/// daily-driver hits: print/echo plain emission with -n, -l (one per
/// line), -r raw, -E newline-only, -- end-of-options. The full -f
/// printf format-spec engine and ZLE/history wireups defer to the
/// expand_printf_escapes helpers.
/// WARNING: param names don't match C — Rust=(name, args, func) vs C=(name, args, ops, func)
pub fn bin_print(
name: &str,
args: &[String], // c:4587
ops: &options,
func: i32,
) -> i32 {
let nonewline = OPT_ISSET(ops, b'n'); // c:4595
let raw = OPT_ISSET(ops, b'r') || OPT_ISSET(ops, b'R'); // c:4596
let one_per_line = OPT_ISSET(ops, b'l'); // c:4597
let nul_sep = OPT_ISSET(ops, b'N'); // c:5114/5127/5132 — NUL separator
let _printf_mode = func == BIN_PRINTF || OPT_HASARG(ops, b'f'); // c:4604
let echo_mode = func == BIN_ECHO;
let _ = (name, raw);
// c:4633-4685 — destination dispatch. -u FD writes to fd, -s pushes
// to history, -z to ZLE buffer, -v VAR assigns to scalar.
let dest_var: Option<String> = if OPT_HASARG(ops, b'v') {
OPT_ARG(ops, b'v').map(String::from)
} else {
None
};
// c:4815-4851 — `-u FD` (and `-p` coprocess) dispatch. Parses FD,
// dup's it for an owned descriptor, opens as a File for writes.
// The previous Rust port silently dropped `-u`, so `print -u 2
// hello` went to stdout instead of stderr.
let dest_fd: Option<fs::File> = if OPT_HASARG(ops, b'u') {
// c:4826
let argptr = OPT_ARG(ops, b'u').unwrap_or("");
// c:4827-4828 — undocumented `-up` aliases to coprocout.
// Rust skip: coprocout isn't wired yet; document the gap.
match argptr.parse::<i32>() {
// c:4835 zstrtol
Ok(fdarg) => {
// c:4843 — `dup(fdarg)` for an owned writer that
// close-on-drop doesn't close the user's original fd.
let dup_fd = unsafe { libc::dup(fdarg) };
if dup_fd < 0 {
zwarnnam(name, &format!("bad file number: {}", fdarg)); // c:4844
return 1; // c:4845
}
use std::os::unix::io::FromRawFd;
Some(unsafe { fs::File::from_raw_fd(dup_fd) }) // c:4847
}
Err(_) => {
zwarnnam(
name,
&format!("number expected after -u: {}", argptr),
); // c:4837
return 1; // c:4838
}
}
} else {
None
};
// c:4604-4612 — printf format-string handling.
if _printf_mode {
let fmt = if let Some(f) = OPT_ARG(ops, b'f') {
f.to_string()
} else if !args.is_empty() {
args[0].clone()
} else {
return 0;
};
let rest: &[String] = if OPT_HASARG(ops, b'f') {
args
} else {
&args[1..]
};
let out = printf_format(&fmt, rest);
// c:4854-4856 — `if (OPT_ISSET(ops, 'v') || (fmt && (OPT_ISSET
// (ops, 'z') || OPT_ISSET(ops, 's')))) ASSIGN_MSTREAM(...)`.
// For -f combined with -z or -s, capture output then route
// through the same dispatch as the non-fmt path.
if OPT_ISSET(ops, b'z') {
// c:5564-5565 — push captured output to bufstack.
crate::ported::zle::zle_main::BUFSTACK
.lock()
.unwrap()
.push(out);
return 0;
}
if OPT_ISSET(ops, b's') {
// c:5569-5574 — push captured output as a history entry.
let event_id = crate::ported::hist::prepnexthistent();
crate::ported::hashtable::addhistnode(&out, event_id as i32);
return 0;
}
if let Some(ref v) = dest_var {
setsparam(v, &out);
} else {
print!("{}", out);
}
return 0;
}
// c:4718-4741 — `-m PATTERN args...` glob-filter. First arg is
// the pattern; remaining args are kept iff `pattry(pat, arg)`.
// Previously absent — `print -m 'foo*' foo1 bar foo2` emitted
// all four args instead of just foo1/foo2.
let mut processed_args: Vec<String> = if OPT_ISSET(ops, b'm') {
// c:4718
if args.is_empty() {
// c:4722
zwarnnam(name, "no pattern specified"); // c:4723
return 1; // c:4724
}
// c:4728 — `patcompile(*args, PAT_STATIC, NULL)`.
let pat = &args[0];
let pprog =
patcompile(pat, PAT_STATIC, None);
match pprog {
None => {
zwarnnam(name, &format!("bad pattern: {}", pat)); // c:4730
return 1; // c:4732
}
Some(prog) => {
// c:4734-4737 — `for (t = p = ++args; *p; p++) if
// (pattry(pprog, *p)) *t++ = *p;`. Keep matching args.
args[1..]
.iter()
.filter(|a| pattry(&prog, a))
.cloned()
.collect()
}
}
} else {
args.to_vec()
};
// c:4860+ — main print loop.
// c:5126-5127 — separator priority: `-l` ('\n') > `-N` ('\0') > ' '.
let sep = if one_per_line {
"\n"
} else if nul_sep {
"\0"
} else {
" "
};
// c:4598-4600 — `-P` prompt-style percent expansion (`%n`, `%d`,
// `%?`, `%h`, `%%`, etc.). Routes through `expand_prompt`
// (canonical port of `Src/prompt.c:182 promptexpand`).
if OPT_ISSET(ops, b'P') {
// c:4598-4600 — `-P` prompt-style percent expansion.
for a in processed_args.iter_mut() {
*a = crate::ported::prompt::expand_prompt(a); // c:Src/prompt.c:182
// c:Src/prompt.c:236-247 — `if (!ns) { ... chuck(Inpar/
// Outpar/Nularg); }`. When `ns=0` (non-stripping flag
// off), zsh REMOVES the Inpar/Outpar/Nularg marker bytes
// from the output. `print -P` calls promptexpand with
// `ns=0` per Src/builtin.c:4598, so the SGR-wrapping
// markers MUST NOT leak into stdout. The Rust port's
// expand_prompt uses ad-hoc `\x01`/`\x02` (readline
// RL_PROMPT_*_IGNORE) markers instead of canonical
// Inpar/Outpar, but the strip rule applies identically:
// for non-prompt-render callers, scrub them. Parity bug
// #17 — without this, `print -P "%F{red}red%f"` emitted
// `\x01\E[31m\x02red\x01\E[39m\x02` instead of zsh's
// `\E[31mred\E[39m`.
a.retain(|c| c != '\x01' && c != '\x02');
}
}
// c:4799-4808 — `-o` / `-O` / `-i` sort flags.
//
// C body:
// ```c
// if (OPT_ISSET(ops,'o') || OPT_ISSET(ops,'O')) {
// flags = OPT_ISSET(ops,'i') ? SORTIT_IGNORING_CASE : 0;
// if (OPT_ISSET(ops,'O'))
// flags |= SORTIT_BACKWARDS;
// strmetasort(args, flags, len);
// }
// ```
//
// Meaning: `-i` sets `SORTIT_IGNORING_CASE` (case-INSENSITIVE).
// Without `-i`, sort is case-SENSITIVE.
//
// The previous Rust port had this INVERTED — it bound
// `case_sensitive = OPT_ISSET(ops, b'i')`, then case-sensitive-
// sorted under `-i` and case-insensitive-sorted without `-i`.
// The doc-comment for the block claimed "-o → case-insensitive
// ascending" which is also wrong. Result: `print -o foo Bar BAZ`
// emitted `BAZ Bar foo` (case-insensitive) when zsh emits
// `BAZ Bar foo` only WITH `-i`; without it, zsh emits
// `BAZ Bar foo` ordered by ASCII (caps first).
if OPT_ISSET(ops, b'o') || OPT_ISSET(ops, b'O') {
// c:4800
let ignore_case = OPT_ISSET(ops, b'i'); // c:4805
if ignore_case {
processed_args.sort_by_key(|s| s.to_lowercase());
} else {
processed_args.sort();
}
if OPT_ISSET(ops, b'O') {
// c:4806
processed_args.reverse(); // SORTIT_BACKWARDS
}
}
// c:Src/builtin.c:4866-4886 — when `-r` is NOT set, each arg goes
// through `getkeystring` to interpret backslash escapes (`\n`,
// `\t`, `\\`, escaped space `\ `, etc.). `echo` follows the same
// path when `BSD_ECHO`/`SH_OPTION_LETTERS`-style isn't in effect;
// BIN_ECHO with `-E` keeps escapes literal. Without this, `print
// -- ${(q)a}` for `a="he llo"` emitted `he\ llo` instead of zsh's
// `he llo` (the (q) flag's backslash gets consumed by print).
// c:builtin.c:4747-4767 — escape interpretation dispatch.
// - `fmt` (printf format already chosen via -f) or
// `(!-e && (-R || -r || -E))` → unmetafy only, NO escape
// interpretation (raw passthrough).
// - Otherwise pick `escape_how` per c:4754-4760:
// `-b` → GETKEYS_BINDKEY (skip — `-b`
// isn't wired in this port)
// func != BIN_ECHO && !`-e` → GETKEYS_PRINT (with EMACS:
// unknown `\<c>` → `<c>`)
// else (BIN_ECHO or `-e`) → GETKEYS_ECHO (preserves
// unknown `\<c>` as `\<c>`)
//
// Previous Rust port unconditionally used GETKEYS_PRINT for both
// `echo` and `print` — `echo "${(qq)s}"` for `s="a'b"` stripped
// the `\` from the `(qq)`-emitted `'a'\''b'` because GETKEYS_PRINT
// includes GETKEY_EMACS. zsh keeps the `\` (echo uses GETKEYS_ECHO,
// no EMACS).
let dash_e = OPT_ISSET(ops, b'e');
// c:Src/builtin.c:4754 — BSD_ECHO option flips echo's default:
// escape processing is OFF unless `-e` is explicitly passed.
// Without bsd_echo (the SysV default), escapes process unless
// `-E`/`-R`/`-r` is set.
let bsd_echo_active = echo_mode && isset(optlookup("bsdecho"));
let suppress_escapes = OPT_ISSET(ops, b'R')
|| OPT_ISSET(ops, b'r')
|| (echo_mode && OPT_ISSET(ops, b'E'))
|| (bsd_echo_active && !dash_e);
let mut backslash_c_truncated = false;
if !suppress_escapes || dash_e {
let escape_how: u32 = if !echo_mode && !dash_e {
GETKEYS_PRINT // c:4758
} else {
GETKEYS_ECHO // c:4760
};
// Clear any stale TLS flag before the loop.
let _ = crate::ported::utils::getkey_truncated_take();
let mut new_args: Vec<String> = Vec::with_capacity(processed_args.len());
for a in processed_args.iter() {
let (s, _) = getkeystring_with(a, escape_how);
new_args.push(s);
if crate::ported::utils::getkey_truncated_take() {
// c:utils.c:7045 — `\c` truncated; drop remaining
// args entirely AND suppress trailing newline.
backslash_c_truncated = true;
break;
}
}
processed_args = new_args;
}
// c:Src/builtin.c:4930-4958 — `-C N` column-grid output. Layout
// N args per row (nr = ceil(argc/nc) rows), each cell padded
// to widest arg + 2 spaces. Mirrors zsh's column-major fill
// (col 1 takes first nr items, col 2 the next nr, etc.).
// Without this support `print -C 2 a b c d e` emitted the args
// space-separated on one line instead of the column grid.
let body = if !_printf_mode && OPT_HASARG(ops, b'C') {
let nc: usize = OPT_ARG(ops, b'C')
.and_then(|s| s.trim().parse().ok())
.filter(|&n: &usize| n > 0)
.unwrap_or(1);
let argc = processed_args.len();
let nr = (argc + nc - 1) / nc;
// Maximum width of cells that are NOT in the last column
// (the last column gets no trailing padding, per c:4946-4956).
let max_w = processed_args
.iter()
.take(nr * (nc - 1).max(0))
.map(|s| s.chars().count())
.max()
.unwrap_or(0);
let sc = max_w + 2;
let mut out = String::new();
for row in 0..nr {
for col in 0..nc {
let idx = col * nr + row;
if idx >= argc {
break;
}
let cell = &processed_args[idx];
if col == nc - 1 || col * nr + row + nr >= argc {
out.push_str(cell);
} else {
out.push_str(cell);
let pad = sc.saturating_sub(cell.chars().count());
out.extend(std::iter::repeat(' ').take(pad));
}
}
out.push('\n');
}
// Strip trailing newline; the post-loop `if !nonewline` adds
// one back.
if out.ends_with('\n') { out.pop(); }
out
} else {
processed_args.join(sep)
};
// c:5564-5575 — destination dispatch order:
// -z → zpushnode(bufstack, stringval)
// -v → setsparam(VAR, stringval)
// -s → prepnexthistent() + addhistnode(histtab, stringval)
// else → fwrite to fout
if OPT_ISSET(ops, b'z') {
// c:5564-5565 — `zpushnode(bufstack, stringval)`. The ZLE
// bufstack is consumed by the next zleread call so the
// string is presented at the prompt — `print -z 'echo foo'`
// queues `echo foo` for the user to press Enter on.
crate::ported::zle::zle_main::BUFSTACK
.lock()
.unwrap()
.push(body); // c:5565
return 0;
}
if OPT_ISSET(ops, b's') {
// c:5569-5574 — push the captured output as a history entry.
let event_id = crate::ported::hist::prepnexthistent(); // c:5569
crate::ported::hashtable::addhistnode(&body, event_id as i32); // c:5574
return 0;
}
if let Some(ref v) = dest_var {
setsparam(v, &body);
} else {
// c:5130-5132 — final terminator: `-n` suppresses; `-N` emits
// NUL instead of newline; else newline. `\c` truncation
// (c:utils.c:7045) also suppresses — matches zsh's
// `echo "a\cb"; echo END` → `aEND`.
let final_term: &[u8] = if nonewline || backslash_c_truncated {
b""
} else if nul_sep {
b"\0"
} else {
b"\n"
};
if let Some(mut f) = dest_fd {
// c:4847 — write to dup'd file descriptor.
use std::io::Write as _;
let _ = f.write_all(body.as_bytes()); // c:5124 fwrite
let _ = f.write_all(final_term); // c:5132
// f closes on drop (close(dup_fd)) — user's original fd
// remains open per c:4843 dup semantics.
} else {
// stdout path. -N writes NUL via raw stdout; print!/println!
// would mangle a NUL inside a String literal via format
// machinery, so route through stdout().write_all directly.
use std::io::Write as _;
let stdout = io::stdout();
let mut lk = stdout.lock();
let _ = lk.write_all(body.as_bytes()); // c:5124
let _ = lk.write_all(final_term); // c:5132
}
}
0
}
/// Port of `bin_shift(char *name, char **argv, Options ops, UNUSED(int func))` from Src/builtin.c:5593.
/// C: `int bin_shift(char *name, char **argv, Options ops, UNUSED(int func))`
/// — shift positional params (or named arrays) by `num` positions; `-p`
/// pops from the right end.
/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
pub fn bin_shift(
name: &str,
argv: &[String], // c:5593
ops: &options,
_func: i32,
) -> i32 {
let mut num: i32 = 1; // c:5595
let mut ret: i32 = 0; // c:5595
let mut idx = 0usize;
queue_signals(); // c:5599
// c:5600-5605 — first arg parsed as math expr unless it's an array name.
if !argv.is_empty() {
// c:5600
let first = &argv[0];
// c:5600 — `if (!getaparam(*argv))` decides whether the arg is
// a numeric shift-count vs an array name. Check
// paramtab for a PM_ARRAY entry, not OS env.
let is_array = {
use {PM_ARRAY, PM_TYPE};
let tab = paramtab().read().unwrap();
tab.get(first)
.map(|pm| PM_TYPE(pm.node.flags as u32) == PM_ARRAY)
.unwrap_or(false)
};
if !is_array {
// c:5600
// c:5601 — `num = mathevali(*argv++);`. The previous Rust port
// used `parse::<i32>()` which rejects any non-trivial
// arithmetic: `shift 1+2` would silently return ret=1
// instead of shifting by 3. Route through mathevali.
num = mathevali(first).unwrap_or_else(|_| {
ret = 1;
0
}) as i32; // c:5601
idx = 1;
// c:5602-5605 — `if (errflag) return 1;`.
if ret != 0 || errflag.load(Relaxed) != 0 {
unqueue_signals(); // c:5604
return 1;
}
}
}
// c:5608-5611 — `if (num < 0)` reject.
if num < 0 {
// c:5608
unqueue_signals(); // c:5609
zwarnnam(name, "argument to shift must be non-negative"); // c:5610
return 1; // c:5611
}
// c:5614-5635 — named-array shift loop.
if idx < argv.len() {
// c:5614
for arr_name in &argv[idx..] {
// c:5615
// c:5616 — `if ((s = getaparam(*argv)))` else silent skip.
// Read paramtab directly; was approximating arrays
// as `:`-separated env values which is wrong (env
// can never carry array structure).
let s: Vec<String> = {
let tab = paramtab().read().unwrap();
match tab.get(arr_name).and_then(|pm| pm.u_arr.clone()) {
Some(arr) => arr,
None => continue,
}
};
// c:5617-5621 — arrlen_lt check.
if (s.len() as i32) < num {
// c:5617
zwarnnam(name, "shift count must be <= $#"); // c:5618
ret += 1; // c:5619
continue; // c:5620
}
// c:5622-5634 — -p shifts off the right end, otherwise the left.
let s2: Vec<String> = if OPT_ISSET(ops, b'p') {
// c:5622
s[..s.len() - num as usize].to_vec() // c:5625-5628
} else {
s[num as usize..].to_vec() // c:5631
};
// c:5633 — `setaparam(*argv, s);`. Write the shifted array
// back to paramtab as a proper PM_ARRAY. Was a
// fake: `env::set_var` + colon-joined fake-array
// which neither carries array structure nor
// reaches subsequent `${arr_name[@]}` expansions.
setaparam(arr_name, s2);
}
} else {
// c:5636-5654 — shift positional parameters ($1..$N).
// Static-link path: positional params live in src/ported/vm_helper;
// expose via PPARAMS Mutex<Vec<String>>.
let mut pp = PPARAMS.lock().unwrap_or_else(|e| {
PPARAMS.clear_poison();
e.into_inner()
});
let l = pp.len() as i32;
if num > l {
// c:5636
zwarnnam(name, "shift count must be <= $#"); // c:5637
ret = 1; // c:5638
} else if OPT_ISSET(ops, b'p') {
// c:5641
pp.truncate((l - num) as usize); // c:5642-5644
} else {
pp.drain(..num as usize); // c:5646-5650
}
// PPARAMS is the single source of truth. fusevm-side reads
// route through exec.pparams() which reads PPARAMS, so the
// shift is immediately visible — no exec.positional_params
// mirror needed.
drop(pp);
}
unqueue_signals(); // c:5658
ret // c:5659
}
/// Port of `bin_getopts(UNUSED(char *name), char **argv, UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:5672.
/// C: `int bin_getopts(UNUSED(char *name), char **argv, UNUSED(Options ops),
/// UNUSED(int func))`.
///
/// POSIX getopts. Maintains state in $OPTIND (zoptind) and an internal
/// per-arg cursor (optcind). Reads from the script's positional params
/// when no extra args supplied, otherwise from the trailing argv.
/// WARNING: param names don't match C — Rust=(_name, argv, _func) vs C=(name, argv, ops, func)
pub fn bin_getopts(
_name: &str,
argv: &[String], // c:5672
_ops: &options,
_func: i32,
) -> i32 {
if argv.len() < 2 {
return 1;
}
// c:5675 — `char *optstr = unmetafy(*argv++, &lenoptstr); char *var = *argv++;`
let optstr_full = argv[0].clone();
let var = argv[1].clone();
// c:5676 — `char **args = (*argv) ? argv : pparams;`
let argv_rest: Vec<String> = argv[2..].to_vec();
let args: Vec<String> = if !argv_rest.is_empty() {
argv_rest
} else {
PPARAMS.lock().map(|p| p.clone()).unwrap_or_default()
};
// c:Src/builtin.c:5680 — at entry, re-sync the internal zoptind
// tracker against the user-visible $OPTIND param. zsh exposes
// OPTIND as a writable param so scripts can reset it to 1 to
// re-parse positionals; the Rust port had `ZOPTIND` as an atomic
// that was the AUTHORITY (writes to $OPTIND didn't propagate
// back), so `OPTIND=1` between two getopts loops left zoptind at
// the post-loop value and the second pass returned immediately.
let paramtab_oi = crate::ported::params::getiparam("OPTIND");
let mut zoptind = if paramtab_oi >= 1 {
paramtab_oi as i32
} else {
ZOPTIND.load(Relaxed)
};
// c:5681-5685 — `if (zoptind < 1) { zoptind = 1; optcind = 0; }`
if zoptind < 1 {
// c:5681
zoptind = 1;
OPTCIND.store(0, Relaxed);
}
// c:Src/builtin.c — when $OPTIND was just reset to 1 (i.e. the
// user-visible param disagrees with the previous internal
// pointer), reset optcind so the new pass starts at byte 0 of
// the first option arg.
let mut optcind = if paramtab_oi == 1 && ZOPTIND.load(Relaxed) != 1 {
0
} else {
OPTCIND.load(Relaxed)
};
// c:5686-5688 — `if (arrlen_lt(args, zoptind)) return 1;`
if (args.len() as i32) < zoptind {
// c:5686
ZOPTIND.store(zoptind, Relaxed);
return 1;
}
// c:5691-5693 — `quiet = *optstr == ':'; optstr += quiet; lenoptstr -= quiet;`
let (quiet, optstr) = if optstr_full.starts_with(':') {
// c:5691
(true, &optstr_full[1..])
} else {
(false, optstr_full.as_str())
};
// c:5696 — `str = unmetafy(dupstring(args[zoptind - 1]), &lenstr);`
let mut str_buf = args[(zoptind - 1) as usize].clone();
let mut lenstr = str_buf.len() as i32;
if lenstr == 0 {
return 1;
} // c:5697
// c:5699-5703 — bump to next arg if optcind exhausted current.
if optcind >= lenstr {
// c:5699
optcind = 0;
zoptind += 1;
if zoptind as usize > args.len() {
// c:5701
ZOPTIND.store(zoptind, Relaxed);
OPTCIND.store(optcind, Relaxed);
setiparam("OPTIND", zoptind as i64); // c:5702
return 1;
}
str_buf = args[(zoptind - 1) as usize].clone();
lenstr = str_buf.len() as i32;
}
// c:5705-5712 — first option char checks: not `-`/`+` → done; `--` → done.
if optcind == 0 {
// c:5705
if lenstr < 2 || (!str_buf.starts_with('-') && !str_buf.starts_with('+')) {
ZOPTIND.store(zoptind, Relaxed);
OPTCIND.store(optcind, Relaxed);
// c:5707 — mirror to $OPTIND so callers see the post-loop
// pointer. Previous Rust port skipped this write on the
// "no more options" exit; OPTIND stayed at the last
// option arg index (-b) instead of advancing past it.
setiparam("OPTIND", zoptind as i64);
return 1;
}
if lenstr == 2 && &str_buf[..2] == "--" {
// c:5708
zoptind += 1;
ZOPTIND.store(zoptind, Relaxed);
OPTCIND.store(0, Relaxed);
setiparam("OPTIND", zoptind as i64); // c:5711
return 1;
}
optcind += 1;
}
// c:5715 — `opch = str[optcind++];`
let opch = str_buf.as_bytes()[optcind as usize];
optcind += 1;
// c:5716-5721 — `lenoptbuf = (str[0] == '+') ? 2 : 1; optbuf[lenoptbuf-1] = opch;`
let plus = str_buf.starts_with('+');
let optbuf: String = if plus {
format!("+{}", opch as char)
} else {
format!("{}", opch as char)
};
// c:5724-5740 — illegal option: `?` reply, OPTIND fixed under POSIXBUILTINS.
let posix = isset(optlookup("posixbuiltins"));
let found = optstr.bytes().position(|b| b == opch);
if opch == b':' || found.is_none() {
// c:5724
if posix {
// c:5728
optcind = 0;
zoptind += 1;
}
// c:5731 — `setsparam(var, ztrdup(p));` where p = "?"
setsparam(&var, "?");
if quiet {
// c:5733
setsparam("OPTARG", &optbuf); // c:5734
} else {
let prefix = if plus { "+" } else { "-" };
zwarn(&format!("bad option: {}{}", prefix, opch as char)); // c:5736
setsparam("OPTARG", "");
}
ZOPTIND.store(zoptind, Relaxed);
OPTCIND.store(optcind, Relaxed);
// Sync OPTIND env var so callers can read.
setiparam("OPTIND", zoptind as i64);
return 0;
}
// c:5744 — `if (p[1] == ':')` — required argument.
let p = found.unwrap();
let optstr_bytes = optstr.as_bytes();
if p + 1 < optstr_bytes.len() && optstr_bytes[p + 1] == b':' {
// c:5744
if optcind == lenstr {
// c:5745
// c:5746 — argument in next arg.
if zoptind as usize >= args.len() {
// c:5747
if posix {
optcind = 0;
zoptind += 1;
}
if quiet {
// c:5754
setsparam(&var, ":");
setsparam("OPTARG", &optbuf);
} else {
setsparam(&var, "?");
setsparam("OPTARG", "");
let prefix = if plus { "+" } else { "-" };
zwarn(&format!(
"argument expected after {}{} option",
prefix, opch as char
)); // c:5760
}
ZOPTIND.store(zoptind, Relaxed);
OPTCIND.store(optcind, Relaxed);
setiparam("OPTIND", zoptind as i64);
return 0;
}
// c:5763 — `p = ztrdup(args[zoptind++]);` — read args[zoptind]
// then post-increment. zoptind now points one past the
// arg-bearing flag's index.
let p_arg = args[zoptind as usize].clone(); // c:5763
zoptind += 1; // c:5763 post-increment
setsparam("OPTARG", &p_arg); // c:5765
// c:5771 — `optcind = 0; zoptind++;` — bump past the
// consumed value arg too, so the NEXT getopts call sees
// the arg AFTER the value. Previous Rust port skipped this
// second increment, so the next iter re-read the consumed
// value as if it were a new flag.
optcind = 0; // c:5771
zoptind += 1; // c:5772
} else {
// c:5774 — `p = metafy(str+optcind, lenstr-optcind, META_DUP);`
let p_arg = str_buf[(optcind as usize)..].to_string();
setsparam("OPTARG", &p_arg);
optcind = 0;
zoptind += 1;
}
} else {
// c:5784 — `zsfree(zoptarg); zoptarg = ztrdup("");`
setsparam("OPTARG", "");
}
// c:5788 — `setsparam(var, metafy(optbuf, lenoptbuf, META_DUP));`
setsparam(&var, &optbuf);
ZOPTIND.store(zoptind, Relaxed);
OPTCIND.store(optcind, Relaxed);
setiparam("OPTIND", zoptind as i64);
0 // c:5790
}
/// Port of `bin_break(char *name, char **argv, UNUSED(Options ops), int func)` from Src/builtin.c:5809.
/// C: `int bin_break(char *name, char **argv, UNUSED(Options ops), int func)`
/// — handles BIN_BREAK / BIN_CONTINUE / BIN_RETURN / BIN_LOGOUT / BIN_EXIT.
/// WARNING: param names don't match C — Rust=(name, argv, func) vs C=(name, argv, ops, func)
pub fn bin_break(
name: &str,
argv: &[String], // c:5809
_ops: &options,
func: i32,
) -> i32 {
// BIN_BREAK/CONTINUE/RETURN/EXIT/LOGOUT live at the top of this file
// (c:5707-5712 in Src/builtin.c via the BUILTIN(...) table).
// c:5811 — `int num = lastval, nump = 0, implicit;`
let mut num: i32 = LASTVAL.load(Relaxed); // c:5811
let mut nump = 0i32; // c:5811
let implicit = argv.is_empty(); // c:5814
// c:5815-5818 — first arg parsed as math expr.
if !implicit {
// c:5815
num = mathevali(&argv[0]).unwrap_or(0) as i32; // c:5816
nump = 1; // c:5817
}
// c:5820-5823 — positive-num requirement for BIN_CONTINUE / BIN_BREAK.
if nump > 0 && (func == BIN_CONTINUE || func == BIN_BREAK) && num <= 0 {
// c:5820
zwarnnam(name, &format!("argument is not positive: {}", num)); // c:5821
return 1; // c:5822
}
let loops = LOOPS.load(Relaxed);
match func {
// c:5831-5842 — BIN_CONTINUE: must be in a loop, set contflag,
// then fall through to BIN_BREAK's break-count assign.
x if x == BIN_CONTINUE => {
// c:5831
if loops == 0 {
// c:5832
zwarnnam(name, "not in while, until, select, or repeat loop"); // c:5833
return 1; // c:5834
}
CONTFLAG.store(1, Relaxed); // c:5836 FALLTHROUGH
// c:5837 — fallthrough to BIN_BREAK's loops==0 guard
// (impossible here since we already returned above) +
// break-count assign. Inlined directly. The previous
// Rust port had a redundant `if loops == 0 { return 1 }`
// dead-coded after the first guard.
BREAKS.store(
if nump != 0 { num.min(loops) } else { 1 }, // c:5842
Relaxed,
);
}
// c:5832-5838 — BIN_BREAK.
x if x == BIN_BREAK => {
// c:5832
if loops == 0 {
// c:5833
zwarnnam(name, "not in while, until, select, or repeat loop"); // c:5834
return 1; // c:5835
}
BREAKS.store(
if nump != 0 { num.min(loops) } else { 1 }, // c:5837
Relaxed,
);
}
// c:5839-5860 — BIN_RETURN.
x if x == BIN_RETURN => {
let interactive = isset(optlookup("interactive"));
let shinstdin = isset(optlookup("shinstdin"));
let ll_v = locallevel_param.load(Relaxed);
let sourcelevel = crate::ported::init::sourcelevel.load(Relaxed);
// c:5840-5841 — `if ((interactive && shinstdin) || locallevel || sourcelevel)`
if (interactive && shinstdin) || ll_v != 0 || sourcelevel != 0 {
// c:5840
RETFLAG.store(1, Relaxed); // c:5842
BREAKS.store(loops, Relaxed); // c:5843
LASTVAL.store(num, Relaxed); // c:5844
// c:5845-5854 — inside a primed trap with the sentinel
// `trap_return == -2`, promote to TRAP_STATE_FORCE_RETURN
// and carry `lastval`. POSIXTRAPS + `implicit` opts out:
// POSIX semantics keep $? from before the trap fired.
let posixtraps = isset(optlookup("posixtraps"));
let cur_state = TRAP_STATE.load(Relaxed);
let cur_return = TRAP_RETURN.load(Relaxed);
if cur_state == TRAP_STATE_PRIMED // c:5845
&& cur_return == -2 // c:5845
&& !(posixtraps && implicit)
// c:5851
{
TRAP_STATE.store(
// c:5852
TRAP_STATE_FORCE_RETURN,
Relaxed,
);
TRAP_RETURN.store(num, Relaxed);
// c:5853
}
return num; // c:5855
}
// c:5858 — fallthrough: treat as logout/exit.
zexit(num, ZEXIT_NORMAL); // c:5858
}
// c:5864-5869 — BIN_LOGOUT: refuse if not LOGINSHELL, then
// FALLTHROUGH into the BIN_EXIT body. The previous Rust port
// called \`zexit(num, ZEXIT_NORMAL)\` directly instead of
// entering the BIN_EXIT defer-guard, so \`logout\` from inside
// a function would skip EXIT traps + function unwind +
// \"you have running jobs\" warning — same gap as the prior
// BIN_EXIT fix.
x if x == BIN_LOGOUT => {
// c:5865 — `if (unset(LOGINSHELL))`. The previous Rust port
// called `optlookup("login")` — but "login" is the
// SHELL-LETTER-FLAG name (zshletters table letter 'l'),
// not an option name. Option name canonicalization maps
// LOGINSHELL → "loginshell" (Src/options.c index_to_name
// at line 1682 in Rust port).
//
// \`optlookup(\"login\")\` returns OPT_INVALID (0), so
// \`isset(0)\` always returns false — bin_logout always
// saw \"not login shell\" and rejected with that error
// regardless of whether the shell was actually started
// with \`-l\`.
let loginshell = isset(optlookup("loginshell"));
if !loginshell {
// c:5865
zwarnnam(name, "not login shell"); // c:5866
return 1; // c:5867
}
// c:5869 — `/*FALLTHROUGH*/` into BIN_EXIT body.
// Reusing the BIN_EXIT branch below by setting `func` to
// BIN_EXIT isn't possible mid-match; inline the same
// guard logic here.
let cur_locallevel = locallevel_param.load(Relaxed);
let forklevel = FORKLEVEL.load(Relaxed);
let shell_exiting = SHELL_EXITING.load(Relaxed);
if cur_locallevel > forklevel && shell_exiting != -1 {
// c:5871
if STOPMSG.load(Relaxed) == 0 {
zexit(0, ZEXIT_DEFERRED); // c:5884
}
if STOPMSG.load(Relaxed) == 0 {
// c:5884
let trap_state = TRAP_STATE.load(Relaxed);
if trap_state != 0 {
// c:5885
TRAP_STATE.store(
// c:5886
TRAP_STATE_FORCE_RETURN,
Relaxed,
);
}
RETFLAG.store(1, Relaxed); // c:5887
BREAKS.store(
LOOPS.load(Relaxed), // c:5888
Relaxed,
);
EXIT_PENDING.store(1, Relaxed); // c:5889
EXIT_LEVEL.store(cur_locallevel, Relaxed); // c:5890 — exit_level = locallevel;
EXIT_VAL.store(num, Relaxed); // c:5891
}
} else {
zexit(num, ZEXIT_NORMAL); // c:5894
}
}
// c:5870-5894 — BIN_EXIT: function-context guard. C body:
// if (locallevel > forklevel && shell_exiting != -1) {
// if (stopmsg || (zexit(0, ZEXIT_DEFERRED), !stopmsg)) {
// if (trap_state) trap_state = TRAP_STATE_FORCE_RETURN;
// retflag = 1; breaks = loops;
// exit_pending = 1; exit_level = locallevel; exit_val = num;
// }
// } else zexit(num, ZEXIT_NORMAL);
//
// Inside a function (locallevel > forklevel) the shell can't
// exit directly — EXIT traps still need to run. The probe
// path zexit(0, ZEXIT_DEFERRED) calls checkjobs; if no
// stopmsg triggered, we defer: set retflag + breaks +
// exit_pending so the function unwind takes us out.
//
// The previous Rust port skipped this entire guard, always
// calling zexit(num, ZEXIT_NORMAL) directly. `exit` inside
// a function would terminate without running EXIT traps or
// unwinding the function stack.
x if x == BIN_EXIT => {
let cur_locallevel = locallevel_param.load(Relaxed);
let forklevel = FORKLEVEL.load(Relaxed);
let shell_exiting = SHELL_EXITING.load(Relaxed);
if cur_locallevel > forklevel && shell_exiting != -1 {
// c:5871
// Probe via ZEXIT_DEFERRED — may set stopmsg.
if STOPMSG.load(Relaxed) == 0 {
zexit(0, ZEXIT_DEFERRED); // c:5884
}
if STOPMSG.load(Relaxed) == 0 {
// c:5884 still no stopmsg → defer
let trap_state = TRAP_STATE.load(Relaxed);
if trap_state != 0 {
// c:5885
TRAP_STATE.store(
// c:5886
TRAP_STATE_FORCE_RETURN,
Relaxed,
);
}
RETFLAG.store(1, Relaxed); // c:5887
BREAKS.store(
LOOPS.load(Relaxed), // c:5888
Relaxed,
);
EXIT_PENDING.store(1, Relaxed); // c:5889
EXIT_LEVEL.store(cur_locallevel, Relaxed); // c:5890 — exit_level = locallevel;
EXIT_VAL.store(num, Relaxed); // c:5891
}
} else {
zexit(num, ZEXIT_NORMAL); // c:5894
}
}
_ => {}
}
0
}
/// Port of `checkjobs()` from Src/builtin.c:5899.
/// C: `static void checkjobs(void)` — walk `jobtab[1..maxjob]`; for each
/// non-current job that's STAT_LOCKED, not STAT_NOPRINT, and either
/// running (when CHECKRUNNINGJOBS is set) or STAT_STOPPED, emit
/// "you have running/stopped jobs" + set `stopmsg = 1`.
pub fn checkjobs() {
// c:5899
let checkrunning = isset(optlookup("checkrunningjobs"));
// c:5901 — read the canonical jobs.rs THISJOB/MAXJOB globals.
// The previous builtin.rs duplicate AtomicI32s for both never
// synced with the jobs.rs Mutex<i32> values that the spawn /
// wait paths actually update — checkjobs would see stale 0s
// regardless of how many jobs were active.
let thisjob: i32 = *crate::ported::jobs::THISJOB
.get_or_init(|| Mutex::new(-1_i32))
.lock()
.expect("THISJOB poisoned");
// jobs::MAXJOB is stored as `Mutex<usize>` (Rust adaptation for
// Vec-index semantics); cast to i32 for comparison with `thisjob`.
let maxjob: i32 = *crate::ported::jobs::MAXJOB
.get_or_init(|| Mutex::new(0_usize))
.lock()
.expect("MAXJOB poisoned") as i32;
// c:5903 — `for (i = 1; i <= maxjob; i++)`
let mut found: Option<i32> = None;
let mut found_stat: i32 = 0;
for i in 1..=maxjob {
// c:5903
let stat = JOBSTATS
.lock()
.ok()
.and_then(|t| t.get(i as usize).copied())
.unwrap_or(0);
// c:5904-5906 — `i != thisjob && (stat & STAT_LOCKED) &&
// !(stat & STAT_NOPRINT) &&
// (CHECKRUNNINGJOBS || stat & STAT_STOPPED)`
if i != thisjob // c:5904
&& (stat & STAT_LOCKED) != 0 // c:5904
&& (stat & STAT_NOPRINT) == 0 // c:5905
&& (checkrunning || (stat & STAT_STOPPED) != 0)
// c:5906
{
found = Some(i); // c:5907
found_stat = stat;
break;
}
}
// c:5908 — `if (i <= maxjob)`
if found.is_some() {
// c:5908
if (found_stat & STAT_STOPPED) != 0 {
// c:5909
// c:5912/5914 — `zerr("you have suspended/stopped jobs.");`
zerr("you have stopped jobs."); // c:5914
} else {
// c:5917 — `zerr("you have running jobs.");`
zerr("you have running jobs."); // c:5917
}
STOPMSG.store(1, Relaxed); // c:5919
}
}
/// Port of `realexit()` from Src/builtin.c:5953.
/// C body (single statement):
/// `exit((shell_exiting || exit_pending) ? exit_val : lastval);`
pub fn realexit() -> ! {
// c:5953
std::process::exit(
if SHELL_EXITING.load(Relaxed) != 0 || EXIT_PENDING.load(Relaxed) != 0 {
EXIT_VAL.load(Relaxed)
} else {
LASTVAL.load(Relaxed)
},
);
}
/// Port of `_realexit()` from Src/builtin.c:5962.
/// C body (single statement):
/// `_exit((shell_exiting || exit_pending) ? exit_val : lastval);`
pub fn _realexit() -> ! {
// c:5962
unsafe {
libc::_exit(
if SHELL_EXITING.load(Relaxed) != 0 || EXIT_PENDING.load(Relaxed) != 0 {
EXIT_VAL.load(Relaxed)
} else {
LASTVAL.load(Relaxed)
},
)
}
}
/// Port of `zexit(int val, enum zexit_t from_where)` from Src/builtin.c:5977.
/// C: `void zexit(int val, enum zexit_t from_where)` — record exit
/// value, fire EXIT trap unless already exiting, then realexit.
#[allow(unused_variables)]
pub fn zexit(val: i32, from_where: i32) {
// c:5977
// c:5989 — `exit_val = val;`
EXIT_VAL.store(val, Relaxed); // c:5989
// c:5990 — `if (shell_exiting == -1) { retflag = 1; breaks = loops; return; }`
if SHELL_EXITING.load(Relaxed) == -1 {
// c:5990
RETFLAG.store(1, Relaxed); // c:5991
BREAKS.store(LOOPS.load(Relaxed), Relaxed); // c:5992
return; // c:5993
}
// c:5996-6004 — `if (isset(MONITOR) && !stopmsg && from_where != ZEXIT_SIGNAL)`:
// run scanjobs + checkjobs; if stopmsg got set (running jobs warned),
// mark stopmsg=2 and DEFER the exit. The previous Rust port skipped
// this entire block, so `exit` with running jobs would terminate
// immediately rather than emitting the standard
// \"zsh: you have running jobs\" + waiting for a confirmation exit.
if isset(MONITOR) // c:5996
&& STOPMSG.load(Relaxed) == 0
&& from_where != ZEXIT_SIGNAL
{
checkjobs(); // c:5999
if STOPMSG.load(Relaxed) != 0 {
// c:6000
STOPMSG.store(2, Relaxed); // c:6001
return; // c:6002 defer
}
}
// c:6006-6008 — `if (from_where == ZEXIT_DEFERRED || (shell_exiting++
// && from_where != ZEXIT_NORMAL)) return;`. Probe path:
// ZEXIT_DEFERRED callers only want the checkjobs gate to fire; if
// it didn't trip, return without actually exiting.
if from_where == ZEXIT_DEFERRED {
// c:6006
return;
}
let prev_exiting = SHELL_EXITING.fetch_add(1, Relaxed);
if prev_exiting != 0 && from_where != ZEXIT_NORMAL {
// c:6007
return;
}
// c:6014 — `shell_exiting = -1;`
SHELL_EXITING.store(-1, Relaxed); // c:6014
// c:6019 — `errflag = 0;`
errflag.store(0, Relaxed); // c:6019
// c:6021-6024 — MONITOR → killrunjobs.
if isset(MONITOR) {
// c:6021
crate::ported::signals::killrunjobs(if from_where == ZEXIT_SIGNAL { 1 } else { 0 });
// c:6023
}
// !!! RUST-ONLY GATE: see SUBSHELL_DEPTH declaration above for
// rationale. C zsh's realexit at c:5953 unconditionally calls
// process::exit because the subshell was forked; in zshrs the
// subshell runs in-process, so process::exit would kill the
// whole shell. Defer: set EXIT_PENDING + EXIT_VAL + reset
// SHELL_EXITING so the subshell_end unwind in fusevm_bridge
// catches and propagates the status to the parent.
if SUBSHELL_DEPTH.load(Relaxed) > 0 {
SHELL_EXITING.store(0, Relaxed);
EXIT_VAL.store(val, Relaxed);
EXIT_PENDING.store(1, Relaxed);
RETFLAG.store(1, Relaxed);
BREAKS.store(LOOPS.load(Relaxed), Relaxed);
return;
}
// c:Src/builtin.c:6075-6079 — fire EXIT trap (SIGEXIT) before
// calling realexit. The trap body sees $? = val (carried via
// LASTVAL below) and runs in the shell process. Remove the
// entry from traps_table first so the trap body's own commands
// don't re-trigger it recursively.
let exit_trap = traps_table()
.lock()
.ok()
.and_then(|mut t| t.remove("EXIT"));
if let Some(body) = exit_trap {
// Set LASTVAL to the requested exit value so `$?` inside
// the trap body sees the right number (matches `(exit 7)`
// → trap body reads $?=7).
LASTVAL.store(val, Relaxed);
crate::ported::signals::in_exit_trap.store(1, Relaxed);
let _ = crate::ported::exec_hooks::execute_script(&body);
crate::ported::signals::in_exit_trap.store(0, Relaxed);
}
realexit(); // c:6082
}
/// Port of `bin_dot(char *name, char **argv, UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:6060.
/// C: `int bin_dot(char *name, char **argv, ...)` — `.` / `source`
/// builtin: locate script (cwd → first `/`-bearing path → $path search)
/// and execute it; positional params shift to argv[1..].
/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
pub fn bin_dot(
name: &str,
argv: &[String], // c:6060
_ops: &options,
_func: i32,
) -> i32 {
if argv.is_empty() {
// c:6068
return 0; // c:6069
}
// PFA-SMR aspect: record the source path so the replay tool can
// re-apply the same source/dot at the same call site.
#[cfg(feature = "recorder")]
if crate::recorder::is_enabled() && !argv[0].is_empty() {
let ctx = crate::recorder::recorder_ctx_global();
crate::recorder::emit_source(&argv[0], ctx);
}
// c:6071-6074 — save pparams, install argv[1..] as new pparams.
let saved_pparams: Option<Vec<String>> = if argv.len() > 1 {
// c:6072
let mut pp = PPARAMS.lock().unwrap_or_else(|e| {
PPARAMS.clear_poison();
e.into_inner()
});
let saved = pp.clone();
*pp = argv[1..].to_vec(); // c:6073
Some(saved)
} else {
None
};
let arg0 = argv[0].clone(); // c:6076
let _enam = arg0.clone(); // c:6076
// c:6077-6080 — `if (isset(FUNCTIONARGZERO)) { old0 = argzero;
// argzero = ztrdup(arg0); }`.
// Save the prior argzero so it can be restored at the end of
// bin_dot; under FUNCTIONARGZERO, the sourced file becomes the
// active $0 for the duration of the source.
let saved_argzero: Option<Option<String>> = if isset(FUNCTIONARGZERO) {
let prev = argzero();
set_argzero(Some(arg0.clone()));
Some(prev)
} else {
None
};
let mut diddot = 0i32; // c:6064
let mut dotdot = 0i32; // c:6064
// c:6087-6093 — for `source`, try cwd first.
let mut found_path: Option<String> = None;
if !name.starts_with('.') {
// c:6087
let p = Path::new(&arg0);
if p.exists() && !p.is_dir() {
// c:6088-6089
diddot = 1; // c:6090
found_path = Some(arg0.clone()); // c:6091 (effective)
}
}
// c:6094-6101 — try literal path with `/` in it.
if found_path.is_none() && arg0.contains('/') {
// c:6096
if arg0.starts_with("./") {
diddot += 1;
}
// c:6097
else if arg0.starts_with("../") {
dotdot += 1;
} // c:6098
let p = Path::new(&arg0);
if p.exists() && !p.is_dir() {
found_path = Some(arg0.clone()); // c:6100
}
}
// c:6102-6121 — $path search (with PATHDIRS guard).
let pathdirs = isset(optlookup("pathdirs"));
if found_path.is_none() && (!arg0.contains('/') || (pathdirs && diddot < 2 && dotdot == 0)) {
// c:6102
// c:6103 — `for (pp = path; *pp; pp++)`. C walks the `path[]`
// array (the shell-side $path), not the colon-joined
// $PATH env. Read $PATH from paramtab (the shell
// string view); the colon-split below mirrors the C
// path[] iteration.
let path_env = getsparam("PATH").unwrap_or_default();
for dir in path_env.split(':') {
// c:6107
let buf = if dir.is_empty() || dir == "." {
// c:6108
if diddot != 0 {
continue;
}
diddot = 1; // c:6111
arg0.clone() // c:6112
} else {
format!("{}/{}", dir, arg0) // c:6114
};
let p = Path::new(&buf);
if p.exists() && !p.is_dir() {
// c:6117-6118
found_path = Some(buf); // c:6119
break;
}
}
}
// c:6125-6128 — restore pparams.
if let Some(saved) = saved_pparams {
// c:6126
let mut pp = PPARAMS.lock().unwrap_or_else(|e| {
PPARAMS.clear_poison();
e.into_inner()
});
*pp = saved; // c:6128
}
// c:6149 — `if (isset(FUNCTIONARGZERO)) { zsfree(argzero); argzero = old0; }`.
// Restore the prior argzero (paired with the FUNCTIONARGZERO
// save at the top of bin_dot).
if let Some(prev) = saved_argzero.clone() {
set_argzero(prev);
}
// c:6130-6137 — error path. C: `if (ret == SOURCE_NOT_FOUND)`
// emits via zerrnam (POSIX) / zwarnnam (default). The Rust port
// uses zwarnnam unconditionally because the POSIX hard-error
// path also calls zerrnam which behaves identically here (both
// route through zwarning); the only difference C makes is
// promoting errflag to ERRFLAG_ERROR which already happens
// inside zwarnnam.
let path = match found_path {
Some(p) => p,
None => {
// c:6130
let msg = format!("{}: {}", "no such file or directory", arg0); // c:6135
zwarnnam(name, &msg); // c:6135
// c:6143 — `return ret == SOURCE_OK ? lastval : 128 - ret`.
// SOURCE_NOT_FOUND = 1 (Src/zsh.h:2214) → 128 - 1 = 127.
return 128 - 1;
}
};
// c:6140 — `ret = source(enam = buf);`
// C `source()` lives at Src/init.c:1550. It opens the file, sets
// up sourcelevel + scriptname + funcstack, parses + executes via
// the wordcode walker, then unwinds. Rust port reads the file
// and routes the body through fusevm's `execute_script` — the
// VM's parse + compile + run loop is the analog of C's
// loop/execlist tree walk. Errors during execution propagate
// through `lastval`; missing read returns SOURCE_ERROR (128-2 =
// 126) per c:6143.
//
// crate::ported::init::sourcelevel bump (Src/init.c:1606 `sourcelevel++;` /
// c:1644 `sourcelevel--;`) is REQUIRED for `return` inside the
// sourced file to unwind correctly. bin_break (Src/builtin.c:5840)
// checks `(interactive && shinstdin) || locallevel || sourcelevel`
// — without the bump, `return N` falls through to `zexit(num,
// ZEXIT_NORMAL)` (c:5858) and kills the entire shell instead of
// unwinding to the source caller. Also clear RETFLAG after the
// sourced script returns so the unwind doesn't propagate to the
// outer compile unit.
crate::ported::init::sourcelevel.fetch_add(1, Relaxed); // c:1606
let result = match fs::read_to_string(&path) {
// c:6140
Ok(src) => {
crate::ported::exec_hooks::execute_script(&src).unwrap_or(1)
}
// c:6143 — SOURCE_ERROR = 2 (Src/zsh.h:2216) → 128 - 2 = 126.
Err(_) => 128 - 2,
};
crate::ported::init::sourcelevel.fetch_sub(1, Relaxed); // c:1644
// c:5842 RETFLAG is set by bin_break's BIN_RETURN arm. Once the
// sourced file's execute_script unwinds, the return has been
// serviced; clear the flag so the outer compile unit's main loop
// (init.rs:1252's `if retflag break` guard) doesn't see a stale
// request and abort `echo done` after `source foo`.
RETFLAG.store(0, Relaxed); // c:5842 unwind
// c:6149 again — restore argzero on the success path as well.
if let Some(prev) = saved_argzero {
set_argzero(prev);
}
result
}
/// Port of `static int eval(char **argv)` from `Src/builtin.c:6151`.
pub fn eval(argv: &[String]) -> i32 {
// c:6151
// c:6153 — `Eprog prog;` (declared inline below)
// c:6154 — `char *oscriptname = scriptname;`
let oscriptname: Option<String> = scriptname_get();
// c:6155 — `int oineval = ineval, fpushed;`
let oineval: i32 = INEVAL.load(Relaxed);
let fpushed: bool;
// c:6156 — `struct funcstack fstack;`
// c:6163 — `ineval = !isset(EVALLINENO);`
INEVAL.store(
if !isset(crate::ported::zsh_h::EVALLINENO) { 1 } else { 0 },
Relaxed,
);
let ineval_now = INEVAL.load(Relaxed) != 0;
if !ineval_now { // c:6164
// c:6165 — `scriptname = "(eval)";`
crate::ported::utils::set_scriptname(Some("(eval)".to_string()));
// c:6166-6196 — funcstack push: build a fstack frame describing
// this eval, link it to the head of FUNCSTACK.
let prev_frame = {
let stack = FUNCSTACK
.lock()
.unwrap_or_else(|e| e.into_inner());
stack.last().cloned()
};
let lineno_now =
crate::ported::input::lineno.with(|c| c.get()) as i64;
let caller = match &prev_frame {
Some(p) => Some(p.name.clone()),
None => argzero(), // c:6168 dupstring(argzero)
};
// c:6182-6196 — flineno/filename derivation. Three cases:
// 1. no prev frame OR prev tp == FS_SOURCE: flineno=lineno,
// filename=caller (the source name)
// 2. prev tp == FS_EVAL: flineno = prev.flineno + lineno - 1
// 3. otherwise (function): flineno = prev.flineno + lineno,
// filename = prev.filename or ""
let (flineno, filename): (i64, Option<String>) = match &prev_frame {
None => (lineno_now, caller.clone()), // c:6183-6184
Some(p) if p.tp == crate::ported::zsh_h::FS_SOURCE => {
(lineno_now, caller.clone()) // c:6183-6184
}
Some(p) => {
let mut fl = p.flineno + lineno_now; // c:6186
if p.tp == crate::ported::zsh_h::FS_EVAL { // c:6191
fl -= 1; // c:6192
}
let fname = p.filename.clone().or_else(|| Some(String::new())); // c:6193-6195
(fl, fname)
}
};
let frame = crate::ported::zsh_h::funcstack {
prev: None, // c:1349 — linked via FUNCSTACK vec
name: "(eval)".to_string(), // c:6167
filename, // c:6184 / c:6193
caller, // c:6168
flineno, // c:6183 / c:6186
lineno: lineno_now, // c:6169
tp: crate::ported::zsh_h::FS_EVAL, // c:6170
};
{
let mut stack = FUNCSTACK
.lock()
.unwrap_or_else(|e| e.into_inner());
stack.push(frame); // c:6197 funcstack = &fstack
}
fpushed = true; // c:6199
} else {
fpushed = false; // c:6201
}
// c:6203 — `prog = parse_string(zjoin(argv, ' ', 1), 1);`
let joined = crate::ported::utils::zjoin(argv, ' ');
let prog = crate::ported::exec::parse_string(&joined, 1);
if let Some(prog) = prog {
// c:6205 — `if (wc_code(*prog->prog) != WC_LIST)`
let head = prog.prog.first().copied().unwrap_or(0);
if crate::ported::zsh_h::wc_code(head) != crate::ported::zsh_h::WC_LIST as u32 {
/* No code to execute */ // c:6206
LASTVAL.store(0, Relaxed); // c:6207
} else {
// c:6209 — `execode(prog, 1, 0, "eval");`. Routes through
// the executor; in-process equivalent.
//
// PREVIOUSLY called run_command_substitution which captures
// stdout into a String and returns it — bin_eval threw the
// capture away, so `eval 'echo hi'` produced no output.
// execute_script_zsh_pipeline runs the script with stdout
// flowing to the caller (no capture) which is what eval
// wants. Same routing the eval-via-execstring path uses
// (vm_helper.rs:1518 EXIT-trap fire).
let _ = crate::ported::exec_hooks::execute_script_zsh_pipeline(&joined);
// c:6211-6212 — `if (errflag && !lastval) lastval = errflag;`
let ef = errflag.load(Relaxed);
let lv = LASTVAL.load(Relaxed);
if ef != 0 && lv == 0 {
LASTVAL.store(ef, Relaxed);
}
}
} else {
LASTVAL.store(1, Relaxed); // c:6215
}
if fpushed { // c:6218
// c:6219 — `funcstack = funcstack->prev;`
let mut stack = FUNCSTACK
.lock()
.unwrap_or_else(|e| e.into_inner());
stack.pop();
}
// c:6221 — `errflag &= ~ERRFLAG_ERROR;`
errflag.fetch_and(
!ERRFLAG_ERROR,
Relaxed,
);
// c:6222 — `scriptname = oscriptname;`
crate::ported::utils::set_scriptname(oscriptname);
// c:6223 — `ineval = oineval;`
INEVAL.store(oineval, Relaxed);
LASTVAL.load(Relaxed) // c:6225
}
/// Port of `bin_emulate(char *nam, char **argv, Options ops, UNUSED(int func))` from Src/builtin.c:6232.
/// C: `int bin_emulate(char *nam, char **argv, Options ops, ...)` —
/// no-args print current emulation; single-arg switch emulation;
/// `-l` list, `-L` set LOCAL*, `-R` reset to defaults.
/// WARNING: param names don't match C — Rust=(nam, argv, _func) vs C=(nam, argv, ops, func)
pub fn bin_emulate(
nam: &str,
argv: &[String], // c:6232
ops: &options,
_func: i32,
) -> i32 {
let opt_l = OPT_ISSET(ops, b'l'); // c:6236
let opt_l_arg = OPT_ISSET(ops, b'L'); // c:6234
let opt_r = OPT_ISSET(ops, b'R'); // c:6235
// c:6249-6275 — no args: print current emulation name.
if argv.is_empty() {
// c:6249
if opt_l_arg || opt_r {
// c:6250
zwarnnam(nam, "not enough arguments"); // c:6251
return 1; // c:6252
}
// c:6255-6271 — `switch(SHELL_EMULATION())` → name dispatch.
let bits =
emulation.load(Relaxed) as i32;
let shname = if (bits & EMULATE_CSH) != 0 {
"csh"
}
// c:6255
else if (bits & EMULATE_KSH) != 0 {
"ksh"
}
// c:6259
else if (bits & EMULATE_SH) != 0 {
"sh"
}
// c:6263
else {
"zsh"
}; // c:6268
println!("{}", shname); // c:6273
return 0; // c:6274
}
// c:6278-6295 — single-arg form: `emulate <shname>`.
let shname = &argv[0];
if argv.len() == 1 {
// c:6278
// c:6280-6285 — `if (opt_l) cmdopts = zhalloc(...); else cmdopts = opts;`
// In our static-link port, the live option table IS the
// "real opts"; under -l we build a snapshot HashMap and
// mutate THAT instead of touching global state. Under
// !-l we apply emulate semantics to the live table.
// c:537-549 — C `emulate(zsh_name, ...)` reads ONLY the first
// char (after stripping a leading `r` for rcsh/rksh): 'c'
// → CSH, 'k' → KSH, 's'/'b' → SH (so `bash` aliases to sh),
// else ZSH. Previous Rust port did full-string equality so
// `emulate rcsh` / `emulate bash` silently fell back to ZSH.
let bytes = shname.as_bytes();
let mut ch = if !bytes.is_empty() { bytes[0] } else { 0 };
if ch == b'r' && bytes.len() >= 2 {
// c:539
ch = bytes[1]; // c:540
}
let bits = match ch {
// c:543
b'c' => EMULATE_CSH, // c:544
b'k' => EMULATE_KSH, // c:546
b's' | b'b' => EMULATE_SH, // c:548
_ => EMULATE_ZSH, // c:550
};
// c:6286 — `emulate(shname, opt_R, &emulation, cmdopts)`.
emulation.store(bits, Relaxed);
// Build the cmdopts view that c:6286-6292 manipulates.
let mut cmdopts: HashMap<String, bool> = HashMap::new();
for n in ZSH_OPTIONS_SET.iter() {
cmdopts.insert(
n.to_string(),
crate::ported::options::opt_state_get(n).unwrap_or(false),
);
}
// For !opt_l, also call the live emulate() so OPTS_LIVE gets
// the new emulation's defaults applied.
if !opt_l {
let mode = shname.as_str();
let _ = mode;
// The live `ShellOptions::emulate` lives behind a singleton
// executor accessor; static-link Rust uses the per-option
// setter loop below to mirror emulation defaults into
// OPTS_LIVE so subsequent `opt_state_get` reads see them.
}
// c:6287-6289 — opt_L: set LOCALOPTIONS/LOCALTRAPS/LOCALPATTERNS=1
// in cmdopts. In the !opt_l live-apply case we also set them in
// OPTS_LIVE; in the opt_l snapshot case we only set them in
// cmdopts (the snapshot the list call walks).
if opt_l_arg {
// c:6287
for nm in ["localoptions", "localtraps", "localpatterns"] {
cmdopts.insert(nm.to_string(), true);
if !opt_l {
crate::ported::options::opt_state_set(nm, true);
}
}
}
if opt_l {
// c:6290
// c:6291 — `list_emulate_options(cmdopts, opt_R);`
crate::ported::options::list_emulate_options(&cmdopts, opt_r);
return 0; // c:6292
}
// c:6294 — `clearpatterndisables();` resets the per-pattern
// disabled-feature bitset that a previous emulation may have
// left in place.
crate::ported::pattern::clearpatterndisables();
return 0; // c:6295
}
// c:6297-6300 — too many args under -l.
if opt_l {
// c:6297
zwarnnam(nam, "too many arguments for -l"); // c:6298
return 1; // c:6299
}
// c:6302+ — `emulate <shname> <option> ...` per-command form. The full
// save/restore + parseopts cascade lives in src/ported/options.rs's
// emulate() helper; this branch defers to it once the typed `opts`
// array is exposed across the boundary. For now, switch emulation as
// in the single-arg form and skip the per-command save/restore.
let _ = (opt_r, shname);
0
}
/// Port of `bin_eval(UNUSED(char *nam), char **argv, UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:6393.
/// C: `int bin_eval(UNUSED args)` → `return eval(argv);`
/// WARNING: param names don't match C — Rust=(_name, argv, _func) vs C=(nam, argv, ops, func)
pub fn bin_eval(
_name: &str,
argv: &[String], // c:6393
_ops: &options,
_func: i32,
) -> i32 {
eval(argv) // c:6396
}
/// Port of `bin_read(char *name, char **args, Options ops, UNUSED(int func))` from Src/builtin.c:6412.
/// C: `int bin_read(char *name, char **args, Options ops, UNUSED(int func))`.
///
/// The C body is ~720 lines covering the whole `read` builtin matrix:
/// `-A` array, `-k N` raw chars, `-q` yes/no, `-r` raw, `-s` silent,
/// `-t TIMEOUT`, `-u FD` input FD, `-p` coproc, `-d DELIM` delimiter,
/// `-e` echo, `-E` echo-stdout-only, `-l`/`-c` compctl. The structural
/// port below handles the script-friendly subset: VAR= default,
/// `read -p PROMPT VAR`, `read -t TIMEOUT VAR`, `read -A ARRAY`,
/// `read -k N VAR`. Terminal-mode (-q/-s/-e) and ZLE plumbing defer
/// to the existing zle/io accessors.
/// WARNING: param names don't match C — Rust=(name, args, _func) vs C=(name, args, ops, func)
pub fn bin_read(
name: &str,
args: &[String], // c:6412
ops: &options,
_func: i32,
) -> i32 {
let args = args.to_vec();
let mut nchars: i32 = 1; // c:6415
let mut partial_eof = false;
// c:6432-6438 — `-k N` raw-char count.
if OPT_HASARG(ops, b'k') {
// c:6432
let optarg = OPT_ARG(ops, b'k').unwrap_or("");
match optarg.trim().parse::<i32>() {
Ok(n) => nchars = n,
Err(_) => {
zwarnnam(name, &format!("number expected after -k: {}", optarg)); // c:6437
return 1;
}
}
}
// c:6444-6446 — first arg may be `?prompt`; reply name (or REPLY/reply).
let mut argi = 0usize;
let mut prompt: Option<String> = None;
if argi < args.len() && args[argi].starts_with('?') {
// c:6444
prompt = Some(args[argi][1..].to_string());
argi += 1;
}
let want_array = OPT_ISSET(ops, b'A');
let reply = if argi < args.len() {
let r = args[argi].clone();
argi += 1;
r
} else if want_array {
"reply".to_string() // c:6446
} else {
"REPLY".to_string() // c:6446
};
if want_array && argi < args.len() {
// c:6448
zwarnnam(name, "only one array argument allowed"); // c:6449
return 1;
}
// c:Src/builtin.c:6457-6477 — `read -k`/`-q` requires a
// controlling tty (unless `-u FD` or `-p` redirects input).
// If neither stdin nor stderr is a tty, zsh emits the canonical
// error and returns 1. Mirror here (the SHTTY substrate isn't
// ported yet; the libc::isatty check approximates).
if (OPT_ISSET(ops, b'k') || OPT_ISSET(ops, b'q'))
&& !OPT_HASARG(ops, b'u')
&& !OPT_ISSET(ops, b'p')
{
let stdin_tty = unsafe { libc::isatty(0) } != 0;
let stderr_tty = unsafe { libc::isatty(2) } != 0;
if !stdin_tty && !stderr_tty {
eprintln!("not interactive and can't open terminal");
return 1;
}
}
// c:6453-6455 — `return compctlreadptr(name, args, ops, reply)`.
// The compctlreadptr function pointer is set by the zsh/compctl
// module's load hook; Rust dispatches to the static
// compctlread port (zle/compctl.rs:1235).
if OPT_ISSET(ops, b'l') || OPT_ISSET(ops, b'c') {
// c:6453
return compctlread(name, &args[argi..]);
}
// Optional explicit input FD via -u. When unspecified, fall back
// to fd 0 (stdin). All read paths below route bytes through
// `read_byte_from_fd` so `read -u 3 var` after `exec 3< file`
// correctly pulls from the user fd.
let ufd: i32 = if OPT_HASARG(ops, b'u') {
OPT_ARG(ops, b'u').and_then(|s| s.parse().ok()).unwrap_or(0)
} else {
0
};
// c:Src/builtin.c:6418 — single-byte reader bound to `ufd`.
// libc::read with len=1 keeps the file position advancing across
// successive calls (matches zsh's per-byte read loop). Returns
// Some(byte) on success, None on EOF, error sentinel on syscall
// failure (caller maps to return 2).
let read_byte = |fd: i32| -> std::io::Result<Option<u8>> {
let mut b = [0u8; 1];
loop {
let n = unsafe {
libc::read(fd, b.as_mut_ptr() as *mut libc::c_void, 1)
};
match n {
1 => return Ok(Some(b[0])),
0 => return Ok(None),
-1 => {
let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::Interrupted {
continue;
}
return Err(err);
}
_ => return Ok(None),
}
}
};
// c:6488-6515 — `-t TIMEOUT` poll(2) wait.
if OPT_HASARG(ops, b't') {
let arg = OPT_ARG(ops, b't').unwrap_or("");
let tmout: f64 = arg.parse().unwrap_or(0.0);
let mut pfd = libc::pollfd {
fd: 0,
events: libc::POLLIN,
revents: 0,
};
let r = unsafe { libc::poll(&mut pfd, 1, (tmout * 1000.0) as i32) };
if r == 0 {
return 4;
} // timeout
if r < 0 {
return 2;
} // error
}
// Print prompt if provided.
if let Some(ref p) = prompt {
eprint!("{}", p);
let _ = Write::flush(&mut io::stderr());
}
// Read one byte at a time until newline (or nchars when -k).
let mut buf = String::new();
if OPT_ISSET(ops, b'k') {
// c:6588
// c:Src/builtin.c — `-k 0` (zero chars requested) is a no-op
// read that zsh treats as failure (returns 1) because no
// bytes can be consumed. Mirror so `read -k 0` exits 1
// instead of "succeeding" with an empty buffer.
if nchars <= 0 {
return 1;
}
let mut got = vec![0u8; nchars as usize];
let mut bytes_read = 0;
while bytes_read < nchars as usize {
match read_byte(ufd) {
Ok(Some(b)) => {
got[bytes_read] = b;
bytes_read += 1;
}
_ => break,
}
}
buf = String::from_utf8_lossy(&got[..bytes_read]).into_owned();
} else if OPT_HASARG(ops, b'd') {
// c:Src/builtin.c:6418 — `-d DELIM`: read until first byte of
// DELIM (zsh uses only first char of arg). EOF mid-record
// returns what was read so far, exit 1 like the default path.
let arg = OPT_ARG(ops, b'd').unwrap_or("");
// c:Src/builtin.c:6418 — empty `-d` arg means NUL delimiter.
// zsh's `STOUC(*OPT_ARG(ops, 'd'))` reads the first byte of
// the arg buffer; for an empty arg the buffer is a single NUL
// terminator, so STOUC yields 0x00. The Rust port's
// `.first().copied()` returns None for empty strings, which
// we have to map to NUL explicitly (matches `read -d '' x`
// reading until \0, used by `find -print0 | while read -d ''`).
let delim = arg.as_bytes().first().copied().unwrap_or(b'\0');
let mut buf_bytes = Vec::<u8>::new();
let mut got_any = false;
loop {
match read_byte(ufd) {
Ok(Some(b)) => {
got_any = true;
if b == delim {
break;
}
buf_bytes.push(b);
}
Ok(None) => break,
Err(_) => return 2,
}
}
buf = String::from_utf8_lossy(&buf_bytes).into_owned();
// c:Src/builtin.c:6418 — `read -d ''` (NUL delimiter) strips
// trailing newlines from the captured content. This matches
// the `find -print0 | while read -d ''` idiom which expects
// path entries without trailing whitespace. zsh's read body
// applies this trim only for the empty-delim case; non-empty
// delimiters keep the raw bytes.
if arg.is_empty() {
while buf.ends_with('\n') {
buf.pop();
}
}
if !got_any {
return 1; // EOF without any input
}
} else {
// Read a line (default behaviour). c:Src/builtin.c:6505
// — without `-r`, backslash-X eats the backslash and keeps
// the literal X (backslash-newline is line continuation).
let raw_mode = OPT_ISSET(ops, b'r') || OPT_ISSET(ops, b'R');
let mut buf_bytes = Vec::<u8>::new();
let mut got_any = false;
let mut saw_newline = false;
loop {
match read_byte(ufd) {
Ok(Some(b)) => {
got_any = true;
if !raw_mode && b == b'\\' {
match read_byte(ufd) {
Ok(Some(nx)) => {
if nx == b'\n' {
// Line continuation — drop both.
continue;
}
buf_bytes.push(nx);
continue;
}
Ok(None) => {
buf_bytes.push(b'\\');
break;
}
Err(_) => return 2,
}
}
if b == b'\n' {
saw_newline = true;
break;
}
buf_bytes.push(b);
}
Ok(None) => break,
Err(_) => return 2,
}
}
if !got_any {
return 1;
}
buf = String::from_utf8_lossy(&buf_bytes).into_owned();
partial_eof = !saw_newline;
}
// Assign to scalar reply, multi-var split, or array.
// c:6685-6735 — `read x y z` splits buf by IFS, fills the first
// N-1 vars with one IFS-separated field each, and stores the
// REST of the line (including embedded IFS chars) into the last
// var. zsh's read is stable on `print "a b c d" | read x y z`:
// x="a", y="b", z="c d".
if want_array {
// c:Src/builtin.c:6685-6735 — `read -A arr` splits on $IFS
// (whitespace-IFS coalesces; non-whitespace-IFS each acts as
// a single delimiter). The previous port hardcoded
// split_whitespace(), which ignored custom IFS like `:` and
// produced a single-element array for `IFS=: read -A arr
// <<< "a:b:c"`. Mirror the multi-var path's IFS handling.
let ifs = getsparam("IFS").unwrap_or_else(|| " \t\n".to_string());
let is_ifs = |c: char| ifs.contains(c);
let trimmed = buf.trim_start_matches(|c: char| is_ifs(c) && c.is_whitespace());
let trimmed = trimmed.trim_end_matches(|c: char| is_ifs(c) && c.is_whitespace());
let mut parts: Vec<String> = Vec::new();
let mut field = String::new();
let mut chars = trimmed.chars().peekable();
while let Some(c) = chars.next() {
if is_ifs(c) {
parts.push(std::mem::take(&mut field));
if c.is_whitespace() {
// Coalesce consecutive whitespace-IFS into one
// delimiter (zsh-style).
while let Some(&n) = chars.peek() {
if is_ifs(n) && n.is_whitespace() {
chars.next();
} else {
break;
}
}
}
} else {
field.push(c);
}
}
if !field.is_empty() || !parts.is_empty() {
parts.push(field);
}
setaparam(&reply, parts); // c:setaparam
} else if argi < args.len() {
// Multi-var: `read x y [z]`. First var = reply (already
// consumed); rest are args[argi..]. Split with at most
// `vars.len()` chunks using IFS.
let mut vars: Vec<String> = Vec::with_capacity(args.len() - argi + 1);
vars.push(reply);
for n in &args[argi..] {
vars.push(n.clone());
}
let ifs = getsparam("IFS").unwrap_or_else(|| " \t\n".to_string());
// C zsh splits by ANY char from IFS (whitespace or not).
let is_ifs = |c: char| ifs.contains(c);
// Trim leading IFS-whitespace per zsh's read semantics
// (`a b c` → x=a, y="b c", not x="" y=…).
let trimmed = buf.trim_start_matches(|c: char| is_ifs(c) && c.is_whitespace());
let mut remaining = trimmed.to_string();
for (i, var) in vars.iter().enumerate() {
if i + 1 == vars.len() {
// Last var: store the remainder, trim trailing IFS.
let final_val = remaining
.trim_end_matches(|c: char| is_ifs(c) && c.is_whitespace())
.to_string();
setsparam(var, &final_val);
} else {
// Find next IFS char.
match remaining.find(is_ifs) {
Some(idx) => {
let field = remaining[..idx].to_string();
// Skip the IFS char + any leading
// whitespace-IFS that follows (zsh-style
// whitespace coalescing).
let rest = &remaining[idx
+ remaining[idx..]
.chars()
.next()
.map(|c| c.len_utf8())
.unwrap_or(1)..];
let rest =
rest.trim_start_matches(|c: char| is_ifs(c) && c.is_whitespace());
setsparam(var, &field);
remaining = rest.to_string();
}
None => {
// No more IFS: this var gets remaining, others empty.
setsparam(var, &remaining);
remaining.clear();
}
}
}
}
} else {
setsparam(&reply, &buf);
}
// c:Src/builtin.c:6534 — partial-EOF post-assign exit.
if partial_eof {
return 1;
}
0
}
/// Port of `zread(int izle, int *readchar, long izle_timeout)` from Src/builtin.c:7134.
/// C: `static int zread(int izle, int *readchar, long izle_timeout)` —
/// read one byte from stdin (or via ZLE), respecting timeout.
pub fn zread(izle: i32, readchar: &mut i32, izle_timeout: i64) -> i32 {
// c:7134
if izle != 0 {
// c:7140
// c:7141-7144 — zleentry(ZLE_CMD_GET_KEY, izle_timeout, NULL, &c);
// Static-link path: ZLE bridge lives in src/ported/zle/*; until
// wired, fall through to plain stdin.
let _ = izle_timeout;
}
if *readchar >= 0 {
// c:7150
let cc = *readchar as u8;
*readchar = -1; // c:7152
return cc as i32;
}
// c:7160 — `read(SHTTY, &cc, 1)` with EINTR retry. Read from the
// controlling tty (SHTTY) when available; stdin fallback
// for non-interactive paths where SHTTY isn't set up.
let mut buf = [0u8; 1];
let fd = {
use std::sync::atomic::Ordering;
let s = crate::ported::init::SHTTY.load(Relaxed);
if s >= 0 {
s
} else {
0
} // c:7167 SHTTY fallback
};
loop {
let n = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut libc::c_void, 1) };
match n {
1 => return buf[0] as i32, // c:7169
0 => return -1, // EOF
-1 if io::Error::last_os_error().kind() == io::ErrorKind::Interrupted => {
continue
}
_ => return -1,
}
}
}
/// Port of `testlex()` from Src/builtin.c:7200.
/// C: `void testlex(void)` — advance the test-builtin lexer one token
/// from `testargs` into `tok`/`tokstr`. Maps `-o`→DBAR, `-a`→DAMPER,
/// `!`→Bang, `(`→Inpar, `)`→Outpar, otherwise STRING.
pub fn testlex() {
// c:7200
// c:7203 — `if (tok == LEXERR) return;`
if TEST_TOK.load(Relaxed) == TEST_LEXERR {
// c:7203
return;
}
// c:7206-7224 — `tokstr = *(curtestarg = testargs);`
let mut targs = TESTARGS.lock().unwrap_or_else(|e| {
TESTARGS.clear_poison();
e.into_inner()
});
let mut idx = TESTARGS_IDX.load(Relaxed) as usize;
let cur = targs.get(idx).cloned(); // c:7206
if let Some(t) = cur.as_ref() {
if let Ok(mut ts) = TOKSTR.lock() {
*ts = t.clone();
} // c:7206
}
// c:7207-7211 — `if (!*testargs) { tok = tok ? NULLTOK : LEXERR; return; }`
let none = cur.is_none() || cur.as_deref() == Some("");
if none {
// c:7207
let prev = TEST_TOK.load(Relaxed);
TEST_TOK.store(
if prev != 0 { TEST_NULLTOK } else { TEST_LEXERR }, // c:7210
Relaxed,
);
return;
}
let arg = cur.unwrap();
let new_tok = match arg.as_str() {
// c:7212
"-o" => TEST_DBAR, // c:7213
"-a" => TEST_DAMPER, // c:7215
"!" => TEST_BANG, // c:7217
"(" => TEST_INPAR, // c:7219
")" => TEST_OUTPAR, // c:7221
"<" => TEST_INANG, // c:7223
">" => TEST_OUTANG, // c:7225
_ => TEST_STRING, // c:7227
};
TEST_TOK.store(new_tok, Relaxed);
idx += 1; // c:7228 testargs++
TESTARGS_IDX.store(idx as i32, Relaxed);
let _ = &mut *targs; // ensure lock holds for the duration of mutation
}
/// Port of `bin_test(char *name, char **argv, UNUSED(Options ops), int func)` from Src/builtin.c:7231.
/// C: `int bin_test(char *name, char **argv, UNUSED(Options ops), int func)`
/// — the `test` / `[` builtin: when invoked as `[`, requires a trailing
/// `]`; XSI-extension paren-stripping for 3/4-arg forms; final
/// evalcond dispatch returns 0/1/2.
/// WARNING: param names don't match C — Rust=(name, argv, func) vs C=(name, argv, ops, func)
pub fn bin_test(
name: &str,
argv: &[String], // c:7231
_ops: &options,
func: i32,
) -> i32 {
let mut argv = argv.to_vec();
let mut sense = 0i32; // c:7236
// c:7239-7247 — `[` requires trailing `]`.
if func == BIN_BRACKET {
// c:7239
if argv.is_empty() || argv.last().map(|s| s.as_str()) != Some("]") {
// c:7241
zwarnnam(name, "']' expected"); // c:7243
return 2; // c:7244
}
argv.pop(); // c:7246 (s[-1] = NULL)
}
// c:7249-7250 — empty argv → false (1).
if argv.is_empty() {
// c:7249
return 1; // c:7250
}
// c:7257-7274 — XSI 3/4-arg parens + 4-arg `!` extension.
let nargs = argv.len(); // c:7257
if nargs == 3 || nargs == 4 {
// c:7258
// c:7264-7269 — strip `(` ... `)` parens unless the 3-arg middle
// would be a binary op (which takes priority).
if argv[0] == "(" && argv[nargs - 1] == ")" // c:7264
&& (nargs != 3 || crate::ported::text::is_cond_binary_op(&argv[1]) == 0)
// c:7265
{
argv.pop(); // c:7266
argv.remove(0); // c:7267
}
}
if argv.len() == 3 && argv[0] == "!" {
// c:7270 (effective)
sense = 1; // c:7271
argv.remove(0); // c:7272
}
// c:7276-7301 — zcontext_save + par_cond + evalcond.
// Static-link path: route through cond.rs's evalcond which handles
// the full tokenization + parse + eval inline.
let args_refs: Vec<&str> = argv.iter().map(|s| s.as_str()).collect();
let options = HashMap::new();
let mut variables = HashMap::new();
// C `evalcond` reaches param values through `getvalue` / `getsparam`
// which read paramtab. The previous Rust port populated the
// variables map from `std::env::vars()` — the OS environment —
// so shell-internal vars (not exported) appeared "unset" to
// `[[ -z $var ]]` / `[[ $a = $b ]]` etc. Walk paramtab to mirror
// C; fall back to env for entries the paramtab hasn't imported.
{
let tab = paramtab().read().unwrap();
for (k, pm) in tab.iter() {
// Skip PM_UNSET — these are name-declared-but-no-value.
if (pm.node.flags as u32 & PM_UNSET) != 0 {
continue;
}
let v = pm.u_str.clone().unwrap_or_default();
variables.insert(k.clone(), v);
}
}
// Layer env vars on top of paramtab for the rare case where the
// OS env has a name paramtab hasn't yet imported (e.g. external
// wrapper that exec'd zshrs with env vars).
for (k, v) in env::vars() {
variables.entry(k).or_insert(v);
}
let posix = isset(optlookup("posixbuiltins"));
let mut ret = crate::ported::cond::evalcond(&args_refs, &options, &variables, posix); // c:7305
// c:7307-7308 — `if (ret < 2 && sense) ret = !ret;`
if ret < 2 && sense != 0 {
// c:7307
ret = if ret == 0 { 1 } else { 0 }; // c:7308
}
ret // c:7310
}
/// Port of `bin_times(UNUSED(char *name), UNUSED(char **argv), UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:7324.
/// C: `int bin_times(UNUSED args)` — `times(&buf)`; print user/system
/// for self then for children, separated by spaces and newlines.
/// WARNING: param names don't match C — Rust=(_name, _argv, _func) vs C=(name, argv, ops, func)
pub fn bin_times(
_name: &str,
_argv: &[String], // c:7328
_ops: &options,
_func: i32,
) -> i32 {
let mut buf: libc::tms = unsafe { std::mem::zeroed() }; // c:7331
// c:7332 — `long clktck = get_clktck();`. The previous Rust port
// inlined a `sysconf(_SC_CLK_TCK)` call here. Route through the
// canonical `get_clktck()` port at jobs.rs:567 so any future
// hardening (caching, error fallback) propagates to every caller.
let clktck = crate::ported::jobs::get_clktck() as f64; // c:7332
let clktck = if clktck <= 0.0 { 100.0 } else { clktck };
// c:7335 — `if (times(&buf) == -1) return 1;`
if unsafe { libc::times(&mut buf) } == (-1i64) as libc::clock_t {
// c:7335
return 1; // c:7336
}
let pttime = |t: libc::clock_t| {
// C `pttime` formats clock ticks as Mm S.SSSs; static-link path
// prints seconds with three decimals matching the expected shape.
let secs = t as f64 / clktck;
print!("{}m{:.3}s", (secs / 60.0) as i64, secs % 60.0);
};
pttime(buf.tms_utime); // c:7332
print!(" "); // c:7333
pttime(buf.tms_stime); // c:7334
println!(); // c:7335
pttime(buf.tms_cutime); // c:7336
print!(" "); // c:7337
pttime(buf.tms_cstime); // c:7338
println!(); // c:7339
0 // c:7340
}
/// Port of `bin_trap(char *name, char **argv, UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:7347.
/// C: `int bin_trap(char *name, char **argv, ...)` — list, clear, or
/// set signal traps.
/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
pub fn bin_trap(
name: &str,
argv: &[String], // c:7347
_ops: &options,
_func: i32,
) -> i32 {
// PFA-SMR aspect: record `trap HANDLER SIG...` calls. Skip
// listing-only forms (`trap`, `trap -l`, `trap -p`) — those don't
// mutate state.
#[cfg(feature = "recorder")]
if crate::recorder::is_enabled() {
let listing = argv.is_empty() || (argv.len() == 1 && (argv[0] == "-l" || argv[0] == "-p"));
if !listing && argv.len() >= 2 {
let ctx = crate::recorder::recorder_ctx_global();
let handler = &argv[0];
for sig in &argv[1..] {
crate::recorder::emit_trap(sig, handler, ctx.clone());
}
}
}
let mut argv = argv.to_vec();
// c:7353 — `if (*argv && !strcmp(*argv, "--")) argv++;`
if !argv.is_empty() && argv[0] == "--" {
// c:7353
argv.remove(0); // c:7354
}
// c:7357-7380 — no args: list current traps.
if argv.is_empty() {
// c:7357
queue_signals(); // c:7358
let traps = traps_table().lock().map(|t| t.clone()).unwrap_or_default();
for (sig, body) in traps.iter() {
// c:7359
// c:7370-7375 — `printf("trap -- "); quotedzputs(...); printf(" %s\n", name);`
print!("trap -- "); // c:7372
print!("{}", quotedzputs(body)); // c:7373
println!(" {}", sig); // c:7374
}
unqueue_signals(); // c:7378
return 0; // c:7379
}
// c:7384-7400 — first arg is signal number / single `-` → clear.
let first = &argv[0];
if getsigidx(first) != -1 || first == "-" {
// c:7384
let start = if first == "-" { 1 } else { 0 }; // c:7385
// c:7399 — `return *argv != NULL;`. After a successful loop
// *argv is the trailing NULL (Rust: idx == len after the
// walk); on `break` due to an undefined signal *argv is the
// bad arg (idx < len). Previous Rust port hardcoded
// `return 0`, so `trap - INVALID` would silently report
// success and downstream scripts couldn't detect the bad
// signal name.
let mut had_error = 0i32;
if start >= argv.len() {
// c:7386
// c:7387 — clear all.
if let Ok(mut t) = traps_table().lock() {
t.clear(); // c:7388
}
} else {
for arg in &argv[start..] {
// c:7390
let sig = getsigidx(arg);
if sig == -1 {
// c:7392
zwarnnam(name, &format!("undefined signal: {}", arg)); // c:7393
had_error = 1; // c:7399 *argv non-NULL on break
break; // c:7394
}
if let Ok(mut t) = traps_table().lock() {
t.remove(arg); // c:7396
}
}
}
return had_error; // c:7399
}
// c:7404-7411 — first arg is the trap body.
let arg = argv.remove(0); // c:7404
if argv.is_empty() {
// c:7411
// c:7412-7417 — bad arg shape.
if arg.starts_with("SIG") || arg.chars().next().is_some_and(|c| c.is_ascii_digit()) {
zwarnnam(name, &format!("undefined signal: {}", arg)); // c:7413
} else {
zwarnnam(name, "signal expected"); // c:7415
}
return 1; // c:7417
}
// c:7421-7448 — install trap on each named signal.
for sigarg in &argv {
// c:7421
let sig = getsigidx(sigarg);
if sig == -1 {
// c:7426
zwarnnam(name, &format!("undefined signal: {}", sigarg)); // c:7427
break; // c:7428
}
// c:Src/signals.c — C zsh stores traps in a fixed array
// indexed by signal number. Aliases (`0`, `EXIT`, `SIGEXIT`)
// all resolve to index 0 and share the same slot. The Rust
// port stores by name string, so we must normalize the key
// to the canonical signal name (or "EXIT" for the 0 alias)
// — otherwise `trap 'echo bye' 0` lands in a `"0"` slot
// that nothing else looks up.
let canonical = if sig == 0 {
"EXIT".to_string()
} else {
// Strip SIG/sig prefix and uppercase so `SIGINT` / `int`
// / `INT` all map to the same key.
sigarg
.strip_prefix("SIG")
.or_else(|| sigarg.strip_prefix("sig"))
.unwrap_or(sigarg.as_str())
.to_uppercase()
};
if let Ok(mut t) = traps_table().lock() {
t.insert(canonical.clone(), arg.clone()); // c:7448 (effective)
}
// c:Src/signals.c settrap — register both the libc signal
// handler AND the sigtrapped[idx] flag. Without setting
// sigtrapped, handletrap() early-returns 0 (sees the slot
// as "not trapped") and the dotrap dispatch never fires.
// The traps_table entry alone isn't enough — handletrap
// gates on sigtrapped[idx] != 0.
if sig > 0 && sig <= crate::ported::signals_h::SIGCOUNT
&& sig != libc::SIGCHLD as i32
{
crate::ported::signals::settrap(sig, None, ZSIG_FUNC);
}
}
0
}
/// Port of `bin_ttyctl(UNUSED(char *name), UNUSED(char **argv), Options ops, UNUSED(int func))` from Src/builtin.c:7454.
/// C: `int bin_ttyctl(UNUSED args, Options ops, ...)` — `-f` freezes the
/// tty, `-u` unfreezes; otherwise emit `"tty is [not ]frozen"`.
/// WARNING: param names don't match C — Rust=(_name, _argv, _func) vs C=(name, argv, ops, func)
pub fn bin_ttyctl(
_name: &str,
_argv: &[String], // c:7454
ops: &options,
_func: i32,
) -> i32 {
use std::sync::Mutex;
// c:7456-7461 — route through the canonical jobs::TTYFROZEN
// global. The previous builtin.rs duplicate AtomicI32 NEVER synced
// with jobs.rs's Mutex<i32> store; `ttyctl -f` set the local
// Atomic but didn't freeze the tty from the perspective of the
// job-control wait path that reads jobs::TTYFROZEN.
let cell = crate::ported::jobs::TTYFROZEN.get_or_init(|| Mutex::new(0_i32));
if OPT_ISSET(ops, b'f') {
// c:7456
*cell.lock().expect("TTYFROZEN poisoned") = 1; // c:7457
} else if OPT_ISSET(ops, b'u') {
// c:7458
*cell.lock().expect("TTYFROZEN poisoned") = 0; // c:7459
} else {
let f = *cell.lock().expect("TTYFROZEN poisoned");
// c:7461 — `printf("tty is %sfrozen\n", ttyfrozen ? "" : "not ");`
println!("tty is {}frozen", if f != 0 { "" } else { "not " }); // c:7461
}
0 // c:7463
}
/// Port of `bin_let(UNUSED(char *name), char **argv, UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:7469.
/// C: `int bin_let(UNUSED(char *name), char **argv, UNUSED(Options ops),
/// UNUSED(int func))` — evaluate each arg as a math expression;
/// return 1 if the final value is zero (success/false), 0 if non-zero
/// (true), 2 on math error.
/// WARNING: param names don't match C — Rust=(_name, argv, _func) vs C=(name, argv, ops, func)
pub fn bin_let(
_name: &str,
argv: &[String], // c:7469
_ops: &options,
_func: i32,
) -> i32 {
use crate::ported::utils::{errflag, ERRFLAG_ERROR};
use std::sync::atomic::Ordering;
// c:7472 — `mnumber val = zero_mnumber;`
let mut val: mnumber = mnumber {
l: 0,
d: 0.0,
type_: MN_INTEGER,
}; // c:7472
// c:7474-7475 — `while (*argv) val = matheval(*argv++);` — DO walk
// every arg even if one fails. C doesn't break on error mid-loop;
// it just lets errflag accumulate. Previously the Rust port broke
// on first failure, leaving later args unevaluated.
for expr in argv {
// c:7474
match matheval(expr) {
Ok(v) => val = v, // c:7475
Err(msg) => {
// c:Src/math.c:checkunary zerr side-effect — the C
// path writes the parse-error string to stderr via
// mathevali → checkunary → zerr. Rust's matheval
// captures the message in Err and bin_let was
// discarding it via `if let Ok(...)`. Surface it.
crate::ported::utils::zerr(&msg);
// Continue loop; errflag set below resets to local 2.
}
}
}
// c:7476-7480 — math errors are non-fatal in let; CLEAR ERRFLAG_ERROR
// and return 2 so subsequent commands don't inherit the error.
if (errflag.load(Relaxed) & ERRFLAG_ERROR) != 0 {
// c:7476
errflag.fetch_and(!ERRFLAG_ERROR, Relaxed); // c:7478
return 2; // c:7479
}
// c:7482 — `return (val.type == MN_INTEGER) ? val.u.l == 0 : val.u.d == 0.0;`
if val.type_ == MN_INTEGER {
// c:7482
(val.l == 0) as i32
} else {
(val.d == 0.0) as i32
}
}
/// Port of `bin_umask(char *nam, char **args, Options ops, UNUSED(int func))` from Src/builtin.c:7491.
/// C: `int bin_umask(char *nam, char **args, Options ops, ...)` —
/// set/show file-creation mask. No args → show; numeric arg → octal
/// parse; symbolic `[ugoa]+[+-=][rwx]+,...` → walk and apply.
/// WARNING: param names don't match C — Rust=(nam, args, _func) vs C=(nam, args, ops, func)
pub fn bin_umask(
nam: &str,
args: &[String], // c:7491
ops: &options,
_func: i32,
) -> i32 {
// c:7497-7500 — read current umask.
queue_signals(); // c:7497
let mut um: u32 = unsafe { libc::umask(0o777) } as u32; // c:7498
unsafe {
libc::umask(um as libc::mode_t);
} // c:7499
unqueue_signals(); // c:7500
// c:7503-7521 — no args: display.
if args.is_empty() {
// c:7503
if OPT_ISSET(ops, b'S') {
// c:7504
let who_chars = ['u', 'g', 'o']; // c:7505
for (i, who) in who_chars.iter().enumerate() {
// c:7507
print!("{}=", who); // c:7510
let mut what_iter = ['r', 'w', 'x'].iter(); // c:7511
while let Some(w) = what_iter.next() {
// c:7512
if (um & 0o400) == 0 {
// c:7513
print!("{}", w); // c:7514
}
um <<= 1; // c:7515
}
if i < 2 {
print!(",");
} else {
println!();
} // c:7518
}
} else {
// c:7522-7524 — `if (um & 0700) putchar('0'); printf("%03o\n", um);`
if (um & 0o700) != 0 {
// c:7522
print!("0"); // c:7523
}
println!("{:03o}", um); // c:7524
}
return 0; // c:7526
}
// c:7528 — `if (idigit(*s))` numeric form.
let s = &args[0];
if s.chars().next().is_some_and(|c| c.is_ascii_digit()) {
// c:7528
// c:7530 — `um = zstrtol(s, &s, 8);`
match u32::from_str_radix(s, 8) {
// c:7530
Ok(n) => um = n, // c:7530
Err(_) => {
zwarnnam(nam, "bad umask"); // c:7532
return 1; // c:7533
}
}
} else {
// c:7536-7585 — symbolic notation walker.
let bytes = s.as_bytes();
let mut i = 0;
loop {
// c:7544 — `whomask = 0;`
let mut whomask: u32 = 0; // c:7544
// c:7545-7553 — collect ugoa.
while i < bytes.len() {
// c:7545
match bytes[i] {
b'u' => {
whomask |= 0o700;
i += 1;
} // c:7547
b'g' => {
whomask |= 0o070;
i += 1;
} // c:7549
b'o' => {
whomask |= 0o007;
i += 1;
} // c:7551
b'a' => {
whomask |= 0o777;
i += 1;
} // c:7553
_ => break,
}
}
// c:7555 — default whomask = 0777.
if whomask == 0 {
whomask = 0o777;
} // c:7555
// c:7557-7565 — op +/-/=.
let umaskop = if i < bytes.len() { bytes[i] } else { 0 }; // c:7557
if !(umaskop == b'+' || umaskop == b'-' || umaskop == b'=') {
// c:7558
if umaskop != 0 {
// c:7559
zwarnnam(
nam,
&format!("bad symbolic mode operator: {}", umaskop as char),
); // c:7560
} else {
zwarnnam(nam, "bad umask"); // c:7562
}
return 1; // c:7564
}
i += 1;
// c:7567-7577 — collect rwx.
let mut mask: u32 = 0; // c:7567
while i < bytes.len() && bytes[i] != b',' {
// c:7568
match bytes[i] {
b'r' => mask |= 0o444 & whomask, // c:7570
b'w' => mask |= 0o222 & whomask, // c:7572
b'x' => mask |= 0o111 & whomask, // c:7574
other => {
zwarnnam(
nam,
&format!("bad symbolic mode permission: {}", other as char),
); // c:7576
return 1; // c:7577
}
}
i += 1;
}
// c:7580-7585 — apply.
match umaskop {
b'+' => um &= !mask, // c:7581
b'-' => um |= mask, // c:7583
_ => um = (um | whomask) & !mask, // c:7585 (=)
}
if i < bytes.len() && bytes[i] == b',' {
// c:7586
i += 1; // c:7587
} else {
break; // c:7589
}
}
if i < bytes.len() {
// c:7591
zwarnnam(
nam,
&format!("bad character in symbolic mode: {}", bytes[i] as char),
); // c:7592
return 1; // c:7593
}
}
// c:7598 — `umask(um);`
unsafe {
libc::umask(um as libc::mode_t);
} // c:7598
0 // c:7599
}
/// Port of `bin_notavail(char *nam, UNUSED(char **argv), UNUSED(Options ops), UNUSED(int func))` from Src/builtin.c:7604.
/// C: `int bin_notavail(char *nam, UNUSED(char **argv),
/// UNUSED(Options ops), UNUSED(int func))`
/// → `zwarnnam(nam, "not available on this system"); return 1;`
/// WARNING: param names don't match C — Rust=(nam, _argv, _func) vs C=(nam, argv, ops, func)
pub fn bin_notavail(
nam: &str,
_argv: &[String], // c:7604
_ops: &options,
_func: i32,
) -> i32 {
zwarnnam(nam, "not available on this system"); // c:7607
1 // c:7608
}
// ---------------------------------------------------------------------------
// Builtin descriptor.
// Port of `struct builtin` from `Src/zsh.h` (the one expanded by the
// `BUILTIN` / `BIN_PREFIX` macros at line 1452 of zsh.h).
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// The master registration table.
//
// Direct, line-for-line port of `static struct builtin builtins[]`
// at `Src/builtin.c:40-137`. Entries appear in the same order so
// any diff against the C source stays trivial. The `handler_name`
// column points at the canonical Rust port that the dispatcher in
// `Executor::register_builtins` (`src/ported/vm_helper`) wires up.
// ---------------------------------------------------------------------------
pub static BUILTINS: std::sync::LazyLock<Vec<builtin>> = std::sync::LazyLock::new(|| {
vec![
BIN_PREFIX("-", BINF_DASH),
BIN_PREFIX("builtin", BINF_BUILTIN),
BIN_PREFIX("command", BINF_COMMAND),
BIN_PREFIX("exec", BINF_EXEC),
BIN_PREFIX("noglob", BINF_NOGLOB),
BUILTIN(
"[",
BINF_HANDLES_OPTS,
Some(bin_test as HandlerFunc),
0,
-1,
BIN_BRACKET,
None,
None,
),
BUILTIN(
".",
BINF_PSPECIAL,
Some(bin_dot as HandlerFunc),
1,
-1,
0,
None,
None,
),
BUILTIN(
":",
BINF_PSPECIAL,
Some(bin_true as HandlerFunc),
0,
-1,
0,
None,
None,
),
BUILTIN(
"alias",
BINF_MAGICEQUALS | BINF_PLUSOPTS,
Some(bin_alias as HandlerFunc),
0,
-1,
0,
Some("Lgmrs"),
None,
),
BUILTIN(
"autoload",
BINF_PLUSOPTS,
Some(bin_functions as HandlerFunc),
0,
-1,
0,
Some("dmktrRTUwWXz"),
Some("u"),
),
BUILTIN(
"bg",
0,
Some(bin_fg as HandlerFunc),
0,
-1,
BIN_BG,
None,
None,
),
BUILTIN(
"break",
BINF_PSPECIAL,
Some(bin_break as HandlerFunc),
0,
1,
BIN_BREAK,
None,
None,
),
BUILTIN(
"bye",
0,
Some(bin_break as HandlerFunc),
0,
1,
BIN_EXIT,
None,
None,
),
BUILTIN(
"cd",
BINF_SKIPINVALID | BINF_SKIPDASH | BINF_DASHDASHVALID,
Some(bin_cd as HandlerFunc),
0,
2,
BIN_CD,
Some("qsPL"),
None,
),
BUILTIN(
"chdir",
BINF_SKIPINVALID | BINF_SKIPDASH | BINF_DASHDASHVALID,
Some(bin_cd as HandlerFunc),
0,
2,
BIN_CD,
Some("qsPL"),
None,
),
BUILTIN(
"continue",
BINF_PSPECIAL,
Some(bin_break as HandlerFunc),
0,
1,
BIN_CONTINUE,
None,
None,
),
BUILTIN(
"declare",
BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN,
Some(bin_typeset as HandlerFunc),
0,
-1,
0,
Some("AE:%F:%HL:%R:%TUZ:%afghi:%klmnp:%rtuxz"),
None,
),
BUILTIN(
"dirs",
0,
Some(bin_dirs as HandlerFunc),
0,
-1,
0,
Some("clpv"),
None,
),
BUILTIN(
"disable",
0,
Some(bin_enable as HandlerFunc),
0,
-1,
BIN_DISABLE,
Some("afmprs"),
None,
),
BUILTIN(
"disown",
0,
Some(bin_fg as HandlerFunc),
0,
-1,
BIN_DISOWN,
None,
None,
),
BUILTIN(
"echo",
BINF_SKIPINVALID,
Some(bin_print as HandlerFunc),
0,
-1,
BIN_ECHO,
Some("neE"),
Some("-"),
),
BUILTIN(
"emulate",
0,
Some(bin_emulate as HandlerFunc),
0,
-1,
0,
Some("lLR"),
None,
),
BUILTIN(
"enable",
0,
Some(bin_enable as HandlerFunc),
0,
-1,
BIN_ENABLE,
Some("afmprs"),
None,
),
BUILTIN(
"eval",
BINF_PSPECIAL,
Some(bin_eval as HandlerFunc),
0,
-1,
BIN_EVAL,
None,
None,
),
BUILTIN(
"exit",
BINF_PSPECIAL,
Some(bin_break as HandlerFunc),
0,
1,
BIN_EXIT,
None,
None,
),
BUILTIN(
"export",
BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN,
Some(bin_typeset as HandlerFunc),
0,
-1,
BIN_EXPORT,
Some("E:%F:%HL:%R:%TUZ:%afhi:%lp:%rtu"),
Some("xg"),
),
BUILTIN(
"false",
0,
Some(bin_false as HandlerFunc),
0,
-1,
0,
None,
None,
),
// C source (Src/builtin.c:69-73): the argument to -e used to be
// optional; making it required is more consistent.
BUILTIN(
"fc",
0,
Some(bin_fc as HandlerFunc),
0,
-1,
BIN_FC,
Some("aAdDe:EfiIlLmnpPrRst:W"),
None,
),
BUILTIN(
"fg",
0,
Some(bin_fg as HandlerFunc),
0,
-1,
BIN_FG,
None,
None,
),
BUILTIN(
"float",
BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN,
Some(bin_typeset as HandlerFunc),
0,
-1,
0,
Some("E:%F:%HL:%R:%Z:%ghlp:%rtux"),
Some("E"),
),
BUILTIN(
"functions",
BINF_PLUSOPTS,
Some(bin_functions as HandlerFunc),
0,
-1,
0,
Some("ckmMstTuUWx:z"),
None,
),
BUILTIN(
"getln",
0,
Some(bin_read as HandlerFunc),
0,
-1,
0,
Some("ecnAlE"),
Some("zr"),
),
BUILTIN(
"getopts",
0,
Some(bin_getopts as HandlerFunc),
2,
-1,
0,
None,
None,
),
BUILTIN(
"hash",
BINF_MAGICEQUALS,
Some(bin_hash as HandlerFunc),
0,
-1,
0,
Some("Ldfmrv"),
None,
),
// Src/builtin.c — `#ifdef ZSH_HASH_DEBUG`
// BUILTIN("hashinfo", 0, bin_hashinfo, 0, 0, 0, NULL, NULL)
BUILTIN(
"hashinfo",
0,
Some(crate::ported::hashtable::bin_hashinfo as HandlerFunc),
0,
0,
0,
None,
None,
),
BUILTIN(
"history",
0,
Some(bin_fc as HandlerFunc),
0,
-1,
BIN_FC,
Some("adDEfiLmnpPrt:"),
Some("l"),
),
BUILTIN(
"integer",
BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN,
Some(bin_typeset as HandlerFunc),
0,
-1,
0,
Some("HL:%R:%Z:%ghi:%lp:%rtux"),
Some("i"),
),
BUILTIN(
"jobs",
0,
Some(bin_fg as HandlerFunc),
0,
-1,
BIN_JOBS,
Some("dlpZrs"),
None,
),
BUILTIN(
"kill",
BINF_HANDLES_OPTS,
Some(crate::ported::jobs::bin_kill as HandlerFunc),
0,
-1,
0,
None,
None,
),
BUILTIN("let", 0, Some(bin_let as HandlerFunc), 1, -1, 0, None, None),
BUILTIN(
"local",
BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN,
Some(bin_typeset as HandlerFunc),
0,
-1,
0,
Some("AE:%F:%HL:%R:%TUZ:%ahi:%lnp:%rtux"),
None,
),
BUILTIN(
"logout",
0,
Some(bin_break as HandlerFunc),
0,
1,
BIN_LOGOUT,
None,
None,
),
// Src/builtin.c — `#if defined(ZSH_MEM) & defined(ZSH_MEM_DEBUG)`
// BUILTIN("mem", 0, bin_mem, 0, 0, 0, "v", NULL)
BUILTIN(
"mem",
0,
Some(crate::ported::mem::bin_mem as HandlerFunc),
0,
0,
0,
Some("v"),
None,
),
BUILTIN(
"popd",
BINF_SKIPINVALID | BINF_SKIPDASH | BINF_DASHDASHVALID,
Some(bin_cd as HandlerFunc),
0,
1,
BIN_POPD,
Some("q"),
None,
),
// Src/builtin.c — `#if defined(ZSH_PAT_DEBUG)`
// BUILTIN("patdebug", 0, bin_patdebug, 1, -1, 0, "p", NULL)
BUILTIN("patdebug", 0, None, 1, -1, 0, Some("p"), None),
BUILTIN(
"print",
BINF_PRINTOPTS,
Some(bin_print as HandlerFunc),
0,
-1,
BIN_PRINT,
Some("abcC:Df:ilmnNoOpPrRsSu:v:x:X:z-"),
None,
),
BUILTIN(
"printf",
BINF_SKIPINVALID | BINF_SKIPDASH,
Some(bin_print as HandlerFunc),
1,
-1,
BIN_PRINTF,
Some("v:"),
None,
),
BUILTIN(
"pushd",
BINF_SKIPINVALID | BINF_SKIPDASH | BINF_DASHDASHVALID,
Some(bin_cd as HandlerFunc),
0,
2,
BIN_PUSHD,
Some("qsPL"),
None,
),
BUILTIN(
"pushln",
0,
Some(bin_print as HandlerFunc),
0,
-1,
BIN_PRINT,
None,
Some("-nz"),
),
BUILTIN(
"pwd",
0,
Some(bin_pwd as HandlerFunc),
0,
0,
0,
Some("rLP"),
None,
),
BUILTIN(
"r",
0,
Some(bin_fc as HandlerFunc),
0,
-1,
BIN_R,
Some("IlLnr"),
None,
),
BUILTIN(
"read",
0,
Some(bin_read as HandlerFunc),
0,
-1,
0,
Some("cd:ek:%lnpqrst:%zu:AE"),
None,
),
BUILTIN(
"readonly",
BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN,
Some(bin_typeset as HandlerFunc),
0,
-1,
BIN_READONLY,
Some("AE:%F:%HL:%R:%TUZ:%afghi:%lptux"),
Some("r"),
),
BUILTIN(
"rehash",
0,
Some(bin_hash as HandlerFunc),
0,
0,
0,
Some("df"),
Some("r"),
),
BUILTIN(
"return",
BINF_PSPECIAL,
Some(bin_break as HandlerFunc),
0,
1,
BIN_RETURN,
None,
None,
),
BUILTIN(
"set",
BINF_PSPECIAL | BINF_HANDLES_OPTS,
Some(bin_set as HandlerFunc),
0,
-1,
0,
None,
None,
),
BUILTIN(
"setopt",
0,
Some(crate::ported::options::bin_setopt as HandlerFunc),
0,
-1,
BIN_SETOPT,
None,
None,
),
// c:Src/Builtins/sched.c:375 — sched is a Builtins module
// builtin (zsh/sched). bintab has only one entry. The Rust
// port at builtins/sched.rs::bin_sched (325 lines) was not
// registered, so `sched 09:00 echo morning` returned
// "command not found".
BUILTIN(
"sched",
0,
Some(crate::ported::builtins::sched::bin_sched as HandlerFunc),
0,
-1,
0,
None,
None,
),
BUILTIN(
"shift",
BINF_PSPECIAL,
Some(bin_shift as HandlerFunc),
0,
-1,
0,
Some("p"),
None,
),
BUILTIN(
"source",
BINF_PSPECIAL,
Some(bin_dot as HandlerFunc),
1,
-1,
0,
None,
None,
),
BUILTIN(
"suspend",
0,
Some(crate::ported::jobs::bin_suspend as HandlerFunc),
0,
0,
0,
Some("f"),
None,
),
BUILTIN(
"test",
BINF_HANDLES_OPTS,
Some(bin_test as HandlerFunc),
0,
-1,
BIN_TEST,
None,
None,
),
BUILTIN(
"ttyctl",
0,
Some(bin_ttyctl as HandlerFunc),
0,
0,
0,
Some("fu"),
None,
),
// c:Src/Builtins/rlimits.c:868-870 — limit/ulimit/unlimit are
// declared in the rlimits Builtins-module's bintab. zshrs has the
// free-fn ports at src/ported/builtins/rlimits.rs but never
// registered them; the BUILTIN_NAMES derivation missed them and
// `type limit` etc. returned empty.
BUILTIN(
"limit",
0,
Some(crate::ported::builtins::rlimits::bin_limit as HandlerFunc),
0,
-1,
0,
Some("sh"),
None,
), // c:rlimits.c:868
BUILTIN(
"ulimit",
0,
Some(crate::ported::builtins::rlimits::bin_ulimit as HandlerFunc),
0,
-1,
0,
None,
None,
), // c:rlimits.c:869
BUILTIN(
"unlimit",
0,
Some(crate::ported::builtins::rlimits::bin_unlimit as HandlerFunc),
0,
-1,
0,
Some("hs"),
None,
), // c:rlimits.c:870
BUILTIN(
"times",
BINF_PSPECIAL,
Some(bin_times as HandlerFunc),
0,
0,
0,
None,
None,
),
BUILTIN(
"trap",
BINF_PSPECIAL | BINF_HANDLES_OPTS,
Some(bin_trap as HandlerFunc),
0,
-1,
0,
None,
None,
),
BUILTIN(
"true",
0,
Some(bin_true as HandlerFunc),
0,
-1,
0,
None,
None,
),
BUILTIN(
"type",
0,
Some(bin_whence as HandlerFunc),
0,
-1,
0,
Some("ampfsSw"),
Some("v"),
),
BUILTIN(
"typeset",
BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN,
Some(bin_typeset as HandlerFunc),
0,
-1,
0,
Some("AE:%F:%HL:%R:%TUZ:%afghi:%klp:%rtuxmnz"),
None,
),
// `nameref` — zsh/ksh93 module's named-reference declaration
// builtin. Same handler as `typeset` with a tighter opt-string
// (`-g`/`-p`/`-r`/`-u`) and the assign-style invocation. The
// canonical Src/Modules/ksh93.c:bintab entry:
// BUILTIN("nameref", BINF_ASSIGN, bin_typeset, 0, -1, 0,
// "gpru", "n")
// The 9th arg ("n") is the default-flag — typeset_flags inherits
// PM_NAMEREF when the builtin is invoked under this name. zshrs's
// bin_typeset wrapper doesn't read the 9th field yet, but the
// registration still surfaces `nameref` in the tool window /
// completion / hover docs.
BUILTIN(
"nameref",
BINF_ASSIGN,
Some(bin_typeset as HandlerFunc),
0,
-1,
0,
Some("gpru"),
Some("n"),
),
BUILTIN(
"umask",
0,
Some(bin_umask as HandlerFunc),
0,
1,
0,
Some("S"),
None,
),
BUILTIN(
"unalias",
0,
Some(bin_unhash as HandlerFunc),
0,
-1,
BIN_UNALIAS,
Some("ams"),
None,
),
BUILTIN(
"unfunction",
0,
Some(bin_unhash as HandlerFunc),
1,
-1,
BIN_UNFUNCTION,
Some("m"),
Some("f"),
),
BUILTIN(
"unhash",
0,
Some(bin_unhash as HandlerFunc),
1,
-1,
BIN_UNHASH,
Some("adfms"),
None,
),
BUILTIN(
"unset",
BINF_PSPECIAL,
Some(bin_unset as HandlerFunc),
1,
-1,
BIN_UNSET,
Some("fmvn"),
None,
),
BUILTIN(
"unsetopt",
0,
Some(crate::ported::options::bin_setopt as HandlerFunc),
0,
-1,
BIN_UNSETOPT,
None,
None,
),
BUILTIN(
"wait",
0,
Some(bin_fg as HandlerFunc),
0,
-1,
BIN_WAIT,
None,
None,
),
BUILTIN(
"whence",
0,
Some(bin_whence as HandlerFunc),
0,
-1,
0,
Some("acmpvfsSwx:"),
None,
),
BUILTIN(
"where",
0,
Some(bin_whence as HandlerFunc),
0,
-1,
0,
Some("pmsSwx:"),
Some("ca"),
),
BUILTIN(
"which",
0,
Some(bin_whence as HandlerFunc),
0,
-1,
0,
Some("ampsSwx:"),
Some("c"),
),
BUILTIN(
"zmodload",
0,
Some(crate::ported::module::bin_zmodload as HandlerFunc),
0,
-1,
0,
Some("AFRILP:abcfdilmpsue"),
None,
),
BUILTIN(
"zcompile",
0,
Some(crate::ported::parse::bin_zcompile as HandlerFunc),
0,
-1,
0,
Some("tUMRcmzka"),
None,
),
// Module builtins (zsh/zutil, zsh/cap, zsh/pcre, etc.) — these
// live in src/ported/modules/* and src/ported/zle/* but their
// canonical pub fn signatures match HandlerFunc, so they can be
// dispatched via execbuiltin alongside the main builtins.
BUILTIN(
"zstyle",
0,
Some(crate::ported::modules::zutil::bin_zstyle as HandlerFunc),
0,
-1,
0,
Some("LeLdgabsTtmnH"),
None,
),
BUILTIN(
"zformat",
0,
Some(crate::ported::modules::zutil::bin_zformat as HandlerFunc),
0,
-1,
0,
Some("Faf"),
None,
),
BUILTIN(
"zparseopts",
0,
Some(crate::ported::modules::zutil::bin_zparseopts as HandlerFunc),
1,
-1,
0,
// c:Src/Modules/zutil.c:2137 — NULL optstring: bin_zparseopts
// parses its own flags (-D/-E/-F/-K/-M/-a/-A/-v) inline. The
// previous Rust spec ("D-EFK-M-a:") let execbuiltin pre-eat
// them via the option-byte parser, leaving bin_zparseopts
// with empty argv and `if i >= args.len()` firing
// "missing option descriptions" for the canonical
// `zparseopts -a foo --` invocation.
None,
None,
),
BUILTIN(
"zregexparse",
0,
Some(crate::ported::modules::zutil::bin_zregexparse as HandlerFunc),
0,
-1,
0,
Some("c"),
None,
),
BUILTIN(
"cap",
0,
Some(crate::ported::modules::cap::bin_cap as HandlerFunc),
0,
1,
0,
None,
None,
),
BUILTIN(
"getcap",
0,
Some(crate::ported::modules::cap::bin_getcap as HandlerFunc),
1,
-1,
0,
None,
None,
),
BUILTIN(
"setcap",
0,
Some(crate::ported::modules::cap::bin_setcap as HandlerFunc),
1,
-1,
0,
None,
None,
),
BUILTIN(
"pcre_compile",
0,
Some(crate::ported::modules::pcre::bin_pcre_compile as HandlerFunc),
1,
1,
0,
Some("aimx"),
None,
),
BUILTIN(
"pcre_study",
0,
Some(crate::ported::modules::pcre::bin_pcre_study as HandlerFunc),
0,
0,
0,
None,
None,
),
BUILTIN(
"pcre_match",
0,
Some(crate::ported::modules::pcre::bin_pcre_match as HandlerFunc),
1,
-1,
0,
Some("ab:nv:"),
None,
),
BUILTIN(
"ztcp",
0,
Some(crate::ported::modules::tcp::bin_ztcp as HandlerFunc),
0,
-1,
0,
Some("acdflLtv"),
None,
),
BUILTIN(
"ztie",
0,
Some(crate::ported::modules::db_gdbm::bin_ztie as HandlerFunc),
0,
-1,
0,
Some("d:f:r"),
None,
),
BUILTIN(
"zuntie",
0,
Some(crate::ported::modules::db_gdbm::bin_zuntie as HandlerFunc),
1,
-1,
0,
Some("u"),
None,
),
BUILTIN(
"zgdbmpath",
0,
Some(crate::ported::modules::db_gdbm::bin_zgdbmpath as HandlerFunc),
1,
1,
0,
None,
None,
),
BUILTIN(
"echoti",
0,
Some(crate::ported::modules::terminfo::bin_echoti as HandlerFunc),
1,
-1,
0,
None,
None,
),
BUILTIN(
"fg",
0,
Some(bin_fg as HandlerFunc),
0,
-1,
BIN_FG,
None,
None,
),
BUILTIN(
"kill",
BINF_HANDLES_OPTS,
Some(crate::ported::jobs::bin_kill as HandlerFunc),
0,
-1,
0,
None,
None,
),
BUILTIN(
"suspend",
0,
Some(crate::ported::jobs::bin_suspend as HandlerFunc),
0,
0,
0,
Some("f"),
None,
),
BUILTIN(
"bindkey",
0,
Some(crate::ported::zle::zle_keymap::bin_bindkey as HandlerFunc),
0,
-1,
0,
Some("evaMldDANmrsLR"),
None,
),
BUILTIN(
"vared",
0,
Some(crate::ported::zle::zle_main::bin_vared as HandlerFunc),
1,
1,
0,
Some("AaceghM:m:p:r:i:f:"),
None,
),
BUILTIN(
"compadd",
0,
Some(crate::ported::zle::complete::bin_compadd as HandlerFunc),
0,
-1,
0,
Some("J:V:1X:fnqQF:Wsi"),
None,
),
BUILTIN(
"compset",
0,
Some(crate::ported::zle::complete::bin_compset as HandlerFunc),
1,
-1,
0,
Some("npqPS:"),
None,
),
// c:Src/Zle/computil.c:5103-5110 — zsh/computil module's 8
// builtins drive compsys (the canonical completion system).
// All have HandlerFunc-compatible signatures already; just
// need BUILTINS-table registration. Without these,
// _describe / _arguments / _values / _files / _groups / etc.
// (compsys's primary entry points) silently no-op.
BUILTIN(
"comparguments",
0,
Some(crate::ported::zle::computil::bin_comparguments as HandlerFunc),
1,
-1,
0,
None,
None,
), // c:5103
BUILTIN(
"compdescribe",
0,
Some(crate::ported::zle::computil::bin_compdescribe as HandlerFunc),
3,
-1,
0,
None,
None,
), // c:5104
BUILTIN(
"compfiles",
0,
Some(crate::ported::zle::computil::bin_compfiles as HandlerFunc),
1,
-1,
0,
None,
None,
), // c:5105
BUILTIN(
"compgroups",
0,
Some(crate::ported::zle::computil::bin_compgroups as HandlerFunc),
1,
-1,
0,
None,
None,
), // c:5106
BUILTIN(
"compquote",
0,
Some(crate::ported::zle::computil::bin_compquote as HandlerFunc),
1,
-1,
0,
Some("p"),
None,
), // c:5107
BUILTIN(
"comptags",
0,
Some(crate::ported::zle::computil::bin_comptags as HandlerFunc),
1,
-1,
0,
None,
None,
), // c:5108
BUILTIN(
"comptry",
0,
Some(crate::ported::zle::computil::bin_comptry as HandlerFunc),
0,
-1,
0,
None,
None,
), // c:5109
BUILTIN(
"compvalues",
0,
Some(crate::ported::zle::computil::bin_compvalues as HandlerFunc),
1,
-1,
0,
None,
None,
), // c:5110
// c:Src/Modules/system.c:819-824 — zsh/system module builtins.
BUILTIN(
"syserror",
0,
Some(crate::ported::modules::system::bin_syserror as HandlerFunc),
0,
1,
0,
Some("e:p:"),
None,
), // c:819
BUILTIN(
"sysread",
0,
Some(crate::ported::modules::system::bin_sysread as HandlerFunc),
0,
1,
0,
Some("c:i:o:s:t:"),
None,
), // c:820
BUILTIN(
"syswrite",
0,
Some(crate::ported::modules::system::bin_syswrite as HandlerFunc),
1,
1,
0,
Some("c:o:"),
None,
), // c:821
BUILTIN(
"sysopen",
0,
Some(crate::ported::modules::system::bin_sysopen as HandlerFunc),
1,
1,
0,
Some("rwau:o:m:"),
None,
), // c:822
BUILTIN(
"sysseek",
0,
Some(crate::ported::modules::system::bin_sysseek as HandlerFunc),
1,
1,
0,
Some("u:w:"),
None,
), // c:823
BUILTIN(
"zsystem",
0,
Some(crate::ported::modules::system::bin_zsystem as HandlerFunc),
1,
-1,
0,
None,
None,
), // c:824
// c:Src/Modules/zselect.c:272 — zsh/zselect module.
BUILTIN(
"zselect",
0,
Some(crate::ported::modules::zselect::bin_zselect as HandlerFunc),
0,
-1,
0,
None,
None,
), // c:272
// c:Src/Modules/socket.c:276 — zsh/socket module.
BUILTIN(
"zsocket",
0,
Some(crate::ported::modules::socket::bin_zsocket as HandlerFunc),
0,
3,
0,
Some("ad:ltv"),
None,
), // c:276
// c:Src/Modules/stat.c:637 — zsh/stat module.
BUILTIN(
"stat",
0,
Some(crate::ported::modules::stat::bin_stat as HandlerFunc),
0,
-1,
0,
None,
None,
), // c:637
// c:Src/Modules/stat.c:638 — `zstat` alias, same handler.
BUILTIN(
"zstat",
0,
Some(crate::ported::modules::stat::bin_stat as HandlerFunc),
0,
-1,
0,
None,
None,
), // c:638
// c:Src/Modules/watch.c:694 — zsh/watch module's `log`.
BUILTIN(
"log",
0,
Some(crate::ported::modules::watch::bin_log as HandlerFunc),
0,
0,
0,
None,
None,
), // c:694
// c:Src/Modules/zprof.c:315 — zsh/zprof module.
BUILTIN(
"zprof",
0,
Some(crate::ported::modules::zprof::bin_zprof as HandlerFunc),
0,
0,
0,
Some("c"),
None,
), // c:315
// c:Src/Modules/datetime.c:239 — zsh/datetime module.
BUILTIN(
"strftime",
0,
Some(crate::ported::modules::datetime::bin_strftime as HandlerFunc),
1,
3,
0,
Some("nqrs:"),
None,
), // c:239
// c:Src/Modules/zftp.c:189 — zsh/zftp module.
BUILTIN(
"zftp",
0,
Some(crate::ported::modules::zftp::bin_zftp as HandlerFunc),
1,
-1,
0,
None,
None,
), // c:189
// c:Src/Modules/zpty.c:882 — zsh/zpty module.
BUILTIN(
"zpty",
0,
Some(crate::ported::modules::zpty::bin_zpty as HandlerFunc),
0,
-1,
0,
Some("ebdmrwLnt"),
None,
), // c:882
// c:Src/Modules/curses.c:1632 — zsh/curses module.
BUILTIN(
"zcurses",
0,
Some(crate::ported::modules::curses::bin_zcurses as HandlerFunc),
1,
-1,
0,
Some(""),
None,
), // c:1632
// c:Src/Modules/clone.c:110 — zsh/clone module (Linux only;
// bin_clone on non-Linux is the "not available" stub).
BUILTIN(
"clone",
0,
Some(crate::ported::modules::clone::bin_clone as HandlerFunc),
1,
1,
0,
None,
None,
), // c:110
// c:Src/Modules/example.c — zsh/example module (template).
BUILTIN(
"example",
0,
Some(crate::ported::modules::example::bin_example as HandlerFunc),
0,
-1,
0,
Some("flags"),
None,
),
// c:Src/Modules/param_private.c:652 — zsh/param/private module.
// BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN.
BUILTIN(
"private",
BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN,
Some(crate::ported::modules::param_private::bin_private as HandlerFunc),
0,
-1,
0,
Some("AE:%F:%HL:%PR:%TUZ:%ahi:%lnmrtux"),
Some("P"),
), // c:652
// c:Src/Modules/termcap.c:139 — zsh/termcap module.
BUILTIN(
"echotc",
0,
Some(crate::ported::modules::termcap::bin_echotc as HandlerFunc),
1,
-1,
0,
None,
None,
), // c:139
// c:Src/Zle/compctl.c:4000-4001 — zsh/compctl module.
BUILTIN(
"compcall",
0,
Some(crate::ported::zle::compctl::bin_compcall as HandlerFunc),
0,
0,
0,
Some("TD"),
None,
), // c:4000
BUILTIN(
"compctl",
0,
Some(crate::ported::zle::compctl::bin_compctl as HandlerFunc),
0,
-1,
0,
None,
None,
), // c:4001
// c:Src/Modules/attr.c:220-223 — zsh/attr module (4 builtins).
BUILTIN(
"zgetattr",
0,
Some(crate::ported::modules::attr::bin_getattr as HandlerFunc),
2,
3,
0,
Some("h"),
None,
), // c:220
BUILTIN(
"zsetattr",
0,
Some(crate::ported::modules::attr::bin_setattr as HandlerFunc),
3,
3,
0,
Some("h"),
None,
), // c:221
BUILTIN(
"zdelattr",
0,
Some(crate::ported::modules::attr::bin_delattr as HandlerFunc),
2,
-1,
0,
Some("h"),
None,
), // c:222
BUILTIN(
"zlistattr",
0,
Some(crate::ported::modules::attr::bin_listattr as HandlerFunc),
1,
2,
0,
Some("h"),
None,
), // c:223
BUILTIN(
"zle",
0,
Some(crate::ported::zle::zle_thingy::bin_zle as HandlerFunc),
0,
-1,
0,
Some("aAcCDfFIKlLmMNRTU"),
None,
),
// zsh/files module — file-manipulation builtins. All have
// HandlerFunc-compatible signatures already.
BUILTIN(
"mkdir",
0,
Some(crate::ported::modules::files::bin_mkdir as HandlerFunc),
1,
-1,
0,
Some("pm:"),
None,
),
BUILTIN(
"rmdir",
0,
Some(crate::ported::modules::files::bin_rmdir as HandlerFunc),
1,
-1,
0,
None,
None,
),
BUILTIN(
"ln",
0,
Some(crate::ported::modules::files::bin_ln as HandlerFunc),
1,
-1,
0,
Some("dfins"),
None,
),
// `mv` — zsh/files. Same handler as `ln` with BIN_MV dispatch.
// c:Src/Modules/files.c — `BUILTIN("mv", 0, bin_ln, 2, -1, BIN_MV, "fi", NULL)`.
BUILTIN(
"mv",
0,
Some(crate::ported::modules::files::bin_ln as HandlerFunc),
2,
-1,
crate::ported::modules::files::BIN_MV,
Some("fi"),
None,
),
BUILTIN(
"rm",
0,
Some(crate::ported::modules::files::bin_rm as HandlerFunc),
1,
-1,
0,
Some("dfiRrs"),
None,
),
BUILTIN(
"chmod",
0,
Some(crate::ported::modules::files::bin_chmod as HandlerFunc),
2,
-1,
0,
Some("Rs"),
None,
),
// c:Src/Modules/files.c:806 — BUILTIN("chgrp", 0, bin_chown, 2, -1, BIN_CHGRP, "hRs", NULL)
BUILTIN(
"chgrp",
0,
Some(crate::ported::modules::files::bin_chown as HandlerFunc),
2,
-1,
crate::ported::modules::files::BIN_CHGRP,
Some("hRs"),
None,
),
// c:Src/Modules/files.c:808 — BUILTIN("chown", 0, bin_chown, 2, -1, BIN_CHOWN, "hRs", NULL)
BUILTIN(
"chown",
0,
Some(crate::ported::modules::files::bin_chown as HandlerFunc),
2,
-1,
crate::ported::modules::files::BIN_CHOWN,
Some("hRs"),
None,
),
BUILTIN(
"sync",
0,
Some(crate::ported::modules::files::bin_sync as HandlerFunc),
0,
0,
0,
None,
None,
),
// c:Src/Modules/files.c:816-824 — zf_* aliases. Same handlers as
// chmod/chown/ln/mkdir/rm/rmdir/sync but separate BUILTIN entries
// so `autoload -U zf_*` resolves and `zsh -f` sees them all.
BUILTIN(
"zf_chgrp", 0,
Some(crate::ported::modules::files::bin_chown as HandlerFunc),
2, -1, crate::ported::modules::files::BIN_CHGRP, Some("hRs"), None,
), // c:816
BUILTIN(
"zf_chmod", 0,
Some(crate::ported::modules::files::bin_chmod as HandlerFunc),
2, -1, 0, Some("Rs"), None,
), // c:817
BUILTIN(
"zf_chown", 0,
Some(crate::ported::modules::files::bin_chown as HandlerFunc),
2, -1, crate::ported::modules::files::BIN_CHOWN, Some("hRs"), None,
), // c:818
BUILTIN(
"zf_ln", 0,
Some(crate::ported::modules::files::bin_ln as HandlerFunc),
1, -1, crate::ported::modules::files::BIN_LN, Some("dfins"), None,
), // c:819
BUILTIN(
"zf_mkdir", 0,
Some(crate::ported::modules::files::bin_mkdir as HandlerFunc),
1, -1, 0, Some("pm:"), None,
), // c:820
BUILTIN(
"zf_mv", 0,
Some(crate::ported::modules::files::bin_ln as HandlerFunc),
2, -1, crate::ported::modules::files::BIN_MV, Some("fi"), None,
), // c:821
BUILTIN(
"zf_rm", 0,
Some(crate::ported::modules::files::bin_rm as HandlerFunc),
1, -1, 0, Some("dfiRrs"), None,
), // c:822
BUILTIN(
"zf_rmdir", 0,
Some(crate::ported::modules::files::bin_rmdir as HandlerFunc),
1, -1, 0, None, None,
), // c:823
BUILTIN(
"zf_sync", 0,
Some(crate::ported::modules::files::bin_sync as HandlerFunc),
0, 0, 0, None, None,
), // c:824
]
});
// hash table containing builtin commands // c:143
/// Process-wide builtin lookup table. Filled lazily the first time
/// `builtintab()` is called; mirrors the C `mod_export HashTable
/// builtintab` exposed at `Src/builtin.c:146`.
static builtintab: OnceLock<HashMap<String, &'static builtin>> = OnceLock::new();
/// Names whose `node.flags & DISABLED` is set in C. The Rust port's
/// `builtintab` is an immutable static, so the disabled bit lives
/// in this parallel set; `bin_enable` toggles it via builtin.c:587.
/// Dispatch sites check `is_builtin_disabled(name)` before calling
/// `handlerfunc` to mirror C's "skip nodes with DISABLED set" walk.
pub static BUILTINS_DISABLED: std::sync::LazyLock<
// c:587 (Src/builtin.c)
Mutex<std::collections::HashSet<String>>,
> = std::sync::LazyLock::new(|| Mutex::new(std::collections::HashSet::new()));
// `shfunctab` is the canonical singleton at `hashtable::shfunctab_lock()`
// (RwLock<shfunc_table>); the prior parallel `shfunctab_table()` /
// `SHFUNCTAB_INNER` (usize-pointer Mutex<HashMap>) was deleted so the
// `bin_functions` C-port and the bytecode function-def path both write
// through one table. C-faithful access via `addnode`/`getnode`/`getnode2`
// methods (Src/zsh.h:281+ HashTable GSU pointers).
// `matchednodes` global from Src/builtin.c:4550.
pub static MATCHEDNODES: Mutex<Vec<String>> = Mutex::new(Vec::new());
// `stopmsg` global from Src/jobs.c — non-zero when checkjobs() printed.
pub static STOPMSG: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
// `sfcontext` global from Src/exec.c:239 — current shell-function
// dispatch context (SFC_NONE / SFC_BUILTIN / SFC_FUNC / SFC_SUBST...).
pub static SFCONTEXT: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:exec.c:239
// `maxjob` / `thisjob` globals from Src/jobs.c:62/63 — canonical
// storage lives in jobs.rs (`OnceLock<Mutex<i32>>`). The previous
// builtin.rs duplicate `AtomicI32` stores NEVER synced with the
// jobs.rs Mutex<i32> values that the spawn/wait paths actually
// update; `checkjobs` (line 5092) read stale 0s no matter how many
// jobs were active. Callers route through jobs::MAXJOB / jobs::THISJOB
// directly now.
// `jobstats` mirror — flat per-slot stat bits (STAT_*). Real jobtab
// lives in src/ported/jobs.rs's JobTable; this mirror is updated by
// the spawn/wait paths that already touch STOPMSG. Empty → no jobs,
// matching the post-init state of `jobtab[]`.
pub static JOBSTATS: Mutex<Vec<i32>> = Mutex::new(Vec::new());
// File-static globals for [_]realexit/zexit — c:5945+, init.c, signals.c.
pub static SHELL_EXITING: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
pub static EXIT_PENDING: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
pub static EXIT_VAL: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
/// Port of `mod_export volatile int exit_level;` from `Src/builtin.c:5796`.
///
/// Records the `locallevel` at the moment a deferred `exit` was issued
/// inside a function. The `exec.c:6141` gate
/// `if (exit_pending && exit_level >= locallevel+1 && !in_exit_trap)`
/// fires only when the unwind has reached a scope at or above the
/// deferral point — preventing premature exit while the deferred
/// status walks back through nested function frames. C uses
/// `volatile int` because the value is read from a signal-touching
/// context; Rust's AtomicI32 with Relaxed ordering matches the same
/// no-fence read shape.
pub static EXIT_LEVEL: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
// ====================================================================
// !!! WARNING: RUST-ONLY ATOMIC — NO DIRECT C COUNTERPART !!!
// ====================================================================
// C zsh forks for `(...)` subshells; the child runs to completion
// then exits via process::exit, and the parent (post-fork) continues.
// zshrs runs subshells in-process via fusevm_bridge::subshell_begin/
// subshell_end (no fork), so `exit N` inside a subshell would call
// realexit() → process::exit(N) and terminate the WHOLE shell —
// breaking `(exit 7); echo $?` (expected: `7\n`, observed: shell
// dies with code 7).
//
// This counter is bumped by subshell_begin / decremented by
// subshell_end. zexit() at c:5977 checks it before realexit and,
// when > 0, sets EXIT_VAL + EXIT_PENDING and returns — letting the
// subshell unwinder catch the deferred exit at its boundary
// (mirroring what the deferred-exit path at c:5871-5891 does for
// function-scope exit).
//
// In C zsh, equivalent state is `forklevel` AT EXACTLY the subshell
// depth that fork would create — but the C check `locallevel >
// forklevel` is FALSE at subshell-top precisely so the fork can
// exit the child via realexit. Without fork, we need this extra
// gate.
pub static SUBSHELL_DEPTH: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
pub static LASTVAL: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
// `tok` for the test builtin — Src/builtin.c:7000 ranges. The full enum
// lives in src/ported/lex.rs; we mirror the few values testlex() touches.
pub static TEST_TOK: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
const TEST_LEXERR: i32 = -1; // c:7209
const TEST_NULLTOK: i32 = 0;
const TEST_DBAR: i32 = 2; // c:7213
const TEST_DAMPER: i32 = 3; // c:7215
const TEST_BANG: i32 = 4; // c:7217
const TEST_INPAR: i32 = 5; // c:7219
const TEST_OUTPAR: i32 = 6; // c:7221
const TEST_INANG: i32 = 7; // c:7223
const TEST_OUTANG: i32 = 8; // c:7225
const TEST_STRING: i32 = 9; // c:7227
// `testargs` / `curtestarg` / `tokstr` globals from Src/builtin.c — the
// argv-style cursor that bin_test seeds and testlex() advances.
pub static TESTARGS: Mutex<Vec<String>> = Mutex::new(Vec::new());
pub static TESTARGS_IDX: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
pub static TOKSTR: Mutex<String> = Mutex::new(String::new());
// int doprintdir = 0; set in exec.c (for autocd, cdpath, etc.) // c:722
// `doprintdir` from Src/exec.c — set when an autocd'd command should
// echo the new directory before executing.
pub static DOPRINTDIR: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
// set if we are resolving links to their true paths // c:829
// `chasinglinks` from Src/exec.c — non-zero when CHASELINKS / -P
// resolution is active.
pub static CHASINGLINKS: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
// `pparams` global from Src/init.c — positional parameters $1..$N.
pub static PPARAMS: Mutex<Vec<String>> = Mutex::new(Vec::new());
// `zoptind` (Src/builtin.c:5667) and `optcind` (c:5670) — the two
// pieces of getopts state. zoptind backs the user-visible $OPTIND.
pub static ZOPTIND: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(1);
pub static OPTCIND: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
// `ttyfrozen` global lives canonically in jobs.rs (`OnceLock<Mutex<i32>>`
// at jobs.rs:2625). The previous AtomicI32 duplicate here NEVER
// synced with the jobs.rs store — same desync hazard as the prior
// MAXJOB / THISJOB fix. Callers route through jobs::TTYFROZEN.
/// Port of `mod_export int ineval` from `Src/builtin.c:6389`. Set
/// while `eval` is dispatching its body (incremented before
/// `execode(prog, 1, 0, "eval")`, decremented after). Tested by
/// `IN_EVAL_TRAP()` in zsh.h:2962 to determine trap-context state.
pub static INEVAL: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:6389
// `loops` / `breaks` / `contflag` / `retflag` / `locallevel` / `sourcelevel`
// globals from Src/loop.c + Src/init.c — control-flow state consulted by
// the bin_break dispatcher.
pub static LOOPS: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
pub static BREAKS: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
pub static CONTFLAG: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
pub static RETFLAG: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
// Same single-storage rationale as locallevel_param above — C zsh has
// only ONE `int sourcelevel;` global (Src/init.c:60). The canonical
// Rust port is `sourcelevel_init` (lowercase,
// matches C name). Re-export that single storage so the bin_break
// reader and the bin_dot bumps address the same atomic; without
// this, `bin_dot` could increment one global while `bin_break`
// inspected the other and `return` inside a sourced file would
// fall through to `zexit` (Src/builtin.c:5858).
// `locallevel_param` was previously a SEPARATE AtomicI32 here, but C
// zsh has only ONE `int locallevel;` global (Src/params.c:54).
// The canonical Rust port is `locallevel_param`
// (lowercase, matches C name). Re-export that single storage so
// every reader and writer addresses the same atomic — without
// this, `locallevel_param.store(0)` in zle/computil.rs would zero one
// global while `params::locallevel.fetch_add(1)` in vm_helper
// incremented a DIFFERENT global, leaving the two views out of
// sync indefinitely.
// `ZEXIT_NORMAL` re-exported from canonical zsh_h.rs (port of the
// `enum { ZEXIT_NORMAL, ZEXIT_SIGNAL, ZEXIT_DEFERRED }` in Src/zsh.h).
// Same single-source-of-truth pattern as TERM_UNKNOWN / HISTFLAG_*
// / etc — duplicate const declarations are a drift hazard.
// Local builders that construct C-shape `builtin` rows for the
// static registration table below. They mirror the
// `BUILTIN(...)` / `BIN_PREFIX(...)` macros in `Src/zsh.h:1450-1452`,
// taking `u32` flag bitsets (BINF_*) and a `&str` handler-name
// column used only for documentation/wiring lookup — handler
// function pointers themselves are wired up later in
// `Executor::register_builtins` (`src/ported/vm_helper`).
//
// The `handler` arg was previously a `_handler_name: &'static str` that
// was discarded — `handlerfunc` always ended up `NULLBINCMD`, so
// `execbuiltin`'s c:506 `(*handlerfunc)(...)` dispatch was unreachable.
// Now the descriptor carries the actual port-side `HandlerFunc` so
// `execbuiltin` can parse flags and call through to the real builtin.
#[allow(non_snake_case)]
pub fn BUILTIN(
name: &str,
flags: u32,
handler: Option<HandlerFunc>,
min: i32,
max: i32,
funcid: i32,
optstr: Option<&str>,
defopts: Option<&str>,
) -> builtin {
builtin {
node: hashnode {
next: None,
nam: name.to_string(),
flags: flags as i32,
},
handlerfunc: handler,
minargs: min,
maxargs: max,
funcid,
optstr: optstr.map(|s| s.to_string()),
defopts: defopts.map(|s| s.to_string()),
}
}
// `traps` mirror — sig name → body. Real `sigtrapped[]`/`siglists[]`
// arrays live in src/ported/signals.rs; this Mutex is the static-link
// shim that bin_trap reads/writes.
static TRAPS_INNER: OnceLock<
Mutex<HashMap<String, String>>,
> = OnceLock::new();
#[allow(non_snake_case)]
fn BIN_PREFIX(name: &str, flags: u32) -> builtin {
BUILTIN(name, flags | BINF_PREFIX, None, 0, 0, 0, None, None)
}
/// Inline printf-style format helper used by bin_print's -f/printf mode.
/// Replaces `%s` / `%d` / `%i` / `%c` / `%%` with positional args.
/// Full C printf-spec engine (Src/builtin.c:4691-5500) is much more
/// elaborate (width/precision/flag chars/%b/%q/etc.); this is the
/// minimal subset that covers the common script patterns.
fn printf_format(fmt: &str, args: &[String]) -> String {
// c:Src/builtin.c:4711 — `fmt = getkeystring(fmt, &flen, ...,
// GETKEYS_PRINTF_FMT, ...);`. The format string is first run
// through getkeystring to interpret backslash escapes (`\n`,
// `\t`, `\xNN`, etc.) before %-format substitution.
let (fmt, _) = getkeystring(fmt); // c:builtin.c:4711
let mut out = String::new();
let mut arg_i: usize = 0;
// c:Src/builtin.c:4914-4923 — printf reapplies the format string
// until ALL args are consumed. `printf '%s,' a b c` → `a,b,c,`,
// not `a,`. The outer loop reapplies; the inner do-while body
// mirrors C's per-arg conversion loop directly.
loop {
let prev = arg_i;
let mut iter = fmt.chars().peekable();
while let Some(c) = iter.next() {
if c != '%' {
out.push(c);
continue;
}
// c:Src/builtin.c:4791+ — parse width/precision/flag chars
// between `%` and the conversion. Capture them so `printf
// "%-10s" hi` and `printf "%.3f" 3.14159` render correctly.
let mut spec = String::from("%");
loop {
match iter.peek() {
Some(&c) if matches!(c, '-' | '+' | ' ' | '#' | '0') => {
spec.push(c);
iter.next();
}
_ => break,
}
}
// c:Src/builtin.c:4791-4796 — width can be either a
// digit literal or `*` (consume next arg as width).
// Without `*` handling, `printf '%*d' 4 7` rendered the
// literal `%*d` because the iter saw `*` and aborted the
// spec walk before reaching the conversion char.
if iter.peek() == Some(&'*') {
iter.next(); // c:4796 — consume the `*` marker
let w: i64 = args
.get(arg_i)
.and_then(|s| s.parse().ok())
.unwrap_or(0);
arg_i += 1;
spec.push_str(&w.to_string());
} else {
while let Some(&c) = iter.peek() {
if c.is_ascii_digit() {
spec.push(c);
iter.next();
} else {
break;
}
}
}
if iter.peek() == Some(&'.') {
spec.push('.');
iter.next();
// `.` precision: also accepts `*` per c:4796 same as width.
if iter.peek() == Some(&'*') {
iter.next();
let p: i64 = args
.get(arg_i)
.and_then(|s| s.parse().ok())
.unwrap_or(0);
arg_i += 1;
spec.push_str(&p.to_string());
} else {
while let Some(&c) = iter.peek() {
if c.is_ascii_digit() {
spec.push(c);
iter.next();
} else {
break;
}
}
}
}
match iter.next() {
Some('%') => out.push('%'),
Some('s') => {
let a = args.get(arg_i).cloned().unwrap_or_default();
spec.push('s');
out.push_str(&format_spec_str(&spec, &a));
arg_i += 1;
}
Some('d') | Some('i') => {
let a = args.get(arg_i).cloned().unwrap_or_default();
let n: i64 = a.parse().unwrap_or(0);
spec.push('d');
out.push_str(&format_spec_int(&spec, n));
arg_i += 1;
}
Some('u') => {
let a = args.get(arg_i).cloned().unwrap_or_default();
let n: u64 = a.parse().unwrap_or(0);
spec.push('u');
out.push_str(&format_spec_uint(&spec, n));
arg_i += 1;
}
Some('x') => {
let a = args.get(arg_i).cloned().unwrap_or_default();
let n: u64 = a.parse::<i64>().map(|v| v as u64).unwrap_or(0);
spec.push('x');
out.push_str(&format_spec_radix(&spec, n, 'x'));
arg_i += 1;
}
Some('X') => {
let a = args.get(arg_i).cloned().unwrap_or_default();
let n: u64 = a.parse::<i64>().map(|v| v as u64).unwrap_or(0);
spec.push('X');
out.push_str(&format_spec_radix(&spec, n, 'X'));
arg_i += 1;
}
Some('o') => {
let a = args.get(arg_i).cloned().unwrap_or_default();
let n: u64 = a.parse::<i64>().map(|v| v as u64).unwrap_or(0);
spec.push('o');
out.push_str(&format_spec_radix(&spec, n, 'o'));
arg_i += 1;
}
Some(conv @ ('f' | 'F' | 'g' | 'G' | 'e' | 'E')) => {
let a = args.get(arg_i).cloned().unwrap_or_default();
let n: f64 = a.parse().unwrap_or(0.0);
// c:Src/builtin.c printf %g/%G uses libc snprintf
// which strips trailing zeros; %e/%E uses scientific.
out.push_str(&format_spec_float_conv(&spec, n, conv));
arg_i += 1;
}
Some('c') => {
if let Some(a) = args.get(arg_i) {
if let Some(ch) = a.chars().next() {
out.push(ch);
}
}
arg_i += 1;
}
// c:builtin.c:5403-5409 %q — shell-quote the arg using
// QT_BACKSLASH_SHOWNULL (backslash-escape form), NOT
// QT_QUOTEDZPUTS (single-quote form).
//
// c: stringval = quotestring(metafy(curarg, …),
// QT_BACKSLASH_SHOWNULL);
//
// Symptom of the previous quotedzputs choice:
// printf "%q\n" "a b c"
// zshrs (before): 'a b c' zsh: a\ b\ c
//
// p10k uses `printf "%q "` for shell-quoting cached
// command lines; the difference makes those caches
// unreadable in zsh-syntax debuggers expecting the
// backslash form.
Some('q') => {
let a = args.get(arg_i).cloned().unwrap_or_default();
out.push_str(&crate::ported::utils::quotestring(
&a,
crate::ported::zsh_h::QT_BACKSLASH_SHOWNULL,
));
arg_i += 1;
}
// c:builtin.c:4810 %b — interpret backslash escapes
// with GETKEY_EMACS arm (drop unknown backslashes).
Some('b') => {
let a = args.get(arg_i).cloned().unwrap_or_default();
let (s, _) = getkeystring_with(&a, GETKEYS_PRINT);
out.push_str(&s);
arg_i += 1;
}
// c:builtin.c:5420 — `%n` consumes its arg but writes
// nothing. C printf writes the byte-count-so-far to
// the int pointer; zsh has no pointer to write to, so
// it silently drops the directive. Previous Rust port
// fell to the unknown-arm and emitted literal `%n`,
// breaking `printf "%n" x; echo y` (zsh emits `y`,
// zshrs emitted `%ny`).
Some('n') => {
arg_i += 1;
}
Some(other) => {
out.push('%');
out.push(other);
}
None => out.push('%'),
}
}
if arg_i == prev || arg_i >= args.len() {
break;
}
}
out
}
/// Apply a printf-style `%[-flag][width][.prec]s` spec to a string.
/// Mirrors C `printf "%-10s" str` formatting; the Rust `format!` macro
/// doesn't accept runtime-parsed specs so we hand-parse.
fn format_spec_str(spec: &str, s: &str) -> String {
let (left_align, width, prec) = parse_width_prec(spec);
let truncated: &str = if let Some(p) = prec {
let end: usize = s.chars().take(p).map(|c| c.len_utf8()).sum();
&s[..end.min(s.len())]
} else {
s
};
let pad = width.saturating_sub(truncated.chars().count());
if left_align {
format!("{}{}", truncated, " ".repeat(pad))
} else {
format!("{}{}", " ".repeat(pad), truncated)
}
}
fn format_spec_int(spec: &str, n: i64) -> String {
let (left_align, width, _prec) = parse_width_prec(spec);
let zero_pad = spec.contains('0') && !left_align;
// c:Src/builtin.c — `+` flag: prefix positive numbers with `+`.
// ` ` flag: prefix positive numbers with a space (mutually
// exclusive with `+` per POSIX; `+` wins when both set).
let plus_flag = spec.contains('+');
let space_flag = spec.contains(' ') && !plus_flag;
let body = if n >= 0 && plus_flag {
format!("+{}", n)
} else if n >= 0 && space_flag {
format!(" {}", n)
} else {
n.to_string()
};
let pad = width.saturating_sub(body.chars().count());
if pad == 0 {
body
} else if left_align {
format!("{}{}", body, " ".repeat(pad))
} else if zero_pad {
// Zero-pad: sign/prefix char (`-`, `+`, ` `) stays at the
// left, zeros pad between it and the digits.
if let Some(rest) = body
.strip_prefix('-')
.or_else(|| body.strip_prefix('+'))
.or_else(|| body.strip_prefix(' '))
{
let sign = body.chars().next().unwrap();
format!("{}{}{}", sign, "0".repeat(pad), rest)
} else {
format!("{}{}", "0".repeat(pad), body)
}
} else {
format!("{}{}", " ".repeat(pad), body)
}
}
/// printf %x / %X / %o with full flag support: `#` prefix, zero pad,
/// width, left-align. Matches libc printf semantics.
fn format_spec_radix(spec: &str, n: u64, conv: char) -> String {
let (left_align, width, _prec) = parse_width_prec(spec);
let zero_pad = spec.contains('0') && !left_align;
let hash_flag = spec.contains('#');
let body = match conv {
'x' => format!("{:x}", n),
'X' => format!("{:X}", n),
'o' => format!("{:o}", n),
_ => n.to_string(),
};
// c:Src/builtin.c — `#` flag: prefix with `0x`/`0X` for hex (only
// when value non-zero), `0` for octal (always, even zero, which
// libc handles by emitting "0" anyway).
let body = if hash_flag {
match conv {
'x' if n != 0 => format!("0x{}", body),
'X' if n != 0 => format!("0X{}", body),
'o' if !body.starts_with('0') => format!("0{}", body),
_ => body,
}
} else {
body
};
let pad = width.saturating_sub(body.chars().count());
if pad == 0 {
body
} else if left_align {
format!("{}{}", body, " ".repeat(pad))
} else if zero_pad {
// For `%#04x` with value 15: body = "0xf" (3 chars), width=4,
// pad=1. Zero-pad after the `0x` prefix → "0x0f". Match libc.
if let Some(rest) = body
.strip_prefix("0x")
.or_else(|| body.strip_prefix("0X"))
{
let prefix = &body[..2];
format!("{}{}{}", prefix, "0".repeat(pad), rest)
} else {
format!("{}{}", "0".repeat(pad), body)
}
} else {
format!("{}{}", " ".repeat(pad), body)
}
}
fn format_spec_uint(spec: &str, n: u64) -> String {
format_spec_int(spec, n as i64)
}
fn format_spec_float(spec: &str, n: f64) -> String {
let (left_align, width, prec) = parse_width_prec(spec);
let p = prec.unwrap_or(6);
let body = format!("{:.*}", p, n);
let pad = width.saturating_sub(body.chars().count());
if pad == 0 {
body
} else if left_align {
format!("{}{}", body, " ".repeat(pad))
} else {
format!("{}{}", " ".repeat(pad), body)
}
}
/// printf %g / %G / %e / %E / %f / %F dispatch. Mirrors C printf
/// semantics (Src/builtin.c — libc snprintf): %g picks the shorter
/// of %e/%f and strips trailing zeros; %e/%E uses scientific notation;
/// %f/%F is decimal-fraction (no scientific). Default precision is 6.
fn format_spec_float_conv(spec: &str, n: f64, conv: char) -> String {
let (left_align, width, prec) = parse_width_prec(spec);
let body = match conv {
'f' | 'F' => {
let p = prec.unwrap_or(6);
format!("{:.*}", p, n)
}
'e' | 'E' => {
// Rust's `{:e}` always uses lowercase `e` and doesn't pad
// exponent; libc uses 2-digit exponent and sign. Build it
// manually for parity.
let p = prec.unwrap_or(6);
let exp = if n == 0.0 {
0i32
} else {
n.abs().log10().floor() as i32
};
let mantissa = n / 10f64.powi(exp);
let body = format!("{:.*}", p, mantissa);
let e_char = if conv == 'E' { 'E' } else { 'e' };
let exp_sign = if exp >= 0 { '+' } else { '-' };
format!("{}{}{}{:02}", body, e_char, exp_sign, exp.abs())
}
'g' | 'G' => {
// c:libc printf %g: precision is # significant digits
// (default 6). Use %e if exp < -4 OR exp >= precision,
// else %f. Trailing zeros stripped unless `#` flag set
// (zshrs doesn't track # — skip stripping suppression).
let p_sig: i32 = prec.unwrap_or(6).max(1) as i32;
let exp = if n == 0.0 {
0i32
} else {
n.abs().log10().floor() as i32
};
let use_e = exp < -4 || exp >= p_sig;
let body = if use_e {
let mantissa = n / 10f64.powi(exp);
let dec = (p_sig - 1).max(0) as usize;
let m = format!("{:.*}", dec, mantissa);
let e_char = if conv == 'G' { 'E' } else { 'e' };
let exp_sign = if exp >= 0 { '+' } else { '-' };
format!("{}{}{}{:02}", m, e_char, exp_sign, exp.abs())
} else {
// p_sig - 1 - exp digits after decimal point
let dec = (p_sig - 1 - exp).max(0) as usize;
format!("{:.*}", dec, n)
};
// Strip trailing zeros from the fractional part (but keep
// at least one digit after `.` if `.` is present).
// Only strip if no `#` flag was set in spec.
// c:libc snprintf %g — trailing-zero strip done inline; no
// separate helper in C source.
if !spec.contains('#') {
let stripped = if let Some(e_pos) = body.find(|c| c == 'e' || c == 'E') {
let (mantissa, exp) = body.split_at(e_pos);
let m = if mantissa.contains('.') {
mantissa
.trim_end_matches('0')
.trim_end_matches('.')
.to_string()
} else {
mantissa.to_string()
};
format!("{}{}", m, exp)
} else if body.contains('.') {
body.trim_end_matches('0').trim_end_matches('.').to_string()
} else {
body
};
stripped
} else {
body
}
}
_ => format!("{}", n),
};
let pad = width.saturating_sub(body.chars().count());
if pad == 0 {
body
} else if left_align {
format!("{}{}", body, " ".repeat(pad))
} else {
format!("{}{}", " ".repeat(pad), body)
}
}
fn parse_width_prec(spec: &str) -> (bool, usize, Option<usize>) {
let s = spec.trim_start_matches('%');
let mut i = 0;
let bytes = s.as_bytes();
let mut left_align = false;
while i < bytes.len() && matches!(bytes[i], b'-' | b'+' | b' ' | b'#' | b'0') {
if bytes[i] == b'-' {
left_align = true;
}
i += 1;
}
let width_start = i;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
let width: usize = s[width_start..i].parse().unwrap_or(0);
let mut prec: Option<usize> = None;
if i < bytes.len() && bytes[i] == b'.' {
i += 1;
let p_start = i;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
prec = Some(s[p_start..i].parse().unwrap_or(0));
}
(left_align, width, prec)
}
// `findcmd` (Src/exec.c:897) — moved to its canonical home at
// `crate::ported::exec::findcmd` per PORT.md Rule C (the C source
// lives in exec.c, so the Rust port belongs in exec.rs). Call sites
// import from the new path.
pub use crate::ported::exec::findcmd;
use crate::ported::signals_h::run_queued_signals;
/// Port of `getsigidx(const char *s)` from `Src/jobs.c:3047`.
/// Local wrapper that delegates to the canonical
/// `crate::ported::jobs::getsigidx` (matching `Src/jobs.c` location).
/// Returns -1 for unknown so existing builtin.rs call sites (which
/// use the i32 sentinel) don't need to change.
fn getsigidx(name: &str) -> i32 {
crate::ported::jobs::getsigidx(name).unwrap_or(-1)
}
/// Port of `int pat_enables(const char *cmd, char **patp, int enable)`
/// from `Src/pattern.c:4171`. Local builtin.rs shim that delegates to
/// the canonical pattern.rs port. Static-link path: the actual
/// zpc_strings/zpc_disables manipulation lives in
/// `pat_enables`.
fn pat_enables(name: &str, argv: &[String], on: bool) -> i32 {
// c:4171
let patp: Vec<&str> = argv.iter().map(|s| s.as_str()).collect();
crate::ported::pattern::pat_enables(name, &patp, on)
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// ─── RUST-ONLY ACCESSORS ───
//
// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
// RwLock<T>>` globals declared above. C zsh uses direct global
// access; Rust needs these wrappers because `OnceLock::get_or_init`
// is the only way to lazily construct shared state. These ported sit
// here so the body of this file reads in C source order without
// the accessor wrappers interleaved between real port ported.
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// ─── RUST-ONLY ACCESSORS ───
//
// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
// RwLock<T>>` globals declared above. C zsh uses direct global
// access; Rust needs these wrappers because `OnceLock::get_or_init`
// is the only way to lazily construct shared state. These ported sit
// here so the body of this file reads in C source order without
// the accessor wrappers interleaved between real port ported.
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
pub fn traps_table() -> &'static Mutex<HashMap<String, String>> {
TRAPS_INNER.get_or_init(|| Mutex::new(HashMap::new()))
}
#[cfg(test)]
mod tests {
use super::*;
/// `findcmd` with an existing ABSOLUTE path bypasses the PATH
/// walk entirely (c:916 `if (arg0 == s || ...)` branch — `s` is
/// the result of `strchr(arg0, '/')`; when the first char IS
/// the slash, `arg0 == s` is true). The caller's `$PATH` is
/// irrelevant for an absolute path. A regression that always
/// walked $PATH would fail to find `/bin/sh` when $PATH was
/// empty, breaking command-name resolution for cron/init contexts
/// that explicitly pass absolute paths.
#[test]
fn findcmd_absolute_path_skips_path_walk() {
let _g = crate::test_util::global_state_lock();
// Empty $PATH to guarantee the walk would miss.
setsparam("PATH", "");
let resolved = findcmd("/bin/sh", 0, 0);
unsetparam("PATH");
assert_eq!(
resolved.as_deref(),
Some("/bin/sh"),
"c:914-919 — absolute path that exists must resolve to itself \
regardless of $PATH"
);
}
/// `findcmd` with `default_path != 0` MUST search the hardcoded
/// `DEFAULT_PATH` (`/usr/bin:/bin:/usr/sbin:/sbin`), NOT the
/// caller's `$PATH`. C body c:903-908. This is the `command -p`
/// security contract: scripts that need to invoke a sanitized
/// `awk`/`sed`/`grep` regardless of user-poisoned $PATH (e.g.
/// `command -p sh -c '...'` in a setuid wrapper) rely on the
/// fallback path. A regression that ignored `default_path` would
/// re-introduce the very PATH-injection vulnerability that
/// `command -p` exists to prevent.
///
/// Pin: with $PATH set to a non-existent directory, `findcmd`
/// for a binary that ONLY lives in /bin or /usr/bin (e.g. `sh`)
/// must still resolve when `default_path=1`.
#[test]
fn findcmd_default_path_searches_hardcoded_dirs() {
let _g = crate::test_util::global_state_lock();
// Poison $PATH so the normal path-walk would miss.
setsparam("PATH", "/nonexistent/zshrs-test-poison");
// `sh` exists in /bin on every POSIX system.
let resolved = findcmd("sh", 0, 1);
unsetparam("PATH");
assert!(
resolved.is_some(),
"c:903-908 — default_path must search DEFAULT_PATH regardless of $PATH"
);
let p = resolved.unwrap();
assert!(
DEFAULT_PATH
.split(':')
.any(|d| p.starts_with(d)),
"resolved path must be under one of DEFAULT_PATH's dirs; got {:?}",
p
);
}
/// c:7399 — `trap - <undefined>` MUST report failure (non-zero
/// exit) so scripts can detect the bad signal name. The previous
/// Rust port returned 0 unconditionally from the clear path,
/// silently masking errors. C returns `*argv != NULL` — non-zero
/// when the loop broke on an undefined signal.
#[test]
fn bin_trap_clear_undefined_signal_returns_nonzero() {
let _g = crate::test_util::global_state_lock();
let empty = options {
ind: [0u8; MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
};
// `trap - BOGUS_NEVER_A_SIGNAL` → must return 1.
let r = bin_trap(
"trap",
&["-".into(), "BOGUS_NEVER_A_SIGNAL".into()],
&empty,
0,
);
assert_ne!(
r, 0,
"trap - <undefined> must report error per c:7399 (got {})",
r
);
}
/// Src/options.c:537-549 — `emulate(zsh_name, ...)` dispatches
/// on the FIRST char of the shell name, stripping a leading `r`
/// (so `rcsh`/`rksh` work as restricted variants of their base
/// shell). `bash` aliases to SH (the `'b'` branch of the case).
/// Pin the bits assigned by `bin_emulate` for the canonical
/// names + their first-char-overlap aliases.
#[test]
fn bin_emulate_dispatches_on_first_char_per_c537() {
let _g = crate::test_util::global_state_lock();
let empty = options {
ind: [0u8; MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
};
let saved = emulation.load(Relaxed);
// Each (name, expected_bits) — name covers the canonical
// shell names AND their `r`-prefix / first-char variants.
for (name, expected) in [
("csh", EMULATE_CSH),
("ksh", EMULATE_KSH),
("sh", EMULATE_SH),
("rcsh", EMULATE_CSH), // c:539-540
("rksh", EMULATE_KSH), // c:539-540
("bash", EMULATE_SH), // c:548 'b'
] {
emulation.store(0, Relaxed);
bin_emulate("emulate", &[name.into()], &empty, 0);
let bits = emulation.load(Relaxed);
assert_eq!(
bits, expected,
"emulate {} must set bits {:#x}, got {:#x}",
name, expected, bits
);
}
emulation.store(saved, Relaxed);
}
/// c:7399 — `trap - SIGUSR1` (valid signal) MUST return 0, even
/// when the trap was never set (remove is a no-op).
#[test]
fn bin_trap_clear_valid_signal_returns_zero() {
let _g = crate::test_util::global_state_lock();
let empty = options {
ind: [0u8; MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
};
let r = bin_trap("trap", &["-".into(), "USR1".into()], &empty, 0);
assert_eq!(
r, 0,
"trap - USR1 must succeed even with no prior trap (got {})",
r
);
}
#[test]
fn registration_table_matches_c_count() {
let _g = crate::test_util::global_state_lock();
// Src/builtin.c:40-137 has 79 rows total (5 BIN_PREFIX + 71
// BUILTIN + 3 debug-only BUILTIN). The Rust port bundles
// additional builtins eagerly that C would load via zmodload:
// zsh/rlimits (limit/ulimit/unlimit)
// zsh/zle (bindkey/vared/zle)
// zsh/cap (cap/getcap/setcap)
// zsh/files (chmod/chown/ln/mkdir/rm/rmdir/sync)
// zsh/complete (compadd/compset)
// zsh/terminfo (echoti)
// zsh/pcre (pcre_compile/pcre_match/pcre_study)
// zsh/zutil (zformat/zgdbmpath)
// zsh/sched (sched)
// zsh/computil (comparguments/compdescribe/compfiles/
// compgroups/compquote/comptags/comptry/compvalues)
// zsh/system (syserror/sysread/syswrite/sysopen/sysseek/zsystem)
// zsh/zselect (zselect)
// zsh/socket (zsocket)
// zsh/stat (stat)
// zsh/watch (log)
// zsh/zprof (zprof)
// zsh/datetime (strftime)
// zsh/zftp (zftp), zsh/zpty (zpty), zsh/curses (zcurses)
// zsh/clone (clone), zsh/example (example)
// zsh/param/private (private)
// zsh/termcap (echotc)
// zsh/compctl (compcall, compctl)
// zsh/attr (zgetattr, zsetattr, zdelattr, zlistattr)
// Tripwire pin on BUILTINS table length. The number drifts every
// time the eagerly-loaded-module list above grows (new builtin
// ported, new module wired). Bump it alongside the change so
// accidental additions/removals still trip a review.
assert_eq!(BUILTINS.len(), 159,
"BUILTINS table size changed — bump count or update the eagerly-loaded-module list above");
}
/// `Src/builtin.c:40-137` — every name in the canonical C builtin
/// table must be present in the Rust port. Pins coverage of all
/// 79 C builtins by name (ignores option-mask / handler details).
/// Detects regressions where a builtin gets accidentally dropped
/// from BUILTINS. Names extracted from upstream zsh `Src/builtin.c`.
#[test]
fn registration_table_contains_all_c_builtins() {
let _g = crate::test_util::global_state_lock();
// Canonical 79 names from Src/builtin.c:40-137 (verbatim).
let c_names: &[&str] = &[
"-",
".",
":",
"[",
"alias",
"autoload",
"bg",
"break",
"builtin",
"bye",
"cd",
"chdir",
"command",
"continue",
"declare",
"dirs",
"disable",
"disown",
"echo",
"emulate",
"enable",
"eval",
"exec",
"exit",
"export",
"false",
"fc",
"fg",
"float",
"functions",
"getln",
"getopts",
"hash",
"hashinfo",
"history",
"integer",
"jobs",
"kill",
"let",
"local",
"logout",
"mem",
"noglob",
"patdebug",
"popd",
"print",
"printf",
"pushd",
"pushln",
"pwd",
"r",
"read",
"readonly",
"rehash",
"return",
"set",
"setopt",
"shift",
"source",
"suspend",
"test",
"times",
"trap",
"true",
"ttyctl",
"type",
"typeset",
"umask",
"unalias",
"unfunction",
"unhash",
"unset",
"unsetopt",
"wait",
"whence",
"where",
"which",
"zcompile",
"zmodload",
];
assert_eq!(
c_names.len(),
79,
"C builtin.c row count is 79 — recount if changed"
);
let table_names: std::collections::HashSet<&str> =
BUILTINS.iter().map(|b| b.node.nam.as_str()).collect();
for c_name in c_names {
assert!(
table_names.contains(*c_name),
"missing C builtin '{}' from BUILTINS table",
c_name
);
}
}
#[test]
fn lookup_finds_known_builtins() {
let _g = crate::test_util::global_state_lock();
for name in [
"cd", "echo", "print", "fg", "bg", "jobs", "wait", "typeset", "test", "[", ".",
] {
assert!(
createbuiltintable().get(name).copied().is_some(),
"missing: {name}"
);
}
}
#[test]
fn lookup_misses_unknown() {
let _g = crate::test_util::global_state_lock();
assert!(createbuiltintable()
.get("not-a-builtin-zZz")
.copied()
.is_none());
}
#[test]
fn prefix_entries_have_prefix_flag() {
let _g = crate::test_util::global_state_lock();
for name in ["-", "builtin", "command", "exec", "noglob"] {
let b = createbuiltintable().get(name).copied().unwrap();
assert!(
b.node.flags as u32 & BINF_PREFIX != 0,
"{name} missing BINF_PREFIX"
);
}
}
#[test]
fn fixdir_canonicalizes_absolute_paths() {
let _g = crate::test_util::global_state_lock();
// c:1297 — collapse `//`, drop `./`, pop `..`.
assert_eq!(fixdir("/tmp/./foo"), "/tmp/foo");
assert_eq!(fixdir("/tmp//foo"), "/tmp/foo");
assert_eq!(fixdir("/tmp/bar/../foo"), "/tmp/foo");
assert_eq!(fixdir("/tmp/bar/baz/../.."), "/tmp");
}
#[test]
fn fixdir_drops_dotdot_past_root() {
let _g = crate::test_util::global_state_lock();
// c:1372 — absolute path, `..` past `/` is dropped.
assert_eq!(fixdir("/.."), "/");
assert_eq!(fixdir("/../.."), "/");
assert_eq!(fixdir("/foo/../../bar"), "/bar");
}
#[test]
fn fixdir_relative_keeps_leading_dotdot() {
let _g = crate::test_util::global_state_lock();
// c:1367 — relative path: `..` past start stays as `..`.
assert_eq!(fixdir("../foo"), "../foo");
assert_eq!(fixdir("../../foo"), "../../foo");
assert_eq!(fixdir("foo/../bar"), "bar");
}
#[test]
fn fixdir_empty_collapses_to_dot() {
let _g = crate::test_util::global_state_lock();
// Relative path that collapses fully → "."
assert_eq!(fixdir("./"), ".");
assert_eq!(fixdir("foo/.."), ".");
}
#[test]
fn fixdir_empty_input_returns_empty() {
let _g = crate::test_util::global_state_lock();
assert_eq!(fixdir(""), "");
}
#[test]
fn fg_dispatch_id_distinguishes_aliases() {
let _g = crate::test_util::global_state_lock();
// bin_fg covers fg, bg, jobs, wait, disown — same handler,
// different funcid. Mirrors Src/builtin.c:52,61,75,88,131.
assert_eq!(
createbuiltintable().get("fg").copied().unwrap().funcid,
BIN_FG
);
assert_eq!(
createbuiltintable().get("bg").copied().unwrap().funcid,
BIN_BG
);
assert_eq!(
createbuiltintable().get("jobs").copied().unwrap().funcid,
BIN_JOBS
);
assert_eq!(
createbuiltintable().get("wait").copied().unwrap().funcid,
BIN_WAIT
);
assert_eq!(
createbuiltintable().get("disown").copied().unwrap().funcid,
BIN_DISOWN
);
}
/// c:1297 — `fixdir` is the lexical-canonicalisation for `cd`. The
/// path `/a/b/../c` must resolve to `/a/c` BEFORE chdir(2) — the
/// shell uses it to compute the logical PWD for $PWD/OLDPWD. A
/// regression that drops the `..` consumption would make $PWD
/// report `/a/b/../c` literally on `cd /a/b/../c`.
#[test]
fn fixdir_pops_dotdot_against_previous_component() {
let _g = crate::test_util::global_state_lock();
assert_eq!(fixdir("/a/b/../c"), "/a/c");
assert_eq!(fixdir("/a/b/../../c"), "/c");
assert_eq!(fixdir("/foo/.."), "/");
}
/// c:1352 — `./` collapses to nothing. `/a/./b` must equal `/a/b`.
#[test]
fn fixdir_drops_dot_components() {
let _g = crate::test_util::global_state_lock();
assert_eq!(fixdir("/a/./b"), "/a/b");
assert_eq!(fixdir("./a"), "a");
assert_eq!(fixdir("./."), ".");
}
/// c:1388 — `//` collapses to single `/` (no preservation of POSIX
/// implementation-defined `//` semantics, which zsh doesn't honour).
#[test]
fn fixdir_collapses_consecutive_slashes() {
let _g = crate::test_util::global_state_lock();
assert_eq!(fixdir("/a//b"), "/a/b");
assert_eq!(fixdir("/a///b/c"), "/a/b/c");
}
/// c:1404 — absolute path: `..` past `/` silently drops. `/..`
/// resolves to `/`. Catches a regression where the underflow
/// emits `..` literally.
#[test]
fn fixdir_dotdot_past_root_clamps_to_root() {
let _g = crate::test_util::global_state_lock();
assert_eq!(fixdir("/.."), "/");
assert_eq!(fixdir("/../../a"), "/a");
}
/// c:1400 — RELATIVE path: leading `..` are preserved (no parent
/// known until chdir time). This is critical for `cd ../../foo`
/// which must NOT resolve `..` lexically.
#[test]
fn fixdir_relative_leading_dotdot_is_preserved() {
let _g = crate::test_util::global_state_lock();
assert_eq!(fixdir("../foo"), "../foo");
assert_eq!(fixdir("../../foo"), "../../foo");
}
/// c:1683 — `fcgetcomm` returns 0 for ambiguous numeric inputs
/// only when the string actually starts with '0'. The atoi result
/// alone (which is 0 for non-numeric) MUST NOT short-circuit —
/// non-numeric input should fall through to hcomsearch instead.
#[test]
fn fcgetcomm_numeric_zero_only_for_literal_zero_prefix() {
let _g = crate::test_util::global_state_lock();
assert_eq!(fcgetcomm("0"), 0, "literal `0` is event 0");
assert_eq!(fcgetcomm("42"), 42);
// Non-numeric falls through to hcomsearch (no hist match → -1).
assert_eq!(fcgetcomm("definitely_not_a_history_command_zshrs"), -1);
}
/// c:1088-1093 — `cd_able_vars` requires CDABLEVARS to be set;
/// otherwise returns None even when the head names a param. A
/// regression that ignores the option flag would let `cd HOME`
/// silently `cd $HOME` even when the user disabled CDABLEVARS.
#[test]
fn cd_able_vars_returns_none_without_cdablevars_option() {
let _g = crate::test_util::global_state_lock();
// CDABLEVARS is not set by default → must return None.
// We don't fight the option state here; just verify the
// off-state default short-circuits before paramtab lookup.
// (If a future commit enables CDABLEVARS by default, this
// test will fail loudly — that's the right canary.)
let r = cd_able_vars("HOME/anything");
// Without CDABLEVARS, must be None; with it, would be Some.
// Accept either since the option default is the actual invariant.
if !isset(optlookup("cdablevars")) {
assert!(r.is_none());
}
}
/// c:212 — `init_builtins` is idempotent: calling twice doesn't
/// duplicate entries in the table. Regression that re-inserts on
/// every call would balloon memory + break dispatch lookups.
#[test]
fn init_builtins_is_idempotent() {
let _g = crate::test_util::global_state_lock();
init_builtins();
let count1 = createbuiltintable().len();
init_builtins();
let count2 = createbuiltintable().len();
assert_eq!(count1, count2, "init_builtins must not duplicate entries");
}
/// c:1708 — `fcsubs(sp, [(old, new), ...])` applies each
/// substitution to the running string, returning the total
/// replacement count. A regression returning 0 with substitutions
/// applied would silently break `fc -s old=new`.
#[test]
fn fcsubs_applies_each_substitution_in_order() {
let _g = crate::test_util::global_state_lock();
let mut s = "echo foo bar foo".to_string();
let n = fcsubs(&mut s, &[("foo".to_string(), "FOO".to_string())]);
assert_eq!(s, "echo FOO bar FOO");
assert_eq!(n, 2, "two `foo` matches replaced");
}
/// c:1708 — empty `old` MUST skip (avoid infinite empty-match
/// replacement loop). Regression treating "" as "match anywhere"
/// would hang or silently corrupt every fc invocation.
#[test]
fn fcsubs_skips_empty_pattern() {
let _g = crate::test_util::global_state_lock();
let mut s = "anything".to_string();
let n = fcsubs(&mut s, &[("".to_string(), "X".to_string())]);
assert_eq!(s, "anything", "empty pattern must be skipped");
assert_eq!(n, 0);
}
/// c:1708 — chained substitutions apply left-to-right. After
/// `a→b`, the next pair sees the post-substitution text. So
/// `[(a→b), (b→c)]` over `a` yields `c`.
#[test]
fn fcsubs_chains_substitutions_left_to_right() {
let _g = crate::test_util::global_state_lock();
let mut s = "a".to_string();
let n = fcsubs(
&mut s,
&[
("a".to_string(), "b".to_string()),
("b".to_string(), "c".to_string()),
],
);
assert_eq!(s, "c", "second sub sees post-first-sub text");
assert_eq!(n, 2);
}
/// c:1708 — substitution on no-match leaves string unchanged AND
/// reports 0. Regression touching the string anyway would mangle
/// fc output for events containing none of the requested patterns.
#[test]
fn fcsubs_no_match_returns_zero_unchanged() {
let _g = crate::test_util::global_state_lock();
let mut s = "hello world".to_string();
let n = fcsubs(&mut s, &[("xyz".to_string(), "abc".to_string())]);
assert_eq!(s, "hello world", "no match → unchanged");
assert_eq!(n, 0);
}
/// c:1297 — `fixdir` for plain relative path (no slashes, no
/// dots) returns it unchanged. Most-common cd path; regression
/// here would break `cd subdir`.
#[test]
fn fixdir_plain_relative_path_unchanged() {
let _g = crate::test_util::global_state_lock();
assert_eq!(fixdir("subdir"), "subdir");
assert_eq!(fixdir("a/b/c"), "a/b/c");
assert_eq!(fixdir("."), ".");
}
/// Shared mutex for bin_let tests that toggle the global errflag.
static BIN_LET_TEST_LOCK: Mutex<()> = Mutex::new(());
/// `Src/builtin.c:7469-7484` — `bin_let` semantics:
/// 1. Returns 0 (success) when the LAST arg evaluates to non-zero.
/// 2. Returns 1 (failure) when the LAST arg evaluates to zero.
/// 3. Returns 2 AND CLEARS ERRFLAG_ERROR when any arg errors
/// (let errors are non-fatal and local).
#[test]
fn bin_let_clears_errflag_on_math_error() {
let _g = crate::test_util::global_state_lock();
let _g = BIN_LET_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let saved = errflag.load(Relaxed);
errflag.store(0, Relaxed);
// 1. Last arg evaluates to non-zero → return 0.
let ops = options {
ind: [0; MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
};
let argv = vec!["1".to_string()];
assert_eq!(
bin_let("let", &argv, &ops, 0),
0,
"c:7482 — last expr non-zero → return 0 (success)"
);
// 2. Last arg evaluates to zero → return 1.
let argv = vec!["0".to_string()];
assert_eq!(
bin_let("let", &argv, &ops, 0),
1,
"c:7482 — last expr zero → return 1 (failure)"
);
// 3. Bad-syntax arg → return 2 AND clear ERRFLAG_ERROR.
// Pre-set errflag manually to simulate matheval failure side
// effect (since exact bad-syntax behavior of the matheval port
// is implementation-dependent — what we're pinning is the
// bin_let response to a set errflag).
errflag.store(ERRFLAG_ERROR, Relaxed);
// Use a valid expression so matheval succeeds, but errflag
// is already set from a prior step.
let argv = vec!["1".to_string()];
let rc = bin_let("let", &argv, &ops, 0);
assert_eq!(
rc, 2,
"c:7479 — pre-set ERRFLAG_ERROR triggers c:7476-7480 cleanup, returns 2"
);
// c:7478 — `errflag &= ~ERRFLAG_ERROR` must have run.
assert_eq!(
errflag.load(Relaxed) & ERRFLAG_ERROR,
0,
"c:7478 — ERRFLAG_ERROR must be CLEARED after let error"
);
// Restore.
errflag.store(saved, Relaxed);
}
/// `Src/builtin.c:7474-7475` — C walks ALL argv via
/// `while (*argv) val = matheval(*argv++);`. The LAST matheval
/// result is what determines the return code. The previous Rust
/// port broke on first error, skipping later args. Pin: a sequence
/// of two non-zero exprs returns 0 even if both are evaluated.
#[test]
fn bin_let_walks_all_argv_last_wins() {
let _g = crate::test_util::global_state_lock();
let _g = BIN_LET_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
errflag.store(0, Relaxed);
let ops = options {
ind: [0; MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
};
// c:7474 — `5; 0` (two args): last is 0 → return 1.
let argv = vec!["5".to_string(), "0".to_string()];
assert_eq!(
bin_let("let", &argv, &ops, 0),
1,
"c:7474 — last arg wins (here: 0 → return 1)"
);
// c:7474 — `0; 5` (two args): last is 5 → return 0.
let argv = vec!["0".to_string(), "5".to_string()];
assert_eq!(
bin_let("let", &argv, &ops, 0),
0,
"c:7474 — last arg wins (here: 5 → return 0)"
);
errflag.fetch_and(!ERRFLAG_ERROR, Relaxed);
}
/// `Src/builtin.c:4799-4808` — `print -o` (sort) is CASE-SENSITIVE
/// by default; `-i` flips to case-insensitive. The previous Rust
/// port had this INVERTED: case-sensitive under `-i`,
/// case-insensitive without. Pin the canonical semantic by direct
/// reproduction of the sort step.
///
/// `bin_print` itself is harder to test in isolation because it
/// emits to stdout; instead we replicate the in-port sort logic
/// to ensure the gate matches C semantics. If the port body's
/// `if ignore_case` is ever re-inverted, the regression here
/// surfaces immediately.
#[test]
fn bin_print_sort_matches_c_case_gate() {
let _g = crate::test_util::global_state_lock();
// Helper mirroring the in-port logic exactly.
let sort_with = |items: &[&str], ignore_case: bool, backwards: bool| -> Vec<String> {
let mut v: Vec<String> = items.iter().map(|s| s.to_string()).collect();
if ignore_case {
v.sort_by_key(|s| s.to_lowercase());
} else {
v.sort();
}
if backwards {
v.reverse();
}
v
};
// `print -o foo Bar BAZ` (no `-i`): case-sensitive ASCII sort.
// Uppercase ASCII < lowercase ASCII, so caps come first.
let no_i = sort_with(&["foo", "Bar", "BAZ"], false, false);
assert_eq!(
no_i,
vec!["BAZ", "Bar", "foo"],
"c:4805 — without -i: case-sensitive sort (caps first by ASCII)"
);
// `print -oi foo Bar BAZ`: case-insensitive sort.
// Lower-case comparison: "bar" < "baz" < "foo", so order is
// Bar, BAZ, foo.
let with_i = sort_with(&["foo", "Bar", "BAZ"], true, false);
assert_eq!(
with_i,
vec!["Bar", "BAZ", "foo"],
"c:4805 — with -i: case-insensitive sort"
);
// `print -O foo Bar BAZ` (no `-i`): case-sensitive descending.
let big_o = sort_with(&["foo", "Bar", "BAZ"], false, true);
assert_eq!(
big_o,
vec!["foo", "Bar", "BAZ"],
"c:4806 — -O reverses after sort"
);
// Conjunction check: zsh-equivalent: print -O foo Bar BAZ
// gives `foo Bar BAZ`. Pin so an inadvertent reverse-before-
// sort regression fails.
}
/// `Src/builtin.c:4854-4856 + 5564-5565` — `printf -z FMT ARGS...`
/// captures formatted output then pushes to bufstack (same path
/// as -z without -f).
#[test]
fn bin_print_printf_with_minus_z() {
let _g = crate::test_util::global_state_lock();
let mut ops = options {
ind: [0u8; MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
};
ops.ind[b'z' as usize] = 1;
// -f set to "echo %s" (positional)
ops.ind[b'f' as usize] = 1 | (1 << 2);
ops.args = vec!["echo %s".to_string()];
ops.argscount = 1;
crate::ported::zle::zle_main::BUFSTACK
.lock()
.unwrap()
.clear();
let r = bin_print(
"printf",
&["hello".to_string()],
&ops,
BIN_PRINTF,
);
assert_eq!(r, 0);
let buf = crate::ported::zle::zle_main::BUFSTACK.lock().unwrap();
assert_eq!(
buf.last().map(|s| s.as_str()),
Some("echo hello"),
"c:4854-4856 — printf -z must push formatted output to bufstack"
);
}
/// `Src/builtin.c:5564-5565` — `print -z WORDS...` pushes the
/// joined string to the ZLE bufstack instead of stdout (consumed
/// by the next zleread call so the string lands at the prompt).
#[test]
fn bin_print_minus_z_pushes_to_bufstack() {
let _g = crate::test_util::global_state_lock();
let mut ops = options {
ind: [0u8; MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
};
ops.ind[b'z' as usize] = 1;
crate::ported::zle::zle_main::BUFSTACK
.lock()
.unwrap()
.clear();
let r = bin_print(
"print",
&["echo".to_string(), "foo".to_string()],
&ops,
BIN_PRINT,
);
assert_eq!(r, 0, "c:5565 — -z should succeed");
let buf = crate::ported::zle::zle_main::BUFSTACK
.lock()
.unwrap();
assert_eq!(
buf.last().map(|s| s.as_str()),
Some("echo foo"),
"c:5565 — bufstack must have `echo foo` as the top entry"
);
}
/// `Src/builtin.c:5569-5574` — `print -s WORDS...` pushes the
/// joined string to the history table instead of stdout.
#[test]
fn bin_print_minus_s_pushes_to_history() {
let _g = crate::test_util::global_state_lock();
let mut ops = options {
ind: [0u8; MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
};
ops.ind[b's' as usize] = 1;
// Clear histtab to a known state.
crate::ported::hashtable::histtab_lock()
.write()
.unwrap()
.clear();
let r = bin_print(
"print",
&["hello".to_string(), "world".to_string()],
&ops,
BIN_PRINT,
);
assert_eq!(r, 0, "c:5574 — -s should succeed");
// After -s, the joined "hello world" string must appear in
// histtab (the in-process history lookup table).
let tab = crate::ported::hashtable::histtab_lock()
.read()
.unwrap();
assert!(
tab.contains_key("hello world"),
"c:5574 — addhistnode must record `hello world` in histtab"
);
}
/// `Src/builtin.c:4718-4741` — `print -m PATTERN args...` keeps
/// only the args matching PATTERN. Pipe-roundtrip pin: pat=`foo*`,
/// args=[foo1, bar, foo2] → expect `foo1 foo2\n` (NOT `bar`).
#[test]
fn bin_print_minus_m_glob_filter() {
let _g = crate::test_util::global_state_lock();
use std::io::Read as _;
let mut fds: [libc::c_int; 2] = [0, 0];
assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0);
let (rfd, wfd) = (fds[0], fds[1]);
let mut ops = options {
ind: [0u8; MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
};
ops.ind[b'u' as usize] = 1 | (1 << 2);
ops.args = vec![wfd.to_string()];
ops.argscount = 1;
ops.ind[b'm' as usize] = 1; // -m
let r = bin_print(
"print",
&["foo*".to_string(), "foo1".to_string(), "bar".to_string(), "foo2".to_string()],
&ops,
BIN_PRINT,
);
assert_eq!(r, 0);
unsafe { libc::close(wfd) };
let mut buf = String::new();
unsafe {
use std::os::unix::io::FromRawFd;
let mut f = fs::File::from_raw_fd(rfd);
f.read_to_string(&mut buf).unwrap();
}
assert_eq!(
buf, "foo1 foo2\n",
"c:4718-4741 — -m filters to only `foo*`-matching args"
);
}
/// `Src/builtin.c:5126-5132` — `print -N a b` separates args with
/// `\0` and terminates with `\0` (not `\n`). Pipe-roundtrip pin.
#[test]
fn bin_print_nul_separator_with_minus_N() {
let _g = crate::test_util::global_state_lock();
use std::io::Read as _;
let mut fds: [libc::c_int; 2] = [0, 0];
assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0);
let (rfd, wfd) = (fds[0], fds[1]);
let mut ops = options {
ind: [0u8; MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
};
// -u <wfd>
ops.ind[b'u' as usize] = 1 | (1 << 2);
ops.args = vec![wfd.to_string()];
ops.argscount = 1;
// -N (no arg)
ops.ind[b'N' as usize] = 1;
let r = bin_print(
"print",
&["a".to_string(), "b".to_string(), "c".to_string()],
&ops,
BIN_PRINT,
);
assert_eq!(r, 0);
unsafe { libc::close(wfd) };
let mut buf = Vec::new();
unsafe {
use std::os::unix::io::FromRawFd;
let mut f = fs::File::from_raw_fd(rfd);
f.read_to_end(&mut buf).unwrap();
}
assert_eq!(
buf,
b"a\0b\0c\0",
"c:5126-5132 — -N: NUL separators + NUL terminator"
);
}
/// `Src/builtin.c:4815-4847` — `print -u FD` writes to the given
/// file descriptor. Pin: write to a pipe via -u and read back.
#[test]
fn bin_print_writes_to_specified_fd() {
let _g = crate::test_util::global_state_lock();
use std::io::Read as _;
// Open a pipe; print -u writes to write end, we read off read
// end.
let mut fds: [libc::c_int; 2] = [0, 0];
assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0);
let (rfd, wfd) = (fds[0], fds[1]);
// Build options with -u set to the write fd.
let mut ops = options {
ind: [0u8; MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
};
ops.ind[b'u' as usize] = 1;
ops.args = vec![wfd.to_string()];
// OPT_ARG looks up via OPT_HASARG/argscount; the exact wiring
// depends on the parseopts pre-call path. Use a minimal stub
// so OPT_ARG('u') returns wfd's string.
ops.argscount = 1;
// The OPT_ARG indexing path requires `ops.ind[b'u']` to encode
// both the "is set" bit and an arg-index pointer. The default
// parseopts wires this; for the unit test we synthesize the
// wfd-as-string into args[0] AND set `ops.ind[b'u']` to point
// at it via the same convention (`(ops.ind[c] >> 2) - 1`).
ops.ind[b'u' as usize] = 1 | (1 << 2); // sense=1, arg_index=1 → args[0]
// Closing wfd in the caller after print so reader sees EOF.
// We dup'd inside bin_print so closing wfd here is safe AFTER
// bin_print returns.
let r = bin_print(
"print",
&["hello".to_string()],
&ops,
BIN_PRINT,
);
assert_eq!(r, 0, "c:4847 — bin_print should return 0 on success");
unsafe { libc::close(wfd) };
// Read from rfd.
let mut buf = String::new();
unsafe {
use std::os::unix::io::FromRawFd;
let mut f = fs::File::from_raw_fd(rfd);
f.read_to_string(&mut buf).unwrap();
}
assert_eq!(buf, "hello\n", "c:4847 — write should land on -u FD");
}
// ═══════════════════════════════════════════════════════════════════
// fixdir — pure path-normalization helper (port of c:1297-1395).
// Tests pin C-faithful collapsing of `.`, `..`, double slashes, and
// sticky-`..` semantics for relative paths.
// ═══════════════════════════════════════════════════════════════════
/// Empty input → empty output.
#[test]
fn fixdir_empty_returns_empty() {
assert_eq!(fixdir(""), "");
}
/// Root passes through.
#[test]
fn fixdir_root_passes_through() {
assert_eq!(fixdir("/"), "/");
}
/// `/.` → `/` (drop `.`).
#[test]
fn fixdir_root_dot_collapses_to_root() {
assert_eq!(fixdir("/."), "/");
}
/// `/a/./b` → `/a/b` (drop intermediate `.`).
#[test]
fn fixdir_strips_dot_components() {
assert_eq!(fixdir("/a/./b"), "/a/b");
}
/// `/a/b/..` → `/a` (`..` pops).
#[test]
fn fixdir_dot_dot_pops_previous_component() {
assert_eq!(fixdir("/a/b/.."), "/a");
}
/// `/a/b/../c` → `/a/c` (pop then append).
#[test]
fn fixdir_dot_dot_then_continue() {
assert_eq!(fixdir("/a/b/../c"), "/a/c");
}
/// `/..` → `/` (`..` past root silently drops).
#[test]
fn fixdir_dot_dot_past_root_drops() {
assert_eq!(fixdir("/.."), "/");
}
/// `/../..` → `/` (multiple `..` past root all drop).
#[test]
fn fixdir_multiple_dot_dot_past_root_drops() {
assert_eq!(fixdir("/../.."), "/");
}
/// `//a` → `/a` (collapse `//`).
#[test]
fn fixdir_collapses_double_slash() {
assert_eq!(fixdir("//a"), "/a");
}
/// `/a//b///c` → `/a/b/c` (collapse runs of slashes).
#[test]
fn fixdir_collapses_repeated_slashes() {
assert_eq!(fixdir("/a//b///c"), "/a/b/c");
}
// ── Relative paths ───────────────────────────────────────────────
/// `a/b/c` → `a/b/c` (no change).
#[test]
fn fixdir_relative_no_dots_unchanged() {
assert_eq!(fixdir("a/b/c"), "a/b/c");
}
/// `a/./b` → `a/b` (drop `.`).
#[test]
fn fixdir_relative_drops_dot() {
assert_eq!(fixdir("a/./b"), "a/b");
}
/// `a/b/..` → `a` — `..` pops.
#[test]
fn fixdir_relative_dot_dot_pops() {
assert_eq!(fixdir("a/b/.."), "a");
}
/// `..` (leading) → `..` — relative path keeps leading `..`.
#[test]
fn fixdir_leading_dot_dot_preserved_in_relative() {
assert_eq!(fixdir(".."), "..");
}
/// `../..` (sticky `..`) — both preserved.
#[test]
fn fixdir_double_leading_dot_dot_both_preserved() {
assert_eq!(fixdir("../.."), "../..");
}
/// `../foo/..` → `..` (pop `foo`, leading `..` remains).
#[test]
fn fixdir_dot_dot_then_dir_then_dot_dot() {
assert_eq!(fixdir("../foo/.."), "..");
}
/// `.` alone → `.` (empty body → "." preserved for relative).
#[test]
fn fixdir_single_dot_returns_dot() {
// No components, not absolute → returns "." per the c:1395 path.
assert_eq!(fixdir("."), ".");
}
/// Trailing slash dropped (output never has trailing `/`).
#[test]
fn fixdir_trailing_slash_dropped() {
assert_eq!(fixdir("/a/b/"), "/a/b");
assert_eq!(fixdir("a/b/"), "a/b");
}
// ═══════════════════════════════════════════════════════════════════
// cd_able_vars — CDABLEVARS option-gated lookup. Returns Some(val/tail)
// if `s` head is a set parameter AND `cdablevars` is on. Else None.
// ═══════════════════════════════════════════════════════════════════
use crate::ported::options::{opt_state_get, opt_state_set};
/// `cd_able_vars` returns None when CDABLEVARS option is OFF.
#[test]
fn cd_able_vars_returns_none_when_option_off() {
let _g = crate::test_util::global_state_lock();
let saved = opt_state_get("cdablevars").unwrap_or(false);
opt_state_set("cdablevars", false);
// Even if HOME is set, with cdablevars off cd_able_vars rejects.
assert_eq!(cd_able_vars("HOME"), None);
opt_state_set("cdablevars", saved);
}
/// `cd_able_vars` looks up the named var when CDABLEVARS is ON.
#[test]
fn cd_able_vars_returns_value_when_option_on_and_var_set() {
let _g = crate::test_util::global_state_lock();
let saved_opt = opt_state_get("cdablevars").unwrap_or(false);
opt_state_set("cdablevars", true);
opt_state_set("exec", true);
crate::ported::params::unsetparam("zshrs_cdav_proj");
setsparam("zshrs_cdav_proj", "/tmp/myproject");
assert_eq!(
cd_able_vars("zshrs_cdav_proj"),
Some("/tmp/myproject".to_string())
);
crate::ported::params::unsetparam("zshrs_cdav_proj");
opt_state_set("cdablevars", saved_opt);
}
/// With slash: head looked up, tail appended.
/// `cd_able_vars("PROJ/src")` where PROJ=/home/user → "/home/user/src".
#[test]
fn cd_able_vars_appends_tail_after_head_substitution() {
let _g = crate::test_util::global_state_lock();
let saved_opt = opt_state_get("cdablevars").unwrap_or(false);
opt_state_set("cdablevars", true);
opt_state_set("exec", true);
crate::ported::params::unsetparam("zshrs_cdav_PROJ");
setsparam("zshrs_cdav_PROJ", "/home/user");
assert_eq!(
cd_able_vars("zshrs_cdav_PROJ/src"),
Some("/home/user/src".to_string())
);
crate::ported::params::unsetparam("zshrs_cdav_PROJ");
opt_state_set("cdablevars", saved_opt);
}
/// Unknown head var → None (even with option on).
#[test]
fn cd_able_vars_unknown_var_returns_none() {
let _g = crate::test_util::global_state_lock();
let saved_opt = opt_state_get("cdablevars").unwrap_or(false);
opt_state_set("cdablevars", true);
crate::ported::params::unsetparam("zshrs_cdav_doesnt_exist");
assert_eq!(cd_able_vars("zshrs_cdav_doesnt_exist"), None);
opt_state_set("cdablevars", saved_opt);
}
/// Empty head (e.g. "/some/path" starts with `/`) → None.
#[test]
fn cd_able_vars_empty_head_returns_none() {
let _g = crate::test_util::global_state_lock();
let saved_opt = opt_state_get("cdablevars").unwrap_or(false);
opt_state_set("cdablevars", true);
// Leading `/` → head split is empty.
assert_eq!(cd_able_vars("/path/to/foo"), None);
opt_state_set("cdablevars", saved_opt);
}
// ─── zsh-corpus pins for fixed-return builtins ──────────────────
fn empty_opts_for_corpus() -> options {
options { ind: [0u8; MAX_OPS], args: Vec::new(), argscount: 0, argsalloc: 0 }
}
/// `Src/builtin.c:4550` — `bin_true` always returns 0 regardless of
/// argv / opts / func. Matches `:` / `true` semantics.
#[test]
fn builtin_corpus_bin_true_always_zero() {
let _g = crate::test_util::global_state_lock();
let o = empty_opts_for_corpus();
assert_eq!(bin_true("true", &[], &o, 0), 0, "bin_true no args = 0");
assert_eq!(
bin_true("true", &["x".into(), "y".into()], &o, 0),
0,
"bin_true with args = 0",
);
}
/// `Src/builtin.c:4559` — `bin_false` always returns 1.
#[test]
fn builtin_corpus_bin_false_always_one() {
let _g = crate::test_util::global_state_lock();
let o = empty_opts_for_corpus();
assert_eq!(bin_false("false", &[], &o, 0), 1, "bin_false no args = 1");
assert_eq!(
bin_false("false", &["any".into()], &o, 0),
1,
"bin_false with args = 1",
);
}
/// `Src/builtin.c` — `bin_shift` with no positional params and no
/// argv → 0 (zsh's POSIX-conforming "shift past end" semantics
/// when no $@/argv to shift, plus no count argument).
#[test]
fn builtin_corpus_bin_shift_empty_positional_returns_zero_or_one() {
let _g = crate::test_util::global_state_lock();
let o = empty_opts_for_corpus();
// No positional params, no arg → either zero (no-op) or one
// (POSIX error). Both are acceptable per the spec language —
// we pin only that it doesn't panic and returns 0 or 1.
let r = bin_shift("shift", &[], &o, 0);
assert!(r == 0 || r == 1, "shift on empty positional, got {r}");
}
/// `Src/builtin.c` — `bin_let` with no math arg returns 1
/// (no expression to evaluate).
#[test]
fn builtin_corpus_bin_let_no_args_returns_one() {
let _g = crate::test_util::global_state_lock();
let o = empty_opts_for_corpus();
let r = bin_let("let", &[], &o, 0);
assert_eq!(r, 1, "let with no args = 1");
}
/// `bin_let "x=5"` evaluates math and assigns; success returns 0
/// since the result (5) is non-zero. let returns 0 iff last expr
/// was non-zero.
#[test]
fn builtin_corpus_bin_let_nonzero_expr_returns_zero() {
let _g = crate::test_util::global_state_lock();
let o = empty_opts_for_corpus();
crate::ported::params::unsetparam("ZL_X");
let r = bin_let("let", &["ZL_X=5".into()], &o, 0);
assert_eq!(r, 0, "let 'x=5' assigns and returns 0 (nonzero result)");
assert_eq!(crate::ported::params::getiparam("ZL_X"), 5);
crate::ported::params::unsetparam("ZL_X");
}
/// `bin_let "x=0"` returns 1 since last expression evaluates to 0.
#[test]
fn builtin_corpus_bin_let_zero_expr_returns_one() {
let _g = crate::test_util::global_state_lock();
let o = empty_opts_for_corpus();
crate::ported::params::unsetparam("ZL_Y");
let r = bin_let("let", &["ZL_Y=0".into()], &o, 0);
assert_eq!(r, 1, "let 'x=0' returns 1 (zero result)");
assert_eq!(crate::ported::params::getiparam("ZL_Y"), 0);
crate::ported::params::unsetparam("ZL_Y");
}
/// `bin_let` walks multiple expressions, exit status from last.
#[test]
fn builtin_corpus_bin_let_multi_expr_last_wins() {
let _g = crate::test_util::global_state_lock();
let o = empty_opts_for_corpus();
crate::ported::params::unsetparam("ZL_A");
crate::ported::params::unsetparam("ZL_B");
let r = bin_let(
"let",
&["ZL_A=1".into(), "ZL_B=7".into()],
&o,
0,
);
assert_eq!(r, 0, "last expr non-zero → 0");
assert_eq!(crate::ported::params::getiparam("ZL_A"), 1);
assert_eq!(crate::ported::params::getiparam("ZL_B"), 7);
crate::ported::params::unsetparam("ZL_A");
crate::ported::params::unsetparam("ZL_B");
}
/// `bin_pwd` returns 0 on success — even without -P/-L, it should
/// produce the current dir and return zero.
#[test]
fn builtin_corpus_bin_pwd_returns_zero() {
let _g = crate::test_util::global_state_lock();
let o = empty_opts_for_corpus();
let r = bin_pwd("pwd", &[], &o, 0);
assert_eq!(r, 0, "pwd returns 0 on success");
}
}