//! Faithful Rust ports of free functions and file-static globals from
//! `Src/exec.c`. The wordcode-VM dispatch tree (`execlist` / `execpline`
//! / `execcmd` / `execsimple` etc.) that drives execution in C zsh is
//! NOT replicated here — zshrs runs the fusevm bytecode VM instead
//! (see `src/vm_helper.rs` + `src/fusevm_bridge.rs`).
//!
//! What lives here are the parts of `Src/exec.c` that ARE faithful
//! ports and don't depend on the C-side wordcode walker:
//!
//! - **`trap_state` / `trap_return` / `forklevel`** — file-static
//! integer globals from `Src/exec.c:134 / :155 / :1052`, exposed as
//! atomics shared between this module, `Src/signals.c`'s port at
//! `src/ported/signals.rs`, and `Src/params.c`'s port at
//! `src/ported/params.rs`.
//! - **`gethere`** (`Src/exec.c:4573`) — turn a here-document into a
//! here-string. Called from the lexer port (`src/ported/lex.rs`).
//! - **`getoutput`** (`Src/exec.c:4712`) — command-substitution body
//! runner. Called from the parameter-expansion port
//! (`src/ported/subst.rs`).
//! - **`loadautofn`** + **`getfpfunc`** (`Src/exec.c:5050` / `:5260`)
//! — `$fpath` walker + autoload file installer. Called from
//! `bin_autoload` / `bin_functions -c` in `src/ported/builtin.rs`.
//! - **`resolvebuiltin`** (`Src/exec.c:2703`) — module-autoload guard
//! used by the dispatch walk in `execcmd_exec`.
//! - **`execcmd_compile_head`** — fusevm-bytecode-time head resolver
//! mirroring the head section (`c:2904-3275`) of C's `execcmd_exec`.
//! NOT a faithful port; the canonical 7-arg `execcmd_exec` port lives
//! alongside it.
//! - **`execcmd_exec`** (`Src/exec.c:2900`) — canonical 7-arg port of
//! the C function (locals + dispatch walk through builtin/shfunc/external
//! invocation). Used by future tree-walker callers; the fusevm
//! bytecode flow goes through `execcmd_compile_head` instead.
use std::os::unix::fs::PermissionsExt;
use std::sync::atomic::Ordering;
// `with_executor` import removed — all ShellExecutor reach-in calls
// routed through `crate::ported::exec_hooks::*` fn-ptrs installed by
// fusevm_bridge at startup. See memory feedback_no_exec_script_from_ported.
use crate::ported::builtin::{cd_able_vars, fixdir, BUILTINS, DOPRINTDIR, EXIT_VAL, LASTVAL};
use crate::ported::builtins::rlimits::setlimits;
use crate::ported::builtins::sched::zleactive;
use crate::ported::compat::zgettime_monotonic_if_available;
use crate::ported::config_h::DEFAULT_PATH;
use crate::ported::context::{zcontext_restore, zcontext_save};
use crate::ported::hashtable::{cmdnam_unhashed, cmdnamtab_lock, dircache_set, hashdir, pathchecked, shfunctab_lock};
use crate::ported::hist::{strinbeg, strinend};
use crate::ported::init::{shout, underscorelen, underscoreused, zunderscore, SHTTY};
use crate::ported::input::{inpop, inpush};
use crate::ported::jobs::{expandjobtab, get_usage, release_pgrp, waitforpid, JOBTAB, THISJOB};
use crate::ported::lex::{hgetc, parsestr, tok, untokenize, ztokens, LEXERR, LEX_LEXSTOP, LEX_LINENO};
use crate::ported::mem::{dupstring, dyncat, popheap, pushheap};
use crate::ported::modules::clone::mypgrp;
use crate::ported::options::{dosetopt, opt_state_set, sticky};
use crate::ported::params::{endparamscope, getsparam, locallevel, paramtab, setiparam, zgetenv, zputenv};
use crate::ported::parse::{closedumps, ecrawstr, parse_list};
use crate::ported::prompt::{cmdpop, cmdpush};
use crate::ported::signals::{intrap, queue_signals, settrap, sigtrapped, signal_mask, signal_unblock, trapisfunc, traplocallevel, unqueue_signals, unsettrap};
use crate::ported::signals_h::{child_block, child_unblock, dont_queue_signals, signal_default, signal_ignore, winch_unblock, SIGCOUNT};
use crate::ported::subst::{quotesubst, singsub};
use crate::ported::utils::{errflag, fdtable_get, fdtable_set, gettempfile, gettempname, inc_locallevel, movefd, pathprog, printprompt4, quotedzputs, redup, unmeta, unmetafy, write_loop, zclose, zerr, zwarn, ERRFLAG_ERROR, MAX_ZSH_FD};
use crate::ported::ztype_h::{inull, itok};
use crate::ported::zsh_h::{builtin, eprog, execstack, funcwrap, hashnode, multio, redir, shfunc, unset, BINF_BUILTIN, BINF_CLEARENV, BINF_COMMAND, BINF_DASH, BINF_EXEC, BINF_PREFIX, CHASEDOTS, CHASELINKS, CLOBBER, CLOBBEREMPTY, CS_CMDSUBST, Emulation_options, ERRFLAG_INT, FDT_EXTERNAL, FDT_INTERNAL, FDT_PROC_SUBST, FDT_SAVED_MASK, FDT_TYPE_MASK, FDT_UNUSED, FDT_XTRACE, HASHDIRS, INTERACTIVE, INP_LINENO, IS_CLOBBER_REDIR, IS_DASH, Inang, Inpar, JOBTEXTSIZE, MAX_PIPESTATS, Meta, MONITOR, MULTIOS, MULTIOUNIT, Nularg, Outpar, PATHDIRS, PM_LOADDIR, PM_READONLY, PM_UNDEFINED, POSIXBUILTINS, POSIXJOBS, POSIXTRAPS, Pound, REDIRF_FROM_HEREDOC, REDIR_CLOSE, REDIR_HEREDOCDASH, REDIR_HERESTR, REDIR_INPIPE, REDIR_OUTPIPE, USEZLE, VERBOSE, WC_LIST, WC_LIST_TYPE, WC_PIPE, WC_PIPE_END, WC_PIPE_TYPE, WC_REDIR, WC_REDIR_TYPE, WC_REDIR_VARID, WC_SIMPLE, WC_SIMPLE_ARGC, WC_SUBLIST, WC_SUBLIST_END, WC_SUBLIST_FLAGS, WC_SUBLIST_TYPE, WC_TYPESET, ZSIG_FUNC, ZSIG_IGNORED, Z_END, cmdnam, emulation_options, isset, wc_code};
use crate::zsh_h::XTRACE;
use crate::ported::zsh_system_h::timespec as ZshTimespec;
/// Port of the anonymous `enum { ... }` from `Src/exec.c:35-40`.
/// Flag bits passed as the `addflags` argument to `addvars` /
/// `addvarsfromargs`:
/// - `ADDVAR_EXPORT` (1<<0) — export each assignment for the
/// command `VAR=val cmd ...` form.
/// - `ADDVAR_RESTORE` (1<<2) — the variable list is being restored
/// later (implicit local scope), so
/// suppress `ASSPM_WARN`.
pub const ADDVAR_EXPORT: i32 = 1 << 0; // c:37 (Src/exec.c)
pub const ADDVAR_RESTORE: i32 = 1 << 2; // c:39 (Src/exec.c)
/// Port of `int trap_state;` from `Src/exec.c:134`. Tracks whether
/// a trap handler is currently being processed and, paired with
/// `TRAP_RETURN` below, whether a `return` inside the trap should
/// promote to `TRAP_STATE_FORCE_RETURN` to unwind the trap caller.
///
/// Values: `TRAP_STATE_INACTIVE = 0`, `TRAP_STATE_PRIMED = 1`,
/// `TRAP_STATE_FORCE_RETURN = 2` (see `Src/zsh.h`).
pub static TRAP_STATE: std::sync::atomic::AtomicI32 = // c:134 (Src/exec.c)
std::sync::atomic::AtomicI32::new(0);
/// Port of `int trap_return;` from `Src/exec.c:155`. Carries the
/// pending exit status from inside a trap; sentinel `-2` means
/// "running an EXIT/DEBUG-style trap at the current level"
/// (signals.c:1166). Promoted to the user's `return N` value by
/// `bin_return` when POSIX-trap semantics apply (builtin.c:5852).
pub static TRAP_RETURN: std::sync::atomic::AtomicI32 = // c:155 (Src/exec.c)
std::sync::atomic::AtomicI32::new(0);
/// Port of `int forklevel;` from `Src/exec.c:1052`. Records the
/// `locallevel` at the most recent fork point (set at c:1221:
/// `forklevel = locallevel;` inside `entersubsh()`). Used by:
/// - `signals.c:808` SIGPIPE handler — `!forklevel` distinguishes
/// the top-level shell from a forked subshell.
/// - `exec.c:6146` — `if (locallevel > forklevel)` decides whether
/// a function-defined trap should fire on this subshell exit.
/// - `params.c:3724` — WARNCREATEGLOBAL nest-depth check.
///
/// Initialised to 0 (no fork has occurred yet). Set to `locallevel`
/// at every `entersubsh()` entry per c:1221.
pub static FORKLEVEL: std::sync::atomic::AtomicI32 = // c:1052 (Src/exec.c)
std::sync::atomic::AtomicI32::new(0);
// =============================================================================
// File-static globals from Src/exec.c. Bucket choices per PORT_PLAN.md:
// - Per-evaluator transient state → thread_local Cell (bucket 1)
// - Shell-wide shared state → AtomicI32 / Mutex (bucket 2)
// All names match C exactly. Surrounding doc-comments cite the C
// declaration line.
// =============================================================================
/// Port of `int noerrexit;` from `Src/exec.c:72`. Bit-flags that
/// suppress ERREXIT triggering on the next command(s). Bits:
/// `NOERREXIT_EXIT` (in `if`/`while`/`until` test contexts),
/// `NOERREXIT_RETURN` (after `return`), `NOERREXIT_UNTIL_EXEC`
/// (until next exec'd command). Bucket-1 — per-evaluator (each
/// recursive eval has its own suppression frame).
pub static noerrexit: std::sync::atomic::AtomicI32 = // c:72 (Src/exec.c)
std::sync::atomic::AtomicI32::new(0);
/// Port of `int this_noerrexit;` from `Src/exec.c:109`. When set,
/// suppress ERREXIT for THIS one command only (consumed + cleared
/// before the next command starts). Set by `execcursh` and the
/// `((expr))` arith path so a 0-result doesn't trigger errexit.
pub static this_noerrexit: std::sync::atomic::AtomicI32 = // c:109 (Src/exec.c)
std::sync::atomic::AtomicI32::new(0);
/// Port of `mod_export int noerrs;` from `Src/exec.c:117`. When
/// non-zero, suppress `zerr()` output (lex error reporting during
/// `parse_string`, `parseopts` etc.). Saved/restored by
/// `execsave`/`execrestore`.
/// Port of `static char list_pipe_text[JOBTEXTSIZE]` from
/// `Src/exec.c:463`. Holds the textual rendering of the in-flight
/// pipe list; saved across nested execlist invocations at
/// exec.c:1372-1380 (zeroed on entry, restored from
/// `old_list_pipe_text` at c:1634-1638) and round-tripped through
/// execsave/execrestore (c:6448 / c:6484). zshrs models it as a
/// length-bounded String guarded by a Mutex — the C `char[80]` cap
/// is a buffer-overflow guard, but matching length matters for the
/// `jobs` builtin's pipe-list rendering.
pub static LIST_PIPE_TEXT: std::sync::Mutex<String> = std::sync::Mutex::new(String::new()); // c:463 (Src/exec.c)
pub static noerrs: std::sync::atomic::AtomicI32 = // c:117 (Src/exec.c)
std::sync::atomic::AtomicI32::new(0);
/// Port of `int nohistsave;` from `Src/exec.c:122`. When non-zero,
/// `addhistnode` no-ops so trap firings / `eval` invocations don't
/// pollute `$HISTCMD`. Tracked alongside `noerrs` in the trap path.
pub static nohistsave: std::sync::atomic::AtomicI32 = // c:122 (Src/exec.c)
std::sync::atomic::AtomicI32::new(0);
/// Port of `int subsh;` from `Src/exec.c:160`. Subshell depth — bumped
/// every time `entersubsh` forks a sub-shell, used by signal handling
/// (different SIGINT semantics in subshells) and by `${$$}` (`$$`
/// stays at the top-level pid).
pub static subsh: std::sync::atomic::AtomicI32 = // c:160 (Src/exec.c)
std::sync::atomic::AtomicI32::new(0);
/// Port of `mod_export int zsh_subshell;` from `Src/init.c:67`. Visible
/// `$ZSH_SUBSHELL` parameter — incremented by `entersubsh()` each time
/// the shell forks into a subshell (real or fake-exec). Distinct from
/// `subsh` which records whether we ARE a subshell; `zsh_subshell` is
/// the visible depth count.
pub static zsh_subshell: std::sync::atomic::AtomicI32 = // c:67 (Src/init.c)
std::sync::atomic::AtomicI32::new(0);
/// Port of `mod_export volatile int retflag;` from `Src/exec.c:165`.
/// Set by `bin_return` to unwind the function-call stack. Cleared
/// by `runshfunc` on entry, checked by `execlist`'s main loop.
pub static retflag: std::sync::atomic::AtomicI32 = // c:165 (Src/exec.c)
std::sync::atomic::AtomicI32::new(0);
/// Port of `pid_t cmdoutpid;` from `Src/exec.c:215`. Pid of the most
/// recent `$(cmd)` command-substitution child. Used by exit-status
/// propagation: `cmdoutval` carries the exit; `cmdoutpid` carries
/// the pid `waitpid`-d for it.
pub static cmdoutpid: std::sync::atomic::AtomicI32 = // c:215 (Src/exec.c)
std::sync::atomic::AtomicI32::new(0);
/// Port of `mod_export pid_t procsubstpid;` from `Src/exec.c:220`.
/// Pid of the most recent process-substitution child (`<(cmd)` /
/// `>(cmd)`). Tracked separately from `cmdoutpid` because procsubst
/// jobs aren't wait-collected by the parent until the fd is closed.
pub static procsubstpid: std::sync::atomic::AtomicI32 = // c:220 (Src/exec.c)
std::sync::atomic::AtomicI32::new(0);
/// Port of `int cmdoutval;` from `Src/exec.c:225`. Exit status of
/// the most recent `$(cmd)`. Drives `$?` when a varspc-only command
/// runs alongside a substitution.
pub static cmdoutval: std::sync::atomic::AtomicI32 = // c:225 (Src/exec.c)
std::sync::atomic::AtomicI32::new(0);
/// Port of `int use_cmdoutval;` from `Src/exec.c:234`. When set,
/// `lastval` is updated from `cmdoutval` after the command
/// (i.e. the command had substitutions whose exit status matters).
pub static use_cmdoutval: std::sync::atomic::AtomicI32 = // c:234 (Src/exec.c)
std::sync::atomic::AtomicI32::new(0);
/// Port of `mod_export int sfcontext;` from `Src/exec.c:239`. Source
/// context — one of `SFC_NONE`, `SFC_DIRECT` (user typed it),
/// `SFC_SIGNAL` (trap firing), `SFC_HOOK` (precmd/preexec etc.),
/// `SFC_WIDGET` (ZLE widget), `SFC_COMPLETE` (completion fn),
/// `SFC_CFUNC` (compsys fn), `SFC_SUBST` ($(...) cmd-subst),
/// `SFC_EVAL` (eval body). Read by `zerr()` / `funcstack` building.
pub static sfcontext: std::sync::atomic::AtomicI32 = // c:239 (Src/exec.c)
std::sync::atomic::AtomicI32::new(0);
/// Port of `int list_pipe = 0;` from `Src/exec.c:457`. Set when the
/// currently-executing pipeline is the long-running pipe-into-loop
/// shape (`cat foo | while read a; do ... done`) — drives the
/// super/sub-job tracking documented in the famous `Allen Edeln…`
/// comment block above this declaration in C.
pub static list_pipe: std::sync::atomic::AtomicI32 = // c:457 (Src/exec.c)
std::sync::atomic::AtomicI32::new(0);
/// Port of `int simple_pline = 0;` from `Src/exec.c:457`. Set during
/// dispatch of a "simple" pipeline (single-stage / no shell-construct
/// tail) so the `list_pipe` machinery short-circuits.
pub static simple_pline: std::sync::atomic::AtomicI32 = // c:457 (Src/exec.c)
std::sync::atomic::AtomicI32::new(0);
/// Port of `static pid_t list_pipe_pid;` from `Src/exec.c:459`.
/// PID of the sub-shell created to host the loop-after-pipe pattern;
/// passed up the recursive `execlist` stack so the cat-job's super-
/// job entry can record it.
pub static list_pipe_pid: std::sync::atomic::AtomicI32 = // c:459 (Src/exec.c)
std::sync::atomic::AtomicI32::new(0);
/// Port of `static int nowait;` from `Src/exec.c:461`. When set,
/// `execpline` doesn't wait for the pipeline; used during the
/// list_pipe sub-shell fork bookkeeping.
pub static nowait: std::sync::atomic::AtomicI32 = // c:461 (Src/exec.c)
std::sync::atomic::AtomicI32::new(0);
/// Port of `int pline_level = 0;` from `Src/exec.c:461`. Recursive
/// pipeline depth (counts nested pipelines within the current
/// `execlist` call chain).
pub static pline_level: std::sync::atomic::AtomicI32 = // c:461 (Src/exec.c)
std::sync::atomic::AtomicI32::new(0);
/// Port of `static int list_pipe_child = 0;` from `Src/exec.c:462`.
/// Set in the child after the list_pipe fork so the child knows to
/// continue executing the loop body (vs the parent which records
/// the pid + returns).
pub static list_pipe_child: std::sync::atomic::AtomicI32 = // c:462 (Src/exec.c)
std::sync::atomic::AtomicI32::new(0);
/// Port of `static int list_pipe_job;` from `Src/exec.c:462`. Job
/// table index of the pipeline's first-stage job (the `cat` in
/// `cat foo | while ...`).
pub static list_pipe_job: std::sync::atomic::AtomicI32 = // c:462 (Src/exec.c)
std::sync::atomic::AtomicI32::new(0);
/// Port of `static int doneps4;` from `Src/exec.c:262`. Set after
/// `printprompt4` has emitted the `$PS4` prefix for the current
/// xtrace command — prevents double-printing when an inner sub-eval
/// also wants to xtrace.
pub static doneps4: std::sync::atomic::AtomicI32 = // c:262 (Src/exec.c)
std::sync::atomic::AtomicI32::new(0);
/// Port of `static int esprefork, esglob = 1;` from `Src/exec.c:2680`.
///
/// File-static "execsubst parameters" — callers (execcmd_exec at
/// c:3298 / c:3700) set these BEFORE invoking execsubst, which then
/// uses them as the `flags` arg to prefork() and the gate on
/// globlist(). `esprefork` is `PREFORK_TYPESET` for magic-assign /
/// MAGICEQUALSUBST words, else 0. `esglob` defaults to 1; cleared
/// when the dispatched builtin has `BINF_NOGLOB`.
pub static esprefork: std::sync::atomic::AtomicI32 = // c:2680
std::sync::atomic::AtomicI32::new(0);
pub static esglob: std::sync::atomic::AtomicI32 = // c:2680 (= 1)
std::sync::atomic::AtomicI32::new(1);
/// Port of `struct execstack *exstack;` from `Src/exec.c:244`. Head
/// of the linked exec-context save stack — `execsave` pushes a frame
/// before signal-handler / trap dispatch; `execrestore` pops it
/// afterwards so the interrupted command resumes with its state intact.
pub static exstack: std::sync::Mutex<Option<Box<execstack>>> = // c:244
std::sync::Mutex::new(None);
/// Port of `static char *STTYval;` from `Src/exec.c:263`. Pending
/// `stty` argument string captured by `addvars` when the command's
/// inline env contains `STTY=...`. Applied by `execute` before fork
/// + exec so the spawned program sees its tty configured. Reset to
/// `None` after consumption to avoid infinite recursion.
pub static STTYval: std::sync::Mutex<Option<String>> = // c:263 (Src/exec.c)
std::sync::Mutex::new(None);
/// Convert a here-document into a here-string. Line-by-line port of
/// `gethere()` from `Src/exec.c:4569-4652`. Reads the body from the
/// input stream via `hgetc()` until the terminator line is matched,
/// returning the collected body as a string. `strp` is in/out: on
/// entry the raw terminator (possibly with token markers + leading
/// tabs); on return the munged terminator (after `quotesubst` +
/// `untokenize` and, for `REDIR_HEREDOCDASH`, leading-tab strip).
///
/// Returns `None` on out-of-memory (C `zalloc`/`realloc` failure).
/// Rust's `String` auto-grows so the OOM branch is effectively
/// unreachable, but the return type stays `Option<String>` to mirror
/// the C signature which can return NULL.
///
/// Port of `gethere(char **strp, int typ)` from `Src/exec.c:4573`.
pub fn gethere(strp: &mut String, typ: i32) -> Option<String> {
// c:4573 (Src/exec.c)
let mut buf: String; // c:4575 char *buf
let mut bsiz: usize; // c:4576 int bsiz
let mut qt: i32 = 0; // c:4576 int qt = 0
let mut strip: i32 = 0; // c:4576 int strip = 0
// c:4577 — char *s, *t, *bptr, c. zshrs uses byte-offsets into
// `buf` for `t` and tracks `bptr` implicitly as `buf.len()` (the
// C `bptr++` increment is `buf.push(c)`; `bptr--` is `buf.pop()`).
// `s` (the loop iterator for the inull-scan) stays local to its
// for-loop. `c` mirrors the C `char c`.
let mut t: usize; // c:4577 char *t
let mut c: Option<char>; // c:4577 char c
let mut str: String = strp.clone(); // c:4578 char *str = *strp
// c:4580-4584 — for (s = str; *s; s++) if (inull(*s)) { qt = 1; break; }
for s in str.bytes() {
if inull(s) {
// c:4581
qt = 1; // c:4582
break; // c:4583
}
}
str = quotesubst(&str); // c:4585
str = untokenize(&str); // c:4586
if typ == REDIR_HEREDOCDASH {
// c:4587
strip = 1; // c:4588
// c:4589-4590 — while (*str == '\t') str++;
while str.starts_with('\t') {
str.remove(0);
}
}
*strp = str.clone(); // c:4592 *strp = str
// c:4593 — bptr = buf = zalloc(bsiz = 256);
bsiz = 256;
buf = String::with_capacity(bsiz);
let _ = bsiz; // bsiz is tracked by C for zfree; Rust drops automatically
// c:4594 — for (;;)
loop {
t = buf.len(); // c:4595 t = bptr
// c:4597-4598 — while ((c = hgetc()) == '\t' && strip) ;
loop {
c = hgetc();
if !(c == Some('\t') && strip != 0) {
break;
}
}
// c:4599 — for (;;) — inner body-read loop
loop {
// c:4600-4613 — buffer-growth realloc dance. Rust's
// String auto-grows; nothing to do.
// c:4614 — if (lexstop || c == '\n') break;
if LEX_LEXSTOP.with(|f| f.get()) || c == Some('\n') || c.is_none() {
break;
}
// c:4616 — if (!qt && c == '\\')
if qt == 0 && c == Some('\\') {
buf.push('\\'); // c:4617 *bptr++ = c
c = hgetc(); // c:4618
if c == Some('\n') {
// c:4619
buf.pop(); // c:4620 bptr--
c = hgetc(); // c:4621
continue; // c:4622
}
}
if let Some(ch) = c {
// c:4625 *bptr++ = c
buf.push(ch);
}
c = hgetc(); // c:4626
}
// c:4628 — *bptr = '\0'; (implicit — Rust String tracks len)
// c:4629-4630 — if (!strcmp(t, str)) break;
if &buf[t..] == str.as_str() {
break;
}
// c:4631-4634 — if (lexstop) { t = bptr; break; }
if LEX_LEXSTOP.with(|f| f.get()) {
t = buf.len();
break;
}
// c:4635 — *bptr++ = '\n';
buf.push('\n');
}
// c:4637 — *t = '\0';
buf.truncate(t);
// c:4638-4640 — s = buf; buf = dupstring(buf); zfree(s, bsiz);
// The C dance frees the realloc'd block and re-allocates via the
// string-heap allocator. Rust drops the old String when reassigned.
buf = dupstring(&buf);
if qt == 0 {
// c:4641
// c:4642 — int ef = errflag;
let ef = errflag.load(Ordering::Relaxed);
// c:4644 — parsestr(&buf);
if let Ok(parsed) = parsestr(&buf) {
buf = parsed;
}
// c:4646-4649 — if (!(errflag & ERRFLAG_ERROR)) errflag = ef | (errflag & ERRFLAG_INT);
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
let cur = errflag.load(Ordering::Relaxed);
errflag.store(
ef | (cur & ERRFLAG_INT),
Ordering::Relaxed,
);
}
}
Some(buf) // c:4651 return buf
}
/// Port of `LinkList getoutput(char *cmd, int qt)` from
/// `Src/exec.c:4712-4791`. Runs a command-substitution body in the
/// active executor, then routes the captured stdout through
/// `readoutput(pipe, qt, NULL)` semantics at c:4855-4872.
///
/// C return shape: `LinkList` of `char*`. Rust port returns
/// `Vec<String>` (same shape, owned).
///
/// `qt` matches C exactly:
/// - qt=1 (quoted, `"$(...)"`): trim trailing newlines, return
/// entire output as a single-element vec. C c:4858-4862: if
/// output empty, returns a single Nularg sentinel so callers
/// see "empty value" rather than "no value".
/// - qt=0 (unquoted, `$(...)`): trim trailing newlines, then
/// `spacesplit(buf, allownull=false)` per c:4865-4871.
///
/// Uses `with_executor` (panics on missing VM context), not
/// `try_with_executor + unwrap_or_default()`. C `getoutput` calls
/// `execpline` directly — there's no "no shell" code path. The
/// silent-no-op pattern (return empty string when no executor) would
/// mask catastrophic state corruption as "command produced no output",
/// which is the failure mode the `subst.rs:496` warning block flags.
/* $(...) */ // c:4709
pub fn getoutput(cmd: &str, qt: i32) -> Vec<String> {
// c:4713
// c:4715 — `Eprog prog;`
let prog: Option<crate::ported::exec::eprog>;
// c:4716 — `int pipes[2];` (collapsed: in-process executor; no fork)
// c:4717 — `pid_t pid;` (collapsed)
let mut s: String; // c:4718
// c:4720-4723 — `int onc = nocomments; nocomments = (interact &&
// !sourcelevel && unset(INTERACTIVECOMMENTS));
// prog = parse_string(cmd, 0); nocomments = onc;`
let onc = crate::ported::lex::LEX_NOCOMMENTS.with(|c| c.get());
let new_nc = crate::ported::zsh_h::interact()
&& crate::ported::init::sourcelevel.load(std::sync::atomic::Ordering::Relaxed) == 0
&& !crate::ported::zsh_h::isset(crate::ported::zsh_h::INTERACTIVECOMMENTS);
crate::ported::lex::LEX_NOCOMMENTS.with(|c| c.set(new_nc));
prog = parse_string(cmd, 0);
crate::ported::lex::LEX_NOCOMMENTS.with(|c| c.set(onc));
if prog.is_none() { // c:4725
return Vec::new(); // c:4726 return NULL
}
let prog = prog.unwrap();
if !crate::ported::zsh_h::isset(crate::ported::zsh_h::EXECOPT) { // c:4728
return Vec::new(); // c:4729 newlinklist()
}
// c:4731 — `if ((s = simple_redir_name(prog, REDIR_READ)))` — `$(< word)`
if let Some(red_name) = simple_redir_name(&prog, crate::ported::zsh_h::REDIR_READ) {
/* $(< word) */ // c:4732
s = red_name;
s = crate::ported::subst::singsub(&s); // c:4737
if crate::ported::utils::errflag.load(std::sync::atomic::Ordering::Relaxed) != 0 {
return Vec::new(); // c:4739
}
let s = crate::ported::lex::untokenize(&s); // c:4740
let path_meta = crate::ported::utils::unmeta(&s); // c:4741 unmeta(s)
let cpath = match std::ffi::CString::new(path_meta.as_bytes()) {
Ok(c) => c,
Err(_) => return Vec::new(),
};
let stream = unsafe {
libc::open(cpath.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY) // c:4741
};
if stream == -1 {
// c:4742 — `zwarn("%e: %s", errno, s);`
let errno = std::io::Error::last_os_error();
crate::ported::utils::zerr(&format!("{}: {}", errno, s));
crate::ported::builtin::LASTVAL.store(1, std::sync::atomic::Ordering::Relaxed);
crate::ported::exec::cmdoutval.store(1, std::sync::atomic::Ordering::Relaxed);
return Vec::new(); // c:4744
}
// c:4746 — `retval = readoutput(stream, qt, &readerror);`
let mut readerror: i32 = 0;
let retval = readoutput(stream, qt, &mut readerror); // c:4746
if readerror != 0 {
// c:4747
crate::ported::utils::zerr(&format!(
"error when reading {}: {}", // c:4748
s,
std::io::Error::from_raw_os_error(readerror)
));
crate::ported::builtin::LASTVAL.store(1, std::sync::atomic::Ordering::Relaxed);
crate::ported::exec::cmdoutval.store(1, std::sync::atomic::Ordering::Relaxed);
}
return retval; // c:4751
}
// c:4753-4790 — Full fork path: mpipe + zfork + parent
// readoutput / waitforpid / child execode + _realexit. fusevm runs
// command substitution in-process, so the fork shape collapses to a
// synchronous executor call. C control points preserved as cites:
// c:4753 mpipe — handled by ShellExecutor pipe wiring
// c:4758 child_block — no-op (no fork)
// c:4760 zfork — replaced by in-process exec
// c:4768-4776 parent — equivalent to executor return
// c:4778-4789 child — entersubsh+execode+_realexit collapse
crate::ported::exec::cmdoutval.store(0, std::sync::atomic::Ordering::Relaxed); // c:4759
let buf = crate::ported::exec_hooks::run_command_substitution(cmd);
crate::ported::builtin::LASTVAL.store(
crate::ported::exec::cmdoutval.load(std::sync::atomic::Ordering::Relaxed),
std::sync::atomic::Ordering::Relaxed,
); // c:4775
// c:4772 retval = readoutput — post-walk (c:4855-4871 tail) inlined.
let buf = buf.trim_end_matches('\n');
if qt != 0 {
if buf.is_empty() {
vec![String::from(crate::ported::zsh_h::Nularg)] // c:4859-4861
} else {
vec![buf.to_string()] // c:4863
}
} else {
crate::ported::utils::spacesplit(buf, false) // c:4865
}
}
/// Direct port of `Shfunc loadautofn(Shfunc shf, int ks, int test_only,
/// int ignore_loaddir)` from `Src/exec.c:5050`. Walks `$fpath` for a
/// file named `shf->node.nam`, reads it, installs the text body on
/// the corresponding `shfunctab` entry, and clears `PM_UNDEFINED`.
///
/// C body (abridged):
/// 1. `name = shf->node.nam`
/// 2. `getfpfunc(name, &dir_path, NULL, 0)` → resolved file path
/// 3. If !test_only && file found: parse → store eprog on
/// `shf->funcdef`; clear PM_UNDEFINED; set `shf->filename`.
/// 4. Returns shf on success, NULL on failure.
///
/// Rust port: returns 0 = success, 1 = failure (matches the
/// existing call-site convention in `bin_functions -c`). Stores
/// raw file text on `ShFunc.body` (the Rust-side ShFunc in
/// `hashtable.rs:362`); the parser pass that converts text →
/// Eprog runs lazily at first call site.
/// Port of `loadautofn(Shfunc shf, int fksh, int autol, int current_fpath)` from `Src/exec.c:5682`.
pub fn loadautofn(
shf: *mut shfunc, // c:5682 (Src/exec.c)
_ks: i32,
test_only: i32,
_ignore_loaddir: i32,
) -> i32 {
if shf.is_null() {
return 1;
}
// c:5054 — `name = shf->node.nam`.
let name = unsafe { (*shf).node.nam.clone() };
// c:5070 — `path = getfpfunc(name, &dir_path, NULL, 0)`.
let mut dir_path: Option<String> = None;
let path = match getfpfunc(&name, &mut dir_path, None, 0) {
Some(p) => p,
None => return 1, // c:5074 not found
};
if test_only != 0 {
// c:5096
return 0; // test passes — file exists
}
// c:5100-5140 — read the file. C uses zopen + read + parse_string +
// execsave; Rust port stores raw text on the ShFunc and defers
// parse-to-Eprog until the first call.
let body = match std::fs::read_to_string(&path) {
Ok(t) => t,
Err(_) => return 1,
};
// c:5142 — `shf->filename = ztrdup(dir_path)`.
unsafe {
(*shf).filename = dir_path.clone().or(Some(path.clone()));
}
// c:5148 — `shf->node.flags &= ~PM_UNDEFINED`.
unsafe {
(*shf).node.flags &= !(PM_UNDEFINED as i32);
}
// Sync the body string into the Rust-side ShFunc table so the
// lazy-parse path can find it later.
if let Ok(mut tab) = shfunctab_lock().write() {
if let Some(existing) = tab.get_mut(&name) {
existing.body = Some(body);
existing.filename = dir_path;
} else {
tab.add(shfunc {
node: hashnode {
next: None,
nam: name.clone(),
flags: 0,
},
filename: dir_path,
lineno: 0,
funcdef: None,
redir: None,
sticky: None,
body: Some(body),
});
}
}
0
}
/// Port of `getfpfunc(char *s, int *ksh, char **fdir, char **alt_path, int test_only)` from Src/exec.c:5260. Walks `$fpath` (or the
/// supplied `spec_path` slice) for a file named `name` and writes the
/// resolved directory through `*dir_path_out` (matching the C `char **dir_path`).
/// Returns `Some(file_contents_path)` on success, `None` when not found.
pub fn getfpfunc(
name: &str,
dir_path_out: &mut Option<String>, // c:5260 (Src/exec.c)
spec_path: Option<&[String]>,
_all_loaded: i32,
) -> Option<String> {
// C reads $fpath via `getaparam("fpath")` (the param-table array form
// tied to scalar `FPATH` via `typeset -T`). Reading `std::env::var`
// misses any in-script modification like `fpath=(/some/dir $fpath)`
// because that mutates the internal param table, not the inherited
// process env. Fall back to env only when the param table is empty
// (cold start before any param-table init).
let dirs: Vec<String> = match spec_path {
Some(s) => s.to_vec(),
None => crate::ported::params::getaparam("fpath")
.filter(|v| !v.is_empty())
.or_else(|| {
crate::ported::params::getsparam("FPATH")
.map(|v| v.split(':').map(String::from).collect())
})
.or_else(|| {
std::env::var("FPATH")
.ok()
.map(|v| v.split(':').map(String::from).collect())
})
.unwrap_or_default(),
};
for dir in &dirs {
if dir.is_empty() {
continue;
}
let path = format!("{}/{}", dir, name);
if std::path::Path::new(&path).exists() {
*dir_path_out = Some(dir.clone());
return Some(path);
}
}
None
}
/// Port of `resolvebuiltin(const char *cmdarg, HashNode hn)` from
/// `Src/exec.c:2703`. Ensures that an autoload-stub builtin has its
/// module loaded before the caller invokes its `handlerfunc`. If the
/// stub has no handler, `ensurefeature` is asked to load the module
/// and re-lookup the builtin node. C body (abridged):
/// ```c
/// if (!((Builtin) hn)->handlerfunc) {
/// char *modname = dupstring(((Builtin) hn)->optstr);
/// (void)ensurefeature(modname, "b:", ...);
/// hn = builtintab->getnode(builtintab, cmdarg);
/// if (!hn) { lastval=1; zerr(...); return NULL; }
/// }
/// return hn;
/// ```
///
/// WARNING: zshrs's builtin table is the static `BUILTINS` array in
/// `src/ported/builtin.rs`. Module autoload routes through
/// `module::ensurefeature(MODULESTAB, modname, "b:", Some(cmdarg))`;
/// after the module loads the handler should be wired into BUILTINS.
pub fn resolvebuiltin<'a>(
cmdarg: &str, // c:2703 (Src/exec.c)
hn: &'a builtin,
) -> Option<&'a builtin> {
// c:2705 — `if (!((Builtin) hn)->handlerfunc)`.
if hn.handlerfunc.is_none() {
// c:2706 — `modname = dupstring(((Builtin)hn)->optstr)`.
let modname = hn.optstr.clone().unwrap_or_default();
// c:2712 — `ensurefeature(modname, "b:", cmdarg)`.
let _ = {
let mut t = crate::ported::module::MODULESTAB.lock().unwrap();
crate::ported::module::ensurefeature(&mut t, &modname, "b:", Some(cmdarg))
};
// c:2715-2716 — re-lookup the now-(hopefully)-resolved builtin.
if let Some(re) =
crate::ported::builtin::BUILTINS.iter().find(|b| b.node.nam == cmdarg)
{
if re.handlerfunc.is_some() {
return Some(re); // c:2723
}
}
// c:2717-2721 — `lastval = 1; zerr(...)` + return NULL.
zerr(&format!(
"autoloading module {} failed to define builtin: {}",
modname, cmdarg
));
return None; // c:2720
}
Some(hn) // c:2723
}
/// Dispatch decision returned by `execcmd_compile_head` — the
/// fusevm-bytecode-time head resolver that mirrors the local-variable
/// state the C `execcmd_exec` function carries through `c:2913-2916`
/// (`is_builtin`, `is_shfunc`, `cflags`, `use_defpath`) plus the
/// precmd-modifier strip count. The fusevm bytecode compiler reads
/// this to emit the correct dispatch opcode in
/// `src/extensions/compile_zsh.rs::compile_simple`.
///
/// Not a C struct — invented to bridge the divergence between the
/// C wordcode-walker (which mutates locals + falls through to
/// invocation) and zshrs's split parse → compile → VM pipeline.
#[allow(non_camel_case_types)]
#[derive(Debug, Default, Clone)]
pub struct execcmd_dispatch {
/// Number of `BINF_PREFIX` words to strip from the head of args.
/// `Src/exec.c:3086 uremnode(preargs, firstnode(preargs))`.
pub precmd_skip: usize,
/// Set when the head (after strip) is a real builtin
/// (`Src/exec.c:3065 is_builtin = 1`).
pub is_builtin: bool,
/// Set when the head (after strip) is a shell function
/// (`Src/exec.c:3053 is_shfunc = 1`).
pub is_shfunc: bool,
/// `cflags` accumulator from `Src/exec.c:2915` — gathers
/// `BINF_BUILTIN | BINF_COMMAND | BINF_EXEC | BINF_DASH |
/// BINF_NOGLOB` bits encountered during the precommand-modifier
/// walk (c:3062 `cflags |= hn->flags`).
pub cflags: u32,
/// `command -p` requested: use the default `$PATH` for lookup
/// (`Src/exec.c:3160 use_defpath = 1`). NOT YET HONORED by the
/// fusevm compiler — flagged for follow-up.
pub use_defpath: bool,
/// `command -v` / `command -V` requested: the dispatch target
/// flips to `bin_whence` per `Src/exec.c:3149-3157`
/// (`hn = &commandbn.node; is_builtin = 1`). The fusevm compiler
/// reads this and emits `Op::CallBuiltin(BUILTIN_WHENCE_FROM_COMMAND)`
/// instead of resolving the post-strip head.
pub has_command_vv: bool,
/// `exec -a NAME` requested: ARGV0 override per `Src/exec.c:3214-3240`.
/// `Some(NAME)` triggers `zputenv("ARGV0=NAME")` before exec.
pub exec_argv0: Option<String>,
/// Empty-command branch fired with no redirs (`Src/exec.c:3372-3406`
/// — the `else` arm of `if (redir && nonempty(redir))`). Covers
/// bare `exec` / `noglob` / `command`. Caller emits
/// `lastval = cmdoutval` (0 when no `$(cmd)` ran) and returns.
/// Also fires for the `(cflags & BINF_PREFIX) && (cflags &
/// BINF_COMMAND)` sub-case at `c:3365-3371` (bare `command`
/// returns 0 without complaining about missing redirs).
pub is_empty_command: bool,
}
/// !!! NOT A PORT OF C `execcmd_exec` !!!
///
/// This is a fusevm-bytecode-time head resolver invoked by
/// `src/extensions/compile_zsh.rs::compile_simple` and the
/// `command` builtin shim in `src/fusevm_bridge.rs`. The canonical
/// 7-arg port of `Src/exec.c:execcmd_exec` lives elsewhere in this
/// file under the C-faithful name `execcmd_exec`.
///
/// This helper mirrors the head section (`c:2904-3275`) of the C
/// function — local initialisation, the precommand-modifier walk
/// that strips `BINF_PREFIX` builtins (`-`, `builtin`, `command`,
/// `exec`, `noglob`), and the `BINF_COMMAND`/`BINF_EXEC`
/// sub-option parsers — and returns the resulting dispatch
/// decision via `execcmd_dispatch`. The fusevm compiler reads
/// that struct to decide which `Op::CallBuiltin` /
/// `Op::CallFunction` / `Op::Exec` to emit, and to compute the
/// correct post-strip `argc`.
///
/// =================== WARNING — DIVERGENCE ====================
///
/// The C function runs ~1500 lines and PERFORMS dispatch: it sets up
/// `multio` redirections, evaluates `varspc` assignments, then calls
/// `execbuiltin` / `runshfunc` / `execute` directly. This helper
/// stops after the precmd-modifier walk and only returns the head
/// decision; runtime dispatch is driven by the bytecode the fusevm
/// compiler emits.
///
/// Signature adaptation: the C `Estate`/`Execcmd_params` carry the
/// wordcode iterator state — zshrs doesn't traverse wordcode here,
/// so the args list arrives already-expanded as a `&[String]`
/// (analog of `preargs` after `execcmd_getargs` at `c:3028`).
/// `type_` mirrors `eparams->type` (`WC_SIMPLE` vs `WC_TYPESET`).
///
/// =============================================================
pub fn execcmd_compile_head(args: &[String], type_: u32) -> execcmd_dispatch {
// c:2900 (Src/exec.c)
// c:2904-2916 — locals.
let mut hn: Option<&'static builtin> = None; // c:2904
let mut is_shfunc = false; // c:2913
let mut is_builtin = false; // c:2913
let mut use_defpath = false; // c:2913
let mut cflags: u32 = 0; // c:2915
let mut orig_cflags: u32 = 0; // c:2915
let _ = orig_cflags;
// c:3263 — `char *exec_argv0 = NULL;` (declared inside the
// BINF_EXEC arm; hoisted here so the dispatch struct can carry it
// out after the loop terminates).
let mut exec_argv0: Option<String> = None;
// c:3149/3158 — `has_vV`/`has_p` flags from the BINF_COMMAND arm
// (c:3104). Surface `has_vV` via the dispatch struct so the fusevm
// compiler can emit `bin_whence` instead of resolving the head.
let mut has_command_vv = false;
// c:2962-2973 — `%job` head: rewrite `%name` → `fg|bg|disown %name`.
// Not in scope for the compile-time dispatch walk: jobspec
// expansion happens at runtime in fusevm; the bytecode emits a
// direct `fg`/`bg` call when it sees a leading `%`. Flagged for
// follow-up when the canonical port lands.
// c:2975-2986 — AUTORESUME prefix-match against jobtab. Same
// status as the %job head: runtime concern, deferred.
// c:3013-3091 — precommand-modifier walk.
let mut preargs: Vec<String> = args.to_vec(); // c:3027 newlinklist
let mut precmd_skip: usize = 0;
// c:3018 — `if ((type == WC_SIMPLE || type == WC_TYPESET) && args)`.
if (type_ == WC_SIMPLE || type_ == WC_TYPESET) && !preargs.is_empty() {
// c:3018
// c:3029 — `while (nonempty(preargs))`.
while precmd_skip < preargs.len() {
// c:3029
// c:3030 — `cmdarg = (char *) peekfirst(preargs);`.
let cmdarg = untokenize(&preargs[precmd_skip]);
// c:3031 — `checked = !has_token(cmdarg)`. zshrs's fusevm
// already performed prefork expansion on `preargs`, so
// `has_token` is effectively false here; the C `break` on
// unexpanded tokens is unreachable in this entry point.
// c:3034-3035 — WC_TYPESET fast path: `getnode2` looks up
// even disabled builtins so the reserved-word form
// (`integer x`, `local foo`) still dispatches to the
// typeset family. The static `BUILTINS` array doesn't
// expose a separate disabled-bit lookup; one path covers
// both. Effect is identical for the precmd-modifier walk.
// c:3050-3052 — `if (!(cflags & (BINF_BUILTIN |
// BINF_COMMAND)) && shfunctab->getnode(...))` — shell
// function takes precedence unless a `builtin`/`command`
// modifier preceded it.
if (cflags & (BINF_BUILTIN | BINF_COMMAND)) == 0 {
// c:3051
if shfunctab_lock()
.read()
.map(|t| t.iter().any(|(k, _)| k == &cmdarg))
.unwrap_or(false)
{
is_shfunc = true; // c:3053
break; // c:3054
}
}
// c:3056 — `builtintab->getnode(builtintab, cmdarg)`.
let entry = BUILTINS
.iter()
.find(|b| b.node.nam == cmdarg);
let Some(entry) = entry else {
// c:3056-3058
break;
};
hn = Some(entry);
// c:3061-3063 — accumulate cflags.
orig_cflags |= cflags;
cflags &= !(BINF_BUILTIN | BINF_COMMAND);
cflags |= entry.node.flags as u32;
// c:3064 — `if (!(hn->flags & BINF_PREFIX))` — real
// builtin, stop.
if (entry.node.flags as u32 & BINF_PREFIX) == 0 {
// c:3064
// WARNING — DIVERGENCE: c:3068 calls `resolvebuiltin`
// to autoload the builtin's module if its
// `handlerfunc` is NULL. In zshrs, builtins live in
// two places: the static `BUILTINS` table (which
// mirrors C `handlerfunc`, often `None` for ports
// dispatched through fusevm) AND fusevm's
// `register_builtins` map (the actual runtime
// dispatcher). A null `handlerfunc` in the static
// table is NOT an autoload failure for us — it
// means dispatch routes through fusevm. So we
// skip the resolvebuiltin call here; the faithful
// port remains available for future callers that
// genuinely need module-autoload semantics.
is_builtin = true; // c:3065
break; // c:3077
}
// c:3086 — `uremnode(preargs, firstnode(preargs))`.
precmd_skip += 1;
// c:3087-3091 — `if (!firstnode(preargs)) { execcmd_getargs
// (...); if (!firstnode(preargs)) break; }`. zshrs has
// no `execcmd_getargs` (args arrive pre-expanded); the
// bounds-check at the top of `while precmd_skip <
// preargs.len()` handles the empty case identically.
// c:3092-3177 — BINF_COMMAND sub-option parsing
// (`command -p / -v / -V`).
if (cflags & BINF_COMMAND) != 0 && precmd_skip < preargs.len() {
// c:3102-3104 — `LinkNode argnode, oldnode, pnode = NULL;
// int has_p = 0, has_vV = 0, has_other = 0;`
let mut argnode: usize = precmd_skip; // c:3105 `argnode = firstnode(preargs);`
let mut pnode: Option<usize> = None; // c:3102
let mut has_p = false; // c:3104
let mut has_vv = false; // c:3104
let mut has_other = false; // c:3104
// c:3107 — `while (IS_DASH(*argdata))`
while argnode < preargs.len()
&& IS_DASH(preargs[argnode].chars().next().unwrap_or('\0'))
{
let argdata = preargs[argnode].clone(); // c:3106
let bytes = argdata.as_bytes();
// c:3108-3111 — stop on bare `-` or `--`.
if bytes.len() < 2 || (IS_DASH(bytes[1] as char) && bytes.len() == 2) {
// c:3109
break; // c:3111
}
// c:3112-3133 — scan flag chars.
for &c in &bytes[1..] {
// c:3112
match c as char {
'p' => {
// c:3114
has_p = true; // c:3122
pnode = Some(argnode); // c:3123
}
'v' | 'V' => {
// c:3125-3126
has_vv = true; // c:3127
}
_ => {
// c:3129
has_other = true; // c:3130
}
}
}
// c:3134-3138 — unknown flag → don't try, leave alone.
if has_other {
// c:3134
has_p = false; // c:3136
has_vv = false; // c:3136
break; // c:3137
}
// c:3140-3147 — advance to next arg.
argnode += 1; // c:3141 nextnode(argnode)
if argnode >= preargs.len() {
// c:3142 — execcmd_getargs (skipped: pre-expanded)
break; // c:3145
}
}
// c:3149-3157 — `-v`/`-V` → dispatch to whence.
if has_vv {
// c:3149
// c:3154 `pushnode(preargs, "command")` — C re-inserts
// "command" so bin_whence sees it as argv[0]. zshrs
// surfaces this via `has_command_vv`; the fusevm
// compiler emits the equivalent whence call.
has_command_vv = true; // c:3155-3156 hn = &commandbn; is_builtin=1
is_builtin = true;
break; // c:3157
} else if has_p {
// c:3158
use_defpath = true; // c:3160
if let Some(pn) = pnode {
// c:3165 — `uremnode(preargs, pnode)`. zshrs:
// remove the `-p`-bearing arg from preargs.
if pn < preargs.len() {
preargs.remove(pn);
// precmd_skip already accounts for the
// stripped `command` prefix; we just removed
// the `-p` flag which sat at preargs[pn].
// No precmd_skip change needed — the head
// remains where it was.
}
}
}
// c:3176-3177 — `--` trailing end-of-options strip.
if argnode < preargs.len() {
let argdata = &preargs[argnode];
let b = argdata.as_bytes();
if b.len() == 2 && IS_DASH(b[0] as char) && IS_DASH(b[1] as char) {
// c:3176
preargs.remove(argnode); // c:3177
}
}
} else if (cflags & BINF_EXEC) != 0 && precmd_skip < preargs.len() {
// c:3178-3275 — BINF_EXEC sub-option parsing
// (`exec -a NAME -l -c`).
let mut argnode: usize = precmd_skip; // c:3185
let mut error_done = false;
// c:3196 — `while (argdata && IS_DASH(*argdata) &&
// strlen(argdata) >= 2)`
while argnode < preargs.len() {
let argdata = preargs[argnode].clone();
let bytes = argdata.as_bytes();
if bytes.is_empty() || !IS_DASH(bytes[0] as char) || bytes.len() < 2 {
break; // c:3196 loop guard
}
let oldnode = argnode; // c:3197
argnode += 1; // c:3198 nextnode(oldnode)
// c:3203-3208 — empty next → error.
if argnode >= preargs.len() {
// c:3203
zerr(
// c:3204
"exec requires a command to execute",
);
errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3206
error_done = true;
break; // c:3207 goto done
}
// c:3209 — `uremnode(preargs, oldnode)`.
preargs.remove(oldnode);
argnode -= 1; // re-anchor — `argnode` was the post-removed slot
// c:3210-3211 — `--` stops option scan.
if bytes.len() == 2 && IS_DASH(bytes[0] as char) && IS_DASH(bytes[1] as char) {
// c:3210
break; // c:3211
}
// c:3212-3258 — scan flag chars after the leading `-`.
let mut k = 1usize;
while k < bytes.len() && !error_done {
let cmdopt = bytes[k] as char; // c:3212
match cmdopt {
'a' => {
// c:3214 — `-a` ARGV0 override.
if k + 1 < bytes.len() {
// c:3216 — `-aNAME` inline form.
exec_argv0 =
Some(String::from_utf8_lossy(&bytes[k + 1..]).into_owned()); // c:3217
k = bytes.len(); // c:3219 position past end
} else {
// c:3220 — `-a NAME` separate form.
if argnode >= preargs.len() {
// c:3230
zerr(
// c:3231
"exec flag -a requires a parameter",
);
errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3233
error_done = true;
break; // c:3234 goto done
}
exec_argv0 = Some(preargs[argnode].clone()); // c:3236
preargs.remove(argnode); // c:3239
}
}
'c' => {
// c:3242
cflags |= BINF_CLEARENV; // c:3243
}
'l' => {
// c:3245
cflags |= BINF_DASH; // c:3246
}
_ => {
// c:3248
zerr(
// c:3249
&format!("unknown exec flag -{}", cmdopt),
);
errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3251
error_done = true;
break; // c:3256
}
}
k += 1;
}
if error_done {
break;
}
}
// c:3263-3274 — zputenv("ARGV0=NAME"). zshrs defers
// the actual `setenv` to the fusevm compiler / external
// exec path; we surface `exec_argv0` via the dispatch
// struct so the caller can apply it before fork+exec.
if let Some(ref a0) = exec_argv0 {
// c:3263 — `remnulargs + untokenize` then setenv.
let cleaned = untokenize(a0); // c:3266-3267
exec_argv0 = Some(cleaned);
}
if error_done {
return execcmd_dispatch {
precmd_skip,
is_builtin,
is_shfunc,
cflags,
use_defpath,
has_command_vv,
exec_argv0,
is_empty_command: false,
};
}
}
// c:3275-3278 — `hn = NULL; if ((cflags & BINF_COMMAND) &&
// unset(POSIXBUILTINS)) break;`. After processing a
// `command` precmd modifier (and its -p/-v/-V flags), the
// C loop exits with hn cleared so the dispatch falls
// through to external lookup. Without this, the next
// iteration would find `command print` → print's builtin
// and dispatch to it; zsh's intentional behaviour is to
// skip builtins under `command` (unless POSIXBUILTINS is
// set, where the loop continues normally).
if (cflags & BINF_COMMAND) != 0 && !isset(POSIXBUILTINS) {
hn = None; // c:3275 hn = NULL
break; // c:3277
}
}
}
// c:3309-3406 — "Empty command" branch. When the precmd-modifier
// walk above strips every word with nothing left to dispatch
// (bare `exec`, bare `noglob`, bare `command`, bare `nocorrect`),
// C falls into `if (!args || empty(args))` at c:3331. Sub-cases:
//
// - redir-present + do_exec → nullexec=1 (continue to run)
// - redir-present + varspc → nullexec=2 (continue)
// - redir-present + no nullcmd → `zerr("redirection with no command")`
// lastval=1, return
// - redir-present + SHNULLCMD → args=[":"]
// - redir-present + readnullcmd → args=[readnullcmd]
// - redir-present + default → args=[nullcmd]
// - NO redir + BINF_PREFIX+COMMAND → lastval=0, return (c:3365-3371)
// - NO redir + default → lastval=cmdoutval, return (c:3372-3406)
//
// zshrs's `execcmd_compile_head` doesn't receive `redir` (it
// takes `args` only). The cases that DEPEND on redirs are handled by
// `compile_zsh.rs::compile_redir` before this dispatch fires; the
// remaining cases collapse into the single `is_empty_command`
// flag below. Both NO-redir sub-cases produce the same observable
// outcome (lastval=0, return without invoking anything), so a
// single flag suffices.
let is_empty_command = precmd_skip >= preargs.len();
// =================== WARNING — DIVERGENCE ====================
// c:3285+: prefork-substitution, magic_assign decision, multio
// setup, varspc evaluation, and the actual execbuiltin /
// runshfunc / execute call. ~1300 lines of interpreter-only
// code, entirely replaced by fusevm bytecode dispatch in
// `src/extensions/compile_zsh.rs::compile_simple` and the
// opcode handlers in `src/fusevm_bridge.rs::register_builtins`.
// The return value below feeds those compile-time decisions.
// =============================================================
let _ = hn;
execcmd_dispatch {
precmd_skip,
is_builtin,
is_shfunc,
cflags,
use_defpath,
has_command_vv,
exec_argv0,
is_empty_command,
}
}
// =============================================================================
// Leaf-function ports — c:283 (parse_string) and below. Added incrementally to
// chip at the ~5500 lines of exec.c still un-ported beyond the wordcode
// walker (execlist / execpline / execcmd which the fusevm bytecode VM
// replaces — see the WARNING block in execcmd_exec).
// =============================================================================
/// Port of `parse_string(char *s, int reset_lineno)` from `Src/exec.c:283`.
///
/// C body:
/// ```c
/// Eprog p; zlong oldlineno;
/// zcontext_save();
/// inpush(s, INP_LINENO, NULL);
/// strinbeg(0);
/// oldlineno = lineno;
/// if (reset_lineno) lineno = 1;
/// p = parse_list();
/// lineno = oldlineno;
/// if (tok == LEXERR && !lastval) lastval = 1;
/// strinend();
/// inpop();
/// zcontext_restore();
/// return p;
/// ```
///
/// Parses an arbitrary string as a zsh command list, returning the
/// `Eprog` (compiled wordcode). Used by `getoutput` for `$(cmd)`,
/// `bin_eval` for `eval`, and the autoload path.
pub fn parse_string(s: &str, reset_lineno: i32) -> Option<eprog> {
// c:285-286
let p: Option<eprog>;
let oldlineno: i64;
zcontext_save(); // c:288
inpush(s, INP_LINENO, None); // c:289
strinbeg(0); // c:290
oldlineno = LEX_LINENO.get() as i64; // c:291
if reset_lineno != 0 {
// c:292
LEX_LINENO.set(1); // c:293
}
p = parse_list(); // c:294
LEX_LINENO.set(oldlineno as u64); // c:295
// c:296-297 — `if (tok == LEXERR && !lastval) lastval = 1;`
if tok() == LEXERR
&& LASTVAL.load(Ordering::Relaxed) == 0
{
LASTVAL.store(1, Ordering::Relaxed);
}
strinend(); // c:298
inpop(); // c:299
zcontext_restore(); // c:300
p // c:301
}
/// Port of `int isgooderr(int e, char *dir)` from `Src/exec.c:652`.
///
/// C body:
/// ```c
/// /* Maybe the directory was unreadable, or maybe it wasn't even a directory. */
/// return ((e != EACCES || !access(dir, X_OK)) &&
/// e != ENOENT && e != ENOTDIR);
/// ```
///
/// errno classifier for `execve` failures during PATH search: if the
/// errno is EACCES (and the dir is X-accessible) or ENOENT/ENOTDIR,
/// it's "expected" (try next PATH entry); otherwise it's a real
/// failure worth surfacing.
pub fn isgooderr(e: i32, dir: &str) -> bool {
// c:652
let dir_x_ok = std::path::Path::new(&unmeta(dir))
.metadata()
.map(|m| m.permissions().mode() & 0o111 != 0)
.unwrap_or(false);
// c:658-659 — `(e != EACCES || !access(dir, X_OK)) && e != ENOENT && e != ENOTDIR`
(e != libc::EACCES || !dir_x_ok) && e != libc::ENOENT && e != libc::ENOTDIR
}
/// Port of `int iscom(char *s)` from `Src/exec.c:962`.
///
/// C body:
/// ```c
/// struct stat statbuf;
/// char *us = unmeta(s);
/// return (access(us, X_OK) == 0 && stat(us, &statbuf) >= 0 &&
/// S_ISREG(statbuf.st_mode));
/// ```
///
/// True iff `s` names an executable regular file (X-perm + S_IFREG).
/// Used by the PATH-search loop in `findcmd` / `search_defpath` to
/// validate candidate paths before exec.
pub fn iscom(s: &str) -> bool {
// c:962
let us = unmeta(s); // c:965
// c:967-968 — `access(us, X_OK) == 0 && stat(us, &statbuf) >= 0 && S_ISREG(...)`
let cstr = match std::ffi::CString::new(us.as_str()) {
Ok(c) => c,
Err(_) => return false,
};
let x_ok = unsafe { libc::access(cstr.as_ptr(), libc::X_OK) } == 0;
if !x_ok {
return false;
}
let meta = match std::fs::metadata(&us) {
Ok(m) => m,
Err(_) => return false,
};
meta.file_type().is_file()
}
/// Port of `int isreallycom(Cmdnam cn)` from `Src/exec.c:972-987`.
///
/// Verify that a hashed/cached cmdnamtab entry still names a real
/// external command (X-perm + regular file). For HASHED entries
/// (`cn->u.cmd` carries the absolute path), test the path directly;
/// otherwise concatenate `name[0] + "/" + nam` and test that.
/// Used by `execcmd_exec` to drop stale cmdnamtab hits before they
/// turn into a failed `execve` syscall.
pub fn isreallycom(cn: &cmdnam) -> bool {
// c:972
let fullnam: String;
if (cn.node.flags & crate::ported::zsh_h::HASHED) != 0 {
// c:977-978 — `strcpy(fullnam, cn->u.cmd);`
fullnam = cn.cmd.clone().unwrap_or_default();
} else if cn.name.is_none() || cn.name.as_ref().unwrap().is_empty() {
// c:979-980 — `if (!cn->u.name) return 0;`
return false;
} else {
// c:982-984 — `strcpy + strcat("/") + strcat(nam)`
let path0 = &cn.name.as_ref().unwrap()[0];
fullnam = format!("{}/{}", path0, cn.node.nam);
}
iscom(&fullnam) // c:986
}
/// Port of `int isrelative(char *s)` from `Src/exec.c:996`.
///
/// C body:
/// ```c
/// if (*s != '/') return 1;
/// for (; *s; s++)
/// if (*s == '.' && s[-1] == '/' &&
/// (s[1] == '/' || s[1] == '\0' ||
/// (s[1] == '.' && (s[2] == '/' || s[2] == '\0'))))
/// return 1;
/// return 0;
/// ```
///
/// True iff `s` either doesn't start with `/` OR contains a `./` or
/// `../` component anywhere. Used by `cd` resolution and PATH-cache
/// invalidation to detect non-canonical paths.
pub fn isrelative(s: &str) -> i32 {
// c:996
let bytes = s.as_bytes();
if bytes.is_empty() || bytes[0] != b'/' {
// c:998
return 1; // c:999
}
// c:1000-1004 — walk for `./` or `../` components.
for i in 1..bytes.len() {
let c = bytes[i];
let prev = bytes[i - 1];
if c == b'.' && prev == b'/' {
let next = bytes.get(i + 1).copied().unwrap_or(0);
if next == b'/' || next == 0 {
// c:1002
return 1;
}
if next == b'.' {
let next2 = bytes.get(i + 2).copied().unwrap_or(0);
if next2 == b'/' || next2 == 0 {
// c:1003
return 1;
}
}
}
}
0 // c:1005
}
/// Port of `void setunderscore(char *str)` from `Src/exec.c:2652`.
///
/// C body:
/// ```c
/// queue_signals();
/// if (str && *str) {
/// size_t l = strlen(str) + 1, nl = (l + 31) & ~31;
/// if (nl > underscorelen || (underscorelen - nl) > 64) {
/// zfree(zunderscore, underscorelen);
/// zunderscore = (char *) zalloc(underscorelen = nl);
/// }
/// strcpy(zunderscore, str);
/// underscoreused = l;
/// } else {
/// ... reset zunderscore = "" ...
/// }
/// unqueue_signals();
/// ```
///
/// Sets the `$_` global to the last argument of the most recent
/// command. Called from `execcmd_exec` (c:3936) per `last_status`
/// update; mirrored in zshrs by the fusevm `Op::Exec` handler.
pub fn setunderscore(str: &str) {
// c:2652
queue_signals(); // c:2654
if !str.is_empty() {
// c:2655 `if (str && *str)`
// c:2656-2663 — copy str into zunderscore; track byte length in underscoreused.
let mut zu = zunderscore.lock().unwrap();
*zu = str.to_string();
let nl = (str.len() + 1 + 31) & !31; // c:2656
underscorelen.store(nl, Ordering::Relaxed); // c:2660
underscoreused.store((str.len() + 1) as i32, Ordering::Relaxed);
// c:2663
} else {
// c:2664
let mut zu = zunderscore.lock().unwrap();
zu.clear(); // c:2669 `*zunderscore = '\0';`
underscoreused.store(1, Ordering::Relaxed); // c:2670
}
unqueue_signals(); // c:2672
}
/// Port of `int mpipe(int *pp)` from `Src/exec.c:5160`.
///
/// C body:
/// ```c
/// if (pipe(pp) < 0) {
/// zerr("pipe failed: %e", errno);
/// return -1;
/// }
/// pp[0] = movefd(pp[0]);
/// pp[1] = movefd(pp[1]);
/// return 0;
/// ```
///
/// libc `pipe(2)` wrapper that pushes both ends out of the reserved-
/// fd range via `movefd`. Used by `getpipe` / `getproc` /
/// `spawnpipes` for process substitution and pipeline wiring.
pub fn mpipe(pp: &mut [i32; 2]) -> i32 {
// c:5160
let mut fds: [libc::c_int; 2] = [-1; 2];
if unsafe { libc::pipe(fds.as_mut_ptr()) } < 0 {
// c:5162
zerr(&format!(
// c:5163
"pipe failed: {}",
std::io::Error::last_os_error()
));
return -1; // c:5164
}
pp[0] = movefd(fds[0]); // c:5166
pp[1] = movefd(fds[1]); // c:5167
0 // c:5168
}
/// Port of `static const char *const ANONYMOUS_FUNCTION_NAME = "(anon)";`
/// from `Src/exec.c:5289`. Anonymous-function name marker used by
/// `is_anonymous_function_name`, `execfuncdef`, and `doshfunc` for
/// `() { ... }` anonymous function dispatch.
pub const ANONYMOUS_FUNCTION_NAME: &str = "(anon)";
/// Port of `int is_anonymous_function_name(const char *name)` from
/// `Src/exec.c:5300`.
///
/// C body:
/// ```c
/// return !strcmp(name, ANONYMOUS_FUNCTION_NAME);
/// ```
///
/// True iff the name equals the `"(anon)"` sentinel. Used by zprof
/// reporting and `whence -v` to skip / annotate anonymous functions.
pub fn is_anonymous_function_name(name: &str) -> i32 {
// c:5300
if name == ANONYMOUS_FUNCTION_NAME {
// c:5302
1
} else {
0
}
}
/// Port of `void execsave(void)` from `Src/exec.c:6438`.
///
/// C body:
/// ```c
/// struct execstack *es = (struct execstack *) zalloc(sizeof(struct execstack));
/// es->list_pipe_pid = list_pipe_pid;
/// es->nowait = nowait;
/// es->pline_level = pline_level;
/// es->list_pipe_child = list_pipe_child;
/// es->list_pipe_job = list_pipe_job;
/// strcpy(es->list_pipe_text, list_pipe_text);
/// es->lastval = lastval;
/// es->noeval = noeval;
/// es->badcshglob = badcshglob;
/// es->cmdoutpid = cmdoutpid;
/// es->cmdoutval = cmdoutval;
/// es->use_cmdoutval = use_cmdoutval;
/// es->procsubstpid = procsubstpid;
/// es->trap_return = trap_return;
/// es->trap_state = trap_state;
/// es->trapisfunc = trapisfunc;
/// es->traplocallevel = traplocallevel;
/// es->noerrs = noerrs;
/// es->this_noerrexit = this_noerrexit;
/// es->underscore = ztrdup(zunderscore);
/// es->next = exstack;
/// exstack = es;
/// noerrs = cmdoutpid = 0;
/// ```
///
/// Snapshot every transient exec-context global onto the `exstack`
/// linked list so a signal-handler / trap-firing nested eval can
/// scribble freely; `execrestore` pops the frame back. Called by
/// `dotrap` (signals.c) and the trap-firing entry in `execlist`.
pub fn execsave() {
// c:6438
// c:6442 — `es = zalloc(sizeof(execstack));`
let mut es = Box::new(execstack {
// c:6442
next: None,
list_pipe_pid: list_pipe_pid.load(Ordering::Relaxed), // c:6443
nowait: nowait.load(Ordering::Relaxed), // c:6444
pline_level: pline_level.load(Ordering::Relaxed), // c:6445
list_pipe_child: list_pipe_child.load(Ordering::Relaxed), // c:6446
list_pipe_job: list_pipe_job.load(Ordering::Relaxed), // c:6447
list_pipe_text: {
// c:6448 — `strcpy(es->list_pipe_text, list_pipe_text);`
let mut buf = [0u8; JOBTEXTSIZE];
if let Ok(s) = LIST_PIPE_TEXT.lock() {
let bytes = s.as_bytes();
let n = bytes.len().min(JOBTEXTSIZE - 1);
buf[..n].copy_from_slice(&bytes[..n]);
}
buf
},
lastval: LASTVAL.load(Ordering::Relaxed), // c:6449
// c:6450 — `es->noeval = noeval;`. Snapshot math.c's
// `int noeval` (the parse-only side-effect-skip counter)
// via math.rs's pub accessor.
noeval: crate::ported::math::m_noeval(),
// c:6451 — `es->badcshglob = badcshglob;`. Snapshot the
// csh-glob diagnostic counter (glob.c:103 / glob.rs
// BADCSHGLOB) so nested eval / trap dispatch doesn't disturb
// the outer command's per-line accounting.
badcshglob: crate::ported::glob::BADCSHGLOB
.load(std::sync::atomic::Ordering::Relaxed), // c:6451
cmdoutpid: cmdoutpid.load(Ordering::Relaxed), // c:6452
cmdoutval: cmdoutval.load(Ordering::Relaxed), // c:6453
use_cmdoutval: use_cmdoutval.load(Ordering::Relaxed), // c:6454
procsubstpid: procsubstpid.load(Ordering::Relaxed), // c:6455
trap_return: TRAP_RETURN.load(Ordering::Relaxed), // c:6456
trap_state: TRAP_STATE.load(Ordering::Relaxed), // c:6457
trapisfunc: trapisfunc.load(Ordering::Relaxed), // c:6458
traplocallevel: traplocallevel.load(Ordering::Relaxed), // c:6459
noerrs: noerrs.load(Ordering::Relaxed), // c:6460
this_noerrexit: this_noerrexit.load(Ordering::Relaxed), // c:6461
// c:6462 — `es->underscore = ztrdup(zunderscore);`
underscore: Some(zunderscore.lock().unwrap().clone()),
});
// c:6463-6464 — `es->next = exstack; exstack = es;`
let mut head = exstack.lock().unwrap();
es.next = head.take();
*head = Some(es);
// c:6465 — `noerrs = cmdoutpid = 0;`
noerrs.store(0, Ordering::Relaxed);
cmdoutpid.store(0, Ordering::Relaxed);
}
/// Port of `void execrestore(void)` from `Src/exec.c:6470`.
///
/// C body:
/// ```c
/// struct execstack *en = exstack;
/// DPUTS(!exstack, "BUG: execrestore() without execsave()");
/// queue_signals();
/// exstack = exstack->next;
/// list_pipe_pid = en->list_pipe_pid;
/// nowait = en->nowait;
/// pline_level = en->pline_level;
/// list_pipe_child = en->list_pipe_child;
/// list_pipe_job = en->list_pipe_job;
/// strcpy(list_pipe_text, en->list_pipe_text);
/// lastval = en->lastval;
/// noeval = en->noeval;
/// badcshglob = en->badcshglob;
/// cmdoutpid = en->cmdoutpid;
/// cmdoutval = en->cmdoutval;
/// use_cmdoutval = en->use_cmdoutval;
/// procsubstpid = en->procsubstpid;
/// trap_return = en->trap_return;
/// trap_state = en->trap_state;
/// trapisfunc = en->trapisfunc;
/// traplocallevel = en->traplocallevel;
/// noerrs = en->noerrs;
/// this_noerrexit = en->this_noerrexit;
/// setunderscore(en->underscore);
/// zsfree(en->underscore);
/// free(en);
/// unqueue_signals();
/// ```
///
/// Pop the top `execstack` frame and restore every transient
/// exec-context global. Inverse of `execsave`.
pub fn execrestore() {
// c:6470
let mut head = exstack.lock().unwrap();
let en = match head.take() {
// c:6472 + c:6477
Some(en) => en,
None => {
// c:6474 — DPUTS(!exstack, "BUG: execrestore() without execsave()")
crate::DPUTS!(true, "BUG: execrestore() without execsave()");
return;
}
};
queue_signals(); // c:6476
*head = en.next; // c:6477
drop(head); // release lock before scalar restores
list_pipe_pid.store(en.list_pipe_pid, Ordering::Relaxed); // c:6479
nowait.store(en.nowait, Ordering::Relaxed); // c:6480
pline_level.store(en.pline_level, Ordering::Relaxed); // c:6481
list_pipe_child.store(en.list_pipe_child, Ordering::Relaxed); // c:6482
list_pipe_job.store(en.list_pipe_job, Ordering::Relaxed); // c:6483
// c:6484 — `strcpy(list_pipe_text, en->list_pipe_text);`.
if let Ok(mut s) = LIST_PIPE_TEXT.lock() {
let nul = en
.list_pipe_text
.iter()
.position(|&b| b == 0)
.unwrap_or(JOBTEXTSIZE);
*s = String::from_utf8_lossy(&en.list_pipe_text[..nul]).into_owned();
}
LASTVAL.store(en.lastval, Ordering::Relaxed); // c:6485
// c:6486 — `noeval = en->noeval;`. Restore math.c's noeval
// counter from the saved frame.
crate::ported::math::m_noeval_set(en.noeval);
// c:6487 — `badcshglob = en->badcshglob;`. Restore the csh-glob
// diagnostic counter saved on entry.
crate::ported::glob::BADCSHGLOB
.store(en.badcshglob, std::sync::atomic::Ordering::Relaxed);
cmdoutpid.store(en.cmdoutpid, Ordering::Relaxed); // c:6488
cmdoutval.store(en.cmdoutval, Ordering::Relaxed); // c:6489
use_cmdoutval.store(en.use_cmdoutval, Ordering::Relaxed); // c:6490
procsubstpid.store(en.procsubstpid, Ordering::Relaxed); // c:6491
TRAP_RETURN.store(en.trap_return, Ordering::Relaxed); // c:6492
TRAP_STATE.store(en.trap_state, Ordering::Relaxed); // c:6493
trapisfunc.store(en.trapisfunc, Ordering::Relaxed); // c:6494
traplocallevel.store(en.traplocallevel, Ordering::Relaxed); // c:6495
noerrs.store(en.noerrs, Ordering::Relaxed); // c:6496
this_noerrexit.store(en.this_noerrexit, Ordering::Relaxed); // c:6497
// c:6498-6499 — `setunderscore(en->underscore); zsfree(en->underscore);`
if let Some(ref u) = en.underscore {
setunderscore(u); // c:6498
}
// c:6500 — `free(en);` — handled by Box drop when `en` falls out of scope.
unqueue_signals(); // c:6502
}
/// Port of `void execstring(char *s, int dont_change_job, int exiting,
/// char *context)` from `Src/exec.c:1228`.
///
/// C body:
/// ```c
/// Eprog prog;
/// pushheap();
/// if (isset(VERBOSE)) {
/// zputs(s, stderr);
/// fputc('\n', stderr);
/// fflush(stderr);
/// }
/// if ((prog = parse_string(s, 0)))
/// execode(prog, dont_change_job, exiting, context);
/// popheap();
/// ```
///
/// Public entry — execute an arbitrary string as a zsh command list.
/// Called by `eval`, `.`/`source`, `trap` action firing, autoload
/// body executors, command substitution body runners.
///
/// =================== WARNING — DIVERGENCE ====================
/// The C path is `parse_string` → `execode` → `execlist` (wordcode
/// walker). zshrs replaces `execode/execlist` with the fusevm
/// bytecode VM at `crate::vm_helper::ShellExecutor::execute_script_zsh_pipeline`.
/// Faithful port: VERBOSE banner + pushheap/popheap intact; the
/// parse+execute chain delegates to the fusevm entry. When `execlist`
/// lands as a strict 1:1 port, swap the delegate for the canonical
/// chain.
/// =============================================================
pub fn execstring(s: &str, _dont_change_job: i32, _exiting: i32, _context: &str) {
// c:1228
pushheap(); // c:1232
// c:1233-1237 — VERBOSE banner.
if isset(VERBOSE) {
// c:1233
let mut stderr = std::io::stderr().lock();
use std::io::Write;
let _ = stderr.write_all(s.as_bytes()); // c:1234 zputs(s, stderr)
let _ = stderr.write_all(b"\n"); // c:1235
let _ = stderr.flush(); // c:1236
}
// c:1238-1239 — parse + execode. zshrs delegates the parse+VM
// chain to the fusevm pipeline via the exec_hooks fn-ptr
// installed by fusevm_bridge at startup. Direct
// `with_executor` / ShellExecutor reach-in from src/ported/ is
// forbidden — see memory feedback_no_exec_script_from_ported.
let _ = crate::ported::exec_hooks::execute_script_zsh_pipeline(s);
popheap(); // c:1240
}
/// Port of `void runshfunc(Eprog prog, FuncWrap wrap, char *name)` from
/// `Src/exec.c:6166`. The inner shell-function executor — fires
/// module-registered wrapper handlers around the function body, with
/// `$_` (zunderscore) save/restore and a paramscope push/pop around
/// the wordcode walk.
///
/// C control flow:
/// ```c
/// queue_signals();
/// ou = zalloc(ouu = underscoreused);
/// if (ou) memcpy(ou, zunderscore, underscoreused);
/// while (wrap) { // wrapper chain
/// wrap->module->wrapper++;
/// cont = wrap->handler(prog, wrap->next, name);
/// wrap->module->wrapper--;
/// if (!wrap->module->wrapper && (wrap->module->node.flags & MOD_UNLOAD))
/// unload_module(wrap->module);
/// if (!cont) { // wrapper handled it
/// if (ou) zfree(ou, ouu);
/// unqueue_signals();
/// return;
/// }
/// wrap = wrap->next;
/// }
/// startparamscope();
/// execode(prog, 1, 0, "shfunc");
/// if (ou) { setunderscore(ou); zfree(ou, ouu); }
/// endparamscope();
/// unqueue_signals();
/// ```
///
/// (a) `wrap->module->wrapper++/--` (c:6178/6180) wired against
/// `module::MODULESTAB.modules[name].wrapper` (i32), looked up
/// by `wrap.module.node.nam`. Recursive unload during handler
/// defers correctly.
/// (b) `unload_module(wrap->module)` (c:6184) wired via
/// `modulestab.unload_module(name)` when wrapper hits 0 AND
/// MOD_UNLOAD flag is set on the module's hashnode.
/// (c) `execode(prog, 1, 0, "shfunc")` (c:6195) ported at
/// exec.rs:6047. Body uses execode for the no-source
/// (compiled-wordcode) branch and fusevm for the
/// source-preserving (autoloaded) branch per cache coherence.
/// (d) `startparamscope/endparamscope` Rust signatures take
/// `&mut HashTable` (params.rs:7425/7435). We pass the global
/// paramtab handle via the params crate.
pub fn runshfunc(
prog: &eprog,
mut wrap: Option<&funcwrap>,
name: &str,
) {
// c:6166
queue_signals(); // c:6171
// c:6173-6175 — snapshot zunderscore into `ou`.
let ouu = underscoreused.load(Ordering::Relaxed) as usize;
let ou: Option<String> = if ouu > 0 {
// c:6174
Some(zunderscore.lock().unwrap().clone()) // c:6175
} else {
None
};
// c:6177-6193 — wrapper chain walk.
while let Some(w) = wrap {
// c:6177
// c:6178 — wrap->module->wrapper++ (WARNING a).
// c:6178 — `wrap->module->wrapper++;` — bump refcount so a
// recursive unload during the handler defers until we return.
let mod_name: Option<String> = w.module.as_ref().map(|m| m.node.nam.clone());
if let Some(ref n) = mod_name {
if let Ok(mut tab) = crate::ported::module::MODULESTAB.lock() {
if let Some(m) = tab.modules.get_mut(n) {
m.wrapper += 1;
}
}
}
let cont = if let Some(h) = w.handler {
// c:6179 — WrapFunc takes Eprog by value + next FuncWrap by value.
// We pass an empty next sentinel (wrapper-chain walks are
// single-step in zshrs — see chain-walk comment below).
let next_sentinel = Box::new(funcwrap {
next: None,
flags: 0,
handler: None,
module: None,
});
h(Box::new(prog.clone()), next_sentinel, name)
} else {
1
};
// c:6180 — `wrap->module->wrapper--;`
// c:6182-6184 — `if (!wrap->module->wrapper && (flags & MOD_UNLOAD)) unload_module(wrap->module);`
if let Some(ref n) = mod_name {
let should_unload = {
if let Ok(mut tab) = crate::ported::module::MODULESTAB.lock() {
if let Some(m) = tab.modules.get_mut(n) {
m.wrapper -= 1;
m.wrapper == 0
&& (m.node.flags & crate::ported::zsh_h::MOD_UNLOAD) != 0
} else {
false
}
} else {
false
}
};
if should_unload {
if let Ok(mut tab) = crate::ported::module::MODULESTAB.lock() {
let _ = tab.unload_module(n); // c:6184
}
}
}
if cont == 0 {
// c:6186 — wrapper claimed the call.
unqueue_signals(); // c:6189
return; // c:6190
}
// c:6192 — wrap = wrap->next; the linked-list step requires
// owning the next ref; the borrowed iteration breaks here.
// Wrapper chains > 1 are extremely rare; we stop at the
// first to avoid a Box::leak.
wrap = None;
}
// c:6194 — startparamscope (just inc_locallevel internally).
inc_locallevel();
// c:6195 — `execode(prog, 1, 0, "shfunc");` — run the function
// body. Prefer the canonical execode (exec.rs:6047) which walks
// execlist on a fresh estate over the prog. If prog.strs carries
// the original source (autoloaded ported that the lazy-compile path
// populated), route through the fusevm pipeline for cache
// coherence with execstring.
if let Some(ref src) = prog.strs {
let _ = crate::ported::exec_hooks::execute_script_zsh_pipeline(src);
} else {
// Pure wordcode body — drive via the canonical execode.
crate::ported::exec::execode(
Box::new(prog.clone()),
1,
0,
"shfunc",
);
let _ = name;
}
if let Some(ou_str) = ou {
// c:6196
setunderscore(&ou_str); // c:6197
// c:6198 — zfree(ou, ouu) — Rust drops on scope exit.
}
endparamscope(); // c:6200
// c:6141 — deferred-exit gate. After endparamscope() unwinds the
// function's local scope (locallevel--), check whether an exit
// queued inside the function has reached its target scope:
// if (exit_pending && exit_level >= locallevel+1 && !in_exit_trap)
// The `+1` accounts for endparamscope having already happened
// here (locallevel is already one less than when exit_level was
// captured at c:5890). When the gate fires:
// - locallevel > forklevel: still in a nested function — force
// the outer frame to return too (retflag=1, breaks=loops).
// - locallevel <= forklevel: out of all functions — actually
// exit the shell now via zexit(exit_val, ZEXIT_NORMAL).
// `in_exit_trap` (c:Src/signals.c:63 — `int in_exit_trap;`) is the
// EXIT-trap reentry counter. dotrap at signals.c:1272/1277 wraps
// SIGEXIT handler dispatch with ++/--, so an exit issued FROM an
// EXIT trap shouldn't re-trigger the gate (or the trap would
// recurse). zshrs's signals::in_exit_trap is the canonical port
// surface — read it directly here.
let exit_pending = crate::ported::builtin::EXIT_PENDING.load(Ordering::Relaxed);
let exit_level = crate::ported::builtin::EXIT_LEVEL.load(Ordering::Relaxed);
let cur_locallevel = crate::ported::params::locallevel.load(Ordering::Relaxed) as i32;
let cur_forklevel = FORKLEVEL.load(Ordering::Relaxed);
let in_exit_trap = crate::ported::signals::in_exit_trap.load(Ordering::Relaxed); // c:Src/signals.c:63
if exit_pending != 0 && exit_level >= cur_locallevel + 1 && in_exit_trap == 0 {
// c:6141
if cur_locallevel > cur_forklevel {
// c:6143 — still inside a nested function: keep unwinding.
crate::ported::builtin::RETFLAG.store(1, Ordering::Relaxed); // c:6144
crate::ported::builtin::BREAKS.store(
crate::ported::builtin::LOOPS.load(Ordering::Relaxed),
Ordering::Relaxed,
); // c:6145
} else {
// c:6151 — out of all functions: exit for real.
crate::ported::builtin::STOPMSG.store(1, Ordering::Relaxed); // c:6151
let val = crate::ported::builtin::EXIT_VAL.load(Ordering::Relaxed);
crate::ported::builtin::zexit(val, crate::ported::zsh_h::ZEXIT_NORMAL); // c:6152
}
}
unqueue_signals(); // c:6202
}
/// Port of `Emulation_options sticky_emulation_dup(Emulation_options src,
/// int useheap)` from `Src/exec.c:5501`.
///
/// C body (`useheap` selects between heap-arena and permanent zalloc;
/// Rust collapses both into owned `Box` clones):
/// ```c
/// Emulation_options newsticky = useheap ?
/// hcalloc(sizeof(*src)) : zshcalloc(sizeof(*src));
/// newsticky->emulation = src->emulation;
/// if (src->n_on_opts) {
/// size_t sz = src->n_on_opts * sizeof(*src->on_opts);
/// newsticky->n_on_opts = src->n_on_opts;
/// newsticky->on_opts = useheap ? zhalloc(sz) : zalloc(sz);
/// memcpy(newsticky->on_opts, src->on_opts, sz);
/// }
/// if (src->n_off_opts) {
/// size_t sz = src->n_off_opts * sizeof(*src->off_opts);
/// newsticky->n_off_opts = src->n_off_opts;
/// newsticky->off_opts = useheap ? zhalloc(sz) : zalloc(sz);
/// memcpy(newsticky->off_opts, src->off_opts, sz);
/// }
/// return newsticky;
/// ```
///
/// Deep-clone a sticky emulation struct. Used by `shfunc_set_sticky`
/// at function-def time to snapshot the pending `sticky` global so
/// the function carries its own immutable copy.
pub fn sticky_emulation_dup(
src: &emulation_options,
_useheap: i32,
) -> Emulation_options {
// c:5501
// c:5503-5505 — `newsticky = hcalloc/zshcalloc; newsticky->emulation = src->emulation;`
let mut newsticky = Box::new(emulation_options {
emulation: src.emulation, // c:5505
n_on_opts: 0,
n_off_opts: 0,
on_opts: Vec::new(),
off_opts: Vec::new(),
});
// c:5506-5511 — copy on_opts.
if src.n_on_opts != 0 {
// c:5506
newsticky.n_on_opts = src.n_on_opts; // c:5508
newsticky.on_opts = src.on_opts.clone(); // c:5510 memcpy
}
// c:5512-5517 — copy off_opts.
if src.n_off_opts != 0 {
// c:5512
newsticky.n_off_opts = src.n_off_opts; // c:5514
newsticky.off_opts = src.off_opts.clone(); // c:5516 memcpy
}
newsticky // c:5519
}
/// Port of `void shfunc_set_sticky(Shfunc shf)` from `Src/exec.c:5527`.
///
/// C body:
/// ```c
/// if (sticky)
/// shf->sticky = sticky_emulation_dup(sticky, 0);
/// else
/// shf->sticky = NULL;
/// ```
///
/// Stamp the function with the current pending sticky-emulation
/// snapshot (deep-copy via `sticky_emulation_dup`), or clear it.
pub fn shfunc_set_sticky(shf: &mut shfunc) {
// c:5527
let sticky_guard = sticky.lock().unwrap();
if let Some(ref s) = *sticky_guard {
// c:5529
shf.sticky = Some(sticky_emulation_dup(s, 0)); // c:5530
} else {
// c:5531
shf.sticky = None; // c:5532
}
}
/// Port of `static char *search_defpath(char *cmd, char *pbuf, int plen)`
/// from `Src/exec.c:691`.
///
/// Walk DEFAULT_PATH for an executable `<dir>/<cmd>` regular file.
/// Used by `command -p` to bypass the user's `$PATH` and search the
/// system default (`/bin:/usr/bin:...`).
pub fn search_defpath(cmd: &str, plen: usize) -> Option<String> {
// c:691
// c:695 — `for (ps = DEFAULT_PATH; ps; ps = pe ? pe+1 : NULL)`.
for ps in DEFAULT_PATH.split(':') {
// c:695
// c:697 — `if (*ps == '/')`.
if !ps.starts_with('/') {
continue;
}
// c:700-707 — PATH_MAX bounds check on `<dir>` segment.
if ps.len() >= plen {
// c:700 / c:704
continue; // c:701 / c:705
}
// c:708 — `*s++ = '/';`. c:709-710 bounds check on `<dir>/<cmd>`.
let full_len = ps.len() + 1 + cmd.len();
if full_len >= plen {
// c:709
continue; // c:710
}
let buf = format!("{}/{}", ps, cmd); // c:711 `strucpy(&s, cmd);`
// c:712 — `if (iscom(pbuf)) return pbuf;`
if iscom(&buf) {
// c:712
return Some(buf); // c:713
}
}
None // c:716
}
/// Port of `static int checkclobberparam(struct redir *f)` from
/// `Src/exec.c:2178`.
///
/// C body:
/// ```c
/// struct value vbuf; Value v;
/// char *s = f->varid; int fd;
/// if (!s) return 1;
/// if (!(v = getvalue(&vbuf, &s, 0))) return 1;
/// if (v->pm->node.flags & PM_READONLY) {
/// zwarn("can't allocate file descriptor to readonly parameter %s",
/// f->varid);
/// errno = 0;
/// return 0;
/// }
/// /* We can't clobber the value in the parameter if it's
/// * already an opened file descriptor */
/// if (!isset(CLOBBER) && (s = getstrvalue(v)) &&
/// (fd = (int)zstrtol(s, &s, 10)) >= 0 && !*s &&
/// fd <= max_zsh_fd && fdtable[fd] == FDT_EXTERNAL) {
/// zwarn("can't clobber parameter %s containing file descriptor %d",
/// f->varid, fd);
/// errno = 0;
/// return 0;
/// }
/// return 1;
/// ```
///
/// Validate that `f->varid` (the `{var}>file` brace-FD form's var
/// name) is writable and (under NOCLOBBER) doesn't currently hold an
/// FDT_EXTERNAL fd number. Returns 1 on OK, 0 on refusal (zwarn
/// already emitted).
///
/// NOCLOBBER + FDT_EXTERNAL clause now ported (c:2199-2213). When
/// NOCLOBBER is set and the param's value is the fd-number of an
/// FDT_EXTERNAL-marked fd in the fdtable, refuse with a warning so
/// the existing fd doesn't get clobbered by the upcoming open(2).
pub fn checkclobberparam(f: &redir) -> i32 {
// c:2178
// c:2182 — `char *s = f->varid;`
let s = match &f.varid {
Some(v) => v.clone(),
None => return 1, // c:2185-2186 — `if (!s) return 1;`
};
// c:2188-2197 — readonly refusal: lookup PM flags directly via
// paramtab (the C `getvalue` returns a Value wrapping the same
// pm; we read pm->node.flags here without the wrap).
let readonly = paramtab()
.read()
.ok()
.and_then(|t| {
t.get(&s)
.map(|p| (p.node.flags as u32 & PM_READONLY) != 0)
})
.unwrap_or(false);
if readonly {
// c:2191
zwarn(&format!(
// c:2192
"can't allocate file descriptor to readonly parameter {}",
s
));
// c:2195 — `errno = 0;` not flagged as a system error.
return 0; // c:2196
}
// c:2199-2213 — NOCLOBBER + FDT_EXTERNAL refusal: if NOCLOBBER set
// AND the param holds a valid fd that's already in our fdtable as
// FDT_EXTERNAL (allocated by sysopen / coproc / etc.), refuse the
// open so we don't clobber it.
if !isset(CLOBBER) {
// c:2201 — `getstrvalue(v)` — read the param's string form.
let val_str = crate::ported::params::paramtab()
.read()
.ok()
.and_then(|t| t.get(&s).and_then(|p| p.u_str.clone()))
.unwrap_or_default();
if let Ok(fd) = val_str.trim().parse::<i32>() {
// c:2202 — `if (fd <= max_zsh_fd && fdtable[fd] == FDT_EXTERNAL)`
let max_fd = crate::ported::utils::MAX_ZSH_FD.load(Ordering::Relaxed);
if fd >= 0 && fd <= max_fd {
let kind = crate::ported::utils::fdtable_get(fd);
if kind == crate::ported::zsh_h::FDT_EXTERNAL {
zwarn(&format!(
"{}: file descriptor {} already open",
s, fd
)); // c:2206-2210
return 0; // c:2211
}
}
}
}
1 // c:2214
}
/// Port of `static int clobber_open(struct redir *f)` from
/// `Src/exec.c:2221`.
///
/// C body:
/// ```c
/// struct stat buf;
/// int fd, oerrno;
/// char *ufname = unmeta(f->name);
/// /* If clobbering, just open. */
/// if (isset(CLOBBER) || IS_CLOBBER_REDIR(f->type))
/// return open(ufname, O_WRONLY | O_CREAT | O_TRUNC | O_NOCTTY, 0666);
/// /* If not clobbering, attempt to create file exclusively. */
/// if ((fd = open(ufname, O_WRONLY | O_CREAT | O_EXCL | O_NOCTTY, 0666)) >= 0)
/// return fd;
/// /* If that fails, we are still allowed to open non-regular files. */
/// oerrno = errno;
/// if ((fd = open(ufname, O_WRONLY | O_NOCTTY)) != -1) {
/// if (!fstat(fd, &buf)) {
/// if (!S_ISREG(buf.st_mode)) return fd;
/// /* CLOBBER_EMPTY allows re-use of empty regular files. */
/// if (isset(CLOBBEREMPTY) && buf.st_size == 0) return fd;
/// }
/// close(fd);
/// }
/// errno = oerrno;
/// return -1;
/// ```
///
/// Open the redir target for write with the NOCLOBBER rules:
/// - CLOBBER set or `>|` form → just open with O_TRUNC
/// - Otherwise → try O_EXCL first; on EEXIST, only allow non-regular
/// files (FIFOs, devices, sockets) OR empty regular files under
/// CLOBBEREMPTY.
pub fn clobber_open(f: &redir) -> i32 {
// c:2221
let ufname_owned = unmeta(f.name.as_deref().unwrap_or("")); // c:2225
let ufname = match std::ffi::CString::new(ufname_owned.as_str()) {
Ok(c) => c,
Err(_) => return -1,
};
// c:2228-2230 — clobber path: just open + truncate.
if isset(CLOBBER)
|| IS_CLOBBER_REDIR(f.typ)
{
// c:2228
// c:2229 — `open(ufname, O_WRONLY|O_CREAT|O_TRUNC|O_NOCTTY, 0666)`
let fd = unsafe {
libc::open(
ufname.as_ptr(),
libc::O_WRONLY | libc::O_CREAT | libc::O_TRUNC | libc::O_NOCTTY,
0o666 as libc::c_uint,
)
};
return fd; // c:2230
}
// c:2233-2235 — try O_EXCL create first.
let fd = unsafe {
// c:2233
libc::open(
ufname.as_ptr(),
libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | libc::O_NOCTTY,
0o666 as libc::c_uint,
)
};
if fd >= 0 {
return fd; // c:2235
}
// c:2240 — `oerrno = errno;` — save for restoration on the recover path.
let oerrno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
// c:2241-2260 — recover: open() w/o O_EXCL, accept if non-regular
// OR (CLOBBEREMPTY && size == 0).
let fd = unsafe {
// c:2241
libc::open(
ufname.as_ptr(),
libc::O_WRONLY | libc::O_NOCTTY,
0o666 as libc::c_uint,
)
};
if fd != -1 {
let mut buf: libc::stat = unsafe { std::mem::zeroed() };
if unsafe { libc::fstat(fd, &mut buf) } == 0 {
// c:2242
// c:2243-2244 — non-regular file: accept.
if (buf.st_mode & libc::S_IFMT) != libc::S_IFREG {
// c:2243
return fd; // c:2244
}
// c:2256-2257 — CLOBBEREMPTY + empty regular: accept.
if isset(CLOBBEREMPTY) && buf.st_size == 0 {
// c:2256
return fd; // c:2257
}
}
unsafe {
libc::close(fd);
} // c:2259
}
// c:2262 — `errno = oerrno;` — restore the EEXIST so caller diagnoses
// "file exists" not the noisier "couldn't reopen" trailing errno.
// Per-platform errno setter: __error() on macOS, __errno_location()
// on Linux. Without cfg gating the build breaks on Linux (CI).
#[cfg(target_os = "macos")]
unsafe {
*libc::__error() = oerrno;
}
#[cfg(target_os = "linux")]
unsafe {
*libc::__errno_location() = oerrno;
}
-1 // c:2263
}
/// Port of `char *findcmd(char *arg0, int docopy, int default_path)`
/// from `Src/exec.c:897`. Walk `$PATH` (or DEFAULT_PATH under
/// `default_path=1`) for `arg0`, returning the matching path on
/// success. `_docopy` is the C source's "duplicate the result"
/// flag — Rust ownership covers it without an explicit copy step.
/// `default_path=1` forces `/bin:/usr/bin:...` search (used by
/// `command -p`).
pub fn findcmd(arg0: &str, _docopy: i32, default_path: i32) -> Option<String> {
// c:897
// c:903-908 — if (default_path) → search_defpath; return.
if default_path != 0 {
return search_defpath(arg0, libc::PATH_MAX as usize);
}
// c:912-913 — strlen(arg0) > PATH_MAX → NULL.
if arg0.len() > libc::PATH_MAX as usize {
return None;
}
// c:914-920 — `/`-bearing arg: accept only if absolute OR (relative
// + PATHDIRS set and not ./ ../).
if arg0.contains('/') {
// c:915 — `RET_IF_COM(arg0)` — accept if it's an existing executable.
if iscom(arg0) {
if arg0.starts_with('/') {
return Some(arg0.to_string()); // c:916
}
// c:917-919 — relative + PATHDIRS set → fall through to walk.
if arg0.starts_with("./")
|| arg0.starts_with("../")
|| !isset(PATHDIRS)
{
return None;
}
// else fall through to PATH walk.
} else {
return None;
}
}
// c:943-951 — walk `path[]` (the shell `$path` array). Read $PATH
// from paramtab so shell-private edits via `path=(...)` take
// effect (not OS env only).
let path = getsparam("PATH")?;
for dir in path.split(':') {
if dir.is_empty() {
continue;
}
let candidate = format!("{}/{}", dir, arg0);
if iscom(&candidate) {
return Some(candidate);
}
}
None // c:952
}
/// Port of `static void addfd(int forked, int *save, struct multio **mfds,
/// int fd1, int fd2, int rflag, char *varid)`
/// from `Src/exec.c:2397`.
///
/// C body (~100 lines, three branches):
/// ```c
/// if (varid) {
/// /* {varid}>file form — move fd above 10 and bind $varid to it */
/// } else if (!mfds[fd1] || unset(MULTIOS)) {
/// /* new multio OR MULTIOS off — first redir on this fd */
/// } else {
/// /* additional redir on a fd that's already a multio (split or extend) */
/// }
/// ```
///
/// Register `fd2` (already-open) as a redirection target for `fd1`.
/// Three branches: `varid` writes the moved fd to `$varid` and bumps
/// `fdtable[fd1]` = FDT_EXTERNAL; new-multio path saves the original fd1
/// (when `!forked`) and stamps `mfds[fd1]` as a single-entry struct;
/// extend-multio path either splits a ct=1 stream into a pipe + 2 fds
/// via `mpipe`, or appends another fd to an already-split stream
/// (re-allocating mfds for fd1 past the MULTIOUNIT boundary).
///
/// `multio.fds` is now `Vec<i32>` (zsh_h.rs:1397) so the C
/// `hrealloc` at c:2485 maps to `Vec::push`; MULTIOUNIT is no
/// longer a hard cap (still 8 for the initial allocation, grown
/// on demand thereafter).
///
/// `fdtable[fdN] |= FDT_SAVED_MASK` at c:2440 — Rust fdtable_set
/// stores the int value but doesn't expose a bitwise-OR setter; we
/// re-read + OR + re-store as two atomic-feeling steps.
pub fn addfd(
forked: i32,
save: &mut [i32; 10],
mfds: &mut [Option<Box<multio>>; 10],
fd1: i32,
fd2: i32,
rflag: i32,
varid: Option<&str>,
) {
// c:2397
let mut pipes: [i32; 2] = [-1; 2]; // c:2400
// c:2402-2417 — `if (varid)` branch — {varid}>file shape.
if let Some(vid) = varid {
// c:2402
let fd_moved = movefd(fd2); // c:2404
if fd_moved == -1 {
// c:2405
zerr(&format!(
// c:2406
"cannot move fd {}: {}",
fd2,
std::io::Error::last_os_error()
));
return; // c:2407
}
// c:2409 — `fdtable[fd1] = FDT_EXTERNAL;`
fdtable_set(fd_moved, FDT_EXTERNAL);
// c:2410 — `setiparam(varid, (zlong)fd1);`
setiparam(vid, fd_moved as i64);
// c:2415-2416 — `if (errflag) zclose(fd1);`
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
// c:2415
let _ = zclose(fd_moved); // c:2416
}
return;
}
// c:2418 — `else if (!mfds[fd1] || unset(MULTIOS))`
let fd1u = fd1 as usize;
if fd1u >= mfds.len() {
return;
}
if mfds[fd1u].is_none() || unset(MULTIOS) {
// c:2418
if mfds[fd1u].is_none() {
// c:2419 — `starting a new multio`
// c:2420 — `mfds[fd1] = zhalloc(sizeof(multio));`
mfds[fd1u] = Some(Box::new(multio {
ct: 0,
rflag: 0,
pipe: -1,
// c:2420 — C allocates VARLENARRAY trailing `int fds[1]`;
// grow on demand via push() below. Pre-fill MULTIOUNIT
// slots with -1 so existing indexed writes (fds[0], fds[1])
// still work without explicit resize().
fds: vec![-1; MULTIOUNIT],
}));
// c:2421 — `if (!forked && save[fd1] == -2)`
if forked == 0 && save[fd1u] == -2 {
if fd1 == fd2 {
// c:2422
save[fd1u] = -1; // c:2423
} else {
// c:2424
let fd_n = movefd(fd1); // c:2425
if fd_n < 0 {
// c:2430
let e = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
if e != libc::EBADF {
// c:2431
zerr(&format!(
// c:2432
"cannot duplicate fd {}: {}",
fd1,
std::io::Error::from_raw_os_error(e)
));
mfds[fd1u] = None; // c:2433
closemnodes(mfds); // c:2434
return; // c:2435
}
} else {
// c:2438-2439 — DPUTS check that the saved fd is FDT_INTERNAL.
crate::DPUTS!(
fdtable_get(fd_n) != FDT_INTERNAL,
"Saved file descriptor not marked as internal"
);
// c:2440 — `fdtable[fdN] |= FDT_SAVED_MASK;`
let cur = fdtable_get(fd_n);
fdtable_set(fd_n, cur | FDT_SAVED_MASK);
}
save[fd1u] = fd_n; // c:2442
}
}
}
// c:2446-2447 — `if (!varid) redup(fd2, fd1);` (varid already
// handled above; this is the non-varid branch.)
let _ = redup(fd2, fd1);
// c:2448-2450 — `mfds[fd1]->ct=1; mfds[fd1]->fds[0]=fd1; mfds[fd1]->rflag=rflag;`
if let Some(mn) = mfds[fd1u].as_mut() {
mn.ct = 1; // c:2448
mn.fds[0] = fd1; // c:2449
mn.rflag = rflag; // c:2450
}
} else {
// c:2451 — extend existing multio.
// c:2452-2456 — rflag mismatch check.
let cur_rflag = mfds[fd1u].as_ref().map(|m| m.rflag).unwrap_or(0);
if cur_rflag != rflag {
// c:2452
zerr(&format!("file mode mismatch on fd {}", fd1)); // c:2453
closemnodes(mfds); // c:2454
return; // c:2455
}
let cur_ct = mfds[fd1u].as_ref().map(|m| m.ct).unwrap_or(0);
if cur_ct == 1 {
// c:2457 — split the stream.
// c:2458 — `int fdN = movefd(fd1);`
let fd_n = movefd(fd1);
if fd_n < 0 {
// c:2459
zerr(&format!(
// c:2460
"multio failed for fd {}: {}",
fd1,
std::io::Error::last_os_error()
));
closemnodes(mfds); // c:2461
return; // c:2462
}
if let Some(mn) = mfds[fd1u].as_mut() {
mn.fds[0] = fd_n; // c:2464
}
// c:2465 — `fdN = movefd(fd2);`
let fd_n2 = movefd(fd2);
if fd_n2 < 0 {
// c:2466
zerr(&format!(
// c:2467
"multio failed for fd {}: {}",
fd2,
std::io::Error::last_os_error()
));
closemnodes(mfds); // c:2468
return; // c:2469
}
if let Some(mn) = mfds[fd1u].as_mut() {
mn.fds[1] = fd_n2; // c:2471
}
// c:2472 — `mpipe(pipes)`
if mpipe(&mut pipes) < 0 {
// c:2472
zerr(&format!(
// c:2473
"multio failed for fd {}: {}",
fd2,
std::io::Error::last_os_error()
));
closemnodes(mfds); // c:2474
return; // c:2475
}
// c:2477 — `mfds[fd1]->pipe = pipes[1 - rflag];`
if let Some(mn) = mfds[fd1u].as_mut() {
mn.pipe = pipes[(1 - rflag) as usize];
}
// c:2478 — `redup(pipes[rflag], fd1);`
let _ = redup(pipes[rflag as usize], fd1);
// c:2479 — `mfds[fd1]->ct = 2;`
if let Some(mn) = mfds[fd1u].as_mut() {
mn.ct = 2;
}
} else {
// c:2480 — extend already-split stream.
// c:2482-2486 — `mn = hrealloc(mn, sizeof + (ct-1)*sizeof(int),
// sizeof + ct*sizeof(int));`
// Rust's `Vec<i32>` grows on demand; ensure capacity for the
// new slot before the indexed write below.
if let Some(mn) = mfds[fd1u].as_mut() {
while mn.fds.len() <= cur_ct as usize {
mn.fds.push(-1);
}
}
// c:2487 — `if ((fdN = movefd(fd2)) < 0)`
let fd_n = movefd(fd2);
if fd_n < 0 {
zerr(&format!(
// c:2488
"multio failed for fd {}: {}",
fd2,
std::io::Error::last_os_error()
));
closemnodes(mfds); // c:2489
return; // c:2490
}
// c:2492 — `mfds[fd1]->fds[mfds[fd1]->ct++] = fdN;`
if let Some(mn) = mfds[fd1u].as_mut() {
let slot = mn.ct as usize;
if slot < mn.fds.len() {
mn.fds[slot] = fd_n;
mn.ct += 1;
}
}
}
}
}
/// Port of `static void closemn(struct multio **mfds, int fd, int type)`
/// from `Src/exec.c:2273`.
///
/// C body (abridged — the meat is the fork-into-tee-or-cat child):
/// ```c
/// if (fd >= 0 && mfds[fd] && mfds[fd]->ct >= 2) {
/// struct multio *mn = mfds[fd];
/// char buf[TCBUFSIZE]; int len, i;
/// pid_t pid; struct timespec bgtime;
/// child_block();
/// if ((pid = zfork(&bgtime))) {
/// for (i = 0; i < mn->ct; i++) zclose(mn->fds[i]);
/// zclose(mn->pipe);
/// if (pid == -1) { mfds[fd] = NULL; child_unblock(); return; }
/// mn->ct = 1; mn->fds[0] = fd;
/// addproc(pid, NULL, 1, &bgtime, -1, -1);
/// child_unblock(); return;
/// }
/// /* pid == 0 (child) */
/// opts[INTERACTIVE] = 0;
/// dont_queue_signals();
/// child_unblock();
/// closeallelse(mn);
/// if (mn->rflag) {
/// /* tee process: read mn->pipe, write each mn->fds[i] */
/// } else {
/// /* cat process: read each mn->fds[i], write mn->pipe */
/// }
/// _exit(0);
/// } else if (fd >= 0 && type == REDIR_CLOSE)
/// mfds[fd] = NULL;
/// ```
///
/// Success-path close of a multio. For ct>=2 (multiple-output
/// redirection), forks a tee/cat child that proxies bytes between
/// the original fd and the per-output fds. Single-output multios
/// (ct=1) skip the fork entirely and just clear the slot.
///
/// c:2299 — `addproc(pid, NULL, 1, &bgtime, -1, -1)` records the
/// tee/cat child in the current job's auxprocs.
pub fn closemn(
mfds: &mut [Option<Box<multio>>; 10],
fd: i32,
type_: i32,
) {
// c:2273
// c:2275 — `if (fd >= 0 && mfds[fd] && mfds[fd]->ct >= 2)`
let needs_tee = fd >= 0
&& (fd as usize) < mfds.len()
&& mfds[fd as usize].as_ref().is_some_and(|m| m.ct >= 2);
if needs_tee {
// c:2275
// Take the multio out of the slot so we can move pieces into
// the child without aliasing the slot.
let mn = mfds[fd as usize].take().unwrap();
let mut buf = [0u8; 4092]; // c:2277 TCBUFSIZE
// c:2287 — `child_block();` block SIGCHLD before fork race.
child_block();
// c:2288 — `pid = zfork(&bgtime);`
let mut bgtime = ZshTimespec {
tv_sec: 0,
tv_nsec: 0,
};
let pid = zfork(Some(&mut bgtime));
if pid != 0 {
// c:2288 parent branch
// c:2289-2290 — close all per-output fds.
for i in 0..mn.ct as usize {
if i < mn.fds.len() {
let _ = zclose(mn.fds[i]); // c:2290
}
}
let _ = zclose(mn.pipe); // c:2291
if pid == -1 {
// c:2292
// c:2293 — `mfds[fd] = NULL;` already done via .take()
child_unblock(); // c:2294
return; // c:2295
}
// c:2297-2298 — `mn->ct = 1; mn->fds[0] = fd;`
let mut mn_back = mn;
mn_back.ct = 1; // c:2297
mn_back.fds[0] = fd; // c:2298
mfds[fd as usize] = Some(mn_back);
// c:2299 — `addproc(pid, NULL, 1, &bgtime, -1, -1);` — record
// the tee/cat child in the current job's auxprocs (aux=true).
if let Some(jt) = crate::ported::jobs::JOBTAB.get() {
let mut guard = jt.lock().unwrap();
let tj = crate::ported::jobs::THISJOB
.get()
.map(|m| *m.lock().unwrap())
.unwrap_or(-1);
if tj >= 0 {
if let Some(j) = guard.get_mut(tj as usize) {
crate::ported::jobs::addproc(
j, pid, "", true,
Some(std::time::Instant::now()),
-1, -1,
);
}
}
}
let _ = bgtime;
child_unblock(); // c:2300
return; // c:2301
}
// c:2303 — child branch (pid == 0).
opt_state_set("interactive", false); // c:2304
dont_queue_signals(); // c:2305
child_unblock(); // c:2306
closeallelse(&mn); // c:2307
// c:2308-2333 — tee or cat loop.
if mn.rflag != 0 {
// c:2308 — `mn->rflag` set → tee process
// c:2310 — `while ((len = read(mn->pipe, buf, TCBUFSIZE)) != 0)`
loop {
let len = unsafe {
libc::read(mn.pipe, buf.as_mut_ptr() as *mut libc::c_void, buf.len())
};
if len == 0 {
break;
}
if len < 0 {
// c:2311
let e = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
if e == libc::EINTR {
// c:2312
continue;
} else {
break; // c:2315
}
}
// c:2317-2319 — `for i: write_loop(mn->fds[i], buf, len)`
for i in 0..mn.ct as usize {
if i >= mn.fds.len() {
break;
}
if write_loop(mn.fds[i], &buf[..len as usize])
.is_err()
{
break; // c:2319
}
}
}
} else {
// c:2321 — cat process
for i in 0..mn.ct as usize {
if i >= mn.fds.len() {
break;
}
// c:2324 — `while ((len = read(mn->fds[i], buf, TCBUFSIZE)) != 0)`
loop {
let len = unsafe {
libc::read(
mn.fds[i],
buf.as_mut_ptr() as *mut libc::c_void,
buf.len(),
)
};
if len == 0 {
break;
}
if len < 0 {
let e = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
// c:2326 — `if (errno == EINTR && !isatty(mn->fds[i]))`
if e == libc::EINTR && unsafe { libc::isatty(mn.fds[i]) } == 0 {
continue;
} else {
break; // c:2329
}
}
// c:2331 — `if (write_loop(mn->pipe, buf, len) < 0) break;`
if write_loop(mn.pipe, &buf[..len as usize]).is_err() {
break; // c:2332
}
}
}
}
// c:2335 — `_exit(0);`
unsafe {
libc::_exit(0);
}
} else if fd >= 0 && type_ == REDIR_CLOSE {
// c:2336
// c:2337 — `mfds[fd] = NULL;`
if (fd as usize) < mfds.len() {
mfds[fd as usize] = None;
}
}
}
/// Port of `static void closemnodes(struct multio **mfds)` from
/// `Src/exec.c:2344`.
///
/// C body:
/// ```c
/// int i, j;
/// for (i = 0; i < 10; i++)
/// if (mfds[i]) {
/// for (j = 0; j < mfds[i]->ct; j++)
/// zclose(mfds[i]->fds[j]);
/// mfds[i] = NULL;
/// }
/// ```
///
/// Failure-path cleanup: close every fd stashed in any of the 10
/// multio slots and null the slot. Called from `execcmd_exec` when
/// a redirect setup fails partway through and we need to roll back.
pub fn closemnodes(mfds: &mut [Option<Box<multio>>; 10]) {
// c:2344
for i in 0..10 {
// c:2348
if let Some(mn) = mfds[i].take() {
// c:2349
for j in 0..mn.ct as usize {
// c:2350
if j < mn.fds.len() {
let _ = zclose(mn.fds[j]); // c:2351
}
}
// c:2352 — `mfds[i] = NULL;` — handled by .take() above.
}
}
}
/// Port of `static void closeallelse(struct multio *mn)` from
/// `Src/exec.c:2358`.
///
/// C body:
/// ```c
/// int i, j;
/// long openmax;
/// openmax = fdtable_size;
/// for (i = 0; i < openmax; i++)
/// if (mn->pipe != i) {
/// for (j = 0; j < mn->ct; j++)
/// if (mn->fds[j] == i) break;
/// if (j == mn->ct)
/// zclose(i);
/// }
/// ```
///
/// Close every fd in the open range EXCEPT `mn->pipe` and the fds
/// stashed in `mn->fds`. Called inside the multio tee/cat child
/// process to release every fd the parent had open — only the pipe
/// + per-output fds stay alive for the read/write loop.
pub fn closeallelse(mn: &multio) {
// c:2358
// c:2363 — `openmax = fdtable_size;`. zshrs models fdtable as a
// Vec; use MAX_ZSH_FD as the upper bound (fdtable_size grows past
// max_zsh_fd in C but every slot past it is FDT_UNUSED anyway).
let openmax = MAX_ZSH_FD.load(Ordering::Relaxed) + 1; // c:2363
for i in 0..openmax {
// c:2365
if mn.pipe == i {
// c:2366
continue;
}
// c:2367-2369 — scan mn->fds[] for i; skip-close if found.
let mut found = false;
for j in 0..mn.ct as usize {
// c:2367
if j < mn.fds.len() && mn.fds[j] == i {
// c:2368
found = true;
break; // c:2369
}
}
// c:2370-2371 — `if (j == mn->ct) zclose(i);`
if !found {
let _ = zclose(i); // c:2371
}
}
}
/// Port of `static void fixfds(int *save)` from `Src/exec.c:4523`.
///
/// C body:
/// ```c
/// int old_errno = errno;
/// int i;
/// for (i = 0; i != 10; i++)
/// if (save[i] != -2)
/// redup(save[i], i);
/// errno = old_errno;
/// ```
///
/// Restore fds 0..9 from the `save[10]` slot array. `-2` sentinel
/// means "no save was made for this fd"; any other value is the
/// stashed fd that gets `dup2`'d back via `redup`. Preserves the
/// caller's errno across the loop so a downstream caller diagnoses
/// the original failure, not a noisy dup2 errno.
pub fn fixfds(save: &[i32; 10]) {
// c:4523
let old_errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0); // c:4525
for i in 0..10i32 {
// c:4528 — `for (i = 0; i != 10; i++)`
if save[i as usize] != -2 {
// c:4529
redup(save[i as usize], i); // c:4530
}
}
// c:4531 — `errno = old_errno;`
#[cfg(target_os = "macos")]
unsafe {
*libc::__error() = old_errno;
}
#[cfg(target_os = "linux")]
unsafe {
*libc::__errno_location() = old_errno;
}
}
/// Port of `mod_export void closem(int how, int all)` from `Src/exec.c:4546`.
///
/// C body:
/// ```c
/// int i;
/// for (i = 10; i <= max_zsh_fd; i++)
/// if (fdtable[i] != FDT_UNUSED &&
/// (all || (fdtable[i] != FDT_PROC_SUBST &&
/// fdtable[i] != FDT_EXTERNAL)) &&
/// (how == FDT_UNUSED || (fdtable[i] & FDT_TYPE_MASK) == how)) {
/// if (i == SHTTY) SHTTY = -1;
/// zclose(i);
/// }
/// ```
///
/// Walk fds 10..=MAX_ZSH_FD and close every internal shell fd that
/// matches the criteria. `how == FDT_UNUSED` matches all kinds (no
/// type filter); otherwise only fds whose low-nibble type equals
/// `how` are closed. `all == 0` preserves user-visible fds
/// (FDT_PROC_SUBST, FDT_EXTERNAL) since those need to outlive the
/// shell's internal-fd lifetime. SHTTY clearing prevents a stale
/// reference if we just closed the controlling tty.
pub fn closem(how: i32, all: i32) {
// c:4546
let max = MAX_ZSH_FD.load(Ordering::Relaxed); // c:4550
for i in 10i32..=max {
// c:4550
let kind = fdtable_get(i); // c:4551 fdtable[i]
if kind == FDT_UNUSED {
// c:4551
continue;
}
// c:4557-4558 — `(all || (kind != FDT_PROC_SUBST && kind != FDT_EXTERNAL))`
if all == 0 && (kind == FDT_PROC_SUBST || kind == FDT_EXTERNAL) {
continue;
}
// c:4559 — `(how == FDT_UNUSED || (fdtable[i] & FDT_TYPE_MASK) == how)`
if how != FDT_UNUSED && (kind & FDT_TYPE_MASK) != how {
continue;
}
// c:4560-4561 — `if (i == SHTTY) SHTTY = -1;`
if i == SHTTY.load(Ordering::Relaxed) {
// c:4560
SHTTY.store(-1, Ordering::Relaxed); // c:4561
}
// c:4562 — `zclose(i);`
let _ = zclose(i);
}
}
/// Port of `Cmdnam hashcmd(char *arg0, char **pp)` from
/// `Src/exec.c:1010`.
///
/// C body:
/// ```c
/// Cmdnam cn;
/// char *s, buf[PATH_MAX+1];
/// char **pq;
/// if (*arg0 == '/') return NULL;
/// for (; *pp; pp++)
/// if (**pp == '/') {
/// s = buf;
/// struncpy(&s, *pp, PATH_MAX);
/// *s++ = '/';
/// if ((s - buf) + strlen(arg0) >= PATH_MAX) continue;
/// strcpy(s, arg0);
/// if (iscom(buf)) break;
/// }
/// if (!*pp) return NULL;
/// cn = (Cmdnam) zshcalloc(sizeof *cn);
/// cn->node.flags = 0;
/// cn->u.name = pp;
/// cmdnamtab->addnode(cmdnamtab, ztrdup(arg0), cn);
/// if (isset(HASHDIRS)) {
/// for (pq = pathchecked; pq <= pp; pq++) hashdir(pq);
/// pathchecked = pp + 1;
/// }
/// return cn;
/// ```
///
/// Walk `pp[]` (a $path slice starting from `pathchecked`) for the
/// first absolute-PATH entry where `<entry>/<arg0>` is an executable
/// regular file. Inserts the unhashed-cmdnam entry into `cmdnamtab`
/// and (under HASHDIRS) bulk-hashes every PATH dir we walked through
/// so subsequent commands hit the cache.
///
/// Returns the just-inserted `cmdnam` (now in `cmdnamtab`) on success,
/// `None` if `arg0` is absolute or no PATH entry contains it.
pub fn hashcmd(arg0: &str, pp: &[String]) -> Option<cmdnam> {
// c:1010
// c:1016 — `if (*arg0 == '/') return NULL;`
if arg0.starts_with('/') {
return None; // c:1017
}
// c:1018-1028 — walk pp[] for first matching absolute entry.
let mut found_idx: Option<usize> = None;
for (i, dir) in pp.iter().enumerate() {
// c:1018
if !dir.starts_with('/') {
// c:1019
continue;
}
// c:1020-1025 — buf = "<dir>/<arg0>"; PATH_MAX bounds check.
if dir.len() + 1 + arg0.len() >= libc::PATH_MAX as usize {
// c:1023
continue; // c:1024
}
let buf = format!("{}/{}", dir, arg0); // c:1025
if iscom(&buf) {
// c:1026
found_idx = Some(i);
break; // c:1027
}
}
// c:1030-1031 — `if (!*pp) return NULL;`
let pp_idx = match found_idx {
Some(i) => i,
None => return None, // c:1031
};
// c:1033-1036 — alloc cn, set flags=0, u.name=pp (the matching slice).
let path_slice: Vec<String> = pp[pp_idx..].to_vec(); // c:1035
let cn = cmdnam_unhashed(arg0, path_slice); // c:1033-1035
// c:1036 — `cmdnamtab->addnode(cmdnamtab, ztrdup(arg0), cn);`
if let Ok(mut tab) = cmdnamtab_lock().write() {
tab.add(cn.clone());
}
// c:1038-1042 — under HASHDIRS, bulk-hash every dir up to and
// including the matching one, then bump pathchecked past it.
if isset(HASHDIRS) {
// c:1038
let start = pathchecked.load(Ordering::Relaxed); // c:1039
for pq in start..=pp_idx {
// c:1039
if pq < pp.len() {
hashdir(&pp[pq], pq); // c:1040
}
}
pathchecked.store(pp_idx + 1, Ordering::Relaxed); // c:1041
}
Some(cn) // c:1044
}
/// Port of `static pid_t zfork(struct timespec *ts)` from
/// `Src/exec.c:349`.
///
/// C body:
/// ```c
/// pid_t pid;
/// if (thisjob != -1 && thisjob >= jobtabsize - 1 && !expandjobtab()) {
/// zerr("job table full");
/// return -1;
/// }
/// if (ts) zgettime_monotonic_if_available(ts);
/// queue_signals();
/// pid = fork();
/// unqueue_signals();
/// if (pid == -1) {
/// zerr("fork failed: %e", errno);
/// return -1;
/// }
/// #ifdef HAVE_GETRLIMIT
/// if (!pid) setlimits(NULL);
/// #endif
/// return pid;
/// ```
///
/// fork(2) wrapper with jobtab capacity check + child rlimit
/// re-application. Used by every subshell-spawning path: pipelines,
/// process substitution, async commands, command substitution.
pub fn zfork(ts: Option<&mut ZshTimespec>) -> libc::pid_t {
// c:349
let pid: libc::pid_t;
// c:356-359 — `if (thisjob != -1 && thisjob >= jobtabsize - 1 && !expandjobtab())`
let thisjob_lock = THISJOB.get_or_init(|| std::sync::Mutex::new(-1));
let thisjob = *thisjob_lock.lock().unwrap();
if thisjob != -1 {
// c:356
let needed = (thisjob + 1) as usize;
let needs_expand = JOBTAB
.get_or_init(|| std::sync::Mutex::new(Vec::new()))
.lock()
.map(|t| needed >= t.len().saturating_sub(1))
.unwrap_or(false);
if needs_expand {
let mut tab = JOBTAB.get().unwrap().lock().unwrap();
if !expandjobtab(&mut tab, needed) {
// c:357
zerr("job table full"); // c:357
return -1; // c:358
}
}
}
// c:360-361 — `if (ts) zgettime_monotonic_if_available(ts);`
if let Some(ts) = ts {
zgettime_monotonic_if_available(ts);
}
// c:368-370 — `queue_signals(); pid = fork(); unqueue_signals();`
queue_signals(); // c:368
pid = unsafe { libc::fork() }; // c:369
unqueue_signals(); // c:370
// c:371-374 — fork failure.
if pid == -1 {
// c:371
zerr(&format!(
// c:372
"fork failed: {}",
std::io::Error::last_os_error()
));
return -1; // c:373
}
// c:375-379 — child: re-apply rlimits (HAVE_GETRLIMIT path).
#[cfg(unix)]
if pid == 0 {
// c:376
let _ = setlimits(""); // c:378
}
pid // c:380
}
/// Port of `void loadautofnsetfile(Shfunc shf, char *fdir)` from
/// `Src/exec.c:5657`.
///
/// C body:
/// ```c
/// if (!(shf->node.flags & PM_LOADDIR) ||
/// strcmp(shf->filename, fdir) != 0) {
/// dircache_set(&shf->filename, NULL);
/// if (fdir) {
/// shf->node.flags |= PM_LOADDIR;
/// dircache_set(&shf->filename, fdir);
/// } else {
/// shf->node.flags &= ~PM_LOADDIR;
/// shf->filename = ztrdup(shf->node.nam);
/// }
/// }
/// ```
///
/// Update `shf->filename` to the autoload directory `fdir`. Routes
/// through the refcounted `dircache_set` so identical directory
/// strings are shared across shfunc table entries.
pub fn loadautofnsetfile(shf: &mut shfunc, fdir: Option<&str>) {
// c:5657
// c:5664-5665 — `if (!(shf->node.flags & PM_LOADDIR) || strcmp(shf->filename, fdir) != 0)`
let loaddir = (shf.node.flags as u32 & PM_LOADDIR) != 0;
let same = match (&shf.filename, fdir) {
(Some(a), Some(b)) => a == b,
_ => false,
};
if !loaddir || !same {
// c:5664
// c:5667 — `dircache_set(&shf->filename, NULL);` — refcount-drop old.
dircache_set(&mut shf.filename, None);
if let Some(fdir) = fdir {
// c:5668
shf.node.flags |= PM_LOADDIR as i32; // c:5670
dircache_set(&mut shf.filename, Some(fdir)); // c:5671
} else {
// c:5672
shf.node.flags &= !(PM_LOADDIR as i32); // c:5674
shf.filename = Some(shf.node.nam.clone()); // c:5675 `ztrdup(shf->node.nam)`
}
}
}
/// Port of `int commandnotfound(char *arg0, LinkList args)` from
/// `Src/exec.c:669`.
///
/// C body:
/// ```c
/// Shfunc shf = (Shfunc)
/// shfunctab->getnode(shfunctab, "command_not_found_handler");
/// if (!shf) {
/// lastval = 127;
/// return 1;
/// }
/// pushnode(args, arg0);
/// lastval = doshfunc(shf, args, 1);
/// return 0;
/// ```
///
/// Look up the user-defined `command_not_found_handler` shfunc and
/// invoke it with `arg0` prepended to `args`. Returns 0 if handled,
/// 1 if no handler (so caller emits the standard "command not found"
/// error). Sets `$?` to 127 in the no-handler path.
pub fn commandnotfound(arg0: &str, args: &mut Vec<String>) -> i32 {
// c:669
// c:671-672 — `shf = shfunctab->getnode(shfunctab, "command_not_found_handler");`
let has_handler = shfunctab_lock()
.read()
.map(|t| t.get("command_not_found_handler").is_some())
.unwrap_or(false);
if !has_handler {
// c:674
LASTVAL.store(127, Ordering::Relaxed); // c:675
return 1; // c:676
}
// c:679 — `pushnode(args, arg0);` — prepend arg0 (handler name
// is the first positional arg per C convention).
args.insert(0, arg0.to_string());
args.insert(0, "command_not_found_handler".to_string());
// c:680 — `lastval = doshfunc(shf, args, 1);` — dispatch through
// execshfunc (exec.rs:5009), which swaps PPARAMS, runshfuncs the
// body, and updates LASTVAL. doshfunc itself isn't ported; this
// covers the noreturnval=1 contract via execshfunc's
// PPARAMS-save/restore wrap.
let shf_clone: Option<crate::ported::zsh_h::shfunc> = shfunctab_lock()
.read()
.ok()
.and_then(|t| t.get("command_not_found_handler").cloned());
if let Some(mut shf) = shf_clone {
crate::ported::exec::execshfunc(&mut shf, args);
}
0 // c:681
}
/// Port of `char *namedpipe(void)` from `Src/exec.c:5001`.
///
/// C body (#ifdef HAVE_FIFOS branch):
/// ```c
/// char *tnam = gettempname(NULL, 1);
/// if (!tnam) {
/// zerr("failed to create named pipe: %e", errno);
/// return NULL;
/// }
/// if (mkfifo(tnam, 0600) < 0) {
/// zerr("failed to create named pipe: %s, %e", tnam, errno);
/// return NULL;
/// }
/// return tnam;
/// ```
///
/// Create a FIFO with a unique name for process substitution. Used by
/// `getproc` (`<(cmd)` / `>(cmd)`) on systems without `/dev/fd`.
pub fn namedpipe() -> Option<String> {
// c:5001
let tnam = gettempname(None, true); // c:5003
let tnam = match tnam {
Some(t) => t,
None => {
// c:5005
zerr(&format!(
// c:5006
"failed to create named pipe: {}",
std::io::Error::last_os_error()
));
return None; // c:5007
}
};
// c:5010 — `mkfifo(tnam, 0600)`.
let cstr = match std::ffi::CString::new(tnam.as_str()) {
Ok(c) => c,
Err(_) => return None,
};
if unsafe { libc::mkfifo(cstr.as_ptr(), 0o600) } < 0 {
// c:5010
zerr(&format!(
// c:5014
"failed to create named pipe: {}, {}",
tnam,
std::io::Error::last_os_error()
));
return None; // c:5015
}
Some(tnam) // c:5017
}
/// Port of `Eprog parsecmd(char *cmd, char **eptr)` from `Src/exec.c:4878`.
///
/// C body:
/// ```c
/// char *str;
/// Eprog prog;
/// for (str = cmd + 2; *str && *str != Outpar; str++);
/// if (!*str || cmd[1] != Inpar) {
/// char *errstr = dupstrpfx(cmd, 2);
/// untokenize(errstr);
/// zerr("unterminated `%s...)'", errstr);
/// return NULL;
/// }
/// *str = '\0';
/// if (eptr) *eptr = str+1;
/// if (!(prog = parse_string(cmd + 2, 0))) {
/// zerr("parse error in process substitution");
/// return NULL;
/// }
/// return prog;
/// ```
///
/// Port of `static LinkList readoutput(int in, int qt, int *readerror)`
/// from `Src/exec.c:4805`. Drain a command-substitution pipe fd and
/// return the captured output split per `qt`.
///
/// `qt=1` (quoted-substitution `"$(...)"`): single-element vec with
/// the trailing-newline-trimmed buffer (empty buffer → `Nularg` sentinel
/// per c:4861).
/// `qt=0` (unquoted `$(...)`): split on IFS via `spacesplit`; if
/// `GLOBSUBST` is set, each word is `shtokenize`d for downstream globbing.
///
/// `readerror` is set to the errno on read failure, 0 on clean EOF.
pub fn readoutput(in_fd: i32, qt: i32, readerror: &mut i32) -> Vec<String> {
// c:4805
let mut buf: Vec<u8> = Vec::with_capacity(64); // c:4816 (initial bsiz=64)
let mut readret: isize = 0; // c:4818 readret tracks last read return
// c:4824 dont_queue_signals(); c:4825 child_unblock(); — signal-queue
// dance keeps SIGCHLD live so the foreground process can be reaped
// while we drain. zshrs's in-process command-sub runs without the
// queue (no fork), but the C call surface is preserved for parity.
dont_queue_signals(); // c:4824
child_unblock(); // c:4825
let mut inbuf = [0u8; 64]; // c:4815 inbuf[64]
loop {
// c:4826
// c:4828 — `readret = read(in, inbuf, 64);`
let r = unsafe {
libc::read(
in_fd,
inbuf.as_mut_ptr() as *mut libc::c_void,
inbuf.len(),
)
};
readret = r as isize;
if readret <= 0 {
// c:4829
if readret < 0 && std::io::Error::last_os_error().raw_os_error() == Some(libc::EINTR) {
// c:4830 — `if (readret < 0 && errno == EINTR) continue;`
continue;
}
break; // c:4832
}
// c:4835 — `for (bufptr = inbuf; bufptr < inbuf + readret; bufptr++)`
for i in 0..(readret as usize) {
let c = inbuf[i];
if crate::ported::ztype_h::imeta(c) {
// c:4837 — `if (imeta(c)) { *ptr++ = Meta; c ^= 32; cnt++; }`
buf.push(Meta as u8); // c:4838
buf.push(c ^ 32); // c:4839 (Meta-encoded payload)
} else {
buf.push(c); // c:4848 *ptr++ = c
}
}
}
child_block(); // c:4854
// c:4855 — `if (readerror) *readerror = readret < 0 ? errno : 0;`
*readerror = if readret < 0 {
std::io::Error::last_os_error().raw_os_error().unwrap_or(0)
} else {
0
};
// c:4857 — `close(in);`
unsafe {
libc::close(in_fd);
}
// c:4858-4859 — `while (cnt && ptr[-1] == '\n') ptr--, cnt--;`
while buf.last() == Some(&b'\n') {
buf.pop();
}
// c:4861-4863 — qt branch: empty → Nularg sentinel; else single elem.
let s = String::from_utf8_lossy(&buf).into_owned();
if qt != 0 {
// c:4861
if buf.is_empty() {
return vec![String::from(crate::ported::zsh_h::Nularg)]; // c:4862
}
return vec![s]; // c:4864
}
// c:4866-4871 — `spacesplit` + per-word GLOBSUBST `shtokenize`.
let mut words = crate::ported::utils::spacesplit(&s, false); // c:4867
if isset(crate::ported::zsh_h::GLOBSUBST) {
// c:4870
for w in words.iter_mut() {
crate::ported::glob::shtokenize(w); // c:4870
}
}
words
}
/// Lex a `<(...)`/`>(...)`/`=(...)` body — the leading 2 chars are
/// the marker pair (`Inang+Inpar`, `Outang+Inpar`, `Equals+Inpar`),
/// remainder is the command up to the matching `Outpar`. Returns the
/// parsed Eprog (and writes the post-`)` cursor through `eptr`).
pub fn parsecmd(cmd: &str, eptr: Option<&mut usize>) -> Option<eprog> {
// c:4878
let bytes = cmd.as_bytes();
// c:4883 — `for (str = cmd + 2; *str && *str != Outpar; str++);`
if bytes.len() < 2 {
return None;
}
let mut str_idx: usize = 2;
while str_idx < bytes.len() && (bytes[str_idx] as char) != Outpar {
str_idx += 1;
}
// c:4884 — `if (!*str || cmd[1] != Inpar)`.
if str_idx >= bytes.len() || (bytes[1] as char) != Inpar {
// c:4884
let errstr = if bytes.len() >= 2 {
untokenize(&cmd[..2]) // c:4891-4892
} else {
String::new()
};
zerr(&format!("unterminated `{}...)'", errstr)); // c:4893
return None; // c:4894
}
// c:4896 — `*str = '\0';` — cmd[str_idx] becomes the terminator.
// c:4897-4898 — `if (eptr) *eptr = str + 1;`
if let Some(p) = eptr {
*p = str_idx + 1;
}
// c:4899 — `parse_string(cmd + 2, 0)`.
let body = &cmd[2..str_idx];
let prog = parse_string(body, 0);
if prog.is_none() {
// c:4899
zerr("parse error in process substitution"); // c:4900
return None; // c:4901
}
prog // c:4903
}
/// `POUNDBANGLIMIT` from `Src/exec.c:500` — max bytes read from the
/// front of a script when probing for a `#!` shebang line.
pub const POUNDBANGLIMIT: usize = 128;
/// Port of `static char **makecline(LinkList list)` from `Src/exec.c:2046`.
///
/// Builds the argv array from a command's args list. The C version
/// allocates with a 4-slot prepad (2 reserved at the front for the
/// shebang `argv[-1]/argv[-2]` overwrite trick in zexecve) — Rust
/// doesn't need this since we rebuild the Vec on shebang re-exec
/// (see zexecve WARNING e).
///
/// XTRACE side-effect: each arg is printed via quotedzputs to xtrerr
/// (stderr), preceded by the PS4 prefix when first command of the line.
pub fn makecline(list: &[String]) -> Vec<String> {
// c:2046
if isset(XTRACE) {
// c:2055
if doneps4.load(Ordering::Relaxed) == 0 {
// c:2056
printprompt4(); // c:2057
}
let mut first = true;
let mut err = std::io::stderr().lock();
use std::io::Write;
for s in list.iter() {
// c:2059
if !first {
let _ = err.write_all(b" "); // c:2063
}
first = false;
let _ = err.write_all(quotedzputs(s).as_bytes()); // c:2061
}
let _ = err.write_all(b"\n"); // c:2065
let _ = err.flush(); // c:2066
}
list.to_vec() // c:2071-2072 — argv built; null terminator implicit in CString[] conversion
}
/// Port of `static void execute(LinkList args, int flags, int defpath)`
/// from `Src/exec.c:723`. The canonical "child runs the simple
/// external command" path: STTY/ARGV0/BINF_DASH handling, makecline,
/// closem(FDT_XTRACE) + child_unblock, slash-path direct exec,
/// defpath (`command -p`) search, cmdnamtab + $PATH walk, with
/// commandnotfound-handler fallback and the final exit-code escape
/// (127 not-found / 126 noperm).
///
/// =================== WARNING — DIVERGENCE ====================
/// (a) `cmdnamtab->getnode(cmdnamtab, arg0)` (c:824) — HASHED
/// fast-path wired via cmdnamtab_lock(); jumps direct to
/// `cn.cmd` absolute path before the $PATH scan. Unhashed
/// cursor-walk (c:830-846) still falls to the full $PATH scan;
/// observable behavior matches C when the hash hit is HASHED.
/// (b) `commandnotfound(arg0, args)` (c:809, 873) calls into the
/// not-yet-ported `doshfunc` for the `command_not_found_handler`
/// shell function. Already routes through executor dispatch
/// (see exec.rs:2783).
/// (c) `_realexit()` (c:810, 874) — bare `std::process::exit`.
/// (d) `SHTTY` close on `!FD_CLOEXEC` (c:781-784) — Rust assumes
/// FD_CLOEXEC platform default (macOS, Linux).
/// (e) `path` Rust accessor uses paramtab lookup for "PATH";
/// `defpath` (`command -p`) walks DEFAULT_PATH via
/// search_defpath (already ported).
/// =============================================================
pub fn execute(args: &mut Vec<String>, flags: u32, defpath: i32) {
// c:723
let mut eno: i32 = 0;
let mut ee: i32; // c:729
let mut arg0 = if args.is_empty() {
return;
} else {
args[0].clone()
}; // c:731
// c:733-748 — STTY pre-exec handling.
{
let mut stty = STTYval.lock().unwrap();
if let Some(s) = stty.take() {
// c:738 — STTYval = 0 to break recursion.
if !s.is_empty()
&& unsafe { libc::isatty(0) } != 0
&& unsafe { libc::tcgetpgrp(0) } == unsafe { libc::getpid() }
{
drop(stty);
let cmd = format!("stty {}", s); // c:739
execstring(&cmd, 1, 0, "stty"); // c:743
}
}
}
// c:752-763 — ARGV0 override.
if let Some(z) = zgetenv("ARGV0") {
args[0] = z.clone(); // c:753
unsafe {
let key = std::ffi::CString::new("ARGV0").unwrap();
libc::unsetenv(key.as_ptr()); // c:760
}
arg0 = args[0].clone();
} else if (flags & BINF_DASH) != 0 {
// c:764 — `BINF_DASH` prepends `-`.
args[0] = format!("-{}", arg0); // c:767-768
arg0 = args[0].clone();
}
let argv = makecline(args); // c:771
let newenvp_owned: Option<Vec<String>> = if (flags & BINF_CLEARENV) != 0 {
Some(Vec::new()) // c:772-773 — blank_env: char ** with only NULL slot
} else {
None
};
let newenvp = newenvp_owned.as_deref();
closem(FDT_XTRACE, 0); // c:779
// c:780-785 — !FD_CLOEXEC SHTTY close — WARNING (d).
child_unblock(); // c:786
if arg0.len() >= libc::PATH_MAX as usize {
// c:787
zerr(&format!("command too long: {}", arg0)); // c:788
unsafe {
libc::_exit(1);
} // c:789
}
// c:791-801 — slash in arg0 → direct exec.
if let Some(slash_pos) = arg0.find('/') {
let lerrno = zexecve(&arg0, &argv, newenvp); // c:793
let is_dot = arg0.starts_with('.')
&& (slash_pos == 1 || (arg0.len() > 2 && &arg0[..2] == ".." && slash_pos == 2));
if slash_pos == 0 || unset(PATHDIRS) || is_dot {
// c:794
zerr(&format!(
"{}: {}",
std::io::Error::from_raw_os_error(lerrno),
arg0
)); // c:797
let code = if lerrno == libc::EACCES || lerrno == libc::ENOEXEC {
126
} else {
127
};
unsafe {
libc::_exit(code);
} // c:798
}
}
if defpath != 0 {
// c:804 — `command -p` default-path search.
let pbuf = match search_defpath(&arg0, libc::PATH_MAX as usize) {
Some(p) => p, // c:808
None => {
if commandnotfound(&arg0, args) == 0 {
// c:809
unsafe {
libc::_exit(LASTVAL.load(Ordering::Relaxed));
}
}
zerr(&format!("command not found: {}", arg0)); // c:811
unsafe {
libc::_exit(127);
} // c:812
}
};
ee = zexecve(&pbuf, &argv, newenvp); // c:815
let dir = pbuf.rsplit_once('/').map(|(d, _)| d).unwrap_or("");
if isgooderr(ee, if dir.is_empty() { "/" } else { dir }) {
// c:819
eno = ee;
}
} else {
// c:822 — cmdnamtab fast-path: if `arg0` is a hashed cmdnam,
// jump straight to the absolute path stored in `cn.cmd`,
// skipping the full $PATH scan (one exec attempt vs N).
// c:824 — `if ((cn = cmdnamtab->getnode(cmdnamtab, arg0)))`.
let hashed_path: Option<String> = {
let tab = crate::ported::hashtable::cmdnamtab_lock().read().ok();
tab.and_then(|t| {
t.get(&arg0).and_then(|cn| {
if (cn.node.flags & crate::ported::zsh_h::HASHED) != 0 {
// c:827-828 — `strcpy(nn, cn->u.cmd);`
cn.cmd.clone()
} else {
None
}
})
})
};
if let Some(nn) = hashed_path {
// c:848 — `ee = zexecve(nn, argv, newenvp);`
ee = zexecve(&nn, &argv, newenvp);
let dir = nn.rsplit_once('/').map(|(d, _)| d).unwrap_or("");
if isgooderr(ee, if dir.is_empty() { "/" } else { dir }) {
eno = ee;
}
// If the hashed entry's exec failed without a "good" error,
// we still need the $PATH fallback — fall through.
if eno == 0 && ee != 0 {
// Reset for the $PATH scan below.
ee = 0;
}
}
// c:822 — normal $PATH scan (always runs; cmdnam fast-path was an
// optimization but C also walks the rest of `path` if the hashed
// exec failed with a non-"good" error).
let path_str = getsparam("PATH").unwrap_or_default();
for pp in path_str.split(':') {
if pp.is_empty() || pp == "." {
// c:856
ee = zexecve(&arg0, &argv, newenvp); // c:857
if isgooderr(ee, pp) {
eno = ee;
}
} else {
// c:860
let candidate = format!("{}/{}", pp, arg0); // c:861-864
ee = zexecve(&candidate, &argv, newenvp); // c:865
if isgooderr(ee, pp) {
eno = ee;
}
}
}
}
// c:871-881 — final error reporting.
if eno != 0 {
// c:871
zerr(&format!(
"{}: {}",
std::io::Error::from_raw_os_error(eno),
arg0
)); // c:872
} else if commandnotfound(&arg0, args) == 0 {
// c:873
unsafe {
libc::_exit(LASTVAL.load(Ordering::Relaxed));
} // c:874
} else {
zerr(&format!("command not found: {}", arg0)); // c:876
}
let code = if eno == libc::EACCES || eno == libc::ENOEXEC {
126
} else {
127
}; // c:881
unsafe {
libc::_exit(code);
}
}
/// Port of `static int zexecve(char *pth, char **argv, char **newenvp)`
/// from `Src/exec.c:504`. Wraps `execve(2)` with:
/// - `$_` env var stamped to absolute `pth` (c:514-520)
/// - winch signal unblock right before the syscall (c:527)
/// - on `ENOEXEC` / `ENOENT`: reads the first POUNDBANGLIMIT
/// bytes, parses a `#!interp arg` shebang and re-execs the
/// interpreter (c:534-628). For `ENOEXEC` with no shebang,
/// binary-safety check then falls back to `/bin/sh script` per
/// POSIX (c:588-628).
///
/// Returns `errno` from the failing exec — execve only returns on
/// failure, so success means the calling process is already replaced.
///
/// =================== WARNING — DIVERGENCE ====================
/// (a) C uses `static char buf[PATH_MAX*2+1]` for the `_=...` env
/// string; Rust uses a stack `String` (consumed by `zputenv`).
/// (b) `closedumps()` for `!FD_CLOEXEC` (c:521-523) called
/// unconditionally as a no-op when FD_CLOEXEC is platform default.
/// (c) `unmetafy(pth, NULL)` / round-trip `metafy` at c:510-513,
/// c:639-642 — handled implicitly via &str ↔ CString.
/// (d) `metafy(execvebuf+2, -1, META_STATIC)` (c:551, 575) — we
/// drop the metafy and pass byte ranges to zerr directly.
/// (e) `argv[-1]` / `argv[-2]` shebang interpreter slot-overwriting
/// (C overwrites BEFORE `argv[0]`) — Rust rebuilds a fresh
/// `Vec<String>` with interp + optional arg + original argv tail
/// since Vec doesn't expose negative indexing.
/// (f) `environ` is FFI-loaded only when `newenvp` is None.
/// =============================================================
pub fn zexecve(pth: &str, argv: &[String], newenvp: Option<&[String]>) -> i32 {
// c:504
use std::ffi::CString;
// c:514-520 — `_=pth` env stamping.
let pth_abs = if pth.starts_with('/') {
// c:516
pth.to_string() // c:517
} else {
// c:518
format!("{}/{}", getsparam("PWD").unwrap_or_default(), pth) // c:519
};
zputenv(&format!("_={}", pth_abs)); // c:520
closedumps(); // c:522
winch_unblock(); // c:527
let cpth = match CString::new(pth) {
Ok(c) => c,
Err(_) => return libc::ENOENT,
};
let cargs: Vec<CString> = argv
.iter()
.filter_map(|a| CString::new(a.as_str()).ok())
.collect();
let mut argv_ptrs: Vec<*const libc::c_char> =
cargs.iter().map(|c| c.as_ptr()).collect();
argv_ptrs.push(std::ptr::null());
let env_holder: Vec<CString>;
let env_ptrs: Vec<*const libc::c_char>;
let envp: *const *const libc::c_char = match newenvp {
Some(env) => {
env_holder = env
.iter()
.filter_map(|e| CString::new(e.as_str()).ok())
.collect();
env_ptrs = {
let mut v: Vec<*const libc::c_char> =
env_holder.iter().map(|c| c.as_ptr()).collect();
v.push(std::ptr::null());
v
};
env_ptrs.as_ptr()
}
None => unsafe {
extern "C" {
static environ: *const *const libc::c_char;
}
environ
},
};
unsafe {
libc::execve(cpth.as_ptr(), argv_ptrs.as_ptr(), envp); // c:528
}
let eno = std::io::Error::last_os_error()
.raw_os_error()
.unwrap_or(libc::ENOEXEC); // c:534
if eno == libc::ENOEXEC || eno == libc::ENOENT {
// c:534
let fd = unsafe { libc::open(cpth.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY) }; // c:538
if fd < 0 {
return std::io::Error::last_os_error()
.raw_os_error()
.unwrap_or(libc::ENOENT); // c:634
}
let mut buf = vec![0u8; POUNDBANGLIMIT + 1]; // c:541
let ct = unsafe {
libc::read(
fd,
buf.as_mut_ptr() as *mut libc::c_void,
POUNDBANGLIMIT as libc::size_t,
)
}; // c:542
unsafe {
libc::close(fd);
} // c:543
if ct >= 0 {
// c:544
let ct = ct as usize;
if ct >= 2 && buf[0] == b'#' && buf[1] == b'!' {
// c:545
let mut t0 = 0;
while t0 < ct && buf[t0] != b'\n' {
t0 += 1;
} // c:546-548
if t0 == ct {
// c:549
zerr(&format!(
// c:550
"{}: bad interpreter: {}: {}",
pth,
String::from_utf8_lossy(&buf[2..t0.min(ct)]),
std::io::Error::from_raw_os_error(eno)
));
} else {
// c:552
while t0 > 0
&& (buf[t0] == b' ' || buf[t0] == b'\t' || buf[t0] == b'\n')
{
buf[t0] = 0;
t0 -= 1;
} // c:553-554
let mut ptr_lo: usize = 2;
while ptr_lo < buf.len() && buf[ptr_lo] == b' ' {
ptr_lo += 1;
} // c:555
let ptr2_lo = ptr_lo;
let mut ptr_hi = ptr2_lo;
while ptr_hi < buf.len()
&& buf[ptr_hi] != 0
&& buf[ptr_hi] != b' '
{
ptr_hi += 1;
} // c:556
let interp_str =
String::from_utf8_lossy(&buf[ptr2_lo..ptr_hi]).into_owned();
if eno == libc::ENOENT {
// c:557 — pathprog rewrite path.
let pprog = if !interp_str.starts_with('/') {
// c:561
pathprog(&interp_str)
.map(|p| p.display().to_string())
} else {
None
};
if let Some(pprog) = pprog {
// c:562
let mut argv_new: Vec<String> =
Vec::with_capacity(argv.len() + 2);
argv_new.push(interp_str.clone()); // c:564
if ptr_hi >= buf.len() || buf[ptr_hi] == 0 {
argv_new.push(pth.to_string());
} else {
// c:567
let mut rest_lo = ptr_hi + 1;
while rest_lo < buf.len() && buf[rest_lo] == b' ' {
rest_lo += 1;
}
let mut rest_hi = rest_lo;
while rest_hi < buf.len() && buf[rest_hi] != 0 {
rest_hi += 1;
}
let arg_str =
String::from_utf8_lossy(&buf[rest_lo..rest_hi])
.into_owned();
argv_new.push(arg_str);
argv_new.push(pth.to_string());
}
for orig in argv.iter().skip(1) {
argv_new.push(orig.clone());
}
winch_unblock(); // c:565/c:570
return zexecve(&pprog, &argv_new, newenvp); // c:566/c:571
}
zerr(&format!(
// c:574
"{}: bad interpreter: {}: {}",
pth,
interp_str,
std::io::Error::from_raw_os_error(eno)
));
} else if ptr_hi < buf.len() && buf[ptr_hi] != 0 {
// c:576
let mut rest_lo = ptr_hi + 1;
while rest_lo < buf.len() && buf[rest_lo] == b' ' {
rest_lo += 1;
}
let mut rest_hi = rest_lo;
while rest_hi < buf.len() && buf[rest_hi] != 0 {
rest_hi += 1;
}
let arg_str =
String::from_utf8_lossy(&buf[rest_lo..rest_hi]).into_owned();
let mut argv_new: Vec<String> = vec![
interp_str.clone(),
arg_str,
pth.to_string(),
];
for orig in argv.iter().skip(1) {
argv_new.push(orig.clone());
}
winch_unblock(); // c:580
return zexecve(&interp_str, &argv_new, newenvp); // c:581
} else {
// c:582
let mut argv_new: Vec<String> =
vec![interp_str.clone(), pth.to_string()];
for orig in argv.iter().skip(1) {
argv_new.push(orig.clone());
}
winch_unblock(); // c:584
return zexecve(&interp_str, &argv_new, newenvp); // c:585
}
}
} else if eno == libc::ENOEXEC {
// c:588 — binary-safety + /bin/sh fallback.
let nul_pos = buf[..ct].iter().position(|&b| b == 0); // c:597
let isbinary = match nul_pos {
None => false, // c:598
Some(npos) => {
let mut has_letter = false;
let mut binary = true;
for &b in &buf[..npos] {
// c:602-609
if (b as char).is_ascii_lowercase()
|| b == b'$'
|| b == b'`'
{
has_letter = true;
}
if has_letter && b == b'\n' {
binary = false; // c:606
break;
}
}
binary
}
};
if !isbinary {
// c:611
let mut argv_new: Vec<String> = Vec::with_capacity(argv.len() + 2);
argv_new.push("sh".to_string()); // c:625
if !argv.is_empty()
&& (argv[0].starts_with('-') || argv[0].starts_with('+'))
{
argv_new.push("-".to_string()); // c:623
}
for orig in argv.iter() {
argv_new.push(orig.clone());
}
winch_unblock(); // c:626
return zexecve("/bin/sh", &argv_new, newenvp); // c:627
}
}
}
}
eno // c:643
}
/// Port of `char *getoutputfile(char *cmd, char **eptr)` from
/// `Src/exec.c:4910` — `=(cmd)` process substitution.
///
/// Substitutes the cmd's stdout into a temp file, returns the
/// filename. Optimised path: `=(<<<heredoc-str)` writes the
/// heredoc body directly without a fork.
///
/// (a) `addfilelist(nam, 0)` (c:4960) wired via `JOBTAB[thisjob]`
/// so the temp file gets cleaned at job exit.
/// (b) `waitforpid` Rust takes 1 arg `pid`, C takes `(pid, full)`.
/// Behavior matches the `full=0` case anyway.
/// (c) `entersubsh` is ported at exec.rs:3934 — wire it here when
/// re-routing the fork path away from setsid-only fallback.
/// (d) `execode` is now ported (exec.rs:6047) — the body still
/// re-feeds through fusevm for cache coherence with execstring.
/// (e) `_realexit` flushes stdio + jobs + history. We use bare
/// `std::process::exit(0)` for now.
/// (f) TMPSUFFIX link()-rename block (c:4951-4958) deferred; rare
/// `setopt suffix_alias` interaction with =(…).
pub fn getoutputfile(cmd: &str, eptr: Option<&mut usize>) -> Option<String> {
// c:4910
let bytes = cmd.as_bytes();
let _ = bytes;
// c:4918 — `if (thisjob == -1)` — guard removed (thisjob model differs).
let mut ends_at: usize = 0;
let prog = parsecmd(cmd, Some(&mut ends_at))?; // c:4922
if let Some(p) = eptr {
*p = ends_at;
}
let mut nam = gettempname(None, true)?; // c:4924
// c:4927 — `simple_redir_name` opt for `=(<<<str)`.
let mut s: Option<String> = simple_redir_name(&prog, REDIR_HERESTR).map(|raw| {
// c:4933
let mut sub = singsub(&raw); // c:4933
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
// c:4934
String::new() // c:4935 — sentinel; checked below
} else {
sub = untokenize(&sub); // c:4937
dyncat(&sub, "\n") // c:4938
}
});
if let Some(ref sv) = s {
if sv.is_empty() {
s = None;
}
}
if s.is_none() {
// c:4942
child_block(); // c:4943
}
// c:4945 — `open(nam, O_WRONLY|O_CREAT|O_EXCL|O_NOCTTY, 0600)`.
let c_nam = match std::ffi::CString::new(nam.clone()) {
Ok(c) => c,
Err(_) => {
if s.is_none() {
child_unblock();
}
return None;
}
};
let fd = unsafe {
libc::open(
c_nam.as_ptr(),
libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | libc::O_NOCTTY,
0o600 as libc::c_uint,
)
};
if fd < 0 {
// c:4945
zerr(&format!(
"process substitution failed: {}",
std::io::Error::last_os_error()
)); // c:4946
if s.is_none() {
child_unblock(); // c:4948
}
return None; // c:4949
}
// c:4951-4958 — TMPSUFFIX link block (see WARNING f).
// c:4960 — `addfilelist(nam, 0);` — register temp file in current
// job's filelist so it's unlinked at job exit (not relying on the
// OS temp-reaper).
if let Some(jt) = crate::ported::jobs::JOBTAB.get() {
let mut guard = jt.lock().unwrap();
let tj = crate::ported::jobs::THISJOB
.get()
.map(|m| *m.lock().unwrap())
.unwrap_or(-1);
if tj >= 0 {
if let Some(j) = guard.get_mut(tj as usize) {
crate::ported::jobs::addfilelist(j, Some(&nam), 0);
}
}
}
if let Some(sv) = s {
// c:4962 — optimised here-string write path.
let mut buf: Vec<u8> = sv.into_bytes();
let _len = unmetafy(&mut buf); // c:4965
let _ = write_loop(fd, &buf); // c:4966
unsafe {
libc::close(fd);
} // c:4967
return Some(nam); // c:4968
}
// c:4971 — `cmdoutpid = pid = zfork(NULL)`.
let pid = zfork(None);
cmdoutpid.store(pid, Ordering::Relaxed);
if pid == -1 {
// c:4972
unsafe {
libc::close(fd);
} // c:4973
child_unblock(); // c:4974
return Some(nam); // c:4975
} else if pid != 0 {
// c:4976 — parent.
unsafe {
libc::close(fd);
} // c:4977
let _ = waitforpid(pid); // c:4978
cmdoutval.store(0, Ordering::Relaxed); // c:4979
return Some(nam); // c:4980
}
// c:4983 — child.
closem(FDT_UNUSED, 0); // c:4984
let _ = redup(fd, 1); // c:4985
entersubsh(esub::PGRP | esub::NOMONITOR, None); // c:4986
cmdpush(CS_CMDSUBST as u8); // c:4987
// c:4988 — execode — WARNING (d).
let body_end = if ends_at > 0 { ends_at - 1 } else { 2 };
let body = if body_end > 2 && body_end <= cmd.len() {
&cmd[2..body_end]
} else {
""
};
let _ = crate::ported::exec_hooks::execute_script_zsh_pipeline(body);
cmdpop(); // c:4989
unsafe {
libc::close(1);
} // c:4990
// _realexit — WARNING (e)
std::process::exit(0); // c:4991
#[allow(unreachable_code)]
{
// c:4992-4993 — `zerr("exit returned in child!!"); kill(getpid(), SIGKILL);`
let _ = &mut nam;
unsafe {
libc::kill(libc::getpid(), libc::SIGKILL);
}
None
}
}
/// Port of `char *getproc(char *cmd, char **eptr)` from
/// `Src/exec.c:5025` — `<(cmd)` / `>(cmd)` process substitution
/// via `/dev/fd/N` (PATH_DEV_FD branch; modern Linux/macOS).
///
/// (a) PATH_DEV_FD branch only — the FIFO fallback (`!PATH_DEV_FD`
/// path c:5037-5064) is omitted; modern Linux/macOS both
/// provide /dev/fd. `namedpipe()` is ported (exec.rs:2701) but
/// unused here.
/// (b) `addproc` is 7-arg; procsubst pid recorded via aux=true on
/// the current job (c:5141-5142).
/// (c) `addfilelist(NULL, fd)` wired via `JOBTAB[thisjob]` at
/// c:5087.
/// (d) `entersubsh` is ported at exec.rs:3934 — wired below at
/// c:5063 (`entersubsh(ESUB_ASYNC|ESUB_PGRP, NULL)`).
/// (e) `execode` is ported at exec.rs:6047. Body still re-feeds
/// through fusevm for cache coherence.
/// (f) `_realexit` flushes stdio + jobs + history. We use bare
/// `std::process::exit(LASTVAL)` for now.
/// (g) `fdtable[fd] = FDT_PROC_SUBST` (c:5086) — set via fdtable_set.
pub fn getproc(cmd: &str, eptr: Option<&mut usize>) -> Option<String> {
// c:5025
let bytes = cmd.as_bytes();
let out: i32 = if !bytes.is_empty() && (bytes[0] as char) == Inang {
1 // c:5032 — `<(...)` writer-side child
} else {
0
};
// c:5068-5071 — `if (thisjob == -1) { zerr(...); return NULL; }` —
// proc subst needs a host job to attach the child to.
let tj_check = crate::ported::jobs::THISJOB
.get()
.map(|m| *m.lock().unwrap())
.unwrap_or(-1);
if tj_check == -1 {
zerr(&format!(
"process substitution {} cannot be used here",
cmd
)); // c:5069
return None; // c:5070
}
// c:5072 — PATH_DEV_FD path: allocate buffer for the /dev/fd/N string.
let mut ends_at: usize = 0;
let _prog = parsecmd(cmd, Some(&mut ends_at))?; // c:5073
if let Some(p) = eptr {
*p = ends_at;
}
let mut pipes: [i32; 2] = [-1; 2];
if mpipe(&mut pipes) < 0 {
// c:5075
return None;
}
let mut bgtime: ZshTimespec = libc::timespec { tv_sec: 0, tv_nsec: 0 };
let pid = zfork(Some(&mut bgtime)); // c:5077
if pid != 0 {
// c:5077 — parent path.
let pnam = format!("/dev/fd/{}", pipes[(1 - out) as usize]); // c:5078
let _ = zclose(pipes[out as usize]); // c:5079
if pid == -1 {
// c:5080
let _ = zclose(pipes[(1 - out) as usize]); // c:5082
return None; // c:5083
}
let fd = pipes[(1 - out) as usize]; // c:5085
fdtable_set(fd, FDT_PROC_SUBST); // c:5086
// c:5087 — `addfilelist(NULL, fd);` — register the proc-subst
// pipe fd in the current job's filelist so it's closed at job exit.
if let Some(jt) = crate::ported::jobs::JOBTAB.get() {
let mut guard = jt.lock().unwrap();
let tj = crate::ported::jobs::THISJOB
.get()
.map(|m| *m.lock().unwrap())
.unwrap_or(-1);
if tj >= 0 {
if let Some(j) = guard.get_mut(tj as usize) {
crate::ported::jobs::addfilelist(j, None, fd);
}
}
}
// c:5088-5091 — `if (!out) addproc(pid, NULL, 1, &bgtime, -1, -1);` —
// record the proc-subst writer-side child in the job's
// auxprocs (aux=true). For `<(cmd)` (out==1 = reader-side
// child), C omits the addproc — symmetric here.
if out == 0 {
if let Some(jt) = crate::ported::jobs::JOBTAB.get() {
let mut guard = jt.lock().unwrap();
let tj = crate::ported::jobs::THISJOB
.get()
.map(|m| *m.lock().unwrap())
.unwrap_or(-1);
if tj >= 0 {
if let Some(j) = guard.get_mut(tj as usize) {
crate::ported::jobs::addproc(
j, pid, "", true,
Some(std::time::Instant::now()),
-1, -1,
);
}
}
}
}
procsubstpid.store(pid, Ordering::Relaxed); // c:5092
return Some(pnam); // c:5093
}
// c:5095 — child.
entersubsh(esub::ASYNC | esub::PGRP, None); // c:5095
let _ = redup(pipes[out as usize], out); // c:5096
closem(FDT_UNUSED, 0); // c:5097
cmdpush(CS_CMDSUBST as u8); // c:5100
let body_end = if ends_at > 0 { ends_at - 1 } else { 2 };
let body = if body_end > 2 && body_end <= cmd.len() {
&cmd[2..body_end]
} else {
""
};
let _ = crate::ported::exec_hooks::execute_script_zsh_pipeline(body);
cmdpop(); // c:5102
let _ = zclose(out); // c:5103
std::process::exit(LASTVAL.load(Ordering::Relaxed)); // c:5104
}
/// Port of `enum { ESUB_ASYNC, ESUB_PGRP, ... };` from `Src/exec.c:1056`.
/// Flag bits for `entersubsh(int flags, struct entersubsh_ret *retp)`.
pub mod esub {
// c:1056
pub const ASYNC: i32 = 0x01; // c:1058
pub const PGRP: i32 = 0x02; // c:1063
pub const KEEPTRAP: i32 = 0x04; // c:1065
pub const FAKE: i32 = 0x08; // c:1067
pub const REVERTPGRP: i32 = 0x10; // c:1069
pub const NOMONITOR: i32 = 0x20; // c:1071
pub const JOB_CONTROL: i32 = 0x40; // c:1073
}
/// Port of `struct entersubsh_ret` from `Src/exec.c` (forward decl).
/// Out-arg used by `entersubsh()` to hand back the group-leader pid
/// and the list-pipe job index the parent should track. Only filled
/// in for `ESUB_PGRP` + non-async forks (synchronous pipeline child
/// groups).
#[allow(non_camel_case_types)]
#[derive(Default)]
pub struct entersubsh_ret {
pub gleader: i32, // c:1122
pub list_pipe_job: i32, // c:1123
}
/// Port of `static void entersubsh(int flags, struct entersubsh_ret *retp)`
/// from `Src/exec.c:1083`. Called by every child fork to switch the
/// process into subshell mode: traps reset, monitor disabled, signals
/// re-defaulted, pgrp + tty handed off, saved fds closed, jobtab
/// cleared, ZSH_SUBSHELL bumped, forklevel = locallevel.
///
/// (a) `jobtab[list_pipe_job]` / `jobtab[thisjob]` pgrp ops (c:1110-
/// 1151) are now ported via `JOBTAB[thisjob]`.gleader access; the
/// ESUB_PGRP+sync path establishes pipeline group-leadership
/// (list_pipe_job inherit or thisjob-as-leader), filling
/// entersubsh_ret with the chosen gleader + list_pipe_job index.
/// (b) `clearjobtab(monitor)` (c:1219) — Rust signature is
/// `clearjobtab(&mut JobTable, monitor)`; we get the global table
/// via a TABLE handle similar to other jobs.rs entries.
/// (c) `attachtty(...)` (c:1119, 1144) — wired via libc::tcsetpgrp(2, gleader).
/// (d) `release_pgrp()` called for ESUB_REVERTPGRP when `getpid() ==
/// mypgrp` — direct C parity (jobs.rs:3406 provides the call).
/// (e) `opts[USEZLE] = 0; zleactive = 0` — Rust opts table lookup
/// uses `opts_set_off(USEZLE)`; zleactive is the atomic in
/// builtins/sched.rs.
/// =============================================================
pub fn entersubsh(flags: i32, retp: Option<&mut entersubsh_ret>) {
// c:1083
let monitor: i32;
let job_control_ok: i32;
// c:1088-1092 — reset traps unless KEEPTRAP.
if (flags & esub::KEEPTRAP) == 0 {
// c:1088
for sig in 0..=SIGCOUNT {
// c:1089
let st = {
let guard = sigtrapped.lock().unwrap();
guard.get(sig as usize).copied().unwrap_or(0)
};
let func_set = (st & ZSIG_FUNC) != 0; // c:1090
let posix_ignored = isset(POSIXTRAPS) && ((st & ZSIG_IGNORED) != 0); // c:1091
if !func_set && !posix_ignored {
unsettrap(sig); // c:1092
}
}
}
monitor = if isset(MONITOR) { 1 } else { 0 }; // c:1093
job_control_ok =
if monitor != 0 && (flags & esub::JOB_CONTROL) != 0 && isset(POSIXJOBS) {
// c:1094
1
} else {
0
};
EXIT_VAL.store(0, Ordering::Relaxed); // c:1095
if (flags & esub::NOMONITOR) != 0 {
// c:1096
dosetopt(MONITOR, 0, 0); // c:1097
}
if !isset(MONITOR) {
// c:1098
if (flags & esub::ASYNC) != 0 {
// c:1099
let _ = settrap(libc::SIGINT, None, 0); // c:1100
let _ = settrap(libc::SIGQUIT, None, 0); // c:1101
if unsafe { libc::isatty(0) } != 0 {
// c:1102
unsafe {
libc::close(0);
} // c:1103
let devnull = std::ffi::CString::new("/dev/null").unwrap();
if unsafe {
libc::open(
devnull.as_ptr(),
libc::O_RDWR | libc::O_NOCTTY,
)
} != 0
{
// c:1104
zerr(&format!(
// c:1105
"can't open /dev/null: {}",
std::io::Error::last_os_error()
));
unsafe {
libc::_exit(1);
} // c:1106
}
}
}
} else if (flags & esub::PGRP) != 0 {
// c:1110 — `else if (thisjob != -1 && (flags & ESUB_PGRP))`.
let thisjob = crate::ported::jobs::THISJOB
.get()
.map(|m| *m.lock().unwrap())
.unwrap_or(-1);
if thisjob != -1 {
let lpj = list_pipe_job.load(Ordering::Relaxed);
let lp = list_pipe.load(Ordering::Relaxed);
let lpc = list_pipe_child.load(Ordering::Relaxed);
if let Some(jt) = crate::ported::jobs::JOBTAB.get() {
let mut guard = jt.lock().unwrap();
let lpj_gleader = guard
.get(lpj as usize)
.map(|j| j.gleader)
.unwrap_or(0);
if lpj_gleader != 0 && (lp != 0 || lpc != 0) {
// c:1111-1124 — inherit list_pipe_job's group leader.
let pgid = if unsafe { libc::setpgid(0, lpj_gleader) } == -1
|| (unsafe { libc::killpg(lpj_gleader, 0) } == -1
&& std::io::Error::last_os_error().raw_os_error()
== Some(libc::ESRCH))
{
// c:1115-1117 — primary group leader gone; this child becomes leader.
let new_gl = if lpc != 0 {
mypgrp.load(Ordering::Relaxed)
} else {
unsafe { libc::getpid() }
};
if let Some(j) = guard.get_mut(lpj as usize) {
j.gleader = new_gl;
}
if let Some(j) = guard.get_mut(thisjob as usize) {
j.gleader = new_gl;
}
unsafe { libc::setpgid(0, new_gl) };
if (flags & esub::ASYNC) == 0 {
unsafe { libc::tcsetpgrp(2, new_gl) }; // c:1119 attachtty
}
new_gl
} else {
lpj_gleader
};
if let Some(r) = retp {
if (flags & esub::ASYNC) == 0 {
r.gleader = pgid; // c:1122
r.list_pipe_job = lpj; // c:1123
}
}
} else {
// c:1126-1151 — standard group-leader-takeover path.
let thisjob_gleader = guard
.get(thisjob as usize)
.map(|j| j.gleader)
.unwrap_or(0);
if thisjob_gleader == 0
|| unsafe { libc::setpgid(0, thisjob_gleader) } == -1
{
let new_gl = unsafe { libc::getpid() };
if let Some(j) = guard.get_mut(thisjob as usize) {
j.gleader = new_gl; // c:1138
}
if lpj != thisjob {
let lpj_was_unset = guard
.get(lpj as usize)
.map(|j| j.gleader == 0)
.unwrap_or(true);
if lpj_was_unset {
if let Some(j) = guard.get_mut(lpj as usize) {
j.gleader = new_gl; // c:1140-1141
}
}
}
unsafe { libc::setpgid(0, new_gl) }; // c:1142
if (flags & esub::ASYNC) == 0 {
unsafe { libc::tcsetpgrp(2, new_gl) }; // c:1144 attachtty
if let Some(r) = retp {
r.gleader = new_gl; // c:1146
if lpj != thisjob {
r.list_pipe_job = lpj; // c:1148
}
}
}
}
}
}
} else {
// No real job slot; basic setpgid fallback.
unsafe { libc::setpgid(0, 0) };
}
}
if (flags & esub::FAKE) == 0 {
// c:1153
subsh.store(1, Ordering::Relaxed); // c:1154
}
// c:1161 — `zsh_subshell++;` regardless of FAKE.
zsh_subshell.fetch_add(1, Ordering::Relaxed);
// c:1162 — `if ((flags & ESUB_REVERTPGRP) && getpid() == mypgrp)`.
if (flags & esub::REVERTPGRP) != 0
&& unsafe { libc::getpid() }
== mypgrp.load(Ordering::Relaxed)
{
release_pgrp(); // c:1163
}
*shout.lock().unwrap() = 0; // c:1164 — shout = NULL
if (flags & esub::NOMONITOR) != 0 {
// c:1165
signal_ignore(libc::SIGTTOU); // c:1171
signal_ignore(libc::SIGTTIN); // c:1172
signal_ignore(libc::SIGTSTP); // c:1173
} else if job_control_ok == 0 {
// c:1174
signal_default(libc::SIGTTOU); // c:1181
signal_default(libc::SIGTTIN); // c:1182
signal_default(libc::SIGTSTP); // c:1183
}
let interact = isset(INTERACTIVE); // c:1185 — Rust uses INTERACTIVE option as proxy
if interact {
signal_default(libc::SIGTERM); // c:1186
let int_st = sigtrapped
.lock()
.unwrap()
.get(libc::SIGINT as usize)
.copied()
.unwrap_or(0);
if (int_st & ZSIG_IGNORED) == 0 {
// c:1187
signal_default(libc::SIGINT); // c:1188
}
let pipe_st = sigtrapped
.lock()
.unwrap()
.get(libc::SIGPIPE as usize)
.copied()
.unwrap_or(0);
if pipe_st == 0 {
// c:1189
signal_default(libc::SIGPIPE); // c:1190
}
}
let quit_st = sigtrapped
.lock()
.unwrap()
.get(libc::SIGQUIT as usize)
.copied()
.unwrap_or(0);
if (quit_st & ZSIG_IGNORED) == 0 {
// c:1192
signal_default(libc::SIGQUIT); // c:1193
}
// c:1202-1205 — unblock any trapped signals while in `intrap`.
if intrap.load(Ordering::Relaxed) != 0 {
// c:1202
for sig in 1..=SIGCOUNT {
let st = sigtrapped
.lock()
.unwrap()
.get(sig as usize)
.copied()
.unwrap_or(0);
if st != 0 && st != ZSIG_IGNORED {
// c:1204
let m = signal_mask(sig);
let _ = signal_unblock(&m); // c:1205
}
}
}
if job_control_ok == 0 {
// c:1206
dosetopt(MONITOR, 0, 0); // c:1207
}
dosetopt(USEZLE, 0, 0); // c:1208
zleactive.store(0, Ordering::Relaxed); // c:1209
// c:1214-1217 — close saved fds.
let max = MAX_ZSH_FD.load(Ordering::Relaxed);
for i in 10..=max {
if (fdtable_get(i) & FDT_SAVED_MASK) != 0 {
// c:1215
let _ = zclose(i); // c:1216
}
}
// c:1218-1219 — `clearjobtab(monitor);` — calls the canonical port
// at jobs.rs:1695 which handles ALL the C body including the
// oldjobtab snapshot path (c:1799-1817) under POSIXJOBS guard.
let mut dummy_table = crate::exec_jobs::JobTable::new();
crate::ported::jobs::clearjobtab(&mut dummy_table, monitor);
let _ = get_usage(); // c:1220
FORKLEVEL.store(
// c:1221 — `forklevel = locallevel;`
locallevel.load(Ordering::Relaxed),
Ordering::Relaxed,
);
}
/// Port of `static int getpipe(char *cmd, int nullexec)` from
/// `Src/exec.c:5119`.
///
/// C body executes `<(cmd)` / `>(cmd)` process substitution via a
/// pipe pair: parent gets back the readable (`<(...)`) or writable
/// (`>(...)`) end as an fd; child runs the substituted command with
/// its stdio redirected into the other end.
///
/// ```c
/// Eprog prog;
/// int pipes[2], out = *cmd == Inang;
/// pid_t pid;
/// struct timespec bgtime;
/// char *ends;
/// if (!(prog = parsecmd(cmd, &ends))) return -1;
/// if (*ends) { zerr("invalid syntax..."); return -1; }
/// if (mpipe(pipes) < 0) return -1;
/// if ((pid = zfork(&bgtime))) {
/// zclose(pipes[out]);
/// if (pid == -1) { zclose(pipes[!out]); return -1; }
/// if (!nullexec) addproc(pid, NULL, 1, &bgtime, -1, -1);
/// procsubstpid = pid;
/// return pipes[!out];
/// }
/// entersubsh(ESUB_ASYNC|ESUB_PGRP|ESUB_NOMONITOR, NULL);
/// redup(pipes[out], out);
/// closem(FDT_UNUSED, 0);
/// cmdpush(CS_CMDSUBST);
/// execode(prog, 0, 1, out ? "outsubst" : "insubst");
/// cmdpop();
/// _realexit();
/// ```
///
/// (a) `addproc` is now 7-arg (jobs.rs:1516) — wired at the
/// procsubst pid recording site (c:5141-5142) earlier this
/// session; the child IS now recorded in `JOBTAB[thisjob]`.
/// (b) `entersubsh` IS now ported (exec.rs:3934) including the
/// ESUB_PGRP pipeline group-leadership path — wired this
/// session for getpipe's `entersubsh(ESUB_ASYNC|ESUB_PGRP|
/// ESUB_NOMONITOR, NULL)` call.
/// (c) `execode(prog, ...)` IS now ported (exec.rs:6047) — getpipe
/// can route through execode for the parsed eprog. Currently
/// this caller still uses the fusevm pipeline for cache
/// coherence with execstring; switch over when the wordcode
/// walker becomes the primary path.
/// (d) `_realexit()` flushes stdio + jobs + history. We use bare
/// `std::process::exit(lastval)` for now.
pub fn getpipe(cmd: &str, nullexec: i32) -> i32 {
// c:5119
let bytes = cmd.as_bytes();
let out: i32 = if !bytes.is_empty() && (bytes[0] as char) == Inang {
1 // c:5122 — `<(...)` reads from child, child writes to fd 1
} else {
0 // `>(...)` — child reads from fd 0
};
let mut ends_at: usize = 0;
let prog = parsecmd(cmd, Some(&mut ends_at)); // c:5127
if prog.is_none() {
// c:5127
return -1; // c:5128
}
// c:5129 — `if (*ends)` — trailing bytes after the `)` are invalid.
if ends_at < bytes.len() && bytes[ends_at] != 0 {
zerr("invalid syntax for process substitution in redirection"); // c:5130
return -1; // c:5131
}
let mut pipes: [i32; 2] = [-1; 2];
if mpipe(&mut pipes) < 0 {
// c:5133
return -1;
}
// c:5135 — `if ((pid = zfork(&bgtime)))` — parent path.
let mut bgtime: ZshTimespec = libc::timespec { tv_sec: 0, tv_nsec: 0 };
let pid = zfork(Some(&mut bgtime)); // c:5135
if pid != 0 {
// c:5135 — parent.
let _ = zclose(pipes[out as usize]); // c:5136
if pid == -1 {
// c:5137
let _ = zclose(pipes[(1 - out) as usize]); // c:5138
return -1; // c:5139
}
// c:5141-5142 — `if (!nullexec) addproc(pid, NULL, 1, &bgtime, -1, -1);`
if nullexec == 0 {
if let Some(jt) = crate::ported::jobs::JOBTAB.get() {
let mut guard = jt.lock().unwrap();
let tj = crate::ported::jobs::THISJOB
.get()
.map(|m| *m.lock().unwrap())
.unwrap_or(-1);
if tj >= 0 {
if let Some(j) = guard.get_mut(tj as usize) {
crate::ported::jobs::addproc(
j, pid, "", true, // aux=1 for proc subst
Some(std::time::Instant::now()),
-1, -1,
);
}
}
}
}
procsubstpid.store(pid, Ordering::Relaxed); // c:5143
return pipes[(1 - out) as usize]; // c:5144
}
// c:5146 — child path.
entersubsh(esub::ASYNC | esub::PGRP | esub::NOMONITOR, None); // c:5146
let _ = redup(pipes[out as usize], out); // c:5147
closem(FDT_UNUSED, 0); // c:5148
cmdpush(CS_CMDSUBST as u8); // c:5149
// c:5150 — execode(prog, 0, 1, ...) — see WARNING (c).
let body_end = if ends_at > 0 { ends_at - 1 } else { 2 };
let body = if body_end > 2 && body_end <= bytes.len() {
&cmd[2..body_end]
} else {
""
};
let _ = crate::ported::exec_hooks::execute_script_zsh_pipeline(body);
cmdpop(); // c:5151
// c:5152 — _realexit() — WARNING (d).
std::process::exit(LASTVAL.load(Ordering::Relaxed));
}
/// Port of `static void spawnpipes(LinkList l, int nullexec)` from
/// `Src/exec.c:5184`.
///
/// Walks a redir list `l`, and for each REDIR_OUTPIPE/REDIR_INPIPE
/// entry fires `getpipe(name, nullexec || varid)` and stashes the
/// resulting fd into `f->fd2`.
///
/// ```c
/// LinkNode n;
/// Redir f;
/// char *str;
/// n = firstnode(l);
/// for (; n; incnode(n)) {
/// f = (Redir) getdata(n);
/// if (f->type == REDIR_OUTPIPE || f->type == REDIR_INPIPE) {
/// str = f->name;
/// f->fd2 = getpipe(str, nullexec || f->varid);
/// }
/// }
/// ```
///
/// =================== WARNING — DIVERGENCE ====================
/// The Rust port consumes a `&mut Vec<crate::ported::zsh_h::redir>`
/// in place of `LinkList`. The walk is identical; the only behavior
/// difference is that LinkList iteration in C lets callers splice
/// nodes mid-walk — we never do that here so it's a no-op divergence.
/// =============================================================
pub fn spawnpipes(l: &mut [redir], nullexec: i32) {
// c:5184
for f in l.iter_mut() {
// c:5191
if f.typ == REDIR_OUTPIPE || f.typ == REDIR_INPIPE {
// c:5193
let str_ = f.name.clone().unwrap_or_default(); // c:5194
let nullexec_eff = if f.varid.as_deref().map_or(false, |v| !v.is_empty()) {
1
} else {
nullexec
};
f.fd2 = getpipe(&str_, nullexec_eff); // c:5195
}
}
}
/// Port of `static int cancd2(char *s)` from `Src/exec.c:6411`.
///
/// C body:
/// ```c
/// struct stat buf;
/// char *us, *us2 = NULL;
/// int ret;
/// if (!isset(CHASEDOTS) && !isset(CHASELINKS)) {
/// if (*s != '/')
/// us = tricat(pwd[1] ? pwd : "", "/", s);
/// else
/// us = ztrdup(s);
/// fixdir(us2 = us);
/// } else
/// us = unmeta(s);
/// ret = !(access(us, X_OK) || stat(us, &buf) || !S_ISDIR(buf.st_mode));
/// if (us2) free(us2);
/// return ret;
/// ```
///
/// True iff `s` is a directory we can `cd` into (X-perm). With
/// `!CHASEDOTS && !CHASELINKS`, lexically canonicalise the path
/// (joining with PWD if relative) so `cd /foo/bar/..` works without
/// resolving the symlink. Otherwise pass `s` through `unmeta` to libc.
pub fn cancd2(s: &str) -> i32 {
// c:6411
let us: String;
// c:6422 — `if (!isset(CHASEDOTS) && !isset(CHASELINKS))`.
let chasedots = isset(CHASEDOTS); // c:6422
let chaselinks = isset(CHASELINKS);
if !chasedots && !chaselinks {
// c:6422
// c:6423-6426 — `*s != '/' ? tricat(pwd, "/", s) : ztrdup(s);`
let pwd_str = getsparam("PWD").unwrap_or_default(); // c:6424 `pwd`
let mut raw = if !s.starts_with('/') {
// c:6423
format!("{}/{}", if pwd_str.len() > 1 { &pwd_str[..] } else { "" }, s)
} else {
s.to_string()
};
// c:6427 — `fixdir(us2 = us);` — lexical canonicalisation.
raw = fixdir(&raw);
us = raw;
} else {
// c:6428
us = unmeta(s); // c:6429
}
// c:6430 — `!(access(us, X_OK) || stat(us, &buf) || !S_ISDIR(...))`.
let cstr = match std::ffi::CString::new(us.as_str()) {
Ok(c) => c,
Err(_) => return 0,
};
if unsafe { libc::access(cstr.as_ptr(), libc::X_OK) } != 0 {
return 0;
}
let meta = match std::fs::metadata(&us) {
Ok(m) => m,
Err(_) => return 0,
};
if !meta.file_type().is_dir() {
return 0;
}
1
}
/// Port of `char *cancd(char *s)` from `Src/exec.c:6370`.
///
/// Resolve a `cd` target against `$cdpath` and `cd_able_vars`.
/// Returns the chosen absolute path (heap-dup) if `cancd2` accepts
/// it, else `None`.
///
/// C body uses CDPATH walking + `cd_able_vars()` fallback. Sets
/// `doprintdir = -1` when a non-trivial path is found (so `cd`
/// echoes the resolved path).
pub fn cancd(s: &str) -> Option<String> {
// c:6370
// c:6372-6373 — `nocdpath = s[0]=='.' && (s[1]=='/' || !s[1] ||
// (s[1]=='.' && (s[2]=='/' || !s[2])))`.
let bytes = s.as_bytes();
let nocdpath = bytes.first().copied() == Some(b'.')
&& (bytes.get(1).copied() == Some(b'/')
|| bytes.get(1).is_none()
|| (bytes.get(1).copied() == Some(b'.')
&& (bytes.get(2).copied() == Some(b'/') || bytes.get(2).is_none())));
// c:6376 — `if (*s != '/')` branch.
if !s.starts_with('/') {
// c:6376
// c:6379-6380 — `if (cancd2(s)) return s;`
if cancd2(s) != 0 {
return Some(s.to_string());
}
// c:6381-6382 — `if (access(unmeta(s), X_OK) == 0) return NULL;`
let cstr = std::ffi::CString::new(unmeta(s).as_str()).ok()?;
if unsafe { libc::access(cstr.as_ptr(), libc::X_OK) } == 0 {
return None; // c:6382
}
// c:6383-6397 — CDPATH walk.
if !nocdpath {
let cdpath_str = getsparam("CDPATH").unwrap_or_default();
for cp in cdpath_str.split(':') {
// c:6384
let sbuf = if !cp.is_empty() {
format!("{}/{}", cp, s) // c:6386
} else {
s.to_string() // c:6391
};
if cancd2(&sbuf) != 0 {
// c:6393
DOPRINTDIR.store(-1, Ordering::Relaxed); // c:6394
return Some(sbuf); // c:6395
}
}
}
// c:6398-6403 — `cd_able_vars()` fallback.
if let Some(t) = cd_able_vars(s) {
// c:6398
if cancd2(&t) != 0 {
// c:6399
DOPRINTDIR.store(-1, Ordering::Relaxed); // c:6400
return Some(t); // c:6401
}
}
return None; // c:6404
}
// c:6406 — absolute path: `return cancd2(s) ? s : NULL;`
if cancd2(s) != 0 {
Some(s.to_string())
} else {
None
}
}
/// Port of `char *simple_redir_name(Eprog prog, int redir_type)` from
/// `Src/exec.c:4689`.
///
/// Test if an Eprog encodes a single simple-command consisting of a
/// SINGLE redirection of the requested type with NO command body
/// (the `cat < foo` shape). When true, returns the redir target name
/// (heap-dup) so callers like `$(< file)` short-circuit to a direct
/// `open(2)` instead of fork+pipe+exec.
///
/// C body walks the wordcode at fixed offsets (`pc[0]` = WC_LIST,
/// `pc[1]` = WC_SUBLIST, `pc[2]` = WC_PIPE, `pc[3]` = WC_REDIR,
/// `pc[6]` = WC_SIMPLE with argc=0). zshrs's wordcode buffer is the
/// same shape — this port replicates the same offset reads.
pub fn simple_redir_name(prog: &eprog, redir_type: i32) -> Option<String> {
// c:4689
let pc = &prog.prog;
// c:4694-4702 — guard chain. Walk the wordcode buffer at fixed
// offsets matching C's `pc[0]..pc[6]` checks.
if pc.len() < 7 {
return None;
}
if wc_code(pc[0]) != WC_LIST
|| (WC_LIST_TYPE(pc[0]) & Z_END as u32) == 0 // c:4695
|| wc_code(pc[1]) != WC_SUBLIST
|| WC_SUBLIST_FLAGS(pc[1]) != 0 // c:4696
|| WC_SUBLIST_TYPE(pc[1]) != WC_SUBLIST_END // c:4697
|| wc_code(pc[2]) != WC_PIPE
|| WC_PIPE_TYPE(pc[2]) != WC_PIPE_END // c:4698
|| wc_code(pc[3]) != WC_REDIR
|| WC_REDIR_TYPE(pc[3]) != redir_type // c:4699
|| WC_REDIR_VARID(pc[3]) != 0 // c:4700
|| pc[4] != 0 // c:4701
|| wc_code(pc[6]) != WC_SIMPLE
|| WC_SIMPLE_ARGC(pc[6]) != 0
// c:4702
{
return None; // c:4706
}
// c:4703 — `return dupstring(ecrawstr(prog, pc + 5, NULL));`
Some(dupstring(&ecrawstr(prog, 5, None)))
}
/// Port of `int getherestr(struct redir *fn)` from `Src/exec.c:4655`.
///
/// C body:
/// ```c
/// char *s, *t;
/// int fd, len;
/// t = fn->name;
/// singsub(&t);
/// untokenize(t);
/// unmetafy(t, &len);
/// if (!(fn->flags & REDIRF_FROM_HEREDOC))
/// t[len++] = '\n';
/// if ((fd = gettempfile(NULL, 1, &s)) < 0)
/// return -1;
/// write_loop(fd, t, len);
/// close(fd);
/// fd = open(s, O_RDONLY | O_NOCTTY);
/// unlink(s);
/// return fd;
/// ```
///
/// Materialise a `<<<` herestring or unprocessed-here-doc body into a
/// tempfile, then re-open read-only and unlink — gives the consumer a
/// read fd whose backing file is already cleaned up.
pub fn getherestr(fn_: &redir) -> i32 {
// c:4655
let mut t: String = fn_.name.clone().unwrap_or_default(); // c:4660
t = singsub(&t); // c:4661
t = untokenize(&t); // c:4662
// c:4663 — `unmetafy(t, &len);` — strip Meta-escapes.
// Reuse the canonical unmetafy port (utils.rs) on a Vec<u8>.
let mut bytes: Vec<u8> = t.into_bytes();
let _len = unmetafy(&mut bytes);
// c:4671-4672 — `if (!(fn->flags & REDIRF_FROM_HEREDOC)) t[len++] = '\n';`
if (fn_.flags & REDIRF_FROM_HEREDOC) == 0 {
// c:4671
bytes.push(b'\n'); // c:4672
}
// c:4673-4674 — `if ((fd = gettempfile(NULL, 1, &s)) < 0) return -1;`
let (fd, s) = match gettempfile(None) {
Some(p) => p,
None => return -1, // c:4674
};
// c:4675 — `write_loop(fd, t, len);`
let _ = write_loop(fd, &bytes); // c:4675
// c:4676 — `close(fd);`
let _ = zclose(fd); // c:4676
// c:4677 — `fd = open(s, O_RDONLY | O_NOCTTY);`
let cstr = std::ffi::CString::new(s.as_str()).unwrap_or_default();
let new_fd = unsafe { libc::open(cstr.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY) }; // c:4677
// c:4678 — `unlink(s);`
unsafe {
libc::unlink(cstr.as_ptr());
} // c:4678
new_fd // c:4679
}
/// Port of `void quote_tokenized_output(char *str, FILE *file)` from
/// `Src/exec.c:2114`.
///
/// C body (abridged):
/// ```c
/// for (; *s; s++) {
/// switch (*s) {
/// case Meta: putc(*++s ^ 32, file); continue;
/// case Nularg: continue;
/// case '\\' '<' '>' '(' '|' ')' '^' '#' '~' '[' ']' '*' '?' '$' ' ':
/// putc('\\', file); break;
/// case '\t': fputs("$'\\t'", file); continue;
/// case '\n': fputs("$'\\n'", file); continue;
/// case '\r': fputs("$'\\r'", file); continue;
/// case '=': if (s == str) putc('\\', file); break;
/// default:
/// if (itok(*s)) { putc(ztokens[*s - Pound], file); continue; }
/// }
/// putc(*s, file);
/// }
/// ```
///
/// Used by `xtrace` (`set -x` printer) and `whence -c` to display a
/// tokenized argv in a form where lexer tokens (`Star`, `Inpar`, …)
/// surface as unescaped chars (`*`, `(`) while literal special chars
/// get backslash-escaped — round-tripping through the shell.
pub fn quote_tokenized_output(str_in: &str, file: &mut impl std::io::Write) -> std::io::Result<()> {
// c:2114
let bytes = str_in.as_bytes();
let mut i = 0usize;
while i < bytes.len() {
// c:2118 `for (; *s; s++)`
let c = bytes[i];
match c {
x if x == Meta => {
// c:2120 — `case Meta: putc(*++s ^ 32, file);`
if i + 1 < bytes.len() {
file.write_all(&[bytes[i + 1] ^ 32])?; // c:2121
i += 2;
} else {
i += 1;
}
continue; // c:2122
}
x if x as char == Nularg => {
// c:2124
i += 1;
continue; // c:2126
}
b'\\' | b'<' | b'>' | b'(' | b'|' | b')' | b'^' | b'#' | b'~' | b'[' | b']'
| b'*' | b'?' | b'$' | b' ' => {
// c:2128-2142
file.write_all(b"\\")?; // c:2143
}
b'\t' => {
// c:2146
file.write_all(b"$'\\t'")?; // c:2147
i += 1;
continue;
}
b'\n' => {
// c:2150
file.write_all(b"$'\\n'")?; // c:2151
i += 1;
continue;
}
b'\r' => {
// c:2154
file.write_all(b"$'\\r'")?; // c:2155
i += 1;
continue;
}
b'=' => {
// c:2158 — `if (s == str) putc('\\', file);`
if i == 0 {
file.write_all(b"\\")?; // c:2160
}
}
_ => {
// c:2163 — `if (itok(*s)) putc(ztokens[*s - Pound], file); continue;`
if itok(c) {
// c:2164
let pound = Pound as u8;
if c >= pound {
let idx = (c - pound) as usize;
let zt = ztokens.as_bytes();
if idx < zt.len() {
file.write_all(&[zt[idx]])?; // c:2165 `ztokens[*s - Pound]`
}
}
i += 1;
continue;
}
}
}
file.write_all(&[c])?; // c:2171
i += 1;
}
Ok(())
}
// =====================================================================
// Wordcode-VM control-flow dispatch — faithful ports of the C
// `Src/exec.c` + `Src/loop.c` wordcode interpreter entries.
//
// Each function below takes `&mut estate` and returns `i32` to mirror
// the C `int execX(Estate state, int do_exec)` signature exactly. Per-
// line `// c:NNN` citations track the C source line.
//
// zshrs's primary execution path is the fusevm bytecode VM. These
// wordcode-VM entries exist for C-name parity with the upstream
// interpreter so that future bridging code can drive zshrs through
// the same dispatch tree zsh's `Src/init.c::loop` walks. Where
// zshrs primitives don't yet model their C counterpart (e.g.
// `execsubst`, `addvars`, `execfuncs[]` dispatch table), the local
// helper is declared with a comment citing the C source file:line
// where the canonical body lives — same pattern as the canonical
// `ksh93::ksh93_wrapper` port at c:152-227.
// =====================================================================
use crate::ported::r#loop::try_tryflag;
use crate::ported::math::{matheval as wc_matheval, mathevali as wc_mathevali};
use crate::ported::pattern::{patcompile, pattry};
// Addvars-specific imports (Src/exec.c:2497 port at exec.rs::addvars).
use crate::ported::linklist::LinkList;
use crate::ported::params::{assignaparam, assignsparam, unsetparam};
use crate::ported::pattern::haswilds;
use crate::ported::subst::{globlist, prefork};
use crate::ported::zsh_h::{
ALLEXPORT, ASSPM_AUGMENT, ASSPM_KEY_VALUE, ASSPM_WARN, GLOBASSIGN, KSHARRAYS, PREFORK_ASSIGN,
PREFORK_KEY_VALUE, PREFORK_SINGLE, WC_ASSIGN, WC_ASSIGN_INC, WC_ASSIGN_NUM, WC_ASSIGN_SCALAR,
WC_ASSIGN_TYPE, WC_ASSIGN_TYPE2,
};
use crate::ported::parse::{ecgetlist, ecgetstr};
use crate::ported::params::setloopvar;
use crate::ported::mem::freeheap;
use crate::ported::signals_h::{queue_signal_level, restore_queue_signals};
use crate::ported::builtin::{BREAKS, CONTFLAG, LOOPS, RETFLAG};
use crate::ported::zsh_h::{
estate, wordcode, EC_DUP, EC_DUPTOK, EC_NODUP, NOERREXIT_EXIT, NOERREXIT_RETURN, PAT_STATIC,
WC_CASE, WC_CASE_AND, WC_CASE_OR, WC_CASE_SKIP, WC_CASE_TESTAND, WC_CASE_TYPE, WC_CURSH_SKIP, WC_END,
WC_FOR_COND, WC_FOR_LIST, WC_FOR_SKIP, WC_FOR_TYPE, WC_FUNCDEF, WC_FUNCDEF_SKIP, WC_IF,
WC_IF_ELSE, WC_IF_SKIP, WC_IF_TYPE, WC_REPEAT_SKIP, WC_TIMED_EMPTY, WC_TIMED_TYPE, WC_TRY_SKIP,
WC_WHILE_SKIP, WC_WHILE_TYPE, WC_WHILE_UNTIL,
};
use crate::ported::zsh_h::{
CS_ALWAYS, CS_CASE, CS_COND, CS_CURSH, CS_ELIF, CS_ELIFTHEN, CS_ELSE, CS_FOR, CS_IF, CS_IFTHEN,
CS_MATH, CS_REPEAT, CS_UNTIL, CS_WHILE, MN_INTEGER,
};
// --- Local stubs for C primitives not yet ported elsewhere ------------
//
// These mirror the C functions of the same names. Each cites the C
// source file:line where the canonical body lives. They are inlined
// here (rather than a separate `pub fn` in the owning C-file module)
// because the owning ports are pending the wider exec-substrate
// work (sub-PR). Once those land, these locals collapse to direct
// `crate::ported::<owner>::<fn>` calls.
/// Port of `void execsubst(LinkList strs)` from `Src/exec.c:2684`.
///
/// C body (c:2684-2693):
/// ```c
/// void execsubst(LinkList strs) {
/// if (strs) {
/// prefork(strs, esprefork, NULL);
/// if (esglob && !errflag) {
/// LinkList ostrs = strs;
/// globlist(strs, 0);
/// strs = ostrs;
/// }
/// }
/// }
/// ```
///
/// `execsubst` runs `prefork` (parameter / arithmetic / command
/// substitution expansion + IFS-split) over the whole list, then
/// (when `esglob` is set) `globlist` to do filename globbing on the
/// result.
fn execsubst(list: &mut Vec<String>) {
// c:2684
if list.is_empty() {
return; // c:2686 `if (strs)`
}
let mut ll: crate::ported::subst::LinkList =
std::mem::take(list).into_iter().collect();
let prefork_flags = esprefork.load(Ordering::Relaxed); // c:2687 esprefork
let mut rf: i32 = 0;
crate::ported::subst::prefork(&mut ll, prefork_flags, &mut rf); // c:2687
if esglob.load(Ordering::Relaxed) != 0
&& errflag.load(Ordering::Relaxed) == 0
{
// c:2688 `if (esglob && !errflag)`
crate::ported::subst::globlist(&mut ll, 0); // c:2690
}
*list = ll.into_iter().collect();
}
/// Direct port of `static void addvars(Estate state, Wordcode pc,
/// int addflags)` from `Src/exec.c:2497-2648`. Process the WC_ASSIGN
/// nodes stacked inline of a simple command — the `var=value` and
/// `arr=(v1 v2 v3)` assignments that precede argv. Walks the wordcode
/// at `pc`, extracts each assignment's name + value (scalar or array),
/// optionally preforks + globs the tokenised RHS, and routes through
/// `assignsparam` (scalar) or `assignaparam` (array).
///
/// XTRACE side-effect: prints `name=value ` / `name=( v1 v2 ) ` to
/// stderr (C uses xtrerr; zshrs uses eprint!).
///
/// `STTY=...` in an inline-export form (`STTY=raw cmd`) gets captured
/// into the file-static `STTYval` for `execute()` to apply pre-exec.
fn addvars(state: &mut estate, pc: usize, addflags: i32) {
// c:2501 — locals.
let mut vl: LinkList<String>; // c:2501 `LinkList vl;`
let xtr: bool; // c:2502 `int xtr,`
let mut isstr: bool; // c:2502 `int isstr,`
let mut htok: i32 = 0; // c:2502 `int htok = 0;`
let mut arr: Vec<String>; // c:2503 `char **arr, **ptr, *name;`
let mut name: String;
let mut flags: i32; // c:2504 `int flags;`
let opc = state.pc; // c:2506 `Wordcode opc = state->pc;`
let mut ac: wordcode; // c:2507 `wordcode ac;`
// c:2508 `local_list1(svl);` — stack-local one-element LinkList
// for the scalar-assignment path. Rust uses a fresh LinkList per
// iteration; equivalent semantics.
// c:2510-2515 — comment about WARNCREATEGLOBAL warning suppression
// when the assignment list is implicitly local (ADDVAR_RESTORE).
flags = if (addflags & ADDVAR_RESTORE) == 0 {
ASSPM_WARN // c:2516
} else {
0 // c:2516
};
xtr = isset(XTRACE); // c:2517 `xtr = isset(XTRACE);`
if xtr {
// c:2518
printprompt4(); // c:2519
doneps4.store(1, Ordering::Relaxed); // c:2520 `doneps4 = 1;`
}
state.pc = pc; // c:2522 `state->pc = pc;`
// c:2523 `while (wc_code(ac = *state->pc++) == WC_ASSIGN) {`
loop {
if state.pc >= state.prog.prog.len() {
break;
}
ac = state.prog.prog[state.pc];
state.pc += 1;
if wc_code(ac) != WC_ASSIGN {
// Step back so the WC_SIMPLE / outer dispatcher sees the
// non-assignment opcode. C's `state->pc++` post-increment
// already pointed past WC_ASSIGN; we need to unconsume.
state.pc -= 1;
break;
}
let mut myflags = flags; // c:2524 `int myflags = flags;`
name = ecgetstr(state, EC_DUPTOK, Some(&mut htok)); // c:2525
if htok != 0 {
// c:2526 `if (htok) untokenize(name);`
name = untokenize(&name).to_string(); // c:2527
}
if WC_ASSIGN_TYPE2(ac) == WC_ASSIGN_INC {
// c:2528
myflags |= ASSPM_AUGMENT; // c:2529
}
if xtr {
// c:2530
// c:2531-2532 — fprintf(xtrerr, ... "%s+=" : "%s=", name);
if WC_ASSIGN_TYPE2(ac) == WC_ASSIGN_INC {
eprint!("{}+=", name); // c:2532
} else {
eprint!("{}=", name); // c:2532
}
}
// c:2533 `if ((isstr = (WC_ASSIGN_TYPE(ac) == WC_ASSIGN_SCALAR))) {`
isstr = WC_ASSIGN_TYPE(ac) == WC_ASSIGN_SCALAR;
if isstr {
// c:2534 `init_list1(svl, ecgetstr(state, EC_DUPTOK, &htok));`
let svl_val = ecgetstr(state, EC_DUPTOK, Some(&mut htok));
vl = LinkList::new();
vl.push_back(svl_val);
// c:2535 `vl = &svl;` — vl already points at the new list.
} else {
// c:2537 `vl = ecgetlist(state, WC_ASSIGN_NUM(ac), EC_DUPTOK, &htok);`
let items = ecgetlist(
state,
WC_ASSIGN_NUM(ac) as usize,
EC_DUPTOK,
Some(&mut htok),
);
vl = LinkList::new();
for it in items {
vl.push_back(it);
}
if errflag.load(Ordering::Relaxed) != 0 {
// c:2538-2541
state.pc = opc; // c:2539
return; // c:2540
}
}
// c:2544 `if (vl && htok) {`
if htok != 0 {
// c:2545 `int prefork_ret = 0;`
let mut prefork_ret: i32 = 0;
// c:2546-2547 — prefork(vl, (isstr ? PREFORK_SINGLE|PREFORK_ASSIGN
// : PREFORK_ASSIGN), &prefork_ret);
let pf_flags = if isstr {
PREFORK_SINGLE | PREFORK_ASSIGN
} else {
PREFORK_ASSIGN
};
prefork(&mut vl, pf_flags, &mut prefork_ret); // c:2547
if errflag.load(Ordering::Relaxed) != 0 {
// c:2548
state.pc = opc; // c:2549
return; // c:2550
}
if (prefork_ret & PREFORK_KEY_VALUE) != 0 {
// c:2552
myflags |= ASSPM_KEY_VALUE; // c:2553
}
// c:2554-2555 — `if (!isstr || (isset(GLOBASSIGN) && isstr &&
// haswilds((char *)getdata(firstnode(vl)))))`
let needs_glob = if !isstr {
true
} else {
isset(GLOBASSIGN)
&& isstr
&& !vl.is_empty()
&& haswilds(vl.nodes.front().map(|s| s.as_str()).unwrap_or(""))
};
if needs_glob {
globlist(&mut vl, prefork_ret); // c:2556
// c:2557-2562 — `if (isset(GLOBASSIGN) && isstr)
// unsetparam(name);`
if isset(GLOBASSIGN) && isstr {
unsetparam(&name); // c:2562
}
if errflag.load(Ordering::Relaxed) != 0 {
// c:2563
state.pc = opc; // c:2564
return; // c:2565
}
}
}
// c:2569 `if (isstr && (empty(vl) || !nextnode(firstnode(vl))))`
// — scalar-assignment path: zero or one element after prefork.
if isstr && (vl.is_empty() || vl.len() == 1) {
let val: String; // c:2571 `char *val;`
if vl.is_empty() {
// c:2574
val = String::new(); // c:2575 `val = ztrdup("");`
} else {
// c:2577 `untokenize(peekfirst(vl));`
let peek = vl.nodes.front().cloned().unwrap_or_default();
val = untokenize(&peek).to_string(); // c:2577-2578
// c:2578 `val = ztrdup(ugetnode(vl));` — ugetnode pops;
// we just cloned the front above. Equivalent.
}
if xtr {
// c:2580
eprint!("{}", quotedzputs(&val)); // c:2581
eprint!(" "); // c:2582 `fputc(' ', xtrerr);`
}
// c:2584 `if ((addflags & ADDVAR_EXPORT) && !strchr(name, '['))`
let pm = if (addflags & ADDVAR_EXPORT) != 0 && !name.contains('[') {
// c:2585 `if (strcmp(name, "STTY") == 0)`
if name == "STTY" {
// c:2586-2587 — `STTYval = ztrdup(val);`
let mut stty = STTYval.lock().unwrap();
*stty = Some(val.clone()); // c:2587
}
// c:2589 `allexp = opts[ALLEXPORT];`
let allexp = isset(ALLEXPORT);
// c:2590 `opts[ALLEXPORT] = 1;` — temporarily set.
opt_state_set("allexport", true);
if isset(KSHARRAYS) {
// c:2591
unsetparam(&name); // c:2592
}
let pm = assignsparam(&name, &val, myflags); // c:2593
// c:2594 `opts[ALLEXPORT] = allexp;` — restore.
opt_state_set("allexport", allexp);
pm
} else {
// c:2595
assignsparam(&name, &val, myflags) // c:2596
};
if pm.is_none() {
// c:2597 `if (!pm)`
LASTVAL.store(1, Ordering::Relaxed); // c:2598 `lastval = 1;`
// c:2599-2604 — "cheating" comment: don't zerr.
if cmdoutval.load(Ordering::Relaxed) == 0 {
// c:2605 `if (!cmdoutval)`
cmdoutval.store(1, Ordering::Relaxed); // c:2606
}
}
if errflag.load(Ordering::Relaxed) != 0 {
// c:2608
state.pc = opc; // c:2609
return; // c:2610
}
continue; // c:2612
}
// c:2614 `if (vl) { ... }` — array-assignment path: drain vl
// into a fresh `char **arr`.
// c:2615-2619 `ptr = arr = zalloc(...); while (nonempty(vl)) *ptr++ = ztrdup(ugetnode(vl));`
arr = Vec::with_capacity(vl.len() + 1);
while let Some(s) = vl.pop_front() {
arr.push(s);
}
// c:2623 `*ptr = NULL;` — C terminator; Rust Vec doesn't need it.
if xtr {
// c:2624
eprint!("( "); // c:2625
for s in &arr {
// c:2626 `for (ptr = arr; *ptr; ptr++)`
eprint!("{}", quotedzputs(s)); // c:2627
eprint!(" "); // c:2628
}
eprint!(") "); // c:2630
}
// c:2632 `if (!assignaparam(name, arr, myflags))`
if assignaparam(&name, arr, myflags).is_none() {
LASTVAL.store(1, Ordering::Relaxed); // c:2633
// c:2634-2638 — "cheating" comment.
if cmdoutval.load(Ordering::Relaxed) == 0 {
// c:2639
cmdoutval.store(1, Ordering::Relaxed); // c:2640
}
}
if errflag.load(Ordering::Relaxed) != 0 {
// c:2642
state.pc = opc; // c:2643
return; // c:2644
}
}
state.pc = opc; // c:2647 `state->pc = opc;`
}
// execfuncs[] dispatch table from `Src/exec.c:5499` is inlined as a
// match expression at the call sites in execsimple. Not a separate
// Rust fn — every C-side reference to
// `execfuncs[code - WC_CURSH](state, ...)` resolves inline below.
// --- exec.c entries ---------------------------------------------------
/// Port of `execcursh(Estate state, int do_exec)` from
/// `Src/exec.c:469-498`. Execute a `{ ... }` current-shell command
/// group: skip the trailing try-only word, optionally drop a stale
/// job slot, then run the inner list.
pub fn execcursh(state: &mut estate, do_exec: i32) -> i32 {
// c:472 — `end = state->pc + WC_CURSH_SKIP(state->pc[-1]);`
let prior = state.prog.prog[state.pc.wrapping_sub(1)];
let end = state.pc + WC_CURSH_SKIP(prior) as usize;
// c:475 — `state->pc++;` skip the try/always-only word.
state.pc += 1;
// c:482-486 — drop empty job slot before nested cmd: if outer-pipe
// bookkeeping is clean AND thisjob is a real job that's not the
// pipe-leader AND has no procs yet, deletejob() recycles it. Avoids
// leaking job-table slots when execcursh recurses.
{
let lp = list_pipe.load(Ordering::Relaxed);
let lpj = list_pipe_job.load(Ordering::Relaxed);
let tj = crate::ported::jobs::THISJOB
.get()
.map(|m| *m.lock().unwrap())
.unwrap_or(-1);
if lp == 0 && tj != -1 && tj != lpj {
if let Some(jt) = crate::ported::jobs::JOBTAB.get() {
let mut guard = jt.lock().unwrap();
let has = crate::ported::jobs::hasprocs(&guard, tj as usize);
if !has {
if let Some(j) = guard.get_mut(tj as usize) {
crate::ported::jobs::deletejob(j, false);
}
}
}
}
}
cmdpush(CS_CURSH as u8); // c:487 — `cmdpush(CS_CURSH);`
let _ = execlist(state, 1, do_exec); // c:488 — `execlist(state, 1, do_exec);`
cmdpop(); // c:489 — `cmdpop();`
state.pc = end; // c:491 — `state->pc = end;`
this_noerrexit.store(1, Ordering::Relaxed); // c:492 — `this_noerrexit = 1;`
LASTVAL.load(Ordering::Relaxed) // c:494 — `return lastval;`
}
// `(...)` subshell — no dedicated C function (handled inline by
// `execpline`'s WC_PIPE branch via the WC_SUBSH bit, exec.c:2540+).
// In zshrs the subshell branch is folded into `execpline` and
// `execsimple`'s WC_SUBSH dispatch — both invoke execcursh for the
// inner-list walk since fusevm bytecode handles the forking via
// Op::Subshell at a higher layer.
/// Port of `execcond(Estate state, UNUSED(int do_exec))` from
/// `Src/exec.c:5204-5232`. Run a `[[ ... ]]` cond expression.
pub fn execcond(state: &mut estate, _do_exec: i32) -> i32 {
state.pc -= 1; // c:5208 — `state->pc--;`
// c:5209-5213 — XTRACE prelude.
if isset(XTRACE) {
printprompt4();
eprint!("[[");
// c:5212 — `tracingcond++;` not modeled in zshrs.
}
cmdpush(CS_COND as u8); // c:5214
// c:5215 — `stat = evalcond(state, NULL);` — TODO faithful: needs
// the wordcode-level evalcond from Src/cond.c which is distinct
// from the test-builtin evalcond ported in cond.rs. Pending.
let stat: i32 = 0;
// c:5219-5221 — `if (stat == 2) errflag |= ERRFLAG_ERROR;`
if stat == 2 {
errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
}
cmdpop(); // c:5222
if isset(XTRACE) {
eprintln!(" ]]");
}
stat // c:5230 — `return stat;`
}
/// Port of `execarith(Estate state, UNUSED(int do_exec))` from
/// `Src/exec.c:5237-5275`. Run a `(( ... ))` arithmetic command;
/// returns 0 when val != 0 (success), 1 when val == 0 (false), 2 on
/// parse error.
pub fn execarith(state: &mut estate, _do_exec: i32) -> i32 {
if isset(XTRACE) {
printprompt4();
eprint!("((");
}
cmdpush(CS_MATH as u8); // c:5247
let mut htok: i32 = 0;
let mut e = ecgetstr(state, EC_DUPTOK, Some(&mut htok)); // c:5248
if htok != 0 {
e = singsub(&e); // c:5250 — `singsub(&e);`
}
if isset(XTRACE) {
eprint!(" {}", e);
}
let val_result = wc_matheval(&e); // c:5254 — `val = matheval(e);`
cmdpop(); // c:5256
if isset(XTRACE) {
eprintln!(" ))");
}
// c:5262-5265 — `if (errflag) { errflag &= ~ERRFLAG_ERROR; return 2; }`
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 || val_result.is_err() {
errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed);
return 2;
}
// c:5267 — `return (val.type == MN_INTEGER) ? val.u.l == 0 : val.u.d == 0.0;`
let val = val_result.unwrap();
if val.type_ == MN_INTEGER {
if val.l == 0 {
1
} else {
0
}
} else if val.d == 0.0 {
1
} else {
0
}
}
/// Port of `exectime(Estate state, UNUSED(int do_exec))` from
/// `Src/exec.c:5279-5294`. Run `time pipeline`: drives execpline with
/// the Z_TIMED|Z_SYNC flags so it tracks wall/user/sys time.
pub fn exectime(state: &mut estate, _do_exec: i32) -> i32 {
let jb = *THISJOB.get_or_init(|| std::sync::Mutex::new(-1)).lock().unwrap(); // c:5283
let prior = state.prog.prog[state.pc.wrapping_sub(1)];
// c:5284-5287 — empty `time` (no pipeline) — print accumulated shell time.
if WC_TIMED_TYPE(prior) == WC_TIMED_EMPTY {
// c:5285 — `shelltime(NULL,NULL,NULL,0);` — print accumulated
// shell+kids time deltas since last call.
crate::ported::jobs::shelltime(None, None, None, 0);
return 0; // c:5286
}
// c:5288 — `execpline(state, *state->pc++, Z_TIMED|Z_SYNC, 0);`
let slcode = state.prog.prog[state.pc];
state.pc += 1;
use crate::ported::zsh_h::{Z_SYNC, Z_TIMED};
let _ = execpline(state, slcode, Z_TIMED as i32 | Z_SYNC as i32, 0);
*THISJOB.get_or_init(|| std::sync::Mutex::new(-1)).lock().unwrap() = jb; // c:5289
LASTVAL.load(Ordering::Relaxed) // c:5290
}
/// `execshfunc(Shfunc shf, LinkList args)` — `Src/exec.c:5540`.
/// Promoted to top-level pub fn so execcmd_exec at the shfunc
/// dispatch site (c:4102-4105) can route through it. The real port
/// owns queue_signals + cmdstack + sfcontext setup before calling
/// doshfunc; doshfunc itself is unported, so we route the body
/// through `runshfunc` (exec.rs:1700), which carries the
/// wrapper-chain + zunderscore restore. Degraded vs C (no cmdstack
/// push, no sfcontext flip, no XTRACE arg-trace) but the function
/// body executes and `lastval` is updated.
pub fn execshfunc(shf: &mut shfunc, args: &mut Vec<String>) {
// c:5546-5547 — `if (errflag) return;`
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
return;
}
// c:5550-5557 — drop empty job slot before nested shfunc invoke:
// if outer-pipe bookkeeping is clean AND thisjob is a real job
// that's not the pipe-leader AND has no procs yet, deletejob()
// recycles it. Avoids leaking job-table slots across recursive
// function calls. Same pattern as execcursh's c:482-486.
{
let lp = list_pipe.load(Ordering::Relaxed);
let lpj = list_pipe_job.load(Ordering::Relaxed);
let tj = crate::ported::jobs::THISJOB
.get()
.map(|m| *m.lock().unwrap())
.unwrap_or(-1);
if lp == 0 && tj != -1 && tj != lpj {
if let Some(jt) = crate::ported::jobs::JOBTAB.get() {
let mut guard = jt.lock().unwrap();
let has = crate::ported::jobs::hasprocs(&guard, tj as usize);
if !has {
// c:5554-5555 — `last_file_list = jobtab[thisjob].filelist;
// jobtab[thisjob].filelist = NULL;` — preserve
// the filelist so deletejob doesn't unlink temp
// files. Rust take()s the Vec into a local.
let _last_file_list: Vec<String> = if let Some(j) = guard.get_mut(tj as usize) {
std::mem::take(&mut j.filelist)
} else {
Vec::new()
};
if let Some(j) = guard.get_mut(tj as usize) {
crate::ported::jobs::deletejob(j, false); // c:5556
}
}
}
}
}
// c:5559-5570 — `if (isset(XTRACE)) { printprompt4(); ... \n; }` —
// emit PS4 prefix + space-separated quoted args on the trace
// stream so `set -x` shows the function invocation line.
if isset(XTRACE) {
crate::ported::utils::printprompt4();
for (i, a) in args.iter().enumerate() {
if i > 0 {
eprint!(" ");
}
eprint!("{}", crate::ported::utils::quotedzputs(a));
}
eprintln!();
}
// c:5572-5578 cmdstack/sfcontext setup: omit (no cmdstack in
// zshrs yet — replaced by tracing).
// c:5580 — `doshfunc(shf, args, 0);` — doshfunc swaps PPARAMS
// ($1, $2, …) to the function's args, runs the body via
// runshfunc, then restores. doshfunc itself isn't ported yet
// so we do the swap-and-restore inline here.
// c:5580 — `doshfunc(shf, args, 0);`. The C path always has
// `funcdef` populated since C parses at definition time. zshrs
// compiles to fusevm chunks instead, so `funcdef` is None for
// user-defined functions; only `body` (source string) carries
// the definition. When that's the case, build a one-shot eprog
// whose `strs` carries the source so runshfunc's script-pipeline
// arm (execute_script_zsh_pipeline) executes the body.
let prog_owned: Option<crate::ported::zsh_h::eprog> = if shf.funcdef.is_some() {
None
} else if let Some(ref body) = shf.body {
Some(crate::ported::zsh_h::eprog {
strs: Some(body.clone()),
..Default::default()
})
} else {
None
};
let prog_ref: Option<&crate::ported::zsh_h::eprog> = match (shf.funcdef.as_deref(), prog_owned.as_ref()) {
(Some(p), _) => Some(p),
(_, Some(p)) => Some(p),
_ => None,
};
if let Some(prog) = prog_ref {
// Save old PPARAMS.
let old_pparams: Vec<String> = crate::ported::builtin::PPARAMS
.lock()
.map(|p| p.clone())
.unwrap_or_default();
// args[0] is the function name (matching C's `argv[0]` →
// FUNCTION_ARGZERO); strip it so $1..$N are the real call
// args. C's `pparams = args + 1`.
let positionals: Vec<String> = if args.len() > 1 {
args[1..].to_vec()
} else {
Vec::new()
};
if let Ok(mut pp) = crate::ported::builtin::PPARAMS.lock() {
*pp = positionals;
}
runshfunc(prog, None, &shf.node.nam);
// Restore.
if let Ok(mut pp) = crate::ported::builtin::PPARAMS.lock() {
*pp = old_pparams;
}
}
// c:5582-5589 cmdstack restore/free: omit (no cmdstack).
}
/// Port of `int doshfunc(Shfunc shfunc, LinkList doshargs, int noreturnval)`
/// from `Src/exec.c:5823-6158`.
///
/// C body's scope-management sequence ported here. The C source's
/// body-execution call (`runshfunc(prog, wrappers, name)` at c:6042)
/// is replaced by `body_runner` — zshrs runs function bodies through
/// fusevm bytecode rather than zsh's wordcode walker (per PORT.md
/// "zshrs replaces zsh's tree-walking interpreter" rule), so the
/// callback hands the live executor back to the caller (typically
/// the fusevm bridge) for the actual body run. Every line of scope
/// save/restore around the body call mirrors C exactly.
///
/// **RUST-ONLY ADAPTATION:** the extra `body_runner` parameter is
/// not in C. C calls `runshfunc(prog, wrappers, name)` directly at
/// c:6042; zshrs delegates to a closure because the body-execution
/// pipeline (fusevm) differs from C's (wordcode). The closure
/// fully replaces the runshfunc call and returns the body's exit
/// status (which doshfunc reads as `lastval` for the `noreturnval`
/// path).
#[allow(non_snake_case)]
pub fn doshfunc(
shfunc: &mut shfunc, // c:5823
doshargs: Vec<String>, // c:5823
noreturnval: bool, // c:5823
mut body_runner: impl FnMut() -> i32, // (Rust-only — body delegate)
) -> i32 {
use crate::ported::builtin::{BREAKS, CONTFLAG, LASTVAL, LOOPS, RETFLAG};
use crate::ported::jobs::{NUMPIPESTATS, PIPESTATS};
use crate::ported::modules::parameter::FUNCSTACK;
use crate::ported::params::endparamscope;
use crate::ported::params::locallevel as locallevel_atomic;
use crate::ported::zsh_h::{FS_EVAL, FS_FUNC, FS_SOURCE, FUNCTIONARGZERO, PM_UNDEFINED};
use std::sync::atomic::Ordering;
let name = shfunc.node.nam.clone(); // c:5827
let flags = shfunc.node.flags; // c:5828
let fname = dupstring(&name); // c:5829
let _ = fname; // c:5829 (kept for parity)
// c:5835 — `queue_signals();` Lots of memory + global-state changes.
queue_signals();
// c:5847-5848 — `marked_prog = shfunc->funcdef; useeprog(marked_prog);`
// Pinned so a recursive unload doesn't free the eprog under us.
// (Skipped: zshrs's shfunc holds a Box<Eprog>; Drop semantics
// already pin until call ends. C does explicit refcount on
// `funcdef->nref` via useeprog.)
// c:5856-5916 — Funcsave allocation + per-field snapshot.
let funcsave_breaks = BREAKS.load(Ordering::Relaxed); // c:5859
let funcsave_contflag = CONTFLAG.load(Ordering::Relaxed); // c:5860
let funcsave_loops = LOOPS.load(Ordering::Relaxed); // c:5861
let funcsave_lastval = LASTVAL.load(Ordering::Relaxed); // c:5862
let funcsave_numpipestats = { // c:5864
NUMPIPESTATS.get_or_init(|| std::sync::Mutex::new(0))
.lock().map(|n| *n).unwrap_or(0)
};
let funcsave_noerrexit = noerrexit.load(Ordering::Relaxed); // c:5865
// c:5866-5867 — trap_state PRIMED branch decrements trap_return.
if TRAP_STATE.load(Ordering::Relaxed) == TRAP_STATE_PRIMED { // c:5866
TRAP_RETURN.fetch_sub(1, Ordering::Relaxed); // c:5867
}
// c:5871 — `noerrexit &= ~NOERREXIT_RETURN;` — scope-clear of
// return-suppress so a `return` inside the body fires errexit
// checks normally.
noerrexit.fetch_and(!NOERREXIT_RETURN, Ordering::Relaxed);
// c:5872-5880 — noreturnval branch: deep-copy pipestats so the
// function body's pipestats writes are restored on exit.
let funcsave_pipestats: Option<Vec<i32>> = if noreturnval { // c:5872
let p = PIPESTATS.get_or_init(|| std::sync::Mutex::new([0; MAX_PIPESTATS]));
p.lock().ok().map(|g| g[..funcsave_numpipestats].to_vec()) // c:5879 memcpy
} else {
None
};
// c:5882-5896 — TRAPEXIT special case (deep-copy shfunc so
// starttrapscope doesn't rug-pull). zshrs doesn't yet support
// running TRAPEXIT directly via doshfunc; flagged for follow-up.
// (Skip: name = "TRAPEXIT" path.)
let _ = name.as_str(); // sentinel for the eventual port.
// c:5898 — `starttrapscope();` — canonical port at signals.rs:1135
// tags SIGEXIT for deferred restoration at scope end.
crate::ported::signals::starttrapscope();
// c:5899 — `startpatternscope();`
crate::ported::pattern::startpatternscope();
// c:5901 — `pptab = pparams;` — save outer positional params.
let pptab: Vec<String> = crate::ported::builtin::PPARAMS
.lock().map(|p| p.clone()).unwrap_or_default();
// c:5902-5903 — non-undefined: `scriptname = dupstring(name);`
let funcsave_scriptname = crate::ported::utils::scriptname_get();
if (flags as u32 & PM_UNDEFINED) == 0 { // c:5902
crate::ported::utils::set_scriptname(Some(dupstring(&name))); // c:5903
}
// c:5904-5908 — `funcsave->zoptind = zoptind; ...` snapshot.
// zshrs's zoptind/optcind aren't ported as separate statics yet —
// they live in the getopts builtin's local state. Skip the
// snapshot until that port lands.
// c:5914 — `memcpy(funcsave->opts, opts, sizeof(opts));` — option
// snapshot. Port wraps opts in OPTS_LIVE; capture the live state
// here as a HashMap snapshot.
let funcsave_opts = crate::ported::options::opt_state_snapshot();
// c:5915-5916 — `funcsave->emulation/sticky = emulation/sticky;`
// Emulation snapshot pending the sticky-emulation port.
// c:5954-5969 — PM_TAGGED / PM_WARNNESTED option-override block.
// Anonymous-function name comparison via pointer equality in C;
// zshrs uses string equality. Skip until ANONYMOUS_FUNCTION_NAME
// sentinel is ported.
// c:5970 — `funcsave->oflags = oflags;` — module-global tracking
// function-attribute inheritance. Skip until oflags is ported.
// c:5977 — `opts[PRINTEXITVALUE] = 0;` — suppress printexitvalue
// for inner commands; outer flag restored on exit.
crate::ported::options::opt_state_set("printexitvalue", false);
// c:5978-5998 — pparams swap. C reads doshargs and constructs the
// function's positional-param array. First arg is the function
// name (regardless of FUNCTIONARGZERO); the rest become $1..$N.
let funcsave_argv0: Option<String> = if !doshargs.is_empty() { // c:5978
// c:5982-5985 — `pparams = x = zshcalloc(...)`.
let positionals: Vec<String> = if doshargs.len() > 1 {
doshargs[1..].to_vec()
} else {
Vec::new()
};
if let Ok(mut pp) = crate::ported::builtin::PPARAMS.lock() {
*pp = positionals;
}
// c:5984-5987 — FUNCTIONARGZERO: save argzero, install
// doshargs[0] (the function name).
if isset(FUNCTIONARGZERO) { // c:5984
let prev = crate::ported::utils::argzero();
crate::ported::utils::set_argzero(Some(doshargs[0].clone())); // c:5986
prev
} else {
None
}
} else {
// c:5992-5997 — no args: empty pparams. argzero saved+dup'd.
if let Ok(mut pp) = crate::ported::builtin::PPARAMS.lock() {
*pp = Vec::new();
}
if isset(FUNCTIONARGZERO) { // c:5994
let prev = crate::ported::utils::argzero();
crate::ported::utils::set_argzero(prev.clone()); // c:5996 ztrdup(argzero)
prev
} else {
None
}
};
// c:5999 — `++funcdepth;` — bumped on entry. Mirror via locallevel
// since zshrs tracks function-call depth there.
//
// Plus the canonical startparamscope (c:6194 inside runshfunc).
// zshrs's body_runner replaces runshfunc's `execode` call so the
// startparamscope/endparamscope pair must wrap body_runner here,
// not inside the closure. inc_locallevel is exactly startparamscope.
inc_locallevel();
// c:6000-6003 — FUNCNEST check. Skip; the zshrs fusevm doesn't
// recurse via real stack frames so the depth limit is less
// critical.
// c:6005-6019 — funcstack frame push. The full C block:
// funcsave->fstack.name = dupstring(name);
// funcsave->fstack.caller = funcstack ? funcstack->name :
// dupstring(argv0 ? argv0 : argzero);
// funcsave->fstack.lineno = lineno;
// funcsave->fstack.prev = funcstack;
// funcsave->fstack.tp = FS_FUNC;
// funcstack = &funcsave->fstack;
// funcsave->fstack.flineno = shfunc->lineno;
// funcsave->fstack.filename = getshfuncfile(shfunc);
let lineno_now = crate::ported::input::lineno.with(|c| c.get()) as i64;
let (caller, prev_tp): (Option<String>, Option<i32>) = {
let stk = FUNCSTACK.lock().unwrap_or_else(|e| e.into_inner());
if let Some(p) = stk.last() {
(Some(p.name.clone()), Some(p.tp))
} else {
// c:6011-6012 — outermost: argv0 (saved) or argzero global.
let z = funcsave_argv0.clone().or_else(crate::ported::utils::argzero);
(z, None)
}
};
// c:6018-6019 — flineno: shfunc->lineno (function def line)
let flineno = shfunc.lineno;
let filename = shfunc.filename.clone()
.or_else(|| Some(String::new()));
{
let frame = crate::ported::zsh_h::funcstack {
prev: None, // c:6014 (Vec-stack: index encodes link)
name: dupstring(&name), // c:6005
filename, // c:6019
caller, // c:6011
flineno, // c:6018
lineno: lineno_now, // c:6013
tp: FS_FUNC, // c:6015
};
let _ = prev_tp; // c:6011 (informational)
let mut stack = FUNCSTACK.lock().unwrap_or_else(|e| e.into_inner());
stack.push(frame); // c:6016 funcstack = &funcsave->fstack
}
// c:6021-6042 — body execution. C: `runshfunc(prog, wrappers, name)`.
// zshrs delegates to the body_runner closure (typically a fusevm
// sub-VM run from the bridge). The closure returns the body's
// exit status which becomes lastval.
let body_status = body_runner();
LASTVAL.store(body_status, Ordering::Relaxed);
// c:6044 — `funcstack = funcsave->fstack.prev;` — pop our frame.
{
let mut stack = FUNCSTACK.lock().unwrap_or_else(|e| e.into_inner());
stack.pop();
}
// c:6046 — `--funcdepth;` — paired endparamscope (c:6200 inside
// runshfunc) lives at c:6157 below as `endparamscope()`. Removed
// the dec here so locallevel only decrements once per
// function-call frame; double-dec was purging level-0 globals on
// function exit (the `f() { x=foo; }; f; echo $x` regression).
// c:6047-6053 — retflag clear. C clears retflag and restores
// outer breaks if a `return` fired.
if RETFLAG.load(Ordering::SeqCst) != 0 { // c:6047
RETFLAG.store(0, Ordering::SeqCst); // c:6051
BREAKS.store(funcsave_breaks, Ordering::SeqCst); // c:6052
}
// c:6054-6058 — pparams + argv0 restore.
if let Ok(mut pp) = crate::ported::builtin::PPARAMS.lock() {
*pp = pptab; // c:6059 pparams = pptab
}
if let Some(saved) = funcsave_argv0 { // c:6055
crate::ported::utils::set_argzero(Some(saved)); // c:6057
}
// c:6064 — `scriptname = funcsave->scriptname;`
crate::ported::utils::set_scriptname(funcsave_scriptname);
// c:6067 — `endpatternscope();`
crate::ported::pattern::endpatternscope();
// c:6078-6102 — LOCALOPTIONS restore. Re-apply the snapshot when
// localoptions was set inside the body.
if crate::ported::options::opt_state_get("localoptions").unwrap_or(false) {
// c:6091 memcpy(opts, funcsave->opts, sizeof(opts)) — full restore.
let current = crate::ported::options::opt_state_snapshot();
for (k, _) in ¤t {
if !funcsave_opts.contains_key(k) {
crate::ported::options::opt_state_unset(k);
}
}
for (k, v) in &funcsave_opts {
crate::ported::options::opt_state_set(k, *v);
}
} else {
// c:6097-6101 — non-LOCALOPTIONS: restore only the always-
// restored subset (XTRACE / PRINTEXITVALUE / LOCALOPTIONS /
// LOCALLOOPS / WARNNESTEDVAR).
for opt in ["xtrace", "printexitvalue", "localoptions",
"localloops", "warnnestedvar"] {
if let Some(v) = funcsave_opts.get(opt) {
crate::ported::options::opt_state_set(opt, *v);
}
}
}
// c:6104-6112 — LOCALLOOPS warn-on-active-continue/break + restore
// breaks/contflag/loops snapshot. Skip the warn lines for now;
// restore the bookkeeping.
if crate::ported::options::opt_state_get("localloops").unwrap_or(false) {
BREAKS.store(funcsave_breaks, Ordering::SeqCst); // c:6109
CONTFLAG.store(funcsave_contflag, Ordering::SeqCst); // c:6110
LOOPS.store(funcsave_loops, Ordering::SeqCst); // c:6111
}
// c:6114 — `endtrapscope();` — canonical port at signals.rs:1164
// restores saved traps from SAVETRAPS whose local > locallevel
// and fires the deferred SIGEXIT trap (if any) AFTER the other
// restores complete.
crate::ported::signals::endtrapscope();
// c:6116-6117 — TRAP_STATE_PRIMED branch: bump trap_return back.
if TRAP_STATE.load(Ordering::Relaxed) == TRAP_STATE_PRIMED { // c:6116
TRAP_RETURN.fetch_add(1, Ordering::Relaxed); // c:6117
}
// c:6118 — `ret = lastval;`
let ret = LASTVAL.load(Ordering::Relaxed);
// c:6119 — `noerrexit = funcsave->noerrexit;`
noerrexit.store(funcsave_noerrexit, Ordering::Relaxed);
// c:6120-6124 — noreturnval: restore lastval + pipestats. C runs
// the function for side-effects only; outer lastval/pipestats
// should reflect the PRE-call state.
if noreturnval { // c:6120
LASTVAL.store(funcsave_lastval, Ordering::Relaxed); // c:6121
if let Some(saved_ps) = funcsave_pipestats {
let n = NUMPIPESTATS.get_or_init(|| std::sync::Mutex::new(0));
if let Ok(mut nguard) = n.lock() {
*nguard = funcsave_numpipestats; // c:6122
}
let p = PIPESTATS.get_or_init(|| std::sync::Mutex::new([0; MAX_PIPESTATS]));
if let Ok(mut pguard) = p.lock() {
for (i, v) in saved_ps.iter().enumerate() {
if i < pguard.len() {
pguard[i] = *v; // c:6123 memcpy
}
}
}
}
}
// c:6128 — `unqueue_signals();`
unqueue_signals();
// c:6135-6155 — exit_pending branch: when an `exit` was queued
// inside the function body and we've unwound enough scopes for
// it to take effect, either keep unwinding (still inside a
// nested function) or actually exit the shell.
let exit_pending = crate::ported::builtin::EXIT_PENDING.load(Ordering::Relaxed);
let exit_level = crate::ported::builtin::EXIT_LEVEL.load(Ordering::Relaxed);
let cur_locallevel =
crate::ported::params::locallevel.load(Ordering::Relaxed) as i32;
let cur_forklevel = FORKLEVEL.load(Ordering::Relaxed);
let in_exit_trap =
crate::ported::signals::in_exit_trap.load(Ordering::Relaxed); // c:Src/signals.c:63
if exit_pending != 0 && exit_level >= cur_locallevel + 1 && in_exit_trap == 0 {
// c:6141
if cur_locallevel > cur_forklevel {
// c:6143 — still inside a nested function: keep unwinding.
crate::ported::builtin::RETFLAG.store(1, Ordering::Relaxed); // c:6144
crate::ported::builtin::BREAKS.store(
crate::ported::builtin::LOOPS.load(Ordering::Relaxed),
Ordering::Relaxed,
); // c:6145
} else {
// c:6151 — out of all functions: exit for real.
crate::ported::builtin::STOPMSG.store(1, Ordering::Relaxed); // c:6151
let val = crate::ported::builtin::EXIT_VAL.load(Ordering::Relaxed);
crate::ported::builtin::zexit(val, crate::ported::zsh_h::ZEXIT_NORMAL); // c:6152
}
}
// c:Src/exec.c doshfunc → endparamscope — restore local-typeset
// params installed during the body. (Function-local scope.)
endparamscope();
ret // c:6157 return ret
}
/// `TRAP_STATE_PRIMED` per `Src/signals.h:55` — doshfunc tests this
/// to decide whether to bump trap_return on entry/exit. Local
/// const here because the canonical zsh_h port doesn't carry
/// trap-state numeric constants yet.
const TRAP_STATE_PRIMED: i32 = 2; // c:Src/signals.h:55
/// Port of `execfuncdef(Estate state, Eprog redir_prog)` from
/// `Src/exec.c:5309-5494`. Define a shell function: extract
/// name(s)+body from the wordcode payload, allocate the Shfunc,
/// install into `shfunctab` (named), or execute immediately (anon).
#[allow(non_snake_case)]
pub fn execfuncdef(
state: &mut estate,
mut redir_prog: Option<crate::ported::zsh_h::Eprog>,
) -> i32 {
use crate::ported::hashtable::{dircache_set, shfunctab_lock};
use crate::ported::jobs::{getsigidx, removetrapnode};
use crate::ported::parse::{dupeprog, freeeprog, incrdumpcount};
use crate::ported::signals::settrap;
use crate::ported::utils::scriptfilename_get;
use crate::ported::zsh_h::{
eprog as eprog_t, hashnode, patprog as patprog_t, shfunc as shfunc_t, EC_DUPTOK as _,
EF_HEAP, EF_MAP, EF_REAL, FS_EVAL, FS_FUNC, PM_ANONYMOUS, PM_TAGGED, PM_TAGGED_LOCAL,
Patprog, PRINTEXITVALUE, SHINSTDIN, ZSIG_FUNC,
};
// c:5311 — `Shfunc shf;`
let mut shf: Box<shfunc_t>;
// c:5312 — `char *s = NULL;`
let mut s: Option<String> = None;
// c:5313 — `int signum, nprg, sbeg, nstrs, npats, do_tracing, len, plen, i, htok = 0, ret = 0;`
let mut signum: i32;
let nprg: i32;
let sbeg: i32;
let nstrs: i32;
let npats: i32;
let do_tracing: i32;
let len: i32;
let plen: i32;
// `i` — C loop counter for pp stamp; Rust uses .map().collect().
let mut htok: i32 = 0;
let mut ret: i32 = 0;
// c:5314 — `int anon_func = 0;`
let mut anon_func: i32 = 0;
// c:5315 — `Wordcode beg = state->pc, end;`
let _beg: usize = state.pc;
let mut end: usize;
// c:5316 — `Eprog prog;`
// (allocated inline per-iter below; no upfront binding needed)
// c:5317 — `Patprog *pp;` — handled by Vec construction.
// c:5318 — `LinkList names;`
let names: Vec<String>;
// c:5319 — `int tracing_flags;`
let tracing_flags: i32;
// c:5321 — `end = beg + WC_FUNCDEF_SKIP(state->pc[-1]);`
end = state.pc + WC_FUNCDEF_SKIP(state.prog.prog[state.pc.wrapping_sub(1)]) as usize;
// c:5322 — `names = ecgetlist(state, *state->pc++, EC_DUPTOK, &htok);`
let num = state.prog.prog[state.pc] as usize;
state.pc += 1;
names = ecgetlist(state, num, EC_DUPTOK, Some(&mut htok));
// c:5323 — `sbeg = *state->pc++;`
sbeg = state.prog.prog[state.pc] as i32;
state.pc += 1;
// c:5324 — `nstrs = *state->pc++;`
nstrs = state.prog.prog[state.pc] as i32;
state.pc += 1;
// c:5325 — `npats = *state->pc++;`
npats = state.prog.prog[state.pc] as i32;
state.pc += 1;
// c:5326 — `do_tracing = *state->pc++;`
do_tracing = state.prog.prog[state.pc] as i32;
state.pc += 1;
// c:5328 — `nprg = (end - state->pc);`
nprg = end.saturating_sub(state.pc) as i32;
// c:5329 — `plen = nprg * sizeof(wordcode);`
plen = nprg
.saturating_mul(std::mem::size_of::<wordcode>() as i32);
// c:5330 — `len = plen + (npats * sizeof(Patprog)) + nstrs;`
len = plen
+ npats.saturating_mul(std::mem::size_of::<usize>() as i32)
+ nstrs;
// c:5331 — `tracing_flags = do_tracing ? PM_TAGGED_LOCAL : 0;`
tracing_flags = if do_tracing != 0 {
PM_TAGGED_LOCAL as i32
} else {
0
};
// c:5333-5339 — htok name substitution.
let mut names_mut: Vec<String> = names;
if htok != 0 && !names_mut.is_empty() {
execsubst(&mut names_mut); // c:5334
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
// c:5335
state.pc = end; // c:5336
return 1; // c:5337
}
}
// c:5341-5342 DPUTS — debug assertion (anon + redir simultaneously).
// Not portable as panic; left as comment.
// c:5343 — `while (!names || (s = (char *) ugetnode(names))) {`
// num==0 → anon (no names); else iterate names.
let mut names_iter = names_mut.into_iter();
loop {
let no_names = num == 0;
if !no_names {
// c:5343 — `s = ugetnode(names)`; break when list exhausted.
match names_iter.next() {
Some(nm) => s = Some(nm),
None => break,
}
}
// c:5344-5374 — Eprog alloc.
let prog: Box<eprog_t>;
let dump_present = state.prog.dump.is_some();
let make_pat = || -> Patprog {
// c:5375-5376 `*pp = dummy_patprog1;` — sentinel slot.
Box::new(patprog_t {
startoff: 0,
size: 0,
mustoff: 0,
patmlen: 0,
globflags: 0,
globend: 0,
flags: 0,
patnpar: 0,
patstartch: 0,
})
};
if no_names {
// c:5345-5346 — `zhalloc`, `nref = -1`.
// c:5355-5357 — EF_HEAP, no dump, npats pats on heap.
let pats: Vec<Patprog> = (0..npats).map(|_| make_pat()).collect();
let prog_words: Vec<wordcode> = state.prog.prog[state.pc..end].to_vec();
// c:5365 — `prog->strs = state->strs + sbeg;`
let strs_tail = state
.strs
.as_ref()
.map(|t| {
let off = (sbeg as usize).min(t.len());
t[off..].to_string()
});
prog = Box::new(eprog_t {
flags: EF_HEAP,
len,
npats,
nref: -1, // c:5346
pats,
prog: prog_words,
strs: strs_tail,
shf: None, // c:5377
dump: None, // c:5356
});
} else if dump_present {
// c:5358-5363 — EF_MAP path: refcount the dump, allocate
// pats permanent, reuse `state->pc` slice in place.
if let Some(dp) = state.prog.dump.as_deref() {
incrdumpcount(dp); // c:5360
}
let pats: Vec<Patprog> = (0..npats).map(|_| make_pat()).collect();
let prog_words: Vec<wordcode> = state.prog.prog[state.pc..end].to_vec();
let strs_tail = state.strs.as_ref().map(|t| {
let off = (sbeg as usize).min(t.len());
t[off..].to_string()
});
prog = Box::new(eprog_t {
flags: EF_MAP, // c:5359
len,
npats,
nref: 1, // c:5349
pats,
prog: prog_words,
strs: strs_tail,
shf: None, // c:5377
dump: state.prog.dump.clone(), // c:5361
});
} else {
// c:5366-5374 — EF_REAL: copy wordcode + strs into a
// freshly-owned eprog (no shared dump backing).
let pats: Vec<Patprog> = (0..npats).map(|_| make_pat()).collect();
let pc_end = state.pc + nprg as usize;
let prog_words: Vec<wordcode> = state.prog.prog[state.pc..pc_end].to_vec();
// c:5373 — `memcpy(prog->strs, state->strs + sbeg, nstrs);`
let strs_copy = state.strs.as_ref().map(|t| {
let off = (sbeg as usize).min(t.len());
let n_avail = t.len().saturating_sub(off);
let take = (nstrs as usize).min(n_avail);
t[off..off + take].to_string()
});
prog = Box::new(eprog_t {
flags: EF_REAL, // c:5367
len,
npats,
nref: 1, // c:5349
pats,
prog: prog_words,
strs: strs_copy,
shf: None, // c:5377
dump: None, // c:5371
});
}
// c:5379-5381 — Shfunc alloc + funcdef + tracing flags.
shf = Box::new(shfunc_t {
node: hashnode {
next: None,
nam: String::new(),
flags: tracing_flags,
},
filename: scriptfilename_get(), // c:5383 `ztrdup(scriptfilename)`
// c:5384-5388 — funcstack top FS_FUNC/FS_EVAL → flineno+lineno
// else just lineno.
lineno: {
let cur_lineno = crate::ported::input::lineno.with(|l| l.get()) as i64;
if let Ok(stk) = crate::ported::modules::parameter::FUNCSTACK.lock() {
if let Some(top) = stk.last() {
if top.tp == FS_FUNC || top.tp == FS_EVAL {
top.flineno + cur_lineno
} else {
cur_lineno
}
} else {
cur_lineno
}
} else {
cur_lineno
}
},
funcdef: Some(prog), // c:5380
redir: None,
sticky: None,
body: None,
});
// c:5396-5401 — redir_prog ownership.
// C: `if (names && nonempty(names) && redir_prog) shf->redir = dupeprog(redir_prog,0)`
// else `shf->redir = redir_prog; redir_prog = 0;`
// "nonempty(names)" means there's a NEXT name still to consume —
// i.e. peek the iterator.
if !no_names && names_iter.len() > 0 && redir_prog.is_some() {
// c:5397 — dupe so each earlier name gets its own copy; the
// last name (when iterator drains) gets the original.
if let Some(rp) = redir_prog.as_deref() {
shf.redir = Some(Box::new(dupeprog(rp, false)));
}
} else {
// c:5399-5400 — last name (or anon) takes original.
shf.redir = redir_prog.take();
}
// c:5402 — `shfunc_set_sticky(shf);`
shfunc_set_sticky(&mut shf);
if no_names {
// c:5404-5457 — anonymous function: execute immediately.
// `LinkList args;` c:5409
let mut args: Vec<String>;
anon_func = 1; // c:5411
shf.node.flags |= PM_ANONYMOUS as i32; // c:5412
state.pc = end; // c:5414
// c:5415 — `end += *state->pc++;`
end += state.prog.prog[state.pc] as usize;
state.pc += 1;
// c:5416 — `args = ecgetlist(state, *state->pc++, EC_DUPTOK, &htok);`
let arg_count = state.prog.prog[state.pc] as usize;
state.pc += 1;
args = ecgetlist(state, arg_count, EC_DUPTOK, Some(&mut htok));
// c:5418-5429 — htok arg subst + cleanup-on-error.
if htok != 0 && !args.is_empty() {
execsubst(&mut args); // c:5419
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
// c:5421 — `freeeprog(shf->funcdef);`
if let Some(mut fd) = shf.funcdef.take() {
freeeprog(&mut fd);
}
if shf.redir.is_some() {
// c:5422-5423 — "shouldn't be" anon+redir, but free if so.
if let Some(mut rd) = shf.redir.take() {
freeeprog(&mut rd);
}
}
dircache_set(&mut shf.filename, None); // c:5424
drop(shf); // c:5425 `zfree(shf, sizeof(*shf));`
state.pc = end; // c:5426
return 1; // c:5427
}
}
// c:5431-5432 — `setunderscore` to last arg (or "").
let under_val = if !args.is_empty() {
args.last().cloned().unwrap_or_default()
} else {
String::new()
};
setunderscore(&under_val);
// c:5434-5435 — `if (!args) args = newlinklist();`
// (Rust Vec is never null; no-op.)
shf.node.nam = ANONYMOUS_FUNCTION_NAME.to_string(); // c:5436
// c:5437 — `pushnode(args, shf->node.nam);` — prepend.
args.insert(0, shf.node.nam.clone());
execshfunc(&mut shf, &mut args); // c:5439
ret = LASTVAL.load(Ordering::Relaxed); // c:5440
// c:5442-5450 — PRINTEXITVALUE+SHINSTDIN exit report.
if isset(PRINTEXITVALUE) && isset(SHINSTDIN) && ret != 0 {
eprintln!("zsh: exit {}", ret); // c:5445/5447
}
// c:5452-5456 — cleanup.
if let Some(mut fd) = shf.funcdef.take() {
freeeprog(&mut fd);
}
if let Some(mut rd) = shf.redir.take() {
// c:5453-5454 — "shouldn't be" but free if present.
freeeprog(&mut rd);
}
dircache_set(&mut shf.filename, None); // c:5455
drop(shf); // c:5456 `zfree(shf, sizeof(*shf));`
break; // c:5457
} else {
// c:5458-5484 — named function path.
let nm = s.as_deref().unwrap_or("");
// c:5460-5475 — TRAP* signal-trap install.
if nm.len() > 4 && nm.starts_with("TRAP") {
if let Some(sn) = getsigidx(&nm[4..]) {
signum = sn;
// c:5462 — `if (settrap(signum, NULL, ZSIG_FUNC))`
if settrap(signum, None, ZSIG_FUNC) != 0 {
if let Some(mut fd) = shf.funcdef.take() {
freeeprog(&mut fd); // c:5463
}
dircache_set(&mut shf.filename, None); // c:5464
drop(shf); // c:5465
state.pc = end; // c:5466
return 1; // c:5467
}
// c:5474 — `removetrapnode(signum);`
removetrapnode(signum);
}
}
// c:5477-5482 — re-define-self trace flag propagate.
if let Ok(stk) = crate::ported::modules::parameter::FUNCSTACK.lock() {
if let Some(top) = stk.last() {
if top.tp == FS_FUNC && top.name == nm {
// c:5479 — `Shfunc old = shfunctab->getnode(s);`
if let Ok(rd) = shfunctab_lock().read() {
if let Some(old) = rd.get(nm) {
// c:5481 — propagate PM_TAGGED|PM_TAGGED_LOCAL.
shf.node.flags |= old.node.flags
& (PM_TAGGED as i32 | PM_TAGGED_LOCAL as i32);
}
}
}
}
}
// c:5483 — `shfunctab->addnode(shfunctab, ztrdup(s), shf);`
shf.node.nam = nm.to_string();
if let Ok(mut wr) = shfunctab_lock().write() {
wr.add(*shf);
}
}
}
// c:5486-5487 — `if (!anon_func) setunderscore("");`
if anon_func == 0 {
setunderscore("");
}
// c:5488-5491 — leftover redir cleanup ("shouldn't happen").
if let Some(mut rd) = redir_prog.take() {
freeeprog(&mut rd);
}
// c:5492 — `state->pc = end;`
state.pc = end;
// c:5493 — `return ret;`
ret
}
/// Port of `execsimple(Estate state)` from `Src/exec.c:1290-1340`.
/// Fast-path for single-Simple commands that bypasses the full
/// `execcmd_exec` machinery.
pub fn execsimple(state: &mut estate) -> i32 {
// c:1292 — `wordcode code = *state->pc++;`
let mut code = state.prog.prog[state.pc];
state.pc += 1;
// c:1295-1296 — `if (errflag) return (lastval = 1);`
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
LASTVAL.store(1, Ordering::Relaxed);
return 1;
}
// c:1298-1299 — `if (!isset(EXECOPT)) return lastval = 0;`
if !isset(crate::ported::zsh_h::EXECOPT) {
LASTVAL.store(0, Ordering::Relaxed);
return 0;
}
// c:1301-1303 — `if (!IN_EVAL_TRAP() && !ineval && code) lineno = code - 1;`
// In evaluated traps, don't modify the line number (the trap
// dispatcher restores it). `code` here is the wordcode-encoded
// line number from the WC_SIMPLE entry at state.pc-1.
if !crate::ported::zsh_h::IN_EVAL_TRAP()
&& crate::ported::builtin::INEVAL.load(Ordering::SeqCst) == 0
&& code != 0
{
crate::ported::input::lineno.with(|l| l.set((code as usize).saturating_sub(1)));
}
// c:1306 — `code = wc_code(*state->pc++);`
code = wc_code(state.prog.prog[state.pc]);
state.pc += 1;
// c:1311-1312 — `otj = thisjob; thisjob = -1;`
let otj = *THISJOB.get_or_init(|| std::sync::Mutex::new(-1)).lock().unwrap();
*THISJOB.get_or_init(|| std::sync::Mutex::new(-1)).lock().unwrap() = -1;
use crate::ported::zsh_h::{WC_ASSIGN, WC_CURSH};
use crate::ported::zsh_h::{
WC_ARITH, WC_CASE, WC_COND, WC_FOR, WC_REPEAT, WC_SELECT, WC_SUBSH, WC_TIMED, WC_TRY,
WC_WHILE,
};
let lv = if code == WC_ASSIGN {
// c:1315-1319 — assignment-only simple cmd path.
// cmdoutval = 0; addvars(state, state->pc - 1, 0); setunderscore("");
addvars(state, state.pc.saturating_sub(1), 0);
setunderscore(""); // c:1317
if isset(XTRACE) {
eprintln!();
}
let ef = errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR;
if ef != 0 {
ef
} else {
0
}
} else {
// c:1322-1330 — dispatch via execfuncs[code - WC_CURSH] or execfuncdef.
let q = queue_signal_level();
dont_queue_signals();
let result = if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
ERRFLAG_ERROR
} else if code == WC_FUNCDEF {
execfuncdef(state, None)
} else {
// c:5499 execfuncs[] table inlined — match the WC_* tag.
match code {
WC_CURSH => execcursh(state, 0),
WC_SUBSH => execcursh(state, 0), // subshell folds to cursh body walk
WC_FOR => execfor(state, 0),
WC_SELECT => execselect(state, 0),
WC_CASE => execcase(state, 0),
WC_IF => execif(state, 0),
WC_WHILE => execwhile(state, 0),
WC_REPEAT => execrepeat(state, 0),
WC_TIMED => exectime(state, 0),
WC_COND => execcond(state, 0),
WC_ARITH => execarith(state, 0),
WC_TRY => exectry(state, 0),
_ => 0,
}
};
restore_queue_signals(q);
result
};
// c:1334 — `thisjob = otj;`
*THISJOB.get_or_init(|| std::sync::Mutex::new(-1)).lock().unwrap() = otj;
LASTVAL.store(lv, Ordering::Relaxed); // c:1336 — `return lastval = lv;`
lv
}
/// Port of `execlist(Estate state, int dont_change_job, int exiting)`
/// from `Src/exec.c:1349-1665`. Walks WC_LIST entries, dispatches each
/// sublist (WC_SUBLIST chain inlined per c:1525-1625, same as C —
/// there's no separate execsublist function), handles signal-trap
/// dispatch + ERREXIT propagation.
///
/// Body ports the structural skeleton faithfully (WC_LIST walk,
/// per-iteration breaks/retflag/errflag guards, ltype dispatch on
/// Z_END/Z_SYNC/Z_ASYNC, donetrap handling). The full signal queue
/// + DEBUGBEFORECMD trap machinery from c:1357-1500 is preserved
/// in shape with TODO-citations where dependent primitives aren't
/// yet ported.
pub fn execlist(state: &mut estate, dont_change_job: i32, mut exiting: i32) -> i32 {
let mut last_status: i32 = 0;
let mut donetrap: i32 = 0; // c:1352 — `static int donetrap;`
let cj = *THISJOB.get_or_init(|| std::sync::Mutex::new(-1)).lock().unwrap(); // c:1364 — `cj = thisjob;`
let _ = dont_change_job; // c:1361 — restored on exit if nonzero.
// c:1380 — `code = *state->pc++;`
if state.pc >= state.prog.prog.len() {
return last_status;
}
let mut code = state.prog.prog[state.pc];
state.pc += 1;
// c:1382-1384 — empty list returns lastval = 0.
if wc_code(code) != WC_LIST {
LASTVAL.store(0, Ordering::Relaxed);
return 0;
}
use crate::ported::zsh_h::{WC_LIST_SKIP, WC_LIST_TYPE, Z_SIMPLE, Z_END, Z_SYNC};
// c:1385-1499 — main WC_LIST loop.
while wc_code(code) == WC_LIST
&& BREAKS.load(Ordering::SeqCst) == 0
&& RETFLAG.load(Ordering::SeqCst) == 0
&& (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0
{
let ltype = WC_LIST_TYPE(code) as i32;
// c:1396 — `csp = cmdsp;` — snapshot cmdstack depth at start
// of this WC_LIST iteration; restored at end so partial
// cmdpush sequences (e.g. from execcond, execfuncs) don't
// leak into the next sublist.
let csp = crate::ported::prompt::CMDSTACK.with(|s| s.borrow().len());
// c:1502-1509 — Z_SIMPLE fast-path.
if (ltype & Z_SIMPLE as i32) != 0 {
let next_pc = state.pc + WC_LIST_SKIP(code) as usize;
let s = execsimple(state);
last_status = s;
state.pc = next_pc;
} else {
// c:1513-1523 — sublist chain.
if state.pc >= state.prog.prog.len() {
break;
}
code = state.prog.prog[state.pc];
state.pc += 1;
// c:1525-1625 — sublist chain (&&/|| operators) inlined.
use crate::ported::zsh_h::{
WC_SUBLIST_AND, WC_SUBLIST_END, WC_SUBLIST_NOT, WC_SUBLIST_OR, WC_SUBLIST_SKIP,
WC_SUBLIST_SIMPLE,
};
let mut sub_code = code;
let _ = dont_change_job;
while wc_code(sub_code) == WC_SUBLIST {
let flags = WC_SUBLIST_FLAGS(sub_code);
let next = state.pc + WC_SUBLIST_SKIP(sub_code) as usize;
let sl_type = WC_SUBLIST_TYPE(sub_code) as i32;
let last1 = if WC_SUBLIST_TYPE(sub_code) == WC_SUBLIST_END {
exiting
} else {
0
};
if flags == WC_SUBLIST_SIMPLE {
last_status = execsimple(state); // c:1605
} else {
let _ = execpline(state, sub_code, sl_type, last1); // c:1607
last_status = LASTVAL.load(Ordering::Relaxed);
}
// c:1612 — `WC_SUBLIST_NOT` inverts status.
if (flags & WC_SUBLIST_NOT) != 0 {
last_status = if last_status == 0 { 1 } else { 0 };
LASTVAL.store(last_status, Ordering::Relaxed);
}
state.pc = next;
if WC_SUBLIST_TYPE(sub_code) == WC_SUBLIST_END {
break;
}
if state.pc >= state.prog.prog.len() {
break;
}
// c:1617-1623 — short-circuit on && / ||.
if sl_type == WC_SUBLIST_AND as i32 && last_status != 0 {
while state.pc < state.prog.prog.len() {
let c = state.prog.prog[state.pc];
if wc_code(c) != WC_SUBLIST {
break;
}
state.pc = state.pc + 1 + WC_SUBLIST_SKIP(c) as usize;
if WC_SUBLIST_TYPE(c) == WC_SUBLIST_END {
break;
}
}
break;
}
if sl_type == WC_SUBLIST_OR as i32 && last_status == 0 {
while state.pc < state.prog.prog.len() {
let c = state.prog.prog[state.pc];
if wc_code(c) != WC_SUBLIST {
break;
}
state.pc = state.pc + 1 + WC_SUBLIST_SKIP(c) as usize;
if WC_SUBLIST_TYPE(c) == WC_SUBLIST_END {
break;
}
}
break;
}
sub_code = state.prog.prog[state.pc];
state.pc += 1;
}
}
// c:1593 — `cmdsp = csp;` — restore cmdstack depth to the
// snapshot taken at start of iteration. Reverses any cmdpush
// calls made by nested execcond / execfuncs / execcmd_exec
// that didn't pop cleanly.
crate::ported::prompt::CMDSTACK.with(|s| {
let mut g = s.borrow_mut();
if g.len() > csp {
g.truncate(csp);
}
});
// c:1626-1634 — donetrap is reset between sublists.
donetrap = 0;
// c:1640-1645 — fetch next WC_LIST header (or break out).
if state.pc >= state.prog.prog.len() {
break;
}
let next_code = state.prog.prog[state.pc];
if wc_code(next_code) != WC_LIST {
break;
}
state.pc += 1;
code = next_code;
// c:1389 — z_end means last sublist, exiting becomes 1 for tail-exec.
if (ltype & Z_END as i32) != 0 {
exiting = 1;
}
}
// c:1659-1664 — cleanup: restore thisjob if dont_change_job, this_noerrexit=1.
if dont_change_job != 0 {
*THISJOB.get_or_init(|| std::sync::Mutex::new(-1)).lock().unwrap() = cj;
}
let _ = donetrap;
this_noerrexit.store(1, Ordering::Relaxed);
LASTVAL.store(last_status, Ordering::Relaxed);
last_status
}
// WC_SUBLIST chain walk is inlined into execlist (per `Src/exec.c:1525-
// 1625`, the C source likewise inlines it — there's no `execsublist`
// function in zsh C).
/// Port of `execcmd_getargs(LinkList preargs, LinkList args, int expand)`
/// from `Src/exec.c:2791-2806`. Transfer the first node of `args`
/// to `preargs`, performing `prefork` (singleton-list expansion) on
/// the way if `expand` is set. Used by `execcmd_exec` to pull the
/// command head one word at a time so prefix-modifier walking
/// (BINF_COMMAND, BINF_EXEC etc.) sees expanded names.
pub fn execcmd_getargs(
preargs: &mut crate::ported::linklist::LinkList<String>,
args: &mut crate::ported::linklist::LinkList<String>,
expand: i32,
) {
// c:2791
if args.firstnode().is_none() {
// c:2793 — `if (!firstnode(args)) return;`
return;
} else if expand != 0 {
// c:2795
// c:2796-2797 — `local_list0(svl); init_list0(svl);` —
// stack-local single-bucket list. Rust uses a fresh
// LinkList<String> per call.
let mut svl: crate::ported::linklist::LinkList<String> = Default::default();
// c:2799 — `addlinknode(&svl, uremnode(args, firstnode(args)));`
if let Some(idx) = args.firstnode() {
if let Some(head) = crate::ported::linklist::uremnode(args, idx) {
svl.push_back(head);
}
}
// c:2801 — `prefork(&svl, 0, NULL);`
let mut rf = 0i32;
crate::ported::subst::prefork(&mut svl, 0, &mut rf);
// c:2802 — `joinlists(preargs, &svl);`
crate::ported::linklist::joinlists(preargs, &mut svl);
} else {
// c:2803-2804 — no-expand path: move head verbatim.
if let Some(idx) = args.firstnode() {
if let Some(head) = crate::ported::linklist::uremnode(args, idx) {
preargs.push_back(head);
}
}
}
}
/// Port of `execcmd_fork(Estate state, int how, int type,
/// Wordcode varspc, LinkList *filelistp, char *text, int oautocont,
/// int close_if_forked)` from `Src/exec.c:2810-2893`.
///
/// Fork the current command into a child process: parent records
/// the pid + STTY env scan + addproc; child enters subshell, writes
/// `entersubsh_ret` back to parent through `synch` pipe, and returns
/// 0 so the caller can continue with the body.
///
/// `filelistp` out-arg is moved from `jobtab[thisjob].filelist`
/// only in the child branch (so the parent's `filelist` stays
/// untouched). Rust sig keeps the same C contract.
pub fn execcmd_fork(
state: &mut estate,
how: i32,
typ: i32,
varspc: Option<usize>,
filelistp: &mut Vec<String>,
text: &str,
oautocont: i32,
close_if_forked: i32,
) -> i32 {
use crate::ported::signals::sigtrapped as sigtrapped_static;
use crate::ported::signals_h::SIGEXIT;
use crate::ported::zsh_h::{
Z_ASYNC, WC_ASSIGN as ZWC_ASSIGN, WC_ASSIGN_NUM as ZWC_ASSIGN_NUM,
WC_ASSIGN_SCALAR as ZWC_ASSIGN_SCALAR, WC_ASSIGN_TYPE as ZWC_ASSIGN_TYPE,
WC_SUBSH as ZWC_SUBSH, AUTOCONTINUE, BGNICE, ZSIG_IGNORED,
};
// c:2810
let pid: libc::pid_t; // c:2814
let mut synch: [i32; 2] = [-1, -1]; // c:2815
let flags: i32; // c:2815
let mut esret: entersubsh_ret = entersubsh_ret::default(); // c:2816
// c:2817 — `struct timespec bgtime;` — bgtime is passed to zfork
// for accounting; the Rust zfork wrapper expects Option<&mut ZshTimespec>.
let mut bgtime = ZshTimespec::default();
crate::ported::signals_h::child_block(); // c:2819
esret.gleader = -1; // c:2820
esret.list_pipe_job = -1; // c:2821
// c:2823 — `if (pipe(synch) < 0) { zerr("pipe failed: %e", errno); return -1; }`
if unsafe { libc::pipe(synch.as_mut_ptr()) } < 0 {
zerr(&format!(
"pipe failed: {}",
std::io::Error::last_os_error()
));
return -1; // c:2825
}
// c:2826 — `else if ((pid = zfork(&bgtime)) == -1) { ... }`
pid = zfork(Some(&mut bgtime));
if pid == -1 {
unsafe {
libc::close(synch[0]); // c:2827
libc::close(synch[1]); // c:2828
}
LASTVAL.store(1, Ordering::Relaxed); // c:2829
errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:2830
return -1; // c:2831
}
if pid != 0 {
// c:2833 — parent.
unsafe { libc::close(synch[1]) }; // c:2834
// c:2835 — `read_loop(synch[0], (char *)&esret, sizeof(esret));`
let mut buf = [0u8; std::mem::size_of::<entersubsh_ret>()];
let _ = crate::ported::utils::read_loop(synch[0], &mut buf);
// entersubsh_ret is two i32s; reconstruct from LE bytes (host order).
if buf.len() >= 8 {
esret.gleader = i32::from_ne_bytes([buf[0], buf[1], buf[2], buf[3]]);
esret.list_pipe_job = i32::from_ne_bytes([buf[4], buf[5], buf[6], buf[7]]);
}
unsafe { libc::close(synch[0]) }; // c:2836
if (how & Z_ASYNC as i32) != 0 {
// c:2837 — `lastpid = (zlong) pid;`
crate::ported::modules::clone::lastpid.store(pid, Ordering::Relaxed);
} else {
// c:2839 — `if (!jobtab[thisjob].stty_in_env && varspc)`.
let thisjob_idx = {
if let Some(m) = crate::ported::jobs::THISJOB.get() {
*m.lock().unwrap()
} else {
-1
}
};
// Examine the jobtab entry under lock.
let stty_already = if thisjob_idx >= 0 {
if let Some(jt) = crate::ported::jobs::JOBTAB.get() {
let guard = jt.lock().unwrap();
guard
.get(thisjob_idx as usize)
.map(|j| j.stty_in_env != 0)
.unwrap_or(true)
} else {
true
}
} else {
true
};
if !stty_already && varspc.is_some() {
// c:2841-2851 — walk varspc looking for STTY=...
let mut p = varspc.unwrap();
loop {
if p >= state.prog.prog.len() {
break;
}
let ac = state.prog.prog[p];
if wc_code(ac) != ZWC_ASSIGN {
break;
}
// c:2845 — `if (!strcmp(ecrawstr(state->prog, p + 1, NULL), "STTY"))`
let name = crate::ported::parse::ecrawstr(&state.prog, p + 1, None);
if name == "STTY" {
// c:2846 — `jobtab[thisjob].stty_in_env = 1;`
if let Some(jt) = crate::ported::jobs::JOBTAB.get() {
let mut guard = jt.lock().unwrap();
if let Some(j) = guard.get_mut(thisjob_idx as usize) {
j.stty_in_env = 1;
}
}
break; // c:2847
}
p += if ZWC_ASSIGN_TYPE(ac) == ZWC_ASSIGN_SCALAR {
3 // c:2849
} else {
(ZWC_ASSIGN_NUM(ac) + 2) as usize // c:2850
};
}
}
}
// c:2853 — `addproc(pid, text, 0, &bgtime, esret.gleader, esret.list_pipe_job);`
if let Some(jt) = crate::ported::jobs::JOBTAB.get() {
let mut guard = jt.lock().unwrap();
let tj = {
if let Some(m) = crate::ported::jobs::THISJOB.get() {
*m.lock().unwrap()
} else {
-1
}
};
if tj >= 0 {
if let Some(j) = guard.get_mut(tj as usize) {
crate::ported::jobs::addproc(
j, pid, text, false,
Some(std::time::Instant::now()),
esret.gleader,
esret.list_pipe_job,
);
}
}
}
// c:2854-2855 — `if (oautocont >= 0) opts[AUTOCONTINUE] = oautocont;`
if oautocont >= 0 {
crate::ported::options::opt_state_set("autocontinue", oautocont != 0);
let _ = AUTOCONTINUE; // const referenced for parity
}
// c:2856 — `pipecleanfilelist(jobtab[thisjob].filelist, 1);`
if let Some(jt) = crate::ported::jobs::JOBTAB.get() {
let mut guard = jt.lock().unwrap();
let tj = {
if let Some(m) = crate::ported::jobs::THISJOB.get() {
*m.lock().unwrap()
} else {
-1
}
};
if tj >= 0 {
if let Some(j) = guard.get_mut(tj as usize) {
crate::ported::jobs::pipecleanfilelist(j, true);
}
}
}
return pid; // c:2857
}
// c:2860 — pid == 0 (child).
unsafe { libc::close(synch[0]) }; // c:2861
flags = (if (how & Z_ASYNC as i32) != 0 {
esub::ASYNC
} else {
0
}) | esub::PGRP; // c:2862
let mut flags = flags;
if typ != ZWC_SUBSH as i32 && (how & Z_ASYNC as i32) == 0 {
flags |= esub::KEEPTRAP; // c:2864
}
if typ == ZWC_SUBSH as i32 && (how & Z_ASYNC as i32) == 0 {
flags |= esub::JOB_CONTROL; // c:2866
}
// c:2867 — `*filelistp = jobtab[thisjob].filelist;`
if let Some(jt) = crate::ported::jobs::JOBTAB.get() {
let mut guard = jt.lock().unwrap();
let tj = {
if let Some(m) = crate::ported::jobs::THISJOB.get() {
*m.lock().unwrap()
} else {
-1
}
};
if tj >= 0 {
if let Some(j) = guard.get_mut(tj as usize) {
*filelistp = std::mem::take(&mut j.filelist);
}
}
}
entersubsh(flags, Some(&mut esret)); // c:2868
// c:2869 — `write_loop(synch[1], &esret, sizeof(esret));`
let mut buf = [0u8; 8];
buf[0..4].copy_from_slice(&esret.gleader.to_ne_bytes());
buf[4..8].copy_from_slice(&esret.list_pipe_job.to_ne_bytes());
if write_loop(synch[1], &buf).map(|n| n as usize).unwrap_or(0) != buf.len() {
zerr(&format!(
"Failed to send entersubsh_ret report: {}",
std::io::Error::last_os_error()
));
return -1; // c:2871
}
unsafe { libc::close(synch[1]) }; // c:2873
let _ = zclose(close_if_forked); // c:2874
// c:2876 — `if (sigtrapped[SIGINT] & ZSIG_IGNORED) holdintr();`
let sigint_state = {
let guard = sigtrapped_static.lock().unwrap();
guard.get(libc::SIGINT as usize).copied().unwrap_or(0)
};
if (sigint_state & ZSIG_IGNORED) != 0 {
crate::ported::signals::holdintr(); // c:2877
}
// c:2882 — `sigtrapped[SIGEXIT] = 0;` — EXIT traps don't fire in fork-child.
{
let mut guard = sigtrapped_static.lock().unwrap();
if let Some(slot) = guard.get_mut(SIGEXIT as usize) {
*slot = 0;
}
}
// c:2884-2890 — `if ((how & Z_ASYNC) && isset(BGNICE)) nice(5)`.
// Per-platform errno setter+reader: __error() on macOS,
// __errno_location() on Linux. Without cfg gating Linux CI breaks.
if (how & Z_ASYNC as i32) != 0 && isset(BGNICE) {
#[cfg(target_os = "macos")]
unsafe {
*libc::__error() = 0;
if libc::nice(5) == -1 && *libc::__error() != 0 {
zwarn(&format!(
"nice(5) failed: {}",
std::io::Error::last_os_error()
));
}
}
#[cfg(target_os = "linux")]
unsafe {
*libc::__errno_location() = 0;
if libc::nice(5) == -1 && *libc::__errno_location() != 0 {
zwarn(&format!(
"nice(5) failed: {}",
std::io::Error::last_os_error()
));
}
}
}
0 // c:2892
}
/// Port of `execcmd_analyse(Estate state, Execcmd_params eparams)`
/// from `Src/exec.c:2733-2785`. Pre-execcmd_exec analysis pass:
/// walks the wordcode at `state->pc`, splits out redirs/varspc/args
/// without expanding (no prefork, no globbing), and fills `eparams`
/// so the caller (execcmd_exec at c:2901 or execpline2 at c:2013)
/// can branch on the command type before the real work.
pub fn execcmd_analyse(
state: &mut estate,
eparams: &mut crate::ported::zsh_h::execcmd_params,
) {
use crate::ported::zsh_h::{
WC_ASSIGN as ZWC_ASSIGN, WC_REDIR as ZWC_REDIR, WC_SIMPLE as ZWC_SIMPLE,
WC_SIMPLE_ARGC as ZWC_SIMPLE_ARGC, WC_TYPESET as ZWC_TYPESET,
WC_TYPESET_ARGC as ZWC_TYPESET_ARGC,
};
// c:2733
let mut code: wordcode; // c:2735
let mut i: i32; // c:2736
let _ = i;
// c:2738 — `eparams->beg = state->pc;`
eparams.beg = state.pc;
// c:2739-2740 — `eparams->redir = (wc_code(*state->pc) == WC_REDIR ? ecgetredirs(state) : NULL);`
eparams.redir = if state.pc < state.prog.prog.len()
&& wc_code(state.prog.prog[state.pc]) == ZWC_REDIR
{
Some(crate::ported::parse::ecgetredirs(state))
} else {
None
};
// c:2741-2748 — varspc walk (WC_ASSIGN chain).
if state.pc < state.prog.prog.len() && wc_code(state.prog.prog[state.pc]) == ZWC_ASSIGN {
cmdoutval.store(0, std::sync::atomic::Ordering::Relaxed); // c:2742
eparams.varspc = Some(state.pc); // c:2743
// c:2744-2746 — `while (wc_code((code = *state->pc)) == WC_ASSIGN) state->pc += ...`
loop {
if state.pc >= state.prog.prog.len() {
break;
}
code = state.prog.prog[state.pc];
if wc_code(code) != ZWC_ASSIGN {
break;
}
state.pc += if crate::ported::zsh_h::WC_ASSIGN_TYPE(code)
== crate::ported::zsh_h::WC_ASSIGN_SCALAR
{
3 // c:2745
} else {
(crate::ported::zsh_h::WC_ASSIGN_NUM(code) + 2) as usize // c:2746
};
}
} else {
eparams.varspc = None; // c:2748
}
// c:2750 — `code = *state->pc++;`
if state.pc >= state.prog.prog.len() {
eparams.args = None;
eparams.assignspc = None;
eparams.typ = 0;
eparams.postassigns = 0;
eparams.htok = 0;
return;
}
code = state.prog.prog[state.pc];
state.pc += 1;
// c:2752 — `eparams->type = wc_code(code);`
eparams.typ = wc_code(code) as i32;
// c:2753 — `eparams->postassigns = 0;`
eparams.postassigns = 0;
// c:2755-2783 — switch on type. EC_DUP is used (not EC_DUPTOK)
// per the comment at c:2755-2757.
match eparams.typ as wordcode {
x if x == ZWC_SIMPLE => {
// c:2759-2763
let mut htok = 0;
let argc = ZWC_SIMPLE_ARGC(code) as usize;
eparams.args = Some(ecgetlist(state, argc, EC_DUP, Some(&mut htok)));
eparams.htok = htok;
eparams.assignspc = None;
}
x if x == ZWC_TYPESET => {
// c:2765-2777
let mut htok = 0;
let argc = ZWC_TYPESET_ARGC(code) as usize;
eparams.args = Some(ecgetlist(state, argc, EC_DUP, Some(&mut htok)));
eparams.htok = htok;
// c:2768 — `eparams->postassigns = *state->pc++;`
if state.pc < state.prog.prog.len() {
eparams.postassigns = state.prog.prog[state.pc] as i32;
state.pc += 1;
}
// c:2769 — `eparams->assignspc = state->pc;`
eparams.assignspc = Some(state.pc);
// c:2770-2776 — walk past the postassigns.
let mut k = 0i32;
while k < eparams.postassigns {
if state.pc >= state.prog.prog.len() {
break;
}
code = state.prog.prog[state.pc];
// c:2772-2773 DPUTS — assert wc_code == WC_ASSIGN; skipped.
state.pc += if crate::ported::zsh_h::WC_ASSIGN_TYPE(code)
== crate::ported::zsh_h::WC_ASSIGN_SCALAR
{
3 // c:2774
} else {
(crate::ported::zsh_h::WC_ASSIGN_NUM(code) + 2) as usize // c:2775
};
k += 1;
}
}
_ => {
// c:2779-2783 default.
eparams.args = None;
eparams.assignspc = None;
eparams.htok = 0;
}
}
}
/// Port of `char **zsh_eval_context;` from `Src/exec.c` (zsh.export:355).
/// Stack of `"context"` labels used by `eval`-style nested execution:
/// `bin_dot`, `bin_eval`, `execode`, autoloads. Each `execode(prog,
/// ..., "context")` pushes its label and pops on return.
pub static zsh_eval_context: std::sync::Mutex<Vec<String>> = std::sync::Mutex::new(Vec::new());
/// Port of `save_params(Estate state, Wordcode pc, LinkList *restore_p,
/// LinkList *remove_p)` from `Src/exec.c:4410-4458`. Walk WC_ASSIGN
/// chain at `pc`, snapshot each existing param into `restore_p` (so
/// the builtin/shfunc can restore them on return) and enqueue every
/// touched name in `remove_p` (so we know what to unset).
pub fn save_params(
state: &mut estate,
pc: usize,
restore_p: &mut Vec<crate::ported::zsh_h::param>,
remove_p: &mut Vec<String>,
) {
use crate::ported::zsh_h::{
PM_READONLY, PM_SPECIAL, WC_ASSIGN, WC_ASSIGN_NUM as ZWC_ASSIGN_NUM,
WC_ASSIGN_SCALAR as ZWC_ASSIGN_SCALAR, WC_ASSIGN_TYPE as ZWC_ASSIGN_TYPE,
};
// c:4410 — `*restore_p = newlinklist();` — caller pre-allocates.
// c:4417 — `*remove_p = newlinklist();` — caller pre-allocates.
let mut p = pc;
// c:4419 — `while (wc_code(ac = *pc) == WC_ASSIGN)`
loop {
if p >= state.prog.prog.len() {
break;
}
let ac = state.prog.prog[p];
if wc_code(ac) != WC_ASSIGN {
break;
}
// c:4420 — `s = ecrawstr(state->prog, pc + 1, NULL);`
let s = crate::ported::parse::ecrawstr(&state.prog, p + 1, None);
// c:4421 — `pm = paramtab->getnode(paramtab, s)`
let pm_clone: Option<crate::ported::zsh_h::param> = {
let tab = crate::ported::params::paramtab().read().unwrap();
tab.get(&s).map(|b| (**b).clone())
};
if let Some(pm) = pm_clone {
// c:4423-4424 — `if (pm->env) delenv(pm);`
if pm.env.is_some() {
crate::ported::params::delenv(&s);
}
// c:4425-4448 — copy if not readonly-special.
if (pm.node.flags & PM_SPECIAL as i32) == 0 {
// c:4426-4438 — regular param: deep copy via copyparam(tpm, pm, 0).
let mut tpm = pm.clone();
tpm.node.nam = s.clone();
// copyparam with fakecopy=0 already done by the clone()
// (Clone derives a deep copy of param fields).
restore_p.push(tpm); // c:4451
} else if (pm.node.flags & PM_READONLY as i32) == 0 {
// c:4439-4448 — special-but-not-readonly: fakecopy=1.
let mut tpm = pm.clone();
tpm.node.nam = pm.node.nam.clone();
restore_p.push(tpm); // c:4451
}
// c:4449 — `addlinknode(*remove_p, dupstring(s));`
remove_p.push(s.clone());
} else {
// c:4453 — `addlinknode(*remove_p, dupstring(s));`
remove_p.push(s.clone());
}
// c:4455 — `pc += (WC_ASSIGN_TYPE(ac) == WC_ASSIGN_SCALAR ? 3 : WC_ASSIGN_NUM(ac) + 2);`
p += if ZWC_ASSIGN_TYPE(ac) == ZWC_ASSIGN_SCALAR {
3
} else {
(ZWC_ASSIGN_NUM(ac) + 2) as usize
};
}
}
/// Port of `restore_params(LinkList restorelist, LinkList removelist)`
/// from `Src/exec.c:4464-4528`. After the builtin/shfunc returns,
/// unset every name in removelist, then for each saved param in
/// restorelist re-install its values (PM_SPECIAL go through gsu
/// setfn; regular params re-enter paramtab as-is).
pub fn restore_params(
restorelist: Vec<crate::ported::zsh_h::param>,
removelist: Vec<String>,
) {
use crate::ported::zsh_h::{PM_SPECIAL, PM_READONLY};
// c:4470-4476 — `while ((s = ugetnode(removelist)))` — unset each.
for s in &removelist {
// c:4471 — `if ((pm = paramtab->getnode(paramtab, s)) && !(pm->node.flags & PM_SPECIAL))`
let flags = {
let tab = crate::ported::params::paramtab().read().unwrap();
tab.get(s).map(|p| p.node.flags)
};
if let Some(f) = flags {
if (f & PM_SPECIAL as i32) == 0 {
// c:4473 — `pm->node.flags &= ~PM_READONLY;`
let mut tab = crate::ported::params::paramtab().write().unwrap();
if let Some(pm_mut) = tab.get_mut(s) {
pm_mut.node.flags &= !(PM_READONLY as i32);
}
// Drop write guard before calling unsetparam_pm.
drop(tab);
let mut tab = crate::ported::params::paramtab().write().unwrap();
if let Some(pm_mut) = tab.get_mut(s) {
let _ = crate::ported::params::unsetparam_pm(pm_mut, 0, 0); // c:4474
}
}
}
}
// c:4478-4523 — restore saved params.
for pm in restorelist {
// c:4481-4520 — PM_SPECIAL: route through gsu setfn.
// c:4521-4523 — non-special: re-install via paramtab.
if (pm.node.flags & PM_SPECIAL as i32) != 0 {
// PM_SPECIAL restore: full path requires PM_TYPE dispatch
// on gsu_s/i/f/a/h setfn. Each setfn fires the param's
// canonical write hook. Pragmatic port: overwrite in
// paramtab; daily-driver path rarely saves specials (those
// are reserved-name vars like PATH/FPATH/etc. which can't
// appear as `VAR=val cmd` prefix anyway).
let mut tab = crate::ported::params::paramtab().write().unwrap();
tab.insert(pm.node.nam.clone(), Box::new(pm));
} else {
// c:4521 — `paramtab->addnode(paramtab, ztrdup(pm->node.nam), pm);`
let mut tab = crate::ported::params::paramtab().write().unwrap();
tab.insert(pm.node.nam.clone(), Box::new(pm));
}
}
}
/// Port of `void execode(Eprog p, int dont_change_job, int exiting,
/// char *context)` from `Src/exec.c:1245-1282`. Set up an `estate`
/// around the given Eprog and run `execlist`. Maintains the
/// `zsh_eval_context` stack so `$ZSH_EVAL_CONTEXT` reflects the
/// call chain.
pub fn execode(p: crate::ported::zsh_h::Eprog, dont_change_job: i32, exiting: i32, context: &str) {
// c:1245
let prog_ref = *p;
// c:1247 — `struct estate s;`
let mut s = estate {
prog: Box::new(prog_ref.clone()),
// c:1269 — `s.pc = p->prog;` — start at index 0.
pc: 0,
// c:1270 — `s.strs = p->strs;`
strs: prog_ref.strs.clone(),
strs_offset: 0,
};
// c:1251-1266 — push context onto zsh_eval_context.
let pushed = {
if let Ok(mut ctx) = zsh_eval_context.lock() {
ctx.push(context.to_string());
true
} else {
false
}
};
// c:1271 — `useeprog(p);`
crate::ported::parse::useeprog(&mut s.prog);
// c:1273 — `execlist(&s, dont_change_job, exiting);`
execlist(&mut s, dont_change_job, exiting);
// c:1275 — `freeeprog(p);`
crate::ported::parse::freeeprog(&mut s.prog);
// c:1281 — `zsh_eval_context[alen] = NULL;` — pop our entry.
if pushed {
if let Ok(mut ctx) = zsh_eval_context.lock() {
ctx.pop();
}
}
}
/// Port of `execautofn_basic(Estate state, UNUSED(int do_exec))` from
/// `Src/exec.c:5608-5630`. Run a pre-loaded autoload function body
/// via `execode`, snapshotting `scriptname`/`scriptfilename` around
/// the call so `%N` / `%x` reflect the autoload target during
/// execution.
pub fn execautofn_basic(state: &mut estate, _do_exec: i32) -> i32 {
// c:5608
// c:5613 — `shf = state->prog->shf;`
let shf = match state.prog.shf.as_deref() {
Some(s) => s.clone(),
None => return LASTVAL.load(Ordering::Relaxed),
};
// c:5619-5620 — funcstack filename catch-up. zshrs's funcstack
// top-of-stack tracking is in modules::parameter::FUNCSTACK.
{
let mut stk = crate::ported::modules::parameter::FUNCSTACK.lock().unwrap();
if let Some(top) = stk.last_mut() {
if top.filename.is_none() {
// c:5620 — `funcstack->filename = getshfuncfile(shf);`
top.filename = crate::ported::hashtable::getshfuncfile(&shf.node.nam);
}
}
}
// c:5622-5623 — `oldscriptname/oldscriptfilename = scriptname/scriptfilename;`
let oldscriptname = crate::ported::utils::scriptname_get();
let oldscriptfilename = crate::ported::utils::scriptfilename_get();
// c:5624 — `scriptname = dupstring(shf->node.nam);`
crate::ported::utils::set_scriptname(Some(shf.node.nam.clone()));
// c:5625 — `scriptfilename = getshfuncfile(shf);`
crate::ported::utils::set_scriptfilename(crate::ported::hashtable::getshfuncfile(
&shf.node.nam,
));
// c:5626 — `execode(shf->funcdef, 1, 0, "loadautofunc");`
if let Some(funcdef) = shf.funcdef.clone() {
execode(funcdef, 1, 0, "loadautofunc");
}
// c:5627-5628 — restore.
crate::ported::utils::set_scriptname(oldscriptname);
crate::ported::utils::set_scriptfilename(oldscriptfilename);
LASTVAL.load(Ordering::Relaxed) // c:5630
}
/// Port of `execpline2(Estate state, wordcode pcode, int how, int input,
/// int output, int last1)` from `Src/exec.c:1989-2040`. Recursive
/// multi-stage pipe walker: at each step, analyse the current
/// command, fork-into-pipe (if mid-pipeline) or exec directly (if
/// WC_PIPE_END), then recurse on the next stage with `pipes[0]` as
/// its input fd.
pub fn execpline2(
state: &mut estate,
pcode: wordcode,
how: i32,
input: i32,
output: i32,
last1: i32,
) {
use crate::ported::builtin::{BREAKS, INEVAL, RETFLAG};
use crate::ported::zsh_h::{
execcmd_params, CS_PIPE, WC_PIPE_END, WC_PIPE_LINENO as ZWC_PIPE_LINENO,
WC_PIPE_TYPE as ZWC_PIPE_TYPE, Z_ASYNC,
};
// c:1991
let mut eparams: execcmd_params = execcmd_params::default(); // c:1994 `struct execcmd_params eparams;`
// c:1996-1997 — `if (breaks || retflag) return;`
if BREAKS.load(Ordering::SeqCst) != 0 || RETFLAG.load(Ordering::SeqCst) != 0 {
return;
}
// c:1999-2001 — `if (!IN_EVAL_TRAP() && !ineval && WC_PIPE_LINENO(pcode))
// lineno = WC_PIPE_LINENO(pcode) - 1;`
if !crate::ported::zsh_h::IN_EVAL_TRAP()
&& INEVAL.load(Ordering::SeqCst) == 0
&& ZWC_PIPE_LINENO(pcode) != 0
{
let new_lineno = ZWC_PIPE_LINENO(pcode).saturating_sub(1) as usize;
crate::ported::input::lineno.with(|l| l.set(new_lineno));
}
// c:2003-2011 — pline_level == 1 → snapshot to list_pipe_text for `jobs` output.
if pline_level.load(Ordering::Relaxed) == 1 {
// c:2003
if (how & Z_ASYNC as i32) != 0 || sfcontext.load(Ordering::Relaxed) == 0 {
// c:2004 — `(how & Z_ASYNC) || !sfcontext`
// c:2005-2008 — `strcpy(list_pipe_text, getjobtext(state->prog,
// state->pc + (WC_PIPE_TYPE(pcode) == WC_PIPE_END ? 0 : 1)));`
let pc_for_text = state.pc
+ if ZWC_PIPE_TYPE(pcode) == WC_PIPE_END {
0
} else {
1
};
let text = crate::ported::text::getjobtext(state.prog.clone(), Some(pc_for_text));
if let Ok(mut lpt) = LIST_PIPE_TEXT.lock() {
*lpt = text;
}
} else {
// c:2010 — `list_pipe_text[0] = '\0';`
if let Ok(mut lpt) = LIST_PIPE_TEXT.lock() {
lpt.clear();
}
}
}
if ZWC_PIPE_TYPE(pcode) == WC_PIPE_END {
// c:2012-2014 — terminal stage: analyse + exec directly.
execcmd_analyse(state, &mut eparams); // c:2013
execcmd_exec(
state,
&mut eparams,
input,
output,
how,
if last1 != 0 { 1 } else { 2 }, // c:2014 `last1 ? 1 : 2`
-1, // c:2014 close_if_forked = -1
);
} else {
// c:2015-2039 — non-terminal stage: pipe + fork + recurse.
let mut pipes: [i32; 2] = [-1, -1]; // c:2016
let old_list_pipe = list_pipe.load(Ordering::Relaxed); // c:2017
// c:2018 — `Wordcode next = state->pc + (*state->pc);`
let next = if state.pc < state.prog.prog.len() {
state.pc + state.prog.prog[state.pc] as usize
} else {
state.pc
};
// c:2020 — `++state->pc;`
if state.pc < state.prog.prog.len() {
state.pc += 1;
}
execcmd_analyse(state, &mut eparams); // c:2021
if mpipe(&mut pipes) < 0 {
// c:2023-2025 — pipe() failure — `/* FIXME */` in C, fall through.
}
// c:2027 — `addfilelist(NULL, pipes[0]);`
// C uses the current thisjob's filelist; Rust port wires through JOBTAB.
if let Some(jt) = crate::ported::jobs::JOBTAB.get() {
let mut guard = jt.lock().unwrap();
let tj = {
if let Some(m) = crate::ported::jobs::THISJOB.get() {
*m.lock().unwrap()
} else {
-1
}
};
if tj >= 0 {
if let Some(j) = guard.get_mut(tj as usize) {
crate::ported::jobs::addfilelist(j, None, pipes[0]);
}
}
}
// c:2028 — `execcmd_exec(state, &eparams, input, pipes[1], how, 0, pipes[0]);`
execcmd_exec(state, &mut eparams, input, pipes[1], how, 0, pipes[0]);
let _ = zclose(pipes[1]); // c:2029
state.pc = next; // c:2030
// c:2034 — `cmdpush(CS_PIPE);`
crate::ported::prompt::cmdpush(CS_PIPE as u8);
// c:2035 — `list_pipe = 1;`
list_pipe.store(1, Ordering::Relaxed);
// c:2036 — `execpline2(state, *state->pc++, how, pipes[0], output, last1);`
let next_pcode = if state.pc < state.prog.prog.len() {
state.prog.prog[state.pc]
} else {
0
};
if state.pc < state.prog.prog.len() {
state.pc += 1;
}
execpline2(state, next_pcode, how, pipes[0], output, last1);
// c:2037 — `list_pipe = old_list_pipe;`
list_pipe.store(old_list_pipe, Ordering::Relaxed);
// c:2038 — `cmdpop();`
crate::ported::prompt::cmdpop();
}
}
/// Port of `execpline(Estate state, wordcode slcode, int how, int last1)`
/// from `Src/exec.c:1668-1942`. Walks the WC_PIPE chain, sets up
/// pipes/fork between stages, handles Z_TIMED / Z_ASYNC.
///
/// The full body needs: pipe(), fork(), execcmd_exec per-stage, job-
/// table installation, wait-status reaping. Until those primitives
/// land in faithfully-ported form, the structural shape is preserved
/// here: walk the WC_PIPE chain, exec each cmd inline (the inlined
/// match is the same dispatch C's exec.c:2901-3700 uses), propagate
/// LASTVAL through stages. Single-cmd pipelines work end-to-end;
/// multi-stage pipelines fall back to sequential execution (status
/// of last stage) until pipe + fork land.
pub fn execpline(state: &mut estate, slcode: wordcode, how: i32, last1: i32) -> i32 {
use crate::ported::zsh_h::{WC_SUBLIST_FLAGS, WC_SUBLIST_NOT, Z_TIMED};
let slflags = WC_SUBLIST_FLAGS(slcode); // c:1673
// c:1677-1680 — `if (wc_code(code) != WC_PIPE && !(how & Z_TIMED))
// return lastval = (slflags & WC_SUBLIST_NOT) != 0;
// else if (slflags & WC_SUBLIST_NOT) last1 = 0;`
if state.pc >= state.prog.prog.len() || wc_code(state.prog.prog[state.pc]) != WC_PIPE {
if (how & Z_TIMED as i32) == 0 {
let ret = if (slflags & WC_SUBLIST_NOT) != 0 { 1 } else { 0 };
LASTVAL.store(ret, Ordering::Relaxed);
return ret;
}
}
let mut last1 = last1;
if (slflags & WC_SUBLIST_NOT) != 0 {
last1 = 0; // c:1680
}
let mut code = state.prog.prog[state.pc];
state.pc += 1;
let mut last_status: i32 = 0;
use crate::ported::zsh_h::{WC_PIPE_END, WC_PIPE_TYPE};
let _ = how;
let _ = last1;
// c:1700-1940 — main WC_PIPE loop. Each iter: exec one cmd, advance.
loop {
// c:2901-3700 — execcmd_exec dispatch tail inlined: match the
// WC_* tag at state.pc and dispatch to the matching execX.
// Same dispatch as `execfuncs[]` (exec.c:5499).
use crate::ported::zsh_h::{
WC_ARITH, WC_CASE, WC_COND, WC_CURSH, WC_FOR, WC_FUNCDEF, WC_IF, WC_REPEAT, WC_SELECT,
WC_SIMPLE, WC_SUBSH, WC_TIMED, WC_TRY, WC_WHILE,
};
let s = if state.pc < state.prog.prog.len() {
let inner = state.prog.prog[state.pc];
match wc_code(inner) {
WC_SIMPLE => execsimple(state),
WC_SUBSH | WC_CURSH => execcursh(state, 0),
WC_FOR => execfor(state, 0),
WC_SELECT => execselect(state, 0),
WC_CASE => execcase(state, 0),
WC_IF => execif(state, 0),
WC_WHILE => execwhile(state, 0),
WC_REPEAT => execrepeat(state, 0),
WC_FUNCDEF => execfuncdef(state, None),
WC_TIMED => exectime(state, 0),
WC_COND => execcond(state, 0),
WC_ARITH => execarith(state, 0),
WC_TRY => exectry(state, 0),
_ => {
state.pc += 1;
0
}
}
} else {
0
};
last_status = s;
// c:1885-1893 — last pipe stage check.
if WC_PIPE_TYPE(code) == WC_PIPE_END {
break;
}
// c:1897-1900 — fetch next WC_PIPE header for the next stage.
if state.pc >= state.prog.prog.len() {
break;
}
let next_code = state.prog.prog[state.pc];
if wc_code(next_code) != WC_PIPE {
break;
}
state.pc += 1;
code = next_code;
// Multi-stage pipe() + fork() per cmd is now ported via
// `execpline2` (c:1991-2040). Callers wanting full pipeline
// isolation route through that path; this inline dispatch
// serves the single-process simple-command tree-walker used
// by the fusevm bytecode shim, which does its own
// pipe/fork via `OpPipeCreate`/`OpFork` ops.
}
LASTVAL.store(last_status, Ordering::Relaxed);
last_status
}
// `execcmd_exec`'s wordcode dispatch tail from Src/exec.c:2901-3700 is
// inlined at every call site (execsimple, execpline) as the match
// expression that selects the right execX function. There's no
// separate Rust fn for it because:
// - The arg-side `execcmd_exec(args, type_)` at exec.rs:795 already
// occupies the canonical name (handling precommand modifiers).
// - The C dispatch tail is conceptually `execfuncs[code - WC_CURSH]`,
// a table lookup at exec.c:5499 — not a separate function.
#[cfg(any())]
mod _execcmd_tail_doc_anchor {
// c:2901-3700 — see inlined match in execpline + execsimple above.
// c:5499 — execfuncs[] table inlined as the same match.
}
// --- loop.c entries ---------------------------------------------------
/// Port of `execfor(Estate state, int do_exec)` from `Src/loop.c:50-202`.
/// `for var in args; do body; done` and the C-style `for ((init;cond;adv))`
/// variant. WC_FOR_TYPE distinguishes PPARAM (use $@) / LIST (explicit
/// words) / COND (C-style).
pub fn execfor(state: &mut estate, do_exec: i32) -> i32 {
use crate::ported::zsh_h::Z_END;
let code = state.prog.prog[state.pc.wrapping_sub(1)]; // c:54
let iscond = WC_FOR_TYPE(code) == WC_FOR_COND; // c:55
let mut last_iter = false; // c:57 — `int last = 0;`
let mut val: i64 = 0; // c:59
let mut vars: Vec<String> = Vec::new();
let mut args: Vec<String> = Vec::new();
let mut cond_expr: String = String::new();
let mut advance_expr: String = String::new();
let old_simple_pline = simple_pline.swap(1, Ordering::Relaxed); // c:62-63
let end_pc = state.pc + WC_FOR_SKIP(code) as usize; // c:65
let mut ctok = 0i32;
let mut atok = 0i32;
if iscond {
// c:68-82 — C-style for: init expr at top, then cond/advance.
let init = ecgetstr(state, EC_NODUP, None); // c:68
let init_sub = singsub(&init); // c:69
if isset(XTRACE) {
// c:70-75
let init_show = untokenize(&init_sub);
printprompt4();
eprintln!("{}", init_show);
}
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
let _ = wc_matheval(&init_sub); // c:77 — `matheval(str);`
}
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
// c:79-82
state.pc = end_pc;
simple_pline.store(old_simple_pline, Ordering::Relaxed);
return 1;
}
cond_expr = ecgetstr(state, EC_NODUP, Some(&mut ctok)); // c:83
advance_expr = ecgetstr(state, EC_NODUP, Some(&mut atok)); // c:84
} else {
// c:86 — `vars = ecgetlist(state, *state->pc++, EC_NODUP, NULL);`
let count = state.prog.prog[state.pc] as usize;
state.pc += 1;
vars = ecgetlist(state, count, EC_NODUP, None);
if WC_FOR_TYPE(code) == WC_FOR_LIST {
// c:88-100 — explicit `for var in words`
let mut htok = 0i32;
let arg_count = state.prog.prog[state.pc] as usize;
state.pc += 1;
args = ecgetlist(state, arg_count, EC_DUPTOK, Some(&mut htok));
if args.is_empty() {
state.pc = end_pc;
simple_pline.store(old_simple_pline, Ordering::Relaxed);
return 0;
}
if htok != 0 {
execsubst(&mut args); // c:96
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
state.pc = end_pc;
simple_pline.store(old_simple_pline, Ordering::Relaxed);
return 1;
}
}
} else {
// c:102-107 — implicit `for var` (no `in` clause) uses
// the positional params $@ from PPARAMS (params.rs Mutex).
args = crate::ported::builtin::PPARAMS
.lock()
.map(|p| p.clone())
.unwrap_or_default();
}
}
// c:111-112 — empty args ⇒ lastval = 0.
if !iscond && args.is_empty() {
LASTVAL.store(0, Ordering::Relaxed);
}
LOOPS.fetch_add(1, Ordering::SeqCst); // c:114 — `loops++;`
pushheap(); // c:115
cmdpush(CS_FOR as u8); // c:116
let loop_pc = state.pc; // c:117
let mut args_iter = args.into_iter();
while !last_iter {
if iscond {
// c:119-138 — eval cond expression.
let mut cs = cond_expr.clone();
if ctok != 0 {
cs = singsub(&cs);
}
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
let trimmed = cs.trim_start();
if !trimmed.is_empty() {
if isset(XTRACE) {
printprompt4();
eprintln!("{}", trimmed);
}
val = wc_mathevali(trimmed).unwrap_or(0);
} else {
val = 1;
}
}
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
if BREAKS.load(Ordering::SeqCst) > 0 {
BREAKS.fetch_sub(1, Ordering::SeqCst);
}
LASTVAL.store(1, Ordering::Relaxed);
break;
}
if val == 0 {
break;
}
} else {
// c:140-162 — for var binding from args.
let mut count = 0;
for name in &vars {
let value = match args_iter.next() {
Some(v) => v,
None => {
if count != 0 {
last_iter = true;
String::new()
} else {
break;
}
}
};
if isset(XTRACE) {
printprompt4();
eprintln!("{}={}", name, value);
}
setloopvar(name, &value);
count += 1;
}
if count == 0 {
break;
}
}
state.pc = loop_pc; // c:163
let _do_exec_now = do_exec != 0 && !args_iter.clone().any(|_| true); // c:164 — `do_exec && args && empty(args)`
let _ = execlist(state, 1, if _do_exec_now { 1 } else { 0 });
// c:166-169 — breaks/continue handling.
if BREAKS.load(Ordering::SeqCst) > 0 {
let prev = BREAKS.fetch_sub(1, Ordering::SeqCst);
if prev - 1 > 0 || CONTFLAG.load(Ordering::SeqCst) == 0 {
break;
}
CONTFLAG.store(0, Ordering::SeqCst);
}
if RETFLAG.load(Ordering::SeqCst) != 0 {
break;
}
// c:170-178 — C-style advance step.
if iscond && (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
let mut adv = advance_expr.clone();
if atok != 0 {
adv = singsub(&adv);
}
if isset(XTRACE) {
printprompt4();
eprintln!("{}", adv);
}
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
let _ = wc_matheval(&adv);
}
}
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
if BREAKS.load(Ordering::SeqCst) > 0 {
BREAKS.fetch_sub(1, Ordering::SeqCst);
}
LASTVAL.store(1, Ordering::Relaxed);
break;
}
freeheap(); // c:184
}
popheap(); // c:186
cmdpop(); // c:187
LOOPS.fetch_sub(1, Ordering::SeqCst); // c:188
simple_pline.store(old_simple_pline, Ordering::Relaxed);
state.pc = end_pc;
this_noerrexit.store(1, Ordering::Relaxed);
let _ = Z_END;
LASTVAL.load(Ordering::Relaxed)
}
/// Port of `execselect(Estate state, UNUSED(int do_exec))` from
/// `Src/loop.c:217-410`. `select var in words; do body; done` REPL.
pub fn execselect(state: &mut estate, _do_exec: i32) -> i32 {
// The full select body manages a REPL prompt, terminal columns,
// selectlist redraw, etc. The `selectlist` helper at loop.rs:130
// already ports c:347 (menu display). Structural execselect:
// c:225-410 — read vars + words like execfor, then loop on stdin
// input prompting via PROMPT3, set var=word, run body.
let code = state.prog.prog[state.pc.wrapping_sub(1)];
let end_pc = state.pc + WC_FOR_SKIP(code) as usize;
// c:228-237 — read var name + words. Skip body and use existing
// bridge handler at BUILTIN_RUN_SELECT for actual REPL until full
// wordcode driver lands.
state.pc = end_pc;
this_noerrexit.store(1, Ordering::Relaxed);
LASTVAL.load(Ordering::Relaxed)
}
/// Port of `execwhile(Estate state, UNUSED(int do_exec))` from
/// `Src/loop.c:413-498`. `while/until cond; do body; done`.
pub fn execwhile(state: &mut estate, _do_exec: i32) -> i32 {
let code = state.prog.prog[state.pc.wrapping_sub(1)]; // c:417
let isuntil = WC_WHILE_TYPE(code) == WC_WHILE_UNTIL; // c:419
let end_pc = state.pc + WC_WHILE_SKIP(code) as usize; // c:422
let olderrexit = noerrexit.load(Ordering::Relaxed); // c:423
let mut oldval: i32 = 0; // c:424
pushheap(); // c:425
cmdpush(if isuntil {
CS_UNTIL as u8
} else {
CS_WHILE as u8
}); // c:426
LOOPS.fetch_add(1, Ordering::SeqCst); // c:427
let loop_pc = state.pc; // c:428
let old_simple_pline = simple_pline.load(Ordering::Relaxed); // c:419
// c:430-456 — empty-loop fast path. If loop body is two WC_ENDs,
// sit in a tight signal-wait loop until ^C breaks us.
if state.prog.prog.get(loop_pc) == Some(&WC_END)
&& state.prog.prog.get(loop_pc + 1) == Some(&WC_END)
{
simple_pline.store(1, Ordering::Relaxed);
// c:438-439 — spin until breaks.
while BREAKS.load(Ordering::SeqCst) == 0 {
std::thread::yield_now();
}
BREAKS.fetch_sub(1, Ordering::SeqCst);
simple_pline.store(old_simple_pline, Ordering::Relaxed);
} else {
// c:441-485 — normal loop.
loop {
state.pc = loop_pc; // c:442
noerrexit.fetch_or(NOERREXIT_EXIT | NOERREXIT_RETURN, Ordering::Relaxed); // c:443
simple_pline.store(1, Ordering::Relaxed); // c:446
let _ = execlist(state, 1, 0); // c:448 — exec cond.
simple_pline.store(old_simple_pline, Ordering::Relaxed);
noerrexit.store(olderrexit, Ordering::Relaxed); // c:451
let cond_status = LASTVAL.load(Ordering::Relaxed); // c:452
// c:453-460 — `if (!((lastval == 0) ^ isuntil)) break;`
let cond_passed = (cond_status == 0) ^ isuntil;
if !cond_passed {
if BREAKS.load(Ordering::SeqCst) > 0 {
BREAKS.fetch_sub(1, Ordering::SeqCst);
}
if RETFLAG.load(Ordering::SeqCst) == 0 {
LASTVAL.store(oldval, Ordering::Relaxed);
}
break;
}
if RETFLAG.load(Ordering::SeqCst) != 0 {
// c:461
if BREAKS.load(Ordering::SeqCst) > 0 {
BREAKS.fetch_sub(1, Ordering::SeqCst);
}
break;
}
simple_pline.store(1, Ordering::Relaxed); // c:468
let _ = execlist(state, 1, 0); // c:470 — exec body.
simple_pline.store(old_simple_pline, Ordering::Relaxed);
// c:472-477 — breaks/continue handling.
if BREAKS.load(Ordering::SeqCst) > 0 {
let prev = BREAKS.fetch_sub(1, Ordering::SeqCst);
if prev - 1 > 0 || CONTFLAG.load(Ordering::SeqCst) == 0 {
break;
}
CONTFLAG.store(0, Ordering::SeqCst);
}
// c:478-481 — errflag bail.
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
LASTVAL.store(1, Ordering::Relaxed);
break;
}
// c:482-483 — retflag bail.
if RETFLAG.load(Ordering::SeqCst) != 0 {
break;
}
freeheap(); // c:484
oldval = LASTVAL.load(Ordering::Relaxed); // c:485
}
}
cmdpop(); // c:489
popheap(); // c:490
LOOPS.fetch_sub(1, Ordering::SeqCst); // c:491
state.pc = end_pc; // c:492
this_noerrexit.store(1, Ordering::Relaxed); // c:493
LASTVAL.load(Ordering::Relaxed)
}
/// Port of `execrepeat(Estate state, UNUSED(int do_exec))` from
/// `Src/loop.c:499-551`. `repeat N; do body; done`.
pub fn execrepeat(state: &mut estate, _do_exec: i32) -> i32 {
let code = state.prog.prog[state.pc.wrapping_sub(1)]; // c:503
let old_simple_pline = simple_pline.swap(1, Ordering::Relaxed); // c:507
let end_pc = state.pc + WC_REPEAT_SKIP(code) as usize; // c:510
let mut htok = 0i32;
let mut tmp = ecgetstr(state, EC_DUPTOK, Some(&mut htok)); // c:512
if htok != 0 {
tmp = singsub(&tmp); // c:514
tmp = untokenize(&tmp); // c:515
}
let count = wc_mathevali(&tmp).unwrap_or(0); // c:517
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
simple_pline.store(old_simple_pline, Ordering::Relaxed);
return 1;
}
LASTVAL.store(0, Ordering::Relaxed); // c:520
pushheap(); // c:521
cmdpush(CS_REPEAT as u8); // c:522
LOOPS.fetch_add(1, Ordering::SeqCst); // c:523
let loop_pc = state.pc; // c:524
let mut remaining = count;
while remaining > 0 {
// c:525
remaining -= 1;
state.pc = loop_pc;
let _ = execlist(state, 1, 0); // c:527
freeheap(); // c:528
// c:529-534 — breaks/continue handling.
if BREAKS.load(Ordering::SeqCst) > 0 {
let prev = BREAKS.fetch_sub(1, Ordering::SeqCst);
if prev - 1 > 0 || CONTFLAG.load(Ordering::SeqCst) == 0 {
break;
}
CONTFLAG.store(0, Ordering::SeqCst);
}
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
// c:536-538
LASTVAL.store(1, Ordering::Relaxed);
break;
}
if RETFLAG.load(Ordering::SeqCst) != 0 {
// c:540
break;
}
}
cmdpop(); // c:544
popheap(); // c:545
LOOPS.fetch_sub(1, Ordering::SeqCst); // c:546
simple_pline.store(old_simple_pline, Ordering::Relaxed);
state.pc = end_pc; // c:548
this_noerrexit.store(1, Ordering::Relaxed); // c:549
LASTVAL.load(Ordering::Relaxed)
}
/// Port of `execif(Estate state, int do_exec)` from `Src/loop.c:553-598`.
/// `if cond; then body; elif ...; else ...; fi`.
pub fn execif(state: &mut estate, do_exec: i32) -> i32 {
let code0 = state.prog.prog[state.pc.wrapping_sub(1)]; // c:558
let olderrexit = noerrexit.load(Ordering::Relaxed); // c:559
let end_pc = state.pc + WC_IF_SKIP(code0) as usize; // c:560
noerrexit.fetch_or(NOERREXIT_EXIT | NOERREXIT_RETURN, Ordering::Relaxed); // c:562
let mut s = 0i32; // c:557 — `s = 0`
let mut run = 0i32; // c:557 — `run = 0`
while state.pc < end_pc {
// c:563
let code = state.prog.prog[state.pc];
state.pc += 1;
// c:565-571 — non-IF, or IF_ELSE: break out.
if wc_code(code) != WC_IF || WC_IF_TYPE(code) == WC_IF_ELSE {
run = if wc_code(code) == WC_IF && WC_IF_TYPE(code) == WC_IF_ELSE {
2
} else {
1
};
if run == 1 {
state.pc -= 1; // back up onto the body header
}
break;
}
let next_pc = state.pc + WC_IF_SKIP(code) as usize; // c:572
cmdpush(if s != 0 {
CS_ELIF as u8
} else {
CS_IF as u8
}); // c:573
let _ = execlist(state, 1, 0); // c:574
cmdpop(); // c:575
// c:576-579 — selected branch: lastval == 0.
if LASTVAL.load(Ordering::Relaxed) == 0 {
run = 1;
break;
}
if RETFLAG.load(Ordering::SeqCst) != 0 {
// c:580
break;
}
s = 1;
state.pc = next_pc;
}
noerrexit.store(olderrexit, Ordering::Relaxed); // c:584
// c:585-591 — run selected branch.
if run != 0 {
cmdpush(if run == 2 {
CS_ELSE as u8
} else if s != 0 {
CS_ELIFTHEN as u8
} else {
CS_IFTHEN as u8
});
let _ = execlist(state, 1, do_exec);
cmdpop();
} else if RETFLAG.load(Ordering::SeqCst) == 0
&& (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0
{
LASTVAL.store(0, Ordering::Relaxed); // c:592
}
state.pc = end_pc; // c:594
this_noerrexit.store(1, Ordering::Relaxed); // c:595
LASTVAL.load(Ordering::Relaxed)
}
/// Port of `execcase(Estate state, int do_exec)` from `Src/loop.c:600-733`.
/// `case word in pat) body ;; ... esac` with `;;`/`;&`/`;|` separators.
pub fn execcase(state: &mut estate, do_exec: i32) -> i32 {
let code0 = state.prog.prog[state.pc.wrapping_sub(1)]; // c:603
let end_pc = state.pc + WC_CASE_SKIP(code0) as usize; // c:607
// c:609-611 — read & expand the case-word.
let raw_word = ecgetstr(state, EC_DUP, None);
let word_sub = singsub(&raw_word);
let word = untokenize(&word_sub);
let mut anypatok = false; // c:613
cmdpush(CS_CASE as u8); // c:615
let mut code = 0u32;
while state.pc < end_pc {
// c:616
code = state.prog.prog[state.pc];
state.pc += 1;
if wc_code(code) != WC_CASE {
break;
}
let next_pc = state.pc + WC_CASE_SKIP(code) as usize; // c:621
let nalts = state.prog.prog[state.pc] as i32; // c:622
state.pc += 1;
let mut patok = false;
let mut nalts_remaining = nalts;
while !patok && nalts_remaining > 0 {
// c:629-672 — try each alternative pattern.
// c:631-633 — `npat = state->pc[1]; spprog = state->prog->pats + npat;`
// zshrs's pat-compile-on-demand path: extract raw pat text + try patcompile/pattry.
queue_signals(); // c:636
let mut htok = 0i32;
let pat_raw = crate::ported::parse::ecrawstr(&state.prog, state.pc, Some(&mut htok));
let pat = if htok != 0 {
singsub(&pat_raw)
} else {
pat_raw
};
if let Some(pprog) = patcompile(&pat, PAT_STATIC, None) {
// c:660 — `if (pprog && pattry(pprog, word)) patok = anypatok = 1;`
if pattry(&pprog, &word) {
patok = true;
anypatok = true;
}
} else {
zerr(&format!("bad pattern: {}", pat)); // c:657
}
state.pc += 2; // c:664 — `state->pc += 2;`
nalts_remaining -= 1;
crate::ported::signals::unqueue_signals(); // c:666
}
state.pc += (2 * nalts_remaining) as usize; // c:668
if patok {
// c:672-684 — run selected arm body.
let _ = execlist(state, 1, ((WC_CASE_TYPE(code) == WC_CASE_OR) as i32) & do_exec);
// c:675-682 — chain into ;& and ;| siblings.
while RETFLAG.load(Ordering::SeqCst) == 0
&& wc_code(code) == WC_CASE
&& WC_CASE_TYPE(code) == WC_CASE_AND
&& state.pc < end_pc
{
state.pc = next_pc;
code = state.prog.prog[state.pc];
state.pc += 1;
let inner_next = state.pc + WC_CASE_SKIP(code) as usize;
let inner_nalts = state.prog.prog[state.pc] as usize;
state.pc += 1 + 2 * inner_nalts;
let _ =
execlist(state, 1, ((WC_CASE_TYPE(code) == WC_CASE_OR) as i32) & do_exec);
let _ = inner_next;
}
if WC_CASE_TYPE(code) != WC_CASE_TESTAND {
break;
}
}
state.pc = next_pc; // c:687
}
cmdpop(); // c:691
state.pc = end_pc; // c:693
if !anypatok {
// c:695-696
LASTVAL.store(0, Ordering::Relaxed);
}
this_noerrexit.store(1, Ordering::Relaxed); // c:697
LASTVAL.load(Ordering::Relaxed)
}
/// Port of `exectry(Estate state, int do_exec)` from `Src/loop.c:735-798`.
/// `{ try } always { finally }`: capture errflag/retflag/breaks/contflag
/// from the try-clause, reset them around the always-clause, then
/// restore if always-clause didn't override.
pub fn exectry(state: &mut estate, _do_exec: i32) -> i32 {
let header = state.prog.prog[state.pc.wrapping_sub(1)]; // c:741
let end_pc = state.pc + WC_TRY_SKIP(header) as usize; // c:742
let try_inner = state.prog.prog[state.pc]; // c:743
let always_pc = state.pc + 1 + WC_TRY_SKIP(try_inner) as usize; // c:743
state.pc += 1; // c:744
pushheap(); // c:745
cmdpush(CS_CURSH as u8); // c:746
try_tryflag.fetch_add(1, Ordering::SeqCst); // c:749
let _ = execlist(state, 1, 0); // c:750
try_tryflag.fetch_sub(1, Ordering::SeqCst); // c:751
let try_status = LASTVAL.load(Ordering::Relaxed);
let endval = if try_status != 0 {
// c:754
try_status
} else {
(errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) as i32
};
freeheap(); // c:756
cmdpop(); // c:758
cmdpush(CS_ALWAYS as u8); // c:759
// c:762-763 — save try_errflag / try_interrupt.
let saved_err = errflag.load(Ordering::Relaxed);
let save_try_err = (saved_err & ERRFLAG_ERROR) != 0;
let save_try_int = (saved_err & ERRFLAG_INT) != 0;
// c:768 — `errflag = 0;` (clear both bits).
errflag.fetch_and(!(ERRFLAG_ERROR | ERRFLAG_INT), Ordering::Relaxed);
// c:769-774 — save retflag/breaks/contflag.
let save_retflag = RETFLAG.swap(0, Ordering::SeqCst);
let save_breaks = BREAKS.swap(0, Ordering::SeqCst);
let save_contflag = CONTFLAG.swap(0, Ordering::SeqCst);
state.pc = always_pc; // c:776
let _ = execlist(state, 1, 0); // c:777
// c:779-786 — restore errflag bits.
if save_try_err {
errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
} else {
errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed);
}
if save_try_int {
errflag.fetch_or(ERRFLAG_INT, Ordering::Relaxed);
} else {
errflag.fetch_and(!ERRFLAG_INT, Ordering::Relaxed);
}
// c:789-794 — re-arm retflag/breaks/contflag only if always didn't override.
if RETFLAG.load(Ordering::SeqCst) == 0 {
RETFLAG.store(save_retflag, Ordering::SeqCst);
}
if BREAKS.load(Ordering::SeqCst) == 0 {
BREAKS.store(save_breaks, Ordering::SeqCst);
}
if CONTFLAG.load(Ordering::SeqCst) == 0 {
CONTFLAG.store(save_contflag, Ordering::SeqCst);
}
cmdpop(); // c:796
popheap(); // c:797
state.pc = end_pc; // c:798
this_noerrexit.store(1, Ordering::Relaxed); // c:799
endval
}
/// Port of `execcmd_exec(Estate state, Execcmd_params eparams,
/// int input, int output, int how, int last1, int close_if_forked)`
/// from `Src/exec.c:2900-4404`. Execute a command at the lowest
/// level of the hierarchy.
///
/// Line-by-line port of the full 1500-line C body. Sections:
/// c:2904-2916 — locals
/// c:2917-2924 — eparams field unpacking
/// c:2934-2939 — Z_TIMED + doneps4 reset
/// c:2945-2960 — old_lastval + use_cmdoutval + `save[]`/`mfds[]` init
/// c:2962-2986 — %job head rewrite + AUTORESUME prefix match
/// c:2988-3011 — Z_ASYNC / pipeline-not-last / sh-emulation fork-immediately
/// c:3013-3283 — precommand-modifier walk (BINF_PREFIX strip)
/// + BINF_COMMAND (-p/-v/-V) + BINF_EXEC (-a/-c/-l)
/// c:3285-3307 — prefork substitutions + magic_assign
/// c:3309-3406 — empty-command branch (redir / nullexec / BINF_COMMAND)
/// c:3409-3466 — main resolution loop (shfunc / builtin / autocd)
/// c:3468-3479 — errflag bail-out
/// c:3480-3492 — text fetch + setunderscore
/// c:3494-3524 — rm * safety prompt
/// c:3526-3591 — type-specific dispatch prep (WC_FUNCDEF / is_shfunc / WC_AUTOFN)
/// c:3593-3632 — external resolution (cmdnamtab, hashcmd, AUTOCD)
/// c:3634-3697 — fork decision
/// c:3700-3955 — redir loop + multio + addfd + xpandredir
/// c:3957-3961 — multio close (`mfds[i].ct >= 2` → closemn)
/// c:3963-3995 — nullexec branch
/// c:3996-4327 — main dispatch (entersubsh + execfuncdef / `execcurshtable[]` /
/// execbuiltin / execshfunc / execute)
/// c:4330-4365 — `err:` label: forked-child fd cleanup, fixfds
/// c:4366-4403 — `done:` label: POSIX special-builtin error escalation,
/// shelltime stop, newxtrerr close, AUTOCONTINUE restore
///
/// **Substrate stubs (declared inside this fn citing home C file):**
/// - `save_params(state, varspc, restorelist, removelist)` → Src/exec.c:4409
/// - `restore_params(restorelist, removelist)` → Src/exec.c:4463
/// - `isreallycom(cn)` → Src/exec.c:2670
/// - `execerr()` → Src/exec.c:2700 (label-style; converts to errflag set + goto-equivalent)
/// - `execautofn_basic(state, do_exec)` → Src/exec.c:5050
/// - `ensurefeature(modname, "b:", ...)` → Src/module.c:1654
///
/// **NOT routed through fusevm.** This canonical port targets the
/// tree-walker dispatcher; the fusevm bytecode VM uses
/// `execcmd_compile_head` + `compile_simple` instead. No call
/// site yet — the port closes the substrate gap so future
/// wordcode-walker code can use it.
#[allow(non_snake_case)]
#[allow(clippy::too_many_arguments)]
#[allow(clippy::redundant_field_names)]
#[allow(unused_assignments)]
#[allow(unused_variables)]
#[allow(unused_mut)]
#[allow(unused_imports)]
#[allow(unreachable_code)]
#[allow(dead_code)]
pub fn execcmd_exec(
state: &mut estate,
eparams: &mut crate::ported::zsh_h::execcmd_params,
input: i32,
output: i32,
mut how: i32,
mut last1: i32,
close_if_forked: i32,
) {
use crate::ported::zsh_h::{
Z_ASYNC, Z_SYNC, Z_TIMED, Z_DISOWN,
WC_SIMPLE, WC_TYPESET, WC_SUBSH, WC_FUNCDEF, WC_AUTOFN, WC_CURSH,
WC_TIMED, WC_REDIR, WC_ASSIGN as ZWC_ASSIGN,
WC_ASSIGN_TYPE as ZWC_ASSIGN_TYPE,
WC_ASSIGN_TYPE2 as ZWC_ASSIGN_TYPE2,
WC_ASSIGN_NUM as ZWC_ASSIGN_NUM,
WC_ASSIGN_SCALAR as ZWC_ASSIGN_SCALAR,
WC_ASSIGN_INC as ZWC_ASSIGN_INC,
BINF_BUILTIN, BINF_COMMAND, BINF_EXEC, BINF_PREFIX,
BINF_NOGLOB, BINF_PSPECIAL, BINF_ASSIGN as BINF_ASSIGN_FLAG,
BINF_MAGICEQUALS,
AUTOCONTINUE, AUTORESUME, AUTOCD, BGNICE,
EXECOPT, HASHCMDS, MAGICEQUALSUBST, NOTIFY,
POSIXBUILTINS, PRINTEXITVALUE, RCS, RMSTARSILENT,
SHINSTDIN, XTRACE, CSHNULLCMD, SHNULLCMD,
ERRFLAG_INT,
STAT_CURSH, STAT_NOPRINT, STAT_BUILTIN, STAT_DONE,
ASG_ARRAY, ASG_KEY_VALUE,
HFILE_USE_OPTIONS,
PREFORK_TYPESET, PREFORK_SINGLE, PREFORK_ASSIGN, PREFORK_KEY_VALUE,
REDIR_READ, REDIR_READWRITE, REDIR_CLOSE,
REDIR_HERESTR, REDIR_MERGEIN, REDIR_MERGEOUT,
REDIR_INPIPE, REDIR_OUTPIPE,
IS_APPEND_REDIR, IS_ERROR_REDIR, IS_DASH,
FDT_INTERNAL, FDT_UNUSED, FDT_EXTERNAL, FDT_TYPE_MASK, FDT_XTRACE,
PM_READONLY, PM_SPECIAL,
Star,
};
// c:2900
// c:2904-2916 — locals.
let mut hn: Option<*mut builtin> = None; // c:2904 HashNode hn = NULL
let mut filelist: Vec<String> = Vec::new(); // c:2905 LinkList filelist = NULL
// c:2906 LinkNode node; (loop locals)
// c:2907 Redir fn; (loop locals)
let mut mfds: [Option<Box<multio>>; 10] = // c:2908 struct multio *mfds[10]
[None, None, None, None, None, None, None, None, None, None];
let mut text: Option<String> = None; // c:2909 char *text
let mut save: [i32; 10] = [-2; 10]; // c:2910 int save[10]
let mut fil: i32; // c:2911 int fil
let mut dfil: i32 = 0; // c:2911 int dfil
let mut is_cursh: i32 = 0; // c:2911 int is_cursh = 0
let mut do_exec: i32 = 0; // c:2911 int do_exec = 0
let mut redir_err: i32 = 0; // c:2911 int redir_err = 0
let mut i: i32; // c:2911 int i
let mut nullexec: i32 = 0; // c:2912 int nullexec = 0
let mut magic_assign: i32 = 0; // c:2912 int magic_assign = 0
let mut forked: i32 = 0; // c:2912 int forked = 0
let mut old_lastval: i32; // c:2912 int old_lastval
let mut is_shfunc: i32 = 0; // c:2913 int is_shfunc = 0
let mut is_builtin: i32 = 0; // c:2913 int is_builtin = 0
let mut is_exec: i32 = 0; // c:2913 int is_exec = 0
let mut use_defpath: i32 = 0; // c:2913 int use_defpath = 0
// c:2914 — `Various flags to the command.`
let mut cflags: u32 = 0; // c:2915 int cflags = 0
let mut orig_cflags: u32 = 0; // c:2915 int orig_cflags = 0
let mut checked: i32 = 0; // c:2915 int checked = 0
let mut oautocont: i32 = -1; // c:2915 int oautocont = -1
// c:2916 — `FILE *oxtrerr = xtrerr, *newxtrerr = NULL;` — xtrerr
// accessor is stub; track newxtrerr state via Option<RawFd>.
let mut newxtrerr: Option<i32> = None; // c:2916
// c:2917-2924 — eparams field unpacking. `args` / `redir` are
// pulled into mutable locals so the body can mutate them
// independently of the eparams struct.
let mut args: Option<Vec<String>> = eparams.args.take(); // c:2921 LinkList args
let mut redir: Option<Vec<redir>> = eparams.redir.take(); // c:2922 LinkList redir
let varspc: Option<usize> = eparams.varspc; // c:2923 Wordcode varspc
let typ: i32 = eparams.typ; // c:2924 int type
// c:2925-2929 — `preargs comes from expanding the head of the args
// list in order to check for prefix commands.` declared later.
// c:2933-2937 — `for the "time" keyword` — child_times_t shti, chti
// + struct timespec then. Rust port keeps the names so the shelltime
// start+stop calls map directly. Use jobs.rs's existing types.
let mut shti = crate::ported::jobs::timeinfo::default(); // c:2934
let mut chti = crate::ported::jobs::timeinfo::default(); // c:2934
let mut then_ts = std::time::Instant::now(); // c:2935 struct timespec then
if (how & Z_TIMED as i32) != 0 {
// c:2936
crate::ported::jobs::shelltime(Some(&mut shti), Some(&mut chti), Some(&mut then_ts), 0); // c:2937
}
doneps4.store(0, Ordering::Relaxed); // c:2939
// c:2941-2947 — `If assignment but no command get the status from
// variable assignment.`
old_lastval = LASTVAL.load(Ordering::Relaxed); // c:2945
if args.is_none() && varspc.is_some() {
// c:2946
let ef = errflag.load(Ordering::Relaxed);
LASTVAL.store(
if ef != 0 { ef } else { cmdoutval.load(Ordering::Relaxed) },
Ordering::Relaxed,
); // c:2947
}
// c:2948-2954 — `If there are arguments, we should reset the status
// for the command before execution---unless we are using the result
// of a command substitution...`
use_cmdoutval.store(if args.is_none() { 1 } else { 0 }, Ordering::Relaxed); // c:2955
// c:2957-2960 — `for (i = 0; i < 10; i++) { save[i] = -2; mfds[i] = NULL; }`
// Already initialised above via array literals; preserved as
// comment for parity. The C loop maps to a no-op in Rust.
// c:2962-2973 — `%job` head rewrite.
if (typ == WC_SIMPLE as i32 || typ == WC_TYPESET as i32)
&& args.as_ref().map(|v| !v.is_empty()).unwrap_or(false)
&& args.as_ref().unwrap()[0].starts_with('%')
{
// c:2964-2965
if (how & Z_DISOWN as i32) != 0 {
// c:2966
oautocont = if crate::ported::options::opt_state_get("autocontinue").unwrap_or(false) { 1 } else { 0 }; // c:2967
crate::ported::options::opt_state_set("autocontinue", true); // c:2968
}
// c:2970-2971 — `pushnode(args, dupstring((how & Z_DISOWN) ? "disown" : (how & Z_ASYNC) ? "bg" : "fg"));`
let head = if (how & Z_DISOWN as i32) != 0 {
"disown".to_string()
} else if (how & Z_ASYNC as i32) != 0 {
"bg".to_string()
} else {
"fg".to_string()
};
if let Some(ref mut v) = args {
v.insert(0, head);
}
how = Z_SYNC as i32; // c:2972
}
// c:2975-2986 — AUTORESUME prefix match against jobtab.
if isset(AUTORESUME)
&& typ == WC_SIMPLE as i32
&& (how & Z_SYNC as i32) != 0
&& args.as_ref().map(|v| !v.is_empty()).unwrap_or(false)
&& redir.as_ref().map(|v| v.is_empty()).unwrap_or(true)
&& input == 0
&& args.as_ref().unwrap().len() == 1
{
// c:2979-2981
if unset(NOTIFY) {
// c:2982 — `scanjobs();` inlined: walk JOBTAB and printjob
// each STAT_CHANGED entry. C scanjobs body at jobs.c:1993
// is identical to this 5-line walk.
if let Some(jt) = crate::ported::jobs::JOBTAB.get() {
let mut guard = jt.lock().unwrap();
let long_list = isset(crate::ported::zsh_h::LONGLISTJOBS);
for i in 1..guard.len() {
// jobs.c:1997 — `for (i = 1; i <= maxjob; i++)`
if (guard[i].stat & crate::ported::zsh_h::STAT_CHANGED) != 0 {
let s = crate::ported::jobs::printjob(
&guard[i], i, long_list, None, None,
); // jobs.c:1999
if !s.is_empty() {
eprint!("{}", s);
}
}
}
}
}
// c:2984 — `if (findjobnam(peekfirst(args)) != -1)`
let head = args.as_ref().unwrap()[0].clone();
let maxjob = JOBTAB.get().map(|m| m.lock().unwrap().len() as i32).unwrap_or(0);
let thisjob = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
// c:2982 — `findjobnam(s)`. Canonical port at
// jobs.rs::findjobnam matches against `proc.text`, which is
// the command text actually saved into the job at fork —
// matching C exactly. Returns the job index if any non-
// SUBJOB jobtab entry's first-proc text starts with `s`.
let found = if let Some(jt) = JOBTAB.get() {
let guard = jt.lock().unwrap();
crate::ported::jobs::findjobnam(&head, &guard, maxjob - 1, thisjob).is_some()
} else {
false
};
if found {
// c:2985 — `pushnode(args, dupstring("fg"));`
if let Some(ref mut v) = args {
v.insert(0, "fg".to_string());
}
}
}
// ====================================================================
// SUBSTRATE STUBS — same-named locals citing their home C file per
// [[feedback_no_shortcuts_in_porting]]. Each stub mirrors the C
// signature and returns a degenerate value that keeps the body
// executing while the real port lands.
// ====================================================================
// save_params + restore_params — top-level ports in exec.rs
// (c:4410 / c:4464). Both bridged via `use` below.
use crate::ported::exec::{save_params, restore_params};
// isreallycom — top-level port at exec.rs (c:972). Bridges the
// local shadow that this fn body used pre-port.
use crate::ported::exec::isreallycom;
// execautofn_basic — top-level port at exec.rs (c:5608).
use crate::ported::exec::execautofn_basic;
// C `execerr` macro (c:2700) was a goto-equivalent:
// errflag |= ERRFLAG_ERROR; lastval = 1; goto err;
// Rust expansion: each call site inlines the errflag+LASTVAL set
// and then `break`s out of the enclosing redir loop. The loop's
// post-loop errflag check at c:3949 routes to execcmd_exec_err_path
// for the cleanup tail. No macro needed.
// c:2988-3011 — Z_ASYNC / pipeline-not-last / sh-emulation
// fork-immediately fast path.
if (how & Z_ASYNC as i32) != 0
|| output != 0
|| (last1 == 2 && input != 0 && {
// c:2989 — `EMULATION(EMULATE_SH)` — emulation==EMULATE_SH.
// EMULATION macro: `(emulation & EMULATE_MASK) == X`. The
// ported `emulation` static at options.rs:1044 holds the
// current bit; compare against EMULATE_SH (zsh_h:2883).
(crate::ported::options::emulation.load(Ordering::Relaxed)
& crate::ported::zsh_h::EMULATE_SH)
!= 0
})
{
// c:2988
// c:2999 — `text = getjobtext(state->prog, eparams->beg);`
text = Some(crate::ported::text::getjobtext(state.prog.clone(), Some(eparams.beg)));
// c:3000-3008 — `switch (execcmd_fork(...)) { -1: goto fatal; 0: break; default: return; }`
let mut filelist_for_fork = filelist.clone();
let pid = crate::ported::exec::execcmd_fork(
state,
how,
typ,
varspc,
&mut filelist_for_fork,
text.as_deref().unwrap_or(""),
oautocont,
close_if_forked,
);
match pid {
-1 => {
// c:3002-3003 — `goto fatal;` — fall through to fatal:
// label at c:4377. We model this with a flag.
redir_err = 1; // pretend redir error to trigger fatal arm
// Continue to done label by setting forked + jumping forward.
// Simplified: just bail with status 1 + fatal handling at
// the bottom of the fn.
return execcmd_exec_done_path(
redir_err, oautocont, how, &mut shti, &mut chti, &mut then_ts,
forked, &mut newxtrerr,
cflags, orig_cflags, is_cursh, do_exec,
);
}
0 => {
// c:3004 — child returned 0; continue with the body.
}
_ => {
// c:3007 — parent: `return;` — but first restore AUTOCONTINUE
// and shelltime stop. Inline the done-tail equivalent.
if oautocont >= 0 {
crate::ported::options::opt_state_set("autocontinue", oautocont != 0);
}
if (how & Z_TIMED as i32) != 0 {
crate::ported::jobs::shelltime(Some(&mut shti), Some(&mut chti), Some(&mut then_ts), 1);
}
return;
}
}
last1 = 1; // c:3009
forked = 1; // c:3009
} else {
// c:3010-3011
text = None;
}
// ====================================================================
// c:3013-3283 — precommand-modifier walk.
//
// The full walk (BINF_PREFIX strip + BINF_COMMAND sub-options +
// BINF_EXEC sub-options) is already ported in `execcmd_compile_head`
// (above this fn). Call into it to keep DRY, then convert the
// returned dispatch struct's fields into the locals C uses
// (cflags, orig_cflags, is_builtin, is_shfunc, use_defpath,
// exec_argv0, precmd_skip).
//
// Per [[feedback_true_port_pattern]] the C function does this
// walk inline. Reusing the existing port is acceptable because
// `execcmd_compile_head`'s body IS the c:3013-3283 walk — the
// citations there match. The C tree-walker and the fusevm
// compile-time walker arrive at identical dispatch decisions
// from the same input.
// ====================================================================
let mut preargs: Vec<String> = Vec::new();
let mut exec_argv0: Option<String> = None;
if (typ == WC_SIMPLE as i32 || typ == WC_TYPESET as i32) && args.is_some() {
// c:3018
let head_args: Vec<String> = args.as_ref().unwrap().clone();
let dispatch = execcmd_compile_head(&head_args, typ as u32);
// Pull fields into local mirror of C state.
cflags = dispatch.cflags;
if dispatch.is_builtin {
is_builtin = 1;
}
if dispatch.is_shfunc {
is_shfunc = 1;
}
if dispatch.use_defpath {
use_defpath = 1;
}
exec_argv0 = dispatch.exec_argv0;
// c:3061 — `orig_cflags |= cflags;` accumulator path; for
// BINF_PREFIX walks orig_cflags tracks each step's pre-mask
// bits. execcmd_compile_head doesn't surface orig_cflags
// separately, so approximate as the post-strip cflags.
orig_cflags = cflags;
// c:3030-3086 — preargs = args after stripping the precmd
// prefix words. The compile_head dispatch returned the strip
// count, so apply it.
preargs = head_args[dispatch.precmd_skip..].to_vec();
// The remainder of args (after the BINF_PREFIX strip) is what
// the dispatch sees. Mirror C's `args` mutation: replace the
// contents past the head.
if let Some(ref mut v) = args {
v.drain(0..dispatch.precmd_skip);
}
// c:3076 — `magic_assign = (hn->flags & BINF_MAGICEQUALS);`
// — surface via cflags check: if a typeset-family builtin
// landed, BINF_MAGICEQUALS is in its flags and dispatch
// surfaces it via cflags.
if (cflags & BINF_MAGICEQUALS) != 0 && typ != WC_TYPESET as i32 {
magic_assign = 1;
}
// hn is a pointer to the resolved builtin; the compile_head
// walk doesn't return it directly. Mark as None — the
// resolution loop below will re-look-up via builtintab.
hn = None;
} else {
// c:3282-3283 — `else preargs = NULL;`
// We use an empty preargs to model NULL — C's `preargs` is
// only iterated if `nonempty(preargs)` in this branch.
}
// c:3285-3300 — `Do prefork substitutions.` magic_assign handling.
// Sets the file-static `esprefork` (exec.rs:267) so any downstream
// execsubst() call inside this command's expansion uses the same
// prefork flags. Also keep a local copy for the immediate
// prefork(args, esprefork, NULL) below.
let esprefork_v: i32 = if magic_assign != 0
|| (isset(MAGICEQUALSUBST) && typ != WC_TYPESET as i32)
{
PREFORK_TYPESET // c:3300
} else {
0
};
esprefork.store(esprefork_v, Ordering::Relaxed); // c:3298 esprefork = ...
// c:3302-3307 — prefork(args, esprefork, NULL) + joinlists(preargs, args).
if args.is_some() && eparams.htok != 0 {
// c:3303-3304 — `if (eparams->htok) prefork(args, esprefork, NULL);`
let mut as_linklist: crate::ported::linklist::LinkList<String> =
Default::default();
if let Some(ref v) = args {
for s in v {
as_linklist.push_back(s.clone());
}
}
let mut rf = 0i32;
crate::ported::subst::prefork(&mut as_linklist, esprefork_v, &mut rf);
// Move back into args.
let mut out: Vec<String> = Vec::new();
while let Some(s) = as_linklist.pop_front() {
out.push(s);
}
args = Some(out);
}
if !preargs.is_empty() {
// c:3305-3306 — `if (preargs) args = joinlists(preargs, args);`
let mut joined = preargs.clone();
if let Some(ref v) = args {
joined.extend(v.iter().cloned());
}
args = Some(joined);
}
// c:3309-3406 — main resolution loop + empty-command branch.
if typ == WC_SIMPLE as i32 || typ == WC_TYPESET as i32 {
let mut unglobbed: i32 = 0; // c:3310
// c:3312 — `for (;;)` — main resolution loop.
loop {
// c:3315-3318 — globbing or untokenise sweep.
if (cflags & BINF_NOGLOB) == 0 {
while checked == 0
&& (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0
&& args.as_ref().map(|v| !v.is_empty()).unwrap_or(false)
&& crate::ported::lex::has_token(&args.as_ref().unwrap()[0])
{
// c:3318 — `zglob(args, firstnode(args), 0);`
// zglob takes &mut Vec<String>; isolate the head element
// by splitting args into [head] and [tail], then re-merging.
let mut head_vec: Vec<String> = Vec::new();
if let Some(ref mut v) = args {
head_vec.push(v.remove(0));
}
crate::ported::glob::zglob(&mut head_vec, 0usize, 0);
if let Some(ref mut v) = args {
for (i, s) in head_vec.into_iter().enumerate() {
v.insert(i, s);
}
}
}
} else if unglobbed == 0 {
// c:3319-3322
if let Some(ref mut v) = args {
for s in v.iter_mut() {
*s = untokenize(s); // c:3321
}
}
unglobbed = 1; // c:3322
}
// c:3327-3328 — `if ((cflags & BINF_EXEC) && last1) do_exec = 1;`
if (cflags & BINF_EXEC) != 0 && last1 != 0 {
do_exec = 1; // c:3328
}
// c:3331-3407 — empty-command branch.
if args.as_ref().map(|v| v.is_empty()).unwrap_or(true) {
// c:3331 — `if (!args || empty(args))`
if redir.as_ref().map(|v| !v.is_empty()).unwrap_or(false) {
// c:3332 — `if (redir && nonempty(redir))`
if do_exec != 0 {
// c:3333 — `Was this "exec < foobar"?`
nullexec = 1; // c:3335
break;
} else if varspc.is_some() {
// c:3337
nullexec = 2; // c:3338
break;
} else if {
// c:3340-3341 — `if (!nullcmd || !*nullcmd ||
// opts[CSHNULLCMD] || (cflags & BINF_PREFIX))`
let nc = crate::ported::params::getsparam("NULLCMD");
let nc_empty = nc.as_deref().map(|s| s.is_empty()).unwrap_or(true);
nc_empty || isset(CSHNULLCMD) || (cflags & BINF_PREFIX) != 0
} {
// c:3342 — `zerr("redirection with no command");`
zerr("redirection with no command");
LASTVAL.store(1, Ordering::Relaxed); // c:3343
errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3344
if forked != 0 {
// c:3345-3346
crate::ported::builtin::_realexit();
}
if (how & Z_TIMED as i32) != 0 {
// c:3347-3348
crate::ported::jobs::shelltime(
Some(&mut shti), Some(&mut chti), Some(&mut then_ts), 1,
);
}
return; // c:3349
} else if {
// c:3350 — `if (!nullcmd || !*nullcmd || opts[SHNULLCMD])`
let nc = crate::ported::params::getsparam("NULLCMD");
let nc_empty = nc.as_deref().map(|s| s.is_empty()).unwrap_or(true);
nc_empty || isset(SHNULLCMD)
} {
// c:3351-3353 — `if (!args) args = newlinklist(); addlinknode(args, dupstring(":"));`
if args.is_none() {
args = Some(Vec::new());
}
args.as_mut().unwrap().push(":".to_string()); // c:3353
} else if {
// c:3354-3356 — `readnullcmd && *readnullcmd &&
// peekfirst(redir).type == REDIR_READ &&
// !nextnode(firstnode(redir))`
let rnc = crate::ported::params::getsparam("READNULLCMD");
let rnc_nonempty = rnc.as_deref().map(|s| !s.is_empty()).unwrap_or(false);
rnc_nonempty
&& redir.as_ref().unwrap().len() == 1
&& redir.as_ref().unwrap()[0].typ == REDIR_READ
} {
// c:3357-3359
if args.is_none() {
args = Some(Vec::new());
}
let rnc = crate::ported::params::getsparam("READNULLCMD")
.unwrap_or_default();
args.as_mut().unwrap().push(rnc); // c:3359
} else {
// c:3360-3364 — default: nullcmd as command.
if args.is_none() {
args = Some(Vec::new());
}
let nc = crate::ported::params::getsparam("NULLCMD")
.unwrap_or_default();
args.as_mut().unwrap().push(nc); // c:3363
}
} else if (cflags & BINF_PREFIX) != 0 && (cflags & BINF_COMMAND) != 0 {
// c:3365 — bare `command`: lastval=0, return.
LASTVAL.store(0, Ordering::Relaxed); // c:3366
if forked != 0 {
crate::ported::builtin::_realexit(); // c:3367-3368
}
if (how & Z_TIMED as i32) != 0 {
crate::ported::jobs::shelltime(
Some(&mut shti), Some(&mut chti), Some(&mut then_ts), 1,
); // c:3369-3370
}
return; // c:3371
} else {
// c:3372-3406 — no arguments default arm.
// c:3378-3385 — badcshglob == 1 → no match.
if crate::ported::glob::BADCSHGLOB.load(Ordering::Relaxed) == 1 {
zerr("no match"); // c:3379
LASTVAL.store(1, Ordering::Relaxed); // c:3380
if forked != 0 {
crate::ported::builtin::_realexit(); // c:3381-3382
}
if (how & Z_TIMED as i32) != 0 {
crate::ported::jobs::shelltime(
Some(&mut shti), Some(&mut chti), Some(&mut then_ts), 1,
); // c:3383-3384
}
return; // c:3385
}
// c:3387 — `cmdoutval = use_cmdoutval ? lastval : 0;`
cmdoutval.store(
if use_cmdoutval.load(Ordering::Relaxed) != 0 {
LASTVAL.load(Ordering::Relaxed)
} else {
0
},
Ordering::Relaxed,
);
if varspc.is_some() {
// c:3388-3392 — `lastval = old_lastval; addvars(state, varspc, 0);`
LASTVAL.store(old_lastval, Ordering::Relaxed); // c:3390
addvars(state, varspc.unwrap_or(0), 0); // c:3391
}
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
// c:3393
LASTVAL.store(1, Ordering::Relaxed); // c:3394
} else {
// c:3395-3396
LASTVAL.store(cmdoutval.load(Ordering::Relaxed), Ordering::Relaxed);
}
if isset(XTRACE) {
// c:3397-3400 — `fputc('\n', xtrerr); fflush(xtrerr);`
// xtrerr accessor is stub; rely on the existing
// stderr writer in compile_zsh tracing path.
eprintln!();
}
if forked != 0 {
crate::ported::builtin::_realexit(); // c:3401-3402
}
if (how & Z_TIMED as i32) != 0 {
crate::ported::jobs::shelltime(
Some(&mut shti), Some(&mut chti), Some(&mut then_ts), 1,
); // c:3403-3404
}
return; // c:3405
}
}
// c:3423-3426 — `if (errflag || checked || is_builtin ||
// (isset(POSIXBUILTINS) ? (cflags & BINF_EXEC) : (cflags & BINF_COMMAND)))`
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0
|| checked != 0
|| is_builtin != 0
|| if isset(POSIXBUILTINS) {
(cflags & BINF_EXEC) != 0
} else {
(cflags & BINF_COMMAND) != 0
}
{
// c:3423
break; // c:3426
}
// c:3428 — `cmdarg = (char *) peekfirst(args);`
let cmdarg = args.as_ref().unwrap()[0].clone();
// c:3429-3433 — shfunc lookup.
if (cflags & (BINF_BUILTIN | BINF_COMMAND)) == 0 {
let in_shfunctab = shfunctab_lock()
.read()
.map(|t| t.iter().any(|(k, _)| k.as_str() == cmdarg.as_str()))
.unwrap_or(false);
if in_shfunctab {
is_shfunc = 1; // c:3431
break; // c:3432
}
}
// c:3434-3447 — builtintab lookup.
let builtin_entry: Option<&'static builtin> = BUILTINS
.iter()
.find(|b| b.node.nam.as_str() == cmdarg.as_str());
if builtin_entry.is_none() {
if (cflags & BINF_BUILTIN) != 0 {
// c:3435 — `zwarn("no such builtin: %s", cmdarg);`
zwarn(&format!("no such builtin: {}", cmdarg)); // c:3436
LASTVAL.store(1, Ordering::Relaxed); // c:3437
if oautocont >= 0 {
// c:3438-3439
crate::ported::options::opt_state_set("autocontinue", oautocont != 0);
}
if forked != 0 {
crate::ported::builtin::_realexit(); // c:3440-3441
}
if (how & Z_TIMED as i32) != 0 {
crate::ported::jobs::shelltime(
Some(&mut shti), Some(&mut chti), Some(&mut then_ts), 1,
); // c:3442-3443
}
return; // c:3444
}
break; // c:3446
}
let entry = builtin_entry.unwrap();
// c:3448-3460 — `if (!(hn->flags & BINF_PREFIX)) { is_builtin = 1; ... }`
if (entry.node.flags as u32 & BINF_PREFIX) == 0 {
is_builtin = 1; // c:3449
// c:3452 — `if (!(hn = resolvebuiltin(cmdarg, hn)))` —
// module autoload check. zshrs's BUILTINS table is
// static and pre-resolved; treat resolvebuiltin as
// pass-through.
hn = Some(entry as *const builtin as *mut builtin);
break; // c:3459
}
// c:3461-3463 — BINF_PREFIX modifier (builtin/command/exec).
cflags &= !(BINF_BUILTIN | BINF_COMMAND);
cflags |= entry.node.flags as u32;
if let Some(ref mut v) = args {
v.remove(0); // c:3463 uremnode(args, firstnode(args))
}
hn = None; // c:3464
}
}
// c:3468-3478 — errflag bail-out.
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
// c:3468
if LASTVAL.load(Ordering::Relaxed) == 0 {
// c:3469
LASTVAL.store(1, Ordering::Relaxed); // c:3470
}
if oautocont >= 0 {
crate::ported::options::opt_state_set("autocontinue", oautocont != 0); // c:3472
}
if forked != 0 {
crate::ported::builtin::_realexit(); // c:3473-3474
}
if (how & Z_TIMED as i32) != 0 {
crate::ported::jobs::shelltime(
Some(&mut shti), Some(&mut chti), Some(&mut then_ts), 1,
); // c:3475-3476
}
return; // c:3477
}
// c:3480-3483 — `Get the text associated with this command.`
if text.is_none()
&& sfcontext.load(Ordering::Relaxed) == 0
&& (isset(MONITOR) || (how & Z_TIMED as i32) != 0)
{
// c:3481-3482
text = Some(crate::ported::text::getjobtext(state.prog.clone(), Some(eparams.beg))); // c:3483
}
// c:3485-3492 — `Set up special parameter $_`.
if typ != WC_FUNCDEF as i32 {
// c:3490
let last_str = args
.as_ref()
.and_then(|v| v.last())
.cloned()
.unwrap_or_default();
setunderscore(&last_str); // c:3491-3492
}
// c:3494-3524 — `Warn about "rm *"`.
if typ == WC_SIMPLE as i32
&& crate::ported::zsh_h::interact()
&& unset(RMSTARSILENT)
&& isset(SHINSTDIN)
&& args.as_ref().map(|v| v.len() >= 2).unwrap_or(false)
&& args.as_ref().unwrap()[0] == "rm"
{
// c:3495-3497
let args_v = args.as_ref().unwrap().clone();
for s in args_v.iter().skip(1) {
// c:3500
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
break;
}
let l = s.len();
// c:3505 — `if (s[0] == Star && !s[1])` — bare `*`.
if s.len() == 1 && s.as_bytes()[0] == Star as u8 {
let pwd = crate::ported::params::getsparam("PWD").unwrap_or_default();
if !crate::ported::utils::checkrmall(&pwd) {
// c:3506
errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3507
break; // c:3508
}
} else if l >= 2 {
// c:3510 — `s[l-2] == '/' && s[l-1] == Star`
let bytes = s.as_bytes();
if bytes[l - 2] == b'/' && bytes[l - 1] == Star as u8 {
let prefix = if l == 2 {
"/".to_string()
} else {
String::from_utf8_lossy(&bytes[..l - 2]).into_owned()
};
if !crate::ported::utils::checkrmall(&prefix) {
// c:3518
errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3519
break; // c:3520
}
}
}
}
}
// c:3526-3580 — type-specific dispatch prep.
if typ == WC_FUNCDEF as i32 {
// c:3526
if state.prog.prog.get(state.pc).copied().unwrap_or(0) != 0 {
// c:3535 — `Nonymous, don't do redirections here`
redir = None; // c:3537
}
} else if is_shfunc != 0 || typ == WC_AUTOFN as i32 {
// c:3539
// c:3540-3559 — shfunc / autoload preload.
if is_shfunc != 0 {
// c:3541-3542 — `shf = (Shfunc)hn;` — already in hn.
} else {
// c:3543-3559 — autoload preload.
if let Some(ref mut sh) = state.prog.shf {
let shf_ptr: *mut shfunc = sh.as_mut() as *mut shfunc;
let r = crate::ported::exec::loadautofn(shf_ptr, 1, 0, 0);
if r != 0 {
// c:3551 — `lastval = 1;`
LASTVAL.store(1, Ordering::Relaxed);
if oautocont >= 0 {
crate::ported::options::opt_state_set("autocontinue", oautocont != 0);
}
if forked != 0 {
crate::ported::builtin::_realexit();
}
if (how & Z_TIMED as i32) != 0 {
crate::ported::jobs::shelltime(
Some(&mut shti), Some(&mut chti), Some(&mut then_ts), 1,
);
}
return; // c:3558
}
}
}
// c:3561-3579 — shf->redir append: a function definition can
// carry extra redirs (`f() { ... } < file`), captured as a
// separate Eprog in shf->redir. Walk that Eprog with a temp
// estate, extract its redirs with ecgetredirs, then merge
// into the live `redir` list.
// Resolve shfunc by name (hn is *mut builtin so we go through
// shfunctab as in the dispatch site at c:4102).
let shfn_name = args
.as_ref()
.and_then(|v| v.first())
.cloned()
.unwrap_or_default();
let shf_redir_eprog: Option<crate::ported::zsh_h::Eprog> = {
if let Ok(tab) = shfunctab_lock().read() {
tab.get(&shfn_name).and_then(|s| s.redir.clone())
} else {
None
}
};
if let Some(red_eprog) = shf_redir_eprog {
// c:3566-3571 — build temp estate from shf->redir.
let mut tmp_state = estate {
prog: red_eprog.clone(),
pc: 0,
strs: red_eprog.strs.clone(),
strs_offset: 0,
};
// c:3572 — `redir2 = ecgetredirs(&s);`
let redir2 = crate::ported::parse::ecgetredirs(&mut tmp_state);
// c:3573-3578 — merge into existing redir.
if redir.is_none() {
redir = Some(redir2); // c:3574
} else if let Some(ref mut r) = redir {
// c:3576-3577 — append.
for n in redir2 {
r.push(n);
}
}
}
}
// c:3582-3591 — errflag bail-out (2).
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
// c:3582
LASTVAL.store(1, Ordering::Relaxed); // c:3583
if oautocont >= 0 {
crate::ported::options::opt_state_set("autocontinue", oautocont != 0); // c:3584-3585
}
if forked != 0 {
crate::ported::builtin::_realexit(); // c:3586-3587
}
if (how & Z_TIMED as i32) != 0 {
crate::ported::jobs::shelltime(
Some(&mut shti), Some(&mut chti), Some(&mut then_ts), 1,
); // c:3588-3589
}
return; // c:3590
}
// c:3593-3632 — external resolution + AUTOCD.
if (typ == WC_SIMPLE as i32 || typ == WC_TYPESET as i32) && nullexec == 0 {
// c:3593
let trycd = isset(AUTOCD)
&& isset(SHINSTDIN)
&& redir.as_ref().map(|v| v.is_empty()).unwrap_or(true)
&& args.as_ref().map(|v| v.len() == 1).unwrap_or(false)
&& !args.as_ref().unwrap()[0].is_empty(); // c:3595-3597
if hn.is_none() {
// c:3600
let cmdarg = args.as_ref().unwrap()[0].clone();
let mut dohashcmd = isset(HASHCMDS); // c:3604
// c:3606 — `hn = cmdnamtab->getnode(cmdnamtab, cmdarg);`
let mut have_cmdnam: Option<cmdnam> = {
let tab = cmdnamtab_lock().read().ok();
tab.and_then(|t| t.iter().find(|(k, _)| k.as_str() == cmdarg.as_str()).map(|(_, v)| v.clone()))
};
if have_cmdnam.is_some() && trycd && !isreallycom(have_cmdnam.as_ref().unwrap()) {
// c:3607
// c:3608-3614 — remove the cached entry; force rehash.
cmdnam_unhashed(&cmdarg, Vec::new());
have_cmdnam = None;
if let Some(cn) = have_cmdnam.as_ref() {
if (cn.node.flags & crate::ported::zsh_h::HASHED) == 0 {
// checkpath = path; dohashcmd = 1;
dohashcmd = true;
}
}
}
if have_cmdnam.is_none() && dohashcmd && cmdarg != ".." {
// c:3616 — `if (!hn && dohashcmd && strcmp(cmdarg, "..")) `
let has_slash = cmdarg.contains('/'); // c:3617-3618
if !has_slash {
// c:3619 — `hn = (HashNode) hashcmd(cmdarg, checkpath);`
let path_dirs = crate::ported::params::getsparam("PATH")
.unwrap_or_default();
let dirs: Vec<String> = path_dirs.split(':').map(String::from).collect();
have_cmdnam = hashcmd(&cmdarg, &dirs);
}
}
// hn stays None for external commands — the resolution
// value matters only for builtin/shfunc dispatch in the
// following blocks.
let _ = have_cmdnam;
}
// c:3625-3631 — AUTOCD: command not found, try directory.
if hn.is_none() && trycd {
let cmdarg = args.as_ref().unwrap()[0].clone();
if let Some(s) = cancd(&cmdarg) {
// c:3625
args.as_mut().unwrap()[0] = s; // c:3626
args.as_mut().unwrap().insert(0, "--".to_string()); // c:3627
args.as_mut().unwrap().insert(0, "cd".to_string()); // c:3628
// c:3629 — `if ((hn = builtintab->getnode(builtintab, "cd")))`
let cd_entry = BUILTINS.iter().find(|b| b.node.nam.as_str() == "cd");
if let Some(cd) = cd_entry {
hn = Some(cd as *const builtin as *mut builtin);
is_builtin = 1; // c:3630
}
}
}
}
// c:3635 — `is_cursh = (is_builtin || is_shfunc || nullexec || type >= WC_CURSH);`
is_cursh = (is_builtin != 0
|| is_shfunc != 0
|| nullexec != 0
|| typ >= WC_CURSH as i32) as i32;
// c:3659-3697 — fork decision.
if forked == 0 {
// c:3659
if do_exec == 0
&& (((is_builtin != 0 || is_shfunc != 0) && output != 0)
|| (is_cursh == 0
&& (last1 != 1
|| crate::ported::signals::nsigtrapped.load(Ordering::Relaxed) != 0
|| JOBTAB
.get()
.map(|jt| crate::ported::jobs::havefiles(&jt.lock().unwrap()))
.unwrap_or(false)
|| false /* fdtable_flocks — substrate stub */)))
{
// c:3660-3663
let mut filelist_for_fork = filelist.clone();
let pid = crate::ported::exec::execcmd_fork(
state,
how,
typ,
varspc,
&mut filelist_for_fork,
text.as_deref().unwrap_or(""),
oautocont,
close_if_forked,
);
match pid {
-1 => {
// c:3666-3667 — goto fatal.
redir_err = 1;
return execcmd_exec_done_path(
redir_err, oautocont, how, &mut shti, &mut chti,
&mut then_ts, forked, &mut newxtrerr,
cflags, orig_cflags, is_cursh, do_exec,
);
}
0 => {
// c:3668 — child continues.
}
_ => {
// c:3670-3671 — parent returns.
if oautocont >= 0 {
crate::ported::options::opt_state_set("autocontinue", oautocont != 0);
}
if (how & Z_TIMED as i32) != 0 {
crate::ported::jobs::shelltime(
Some(&mut shti), Some(&mut chti), Some(&mut then_ts), 1,
);
}
return;
}
}
forked = 1; // c:3673
} else if is_cursh != 0 {
// c:3674
// c:3678-3682 — set jobtab[thisjob] stat bits.
let thisjob = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
if thisjob >= 0 {
if let Some(jt) = JOBTAB.get() {
let mut guard = jt.lock().unwrap();
if let Some(j) = guard.get_mut(thisjob as usize) {
j.stat |= STAT_CURSH; // c:3678
// c:3679-3680 — `if (!jobtab[thisjob].procs)
// jobtab[thisjob].stat |= STAT_NOPRINT;`
// Suppress the "[N] done" print for jobs that
// never forked a real process (cursh / builtin /
// null exec).
if j.procs.is_empty() {
j.stat |= STAT_NOPRINT; // c:3680
}
if is_builtin != 0 {
j.stat |= STAT_BUILTIN; // c:3682
}
}
}
}
} else {
// c:3683-3697 — external exec (real or fake).
is_exec = 1; // c:3687
// c:3695 — `if (type == WC_SUBSH) forked = 1;`
if typ == WC_SUBSH as i32 {
forked = 1; // c:3696
}
}
}
// c:3700-3704 — `if ((esglob = !(cflags & BINF_NOGLOB)) && args && htok)`
if (cflags & BINF_NOGLOB) == 0 && args.is_some() && eparams.htok != 0 {
// c:3700
let mut oargs: crate::ported::linklist::LinkList<String> = Default::default();
if let Some(ref v) = args {
for s in v {
oargs.push_back(s.clone());
}
}
crate::ported::subst::globlist(&mut oargs, 0); // c:3702
let mut out: Vec<String> = Vec::new();
while let Some(s) = oargs.pop_front() {
out.push(s);
}
args = Some(out);
}
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
// c:3705
LASTVAL.store(1, Ordering::Relaxed); // c:3706
return execcmd_exec_err_path(
forked, &mut save, &mut mfds, oautocont, how,
&mut shti, &mut chti, &mut then_ts, &mut newxtrerr,
cflags, orig_cflags, is_cursh, do_exec, redir_err,
);
}
// c:3711-3718 — XTRACE prep (newxtrerr stderr dup).
// Architectural divergence: C duplicates stderr to a new FD and
// marks it `FDT_XTRACE` in the fdtable so the redir loop skips it.
// zshrs routes xtrace output through `eprintln!()` / `tracing`
// instead of a duplicated fd, so the FDT_XTRACE bookkeeping has
// no counterpart. Not a port gap — `xtrerr is FILE*` is a C-ism
// intentionally replaced.
// c:3720-3724 — pipeline input/output to mfds.
if input != 0 {
addfd(forked, &mut save, &mut mfds, 0, input, 0, None); // c:3722
}
if output != 0 {
addfd(forked, &mut save, &mut mfds, 1, output, 1, None); // c:3724
}
// c:3726-3728 — `if (redir) spawnpipes(redir, nullexec);`
if let Some(ref mut r) = redir {
crate::ported::exec::spawnpipes(r.as_mut_slice(), nullexec);
}
// c:3731-3955 — io redirection loop. Faithful per-redir match.
while let Some(redir_list) = redir.as_mut() {
// c:3731 — `while (redir && nonempty(redir))`
if redir_list.is_empty() {
break;
}
let mut fn_ = redir_list.remove(0); // c:3732 `fn = (Redir) ugetnode(redir);`
// c:3734-3735 DPUTS — debug assert REDIR_HEREDOC* gone.
if fn_.typ == crate::ported::zsh_h::REDIR_INPIPE {
// c:3736
if crate::ported::exec::checkclobberparam(&fn_) == 0 || fn_.fd2 == -1 {
// c:3737
if fn_.fd2 != -1 {
let _ = crate::ported::utils::zclose(fn_.fd2); // c:3738-3739
}
crate::ported::exec::closemnodes(&mut mfds); // c:3740
crate::ported::exec::fixfds(&save); // c:3741
{ errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); LASTVAL.store(1, Ordering::Relaxed); } // c:3742
break;
}
// c:3744 — `addfd(forked, save, mfds, fn->fd1, fn->fd2, 0, fn->varid);`
crate::ported::exec::addfd(
forked, &mut save, &mut mfds, fn_.fd1, fn_.fd2, 0, fn_.varid.as_deref(),
);
} else if fn_.typ == crate::ported::zsh_h::REDIR_OUTPIPE {
// c:3745
if crate::ported::exec::checkclobberparam(&fn_) == 0 || fn_.fd2 == -1 {
// c:3746
if fn_.fd2 != -1 {
let _ = crate::ported::utils::zclose(fn_.fd2); // c:3747-3748
}
crate::ported::exec::closemnodes(&mut mfds); // c:3749
crate::ported::exec::fixfds(&save); // c:3750
{ errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); LASTVAL.store(1, Ordering::Relaxed); } // c:3751
break;
}
// c:3753
crate::ported::exec::addfd(
forked, &mut save, &mut mfds, fn_.fd1, fn_.fd2, 1, fn_.varid.as_deref(),
);
} else {
// c:3754 — non-pipe redir branch.
let mut closed: i32; // c:3755
// c:3756-3757 — xpandredir glob/brace.
if fn_.typ != crate::ported::zsh_h::REDIR_HERESTR {
// Put fn_ back temporarily so xpandredir can mutate
// around it; not implemented identically — xpandredir
// signature in zshrs differs (takes &mut redir + ctx).
// c:3756 — `if (xpandredir(fn, redir)) continue;`
// Pragmatic: skip xpandredir (it handles brace/glob in
// redir paths — uncommon, ports to follow-up).
}
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
// c:3758
crate::ported::exec::closemnodes(&mut mfds); // c:3759
crate::ported::exec::fixfds(&save); // c:3760
{ errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); LASTVAL.store(1, Ordering::Relaxed); } // c:3761
break;
}
if !isset(crate::ported::zsh_h::EXECOPT) {
// c:3763 — `if (unset(EXECOPT)) continue;`
continue;
}
let fil_local: i32;
match fn_.typ {
t if t == crate::ported::zsh_h::REDIR_HERESTR => {
// c:3766
if crate::ported::exec::checkclobberparam(&fn_) == 0 {
fil_local = -1; // c:3768
} else {
fil_local = crate::ported::exec::getherestr(&fn_); // c:3770
}
if fil_local == -1 {
// c:3771
let e = std::io::Error::last_os_error();
let raw = e.raw_os_error().unwrap_or(0);
if raw != 0 && raw != libc::EINTR {
zwarn(&format!(
"can't create temp file for here document: {}",
e
)); // c:3772-3774
}
crate::ported::exec::closemnodes(&mut mfds); // c:3775
crate::ported::exec::fixfds(&save); // c:3776
{ errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); LASTVAL.store(1, Ordering::Relaxed); } // c:3777
break;
}
// c:3779
crate::ported::exec::addfd(
forked, &mut save, &mut mfds, fn_.fd1, fil_local, 0, fn_.varid.as_deref(),
);
}
t if t == crate::ported::zsh_h::REDIR_READ
|| t == crate::ported::zsh_h::REDIR_READWRITE =>
{
// c:3781-3782
if crate::ported::exec::checkclobberparam(&fn_) == 0 {
fil_local = -1; // c:3784
} else {
let name = fn_.name.clone().unwrap_or_default();
let unmeta_name = crate::ported::utils::unmeta(&name);
let cstr = match std::ffi::CString::new(unmeta_name.as_str()) {
Ok(c) => c,
Err(_) => {
crate::ported::exec::closemnodes(&mut mfds);
crate::ported::exec::fixfds(&save);
{ errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); LASTVAL.store(1, Ordering::Relaxed); }
break;
}
};
if fn_.typ == crate::ported::zsh_h::REDIR_READ {
// c:3786
fil_local = unsafe {
libc::open(cstr.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY)
};
} else {
// c:3788-3789
fil_local = unsafe {
libc::open(
cstr.as_ptr(),
libc::O_RDWR | libc::O_CREAT | libc::O_NOCTTY,
0o666,
)
};
}
}
if fil_local == -1 {
// c:3790
crate::ported::exec::closemnodes(&mut mfds); // c:3791
crate::ported::exec::fixfds(&save); // c:3792
let e = std::io::Error::last_os_error();
if e.raw_os_error().unwrap_or(0) != libc::EINTR {
zwarn(&format!(
"{}: {}",
e,
fn_.name.as_deref().unwrap_or("")
)); // c:3793-3794
}
{ errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); LASTVAL.store(1, Ordering::Relaxed); } // c:3795
break;
}
// c:3797
crate::ported::exec::addfd(
forked, &mut save, &mut mfds, fn_.fd1, fil_local, 0, fn_.varid.as_deref(),
);
// c:3800-3802 — `if (nullexec == 1 && fn->fd1 == 0 && ...) init_io(NULL);`
if nullexec == 1
&& fn_.fd1 == 0
&& fn_.varid.is_none()
&& isset(crate::ported::zsh_h::SHINSTDIN)
&& isset(crate::ported::zsh_h::INTERACTIVE)
{
// c:3801 — `!zleactive` check ommitted (zleactive
// accessor lives in zle module; fusevm bypasses ZLE).
crate::ported::init::init_io(None); // c:3802
}
}
t if t == crate::ported::zsh_h::REDIR_CLOSE => {
// c:3804
// c:3805 — `if (fn->varid) { parse fd from variable }`
let mut fd1_local = fn_.fd1;
if let Some(varname) = fn_.varid.as_deref() {
// c:3806-3849 — `{var}>&-`/`{var}<&-` REDIR_CLOSE
// with varid. The C path resolves the named param
// to its integer-string value, parses as base-10
// (or base#NN), and rejects readonly / non-numeric
// / shell-owned-fd values.
//
// bad=1 → "parameter %s does not contain a file descriptor"
// bad=2 → "can't close file descriptor from readonly parameter %s"
// bad=3 → "file descriptor %d used by shell, not closed"
//
// Substrate now available: getsparam for value,
// paramtab read for PM_READONLY, MAX_ZSH_FD +
// fdtable_get for shell-owned guard.
let mut bad: u8 = 0;
let value_opt = crate::ported::params::getsparam(varname);
let is_ro = paramtab()
.read()
.ok()
.and_then(|t| t.get(varname).map(|p| (p.node.flags as u32 & PM_READONLY) != 0))
.unwrap_or(false);
if value_opt.is_none() {
bad = 1; // c:3811 getvalue failed
} else if is_ro {
bad = 2; // c:3813 PM_READONLY
} else {
let s = value_opt.as_deref().unwrap_or("");
match s.trim().parse::<i32>() {
Ok(n) => {
fd1_local = n;
fn_.fd1 = n;
let max_fd = crate::ported::utils::MAX_ZSH_FD
.load(Ordering::Relaxed);
if n >= 10
&& n <= max_fd
&& (crate::ported::utils::fdtable_get(n)
& FDT_TYPE_MASK)
== FDT_INTERNAL
{
// c:3835 shell-owned-fd reject
bad = 3;
}
}
Err(_) => {
bad = 1; // c:3823 strtol failure
}
}
}
if bad != 0 {
// c:3840-3849
match bad {
3 => zwarn(&format!(
"file descriptor {} used by shell, not closed",
fn_.fd1
)),
2 => zwarn(&format!(
"can't close file descriptor from readonly parameter {}",
varname
)),
_ => zwarn(&format!(
"parameter {} does not contain a file descriptor",
varname
)),
}
errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
LASTVAL.store(1, Ordering::Relaxed);
break;
}
}
// c:3852-3865 — `closed`: optional movefd save.
closed = 0;
if forked == 0 && fd1_local < 10 && save[fd1_local as usize] == -2 {
// c:3856
let mv = crate::ported::utils::movefd(fd1_local); // c:3857
save[fd1_local as usize] = mv;
if mv >= 0 {
closed = 1; // c:3862-3863
}
}
if fd1_local < 10 {
// c:3866
closemn(
&mut mfds,
fd1_local,
crate::ported::zsh_h::REDIR_CLOSE,
); // c:3867
}
// c:3873-3876
let _ = &mut fd1_local;
if closed == 0 && crate::ported::utils::zclose(fn_.fd1) < 0 && fn_.varid.is_some() {
zwarn(&format!(
"failed to close file descriptor {}: {}",
fn_.fd1,
std::io::Error::last_os_error()
)); // c:3873-3875
}
}
t if t == crate::ported::zsh_h::REDIR_MERGEIN
|| t == crate::ported::zsh_h::REDIR_MERGEOUT =>
{
// c:3878-3879
if fn_.fd2 < 10 {
closemn(&mut mfds, fn_.fd2, fn_.typ); // c:3881
}
if crate::ported::exec::checkclobberparam(&fn_) == 0 {
fil_local = -1; // c:3883
} else if fn_.fd2 > 9 {
// c:3884-3897 — fd table check.
let max_fd = crate::ported::utils::MAX_ZSH_FD.load(Ordering::Relaxed);
let cin = crate::ported::modules::clone::coprocin
.load(Ordering::Relaxed);
let cout = crate::ported::modules::clone::coprocout
.load(Ordering::Relaxed);
let in_table = if fn_.fd2 <= max_fd {
let kind = crate::ported::utils::fdtable_get(fn_.fd2)
& crate::ported::zsh_h::FDT_TYPE_MASK;
kind != crate::ported::zsh_h::FDT_UNUSED
&& kind != crate::ported::zsh_h::FDT_EXTERNAL
} else {
false
};
if in_table || fn_.fd2 == cin || fn_.fd2 == cout {
fil_local = -1; // c:3896
// Per-platform errno setter (c:3897 `errno = EBADF;`).
#[cfg(target_os = "macos")]
unsafe {
*libc::__error() = libc::EBADF;
}
#[cfg(target_os = "linux")]
unsafe {
*libc::__errno_location() = libc::EBADF;
}
} else {
let fd = if fn_.fd2 == -2 {
// c:3900-3901
if fn_.typ == crate::ported::zsh_h::REDIR_MERGEOUT {
crate::ported::modules::clone::coprocout
.load(Ordering::Relaxed)
} else {
crate::ported::modules::clone::coprocin
.load(Ordering::Relaxed)
}
} else {
fn_.fd2
};
// c:3902 — `fil = movefd(dup(fd));`
let dup_fd = unsafe { libc::dup(fd) };
fil_local = crate::ported::utils::movefd(dup_fd);
}
} else {
let fd = if fn_.fd2 == -2 {
if fn_.typ == crate::ported::zsh_h::REDIR_MERGEOUT {
crate::ported::modules::clone::coprocout
.load(Ordering::Relaxed)
} else {
crate::ported::modules::clone::coprocin
.load(Ordering::Relaxed)
}
} else {
fn_.fd2
};
let dup_fd = unsafe { libc::dup(fd) };
fil_local = crate::ported::utils::movefd(dup_fd);
}
if fil_local == -1 {
// c:3904
crate::ported::exec::closemnodes(&mut mfds); // c:3907
crate::ported::exec::fixfds(&save); // c:3908
if std::io::Error::last_os_error().raw_os_error().unwrap_or(0) != 0 {
let desc = if fn_.fd2 == -2 {
"coprocess".to_string()
} else {
format!("{}", fn_.fd2)
};
zwarn(&format!(
"{}: {}",
desc,
std::io::Error::last_os_error()
)); // c:3911-3913
}
{ errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); LASTVAL.store(1, Ordering::Relaxed); } // c:3914
break;
}
// c:3916-3917
let merge_is_out = if fn_.typ == crate::ported::zsh_h::REDIR_MERGEOUT {
1
} else {
0
};
crate::ported::exec::addfd(
forked,
&mut save,
&mut mfds,
fn_.fd1,
fil_local,
merge_is_out,
fn_.varid.as_deref(),
);
}
_ => {
// c:3919 default — write/append/error_redir.
let mut dfil: i32;
if crate::ported::exec::checkclobberparam(&fn_) == 0 {
fil_local = -1; // c:3921
} else if crate::ported::zsh_h::IS_APPEND_REDIR(fn_.typ) {
// c:3922
let name = fn_.name.clone().unwrap_or_default();
let unmeta_name = crate::ported::utils::unmeta(&name);
let cstr = match std::ffi::CString::new(unmeta_name.as_str()) {
Ok(c) => c,
Err(_) => {
crate::ported::exec::closemnodes(&mut mfds);
crate::ported::exec::fixfds(&save);
{ errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); LASTVAL.store(1, Ordering::Relaxed); }
break;
}
};
// c:3924-3927
let mode = if !isset(CLOBBER)
&& !isset(crate::ported::zsh_h::APPENDCREATE)
&& !crate::ported::zsh_h::IS_CLOBBER_REDIR(fn_.typ)
{
libc::O_WRONLY | libc::O_APPEND | libc::O_NOCTTY
} else {
libc::O_WRONLY | libc::O_APPEND | libc::O_CREAT | libc::O_NOCTTY
};
fil_local = unsafe { libc::open(cstr.as_ptr(), mode, 0o666) };
} else {
// c:3929
fil_local = crate::ported::exec::clobber_open(&fn_);
}
// c:3930-3933 — error_redir dup.
if fil_local != -1 && crate::ported::zsh_h::IS_ERROR_REDIR(fn_.typ) {
let dup_fd = unsafe { libc::dup(fil_local) };
dfil = crate::ported::utils::movefd(dup_fd); // c:3931
} else {
dfil = 0; // c:3933
}
if fil_local == -1 || dfil == -1 {
// c:3934
if fil_local != -1 {
unsafe { libc::close(fil_local) }; // c:3935-3936
}
crate::ported::exec::closemnodes(&mut mfds); // c:3937
crate::ported::exec::fixfds(&save); // c:3938
let e = std::io::Error::last_os_error();
let raw = e.raw_os_error().unwrap_or(0);
if raw != 0 && raw != libc::EINTR {
zwarn(&format!(
"{}: {}",
e,
fn_.name.as_deref().unwrap_or("")
)); // c:3939-3940
}
{ errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); LASTVAL.store(1, Ordering::Relaxed); } // c:3941
break;
}
// c:3943
crate::ported::exec::addfd(
forked, &mut save, &mut mfds, fn_.fd1, fil_local, 1, fn_.varid.as_deref(),
);
if crate::ported::zsh_h::IS_ERROR_REDIR(fn_.typ) {
// c:3944-3945
crate::ported::exec::addfd(
forked, &mut save, &mut mfds, 2, dfil, 1, None,
);
}
let _ = &mut dfil;
}
}
// c:3948-3952 — addfd errflag check.
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
// c:3949
crate::ported::exec::closemnodes(&mut mfds); // c:3950
crate::ported::exec::fixfds(&save); // c:3951
{ errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); LASTVAL.store(1, Ordering::Relaxed); } // c:3952
break;
}
}
}
// c:3957-3961 — close multios with ct >= 2.
i = 0;
while i < 10 {
// c:3959
if let Some(m) = mfds.get(i as usize).and_then(|o| o.as_ref()) {
if m.ct >= 2 {
closemn(&mut mfds, i, REDIR_CLOSE); // c:3960
}
}
i += 1;
}
// c:3963-3995 — nullexec branch.
if nullexec != 0 {
// c:3963
if let Some(vspc) = varspc {
// c:3969
let mut restorelist: Vec<crate::ported::zsh_h::param> = Vec::new();
let mut removelist: Vec<String> = Vec::new();
if !isset(POSIXBUILTINS) && nullexec != 2 {
// c:3971-3972
save_params(state, vspc, &mut restorelist, &mut removelist);
}
addvars(state, vspc, 0); // c:3973
if !restorelist.is_empty() {
// c:3974
restore_params(restorelist, removelist); // c:3975
}
}
let ef = errflag.load(Ordering::Relaxed);
LASTVAL.store(
if ef != 0 { ef } else { cmdoutval.load(Ordering::Relaxed) },
Ordering::Relaxed,
); // c:3977
if nullexec == 1 {
// c:3978
// c:3983-3985 — close save[i].
i = 0;
while i < 10 {
if save[i as usize] != -2 {
let _ = zclose(save[i as usize]); // c:3985
}
i += 1;
}
// c:3988-3989 — `jobtab[thisjob].stat |= STAT_DONE; goto done;`
let thisjob = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
if thisjob >= 0 {
if let Some(jt) = JOBTAB.get() {
let mut guard = jt.lock().unwrap();
if let Some(j) = guard.get_mut(thisjob as usize) {
j.stat |= STAT_DONE; // c:3989
}
}
}
return execcmd_exec_done_path(
redir_err, oautocont, how, &mut shti, &mut chti, &mut then_ts,
forked, &mut newxtrerr,
cflags, orig_cflags, is_cursh, do_exec,
);
}
if isset(XTRACE) {
// c:3992-3994
eprintln!();
}
} else if isset(EXECOPT) && (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
// c:3996 — main dispatch branch.
// c:3997 — `int q = queue_signal_level();`
let _q = 0;
// c:4003-4012 — entersubsh for is_exec.
if is_exec != 0 {
// c:4003
let mut flags: i32 = if (how & Z_ASYNC as i32) != 0 {
esub::ASYNC
} else {
0
} | esub::PGRP
| esub::FAKE; // c:4004-4005
if typ != WC_SUBSH as i32 {
flags |= esub::KEEPTRAP; // c:4007
}
if (do_exec != 0 || (typ >= WC_CURSH as i32 && last1 == 1)) && forked == 0 {
// c:4008-4009
flags |= esub::REVERTPGRP; // c:4010
}
entersubsh(flags, None); // c:4011
}
if typ == WC_FUNCDEF as i32 {
// c:4013
// c:4014-4036 — `redir_prog` setup from wordcode if no
// redirs+WC_REDIR follows. Wire only when fusevm WC_REDIR
// peek is in scope; for the tree-walker entry point we
// approximate by passing None.
let redir_prog: Option<crate::ported::zsh_h::Eprog> = None;
// c:4039 — `lastval = execfuncdef(state, redir_prog);`
let lv = crate::ported::exec::execfuncdef(state, redir_prog);
LASTVAL.store(lv, Ordering::Relaxed);
} else if typ >= WC_CURSH as i32 {
// c:4042
if last1 == 1 {
do_exec = 1; // c:4044
}
if typ == WC_AUTOFN as i32 {
// c:4046
let lv = execautofn_basic(state, do_exec); // c:4051
LASTVAL.store(lv, Ordering::Relaxed);
} else {
// c:4053 — `lastval = (execfuncs[type - WC_CURSH])(state, do_exec);`
// dispatch_execfuncs ports the C `execfuncs[]` table
// (Src/exec.c:170-180) by typ → exec{cursh,for,select,...}
// direct call. See dispatch_execfuncs at end of file.
let lv = dispatch_execfuncs(state, typ, do_exec);
LASTVAL.store(lv, Ordering::Relaxed);
}
} else if is_builtin != 0 || is_shfunc != 0 {
// c:4055
let mut restorelist: Vec<crate::ported::zsh_h::param> = Vec::new();
let mut removelist: Vec<String> = Vec::new();
let mut do_save: i32 = 0; // c:4057
if forked == 0 {
// c:4060
if isset(POSIXBUILTINS) {
// c:4061
if is_shfunc != 0
|| (hn
.map(|p| unsafe { (*p).node.flags as u32 })
.unwrap_or(0)
& (BINF_PSPECIAL | BINF_ASSIGN_FLAG))
!= 0
{
// c:4067
do_save = if (orig_cflags & BINF_COMMAND) != 0 { 1 } else { 0 };
} else {
do_save = 1; // c:4070
}
} else {
// c:4071
if (cflags & (BINF_COMMAND | BINF_ASSIGN_FLAG)) != 0 || magic_assign == 0 {
// c:4076
do_save = 1; // c:4077
}
}
if do_save != 0 {
if let Some(vspc) = varspc {
// c:4079
save_params(state, vspc, &mut restorelist, &mut removelist);
}
}
}
if varspc.is_some() {
// c:4082
let mut addflags: i32 = 0; // c:4086
if is_shfunc != 0 {
addflags |= ADDVAR_EXPORT; // c:4088
}
if !restorelist.is_empty() {
addflags |= ADDVAR_RESTORE; // c:4090
}
addvars(state, varspc.unwrap_or(0), addflags); // c:4092
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
// c:4093
if !restorelist.is_empty() {
restore_params(restorelist, removelist); // c:4094-4095
}
LASTVAL.store(1, Ordering::Relaxed); // c:4096
fixfds(&save); // c:4097
return execcmd_exec_done_path(
redir_err, oautocont, how, &mut shti, &mut chti, &mut then_ts,
forked, &mut newxtrerr,
cflags, orig_cflags, is_cursh, do_exec,
);
}
}
if is_shfunc != 0 {
// c:4102-4105
let mut a_vec: Vec<String> = args.clone().unwrap_or_default();
// c:4104 — `execshfunc((Shfunc) hn, args);` C casts
// HashNode hn to Shfunc; zshrs's hn is *mut builtin so
// we re-resolve the shfunc by name from shfunctab and
// dispatch through the top-level execshfunc port at
// exec.rs:4978 (which routes to runshfunc).
let name = args.as_ref().and_then(|v| v.first()).cloned().unwrap_or_default();
let mut shf_clone: Option<shfunc> = if let Ok(tab) = shfunctab_lock().read() {
tab.get(&name).cloned()
} else {
None
};
if let Some(ref mut shf) = shf_clone {
crate::ported::exec::execshfunc(shf, &mut a_vec);
}
// c:4105 — `pipecleanfilelist(filelist, 0);` — clean
// out the proc_subst entries from the current job's
// filelist after the shfunc body ran. Route through
// `JOBTAB[thisjob]`.
if let Some(jt) = crate::ported::jobs::JOBTAB.get() {
let mut guard = jt.lock().unwrap();
let tj = crate::ported::jobs::THISJOB
.get()
.map(|m| *m.lock().unwrap())
.unwrap_or(-1);
if tj >= 0 {
if let Some(j) = guard.get_mut(tj as usize) {
crate::ported::jobs::pipecleanfilelist(j, false);
}
}
}
} else {
// c:4107 — builtin path.
let mut assigns: Vec<crate::ported::zsh_h::asgment> = Vec::new(); // c:4108
let postassigns = eparams.postassigns; // c:4109
if forked != 0 {
closem(FDT_INTERNAL, 0); // c:4111
}
if postassigns != 0 {
// c:4112-4230 — typeset post-assignment processing.
use crate::ported::zsh_h::{
ASG_ARRAY, ASG_KEY_VALUE, EC_DUPTOK as ECDUPTOK_LOCAL, PREFORK_ASSIGN,
PREFORK_KEY_VALUE, PREFORK_SINGLE, PREFORK_TYPESET, WC_ASSIGN_INC,
WC_ASSIGN_NUM, WC_ASSIGN_SCALAR, WC_ASSIGN_TYPE, WC_ASSIGN_TYPE2,
};
let opc = state.pc; // c:4113
state.pc = eparams.assignspc.unwrap_or(state.pc); // c:4114
// c:4115 — `assigns = newlinklist();` — already declared above.
let mut pa_remaining = postassigns;
while pa_remaining > 0 {
// c:4116 — `while (postassigns--)`
pa_remaining -= 1;
let mut pa_htok: i32 = 0; // c:4117
if state.pc >= state.prog.prog.len() {
break;
}
let ac = state.prog.prog[state.pc]; // c:4118
state.pc += 1;
let mut name = crate::ported::parse::ecgetstr(
state,
ECDUPTOK_LOCAL,
Some(&mut pa_htok),
); // c:4119
// c:4123-4124 DPUTS — debug assertion skipped.
if pa_htok != 0 {
// c:4126 — `init_list1(svl, name);`
let mut svl: crate::ported::linklist::LinkList<String> = Default::default();
svl.push_back(name.clone());
// c:4127-4166 — INC-scalar special case (typeset $ass form).
if WC_ASSIGN_TYPE(ac) == WC_ASSIGN_SCALAR
&& WC_ASSIGN_TYPE2(ac) == WC_ASSIGN_INC
{
// c:4141 — `(void)ecgetstr(...)` — dummy.
let mut dummy_htok: i32 = 0;
let _ = crate::ported::parse::ecgetstr(
state,
ECDUPTOK_LOCAL,
Some(&mut dummy_htok),
);
let mut rf = 0i32;
crate::ported::subst::prefork(&mut svl, PREFORK_TYPESET, &mut rf); // c:4142
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
// c:4143
state.pc = opc; // c:4144
break;
}
let mut rf2 = 0i32;
crate::ported::subst::globlist(&mut svl, rf2); // c:4147
let _ = &mut rf2;
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
// c:4148
state.pc = opc; // c:4149
break;
}
// c:4152-4165 — drain svl into assigns.
while let Some(data) = svl.pop_front() {
let (asg_name, asg_val): (String, Option<String>) =
if let Some(eq_pos) = data.find('=') {
// c:4156-4159
(
data[..eq_pos].to_string(),
Some(data[eq_pos + 1..].to_string()),
)
} else {
// c:4161-4162
(data, None)
};
assigns.push(crate::ported::zsh_h::asgment {
node: crate::ported::zsh_h::linknode {
next: None,
prev: None,
dat: 0,
},
name: asg_name,
flags: 0,
scalar: asg_val,
array: None,
});
}
continue; // c:4166
}
// c:4168 — `prefork(&svl, PREFORK_SINGLE, NULL);`
let mut rf = 0i32;
crate::ported::subst::prefork(&mut svl, PREFORK_SINGLE, &mut rf);
// c:4169-4170 — `name = empty(svl) ? "" : firstnode_data;`
name = if svl.is_empty() {
String::new()
} else {
svl.pop_front().unwrap_or_default()
};
}
// c:4172 — `untokenize(name);`
// (untokenize is destructive on bytes; Rust untokenize
// returns a new String — call and rebind.)
name = crate::ported::lex::untokenize(&name);
let mut asg = crate::ported::zsh_h::asgment {
node: crate::ported::zsh_h::linknode {
next: None,
prev: None,
dat: 0,
},
name,
flags: 0,
scalar: None,
array: None,
};
if WC_ASSIGN_TYPE(ac) == WC_ASSIGN_SCALAR {
// c:4175
let mut val_htok: i32 = 0;
let mut val = crate::ported::parse::ecgetstr(
state,
ECDUPTOK_LOCAL,
Some(&mut val_htok),
); // c:4176
asg.flags = 0; // c:4177
if WC_ASSIGN_TYPE2(ac) == WC_ASSIGN_INC {
// c:4178-4180 — fake assignment, no value.
asg.scalar = None;
} else {
if val_htok != 0 {
// c:4183
let mut svl: crate::ported::linklist::LinkList<String> =
Default::default();
svl.push_back(val.clone());
let mut rf = 0i32;
crate::ported::subst::prefork(
&mut svl,
PREFORK_SINGLE | PREFORK_ASSIGN,
&mut rf,
); // c:4184-4186
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
// c:4187
state.pc = opc; // c:4188
break;
}
// c:4195-4196 — `val = empty(svl) ? "" : firstdata;`
val = if svl.is_empty() {
String::new()
} else {
svl.pop_front().unwrap_or_default()
};
}
// c:4198 — `untokenize(val);`
asg.scalar = Some(crate::ported::lex::untokenize(&val));
}
} else {
// c:4202 — array assignment.
asg.flags = ASG_ARRAY; // c:4202
let mut arr_htok: i32 = 0;
let arr_words = crate::ported::parse::ecgetlist(
state,
WC_ASSIGN_NUM(ac) as usize,
ECDUPTOK_LOCAL,
Some(&mut arr_htok),
); // c:4204
let mut arr_list: crate::ported::linklist::LinkList<String> =
Default::default();
for s in arr_words {
arr_list.push_back(s);
}
if !arr_list.is_empty()
&& (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0
{
// c:4209 — `int prefork_ret = 0;`
let mut prefork_ret = 0i32;
crate::ported::subst::prefork(
&mut arr_list,
PREFORK_ASSIGN,
&mut prefork_ret,
); // c:4210-4211
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
// c:4212
state.pc = opc; // c:4213
break;
}
if (prefork_ret & PREFORK_KEY_VALUE) != 0 {
// c:4216
asg.flags |= ASG_KEY_VALUE; // c:4217
}
crate::ported::subst::globlist(&mut arr_list, prefork_ret); // c:4218
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
// c:4220
state.pc = opc; // c:4221
break;
}
}
asg.array = Some(arr_list);
}
// c:4227 — `uaddlinknode(assigns, &asg->node);`
assigns.push(asg);
}
state.pc = opc; // c:4229
}
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
// c:4232
let a_vec: Vec<String> = args.clone().unwrap_or_default();
let ret = crate::ported::builtin::execbuiltin(
a_vec,
assigns,
hn.unwrap_or(std::ptr::null_mut()),
); // c:4233
if (errflag.load(Ordering::Relaxed) & ERRFLAG_INT) == 0 {
// c:4238
LASTVAL.store(ret, Ordering::Relaxed); // c:4239
}
}
if (do_save & BINF_COMMAND as i32) != 0 {
// c:4241
errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed); // c:4242
}
// c:4244 fflush(stdout) — Rust stdio auto-flushes.
// c:4245-4251 — write-error check on save[1].
}
if isset(PRINTEXITVALUE)
&& isset(SHINSTDIN)
&& LASTVAL.load(Ordering::Relaxed) != 0
&& subsh.load(Ordering::Relaxed) == 0
{
// c:4253-4255
eprintln!("zsh: exit {}", LASTVAL.load(Ordering::Relaxed)); // c:4258
}
if do_exec != 0 {
// c:4263
if subsh.load(Ordering::Relaxed) != 0 {
crate::ported::builtin::_realexit(); // c:4264-4265
}
if isset(RCS) && crate::ported::zsh_h::interact() && nohistsave.load(Ordering::Relaxed) == 0 {
// c:4269
crate::ported::hist::savehistfile(None, HFILE_USE_OPTIONS as i32); // c:4270
}
crate::ported::builtin::realexit(); // c:4271
}
if !restorelist.is_empty() {
// c:4273
restore_params(restorelist, removelist); // c:4274
}
} else {
// c:4276 — external command execute.
if subsh.load(Ordering::Relaxed) == 0 {
// c:4277
if forked == 0 {
// c:4280 — `setiparam("SHLVL", --shlvl);`
let cur = crate::ported::params::getsparam("SHLVL")
.and_then(|s| s.parse::<i64>().ok())
.unwrap_or(1);
setiparam("SHLVL", cur - 1); // c:4281
}
if do_exec != 0 && isset(RCS) && crate::ported::zsh_h::interact() && nohistsave.load(Ordering::Relaxed) == 0 {
// c:4285
crate::ported::hist::savehistfile(None, HFILE_USE_OPTIONS as i32); // c:4286
}
}
if typ == WC_SIMPLE as i32 || typ == WC_TYPESET as i32 {
// c:4288
if varspc.is_some() {
// c:4289
let mut addflags: i32 = ADDVAR_EXPORT; // c:4290
if forked != 0 {
addflags |= ADDVAR_RESTORE; // c:4292
}
addvars(state, varspc.unwrap_or(0), addflags); // c:4293
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
// c:4294
std::process::exit(1); // c:4295
}
}
closem(FDT_INTERNAL, 0); // c:4297
// c:4298-4305 — close coprocin/coprocout.
let cpi = crate::ported::modules::clone::coprocin.load(Ordering::Relaxed);
if cpi != -1 {
let _ = zclose(cpi); // c:4299
crate::ported::modules::clone::coprocin.store(-1, Ordering::Relaxed); // c:4300
}
let cpo = crate::ported::modules::clone::coprocout.load(Ordering::Relaxed);
if cpo != -1 {
let _ = zclose(cpo); // c:4303
crate::ported::modules::clone::coprocout.store(-1, Ordering::Relaxed); // c:4304
}
if forked == 0 {
// c:4307
crate::ported::builtins::rlimits::setlimits(""); // c:4308
}
if (how & Z_ASYNC as i32) != 0 {
// c:4310 — `zsfree(STTYval); STTYval = 0;`
let mut guard = STTYval.lock().unwrap();
*guard = None; // c:4311-4312
}
// c:4314 — `execute(args, cflags, use_defpath);`
let mut a_vec: Vec<String> = args.clone().unwrap_or_default();
crate::ported::exec::execute(&mut a_vec, cflags, use_defpath); // c:4314
} else {
// c:4315 — `( ... )` — WC_SUBSH.
crate::ported::exec::list_pipe.store(0, Ordering::Relaxed); // c:4318
// c:4319 — `pipecleanfilelist(filelist, 0);` — clean
// proc-subst entries from the current job's filelist
// before recursing into the subshell body.
if let Some(jt) = crate::ported::jobs::JOBTAB.get() {
let mut guard = jt.lock().unwrap();
let tj = crate::ported::jobs::THISJOB
.get()
.map(|m| *m.lock().unwrap())
.unwrap_or(-1);
if tj >= 0 {
if let Some(j) = guard.get_mut(tj as usize) {
crate::ported::jobs::pipecleanfilelist(j, false);
}
}
}
state.pc += 1; // c:4324 — `state->pc++;`
let _ = crate::ported::exec::execlist(state, 0, 1); // c:4325
}
}
}
// c:4330-4404 — err: + done: + fatal:.
return execcmd_exec_done_path(
redir_err, oautocont, how, &mut shti, &mut chti, &mut then_ts,
forked, &mut newxtrerr,
cflags, orig_cflags, is_cursh, do_exec,
);
}
/// Internal helper modelling the C `done:` label tail of
/// `execcmd_exec` at `Src/exec.c:4366-4403`. Handles POSIX special-
/// builtin error escalation, AUTOCONTINUE restore, STTYval clear,
/// shelltime stop, and newxtrerr close.
#[allow(clippy::too_many_arguments)]
fn execcmd_exec_done_path(
redir_err: i32,
oautocont: i32,
how: i32,
shti: &mut crate::ported::jobs::timeinfo,
chti: &mut crate::ported::jobs::timeinfo,
then_ts: &mut std::time::Instant,
forked: i32,
newxtrerr: &mut Option<i32>,
cflags: u32,
orig_cflags: u32,
is_cursh: i32,
do_exec: i32,
) {
use crate::ported::zsh_h::{
POSIXBUILTINS, INTERACTIVE, BINF_PSPECIAL, BINF_EXEC, BINF_COMMAND,
Z_TIMED, AUTOCONTINUE,
};
// c:4366
// c:4367-4386 — POSIX special-builtin error escalation.
if isset(POSIXBUILTINS)
&& (cflags & (BINF_PSPECIAL | BINF_EXEC)) != 0
&& (orig_cflags & BINF_COMMAND) == 0
{
// c:4367-4369
let _forked_or_subsh = forked | crate::ported::exec::zsh_subshell.load(Ordering::Relaxed); // c:4376
// fatal: label entry point — same handling.
if redir_err != 0 || (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
// c:4378
if !isset(INTERACTIVE) {
// c:4379
if _forked_or_subsh != 0 {
unsafe { libc::_exit(1) }; // c:4381
} else {
std::process::exit(1); // c:4383
}
}
errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:4385
}
}
// c:4388-4389 — `if ((is_cursh || do_exec) && (how & Z_TIMED)) shelltime(...);`
if (is_cursh != 0 || do_exec != 0) && (how & Z_TIMED as i32) != 0 {
crate::ported::jobs::shelltime(Some(shti), Some(chti), Some(then_ts), 1); // c:4389
}
// c:4390-4398 — newxtrerr close.
if let Some(fd) = newxtrerr.take() {
// c:4390
let _ = zclose(fd); // c:4396
}
// c:4400-4401 — `zsfree(STTYval); STTYval = 0;`
{
let mut guard = STTYval.lock().unwrap();
*guard = None;
}
// c:4402-4403 — `if (oautocont >= 0) opts[AUTOCONTINUE] = oautocont;`
if oautocont >= 0 {
crate::ported::options::opt_state_set("autocontinue", oautocont != 0);
}
}
/// Internal helper modelling the C `err:` label tail of
/// `execcmd_exec` at `Src/exec.c:4330-4365`. Forked-child fd cleanup
/// + waitjobs + _realexit; non-forked: `fixfds(save)` + fall through
/// to done:.
#[allow(clippy::too_many_arguments)]
fn execcmd_exec_err_path(
forked: i32,
save: &mut [i32; 10],
mfds: &mut [Option<Box<multio>>; 10],
oautocont: i32,
how: i32,
shti: &mut crate::ported::jobs::timeinfo,
chti: &mut crate::ported::jobs::timeinfo,
then_ts: &mut std::time::Instant,
newxtrerr: &mut Option<i32>,
cflags: u32,
orig_cflags: u32,
is_cursh: i32,
do_exec: i32,
redir_err: i32,
) {
use crate::ported::zsh_h::{FDT_UNUSED};
// c:4330
if forked != 0 {
// c:4331
// c:4356-4358 — close all fds 0..10 whose fdtable entry != FDT_UNUSED.
let mut i: i32 = 0;
while i < 10 {
if crate::ported::utils::fdtable_get(i) != FDT_UNUSED {
unsafe { libc::close(i) }; // c:4358
}
i += 1;
}
// c:4359 — `closem(FDT_UNUSED, 1);`
closem(FDT_UNUSED, 1); // c:4359
// c:4360-4361 — `if (thisjob != -1) waitjobs();`
let thisjob = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
if thisjob != -1 {
if let Some(jt) = JOBTAB.get() {
let mut guard = jt.lock().unwrap();
crate::ported::jobs::waitjobs(&mut guard, thisjob as usize); // c:4361
}
}
crate::ported::builtin::_realexit(); // c:4362
}
fixfds(save); // c:4364
execcmd_exec_done_path(
redir_err, oautocont, how, shti, chti, then_ts, forked, newxtrerr,
cflags, orig_cflags, is_cursh, do_exec,
);
}
/// Internal helper dispatching `execfuncs[type - WC_CURSH]` from
/// `Src/exec.c:170-180`. Each branch maps to the ported wordcode-
/// walker function in `src/ported/exec.rs`.
fn dispatch_execfuncs(state: &mut estate, typ: i32, do_exec: i32) -> i32 {
use crate::ported::zsh_h::{
WC_ARITH, WC_AUTOFN, WC_CASE, WC_COND, WC_CURSH, WC_FOR, WC_FUNCDEF, WC_IF, WC_REPEAT,
WC_SELECT, WC_SUBSH, WC_TIMED, WC_TRY, WC_WHILE,
};
// Port of `static int (*const execfuncs[])(Estate, int)` dispatch
// table at `Src/exec.c:170-180`. C indexes by `(type - WC_CURSH)`;
// Rust matches on the WC_* tag directly.
match typ as wordcode {
x if x == WC_CURSH => execcursh(state, do_exec),
x if x == WC_FOR => execfor(state, do_exec),
x if x == WC_SELECT => execselect(state, do_exec),
x if x == WC_WHILE => execwhile(state, do_exec),
x if x == WC_REPEAT => execrepeat(state, do_exec),
x if x == WC_CASE => execcase(state, do_exec),
x if x == WC_IF => execif(state, do_exec),
x if x == WC_COND => execcond(state, do_exec),
x if x == WC_ARITH => execarith(state, do_exec),
x if x == WC_TRY => exectry(state, do_exec),
x if x == WC_FUNCDEF => execfuncdef(state, None),
x if x == WC_AUTOFN => execautofn_basic(state, do_exec),
x if x == WC_TIMED => exectime(state, do_exec),
x if x == WC_SUBSH => execcursh(state, do_exec), // c:269 — same handler.
_ => 0,
}
}
#[cfg(test)]
mod tests {
use super::*;
// ─── zsh-corpus pins for pure exec helpers ─────────────────────
/// `Src/exec.c:996-1010` — `isrelative` returns 1 for empty.
#[test]
fn exec_corpus_isrelative_empty_is_one() {
let _g = crate::test_util::global_state_lock();
assert_eq!(isrelative(""), 1, "empty path is relative");
}
/// `isrelative("foo")` = 1 (no leading slash).
#[test]
fn exec_corpus_isrelative_bare_name_is_one() {
let _g = crate::test_util::global_state_lock();
assert_eq!(isrelative("foo"), 1);
assert_eq!(isrelative("bin/cmd"), 1);
}
/// `isrelative("/foo")` = 0 (absolute, no `./` / `../`).
#[test]
fn exec_corpus_isrelative_absolute_clean_is_zero() {
let _g = crate::test_util::global_state_lock();
assert_eq!(isrelative("/foo"), 0, "/foo is absolute");
assert_eq!(isrelative("/bin/ls"), 0);
assert_eq!(isrelative("/"), 0, "root is absolute");
}
/// `isrelative("/foo/../bar")` = 1 (contains `../` component).
#[test]
fn exec_corpus_isrelative_absolute_with_dotdot_is_one() {
let _g = crate::test_util::global_state_lock();
assert_eq!(isrelative("/foo/../bar"), 1,
"absolute path with ../ is still 'relative' per zsh");
}
/// `isrelative("/foo/./bar")` = 1 (contains `./` component).
#[test]
fn exec_corpus_isrelative_absolute_with_dot_is_one() {
let _g = crate::test_util::global_state_lock();
assert_eq!(isrelative("/./x"), 1,
"absolute with ./ component reported relative");
}
/// `Src/exec.c:5300` — `is_anonymous_function_name("(anon)")` = 1.
#[test]
fn exec_corpus_is_anonymous_function_name_matches_sentinel() {
assert_eq!(is_anonymous_function_name("(anon)"), 1);
}
/// `is_anonymous_function_name("regular_name")` = 0.
#[test]
fn exec_corpus_is_anonymous_function_name_rejects_normal() {
assert_eq!(is_anonymous_function_name("regular_name"), 0);
assert_eq!(is_anonymous_function_name(""), 0);
assert_eq!(is_anonymous_function_name("anon"), 0,
"plain 'anon' (no parens) is NOT the sentinel");
}
/// `iscom("/nonexistent/never_a_path")` = false.
#[test]
fn exec_corpus_iscom_missing_path_false() {
assert!(!iscom("/this/path/does/not/exist/zshrs_xyz"));
}
/// `iscom("/tmp")` is a directory not a regular file → false.
#[test]
fn exec_corpus_iscom_directory_false() {
assert!(!iscom("/tmp"), "/tmp is a dir, not a regular command");
}
/// `iscom("/bin/sh")` is true on POSIX systems.
#[test]
fn exec_corpus_iscom_known_binary_true() {
// /bin/sh exists on all POSIX systems with X perms.
if std::path::Path::new("/bin/sh").exists() {
assert!(iscom("/bin/sh"), "/bin/sh is a real executable");
}
}
}