zsh/ported/exec.rs
1//! Faithful Rust ports of free functions and file-static globals from
2//! `Src/exec.c`. The wordcode-VM dispatch tree (`execlist` / `execpline`
3//! / `execcmd` / `execsimple` etc.) that drives execution in C zsh is
4//! NOT replicated here — zshrs runs the fusevm bytecode VM instead
5//! (see `src/vm_helper.rs` + `src/fusevm_bridge.rs`).
6//!
7//! What lives here are the parts of `Src/exec.c` that ARE faithful
8//! ports and don't depend on the C-side wordcode walker:
9//!
10//! - **`trap_state` / `trap_return` / `forklevel`** — file-static
11//! integer globals from `Src/exec.c:134 / :155 / :1052`, exposed as
12//! atomics shared between this module, `Src/signals.c`'s port at
13//! `src/ported/signals.rs`, and `Src/params.c`'s port at
14//! `src/ported/params.rs`.
15//! - **`gethere`** (`Src/exec.c:4573`) — turn a here-document into a
16//! here-string. Called from the lexer port (`src/ported/lex.rs`).
17//! - **`getoutput`** (`Src/exec.c:4712`) — command-substitution body
18//! runner. Called from the parameter-expansion port
19//! (`src/ported/subst.rs`).
20//! - **`loadautofn`** + **`getfpfunc`** (`Src/exec.c:5682` / `:6219`)
21//! — `$fpath` walker + autoload file installer. Called from
22//! `bin_autoload` / `bin_functions -c` in `src/ported/builtin.rs`.
23//! - **`resolvebuiltin`** (`Src/exec.c:2703`) — module-autoload guard
24//! used by the dispatch walk in `execcmd_exec`.
25//! - **`execcmd_compile_head`** — fusevm-bytecode-time head resolver
26//! mirroring the head section (`c:2904-3275`) of C's `execcmd_exec`.
27//! NOT a faithful port; the canonical 7-arg `execcmd_exec` port lives
28//! alongside it.
29//! - **`execcmd_exec`** (`Src/exec.c:2900`) — canonical 7-arg port of
30//! the C function (locals + dispatch walk through builtin/shfunc/external
31//! invocation). Used by future tree-walker callers; the fusevm
32//! bytecode flow goes through `execcmd_compile_head` instead.
33
34use std::os::unix::fs::PermissionsExt;
35use std::sync::atomic::Ordering;
36
37// `with_executor` import removed — all ShellExecutor reach-in calls
38// routed through `crate::ported::exec::*` fn-ptrs installed by
39// fusevm_bridge at startup. See memory feedback_no_exec_script_from_ported.
40use crate::ported::builtin::{cd_able_vars, fixdir, BUILTINS, DOPRINTDIR, EXIT_VAL, LASTVAL};
41use crate::ported::builtins::rlimits::setlimits;
42use crate::ported::builtins::sched::zleactive;
43use crate::ported::compat::zgettime_monotonic_if_available;
44use crate::ported::config_h::DEFAULT_PATH;
45use crate::ported::context::{zcontext_restore, zcontext_save};
46use crate::ported::hashtable::{
47 cmdnam_unhashed, cmdnamtab_lock, dircache_set, hashdir, pathchecked, shfunctab_lock,
48};
49use crate::ported::hist::{strinbeg, strinend};
50use crate::ported::init::{shout, underscorelen, underscoreused, zunderscore, SHTTY};
51use crate::ported::input::{inpop, inpush};
52use crate::ported::jobs::{expandjobtab, get_usage, release_pgrp, waitforpid, JOBTAB, THISJOB};
53use crate::ported::lex::{
54 hgetc, parsestr, tok, untokenize, ztokens, LEXERR, LEX_LEXSTOP, LEX_LINENO,
55};
56use crate::ported::mem::{dupstring, dyncat, popheap, pushheap};
57use crate::ported::modules::clone::mypgrp;
58use crate::ported::options::{dosetopt, opt_state_set, sticky};
59use crate::ported::params::{
60 endparamscope, getsparam, locallevel, paramtab, setiparam, zgetenv, zputenv,
61};
62use crate::ported::parse::{closedumps, ecrawstr, parse_list};
63use crate::ported::prompt::{cmdpop, cmdpush};
64use crate::ported::signals::{
65 intrap, queue_signals, settrap, signal_mask, signal_unblock, sigtrapped, trapisfunc,
66 traplocallevel, unqueue_signals, unsettrap,
67};
68use crate::ported::signals_h::{
69 child_block, child_unblock, dont_queue_signals, signal_default, signal_ignore, winch_unblock,
70 SIGCOUNT,
71};
72use crate::ported::subst::{quotesubst, singsub};
73use crate::ported::utils::{
74 errflag, fdtable_get, fdtable_set, gettempfile, gettempname, inc_locallevel, movefd, pathprog,
75 printprompt4, quotedzputs, redup, unmeta, unmetafy, write_loop, zclose, zerr, zwarn,
76 ERRFLAG_ERROR, MAX_ZSH_FD,
77};
78use crate::ported::r#loop::{execcase, execfor, execif, execrepeat, execselect, exectry, execwhile};
79use crate::ported::zsh_h::{
80 builtin, cmdnam, emulation_options, eprog, execstack, funcwrap, hashnode, isset, jobfile,
81 multio, redir, shfunc, unset, wc_code, Emulation_options, Inang, Inpar, Meta, Nularg, Outpar,
82 Pound, BINF_BUILTIN, BINF_CLEARENV, BINF_COMMAND, BINF_DASH, BINF_EXEC, BINF_PREFIX, CHASEDOTS,
83 CHASELINKS, CLOBBER, CLOBBEREMPTY, CS_CMDSUBST, ERRFLAG_INT, FDT_EXTERNAL, FDT_INTERNAL,
84 FDT_PROC_SUBST, FDT_SAVED_MASK, FDT_TYPE_MASK, FDT_UNUSED, FDT_XTRACE, HASHDIRS, INP_LINENO,
85 INTERACTIVE, IS_CLOBBER_REDIR, IS_DASH, JOBTEXTSIZE, MAX_PIPESTATS, MONITOR, MULTIOS,
86 MULTIOUNIT, PATHDIRS, PM_LOADDIR, PM_READONLY, PM_UNDEFINED, POSIXBUILTINS, POSIXJOBS,
87 POSIXTRAPS, REDIRF_FROM_HEREDOC, REDIR_CLOSE, REDIR_HEREDOCDASH, REDIR_HERESTR, REDIR_INPIPE,
88 REDIR_OUTPIPE, USEZLE, VERBOSE, WC_LIST, WC_LIST_TYPE, WC_PIPE, WC_PIPE_END, WC_PIPE_TYPE,
89 WC_REDIR, WC_REDIR_TYPE, WC_REDIR_VARID, WC_SIMPLE, WC_SIMPLE_ARGC, WC_SUBLIST, WC_SUBLIST_END,
90 WC_SUBLIST_FLAGS, WC_SUBLIST_TYPE, WC_TYPESET, ZSIG_FUNC, ZSIG_IGNORED, Z_END,
91};
92use crate::ported::zsh_system_h::timespec as ZshTimespec;
93use crate::ported::ztype_h::{inull, itok};
94use crate::zsh_h::XTRACE;
95
96/// Port of the anonymous `enum { ... }` from `Src/exec.c:35-40`.
97/// Flag bits passed as the `addflags` argument to `addvars` /
98/// `addvarsfromargs`:
99/// - `ADDVAR_EXPORT` (1<<0) — export each assignment for the
100/// command `VAR=val cmd ...` form.
101/// - `ADDVAR_RESTORE` (1<<2) — the variable list is being restored
102/// later (implicit local scope), so
103/// suppress `ASSPM_WARN`.
104pub const ADDVAR_EXPORT: i32 = 1 << 0; // c:37 (Src/exec.c)
105/// `ADDVAR_RESTORE` constant.
106pub const ADDVAR_RESTORE: i32 = 1 << 2; // c:39 (Src/exec.c)
107
108/// Port of `int trap_state;` from `Src/exec.c:134`. Tracks whether
109/// a trap handler is currently being processed and, paired with
110/// `TRAP_RETURN` below, whether a `return` inside the trap should
111/// promote to `TRAP_STATE_FORCE_RETURN` to unwind the trap caller.
112///
113/// Values: `TRAP_STATE_INACTIVE = 0`, `TRAP_STATE_PRIMED = 1`,
114/// `TRAP_STATE_FORCE_RETURN = 2` (see `Src/zsh.h`).
115pub static TRAP_STATE: std::sync::atomic::AtomicI32 = // c:134 (Src/exec.c)
116 std::sync::atomic::AtomicI32::new(0);
117
118/// Port of `int trap_return;` from `Src/exec.c:155`. Carries the
119/// pending exit status from inside a trap; sentinel `-2` means
120/// "running an EXIT/DEBUG-style trap at the current level"
121/// (signals.c:1166). Promoted to the user's `return N` value by
122/// `bin_return` when POSIX-trap semantics apply (builtin.c:5852).
123pub static TRAP_RETURN: std::sync::atomic::AtomicI32 = // c:155 (Src/exec.c)
124 std::sync::atomic::AtomicI32::new(0);
125
126/// Port of `int forklevel;` from `Src/exec.c:1053`. Records the
127/// `locallevel` at the most recent fork point (set at c:1221:
128/// `forklevel = locallevel;` inside `entersubsh()`). Used by:
129/// - `signals.c:808` SIGPIPE handler — `!forklevel` distinguishes
130/// the top-level shell from a forked subshell.
131/// - `exec.c:6146` — `if (locallevel > forklevel)` decides whether
132/// a function-defined trap should fire on this subshell exit.
133/// - `params.c:3724` — WARNCREATEGLOBAL nest-depth check.
134///
135/// Initialised to 0 (no fork has occurred yet). Set to `locallevel`
136/// at every `entersubsh()` entry per c:1221.
137pub static FORKLEVEL: std::sync::atomic::AtomicI32 = // c:1052 (Src/exec.c)
138 std::sync::atomic::AtomicI32::new(0);
139
140// =============================================================================
141// File-static globals from Src/exec.c. Bucket choices per PORT_PLAN.md:
142// - Per-evaluator transient state → thread_local Cell (bucket 1)
143// - Shell-wide shared state → AtomicI32 / Mutex (bucket 2)
144// All names match C exactly. Surrounding doc-comments cite the C
145// declaration line.
146// =============================================================================
147
148/// Port of `int noerrexit;` from `Src/exec.c:72`. Bit-flags that
149/// suppress ERREXIT triggering on the next command(s). Bits:
150/// `NOERREXIT_EXIT` (in `if`/`while`/`until` test contexts),
151/// `NOERREXIT_RETURN` (after `return`), `NOERREXIT_UNTIL_EXEC`
152/// (until next exec'd command). Bucket-1 — per-evaluator (each
153/// recursive eval has its own suppression frame).
154pub static noerrexit: std::sync::atomic::AtomicI32 = // c:72 (Src/exec.c)
155 std::sync::atomic::AtomicI32::new(0);
156
157/// Port of `int this_noerrexit;` from `Src/exec.c:109`. When set,
158/// suppress ERREXIT for THIS one command only (consumed + cleared
159/// before the next command starts). Set by `execcursh` and the
160/// `((expr))` arith path so a 0-result doesn't trigger errexit.
161pub static this_noerrexit: std::sync::atomic::AtomicI32 = // c:109 (Src/exec.c)
162 std::sync::atomic::AtomicI32::new(0);
163
164/// Port of `mod_export int noerrs;` from `Src/exec.c:117`. When
165/// non-zero, suppress `zerr()` output (lex error reporting during
166/// `parse_string`, `parseopts` etc.). Saved/restored by
167/// `execsave`/`execrestore`.
168/// Port of `static char list_pipe_text[JOBTEXTSIZE]` from
169/// `Src/exec.c:463`. Holds the textual rendering of the in-flight
170/// pipe list; saved across nested execlist invocations at
171/// exec.c:1372-1380 (zeroed on entry, restored from
172/// `old_list_pipe_text` at c:1634-1638) and round-tripped through
173/// execsave/execrestore (c:6448 / c:6484). zshrs models it as a
174/// length-bounded String guarded by a Mutex — the C `char[80]` cap
175/// is a buffer-overflow guard, but matching length matters for the
176/// `jobs` builtin's pipe-list rendering.
177pub static LIST_PIPE_TEXT: std::sync::Mutex<String> = std::sync::Mutex::new(String::new()); // c:463 (Src/exec.c)
178
179// c:117 (Src/exec.c) — `mod_export int noerrs;`
180//
181// C has exactly ONE `noerrs`. This port had TWO disjoint storages: an
182// `AtomicI32` here, and `utils::noerrs_lock()`'s `Mutex<i32>` (flagged in
183// utils.rs as "NOT IN UTILS.C"). The four diagnostic emitters — `zerr`,
184// `zwarn`, `zerrnam`, `zwarnnam` (utils.rs:221/244/262/279) — read the utils
185// one, while `docomplete`'s suppression window (`noerrs = 1` around
186// `doexpansion`, zle_tricky.c:825-828) wrote this one. So the suppression
187// never reached the printer and completion-time diagnostics leaked onto the
188// screen: `ls *(` printed `zsh: bad pattern: *(` where zsh prints nothing,
189// consuming a row and shifting the whole match grid.
190//
191// Unified onto the storage every emitter already consults; the accessor lives
192// at `crate::ported::utils::noerrs_lock()`. Do not reintroduce a second one.
193// The storage is `crate::ported::utils::noerrs_lock()`; read/write it directly.
194
195/// Port of `int nohistsave;` from `Src/exec.c:122`. When non-zero,
196/// the history file is not saved on `exec` (c:4324/c:4340), on shell exit
197/// (builtin.c:6014) or by `saveandpophiststack` (hist.c:3960). Set to 1 by
198/// `setupvals` (init.c:1332) and cleared once the startup files have run
199/// (init.c:1550).
200pub static nohistsave: std::sync::atomic::AtomicI32 = // c:122 (Src/exec.c)
201 std::sync::atomic::AtomicI32::new(0);
202
203/// Port of `int subsh;` from `Src/exec.c:160`. Subshell depth — bumped
204/// every time `entersubsh` forks a sub-shell, used by signal handling
205/// (different SIGINT semantics in subshells) and by `${$$}` (`$$`
206/// stays at the top-level pid).
207pub static subsh: std::sync::atomic::AtomicI32 = // c:160 (Src/exec.c)
208 std::sync::atomic::AtomicI32::new(0);
209
210/// Port of `int zsh_subshell;` from `Src/params.c:108`. Visible
211/// `$ZSH_SUBSHELL` parameter — incremented by `entersubsh()` each time
212/// the shell forks into a subshell (real or fake-exec). Distinct from
213/// `subsh` which records whether we ARE a subshell; `zsh_subshell` is
214/// the visible depth count.
215pub static zsh_subshell: std::sync::atomic::AtomicI32 = // c:108 (Src/params.c)
216 std::sync::atomic::AtomicI32::new(0);
217
218/// Port of `mod_export volatile int retflag;` from `Src/exec.c:165`.
219/// Set by `bin_return` to unwind the function-call stack. Cleared
220/// by `runshfunc` on entry, checked by `execlist`'s main loop.
221///
222/// Re-export alias of the canonical [`crate::ported::builtin::RETFLAG`] — C
223/// has ONE `retflag` (exec.c:165). All return-flow logic (execlist, the
224/// `return` builtin, signals) uses `RETFLAG`; this lowercase twin was never
225/// stored, so `zpty`/`system` module read-loops that check `exec::retflag`
226/// saw a permanent 0 and never aborted on a pending `return`.
227pub use crate::ported::builtin::RETFLAG as retflag; // c:165 (Src/exec.c)
228
229/// Port of `pid_t cmdoutpid;` from `Src/exec.c:215`. Pid of the most
230/// recent `$(cmd)` command-substitution child. Used by exit-status
231/// propagation: `cmdoutval` carries the exit; `cmdoutpid` carries
232/// the pid `waitpid`-d for it.
233pub static cmdoutpid: std::sync::atomic::AtomicI32 = // c:215 (Src/exec.c)
234 std::sync::atomic::AtomicI32::new(0);
235
236/// Port of `mod_export pid_t procsubstpid;` from `Src/exec.c:220`.
237/// Pid of the most recent process-substitution child (`<(cmd)` /
238/// `>(cmd)`). Tracked separately from `cmdoutpid` because procsubst
239/// jobs aren't wait-collected by the parent until the fd is closed.
240pub static procsubstpid: std::sync::atomic::AtomicI32 = // c:220 (Src/exec.c)
241 std::sync::atomic::AtomicI32::new(0);
242
243/// Port of `int cmdoutval;` from `Src/exec.c:225`. Exit status of
244/// the most recent `$(cmd)`. Drives `$?` when a varspc-only command
245/// runs alongside a substitution.
246pub static cmdoutval: std::sync::atomic::AtomicI32 = // c:225 (Src/exec.c)
247 std::sync::atomic::AtomicI32::new(0);
248
249/// Port of `int use_cmdoutval;` from `Src/exec.c:234`. When set,
250/// `lastval` is updated from `cmdoutval` after the command
251/// (i.e. the command had substitutions whose exit status matters).
252pub static use_cmdoutval: std::sync::atomic::AtomicI32 = // c:234 (Src/exec.c)
253 std::sync::atomic::AtomicI32::new(0);
254
255/// Port of `mod_export int sfcontext;` from `Src/exec.c:239`. Source
256/// context — one of `SFC_NONE`, `SFC_DIRECT` (user typed it),
257/// `SFC_SIGNAL` (trap firing), `SFC_HOOK` (precmd/preexec etc.),
258/// `SFC_WIDGET` (ZLE widget), `SFC_COMPLETE` (completion fn),
259/// `SFC_CFUNC` (compsys fn), `SFC_SUBST` ($(...) cmd-subst),
260/// `SFC_EVAL` (eval body). Read by `zerr()` / `funcstack` building.
261pub static sfcontext: std::sync::atomic::AtomicI32 = // c:239 (Src/exec.c)
262 std::sync::atomic::AtomicI32::new(0);
263
264/// Port of `int list_pipe = 0;` from `Src/exec.c:457`. Set when the
265/// currently-executing pipeline is the long-running pipe-into-loop
266/// shape (`cat foo | while read a; do ... done`) — drives the
267/// super/sub-job tracking documented in the famous `Allen Edeln…`
268/// comment block above this declaration in C.
269pub static list_pipe: std::sync::atomic::AtomicI32 = // c:457 (Src/exec.c)
270 std::sync::atomic::AtomicI32::new(0);
271
272/// Port of `int simple_pline = 0;` from `Src/exec.c:457`. Set during
273/// dispatch of a "simple" pipeline (single-stage / no shell-construct
274/// tail) so the `list_pipe` machinery short-circuits.
275pub static simple_pline: std::sync::atomic::AtomicI32 = // c:457 (Src/exec.c)
276 std::sync::atomic::AtomicI32::new(0);
277
278/// Port of `static pid_t list_pipe_pid;` from `Src/exec.c:459`.
279/// PID of the sub-shell created to host the loop-after-pipe pattern;
280/// passed up the recursive `execlist` stack so the cat-job's super-
281/// job entry can record it.
282pub static list_pipe_pid: std::sync::atomic::AtomicI32 = // c:459 (Src/exec.c)
283 std::sync::atomic::AtomicI32::new(0);
284
285/// Port of `static int nowait;` from `Src/exec.c:461`. When set,
286/// `execpline` doesn't wait for the pipeline; used during the
287/// list_pipe sub-shell fork bookkeeping.
288pub static nowait: std::sync::atomic::AtomicI32 = // c:461 (Src/exec.c)
289 std::sync::atomic::AtomicI32::new(0);
290
291/// Port of `int pline_level = 0;` from `Src/exec.c:461`. Recursive
292/// pipeline depth (counts nested pipelines within the current
293/// `execlist` call chain).
294pub static pline_level: std::sync::atomic::AtomicI32 = // c:461 (Src/exec.c)
295 std::sync::atomic::AtomicI32::new(0);
296
297/// Port of `static int list_pipe_child = 0;` from `Src/exec.c:462`.
298/// Set in the child after the list_pipe fork so the child knows to
299/// continue executing the loop body (vs the parent which records
300/// the pid + returns).
301pub static list_pipe_child: std::sync::atomic::AtomicI32 = // c:462 (Src/exec.c)
302 std::sync::atomic::AtomicI32::new(0);
303
304/// Port of `static int list_pipe_job;` from `Src/exec.c:462`. Job
305/// table index of the pipeline's first-stage job (the `cat` in
306/// `cat foo | while ...`).
307pub static list_pipe_job: std::sync::atomic::AtomicI32 = // c:462 (Src/exec.c)
308 std::sync::atomic::AtomicI32::new(0);
309
310/// Port of `static int doneps4;` from `Src/exec.c:262`. Set after
311/// `printprompt4` has emitted the `$PS4` prefix for the current
312/// xtrace command — prevents double-printing when an inner sub-eval
313/// also wants to xtrace.
314pub static doneps4: std::sync::atomic::AtomicI32 = // c:262 (Src/exec.c)
315 std::sync::atomic::AtomicI32::new(0);
316
317/// Port of `static int esprefork, esglob = 1;` from `Src/exec.c:2680`.
318///
319/// File-static "execsubst parameters" — callers (execcmd_exec at
320/// c:3298 / c:3700) set these BEFORE invoking execsubst, which then
321/// uses them as the `flags` arg to prefork() and the gate on
322/// globlist(). `esprefork` is `PREFORK_TYPESET` for magic-assign /
323/// MAGICEQUALSUBST words, else 0. `esglob` defaults to 1; cleared
324/// when the dispatched builtin has `BINF_NOGLOB`.
325pub static esprefork: std::sync::atomic::AtomicI32 = // c:2680
326 std::sync::atomic::AtomicI32::new(0);
327pub static esglob: std::sync::atomic::AtomicI32 = // c:2680 (= 1)
328 std::sync::atomic::AtomicI32::new(1);
329
330/// Port of `struct execstack *exstack;` from `Src/exec.c:244`. Head
331/// of the linked exec-context save stack — `execsave` pushes a frame
332/// before signal-handler / trap dispatch; `execrestore` pops it
333/// afterwards so the interrupted command resumes with its state intact.
334pub static exstack: std::sync::Mutex<Option<Box<execstack>>> = // c:244
335 std::sync::Mutex::new(None);
336
337/// Port of `static char *STTYval;` from `Src/exec.c:263`. Pending
338/// `stty` argument string captured by `addvars` when the command's
339/// inline env contains `STTY=...`. Applied by `execute` before fork
340/// + exec so the spawned program sees its tty configured. Reset to
341/// `None` after consumption to avoid infinite recursion.
342pub static STTYval: std::sync::Mutex<Option<String>> = // c:263 (Src/exec.c)
343 std::sync::Mutex::new(None);
344
345/// Convert a here-document into a here-string. Line-by-line port of
346/// `gethere()` from `Src/exec.c:4569-4652`. Reads the body from the
347/// input stream via `hgetc()` until the terminator line is matched,
348/// returning the collected body as a string. `strp` is in/out: on
349/// entry the raw terminator (possibly with token markers + leading
350/// tabs); on return the munged terminator (after `quotesubst` +
351/// `untokenize` and, for `REDIR_HEREDOCDASH`, leading-tab strip).
352///
353/// Returns `None` on out-of-memory (C `zalloc`/`realloc` failure).
354/// Rust's `String` auto-grows so the OOM branch is effectively
355/// unreachable, but the return type stays `Option<String>` to mirror
356/// the C signature which can return NULL.
357///
358/// Port of `gethere()` from `Src/exec.c:4570` — C decl `gethere(char **strp, int typ)`.
359pub fn gethere(strp: &mut String, typ: i32) -> Option<String> {
360 // c:4573 (Src/exec.c)
361 let mut buf: String; // c:4575 char *buf
362 let mut bsiz: usize; // c:4576 int bsiz
363 let mut qt: i32 = 0; // c:4576 int qt = 0
364 let mut strip: i32 = 0; // c:4576 int strip = 0
365 // c:4577 — char *s, *t, *bptr, c. zshrs uses byte-offsets into
366 // `buf` for `t` and tracks `bptr` implicitly as `buf.len()` (the
367 // C `bptr++` increment is `buf.push(c)`; `bptr--` is `buf.pop()`).
368 // `s` (the loop iterator for the inull-scan) stays local to its
369 // for-loop. `c` mirrors the C `char c`.
370 let mut t: usize; // c:4577 char *t
371 let mut c: Option<char>; // c:4577 char c
372 let mut str: String = strp.clone(); // c:4578 char *str = *strp
373
374 // c:4580-4584 — for (s = str; *s; s++) if (inull(*s)) { qt = 1; break; }
375 for s in str.chars() {
376 if crate::token_char::token_byte(s).is_some_and(inull) {
377 // c:4581
378 qt = 1; // c:4582
379 break; // c:4583
380 }
381 }
382 str = quotesubst(&str); // c:4585
383 str = untokenize(&str); // c:4586
384 if typ == REDIR_HEREDOCDASH {
385 // c:4587
386 strip = 1; // c:4588
387 // c:4589-4590 — while (*str == '\t') str++;
388 while str.starts_with('\t') {
389 str.remove(0);
390 }
391 }
392 *strp = str.clone(); // c:4592 *strp = str
393
394 // c:4593 — bptr = buf = zalloc(bsiz = 256);
395 bsiz = 256;
396 buf = String::with_capacity(bsiz);
397 let _ = bsiz; // bsiz is tracked by C for zfree; Rust drops automatically
398
399 // c:4594 — for (;;)
400 loop {
401 t = buf.len(); // c:4595 t = bptr
402
403 // c:4597-4598 — while ((c = hgetc()) == '\t' && strip) ;
404 loop {
405 c = hgetc();
406 if !(c == Some('\t') && strip != 0) {
407 break;
408 }
409 }
410
411 // c:4599 — for (;;) — inner body-read loop
412 loop {
413 // c:4600-4613 — buffer-growth realloc dance. Rust's
414 // String auto-grows; nothing to do.
415 // c:4614 — if (lexstop || c == '\n') break;
416 if LEX_LEXSTOP.with(|f| f.get()) || c == Some('\n') || c.is_none() {
417 break;
418 }
419 // c:4616 — if (!qt && c == '\\')
420 if qt == 0 && c == Some('\\') {
421 buf.push('\\'); // c:4617 *bptr++ = c
422 c = hgetc(); // c:4618
423 if c == Some('\n') {
424 // c:4619
425 buf.pop(); // c:4620 bptr--
426 c = hgetc(); // c:4621
427 continue; // c:4622
428 }
429 }
430 if let Some(ch) = c {
431 // c:4625 *bptr++ = c
432 buf.push(ch);
433 }
434 c = hgetc(); // c:4626
435 }
436 // c:4628 — *bptr = '\0'; (implicit — Rust String tracks len)
437
438 // c:4629-4630 — if (!strcmp(t, str)) break;
439 if &buf[t..] == str.as_str() {
440 break;
441 }
442 // c:4631-4634 — if (lexstop) { t = bptr; break; }
443 // C's ingetc sets `lexstop = 1` when input is exhausted
444 // (Src/input.c:289-292), so the flag check alone suffices
445 // there. zshrs's hgetc signals the same exhaustion by
446 // returning None WITHOUT setting LEX_LEXSTOP — treat both as
447 // the c:4631 condition, else an unterminated heredoc loops
448 // here forever appending '\n'. Gap #3 2026-06-12 (A04
449 // "Here-documents don't perform shell expansion" wedge).
450 if LEX_LEXSTOP.with(|f| f.get()) || c.is_none() {
451 t = buf.len();
452 break;
453 }
454 // c:4635 — *bptr++ = '\n';
455 buf.push('\n');
456 }
457 // c:4637 — *t = '\0';
458 buf.truncate(t);
459
460 // c:4638-4640 — s = buf; buf = dupstring(buf); zfree(s, bsiz);
461 // The C dance frees the realloc'd block and re-allocates via the
462 // string-heap allocator. Rust drops the old String when reassigned.
463 buf = dupstring(&buf);
464
465 if qt == 0 {
466 // c:4641
467 // c:4642 — int ef = errflag;
468 let ef = errflag.load(Ordering::Relaxed);
469 // c:4644 — parsestr(&buf);
470 if let Ok(parsed) = parsestr(&buf) {
471 buf = parsed;
472 }
473 // c:4646-4649 — if (!(errflag & ERRFLAG_ERROR)) errflag = ef | (errflag & ERRFLAG_INT);
474 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
475 let cur = errflag.load(Ordering::Relaxed);
476 errflag.store(ef | (cur & ERRFLAG_INT), Ordering::Relaxed);
477 }
478 }
479 Some(buf) // c:4651 return buf
480}
481
482/// Port of `LinkList getoutput(char *cmd, int qt)` from
483/// `Src/exec.c:4712-4791`. Runs a command-substitution body in the
484/// active executor, then routes the captured stdout through
485/// `readoutput(pipe, qt, NULL)` semantics at c:4855-4872.
486///
487/// C return shape: `LinkList` of `char*`. Rust port returns
488/// `Vec<String>` (same shape, owned).
489///
490/// `qt` matches C exactly:
491/// - qt=1 (quoted, `"$(...)"`): trim trailing newlines, return
492/// entire output as a single-element vec. C c:4858-4862: if
493/// output empty, returns a single Nularg sentinel so callers
494/// see "empty value" rather than "no value".
495/// - qt=0 (unquoted, `$(...)`): trim trailing newlines, then
496/// `spacesplit(buf, allownull=false)` per c:4865-4871, and
497/// `shtokenize` each word when `GLOBSUBST` is set (c:4868-4869)
498/// so the substituted words glob downstream.
499///
500/// Both arms — `$(< file)` (c:4746) and `$( cmd )` (c:4772) — go through
501/// the same c:4858-4872 tail (inlined twice — see the cross-reference
502/// notes at :619 and :3829), mirroring C where both go through `readoutput`.
503///
504/// Uses `with_executor` (panics on missing VM context), not
505/// `try_with_executor + unwrap_or_default()`. C `getoutput` calls
506/// `execpline` directly — there's no "no shell" code path. The
507/// silent-no-op pattern (return empty string when no executor) would
508/// mask catastrophic state corruption as "command produced no output",
509/// which is the failure mode the `subst.rs:496` warning block flags.
510/* $(...) */
511/// Port of `getoutput()` from `Src/exec.c:4713` — C decl `getoutput(char *cmd, int qt)`.
512// c:4709
513/// `getoutput` — see implementation.
514pub fn getoutput(cmd: &str, qt: i32) -> Option<Vec<String>> {
515 // c:4713
516 // c:4715 — `Eprog prog;`
517 let prog: Option<eprog>;
518 // c:4716 — `int pipes[2];` (collapsed: in-process executor; no fork)
519 // c:4717 — `pid_t pid;` (collapsed)
520 let mut s: String; // c:4718
521 // c:4720-4723 — `int onc = nocomments; nocomments = (interact &&
522 // !sourcelevel && unset(INTERACTIVECOMMENTS));
523 // prog = parse_string(cmd, 0); nocomments = onc;`
524 let onc = crate::ported::lex::LEX_NOCOMMENTS.with(|c| c.get());
525 let new_nc = crate::ported::zsh_h::interact()
526 && crate::ported::init::sourcelevel.load(Ordering::Relaxed) == 0
527 && !isset(crate::ported::zsh_h::INTERACTIVECOMMENTS);
528 crate::ported::lex::LEX_NOCOMMENTS.with(|c| c.set(new_nc));
529 prog = parse_string(cmd, 0);
530 crate::ported::lex::LEX_NOCOMMENTS.with(|c| c.set(onc));
531
532 if prog.is_none() {
533 // c:4725
534 return None; // c:4726 return NULL
535 }
536 let prog = prog.unwrap();
537
538 if !isset(crate::ported::zsh_h::EXECOPT) {
539 // c:4728
540 return Some(Vec::new()); // c:4729 newlinklist()
541 }
542
543 // c:4731 — `if ((s = simple_redir_name(prog, REDIR_READ)))` — `$(< word)`
544 if let Some(red_name) = simple_redir_name(&prog, crate::ported::zsh_h::REDIR_READ) {
545 /* $(< word) */
546 // c:4732
547 s = red_name;
548 s = singsub(&s); // c:4737
549 if errflag.load(Ordering::Relaxed) != 0 {
550 return None; // c:4739
551 }
552 let s = untokenize(&s); // c:4740
553 let path_meta = unmeta(&s); // c:4741 unmeta(s)
554 let cpath = match std::ffi::CString::new(path_meta.as_bytes()) {
555 Ok(c) => c,
556 Err(_) => return Some(Vec::new()),
557 };
558 let stream = unsafe {
559 libc::open(cpath.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY) // c:4741
560 };
561 if stream == -1 {
562 // c:4798 — `zwarn("%e: %s", errno, s);`
563 //
564 // `zwarn`, NOT `zerr`: `zerr` raises ERRFLAG_ERROR, which
565 // abandons the rest of the line, so `print -r -- "[$(<f)]"`
566 // against a missing `f` printed nothing and exited 1. zsh
567 // warns, substitutes empty, and runs the command — the
568 // observed output is `[]` with status 0.
569 //
570 // `%e` is zsh's errno formatter (`zsh_errno_msg`, utils.c:355),
571 // which lowercases strerror's first letter: "no such file or
572 // directory". Rust's `io::Error` Display renders "No such file
573 // or directory (os error 2)" instead.
574 let errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
575 zwarn(&format!(
576 "{}: {}",
577 crate::ported::utils::zsh_errno_msg(errno),
578 s
579 ));
580 LASTVAL.store(1, Ordering::Relaxed);
581 cmdoutval.store(1, Ordering::Relaxed);
582 return Some(Vec::new()); // c:4800
583 }
584 // c:4746 — `retval = readoutput(stream, qt, &readerror);`
585 let mut readerror: i32 = 0;
586 let retval = readoutput(stream, qt, &mut readerror); // c:4746
587 if readerror != 0 {
588 // c:4803-4805 — `zwarn("error when reading %s: %e", s, readerror);`
589 // Non-fatal for the same reason as the open failure above.
590 zwarn(&format!(
591 "error when reading {}: {}", // c:4804
592 s,
593 crate::ported::utils::zsh_errno_msg(readerror)
594 ));
595 LASTVAL.store(1, Ordering::Relaxed);
596 cmdoutval.store(1, Ordering::Relaxed);
597 }
598 return Some(retval); // c:4751
599 }
600
601 // c:4753-4790 — Full fork path: mpipe + zfork + parent
602 // readoutput / waitforpid / child execode + _realexit. fusevm runs
603 // command substitution in-process, so the fork shape collapses to a
604 // synchronous executor call. C control points preserved as cites:
605 // c:4753 mpipe — handled by ShellExecutor pipe wiring
606 // c:4758 child_block — no-op (no fork)
607 // c:4760 zfork — replaced by in-process exec
608 // c:4768-4776 parent — equivalent to executor return
609 // c:4778-4789 child — entersubsh+execode+_realexit collapse.
610 // The `entersubsh(ESUB_PGRP|ESUB_NOMONITOR, NULL)` at c:4781 is NOT
611 // dropped by that collapse: `run_command_substitution` below enters
612 // `SubshStateGuard`, which applies the deltas that are correct
613 // without a fork (opts[MONITOR], shout, opts[USEZLE], zleactive) and
614 // restores them on return. See SubshStateGuard for what is skipped
615 // and why. `_realexit()` has no in-process counterpart — the
616 // executor simply returns the captured buffer.
617 cmdoutval.store(0, Ordering::Relaxed); // c:4759
618 let buf = crate::ported::exec::run_command_substitution(cmd);
619 LASTVAL.store(cmdoutval.load(Ordering::Relaxed), Ordering::Relaxed); // c:4775
620
621 // ── c:4772 `retval = readoutput(pipes[0], qt, NULL);` ─────────────
622 // The fd died with the fork, but readoutput's POST-READ half (c:4858-
623 // 4872) still applies verbatim, so it is inlined here.
624 //
625 // !!! THIS BLOCK IS DUPLICATED at `readoutput` (:3829). See the note
626 // there for why build.rs's no-new-functions gate rules out a shared
627 // helper. ANY EDIT HERE MUST BE MIRRORED AT :3829 AND VICE VERSA.
628 //
629 // What was missing before: the c:4868-4869 `if (isset(GLOBSUBST))
630 // shtokenize(*words);` step. `readoutput` has it, this copy did not, so
631 // `setopt globsubst; echo $(echo '*')` printed `*` where zsh expands the
632 // glob — while `setopt globsubst; echo $(< star.txt)`, which reaches
633 // `readoutput` through the c:4746 arm, globbed correctly.
634 //
635 // `buf` arrives as a plain (un-metafied) String from
636 // `run_command_substitution`; `readoutput` now hands its own tail the
637 // same representation (see the deviation note at :3792).
638 //
639 // c:4858-4859 — `while (cnt && ptr[-1] == '\n') ptr--, cnt--;`
640 let s = buf.trim_end_matches('\n');
641 // c:4861-4863 — qt branch: empty → Nularg sentinel; else single elem.
642 if qt != 0 {
643 // c:4861
644 if s.is_empty() {
645 return Some(vec![String::from(Nularg)]); // c:4862
646 }
647 return Some(vec![s.to_string()]); // c:4864
648 }
649 // c:4866-4871 — `spacesplit` + per-word GLOBSUBST `shtokenize`.
650 let mut words = crate::ported::utils::spacesplit(s, false, false); // c:4867
651 if isset(crate::ported::zsh_h::GLOBSUBST) {
652 // c:4870
653 for w in words.iter_mut() {
654 crate::ported::glob::shtokenize(w); // c:4870
655 }
656 }
657 Some(words)
658}
659
660/// Direct port of `Shfunc loadautofn(Shfunc shf, int ks, int test_only,
661/// int ignore_loaddir)` from `Src/exec.c:5682`. Walks `$fpath` for a
662/// file named `shf->node.nam`, reads it, installs the text body on
663/// the corresponding `shfunctab` entry, and clears `PM_UNDEFINED`.
664///
665/// C body (abridged):
666/// 1. `name = shf->node.nam`
667/// 2. `getfpfunc(name, &dir_path, NULL, 0)` → resolved file path
668/// 3. If !test_only && file found: parse → store eprog on
669/// `shf->funcdef`; clear PM_UNDEFINED; set `shf->filename`.
670/// 4. Returns shf on success, NULL on failure.
671///
672/// Rust port: returns 0 = success, 1 = failure (matches the
673/// existing call-site convention in `bin_functions -c`). Stores
674/// raw file text on `ShFunc.body` (the Rust-side ShFunc in
675/// `hashtable.rs:362`); the parser pass that converts text →
676/// Eprog runs lazily at first call site.
677/// Port of `loadautofn()` from `Src/exec.c:5682` — C decl `loadautofn(Shfunc shf, int fksh, int autol, int current_fpath)`.
678pub fn loadautofn(
679 shf: *mut shfunc, // c:5682 (Src/exec.c)
680 _ks: i32,
681 autol: i32,
682 current_fpath: i32, // c:5735 — 4th C param is `current_fpath`, NOT an
683 // "ignore loaddir" flag; `eval_autoload` passes
684 // `OPT_ISSET(ops,'d')` here (c:3182).
685) -> i32 {
686 if shf.is_null() {
687 return 1;
688 }
689 // c:5054 — `name = shf->node.nam`.
690 let name = unsafe { (*shf).node.nam.clone() };
691 // c:5070 — `path = getfpfunc(name, &dir_path, NULL, 0)`.
692 let mut dir_path: Option<String> = None;
693 let mut dump_hit: Option<(eprog, i32)> = None;
694 // A function autoloaded by ABSOLUTE PATH (`autoload -Uz /dir/name`) — or
695 // one already resolved via `autoload -r` — caches its directory in
696 // `shf.filename` with PM_LOADDIR set. Load-on-call must search THAT dir
697 // (`shf.filename/name`), not walk `$fpath` (which won't contain it).
698 // Without this, `autoload -Uz $fdir/.hist.*` (zsh-hist) named the
699 // functions correctly but every call died with "definition file not
700 // found". Mirrors C loadautofn's PM_LOADDIR spec-path branch.
701 //
702 // c:5747-5757 —
703 // if (shf->filename && shf->filename[0] == '/' &&
704 // (shf->node.flags & PM_LOADDIR))
705 // {
706 // char *spec_path[2];
707 // spec_path[0] = dupstring(shf->filename);
708 // spec_path[1] = NULL;
709 // prog = getfpfunc(shf->node.nam, &ksh, &fdir, spec_path, 0);
710 // if (prog == &dummy_eprog &&
711 // (current_fpath || (shf->node.flags & PM_CUR_FPATH)))
712 // prog = getfpfunc(shf->node.nam, &ksh, &fdir, NULL, 0);
713 // }
714 // else
715 // prog = getfpfunc(shf->node.nam, &ksh, &fdir, NULL, 0);
716 let fn_flags = unsafe { (*shf).node.flags } as u32;
717 let loaddir_spec: Option<Vec<String>> = {
718 let s = unsafe { &*shf };
719 // c:5747 — the spec-path arm needs an ABSOLUTE filename, not just
720 // PM_LOADDIR.
721 if s.filename.as_deref().is_some_and(|f| f.starts_with('/')) && (fn_flags & PM_LOADDIR) != 0
722 {
723 s.filename.clone().map(|d| vec![d])
724 } else {
725 None
726 }
727 };
728 // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
729 // C runs getfpfunc (c:5753/c:5759) — a walk of `$fpath`, then a read and
730 // parse of the file — on the first call of every autoload in every
731 // process. autoloads.rkyv records what that search chose and read
732 // (`autoload_cache::ResolvedLoad`), so a plain `$fpath` autoload whose
733 // entry is still current installs the stored body without touching
734 // `$fpath` at all. With an explicit load directory (the c:5747 arm) the
735 // entry must have come from that directory.
736 let shard_hit = {
737 let dirs = match loaddir_spec.as_deref() {
738 Some(spec) => spec.to_vec(),
739 None => crate::ported::params::getaparam("fpath").unwrap_or_default(),
740 };
741 crate::autoload_cache::try_resolve(&name, &dirs)
742 };
743 let (path, dump_ksh, from_wordcode, body) = if let Some((dir, resolved)) = shard_hit {
744 dir_path = Some(dir.clone());
745 let hit = (
746 format!("{dir}/{name}"),
747 (resolved.dump_ksh >= 0).then_some(resolved.dump_ksh),
748 resolved.from_wordcode,
749 resolved.body.clone(),
750 );
751 crate::autoload_cache::stage_hit(&name, resolved);
752 hit
753 } else {
754 let mut looked_up = getfpfunc(
755 &name,
756 &mut dir_path,
757 loaddir_spec.as_deref(),
758 0,
759 &mut dump_hit,
760 ); // c:5753 / c:5759
761 // c:5754-5756 — the explicit load directory missed; `-d` (PM_CUR_FPATH,
762 // set by `autoload -d`, c:3383) or an explicit `current_fpath` argument
763 // means "also try $fpath". The Rust port never retried, so
764 // `autoload -dUz $PWD/extra/def; def` and
765 // `def() { autoload -dXUz $PWD/extra; }; def` both reported
766 // "function definition file not found" where zsh loads ./def
767 // (C04funcdef:33,40).
768 if looked_up.is_none()
769 && loaddir_spec.is_some()
770 && (current_fpath != 0 || (fn_flags & crate::ported::zsh_h::PM_CUR_FPATH) != 0)
771 {
772 dir_path = None;
773 dump_hit = None;
774 looked_up = getfpfunc(&name, &mut dir_path, None, 0, &mut dump_hit); // c:5756
775 }
776 let path = match looked_up {
777 Some(p) => p,
778 None => {
779 // !!! WARNING: RUST-ONLY BRANCH — NO DIRECT C COUNTERPART !!!
780 // compsys ships as native Rust functions (src/compsys/router.rs),
781 // so names like `_main_complete` have no definition file in
782 // $fpath. C zsh always loads them from files; zshrs must let
783 // `autoload +X -Uz _main_complete` (e.g. fzf-tab via zinit's
784 // :zinit-tmp-subst-autoload, zinit.zsh:356) succeed without one.
785 // Mark the stub loaded and return success — call-time dispatch
786 // short-circuits to the native fn (vm_helper.rs:2276), so no
787 // funcdef/body is needed.
788 if crate::compsys::router::is_intercepted(&name) {
789 unsafe {
790 (*shf).node.flags &= !(PM_UNDEFINED as i32);
791 }
792 if let Ok(mut tab) = shfunctab_lock().write() {
793 if let Some(existing) = tab.get_mut(&name) {
794 existing.node.flags &= !(PM_UNDEFINED as i32);
795 }
796 }
797 return 0;
798 }
799 // c:Src/exec.c:5713-5719 — file not found path. C:
800 // `if (prog == &dummy_eprog) {
801 // locallevel--;
802 // zwarn("%s: function definition file not found",
803 // shf->node.nam);
804 // locallevel++;
805 // popheap();
806 // return NULL;
807 // }`
808 // C's getfpfunc returns &dummy_eprog as the "not found"
809 // sentinel when test_only==0; loadautofn detects it and
810 // emits the diagnostic before returning NULL. Rust's
811 // getfpfunc returns Option::None for the same condition,
812 // so we emit the same diagnostic here. The locallevel
813 // dance is preserved as a comment because the Rust
814 // port's zwarn doesn't reference locallevel in the
815 // format string itself (the dance in C is only to keep
816 // the prefix line counter consistent with the function-
817 // body context). Bug #107 in docs/BUGS.md.
818 crate::ported::utils::zwarn(&format!("{}: function definition file not found", name));
819 return 1; // c:5719 NULL
820 }
821 };
822 let _ = autol;
823 // Previously the Rust port treated this parameter as
824 // "test_only" and early-returned when set, so the `+X`
825 // call from `eval_autoload` (`loadautofn(shf, mode, 1, d)`)
826 // never actually loaded the file. C's parameter is `autol`
827 // (autoload mode), NOT a test-only flag — the C body
828 // unconditionally loads/parses regardless of autol. autol=1
829 // controls the EF_RUN / map-flag dance for the wordcode prog
830 // (c:5725-5749), but the loaded-body / PM_UNDEFINED-clear
831 // path runs in all cases. Removing the early-return so
832 // `autoload -U +X funcname` actually loads the body and
833 // `type funcname` reports `function from /path/file` instead
834 // of `autoload shell function`. Bug #160 in docs/BUGS.md.
835 // c:5100-5140 — read the file. C uses zopen + read + parse_string +
836 // execsave; Rust port stores raw text on the ShFunc and defers
837 // parse-to-Eprog until the first call.
838 //
839 // c:Src/exec.c:6238 / parse.c:3833 — when getfpfunc resolved the
840 // function out of a compiled `.zwc` dump, there is no source file
841 // to read; the wordcode Eprog came back through `dump_hit`. C
842 // executes that wordcode directly (`shf->funcdef = stripkshdef(
843 // prog, ...)`, exec.c:5753-5755). zshrs executes function bodies
844 // through the fusevm bytecode pipeline which consumes source
845 // text, so bridge wordcode → text with the canonical text.c
846 // renderer (`getpermtext`, ported at text.rs:189) — the same
847 // walker `functions NAME` printing uses for wordcode-backed
848 // funcdefs (C hashtable.c:954). The downstream
849 // `autoload_register_source` step (vm_helper.rs:3162) performs
850 // the `stripkshdef` shape decision, matching c:5725-5760.
851 // c:5706-5710 — the ksh-mode precedence chain:
852 // `if (ksh == 1) { ksh = fksh; if (ksh == 1)
853 // ksh = PM_KSHSTORED ? 2 : PM_ZSHSTORED ? 0 : 1; }`
854 // The dump header flag (FDHF_KSHLOAD/FDHF_ZSHLOAD via `*ksh`
855 // from try_dump_file) outranks the stub's PM_*STORED bits, which
856 // are only consulted when the dump says 1 (no explicit style).
857 // zshrs's load/register split (vm_helper's
858 // `autoload_register_source` makes the c:5725 ksh-vs-zsh
859 // decision later, from the tab entry's flags + KSHAUTOLOAD) —
860 // fold a decisive dump flag into the PM bits so the downstream
861 // decision sees the same precedence.
862 let dump_ksh = dump_hit.as_ref().map(|(_, k)| *k);
863 // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
864 // C installs the dump's wordcode itself (c:5753-5755) and never lexes it
865 // again; zshrs installs a `getpermtext` deparse whose real compile happens
866 // at the call that defines the function, so the provenance has to be
867 // recorded for that later compile. See vm_helper::autoload_note_wordcode_body.
868 let from_wordcode = dump_hit.is_some();
869 let body = match dump_hit {
870 // The wordcode C executes carries a line number on every pipe
871 // (`WCB_PIPE(type, toklineno + 1)`, c:Src/parse.c:911/935/944, read
872 // back by `lineno = WC_PIPE_LINENO(pcode) - 1` at c:Src/exec.c:2057
873 // and `lineno = code - 1` at c:Src/exec.c:1356), so a dump-loaded
874 // function reports exactly the `$LINENO` of its ORIGINAL source.
875 //
876 // `getpermtext` cannot reproduce that: `gettext2` is a pretty-printer
877 // (`Src/text.c`), so it drops comments and blank lines and re-breaks
878 // compounds onto its own lines — `if X; then` becomes `if X` NEWLINE
879 // `then`. Re-parsing that text yields a DIFFERENT line for every
880 // statement, and `$LINENO` / error prefixes / `funcfiletrace` inside
881 // any `.zwc`-loaded function drift from zsh (measured: 29 vs 32 for
882 // `_parameters` loaded out of a `comp_utils.zwc` digest).
883 //
884 // !!! WARNING: RUST-ONLY BRANCH — NO DIRECT C COUNTERPART !!!
885 // C has no choice to make here: `shf->funcdef = stripkshdef(prog, …)`
886 // (c:Src/exec.c:5753-5755) runs the DUMP's wordcode and never looks at
887 // the source file again. zshrs executes function bodies as TEXT, so it
888 // has to render the wordcode back — and that render is what loses the
889 // line numbers. The source file is preferred ONLY when it still renders
890 // to the same program as the dump; then it is provably the text the
891 // wordcode was compiled from and keeps the original line numbering.
892 //
893 // The previous version skipped that test and took the source file
894 // whenever it existed, on the theory that try_dump_file's mtime gate
895 // (parse.rs, c:Src/parse.c:3762-3784) already proved they agree. It
896 // does not: the gate is `stc.st_mtime >= stn.st_mtime` at SECOND
897 // granularity, so a source rewritten within the same second as the
898 // dump — or back-dated — passes it while holding completely different
899 // code. `zcompile f; print 'print FRESH' > f; touch f; autoload f; f`
900 // ran FRESH where zsh runs the compiled body.
901 Some((prog, _ksh)) => {
902 let dump_text = crate::ported::text::getpermtext(Box::new(prog), None, 0); // c:5753
903 // The equality test below LEXES the source file, and it is asking
904 // whether that file is the text the dump's wordcode was compiled
905 // from. `zcompile` resolved the dump with the lexer state C uses
906 // for a compile, so the comparison has to be made in that same
907 // state or it answers a different question: under RCQUOTES an
908 // adjacent quote pair re-lexes as one literal quote
909 // (c:Src/lex.c:1328) and a live alias rewrites words from inside
910 // the lexer (c:Src/lex.c:1909), so a source that IS the dump's
911 // original compared unequal and the deparse was taken instead —
912 // losing the original line numbering for no reason. Same pin the
913 // `source` leg uses (vm_helper::execute_zwc_program).
914 let _relex = crate::vm_helper::ZwcRelexGuard::enter();
915 // Raw-byte read (c:Src/exec.c:5745 `getfpfunc` → `metafy`):
916 // an autoload file is not required to be UTF-8.
917 match crate::script_bytes::read_script_file(&path) {
918 // Both sides go through the SAME renderer, so a `.zwc` written
919 // by C zsh (metafied string pool) still compares equal to a
920 // locally parsed source.
921 Ok(t)
922 if crate::ported::exec::parse_string(&t, 1).is_some_and(|p| {
923 crate::ported::text::getpermtext(Box::new(p), None, 0) == dump_text
924 }) =>
925 {
926 t
927 }
928 _ => dump_text,
929 }
930 }
931 // c:Src/exec.c:5745 `getfpfunc` reads the function file with
932 // `read()` and hands the bytes to `metafy` — there is no encoding
933 // requirement. `read_to_string` rejected the file on the first
934 // non-UTF-8 byte and this arm returned 1, so an $fpath completer
935 // carrying one legacy byte autoloaded to NOTHING, silently.
936 None => match crate::script_bytes::read_script_file(&path) {
937 Ok(t) => t,
938 Err(_) => return 1,
939 },
940 };
941 if let Some(dir) = dir_path.as_deref() {
942 crate::autoload_cache::stage_search(&name, dir, &body, dump_ksh, from_wordcode);
943 }
944 (path, dump_ksh, from_wordcode, body)
945 };
946 // c:Src/exec.c:5735/5757 — `loadautofnsetfile(shf, fdir)`. The
947 // helper stamps PM_LOADDIR alongside the filename when fdir is
948 // present, so `whence -v NAME` later concatenates the directory
949 // with `/NAME` (PM_LOADDIR branch at hashtable.rs:1350). zshrs's
950 // prior `shf->filename = dir_path` assignment skipped the flag
951 // → `type colors` printed `from /path/to/functions` instead of
952 // `from /path/to/functions/colors`. Mirror C exactly.
953 unsafe {
954 loadautofnsetfile(&mut *shf, dir_path.as_deref().or(Some(&path)));
955 }
956 // c:5148 — `shf->node.flags &= ~PM_UNDEFINED`.
957 unsafe {
958 (*shf).node.flags &= !(PM_UNDEFINED as i32);
959 }
960 // c:5706-5710 fold (see comment above): decisive dump style wins
961 // over the stub's stored-style bits.
962 let (ksh_on, ksh_off): (i32, i32) = match dump_ksh {
963 Some(2) => (
964 crate::ported::zsh_h::PM_KSHSTORED as i32,
965 crate::ported::zsh_h::PM_ZSHSTORED as i32,
966 ),
967 Some(0) => (
968 crate::ported::zsh_h::PM_ZSHSTORED as i32,
969 crate::ported::zsh_h::PM_KSHSTORED as i32,
970 ),
971 _ => (0, 0),
972 };
973 unsafe {
974 (*shf).node.flags = ((*shf).node.flags | ksh_on) & !ksh_off;
975 }
976 // c:5753-5755 — C's `shf->funcdef = stripkshdef(prog, …)` installs the
977 // dump's own wordcode; zshrs installs TEXT and compiles it later, so mark
978 // (or, on a plain-file load, un-mark) the name so the compile that finally
979 // lexes this text knows it is a deparse and pins the lexer accordingly.
980 crate::vm_helper::autoload_note_wordcode_body(
981 &name,
982 if from_wordcode { Some(&body[..]) } else { None },
983 );
984 // Sync the body string into the Rust-side ShFunc table so the
985 // lazy-parse path can find it later.
986 if let Ok(mut tab) = shfunctab_lock().write() {
987 if let Some(existing) = tab.get_mut(&name) {
988 existing.body = Some(body);
989 existing.node.flags = (existing.node.flags | ksh_on) & !ksh_off;
990 // c:5657 loadautofnsetfile — store the fpath DIRECTORY absolutized
991 // with PM_LOADDIR (not the raw relative `./fns`), so `whence -v`
992 // prints the absolute source. The `+X` (eval-autoload) path relies
993 // on this since it does not re-register through vm_helper.
994 loadautofnsetfile(existing, dir_path.as_deref());
995 } else {
996 let mut shf = shfunc {
997 node: hashnode {
998 next: None,
999 nam: name.clone(),
1000 flags: ksh_on, // c:5706-5710 dump-style fold
1001 },
1002 filename: None,
1003 lineno: 0,
1004 funcdef: None,
1005 redir: None,
1006 sticky: None,
1007 body: Some(body),
1008 redir_text: None,
1009 };
1010 loadautofnsetfile(&mut shf, dir_path.as_deref()); // c:5657
1011 tab.add(shf);
1012 }
1013 }
1014 0
1015}
1016
1017/// Port of `getfpfunc()` from `Src/exec.c:6219` — C decl `getfpfunc(char *s, int *ksh, char **fdir, char **alt_path, int test_only)`.
1018/// supplied `spec_path` slice) for a file named `name` and writes the
1019/// resolved directory through `*dir_path_out` (matching the C `char **dir_path`).
1020/// Returns `Some(file_path)` on success, `None` when not found.
1021///
1022/// Per dir, the compiled-dump lookup runs FIRST (c:6238
1023/// `try_dump_file(*pp, s, buf, ksh, test_only)`) — a directory
1024/// digest `<dir>.zwc` or per-function `<dir>/<name>.zwc` wins over
1025/// the plain file when newer (mtime logic inside try_dump_file,
1026/// c:parse.c:3762-3784). On a dump hit the loaded program + ksh
1027/// mode (C's `*ksh` out-param) are written through `dump_out` and
1028/// the nominal `<dir>/<name>` path is returned; the caller must
1029/// check `dump_out` before reading the returned path as a plain
1030/// file.
1031pub fn getfpfunc(
1032 name: &str,
1033 dir_path_out: &mut Option<String>, // c:6219 (Src/exec.c)
1034 spec_path: Option<&[String]>,
1035 test_only: i32, // c:6219 `int test_only`
1036 dump_out: &mut Option<(eprog, i32)>, // c:6219 `int *ksh` + dump Eprog return
1037) -> Option<String> {
1038 // C reads $fpath via `getaparam("fpath")` (the param-table array form
1039 // tied to scalar `FPATH` via `typeset -T`). Reading `std::env::var`
1040 // misses any in-script modification like `fpath=(/some/dir $fpath)`
1041 // because that mutates the internal param table, not the inherited
1042 // process env. Fall back to env only when the param table is empty
1043 // (cold start before any param-table init).
1044 let dirs: Vec<String> = match spec_path {
1045 Some(s) => s.to_vec(),
1046 None => crate::ported::params::getaparam("fpath")
1047 .filter(|v| !v.is_empty())
1048 .or_else(|| getsparam("FPATH").map(|v| v.split(':').map(String::from).collect()))
1049 .or_else(|| {
1050 std::env::var("FPATH")
1051 .ok()
1052 .map(|v| v.split(':').map(String::from).collect())
1053 })
1054 .unwrap_or_default(),
1055 };
1056 for dir in &dirs {
1057 if dir.is_empty() {
1058 continue;
1059 }
1060 let path = format!("{}/{}", dir, name); // c:6230 snprintf(buf, ..., "%s/%s", *pp, s)
1061 // c:6238 — `if ((r = try_dump_file(*pp, s, buf, ksh, test_only)))`
1062 // — the .zwc digest / per-function dump is tried BEFORE the
1063 // plain file in each directory.
1064 if let Some(hit) = crate::ported::parse::try_dump_file(dir, name, &path, test_only != 0) {
1065 *dump_out = Some(hit);
1066 *dir_path_out = Some(dir.clone()); // c:6240 `*fdir = *pp;`
1067 return Some(path); // c:6241
1068 }
1069 if std::path::Path::new(&path).exists() {
1070 // c:6245 access(buf, R_OK)
1071 *dir_path_out = Some(dir.clone());
1072 return Some(path);
1073 }
1074 }
1075 None
1076}
1077
1078/// Port of `resolvebuiltin()` from `Src/exec.c:2703`. — C decl `resolvebuiltin(const char *cmdarg, HashNode hn)`.
1079/// the autoloaded-builtin stub firing.
1080///
1081/// C body:
1082/// ```c
1083/// if (!((Builtin) hn)->handlerfunc) {
1084/// char *modname = dupstring(((Builtin) hn)->optstr);
1085/// (void)ensurefeature(modname, "b:",
1086/// (hn->flags & BINF_AUTOALL) ? NULL : hn->nam);
1087/// hn = builtintab->getnode(builtintab, cmdarg);
1088/// if (!hn) {
1089/// lastval = 1;
1090/// zerr("autoloading module %s failed to define builtin: %s",
1091/// modname, cmdarg);
1092/// return NULL;
1093/// }
1094/// }
1095/// return hn;
1096/// ```
1097///
1098/// zshrs split: the C autoload stub (builtintab node with NULL
1099/// handlerfunc, installed by `add_autobin` c:426) lives in the
1100/// `autoload_builtins` ledger (name → module). This fn is the
1101/// dispatch-time consult:
1102/// - `None` — name has no autoload stub; caller continues its
1103/// normal lookup chain.
1104/// - `Some(0)` — module loaded; caller re-dispatches the (now
1105/// registered) builtin.
1106/// - `Some(1)` — load failed or the loaded module didn't define
1107/// the feature; diagnostics already printed (load_module's
1108/// `failed to load module` zwarn, or the c:2718 zerr here).
1109/// Caller returns status 1.
1110///
1111/// Ledger upkeep: the entry is removed in every fired path —
1112/// - success: C's addbuiltin (module.c:411-415) replaces the stub
1113/// with the real node, so the stub is gone;
1114/// - load failure: C's execbuiltin head (Src/builtin.c:264-267)
1115/// hits the still-NULL handlerfunc and `deletebuiltin`s the
1116/// stub — a second call reports `command not found` / 127
1117/// (probed: zsh 5.9 `zmodload -ab zsh/bogus mybltn; mybltn;
1118/// mybltn` → rc1=1, rc2=127).
1119///
1120/// AUTOALL note: the ledger doesn't carry BINF_AUTOALL, so the
1121/// ensurefeature arg is always `Some(name)` (the non-AUTOALL form).
1122/// The AUTOALL path is unreachable for builtins via `zmodload -a MOD`
1123/// (both shells error "`/' is invalid in a builtin"); revisit if the
1124/// ledger grows flags.
1125pub fn resolvebuiltin(name: &str) -> Option<i32> {
1126 // c:2700
1127 let mut tab = crate::ported::module::MODULESTAB.lock().ok()?;
1128 // c:2705 — `if (!((Builtin) hn)->handlerfunc)`: ledger hit IS the
1129 // "no handlerfunc" stub in zshrs.
1130 let module = tab.autoload_builtins.get(name)?.clone();
1131 // c:2706 — `modname = dupstring(hn->optstr)` (done: `module`).
1132 // Stub fires exactly once per registration (see ledger upkeep
1133 // in the doc above).
1134 tab.autoload_builtins.remove(name);
1135 // c:2711-2713 — ensurefeature(modname, "b:", hn->nam).
1136 let _ = crate::ported::module::ensurefeature(&mut tab, &module, "b:", Some(name));
1137 // c:2714 — `hn = builtintab->getnode(builtintab, cmdarg);`
1138 // zshrs analog: the module booted (is_loaded) AND the name is in
1139 // the static builtintab (createbuiltintable pre-registers every
1140 // module bintab entry).
1141 let defined =
1142 tab.is_loaded(&module) && crate::ported::builtin::createbuiltintable().contains_key(name);
1143 if defined {
1144 return Some(0); // c:2723 `return hn;`
1145 }
1146 if tab.is_loaded(&module) {
1147 // c:2716-2720 — module loaded but feature missing.
1148 crate::ported::builtin::LASTVAL.store(1, std::sync::atomic::Ordering::Relaxed); // c:2717 lastval = 1
1149 crate::ported::utils::zerr(&format!(
1150 "autoloading module {} failed to define builtin: {}",
1151 module, name
1152 )); // c:2718
1153 }
1154 // Load failure: load_module already printed `failed to load
1155 // module \`...'`; C's execbuiltin head returns 1 silently
1156 // (Src/builtin.c:264-267).
1157 Some(1)
1158}
1159
1160/// Port of `static struct builtin commandbn` from `Src/exec.c:276`.
1161///
1162/// c:275 — /* structure for command builtin for when it is used with -v or -V */
1163///
1164/// `BUILTIN("command", 0, bin_whence, 0, -1, BIN_COMMAND, "pvV", NULL)`.
1165/// This is a SEPARATE descriptor from the `BIN_PREFIX("command", …)` row in
1166/// `builtintab` (which carries no handler at all): c:3209 swaps `hn` to this
1167/// one when `command -v` / `command -V` is seen, so the dispatch lands in
1168/// `bin_whence` with funcid `BIN_COMMAND`.
1169pub static commandbn: std::sync::LazyLock<crate::ported::zsh_h::builtin> =
1170 std::sync::LazyLock::new(|| {
1171 crate::ported::builtin::BUILTIN(
1172 "command",
1173 0,
1174 Some(crate::ported::builtin::bin_whence),
1175 0,
1176 -1,
1177 crate::ported::hashtable_h::BIN_COMMAND,
1178 Some("pvV"),
1179 None,
1180 ) // c:282
1181 });
1182
1183/// Dispatch decision returned by `execcmd_compile_head` — the
1184/// fusevm-bytecode-time head resolver that mirrors the local-variable
1185/// state the C `execcmd_exec` function carries through `c:2913-2916`
1186/// (`is_builtin`, `is_shfunc`, `cflags`, `use_defpath`) plus the
1187/// precmd-modifier strip count. The fusevm bytecode compiler reads
1188/// this to emit the correct dispatch opcode in
1189/// `src/extensions/compile_zsh.rs::compile_simple`.
1190///
1191/// Not a C struct — invented to bridge the divergence between the
1192/// C wordcode-walker (which mutates locals + falls through to
1193/// invocation) and zshrs's split parse → compile → VM pipeline.
1194#[allow(non_camel_case_types)]
1195#[derive(Debug, Default, Clone)]
1196pub struct execcmd_dispatch {
1197 /// Number of `BINF_PREFIX` words to strip from the head of args.
1198 /// `Src/exec.c:3086 uremnode(preargs, firstnode(preargs))`.
1199 pub precmd_skip: usize,
1200 /// Set when the head (after strip) is a real builtin
1201 /// (`Src/exec.c:3065 is_builtin = 1`).
1202 pub is_builtin: bool,
1203 /// Set when the head (after strip) is a shell function
1204 /// (`Src/exec.c:3053 is_shfunc = 1`).
1205 pub is_shfunc: bool,
1206 /// `cflags` accumulator from `Src/exec.c:2915` — gathers
1207 /// `BINF_BUILTIN | BINF_COMMAND | BINF_EXEC | BINF_DASH |
1208 /// BINF_NOGLOB` bits encountered during the precommand-modifier
1209 /// walk (c:3062 `cflags |= hn->flags`).
1210 pub cflags: u32,
1211 /// `orig_cflags` accumulator from `Src/exec.c:2915` — the `cflags` in
1212 /// force before each `cflags |= hn->flags` step (c:3115), i.e. every
1213 /// modifier flag the walk consumed, `BINF_COMMAND` included (c:4123,
1214 /// c:4424 test it).
1215 pub orig_cflags: u32,
1216 /// `command -p` requested: use the default `$PATH` for lookup
1217 /// (`Src/exec.c:3160 use_defpath = 1`). Consumed by the fusevm
1218 /// compiler (`fusevm_bridge.rs`) when it emits the exec call.
1219 pub use_defpath: bool,
1220 /// `command -v` / `command -V` requested: the dispatch target
1221 /// flips to `bin_whence` per `Src/exec.c:3149-3157`
1222 /// (`hn = &commandbn.node; is_builtin = 1`). The fusevm compiler
1223 /// reads this and emits `Op::CallBuiltin(BUILTIN_WHENCE_FROM_COMMAND)`
1224 /// instead of resolving the post-strip head.
1225 pub has_command_vv: bool,
1226 /// `exec -a NAME` requested: ARGV0 override per `Src/exec.c:3214-3240`.
1227 /// `Some(NAME)` triggers `zputenv("ARGV0=NAME")` before exec.
1228 pub exec_argv0: Option<String>,
1229 /// Empty-command branch fired with no redirs (`Src/exec.c:3372-3406`
1230 /// — the `else` arm of `if (redir && nonempty(redir))`). Covers
1231 /// bare `exec` / `noglob` / `command`. Caller emits
1232 /// `lastval = cmdoutval` (0 when no `$(cmd)` ran) and returns.
1233 /// Also fires for the `(cflags & BINF_PREFIX) && (cflags &
1234 /// BINF_COMMAND)` sub-case at `c:3365-3371` (bare `command`
1235 /// returns 0 without complaining about missing redirs).
1236 pub is_empty_command: bool,
1237 /// `preargs` as the walk left it (the c:3086 / c:3165 / c:3177 /
1238 /// c:3209 / c:3239 `uremnode`s): the command words are
1239 /// `preargs[precmd_skip..]`, with `command -p` / `--` and the `exec`
1240 /// option words already removed.
1241 pub preargs: Vec<String>,
1242}
1243
1244/// !!! WARNING: RUST-ONLY HELPER !!!
1245///
1246/// No function of this name exists in `Src/exec.c`. This helper stands in for
1247/// the head section of `execcmd_exec` (c:2901; locals + the BINF_PREFIX
1248/// precommand-modifier walk at c:2966-3096), called at fusevm
1249/// bytecode-compile time. It does NOT perform dispatch the way the C function
1250/// does.
1251/// !!! NOT A PORT OF C `execcmd_exec` !!!
1252///
1253/// This is a fusevm-bytecode-time head resolver invoked by
1254/// `src/extensions/compile_zsh.rs::compile_simple` and the
1255/// `command` builtin shim in `src/fusevm_bridge.rs`. The canonical
1256/// 7-arg port of `Src/exec.c:execcmd_exec` lives elsewhere in this
1257/// file under the C-faithful name `execcmd_exec`.
1258///
1259/// This helper mirrors the head section (`c:2904-3275`) of the C
1260/// function — local initialisation, the precommand-modifier walk
1261/// that strips `BINF_PREFIX` builtins (`-`, `builtin`, `command`,
1262/// `exec`, `noglob`), and the `BINF_COMMAND`/`BINF_EXEC`
1263/// sub-option parsers — and returns the resulting dispatch
1264/// decision via `execcmd_dispatch`. The fusevm compiler reads
1265/// that struct to decide which `Op::CallBuiltin` /
1266/// `Op::CallFunction` / `Op::Exec` to emit, and to compute the
1267/// correct post-strip `argc`.
1268///
1269/// =================== WARNING — DIVERGENCE ====================
1270///
1271/// The C function runs ~1500 lines and PERFORMS dispatch: it sets up
1272/// `multio` redirections, evaluates `varspc` assignments, then calls
1273/// `execbuiltin` / `runshfunc` / `execute` directly. This helper
1274/// stops after the precmd-modifier walk and only returns the head
1275/// decision; runtime dispatch is driven by the bytecode the fusevm
1276/// compiler emits.
1277///
1278/// Signature adaptation: the C `Estate`/`Execcmd_params` carry the
1279/// wordcode iterator state — zshrs doesn't traverse wordcode here,
1280/// so the args list arrives already-expanded as a `&[String]`
1281/// (analog of `preargs` after `execcmd_getargs` at `c:3028`).
1282/// `type_` mirrors `eparams->type` (`WC_SIMPLE` vs `WC_TYPESET`).
1283///
1284/// =============================================================
1285pub fn execcmd_compile_head(args: &[String], type_: u32) -> execcmd_dispatch {
1286 // c:2900 (Src/exec.c)
1287
1288 // c:2904-2916 — locals.
1289 let mut hn: Option<&'static builtin> = None; // c:2904
1290 let mut is_shfunc = false; // c:2913
1291 let mut is_builtin = false; // c:2913
1292 let mut use_defpath = false; // c:2913
1293 let mut cflags: u32 = 0; // c:2915
1294 let mut orig_cflags: u32 = 0; // c:2915
1295 // c:3263 — `char *exec_argv0 = NULL;` (declared inside the
1296 // BINF_EXEC arm; hoisted here so the dispatch struct can carry it
1297 // out after the loop terminates).
1298 let mut exec_argv0: Option<String> = None;
1299 // c:3149/3158 — `has_vV`/`has_p` flags from the BINF_COMMAND arm
1300 // (c:3104). Surface `has_vV` via the dispatch struct so the fusevm
1301 // compiler can emit `bin_whence` instead of resolving the head.
1302 let mut has_command_vv = false;
1303
1304 // c:2962-2973 (`%job` head rewrite) and c:2975-2986 (AUTORESUME
1305 // prefix match) need `how`, `redir` and `input`, which this head
1306 // classifier does not receive; execcmd_exec performs both rewrites
1307 // before calling it.
1308
1309 // c:3013-3091 — precommand-modifier walk.
1310 let mut preargs: Vec<String> = args.to_vec(); // c:3027 newlinklist
1311 let mut precmd_skip: usize = 0;
1312
1313 // c:3018 — `if ((type == WC_SIMPLE || type == WC_TYPESET) && args)`.
1314 if (type_ == WC_SIMPLE || type_ == WC_TYPESET) && !preargs.is_empty() {
1315 // c:3018
1316 // c:3029 — `while (nonempty(preargs))`.
1317 while precmd_skip < preargs.len() {
1318 // c:3029
1319 // c:3030 — `cmdarg = (char *) peekfirst(preargs);`.
1320 let cmdarg = untokenize(&preargs[precmd_skip]);
1321 // c:3031 — `checked = !has_token(cmdarg)`. zshrs's fusevm
1322 // already performed prefork expansion on `preargs`, so
1323 // `has_token` is effectively false here; the C `break` on
1324 // unexpanded tokens is unreachable in this entry point.
1325
1326 // c:3034-3035 — WC_TYPESET fast path: `getnode2` looks up
1327 // even disabled builtins so the reserved-word form
1328 // (`integer x`, `local foo`) still dispatches to the
1329 // typeset family. The static `BUILTINS` array doesn't
1330 // expose a separate disabled-bit lookup; one path covers
1331 // both. Effect is identical for the precmd-modifier walk.
1332
1333 // c:3098-3103 — `} else if (isset(POSIXBUILTINS) && (cflags &
1334 // BINF_EXEC)) { /* POSIX doesn't allow "exec" to operate on
1335 // builtins or shell functions. */ break; }`
1336 let typeset_hit = type_ == WC_TYPESET && BUILTINS.iter().any(|b| b.node.nam == cmdarg); // c:3088-3089
1337 if !typeset_hit && isset(POSIXBUILTINS) && (cflags & BINF_EXEC) != 0 {
1338 break; // c:3103
1339 }
1340
1341 // c:3050-3052 — `if (!(cflags & (BINF_BUILTIN |
1342 // BINF_COMMAND)) && shfunctab->getnode(...))` — shell
1343 // function takes precedence unless a `builtin`/`command`
1344 // modifier preceded it.
1345 if (cflags & (BINF_BUILTIN | BINF_COMMAND)) == 0 {
1346 // c:3106 — `(hn = shfunctab->getnode(shfunctab, cmdarg))`.
1347 // `shfunctab->getnode` is `gethashnode`
1348 // (Src/hashtable.c:821 in `createshfunctable`), i.e. an
1349 // O(1) hash probe that also skips DISABLED nodes. The
1350 // previous Rust code walked the whole table with
1351 // `iter().any(...)`, which is O(#functions) per command
1352 // head AND ignored the DISABLED bit `gethashnode`
1353 // honours. With a real `.zcompdump` loaded the table
1354 // holds ~50k autoload stubs, so every compiled command
1355 // head paid a 50k-entry String scan under the read lock.
1356 if shfunctab_lock()
1357 .read()
1358 .map(|t| !t.getnode(&cmdarg).is_null())
1359 .unwrap_or(false)
1360 {
1361 is_shfunc = true; // c:3107
1362 break; // c:3108
1363 }
1364 }
1365 // c:3056 — `builtintab->getnode(builtintab, cmdarg)`.
1366 let entry = BUILTINS.iter().find(|b| b.node.nam == cmdarg);
1367 let Some(entry) = entry else {
1368 // c:3056-3058
1369 break;
1370 };
1371 hn = Some(entry);
1372 // c:3061-3063 — accumulate cflags.
1373 orig_cflags |= cflags;
1374 cflags &= !(BINF_BUILTIN | BINF_COMMAND);
1375 cflags |= entry.node.flags as u32;
1376 // c:3064 — `if (!(hn->flags & BINF_PREFIX))` — real
1377 // builtin, stop.
1378 if (entry.node.flags as u32 & BINF_PREFIX) == 0 {
1379 // c:3064
1380 // WARNING — DIVERGENCE: c:3068 calls `resolvebuiltin`
1381 // to autoload the builtin's module if its
1382 // `handlerfunc` is NULL. In zshrs, builtins live in
1383 // two places: the static `BUILTINS` table (which
1384 // mirrors C `handlerfunc`, often `None` for ports
1385 // dispatched through fusevm) AND fusevm's
1386 // `register_builtins` map (the actual runtime
1387 // dispatcher). A null `handlerfunc` in the static
1388 // table is NOT an autoload failure for us — it
1389 // means dispatch routes through fusevm. So we
1390 // skip the resolvebuiltin call here; the faithful
1391 // port remains available for future callers that
1392 // genuinely need module-autoload semantics.
1393 is_builtin = true; // c:3065
1394 break; // c:3077
1395 }
1396 // c:3086 — `uremnode(preargs, firstnode(preargs))`.
1397 precmd_skip += 1;
1398 // c:3087-3091 — `if (!firstnode(preargs)) { execcmd_getargs
1399 // (...); if (!firstnode(preargs)) break; }`. zshrs has
1400 // no `execcmd_getargs` (args arrive pre-expanded); the
1401 // bounds-check at the top of `while precmd_skip <
1402 // preargs.len()` handles the empty case identically.
1403
1404 // c:3092-3177 — BINF_COMMAND sub-option parsing
1405 // (`command -p / -v / -V`).
1406 if (cflags & BINF_COMMAND) != 0 && precmd_skip < preargs.len() {
1407 // c:3102-3104 — `LinkNode argnode, oldnode, pnode = NULL;
1408 // int has_p = 0, has_vV = 0, has_other = 0;`
1409 let mut argnode: usize = precmd_skip; // c:3105 `argnode = firstnode(preargs);`
1410 let mut pnode: Option<usize> = None; // c:3102
1411 let mut has_p = false; // c:3104
1412 let mut has_vv = false; // c:3104
1413 let mut has_other = false; // c:3104
1414 // c:3107 — `while (IS_DASH(*argdata))`
1415 while argnode < preargs.len()
1416 && IS_DASH(preargs[argnode].chars().next().unwrap_or('\0'))
1417 {
1418 let argdata = preargs[argnode].clone(); // c:3106
1419 let bytes = argdata.as_bytes();
1420 // c:3108-3111 — stop on bare `-` or `--`.
1421 if bytes.len() < 2 || (IS_DASH(bytes[1] as char) && bytes.len() == 2) {
1422 // c:3109
1423 break; // c:3111
1424 }
1425 // c:3112-3133 — scan flag chars.
1426 for &c in &bytes[1..] {
1427 // c:3112
1428 match c as char {
1429 'p' => {
1430 // c:3114
1431 has_p = true; // c:3122
1432 pnode = Some(argnode); // c:3123
1433 }
1434 'v' | 'V' => {
1435 // c:3125-3126
1436 has_vv = true; // c:3127
1437 }
1438 _ => {
1439 // c:3129
1440 has_other = true; // c:3130
1441 }
1442 }
1443 }
1444 // c:3134-3138 — unknown flag → don't try, leave alone.
1445 if has_other {
1446 // c:3134
1447 has_p = false; // c:3136
1448 has_vv = false; // c:3136
1449 break; // c:3137
1450 }
1451 // c:3140-3147 — advance to next arg.
1452 argnode += 1; // c:3141 nextnode(argnode)
1453 if argnode >= preargs.len() {
1454 // c:3142 — execcmd_getargs (skipped: pre-expanded)
1455 break; // c:3145
1456 }
1457 }
1458 // c:3149-3157 — `-v`/`-V` → dispatch to whence.
1459 if has_vv {
1460 // c:3149
1461 // c:3154 `pushnode(preargs, "command")` — C re-inserts
1462 // "command" so bin_whence sees it as argv[0]. zshrs
1463 // surfaces this via `has_command_vv`; the fusevm
1464 // compiler emits the equivalent whence call.
1465 has_command_vv = true; // c:3155-3156 hn = &commandbn; is_builtin=1
1466 is_builtin = true;
1467 break; // c:3157
1468 } else if has_p {
1469 // c:3158
1470 use_defpath = true; // c:3160
1471 if let Some(pn) = pnode {
1472 // c:3165 — `uremnode(preargs, pnode)`. zshrs:
1473 // remove the `-p`-bearing arg from preargs.
1474 if pn < preargs.len() {
1475 preargs.remove(pn);
1476 // precmd_skip already accounts for the
1477 // stripped `command` prefix; we just removed
1478 // the `-p` flag which sat at preargs[pn].
1479 // No precmd_skip change needed — the head
1480 // remains where it was.
1481 }
1482 }
1483 }
1484 // c:3176-3177 — `--` trailing end-of-options strip.
1485 if argnode < preargs.len() {
1486 let argdata = &preargs[argnode];
1487 let b = argdata.as_bytes();
1488 if b.len() == 2 && IS_DASH(b[0] as char) && IS_DASH(b[1] as char) {
1489 // c:3176
1490 preargs.remove(argnode); // c:3177
1491 }
1492 }
1493 } else if (cflags & BINF_EXEC) != 0 && precmd_skip < preargs.len() {
1494 // c:3178-3275 — BINF_EXEC sub-option parsing
1495 // (`exec -a NAME -l -c`).
1496 let mut argnode: usize = precmd_skip; // c:3185
1497 let mut error_done = false;
1498 // c:3196 — `while (argdata && IS_DASH(*argdata) &&
1499 // strlen(argdata) >= 2)`
1500 while argnode < preargs.len() {
1501 let argdata = preargs[argnode].clone();
1502 let bytes = argdata.as_bytes();
1503 if bytes.is_empty() || !IS_DASH(bytes[0] as char) || bytes.len() < 2 {
1504 break; // c:3196 loop guard
1505 }
1506 let oldnode = argnode; // c:3197
1507 argnode += 1; // c:3198 nextnode(oldnode)
1508 // c:3203-3208 — empty next → error.
1509 if argnode >= preargs.len() {
1510 // c:3203
1511 zerr(
1512 // c:3204
1513 "exec requires a command to execute",
1514 );
1515 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3206
1516 error_done = true;
1517 break; // c:3207 goto done
1518 }
1519 // c:3209 — `uremnode(preargs, oldnode)`.
1520 preargs.remove(oldnode);
1521 argnode -= 1; // re-anchor — `argnode` was the post-removed slot
1522 // c:3210-3211 — `--` stops option scan.
1523 if bytes.len() == 2 && IS_DASH(bytes[0] as char) && IS_DASH(bytes[1] as char) {
1524 // c:3210
1525 break; // c:3211
1526 }
1527 // c:3212-3258 — scan flag chars after the leading `-`.
1528 let mut k = 1usize;
1529 while k < bytes.len() && !error_done {
1530 let cmdopt = bytes[k] as char; // c:3212
1531 match cmdopt {
1532 'a' => {
1533 // c:3214 — `-a` ARGV0 override.
1534 if k + 1 < bytes.len() {
1535 // c:3216 — `-aNAME` inline form.
1536 exec_argv0 =
1537 Some(String::from_utf8_lossy(&bytes[k + 1..]).into_owned()); // c:3217
1538 k = bytes.len(); // c:3219 position past end
1539 } else {
1540 // c:3220 — `-a NAME` separate form.
1541 if argnode >= preargs.len() {
1542 // c:3274-3279 — `if (!argnode)`.
1543 zerr("exec requires a command to execute");
1544 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
1545 error_done = true;
1546 break;
1547 }
1548 // c:3280-3283 — `if (!nextnode(argnode))`:
1549 // the NAME must still leave a command.
1550 if argnode + 1 >= preargs.len() {
1551 // c:3230
1552 zerr(
1553 // c:3231
1554 "exec flag -a requires a parameter",
1555 );
1556 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3233
1557 error_done = true;
1558 break; // c:3234 goto done
1559 }
1560 exec_argv0 = Some(preargs[argnode].clone()); // c:3236
1561 preargs.remove(argnode); // c:3239
1562 }
1563 }
1564 'c' => {
1565 // c:3242
1566 cflags |= BINF_CLEARENV; // c:3243
1567 }
1568 'l' => {
1569 // c:3245
1570 cflags |= BINF_DASH; // c:3246
1571 }
1572 _ => {
1573 // c:3248
1574 zerr(
1575 // c:3249
1576 &format!("unknown exec flag -{}", cmdopt),
1577 );
1578 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3251
1579 error_done = true;
1580 break; // c:3256
1581 }
1582 }
1583 k += 1;
1584 }
1585 if error_done {
1586 break;
1587 }
1588 }
1589 // c:3263-3274 — zputenv("ARGV0=NAME"). zshrs defers
1590 // the actual `setenv` to the fusevm compiler / external
1591 // exec path; we surface `exec_argv0` via the dispatch
1592 // struct so the caller can apply it before fork+exec.
1593 if let Some(ref a0) = exec_argv0 {
1594 // c:3263 — `remnulargs + untokenize` then setenv.
1595 let cleaned = untokenize(a0); // c:3266-3267
1596 exec_argv0 = Some(cleaned);
1597 }
1598 if error_done {
1599 return execcmd_dispatch {
1600 precmd_skip,
1601 is_builtin,
1602 is_shfunc,
1603 cflags,
1604 orig_cflags,
1605 use_defpath,
1606 has_command_vv,
1607 exec_argv0,
1608 is_empty_command: false,
1609 preargs,
1610 };
1611 }
1612 }
1613 // c:3275-3278 — `hn = NULL; if ((cflags & BINF_COMMAND) &&
1614 // unset(POSIXBUILTINS)) break;`. After processing a
1615 // `command` precmd modifier (and its -p/-v/-V flags), the
1616 // C loop exits with hn cleared so the dispatch falls
1617 // through to external lookup. Without this, the next
1618 // iteration would find `command print` → print's builtin
1619 // and dispatch to it; zsh's intentional behaviour is to
1620 // skip builtins under `command` (unless POSIXBUILTINS is
1621 // set, where the loop continues normally).
1622 if (cflags & BINF_COMMAND) != 0 && !isset(POSIXBUILTINS) {
1623 hn = None; // c:3275 hn = NULL
1624 break; // c:3277
1625 }
1626 }
1627 }
1628
1629 // c:3309-3406 — "Empty command" branch. When the precmd-modifier
1630 // walk above strips every word with nothing left to dispatch
1631 // (bare `exec`, bare `noglob`, bare `command`, bare `nocorrect`),
1632 // C falls into `if (!args || empty(args))` at c:3331. Sub-cases:
1633 //
1634 // - redir-present + do_exec → nullexec=1 (continue to run)
1635 // - redir-present + varspc → nullexec=2 (continue)
1636 // - redir-present + no nullcmd → `zerr("redirection with no command")`
1637 // lastval=1, return
1638 // - redir-present + SHNULLCMD → args=[":"]
1639 // - redir-present + readnullcmd → args=[readnullcmd]
1640 // - redir-present + default → args=[nullcmd]
1641 // - NO redir + BINF_PREFIX+COMMAND → lastval=0, return (c:3365-3371)
1642 // - NO redir + default → lastval=cmdoutval, return (c:3372-3406)
1643 //
1644 // zshrs's `execcmd_compile_head` doesn't receive `redir` (it
1645 // takes `args` only). The cases that DEPEND on redirs are handled by
1646 // `compile_zsh.rs::compile_redir` before this dispatch fires; the
1647 // remaining cases collapse into the single `is_empty_command`
1648 // flag below. Both NO-redir sub-cases produce the same observable
1649 // outcome (lastval=0, return without invoking anything), so a
1650 // single flag suffices.
1651 let is_empty_command = precmd_skip >= preargs.len();
1652
1653 // =================== WARNING — DIVERGENCE ====================
1654 // c:3285+: prefork-substitution, magic_assign decision, multio
1655 // setup, varspc evaluation, and the actual execbuiltin /
1656 // runshfunc / execute call. ~1300 lines of interpreter-only
1657 // code, entirely replaced by fusevm bytecode dispatch in
1658 // `src/extensions/compile_zsh.rs::compile_simple` and the
1659 // opcode handlers in `src/fusevm_bridge.rs::register_builtins`.
1660 // The return value below feeds those compile-time decisions.
1661 // =============================================================
1662
1663 let _ = hn;
1664 execcmd_dispatch {
1665 precmd_skip,
1666 is_builtin,
1667 is_shfunc,
1668 cflags,
1669 orig_cflags,
1670 use_defpath,
1671 has_command_vv,
1672 exec_argv0,
1673 is_empty_command,
1674 preargs,
1675 }
1676}
1677
1678// =============================================================================
1679// Leaf-function ports — c:283 (parse_string) and below. The wordcode walker
1680// (execlist / execpline / execcmd_exec) is ported below; the fusevm bytecode
1681// VM is the primary execution path (see the WARNING block in execcmd_exec).
1682// =============================================================================
1683
1684/// Port of `parse_string()` from `Src/exec.c:283` — C decl `parse_string(char *s, int reset_lineno)`.
1685///
1686/// C body:
1687/// ```c
1688/// Eprog p; zlong oldlineno;
1689/// zcontext_save();
1690/// inpush(s, INP_LINENO, NULL);
1691/// strinbeg(0);
1692/// oldlineno = lineno;
1693/// if (reset_lineno) lineno = 1;
1694/// p = parse_list();
1695/// lineno = oldlineno;
1696/// if (tok == LEXERR && !lastval) lastval = 1;
1697/// strinend();
1698/// inpop();
1699/// zcontext_restore();
1700/// return p;
1701/// ```
1702///
1703/// Parses an arbitrary string as a zsh command list, returning the
1704/// `Eprog` (compiled wordcode). Used by `getoutput` for `$(cmd)`,
1705/// `bin_eval` for `eval`, and the autoload path.
1706pub fn parse_string(s: &str, reset_lineno: i32) -> Option<eprog> {
1707 use crate::ported::lex::{LEX_FILE_WINDOW_STRIN, LEX_INPUT, LEX_POS, LEX_UNGET_BUF};
1708
1709 // c:285-286
1710 let p: Option<eprog>;
1711 let oldlineno: i64;
1712
1713 zcontext_save(); // c:288
1714
1715 // zshrs-only: park the `LEX_INPUT` window. C has ONE reader — the
1716 // input stack — so `inpush` at c:289 is all the isolation `parse_list`
1717 // needs. zshrs lexes from a second source as well, the `LEX_INPUT` /
1718 // `LEX_POS` char window (lex.rs:5181), which `inpush` does not touch,
1719 // so a nested parse ran on whatever text the OUTER lexer still had
1720 // parked there. That was invisible while every caller's window was
1721 // already drained at execution time, and became live when `bin_dot`
1722 // started running a plain sourced file per command
1723 // (`execute_script_per_command`, vm_helper.rs:3321): the file is now
1724 // the lexer's own window WHILE its commands execute, so
1725 // `zstyle -e ':x' lc 'reply=(1)'` (zutil.rs:856 → here) parsed the
1726 // REST OF THE FILE as its eval body and everything after that line
1727 // silently vanished. Draining the window makes `hgetc`'s two-input
1728 // bridge read only the `inpush`ed frame — the same parking
1729 // `parsestrnoerr` (lex.rs:3402-3444) and `parse_isolated`
1730 // (vm_helper.rs:843-877) already do around their own nested parses.
1731 let saved_input = LEX_INPUT.with_borrow(|w| w.clone());
1732 let saved_pos = LEX_POS.get();
1733 let saved_unget = LEX_UNGET_BUF.with_borrow(|b| b.clone());
1734 let saved_file_window = LEX_FILE_WINDOW_STRIN.get();
1735 let saved_in_lexstop = crate::ported::input::lexstop.with(|c| c.get());
1736 LEX_INPUT.with_borrow_mut(|w| w.clear());
1737 LEX_POS.set(0);
1738 LEX_UNGET_BUF.with_borrow_mut(|b| b.clear());
1739 // The pushed frame is a STRING unit, whatever the outer window was.
1740 LEX_FILE_WINDOW_STRIN.set(0);
1741
1742 inpush(s, INP_LINENO, None); // c:289
1743 strinbeg(0); // c:290
1744 oldlineno = LEX_LINENO.get() as i64; // c:291
1745 if reset_lineno != 0 {
1746 // c:292
1747 LEX_LINENO.set(1); // c:293
1748 }
1749 p = parse_list(); // c:294
1750 LEX_LINENO.set(oldlineno as u64); // c:295
1751 // c:296-297 — `if (tok == LEXERR && !lastval) lastval = 1;`
1752 if tok() == LEXERR && LASTVAL.load(Ordering::Relaxed) == 0 {
1753 LASTVAL.store(1, Ordering::Relaxed);
1754 }
1755 strinend(); // c:298
1756 inpop(); // c:299
1757
1758 // Put the outer window back where the nested parse found it. Draining
1759 // it set the input-side `lexstop`, whose lex.rs half is the only one
1760 // `zcontext` covers (parse_isolated, vm_helper.rs:857), so the outer
1761 // reader would otherwise resume at EOF.
1762 LEX_INPUT.with_borrow_mut(|w| *w = saved_input);
1763 LEX_POS.set(saved_pos);
1764 LEX_UNGET_BUF.with_borrow_mut(|b| *b = saved_unget);
1765 LEX_FILE_WINDOW_STRIN.set(saved_file_window);
1766 crate::ported::input::lexstop.with(|c| c.set(saved_in_lexstop));
1767
1768 zcontext_restore(); // c:300
1769 p // c:301
1770}
1771
1772/// Port of `isgooderr()` from `Src/exec.c:652` — C decl `isgooderr(int e, char *dir)`.
1773///
1774/// C body:
1775/// ```c
1776/// /* Maybe the directory was unreadable, or maybe it wasn't even a directory. */
1777/// return ((e != EACCES || !access(dir, X_OK)) &&
1778/// e != ENOENT && e != ENOTDIR);
1779/// ```
1780///
1781/// errno classifier for `execve` failures during PATH search: if the
1782/// errno is EACCES (and the dir is X-accessible) or ENOENT/ENOTDIR,
1783/// it's "expected" (try next PATH entry); otherwise it's a real
1784/// failure worth surfacing.
1785pub fn isgooderr(e: i32, dir: &str) -> bool {
1786 // c:652
1787 // c:Src/exec.c:658-659 — `(e != EACCES || !access(dir, X_OK)) &&
1788 // e != ENOENT && e != ENOTDIR`. C's `access(dir, X_OK)` returns
1789 // 0 on success / -1 on failure. The previous Rust port used
1790 // `metadata().permissions().mode() & 0o111` which reports the
1791 // X bit even when the path doesn't exist as the EFFECTIVE caller
1792 // (root, ACLs, capabilities all flip access() vs raw mode).
1793 // `/no/such/dir` metadata() fails → returned false for
1794 // dir_x_ok, then `!false` = true, giving "good error" for a
1795 // nonexistent path. Use libc::access directly to match C exactly.
1796 let unmeta_dir = unmeta(dir);
1797 let cstr = std::ffi::CString::new(unmeta_dir.as_bytes()).unwrap_or_default();
1798 let access_ok = unsafe { libc::access(cstr.as_ptr(), libc::X_OK) } == 0;
1799 (e != libc::EACCES || access_ok) && e != libc::ENOENT && e != libc::ENOTDIR
1800}
1801
1802/// Port of `iscom()` from `Src/exec.c:962` — C decl `iscom(char *s)`.
1803///
1804/// C body:
1805/// ```c
1806/// struct stat statbuf;
1807/// char *us = unmeta(s);
1808/// return (access(us, X_OK) == 0 && stat(us, &statbuf) >= 0 &&
1809/// S_ISREG(statbuf.st_mode));
1810/// ```
1811///
1812/// True iff `s` names an executable regular file (X-perm + S_IFREG).
1813/// Used by the PATH-search loop in `findcmd` / `search_defpath` to
1814/// validate candidate paths before exec.
1815pub fn iscom(s: &str) -> bool {
1816 // c:962
1817 let us = unmeta(s); // c:965
1818 // c:967-968 — `access(us, X_OK) == 0 && stat(us, &statbuf) >= 0 && S_ISREG(...)`
1819 let cstr = match std::ffi::CString::new(us.as_str()) {
1820 Ok(c) => c,
1821 Err(_) => return false,
1822 };
1823 let x_ok = unsafe { libc::access(cstr.as_ptr(), libc::X_OK) } == 0;
1824 if !x_ok {
1825 return false;
1826 }
1827 let meta = match std::fs::metadata(&us) {
1828 Ok(m) => m,
1829 Err(_) => return false,
1830 };
1831 meta.file_type().is_file()
1832}
1833
1834/// Port of `isreallycom()` from `Src/exec.c:973` — C decl `isreallycom(Cmdnam cn)`.
1835///
1836/// Verify that a hashed/cached cmdnamtab entry still names a real
1837/// external command (X-perm + regular file). For HASHED entries
1838/// (`cn->u.cmd` carries the absolute path), test the path directly;
1839/// otherwise concatenate `name[0] + "/" + nam` and test that.
1840/// Used by `execcmd_exec` to drop stale cmdnamtab hits before they
1841/// turn into a failed `execve` syscall.
1842pub fn isreallycom(cn: &cmdnam) -> bool {
1843 // c:972
1844 let fullnam: String;
1845 if (cn.node.flags & crate::ported::zsh_h::HASHED) != 0 {
1846 // c:977-978 — `strcpy(fullnam, cn->u.cmd);`
1847 fullnam = cn.cmd.clone().unwrap_or_default();
1848 } else if cn.name.is_none() || cn.name.as_ref().unwrap().is_empty() {
1849 // c:979-980 — `if (!cn->u.name) return 0;`
1850 return false;
1851 } else {
1852 // c:982-984 — `strcpy + strcat("/") + strcat(nam)`
1853 let path0 = &cn.name.as_ref().unwrap()[0];
1854 fullnam = format!("{}/{}", path0, cn.node.nam);
1855 }
1856 iscom(&fullnam) // c:986
1857}
1858
1859/// Port of `isrelative()` from `Src/exec.c:996` — C decl `isrelative(char *s)`.
1860///
1861/// C body:
1862/// ```c
1863/// if (*s != '/') return 1;
1864/// for (; *s; s++)
1865/// if (*s == '.' && s[-1] == '/' &&
1866/// (s[1] == '/' || s[1] == '\0' ||
1867/// (s[1] == '.' && (s[2] == '/' || s[2] == '\0'))))
1868/// return 1;
1869/// return 0;
1870/// ```
1871///
1872/// True iff `s` either doesn't start with `/` OR contains a `./` or
1873/// `../` component anywhere. Used by `cd` resolution and PATH-cache
1874/// invalidation to detect non-canonical paths.
1875pub fn isrelative(s: &str) -> i32 {
1876 // c:996
1877 let bytes = s.as_bytes();
1878 if bytes.is_empty() || bytes[0] != b'/' {
1879 // c:998
1880 return 1; // c:999
1881 }
1882 // c:1000-1004 — walk for `./` or `../` components.
1883 for i in 1..bytes.len() {
1884 let c = bytes[i];
1885 let prev = bytes[i - 1];
1886 if c == b'.' && prev == b'/' {
1887 let next = bytes.get(i + 1).copied().unwrap_or(0);
1888 if next == b'/' || next == 0 {
1889 // c:1002
1890 return 1;
1891 }
1892 if next == b'.' {
1893 let next2 = bytes.get(i + 2).copied().unwrap_or(0);
1894 if next2 == b'/' || next2 == 0 {
1895 // c:1003
1896 return 1;
1897 }
1898 }
1899 }
1900 }
1901 0 // c:1005
1902}
1903
1904/// Port of `setunderscore()` from `Src/exec.c:2652` — C decl `setunderscore(char *str)`.
1905///
1906/// C body:
1907/// ```c
1908/// queue_signals();
1909/// if (str && *str) {
1910/// size_t l = strlen(str) + 1, nl = (l + 31) & ~31;
1911/// if (nl > underscorelen || (underscorelen - nl) > 64) {
1912/// zfree(zunderscore, underscorelen);
1913/// zunderscore = (char *) zalloc(underscorelen = nl);
1914/// }
1915/// strcpy(zunderscore, str);
1916/// underscoreused = l;
1917/// } else {
1918/// ... reset zunderscore = "" ...
1919/// }
1920/// unqueue_signals();
1921/// ```
1922///
1923/// Sets the `$_` global to the last argument of the most recent
1924/// command. Called from `execcmd_exec` (c:3936) per `last_status`
1925/// update; mirrored in zshrs by the fusevm `Op::Exec` handler.
1926pub fn setunderscore(str: &str) {
1927 // c:2652
1928 queue_signals(); // c:2654
1929 if !str.is_empty() {
1930 // c:2655 `if (str && *str)`
1931 // c:2656-2663 — copy str into zunderscore; track byte length in underscoreused.
1932 let mut zu = zunderscore.lock().unwrap();
1933 *zu = str.to_string();
1934 let nl = (str.len() + 1 + 31) & !31; // c:2656
1935 underscorelen.store(nl, Ordering::Relaxed); // c:2660
1936 underscoreused.store((str.len() + 1) as i32, Ordering::Relaxed);
1937 // c:2663
1938 } else {
1939 // c:2664
1940 let mut zu = zunderscore.lock().unwrap();
1941 zu.clear(); // c:2669 `*zunderscore = '\0';`
1942 underscoreused.store(1, Ordering::Relaxed); // c:2670
1943 }
1944 unqueue_signals(); // c:2672
1945}
1946
1947/// Port of `mpipe()` from `Src/exec.c:5160` — C decl `mpipe(int *pp)`.
1948///
1949/// C body:
1950/// ```c
1951/// if (pipe(pp) < 0) {
1952/// zerr("pipe failed: %e", errno);
1953/// return -1;
1954/// }
1955/// pp[0] = movefd(pp[0]);
1956/// pp[1] = movefd(pp[1]);
1957/// return 0;
1958/// ```
1959///
1960/// libc `pipe(2)` wrapper that pushes both ends out of the reserved-
1961/// fd range via `movefd`. Used by `getpipe` / `getproc` /
1962/// `spawnpipes` for process substitution and pipeline wiring.
1963pub fn mpipe(pp: &mut [i32; 2]) -> i32 {
1964 // c:5160
1965 let mut fds: [libc::c_int; 2] = [-1; 2];
1966 if unsafe { libc::pipe(fds.as_mut_ptr()) } < 0 {
1967 // c:5162
1968 zerr(&format!(
1969 // c:5163
1970 "pipe failed: {}",
1971 std::io::Error::last_os_error()
1972 ));
1973 return -1; // c:5164
1974 }
1975 pp[0] = movefd(fds[0]); // c:5166
1976 pp[1] = movefd(fds[1]); // c:5167
1977 0 // c:5168
1978}
1979
1980/// Port of `static const char *const ANONYMOUS_FUNCTION_NAME = "(anon)";`
1981/// from `Src/exec.c:5289`. Anonymous-function name marker used by
1982/// `is_anonymous_function_name`, `execfuncdef`, and `doshfunc` for
1983/// `() { ... }` anonymous function dispatch.
1984pub const ANONYMOUS_FUNCTION_NAME: &str = "(anon)";
1985
1986/// Port of `is_anonymous_function_name()` from `Src/exec.c:5300` — C decl `is_anonymous_function_name(const char *name)`.
1987///
1988/// C body:
1989/// ```c
1990/// return !strcmp(name, ANONYMOUS_FUNCTION_NAME);
1991/// ```
1992///
1993/// True iff the name equals the `"(anon)"` sentinel. Used by zprof
1994/// reporting and `whence -v` to skip / annotate anonymous functions.
1995pub fn is_anonymous_function_name(name: &str) -> i32 {
1996 // c:5300
1997 if name == ANONYMOUS_FUNCTION_NAME {
1998 // c:5302
1999 1
2000 } else {
2001 0
2002 }
2003}
2004
2005/// Port of `execsave()` from `Src/exec.c:6438` — C decl `execsave(void)`.
2006///
2007/// C body:
2008/// ```c
2009/// struct execstack *es = (struct execstack *) zalloc(sizeof(struct execstack));
2010/// es->list_pipe_pid = list_pipe_pid;
2011/// es->nowait = nowait;
2012/// es->pline_level = pline_level;
2013/// es->list_pipe_child = list_pipe_child;
2014/// es->list_pipe_job = list_pipe_job;
2015/// strcpy(es->list_pipe_text, list_pipe_text);
2016/// es->lastval = lastval;
2017/// es->noeval = noeval;
2018/// es->badcshglob = badcshglob;
2019/// es->cmdoutpid = cmdoutpid;
2020/// es->cmdoutval = cmdoutval;
2021/// es->use_cmdoutval = use_cmdoutval;
2022/// es->procsubstpid = procsubstpid;
2023/// es->trap_return = trap_return;
2024/// es->trap_state = trap_state;
2025/// es->trapisfunc = trapisfunc;
2026/// es->traplocallevel = traplocallevel;
2027/// es->noerrs = noerrs;
2028/// es->this_noerrexit = this_noerrexit;
2029/// es->underscore = ztrdup(zunderscore);
2030/// es->next = exstack;
2031/// exstack = es;
2032/// noerrs = cmdoutpid = 0;
2033/// ```
2034///
2035/// Snapshot every transient exec-context global onto the `exstack`
2036/// linked list so a signal-handler / trap-firing nested eval can
2037/// scribble freely; `execrestore` pops the frame back. Called by
2038/// `dotrap` (signals.c) and the trap-firing entry in `execlist`.
2039pub fn execsave() {
2040 // c:6438
2041 // c:6442 — `es = zalloc(sizeof(execstack));`
2042 let mut es = Box::new(execstack {
2043 // c:6442
2044 next: None,
2045 list_pipe_pid: list_pipe_pid.load(Ordering::Relaxed), // c:6443
2046 nowait: nowait.load(Ordering::Relaxed), // c:6444
2047 pline_level: pline_level.load(Ordering::Relaxed), // c:6445
2048 list_pipe_child: list_pipe_child.load(Ordering::Relaxed), // c:6446
2049 list_pipe_job: list_pipe_job.load(Ordering::Relaxed), // c:6447
2050 list_pipe_text: {
2051 // c:6448 — `strcpy(es->list_pipe_text, list_pipe_text);`
2052 let mut buf = [0u8; JOBTEXTSIZE];
2053 if let Ok(s) = LIST_PIPE_TEXT.lock() {
2054 let bytes = s.as_bytes();
2055 let n = bytes.len().min(JOBTEXTSIZE - 1);
2056 buf[..n].copy_from_slice(&bytes[..n]);
2057 }
2058 buf
2059 },
2060 lastval: LASTVAL.load(Ordering::Relaxed), // c:6449
2061 // c:6450 — `es->noeval = noeval;`. Snapshot math.c's
2062 // `int noeval` (the parse-only side-effect-skip counter)
2063 // via math.rs's pub accessor.
2064 noeval: crate::ported::math::m_noeval(),
2065 // c:6451 — `es->badcshglob = badcshglob;`. Snapshot the
2066 // csh-glob diagnostic counter (glob.c:103 / glob.rs
2067 // BADCSHGLOB) so nested eval / trap dispatch doesn't disturb
2068 // the outer command's per-line accounting.
2069 badcshglob: crate::ported::glob::BADCSHGLOB.load(Ordering::Relaxed), // c:6451
2070 cmdoutpid: cmdoutpid.load(Ordering::Relaxed), // c:6452
2071 cmdoutval: cmdoutval.load(Ordering::Relaxed), // c:6453
2072 use_cmdoutval: use_cmdoutval.load(Ordering::Relaxed), // c:6454
2073 procsubstpid: procsubstpid.load(Ordering::Relaxed), // c:6455
2074 trap_return: TRAP_RETURN.load(Ordering::Relaxed), // c:6456
2075 trap_state: TRAP_STATE.load(Ordering::Relaxed), // c:6457
2076 trapisfunc: trapisfunc.load(Ordering::Relaxed), // c:6458
2077 traplocallevel: traplocallevel.load(Ordering::Relaxed), // c:6459
2078 noerrs: *crate::ported::utils::noerrs_lock().lock().unwrap(), // c:6460
2079 this_noerrexit: this_noerrexit.load(Ordering::Relaxed), // c:6461
2080 // c:6462 — `es->underscore = ztrdup(zunderscore);`
2081 underscore: Some(zunderscore.lock().unwrap().clone()),
2082 });
2083 // c:6463-6464 — `es->next = exstack; exstack = es;`
2084 let mut head = exstack.lock().unwrap();
2085 es.next = head.take();
2086 *head = Some(es);
2087 // c:6465 — `noerrs = cmdoutpid = 0;`
2088 *crate::ported::utils::noerrs_lock().lock().unwrap() = 0;
2089 cmdoutpid.store(0, Ordering::Relaxed);
2090}
2091
2092/// Port of `execrestore()` from `Src/exec.c:6470` — C decl `execrestore(void)`.
2093///
2094/// C body:
2095/// ```c
2096/// struct execstack *en = exstack;
2097/// DPUTS(!exstack, "BUG: execrestore() without execsave()");
2098/// queue_signals();
2099/// exstack = exstack->next;
2100/// list_pipe_pid = en->list_pipe_pid;
2101/// nowait = en->nowait;
2102/// pline_level = en->pline_level;
2103/// list_pipe_child = en->list_pipe_child;
2104/// list_pipe_job = en->list_pipe_job;
2105/// strcpy(list_pipe_text, en->list_pipe_text);
2106/// lastval = en->lastval;
2107/// noeval = en->noeval;
2108/// badcshglob = en->badcshglob;
2109/// cmdoutpid = en->cmdoutpid;
2110/// cmdoutval = en->cmdoutval;
2111/// use_cmdoutval = en->use_cmdoutval;
2112/// procsubstpid = en->procsubstpid;
2113/// trap_return = en->trap_return;
2114/// trap_state = en->trap_state;
2115/// trapisfunc = en->trapisfunc;
2116/// traplocallevel = en->traplocallevel;
2117/// noerrs = en->noerrs;
2118/// this_noerrexit = en->this_noerrexit;
2119/// setunderscore(en->underscore);
2120/// zsfree(en->underscore);
2121/// free(en);
2122/// unqueue_signals();
2123/// ```
2124///
2125/// Pop the top `execstack` frame and restore every transient
2126/// exec-context global. Inverse of `execsave`.
2127pub fn execrestore() {
2128 // c:6470
2129 let mut head = exstack.lock().unwrap();
2130 let en = match head.take() {
2131 // c:6472 + c:6477
2132 Some(en) => en,
2133 None => {
2134 // c:6474 — DPUTS(!exstack, "BUG: execrestore() without execsave()")
2135 crate::DPUTS!(true, "BUG: execrestore() without execsave()");
2136 return;
2137 }
2138 };
2139 queue_signals(); // c:6476
2140 *head = en.next; // c:6477
2141 drop(head); // release lock before scalar restores
2142
2143 list_pipe_pid.store(en.list_pipe_pid, Ordering::Relaxed); // c:6479
2144 nowait.store(en.nowait, Ordering::Relaxed); // c:6480
2145 pline_level.store(en.pline_level, Ordering::Relaxed); // c:6481
2146 list_pipe_child.store(en.list_pipe_child, Ordering::Relaxed); // c:6482
2147 list_pipe_job.store(en.list_pipe_job, Ordering::Relaxed); // c:6483
2148 // c:6484 — `strcpy(list_pipe_text, en->list_pipe_text);`.
2149 if let Ok(mut s) = LIST_PIPE_TEXT.lock() {
2150 let nul = en
2151 .list_pipe_text
2152 .iter()
2153 .position(|&b| b == 0)
2154 .unwrap_or(JOBTEXTSIZE);
2155 *s = String::from_utf8_lossy(&en.list_pipe_text[..nul]).into_owned();
2156 }
2157 LASTVAL.store(en.lastval, Ordering::Relaxed); // c:6485
2158 // c:6486 — `noeval = en->noeval;`. Restore math.c's noeval
2159 // counter from the saved frame.
2160 crate::ported::math::m_noeval_set(en.noeval);
2161 // c:6487 — `badcshglob = en->badcshglob;`. Restore the csh-glob
2162 // diagnostic counter saved on entry.
2163 crate::ported::glob::BADCSHGLOB.store(en.badcshglob, Ordering::Relaxed);
2164 cmdoutpid.store(en.cmdoutpid, Ordering::Relaxed); // c:6488
2165 cmdoutval.store(en.cmdoutval, Ordering::Relaxed); // c:6489
2166 use_cmdoutval.store(en.use_cmdoutval, Ordering::Relaxed); // c:6490
2167 procsubstpid.store(en.procsubstpid, Ordering::Relaxed); // c:6491
2168 TRAP_RETURN.store(en.trap_return, Ordering::Relaxed); // c:6492
2169 TRAP_STATE.store(en.trap_state, Ordering::Relaxed); // c:6493
2170 trapisfunc.store(en.trapisfunc, Ordering::Relaxed); // c:6494
2171 traplocallevel.store(en.traplocallevel, Ordering::Relaxed); // c:6495
2172 *crate::ported::utils::noerrs_lock().lock().unwrap() = en.noerrs; // c:6496
2173 this_noerrexit.store(en.this_noerrexit, Ordering::Relaxed); // c:6497
2174 // c:6498-6499 — `setunderscore(en->underscore); zsfree(en->underscore);`
2175 if let Some(ref u) = en.underscore {
2176 setunderscore(u); // c:6498
2177 }
2178 // c:6500 — `free(en);` — handled by Box drop when `en` falls out of scope.
2179 unqueue_signals(); // c:6502
2180}
2181
2182/// Port of `execstring()` from `Src/exec.c:1228` — C decl `execstring(char *s, int dont_change_job, int exiting, char *context)`.
2183///
2184/// C body:
2185/// ```c
2186/// Eprog prog;
2187/// pushheap();
2188/// if (isset(VERBOSE)) {
2189/// zputs(s, stderr);
2190/// fputc('\n', stderr);
2191/// fflush(stderr);
2192/// }
2193/// if ((prog = parse_string(s, 0)))
2194/// execode(prog, dont_change_job, exiting, context);
2195/// popheap();
2196/// ```
2197///
2198/// Public entry — execute an arbitrary string as a zsh command list.
2199/// Called by `eval`, `.`/`source`, `trap` action firing, autoload
2200/// body executors, command substitution body runners.
2201///
2202/// =================== WARNING — DIVERGENCE ====================
2203/// The C path is `parse_string` → `execode` → `execlist` (wordcode
2204/// walker). zshrs replaces `execode/execlist` with the fusevm
2205/// bytecode VM at `crate::vm_helper::ShellExecutor::execute_script_zsh_pipeline`.
2206/// Faithful port: VERBOSE banner + pushheap/popheap intact; the
2207/// parse+execute chain delegates to the fusevm entry. When `execlist`
2208/// lands as a strict 1:1 port, swap the delegate for the canonical
2209/// chain.
2210/// =============================================================
2211pub fn execstring(s: &str, _dont_change_job: i32, _exiting: i32, _context: &str) {
2212 // c:1228
2213 pushheap(); // c:1232
2214 // c:1233-1237 — VERBOSE banner.
2215 if isset(VERBOSE) {
2216 // c:1233
2217 let mut stderr = std::io::stderr().lock();
2218 use std::io::Write;
2219 let _ = stderr.write_all(s.as_bytes()); // c:1234 zputs(s, stderr)
2220 let _ = stderr.write_all(b"\n"); // c:1235
2221 let _ = stderr.flush(); // c:1236
2222 }
2223 // c:1238-1239 — parse + execode. zshrs delegates the parse+VM
2224 // chain to the fusevm pipeline via the exec_hooks fn-ptr
2225 // installed by fusevm_bridge at startup. Direct
2226 // `with_executor` / ShellExecutor reach-in from src/ported/ is
2227 // forbidden — see memory feedback_no_exec_script_from_ported.
2228 // c:1277 — `parse_string(s, 0)`: reset_lineno is 0, so the string is
2229 // parsed from the line being executed, not from 1. A `${| … }` body
2230 // re-run on the glob path left `lineno` at 1 and the NOMATCH error
2231 // for its own word said `zsh:1:`.
2232 let running = crate::ported::lex::lineno();
2233 if crate::fusevm_bridge::try_with_executor(|exec| exec.execute_string_at_lineno(s, running))
2234 .is_none()
2235 {
2236 let _ = crate::ported::exec::execute_script_zsh_pipeline(s);
2237 }
2238 popheap(); // c:1240
2239}
2240
2241// =====================================================================
2242// Function-wrapper chain — `Src/module.c:570` `FuncWrap wrappers`, as
2243// walked by `runshfunc` (`Src/exec.c:6166`).
2244// =====================================================================
2245
2246/// Which module-registered wrapper a [`BodyWrap`] node stands for.
2247///
2248/// C reaches the handler through a fn pointer stored in the node
2249/// (`WrapFunc handler` — `Src/zsh.h:1365`); this port dispatches on the
2250/// discriminant instead, because the two handlers take a body DELEGATE
2251/// that a `WrapFunc` pointer cannot carry (see [`BodyWrap`]).
2252#[derive(Clone, Copy)]
2253enum WrapKind {
2254 /// `WRAPDEF(comp_wrapper)` — `Src/Zle/complete.c:1694-1695`.
2255 Comp,
2256 /// `WRAPDEF(wrap_private)` — `Src/Modules/param_private.c:541-542`.
2257 Private,
2258 /// `WRAPDEF(zprof_wrapper)` — `Src/Modules/zprof.c:318-320`.
2259 Zprof,
2260 /// `WRAPDEF(ksh93_wrapper)` — `Src/Modules/ksh93.c:230-232`.
2261 Ksh93,
2262}
2263
2264/// One node of the `wrappers` linked list C keeps at `Src/module.c:570`,
2265/// tail-appended by `addwrapper` (`Src/module.c:592-597`) from a module's
2266/// `boot_`.
2267///
2268/// Why the live chain is this table and not `module::WRAPPERS`: C's
2269/// `struct funcwrap` (`Src/zsh.h:1362-1367`) stores an
2270/// `int (*handler)(Eprog prog, FuncWrap w, char *name)`, and each handler
2271/// re-enters the chain on its own with `runshfunc(prog, w->next, name)`
2272/// (`Src/Zle/complete.c:1591`, `Src/Modules/param_private.c:556`) because
2273/// in C the function body is an `Eprog` anyone can walk. A zshrs function
2274/// body is not — it is `doshfunc`'s caller-supplied `body_runner` closure
2275/// (fusevm chunk runner, Rust compsys port, or plugin override), so the
2276/// port's handlers take the continuation as an explicit delegate that a
2277/// plain fn pointer has nowhere to hold. `zsh_h::funcwrap` and
2278/// `module::WRAPPERS` keep C's pointer shape so the `addwrapper` /
2279/// `deletewrapper` ports stay 1:1; this table is the same list expressed
2280/// in the shape `runshfunc` can actually call.
2281struct BodyWrap {
2282 /// The module whose `boot_` calls `addwrapper(m, wrapper)`. C reads it
2283 /// back through `wrap->module` for the refcount at c:6178/6180 and the
2284 /// deferred `unload_module` at c:6184.
2285 module: &'static str,
2286 /// Which handler this node holds.
2287 kind: WrapKind,
2288}
2289
2290/// The chain in `addwrapper` tail-append order — module boot order, so
2291/// the wrapper of the module booted first ends up OUTERMOST.
2292static BODY_WRAPPERS: &[BodyWrap] = &[
2293 // `Src/Modules/ksh93.c:230-232` — `static struct funcwrap wrapper[] =
2294 // { WRAPDEF(ksh93_wrapper) };` — installed by c:260
2295 // `return addwrapper(m, wrapper);` in `zsh/ksh93`'s `boot_`, removed
2296 // by c:269 `deletewrapper(m, wrapper);` in `cleanup_`. zsh/ksh93 is a
2297 // `load=yes` module (config.modules) that boots ahead of
2298 // zsh/param/private and zsh/zprof, so its node comes first. The
2299 // handler never calls the body — it creates the `.sh.*` locals and
2300 // returns 1 (c:227), letting the walk continue.
2301 BodyWrap {
2302 module: "zsh/ksh93",
2303 kind: WrapKind::Ksh93,
2304 },
2305 // `Src/Zle/complete.c:1694-1695` — `static struct funcwrap wrapper[] =
2306 // { WRAPDEF(comp_wrapper) };` — installed by c:1767
2307 // `return addwrapper(m, wrapper);` in `zsh/complete`'s `boot_`.
2308 BodyWrap {
2309 module: "zsh/complete",
2310 kind: WrapKind::Comp,
2311 },
2312 // `Src/Modules/param_private.c:541-542` — `static struct funcwrap
2313 // wrapper[] = { WRAPDEF(wrap_private) };` — installed by c:712
2314 // `return addwrapper(m, wrapper);` in `zsh/param/private`'s `boot_`.
2315 BodyWrap {
2316 module: "zsh/param/private",
2317 kind: WrapKind::Private,
2318 },
2319 // `Src/Modules/zprof.c:318-320` — `static struct funcwrap wrapper[] =
2320 // { WRAPDEF(zprof_wrapper) };` — installed by c:362
2321 // `return addwrapper(m, wrapper);` in `zsh/zprof`'s `boot_`, removed
2322 // by c:371 `deletewrapper(m, wrapper);` in `cleanup_`. `zsh/zprof` is
2323 // never linked in at startup, so it always boots after the two nodes
2324 // above and `addwrapper`'s tail-append (`Src/module.c:591-595`) puts
2325 // it LAST — innermost, closest to the body, which is what makes its
2326 // measured window the function itself.
2327 BodyWrap {
2328 module: "zsh/zprof",
2329 kind: WrapKind::Zprof,
2330 },
2331];
2332
2333/// Port of `runshfunc()` from `Src/exec.c:6166` — C decl `runshfunc(Eprog prog, FuncWrap wrap, char *name)`.
2334/// the wrapper-chain walk that stands between
2335/// `doshfunc` and a function body. C:
2336///
2337/// ```c
2338/// queue_signals();
2339/// ou = zalloc(ouu = underscoreused);
2340/// if (ou) memcpy(ou, zunderscore, underscoreused);
2341/// while (wrap) { // wrapper chain
2342/// wrap->module->wrapper++;
2343/// cont = wrap->handler(prog, wrap->next, name);
2344/// wrap->module->wrapper--;
2345/// if (!wrap->module->wrapper && (wrap->module->node.flags & MOD_UNLOAD))
2346/// unload_module(wrap->module);
2347/// if (!cont) { // wrapper handled it
2348/// if (ou) zfree(ou, ouu);
2349/// unqueue_signals();
2350/// return;
2351/// }
2352/// wrap = wrap->next;
2353/// }
2354/// startparamscope();
2355/// execode(prog, 1, 0, "shfunc");
2356/// if (ou) { setunderscore(ou); zfree(ou, ouu); }
2357/// endparamscope();
2358/// unqueue_signals();
2359/// ```
2360///
2361/// **RUST-ONLY ADAPTATION — what this port does and does NOT own.** When
2362/// zshrs replaced C's wordcode walk with a body delegate, everything in
2363/// the listing above except the chain walk moved into `doshfunc`, which is
2364/// the only caller:
2365///
2366/// | C line | Where it lives in zshrs |
2367/// |---|---|
2368/// | c:6171 / c:6202 `queue_signals` / `unqueue_signals` | `doshfunc`'s own outer pair (c:5835 / c:6128) already brackets this window; C's inner pair is a nested refcount bump inside it |
2369/// | c:6173-6175 / c:6196-6198 `$_` save / `setunderscore` | `doshfunc`, around this call (`saved_zunderscore`) |
2370/// | c:6194 `startparamscope()` | `doshfunc` — `inc_locallevel()` before the funcstack push |
2371/// | c:6200 `endparamscope()` | `doshfunc`'s epilogue |
2372/// | c:6195 `execode(prog, 1, 0, "shfunc")` | the `body` delegate |
2373///
2374/// Duplicating any of them here would double-apply it, so the body below
2375/// is c:6177-6193 plus the c:6195 body call, and nothing else.
2376///
2377/// WARNING: param names don't match C — Rust=(_prog, wrap, name, body) vs
2378/// C=(prog, wrap, name). `wrap` is an INDEX into [`BODY_WRAPPERS`] rather
2379/// than a `FuncWrap` pointer (a `Vec`-shaped chain has no `->next` to
2380/// hand out); `wrap = 0` is C's `wrappers` head and `wrap = i + 1` is the
2381/// `wrap->next` a handler re-enters with. `_prog` is unused because the
2382/// body arrives as `body`, and the return value is the body's exit status
2383/// (C returns void and communicates through `lastval`).
2384pub fn runshfunc(
2385 _prog: Option<&eprog>, // c:6166
2386 wrap: usize, // c:6166 (FuncWrap wrap — see WARNING)
2387 name: &str, // c:6166
2388 body: &mut dyn FnMut() -> i32,
2389) -> i32 {
2390 // c:6166
2391 let mut w_idx = wrap;
2392 while w_idx < BODY_WRAPPERS.len() {
2393 // c:6177 — `while (wrap)`
2394 let w = &BODY_WRAPPERS[w_idx];
2395
2396 // Chain membership. C asks nothing here because a wrapper is in
2397 // the list only while its module has it registered; this port
2398 // evaluates the equivalent condition per node.
2399 let armed = match w.kind {
2400 // zshrs statically links the completion system — `compadd` /
2401 // `compset` are always present and `zsh/complete` is never
2402 // dlopen'd — so there is no boot event to key on. The
2403 // wrapper's own first statement is the gate that matters and
2404 // it is exact: `if (incompfunc != 1) return 1;`
2405 // (`Src/Zle/complete.c:1558-1559`). `incompfunc` is 1 only
2406 // between `Src/Zle/compcore.c:815` and c:841, i.e. strictly
2407 // inside `callcompfunc`, which is the only thing that can run
2408 // a completion function at all. Hoisting the test here keeps
2409 // an ordinary function call at one relaxed load with no chain
2410 // node entered.
2411 WrapKind::Comp => crate::ported::zle::complete::INCOMPFUNC.load(Ordering::Relaxed) == 1,
2412 // Module BOOT STATE (`MOD_INIT_B` — the bit `zmodload -e`
2413 // reads and `load_module` sets after `do_boot_module`,
2414 // `Src/module.c:2317`), which is exactly C's condition for
2415 // `wrap_private` being in the list: the `private` dispatch
2416 // runs `require_module` via the `Src/exec.c:2710` autofeature
2417 // path, which boots the module. Deliberately NOT
2418 // `is_loaded()` — default-registered static modules carry
2419 // `MOD_LINKED` from startup, which would arm the wrapper
2420 // before any `private` was ever used.
2421 WrapKind::Private => crate::ported::module::MODULESTAB
2422 .lock()
2423 .map(|t| {
2424 t.modules
2425 .get(w.module)
2426 .map(|m| (m.node.flags & crate::ported::zsh_h::MOD_INIT_B) != 0)
2427 .unwrap_or(false)
2428 })
2429 .unwrap_or(false),
2430 // Genuine `addwrapper` membership: `zsh/zprof`'s `boot_`
2431 // calls `addwrapper(m, wrapper)` (`Src/Modules/zprof.c:362`)
2432 // and its `cleanup_` calls `deletewrapper` (c:371), and both
2433 // mirror the list into `module::WRAPPERS_ADDED`. So this is
2434 // C's `while (wrap)` test verbatim — "is this node in the
2435 // `wrappers` list" — costing one relaxed load, with no lock
2436 // and no allocation when `zsh/zprof` was never loaded.
2437 WrapKind::Zprof => {
2438 (crate::ported::module::WRAPPERS_ADDED.load(Ordering::Relaxed)
2439 & crate::ported::module::WRAPPER_BIT_ZPROF)
2440 != 0
2441 }
2442 // Same `addwrapper` membership test as zprof: zsh/ksh93's boot_
2443 // (c:260) and cleanup_ (c:269) mirror it into WRAPPERS_ADDED.
2444 WrapKind::Ksh93 => {
2445 (crate::ported::module::WRAPPERS_ADDED.load(Ordering::Relaxed)
2446 & crate::ported::module::WRAPPER_BIT_KSH93)
2447 != 0
2448 }
2449 };
2450 if !armed {
2451 // Not in C's `wrappers` list right now — no node to step over.
2452 w_idx += 1;
2453 continue;
2454 }
2455
2456 // c:6178 — `wrap->module->wrapper++;` — hold the module open so a
2457 // recursive unload during the handler defers until we return.
2458 if let Ok(mut tab) = crate::ported::module::MODULESTAB.lock() {
2459 if let Some(m) = tab.modules.get_mut(w.module) {
2460 m.wrapper += 1;
2461 }
2462 }
2463
2464 // c:6179 — `cont = wrap->handler(prog, wrap->next, name);`. The
2465 // closure IS `wrap->next` made callable: the rest of the chain,
2466 // then the body — which is what each handler's own
2467 // `runshfunc(prog, w, name)` reaches in C.
2468 let mut status = 0;
2469 let cont = {
2470 let mut run_next = || runshfunc(_prog, w_idx + 1, name, &mut *body);
2471 match w.kind {
2472 WrapKind::Comp => crate::ported::zle::complete::comp_wrapper(
2473 std::ptr::null(),
2474 std::ptr::null(),
2475 name,
2476 || status = run_next(), // complete.c:1591
2477 ),
2478 WrapKind::Private => crate::ported::modules::param_private::wrap_private(
2479 std::ptr::null(),
2480 std::ptr::null(),
2481 std::ptr::null_mut(),
2482 || status = run_next(), // param_private.c:556
2483 ),
2484 WrapKind::Zprof => crate::ported::modules::zprof::zprof_wrapper(
2485 std::ptr::null(),
2486 std::ptr::null(),
2487 name,
2488 || status = run_next(), // zprof.c:285
2489 ),
2490 // ksh93.c:143-227 never re-enters the chain: it sets up the
2491 // `.sh.*` locals and returns 1, so `run_next` stays unused
2492 // and the walk continues at c:6192.
2493 WrapKind::Ksh93 => {
2494 let cname = std::ffi::CString::new(name).unwrap_or_default();
2495 crate::ported::modules::ksh93::ksh93_wrapper(
2496 std::ptr::null(),
2497 std::ptr::null(),
2498 cname.as_ptr() as *mut libc::c_char,
2499 )
2500 }
2501 }
2502 };
2503
2504 // c:6180 — `wrap->module->wrapper--;`
2505 // c:6182-6184 — `if (!wrap->module->wrapper && (flags & MOD_UNLOAD))
2506 // unload_module(wrap->module);`
2507 let should_unload = {
2508 if let Ok(mut tab) = crate::ported::module::MODULESTAB.lock() {
2509 if let Some(m) = tab.modules.get_mut(w.module) {
2510 m.wrapper -= 1;
2511 m.wrapper == 0 && (m.node.flags & crate::ported::zsh_h::MOD_UNLOAD) != 0
2512 } else {
2513 false
2514 }
2515 } else {
2516 false
2517 }
2518 };
2519 if should_unload {
2520 if let Ok(mut tab) = crate::ported::module::MODULESTAB.lock() {
2521 let _ = tab.unload_module(w.module); // c:6184
2522 }
2523 }
2524
2525 if cont == 0 {
2526 // c:6186 — the wrapper claimed the call and has already run
2527 // the body through the delegate.
2528 return status; // c:6190
2529 }
2530 w_idx += 1; // c:6192
2531 }
2532 // c:6195 — `execode(prog, 1, 0, "shfunc");`. execode runs execlist,
2533 // whose loop is `while (wc_code(code) == WC_LIST && !breaks && !retflag
2534 // && !errflag)` (c:1390), and doshfunc saved `breaks` without clearing
2535 // it (c:5859): a function entered while a `break` is still unwinding —
2536 // `repeat 3 do f ${ for …; do break 2; done }; done` — runs no list and
2537 // leaves lastval alone (D10nofork.ztst "break N propagates").
2538 if crate::ported::builtin::BREAKS.load(Ordering::Relaxed) != 0 {
2539 return crate::ported::builtin::LASTVAL.load(Ordering::Relaxed);
2540 }
2541 body()
2542}
2543
2544/// Port of `sticky_emulation_dup()` from `Src/exec.c:5501` — C decl `sticky_emulation_dup(Emulation_options src, int useheap)`.
2545///
2546/// C body (`useheap` selects between heap-arena and permanent zalloc;
2547/// Rust collapses both into owned `Box` clones):
2548/// ```c
2549/// Emulation_options newsticky = useheap ?
2550/// hcalloc(sizeof(*src)) : zshcalloc(sizeof(*src));
2551/// newsticky->emulation = src->emulation;
2552/// if (src->n_on_opts) {
2553/// size_t sz = src->n_on_opts * sizeof(*src->on_opts);
2554/// newsticky->n_on_opts = src->n_on_opts;
2555/// newsticky->on_opts = useheap ? zhalloc(sz) : zalloc(sz);
2556/// memcpy(newsticky->on_opts, src->on_opts, sz);
2557/// }
2558/// if (src->n_off_opts) {
2559/// size_t sz = src->n_off_opts * sizeof(*src->off_opts);
2560/// newsticky->n_off_opts = src->n_off_opts;
2561/// newsticky->off_opts = useheap ? zhalloc(sz) : zalloc(sz);
2562/// memcpy(newsticky->off_opts, src->off_opts, sz);
2563/// }
2564/// return newsticky;
2565/// ```
2566///
2567/// Deep-clone a sticky emulation struct. Used by `shfunc_set_sticky`
2568/// at function-def time to snapshot the pending `sticky` global so
2569/// the function carries its own immutable copy.
2570pub fn sticky_emulation_dup(src: &emulation_options, _useheap: i32) -> Emulation_options {
2571 // c:5501
2572 // c:5503-5505 — `newsticky = hcalloc/zshcalloc; newsticky->emulation = src->emulation;`
2573 let mut newsticky = Box::new(emulation_options {
2574 emulation: src.emulation, // c:5505
2575 n_on_opts: 0,
2576 n_off_opts: 0,
2577 on_opts: Vec::new(),
2578 off_opts: Vec::new(),
2579 });
2580 // c:5506-5511 — copy on_opts.
2581 if src.n_on_opts != 0 {
2582 // c:5506
2583 newsticky.n_on_opts = src.n_on_opts; // c:5508
2584 newsticky.on_opts = src.on_opts.clone(); // c:5510 memcpy
2585 }
2586 // c:5512-5517 — copy off_opts.
2587 if src.n_off_opts != 0 {
2588 // c:5512
2589 newsticky.n_off_opts = src.n_off_opts; // c:5514
2590 newsticky.off_opts = src.off_opts.clone(); // c:5516 memcpy
2591 }
2592 newsticky // c:5519
2593}
2594
2595/// Port of `sticky_emulation_differs()` from `Src/exec.c:5770` — C decl `int sticky_emulation_differs(Emulation_options sticky2)`.
2596///
2597/// C body:
2598/// ```c
2599/// /* If no new sticky emulation, not a different emulation */
2600/// if (!sticky2)
2601/// return 0;
2602/// /* If no current sticky emulation, different */
2603/// if (!sticky)
2604/// return 1;
2605/// /* If basic emulation different, different */
2606/// if (sticky->emulation != sticky2->emulation)
2607/// return 1;
2608/// /* If differing numbers of options, different */
2609/// if (sticky->n_on_opts != sticky2->n_on_opts ||
2610/// sticky->n_off_opts != sticky2->n_off_opts)
2611/// return 1;
2612/// /* If different options turned on, different */
2613/// if (sticky->n_on_opts &&
2614/// memcmp(sticky->on_opts, sticky2->on_opts,
2615/// sticky->n_on_opts * sizeof(*sticky->on_opts)) != 0)
2616/// return 1;
2617/// /* If different options turned on, different */
2618/// if (sticky->n_off_opts &&
2619/// memcmp(sticky->off_opts, sticky2->off_opts,
2620/// sticky->n_off_opts * sizeof(*sticky->off_opts)) != 0)
2621/// return 1;
2622/// return 0;
2623/// ```
2624pub fn sticky_emulation_differs(sticky2: Option<&emulation_options>) -> i32 {
2625 // c:5829
2626 let Some(sticky2) = sticky2 else {
2627 return 0; // c:5832-5833
2628 };
2629 let guard = sticky.lock().unwrap_or_else(|e| e.into_inner());
2630 let Some(ref cur) = *guard else {
2631 return 1; // c:5835-5836
2632 };
2633 if cur.emulation != sticky2.emulation {
2634 return 1; // c:5838-5839
2635 }
2636 if cur.n_on_opts != sticky2.n_on_opts || cur.n_off_opts != sticky2.n_off_opts {
2637 return 1; // c:5841-5843
2638 }
2639 if cur.n_on_opts != 0 && cur.on_opts != sticky2.on_opts {
2640 return 1; // c:5850-5853
2641 }
2642 if cur.n_off_opts != 0 && cur.off_opts != sticky2.off_opts {
2643 return 1; // c:5855-5858
2644 }
2645 0 // c:5859
2646}
2647
2648/// Port of `shfunc_set_sticky()` from `Src/exec.c:5527` — C decl `shfunc_set_sticky(Shfunc shf)`.
2649///
2650/// C body:
2651/// ```c
2652/// if (sticky)
2653/// shf->sticky = sticky_emulation_dup(sticky, 0);
2654/// else
2655/// shf->sticky = NULL;
2656/// ```
2657///
2658/// Stamp the function with the current pending sticky-emulation
2659/// snapshot (deep-copy via `sticky_emulation_dup`), or clear it.
2660pub fn shfunc_set_sticky(shf: &mut shfunc) {
2661 // c:5527
2662 let sticky_guard = sticky.lock().unwrap();
2663 if let Some(ref s) = *sticky_guard {
2664 // c:5529
2665 shf.sticky = Some(sticky_emulation_dup(s, 0)); // c:5530
2666 } else {
2667 // c:5531
2668 shf.sticky = None; // c:5532
2669 }
2670}
2671
2672/// Port of `search_defpath()` from `Src/exec.c:691` — C decl `search_defpath(char *cmd, char *pbuf, int plen)`.
2673///
2674/// Walk DEFAULT_PATH for an executable `<dir>/<cmd>` regular file.
2675/// Used by `command -p` to bypass the user's `$PATH` and search the
2676/// system default (`/bin:/usr/bin:...`).
2677pub fn search_defpath(cmd: &str, plen: usize) -> Option<String> {
2678 // c:691
2679 // c:695 — `for (ps = DEFAULT_PATH; ps; ps = pe ? pe+1 : NULL)`.
2680 for ps in DEFAULT_PATH.split(':') {
2681 // c:695
2682 // c:697 — `if (*ps == '/')`.
2683 if !ps.starts_with('/') {
2684 continue;
2685 }
2686 // c:700-707 — PATH_MAX bounds check on `<dir>` segment.
2687 if ps.len() >= plen {
2688 // c:700 / c:704
2689 continue; // c:701 / c:705
2690 }
2691 // c:708 — `*s++ = '/';`. c:709-710 bounds check on `<dir>/<cmd>`.
2692 let full_len = ps.len() + 1 + cmd.len();
2693 if full_len >= plen {
2694 // c:709
2695 continue; // c:710
2696 }
2697 let buf = format!("{}/{}", ps, cmd); // c:711 `strucpy(&s, cmd);`
2698 // c:712 — `if (iscom(pbuf)) return pbuf;`
2699 if iscom(&buf) {
2700 // c:712
2701 return Some(buf); // c:713
2702 }
2703 }
2704 None // c:716
2705}
2706
2707/// Port of `checkclobberparam()` from `Src/exec.c:2178` — C decl `checkclobberparam(struct redir *f)`.
2708///
2709/// C body:
2710/// ```c
2711/// struct value vbuf; Value v;
2712/// char *s = f->varid; int fd;
2713/// if (!s) return 1;
2714/// if (!(v = getvalue(&vbuf, &s, 0))) return 1;
2715/// if (v->pm->node.flags & PM_READONLY) {
2716/// zwarn("can't allocate file descriptor to readonly parameter %s",
2717/// f->varid);
2718/// errno = 0;
2719/// return 0;
2720/// }
2721/// /* We can't clobber the value in the parameter if it's
2722/// * already an opened file descriptor */
2723/// if (!isset(CLOBBER) && (s = getstrvalue(v)) &&
2724/// (fd = (int)zstrtol(s, &s, 10)) >= 0 && !*s &&
2725/// fd <= max_zsh_fd && fdtable[fd] == FDT_EXTERNAL) {
2726/// zwarn("can't clobber parameter %s containing file descriptor %d",
2727/// f->varid, fd);
2728/// errno = 0;
2729/// return 0;
2730/// }
2731/// return 1;
2732/// ```
2733///
2734/// Validate that `f->varid` (the `{var}>file` brace-FD form's var
2735/// name) is writable and (under NOCLOBBER) doesn't currently hold an
2736/// FDT_EXTERNAL fd number. Returns 1 on OK, 0 on refusal (zwarn
2737/// already emitted).
2738///
2739/// NOCLOBBER + FDT_EXTERNAL clause now ported (c:2199-2213). When
2740/// NOCLOBBER is set and the param's value is the fd-number of an
2741/// FDT_EXTERNAL-marked fd in the fdtable, refuse with a warning so
2742/// the existing fd doesn't get clobbered by the upcoming open(2).
2743pub fn checkclobberparam(f: &redir) -> i32 {
2744 // c:2178
2745 // c:2182 — `char *s = f->varid;`
2746 let s = match &f.varid {
2747 Some(v) => v.clone(),
2748 None => return 1, // c:2185-2186 — `if (!s) return 1;`
2749 };
2750 // c:2186 — `if (!(v = getvalue(&vbuf, &s, 0))) return 1;`
2751 let mut vbuf = crate::ported::zsh_h::value {
2752 pm: None,
2753 arr: Vec::new(),
2754 scanflags: 0,
2755 valflags: 0,
2756 start: 0,
2757 end: 0,
2758 };
2759 let mut cursor: &str = s.as_str();
2760 let v_opt = crate::ported::params::getvalue(Some(&mut vbuf), &mut cursor, 0);
2761 if v_opt.is_none() {
2762 return 1; // c:2187
2763 }
2764 // c:2188-2197 — readonly refusal via v->pm->node.flags.
2765 let readonly = vbuf
2766 .pm
2767 .as_ref()
2768 .map(|p| (p.node.flags as u32 & PM_READONLY) != 0)
2769 .unwrap_or(false);
2770 if readonly {
2771 // c:2191
2772 zwarn(&format!(
2773 // c:2192
2774 "can't allocate file descriptor to readonly parameter {}",
2775 s
2776 ));
2777 // c:2195 — `errno = 0;` not flagged as a system error.
2778 return 0; // c:2196
2779 }
2780 // c:2199-2213 — NOCLOBBER + FDT_EXTERNAL refusal: if NOCLOBBER set
2781 // AND the param holds a valid fd that's already in our fdtable as
2782 // FDT_EXTERNAL (allocated by sysopen / coproc / etc.), refuse the
2783 // open so we don't clobber it.
2784 if !isset(CLOBBER) {
2785 // c:2201 — `getstrvalue(v)` — read the param's string form.
2786 let val_str = crate::ported::params::getstrvalue(Some(&mut vbuf));
2787 if let Ok(fd) = val_str.trim().parse::<i32>() {
2788 // c:2202 — `if (fd <= max_zsh_fd && fdtable[fd] == FDT_EXTERNAL)`
2789 let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed);
2790 if fd >= 0 && fd <= max_fd {
2791 let kind = fdtable_get(fd);
2792 if kind == FDT_EXTERNAL {
2793 zwarn(&format!("{}: file descriptor {} already open", s, fd)); // c:2206-2210
2794 return 0; // c:2211
2795 }
2796 }
2797 }
2798 }
2799 1 // c:2214
2800}
2801
2802/// Port of `clobber_open()` from `Src/exec.c:2221` — C decl `clobber_open(struct redir *f)`.
2803///
2804/// C body:
2805/// ```c
2806/// struct stat buf;
2807/// int fd, oerrno;
2808/// char *ufname = unmeta(f->name);
2809/// /* If clobbering, just open. */
2810/// if (isset(CLOBBER) || IS_CLOBBER_REDIR(f->type))
2811/// return open(ufname, O_WRONLY | O_CREAT | O_TRUNC | O_NOCTTY, 0666);
2812/// /* If not clobbering, attempt to create file exclusively. */
2813/// if ((fd = open(ufname, O_WRONLY | O_CREAT | O_EXCL | O_NOCTTY, 0666)) >= 0)
2814/// return fd;
2815/// /* If that fails, we are still allowed to open non-regular files. */
2816/// oerrno = errno;
2817/// if ((fd = open(ufname, O_WRONLY | O_NOCTTY)) != -1) {
2818/// if (!fstat(fd, &buf)) {
2819/// if (!S_ISREG(buf.st_mode)) return fd;
2820/// /* CLOBBER_EMPTY allows re-use of empty regular files. */
2821/// if (isset(CLOBBEREMPTY) && buf.st_size == 0) return fd;
2822/// }
2823/// close(fd);
2824/// }
2825/// errno = oerrno;
2826/// return -1;
2827/// ```
2828///
2829/// Open the redir target for write with the NOCLOBBER rules:
2830/// - CLOBBER set or `>|` form → just open with O_TRUNC
2831/// - Otherwise → try O_EXCL first; on EEXIST, only allow non-regular
2832/// files (FIFOs, devices, sockets) OR empty regular files under
2833/// CLOBBEREMPTY.
2834pub fn clobber_open(f: &redir) -> i32 {
2835 // c:2221
2836 let ufname_owned = unmeta(f.name.as_deref().unwrap_or("")); // c:2225
2837 let ufname = match std::ffi::CString::new(ufname_owned.as_str()) {
2838 Ok(c) => c,
2839 Err(_) => return -1,
2840 };
2841 // c:2228-2230 — clobber path: just open + truncate.
2842 if isset(CLOBBER) || IS_CLOBBER_REDIR(f.typ) {
2843 // c:2228
2844 // c:2229 — `open(ufname, O_WRONLY|O_CREAT|O_TRUNC|O_NOCTTY, 0666)`
2845 let fd = unsafe {
2846 libc::open(
2847 ufname.as_ptr(),
2848 libc::O_WRONLY | libc::O_CREAT | libc::O_TRUNC | libc::O_NOCTTY,
2849 0o666 as libc::c_uint,
2850 )
2851 };
2852 return fd; // c:2230
2853 }
2854 // c:2233-2235 — try O_EXCL create first.
2855 let fd = unsafe {
2856 // c:2233
2857 libc::open(
2858 ufname.as_ptr(),
2859 libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | libc::O_NOCTTY,
2860 0o666 as libc::c_uint,
2861 )
2862 };
2863 if fd >= 0 {
2864 return fd; // c:2235
2865 }
2866 // c:2240 — `oerrno = errno;` — save for restoration on the recover path.
2867 let oerrno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
2868 // c:2241-2260 — recover: open() w/o O_EXCL, accept if non-regular
2869 // OR (CLOBBEREMPTY && size == 0).
2870 let fd = unsafe {
2871 // c:2241
2872 libc::open(
2873 ufname.as_ptr(),
2874 libc::O_WRONLY | libc::O_NOCTTY,
2875 0o666 as libc::c_uint,
2876 )
2877 };
2878 if fd != -1 {
2879 let mut buf: libc::stat = unsafe { std::mem::zeroed() };
2880 if unsafe { libc::fstat(fd, &mut buf) } == 0 {
2881 // c:2242
2882 // c:2243-2244 — non-regular file: accept.
2883 if (buf.st_mode & libc::S_IFMT) != libc::S_IFREG {
2884 // c:2243
2885 return fd; // c:2244
2886 }
2887 // c:2256-2257 — CLOBBEREMPTY + empty regular: accept.
2888 if isset(CLOBBEREMPTY) && buf.st_size == 0 {
2889 // c:2256
2890 return fd; // c:2257
2891 }
2892 }
2893 unsafe {
2894 libc::close(fd);
2895 } // c:2259
2896 }
2897 // c:2262 — `errno = oerrno;` — restore the EEXIST so caller diagnoses
2898 // "file exists" not the noisier "couldn't reopen" trailing errno.
2899 // Per-platform errno setter: __error() on macOS, __errno_location()
2900 // on Linux. Without cfg gating the build breaks on Linux (CI).
2901 #[cfg(target_os = "macos")]
2902 unsafe {
2903 *libc::__error() = oerrno;
2904 }
2905 #[cfg(target_os = "linux")]
2906 unsafe {
2907 *libc::__errno_location() = oerrno;
2908 }
2909 -1 // c:2263
2910}
2911
2912/// Port of `findcmd()` from `Src/exec.c:897` — C decl `findcmd(char *arg0, int docopy, int default_path)`.
2913/// Walk `$PATH` (or DEFAULT_PATH under
2914/// `default_path=1`) for `arg0`, returning the matching path on
2915/// success. `_docopy` is the C source's "duplicate the result"
2916/// flag — Rust ownership covers it without an explicit copy step.
2917/// `default_path=1` forces `/bin:/usr/bin:...` search (used by
2918/// `command -p`).
2919pub fn findcmd(arg0: &str, _docopy: i32, default_path: i32) -> Option<String> {
2920 // c:897
2921 // c:903-908 — if (default_path) → search_defpath; return.
2922 if default_path != 0 {
2923 return search_defpath(arg0, libc::PATH_MAX as usize);
2924 }
2925 // c:936-938 — the table comes FIRST, and a miss populates it:
2926 // cn = (Cmdnam) cmdnamtab->getnode(cmdnamtab, arg0);
2927 // if (!cn && isset(HASHCMDS))
2928 // cn = hashcmd(arg0, path);
2929 // This port went straight to the `$PATH` walk below, so `whence -p foo`
2930 // after `hash foo=/usr/bin/awk` reported nothing (a HASHED entry names a
2931 // path that no `$PATH` directory contains), and no lookup through
2932 // `findcmd` — `whence`, `type`, `command -v`, compctl — ever hashed
2933 // anything, where C hashes on every one of them.
2934 let cn: Option<cmdnam> = {
2935 // c:936
2936 let hit = cmdnamtab_lock()
2937 .read()
2938 .ok()
2939 .and_then(|t| t.get_including_disabled(arg0).cloned());
2940 match hit {
2941 Some(c) => Some(c),
2942 // c:937-938
2943 None if isset(crate::ported::zsh_h::HASHCMDS) => {
2944 let dirs: Vec<String> = getsparam("PATH")
2945 .unwrap_or_default()
2946 .split(':')
2947 .map(String::from)
2948 .collect();
2949 hashcmd(arg0, &dirs)
2950 }
2951 None => None,
2952 }
2953 };
2954 // c:939-940 — `if ((int) ztrlen(arg0) >= PATH_MAX) return NULL;`.
2955 if arg0.len() > libc::PATH_MAX as usize {
2956 return None;
2957 }
2958 // c:Src/exec.c:914-920 — `/`-bearing arg path resolution.
2959 // if ((s = strchr(arg0, '/'))) {
2960 // RET_IF_COM(arg0); // ← unconditional accept on iscom hit
2961 // if (arg0 == s || unset(PATHDIRS) ||
2962 // !strncmp(arg0, "./", 2) ||
2963 // !strncmp(arg0, "../", 3))
2964 // return NULL;
2965 // }
2966 // The Rust port had the iscom check gated on `starts_with('/')`,
2967 // so `type ./target/debug/zshrs` returned None even when the
2968 // file was executable. Bug #496 family.
2969 if arg0.contains('/') {
2970 if iscom(arg0) {
2971 return Some(arg0.to_string()); // c:915 RET_IF_COM
2972 }
2973 // c:916-919 — absolute OR PATHDIRS-off OR `./` / `../` →
2974 // give up here (no $PATH walk for these). Relative without
2975 // those prefixes falls through to the $PATH scan below for
2976 // the PATHDIRS=set case.
2977 if arg0.starts_with('/')
2978 || !isset(PATHDIRS)
2979 || arg0.starts_with("./")
2980 || arg0.starts_with("../")
2981 {
2982 return None;
2983 }
2984 // else fall through to PATH walk.
2985 }
2986 // c:948-977 — resolve through the node the table gave us.
2987 // if (cn) {
2988 // if (cn->node.flags & HASHED) nn = cn->u.cmd;
2989 // else {
2990 // for (pp = path; pp < cn->u.name; pp++)
2991 // if (**pp != '/') { buf = "<*pp>/<arg0>"; RET_IF_COM(buf); }
2992 // nn = "<*cn->u.name>/<arg0>";
2993 // }
2994 // RET_IF_COM(nn);
2995 // }
2996 let path = getsparam("PATH").unwrap_or_default();
2997 let dirs: Vec<&str> = path.split(':').collect();
2998 if let Some(cn) = cn.as_ref() {
2999 // c:948
3000 let nn = if (cn.node.flags & (crate::ported::zsh_h::HASHED as i32)) != 0 {
3001 // c:951-954
3002 cn.cmd.clone()
3003 } else {
3004 // c:956-968 — the `$path` entries BEFORE the matched one still
3005 // get a look-in, because `hashcmd` only ever matches an ABSOLUTE
3006 // entry (c:1058 `if (**pp == '/')`) and a relative one earlier in
3007 // `$path` takes precedence. `cn.name` is C's `cn->u.name` tail
3008 // slice, so its length recovers the index C compares against.
3009 let matched_at = dirs.len().saturating_sub(cn.name.as_ref().map_or(0, |v| v.len()));
3010 for d in dirs.iter().take(matched_at) {
3011 // c:956
3012 if !d.starts_with('/') {
3013 // c:957
3014 let buf = if d.is_empty() {
3015 arg0.to_string() // c:959 — an empty entry means `.`
3016 } else {
3017 format!("{}/{}", d, arg0) // c:960-965
3018 };
3019 if iscom(&buf) {
3020 return Some(buf); // c:966 RET_IF_COM
3021 }
3022 }
3023 }
3024 // c:969-974 — `"%s/%s", *(cn->u.name), cn->node.nam`.
3025 cn.name
3026 .as_ref()
3027 .and_then(|v| v.first())
3028 .map(|d| format!("{}/{}", d, arg0))
3029 };
3030 if let Some(nn) = nn {
3031 if iscom(&nn) {
3032 return Some(nn); // c:976 RET_IF_COM
3033 }
3034 }
3035 }
3036 // c:978-988 — walk `path[]` (the shell `$path` array). Read $PATH
3037 // from paramtab so shell-private edits via `path=(...)` take
3038 // effect (not OS env only).
3039 for dir in dirs {
3040 if dir.is_empty() {
3041 continue;
3042 }
3043 let candidate = format!("{}/{}", dir, arg0);
3044 if iscom(&candidate) {
3045 return Some(candidate);
3046 }
3047 }
3048 None // c:989
3049}
3050
3051/// Port of `addfd()` from `Src/exec.c:2397` — C decl `addfd(int forked, int *save, struct multio **mfds, int fd1, int fd2, int rflag, char *varid)`.
3052/// int fd1, int fd2, int rflag, char *varid)`
3053/// from `Src/exec.c:2397`.
3054///
3055/// C body (~100 lines, three branches):
3056/// ```c
3057/// if (varid) {
3058/// /* {varid}>file form — move fd above 10 and bind $varid to it */
3059/// } else if (!mfds[fd1] || unset(MULTIOS)) {
3060/// /* new multio OR MULTIOS off — first redir on this fd */
3061/// } else {
3062/// /* additional redir on a fd that's already a multio (split or extend) */
3063/// }
3064/// ```
3065///
3066/// Register `fd2` (already-open) as a redirection target for `fd1`.
3067/// Three branches: `varid` writes the moved fd to `$varid` and bumps
3068/// `fdtable[fd1]` = FDT_EXTERNAL; new-multio path saves the original fd1
3069/// (when `!forked`) and stamps `mfds[fd1]` as a single-entry struct;
3070/// extend-multio path either splits a ct=1 stream into a pipe + 2 fds
3071/// via `mpipe`, or appends another fd to an already-split stream
3072/// (re-allocating mfds for fd1 past the MULTIOUNIT boundary).
3073///
3074/// `multio.fds` is now `Vec<i32>` (zsh_h.rs:1397) so the C
3075/// `hrealloc` at c:2485 maps to `Vec::push`; MULTIOUNIT is no
3076/// longer a hard cap (still 8 for the initial allocation, grown
3077/// on demand thereafter).
3078///
3079/// `fdtable[fdN] |= FDT_SAVED_MASK` at c:2440 — Rust fdtable_set
3080/// stores the int value but doesn't expose a bitwise-OR setter; we
3081/// re-read + OR + re-store as two atomic-feeling steps.
3082pub fn addfd(
3083 forked: i32,
3084 save: &mut [i32; 10],
3085 mfds: &mut [Option<Box<multio>>; 10],
3086 fd1: i32,
3087 fd2: i32,
3088 rflag: i32,
3089 varid: Option<&str>,
3090) {
3091 // c:2397
3092 let mut pipes: [i32; 2] = [-1; 2]; // c:2400
3093
3094 // c:2402-2417 — `if (varid)` branch — {varid}>file shape.
3095 if let Some(vid) = varid {
3096 // c:2402
3097 let fd_moved = movefd(fd2); // c:2404
3098 if fd_moved == -1 {
3099 // c:2405
3100 zerr(&format!(
3101 // c:2406
3102 "cannot move fd {}: {}",
3103 fd2,
3104 std::io::Error::last_os_error()
3105 ));
3106 return; // c:2407
3107 }
3108 // c:2409 — `fdtable[fd1] = FDT_EXTERNAL;`
3109 fdtable_set(fd_moved, FDT_EXTERNAL);
3110 // c:2410 — `setiparam(varid, (zlong)fd1);`
3111 setiparam(vid, fd_moved as i64);
3112 // c:2415-2416 — `if (errflag) zclose(fd1);`
3113 // A user interrupt sets ERRFLAG_INT, never ERRFLAG_ERROR (signals.c:457), and
3114 // the C line cited above tests the WHOLE errflag, so masking here let an
3115 // interrupted shell keep going where zsh stops.
3116 if errflag.load(Ordering::Relaxed) != 0 {
3117 // c:2415
3118 let _ = zclose(fd_moved); // c:2416
3119 }
3120 return;
3121 }
3122 // c:2418 — `else if (!mfds[fd1] || unset(MULTIOS))`
3123 let fd1u = fd1 as usize;
3124 if fd1u >= mfds.len() {
3125 return;
3126 }
3127 if mfds[fd1u].is_none() || unset(MULTIOS) {
3128 // c:2418
3129 if mfds[fd1u].is_none() {
3130 // c:2419 — `starting a new multio`
3131 // c:2420 — `mfds[fd1] = zhalloc(sizeof(multio));`
3132 mfds[fd1u] = Some(Box::new(multio {
3133 ct: 0,
3134 rflag: 0,
3135 pipe: -1,
3136 // c:2420 — C allocates VARLENARRAY trailing `int fds[1]`;
3137 // grow on demand via push() below. Pre-fill MULTIOUNIT
3138 // slots with -1 so existing indexed writes (fds[0], fds[1])
3139 // still work without explicit resize().
3140 fds: vec![-1; MULTIOUNIT],
3141 }));
3142 // c:2421 — `if (!forked && save[fd1] == -2)`
3143 if forked == 0 && save[fd1u] == -2 {
3144 if fd1 == fd2 {
3145 // c:2422
3146 save[fd1u] = -1; // c:2423
3147 } else {
3148 // c:2424
3149 let fd_n = movefd(fd1); // c:2425
3150 if fd_n < 0 {
3151 // c:2430
3152 let e = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
3153 if e != libc::EBADF {
3154 // c:2431
3155 zerr(&format!(
3156 // c:2432
3157 "cannot duplicate fd {}: {}",
3158 fd1,
3159 std::io::Error::from_raw_os_error(e)
3160 ));
3161 mfds[fd1u] = None; // c:2433
3162 closemnodes(mfds); // c:2434
3163 return; // c:2435
3164 }
3165 } else {
3166 // c:2438-2439 — DPUTS check that the saved fd is FDT_INTERNAL.
3167 crate::DPUTS!(
3168 fdtable_get(fd_n) != FDT_INTERNAL,
3169 "Saved file descriptor not marked as internal"
3170 );
3171 // c:2440 — `fdtable[fdN] |= FDT_SAVED_MASK;`
3172 let cur = fdtable_get(fd_n);
3173 fdtable_set(fd_n, cur | FDT_SAVED_MASK);
3174 }
3175 save[fd1u] = fd_n; // c:2442
3176 }
3177 }
3178 }
3179 // c:2446-2447 — `if (!varid) redup(fd2, fd1);` (varid already
3180 // handled above; this is the non-varid branch.)
3181 let _ = redup(fd2, fd1);
3182 // c:2448-2450 — `mfds[fd1]->ct=1; mfds[fd1]->fds[0]=fd1; mfds[fd1]->rflag=rflag;`
3183 if let Some(mn) = mfds[fd1u].as_mut() {
3184 mn.ct = 1; // c:2448
3185 mn.fds[0] = fd1; // c:2449
3186 mn.rflag = rflag; // c:2450
3187 }
3188 } else {
3189 // c:2451 — extend existing multio.
3190 // c:2452-2456 — rflag mismatch check.
3191 let cur_rflag = mfds[fd1u].as_ref().map(|m| m.rflag).unwrap_or(0);
3192 if cur_rflag != rflag {
3193 // c:2452
3194 zerr(&format!("file mode mismatch on fd {}", fd1)); // c:2453
3195 closemnodes(mfds); // c:2454
3196 return; // c:2455
3197 }
3198 let cur_ct = mfds[fd1u].as_ref().map(|m| m.ct).unwrap_or(0);
3199 if cur_ct == 1 {
3200 // c:2457 — split the stream.
3201 // c:2458 — `int fdN = movefd(fd1);`
3202 let fd_n = movefd(fd1);
3203 if fd_n < 0 {
3204 // c:2459
3205 zerr(&format!(
3206 // c:2460
3207 "multio failed for fd {}: {}",
3208 fd1,
3209 std::io::Error::last_os_error()
3210 ));
3211 closemnodes(mfds); // c:2461
3212 return; // c:2462
3213 }
3214 if let Some(mn) = mfds[fd1u].as_mut() {
3215 mn.fds[0] = fd_n; // c:2464
3216 }
3217 // c:2465 — `fdN = movefd(fd2);`
3218 let fd_n2 = movefd(fd2);
3219 if fd_n2 < 0 {
3220 // c:2466
3221 zerr(&format!(
3222 // c:2467
3223 "multio failed for fd {}: {}",
3224 fd2,
3225 std::io::Error::last_os_error()
3226 ));
3227 closemnodes(mfds); // c:2468
3228 return; // c:2469
3229 }
3230 if let Some(mn) = mfds[fd1u].as_mut() {
3231 mn.fds[1] = fd_n2; // c:2471
3232 }
3233 // c:2472 — `mpipe(pipes)`
3234 if mpipe(&mut pipes) < 0 {
3235 // c:2472
3236 zerr(&format!(
3237 // c:2473
3238 "multio failed for fd {}: {}",
3239 fd2,
3240 std::io::Error::last_os_error()
3241 ));
3242 closemnodes(mfds); // c:2474
3243 return; // c:2475
3244 }
3245 // c:2477 — `mfds[fd1]->pipe = pipes[1 - rflag];`
3246 if let Some(mn) = mfds[fd1u].as_mut() {
3247 mn.pipe = pipes[(1 - rflag) as usize];
3248 }
3249 // c:2478 — `redup(pipes[rflag], fd1);`
3250 let _ = redup(pipes[rflag as usize], fd1);
3251 // c:2479 — `mfds[fd1]->ct = 2;`
3252 if let Some(mn) = mfds[fd1u].as_mut() {
3253 mn.ct = 2;
3254 }
3255 } else {
3256 // c:2480 — extend already-split stream.
3257 // c:2482-2486 — `mn = hrealloc(mn, sizeof + (ct-1)*sizeof(int),
3258 // sizeof + ct*sizeof(int));`
3259 // Rust's `Vec<i32>` grows on demand; ensure capacity for the
3260 // new slot before the indexed write below.
3261 if let Some(mn) = mfds[fd1u].as_mut() {
3262 while mn.fds.len() <= cur_ct as usize {
3263 mn.fds.push(-1);
3264 }
3265 }
3266 // c:2487 — `if ((fdN = movefd(fd2)) < 0)`
3267 let fd_n = movefd(fd2);
3268 if fd_n < 0 {
3269 zerr(&format!(
3270 // c:2488
3271 "multio failed for fd {}: {}",
3272 fd2,
3273 std::io::Error::last_os_error()
3274 ));
3275 closemnodes(mfds); // c:2489
3276 return; // c:2490
3277 }
3278 // c:2492 — `mfds[fd1]->fds[mfds[fd1]->ct++] = fdN;`
3279 if let Some(mn) = mfds[fd1u].as_mut() {
3280 let slot = mn.ct as usize;
3281 if slot < mn.fds.len() {
3282 mn.fds[slot] = fd_n;
3283 mn.ct += 1;
3284 }
3285 }
3286 }
3287 }
3288}
3289
3290/// Port of `closemn()` from `Src/exec.c:2273` — C decl `closemn(struct multio **mfds, int fd, int type)`.
3291///
3292/// C body (abridged — the meat is the fork-into-tee-or-cat child):
3293/// ```c
3294/// if (fd >= 0 && mfds[fd] && mfds[fd]->ct >= 2) {
3295/// struct multio *mn = mfds[fd];
3296/// char buf[TCBUFSIZE]; int len, i;
3297/// pid_t pid; struct timespec bgtime;
3298/// child_block();
3299/// if ((pid = zfork(&bgtime))) {
3300/// for (i = 0; i < mn->ct; i++) zclose(mn->fds[i]);
3301/// zclose(mn->pipe);
3302/// if (pid == -1) { mfds[fd] = NULL; child_unblock(); return; }
3303/// mn->ct = 1; mn->fds[0] = fd;
3304/// addproc(pid, NULL, 1, &bgtime, -1, -1);
3305/// child_unblock(); return;
3306/// }
3307/// /* pid == 0 (child) */
3308/// opts[INTERACTIVE] = 0;
3309/// dont_queue_signals();
3310/// child_unblock();
3311/// closeallelse(mn);
3312/// if (mn->rflag) {
3313/// /* tee process: read mn->pipe, write each mn->fds[i] */
3314/// } else {
3315/// /* cat process: read each mn->fds[i], write mn->pipe */
3316/// }
3317/// _exit(0);
3318/// } else if (fd >= 0 && type == REDIR_CLOSE)
3319/// mfds[fd] = NULL;
3320/// ```
3321///
3322/// Success-path close of a multio. For ct>=2 (multiple-output
3323/// redirection), forks a tee/cat child that proxies bytes between
3324/// the original fd and the per-output fds. Single-output multios
3325/// (ct=1) skip the fork entirely and just clear the slot.
3326///
3327/// c:2299 — `addproc(pid, NULL, 1, &bgtime, -1, -1)` records the
3328/// tee/cat child in the current job's auxprocs.
3329pub fn closemn(mfds: &mut [Option<Box<multio>>; 10], fd: i32, type_: i32) {
3330 // c:2273
3331 // c:2275 — `if (fd >= 0 && mfds[fd] && mfds[fd]->ct >= 2)`
3332 let needs_tee = fd >= 0
3333 && (fd as usize) < mfds.len()
3334 && mfds[fd as usize].as_ref().is_some_and(|m| m.ct >= 2);
3335 if needs_tee {
3336 // c:2275
3337 // Take the multio out of the slot so we can move pieces into
3338 // the child without aliasing the slot.
3339 let mn = mfds[fd as usize].take().unwrap();
3340 let mut buf = [0u8; 4092]; // c:2277 TCBUFSIZE
3341 // c:2287 — `child_block();` block SIGCHLD before fork race.
3342 child_block();
3343 // c:2288 — `pid = zfork(&bgtime);`
3344 let mut bgtime = ZshTimespec {
3345 tv_sec: 0,
3346 tv_nsec: 0,
3347 };
3348 let pid = zfork(Some(&mut bgtime));
3349 if pid != 0 {
3350 // c:2288 parent branch
3351 // c:2289-2290 — close all per-output fds.
3352 for i in 0..mn.ct as usize {
3353 if i < mn.fds.len() {
3354 let _ = zclose(mn.fds[i]); // c:2290
3355 }
3356 }
3357 let _ = zclose(mn.pipe); // c:2291
3358 if pid == -1 {
3359 // c:2292
3360 // c:2293 — `mfds[fd] = NULL;` already done via .take()
3361 child_unblock(); // c:2294
3362 return; // c:2295
3363 }
3364 // c:2297-2298 — `mn->ct = 1; mn->fds[0] = fd;`
3365 let mut mn_back = mn;
3366 mn_back.ct = 1; // c:2297
3367 mn_back.fds[0] = fd; // c:2298
3368 mfds[fd as usize] = Some(mn_back);
3369 // c:2299 — `addproc(pid, NULL, 1, &bgtime, -1, -1);` — record
3370 // the tee/cat child in the current job's auxprocs (aux=true).
3371 if let Some(jt) = JOBTAB.get() {
3372 let mut guard = jt.lock().unwrap();
3373 let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
3374 if tj >= 0 {
3375 if let Some(j) = guard.get_mut(tj as usize) {
3376 crate::ported::jobs::addproc(
3377 j,
3378 pid,
3379 "",
3380 true,
3381 Some(std::time::Instant::now()),
3382 -1,
3383 -1,
3384 );
3385 }
3386 }
3387 }
3388 let _ = bgtime;
3389 child_unblock(); // c:2300
3390 return; // c:2301
3391 }
3392 // c:2303 — child branch (pid == 0).
3393 opt_state_set("interactive", false); // c:2304
3394 dont_queue_signals(); // c:2305
3395 child_unblock(); // c:2306
3396 closeallelse(&mn); // c:2307
3397 // c:2308-2333 — tee or cat loop.
3398 if mn.rflag != 0 {
3399 // c:2308 — `mn->rflag` set → tee process
3400 // c:2310 — `while ((len = read(mn->pipe, buf, TCBUFSIZE)) != 0)`
3401 loop {
3402 let len = unsafe {
3403 libc::read(mn.pipe, buf.as_mut_ptr() as *mut libc::c_void, buf.len())
3404 };
3405 if len == 0 {
3406 break;
3407 }
3408 if len < 0 {
3409 // c:2311
3410 let e = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
3411 if e == libc::EINTR {
3412 // c:2312
3413 continue;
3414 } else {
3415 break; // c:2315
3416 }
3417 }
3418 // c:2317-2319 — `for i: write_loop(mn->fds[i], buf, len)`
3419 for i in 0..mn.ct as usize {
3420 if i >= mn.fds.len() {
3421 break;
3422 }
3423 if write_loop(mn.fds[i], &buf[..len as usize]).is_err() {
3424 break; // c:2319
3425 }
3426 }
3427 }
3428 } else {
3429 // c:2321 — cat process
3430 for i in 0..mn.ct as usize {
3431 if i >= mn.fds.len() {
3432 break;
3433 }
3434 // c:2324 — `while ((len = read(mn->fds[i], buf, TCBUFSIZE)) != 0)`
3435 loop {
3436 let len = unsafe {
3437 libc::read(mn.fds[i], buf.as_mut_ptr() as *mut libc::c_void, buf.len())
3438 };
3439 if len == 0 {
3440 break;
3441 }
3442 if len < 0 {
3443 let e = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
3444 // c:2326 — `if (errno == EINTR && !isatty(mn->fds[i]))`
3445 if e == libc::EINTR && unsafe { libc::isatty(mn.fds[i]) } == 0 {
3446 continue;
3447 } else {
3448 break; // c:2329
3449 }
3450 }
3451 // c:2331 — `if (write_loop(mn->pipe, buf, len) < 0) break;`
3452 if write_loop(mn.pipe, &buf[..len as usize]).is_err() {
3453 break; // c:2332
3454 }
3455 }
3456 }
3457 }
3458 // c:2335 — `_exit(0);`
3459 unsafe {
3460 libc::_exit(0);
3461 }
3462 } else if fd >= 0 && type_ == REDIR_CLOSE {
3463 // c:2336
3464 // c:2337 — `mfds[fd] = NULL;`
3465 if (fd as usize) < mfds.len() {
3466 mfds[fd as usize] = None;
3467 }
3468 }
3469}
3470
3471/// Port of `closemnodes()` from `Src/exec.c:2344` — C decl `closemnodes(struct multio **mfds)`.
3472///
3473/// C body:
3474/// ```c
3475/// int i, j;
3476/// for (i = 0; i < 10; i++)
3477/// if (mfds[i]) {
3478/// for (j = 0; j < mfds[i]->ct; j++)
3479/// zclose(mfds[i]->fds[j]);
3480/// mfds[i] = NULL;
3481/// }
3482/// ```
3483///
3484/// Failure-path cleanup: close every fd stashed in any of the 10
3485/// multio slots and null the slot. Called from `execcmd_exec` when
3486/// a redirect setup fails partway through and we need to roll back.
3487pub fn closemnodes(mfds: &mut [Option<Box<multio>>; 10]) {
3488 // c:2344
3489 for i in 0..10 {
3490 // c:2348
3491 if let Some(mn) = mfds[i].take() {
3492 // c:2349
3493 for j in 0..mn.ct as usize {
3494 // c:2350
3495 if j < mn.fds.len() {
3496 let _ = zclose(mn.fds[j]); // c:2351
3497 }
3498 }
3499 // c:2352 — `mfds[i] = NULL;` — handled by .take() above.
3500 }
3501 }
3502}
3503
3504/// Port of `closeallelse()` from `Src/exec.c:2358` — C decl `closeallelse(struct multio *mn)`.
3505///
3506/// C body:
3507/// ```c
3508/// int i, j;
3509/// long openmax;
3510/// openmax = fdtable_size;
3511/// for (i = 0; i < openmax; i++)
3512/// if (mn->pipe != i) {
3513/// for (j = 0; j < mn->ct; j++)
3514/// if (mn->fds[j] == i) break;
3515/// if (j == mn->ct)
3516/// zclose(i);
3517/// }
3518/// ```
3519///
3520/// Close every fd in the open range EXCEPT `mn->pipe` and the fds
3521/// stashed in `mn->fds`. Called inside the multio tee/cat child
3522/// process to release every fd the parent had open — only the pipe
3523/// + per-output fds stay alive for the read/write loop.
3524pub fn closeallelse(mn: &multio) {
3525 // c:2358
3526 // c:2363 — `openmax = fdtable_size;`. fdtable_size starts at
3527 // `zopenmax()` (c:Src/init.c:1931) and only grows (check_fd_table,
3528 // c:Src/utils.c:1974-1981) to cover max_zsh_fd, so it spans every
3529 // descriptor the process may hold — not just the ones the table
3530 // records. Stopping at max_zsh_fd left the tee holding a descriptor
3531 // the table never saw (a user `exec 3>&1 3>&2` target, a Rust-side
3532 // pipe), and a tee that holds its own pipe's write end never exits.
3533 let openmax = std::cmp::max(
3534 MAX_ZSH_FD.load(Ordering::Relaxed) + 1,
3535 crate::ported::compat::zopenmax() as i32,
3536 ); // c:2363
3537 for i in 0..openmax {
3538 // c:2365
3539 if mn.pipe == i {
3540 // c:2366
3541 continue;
3542 }
3543 // c:2367-2369 — scan mn->fds[] for i; skip-close if found.
3544 let mut found = false;
3545 for j in 0..mn.ct as usize {
3546 // c:2367
3547 if j < mn.fds.len() && mn.fds[j] == i {
3548 // c:2368
3549 found = true;
3550 break; // c:2369
3551 }
3552 }
3553 // c:2370-2371 — `if (j == mn->ct) zclose(i);`
3554 if !found {
3555 // c:2371 zclose → c:Src/utils.c:2145 `close(fd)`, blind. zclose's
3556 // skip-unowned guard exists for sibling threads recycling fds;
3557 // this runs in the forked child, which has only one thread.
3558 unsafe { libc::close(i) }; // c:2371
3559 }
3560 }
3561}
3562
3563/// Port of `fixfds()` from `Src/exec.c:4523` — C decl `fixfds(int *save)`.
3564///
3565/// C body:
3566/// ```c
3567/// int old_errno = errno;
3568/// int i;
3569/// for (i = 0; i != 10; i++)
3570/// if (save[i] != -2)
3571/// redup(save[i], i);
3572/// errno = old_errno;
3573/// ```
3574///
3575/// Restore fds 0..9 from the `save[10]` slot array. `-2` sentinel
3576/// means "no save was made for this fd"; any other value is the
3577/// stashed fd that gets `dup2`'d back via `redup`. Preserves the
3578/// caller's errno across the loop so a downstream caller diagnoses
3579/// the original failure, not a noisy dup2 errno.
3580pub fn fixfds(save: &[i32; 10]) {
3581 // c:4523
3582 let old_errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0); // c:4525
3583 for i in 0..10i32 {
3584 // c:4528 — `for (i = 0; i != 10; i++)`
3585 if save[i as usize] != -2 {
3586 // c:4529
3587 redup(save[i as usize], i); // c:4530
3588 }
3589 }
3590 // c:4531 — `errno = old_errno;`
3591 #[cfg(target_os = "macos")]
3592 unsafe {
3593 *libc::__error() = old_errno;
3594 }
3595 #[cfg(target_os = "linux")]
3596 unsafe {
3597 *libc::__errno_location() = old_errno;
3598 }
3599}
3600
3601/// Port of `closem()` from `Src/exec.c:4546` — C decl `closem(int how, int all)`.
3602///
3603/// C body:
3604/// ```c
3605/// int i;
3606/// for (i = 10; i <= max_zsh_fd; i++)
3607/// if (fdtable[i] != FDT_UNUSED &&
3608/// (all || (fdtable[i] != FDT_PROC_SUBST &&
3609/// fdtable[i] != FDT_EXTERNAL)) &&
3610/// (how == FDT_UNUSED || (fdtable[i] & FDT_TYPE_MASK) == how)) {
3611/// if (i == SHTTY) SHTTY = -1;
3612/// zclose(i);
3613/// }
3614/// ```
3615///
3616/// Walk fds 10..=MAX_ZSH_FD and close every internal shell fd that
3617/// matches the criteria. `how == FDT_UNUSED` matches all kinds (no
3618/// type filter); otherwise only fds whose low-nibble type equals
3619/// `how` are closed. `all == 0` preserves user-visible fds
3620/// (FDT_PROC_SUBST, FDT_EXTERNAL) since those need to outlive the
3621/// shell's internal-fd lifetime. SHTTY clearing prevents a stale
3622/// reference if we just closed the controlling tty.
3623pub fn closem(how: i32, all: i32) {
3624 // c:4546
3625 let max = MAX_ZSH_FD.load(Ordering::Relaxed); // c:4550
3626 for i in 10i32..=max {
3627 // c:4550
3628 let kind = fdtable_get(i); // c:4551 fdtable[i]
3629 if kind == FDT_UNUSED {
3630 // c:4551
3631 continue;
3632 }
3633 // c:4557-4558 — `(all || (kind != FDT_PROC_SUBST && kind != FDT_EXTERNAL))`
3634 if all == 0 && (kind == FDT_PROC_SUBST || kind == FDT_EXTERNAL) {
3635 continue;
3636 }
3637 // c:4559 — `(how == FDT_UNUSED || (fdtable[i] & FDT_TYPE_MASK) == how)`
3638 if how != FDT_UNUSED && (kind & FDT_TYPE_MASK) != how {
3639 continue;
3640 }
3641 // c:4560-4561 — `if (i == SHTTY) SHTTY = -1;`
3642 if i == SHTTY.load(Ordering::Relaxed) {
3643 // c:4560
3644 SHTTY.store(-1, Ordering::Relaxed); // c:4561
3645 }
3646 // c:4562 — `zclose(i);`
3647 let _ = zclose(i);
3648 }
3649}
3650
3651/// Port of `hashcmd()` from `Src/exec.c:1010` — C decl `hashcmd(char *arg0, char **pp)`.
3652///
3653/// C body:
3654/// ```c
3655/// Cmdnam cn;
3656/// char *s, buf[PATH_MAX+1];
3657/// char **pq;
3658/// if (*arg0 == '/') return NULL;
3659/// for (; *pp; pp++)
3660/// if (**pp == '/') {
3661/// s = buf;
3662/// struncpy(&s, *pp, PATH_MAX);
3663/// *s++ = '/';
3664/// if ((s - buf) + strlen(arg0) >= PATH_MAX) continue;
3665/// strcpy(s, arg0);
3666/// if (iscom(buf)) break;
3667/// }
3668/// if (!*pp) return NULL;
3669/// cn = (Cmdnam) zshcalloc(sizeof *cn);
3670/// cn->node.flags = 0;
3671/// cn->u.name = pp;
3672/// cmdnamtab->addnode(cmdnamtab, ztrdup(arg0), cn);
3673/// if (isset(HASHDIRS)) {
3674/// for (pq = pathchecked; pq <= pp; pq++) hashdir(pq);
3675/// pathchecked = pp + 1;
3676/// }
3677/// return cn;
3678/// ```
3679///
3680/// Walk `pp[]` (a $path slice starting from `pathchecked`) for the
3681/// first absolute-PATH entry where `<entry>/<arg0>` is an executable
3682/// regular file. Inserts the unhashed-cmdnam entry into `cmdnamtab`
3683/// and (under HASHDIRS) bulk-hashes every PATH dir we walked through
3684/// so subsequent commands hit the cache.
3685///
3686/// Returns the just-inserted `cmdnam` (now in `cmdnamtab`) on success,
3687/// `None` if `arg0` is absolute or no PATH entry contains it.
3688pub fn hashcmd(arg0: &str, pp: &[String]) -> Option<cmdnam> {
3689 // c:1010
3690 // c:1055 — `if ((*arg0 == '/') || !strncmp(arg0, "./", 2) ||
3691 // !strncmp(arg0, "../", 3)) return NULL;`
3692 // The doc-comment above quotes an older zsh where only the leading `/`
3693 // was rejected; current C rejects the two explicitly-relative spellings
3694 // too, and this port had kept the old single test.
3695 if arg0.starts_with('/') || arg0.starts_with("./") || arg0.starts_with("../") {
3696 return None; // c:1056
3697 }
3698 // c:1018-1028 — walk pp[] for first matching absolute entry.
3699 let mut found_idx: Option<usize> = None;
3700 for (i, dir) in pp.iter().enumerate() {
3701 // c:1018
3702 if !dir.starts_with('/') {
3703 // c:1019
3704 continue;
3705 }
3706 // c:1020-1025 — buf = "<dir>/<arg0>"; PATH_MAX bounds check.
3707 if dir.len() + 1 + arg0.len() >= libc::PATH_MAX as usize {
3708 // c:1023
3709 continue; // c:1024
3710 }
3711 let buf = format!("{}/{}", dir, arg0); // c:1025
3712 if iscom(&buf) {
3713 // c:1026
3714 found_idx = Some(i);
3715 break; // c:1027
3716 }
3717 }
3718 // c:1030-1031 — `if (!*pp) return NULL;`
3719 let pp_idx = match found_idx {
3720 Some(i) => i,
3721 None => return None, // c:1031
3722 };
3723 // c:1033-1036 — alloc cn, set flags=0, u.name=pp (the matching slice).
3724 let path_slice: Vec<String> = pp[pp_idx..].to_vec(); // c:1035
3725 let cn = cmdnam_unhashed(arg0, path_slice); // c:1033-1035
3726 // c:1036 — `cmdnamtab->addnode(cmdnamtab, ztrdup(arg0), cn);`
3727 if let Ok(mut tab) = cmdnamtab_lock().write() {
3728 tab.add(cn.clone());
3729 }
3730 // c:1038-1042 — under HASHDIRS, bulk-hash every dir up to and
3731 // including the matching one, then bump pathchecked past it.
3732 if isset(HASHDIRS) {
3733 // c:1038
3734 let start = pathchecked.load(Ordering::Relaxed); // c:1039
3735 for pq in start..=pp_idx {
3736 // c:1039
3737 if pq < pp.len() {
3738 hashdir(&pp[pq], pq); // c:1040
3739 }
3740 }
3741 pathchecked.store(pp_idx + 1, Ordering::Relaxed); // c:1041
3742 }
3743 Some(cn) // c:1044
3744}
3745
3746/// Port of `zfork()` from `Src/exec.c:349` — C decl `zfork(struct timespec *ts)`.
3747///
3748/// C body:
3749/// ```c
3750/// pid_t pid;
3751/// if (thisjob != -1 && thisjob >= jobtabsize - 1 && !expandjobtab()) {
3752/// zerr("job table full");
3753/// return -1;
3754/// }
3755/// if (ts) zgettime_monotonic_if_available(ts);
3756/// queue_signals();
3757/// pid = fork();
3758/// unqueue_signals();
3759/// if (pid == -1) {
3760/// zerr("fork failed: %e", errno);
3761/// return -1;
3762/// }
3763/// #ifdef HAVE_GETRLIMIT
3764/// if (!pid) setlimits(NULL);
3765/// #endif
3766/// return pid;
3767/// ```
3768///
3769/// fork(2) wrapper with jobtab capacity check + child rlimit
3770/// re-application. Used by every subshell-spawning path: pipelines,
3771/// process substitution, async commands, command substitution.
3772pub fn zfork(ts: Option<&mut ZshTimespec>) -> libc::pid_t {
3773 // c:349
3774 let pid: libc::pid_t;
3775
3776 // c:356-359 — `if (thisjob != -1 && thisjob >= jobtabsize - 1 && !expandjobtab())`
3777 let thisjob_lock = THISJOB.get_or_init(|| std::sync::Mutex::new(-1));
3778 let thisjob = *thisjob_lock.lock().unwrap();
3779 if thisjob != -1 {
3780 // c:356
3781 let needed = (thisjob + 1) as usize;
3782 let needs_expand = JOBTAB
3783 .get_or_init(|| std::sync::Mutex::new(Vec::new()))
3784 .lock()
3785 .map(|t| needed >= t.len().saturating_sub(1))
3786 .unwrap_or(false);
3787 if needs_expand {
3788 let mut tab = JOBTAB.get().unwrap().lock().unwrap();
3789 if !expandjobtab(&mut tab, needed) {
3790 // c:357
3791 zerr("job table full"); // c:357
3792 return -1; // c:358
3793 }
3794 }
3795 }
3796 // c:360-361 — `if (ts) zgettime_monotonic_if_available(ts);`
3797 if let Some(ts) = ts {
3798 zgettime_monotonic_if_available(ts);
3799 }
3800 // c:368-370 — `queue_signals(); pid = fork(); unqueue_signals();`
3801 queue_signals(); // c:368
3802 pid = unsafe { libc::fork() }; // c:369
3803 unqueue_signals(); // c:370
3804 // c:371-374 — fork failure.
3805 if pid == -1 {
3806 // c:371
3807 zerr(&format!(
3808 // c:372
3809 "fork failed: {}",
3810 std::io::Error::last_os_error()
3811 ));
3812 return -1; // c:373
3813 }
3814 // c:375-379 — child: re-apply rlimits (HAVE_GETRLIMIT path).
3815 #[cfg(unix)]
3816 if pid == 0 {
3817 // c:376
3818 let _ = setlimits(""); // c:378
3819 }
3820 pid // c:380
3821}
3822
3823/// Port of `loadautofnsetfile()` from `Src/exec.c:5657` — C decl `loadautofnsetfile(Shfunc shf, char *fdir)`.
3824///
3825/// C body:
3826/// ```c
3827/// if (!(shf->node.flags & PM_LOADDIR) ||
3828/// strcmp(shf->filename, fdir) != 0) {
3829/// dircache_set(&shf->filename, NULL);
3830/// if (fdir) {
3831/// shf->node.flags |= PM_LOADDIR;
3832/// dircache_set(&shf->filename, fdir);
3833/// } else {
3834/// shf->node.flags &= ~PM_LOADDIR;
3835/// shf->filename = ztrdup(shf->node.nam);
3836/// }
3837/// }
3838/// ```
3839///
3840/// Update `shf->filename` to the autoload directory `fdir`. Routes
3841/// through the refcounted `dircache_set` so identical directory
3842/// strings are shared across shfunc table entries.
3843pub fn loadautofnsetfile(shf: &mut shfunc, fdir: Option<&str>) {
3844 // c:5657
3845 // c:5664-5665 — `if (!(shf->node.flags & PM_LOADDIR) || strcmp(shf->filename, fdir) != 0)`
3846 let loaddir = (shf.node.flags as u32 & PM_LOADDIR) != 0;
3847 let same = match (&shf.filename, fdir) {
3848 (Some(a), Some(b)) => a == b,
3849 _ => false,
3850 };
3851 if !loaddir || !same {
3852 // c:5664
3853 // c:5667 — `dircache_set(&shf->filename, NULL);` — refcount-drop old.
3854 dircache_set(&mut shf.filename, None);
3855 if let Some(fdir) = fdir {
3856 // c:5668
3857 shf.node.flags |= PM_LOADDIR as i32; // c:5670
3858 dircache_set(&mut shf.filename, Some(fdir)); // c:5671
3859 } else {
3860 // c:5672
3861 shf.node.flags &= !(PM_LOADDIR as i32); // c:5674
3862 shf.filename = Some(shf.node.nam.clone()); // c:5675 `ztrdup(shf->node.nam)`
3863 }
3864 }
3865}
3866
3867/// Port of `commandnotfound()` from `Src/exec.c:669` — C decl `commandnotfound(char *arg0, LinkList args)`.
3868///
3869/// C body:
3870/// ```c
3871/// Shfunc shf = (Shfunc)
3872/// shfunctab->getnode(shfunctab, "command_not_found_handler");
3873/// if (!shf) {
3874/// lastval = 127;
3875/// return 1;
3876/// }
3877/// pushnode(args, arg0);
3878/// lastval = doshfunc(shf, args, 1);
3879/// return 0;
3880/// ```
3881///
3882/// Look up the user-defined `command_not_found_handler` shfunc and
3883/// invoke it with `arg0` prepended to `args`. Returns 0 if handled,
3884/// 1 if no handler (so caller emits the standard "command not found"
3885/// error). Sets `$?` to 127 in the no-handler path.
3886pub fn commandnotfound(arg0: &str, args: &mut Vec<String>) -> i32 {
3887 // c:669
3888 // c:671-672 — `shf = shfunctab->getnode(shfunctab, "command_not_found_handler");`
3889 let has_handler = shfunctab_lock()
3890 .read()
3891 .map(|t| t.get("command_not_found_handler").is_some())
3892 .unwrap_or(false);
3893 if !has_handler {
3894 // c:674
3895 LASTVAL.store(127, Ordering::Relaxed); // c:675
3896 return 1; // c:676
3897 }
3898 // c:679 — `pushnode(args, arg0);` — prepend arg0 (handler name
3899 // is the first positional arg per C convention).
3900 args.insert(0, arg0.to_string());
3901 args.insert(0, "command_not_found_handler".to_string());
3902 // c:680 — `lastval = doshfunc(shf, args, 1);`. Direct doshfunc
3903 // call mirrors C — body_runner routes through the host body-only
3904 // entry so the function body runs once inside doshfunc's scope.
3905 let shf_clone: Option<shfunc> = shfunctab_lock()
3906 .read()
3907 .ok()
3908 .and_then(|t| t.get("command_not_found_handler").cloned());
3909 if let Some(mut shf) = shf_clone {
3910 let body_args = args.clone();
3911 let body_runner = move || -> i32 {
3912 crate::ported::exec::run_function_body("command_not_found_handler", &body_args[1..])
3913 .unwrap_or(0)
3914 };
3915 let lv = doshfunc(&mut shf, args.clone(), true, body_runner);
3916 LASTVAL.store(lv, Ordering::Relaxed);
3917 }
3918 0 // c:681
3919}
3920
3921/// Port of `namedpipe()` from `Src/exec.c:5001` — C decl `namedpipe(void)`.
3922///
3923/// C body (#ifdef HAVE_FIFOS branch):
3924/// ```c
3925/// char *tnam = gettempname(NULL, 1);
3926/// if (!tnam) {
3927/// zerr("failed to create named pipe: %e", errno);
3928/// return NULL;
3929/// }
3930/// if (mkfifo(tnam, 0600) < 0) {
3931/// zerr("failed to create named pipe: %s, %e", tnam, errno);
3932/// return NULL;
3933/// }
3934/// return tnam;
3935/// ```
3936///
3937/// Create a FIFO with a unique name for process substitution. Used by
3938/// `getproc` (`<(cmd)` / `>(cmd)`) on systems without `/dev/fd`.
3939pub fn namedpipe() -> Option<String> {
3940 // c:5001
3941 let tnam = gettempname(None, true); // c:5003
3942 let tnam = match tnam {
3943 Some(t) => t,
3944 None => {
3945 // c:5005
3946 zerr(&format!(
3947 // c:5006
3948 "failed to create named pipe: {}",
3949 std::io::Error::last_os_error()
3950 ));
3951 return None; // c:5007
3952 }
3953 };
3954 // c:5010 — `mkfifo(tnam, 0600)`.
3955 let cstr = match std::ffi::CString::new(tnam.as_str()) {
3956 Ok(c) => c,
3957 Err(_) => return None,
3958 };
3959 if unsafe { libc::mkfifo(cstr.as_ptr(), 0o600) } < 0 {
3960 // c:5010
3961 zerr(&format!(
3962 // c:5014
3963 "failed to create named pipe: {}, {}",
3964 tnam,
3965 std::io::Error::last_os_error()
3966 ));
3967 return None; // c:5015
3968 }
3969 Some(tnam) // c:5017
3970}
3971
3972/// Port of `Eprog parsecmd(char *cmd, char **eptr)` from `Src/exec.c:4878`.
3973///
3974/// C body:
3975/// ```c
3976/// char *str;
3977/// Eprog prog;
3978/// for (str = cmd + 2; *str && *str != Outpar; str++);
3979/// if (!*str || cmd[1] != Inpar) {
3980/// char *errstr = dupstrpfx(cmd, 2);
3981/// untokenize(errstr);
3982/// zerr("unterminated `%s...)'", errstr);
3983/// return NULL;
3984/// }
3985/// *str = '\0';
3986/// if (eptr) *eptr = str+1;
3987/// if (!(prog = parse_string(cmd + 2, 0))) {
3988/// zerr("parse error in process substitution");
3989/// return NULL;
3990/// }
3991/// return prog;
3992/// ```
3993///
3994/// Port of `readoutput()` from `Src/exec.c:4805` — C decl `readoutput(int in, int qt, int *readerror)`.
3995/// Drain a command-substitution pipe fd and
3996/// return the captured output split per `qt`.
3997///
3998/// `qt=1` (quoted-substitution `"$(...)"`): single-element vec with
3999/// the trailing-newline-trimmed buffer (empty buffer → `Nularg` sentinel
4000/// per c:4861).
4001/// `qt=0` (unquoted `$(...)`): split on IFS via `spacesplit`; if
4002/// `GLOBSUBST` is set, each word is `shtokenize`d for downstream globbing.
4003///
4004/// `readerror` is set to the errno on read failure, 0 on clean EOF.
4005pub fn readoutput(in_fd: i32, qt: i32, readerror: &mut i32) -> Vec<String> {
4006 // c:4805
4007 let mut buf: Vec<u8> = Vec::with_capacity(64); // c:4816 (initial bsiz=64)
4008 let mut readret: isize = 0; // c:4818 readret tracks last read return
4009 // c:4824 dont_queue_signals(); c:4825 child_unblock(); — signal-queue
4010 // dance keeps SIGCHLD live so the foreground process can be reaped
4011 // while we drain. zshrs's in-process command-sub runs without the
4012 // queue (no fork), but the C call surface is preserved for parity.
4013 dont_queue_signals(); // c:4824
4014 child_unblock(); // c:4825
4015 let mut inbuf = [0u8; 64]; // c:4815 inbuf[64]
4016 loop {
4017 // c:4826
4018 // c:4828 — `readret = read(in, inbuf, 64);`
4019 let r = unsafe { libc::read(in_fd, inbuf.as_mut_ptr() as *mut libc::c_void, inbuf.len()) };
4020 readret = r as isize;
4021 if readret <= 0 {
4022 // c:4829
4023 if readret < 0 && std::io::Error::last_os_error().raw_os_error() == Some(libc::EINTR) {
4024 // c:4830 — `if (readret < 0 && errno == EINTR) continue;`
4025 continue;
4026 }
4027 break; // c:4832
4028 }
4029 // c:4835-4849 — `for (bufptr = inbuf; ...) { c = *bufptr; if
4030 // (imeta(c)) { *ptr++ = Meta; c ^= 32; cnt++; } ... *ptr++ = c; }`
4031 //
4032 // !!! DELIBERATE DEVIATION — the byte-level metafication is DROPPED.
4033 //
4034 // C's buffer is a `char *` that may hold arbitrary bytes, so it
4035 // escapes every `imeta` byte (NUL, and Meta 0x83 .. Marker 0xa2 —
4036 // `Src/utils.c:4195-4201`) to keep raw input bytes from colliding
4037 // with the token bytes the lexer/glob layers use.
4038 //
4039 // zshrs's buffer is a Rust `String`. A lone 0x83 is not valid UTF-8,
4040 // so `String::from_utf8_lossy` below replaced every emitted `Meta`
4041 // with U+FFFD — the metafied form could never survive the conversion,
4042 // and the "escaped" payload byte was left stranded next to a
4043 // replacement char. `utils::spacesplit`'s Meta decoder
4044 // (`utils.rs:5307`, `bytes[i] == Meta`) can never see a real Meta in a
4045 // `String` for the same reason, so nothing downstream un-did it
4046 // either. The loop was pure corruption for any input containing a
4047 // byte in {0x00} ∪ [0x83,0xa2] — which includes the UTF-8 tails of
4048 // common characters (U+2003 EM SPACE is `e2 80 83`, U+2022 BULLET is
4049 // `e2 80 a2`).
4050 //
4051 // The in-process `$( cmd )` arm in `getoutput` (:619) never had a
4052 // metafication step at all — it receives a plain `String` from
4053 // `run_command_substitution`. Dropping it here is what makes the two
4054 // arms hand this tail the SAME representation, which is the
4055 // property C gets for free by routing both arms through `readoutput`.
4056 buf.extend_from_slice(&inbuf[..readret as usize]); // c:4848
4057 }
4058 child_block(); // c:4854
4059 // c:4855 — `if (readerror) *readerror = readret < 0 ? errno : 0;`
4060 *readerror = if readret < 0 {
4061 std::io::Error::last_os_error().raw_os_error().unwrap_or(0)
4062 } else {
4063 0
4064 };
4065 // c:4857 — `close(in);`
4066 unsafe {
4067 libc::close(in_fd);
4068 }
4069 // ── c:4858-4872 tail ──────────────────────────────────────────────
4070 // !!! THIS BLOCK IS DUPLICATED at `getoutput` (:619), which is the
4071 // `$( cmd )` arm. C needs no duplicate: both of getoutput's arms
4072 // (c:4746 `$(< file)` and c:4772 the pipe) call `readoutput`, so they
4073 // share this tail by construction. zshrs collapses the fork, so its
4074 // `$( cmd )` arm holds an in-memory String and has no fd to hand
4075 // `readoutput`. Factoring the tail into a helper is what build.rs's
4076 // no-new-functions gate on src/ported/ forbids (there is no C function
4077 // to name it after), and its own remedy #1 is "inline the body at every
4078 // call site" — so the two copies must be kept in lockstep BY HAND.
4079 // ANY EDIT HERE MUST BE MIRRORED AT :619 AND VICE VERSA. The copies
4080 // already drifted once: the getoutput copy (:619) had silently lost the
4081 // c:4868-4869 `isset(GLOBSUBST)` → `shtokenize` step, so `setopt
4082 // globsubst; echo $(echo '*')` did not glob while `$(< f)` did.
4083 // c:4835-4849 — this is where C's `metafy` lands the read bytes. The
4084 // byte-level loop stays dropped (see the deviation note at :3799), but
4085 // the CONVERSION must still be lossless: `String::from_utf8_lossy` here
4086 // turned every byte that is not valid UTF-8 into U+FFFD, so `$(< f)` on
4087 // a latin-1 file printed `caf\xef\xbf\xbd` where zsh prints `caf\xe9`.
4088 // `decode_script_bytes` keeps valid UTF-8 as `char`s and Meta-encodes
4089 // only the bytes that cannot be, which `utils::unmetafy_str` turns back
4090 // into the original byte at the output boundary.
4091 let s = crate::script_bytes::decode_script_bytes(&buf);
4092 // c:4858-4859 — `while (cnt && ptr[-1] == '\n') ptr--, cnt--;`
4093 let s = s.trim_end_matches('\n');
4094 // c:4861-4863 — qt branch: empty → Nularg sentinel; else single elem.
4095 if qt != 0 {
4096 // c:4861
4097 if s.is_empty() {
4098 return vec![String::from(Nularg)]; // c:4862
4099 }
4100 return vec![s.to_string()]; // c:4864
4101 }
4102 // c:4866-4871 — `spacesplit` + per-word GLOBSUBST `shtokenize`.
4103 let mut words = crate::ported::utils::spacesplit(s, false, false); // c:4867
4104 if isset(crate::ported::zsh_h::GLOBSUBST) {
4105 // c:4870
4106 for w in words.iter_mut() {
4107 crate::ported::glob::shtokenize(w); // c:4870
4108 }
4109 }
4110 words
4111}
4112
4113/// Port of `parsecmd()` from `Src/exec.c:4934` — C decl `parsecmd(char *cmd, char **eptr)`.
4114/// Lex a `<(...)`/`>(...)`/`=(...)` body — the leading 2 chars are
4115/// the marker pair (`Inang+Inpar`, `Outang+Inpar`, `Equals+Inpar`),
4116/// remainder is the command up to the matching `Outpar`. Returns the
4117/// parsed Eprog (and writes the post-`)` cursor through `eptr`, as a
4118/// BYTE offset into `cmd`). The tokens are multi-byte in a Rust `String`,
4119/// so the scan walks chars: comparing single bytes against `Inpar` /
4120/// `Outpar` (as the port did) could never match and every call failed.
4121pub fn parsecmd(cmd: &str, eptr: Option<&mut usize>) -> Option<eprog> {
4122 // c:4934
4123 let mut it = cmd.char_indices();
4124 let _marker = it.next(); // cmd[0]
4125 let second = it.next().map(|(_, c)| c); // cmd[1]
4126 let body_start = it.next().map_or(cmd.len(), |(i, _)| i); // cmd + 2
4127 // c:4939 — `for (str = cmd + 2; *str && *str != Outpar; str++);`
4128 let close = cmd[body_start..].find(Outpar).map(|i| body_start + i);
4129 // c:4940 — `if (!*str || cmd[1] != Inpar)`.
4130 let close = match close {
4131 Some(c) if second == Some(Inpar) => c,
4132 _ => {
4133 // c:4941-4946 — `This can happen if the expression is being parsed
4134 // inside another construct, e.g. as a value within ${..:..} etc.
4135 // So print a proper error message instead of the not very
4136 // useful but traditional "oops".`
4137 let pfx_end = cmd.char_indices().nth(2).map_or(cmd.len(), |(i, _)| i);
4138 let errstr = untokenize(&cmd[..pfx_end]); // c:4947-4948
4139 zerr(&format!("unterminated `{}...)'", errstr)); // c:4949
4140 return None; // c:4950
4141 }
4142 };
4143 // c:4952 — `*str = '\0';` — cmd[close] becomes the terminator.
4144 // c:4953-4954 — `if (eptr) *eptr = str + 1;`
4145 if let Some(p) = eptr {
4146 *p = close + Outpar.len_utf8();
4147 }
4148 // c:4955 — `parse_string(cmd + 2, 0)`.
4149 let prog = parse_string(&cmd[body_start..close], 0);
4150 if prog.is_none() {
4151 // c:4955
4152 zerr("parse error in process substitution"); // c:4956
4153 return None; // c:4957
4154 }
4155 prog // c:4959
4156}
4157
4158
4159/// `POUNDBANGLIMIT` from `Src/exec.c:500` — max bytes read from the
4160/// front of a script when probing for a `#!` shebang line.
4161pub const POUNDBANGLIMIT: usize = 128;
4162
4163/// Port of `makecline()` from `Src/exec.c:2046` — C decl `makecline(LinkList list)`.
4164///
4165/// Builds the argv array from a command's args list. The C version
4166/// allocates with a 4-slot prepad (2 reserved at the front for the
4167/// shebang `argv[-1]/argv[-2]` overwrite trick in zexecve) — Rust
4168/// doesn't need this since we rebuild the Vec on shebang re-exec
4169/// (see zexecve WARNING e).
4170///
4171/// XTRACE side-effect: each arg is printed via quotedzputs to xtrerr
4172/// (stderr), preceded by the PS4 prefix when first command of the line.
4173pub fn makecline(list: &[String]) -> Vec<String> {
4174 // c:2046
4175 if isset(XTRACE) {
4176 // c:2055
4177 if doneps4.load(Ordering::Relaxed) == 0 {
4178 // c:2056
4179 printprompt4(); // c:2057
4180 }
4181 let mut first = true;
4182 let mut err = std::io::stderr().lock();
4183 use std::io::Write;
4184 for s in list.iter() {
4185 // c:2059
4186 if !first {
4187 let _ = err.write_all(b" "); // c:2063
4188 }
4189 first = false;
4190 let _ = err.write_all(quotedzputs(s).as_bytes()); // c:2061
4191 }
4192 let _ = err.write_all(b"\n"); // c:2065
4193 let _ = err.flush(); // c:2066
4194 }
4195 list.to_vec() // c:2071-2072 — argv built; null terminator implicit in CString[] conversion
4196}
4197
4198/// Port of `execute()` from `Src/exec.c:723` — C decl `execute(LinkList args, int flags, int defpath)`.
4199/// The canonical "child runs the simple
4200/// external command" path: STTY/ARGV0/BINF_DASH handling, makecline,
4201/// closem(FDT_XTRACE) + child_unblock, slash-path direct exec,
4202/// defpath (`command -p`) search, cmdnamtab + $PATH walk, with
4203/// commandnotfound-handler fallback and the final exit-code escape
4204/// (127 not-found / 126 noperm).
4205///
4206/// =================== WARNING — DIVERGENCE ====================
4207/// (a) `cmdnamtab->getnode(cmdnamtab, arg0)` (c:824) — HASHED
4208/// fast-path wired via cmdnamtab_lock(); jumps direct to
4209/// `cn.cmd` absolute path before the $PATH scan. Unhashed
4210/// cursor-walk (c:830-846) still falls to the full $PATH scan;
4211/// observable behavior matches C when the hash hit is HASHED.
4212/// (b) `commandnotfound(arg0, args)` (c:809, 873) calls into the
4213/// `doshfunc` for the `command_not_found_handler` shell function.
4214/// Routes through executor dispatch (see exec.rs:2783).
4215/// (c) `_realexit()` (c:810, 874) — `builtin::_realexit`.
4216/// (d) `SHTTY` close on `!FD_CLOEXEC` (c:781-784) — Rust assumes
4217/// FD_CLOEXEC platform default (macOS, Linux).
4218/// (e) `path` Rust accessor uses paramtab lookup for "PATH";
4219/// `defpath` (`command -p`) walks DEFAULT_PATH via
4220/// search_defpath (already ported).
4221/// =============================================================
4222pub fn execute(args: &mut Vec<String>, flags: u32, defpath: i32) {
4223 // c:723
4224 let mut eno: i32 = 0;
4225 let mut ee: i32; // c:729
4226 // c:737 — `arg0 = (char *) peekfirst(args);` — the COMMAND WORD.
4227 let arg0 = if args.is_empty() {
4228 return;
4229 } else {
4230 args[0].clone()
4231 }; // c:737
4232 // c:733-748 — STTY pre-exec handling.
4233 {
4234 let mut stty = STTYval.lock().unwrap();
4235 if let Some(s) = stty.take() {
4236 // c:738 — STTYval = 0 to break recursion.
4237 if !s.is_empty()
4238 && unsafe { libc::isatty(0) } != 0
4239 && unsafe { libc::tcgetpgrp(0) } == unsafe { libc::getpid() }
4240 {
4241 drop(stty);
4242 let cmd = format!("stty {}", s); // c:739
4243 execstring(&cmd, 1, 0, "stty"); // c:743
4244 }
4245 }
4246 }
4247 // c:758-777 — ARGV0 override.
4248 // c:758-759 — /* If ARGV0 is in the commands environment, we use *
4249 // * that as argv[0] for this external command */
4250 // NOTE: C rewrites only `argv[0]` (the exec argv), NEVER `arg0`
4251 // (`peekfirst(args)`, c:737) — `arg0` stays the COMMAND WORD and is what
4252 // the slash-exec / cmdnamtab / $PATH scan below resolves. The previous
4253 // Rust port assigned `arg0 = args[0].clone()` in both arms, so
4254 // `ARGV0=sh $cmdvar args` looked up `sh` on $PATH instead of `$cmdvar`
4255 // (E03posix:16 — `command not found: sh`), and a `-` precommand made the
4256 // shell search for `-name`.
4257 if let Some(z) = zgetenv("ARGV0") {
4258 args[0] = z.clone(); // c:761 `argv[0] = ztrdup(z);`
4259 unsafe {
4260 let key = std::ffi::CString::new("ARGV0").unwrap();
4261 libc::unsetenv(key.as_ptr()); // c:768
4262 }
4263 } else if (flags & BINF_DASH) != 0 {
4264 // c:772-776 — /* Else if the pre-command `-' was given, we add `-' *
4265 // * to the front of argv[0] for this command. */
4266 args[0] = format!("-{}", arg0); // c:775-776
4267 }
4268 let argv = makecline(args); // c:771
4269 let newenvp_owned: Option<Vec<String>> = if (flags & BINF_CLEARENV) != 0 {
4270 Some(Vec::new()) // c:772-773 — blank_env: char ** with only NULL slot
4271 } else {
4272 None
4273 };
4274 let newenvp = newenvp_owned.as_deref();
4275 closem(FDT_XTRACE, 0); // c:779
4276 // c:780-785 — !FD_CLOEXEC SHTTY close — WARNING (d).
4277 child_unblock(); // c:786
4278 if arg0.len() >= libc::PATH_MAX as usize {
4279 // c:787
4280 zerr(&format!("command too long: {}", arg0)); // c:788
4281 unsafe {
4282 libc::_exit(1);
4283 } // c:789
4284 }
4285 // c:791-801 — slash in arg0 → direct exec.
4286 if let Some(slash_pos) = arg0.find('/') {
4287 let lerrno = zexecve(&arg0, &argv, newenvp); // c:793
4288 let is_dot = arg0.starts_with('.')
4289 && (slash_pos == 1 || (arg0.len() > 2 && &arg0[..2] == ".." && slash_pos == 2));
4290 if slash_pos == 0 || unset(PATHDIRS) || is_dot {
4291 // c:794
4292 // c:797 — `zerr("%e: %s", lerrno, arg0)`. `%e` is zerrmsg's
4293 // errno arm (Src/utils.c:348-365): strerror with the first
4294 // letter lowercased, NOT Rust's `io::Error` Display (which
4295 // appends " (os error N)" and keeps the capital, so `~`
4296 // printed `Permission denied (os error 13): /Users/wizard`
4297 // where zsh prints `permission denied: /Users/wizard`).
4298 zerr(&format!(
4299 "{}: {}",
4300 crate::ported::utils::zsh_errno_msg(lerrno),
4301 arg0
4302 )); // c:797
4303 let code = if lerrno == libc::EACCES || lerrno == libc::ENOEXEC {
4304 126
4305 } else {
4306 127
4307 };
4308 unsafe {
4309 libc::_exit(code);
4310 } // c:798
4311 }
4312 }
4313 if defpath != 0 {
4314 // c:804 — `command -p` default-path search.
4315 let pbuf = match search_defpath(&arg0, libc::PATH_MAX as usize) {
4316 Some(p) => p, // c:808
4317 None => {
4318 if commandnotfound(&arg0, args) == 0 {
4319 // c:809
4320 unsafe {
4321 libc::_exit(LASTVAL.load(Ordering::Relaxed));
4322 }
4323 }
4324 zerr(&format!("command not found: {}", crate::ported::utils::nicedupstring(&arg0))); // c:818 — `%s` is nicezputs (c:Src/utils.c:316)
4325 unsafe {
4326 libc::_exit(127);
4327 } // c:812
4328 }
4329 };
4330 ee = zexecve(&pbuf, &argv, newenvp); // c:815
4331 let dir = pbuf.rsplit_once('/').map(|(d, _)| d).unwrap_or("");
4332 if isgooderr(ee, if dir.is_empty() { "/" } else { dir }) {
4333 // c:819
4334 eno = ee;
4335 }
4336 } else {
4337 // c:822 — cmdnamtab fast-path: if `arg0` is a hashed cmdnam,
4338 // jump straight to the absolute path stored in `cn.cmd`,
4339 // skipping the full $PATH scan (one exec attempt vs N).
4340 // c:824 — `if ((cn = cmdnamtab->getnode(cmdnamtab, arg0)))`.
4341 let hashed_path: Option<String> = {
4342 let tab = cmdnamtab_lock().read().ok();
4343 tab.and_then(|t| {
4344 t.get(&arg0).and_then(|cn| {
4345 if (cn.node.flags & crate::ported::zsh_h::HASHED) != 0 {
4346 // c:827-828 — `strcpy(nn, cn->u.cmd);`
4347 cn.cmd.clone()
4348 } else {
4349 None
4350 }
4351 })
4352 })
4353 };
4354 if let Some(nn) = hashed_path {
4355 // c:848 — `ee = zexecve(nn, argv, newenvp);`
4356 ee = zexecve(&nn, &argv, newenvp);
4357 let dir = nn.rsplit_once('/').map(|(d, _)| d).unwrap_or("");
4358 if isgooderr(ee, if dir.is_empty() { "/" } else { dir }) {
4359 eno = ee;
4360 }
4361 // If the hashed entry's exec failed without a "good" error,
4362 // we still need the $PATH fallback — fall through.
4363 if eno == 0 && ee != 0 {
4364 // Reset for the $PATH scan below.
4365 ee = 0;
4366 }
4367 }
4368 // c:822 — normal $PATH scan (always runs; cmdnam fast-path was an
4369 // optimization but C also walks the rest of `path` if the hashed
4370 // exec failed with a non-"good" error).
4371 let path_str = getsparam("PATH").unwrap_or_default();
4372 for pp in path_str.split(':') {
4373 if pp.is_empty() || pp == "." {
4374 // c:856
4375 ee = zexecve(&arg0, &argv, newenvp); // c:857
4376 if isgooderr(ee, pp) {
4377 eno = ee;
4378 }
4379 } else {
4380 // c:860
4381 let candidate = format!("{}/{}", pp, arg0); // c:861-864
4382 ee = zexecve(&candidate, &argv, newenvp); // c:865
4383 if isgooderr(ee, pp) {
4384 eno = ee;
4385 }
4386 }
4387 }
4388 }
4389 // c:871-881 — final error reporting.
4390 if eno != 0 {
4391 // c:871
4392 // c:872 — `zerr("%e: %s", eno, arg0)`; same `%e` (Src/utils.c:
4393 // 348-365) rendering as the slash-in-arg0 branch above.
4394 zerr(&format!(
4395 "{}: {}",
4396 crate::ported::utils::zsh_errno_msg(eno),
4397 arg0
4398 )); // c:872
4399 } else if commandnotfound(&arg0, args) == 0 {
4400 // c:873
4401 unsafe {
4402 libc::_exit(LASTVAL.load(Ordering::Relaxed));
4403 } // c:874
4404 } else {
4405 zerr(&format!("command not found: {}", crate::ported::utils::nicedupstring(&arg0))); // c:903 — `%s` is nicezputs (c:Src/utils.c:316)
4406 }
4407 let code = if eno == libc::EACCES || eno == libc::ENOEXEC {
4408 126
4409 } else {
4410 127
4411 }; // c:881
4412 unsafe {
4413 libc::_exit(code);
4414 }
4415}
4416
4417/// Port of `zexecve()` from `Src/exec.c:504` — C decl `zexecve(char *pth, char **argv, char **newenvp)`.
4418/// Wraps `execve(2)` with:
4419/// - `$_` env var stamped to absolute `pth` (c:514-520)
4420/// - winch signal unblock right before the syscall (c:527)
4421/// - on `ENOEXEC` / `ENOENT`: reads the first POUNDBANGLIMIT
4422/// bytes, parses a `#!interp arg` shebang and re-execs the
4423/// interpreter (c:534-628). For `ENOEXEC` with no shebang,
4424/// binary-safety check then falls back to `/bin/sh script` per
4425/// POSIX (c:588-628).
4426///
4427/// Returns `errno` from the failing exec — execve only returns on
4428/// failure, so success means the calling process is already replaced.
4429///
4430/// =================== WARNING — DIVERGENCE ====================
4431/// (a) C uses `static char buf[PATH_MAX*2+1]` for the `_=...` env
4432/// string; Rust uses a stack `String` (consumed by `zputenv`).
4433/// (b) `closedumps()` for `!FD_CLOEXEC` (c:521-523) called
4434/// unconditionally as a no-op when FD_CLOEXEC is platform default.
4435/// (c) `unmetafy(pth, NULL)` / round-trip `metafy` at c:510-513,
4436/// c:639-642 — handled implicitly via &str ↔ CString.
4437/// (d) `metafy(execvebuf+2, -1, META_STATIC)` (c:551, 575) — we
4438/// drop the metafy and pass byte ranges to zerr directly.
4439/// (e) `argv[-1]` / `argv[-2]` shebang interpreter slot-overwriting
4440/// (C overwrites BEFORE `argv[0]`) — Rust rebuilds a fresh
4441/// `Vec<String>` with interp + optional arg + original argv tail
4442/// since Vec doesn't expose negative indexing.
4443/// (f) `environ` is FFI-loaded only when `newenvp` is None.
4444/// =============================================================
4445pub fn zexecve(pth: &str, argv: &[String], newenvp: Option<&[String]>) -> i32 {
4446 // c:504
4447 use std::ffi::CString;
4448 // c:514-520 — `_=pth` env stamping.
4449 let pth_abs = if pth.starts_with('/') {
4450 // c:516
4451 pth.to_string() // c:517
4452 } else {
4453 // c:518
4454 format!("{}/{}", getsparam("PWD").unwrap_or_default(), pth) // c:519
4455 };
4456 zputenv(&format!("_={}", pth_abs)); // c:520
4457 closedumps(); // c:522
4458 winch_unblock(); // c:527
4459 let cpth = match CString::new(pth) {
4460 Ok(c) => c,
4461 Err(_) => return libc::ENOENT,
4462 };
4463 let cargs: Vec<CString> = argv
4464 .iter()
4465 .filter_map(|a| CString::new(a.as_str()).ok())
4466 .collect();
4467 let mut argv_ptrs: Vec<*const libc::c_char> = cargs.iter().map(|c| c.as_ptr()).collect();
4468 argv_ptrs.push(std::ptr::null());
4469 let env_holder: Vec<CString>;
4470 let env_ptrs: Vec<*const libc::c_char>;
4471 let envp: *const *const libc::c_char = match newenvp {
4472 Some(env) => {
4473 env_holder = env
4474 .iter()
4475 .filter_map(|e| CString::new(e.as_str()).ok())
4476 .collect();
4477 env_ptrs = {
4478 let mut v: Vec<*const libc::c_char> =
4479 env_holder.iter().map(|c| c.as_ptr()).collect();
4480 v.push(std::ptr::null());
4481 v
4482 };
4483 env_ptrs.as_ptr()
4484 }
4485 None => unsafe {
4486 extern "C" {
4487 static environ: *const *const libc::c_char;
4488 }
4489 environ
4490 },
4491 };
4492 unsafe {
4493 libc::execve(cpth.as_ptr(), argv_ptrs.as_ptr(), envp); // c:528
4494 }
4495 let eno = std::io::Error::last_os_error()
4496 .raw_os_error()
4497 .unwrap_or(libc::ENOEXEC); // c:534
4498 if eno == libc::ENOEXEC || eno == libc::ENOENT {
4499 // c:534 — the `#!` / shebang-less recovery; see zexecve_recover.
4500 match crate::vm_helper::zexecve_recover(pth, argv, eno) {
4501 // c:566/571/581/585/627 — `execve(<prog>, <argv>, newenvp);`
4502 Ok((prog, argv_new)) => return zexecve(&prog, &argv_new, newenvp),
4503 Err(e) => return e, // c:643
4504 }
4505 }
4506 eno // c:643
4507}
4508
4509/// Port of `getoutputfile()` from `Src/exec.c:4910` — C decl `getoutputfile(char *cmd, char **eptr)`.
4510/// `=(cmd)` process substitution.
4511///
4512/// Substitutes the cmd's stdout into a temp file, returns the
4513/// filename. Optimised path: `=(<<<heredoc-str)` writes the
4514/// heredoc body directly without a fork.
4515///
4516/// (a) `addfilelist(nam, 0)` (c:4960) wired via `JOBTAB[thisjob]`
4517/// so the temp file gets cleaned at job exit.
4518/// (b) `waitforpid` Rust takes 1 arg `pid`, C takes `(pid, full)`.
4519/// Behavior matches the `full=0` case anyway.
4520/// (c) This is a REAL fork (`zfork` below), not a collapsed one, so the
4521/// child gets the full `entersubsh(ESUB_PGRP|ESUB_NOMONITOR, NULL)`
4522/// (c:4986) via the canonical port at exec.rs:4329 — no in-process
4523/// `SubshStateGuard` substitute is needed or wanted here.
4524/// (d) `execode` is now ported (exec.rs:6047) — the body still
4525/// re-feeds through fusevm for cache coherence with execstring.
4526/// (e) The child ends with `_realexit()` (c:4991) after flushing
4527/// Rust's in-process stdout buffer.
4528/// (f) The TMPSUFFIX link() block (c:4951-4958) hard-links the temp
4529/// file to `nam` + `$TMPSUFFIX`; both names are registered for
4530/// cleanup.
4531pub fn getoutputfile(cmd: &str, eptr: Option<&mut usize>) -> Option<String> {
4532 // c:4910
4533 let bytes = cmd.as_bytes();
4534 let _ = bytes;
4535 // c:4918 — `if (thisjob == -1)` — guard removed (thisjob model differs).
4536 let mut ends_at: usize = 0;
4537 let prog = parsecmd(cmd, Some(&mut ends_at))?; // c:4922
4538 if let Some(p) = eptr {
4539 *p = ends_at;
4540 }
4541 let mut nam = gettempname(None, true)?; // c:4924
4542 // c:4927 — `simple_redir_name` opt for `=(<<<str)`.
4543 let mut s: Option<String> = simple_redir_name(&prog, REDIR_HERESTR).map(|raw| {
4544 // c:4933
4545 let mut sub = singsub(&raw); // c:4933
4546 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
4547 // c:4934
4548 String::new() // c:4935 — sentinel; checked below
4549 } else {
4550 sub = untokenize(&sub); // c:4937
4551 dyncat(&sub, "\n") // c:4938
4552 }
4553 });
4554 if let Some(ref sv) = s {
4555 if sv.is_empty() {
4556 s = None;
4557 }
4558 }
4559 if s.is_none() {
4560 // c:4942
4561 child_block(); // c:4943
4562 }
4563 // c:4945 — `open(nam, O_WRONLY|O_CREAT|O_EXCL|O_NOCTTY, 0600)`.
4564 let c_nam = match std::ffi::CString::new(nam.clone()) {
4565 Ok(c) => c,
4566 Err(_) => {
4567 if s.is_none() {
4568 child_unblock();
4569 }
4570 return None;
4571 }
4572 };
4573 let fd = unsafe {
4574 libc::open(
4575 c_nam.as_ptr(),
4576 libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | libc::O_NOCTTY,
4577 0o600 as libc::c_uint,
4578 )
4579 };
4580 if fd < 0 {
4581 // c:4945
4582 zerr(&format!(
4583 "process substitution failed: {}",
4584 std::io::Error::last_os_error()
4585 )); // c:4946
4586 if s.is_none() {
4587 child_unblock(); // c:4948
4588 }
4589 return None; // c:4949
4590 }
4591 // c:4951-4958 — `char *suffix = getsparam("TMPSUFFIX"); if (suffix &&
4592 // *suffix && !strstr(suffix, "/")) { suffix = dyncat(nam,
4593 // unmeta(suffix)); if (link(nam, suffix) == 0) { addfilelist(nam, 0);
4594 // nam = suffix; } }`
4595 let mut linked_orig: Option<String> = None;
4596 if let Some(suffix) = getsparam("TMPSUFFIX") {
4597 if !suffix.is_empty() && !suffix.contains('/') {
4598 let linkname = dyncat(&nam, &unmeta(&suffix));
4599 let c_link = std::ffi::CString::new(linkname.clone());
4600 if let Ok(c_link) = c_link {
4601 if unsafe { libc::link(c_nam.as_ptr(), c_link.as_ptr()) } == 0 {
4602 linked_orig = Some(std::mem::replace(&mut nam, linkname));
4603 }
4604 }
4605 }
4606 }
4607 // c:4960 — `addfilelist(nam, 0);` — register temp file in current
4608 // job's filelist so it's unlinked at job exit (not relying on the
4609 // OS temp-reaper).
4610 // zshrs runs commands in-process and the VM owns that job-end unlink
4611 // (the `=(cmd)` temp files the compiled path creates are parked there
4612 // too), so the parent also parks the name there on every return below.
4613 // Not before the fork: the child's own commands would drain the
4614 // inherited list and unlink the file it is writing.
4615 if let Some(jt) = JOBTAB.get() {
4616 let mut guard = jt.lock().unwrap();
4617 let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
4618 if tj >= 0 {
4619 if let Some(j) = guard.get_mut(tj as usize) {
4620 if let Some(orig) = &linked_orig {
4621 crate::ported::jobs::addfilelist(j, Some(orig), 0); // c:4955
4622 }
4623 crate::ported::jobs::addfilelist(j, Some(&nam), 0);
4624 }
4625 }
4626 }
4627 if let Some(sv) = s {
4628 // c:4962 — optimised here-string write path.
4629 let mut buf: Vec<u8> = sv.into_bytes();
4630 let _len = unmetafy(&mut buf); // c:4965
4631 let _ = write_loop(fd, &buf); // c:4966
4632 unsafe {
4633 libc::close(fd);
4634 } // c:4967
4635 if let Some(orig) = &linked_orig {
4636 crate::fusevm_bridge::psub_pending_file_add(orig);
4637 }
4638 crate::fusevm_bridge::psub_pending_file_add(&nam);
4639 return Some(nam); // c:4968
4640 }
4641 // c:4971 — `cmdoutpid = pid = zfork(NULL)`.
4642 let pid = zfork(None);
4643 cmdoutpid.store(pid, Ordering::Relaxed);
4644 if pid == -1 {
4645 // c:4972
4646 unsafe {
4647 libc::close(fd);
4648 } // c:4973
4649 child_unblock(); // c:4974
4650 if let Some(orig) = &linked_orig {
4651 crate::fusevm_bridge::psub_pending_file_add(orig);
4652 }
4653 crate::fusevm_bridge::psub_pending_file_add(&nam);
4654 return Some(nam); // c:4975
4655 } else if pid != 0 {
4656 // c:4976 — parent.
4657 unsafe {
4658 libc::close(fd);
4659 } // c:4977
4660 let _ = waitforpid(pid); // c:4978
4661 cmdoutval.store(0, Ordering::Relaxed); // c:4979
4662 if let Some(orig) = &linked_orig {
4663 crate::fusevm_bridge::psub_pending_file_add(orig);
4664 }
4665 crate::fusevm_bridge::psub_pending_file_add(&nam);
4666 return Some(nam); // c:4980
4667 }
4668 // c:4983 — child.
4669 closem(FDT_UNUSED, 0); // c:4984
4670 let _ = redup(fd, 1); // c:4985
4671 entersubsh(esub::PGRP | esub::NOMONITOR, None); // c:4986
4672 cmdpush(CS_CMDSUBST as u8); // c:4987
4673 // c:4988 — execode — WARNING (d).
4674 // The re-fed text is what parsecmd parsed: after the two marker chars,
4675 // up to the Outpar ending at `ends_at` (a byte offset). The markers are
4676 // multi-byte chars, so a `cmd[2..]` byte slice split them.
4677 let body_start = cmd.char_indices().nth(2).map_or(cmd.len(), |(i, _)| i);
4678 let body_end = ends_at.saturating_sub(Outpar.len_utf8());
4679 let body = if body_end > body_start && body_end <= cmd.len() {
4680 untokenize(&cmd[body_start..body_end])
4681 } else {
4682 String::new()
4683 };
4684 let _ = crate::ported::exec::execute_script_zsh_pipeline(&body);
4685 cmdpop(); // c:4989
4686 unsafe {
4687 libc::close(1);
4688 } // c:4990
4689 // Rust's stdout is buffered in-process; C builtins fflush their own.
4690 let _ = std::io::Write::flush(&mut std::io::stdout());
4691 crate::ported::builtin::_realexit(); // c:4991
4692 #[allow(unreachable_code)]
4693 {
4694 // c:4992-4993 — `zerr("exit returned in child!!"); kill(getpid(), SIGKILL);`
4695 let _ = &mut nam;
4696 unsafe {
4697 libc::kill(libc::getpid(), libc::SIGKILL);
4698 }
4699 None
4700 }
4701}
4702
4703/// Port of `getproc()` from `Src/exec.c:5025` — C decl `getproc(char *cmd, char **eptr)`.
4704/// `<(cmd)` / `>(cmd)` process substitution
4705/// via `/dev/fd/N` (PATH_DEV_FD branch; modern Linux/macOS).
4706///
4707/// (a) PATH_DEV_FD branch only — the FIFO fallback (`!PATH_DEV_FD`
4708/// path c:5037-5064) is omitted; modern Linux/macOS both
4709/// provide /dev/fd. `namedpipe()` is ported (exec.rs:2701) but
4710/// unused here.
4711/// (b) `addproc` is 7-arg; procsubst pid recorded via aux=true on
4712/// the current job (c:5141-5142).
4713/// (c) `addfilelist(NULL, fd)` wired via `JOBTAB[thisjob]` at
4714/// c:5087.
4715/// (d) `entersubsh` is ported at exec.rs:4329 — wired below at
4716/// c:5095 (`entersubsh(ESUB_ASYNC|ESUB_PGRP, NULL)`) in the real
4717/// forked child.
4718/// (e) `execode` is ported at exec.rs:6047. Body still re-feeds
4719/// through fusevm for cache coherence.
4720/// (f) The child ends with `_realexit()` (c:5104) after flushing
4721/// Rust's in-process stdout buffer.
4722/// (g) `fdtable[fd] = FDT_PROC_SUBST` (c:5086) — set via fdtable_set.
4723pub fn getproc(cmd: &str, eptr: Option<&mut usize>) -> Option<String> {
4724 // c:5025
4725 let bytes = cmd.as_bytes();
4726 let out: i32 = if cmd.starts_with(Inang) {
4727 1 // c:5032 — `<(...)` writer-side child
4728 } else {
4729 0
4730 };
4731 // c:5068-5071 — `if (thisjob == -1) { zerr(...); return NULL; }` —
4732 // proc subst needs a host job to attach the child to.
4733 let tj_check = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
4734 if tj_check == -1 {
4735 zerr(&format!("process substitution {} cannot be used here", cmd)); // c:5069
4736 return None; // c:5070
4737 }
4738 // c:5072 — PATH_DEV_FD path: allocate buffer for the /dev/fd/N string.
4739 let mut ends_at: usize = 0;
4740 let _prog = parsecmd(cmd, Some(&mut ends_at))?; // c:5073
4741 if let Some(p) = eptr {
4742 *p = ends_at;
4743 }
4744 let mut pipes: [i32; 2] = [-1; 2];
4745 if mpipe(&mut pipes) < 0 {
4746 // c:5075
4747 return None;
4748 }
4749 let mut bgtime: ZshTimespec = libc::timespec {
4750 tv_sec: 0,
4751 tv_nsec: 0,
4752 };
4753 let pid = zfork(Some(&mut bgtime)); // c:5077
4754 if pid != 0 {
4755 // c:5077 — parent path.
4756 let pnam = format!("{}/{}", crate::ported::config_h::PATH_DEV_FD, pipes[(1 - out) as usize]); // c:5144
4757 let _ = zclose(pipes[out as usize]); // c:5079
4758 if pid == -1 {
4759 // c:5080
4760 let _ = zclose(pipes[(1 - out) as usize]); // c:5082
4761 return None; // c:5083
4762 }
4763 let fd = pipes[(1 - out) as usize]; // c:5085
4764 fdtable_set(fd, FDT_PROC_SUBST); // c:5086
4765 // c:5087 — `addfilelist(NULL, fd);` — register the proc-subst
4766 // pipe fd in the current job's filelist so it's closed at job exit.
4767 if let Some(jt) = JOBTAB.get() {
4768 let mut guard = jt.lock().unwrap();
4769 let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
4770 if tj >= 0 {
4771 if let Some(j) = guard.get_mut(tj as usize) {
4772 crate::ported::jobs::addfilelist(j, None, fd);
4773 }
4774 }
4775 }
4776 // c:5088-5091 — `if (!out) addproc(pid, NULL, 1, &bgtime, -1, -1);` —
4777 // record the proc-subst writer-side child in the job's
4778 // auxprocs (aux=true). For `<(cmd)` (out==1 = reader-side
4779 // child), C omits the addproc — symmetric here.
4780 if out == 0 {
4781 if let Some(jt) = JOBTAB.get() {
4782 let mut guard = jt.lock().unwrap();
4783 let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
4784 if tj >= 0 {
4785 if let Some(j) = guard.get_mut(tj as usize) {
4786 crate::ported::jobs::addproc(
4787 j,
4788 pid,
4789 "",
4790 true,
4791 Some(std::time::Instant::now()),
4792 -1,
4793 -1,
4794 );
4795 }
4796 }
4797 }
4798 }
4799 procsubstpid.store(pid, Ordering::Relaxed); // c:5092
4800 return Some(pnam); // c:5093
4801 }
4802 // c:5095 — child.
4803 entersubsh(esub::ASYNC | esub::PGRP, None); // c:5095
4804 let _ = redup(pipes[out as usize], out); // c:5096
4805 closem(FDT_UNUSED, 0); // c:5097
4806 cmdpush(CS_CMDSUBST as u8); // c:5100
4807 // The re-fed text is what parsecmd parsed: after the two marker chars,
4808 // up to the Outpar ending at `ends_at` (a byte offset). The markers are
4809 // multi-byte chars, so a `cmd[2..]` byte slice split them.
4810 let body_start = cmd.char_indices().nth(2).map_or(cmd.len(), |(i, _)| i);
4811 let body_end = ends_at.saturating_sub(Outpar.len_utf8());
4812 let body = if body_end > body_start && body_end <= cmd.len() {
4813 untokenize(&cmd[body_start..body_end])
4814 } else {
4815 String::new()
4816 };
4817 let _ = crate::ported::exec::execute_script_zsh_pipeline(&body);
4818 cmdpop(); // c:5102
4819 let _ = zclose(out); // c:5103
4820 let _ = std::io::Write::flush(&mut std::io::stdout());
4821 crate::ported::builtin::_realexit(); // c:5104
4822}
4823
4824/// Port of `enum { ESUB_ASYNC, ESUB_PGRP, ... };` from `Src/exec.c:1056`.
4825/// Flag bits for `entersubsh(int flags, struct entersubsh_ret *retp)`.
4826pub mod esub {
4827 // c:1056
4828 /// `ASYNC` constant.
4829 pub const ASYNC: i32 = 0x01; // c:1058
4830 /// `PGRP` constant.
4831 pub const PGRP: i32 = 0x02; // c:1063
4832 /// `KEEPTRAP` constant.
4833 pub const KEEPTRAP: i32 = 0x04; // c:1065
4834 /// `FAKE` constant.
4835 pub const FAKE: i32 = 0x08; // c:1067
4836 /// `REVERTPGRP` constant.
4837 pub const REVERTPGRP: i32 = 0x10; // c:1069
4838 /// `NOMONITOR` constant.
4839 pub const NOMONITOR: i32 = 0x20; // c:1071
4840 /// `JOB_CONTROL` constant.
4841 pub const JOB_CONTROL: i32 = 0x40; // c:1073
4842}
4843
4844/// Port of `struct entersubsh_ret` from `Src/exec.c` (forward decl).
4845/// Out-arg used by `entersubsh()` to hand back the group-leader pid
4846/// and the list-pipe job index the parent should track. Only filled
4847/// in for `ESUB_PGRP` + non-async forks (synchronous pipeline child
4848/// groups).
4849#[allow(non_camel_case_types)]
4850#[derive(Default)]
4851pub struct entersubsh_ret {
4852 pub gleader: i32, // c:1122
4853 pub list_pipe_job: i32, // c:1123
4854}
4855
4856/// Port of `entersubsh()` from `Src/exec.c:1084` — C decl `entersubsh(int flags, struct entersubsh_ret *retp)`.
4857/// Called by every child fork to switch the
4858/// process into subshell mode: traps reset, monitor disabled, signals
4859/// re-defaulted, pgrp + tty handed off, saved fds closed, jobtab
4860/// cleared, ZSH_SUBSHELL bumped, forklevel = locallevel.
4861///
4862/// (a) `jobtab[list_pipe_job]` / `jobtab[thisjob]` pgrp ops (c:1110-
4863/// 1151) are now ported via `JOBTAB[thisjob]`.gleader access; the
4864/// ESUB_PGRP+sync path establishes pipeline group-leadership
4865/// (list_pipe_job inherit or thisjob-as-leader), filling
4866/// entersubsh_ret with the chosen gleader + list_pipe_job index.
4867/// (b) `clearjobtab(monitor)` (c:1219) — Rust signature is
4868/// `clearjobtab(&mut JobTable, monitor)`; we get the global table
4869/// via a TABLE handle similar to other jobs.rs entries.
4870/// (c) `attachtty(...)` (c:1119, 1144) — wired via libc::tcsetpgrp(2, gleader).
4871/// (d) `release_pgrp()` called for ESUB_REVERTPGRP when `getpid() ==
4872/// mypgrp` — direct C parity (jobs.rs:3406 provides the call).
4873/// (e) `opts[USEZLE] = 0; zleactive = 0` — Rust opts table lookup
4874/// uses `opts_set_off(USEZLE)`; zleactive is the atomic in
4875/// builtins/sched.rs.
4876/// =============================================================
4877pub fn entersubsh(flags: i32, retp: Option<&mut entersubsh_ret>) {
4878 // c:1083
4879 let monitor: i32;
4880 let job_control_ok: i32;
4881 // c:1088-1092 — reset traps unless KEEPTRAP.
4882 if (flags & esub::KEEPTRAP) == 0 {
4883 // c:1088
4884 for sig in 0..=SIGCOUNT {
4885 // c:1089
4886 let st = {
4887 let guard = sigtrapped.lock().unwrap();
4888 guard.get(sig as usize).copied().unwrap_or(0)
4889 };
4890 let func_set = (st & ZSIG_FUNC) != 0; // c:1090
4891 let posix_ignored = isset(POSIXTRAPS) && ((st & ZSIG_IGNORED) != 0); // c:1091
4892 if !func_set && !posix_ignored {
4893 unsettrap(sig); // c:1092
4894 }
4895 }
4896 }
4897 monitor = if isset(MONITOR) { 1 } else { 0 }; // c:1093
4898 job_control_ok = if monitor != 0 && (flags & esub::JOB_CONTROL) != 0 && isset(POSIXJOBS) {
4899 // c:1094
4900 1
4901 } else {
4902 0
4903 };
4904 EXIT_VAL.store(0, Ordering::Relaxed); // c:1095
4905 if (flags & esub::NOMONITOR) != 0 {
4906 // c:1096
4907 dosetopt(MONITOR, 0, 0); // c:1097
4908 }
4909 if !isset(MONITOR) {
4910 // c:1098
4911 if (flags & esub::ASYNC) != 0 {
4912 // c:1099
4913 let _ = settrap(libc::SIGINT, None, 0); // c:1100
4914 let _ = settrap(libc::SIGQUIT, None, 0); // c:1101
4915 if unsafe { libc::isatty(0) } != 0 {
4916 // c:1102
4917 unsafe {
4918 libc::close(0);
4919 } // c:1103
4920 let devnull = std::ffi::CString::new("/dev/null").unwrap();
4921 if unsafe { libc::open(devnull.as_ptr(), libc::O_RDWR | libc::O_NOCTTY) } != 0 {
4922 // c:1104
4923 zerr(&format!(
4924 // c:1105
4925 "can't open /dev/null: {}",
4926 std::io::Error::last_os_error()
4927 ));
4928 unsafe {
4929 libc::_exit(1);
4930 } // c:1106
4931 }
4932 }
4933 }
4934 } else if (flags & esub::PGRP) != 0 {
4935 // c:1110 — `else if (thisjob != -1 && (flags & ESUB_PGRP))`.
4936 let thisjob = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
4937 if thisjob != -1 {
4938 let lpj = list_pipe_job.load(Ordering::Relaxed);
4939 let lp = list_pipe.load(Ordering::Relaxed);
4940 let lpc = list_pipe_child.load(Ordering::Relaxed);
4941 if let Some(jt) = JOBTAB.get() {
4942 let mut guard = jt.lock().unwrap();
4943 let lpj_gleader = guard.get(lpj as usize).map(|j| j.gleader).unwrap_or(0);
4944 if lpj_gleader != 0 && (lp != 0 || lpc != 0) {
4945 // c:1111-1124 — inherit list_pipe_job's group leader.
4946 let pgid = if unsafe { libc::setpgid(0, lpj_gleader) } == -1
4947 || (unsafe { libc::killpg(lpj_gleader, 0) } == -1
4948 && std::io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH))
4949 {
4950 // c:1115-1117 — primary group leader gone; this child becomes leader.
4951 let new_gl = if lpc != 0 {
4952 mypgrp.load(Ordering::Relaxed)
4953 } else {
4954 unsafe { libc::getpid() }
4955 };
4956 if let Some(j) = guard.get_mut(lpj as usize) {
4957 j.gleader = new_gl;
4958 }
4959 if let Some(j) = guard.get_mut(thisjob as usize) {
4960 j.gleader = new_gl;
4961 }
4962 unsafe { libc::setpgid(0, new_gl) };
4963 if (flags & esub::ASYNC) == 0 {
4964 unsafe { libc::tcsetpgrp(2, new_gl) }; // c:1119 attachtty
4965 }
4966 new_gl
4967 } else {
4968 lpj_gleader
4969 };
4970 if let Some(r) = retp {
4971 if (flags & esub::ASYNC) == 0 {
4972 r.gleader = pgid; // c:1122
4973 r.list_pipe_job = lpj; // c:1123
4974 }
4975 }
4976 } else {
4977 // c:1126-1151 — standard group-leader-takeover path.
4978 let thisjob_gleader =
4979 guard.get(thisjob as usize).map(|j| j.gleader).unwrap_or(0);
4980 if thisjob_gleader == 0 || unsafe { libc::setpgid(0, thisjob_gleader) } == -1 {
4981 let new_gl = unsafe { libc::getpid() };
4982 if let Some(j) = guard.get_mut(thisjob as usize) {
4983 j.gleader = new_gl; // c:1138
4984 }
4985 if lpj != thisjob {
4986 let lpj_was_unset = guard
4987 .get(lpj as usize)
4988 .map(|j| j.gleader == 0)
4989 .unwrap_or(true);
4990 if lpj_was_unset {
4991 if let Some(j) = guard.get_mut(lpj as usize) {
4992 j.gleader = new_gl; // c:1140-1141
4993 }
4994 }
4995 }
4996 unsafe { libc::setpgid(0, new_gl) }; // c:1142
4997 if (flags & esub::ASYNC) == 0 {
4998 unsafe { libc::tcsetpgrp(2, new_gl) }; // c:1144 attachtty
4999 if let Some(r) = retp {
5000 r.gleader = new_gl; // c:1146
5001 if lpj != thisjob {
5002 r.list_pipe_job = lpj; // c:1148
5003 }
5004 }
5005 }
5006 }
5007 }
5008 }
5009 } else {
5010 // No real job slot; basic setpgid fallback.
5011 unsafe { libc::setpgid(0, 0) };
5012 }
5013 }
5014 if (flags & esub::FAKE) == 0 {
5015 // c:1153
5016 subsh.store(1, Ordering::Relaxed); // c:1154
5017 }
5018 // c:1161 — `zsh_subshell++;` regardless of FAKE.
5019 zsh_subshell.fetch_add(1, Ordering::Relaxed);
5020 // c:1162 — `if ((flags & ESUB_REVERTPGRP) && getpid() == mypgrp)`.
5021 if (flags & esub::REVERTPGRP) != 0
5022 && unsafe { libc::getpid() } == mypgrp.load(Ordering::Relaxed)
5023 {
5024 release_pgrp(); // c:1163
5025 }
5026 *shout.lock().unwrap() = 0; // c:1164 — shout = NULL
5027 if (flags & esub::NOMONITOR) != 0 {
5028 // c:1165
5029 signal_ignore(libc::SIGTTOU); // c:1171
5030 signal_ignore(libc::SIGTTIN); // c:1172
5031 signal_ignore(libc::SIGTSTP); // c:1173
5032 } else if job_control_ok == 0 {
5033 // c:1174
5034 signal_default(libc::SIGTTOU); // c:1181
5035 signal_default(libc::SIGTTIN); // c:1182
5036 signal_default(libc::SIGTSTP); // c:1183
5037 }
5038 let interact = isset(INTERACTIVE); // c:1185 — Rust uses INTERACTIVE option as proxy
5039 if interact {
5040 signal_default(libc::SIGTERM); // c:1186
5041 let int_st = sigtrapped
5042 .lock()
5043 .unwrap()
5044 .get(libc::SIGINT as usize)
5045 .copied()
5046 .unwrap_or(0);
5047 if (int_st & ZSIG_IGNORED) == 0 {
5048 // c:1187
5049 signal_default(libc::SIGINT); // c:1188
5050 }
5051 let pipe_st = sigtrapped
5052 .lock()
5053 .unwrap()
5054 .get(libc::SIGPIPE as usize)
5055 .copied()
5056 .unwrap_or(0);
5057 if pipe_st == 0 {
5058 // c:1189
5059 signal_default(libc::SIGPIPE); // c:1190
5060 }
5061 }
5062 let quit_st = sigtrapped
5063 .lock()
5064 .unwrap()
5065 .get(libc::SIGQUIT as usize)
5066 .copied()
5067 .unwrap_or(0);
5068 if (quit_st & ZSIG_IGNORED) == 0 {
5069 // c:1192
5070 signal_default(libc::SIGQUIT); // c:1193
5071 }
5072 // c:1202-1205 — unblock any trapped signals while in `intrap`.
5073 if intrap.load(Ordering::Relaxed) != 0 {
5074 // c:1202
5075 for sig in 1..=SIGCOUNT {
5076 let st = sigtrapped
5077 .lock()
5078 .unwrap()
5079 .get(sig as usize)
5080 .copied()
5081 .unwrap_or(0);
5082 if st != 0 && st != ZSIG_IGNORED {
5083 // c:1204
5084 let m = signal_mask(sig);
5085 let _ = signal_unblock(&m); // c:1205
5086 }
5087 }
5088 }
5089 if job_control_ok == 0 {
5090 // c:1206
5091 dosetopt(MONITOR, 0, 0); // c:1207
5092 }
5093 dosetopt(USEZLE, 0, 0); // c:1208
5094 zleactive.store(0, Ordering::Relaxed); // c:1209
5095 // c:1214-1217 — close saved fds.
5096 let max = MAX_ZSH_FD.load(Ordering::Relaxed);
5097 for i in 10..=max {
5098 if (fdtable_get(i) & FDT_SAVED_MASK) != 0 {
5099 // c:1215
5100 let _ = zclose(i); // c:1216
5101 }
5102 }
5103 // c:1218-1219 — `clearjobtab(monitor);` — calls the canonical port
5104 // at jobs.rs:1695 which handles ALL the C body including the
5105 // oldjobtab snapshot path (c:1799-1817) under POSIXJOBS guard.
5106 let mut dummy_table = crate::exec_jobs::JobTable::new();
5107 crate::ported::jobs::clearjobtab(&mut dummy_table, monitor);
5108 let _ = get_usage(); // c:1220
5109 FORKLEVEL.store(
5110 // c:1221 — `forklevel = locallevel;`
5111 locallevel.load(Ordering::Relaxed),
5112 Ordering::Relaxed,
5113 );
5114 // c:1263-1264 (55061) — `ancestor_loops += loops; loops = 0;`
5115 crate::ported::builtin::ANCESTOR_LOOPS.fetch_add(
5116 crate::ported::builtin::LOOPS.swap(0, Ordering::Relaxed),
5117 Ordering::Relaxed,
5118 );
5119}
5120
5121/// RAII guard applying the fork-safe subset of
5122/// `entersubsh(ESUB_PGRP|ESUB_NOMONITOR, NULL)` (`Src/exec.c:1083`) around a
5123/// command substitution that zshrs runs **in-process**, restoring every
5124/// touched global on drop.
5125///
5126/// C's `getoutput()` forks, and only the child runs `entersubsh`:
5127///
5128/// ```c
5129/// /* pid == 0 */
5130/// child_unblock();
5131/// zclose(pipes[0]);
5132/// redup(pipes[1], 1);
5133/// entersubsh(ESUB_PGRP|ESUB_NOMONITOR, NULL); /* c:4781 */
5134/// cmdpush(CS_CMDSUBST);
5135/// execode(prog, 0, 1, "cmdsubst");
5136/// ```
5137///
5138/// so every state delta dies with the child and the parent is untouched.
5139/// zshrs collapses that fork (`getoutput` below → `run_command_substitution`),
5140/// which means the deltas were previously never applied **at all**: inside
5141/// `$( )` the shell kept the caller's `zleactive` / `opts[USEZLE]`, so every
5142/// reader of them took the "we are inside the line editor" branch. Observed
5143/// failure: `lt=( ${(f)"$(fc -l -200)"} )` bound to a ZLE widget died with
5144/// `no interactive history within ZLE` — `bin_fc`'s guard
5145/// (`builtin.rs:2757`, a correct port of `Src/builtin.c:1523-1527`) firing on
5146/// a stale `zleactive`. Same class of bug for every other reader:
5147/// `jobs.rs:1114` (time-report suppression), `utils.rs:1785` (screen
5148/// ownership), `signals.rs:2050` (refresh-on-signal).
5149///
5150/// APPLIED (pure restorable scalar state; correct without a fork):
5151/// * c:1097 / c:1207 — `opts[MONITOR] = 0`. ESUB_NOMONITOR is set and
5152/// `job_control_ok` is 0 (no ESUB_JOB_CONTROL), so both C branches
5153/// agree. A substitution body must not do job control.
5154/// * c:1164 — `shout = NULL`. Its readers already handle the NULL case
5155/// the way the C child does (`putshout` falls back to stderr,
5156/// `checkrmall` returns 1 at `utils.rs:3565` per c:2871, `mailcheck`
5157/// stays silent per c:1670).
5158/// * c:1208 — `opts[USEZLE] = 0`.
5159/// * c:1209 — `zleactive = 0`.
5160///
5161/// DELIBERATELY SKIPPED — each would be wrong, or is already handled
5162/// elsewhere on the in-process path:
5163/// * c:1127-1131 `unsettrap(sig)` — APPLIED, but not here: the trap table is
5164/// not scalar state (trap bodies and `sigaction` dispositions), so
5165/// `run_command_substitution` resets it with the same helper `( … )`
5166/// uses (`fusevm_bridge::entersubsh_reset_traps`) and puts the parent's
5167/// flags and dispositions back on exit.
5168/// * c:1095 `exit_val = 0` — ALREADY DONE on this path:
5169/// `vm_helper.rs:3893` swaps `EXIT_VAL` to 0 before the nested VM runs
5170/// and restores it at `vm_helper.rs:4004`.
5171/// * c:1110-1151 `setpgrp`/`attachtty`/gleader bookkeeping — process-group
5172/// and tty surgery. With no fork this moves the interactive shell's own
5173/// process group and hands the terminal away.
5174/// * c:1154 `subsh = 1` — `init.rs:2289` does `if subsh != 0 { realexit() }`.
5175/// With no fork, an `exit` inside `$( )` would then hard-kill the
5176/// interactive shell instead of ending the substitution.
5177/// * c:1161 `zsh_subshell++` — ALREADY DONE on this path by
5178/// `CmdSubstSubshellBump` (`fusevm_bridge.rs:232`, entered at
5179/// `vm_helper.rs:3734`). Bumping again would make `$ZSH_SUBSHELL` read 2
5180/// inside `$( )` instead of 1.
5181/// * c:1165-1193 `signal_ignore`/`signal_default` — per-process signal
5182/// dispositions. Defaulting SIGINT/SIGQUIT/SIGTSTP in the live
5183/// interactive shell would let ^C kill and ^Z stop the shell itself.
5184/// * c:1202-1205 `signal_unblock` — the process signal mask is owned by
5185/// the trap dispatcher's block/unblock pairing; unblocking here breaks
5186/// that pairing.
5187/// * c:1214-1217 close FDT_SAVED_MASK fds — fd surgery. These are the
5188/// parent's saved descriptors for redirection restore; closing them is
5189/// unrecoverable.
5190/// * c:1219 `clearjobtab(monitor)` — would destroy the parent's job table.
5191/// * c:1220 `get_usage()` — overwrites the parent's `times` baseline with
5192/// no fork to discard it, and nothing inside the substitution reads it.
5193/// * c:1221 `forklevel = locallevel` — FORKLEVEL gates local-scope
5194/// unwinding; the in-process body pushes and pops its own locals
5195/// normally, so moving the fork boundary would strand them.
5196///
5197/// Nesting is safe: each guard saves what it observed on entry and restores
5198/// exactly that on drop, so LIFO nesting (`$( … $( … ) … )`) unwinds
5199/// correctly.
5200pub struct SubshStateGuard {
5201 saved_monitor: bool,
5202 saved_shout: usize,
5203 saved_usezle: bool,
5204 saved_zleactive: i32,
5205 saved_subsh: i32,
5206 saved_loops: (i32, i32),
5207}
5208
5209/// The `zleactive` the shell had before the innermost in-process subshell
5210/// state was applied, or `-1` when no such state is in effect.
5211///
5212/// !!! WARNING: RUST-ONLY STATE !!! C has no counterpart and needs none.
5213/// `Src/exec.c:1248`'s `zleactive = 0;` is inside `entersubsh`, and every
5214/// caller reaches it in the FORKED CHILD — `getoutput`, the `$(...)` one,
5215/// calls it at `Src/exec.c:4838` under the `/* pid == 0 */` label
5216/// (c:4834), with the parent left at `waitforpid(pid, 0)` on c:4830.
5217/// The parent's `zleactive` is never touched,
5218/// so a signal handler that lands in the parent while a `$(...)` is running
5219/// still sees ZLE as active — which is the whole reason `adjustwinsize`'s
5220/// `if (zleactive && resetzle)` (`Src/utils.c:1954`) can repaint on a resize
5221/// that arrives during a completion.
5222///
5223/// `SubshStateGuard` runs that child-side write IN THE PARENT because zshrs
5224/// executes command substitutions in-process, so the parent's own handler
5225/// read 0 and declined to repaint. This publishes what C's parent would have
5226/// had, for the handler to consult. Only the outermost guard records, so a
5227/// nested substitution cannot overwrite it with its own zero.
5228pub static SUBSH_PARENT_ZLEACTIVE: std::sync::atomic::AtomicI32 =
5229 std::sync::atomic::AtomicI32::new(-1);
5230
5231/// Nesting depth of the in-process subshell state above.
5232pub static SUBSH_STATE_DEPTH: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
5233
5234impl SubshStateGuard {
5235 /// Apply the deltas listed above and capture the values to restore.
5236 pub fn enter() -> Self {
5237 let g = SubshStateGuard {
5238 saved_monitor: isset(MONITOR),
5239 saved_shout: *shout.lock().unwrap(),
5240 saved_usezle: isset(USEZLE),
5241 saved_zleactive: zleactive.load(Ordering::Relaxed),
5242 saved_subsh: subsh.load(Ordering::Relaxed),
5243 saved_loops: (
5244 crate::ported::builtin::LOOPS.load(Ordering::Relaxed),
5245 crate::ported::builtin::ANCESTOR_LOOPS.load(Ordering::Relaxed),
5246 ),
5247 };
5248 // c:1263-1264 (55061) — `ancestor_loops += loops; loops = 0;`
5249 crate::ported::builtin::ANCESTOR_LOOPS.fetch_add(
5250 crate::ported::builtin::LOOPS.swap(0, Ordering::Relaxed),
5251 Ordering::Relaxed,
5252 );
5253 // c:1153-1154 — `if (!(flags & ESUB_FAKE)) subsh = 1;`. The
5254 // substitution's body is "in a subshell" for every consumer that
5255 // asks, which is what keeps PRINT_EXIT_VALUE quiet inside
5256 // `x=$(false)` (c:4309 `&& !subsh`).
5257 subsh.store(1, Ordering::Relaxed); // c:1154
5258 // `force = 1`: C assigns the `opts[]` slots directly, so the
5259 // dosetopt gatekeeping (c:743-861, which only guards turning
5260 // options ON) must not apply in either direction.
5261 dosetopt(MONITOR, 0, 1); // c:1097 / c:1207
5262 *shout.lock().unwrap() = 0; // c:1164
5263 dosetopt(USEZLE, 0, 1); // c:1208
5264 // Publish the pre-substitution value BEFORE zeroing it, so a signal
5265 // handler that lands in this process during the substitution can read
5266 // the `zleactive` C's parent would still have. Outermost guard only.
5267 if SUBSH_STATE_DEPTH.fetch_add(1, Ordering::SeqCst) == 0 {
5268 SUBSH_PARENT_ZLEACTIVE.store(g.saved_zleactive, Ordering::SeqCst);
5269 }
5270 zleactive.store(0, Ordering::Relaxed); // c:1209
5271 g
5272 }
5273}
5274
5275impl Drop for SubshStateGuard {
5276 fn drop(&mut self) {
5277 // Reverse order of `enter`; the C child never restores because it
5278 // `_realexit()`s, so this half has no C counterpart to cite.
5279 subsh.store(self.saved_subsh, Ordering::Relaxed);
5280 crate::ported::builtin::LOOPS.store(self.saved_loops.0, Ordering::Relaxed);
5281 crate::ported::builtin::ANCESTOR_LOOPS.store(self.saved_loops.1, Ordering::Relaxed);
5282 if SUBSH_STATE_DEPTH.fetch_sub(1, Ordering::SeqCst) <= 1 {
5283 SUBSH_PARENT_ZLEACTIVE.store(-1, Ordering::SeqCst);
5284 }
5285 zleactive.store(self.saved_zleactive, Ordering::Relaxed);
5286 dosetopt(USEZLE, self.saved_usezle as i32, 1);
5287 *shout.lock().unwrap() = self.saved_shout;
5288 dosetopt(MONITOR, self.saved_monitor as i32, 1);
5289 }
5290}
5291
5292/// The state a forked subshell owns a private copy of — the shell's hash
5293/// tables, its environment, its working directory and its process
5294/// attributes — taken on entry to an in-process subshell and put back on
5295/// exit.
5296///
5297/// !!! WARNING: RUST-ONLY TYPE — C forks and needs none of this !!!
5298/// `getoutput` (`Src/exec.c:4816`) and `( … )` (`c:2880`) run the body in
5299/// a `zfork()` child. `entersubsh` (`c:1123-1261`) resets traps, job
5300/// control, `subsh` and `zsh_subshell`, but it does not touch any of the
5301/// state below: the child simply writes its fork-copied tables, its own
5302/// `environ`, its own cwd, umask and rlimits, and all of it dies at
5303/// `_realexit()` (`c:4843`). The parent never sees
5304/// x=$(alias a=b; export v=1; cd /tmp; umask 077)
5305/// because nothing it owns was written. zshrs runs both forms IN
5306/// PROCESS, so each piece the body can write must be handed back here.
5307///
5308/// Cost of `save`, per substitution: every table is copy-on-write, so a
5309/// refcount bump each, and only a body that writes a table pays for
5310/// copying it; the environment is one byte copy (`environ_image`); the
5311/// directory stack and the two rlimit arrays are short vectors; the cwd
5312/// is one `stat` and the umask two `umask(2)` calls.
5313/// !!! WARNING: RUST-ONLY TYPE — C forks and needs none of this !!!
5314/// The entry-directory descriptor `SubshForkCopy` holds for the in-process
5315/// subshell. A forked child can never move its parent; zshrs's body shares
5316/// the process cwd, so the parent must be put back into the same directory
5317/// object, not the same path. The fd is recorded `FDT_INTERNAL`, like every
5318/// other descriptor the shell keeps for itself (`movefd`,
5319/// `Src/utils.c:1990-2011`), but sits past the script's `{var}` range
5320/// (`lowfd::movefd_past_script`), and is close-on-exec so an external command
5321/// run by the body never inherits it. Dropping closes it.
5322struct SubshCwdFd(i32);
5323
5324impl SubshCwdFd {
5325 fn open() -> Self {
5326 let fd = unsafe { libc::open(b".\0".as_ptr() as *const libc::c_char, libc::O_RDONLY | libc::O_CLOEXEC) };
5327 if fd < 0 {
5328 return SubshCwdFd(-1);
5329 }
5330 // Moved past the script's `{var}` range as well: a forked child has
5331 // no such descriptor, so `(exec {v}>f; print $v)` must still get 11.
5332 let moved = crate::lowfd::movefd_past_script(fd);
5333 if moved == fd {
5334 unsafe { libc::close(fd) };
5335 return SubshCwdFd(-1);
5336 }
5337 SubshCwdFd(moved)
5338 }
5339}
5340
5341impl Drop for SubshCwdFd {
5342 fn drop(&mut self) {
5343 if self.0 >= 0 {
5344 crate::ported::utils::zclose(self.0);
5345 }
5346 }
5347}
5348
5349pub struct SubshForkCopy {
5350 /// `aliastab` (`Src/hashtable.c:1177`) — `alias`, `unalias`,
5351 /// `disable -a`, `aliases[x]=…`.
5352 aliastab: crate::ported::hashtable::alias_table,
5353 /// `sufaliastab` (`Src/hashtable.c:1182`) — `alias -s`, `unalias -s`.
5354 sufaliastab: crate::ported::hashtable::alias_table,
5355 /// `nameddirtab` (`Src/hashnameddir.c:48`) — `hash -d`, `unhash -d`,
5356 /// `nameddirs[x]=…`, and the nodes a `~name` lookup adds.
5357 nameddirtab: crate::cow_map::CowArc<crate::ported::hashtable::hashtable_nodes<crate::ported::zsh_h::nameddir>>,
5358 /// `allusersadded` (`Src/hashnameddir.c:53`) — set by a
5359 /// `fillnameddirtable` that ran in the body; the parent's table
5360 /// never got those user entries, so its flag must not claim it did.
5361 allusersadded: i32,
5362 /// `cmdnamtab` (`Src/hashtable.c:590`) — `hash`, `unhash`, `hash -r`,
5363 /// `commands[x]=…`, and a fill run by reading `$commands`.
5364 cmdnamtab: crate::ported::hashtable::cmdnam_table,
5365 /// `pathchecked` (`Src/hashtable.c:595`) — how far a fill has walked
5366 /// `$path`; it describes `cmdnamtab`, so it travels with it.
5367 pathchecked: usize,
5368 /// `environ` — every `export`, `unset` of an exported name, `cd`
5369 /// (`PWD`/`OLDPWD`) and `allexport` assignment writes it with
5370 /// `setenv`/`unsetenv` (`Src/params.c:5320`, `c:5537`).
5371 environ: crate::ported::params::environ_image,
5372 /// The process working directory, as the device and inode of `.`.
5373 /// `cd`, `pushd` and `popd` in the body move it for the whole
5374 /// process; `restore` compares and moves back only if they did.
5375 cwd: Option<(u64, u64)>,
5376 /// An fd open on the entry directory itself. `restore` goes back
5377 /// through it, so a body that renames the directory out from under
5378 /// the parent (`( cd .. && mv foo bar )` run from `foo`) still returns
5379 /// the parent to the very directory it was in, as a fork does.
5380 cwd_fd: SubshCwdFd,
5381 /// `$PWD` on entry — the fallback when `cwd_fd` could not be opened.
5382 pwd: Option<String>,
5383 /// `dirstack` (`Src/builtin.c:744`) — `pushd` / `popd`.
5384 dirstack: Vec<String>,
5385 /// The file-creation mask — `umask` (`Src/builtin.c:7483`).
5386 umask: libc::mode_t,
5387 /// `limits[]` and `current_limits[]` (`Src/exec.c:315`) — `ulimit`,
5388 /// `limit`, `unlimit`. `zfork` applies the child's copy to the child
5389 /// (`c:381-383` `setlimits(NULL)`), so the parent never sees it. Both
5390 /// arrays are needed and are rolled back differently: `limits[]` is
5391 /// what the shell WANTS, `current_limits[]` what `setrlimit` was
5392 /// actually given (`zsetlimit`, `c:319-332`, only calls `setrlimit`
5393 /// where they disagree), so `limit descriptors 256` without `-s`
5394 /// moves only the first.
5395 limits: Vec<libc::rlimit>,
5396 current_limits: Vec<libc::rlimit>,
5397 /// `zstyletab` (`Src/Modules/zutil.c:106`) — `zstyle`, `zstyle -d`.
5398 zstyletab: crate::ported::modules::zutil::style_table,
5399 /// `modulestab` (`Src/module.c:49`) as `(name → flags)`: `zmodload`
5400 /// flips `MOD_INIT_B` / `MOD_UNLOAD`, and loading a module the table
5401 /// has never seen adds a node (see `restore`).
5402 modules: std::collections::HashMap<String, i32>,
5403 /// Builtins carrying `DISABLED` in `builtintab` (`Src/builtin.c:146`)
5404 /// — `disable`/`enable` (`c:535-546`).
5405 builtins_disabled: std::collections::HashSet<String>,
5406 /// Reserved words carrying `DISABLED` in `reswdtab`
5407 /// (`Src/hashtable.c:1114`) — `disable -r`. Changes how the parent
5408 /// PARSES, e.g. `repeat 2 print r` stops being a loop.
5409 reswds_disabled: std::collections::HashSet<String>,
5410 /// `schedcmds` (`Src/Builtins/sched.c:52`) — `sched`.
5411 schedcmds: Option<Box<crate::ported::builtins::sched::schedcmd>>,
5412 /// `watch_fds` (`Src/Zle/zle_main.c:204`) — `zle -F`. A forked
5413 /// child's handlers die with it; kept here, a `$(cd x)` whose `chpwd`
5414 /// hook armed one (zinit's `@zinit-scheduler`: `exec {AFD}< <(…);
5415 /// zle -F $AFD @zinit-scheduler`) left the parent watching a descriptor
5416 /// the frame had closed: `failed to close file descriptor 17`.
5417 watch_fds: Vec<crate::ported::zle::zle_h::watch_fd>,
5418 /// `shtimer` (`Src/params.c:147`) — assigning `SECONDS` moves it.
5419 shtimer: std::time::Duration,
5420 /// `donetrap` (`Src/exec.c:1413`) — a ZERR trap that ran inside the body
5421 /// sets it (`c:1658`) in the forked child only. The parent's own check
5422 /// for the subshell's failing status (`c:1651-1659`) must still see the
5423 /// value it had on entry, or `(false)` runs ZERR once instead of twice.
5424 donetrap: i32,
5425 /// `zpc_disables[]` (`Src/pattern.c:4179`) — `disable -p` / `enable -p`.
5426 /// A fork leaves the parent's array alone, so `( disable -p '*' )`
5427 /// must not keep `*` disabled afterwards. Held as the
5428 /// `savepatterndisables()` bitmap (`c:4220`).
5429 pattern_disables: u32,
5430}
5431
5432impl SubshForkCopy {
5433 /// Take the parent's state. O(1) per table; see the type docs for
5434 /// the rest.
5435 pub fn save() -> Self {
5436 use std::os::unix::fs::MetadataExt;
5437 crate::ported::builtins::rlimits::ensure_limits_initialized();
5438 let rlimits = |a: &std::sync::OnceLock<std::sync::Mutex<Vec<libc::rlimit>>>| {
5439 a.get()
5440 .and_then(|l| l.lock().ok().map(|g| g.clone()))
5441 .unwrap_or_default()
5442 };
5443 SubshForkCopy {
5444 aliastab: crate::ported::hashtable::aliastab_lock()
5445 .read()
5446 .map(|t| t.snapshot())
5447 .unwrap_or_default(),
5448 sufaliastab: crate::ported::hashtable::sufaliastab_lock()
5449 .read()
5450 .map(|t| t.snapshot())
5451 .unwrap_or_default(),
5452 nameddirtab: crate::ported::hashnameddir::nameddirtab()
5453 .lock()
5454 .map(|t| t.clone())
5455 .unwrap_or_default(),
5456 allusersadded: crate::ported::hashnameddir::allusersadded.load(Ordering::Relaxed),
5457 cmdnamtab: crate::ported::hashtable::cmdnamtab_lock()
5458 .read()
5459 .map(|t| t.snapshot())
5460 .unwrap_or_default(),
5461 pathchecked: pathchecked.load(Ordering::SeqCst),
5462 environ: crate::ported::params::environ_image::save(),
5463 cwd: std::fs::metadata(".").ok().map(|m| (m.dev(), m.ino())),
5464 cwd_fd: SubshCwdFd::open(),
5465 pwd: getsparam("PWD"),
5466 dirstack: crate::ported::modules::parameter::DIRSTACK
5467 .lock()
5468 .map(|d| d.clone())
5469 .unwrap_or_default(),
5470 // umask(2) can only be read by setting it; put it straight back.
5471 umask: unsafe {
5472 let m = libc::umask(0o022);
5473 libc::umask(m);
5474 m
5475 },
5476 limits: rlimits(&crate::ported::builtins::rlimits::LIMITS),
5477 current_limits: rlimits(&crate::ported::builtins::rlimits::CURRENT_LIMITS),
5478 zstyletab: crate::ported::modules::zutil::zstyletab
5479 .lock()
5480 .map(|t| t.clone())
5481 .unwrap_or_default(),
5482 modules: crate::ported::module::MODULESTAB
5483 .lock()
5484 .map(|t| t.modules.iter().map(|(k, v)| (k.clone(), v.node.flags)).collect())
5485 .unwrap_or_default(),
5486 builtins_disabled: crate::ported::builtin::BUILTINS_DISABLED
5487 .lock()
5488 .map(|s| s.clone())
5489 .unwrap_or_default(),
5490 reswds_disabled: crate::ported::hashtable::reswdtab_lock()
5491 .read()
5492 .map(|t| {
5493 t.iter()
5494 .filter(|(_, r)| (r.node.flags & crate::ported::zsh_h::DISABLED as i32) != 0)
5495 .map(|(n, _)| n.clone())
5496 .collect()
5497 })
5498 .unwrap_or_default(),
5499 schedcmds: crate::ported::builtins::sched::schedcmd::subsh_save(),
5500 watch_fds: crate::ported::zle::zle_main::WATCH_FDS
5501 .lock()
5502 .map(|t| t.clone())
5503 .unwrap_or_default(),
5504 shtimer: crate::ported::params::shtimer_lock()
5505 .lock()
5506 .map(|t| *t)
5507 .unwrap_or_default(),
5508 donetrap: DONETRAP.load(Ordering::Relaxed),
5509 pattern_disables: crate::ported::pattern::savepatterndisables(),
5510 }
5511 }
5512
5513 /// Put the parent's state back, discarding whatever the subshell
5514 /// body did to it — the in-process stand-in for the child exiting.
5515 pub fn restore(self) {
5516 use std::os::unix::fs::MetadataExt;
5517 // modulestab first: rolling a module back runs `cleanup_module`
5518 // → `setfeatureenables` (`Src/module.c:3354`) → `deleteparamdef`
5519 // (c:1128), which looks its parameters up in the LIVE paramtab —
5520 // so callers restore this struct before they put paramtab back.
5521 //
5522 // A `zmodload zsh/X` for a module with no node yet takes
5523 // load_module's allocate-on-miss branch (c:2229-2237) and CREATES
5524 // the node. In C that node is allocated in the forked child and
5525 // dies with it; here it survives, and restoring flags alone would
5526 // never touch it — `(zmodload zsh/datetime)` left the parent with a
5527 // loaded node and `zmodload -e zsh/datetime` answering 0 where zsh
5528 // answers 1. So nodes the parent did not have are rolled back the
5529 // way a real unload does it — `cleanup_module` (c:1922) →
5530 // `finish_module` (c:1930), undoing the feature enables the load
5531 // made in the module's own statics — and dropped; then the flags
5532 // of the parent's nodes are put back.
5533 if let Ok(mut t) = crate::ported::module::MODULESTAB.lock() {
5534 let strays: Vec<String> = t
5535 .modules
5536 .keys()
5537 .filter(|n| !self.modules.contains_key(*n))
5538 .cloned()
5539 .collect();
5540 for name in &strays {
5541 let loaded = t
5542 .modules
5543 .get(name)
5544 .map(|m| (m.node.flags & crate::ported::zsh_h::MOD_INIT_B) != 0)
5545 .unwrap_or(false);
5546 if loaded {
5547 let _ = crate::ported::module::cleanup_module(&mut t, name);
5548 let _ = crate::ported::module::finish_module(&mut t, name);
5549 }
5550 }
5551 t.modules.retain(|name, _| self.modules.contains_key(name));
5552 for (name, saved_flags) in &self.modules {
5553 if let Some(m) = t.modules.get_mut(name) {
5554 m.node.flags = *saved_flags;
5555 }
5556 }
5557 }
5558 if let Ok(mut t) = crate::ported::modules::zutil::zstyletab.lock() {
5559 *t = self.zstyletab;
5560 }
5561 if let Ok(mut s) = crate::ported::builtin::BUILTINS_DISABLED.lock() {
5562 *s = self.builtins_disabled;
5563 }
5564 if let Ok(mut t) = crate::ported::hashtable::reswdtab_lock().write() {
5565 let names: Vec<String> = t.iter().map(|(n, _)| n.clone()).collect();
5566 for n in names {
5567 if self.reswds_disabled.contains(&n) {
5568 t.disable(&n);
5569 } else {
5570 t.enable(&n);
5571 }
5572 }
5573 }
5574 crate::ported::builtins::sched::schedcmd::subsh_restore(self.schedcmds);
5575 if let Ok(mut t) = crate::ported::zle::zle_main::WATCH_FDS.lock() {
5576 *t = self.watch_fds;
5577 }
5578 if let Ok(mut t) = crate::ported::params::shtimer_lock().lock() {
5579 *t = self.shtimer;
5580 }
5581 DONETRAP.store(self.donetrap, Ordering::Relaxed);
5582 crate::ported::pattern::restorepatterndisables(self.pattern_disables); // c:4258
5583 if let Ok(mut t) = crate::ported::hashtable::aliastab_lock().write() {
5584 t.restore(self.aliastab);
5585 }
5586 if let Ok(mut t) = crate::ported::hashtable::sufaliastab_lock().write() {
5587 t.restore(self.sufaliastab);
5588 }
5589 if let Ok(mut t) = crate::ported::hashnameddir::nameddirtab().lock() {
5590 *t = self.nameddirtab;
5591 }
5592 crate::ported::hashnameddir::allusersadded.store(self.allusersadded, Ordering::Relaxed);
5593 if let Ok(mut t) = crate::ported::hashtable::cmdnamtab_lock().write() {
5594 t.restore(self.cmdnamtab);
5595 }
5596 pathchecked.store(self.pathchecked, Ordering::SeqCst);
5597 self.environ.restore();
5598 let here = std::fs::metadata(".").ok().map(|m| (m.dev(), m.ino()));
5599 if here != self.cwd {
5600 // By descriptor first: the entry path may no longer name the
5601 // entry directory (the body renamed it), and chdir-by-path
5602 // would then leave the parent somewhere else entirely.
5603 let back = self.cwd_fd.0 >= 0 && unsafe { libc::fchdir(self.cwd_fd.0) } == 0;
5604 if !back {
5605 if let Some(pwd) = &self.pwd {
5606 let _ = std::env::set_current_dir(pwd);
5607 }
5608 }
5609 }
5610 if let Ok(mut d) = crate::ported::modules::parameter::DIRSTACK.lock() {
5611 *d = self.dirstack;
5612 }
5613 unsafe {
5614 libc::umask(self.umask);
5615 }
5616 // `zsetlimit` (c:319-332) applied to before/after: replay
5617 // `setrlimit` only for the resources whose `current_limits[]` the
5618 // BODY moved, so a `limit` the parent set without `-s` is not
5619 // suddenly installed here. A body that LOWERED a hard limit is the
5620 // one case this cannot undo — the process cannot raise it again;
5621 // C escapes that only because the child dies with it.
5622 if let Some(lock) = crate::ported::builtins::rlimits::CURRENT_LIMITS.get() {
5623 if let Ok(mut cur) = lock.lock() {
5624 for (i, want) in self.current_limits.iter().enumerate() {
5625 let Some(have) = cur.get(i).copied() else {
5626 continue;
5627 };
5628 if have.rlim_max == want.rlim_max && have.rlim_cur == want.rlim_cur {
5629 continue; // c:321-322
5630 }
5631 if unsafe { libc::setrlimit(i as _, want) } == 0 {
5632 cur[i] = *want; // c:329
5633 }
5634 }
5635 }
5636 }
5637 if let Some(lock) = crate::ported::builtins::rlimits::LIMITS.get() {
5638 if let Ok(mut g) = lock.lock() {
5639 *g = self.limits;
5640 }
5641 }
5642 }
5643}
5644
5645/// The descriptors a bare `exec` redirection moved inside an in-process
5646/// subshell, each with a copy of what it was before, and the ones the body
5647/// opened for itself (`{var}`, `sysopen -u`, `zsystem flock -f`).
5648///
5649/// !!! WARNING: RUST-ONLY TYPE — C forks and needs none of this !!!
5650/// `exec 3>file` / `exec 3>&-` with no command is permanent — c:4035
5651/// "specifically *don't* restore the original fd's" — but inside
5652/// `$( … )` or `( … )` it is permanent only for the forked child
5653/// (`Src/exec.c:4816`, `c:2880`), whose fd table dies with it. zshrs runs
5654/// the body in process, so the parent's descriptors are saved here the
5655/// first time the body's permanent redirection touches each one, and put
5656/// back when the frame is dropped; a descriptor from 10 up that the body
5657/// allocated is closed, as the child's exit would (`(exec {v}>f); exec
5658/// {w}>g` gives `w` 11 in zsh). A body that redirects nothing permanently
5659/// costs nothing.
5660pub struct SubshFdFrame {
5661 /// Only `enter` builds one, so every frame on `SUBSH_FD_FRAMES` has
5662 /// exactly one owner to pop it.
5663 _entered: (),
5664}
5665
5666thread_local! {
5667 /// `(fd, copy, fdtable kind)` per frame, innermost last; a copy of -1
5668 /// means the fd was closed on entry. Thread-local: a frame is entered and left on
5669 /// the thread that runs the body.
5670 static SUBSH_FD_FRAMES: std::cell::RefCell<Vec<Vec<(i32, i32, i32)>>> =
5671 const { std::cell::RefCell::new(Vec::new()) };
5672}
5673
5674impl SubshFdFrame {
5675 /// Open a frame for a subshell body.
5676 pub fn enter() -> Self {
5677 SUBSH_FD_FRAMES.with(|f| f.borrow_mut().push(Vec::new()));
5678 SubshFdFrame { _entered: () }
5679 }
5680
5681 /// Called before a permanent redirection changes `fd`: keep a copy
5682 /// (past the script's `{var}` range — see `lowfd::movefd_past_script`)
5683 /// unless the innermost frame already has one. Outside any frame
5684 /// the redirection really is permanent and nothing is kept.
5685 pub fn touch(fd: i32) {
5686 if fd < 0 {
5687 return;
5688 }
5689 SUBSH_FD_FRAMES.with(|f| {
5690 if let Some(frame) = f.borrow_mut().last_mut() {
5691 if !frame.iter().any(|(saved_fd, _, _)| *saved_fd == fd) {
5692 let copy = unsafe {
5693 libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, crate::lowfd::EXTENSION_FD_FLOOR)
5694 };
5695 frame.push((fd, copy, crate::ported::utils::fdtable_get(fd)));
5696 }
5697 }
5698 });
5699 }
5700
5701 /// Called when the body takes a descriptor from 10 up for the script
5702 /// (fdtable_set to a user kind): it did not exist for the parent, so
5703 /// the frame closes it on the way out.
5704 pub fn opened(fd: i32) {
5705 if fd < 10 {
5706 return;
5707 }
5708 SUBSH_FD_FRAMES.with(|f| {
5709 if let Ok(mut frames) = f.try_borrow_mut() {
5710 if let Some(frame) = frames.last_mut() {
5711 if !frame.iter().any(|(saved_fd, _, _)| *saved_fd == fd) {
5712 frame.push((fd, -1, crate::ported::zsh_h::FDT_UNUSED));
5713 }
5714 }
5715 }
5716 });
5717 }
5718}
5719
5720impl Drop for SubshFdFrame {
5721 /// Put the parent's descriptors back, most recent first.
5722 fn drop(&mut self) {
5723 let frame = SUBSH_FD_FRAMES.with(|f| f.borrow_mut().pop().unwrap_or_default());
5724 if frame.is_empty() {
5725 return;
5726 }
5727 // Bytes the body printed belong to the body's fd 1.
5728 let _ = std::io::Write::flush(&mut std::io::stdout());
5729 for (fd, copy, kind) in frame.into_iter().rev() {
5730 if copy >= 0 {
5731 unsafe {
5732 libc::dup2(copy, fd);
5733 libc::close(copy);
5734 }
5735 if fd >= 10 {
5736 crate::ported::utils::fdtable_set(fd, kind);
5737 }
5738 } else if fd >= 10 {
5739 crate::ported::utils::zclose(fd); // also drops the fdtable mark
5740 } else {
5741 unsafe { libc::close(fd) };
5742 }
5743 }
5744 }
5745}
5746
5747/// Port of `getpipe()` from `Src/exec.c:5119` — C decl `getpipe(char *cmd, int nullexec)`.
5748///
5749/// C body executes `<(cmd)` / `>(cmd)` process substitution via a
5750/// pipe pair: parent gets back the readable (`<(...)`) or writable
5751/// (`>(...)`) end as an fd; child runs the substituted command with
5752/// its stdio redirected into the other end.
5753///
5754/// ```c
5755/// Eprog prog;
5756/// int pipes[2], out = *cmd == Inang;
5757/// pid_t pid;
5758/// struct timespec bgtime;
5759/// char *ends;
5760/// if (!(prog = parsecmd(cmd, &ends))) return -1;
5761/// if (*ends) { zerr("invalid syntax..."); return -1; }
5762/// if (mpipe(pipes) < 0) return -1;
5763/// if ((pid = zfork(&bgtime))) {
5764/// zclose(pipes[out]);
5765/// if (pid == -1) { zclose(pipes[!out]); return -1; }
5766/// if (!nullexec) addproc(pid, NULL, 1, &bgtime, -1, -1);
5767/// procsubstpid = pid;
5768/// return pipes[!out];
5769/// }
5770/// entersubsh(ESUB_ASYNC|ESUB_PGRP|ESUB_NOMONITOR, NULL);
5771/// redup(pipes[out], out);
5772/// closem(FDT_UNUSED, 0);
5773/// cmdpush(CS_CMDSUBST);
5774/// execode(prog, 0, 1, out ? "outsubst" : "insubst");
5775/// cmdpop();
5776/// _realexit();
5777/// ```
5778///
5779/// (a) `addproc` is now 7-arg (jobs.rs:1516) — wired at the
5780/// procsubst pid recording site (c:5141-5142) earlier this
5781/// session; the child IS now recorded in `JOBTAB[thisjob]`.
5782/// (b) `entersubsh` IS ported (exec.rs:4329) including the
5783/// ESUB_PGRP pipeline group-leadership path, and is called from
5784/// the real forked child below for getpipe's
5785/// `entersubsh(ESUB_ASYNC|ESUB_PGRP|ESUB_NOMONITOR, NULL)`.
5786/// (c) `execode(prog, ...)` IS now ported (exec.rs:6047) — getpipe
5787/// can route through execode for the parsed eprog. Currently
5788/// this caller still uses the fusevm pipeline for cache
5789/// coherence with execstring; switch over when the wordcode
5790/// walker becomes the primary path.
5791/// (d) The child ends with `_realexit()` (c:5152) after flushing
5792/// Rust's in-process stdout buffer.
5793pub fn getpipe(cmd: &str, nullexec: i32) -> i32 {
5794 // c:5119
5795 let bytes = cmd.as_bytes();
5796 let out: i32 = if cmd.starts_with(Inang) {
5797 1 // c:5122 — `<(...)` reads from child, child writes to fd 1
5798 } else {
5799 0 // `>(...)` — child reads from fd 0
5800 };
5801 let mut ends_at: usize = 0;
5802 let prog = parsecmd(cmd, Some(&mut ends_at)); // c:5127
5803 if prog.is_none() {
5804 // c:5127
5805 return -1; // c:5128
5806 }
5807 // c:5129 — `if (*ends)` — trailing bytes after the `)` are invalid.
5808 if ends_at < bytes.len() && bytes[ends_at] != 0 {
5809 zerr("invalid syntax for process substitution in redirection"); // c:5130
5810 return -1; // c:5131
5811 }
5812 let mut pipes: [i32; 2] = [-1; 2];
5813 if mpipe(&mut pipes) < 0 {
5814 // c:5133
5815 return -1;
5816 }
5817 // c:5135 — `if ((pid = zfork(&bgtime)))` — parent path.
5818 let mut bgtime: ZshTimespec = libc::timespec {
5819 tv_sec: 0,
5820 tv_nsec: 0,
5821 };
5822 let pid = zfork(Some(&mut bgtime)); // c:5135
5823 if pid != 0 {
5824 // c:5135 — parent.
5825 let _ = zclose(pipes[out as usize]); // c:5136
5826 if pid == -1 {
5827 // c:5137
5828 let _ = zclose(pipes[(1 - out) as usize]); // c:5138
5829 return -1; // c:5139
5830 }
5831 // c:5141-5142 — `if (!nullexec) addproc(pid, NULL, 1, &bgtime, -1, -1);`
5832 if nullexec == 0 {
5833 if let Some(jt) = JOBTAB.get() {
5834 let mut guard = jt.lock().unwrap();
5835 let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
5836 if tj >= 0 {
5837 if let Some(j) = guard.get_mut(tj as usize) {
5838 crate::ported::jobs::addproc(
5839 j,
5840 pid,
5841 "",
5842 true, // aux=1 for proc subst
5843 Some(std::time::Instant::now()),
5844 -1,
5845 -1,
5846 );
5847 }
5848 }
5849 }
5850 }
5851 procsubstpid.store(pid, Ordering::Relaxed); // c:5143
5852 return pipes[(1 - out) as usize]; // c:5144
5853 }
5854 // c:5146 — child path.
5855 entersubsh(esub::ASYNC | esub::PGRP | esub::NOMONITOR, None); // c:5146
5856 let _ = redup(pipes[out as usize], out); // c:5147
5857 closem(FDT_UNUSED, 0); // c:5148
5858 cmdpush(CS_CMDSUBST as u8); // c:5149
5859 // c:5150 — execode(prog, 0, 1, ...) — see WARNING (c).
5860 // The re-fed text is what parsecmd parsed: after the two marker chars,
5861 // up to the Outpar ending at `ends_at` (a byte offset). The markers are
5862 // multi-byte chars, so a `cmd[2..]` byte slice split them.
5863 let body_start = cmd.char_indices().nth(2).map_or(cmd.len(), |(i, _)| i);
5864 let body_end = ends_at.saturating_sub(Outpar.len_utf8());
5865 let body = if body_end > body_start && body_end <= cmd.len() {
5866 untokenize(&cmd[body_start..body_end])
5867 } else {
5868 String::new()
5869 };
5870 let _ = crate::ported::exec::execute_script_zsh_pipeline(&body);
5871 cmdpop(); // c:5151
5872 let _ = std::io::Write::flush(&mut std::io::stdout());
5873 crate::ported::builtin::_realexit(); // c:5152
5874}
5875
5876/// Port of `spawnpipes()` from `Src/exec.c:5184` — C decl `spawnpipes(LinkList l, int nullexec)`.
5877///
5878/// Walks a redir list `l`, and for each REDIR_OUTPIPE/REDIR_INPIPE
5879/// entry fires `getpipe(name, nullexec || varid)` and stashes the
5880/// resulting fd into `f->fd2`.
5881///
5882/// ```c
5883/// LinkNode n;
5884/// Redir f;
5885/// char *str;
5886/// n = firstnode(l);
5887/// for (; n; incnode(n)) {
5888/// f = (Redir) getdata(n);
5889/// if (f->type == REDIR_OUTPIPE || f->type == REDIR_INPIPE) {
5890/// str = f->name;
5891/// f->fd2 = getpipe(str, nullexec || f->varid);
5892/// }
5893/// }
5894/// ```
5895///
5896/// =================== WARNING — DIVERGENCE ====================
5897/// The Rust port consumes a `&mut Vec<crate::ported::zsh_h::redir>`
5898/// in place of `LinkList`. The walk is identical; the only behavior
5899/// difference is that LinkList iteration in C lets callers splice
5900/// nodes mid-walk — we never do that here so it's a no-op divergence.
5901/// =============================================================
5902pub fn spawnpipes(l: &mut [redir], nullexec: i32) {
5903 // c:5184
5904 for f in l.iter_mut() {
5905 // c:5191
5906 if f.typ == REDIR_OUTPIPE || f.typ == REDIR_INPIPE {
5907 // c:5193
5908 let str_ = f.name.clone().unwrap_or_default(); // c:5194
5909 let nullexec_eff = if f.varid.as_deref().map_or(false, |v| !v.is_empty()) {
5910 1
5911 } else {
5912 nullexec
5913 };
5914 f.fd2 = getpipe(&str_, nullexec_eff); // c:5195
5915 }
5916 }
5917}
5918
5919/// Port of `cancd2()` from `Src/exec.c:6411` — C decl `cancd2(char *s)`.
5920///
5921/// C body:
5922/// ```c
5923/// struct stat buf;
5924/// char *us, *us2 = NULL;
5925/// int ret;
5926/// if (!isset(CHASEDOTS) && !isset(CHASELINKS)) {
5927/// if (*s != '/')
5928/// us = tricat(pwd[1] ? pwd : "", "/", s);
5929/// else
5930/// us = ztrdup(s);
5931/// fixdir(us2 = us);
5932/// } else
5933/// us = unmeta(s);
5934/// ret = !(access(us, X_OK) || stat(us, &buf) || !S_ISDIR(buf.st_mode));
5935/// if (us2) free(us2);
5936/// return ret;
5937/// ```
5938///
5939/// True iff `s` is a directory we can `cd` into (X-perm). With
5940/// `!CHASEDOTS && !CHASELINKS`, lexically canonicalise the path
5941/// (joining with PWD if relative) so `cd /foo/bar/..` works without
5942/// resolving the symlink. Otherwise pass `s` through `unmeta` to libc.
5943pub fn cancd2(s: &str) -> i32 {
5944 // c:6411
5945 let us: String;
5946 // c:6422 — `if (!isset(CHASEDOTS) && !isset(CHASELINKS))`.
5947 let chasedots = isset(CHASEDOTS); // c:6422
5948 let chaselinks = isset(CHASELINKS);
5949 if !chasedots && !chaselinks {
5950 // c:6422
5951 // c:6423-6426 — `*s != '/' ? tricat(pwd, "/", s) : ztrdup(s);`
5952 let pwd_str = getsparam("PWD").unwrap_or_default(); // c:6424 `pwd`
5953 let mut raw = if !s.starts_with('/') {
5954 // c:6423
5955 format!(
5956 "{}/{}",
5957 if pwd_str.len() > 1 { &pwd_str[..] } else { "" },
5958 s
5959 )
5960 } else {
5961 s.to_string()
5962 };
5963 // c:6427 — `fixdir(us2 = us);` — lexical canonicalisation.
5964 fixdir(&mut raw);
5965 us = raw;
5966 } else {
5967 // c:6428
5968 us = unmeta(s); // c:6429
5969 }
5970 // c:6430 — `!(access(us, X_OK) || stat(us, &buf) || !S_ISDIR(...))`.
5971 let cstr = match std::ffi::CString::new(us.as_str()) {
5972 Ok(c) => c,
5973 Err(_) => return 0,
5974 };
5975 if unsafe { libc::access(cstr.as_ptr(), libc::X_OK) } != 0 {
5976 return 0;
5977 }
5978 let meta = match std::fs::metadata(&us) {
5979 Ok(m) => m,
5980 Err(_) => return 0,
5981 };
5982 if !meta.file_type().is_dir() {
5983 return 0;
5984 }
5985 1
5986}
5987
5988/// Port of `cancd()` from `Src/exec.c:6370` — C decl `cancd(char *s)`.
5989///
5990/// Resolve a `cd` target against `$cdpath` and `cd_able_vars`.
5991/// Returns the chosen absolute path (heap-dup) if `cancd2` accepts
5992/// it, else `None`.
5993///
5994/// C body uses CDPATH walking + `cd_able_vars()` fallback. Sets
5995/// `doprintdir = -1` when a non-trivial path is found (so `cd`
5996/// echoes the resolved path).
5997pub fn cancd(s: &str) -> Option<String> {
5998 // c:6370
5999 // c:6372-6373 — `nocdpath = s[0]=='.' && (s[1]=='/' || !s[1] ||
6000 // (s[1]=='.' && (s[2]=='/' || !s[2])))`.
6001 let bytes = s.as_bytes();
6002 let nocdpath = bytes.first().copied() == Some(b'.')
6003 && (bytes.get(1).copied() == Some(b'/')
6004 || bytes.get(1).is_none()
6005 || (bytes.get(1).copied() == Some(b'.')
6006 && (bytes.get(2).copied() == Some(b'/') || bytes.get(2).is_none())));
6007 // c:6376 — `if (*s != '/')` branch.
6008 if !s.starts_with('/') {
6009 // c:6376
6010 // c:6379-6380 — `if (cancd2(s)) return s;`
6011 if cancd2(s) != 0 {
6012 return Some(s.to_string());
6013 }
6014 // c:6381-6382 — `if (access(unmeta(s), X_OK) == 0) return NULL;`
6015 let cstr = std::ffi::CString::new(unmeta(s).as_str()).ok()?;
6016 if unsafe { libc::access(cstr.as_ptr(), libc::X_OK) } == 0 {
6017 return None; // c:6382
6018 }
6019 // c:6383-6397 — CDPATH walk.
6020 if !nocdpath {
6021 let cdpath_str = getsparam("CDPATH").unwrap_or_default();
6022 for cp in cdpath_str.split(':') {
6023 // c:6384
6024 let sbuf = if !cp.is_empty() {
6025 format!("{}/{}", cp, s) // c:6386
6026 } else {
6027 s.to_string() // c:6391
6028 };
6029 if cancd2(&sbuf) != 0 {
6030 // c:6393
6031 DOPRINTDIR.store(-1, Ordering::Relaxed); // c:6394
6032 return Some(sbuf); // c:6395
6033 }
6034 }
6035 }
6036 // c:6398-6403 — `cd_able_vars()` fallback.
6037 if let Some(t) = cd_able_vars(s) {
6038 // c:6398
6039 if cancd2(&t) != 0 {
6040 // c:6399
6041 DOPRINTDIR.store(-1, Ordering::Relaxed); // c:6400
6042 return Some(t); // c:6401
6043 }
6044 }
6045 return None; // c:6404
6046 }
6047 // c:6406 — absolute path: `return cancd2(s) ? s : NULL;`
6048 if cancd2(s) != 0 {
6049 Some(s.to_string())
6050 } else {
6051 None
6052 }
6053}
6054
6055/// Port of `simple_redir_name()` from `Src/exec.c:4689` — C decl `simple_redir_name(Eprog prog, int redir_type)`.
6056///
6057/// Test if an Eprog encodes a single simple-command consisting of a
6058/// SINGLE redirection of the requested type with NO command body
6059/// (the `cat < foo` shape). When true, returns the redir target name
6060/// (heap-dup) so callers like `$(< file)` short-circuit to a direct
6061/// `open(2)` instead of fork+pipe+exec.
6062///
6063/// C body walks the wordcode at fixed offsets (`pc[0]` = WC_LIST,
6064/// `pc[1]` = WC_SUBLIST, `pc[2]` = WC_PIPE, `pc[3]` = WC_REDIR,
6065/// `pc[6]` = WC_SIMPLE with argc=0). zshrs's wordcode buffer is the
6066/// same shape — this port replicates the same offset reads.
6067pub fn simple_redir_name(prog: &eprog, redir_type: i32) -> Option<String> {
6068 // c:4689
6069 let pc = &prog.prog;
6070 // c:4694-4702 — guard chain. Walk the wordcode buffer at fixed
6071 // offsets matching C's `pc[0]..pc[6]` checks.
6072 if pc.len() < 7 {
6073 return None;
6074 }
6075
6076 if wc_code(pc[0]) != WC_LIST
6077 || (WC_LIST_TYPE(pc[0]) & Z_END as u32) == 0 // c:4695
6078 || wc_code(pc[1]) != WC_SUBLIST
6079 || WC_SUBLIST_FLAGS(pc[1]) != 0 // c:4696
6080 || WC_SUBLIST_TYPE(pc[1]) != WC_SUBLIST_END // c:4697
6081 || wc_code(pc[2]) != WC_PIPE
6082 || WC_PIPE_TYPE(pc[2]) != WC_PIPE_END // c:4698
6083 || wc_code(pc[3]) != WC_REDIR
6084 || WC_REDIR_TYPE(pc[3]) != redir_type // c:4699
6085 || WC_REDIR_VARID(pc[3]) != 0 // c:4700
6086 || pc[4] != 0 // c:4701
6087 || wc_code(pc[6]) != WC_SIMPLE
6088 || WC_SIMPLE_ARGC(pc[6]) != 0
6089 // c:4702
6090 {
6091 return None; // c:4706
6092 }
6093 // c:4703 — `return dupstring(ecrawstr(prog, pc + 5, NULL));`
6094 Some(dupstring(&ecrawstr(prog, 5, None)))
6095}
6096
6097/// Port of `getherestr()` from `Src/exec.c:4655` — C decl `getherestr(struct redir *fn)`.
6098///
6099/// C body:
6100/// ```c
6101/// char *s, *t;
6102/// int fd, len;
6103/// t = fn->name;
6104/// singsub(&t);
6105/// untokenize(t);
6106/// unmetafy(t, &len);
6107/// if (!(fn->flags & REDIRF_FROM_HEREDOC))
6108/// t[len++] = '\n';
6109/// if ((fd = gettempfile(NULL, 1, &s)) < 0)
6110/// return -1;
6111/// write_loop(fd, t, len);
6112/// close(fd);
6113/// fd = open(s, O_RDONLY | O_NOCTTY);
6114/// unlink(s);
6115/// return fd;
6116/// ```
6117///
6118/// Materialise a `<<<` herestring or unprocessed-here-doc body into a
6119/// tempfile, then re-open read-only and unlink — gives the consumer a
6120/// read fd whose backing file is already cleaned up.
6121pub fn getherestr(fn_: &redir) -> i32 {
6122 // c:4655
6123 let mut t: String = fn_.name.clone().unwrap_or_default(); // c:4660
6124 t = singsub(&t); // c:4661
6125 t = untokenize(&t); // c:4662
6126 // c:4663 — `unmetafy(t, &len);` — strip Meta-escapes.
6127 // Reuse the canonical unmetafy port (utils.rs) on a Vec<u8>.
6128 let mut bytes: Vec<u8> = t.into_bytes();
6129 let _len = unmetafy(&mut bytes);
6130 // c:4671-4672 — `if (!(fn->flags & REDIRF_FROM_HEREDOC)) t[len++] = '\n';`
6131 if (fn_.flags & REDIRF_FROM_HEREDOC) == 0 {
6132 // c:4671
6133 bytes.push(b'\n'); // c:4672
6134 }
6135 // c:4673-4674 — `if ((fd = gettempfile(NULL, 1, &s)) < 0) return -1;`
6136 let (fd, s) = match gettempfile(None) {
6137 Some(p) => p,
6138 None => return -1, // c:4674
6139 };
6140 // c:4675 — `write_loop(fd, t, len);`
6141 let _ = write_loop(fd, &bytes); // c:4675
6142 // c:4676 — `close(fd);`
6143 let _ = zclose(fd); // c:4676
6144 // c:4677 — `fd = open(s, O_RDONLY | O_NOCTTY);`
6145 let cstr = std::ffi::CString::new(s.as_str()).unwrap_or_default();
6146 let new_fd = unsafe { libc::open(cstr.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY) }; // c:4677
6147 // c:4678 — `unlink(s);`
6148 unsafe {
6149 libc::unlink(cstr.as_ptr());
6150 } // c:4678
6151 new_fd // c:4679
6152}
6153
6154/// Port of `quote_tokenized_output()` from `Src/exec.c:2114` — C decl `quote_tokenized_output(char *str, FILE *file)`.
6155///
6156/// C body (abridged):
6157/// ```c
6158/// for (; *s; s++) {
6159/// switch (*s) {
6160/// case Meta: putc(*++s ^ 32, file); continue;
6161/// case Nularg: continue;
6162/// case '\\' '<' '>' '(' '|' ')' '^' '#' '~' '[' ']' '*' '?' '$' ' ':
6163/// putc('\\', file); break;
6164/// case '\t': fputs("$'\\t'", file); continue;
6165/// case '\n': fputs("$'\\n'", file); continue;
6166/// case '\r': fputs("$'\\r'", file); continue;
6167/// case '=': if (s == str) putc('\\', file); break;
6168/// default:
6169/// if (itok(*s)) { putc(ztokens[*s - Pound], file); continue; }
6170/// }
6171/// putc(*s, file);
6172/// }
6173/// ```
6174///
6175/// Used by `xtrace` (`set -x` printer) and `whence -c` to display a
6176/// tokenized argv in a form where lexer tokens (`Star`, `Inpar`, …)
6177/// surface as unescaped chars (`*`, `(`) while literal special chars
6178/// get backslash-escaped — round-tripping through the shell.
6179pub fn quote_tokenized_output(str_in: &str, file: &mut impl std::io::Write) -> std::io::Result<()> {
6180 // c:2114
6181 let bytes = str_in.as_bytes();
6182 let mut i = 0usize;
6183 while i < bytes.len() {
6184 // c:2118 `for (; *s; s++)`
6185 let c = bytes[i];
6186 match c {
6187 x if x == Meta => {
6188 // c:2120 — `case Meta: putc(*++s ^ 32, file);`
6189 if i + 1 < bytes.len() {
6190 file.write_all(&[bytes[i + 1] ^ 32])?; // c:2121
6191 i += 2;
6192 } else {
6193 i += 1;
6194 }
6195 continue; // c:2122
6196 }
6197 x if x as char == Nularg => {
6198 // c:2124
6199 i += 1;
6200 continue; // c:2126
6201 }
6202 b'\\' | b'<' | b'>' | b'(' | b'|' | b')' | b'^' | b'#' | b'~' | b'[' | b']' | b'*'
6203 | b'?' | b'$' | b' ' => {
6204 // c:2128-2142
6205 file.write_all(b"\\")?; // c:2143
6206 }
6207 b'\t' => {
6208 // c:2146
6209 file.write_all(b"$'\\t'")?; // c:2147
6210 i += 1;
6211 continue;
6212 }
6213 b'\n' => {
6214 // c:2150
6215 file.write_all(b"$'\\n'")?; // c:2151
6216 i += 1;
6217 continue;
6218 }
6219 b'\r' => {
6220 // c:2154
6221 file.write_all(b"$'\\r'")?; // c:2155
6222 i += 1;
6223 continue;
6224 }
6225 b'=' => {
6226 // c:2158 — `if (s == str) putc('\\', file);`
6227 if i == 0 {
6228 file.write_all(b"\\")?; // c:2160
6229 }
6230 }
6231 _ => {
6232 // c:2163 — `if (itok(*s)) putc(ztokens[*s - Pound], file); continue;`
6233 if itok(c) {
6234 // c:2164
6235 let pound = Pound as u8;
6236 if c >= pound {
6237 let idx = (c - pound) as usize;
6238 let zt = ztokens.as_bytes();
6239 if idx < zt.len() {
6240 file.write_all(&[zt[idx]])?; // c:2165 `ztokens[*s - Pound]`
6241 }
6242 }
6243 i += 1;
6244 continue;
6245 }
6246 }
6247 }
6248 file.write_all(&[c])?; // c:2171
6249 i += 1;
6250 }
6251 Ok(())
6252}
6253
6254// =====================================================================
6255// Wordcode-VM control-flow dispatch — faithful ports of the C
6256// `Src/exec.c` + `Src/loop.c` wordcode interpreter entries.
6257//
6258// Each function below takes `&mut estate` and returns `i32` to mirror
6259// the C `int execX(Estate state, int do_exec)` signature exactly. Per-
6260// line `// c:NNN` citations track the C source line.
6261//
6262// zshrs's primary execution path is the fusevm bytecode VM. These
6263// wordcode-VM entries exist for C-name parity with the upstream
6264// interpreter so that future bridging code can drive zshrs through
6265// the same dispatch tree zsh's `Src/init.c::loop` walks. Where
6266// zshrs primitives don't yet model their C counterpart (e.g.
6267// `execsubst`, `addvars`, `execfuncs[]` dispatch table), the local
6268// helper is declared with a comment citing the C source file:line
6269// where the canonical body lives — same pattern as the canonical
6270// `ksh93::ksh93_wrapper` port at c:152-227.
6271// =====================================================================
6272
6273use crate::ported::math::{matheval as wc_matheval, mathevali as wc_mathevali};
6274use crate::ported::pattern::{patcompile, pattry};
6275use crate::ported::r#loop::try_tryflag;
6276
6277// Addvars-specific imports (Src/exec.c:2497 port at exec.rs::addvars).
6278use crate::ported::builtin::{BREAKS, CONTFLAG, LOOPS, RETFLAG};
6279use crate::ported::linklist::LinkList;
6280use crate::ported::mem::freeheap;
6281use crate::ported::params::setloopvar;
6282use crate::ported::params::{assignaparam, assignsparam, unsetparam};
6283use crate::ported::parse::{ecgetlist, ecgetstr};
6284use crate::ported::pattern::haswilds;
6285use crate::ported::signals_h::{queue_signal_level, restore_queue_signals};
6286use crate::ported::subst::{globlist, prefork};
6287use crate::ported::zsh_h::{
6288 estate, wordcode, EC_DUP, EC_DUPTOK, EC_NODUP, NOERREXIT_EXIT, NOERREXIT_RETURN, PAT_STATIC,
6289 WC_CASE, WC_CASE_AND, WC_CASE_OR, WC_CASE_SKIP, WC_CASE_TESTAND, WC_CASE_TYPE, WC_CURSH_SKIP,
6290 WC_END, WC_FOR_COND, WC_FOR_LIST, WC_FOR_SKIP, WC_FOR_TYPE, WC_FUNCDEF, WC_FUNCDEF_SKIP, WC_IF,
6291 WC_IF_ELSE, WC_IF_SKIP, WC_IF_TYPE, WC_REPEAT_SKIP, WC_TIMED_EMPTY, WC_TIMED_TYPE, WC_TRY_SKIP,
6292 WC_WHILE_SKIP, WC_WHILE_TYPE, WC_WHILE_UNTIL,
6293};
6294use crate::ported::zsh_h::{
6295 ALLEXPORT, ASSPM_AUGMENT, ASSPM_KEY_VALUE, ASSPM_WARN, GLOBASSIGN, KSHARRAYS, PREFORK_ASSIGN,
6296 PREFORK_KEY_VALUE, PREFORK_SINGLE, WC_ASSIGN, WC_ASSIGN_INC, WC_ASSIGN_NUM, WC_ASSIGN_SCALAR,
6297 WC_ASSIGN_TYPE, WC_ASSIGN_TYPE2,
6298};
6299use crate::ported::zsh_h::{
6300 CS_ALWAYS, CS_CASE, CS_COND, CS_CURSH, CS_ELIF, CS_ELIFTHEN, CS_ELSE, CS_FOR, CS_IF, CS_IFTHEN,
6301 CS_MATH, CS_REPEAT, CS_UNTIL, CS_WHILE, MN_INTEGER,
6302};
6303
6304// --- exec.c ports used by the exec paths below --------------------------
6305
6306/// Port of `execsubst()` from `Src/exec.c:2684` — C decl `execsubst(LinkList strs)`.
6307///
6308/// C body (c:2684-2693):
6309/// ```c
6310/// void execsubst(LinkList strs) {
6311/// if (strs) {
6312/// prefork(strs, esprefork, NULL);
6313/// if (esglob && !errflag) {
6314/// LinkList ostrs = strs;
6315/// globlist(strs, 0);
6316/// strs = ostrs;
6317/// }
6318/// }
6319/// }
6320/// ```
6321///
6322/// `execsubst` runs `prefork` (parameter / arithmetic / command
6323/// substitution expansion + IFS-split) over the whole list, then
6324/// (when `esglob` is set) `globlist` to do filename globbing on the
6325/// result.
6326pub(crate) fn execsubst(list: &mut Vec<String>) {
6327 // c:2684
6328 if list.is_empty() {
6329 return; // c:2686 `if (strs)`
6330 }
6331 let mut ll: crate::ported::subst::LinkList = std::mem::take(list).into_iter().collect();
6332 let prefork_flags = esprefork.load(Ordering::Relaxed); // c:2687 esprefork
6333 let mut rf: i32 = 0;
6334 prefork(&mut ll, prefork_flags, &mut rf); // c:2687
6335 if esglob.load(Ordering::Relaxed) != 0 && errflag.load(Ordering::Relaxed) == 0 {
6336 // c:2688 `if (esglob && !errflag)`
6337 globlist(&mut ll, 0); // c:2690
6338 }
6339 *list = ll.into_iter().collect();
6340}
6341
6342/// Port of `addvars()` from `Src/exec.c:2499` — C decl `addvars(Estate state, Wordcode pc, int addflags)`.
6343/// Direct port of `static void addvars(Estate state, Wordcode pc,
6344/// int addflags)` from `Src/exec.c:2497-2648`. Process the WC_ASSIGN
6345/// nodes stacked inline of a simple command — the `var=value` and
6346/// `arr=(v1 v2 v3)` assignments that precede argv. Walks the wordcode
6347/// at `pc`, extracts each assignment's name + value (scalar or array),
6348/// optionally preforks + globs the tokenised RHS, and routes through
6349/// `assignsparam` (scalar) or `assignaparam` (array).
6350///
6351/// XTRACE side-effect: prints `name=value ` / `name=( v1 v2 ) ` to
6352/// stderr (C uses xtrerr; zshrs uses eprint!).
6353///
6354/// `STTY=...` in an inline-export form (`STTY=raw cmd`) gets captured
6355/// into the file-static `STTYval` for `execute()` to apply pre-exec.
6356fn addvars(state: &mut estate, pc: usize, addflags: i32) {
6357 // c:2501 — locals.
6358 let mut vl: LinkList<String>; // c:2501 `LinkList vl;`
6359 let xtr: bool; // c:2502 `int xtr,`
6360 let mut isstr: bool; // c:2502 `int isstr,`
6361 let mut htok: i32 = 0; // c:2502 `int htok = 0;`
6362 let mut arr: Vec<String>; // c:2503 `char **arr, **ptr, *name;`
6363 let mut name: String;
6364 let mut flags: i32; // c:2504 `int flags;`
6365 let opc = state.pc; // c:2506 `Wordcode opc = state->pc;`
6366 let mut ac: wordcode; // c:2507 `wordcode ac;`
6367 // c:2508 `local_list1(svl);` — stack-local one-element LinkList
6368 // for the scalar-assignment path. Rust uses a fresh LinkList per
6369 // iteration; equivalent semantics.
6370
6371 // c:2510-2515 — comment about WARNCREATEGLOBAL warning suppression
6372 // when the assignment list is implicitly local (ADDVAR_RESTORE).
6373 flags = if (addflags & ADDVAR_RESTORE) == 0 {
6374 ASSPM_WARN // c:2516
6375 } else {
6376 0 // c:2516
6377 };
6378 xtr = isset(XTRACE); // c:2517 `xtr = isset(XTRACE);`
6379 if xtr {
6380 // c:2518
6381 printprompt4(); // c:2519
6382 doneps4.store(1, Ordering::Relaxed); // c:2520 `doneps4 = 1;`
6383 }
6384 state.pc = pc; // c:2522 `state->pc = pc;`
6385
6386 // c:2523 `while (wc_code(ac = *state->pc++) == WC_ASSIGN) {`
6387 loop {
6388 if state.pc >= state.prog.prog.len() {
6389 break;
6390 }
6391 ac = state.prog.prog[state.pc];
6392 state.pc += 1;
6393 if wc_code(ac) != WC_ASSIGN {
6394 // Step back so the WC_SIMPLE / outer dispatcher sees the
6395 // non-assignment opcode. C's `state->pc++` post-increment
6396 // already pointed past WC_ASSIGN; we need to unconsume.
6397 state.pc -= 1;
6398 break;
6399 }
6400 let mut myflags = flags; // c:2524 `int myflags = flags;`
6401 name = ecgetstr(state, EC_DUPTOK, Some(&mut htok)); // c:2525
6402 if htok != 0 {
6403 // c:2526 `if (htok) untokenize(name);`
6404 name = untokenize(&name).to_string(); // c:2527
6405 }
6406 if WC_ASSIGN_TYPE2(ac) == WC_ASSIGN_INC {
6407 // c:2528
6408 myflags |= ASSPM_AUGMENT; // c:2529
6409 }
6410 if xtr {
6411 // c:2530
6412 // c:2531-2532 — fprintf(xtrerr, ... "%s+=" : "%s=", name);
6413 if WC_ASSIGN_TYPE2(ac) == WC_ASSIGN_INC {
6414 eprint!("{}+=", name); // c:2532
6415 } else {
6416 eprint!("{}=", name); // c:2532
6417 }
6418 }
6419
6420 // c:2533 `if ((isstr = (WC_ASSIGN_TYPE(ac) == WC_ASSIGN_SCALAR))) {`
6421 isstr = WC_ASSIGN_TYPE(ac) == WC_ASSIGN_SCALAR;
6422 if isstr {
6423 // c:2534 `init_list1(svl, ecgetstr(state, EC_DUPTOK, &htok));`
6424 let svl_val = ecgetstr(state, EC_DUPTOK, Some(&mut htok));
6425 vl = LinkList::new();
6426 vl.push_back(svl_val);
6427 // c:2535 `vl = &svl;` — vl already points at the new list.
6428 } else {
6429 // c:2537 `vl = ecgetlist(state, WC_ASSIGN_NUM(ac), EC_DUPTOK, &htok);`
6430 let items = ecgetlist(
6431 state,
6432 WC_ASSIGN_NUM(ac) as usize,
6433 EC_DUPTOK,
6434 Some(&mut htok),
6435 );
6436 vl = LinkList::new();
6437 for it in items {
6438 vl.push_back(it);
6439 }
6440 if errflag.load(Ordering::Relaxed) != 0 {
6441 // c:2538-2541
6442 state.pc = opc; // c:2539
6443 return; // c:2540
6444 }
6445 }
6446
6447 // c:2544 `if (vl && htok) {`
6448 if htok != 0 {
6449 // c:2545 `int prefork_ret = 0;`
6450 let mut prefork_ret: i32 = 0;
6451 // c:2546-2547 — prefork(vl, (isstr ? PREFORK_SINGLE|PREFORK_ASSIGN
6452 // : PREFORK_ASSIGN), &prefork_ret);
6453 let pf_flags = if isstr {
6454 PREFORK_SINGLE | PREFORK_ASSIGN
6455 } else {
6456 PREFORK_ASSIGN
6457 };
6458 prefork(&mut vl, pf_flags, &mut prefork_ret); // c:2547
6459 if errflag.load(Ordering::Relaxed) != 0 {
6460 // c:2548
6461 state.pc = opc; // c:2549
6462 return; // c:2550
6463 }
6464 if (prefork_ret & PREFORK_KEY_VALUE) != 0 {
6465 // c:2552
6466 myflags |= ASSPM_KEY_VALUE; // c:2553
6467 }
6468 // c:2554-2555 — `if (!isstr || (isset(GLOBASSIGN) && isstr &&
6469 // haswilds((char *)getdata(firstnode(vl)))))`
6470 let needs_glob = if !isstr {
6471 true
6472 } else {
6473 isset(GLOBASSIGN)
6474 && isstr
6475 && !vl.is_empty()
6476 && haswilds(vl.nodes.front().map(|s| s.as_str()).unwrap_or(""))
6477 };
6478 if needs_glob {
6479 globlist(&mut vl, prefork_ret); // c:2556
6480 // c:2557-2562 — `if (isset(GLOBASSIGN) && isstr)
6481 // unsetparam(name);`
6482 if isset(GLOBASSIGN) && isstr {
6483 unsetparam(&name); // c:2562
6484 }
6485 if errflag.load(Ordering::Relaxed) != 0 {
6486 // c:2563
6487 state.pc = opc; // c:2564
6488 return; // c:2565
6489 }
6490 }
6491 }
6492 // c:2569 `if (isstr && (empty(vl) || !nextnode(firstnode(vl))))`
6493 // — scalar-assignment path: zero or one element after prefork.
6494 if isstr && (vl.is_empty() || vl.len() == 1) {
6495 let val: String; // c:2571 `char *val;`
6496 if vl.is_empty() {
6497 // c:2574
6498 val = String::new(); // c:2575 `val = ztrdup("");`
6499 } else {
6500 // c:2577 `untokenize(peekfirst(vl));`
6501 let peek = vl.nodes.front().cloned().unwrap_or_default();
6502 val = untokenize(&peek).to_string(); // c:2577-2578
6503 // c:2578 `val = ztrdup(ugetnode(vl));` — ugetnode pops;
6504 // we just cloned the front above. Equivalent.
6505 }
6506 if xtr {
6507 // c:2580
6508 eprint!("{}", quotedzputs(&val)); // c:2581
6509 eprint!(" "); // c:2582 `fputc(' ', xtrerr);`
6510 }
6511 // c:2584 `if ((addflags & ADDVAR_EXPORT) && !strchr(name, '['))`
6512 let pm = if (addflags & ADDVAR_EXPORT) != 0 && !name.contains('[') {
6513 // c:2585 `if (strcmp(name, "STTY") == 0)`
6514 if name == "STTY" {
6515 // c:2586-2587 — `STTYval = ztrdup(val);`
6516 let mut stty = STTYval.lock().unwrap();
6517 *stty = Some(val.clone()); // c:2587
6518 }
6519 // c:2589 `allexp = opts[ALLEXPORT];`
6520 let allexp = isset(ALLEXPORT);
6521 // c:2590 `opts[ALLEXPORT] = 1;` — temporarily set.
6522 opt_state_set("allexport", true);
6523 if isset(KSHARRAYS) {
6524 // c:2591
6525 unsetparam(&name); // c:2592
6526 }
6527 let pm = assignsparam(&name, &val, myflags); // c:2593
6528 // c:2594 `opts[ALLEXPORT] = allexp;` — restore.
6529 opt_state_set("allexport", allexp);
6530 pm
6531 } else {
6532 // c:2595
6533 assignsparam(&name, &val, myflags) // c:2596
6534 };
6535 if pm.is_none() {
6536 // c:2597 `if (!pm)`
6537 LASTVAL.store(1, Ordering::Relaxed); // c:2598 `lastval = 1;`
6538 // c:2599-2604 — "cheating" comment: don't zerr.
6539 if cmdoutval.load(Ordering::Relaxed) == 0 {
6540 // c:2605 `if (!cmdoutval)`
6541 cmdoutval.store(1, Ordering::Relaxed); // c:2606
6542 }
6543 }
6544 if errflag.load(Ordering::Relaxed) != 0 {
6545 // c:2608
6546 state.pc = opc; // c:2609
6547 return; // c:2610
6548 }
6549 continue; // c:2612
6550 }
6551 // c:2614 `if (vl) { ... }` — array-assignment path: drain vl
6552 // into a fresh `char **arr`.
6553 // c:2615-2619 `ptr = arr = zalloc(...); while (nonempty(vl)) *ptr++ = ztrdup(ugetnode(vl));`
6554 arr = Vec::with_capacity(vl.len() + 1);
6555 while let Some(s) = vl.pop_front() {
6556 arr.push(s);
6557 }
6558 // c:2623 `*ptr = NULL;` — C terminator; Rust Vec doesn't need it.
6559 if xtr {
6560 // c:2624
6561 eprint!("( "); // c:2625
6562 for s in &arr {
6563 // c:2626 `for (ptr = arr; *ptr; ptr++)`
6564 eprint!("{}", quotedzputs(s)); // c:2627
6565 eprint!(" "); // c:2628
6566 }
6567 eprint!(") "); // c:2630
6568 }
6569 // c:2632 `if (!assignaparam(name, arr, myflags))`
6570 if assignaparam(&name, arr, myflags).is_none() {
6571 LASTVAL.store(1, Ordering::Relaxed); // c:2633
6572 // c:2634-2638 — "cheating" comment.
6573 if cmdoutval.load(Ordering::Relaxed) == 0 {
6574 // c:2639
6575 cmdoutval.store(1, Ordering::Relaxed); // c:2640
6576 }
6577 }
6578 if errflag.load(Ordering::Relaxed) != 0 {
6579 // c:2642
6580 state.pc = opc; // c:2643
6581 return; // c:2644
6582 }
6583 }
6584 state.pc = opc; // c:2647 `state->pc = opc;`
6585}
6586
6587// execfuncs[] dispatch table from `Src/exec.c:5499` is inlined as a
6588// match expression at the call sites in execsimple. Not a separate
6589// Rust fn — every C-side reference to
6590// `execfuncs[code - WC_CURSH](state, ...)` resolves inline below.
6591
6592// --- exec.c entries ---------------------------------------------------
6593
6594/// Port of `execcursh()` from `Src/exec.c:469` — C decl `execcursh(Estate state, int do_exec)`.
6595/// Execute a `{ ... }` current-shell command
6596/// group: skip the trailing try-only word, optionally drop a stale
6597/// job slot, then run the inner list.
6598pub fn execcursh(state: &mut estate, do_exec: i32) -> i32 {
6599 // c:472 — `end = state->pc + WC_CURSH_SKIP(state->pc[-1]);`
6600 let prior = state.prog.prog[state.pc.wrapping_sub(1)];
6601 let end = state.pc + WC_CURSH_SKIP(prior) as usize;
6602 // c:475 — `state->pc++;` skip the try/always-only word.
6603 state.pc += 1;
6604 // c:482-486 — drop empty job slot before nested cmd: if outer-pipe
6605 // bookkeeping is clean AND thisjob is a real job that's not the
6606 // pipe-leader AND has no procs yet, deletejob() recycles it. Avoids
6607 // leaking job-table slots when execcursh recurses.
6608 {
6609 let lp = list_pipe.load(Ordering::Relaxed);
6610 let lpj = list_pipe_job.load(Ordering::Relaxed);
6611 let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
6612 if lp == 0 && tj != -1 && tj != lpj {
6613 if let Some(jt) = JOBTAB.get() {
6614 let mut guard = jt.lock().unwrap();
6615 let has = crate::ported::jobs::hasprocs(&guard, tj as usize);
6616 if !has && (tj as usize) < guard.len() {
6617 crate::ported::jobs::deletejob(&mut guard, tj as usize, false);
6618 }
6619 }
6620 }
6621 }
6622 cmdpush(CS_CURSH as u8); // c:487 — `cmdpush(CS_CURSH);`
6623 let _ = execlist(state, 1, do_exec); // c:488 — `execlist(state, 1, do_exec);`
6624 cmdpop(); // c:489 — `cmdpop();`
6625 state.pc = end; // c:491 — `state->pc = end;`
6626 this_noerrexit.store(1, Ordering::Relaxed); // c:492 — `this_noerrexit = 1;`
6627 LASTVAL.load(Ordering::Relaxed) // c:494 — `return lastval;`
6628}
6629
6630// `(...)` subshell — no dedicated C function (handled inline by
6631// `execpline`'s WC_PIPE branch via the WC_SUBSH bit, exec.c:2540+).
6632// In zshrs the subshell branch is folded into `execpline` and
6633// `execsimple`'s WC_SUBSH dispatch — both invoke execcursh for the
6634// inner-list walk since fusevm bytecode handles the forking via
6635// Op::Subshell at a higher layer.
6636
6637/// Port of `execcond()` from `Src/exec.c:5260` — C decl `execcond(Estate state, UNUSED(int do_exec))`.
6638/// Run a `[[ ... ]]` cond expression.
6639pub fn execcond(state: &mut estate, _do_exec: i32) -> i32 {
6640 state.pc -= 1; // c:5265 — `state->pc--;`
6641 if isset(XTRACE) {
6642 printprompt4();
6643 eprint!("[["); // `fprintf(xtrerr, "[[");`
6644 crate::ported::cond::tracingcond.fetch_add(1, Ordering::Relaxed); // `tracingcond++;`
6645 }
6646 cmdpush(CS_COND as u8);
6647 let stat: i32 = crate::ported::cond::evalcond(state, None); // `stat = evalcond(state, NULL);`
6648 // 2 indicates a syntax error. For compatibility, turn this into a shell error.
6649 if stat == 2 {
6650 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
6651 }
6652 cmdpop();
6653 if isset(XTRACE) {
6654 eprintln!(" ]]"); // `fprintf(xtrerr, " ]]\n");`
6655 crate::ported::cond::tracingcond.fetch_sub(1, Ordering::Relaxed); // `tracingcond--;`
6656 }
6657 stat // `return stat;`
6658}
6659
6660/// Port of `execarith()` from `Src/exec.c:5235` — C decl `execarith(Estate state, UNUSED(int do_exec))`.
6661/// Run a `(( ... ))` arithmetic command;
6662/// returns 0 when val != 0 (success), 1 when val == 0 (false), 2 on
6663/// parse error.
6664pub fn execarith(state: &mut estate, _do_exec: i32) -> i32 {
6665 if isset(XTRACE) {
6666 printprompt4();
6667 eprint!("((");
6668 }
6669 cmdpush(CS_MATH as u8); // c:5247
6670 let mut htok: i32 = 0;
6671 let mut e = ecgetstr(state, EC_DUPTOK, Some(&mut htok)); // c:5248
6672 if htok != 0 {
6673 e = singsub(&e); // c:5250 — `singsub(&e);`
6674 }
6675 if isset(XTRACE) {
6676 eprint!(" {}", e);
6677 }
6678 let val_result = wc_matheval(&e); // c:5254 — `val = matheval(e);`
6679 cmdpop(); // c:5256
6680 if isset(XTRACE) {
6681 eprintln!(" ))");
6682 }
6683 // c:5262-5265 — `if (errflag) { errflag &= ~ERRFLAG_ERROR; return 2; }`
6684 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 || val_result.is_err() {
6685 errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed);
6686 return 2;
6687 }
6688 // c:5267 — `return (val.type == MN_INTEGER) ? val.u.l == 0 : val.u.d == 0.0;`
6689 let val = val_result.unwrap();
6690 if val.type_ == MN_INTEGER {
6691 if val.l == 0 {
6692 1
6693 } else {
6694 0
6695 }
6696 } else if val.d == 0.0 {
6697 1
6698 } else {
6699 0
6700 }
6701}
6702
6703/// Port of `exectime()` from `Src/exec.c:5272` — C decl `exectime(Estate state, UNUSED(int do_exec))`.
6704/// Run `time pipeline`: drives execpline with
6705/// the Z_TIMED|Z_SYNC flags so it tracks wall/user/sys time.
6706pub fn exectime(state: &mut estate, _do_exec: i32) -> i32 {
6707 let jb = *THISJOB
6708 .get_or_init(|| std::sync::Mutex::new(-1))
6709 .lock()
6710 .unwrap(); // c:5283
6711 let prior = state.prog.prog[state.pc.wrapping_sub(1)];
6712 // c:5284-5287 — empty `time` (no pipeline) — print accumulated shell time.
6713 if WC_TIMED_TYPE(prior) == WC_TIMED_EMPTY {
6714 // c:5285 — `shelltime(NULL,NULL,NULL,0);` — print accumulated
6715 // shell+kids time deltas since last call.
6716 crate::ported::jobs::shelltime(None, None, None, 0);
6717 return 0; // c:5286
6718 }
6719 // c:5288 — `execpline(state, *state->pc++, Z_TIMED|Z_SYNC, 0);`
6720 let slcode = state.prog.prog[state.pc];
6721 state.pc += 1;
6722 use crate::ported::zsh_h::{Z_SYNC, Z_TIMED};
6723 let _ = execpline(state, slcode, Z_TIMED as i32 | Z_SYNC as i32, 0);
6724 *THISJOB
6725 .get_or_init(|| std::sync::Mutex::new(-1))
6726 .lock()
6727 .unwrap() = jb; // c:5289
6728 LASTVAL.load(Ordering::Relaxed) // c:5290
6729}
6730
6731/// Port of `execshfunc()` from `Src/exec.c:5540` — C decl `execshfunc(Shfunc shf, LinkList args)`.
6732/// `execshfunc(Shfunc shf, LinkList args)` — `Src/exec.c:5540`.
6733/// Promoted to top-level pub fn so execcmd_exec at the shfunc
6734/// dispatch site (c:4102-4105) can route through it. Owns the
6735/// queue_signals + cmdstack + sfcontext setup around `doshfunc`, as C.
6736pub fn execshfunc(shf: &mut shfunc, args: &mut Vec<String>) {
6737 // c:5546-5547 — `if (errflag) return;`
6738 // A user interrupt sets ERRFLAG_INT, never ERRFLAG_ERROR (signals.c:457), and
6739 // the C line cited above tests the WHOLE errflag, so masking here let an
6740 // interrupted shell keep going where zsh stops.
6741 if errflag.load(Ordering::Relaxed) != 0 {
6742 return;
6743 }
6744 // c:5550-5557 — drop empty job slot before nested shfunc invoke:
6745 // if outer-pipe bookkeeping is clean AND thisjob is a real job
6746 // that's not the pipe-leader AND has no procs yet, deletejob()
6747 // recycles it. Avoids leaking job-table slots across recursive
6748 // function calls. Same pattern as execcursh's c:482-486.
6749 let mut last_file_list: Vec<jobfile> = Vec::new(); // c:5598
6750 {
6751 let lp = list_pipe.load(Ordering::Relaxed);
6752 let lpj = list_pipe_job.load(Ordering::Relaxed);
6753 let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
6754 if lp == 0 && tj != -1 && tj != lpj {
6755 if let Some(jt) = JOBTAB.get() {
6756 let mut guard = jt.lock().unwrap();
6757 let has = crate::ported::jobs::hasprocs(&guard, tj as usize);
6758 if !has {
6759 // c:5554-5555 — `last_file_list = jobtab[thisjob].filelist;
6760 // jobtab[thisjob].filelist = NULL;` — preserve
6761 // the filelist so deletejob doesn't unlink temp
6762 // files. Rust take()s the Vec into a local.
6763 last_file_list = if let Some(j) = guard.get_mut(tj as usize)
6764 {
6765 std::mem::take(&mut j.filelist)
6766 } else {
6767 Vec::new()
6768 };
6769 if (tj as usize) < guard.len() {
6770 crate::ported::jobs::deletejob(&mut guard, tj as usize, false); // c:5556
6771 }
6772 }
6773 }
6774 }
6775 }
6776 // c:5559-5570 — `if (isset(XTRACE)) { printprompt4(); ... \n; }` —
6777 // emit PS4 prefix + space-separated quoted args on the trace
6778 // stream so `set -x` shows the function invocation line.
6779 if isset(XTRACE) {
6780 printprompt4();
6781 for (i, a) in args.iter().enumerate() {
6782 if i > 0 {
6783 crate::fusevm_bridge::xtrerr_fputs(" "); // c:5565 fputc(' ', xtrerr)
6784 }
6785 crate::fusevm_bridge::xtrerr_fputs("edzputs(a)); // c:5566
6786 }
6787 crate::fusevm_bridge::xtrerr_fputs("\n"); // c:5568
6788 crate::fusevm_bridge::xtrerr_flush(); // c:5569
6789 }
6790 queue_signals(); // c:5627
6791 // c:5628-5631 — `ocs = cmdstack; ocsp = cmdsp; cmdstack = zalloc(CMDSTACKSZ); cmdsp = 0;`
6792 let ocs: Vec<u8> = crate::ported::prompt::CMDSTACK.with(|s| std::mem::take(&mut *s.borrow_mut()));
6793 // c:5632-5633 — `if ((osfc = sfcontext) == SFC_NONE) sfcontext = SFC_DIRECT;`
6794 let osfc = sfcontext.load(Ordering::Relaxed);
6795 if osfc == crate::ported::zsh_h::SFC_NONE {
6796 sfcontext.store(crate::ported::zsh_h::SFC_DIRECT, Ordering::Relaxed);
6797 }
6798 // c:5580 — `doshfunc(shf, args, 0);`. The C path always has
6799 // `funcdef` populated since C parses at definition time. zshrs
6800 // compiles to fusevm chunks instead, so `funcdef` is None for
6801 // user-defined functions; only `body` (source string) carries
6802 // the definition. When that's the case, build a one-shot eprog
6803 // whose `strs` carries the source so runshfunc's script-pipeline
6804 // arm (execute_script_zsh_pipeline) executes the body.
6805 let prog_owned: Option<eprog> = if shf.funcdef.is_some() {
6806 None
6807 } else if let Some(ref body) = shf.body {
6808 Some(eprog {
6809 strs: Some(body.clone()),
6810 ..Default::default()
6811 })
6812 } else if (shf.node.flags as u32 & PM_UNDEFINED) != 0 {
6813 // c:Src/builtin.c:3763 `shf->funcdef = mkautofn(shf);` — an
6814 // `autoload`ed-but-not-yet-loaded function is NOT body-less in C.
6815 // `mkautofn` (c:3786-3809) hands it a five-word program whose only
6816 // instruction is `WCB_AUTOFN()`, which `execautofn`
6817 // (Src/exec.c:5786) turns into `loadautofn`; `functions NAME`
6818 // prints that program back as `builtin autoload -X`.
6819 //
6820 // zshrs's autoload stub carries neither `funcdef` nor `body`, so
6821 // this arm produced `None` and `execshfunc` returned having done
6822 // NOTHING, status 0. That silently killed every autoloaded function
6823 // reached through `execcmd_exec` — i.e. every call whose command
6824 // word is not a compile-time literal (`$c`, `my$v`, `"$f"`), since
6825 // a literal head goes through `CallFunction` →
6826 // `dispatch_function_call`, which has its own autoload prelude.
6827 // `_sh` dispatches with `_$variant "$@"` (Completion/Unix/Command/
6828 // _sh sh:51), so `sh <TAB>` ran `_bash` as a no-op and completed
6829 // nothing.
6830 //
6831 // Emit mkautofn's program as its printed source; zshrs's
6832 // `autoload -X` arm (builtin.rs `bin_functions`, c:3613-3654) is
6833 // the port of `execautofn`'s effect and re-invokes the function
6834 // with the original arguments via `eval_autoload` (c:3167-3177).
6835 Some(eprog {
6836 strs: Some("builtin autoload -X".to_string()),
6837 ..Default::default()
6838 })
6839 } else {
6840 None
6841 };
6842 let prog_ref: Option<&eprog> = match (shf.funcdef.as_deref(), prog_owned.as_ref()) {
6843 (Some(p), _) => Some(p),
6844 (_, Some(p)) => Some(p),
6845 _ => None,
6846 };
6847 if let Some(_prog) = prog_ref {
6848 // c:5580 — `doshfunc(shf, args, 0);`. Direct doshfunc call —
6849 // noreturnval=0 means the body's return value updates LASTVAL
6850 // (caller of execfuncdef reads it back). PPARAMS swap +
6851 // restore happens INSIDE doshfunc's scope; body_runner just
6852 // runs the body.
6853 let name_for_body = shf.node.nam.clone();
6854 let body_args_owned: Vec<String> = if args.len() > 1 {
6855 args[1..].to_vec()
6856 } else {
6857 Vec::new()
6858 };
6859 let body_runner = move || -> i32 {
6860 crate::ported::exec::run_function_body(&name_for_body, &body_args_owned).unwrap_or(0)
6861 };
6862 crate::ported::utils::xtrerr.store(2, Ordering::Relaxed); // c:5634 xtrerr = stderr;
6863 let _ = doshfunc(shf, args.clone(), false, body_runner);
6864 }
6865 sfcontext.store(osfc, Ordering::Relaxed); // c:5638
6866 // c:5639-5641 — `free(cmdstack); cmdstack = ocs; cmdsp = ocsp;`
6867 crate::ported::prompt::CMDSTACK.with(|s| *s.borrow_mut() = ocs);
6868 if list_pipe.load(Ordering::Relaxed) == 0 {
6869 // c:5643-5644 — `deletefilelist(last_file_list, 0);`
6870 let mut holder = crate::ported::zsh_h::job::default();
6871 holder.filelist = last_file_list;
6872 crate::ported::jobs::deletefilelist(&mut holder, false);
6873 }
6874 unqueue_signals(); // c:5645
6875}
6876
6877/// Port of `int doshfunc(Shfunc shfunc, LinkList doshargs, int noreturnval)`
6878/// from `Src/exec.c:5823-6158`.
6879///
6880/// C body's scope-management sequence ported here. The C source's
6881/// body-execution call (`runshfunc(prog, wrappers, name)` at c:6042)
6882/// is replaced by `body_runner` — zshrs runs function bodies through
6883/// fusevm bytecode rather than zsh's wordcode walker (per PORT.md
6884/// "zshrs replaces zsh's tree-walking interpreter" rule), so the
6885/// callback hands the live executor back to the caller (typically
6886/// the fusevm bridge) for the actual body run. Every line of scope
6887/// save/restore around the body call mirrors C exactly.
6888///
6889/// **RUST-ONLY ADAPTATION:** the extra `body_runner` parameter is
6890/// not in C. C calls `runshfunc(prog, wrappers, name)` directly at
6891/// c:6042; zshrs delegates to a closure because the body-execution
6892/// pipeline (fusevm) differs from C's (wordcode). The closure
6893/// fully replaces the runshfunc call and returns the body's exit
6894/// status (which doshfunc reads as `lastval` for the `noreturnval`
6895/// path).
6896#[allow(non_snake_case)]
6897/// Port of the module-global `oflags` in `Src/exec.c` (set at c:5970 via
6898/// `funcsave->oflags = oflags;`). Holds the attribute flags of the function
6899/// CURRENTLY executing, so a nested call can tell whether its caller carried
6900/// PM_TAGGED_LOCAL — that is what makes `functions -T` non-recursive.
6901/// zshrs-original storage shape (atomic rather than a plain global); the
6902/// save/restore discipline matches C's funcsave. Bug #1058.
6903pub static FUNC_OFLAGS: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
6904
6905/// Port of `doshfunc()` from `Src/exec.c:5823` — C decl `doshfunc(Shfunc shfunc, LinkList doshargs, int noreturnval)`.
6906pub fn doshfunc(
6907 shfunc: &mut shfunc, // c:5823
6908 doshargs: Vec<String>, // c:5823
6909 noreturnval: bool, // c:5823
6910 mut body_runner: impl FnMut() -> i32, // (Rust-only — body delegate)
6911) -> i32 {
6912 use crate::ported::builtin::{BREAKS, CONTFLAG, LASTVAL, LOOPS, RETFLAG};
6913 // TEMP-INSTRUMENTATION (remove before commit): shell-function invocation
6914 // volume, to tell "few slow calls" from "very many cheap calls".
6915 {
6916 use std::sync::atomic::{AtomicU64, Ordering as O};
6917 static CALLS: AtomicU64 = AtomicU64::new(0);
6918 let n = CALLS.fetch_add(1, O::Relaxed) + 1;
6919 if n % 5000 == 0 {
6920 tracing::info!(target: "fncount", calls = n, "doshfunc cumulative");
6921 }
6922 }
6923 use crate::ported::jobs::{NUMPIPESTATS, PIPESTATS};
6924 use crate::ported::modules::parameter::FUNCSTACK;
6925 use crate::ported::params::endparamscope;
6926 use crate::ported::params::locallevel as locallevel_atomic;
6927 use crate::ported::zsh_h::{
6928 FS_EVAL, FS_FUNC, FS_SOURCE, FUNCTIONARGZERO, PM_TAGGED, PM_TAGGED_LOCAL, PM_UNDEFINED,
6929 PM_WARNNESTED,
6930 };
6931 use std::sync::atomic::Ordering;
6932
6933 let name = shfunc.node.nam.clone(); // c:5827
6934 let flags = shfunc.node.flags; // c:5828
6935 // Lineage tap, before the funcstack frame goes on: the op must
6936 // record where the *caller* stands, not the body about to run.
6937 if crate::provenance::active() {
6938 // c:5978-5998 — doshargs[0] is the function name, doshargs[1..] the
6939 // positionals the body will see as $1, $2, …; the chain records those,
6940 // not the name again.
6941 let prov_args: &[String] = if doshargs.len() > 1 {
6942 &doshargs[1..]
6943 } else {
6944 &[]
6945 };
6946 crate::provenance::on_func_call(
6947 &name,
6948 prov_args,
6949 shfunc.body.as_deref(),
6950 shfunc.filename.as_deref(),
6951 shfunc.lineno,
6952 );
6953 }
6954 let fname = dupstring(&name); // c:5829
6955 let _ = fname; // c:5829 (kept for parity)
6956
6957 // c:6000-6004 — FUNCNEST guard. C:
6958 // if (zsh_funcnest >= 0 && funcstack && funcstacksz >= zsh_funcnest) {
6959 // zerr("maximum nested function level reached; increase FUNCNEST?");
6960 // goto undoshfunc;
6961 // }
6962 // This was previously stubbed out on the assumption that the zshrs
6963 // fusevm "doesn't recurse via real stack frames" — but it DOES:
6964 // dispatch_function_call -> doshfunc -> dispatch_function_call -> …
6965 // nests real native frames, so an accidental infinite recursion
6966 // (e.g. a self-referential hook, a wrapped function that re-invokes
6967 // itself, a zle widget cycle) overflowed the OS stack and
6968 // SEGFAULTED instead of producing zsh's graceful error. Check at
6969 // entry — before queue_signals / inc_locallevel / the funcsave
6970 // snapshot — so the early return needs no unwinding. FUNCNEST < 0
6971 // means "unlimited" (zsh's `zsh_funcnest >= 0` gate). Depth is the
6972 // count of already-active frames on FUNCSTACK; the 501st nested
6973 // call (depth == 500) trips it, matching C's `>=` on the default
6974 // FUNCNEST=500.
6975 let funcnest = crate::ported::params::getiparam("FUNCNEST");
6976 if funcnest >= 0 {
6977 // Count only real function frames (FS_FUNC) — the raw FUNCSTACK
6978 // Vec also carries FS_EVAL/FS_SOURCE/anon frames, which inflate
6979 // the depth several× and would falsely trip on legitimately deep
6980 // (but finite) recursion. FS_FUNC-only matches zsh's nesting
6981 // depth (`${#funcstack}`) and its funcnest accounting.
6982 let depth = crate::ported::modules::parameter::FUNCSTACK
6983 .lock()
6984 .map(|s| s.iter().filter(|f| f.tp == FS_FUNC).count())
6985 .unwrap_or(0) as i64;
6986 if depth >= funcnest {
6987 zerr("maximum nested function level reached; increase FUNCNEST?");
6988 crate::ported::builtin::LASTVAL.store(1, std::sync::atomic::Ordering::Relaxed);
6989 return 1;
6990 }
6991 }
6992
6993 // c:5835 — `queue_signals();` Lots of memory + global-state changes.
6994 queue_signals();
6995
6996 // c:5847-5848 — `marked_prog = shfunc->funcdef; useeprog(marked_prog);`
6997 // Pinned so a recursive unload doesn't free the eprog under us.
6998 // (Skipped: zshrs's shfunc holds a Box<Eprog>; Drop semantics
6999 // already pin until call ends. C does explicit refcount on
7000 // `funcdef->nref` via useeprog.)
7001
7002 // c:5856-5916 — Funcsave allocation + per-field snapshot.
7003 let funcsave_breaks = BREAKS.load(Ordering::Relaxed); // c:5859
7004 let funcsave_contflag = CONTFLAG.load(Ordering::Relaxed); // c:5860
7005 let funcsave_loops = LOOPS.load(Ordering::Relaxed); // c:5861
7006 let funcsave_lastval = LASTVAL.load(Ordering::Relaxed); // c:5862
7007 let funcsave_numpipestats = {
7008 // c:5864
7009 NUMPIPESTATS
7010 .get_or_init(|| std::sync::Mutex::new(0))
7011 .lock()
7012 .map(|n| *n)
7013 .unwrap_or(0)
7014 };
7015 let funcsave_noerrexit = noerrexit.load(Ordering::Relaxed); // c:5865
7016 // c:5866-5867 — trap_state PRIMED branch decrements trap_return.
7017 if TRAP_STATE.load(Ordering::Relaxed) == TRAP_STATE_PRIMED {
7018 // c:5866
7019 TRAP_RETURN.fetch_sub(1, Ordering::Relaxed); // c:5867
7020 }
7021 // c:5871 — `noerrexit &= ~NOERREXIT_RETURN;` — scope-clear of
7022 // return-suppress so a `return` inside the body fires errexit
7023 // checks normally.
7024 noerrexit.fetch_and(!NOERREXIT_RETURN, Ordering::Relaxed);
7025
7026 // c:5872-5880 — noreturnval branch: deep-copy pipestats so the
7027 // function body's pipestats writes are restored on exit.
7028 let funcsave_pipestats: Option<Vec<i32>> = if noreturnval {
7029 // c:5872
7030 let p = PIPESTATS.get_or_init(|| std::sync::Mutex::new([0; MAX_PIPESTATS]));
7031 p.lock().ok().map(|g| g[..funcsave_numpipestats].to_vec()) // c:5879 memcpy
7032 } else {
7033 None
7034 };
7035
7036 // c:5882-5896 — `if (!strcmp(fname, "TRAPEXIT")) { shcopy = copy of
7037 // *shfunc; shfunc = shcopy; name = shfunc->node.nam; }`: C copies the
7038 // node because starttrapscope() unsets the table's TRAPEXIT entry. Every
7039 // doshfunc caller passes its own `&mut shfunc` copy (not the shfunctab
7040 // node), so unsettrap() cannot invalidate it and no copy is needed.
7041
7042 // c:5898 — `starttrapscope();` — canonical port at signals.rs:1135
7043 // tags SIGEXIT for deferred restoration at scope end.
7044 crate::ported::signals::starttrapscope();
7045 crate::extensions::emulation_output::bash_return_trap_enter();
7046 // c:5899 — `startpatternscope();`
7047 crate::ported::pattern::startpatternscope();
7048
7049 // c:5901 — `pptab = pparams;` — save outer positional params.
7050 let pptab: Vec<String> = crate::ported::builtin::PPARAMS
7051 .lock()
7052 .map(|p| p.clone())
7053 .unwrap_or_default();
7054
7055 // c:5902-5903 — non-undefined: `scriptname = dupstring(name);`
7056 let funcsave_scriptname = crate::ported::utils::scriptname_get();
7057 if (flags as u32 & PM_UNDEFINED) == 0 {
7058 // c:5902
7059 crate::ported::utils::set_scriptname(Some(dupstring(&name))); // c:5903
7060 }
7061
7062 // c:5964-5965 — `funcsave->zoptind = zoptind; funcsave->optcind
7063 // = optcind;`. C makes OPTIND implicitly function-local: a
7064 // `getopts` loop inside the function gets its own counter that
7065 // snaps back to the caller's on return. zshrs keeps the counter
7066 // in the `$OPTIND` param plus the ZOPTIND/OPTCIND trackers
7067 // `getopts` syncs against, so all three are snapshotted here.
7068 //
7069 // OPTARG is deliberately NOT snapshotted. The funcsave struct
7070 // (c:44-52) is `char opts[OPT_SIZE]; char *argv0; int zoptind,
7071 // lastval, optcind, numpipestats; …` — there is no `zoptarg`
7072 // member, and `grep -n zoptarg Src/exec.c` returns nothing, so C
7073 // lets a callee's OPTARG leak to its caller. Bug #513 added an
7074 // OPTARG save/restore citing "C's `funcsave->zoptind / zoptarg`
7075 // save/restore pair"; that field does not exist. The extra
7076 // restore was observable:
7077 // g() { OPTARG=inner; }; h() { local OPTARG=Z; g; print $OPTARG }
7078 // zsh -> inner zshrs (before) -> Z
7079 // g() { getopts "a:" o -a VAL; }
7080 // k() { local OPTARG=Z OPTIND=1; g; print $OPTARG }
7081 // zsh -> VAL zshrs (before) -> Z
7082 let funcsave_optind: Option<String> = crate::ported::params::getsparam("OPTIND");
7083 let funcsave_optcind = crate::ported::builtin::OPTCIND.load(Ordering::Relaxed); // c:5965
7084 // c:5966-5969 — `if (!isset(POSIXBUILTINS)) { zoptind = 1; optcind = 0; }`.
7085 // The snapshot above is only half the contract: C also RESETS the
7086 // counter on entry so an inner `getopts` loop starts fresh at the
7087 // first positional, independent of the caller's OPTIND. Without
7088 // this, a function whose body runs `getopts` after the caller
7089 // advanced OPTIND mis-parses its own args — e.g. `add-zsh-hook`
7090 // (which `getopts`-parses `precmd func`) printed its usage and
7091 // failed when invoked from a config that had run getopts earlier.
7092 // zshrs keeps the counter in the $OPTIND param plus the ZOPTIND/
7093 // OPTCIND trackers getopts syncs against; reset all three.
7094 if !crate::ported::zsh_h::isset(crate::ported::zsh_h::POSIXBUILTINS) {
7095 crate::ported::params::setiparam("OPTIND", 1); // c:5967 zoptind = 1
7096 crate::ported::builtin::ZOPTIND.store(1, Ordering::Relaxed);
7097 crate::ported::builtin::OPTCIND.store(0, Ordering::Relaxed); // c:5968 optcind = 0
7098 }
7099
7100 // c:5911-5914 — `memcpy(funcsave->opts, opts, sizeof(opts));`
7101 //
7102 // C copies 186 bytes. The port used to clone the whole option table
7103 // as a `HashMap<String, bool>` — a heap allocation per option name,
7104 // on every call of every function, whether or not the body touches
7105 // an option. `opt_state_store` is C's array, so this is C's memcpy.
7106 let funcsave_opts = crate::ported::options::opt_state_store::save();
7107
7108 // c:5974-5975 — `funcsave->emulation = emulation;
7109 // funcsave->sticky = sticky;`
7110 let funcsave_emulation = crate::ported::options::emulation.load(Ordering::Relaxed);
7111 let funcsave_emulation_live = crate::ported::options::EMULATION.load(Ordering::Relaxed);
7112 let funcsave_fully = crate::ported::options::FULLY_EMULATING.load(Ordering::Relaxed);
7113 let funcsave_sticky = sticky
7114 .lock()
7115 .unwrap_or_else(|e| e.into_inner())
7116 .as_ref()
7117 .map(|b| sticky_emulation_dup(b, 0));
7118
7119 // c:5977-6012 —
7120 // if (sticky_emulation_differs(shfunc->sticky)) {
7121 // sticky = sticky_emulation_dup(shfunc->sticky, 1);
7122 // emulation = sticky->emulation;
7123 // funcsave->restore_sticky = 1;
7124 // installemulation(emulation, opts);
7125 // if (sticky->n_on_opts) { ... opts[*onptr] = 1; }
7126 // if (sticky->n_off_opts) { ... opts[*offptr] = 0; }
7127 // /* All emulations start with pattern disables clear */
7128 // clearpatterndisables();
7129 // } else
7130 // funcsave->restore_sticky = 0;
7131 //
7132 // This whole block was missing, so a function DEFINED inside
7133 // `emulate sh -c '...'` did not re-enter that emulation when
7134 // called (B07emulate.ztst:6,7,8,12,13,14).
7135 let mut funcsave_restore_sticky = 0; // c:6012
7136 if sticky_emulation_differs(shfunc.sticky.as_deref()) != 0 {
7137 // c:5978
7138 let newsticky = sticky_emulation_dup(shfunc.sticky.as_deref().unwrap(), 1); // c:5990
7139 let emu = newsticky.emulation; // c:5991
7140 *sticky.lock().unwrap_or_else(|e| e.into_inner()) = Some(newsticky);
7141 // C's single `emulation` global carries EMULATE_FULLY; the Rust
7142 // port splits it into EMULATION (base bits) + FULLY_EMULATING.
7143 let emu_base = emu & !crate::ported::zsh_h::EMULATE_FULLY;
7144 let emu_fully = (emu & crate::ported::zsh_h::EMULATE_FULLY) != 0;
7145 crate::ported::options::emulation.store(emu_base, Ordering::Relaxed); // c:5991
7146 crate::ported::options::EMULATION.store(emu_base, Ordering::Relaxed); // (port keeps 2 cells)
7147 crate::ported::options::FULLY_EMULATING.store(emu_fully, Ordering::Relaxed);
7148 funcsave_restore_sticky = 1; // c:5992
7149 // c:5993 — `installemulation(emulation, opts);`
7150 let mut new_opts = [-1i8; crate::ported::zsh_h::OPT_SIZE as usize];
7151 crate::ported::options::installemulation(emu, &mut new_opts); // c:5993
7152 for (optno, &v) in new_opts.iter().enumerate().skip(1) {
7153 if v >= 0 {
7154 opt_state_set(crate::ported::zsh_h::opt_name(optno as i32), v == 1);
7155 }
7156 }
7157 {
7158 let guard = sticky.lock().unwrap_or_else(|e| e.into_inner());
7159 if let Some(ref st) = *guard {
7160 // c:5995-6001 — `opts[*onptr] = 1;`
7161 for on in &st.on_opts {
7162 opt_state_set(crate::ported::zsh_h::opt_name(*on as i32), true);
7163 }
7164 // c:6002-6008 — `opts[*offptr] = 0;`
7165 for off in &st.off_opts {
7166 opt_state_set(crate::ported::zsh_h::opt_name(*off as i32), false);
7167 }
7168 }
7169 }
7170 // c:6010 — `clearpatterndisables();`
7171 crate::ported::pattern::clearpatterndisables();
7172 }
7173
7174 // c:5954-5960 — PM_TAGGED / PM_TAGGED_LOCAL turn XTRACE on for the
7175 // duration of the call:
7176 // if (flags & (PM_TAGGED|PM_TAGGED_LOCAL))
7177 // opts[XTRACE] = 1;
7178 // else if (oflags & PM_TAGGED_LOCAL) {
7179 // if (shfunc->node.nam == ANONYMOUS_FUNCTION_NAME)
7180 // flags |= PM_TAGGED_LOCAL;
7181 // else
7182 // opts[XTRACE] = 0;
7183 // }
7184 // This is what makes `functions -t f` / `-T f` trace a SINGLE function
7185 // without `setopt xtrace` globally. It was previously skipped, so both
7186 // flags parsed and stored correctly (builtin.rs c:3358-3365) but nothing
7187 // ever consulted them and the traced function ran silently.
7188 //
7189 // `-T` is the non-recursive form: a function called FROM a `-T` function
7190 // gets XTRACE turned back OFF unless it is itself tagged. C tracks the
7191 // caller's flags in the module-global `oflags`, saved per call at c:5970
7192 // (`funcsave->oflags = oflags;`) and restored on return — mirrored here by
7193 // FUNC_OFLAGS. Anonymous functions are the documented exception: they
7194 // INHERIT the local tag instead of clearing it, so `-T` tracing spans the
7195 // `(anon)` bodies inside the traced function.
7196 //
7197 // No restore of XTRACE is needed here: it is already in the
7198 // always-restored subset at the exit block below (c:6097-6101).
7199 // Bug #1058.
7200 let mut fn_flags = shfunc.node.flags as u32;
7201 let oflags_prev = FUNC_OFLAGS.load(Ordering::Relaxed);
7202 if (fn_flags & (PM_TAGGED | PM_TAGGED_LOCAL)) != 0 {
7203 opt_state_set("xtrace", true); // c:5955
7204 } else if (oflags_prev & PM_TAGGED_LOCAL) != 0 {
7205 // c:5956
7206 if shfunc.node.nam == ANONYMOUS_FUNCTION_NAME {
7207 fn_flags |= PM_TAGGED_LOCAL; // c:5958
7208 } else {
7209 opt_state_set("xtrace", false); // c:5960
7210 }
7211 }
7212 // c:6022-6029 — `functions -W` (PM_WARNNESTED) is the per-function
7213 // form of WARN_NESTED_VAR, with the same anonymous-function inheritance
7214 // as `-T` above:
7215 // if (flags & PM_WARNNESTED)
7216 // opts[WARNNESTEDVAR] = 1;
7217 // else if (oflags & PM_WARNNESTED) {
7218 // if (shfunc->node.nam == ANONYMOUS_FUNCTION_NAME)
7219 // flags |= PM_WARNNESTED;
7220 // else
7221 // opts[WARNNESTEDVAR] = 0;
7222 // }
7223 // WARNNESTEDVAR is in the always-restored subset at the exit block
7224 // (c:6161).
7225 if (fn_flags & PM_WARNNESTED) != 0 {
7226 opt_state_set("warnnestedvar", true); // c:6023
7227 } else if (oflags_prev & PM_WARNNESTED) != 0 {
7228 // c:6024
7229 if shfunc.node.nam == ANONYMOUS_FUNCTION_NAME {
7230 fn_flags |= PM_WARNNESTED; // c:6026
7231 } else {
7232 opt_state_set("warnnestedvar", false); // c:6028
7233 }
7234 }
7235 FUNC_OFLAGS.store(fn_flags, Ordering::Relaxed); // c:5970
7236
7237 // c:5977 — `opts[PRINTEXITVALUE] = 0;` — suppress printexitvalue
7238 // for inner commands; outer flag restored on exit.
7239 opt_state_set("printexitvalue", false);
7240
7241 // c:5978-5998 — pparams swap. C reads doshargs and constructs the
7242 // function's positional-param array. First arg is the function
7243 // name (regardless of FUNCTIONARGZERO); the rest become $1..$N.
7244 let funcsave_argv0: Option<String> = if !doshargs.is_empty() {
7245 // c:5978
7246 // c:5982-5985 — `pparams = x = zshcalloc(...)`.
7247 let positionals: Vec<String> = if doshargs.len() > 1 {
7248 doshargs[1..].to_vec()
7249 } else {
7250 Vec::new()
7251 };
7252 if let Ok(mut pp) = crate::ported::builtin::PPARAMS.lock() {
7253 *pp = positionals;
7254 }
7255 // c:5984-5987 — FUNCTIONARGZERO: save argzero, install
7256 // doshargs[0] (the function name).
7257 if isset(FUNCTIONARGZERO) {
7258 // c:5984
7259 let prev = crate::ported::utils::argzero();
7260 crate::ported::utils::set_argzero(Some(doshargs[0].clone())); // c:5986
7261 prev
7262 } else {
7263 None
7264 }
7265 } else {
7266 // c:5992-5997 — no args: empty pparams. argzero saved+dup'd.
7267 if let Ok(mut pp) = crate::ported::builtin::PPARAMS.lock() {
7268 *pp = Vec::new();
7269 }
7270 if isset(FUNCTIONARGZERO) {
7271 // c:5994
7272 let prev = crate::ported::utils::argzero();
7273 crate::ported::utils::set_argzero(prev.clone()); // c:5996 ztrdup(argzero)
7274 prev
7275 } else {
7276 None
7277 }
7278 };
7279
7280 // c:5999 — `++funcdepth;` — bumped on entry. Mirror via locallevel
7281 // since zshrs tracks function-call depth there.
7282 //
7283 // Plus the canonical startparamscope (c:6194 inside runshfunc).
7284 // zshrs's body_runner replaces runshfunc's `execode` call so the
7285 // startparamscope/endparamscope pair must wrap body_runner here,
7286 // not inside the closure. inc_locallevel is exactly startparamscope.
7287 inc_locallevel();
7288
7289 // c:6000-6004 — FUNCNEST check + `goto undoshfunc` on overflow.
7290 // Skip the runtime check (the zshrs fusevm doesn't recurse via
7291 // real stack frames so the depth limit is less critical), but
7292 // keep the comment so the C label `undoshfunc:` target is
7293 // visible — `goto undoshfunc;` here would jump straight to the
7294 // epilogue at the `undoshfunc:` label below.
7295
7296 // c:6005-6019 — funcstack frame push. The full C block:
7297 // funcsave->fstack.name = dupstring(name);
7298 // funcsave->fstack.caller = funcstack ? funcstack->name :
7299 // dupstring(argv0 ? argv0 : argzero);
7300 // funcsave->fstack.lineno = lineno;
7301 // funcsave->fstack.prev = funcstack;
7302 // funcsave->fstack.tp = FS_FUNC;
7303 // funcstack = &funcsave->fstack;
7304 // funcsave->fstack.flineno = shfunc->lineno;
7305 // funcsave->fstack.filename = getshfuncfile(shfunc);
7306 // c:6013 — `funcsave->fstack.lineno = lineno;` C has ONE lineno
7307 // global (params.c:123); zshrs mirrors it in both input::lineno
7308 // and lex::LEX_LINENO. The lex mirror is the one driven by
7309 // BUILTIN_SET_LINENO per statement AND zeroed for the duration
7310 // of a function body (see set_lineno(0) below), so it is the
7311 // one that matches C's value at call time: a call made INSIDE a
7312 // caller's body records the caller-relative line (0 for a
7313 // single-line fn), giving `$functrace` entries like `g:0`.
7314 // input::lineno stayed parked at the script-wide line and
7315 // produced `g:1`.
7316 let lineno_now = crate::ported::lex::lineno() as i64;
7317 let (caller, prev_tp): (Option<String>, Option<i32>) = {
7318 let stk = FUNCSTACK.lock().unwrap_or_else(|e| e.into_inner());
7319 if let Some(p) = stk.last() {
7320 (Some(p.name.clone()), Some(p.tp))
7321 } else {
7322 // c:6011-6012 — outermost: argv0 (saved) or argzero global.
7323 let z = funcsave_argv0
7324 .clone()
7325 .or_else(crate::ported::utils::argzero);
7326 (z, None)
7327 }
7328 };
7329 // c:6018-6019 — flineno: shfunc->lineno (function def line)
7330 let mut flineno = shfunc.lineno;
7331 // c:6079 — `funcsave->fstack.filename = getshfuncfile(shfunc);`, NOT a raw
7332 // read of `shfunc->filename`. `getshfuncfile` (c:Src/hashtable.c:1058-1068)
7333 // appends `/name` when PM_LOADDIR is set:
7334 // if (shf->node.flags & PM_LOADDIR)
7335 // return zhtricat(shf->filename, "/", shf->node.nam);
7336 // An fpath autoload stores the DIRECTORY in `filename` (loadautofnsetfile,
7337 // c:5657), so reading the field directly rendered a funcstack frame as the
7338 // directory — `$funcfiletrace` showed `/Users/…/.zshrs/functions:117`
7339 // where zsh has `/…/functions/_complete:117`.
7340 //
7341 // The `.or_else(Some(String::new()))` coercion is DELIBERATELY kept: C's
7342 // `getshfuncfile` can return NULL and c:5675 tests `!funcstack->filename`
7343 // for it, but several readers here assume non-`None`, so making that
7344 // distinction expressible is a separate change.
7345 let mut filename = if (shfunc.node.flags as u32 & crate::ported::zsh_h::PM_LOADDIR) != 0 {
7346 // c:1061-1062
7347 shfunc
7348 .filename
7349 .as_ref()
7350 .map(|d| format!("{}/{}", d, shfunc.node.nam))
7351 .or_else(|| Some(String::new()))
7352 } else {
7353 // c:1063-1066
7354 shfunc.filename.clone().or_else(|| Some(String::new()))
7355 };
7356 // !!! WARNING: RUST-ONLY HELPER !!!
7357 //
7358 // c:6019 — `funcsave->fstack.filename = getshfuncfile(shfunc);`, which
7359 // reads the shfunc's OWN `filename`, set by `loadautofnsetfile`
7360 // (c:5657, called from `loadautofn` at c:5735 / c:5757) when the
7361 // function was autoloaded out of `$fpath`.
7362 //
7363 // zshrs has no C counterpart for a function that exists ONLY as a Rust
7364 // port: `_main_complete`, `_normal`, `_dispatch`, … are dispatched from
7365 // `compsys::router::try_rust_dispatch` and the autoload prelude is
7366 // skipped entirely (vm_helper.rs — "no upstream shell function to
7367 // load"), so nothing ever records a `$fpath` file for the name and the
7368 // synthesized shfunc handed to us carries the caller's `scriptfilename`
7369 // ("zsh") or nothing at all. `$funcsourcetrace` / `$funcfiletrace` then
7370 // reported `zsh:1` for every completer frame where zsh names the
7371 // `$fpath` file — and `funcfiletrace` is read by real completion code
7372 // (`_git` derives its git-completion.bash search path from
7373 // `${funcsourcetrace[1]%:*}`).
7374 //
7375 // Stand in for `loadautofnsetfile` in exactly that case by resolving the
7376 // defining file out of `$fpath` the way `getfpfunc` (c:6219) does for a
7377 // real autoload. An autoload-installed function gets `shf->lineno == 0`
7378 // (c:5384-5388 only stamps a `name() { … }` STATEMENT), so the def line
7379 // is 0, matching zsh's `<fpath-file>:0` in `$funcsourcetrace`.
7380 //
7381 // The gate is `try_rust_dispatch` ALONE. That predicate is the same one
7382 // `vm_helper::dispatch_function_call` and `compcore::callcompfunc` use to
7383 // pick the body_runner, so `Some` means the frame being pushed IS the
7384 // port standing in for the stock `$fpath` file — and the router has
7385 // already refused every name a shell definition owns
7386 // (`has_fpath_override` / `has_shfunc_override`, router.rs:53,62), so
7387 // there is nothing left here to shadow.
7388 //
7389 // An additional "…and the name has no shfunctab node" clause used to
7390 // guard this, which meant it never fired in practice: `compinit` writes
7391 // one bare `autoload -Uz <every completer>` line into its dump
7392 // (Completion/compdump:113), so EVERY completer has a
7393 // PM_UNDEFINED stub by the time Tab is pressed. A bare-name stub records
7394 // neither `filename` nor PM_LOADDIR (`add_autoload_function`,
7395 // Src/builtin.c:3278-3334 — only the `/abs/dir/name` arm and the
7396 // inherited-loaddir arm set them), so `getshfuncfile` returns NULL for
7397 // it and the fallback chain in vm_helper landed on `scriptfilename`.
7398 // That is how `$funcsourcetrace` read `zsh:1` for `_vars`, `_dispatch`,
7399 // `_normal`, `_complete` where zsh reads `/usr/share/zsh/5.9/functions/
7400 // _vars:0` etc.
7401 //
7402 // Memoised, and deliberately never invalidated: C stamps `shf->filename`
7403 // ONCE, at autoload time, and a later `fpath=(…)` does not restamp it —
7404 // so a permanent per-name answer is what matches zsh, not a re-scan. It
7405 // also keeps `doshfunc` off the filesystem on the completion hot path.
7406 if crate::compsys::router::try_rust_dispatch(&name).is_some() {
7407 static RUST_PORT_FILE: std::sync::OnceLock<
7408 std::sync::Mutex<std::collections::HashMap<String, Option<String>>>,
7409 > = std::sync::OnceLock::new();
7410 let cache = RUST_PORT_FILE.get_or_init(|| std::sync::Mutex::new(Default::default()));
7411 let cached = cache
7412 .lock()
7413 .unwrap_or_else(|e| e.into_inner())
7414 .get(&name)
7415 .cloned();
7416 let resolved = match cached {
7417 Some(hit) => hit,
7418 None => {
7419 let mut fdir: Option<String> = None;
7420 let mut dump: Option<(eprog, i32)> = None;
7421 // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
7422 // autoloads.rkyv already records which `$fpath` directory
7423 // holds this name; take it from there instead of walking
7424 // `$fpath` for every native completer a new shell calls.
7425 let fpath = crate::ported::params::getaparam("fpath").unwrap_or_default();
7426 let from_shard = crate::autoload_cache::try_source_dir(&name, &fpath);
7427 // c:6219 `getfpfunc(s, ksh, test, fdir, 1)` with test_only —
7428 // a pure probe: it fills `*fdir` (c:6240 `*fdir = *pp;`)
7429 // without parsing the file.
7430 let hit = from_shard
7431 .or_else(|| getfpfunc(&name, &mut fdir, None, 1, &mut dump).and(fdir))
7432 // c:1061 (Src/hashtable.c, getshfuncfile) — a PM_LOADDIR
7433 // filename renders as
7434 // `zhtricat(shf->filename, "/", shf->node.nam)`.
7435 .map(|d| {
7436 if d.is_empty() {
7437 name.clone()
7438 } else {
7439 format!("{d}/{name}")
7440 }
7441 });
7442 cache
7443 .lock()
7444 .unwrap_or_else(|e| e.into_inner())
7445 .insert(name.clone(), hit.clone());
7446 hit
7447 }
7448 };
7449 if let Some(file) = resolved {
7450 filename = Some(file);
7451 flineno = 0; // c:5384-5388 — never runs for an autoload stub
7452 }
7453 }
7454 {
7455 let frame = crate::ported::zsh_h::funcstack {
7456 prev: None, // c:6014 (Vec-stack: index encodes link)
7457 name: dupstring(&name), // c:6005
7458 filename, // c:6019
7459 caller, // c:6011
7460 flineno, // c:6018
7461 lineno: lineno_now, // c:6013
7462 tp: FS_FUNC, // c:6015
7463 };
7464 let _ = prev_tp; // c:6011 (informational)
7465 let mut stack = FUNCSTACK.lock().unwrap_or_else(|e| e.into_inner());
7466 stack.push(frame); // c:6016 funcstack = &funcsave->fstack
7467 }
7468
7469 // c:6021-6042 — body execution. C: `runshfunc(prog, wrappers, name)`.
7470 // zshrs delegates to the body_runner closure (typically a fusevm
7471 // sub-VM run from the bridge). The closure returns the body's
7472 // exit status which becomes lastval.
7473 //
7474 // c:Src/exec.c:1251-1266 — push "shfunc" onto zsh_eval_context
7475 // so the body sees `${zsh_eval_context[*]}` containing the call
7476 // chain context. The execode-based path (c:1245-1282 port at
7477 // exec.rs:7092) already did this, but the fusevm body_runner
7478 // path skipped doshfunc's body_runner invocation without the
7479 // push. Bug #262 in docs/BUGS.md.
7480 //
7481 // Push BOTH the static `zsh_eval_context` (matches C's variable)
7482 // AND the paramtab array entry (what `${zsh_eval_context[*]}`
7483 // reads). Pop on every return path via the guard struct so
7484 // panics / early returns don't leak the entry. Inlined here
7485 // (sole caller) — `zsh_eval_context` is this module's own static.
7486 //
7487 // c:Src/exec.c:1251-1266 — `zsh_eval_context[*]` shell-visible
7488 // mirror: the array entry holds the stack; the scalar
7489 // `ZSH_EVAL_CONTEXT` holds the `:`-joined form. Both written via
7490 // the PM_READONLY bypass (`u_arr`/`u_str` direct), the same shape
7491 // the binary's `-c` ZSH_EVAL_CONTEXT init uses (bins/zshrs.rs).
7492 // `EvalContextFrame` owns both halves (push + paramtab sync, then
7493 // pop + sync on Drop) and is shared with the other C sites that
7494 // reach `execode` with a label — `bin_eval` ("eval", builtin.c:6209)
7495 // and the autoload body run ("loadautofunc", exec.c:5626).
7496 let _eval_ctx_guard = EvalContextFrame::push("shfunc");
7497 // c:Src/exec.c — function bodies execute with `lineno` reset to
7498 // the relative line within the body (incremented per WC_PIPE
7499 // from the wordcode-encoded lineno). zsh's zerrmsg
7500 // (Src/utils.c:301) emits the lineno prefix only when lineno
7501 // is non-zero AND (!SHINSTDIN || locallevel != 0). For an
7502 // inline single-line function like `f() { x=1 }`, the body's
7503 // WC_PIPE encodes lineno=1, exec sets `lineno = lineno - 1 =
7504 // 0`, and the zerrmsg path falls through to space-only ("f: ").
7505 //
7506 // zshrs's compiler doesn't thread WC_PIPE_LINENO into the
7507 // bytecode, so the global lineno stays at the script-wide
7508 // value (1 for inline `-c`). Suppress the line-number prefix
7509 // inside function bodies by saving lineno on entry and forcing
7510 // it to 0 during body execution; restore on exit. This makes
7511 // warnings inside functions emit `f: ...` matching zsh's
7512 // single-line-function format. Bug #54/#74/#86 in docs/BUGS.md.
7513 //
7514 // NOT for a PM_UNDEFINED function. Its `prog` is the `mkautofn` stub
7515 // (c:Src/builtin.c:3662, zsh-5.9.1), which carries no line marker, so in C the
7516 // `loadautofn` -> `getfpfunc` lookup it runs (c:5626-5632) still sees the
7517 // CALLER's `lineno`: a corrupt digest on `$fpath` is reported as
7518 // `_main_complete:218: invalid zwc file:` (c:Src/parse.c:3288 `zwarnnam`),
7519 // and zeroing it here printed `_main_complete: invalid zwc file:`. The load
7520 // runs inside `body_runner`; `run_function_body_only` zeroes `lineno` once
7521 // the definition is loaded, just before the body itself runs.
7522 let saved_lineno = crate::ported::lex::lineno();
7523 if (flags as u32 & PM_UNDEFINED) == 0 {
7524 crate::ported::lex::set_lineno(0);
7525 }
7526 // c:Src/exec.c:6173-6175 + c:6196-6198 — `runshfunc` saves
7527 // zunderscore before the body runs and restores it after, so
7528 // `$_` reads outside the function continue to reflect the
7529 // function CALL's last arg (set by setunderscore at c:3491
7530 // before doshfunc enters). Without this, commands inside the
7531 // body (`:`, `echo`, etc.) update `$_` to their own last arg,
7532 // and the post-call `echo "[$_]"` sees the body's residue
7533 // instead of the call's arg. Bug surfaced via
7534 // test_dollar_underscore_after_function_call.
7535 let saved_zunderscore = crate::ported::params::getsparam("_").unwrap_or_default();
7536 // c:6042 — `runshfunc(prog, wrappers, funcsave->fstack.name);` — run
7537 // the body through the module-registered wrapper chain, starting at
7538 // its head (`wrappers` == [`BODY_WRAPPERS`] index 0). `prog` is `None`
7539 // because the body arrives as `body_runner`; see [`runshfunc`] for the
7540 // split of C's c:6166-6202 between it and this function.
7541 let body_status = runshfunc(None, 0, &name, &mut body_runner);
7542 crate::ported::params::set_zunderscore(std::slice::from_ref(&saved_zunderscore));
7543 crate::ported::lex::set_lineno(saved_lineno);
7544 LASTVAL.store(body_status, Ordering::Relaxed);
7545 crate::extensions::emulation_output::bash_return_trap_exit();
7546
7547 // c:6043 — `doneshfunc:` label. The C `runshfunc` happy-path
7548 // falls through here from c:6042.
7549 // c:6044 — `funcstack = funcsave->fstack.prev;` — pop our frame.
7550 {
7551 let mut stack = FUNCSTACK.lock().unwrap_or_else(|e| e.into_inner());
7552 stack.pop();
7553 }
7554 // c:Src/exec.c:6260 — `endparamscope();` inside `runshfunc`, i.e.
7555 // BEFORE control returns to doshfunc's epilogue at c:6103. zshrs
7556 // hoists runshfunc's `startparamscope()` (c:6254) up to
7557 // `inc_locallevel()` above because the body arrives as a closure,
7558 // so its partner must be dropped here — at the same point in the
7559 // sequence C reaches it — not at the tail of the epilogue.
7560 //
7561 // The whole epilogue below therefore runs with the callee's locals
7562 // already popped, which is what C's own comment at c:6195-6196
7563 // asserts ("The endparamscope() has already happened, hence the
7564 // `+1` here" for the exit_pending test at c:6201). Three orderings
7565 // depend on it and all three were wrong while this call sat at the
7566 // tail:
7567 // * `endtrapscope()` (c:6174) dispatches a function-scoped EXIT
7568 // trap. In C the trap body runs in the CALLER's param scope:
7569 // X=5; f(){ local X=1; trap 'print IN=$X; X=99' EXIT; }; f
7570 // zsh -> IN=5 then X==99 outside
7571 // With endparamscope last, zshrs printed `IN=1` and the trap's
7572 // `X=99` landed in the doomed local shadow (outer X stayed 5).
7573 // * the OPTIND/optcind restore (c:6120-6122) must write the
7574 // CALLER's parameter, so it has to follow this pop — see the
7575 // restore site below.
7576 // * `foo() { exit 7; }; foo` needs the decrement before the
7577 // exit_pending comparison or `exit_level >= locallevel+1` is
7578 // off by one and the shell exits 0.
7579 endparamscope();
7580
7581 // c:6045 — `undoshfunc:` label. Reached either by fall-through
7582 // from c:6044 or by `goto undoshfunc;` from the FUNCNEST check
7583 // at c:6003. Tail epilogue follows.
7584
7585 // c:6046 — `--funcdepth;` — paired endparamscope (c:6200 inside
7586 // runshfunc) lives at c:6157 below as `endparamscope()`. Removed
7587 // the dec here so locallevel only decrements once per
7588 // function-call frame; double-dec was purging level-0 globals on
7589 // function exit (the `f() { x=foo; }; f; echo $x` regression).
7590
7591 // c:6047-6053 — retflag clear. C clears retflag and restores
7592 // outer breaks if a `return` fired.
7593 if RETFLAG.load(Ordering::SeqCst) != 0 {
7594 // c:6047
7595 RETFLAG.store(0, Ordering::SeqCst); // c:6051
7596 BREAKS.store(funcsave_breaks, Ordering::SeqCst); // c:6052
7597 }
7598
7599 // ksh93 fires the ERR trap for a function CALL that returns non-zero in
7600 // addition to the failing command inside it (`f() { false; }; f` -> two
7601 // ERR, `f() { return 3; }; f` -> one, from the call). zsh's process-global
7602 // `donetrap` (c:1598) would spare the call site, so a Korn shell re-arms
7603 // it as the function returns. Verified against ksh93u+m 1.0.10.
7604 if crate::extensions::dash_mode::ksh93_mode() {
7605 DONETRAP.store(0, Ordering::SeqCst);
7606 }
7607
7608 // c:6054-6058 — pparams + argv0 restore.
7609 if let Ok(mut pp) = crate::ported::builtin::PPARAMS.lock() {
7610 *pp = pptab; // c:6059 pparams = pptab
7611 }
7612 if let Some(saved) = funcsave_argv0 {
7613 // c:6055
7614 crate::ported::utils::set_argzero(Some(saved)); // c:6057
7615 }
7616
7617 // c:6120-6123 — `if (!isset(POSIXBUILTINS)) { zoptind =
7618 // funcsave->zoptind; optcind = funcsave->optcind; }`.
7619 //
7620 // Placement is load-bearing in both directions, which is why this
7621 // sits exactly where C has it — after the pparams/argv0 restore
7622 // (c:6114-6119) and before the option restore (c:6129-6162):
7623 //
7624 // * AFTER `endparamscope()` (already run above). In C `zoptind`
7625 // is a plain global int (c:47); in zshrs `$OPTIND` is an
7626 // ordinary paramtab entry, so "restore OPTIND" means "write the
7627 // visible parameter". Written before the scope pop it landed in
7628 // the CALLEE's own `local OPTIND` shadow, which the pop then
7629 // discarded — re-exposing the caller's parameter that the entry
7630 // reset at c:5967 had already clobbered to 1:
7631 // g() { local OPTIND OPTARG; }
7632 // f() { local OPTIND=9; g; print $OPTIND }
7633 // zsh -> 9 zshrs (before) -> 1
7634 // A callee with no `local OPTIND` was unaffected, which is why
7635 // it hid for so long; it reached every compsys port that
7636 // declares OPTIND local (`_describe` and thus every completer
7637 // calling it).
7638 //
7639 // * BEFORE `endtrapscope()` (c:6174). A function-scoped EXIT trap
7640 // that assigns OPTIND must win, because in C it runs after the
7641 // restore:
7642 // t1(){ trap 'OPTIND=99' EXIT; }; OPTIND=5; t1; print $OPTIND
7643 // zsh -> 99
7644 // Restoring after endtrapscope clobbered the trap's write to 5.
7645 //
7646 // * BEFORE the option restore (c:6129-6162), so `isset(POSIXBUILTINS)`
7647 // reads the CALLEE's setting, matching the entry-side reset at
7648 // c:5966 which is likewise evaluated inside the function's option
7649 // scope:
7650 // OPTIND=5; f(){ setopt localoptions posixbuiltins; OPTIND=42 }
7651 // f; print $OPTIND zsh -> 42
7652 if !crate::ported::zsh_h::isset(crate::ported::zsh_h::POSIXBUILTINS) {
7653 if let Some(saved) = funcsave_optind {
7654 // c:6121 `zoptind = funcsave->zoptind;`
7655 if let Ok(n) = saved.parse::<i64>() {
7656 crate::ported::params::setiparam("OPTIND", n);
7657 crate::ported::builtin::ZOPTIND.store(n as i32, Ordering::Relaxed);
7658 } else {
7659 crate::ported::params::setsparam("OPTIND", &saved);
7660 }
7661 }
7662 // c:6122 `optcind = funcsave->optcind;`
7663 crate::ported::builtin::OPTCIND.store(funcsave_optcind, Ordering::Relaxed);
7664 }
7665
7666 // c:6064 — `scriptname = funcsave->scriptname;`
7667 crate::ported::utils::set_scriptname(funcsave_scriptname);
7668
7669 // c:6067 — `endpatternscope();`
7670 crate::ported::pattern::endpatternscope();
7671
7672 // c:6078-6102 — LOCALOPTIONS restore. Re-apply the snapshot when
7673 // localoptions was set inside the body.
7674 // c:6129 — `if (isset(LOCALOPTIONS) || funcsave->restore_sticky)`.
7675 // A sticky-emulation call ALWAYS restores the full option set on
7676 // return, whether or not LOCAL_OPTIONS is on — otherwise the
7677 // emulation the call installed leaks into the caller.
7678 if crate::ported::options::opt_state_get("localoptions").unwrap_or(false)
7679 || funcsave_restore_sticky != 0
7680 {
7681 // c:6091 memcpy(opts, funcsave->opts, sizeof(opts)) — full restore.
7682 let mut restore = funcsave_opts.clone();
7683 // c:6089 — `funcsave->opts[PRIVILEGED] = opts[PRIVILEGED];`.
7684 // PRIVILEGED is carved out of the restore in C: a function that
7685 // dropped privileges does not get them back on return. The port
7686 // restored it with everything else.
7687 restore.carry_privileged_from_live();
7688 restore.restore();
7689 // c:6136 / c:6153 — `emulation = funcsave->emulation;`
7690 crate::ported::options::emulation.store(funcsave_emulation, Ordering::Relaxed);
7691 crate::ported::options::EMULATION.store(funcsave_emulation_live, Ordering::Relaxed);
7692 crate::ported::options::FULLY_EMULATING.store(funcsave_fully, Ordering::Relaxed);
7693 // c:6137 — `sticky = funcsave->sticky;` (restore_sticky arm only)
7694 if funcsave_restore_sticky != 0 {
7695 *sticky.lock().unwrap_or_else(|e| e.into_inner()) = funcsave_sticky;
7696 }
7697 } else {
7698 // c:6097-6101 — non-LOCALOPTIONS: restore only the always-
7699 // restored subset (XTRACE / PRINTEXITVALUE / LOCALOPTIONS /
7700 // LOCALLOOPS / WARNNESTEDVAR).
7701 for opt in [
7702 "xtrace",
7703 "printexitvalue",
7704 "localoptions",
7705 "localloops",
7706 "warnnestedvar",
7707 ] {
7708 if let Some(v) = funcsave_opts.saved_get(opt) {
7709 opt_state_set(opt, v);
7710 }
7711 }
7712 }
7713
7714 // c:5970 counterpart — `oflags = funcsave->oflags;` restores the caller's
7715 // attribute set so a sibling call after this one sees the right `-T`
7716 // inheritance. Bug #1058.
7717 FUNC_OFLAGS.store(oflags_prev, Ordering::Relaxed);
7718
7719 // c:6104-6112 — LOCALLOOPS warn-on-active-continue/break + restore
7720 // breaks/contflag/loops snapshot.
7721 if crate::ported::options::opt_state_get("localloops").unwrap_or(false) {
7722 // c:6106-6108 —
7723 // if (contflag) zwarn("`continue' active at end of function scope");
7724 // if (breaks) zwarn("`break' active at end of function scope");
7725 if CONTFLAG.load(Ordering::SeqCst) != 0 {
7726 crate::ported::utils::zwarn("`continue' active at end of function scope");
7727 // c:6106
7728 }
7729 if BREAKS.load(Ordering::SeqCst) != 0 {
7730 crate::ported::utils::zwarn("`break' active at end of function scope");
7731 // c:6107
7732 }
7733 BREAKS.store(funcsave_breaks, Ordering::SeqCst); // c:6109
7734 CONTFLAG.store(funcsave_contflag, Ordering::SeqCst); // c:6110
7735 LOOPS.store(funcsave_loops, Ordering::SeqCst); // c:6111
7736 }
7737
7738 // c:6174 — `endtrapscope();`
7739 //
7740 // Bug #80 in docs/BUGS.md: zshrs used to run endtrapscope while
7741 // locallevel was still at the function's own level, so savetrap
7742 // entries tagged `local == current_function_level` failed the
7743 // `local > locallevel` pop condition and nested EXIT traps never
7744 // restored — outer EXIT traps fired at script exit instead. That
7745 // was worked around by bracketing this call with a manual
7746 // decrement/re-bump of locallevel. The workaround is gone: the
7747 // real `endparamscope()` now runs at its C position above, so the
7748 // pop loop observes the genuine post-decrement level, and the
7749 // trap body executes in the caller's param scope as C does.
7750 //
7751 // The function body's "shfunc" context was pushed by runshfunc's
7752 // execode (c:Src/exec.c:5960 → c:1251) and popped when that execode
7753 // returned, before doshfunc reaches endtrapscope; a function-scoped
7754 // EXIT trap therefore sees the CALLER's context plus its own "trap".
7755 drop(_eval_ctx_guard);
7756 crate::ported::signals::endtrapscope();
7757
7758 // c:6116-6117 — TRAP_STATE_PRIMED branch: bump trap_return back.
7759 if TRAP_STATE.load(Ordering::Relaxed) == TRAP_STATE_PRIMED {
7760 // c:6116
7761 TRAP_RETURN.fetch_add(1, Ordering::Relaxed); // c:6117
7762 }
7763
7764 // c:6118 — `ret = lastval;`
7765 let ret = LASTVAL.load(Ordering::Relaxed);
7766
7767 // c:6119 — `noerrexit = funcsave->noerrexit;`
7768 noerrexit.store(funcsave_noerrexit, Ordering::Relaxed);
7769
7770 // c:6120-6124 — noreturnval: restore lastval + pipestats. C runs
7771 // the function for side-effects only; outer lastval/pipestats
7772 // should reflect the PRE-call state.
7773 if noreturnval {
7774 // c:6120
7775 LASTVAL.store(funcsave_lastval, Ordering::Relaxed); // c:6121
7776 if let Some(saved_ps) = funcsave_pipestats {
7777 let n = NUMPIPESTATS.get_or_init(|| std::sync::Mutex::new(0));
7778 if let Ok(mut nguard) = n.lock() {
7779 *nguard = funcsave_numpipestats; // c:6122
7780 }
7781 let p = PIPESTATS.get_or_init(|| std::sync::Mutex::new([0; MAX_PIPESTATS]));
7782 if let Ok(mut pguard) = p.lock() {
7783 for (i, v) in saved_ps.iter().enumerate() {
7784 if i < pguard.len() {
7785 pguard[i] = *v; // c:6123 memcpy
7786 }
7787 }
7788 }
7789 }
7790 }
7791
7792 // c:6128 — `unqueue_signals();`
7793 unqueue_signals();
7794
7795 // c:6135-6155 — exit_pending branch: when an `exit` was queued
7796 // inside the function body and we've unwound enough scopes for
7797 // it to take effect, either keep unwinding (still inside a
7798 // nested function) or actually exit the shell.
7799 let exit_pending = crate::ported::builtin::EXIT_PENDING.load(Ordering::Relaxed);
7800 let exit_level = crate::ported::builtin::EXIT_LEVEL.load(Ordering::Relaxed);
7801 let cur_locallevel = locallevel.load(Ordering::Relaxed) as i32;
7802 let cur_forklevel = FORKLEVEL.load(Ordering::Relaxed);
7803 let in_exit_trap = crate::ported::signals::in_exit_trap.load(Ordering::Relaxed); // c:Src/signals.c:63
7804 if exit_pending != 0 && exit_level >= cur_locallevel + 1 && in_exit_trap == 0 {
7805 // c:6141
7806 if cur_locallevel > cur_forklevel {
7807 // c:6143 — still inside a nested function: keep unwinding.
7808 RETFLAG.store(1, Ordering::Relaxed); // c:6144
7809 BREAKS.store(LOOPS.load(Ordering::Relaxed), Ordering::Relaxed); // c:6145
7810 } else {
7811 // c:6151 — out of all functions: exit for real.
7812 crate::ported::builtin::STOPMSG.store(1, Ordering::Relaxed); // c:6151
7813 let val = EXIT_VAL.load(Ordering::Relaxed);
7814 crate::ported::builtin::zexit(val, crate::ported::zsh_h::ZEXIT_NORMAL);
7815 // c:6152
7816 }
7817 }
7818
7819 ret // c:6157 return ret
7820}
7821
7822// ═══════════════════════════════════════════════════════════════════════
7823// `(eval)` funcstack frame — the `FS_EVAL` half of the funcstack/functrace/
7824// funcfiletrace subsystem whose `FS_FUNC` half is `doshfunc` above.
7825// ═══════════════════════════════════════════════════════════════════════
7826
7827/// !!! WARNING: RUST-ONLY HELPER !!!
7828///
7829/// C keeps `struct funcstack fstack;` as an automatic in `eval()`
7830/// (`Src/builtin.c:6148`) and unwinds it with a plain
7831/// `funcstack = funcstack->prev;` on the single return path
7832/// (`Src/builtin.c:6210`). zshrs's `FUNCSTACK` is a `Mutex<Vec<funcstack>>`
7833/// and its eval entry points have MANY early-return paths (errflag
7834/// containment, recursion backstop, `?` from `execute_script`), so the pop
7835/// is carried by `Drop` instead. There is no C counterpart for this type;
7836/// the pushed frame itself is a line-by-line port of `Src/builtin.c:6155-6193`.
7837///
7838/// Every caller that stands in for C's `eval()` — the `BUILTIN_EVAL`
7839/// handler and `eval_string` below — must hold one of these for the
7840/// duration of the eval'd body, or `$funcstack` / `$functrace` /
7841/// `$funcfiletrace` lose a frame relative to zsh.
7842pub struct EvalFuncstackFrame {
7843 /// c:6191/6193 `fpushed` — whether the frame was actually pushed.
7844 fpushed: bool,
7845 /// c:6147 — `int oineval = ineval, fpushed;`. Restored at c:6215
7846 /// (`ineval = oineval;`) by this guard's Drop.
7847 oineval: i32,
7848}
7849
7850impl EvalFuncstackFrame {
7851 /// Port of the funcstack prologue of `eval(char **argv)` from
7852 /// `Src/builtin.c:6155-6193`.
7853 ///
7854 /// ```c
7855 /// ineval = !isset(EVALLINENO);
7856 /// if (!ineval) {
7857 /// scriptname = "(eval)";
7858 /// fstack.prev = funcstack;
7859 /// fstack.name = scriptname;
7860 /// fstack.caller = funcstack ? funcstack->name : dupstring(argzero);
7861 /// fstack.lineno = lineno;
7862 /// fstack.tp = FS_EVAL;
7863 /// if (!funcstack || funcstack->tp == FS_SOURCE) {
7864 /// fstack.flineno = fstack.lineno;
7865 /// fstack.filename = fstack.caller;
7866 /// } else {
7867 /// fstack.flineno = funcstack->flineno + lineno;
7868 /// if (funcstack->tp == FS_EVAL)
7869 /// fstack.flineno--;
7870 /// fstack.filename = funcstack->filename;
7871 /// if (!fstack.filename)
7872 /// fstack.filename = "";
7873 /// }
7874 /// funcstack = &fstack;
7875 /// fpushed = 1;
7876 /// } else
7877 /// fpushed = 0;
7878 /// ```
7879 ///
7880 /// `scriptname` (c:6157) is left to the caller: the `BUILTIN_EVAL`
7881 /// handler saves/restores it around the whole builtin, which is where
7882 /// C's `oscriptname` (c:6146) / restore (c:6222) live.
7883 pub fn push() -> Self {
7884 use crate::ported::modules::parameter::FUNCSTACK;
7885 use crate::ported::zsh_h::{FS_EVAL, FS_SOURCE};
7886 // c:6147 — `int oineval = ineval, fpushed;`
7887 let oineval = crate::ported::builtin::ineval.load(std::sync::atomic::Ordering::Relaxed);
7888 // c:6155 — `ineval = !isset(EVALLINENO);`
7889 let ineval = !crate::ported::zsh_h::isset(crate::ported::zsh_h::EVALLINENO);
7890 // The global drives `Src/exec.c`'s three `!ineval` lineno gates
7891 // (c:1355 / c:1451 / c:2056); zshrs reads it in the
7892 // `BUILTIN_SET_LINENO` handler. Only the funcstack half of this
7893 // prologue was ported, so `unsetopt evallineno; eval 'print
7894 // $LINENO'` counted lines inside the eval string (1) instead of
7895 // reporting the caller's line (E01options.ztst:29).
7896 crate::ported::builtin::ineval.store(
7897 if ineval { 1 } else { 0 },
7898 std::sync::atomic::Ordering::Relaxed,
7899 );
7900 if ineval {
7901 return EvalFuncstackFrame {
7902 fpushed: false, // c:6193 fpushed = 0
7903 oineval,
7904 };
7905 }
7906 // c:6161 — `fstack.lineno = lineno;`. zshrs mirrors C's single
7907 // `lineno` global (params.c:123) in `lex::LEX_LINENO`, the one
7908 // `BUILTIN_SET_LINENO` drives per statement — the same mirror
7909 // `doshfunc` reads for its `FS_FUNC` frame (see c:6013 there).
7910 let lineno = crate::ported::lex::lineno() as i64;
7911 let frame = {
7912 let stk = FUNCSTACK.lock().unwrap_or_else(|e| e.into_inner());
7913 // c:6160 — `fstack.caller = funcstack ? funcstack->name
7914 // : dupstring(argzero);`
7915 let caller = match stk.last() {
7916 Some(f) => Some(f.name.clone()),
7917 None => crate::ported::utils::argzero(),
7918 };
7919 // c:6174-6188 — flineno/filename deduction. Identical logic to
7920 // `funcfiletracegetfn` (Src/Modules/parameter.c:724-762): an eval
7921 // is an inlined call from a tracing perspective.
7922 let (flineno, filename) = match stk.last() {
7923 // c:6174 — `if (!funcstack || funcstack->tp == FS_SOURCE)`
7924 None => (lineno, caller.clone()), // c:6175-6176
7925 Some(p) if p.tp == FS_SOURCE => (lineno, caller.clone()), // c:6175-6176
7926 Some(p) => {
7927 // c:6178 — `fstack.flineno = funcstack->flineno + lineno;`
7928 let mut flineno = p.flineno + lineno;
7929 // c:6183-6184 — `if (funcstack->tp == FS_EVAL) fstack.flineno--;`
7930 // Line numbers in eval start from 1, not zero, so offset
7931 // by one to get line in file.
7932 if p.tp == FS_EVAL {
7933 flineno -= 1;
7934 }
7935 // c:6185-6187 — `fstack.filename = funcstack->filename;
7936 // if (!fstack.filename) fstack.filename = "";`
7937 (
7938 flineno,
7939 Some(p.filename.clone().unwrap_or_else(String::new)),
7940 )
7941 }
7942 };
7943 crate::ported::zsh_h::funcstack {
7944 prev: None, // c:6158 (Vec-stack: index encodes link)
7945 name: "(eval)".to_string(), // c:6159 fstack.name = scriptname
7946 filename, // c:6176 / c:6185
7947 caller, // c:6160
7948 flineno, // c:6175 / c:6178
7949 lineno, // c:6161
7950 tp: FS_EVAL, // c:6162
7951 }
7952 };
7953 FUNCSTACK
7954 .lock()
7955 .unwrap_or_else(|e| e.into_inner())
7956 .push(frame); // c:6189 funcstack = &fstack
7957 EvalFuncstackFrame {
7958 fpushed: true, // c:6191 fpushed = 1
7959 oineval,
7960 }
7961 }
7962
7963 /// c:6191/6193 `fpushed` — whether `EVALLINENO` was set and a frame
7964 /// really went on. Callers mirror C's `if (!ineval) scriptname =
7965 /// "(eval)";` (c:6157) off the same test.
7966 pub fn pushed(&self) -> bool {
7967 self.fpushed
7968 }
7969}
7970
7971impl Drop for EvalFuncstackFrame {
7972 /// c:6209-6210 — `if (fpushed) funcstack = funcstack->prev;`
7973 /// c:6215 — `ineval = oineval;`
7974 fn drop(&mut self) {
7975 crate::ported::builtin::ineval.store(self.oineval, std::sync::atomic::Ordering::Relaxed); // c:6215
7976 if self.fpushed {
7977 crate::ported::modules::parameter::FUNCSTACK
7978 .lock()
7979 .unwrap_or_else(|e| e.into_inner())
7980 .pop();
7981 }
7982 }
7983}
7984
7985/// `TRAP_STATE_PRIMED` — re-exported from the canonical enum port rather
7986/// than redefined here.
7987///
7988/// This was a LOCAL `const … = 2`, which is `TRAP_STATE_FORCE_RETURN`'s value:
7989/// `enum trap_state { TRAP_STATE_INACTIVE, TRAP_STATE_PRIMED,
7990/// TRAP_STATE_FORCE_RETURN }` (zsh.h:2947-2960) makes PRIMED 1, and
7991/// zsh_h.rs:4501-4505 already carries all three with the right values. With
7992/// the wrong number, doshfunc's two trap_return adjustments (c:5866-5867 on
7993/// entry, c:6116-6117 on exit) never fired for a primed trap, so trap_return
7994/// stayed at -1 instead of reaching the -2 sentinel bin_break tests
7995/// (builtin.c:5845). `return N` inside a TRAPxxx function therefore never
7996/// promoted to TRAP_STATE_FORCE_RETURN:
7997/// TRAPINT() { print CAUGHT; return 130 }; kill -INT $$; print DONE
7998/// zsh : CAUGHT
7999/// zshrs: CAUGHT DONE
8000use crate::ported::zsh_h::TRAP_STATE_PRIMED;
8001
8002/// Port of `execfuncdef()` from `Src/exec.c:5309` — C decl `execfuncdef(Estate state, Eprog redir_prog)`.
8003/// Define a shell function: extract
8004/// name(s)+body from the wordcode payload, allocate the Shfunc,
8005/// install into `shfunctab` (named), or execute immediately (anon).
8006#[allow(non_snake_case)]
8007pub fn execfuncdef(state: &mut estate, mut redir_prog: Option<crate::ported::zsh_h::Eprog>) -> i32 {
8008 use crate::ported::hashtable::{dircache_set, shfunctab_lock};
8009 use crate::ported::jobs::{getsigidx, removetrapnode};
8010 use crate::ported::parse::{dupeprog, freeeprog, incrdumpcount};
8011 use crate::ported::signals::settrap;
8012 use crate::ported::utils::scriptfilename_get;
8013 use crate::ported::zsh_h::{
8014 eprog as eprog_t, hashnode, patprog as patprog_t, shfunc as shfunc_t, Patprog,
8015 EC_DUPTOK as _, EF_HEAP, EF_MAP, EF_REAL, FS_EVAL, FS_FUNC, PM_ANONYMOUS, PM_TAGGED,
8016 PM_TAGGED_LOCAL, PRINTEXITVALUE, SHINSTDIN, ZSIG_FUNC,
8017 };
8018 // c:5311 — `Shfunc shf;`
8019 let mut shf: Box<shfunc_t>;
8020 // c:5312 — `char *s = NULL;`
8021 let mut s: Option<String> = None;
8022 // c:5313 — `int signum, nprg, sbeg, nstrs, npats, do_tracing, len, plen, i, htok = 0, ret = 0;`
8023 let mut signum: i32;
8024 let nprg: i32;
8025 let sbeg: i32;
8026 let nstrs: i32;
8027 let npats: i32;
8028 let do_tracing: i32;
8029 let len: i32;
8030 let plen: i32;
8031 // `i` — C loop counter for pp stamp; Rust uses .map().collect().
8032 let mut htok: i32 = 0;
8033 let mut ret: i32 = 0;
8034 // c:5314 — `int anon_func = 0;`
8035 let mut anon_func: i32 = 0;
8036 // c:5315 — `Wordcode beg = state->pc, end;`
8037 let _beg: usize = state.pc;
8038 let mut end: usize;
8039 // c:5316 — `Eprog prog;`
8040 // (allocated inline per-iter below; no upfront binding needed)
8041 // c:5317 — `Patprog *pp;` — handled by Vec construction.
8042 // c:5318 — `LinkList names;`
8043 let names: Vec<String>;
8044 // c:5319 — `int tracing_flags;`
8045 let tracing_flags: i32;
8046
8047 // c:5321 — `end = beg + WC_FUNCDEF_SKIP(state->pc[-1]);`
8048 end = state.pc + WC_FUNCDEF_SKIP(state.prog.prog[state.pc.wrapping_sub(1)]) as usize;
8049 // c:5322 — `names = ecgetlist(state, *state->pc++, EC_DUPTOK, &htok);`
8050 let num = state.prog.prog[state.pc] as usize;
8051 state.pc += 1;
8052 names = ecgetlist(state, num, EC_DUPTOK, Some(&mut htok));
8053 // c:5323 — `sbeg = *state->pc++;`
8054 sbeg = state.prog.prog[state.pc] as i32;
8055 state.pc += 1;
8056 // c:5324 — `nstrs = *state->pc++;`
8057 nstrs = state.prog.prog[state.pc] as i32;
8058 state.pc += 1;
8059 // c:5325 — `npats = *state->pc++;`
8060 npats = state.prog.prog[state.pc] as i32;
8061 state.pc += 1;
8062 // c:5326 — `do_tracing = *state->pc++;`
8063 do_tracing = state.prog.prog[state.pc] as i32;
8064 state.pc += 1;
8065
8066 // c:5328 — `nprg = (end - state->pc);`
8067 nprg = end.saturating_sub(state.pc) as i32;
8068 // c:5329 — `plen = nprg * sizeof(wordcode);`
8069 plen = nprg.saturating_mul(size_of::<wordcode>() as i32);
8070 // c:5330 — `len = plen + (npats * sizeof(Patprog)) + nstrs;`
8071 len = plen + npats.saturating_mul(size_of::<usize>() as i32) + nstrs;
8072 // c:5331 — `tracing_flags = do_tracing ? PM_TAGGED_LOCAL : 0;`
8073 tracing_flags = if do_tracing != 0 {
8074 PM_TAGGED_LOCAL as i32
8075 } else {
8076 0
8077 };
8078
8079 // c:5333-5339 — htok name substitution.
8080 let mut names_mut: Vec<String> = names;
8081 if htok != 0 && !names_mut.is_empty() {
8082 execsubst(&mut names_mut); // c:5334
8083 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
8084 // c:5335
8085 state.pc = end; // c:5336
8086 return 1; // c:5337
8087 }
8088 }
8089
8090 // c:5397-5398 — `DPUTS(!names && redir_prog, "Passing redirection to anon function definition.");`
8091 crate::DPUTS!(
8092 num == 0 && redir_prog.is_some(),
8093 "Passing redirection to anon function definition."
8094 );
8095
8096 // c:5343 — `while (!names || (s = (char *) ugetnode(names))) {`
8097 // num==0 → anon (no names); else iterate names.
8098 let mut names_iter = names_mut.into_iter();
8099 loop {
8100 let no_names = num == 0;
8101 if !no_names {
8102 // c:5343 — `s = ugetnode(names)`; break when list exhausted.
8103 match names_iter.next() {
8104 Some(nm) => s = Some(nm),
8105 None => break,
8106 }
8107 }
8108 // c:5344-5374 — Eprog alloc.
8109 let prog: Box<eprog_t>;
8110 let dump_present = state.prog.dump.is_some();
8111 let make_pat = || -> Patprog {
8112 // c:5375-5376 `*pp = dummy_patprog1;` — sentinel slot.
8113 Box::new(patprog_t {
8114 startoff: 0,
8115 size: 0,
8116 mustoff: 0,
8117 patmlen: 0,
8118 globflags: 0,
8119 globend: 0,
8120 flags: 0,
8121 patnpar: 0,
8122 patstartch: 0,
8123 })
8124 };
8125 if no_names {
8126 // c:5345-5346 — `zhalloc`, `nref = -1`.
8127 // c:5355-5357 — EF_HEAP, no dump, npats pats on heap.
8128 let pats: Vec<Patprog> = (0..npats).map(|_| make_pat()).collect();
8129 let prog_words: Vec<wordcode> = state.prog.prog[state.pc..end].to_vec();
8130 // c:5365 — `prog->strs = state->strs + sbeg;`
8131 let strs_tail = state.strs.as_ref().map(|t| {
8132 let off = (sbeg as usize).min(t.len());
8133 t[off..].to_string()
8134 });
8135 prog = Box::new(eprog_t {
8136 flags: EF_HEAP,
8137 len,
8138 npats,
8139 nref: -1, // c:5346
8140 pats,
8141 prog: prog_words,
8142 strs: strs_tail,
8143 shf: None, // c:5377
8144 dump: None, // c:5356
8145 // c:5365 — `prog->strs = state->strs + sbeg;` — the tail is
8146 // the SAME pool, so it keeps the same encoding.
8147 strs_metafied: state.prog.strs_metafied,
8148 });
8149 } else if dump_present {
8150 // c:5358-5363 — EF_MAP path: refcount the dump, allocate
8151 // pats permanent, reuse `state->pc` slice in place.
8152 if let Some(dp) = state.prog.dump.as_deref() {
8153 incrdumpcount(dp); // c:5360
8154 }
8155 let pats: Vec<Patprog> = (0..npats).map(|_| make_pat()).collect();
8156 let prog_words: Vec<wordcode> = state.prog.prog[state.pc..end].to_vec();
8157 let strs_tail = state.strs.as_ref().map(|t| {
8158 let off = (sbeg as usize).min(t.len());
8159 t[off..].to_string()
8160 });
8161 prog = Box::new(eprog_t {
8162 flags: EF_MAP, // c:5359
8163 len,
8164 npats,
8165 nref: 1, // c:5349
8166 pats,
8167 prog: prog_words,
8168 strs: strs_tail,
8169 shf: None, // c:5377
8170 dump: state.prog.dump.clone(), // c:5361
8171 strs_metafied: state.prog.strs_metafied, // pool copied verbatim — carry provenance
8172 });
8173 } else {
8174 // c:5366-5374 — EF_REAL: copy wordcode + strs into a
8175 // freshly-owned eprog (no shared dump backing).
8176 let pats: Vec<Patprog> = (0..npats).map(|_| make_pat()).collect();
8177 let pc_end = state.pc + nprg as usize;
8178 let prog_words: Vec<wordcode> = state.prog.prog[state.pc..pc_end].to_vec();
8179 // c:5373 — `memcpy(prog->strs, state->strs + sbeg, nstrs);`
8180 let strs_copy = state.strs.as_ref().map(|t| {
8181 let off = (sbeg as usize).min(t.len());
8182 let n_avail = t.len().saturating_sub(off);
8183 let take = (nstrs as usize).min(n_avail);
8184 t[off..off + take].to_string()
8185 });
8186 prog = Box::new(eprog_t {
8187 flags: EF_REAL, // c:5367
8188 len,
8189 npats,
8190 nref: 1, // c:5349
8191 pats,
8192 prog: prog_words,
8193 strs: strs_copy,
8194 shf: None, // c:5377
8195 dump: None, // c:5371
8196 // c:5373 — `memcpy(prog->strs, state->strs + sbeg, nstrs);` —
8197 // a byte copy of the source pool, so the same encoding.
8198 strs_metafied: state.prog.strs_metafied,
8199 });
8200 }
8201
8202 // c:5379-5381 — Shfunc alloc + funcdef + tracing flags.
8203 shf = Box::new(shfunc_t {
8204 node: hashnode {
8205 next: None,
8206 nam: String::new(),
8207 flags: tracing_flags,
8208 },
8209 filename: scriptfilename_get(), // c:5383 `ztrdup(scriptfilename)`
8210 // c:5384-5388 — funcstack top FS_FUNC/FS_EVAL → flineno+lineno
8211 // else just lineno.
8212 lineno: {
8213 let cur_lineno = crate::ported::input::lineno.with(|l| l.get()) as i64;
8214 if let Ok(stk) = crate::ported::modules::parameter::FUNCSTACK.lock() {
8215 if let Some(top) = stk.last() {
8216 if top.tp == FS_FUNC || top.tp == FS_EVAL {
8217 top.flineno + cur_lineno
8218 } else {
8219 cur_lineno
8220 }
8221 } else {
8222 cur_lineno
8223 }
8224 } else {
8225 cur_lineno
8226 }
8227 },
8228 funcdef: Some(prog), // c:5380
8229 redir: None,
8230 sticky: None,
8231 body: None,
8232 redir_text: None,
8233 });
8234 // c:5396-5401 — redir_prog ownership.
8235 // C: `if (names && nonempty(names) && redir_prog) shf->redir = dupeprog(redir_prog,0)`
8236 // else `shf->redir = redir_prog; redir_prog = 0;`
8237 // "nonempty(names)" means there's a NEXT name still to consume —
8238 // i.e. peek the iterator.
8239 if !no_names && names_iter.len() > 0 && redir_prog.is_some() {
8240 // c:5397 — dupe so each earlier name gets its own copy; the
8241 // last name (when iterator drains) gets the original.
8242 if let Some(rp) = redir_prog.as_deref() {
8243 shf.redir = Some(Box::new(dupeprog(rp, false)));
8244 }
8245 } else {
8246 // c:5399-5400 — last name (or anon) takes original.
8247 shf.redir = redir_prog.take();
8248 }
8249 // c:5402 — `shfunc_set_sticky(shf);`
8250 shfunc_set_sticky(&mut shf);
8251
8252 if no_names {
8253 // c:5404-5457 — anonymous function: execute immediately.
8254 // `LinkList args;` c:5409
8255 let mut args: Vec<String>;
8256
8257 anon_func = 1; // c:5411
8258 shf.node.flags |= PM_ANONYMOUS as i32; // c:5412
8259
8260 state.pc = end; // c:5414
8261 // c:5415 — `end += *state->pc++;`
8262 end += state.prog.prog[state.pc] as usize;
8263 state.pc += 1;
8264 // c:5416 — `args = ecgetlist(state, *state->pc++, EC_DUPTOK, &htok);`
8265 let arg_count = state.prog.prog[state.pc] as usize;
8266 state.pc += 1;
8267 args = ecgetlist(state, arg_count, EC_DUPTOK, Some(&mut htok));
8268
8269 // c:5418-5429 — htok arg subst + cleanup-on-error.
8270 if htok != 0 && !args.is_empty() {
8271 execsubst(&mut args); // c:5419
8272 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
8273 // c:5421 — `freeeprog(shf->funcdef);`
8274 if let Some(mut fd) = shf.funcdef.take() {
8275 freeeprog(&mut fd);
8276 }
8277 if shf.redir.is_some() {
8278 // c:5422-5423 — "shouldn't be" anon+redir, but free if so.
8279 if let Some(mut rd) = shf.redir.take() {
8280 freeeprog(&mut rd);
8281 }
8282 }
8283 dircache_set(&mut shf.filename, None); // c:5424
8284 drop(shf); // c:5425 `zfree(shf, sizeof(*shf));`
8285 state.pc = end; // c:5426
8286 return 1; // c:5427
8287 }
8288 }
8289
8290 // c:5431-5432 — `setunderscore` to last arg (or "").
8291 let under_val = if !args.is_empty() {
8292 args.last().cloned().unwrap_or_default()
8293 } else {
8294 String::new()
8295 };
8296 setunderscore(&under_val);
8297
8298 // c:5434-5435 — `if (!args) args = newlinklist();`
8299 // (Rust Vec is never null; no-op.)
8300 shf.node.nam = ANONYMOUS_FUNCTION_NAME.to_string(); // c:5436
8301 // c:5437 — `pushnode(args, shf->node.nam);` — prepend.
8302 args.insert(0, shf.node.nam.clone());
8303
8304 execshfunc(&mut shf, &mut args); // c:5439
8305 ret = LASTVAL.load(Ordering::Relaxed); // c:5440
8306
8307 // c:5442-5450 — PRINTEXITVALUE+SHINSTDIN exit report.
8308 if isset(PRINTEXITVALUE) && isset(SHINSTDIN) && ret != 0 {
8309 eprintln!("zsh: exit {}", ret); // c:5445/5447
8310 }
8311
8312 // c:5452-5456 — cleanup.
8313 if let Some(mut fd) = shf.funcdef.take() {
8314 freeeprog(&mut fd);
8315 }
8316 if let Some(mut rd) = shf.redir.take() {
8317 // c:5453-5454 — "shouldn't be" but free if present.
8318 freeeprog(&mut rd);
8319 }
8320 dircache_set(&mut shf.filename, None); // c:5455
8321 drop(shf); // c:5456 `zfree(shf, sizeof(*shf));`
8322 break; // c:5457
8323 } else {
8324 // c:5458-5484 — named function path.
8325 let nm = s.as_deref().unwrap_or("");
8326 // c:5460-5475 — TRAP* signal-trap install.
8327 // BUGS.md #1114 — the TRAPxxx() FUNCTION-trap form is zsh-only.
8328 // bash, ksh and dash have no such concept: there `TRAPINT` is an
8329 // ordinary function whose name merely begins with TRAP, and SIGINT
8330 // keeps its default disposition. Recognising it in a drop-in mode
8331 // also SILENTLY DESTROYS an already-installed list trap for that
8332 // signal, since the two forms are alternatives in zsh.
8333 // posix_faithful() is raised only for a bare drop-in flag, so
8334 // --zsh, native zshrs and `emulate sh` are untouched.
8335 if !crate::extensions::dash_mode::posix_faithful() && nm.len() > 4 && nm.starts_with("TRAP") {
8336 if let Some(sn) = getsigidx(&nm[4..]) {
8337 signum = sn;
8338 // c:5462 — `if (settrap(signum, NULL, ZSIG_FUNC))`
8339 if settrap(signum, None, ZSIG_FUNC) != 0 {
8340 if let Some(mut fd) = shf.funcdef.take() {
8341 freeeprog(&mut fd); // c:5463
8342 }
8343 dircache_set(&mut shf.filename, None); // c:5464
8344 drop(shf); // c:5465
8345 state.pc = end; // c:5466
8346 return 1; // c:5467
8347 }
8348 // c:5474 — `removetrapnode(signum);`
8349 removetrapnode(signum);
8350 // c:Src/signals.c::settrap → unsettrap →
8351 // removetrap also clears sigfuncs[sig] (the C
8352 // string-form trap slot). zshrs's port stores
8353 // string-form bodies in a separate
8354 // `traps_table` HashMap not touched by
8355 // removetrap. Drop the string-form entry here
8356 // so dotrap's fallback doesn't double-dispatch
8357 // when a TRAPxxx function REPLACES an
8358 // existing `trap '...' SIG` registration. Bug
8359 // #541 in docs/BUGS.md.
8360 if let Ok(mut t) = crate::ported::builtin::traps_table().lock() {
8361 t.remove(&nm[4..]);
8362 }
8363 }
8364 }
8365 // c:5477-5482 — re-define-self trace flag propagate.
8366 if let Ok(stk) = crate::ported::modules::parameter::FUNCSTACK.lock() {
8367 if let Some(top) = stk.last() {
8368 if top.tp == FS_FUNC && top.name == nm {
8369 // c:5479 — `Shfunc old = shfunctab->getnode(s);`
8370 if let Ok(rd) = shfunctab_lock().read() {
8371 if let Some(old) = rd.get(nm) {
8372 // c:5481 — propagate PM_TAGGED|PM_TAGGED_LOCAL.
8373 shf.node.flags |=
8374 old.node.flags & (PM_TAGGED as i32 | PM_TAGGED_LOCAL as i32);
8375 }
8376 }
8377 }
8378 }
8379 }
8380 // c:5483 — `shfunctab->addnode(shfunctab, ztrdup(s), shf);`
8381 shf.node.nam = nm.to_string();
8382 // Lineage tap: the definition is a function's origin, and a
8383 // second definition of the same name is a `redefine` op.
8384 if crate::provenance::active() {
8385 crate::provenance::on_func_define(
8386 nm,
8387 shf.body.as_deref(),
8388 shf.filename.as_deref(),
8389 shf.lineno,
8390 );
8391 }
8392 if let Ok(mut wr) = shfunctab_lock().write() {
8393 wr.add(*shf);
8394 }
8395 }
8396 }
8397 // c:5486-5487 — `if (!anon_func) setunderscore("");`
8398 if anon_func == 0 {
8399 setunderscore("");
8400 }
8401 // c:5488-5491 — leftover redir cleanup ("shouldn't happen").
8402 if let Some(mut rd) = redir_prog.take() {
8403 freeeprog(&mut rd);
8404 }
8405 // c:5492 — `state->pc = end;`
8406 state.pc = end;
8407 // c:5493 — `return ret;`
8408 ret
8409}
8410
8411/// Port of `execsimple()` from `Src/exec.c:1290` — C decl `execsimple(Estate state)`.
8412/// Fast-path for single-Simple commands that bypasses the full
8413/// `execcmd_exec` machinery.
8414pub fn execsimple(state: &mut estate) -> i32 {
8415 // c:1292 — `wordcode code = *state->pc++;`
8416 let mut code = state.prog.prog[state.pc];
8417 state.pc += 1;
8418 // c:1295-1296 — `if (errflag) return (lastval = 1);`
8419 // A user interrupt sets ERRFLAG_INT, never ERRFLAG_ERROR (signals.c:457), and
8420 // the C line cited above tests the WHOLE errflag, so masking here let an
8421 // interrupted shell keep going where zsh stops.
8422 if errflag.load(Ordering::Relaxed) != 0 {
8423 LASTVAL.store(1, Ordering::Relaxed);
8424 return 1;
8425 }
8426 // c:1298-1299 — `if (!isset(EXECOPT)) return lastval = 0;`
8427 if !isset(crate::ported::zsh_h::EXECOPT) {
8428 LASTVAL.store(0, Ordering::Relaxed);
8429 return 0;
8430 }
8431 // c:1301-1303 — `if (!IN_EVAL_TRAP() && !ineval && code) lineno = code - 1;`
8432 // In evaluated traps, don't modify the line number (the trap
8433 // dispatcher restores it). `code` here is the wordcode-encoded
8434 // line number from the WC_SIMPLE entry at state.pc-1.
8435 if !crate::ported::zsh_h::IN_EVAL_TRAP()
8436 && crate::ported::builtin::INEVAL.load(Ordering::SeqCst) == 0
8437 && code != 0
8438 {
8439 crate::ported::input::lineno.with(|l| l.set((code as usize).saturating_sub(1)));
8440 }
8441 // c:1306 — `code = wc_code(*state->pc++);`
8442 code = wc_code(state.prog.prog[state.pc]);
8443 state.pc += 1;
8444 // c:1311-1312 — `otj = thisjob; thisjob = -1;`
8445 let otj = *THISJOB
8446 .get_or_init(|| std::sync::Mutex::new(-1))
8447 .lock()
8448 .unwrap();
8449 *THISJOB
8450 .get_or_init(|| std::sync::Mutex::new(-1))
8451 .lock()
8452 .unwrap() = -1;
8453 use crate::ported::zsh_h::{
8454 WC_ARITH, WC_CASE, WC_COND, WC_FOR, WC_REPEAT, WC_SELECT, WC_SUBSH, WC_TIMED, WC_TRY,
8455 WC_WHILE,
8456 };
8457 use crate::ported::zsh_h::{WC_ASSIGN, WC_CURSH};
8458 let lv = if code == WC_ASSIGN {
8459 // c:1315-1319 — assignment-only simple cmd path.
8460 // cmdoutval = 0; addvars(state, state->pc - 1, 0); setunderscore("");
8461 addvars(state, state.pc.saturating_sub(1), 0);
8462 setunderscore(""); // c:1317
8463 if isset(XTRACE) {
8464 eprintln!();
8465 }
8466 let ef = errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR;
8467 if ef != 0 {
8468 ef
8469 } else {
8470 0
8471 }
8472 } else {
8473 // c:1322-1330 — dispatch via execfuncs[code - WC_CURSH] or execfuncdef.
8474 let q = queue_signal_level();
8475 dont_queue_signals();
8476 let result = if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
8477 ERRFLAG_ERROR
8478 } else if code == WC_FUNCDEF {
8479 execfuncdef(state, None)
8480 } else {
8481 // c:5499 execfuncs[] table inlined — match the WC_* tag.
8482 match code {
8483 WC_CURSH => execcursh(state, 0),
8484 WC_SUBSH => execcursh(state, 0), // subshell folds to cursh body walk
8485 WC_FOR => execfor(state, 0),
8486 WC_SELECT => execselect(state, 0),
8487 WC_CASE => execcase(state, 0),
8488 WC_IF => execif(state, 0),
8489 WC_WHILE => execwhile(state, 0),
8490 WC_REPEAT => execrepeat(state, 0),
8491 WC_TIMED => exectime(state, 0),
8492 WC_COND => execcond(state, 0),
8493 WC_ARITH => execarith(state, 0),
8494 WC_TRY => exectry(state, 0),
8495 _ => 0,
8496 }
8497 };
8498 restore_queue_signals(q);
8499 result
8500 };
8501 // c:1334 — `thisjob = otj;`
8502 *THISJOB
8503 .get_or_init(|| std::sync::Mutex::new(-1))
8504 .lock()
8505 .unwrap() = otj;
8506 LASTVAL.store(lv, Ordering::Relaxed); // c:1336 — `return lastval = lv;`
8507 lv
8508}
8509
8510/// Port of `execlist()` from `Src/exec.c:1402` — C decl `execlist(Estate state, int dont_change_job, int exiting)`.
8511/// Walks WC_LIST entries, dispatches each sublist (WC_SUBLIST chain
8512/// inlined, same as C — there is no separate execsublist function),
8513/// runs the DEBUG / ERR / EXIT traps and applies ERREXIT / ERRRETURN.
8514pub fn execlist(state: &mut estate, dont_change_job: i32, mut exiting: i32) {
8515 use crate::ported::signals_h::{SIGDEBUG, SIGEXIT, SIGZERR};
8516 use crate::ported::zsh_h::{
8517 CS_CMDAND, CS_CMDOR, DEBUGBEFORECMD, ERREXIT, ERRRETURN, SHINSTDIN, WC_LIST_SKIP,
8518 WC_PIPE_LINENO, WC_SUBLIST_AND, WC_SUBLIST_NOT, WC_SUBLIST_OR, WC_SUBLIST_SIMPLE,
8519 WC_SUBLIST_SKIP, Z_SIMPLE, Z_SYNC,
8520 };
8521 static DONETRAP: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:1404
8522
8523 // c:1415 — `int oldnoerrexit = noerrexit;`
8524 let oldnoerrexit = noerrexit.load(Ordering::Relaxed);
8525
8526 queue_signals(); // c:1417
8527
8528 let cj = *THISJOB
8529 .get_or_init(|| std::sync::Mutex::new(-1))
8530 .lock()
8531 .unwrap(); // c:1419
8532 let old_pline_level = pline_level.load(Ordering::Relaxed); // c:1420
8533 let old_list_pipe = list_pipe.load(Ordering::Relaxed); // c:1421
8534 let old_list_pipe_job = list_pipe_job.load(Ordering::Relaxed); // c:1422
8535 let old_list_pipe_text: Option<String> = {
8536 // c:1423-1426
8537 let t = LIST_PIPE_TEXT.lock().unwrap();
8538 if t.is_empty() {
8539 None
8540 } else {
8541 Some(t.clone())
8542 }
8543 };
8544 let oldlineno = crate::ported::input::lineno.with(|l| l.get()); // c:1427
8545
8546 // c:1429-1432 — `if (sourcelevel && unset(SHINSTDIN)) {...}`
8547 if crate::ported::init::sourcelevel.load(Ordering::Relaxed) != 0 && !isset(SHINSTDIN) {
8548 pline_level.store(0, Ordering::Relaxed);
8549 list_pipe.store(0, Ordering::Relaxed);
8550 list_pipe_job.store(0, Ordering::Relaxed);
8551 LIST_PIPE_TEXT.lock().unwrap().clear();
8552 }
8553
8554 // c:1436-1440 — `code = *state->pc++; if (wc_code(code) != WC_LIST) lastval = 0;`
8555 let mut code = state.prog.prog[state.pc];
8556 state.pc += 1;
8557 if wc_code(code) != WC_LIST {
8558 LASTVAL.store(0, Ordering::Relaxed);
8559 }
8560 while wc_code(code) == WC_LIST
8561 && BREAKS.load(Ordering::SeqCst) == 0
8562 && RETFLAG.load(Ordering::SeqCst) == 0
8563 && errflag.load(Ordering::Relaxed) == 0
8564 {
8565 let mut this_donetrap = 0; // c:1443
8566 this_noerrexit.store(0, Ordering::Relaxed); // c:1444
8567
8568 let ltype = WC_LIST_TYPE(code) as i32; // c:1446
8569 let csp = crate::ported::prompt::CMDSTACK.with(|s| s.borrow().len()); // c:1447
8570
8571 if !crate::ported::zsh_h::IN_EVAL_TRAP()
8572 && crate::ported::builtin::INEVAL.load(Ordering::SeqCst) == 0
8573 {
8574 // c:1449-1469 — valid line number for debugging.
8575 let code2 = state.prog.prog[state.pc];
8576 let mut lnp1: wordcode = 0;
8577 if (ltype & Z_SIMPLE as i32) != 0 {
8578 lnp1 = code2;
8579 } else if wc_code(code2) == WC_SUBLIST {
8580 if WC_SUBLIST_FLAGS(code2) == WC_SUBLIST_SIMPLE {
8581 lnp1 = state.prog.prog[state.pc + 1];
8582 } else {
8583 lnp1 = WC_PIPE_LINENO(state.prog.prog[state.pc + 1]);
8584 }
8585 }
8586 if lnp1 != 0 {
8587 crate::ported::input::lineno.with(|l| l.set(lnp1 as usize - 1)); // c:1469
8588 }
8589 }
8590
8591 let debug_trapped = sigtrapped
8592 .lock()
8593 .unwrap()
8594 .get(SIGDEBUG as usize)
8595 .copied()
8596 .unwrap_or(0)
8597 != 0;
8598 let donedebug: i32;
8599 if debug_trapped && isset(DEBUGBEFORECMD) && intrap.load(Ordering::Relaxed) == 0 {
8600 // c:1472-1496
8601 let mut pc2 = state.pc;
8602 let oerrexit_opt = isset(ERREXIT);
8603 opt_state_set("errexit", false);
8604 noerrexit.fetch_or(NOERREXIT_EXIT | NOERREXIT_RETURN, Ordering::Relaxed);
8605 if (ltype & Z_SIMPLE as i32) != 0 {
8606 pc2 += 1; // c:1478 skip the line number
8607 }
8608 let pm = assignsparam(
8609 "ZSH_DEBUG_CMD",
8610 &crate::ported::text::getpermtext(state.prog.clone(), Some(pc2), 0),
8611 0,
8612 ); // c:1479
8613
8614 exiting = DONETRAP.load(Ordering::Relaxed); // c:1483
8615 let ret = LASTVAL.load(Ordering::Relaxed); // c:1484
8616 crate::ported::signals::dotrap(SIGDEBUG); // c:1485
8617 if RETFLAG.load(Ordering::SeqCst) == 0 {
8618 LASTVAL.store(ret, Ordering::Relaxed); // c:1487
8619 }
8620 DONETRAP.store(exiting, Ordering::Relaxed); // c:1488
8621 noerrexit.store(oldnoerrexit, Ordering::Relaxed); // c:1489
8622 // c:1493 — only execute the trap once per sublist, even if
8623 // the DEBUGBEFORECMD option changes.
8624 donedebug = if isset(ERREXIT) { 2 } else { 1 };
8625 opt_state_set("errexit", oerrexit_opt); // c:1494
8626 if pm.is_some() {
8627 unsetparam("ZSH_DEBUG_CMD"); // c:1496 unsetparam_pm(pm, 0, 1)
8628 }
8629 } else {
8630 donedebug = if intrap.load(Ordering::Relaxed) != 0 { 1 } else { 0 }; // c:1498
8631 }
8632
8633 // c:1502 — reset donetrap: a trap is only called once for each
8634 // sublist that fails.
8635 DONETRAP.store(0, Ordering::Relaxed);
8636 'sublist_done: {
8637 if (ltype & Z_SIMPLE as i32) != 0 {
8638 // c:1503-1509
8639 let next = state.pc + WC_LIST_SKIP(code) as usize;
8640 if donedebug != 2 {
8641 execsimple(state);
8642 }
8643 state.pc = next;
8644 break 'sublist_done;
8645 }
8646
8647 // c:1511 — loop through code followed by &&, ||, or end of sublist.
8648 code = state.prog.prog[state.pc];
8649 state.pc += 1;
8650 if donedebug == 2 {
8651 // c:1513-1522 — skip sublist.
8652 while wc_code(code) == WC_SUBLIST {
8653 state.pc += WC_SUBLIST_SKIP(code) as usize;
8654 if WC_SUBLIST_TYPE(code) == WC_SUBLIST_END {
8655 break;
8656 }
8657 code = state.prog.prog[state.pc];
8658 state.pc += 1;
8659 }
8660 DONETRAP.store(1, Ordering::Relaxed);
8661 break 'sublist_done;
8662 }
8663 while wc_code(code) == WC_SUBLIST {
8664 this_noerrexit.store(0, Ordering::Relaxed); // c:1525
8665 let isandor = WC_SUBLIST_TYPE(code) != WC_SUBLIST_END; // c:1526
8666 let isnot = (WC_SUBLIST_FLAGS(code) & WC_SUBLIST_NOT) != 0; // c:1527
8667 let mut next = state.pc + WC_SUBLIST_SKIP(code) as usize; // c:1528
8668 // c:1530 — suppress errexit for commands before && and || and after !
8669 if isandor || isnot {
8670 noerrexit.fetch_or(NOERREXIT_EXIT | NOERREXIT_RETURN, Ordering::Relaxed);
8671 }
8672 let simple = (WC_SUBLIST_FLAGS(code) & WC_SUBLIST_SIMPLE) != 0;
8673 match WC_SUBLIST_TYPE(code) {
8674 x if x == WC_SUBLIST_END => {
8675 // c:1533-1543 — end of sublist; just execute, ignoring status.
8676 if simple {
8677 execsimple(state);
8678 } else {
8679 execpline(
8680 state,
8681 code,
8682 ltype,
8683 if (ltype & Z_END as i32) != 0 && exiting != 0 { 1 } else { 0 },
8684 );
8685 }
8686 state.pc = next;
8687 // c:1541 — suppress errexit for the command "! ..."
8688 if isnot {
8689 this_noerrexit.store(1, Ordering::Relaxed);
8690 }
8691 break 'sublist_done;
8692 }
8693 x if x == WC_SUBLIST_AND => {
8694 // c:1545 — if the return code is non-zero, skip pipelines
8695 // until we find a sublist followed by ORNEXT.
8696 let r = if simple {
8697 execsimple(state)
8698 } else {
8699 execpline(state, code, Z_SYNC, 0)
8700 };
8701 if r != 0 || BREAKS.load(Ordering::SeqCst) != 0 {
8702 state.pc = next;
8703 code = state.prog.prog[state.pc];
8704 state.pc += 1;
8705 next = state.pc + WC_SUBLIST_SKIP(code) as usize;
8706 while wc_code(code) == WC_SUBLIST
8707 && WC_SUBLIST_TYPE(code) == WC_SUBLIST_AND
8708 {
8709 state.pc = next;
8710 code = state.prog.prog[state.pc];
8711 state.pc += 1;
8712 next = state.pc + WC_SUBLIST_SKIP(code) as usize;
8713 }
8714 if wc_code(code) != WC_SUBLIST {
8715 // c:1562-1567 — skipped to the end of the list
8716 // without executing the final pipeline: no error
8717 // handling for this sublist.
8718 this_donetrap = 1;
8719 break 'sublist_done;
8720 } else if WC_SUBLIST_TYPE(code) == WC_SUBLIST_END {
8721 this_donetrap = 1;
8722 state.pc = next;
8723 break 'sublist_done;
8724 }
8725 }
8726 cmdpush(CS_CMDAND as u8); // c:1580
8727 }
8728 x if x == WC_SUBLIST_OR => {
8729 // c:1582 — if the return code is zero, skip pipelines
8730 // until we find a sublist followed by ANDNEXT.
8731 let r = if simple {
8732 execsimple(state)
8733 } else {
8734 execpline(state, code, Z_SYNC, 0)
8735 };
8736 if r == 0 || BREAKS.load(Ordering::SeqCst) != 0 {
8737 state.pc = next;
8738 code = state.prog.prog[state.pc];
8739 state.pc += 1;
8740 next = state.pc + WC_SUBLIST_SKIP(code) as usize;
8741 while wc_code(code) == WC_SUBLIST
8742 && WC_SUBLIST_TYPE(code) == WC_SUBLIST_OR
8743 {
8744 state.pc = next;
8745 code = state.prog.prog[state.pc];
8746 state.pc += 1;
8747 next = state.pc + WC_SUBLIST_SKIP(code) as usize;
8748 }
8749 if wc_code(code) != WC_SUBLIST {
8750 this_donetrap = 1;
8751 break 'sublist_done;
8752 } else if WC_SUBLIST_TYPE(code) == WC_SUBLIST_END {
8753 this_donetrap = 1;
8754 state.pc = next;
8755 break 'sublist_done;
8756 }
8757 }
8758 cmdpush(CS_CMDOR as u8); // c:1617
8759 }
8760 _ => {}
8761 }
8762 state.pc = next; // c:1621
8763 code = state.prog.prog[state.pc];
8764 state.pc += 1;
8765 noerrexit.store(oldnoerrexit, Ordering::Relaxed); // c:1623
8766 }
8767 state.pc -= 1; // c:1625
8768 }
8769 // sublist_done:
8770 noerrexit.store(oldnoerrexit, Ordering::Relaxed); // c:1628
8771
8772 let debug_trapped = sigtrapped
8773 .lock()
8774 .unwrap()
8775 .get(SIGDEBUG as usize)
8776 .copied()
8777 .unwrap_or(0)
8778 != 0;
8779 if debug_trapped && !isset(DEBUGBEFORECMD) && donedebug == 0 {
8780 // c:1630-1648 — save and restore ERREXIT for consistency with
8781 // DEBUGBEFORECMD, even though it's not used.
8782 let oerrexit_opt = isset(ERREXIT);
8783 opt_state_set("errexit", false);
8784 noerrexit.fetch_or(NOERREXIT_EXIT | NOERREXIT_RETURN, Ordering::Relaxed);
8785 exiting = DONETRAP.load(Ordering::Relaxed);
8786 let ret = LASTVAL.load(Ordering::Relaxed);
8787 crate::ported::signals::dotrap(SIGDEBUG);
8788 if RETFLAG.load(Ordering::SeqCst) == 0 {
8789 LASTVAL.store(ret, Ordering::Relaxed);
8790 }
8791 DONETRAP.store(exiting, Ordering::Relaxed);
8792 noerrexit.store(oldnoerrexit, Ordering::Relaxed);
8793 opt_state_set("errexit", oerrexit_opt);
8794 }
8795
8796 // c:1650 — `cmdsp = csp;`
8797 crate::ported::prompt::CMDSTACK.with(|s| {
8798 let mut g = s.borrow_mut();
8799 if g.len() > csp {
8800 g.truncate(csp);
8801 }
8802 });
8803
8804 // c:1652-1690 — check whether we are suppressing traps/errexit
8805 // and if we haven't already performed them for this sublist.
8806 if this_noerrexit.load(Ordering::Relaxed) == 0
8807 && DONETRAP.load(Ordering::Relaxed) == 0
8808 && this_donetrap == 0
8809 {
8810 let zerr_trapped = sigtrapped
8811 .lock()
8812 .unwrap()
8813 .get(SIGZERR as usize)
8814 .copied()
8815 .unwrap_or(0)
8816 != 0;
8817 if zerr_trapped
8818 && LASTVAL.load(Ordering::Relaxed) != 0
8819 && (noerrexit.load(Ordering::Relaxed) & NOERREXIT_EXIT) == 0
8820 {
8821 let eflag = errflag.load(Ordering::Relaxed);
8822 errflag.store(0, Ordering::Relaxed);
8823 crate::ported::signals::dotrap(SIGZERR);
8824 errflag.store(eflag, Ordering::Relaxed);
8825 DONETRAP.store(1, Ordering::Relaxed);
8826 }
8827 if LASTVAL.load(Ordering::Relaxed) != 0 {
8828 let ne = noerrexit.load(Ordering::Relaxed);
8829 let errreturn = isset(ERRRETURN)
8830 && (isset(INTERACTIVE)
8831 || (locallevel.load(Ordering::Relaxed) as i32) != 0
8832 || crate::ported::init::sourcelevel.load(Ordering::Relaxed) != 0)
8833 && (ne & NOERREXIT_RETURN) == 0;
8834 let errexit = (isset(ERREXIT) || (isset(ERRRETURN) && !errreturn))
8835 && (ne & NOERREXIT_EXIT) == 0;
8836 if errexit {
8837 errflag.store(0, Ordering::Relaxed);
8838 let exit_trapped = sigtrapped
8839 .lock()
8840 .unwrap()
8841 .get(SIGEXIT as usize)
8842 .copied()
8843 .unwrap_or(0)
8844 != 0;
8845 if exit_trapped {
8846 crate::ported::signals::dotrap(SIGEXIT);
8847 }
8848 if crate::ported::params::mypid.load(Ordering::Relaxed)
8849 != unsafe { libc::getpid() } as i64
8850 {
8851 crate::ported::builtin::_realexit();
8852 } else {
8853 crate::ported::builtin::realexit();
8854 }
8855 }
8856 if errreturn {
8857 RETFLAG.store(1, Ordering::SeqCst);
8858 BREAKS.store(LOOPS.load(Ordering::SeqCst), Ordering::SeqCst);
8859 }
8860 }
8861 }
8862 if (ltype & Z_END as i32) != 0 {
8863 break; // c:1692
8864 }
8865 code = state.prog.prog[state.pc]; // c:1694
8866 state.pc += 1;
8867 }
8868 this_noerrexit.store(0, Ordering::Relaxed); // c:1696
8869 pline_level.store(old_pline_level, Ordering::Relaxed);
8870 list_pipe.store(old_list_pipe, Ordering::Relaxed);
8871 list_pipe_job.store(old_list_pipe_job, Ordering::Relaxed);
8872 match old_list_pipe_text {
8873 Some(t) => *LIST_PIPE_TEXT.lock().unwrap() = t, // c:1701
8874 None => LIST_PIPE_TEXT.lock().unwrap().clear(), // c:1704
8875 }
8876 crate::ported::input::lineno.with(|l| l.set(oldlineno)); // c:1706
8877 if dont_change_job != 0 {
8878 *THISJOB
8879 .get_or_init(|| std::sync::Mutex::new(-1))
8880 .lock()
8881 .unwrap() = cj; // c:1708
8882 }
8883
8884 let exit_trapped = sigtrapped
8885 .lock()
8886 .unwrap()
8887 .get(SIGEXIT as usize)
8888 .copied()
8889 .unwrap_or(0)
8890 != 0;
8891 if exiting != 0 && exit_trapped {
8892 // c:1710-1716
8893 let eflag = errflag.load(Ordering::Relaxed);
8894 errflag.store(0, Ordering::Relaxed); // clear the context for trap
8895 crate::ported::signals::dotrap(SIGEXIT);
8896 // make sure this doesn't get executed again.
8897 if let Some(slot) = sigtrapped.lock().unwrap().get_mut(SIGEXIT as usize) {
8898 *slot = 0;
8899 }
8900 errflag.store(eflag, Ordering::Relaxed);
8901 }
8902
8903 unqueue_signals(); // c:1719
8904}
8905
8906// WC_SUBLIST chain walk is inlined into execlist (per `Src/exec.c:1525-
8907// 1625`, the C source likewise inlines it — there's no `execsublist`
8908// function in zsh C).
8909
8910/// Port of `execcmd_getargs()` from `Src/exec.c:2791` — C decl `static void execcmd_getargs(LinkList preargs, LinkList args, int expand)`.
8911/// Transfer the first node of `args`
8912/// to `preargs`, performing `prefork` (singleton-list expansion) on
8913/// the way if `expand` is set. Used by `execcmd_exec` to pull the
8914/// command head one word at a time so prefix-modifier walking
8915/// (BINF_COMMAND, BINF_EXEC etc.) sees expanded names.
8916pub fn execcmd_getargs(preargs: &mut LinkList<String>, args: &mut LinkList<String>, expand: i32) {
8917 // c:2791
8918 if args.firstnode().is_none() {
8919 // c:2793 — `if (!firstnode(args)) return;`
8920 return;
8921 } else if expand != 0 {
8922 // c:2795
8923 // c:2796-2797 — `local_list0(svl); init_list0(svl);` —
8924 // stack-local single-bucket list. Rust uses a fresh
8925 // LinkList<String> per call.
8926 let mut svl: LinkList<String> = Default::default();
8927 // c:2799 — `addlinknode(&svl, uremnode(args, firstnode(args)));`
8928 if let Some(idx) = args.firstnode() {
8929 if let Some(head) = crate::ported::linklist::uremnode(args, idx) {
8930 svl.push_back(head);
8931 }
8932 }
8933 // c:2801 — `prefork(&svl, 0, NULL);`
8934 let mut rf = 0i32;
8935 prefork(&mut svl, 0, &mut rf);
8936 // c:2802 — `joinlists(preargs, &svl);`
8937 crate::ported::linklist::joinlists(preargs, &mut svl);
8938 } else {
8939 // c:2803-2804 — no-expand path: move head verbatim.
8940 if let Some(idx) = args.firstnode() {
8941 if let Some(head) = crate::ported::linklist::uremnode(args, idx) {
8942 preargs.push_back(head);
8943 }
8944 }
8945 }
8946}
8947
8948/// Port of `execcmd_fork()` from `Src/exec.c:2810` — C decl `execcmd_fork(Estate state, int how, int type, Wordcode varspc, LinkList *filelistp, char *text, int oautocont, int close_if_forked)`.
8949/// Wordcode varspc, LinkList *filelistp, char *text, int oautocont,
8950/// int close_if_forked)` from `Src/exec.c:2810-2893`.
8951///
8952/// Fork the current command into a child process: parent records
8953/// the pid + STTY env scan + addproc; child enters subshell, writes
8954/// `entersubsh_ret` back to parent through `synch` pipe, and returns
8955/// 0 so the caller can continue with the body.
8956///
8957/// `filelistp` out-arg is moved from `jobtab[thisjob].filelist`
8958/// only in the child branch (so the parent's `filelist` stays
8959/// untouched). Rust sig keeps the same C contract.
8960pub fn execcmd_fork(
8961 state: &mut estate,
8962 how: i32,
8963 typ: i32,
8964 varspc: Option<usize>,
8965 filelistp: &mut Vec<jobfile>,
8966 text: &str,
8967 oautocont: i32,
8968 close_if_forked: i32,
8969) -> i32 {
8970 use crate::ported::signals::sigtrapped as sigtrapped_static;
8971 use crate::ported::signals_h::SIGEXIT;
8972 use crate::ported::zsh_h::{
8973 AUTOCONTINUE, BGNICE, WC_ASSIGN as ZWC_ASSIGN, WC_ASSIGN_NUM as ZWC_ASSIGN_NUM,
8974 WC_ASSIGN_SCALAR as ZWC_ASSIGN_SCALAR, WC_ASSIGN_TYPE as ZWC_ASSIGN_TYPE,
8975 WC_SUBSH as ZWC_SUBSH, ZSIG_IGNORED, Z_ASYNC,
8976 };
8977 // c:2810
8978 let pid: libc::pid_t; // c:2814
8979 let mut synch: [i32; 2] = [-1, -1]; // c:2815
8980 let flags: i32; // c:2815
8981 let mut esret: entersubsh_ret = entersubsh_ret::default(); // c:2816
8982 // c:2817 — `struct timespec bgtime;` — bgtime is passed to zfork
8983 // for accounting; the Rust zfork wrapper expects Option<&mut ZshTimespec>.
8984 let mut bgtime = ZshTimespec::default();
8985
8986 child_block(); // c:2819
8987 esret.gleader = -1; // c:2820
8988 esret.list_pipe_job = -1; // c:2821
8989
8990 // c:2823 — `if (pipe(synch) < 0) { zerr("pipe failed: %e", errno); return -1; }`
8991 if unsafe { libc::pipe(synch.as_mut_ptr()) } < 0 {
8992 zerr(&format!("pipe failed: {}", std::io::Error::last_os_error()));
8993 return -1; // c:2825
8994 }
8995 // c:2826 — `else if ((pid = zfork(&bgtime)) == -1) { ... }`
8996 pid = zfork(Some(&mut bgtime));
8997 if pid == -1 {
8998 unsafe {
8999 libc::close(synch[0]); // c:2827
9000 libc::close(synch[1]); // c:2828
9001 }
9002 LASTVAL.store(1, Ordering::Relaxed); // c:2829
9003 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:2830
9004 return -1; // c:2831
9005 }
9006 if pid != 0 {
9007 // c:2833 — parent.
9008 unsafe { libc::close(synch[1]) }; // c:2834
9009 // c:2835 — `read_loop(synch[0], (char *)&esret, sizeof(esret));`
9010 let mut buf = [0u8; size_of::<entersubsh_ret>()];
9011 let _ = crate::ported::utils::read_loop(synch[0], &mut buf);
9012 // entersubsh_ret is two i32s; reconstruct from LE bytes (host order).
9013 if buf.len() >= 8 {
9014 esret.gleader = i32::from_ne_bytes([buf[0], buf[1], buf[2], buf[3]]);
9015 esret.list_pipe_job = i32::from_ne_bytes([buf[4], buf[5], buf[6], buf[7]]);
9016 }
9017 unsafe { libc::close(synch[0]) }; // c:2836
9018 if (how & Z_ASYNC as i32) != 0 {
9019 // c:2837 — `lastpid = (zlong) pid;`
9020 crate::ported::modules::clone::lastpid.store(pid, Ordering::Relaxed);
9021 } else {
9022 // c:2839 — `if (!jobtab[thisjob].stty_in_env && varspc)`.
9023 let thisjob_idx = {
9024 if let Some(m) = THISJOB.get() {
9025 *m.lock().unwrap()
9026 } else {
9027 -1
9028 }
9029 };
9030 // Examine the jobtab entry under lock.
9031 let stty_already = if thisjob_idx >= 0 {
9032 if let Some(jt) = JOBTAB.get() {
9033 let guard = jt.lock().unwrap();
9034 guard
9035 .get(thisjob_idx as usize)
9036 .map(|j| j.stty_in_env != 0)
9037 .unwrap_or(true)
9038 } else {
9039 true
9040 }
9041 } else {
9042 true
9043 };
9044 if !stty_already && varspc.is_some() {
9045 // c:2841-2851 — walk varspc looking for STTY=...
9046 let mut p = varspc.unwrap();
9047 loop {
9048 if p >= state.prog.prog.len() {
9049 break;
9050 }
9051 let ac = state.prog.prog[p];
9052 if wc_code(ac) != ZWC_ASSIGN {
9053 break;
9054 }
9055 // c:2845 — `if (!strcmp(ecrawstr(state->prog, p + 1, NULL), "STTY"))`
9056 let name = ecrawstr(&state.prog, p + 1, None);
9057 if name == "STTY" {
9058 // c:2846 — `jobtab[thisjob].stty_in_env = 1;`
9059 if let Some(jt) = JOBTAB.get() {
9060 let mut guard = jt.lock().unwrap();
9061 if let Some(j) = guard.get_mut(thisjob_idx as usize) {
9062 j.stty_in_env = 1;
9063 }
9064 }
9065 break; // c:2847
9066 }
9067 p += if ZWC_ASSIGN_TYPE(ac) == ZWC_ASSIGN_SCALAR {
9068 3 // c:2849
9069 } else {
9070 (ZWC_ASSIGN_NUM(ac) + 2) as usize // c:2850
9071 };
9072 }
9073 }
9074 }
9075 // c:2853 — `addproc(pid, text, 0, &bgtime, esret.gleader, esret.list_pipe_job);`
9076 if let Some(jt) = JOBTAB.get() {
9077 let mut guard = jt.lock().unwrap();
9078 let tj = {
9079 if let Some(m) = THISJOB.get() {
9080 *m.lock().unwrap()
9081 } else {
9082 -1
9083 }
9084 };
9085 if tj >= 0 {
9086 if let Some(j) = guard.get_mut(tj as usize) {
9087 crate::ported::jobs::addproc(
9088 j,
9089 pid,
9090 text,
9091 false,
9092 Some(std::time::Instant::now()),
9093 esret.gleader,
9094 esret.list_pipe_job,
9095 );
9096 }
9097 }
9098 }
9099 // c:2854-2855 — `if (oautocont >= 0) opts[AUTOCONTINUE] = oautocont;`
9100 if oautocont >= 0 {
9101 opt_state_set("autocontinue", oautocont != 0);
9102 let _ = AUTOCONTINUE; // const referenced for parity
9103 }
9104 // c:2856 — `pipecleanfilelist(jobtab[thisjob].filelist, 1);`
9105 if let Some(jt) = JOBTAB.get() {
9106 let mut guard = jt.lock().unwrap();
9107 let tj = {
9108 if let Some(m) = THISJOB.get() {
9109 *m.lock().unwrap()
9110 } else {
9111 -1
9112 }
9113 };
9114 if tj >= 0 {
9115 if let Some(j) = guard.get_mut(tj as usize) {
9116 crate::ported::jobs::pipecleanfilelist(j, true);
9117 }
9118 }
9119 }
9120 return pid; // c:2857
9121 }
9122
9123 // c:2860 — pid == 0 (child).
9124 unsafe { libc::close(synch[0]) }; // c:2861
9125 flags = (if (how & Z_ASYNC as i32) != 0 {
9126 esub::ASYNC
9127 } else {
9128 0
9129 }) | esub::PGRP; // c:2862
9130 let mut flags = flags;
9131 if typ != ZWC_SUBSH as i32 && (how & Z_ASYNC as i32) == 0 {
9132 flags |= esub::KEEPTRAP; // c:2864
9133 }
9134 if typ == ZWC_SUBSH as i32 && (how & Z_ASYNC as i32) == 0 {
9135 flags |= esub::JOB_CONTROL; // c:2866
9136 }
9137 // c:2867 — `*filelistp = jobtab[thisjob].filelist;`
9138 if let Some(jt) = JOBTAB.get() {
9139 let mut guard = jt.lock().unwrap();
9140 let tj = {
9141 if let Some(m) = THISJOB.get() {
9142 *m.lock().unwrap()
9143 } else {
9144 -1
9145 }
9146 };
9147 if tj >= 0 {
9148 if let Some(j) = guard.get_mut(tj as usize) {
9149 *filelistp = std::mem::take(&mut j.filelist);
9150 }
9151 }
9152 }
9153 entersubsh(flags, Some(&mut esret)); // c:2868
9154 // c:2869 — `write_loop(synch[1], &esret, sizeof(esret));`
9155 let mut buf = [0u8; 8];
9156 buf[0..4].copy_from_slice(&esret.gleader.to_ne_bytes());
9157 buf[4..8].copy_from_slice(&esret.list_pipe_job.to_ne_bytes());
9158 if write_loop(synch[1], &buf).map(|n| n as usize).unwrap_or(0) != buf.len() {
9159 zerr(&format!(
9160 "Failed to send entersubsh_ret report: {}",
9161 std::io::Error::last_os_error()
9162 ));
9163 return -1; // c:2871
9164 }
9165 unsafe { libc::close(synch[1]) }; // c:2873
9166 let _ = zclose(close_if_forked); // c:2874
9167
9168 // c:2876 — `if (sigtrapped[SIGINT] & ZSIG_IGNORED) holdintr();`
9169 let sigint_state = {
9170 let guard = sigtrapped_static.lock().unwrap();
9171 guard.get(libc::SIGINT as usize).copied().unwrap_or(0)
9172 };
9173 if (sigint_state & ZSIG_IGNORED) != 0 {
9174 crate::ported::signals::holdintr(); // c:2877
9175 }
9176 // c:2882 — `sigtrapped[SIGEXIT] = 0;` — EXIT traps don't fire in fork-child.
9177 {
9178 let mut guard = sigtrapped_static.lock().unwrap();
9179 if let Some(slot) = guard.get_mut(SIGEXIT as usize) {
9180 *slot = 0;
9181 }
9182 }
9183 // c:2884-2890 — `if ((how & Z_ASYNC) && isset(BGNICE)) nice(5)`.
9184 // Per-platform errno setter+reader: __error() on macOS,
9185 // __errno_location() on Linux. Without cfg gating Linux CI breaks.
9186 if (how & Z_ASYNC as i32) != 0 && isset(BGNICE) {
9187 #[cfg(target_os = "macos")]
9188 unsafe {
9189 *libc::__error() = 0;
9190 if libc::nice(5) == -1 && *libc::__error() != 0 {
9191 zwarn(&format!(
9192 "nice(5) failed: {}",
9193 std::io::Error::last_os_error()
9194 ));
9195 }
9196 }
9197 #[cfg(target_os = "linux")]
9198 unsafe {
9199 *libc::__errno_location() = 0;
9200 if libc::nice(5) == -1 && *libc::__errno_location() != 0 {
9201 zwarn(&format!(
9202 "nice(5) failed: {}",
9203 std::io::Error::last_os_error()
9204 ));
9205 }
9206 }
9207 }
9208 0 // c:2892
9209}
9210
9211/// Port of `execcmd_analyse()` from `Src/exec.c:2733` — C decl `execcmd_analyse(Estate state, Execcmd_params eparams)`.
9212/// Pre-execcmd_exec analysis pass:
9213/// walks the wordcode at `state->pc`, splits out redirs/varspc/args
9214/// without expanding (no prefork, no globbing), and fills `eparams`
9215/// so the caller (execcmd_exec at c:2901 or execpline2 at c:2013)
9216/// can branch on the command type before the real work.
9217pub fn execcmd_analyse(state: &mut estate, eparams: &mut crate::ported::zsh_h::execcmd_params) {
9218 use crate::ported::zsh_h::{
9219 WC_ASSIGN as ZWC_ASSIGN, WC_REDIR as ZWC_REDIR, WC_SIMPLE as ZWC_SIMPLE,
9220 WC_SIMPLE_ARGC as ZWC_SIMPLE_ARGC, WC_TYPESET as ZWC_TYPESET,
9221 WC_TYPESET_ARGC as ZWC_TYPESET_ARGC,
9222 };
9223 // c:2733
9224 let mut code: wordcode; // c:2735
9225 let mut i: i32; // c:2736
9226 let _ = i;
9227
9228 // c:2738 — `eparams->beg = state->pc;`
9229 eparams.beg = state.pc;
9230 // c:2739-2740 — `eparams->redir = (wc_code(*state->pc) == WC_REDIR ? ecgetredirs(state) : NULL);`
9231 eparams.redir =
9232 if state.pc < state.prog.prog.len() && wc_code(state.prog.prog[state.pc]) == ZWC_REDIR {
9233 Some(crate::ported::parse::ecgetredirs(state))
9234 } else {
9235 None
9236 };
9237 // c:2741-2748 — varspc walk (WC_ASSIGN chain).
9238 if state.pc < state.prog.prog.len() && wc_code(state.prog.prog[state.pc]) == ZWC_ASSIGN {
9239 cmdoutval.store(0, Ordering::Relaxed); // c:2742
9240 eparams.varspc = Some(state.pc); // c:2743
9241 // c:2744-2746 — `while (wc_code((code = *state->pc)) == WC_ASSIGN) state->pc += ...`
9242 loop {
9243 if state.pc >= state.prog.prog.len() {
9244 break;
9245 }
9246 code = state.prog.prog[state.pc];
9247 if wc_code(code) != ZWC_ASSIGN {
9248 break;
9249 }
9250 state.pc += if WC_ASSIGN_TYPE(code) == WC_ASSIGN_SCALAR {
9251 3 // c:2745
9252 } else {
9253 (WC_ASSIGN_NUM(code) + 2) as usize // c:2746
9254 };
9255 }
9256 } else {
9257 eparams.varspc = None; // c:2748
9258 }
9259
9260 // c:2750 — `code = *state->pc++;`
9261 if state.pc >= state.prog.prog.len() {
9262 eparams.args = None;
9263 eparams.assignspc = None;
9264 eparams.typ = 0;
9265 eparams.postassigns = 0;
9266 eparams.htok = 0;
9267 return;
9268 }
9269 code = state.prog.prog[state.pc];
9270 state.pc += 1;
9271
9272 // c:2752 — `eparams->type = wc_code(code);`
9273 eparams.typ = wc_code(code) as i32;
9274 // c:2753 — `eparams->postassigns = 0;`
9275 eparams.postassigns = 0;
9276
9277 // c:2755-2783 — switch on type. EC_DUP is used (not EC_DUPTOK)
9278 // per the comment at c:2755-2757.
9279 match eparams.typ as wordcode {
9280 x if x == ZWC_SIMPLE => {
9281 // c:2759-2763
9282 let mut htok = 0;
9283 let argc = ZWC_SIMPLE_ARGC(code) as usize;
9284 eparams.args = Some(ecgetlist(state, argc, EC_DUP, Some(&mut htok)));
9285 eparams.htok = htok;
9286 eparams.assignspc = None;
9287 }
9288 x if x == ZWC_TYPESET => {
9289 // c:2765-2777
9290 let mut htok = 0;
9291 let argc = ZWC_TYPESET_ARGC(code) as usize;
9292 eparams.args = Some(ecgetlist(state, argc, EC_DUP, Some(&mut htok)));
9293 eparams.htok = htok;
9294 // c:2768 — `eparams->postassigns = *state->pc++;`
9295 if state.pc < state.prog.prog.len() {
9296 eparams.postassigns = state.prog.prog[state.pc] as i32;
9297 state.pc += 1;
9298 }
9299 // c:2769 — `eparams->assignspc = state->pc;`
9300 eparams.assignspc = Some(state.pc);
9301 // c:2770-2776 — walk past the postassigns.
9302 let mut k = 0i32;
9303 while k < eparams.postassigns {
9304 if state.pc >= state.prog.prog.len() {
9305 break;
9306 }
9307 code = state.prog.prog[state.pc];
9308 // c:2772-2773 DPUTS — assert wc_code == WC_ASSIGN; skipped.
9309 state.pc += if WC_ASSIGN_TYPE(code) == WC_ASSIGN_SCALAR {
9310 3 // c:2774
9311 } else {
9312 (WC_ASSIGN_NUM(code) + 2) as usize // c:2775
9313 };
9314 k += 1;
9315 }
9316 }
9317 _ => {
9318 // c:2779-2783 default.
9319 eparams.args = None;
9320 eparams.assignspc = None;
9321 eparams.htok = 0;
9322 }
9323 }
9324}
9325
9326/// Port of `char **zsh_eval_context;` from `Src/exec.c` (zsh.export:355).
9327/// Stack of `"context"` labels used by `eval`-style nested execution:
9328/// `bin_dot`, `bin_eval`, `execode`, autoloads. Each `execode(prog,
9329/// ..., "context")` pushes its label and pops on return.
9330///
9331/// Storage is [`crate::thread_shell_state::ThreadMutex`], not a plain
9332/// `Mutex`: like `funcstack`, the stack describes where the ONE thread of
9333/// execution is, so a hook function running on a pool worker must not push
9334/// `"shfunc"` onto the shell thread's `$ZSH_EVAL_CONTEXT`.
9335pub static zsh_eval_context: crate::thread_shell_state::ThreadMutex<Vec<String>> =
9336 crate::thread_shell_state::ThreadMutex::new(Vec::new(), Vec::new);
9337
9338/// RAII form of C's `execode` context push/pop (`Src/exec.c:1265-1266`
9339/// and the matching `zsh_eval_context[alen] = NULL` at c:1281).
9340///
9341/// zshrs does not route every nested execution through `execode`, so
9342/// the callers that C reaches via `execode(prog, …, "label")` push
9343/// their label with this guard instead. Dropping it pops, on every
9344/// return path including a panic — C relies on the plain assignment at
9345/// c:1281 being reached, which the guard reproduces more robustly.
9346pub struct EvalContextFrame {
9347 pushed: bool,
9348}
9349
9350impl EvalContextFrame {
9351 /// c:1265-1266 — `zsh_eval_context[alen] = context;`
9352 pub fn push(context: &str) -> Self {
9353 let mut pushed = false;
9354 if let Ok(mut ctx) = zsh_eval_context.lock() {
9355 ctx.push(context.to_string());
9356 Self::sync(&ctx);
9357 pushed = true;
9358 }
9359 Self { pushed }
9360 }
9361
9362 /// Publish the `zsh_eval_context` stack into the shell-visible
9363 /// params. C keeps ONE `char **zsh_eval_context` that the parameter
9364 /// table points at directly (`IPDEF8`/`IPDEF9`, params.c:401/431),
9365 /// so a push is instantly visible as `$zsh_eval_context` /
9366 /// `$ZSH_EVAL_CONTEXT`. The Rust params own their storage, so every
9367 /// mutation of the static has to be mirrored — through the
9368 /// `u_arr`/`u_str` fields directly because both names are
9369 /// `PM_READONLY_SPECIAL`.
9370 pub(crate) fn sync(stack: &[String]) {
9371 // !!! WARNING: RUST-ONLY GUARD — NO C COUNTERPART !!!
9372 // C has one `char **zsh_eval_context` and one thread, so the
9373 // publish below cannot be reached from anywhere but the frame that
9374 // pushed. zshrs's param table is ONE shared table that
9375 // `async_precmd` hooks deliberately write into, so a worker's push
9376 // would overwrite the shell thread's `$ZSH_EVAL_CONTEXT` with the
9377 // hook's context. The worker still tracks its own stack (the
9378 // `ThreadMutex` above); only the shell-visible parameters stay the
9379 // shell thread's.
9380 if !crate::thread_shell_state::is_shell_thread() {
9381 return;
9382 }
9383 let joined = stack.join(":");
9384 if let Ok(mut tab) = crate::ported::params::paramtab().write() {
9385 // C writes through the one global `zsh_eval_context` the SPECIAL
9386 // points at. `typeset -h` / `local -h` over a special leaves a
9387 // plain local shadowing it in the table (c:Src/builtin.c:2062-2073,
9388 // `newspecial` stays NS_NONE); the shadow must not receive the
9389 // publish, so walk the `old` chain down to the node that still
9390 // carries PM_SPECIAL.
9391 fn special_of<'a>(
9392 mut pm: Option<&'a mut crate::ported::zsh_h::param>,
9393 ) -> Option<&'a mut crate::ported::zsh_h::param> {
9394 while let Some(p) = pm {
9395 if p.node.flags as u32 & crate::ported::zsh_h::PM_SPECIAL != 0 {
9396 return Some(p);
9397 }
9398 pm = p.old.as_deref_mut();
9399 }
9400 None
9401 }
9402 if let Some(pm) = special_of(tab.get_mut("zsh_eval_context").map(|p| &mut **p)) {
9403 pm.u_arr = Some(stack.to_vec());
9404 pm.node.flags &= !(crate::ported::zsh_h::PM_UNSET as i32);
9405 }
9406 if let Some(pm) = special_of(tab.get_mut("ZSH_EVAL_CONTEXT").map(|p| &mut **p)) {
9407 pm.u_str = Some(joined);
9408 pm.node.flags &= !(crate::ported::zsh_h::PM_UNSET as i32);
9409 }
9410 }
9411 }
9412}
9413
9414impl Drop for EvalContextFrame {
9415 fn drop(&mut self) {
9416 // c:1281 — `zsh_eval_context[alen] = NULL;`
9417 if self.pushed {
9418 if let Ok(mut ctx) = zsh_eval_context.lock() {
9419 ctx.pop();
9420 Self::sync(&ctx);
9421 }
9422 }
9423 }
9424}
9425
9426/// Port of `static int donetrap;` from `Src/exec.c:1351`. Tracks
9427/// whether the ZERR trap has already fired for the current sublist.
9428/// C source resets to 0 at sublist start (c:1455) and sets to 1
9429/// after `dotrap(SIGZERR)` (c:1602). The check
9430/// `if (!this_noerrexit && !donetrap && !this_donetrap)` at c:1598
9431/// suppresses re-firing within the same sublist AND, crucially,
9432/// carries the "already fired" state across a function-call return
9433/// boundary so the outer caller's post-command check doesn't fire
9434/// ZERR a second time for the same logical error. Bug #303 in
9435/// docs/BUGS.md.
9436///
9437/// Reset at each top-level statement boundary via
9438/// `BUILTIN_DONETRAP_RESET` emitted by `compile_list`. Set after
9439/// `dotrap(SIGZERR)` fires inside `BUILTIN_ERREXIT_CHECK`.
9440pub static DONETRAP: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
9441
9442/// Port of `save_params()` from `Src/exec.c:4410` — C decl `save_params(Estate state, Wordcode pc, LinkList *restore_p, LinkList *remove_p)`.
9443/// Walk WC_ASSIGN
9444/// chain at `pc`, snapshot each existing param into `restore_p` (so
9445/// the builtin/shfunc can restore them on return) and enqueue every
9446/// touched name in `remove_p` (so we know what to unset).
9447pub fn save_params(
9448 state: &mut estate,
9449 pc: usize,
9450 restore_p: &mut Vec<crate::ported::zsh_h::param>,
9451 remove_p: &mut Vec<String>,
9452) {
9453 use crate::ported::zsh_h::{
9454 PM_READONLY, PM_SPECIAL, WC_ASSIGN, WC_ASSIGN_NUM as ZWC_ASSIGN_NUM,
9455 WC_ASSIGN_SCALAR as ZWC_ASSIGN_SCALAR, WC_ASSIGN_TYPE as ZWC_ASSIGN_TYPE,
9456 };
9457 // c:4410 — `*restore_p = newlinklist();` — caller pre-allocates.
9458 // c:4417 — `*remove_p = newlinklist();` — caller pre-allocates.
9459 let mut p = pc;
9460 // c:4419 — `while (wc_code(ac = *pc) == WC_ASSIGN)`
9461 loop {
9462 if p >= state.prog.prog.len() {
9463 break;
9464 }
9465 let ac = state.prog.prog[p];
9466 if wc_code(ac) != WC_ASSIGN {
9467 break;
9468 }
9469 // c:4420 — `s = ecrawstr(state->prog, pc + 1, NULL);`
9470 let s = ecrawstr(&state.prog, p + 1, None);
9471 // c:4421 — `pm = paramtab->getnode(paramtab, s)`
9472 let pm_clone: Option<crate::ported::zsh_h::param> = {
9473 let tab = paramtab().read().unwrap();
9474 tab.get(&s).map(|b| (**b).clone())
9475 };
9476 if let Some(pm) = pm_clone {
9477 // c:4423-4424 — `if (pm->env) delenv(pm);`
9478 if pm.env.is_some() {
9479 crate::ported::params::delenv(&s);
9480 }
9481 // c:4425-4448 — copy if not readonly-special.
9482 if (pm.node.flags & PM_SPECIAL as i32) == 0 {
9483 // c:4426-4438 — regular param: deep copy via copyparam(tpm, pm, 0).
9484 let mut tpm = pm.clone();
9485 tpm.node.nam = s.clone();
9486 // copyparam with fakecopy=0 already done by the clone()
9487 // (Clone derives a deep copy of param fields).
9488 restore_p.push(tpm); // c:4451
9489 } else if (pm.node.flags & PM_READONLY as i32) == 0
9490 {
9491 // c:4439-4448 — special-but-not-readonly: fakecopy=1.
9492 let mut tpm = pm.clone();
9493 tpm.node.nam = pm.node.nam.clone();
9494 restore_p.push(tpm); // c:4451
9495 }
9496 // c:4449 — `addlinknode(*remove_p, dupstring(s));`
9497 remove_p.push(s.clone());
9498 } else {
9499 // c:4453 — `addlinknode(*remove_p, dupstring(s));`
9500 remove_p.push(s.clone());
9501 }
9502 // c:4455 — `pc += (WC_ASSIGN_TYPE(ac) == WC_ASSIGN_SCALAR ? 3 : WC_ASSIGN_NUM(ac) + 2);`
9503 p += if ZWC_ASSIGN_TYPE(ac) == ZWC_ASSIGN_SCALAR {
9504 3
9505 } else {
9506 (ZWC_ASSIGN_NUM(ac) + 2) as usize
9507 };
9508 }
9509}
9510
9511/// Port of `restore_params()` from `Src/exec.c:4464` — C decl `restore_params(LinkList restorelist, LinkList removelist)`.
9512/// After the builtin/shfunc returns,
9513/// unset every name in removelist, then for each saved param in
9514/// restorelist re-install its values (PM_SPECIAL go through gsu
9515/// setfn; regular params re-enter paramtab as-is).
9516pub fn restore_params(restorelist: Vec<crate::ported::zsh_h::param>, removelist: Vec<String>) {
9517 use crate::ported::zsh_h::{PM_READONLY, PM_SPECIAL};
9518 // c:4470-4476 — `while ((s = ugetnode(removelist)))` — unset each.
9519 for s in &removelist {
9520 // c:4471 — `if ((pm = paramtab->getnode(paramtab, s)) && !(pm->node.flags & PM_SPECIAL))`
9521 let flags = {
9522 let tab = paramtab().read().unwrap();
9523 tab.get(s).map(|p| p.node.flags)
9524 };
9525 if let Some(f) = flags {
9526 if (f & PM_SPECIAL as i32) == 0 {
9527 // !!! LOCK DISCIPLINE — NO GSU CALLBACK MAY RUN UNDER THE GUARD !!!
9528 // Same rule, and the same reasoning, as the long note in
9529 // `assignnparam` (params.rs). C holds no lock at all: c:4474
9530 // hands `unsetparam_pm` a pointer to the live node, so the
9531 // callee may reach anything, and this one does — its
9532 // `if (pm->env) delenv(pm)` arm (c:Src/params.c:3872) lands in
9533 // a `delenv` that re-takes `paramtab().write()`, and its
9534 // read-only rejection (c:Src/params.c:3852) calls `zerr`,
9535 // which is not a leaf either (zwarning →
9536 // zleentry(ZLE_CMD_TRASH) → zrefresh →
9537 // getaparam("zle_highlight") → this same lock). `paramtab()`
9538 // is a `std::sync::RwLock` and is NOT reentrant.
9539 //
9540 // Neither arm can fire from HERE today, and neither is
9541 // disarmed by the callee being a leaf — both are disarmed by
9542 // this caller's preconditions, one line apart:
9543 // * the `zerr` arm, by c:4473 clearing PM_READONLY
9544 // immediately below;
9545 // * the `delenv` arm, by `save_params` having already run
9546 // `if (pm->env) delenv(pm)` (c:4423-4424) over this very
9547 // name on the way in, which left `pm.env` None.
9548 // That is a guarantee held by a different function, for its
9549 // own reasons. `bin_ztie` had the identical shape with no such
9550 // guarantee and hung outright. The previous spelling here even
9551 // said `// Drop write guard before calling unsetparam_pm.` and
9552 // then re-acquired the guard on the next line, so the dispatch
9553 // ran under it anyway.
9554 //
9555 // Detach under the guard, release, dispatch, publish — C's
9556 // order. `unsetparam_pm` mutates only the node it is handed
9557 // and never unlinks it (its own comment records that the
9558 // c:3853-3935 removenode postlude is unported), so
9559 // republishing that node is the whole of the write-back.
9560 let staged = {
9561 // c:4473 — `pm->node.flags &= ~PM_READONLY;`
9562 let mut tab = paramtab().write().unwrap();
9563 tab.get_mut(s).map(|pm_mut| {
9564 pm_mut.node.flags &= !(PM_READONLY as i32);
9565 (**pm_mut).clone()
9566 })
9567 };
9568 // ---- guard released; none is held across the dispatch ----
9569 if let Some(mut pm) = staged {
9570 let _ = crate::ported::params::unsetparam_pm(&mut pm, 0, 0); // c:4474
9571 let mut tab = paramtab().write().unwrap();
9572 // Only if the node is still there — re-adding one the
9573 // dispatch removed would resurrect a parameter C dropped.
9574 if let Some(slot) = tab.get_mut(s) {
9575 **slot = pm;
9576 }
9577 }
9578 }
9579 }
9580 }
9581 // c:4478-4523 — restore saved params.
9582 for pm in restorelist {
9583 // c:4481-4520 — PM_SPECIAL: route through gsu setfn.
9584 // c:4521-4523 — non-special: re-install via paramtab.
9585 if (pm.node.flags & PM_SPECIAL as i32) != 0 {
9586 // c:4537 — `tpm = paramtab->getnode(paramtab, pm->node.nam);`
9587 // c:4542-4543 — `if (!pm->env && tpm->env) delenv(tpm);`
9588 if pm.env.is_none() {
9589 let live_env = {
9590 let tab = paramtab().read().unwrap();
9591 tab.get(&pm.node.nam).and_then(|p| p.env.clone())
9592 };
9593 if live_env.is_some() {
9594 crate::ported::params::delenv(&pm.node.nam);
9595 }
9596 }
9597 // c:4544 — `tpm->node.flags = pm->node.flags;`
9598 {
9599 let mut tab = paramtab().write().unwrap();
9600 if let Some(slot) = tab.get_mut(&pm.node.nam) {
9601 slot.node.flags = pm.node.flags;
9602 }
9603 }
9604 // c:4545-4563 — the value goes back through the LIVE node's own
9605 // setfn, so the special's write hook fires: `PATH=/tmp cmd` has
9606 // to leave `pathsetfn` re-deriving `$path` and the command hash
9607 // from the restored string. The previous spelling overwrote the
9608 // paramtab entry instead, which restored `$PATH` but left every
9609 // global the setfn derives sitting at the prefix assignment's
9610 // value.
9611 match crate::ported::zsh_h::PM_TYPE(pm.node.flags as u32) {
9612 crate::ported::zsh_h::PM_INTEGER => {
9613 // c:4551 — `tpm->gsu.i->setfn(tpm, pm->u.val);`
9614 crate::ported::params::setiparam(&pm.node.nam, pm.u_val);
9615 }
9616 crate::ported::zsh_h::PM_EFLOAT | crate::ported::zsh_h::PM_FFLOAT => {
9617 // c:4555 — `tpm->gsu.f->setfn(tpm, pm->u.dval);`
9618 crate::ported::params::setnparam(
9619 &pm.node.nam,
9620 crate::ported::zsh_h::mnumber {
9621 l: 0,
9622 d: pm.u_dval,
9623 type_: crate::ported::zsh_h::MN_FLOAT,
9624 },
9625 );
9626 }
9627 crate::ported::zsh_h::PM_ARRAY => {
9628 // c:4558 — `tpm->gsu.a->setfn(tpm, pm->u.arr);`
9629 crate::ported::params::setaparam(
9630 &pm.node.nam,
9631 pm.u_arr.clone().unwrap_or_default(),
9632 );
9633 }
9634 crate::ported::zsh_h::PM_HASHED => {
9635 // c:4561 — `tpm->gsu.h->setfn(tpm, pm->u.hash);`. zshrs
9636 // keeps an association's pairs OUTSIDE the Param, in
9637 // `paramtab_hashed_storage` (see `stdunsetfn`'s note at
9638 // params.rs:11044), so they are not in `pm` to hand over
9639 // here; the caller that saved them restores them.
9640 }
9641 _ => {
9642 // c:4548 — `tpm->gsu.s->setfn(tpm, pm->u.str);`
9643 crate::ported::params::setsparam(
9644 &pm.node.nam,
9645 pm.u_str.as_deref().unwrap_or(""),
9646 );
9647 }
9648 }
9649 } else {
9650 // c:4521 — `paramtab->addnode(paramtab, ztrdup(pm->node.nam), pm);`
9651 let mut tab = paramtab().write().unwrap();
9652 tab.insert(pm.node.nam.clone(), Box::new(pm));
9653 }
9654 }
9655}
9656
9657/// Port of `execode` from `Src/exec.c:1245` — C decl `execode(Eprog p, int dont_change_job, int exiting, char *context)`. Rust fn `execode_wordcode` is the wordcode form, driving the ported `execlist` interpreter; `execode` at exec.rs:8413 is the fusevm-pipeline entry that carries the C name.
9658/// Set up an `estate`
9659/// around the given Eprog and run `execlist`. Maintains the
9660/// `zsh_eval_context` stack so `$ZSH_EVAL_CONTEXT` reflects the
9661/// call chain.
9662///
9663/// NOTE: this is the WORDCODE form (drives the ported `execlist`
9664/// interpreter). zshrs's live execution pipeline is fusevm
9665/// (`compile_zsh` → VM), so the top-level REPL `loop()` and most call
9666/// sites run through [`execode`] (the ZshProgram/fusevm form below)
9667/// instead. This wordcode entry is retained for the internal
9668/// function-body callers (`doshfunc` / autoload) that already hold an
9669/// `Eprog`.
9670pub fn execode_wordcode(
9671 p: crate::ported::zsh_h::Eprog,
9672 dont_change_job: i32,
9673 exiting: i32,
9674 context: &str,
9675) {
9676 // c:1245
9677 let prog_ref = *p;
9678 // c:1247 — `struct estate s;`
9679 let mut s = estate {
9680 prog: Box::new(prog_ref.clone()),
9681 // c:1269 — `s.pc = p->prog;` — start at index 0.
9682 pc: 0,
9683 // c:1270 — `s.strs = p->strs;`
9684 strs: prog_ref.strs.clone(),
9685 strs_offset: 0,
9686 };
9687 // c:1251-1266 — push context onto zsh_eval_context.
9688 let pushed = {
9689 if let Ok(mut ctx) = zsh_eval_context.lock() {
9690 ctx.push(context.to_string());
9691 true
9692 } else {
9693 false
9694 }
9695 };
9696 // c:1271 — `useeprog(p);`
9697 crate::ported::parse::useeprog(&mut s.prog);
9698 // c:1273 — `execlist(&s, dont_change_job, exiting);`
9699 execlist(&mut s, dont_change_job, exiting);
9700 // c:1275 — `freeeprog(p);`
9701 crate::ported::parse::freeeprog(&mut s.prog);
9702 // c:1281 — `zsh_eval_context[alen] = NULL;` — pop our entry.
9703 if pushed {
9704 if let Ok(mut ctx) = zsh_eval_context.lock() {
9705 ctx.pop();
9706 }
9707 }
9708}
9709
9710thread_local! {
9711 /// !!! WARNING: RUST-ONLY CARRIER !!! The VM opens a simple command's
9712 /// redirections itself and hands `execcmd_exec` an empty `redir` list
9713 /// for a run-time-expanded head (fusevm_bridge BUILTIN_EXEC_DYNAMIC).
9714 /// Set for that call when the command HAD redirections, so `trycd`
9715 /// still sees c:3596 `(!redir || empty(redir))` as false.
9716 pub(crate) static REDIRS_PREAPPLIED: std::cell::Cell<bool> =
9717 const { std::cell::Cell::new(false) };
9718}
9719
9720thread_local! {
9721 /// The long-lived interactive executor for the top-level `loop()`
9722 /// REPL (the `zsh_main` path). Set once by the bin before
9723 /// `zsh_main`; [`execode`] runs each parsed program through it.
9724 /// Persists for the whole session so variables/functions survive
9725 /// across prompts.
9726 static SESSION_EXECUTOR: std::cell::Cell<Option<*mut crate::vm_helper::ShellExecutor>> =
9727 const { std::cell::Cell::new(None) };
9728}
9729
9730/// !!! WARNING: RUST-ONLY HELPER !!!
9731///
9732/// No function of this name exists in `Src/exec.c`. This helper stands in for
9733/// installing the process-lifetime fusevm executor that `execode`
9734/// (exec.rs:8413) runs programs on. C has no analogue: its interpreter state
9735/// is the `estate` built per call in `execode` (c:1245).
9736/// Register the persistent session executor used by [`execode`] for the
9737/// interactive `loop()` REPL. The pointer must outlive the session (the
9738/// bin keeps the executor alive until `zsh_main` exits the process).
9739pub fn install_session_executor(exec: &mut crate::vm_helper::ShellExecutor) {
9740 SESSION_EXECUTOR.with(|c| c.set(Some(exec as *mut crate::vm_helper::ShellExecutor)));
9741 // Mirror the pointer into the bridge so `with_session_context` can
9742 // establish a VM context for startup rc-sourcing (run_init_scripts,
9743 // c:1914) before the loop's first execode enters one.
9744 crate::fusevm_bridge::register_session_executor(exec);
9745}
9746
9747/// Port of `execode()` from `Src/exec.c:1245` — C decl `execode(Eprog p, int dont_change_job, int exiting, char *context)`. DIVERGENCE: this entry drives the fusevm pipeline over a `ZshProgram`; the faithful wordcode walker is `execode_wordcode` (exec.rs:8338).
9748/// zshrs `execode` — run an already-parsed `ZshProgram` (Src/exec.c:1245
9749/// `execode(prog, ...)`, called from `loop()`). This is the **exec.rs
9750/// exception** to the line-by-line port: rather than walk wordcode via
9751/// `execlist`, it drives zshrs's live engine — compile the program with
9752/// `compile_zsh` and run it on the session executor's fusevm VM. The
9753/// faithful `loop()` in init.rs calls this exactly as C calls execode.
9754/// Returns `$?` (0 when no session executor is installed).
9755pub fn execode(
9756 program: &crate::parse::ZshProgram,
9757 _dont_change_job: i32,
9758 _exiting: i32,
9759 context: &str,
9760) -> i32 {
9761 // c:1319 — `zsh_eval_context_push(context)`. The wordcode twin
9762 // (`execode_wordcode`) already does this; this fusevm variant is the
9763 // one `loop()` (Src/init.c:220) calls with "toplevel", so dropping the
9764 // push left `$zsh_eval_context` / `$ZSH_EVAL_CONTEXT` empty for the
9765 // whole interactive session (a `-c` shell got "cmdarg" from its own
9766 // caller and looked correct, hiding this).
9767 // c:1334 — the matching `zsh_eval_context_pop()` runs when this
9768 // guard drops, i.e. after execution, exactly as in C.
9769 let _ctx_frame = EvalContextFrame::push(context); // c:1319
9770 SESSION_EXECUTOR.with(|c| match c.get() {
9771 // SAFETY: set by install_session_executor to an executor that
9772 // lives for the whole single-threaded interactive session;
9773 // loop() runs only after the bin installs it.
9774 Some(ptr) => unsafe { (*ptr).execute_program(program) },
9775 None => 0,
9776 })
9777}
9778
9779// =========================================================================
9780// Live-executor accessors (former `exec_hooks` OnceLock layer).
9781//
9782// These are the **exec.rs exception**: src/ported/ code reaches the
9783// live fusevm `ShellExecutor` (param store, function dispatch, nested
9784// script/cmdsubst execution) through these thin wrappers instead of the
9785// deleted `exec_hooks` fn-pointer registry. Each delegates to
9786// `fusevm_bridge::try_with_executor` — `Some` when a VM execution
9787// context is in scope, `None` in unit-test / compsys contexts with no
9788// bridge running — and reproduces the exact per-call fallback the old
9789// `exec_hooks` wrappers used when no hook was installed. Behavior is
9790// byte-for-byte identical to the OnceLock path; only the indirection is
9791// gone. See `feedback_no_shellexecutor_in_ported` /
9792// `feedback_no_exec_script_from_ported`: the bridge belongs in exec.rs
9793// (the sanctioned exception), not scattered through src/ported.
9794// =========================================================================
9795
9796/// !!! WARNING: RUST-ONLY HELPER !!!
9797///
9798/// No function of this name exists in `Src/exec.c`. This helper stands in for
9799/// reading an array parameter through the live fusevm executor, falling back
9800/// to `params::getaparam`. C reads `paramtab` directly wherever it needs
9801/// this.
9802/// Array param value via the live executor; falls back to the direct
9803/// param table (`params::getaparam`) when no executor is in scope, so
9804/// compsys / unit-test environments still observe shell-side arrays.
9805pub fn array(name: &str) -> Option<Vec<String>> {
9806 // bash special arrays (PIPESTATUS / FUNCNAME / BASH_VERSINFO) alias the
9807 // zsh-native specials in --bash mode. Checked before the stored-array
9808 // lookup so a user array of the same name can still shadow it (rare), but
9809 // after the bash-mode gate so --zsh is untouched. Guard against the
9810 // PIPESTATUS→pipestatus self-alias recursing by only aliasing uppercase.
9811 if name.starts_with(|c: char| c.is_ascii_uppercase()) {
9812 if let Some(v) = crate::dash_mode::bash_special_array(name) {
9813 return Some(v);
9814 }
9815 }
9816 if let Some(Some(v)) = crate::fusevm_bridge::try_with_executor(|exec| exec.array(name)) {
9817 return Some(v);
9818 }
9819 crate::ported::params::getaparam(name)
9820}
9821
9822/// !!! WARNING: RUST-ONLY HELPER !!!
9823///
9824/// No function of this name exists in `Src/exec.c`. This helper stands in for
9825/// reading an associative parameter through the live fusevm executor. C reads
9826/// `paramtab` directly.
9827/// Associative-array param value via the live executor (`None` when no
9828/// executor / not set).
9829pub fn assoc(name: &str) -> Option<indexmap::IndexMap<String, String>> {
9830 crate::fusevm_bridge::try_with_executor(|exec| exec.assoc(name)).flatten()
9831}
9832
9833/// !!! WARNING: RUST-ONLY HELPER !!!
9834///
9835/// No function of this name exists in `Src/exec.c`. This helper stands in for
9836/// writing an array parameter through the live fusevm executor. C calls
9837/// `setaparam` directly.
9838/// Store an array param into the live executor (no-op without one).
9839pub fn set_array(name: &str, val: Vec<String>) {
9840 let _ = crate::fusevm_bridge::try_with_executor(|exec| exec.set_array(name.to_string(), val));
9841}
9842
9843/// !!! WARNING: RUST-ONLY HELPER !!!
9844///
9845/// No function of this name exists in `Src/exec.c`. This helper stands in for
9846/// writing an associative parameter through the live fusevm executor. C calls
9847/// `sethparam` directly.
9848/// Store an associative-array param into the live executor (no-op
9849/// without one).
9850pub fn set_assoc(name: &str, val: indexmap::IndexMap<String, String>) {
9851 let _ = crate::fusevm_bridge::try_with_executor(|exec| exec.set_assoc(name.to_string(), val));
9852}
9853
9854/// !!! WARNING: RUST-ONLY HELPER !!!
9855///
9856/// No function of this name exists in `Src/exec.c`. This helper stands in for
9857/// unsetting a scalar parameter through the live fusevm executor. C calls
9858/// `unsetparam` directly.
9859/// Unset a scalar param in the live executor (no-op without one).
9860pub fn unset_scalar(name: &str) {
9861 let _ = crate::fusevm_bridge::try_with_executor(|exec| exec.unset_scalar(name));
9862}
9863
9864/// !!! WARNING: RUST-ONLY HELPER !!!
9865///
9866/// No function of this name exists in `Src/exec.c`. This helper stands in for
9867/// unsetting an array parameter through the live fusevm executor. C calls
9868/// `unsetparam` directly.
9869/// Unset an array param in the live executor (no-op without one).
9870pub fn unset_array(name: &str) {
9871 let _ = crate::fusevm_bridge::try_with_executor(|exec| exec.unset_array(name));
9872}
9873
9874/// !!! WARNING: RUST-ONLY HELPER !!!
9875///
9876/// No function of this name exists in `Src/exec.c`. This helper stands in for
9877/// unsetting an associative parameter through the live fusevm executor. C
9878/// calls `unsetparam` directly.
9879/// Unset an associative-array param in the live executor (no-op
9880/// without one).
9881pub fn unset_assoc(name: &str) {
9882 let _ = crate::fusevm_bridge::try_with_executor(|exec| exec.unset_assoc(name));
9883}
9884
9885/// !!! WARNING: RUST-ONLY HELPER !!!
9886///
9887/// No function of this name exists in `Src/exec.c`. This helper stands in for
9888/// calling a shell function by name on the live fusevm executor, with the
9889/// full `doshfunc` (c:5823) scope wrap. C reaches the function through
9890/// `execshfunc` (c:5540) off a wordcode `Shfunc`.
9891/// Dispatch a shell-function call by name through the live executor
9892/// (full doshfunc scope wrap). `None` when no executor / not a
9893/// function.
9894pub fn dispatch_function_call(name: &str, args: &[String]) -> Option<i32> {
9895 // c:3490-3492 — `if (type != WC_FUNCDEF) setunderscore((args &&
9896 // nonempty(args)) ? getdata(lastnode(args)) : "")`. C's `args` list
9897 // carries the command word, so a bare `_pre` leaves `$_` == "_pre".
9898 // The compsys ports call shell functions through this shim rather
9899 // than through a parsed command, so those calls never reached
9900 // execcmd's write and the callee saw whatever `$_` the caller left:
9901 // zsh reports `_post` inside comppostfuncs where zshrs reported the
9902 // stale value.
9903 {
9904 let last = args.last().cloned().unwrap_or_else(|| name.to_string());
9905 crate::ported::params::set_zunderscore(std::slice::from_ref(&last)); // c:3491
9906 }
9907 let __ft = crate::ftime::start(name);
9908 if let Some(r) =
9909 crate::fusevm_bridge::try_with_executor(|exec| exec.dispatch_function_call(name, args))
9910 {
9911 crate::ftime::stop(__ft);
9912 return r;
9913 }
9914 // No active VM context: this is the loop()/zsh_main exit path where
9915 // `zexit` fires a `TRAPEXIT() { ... }` (via dotrap, Src/builtin.c:6043)
9916 // or the `zshexit` hook after `loop()` returned. Enter the installed
9917 // session executor so the handler runs in the shell. SAFETY per execode.
9918 SESSION_EXECUTOR.with(|c| match c.get() {
9919 Some(ptr) => {
9920 let _ctx = crate::fusevm_bridge::ExecutorContext::enter(unsafe { &mut *ptr });
9921 unsafe { (*ptr).dispatch_function_call(name, args) }
9922 }
9923 None => None,
9924 })
9925}
9926
9927/// !!! WARNING: RUST-ONLY HELPER !!!
9928///
9929/// No function of this name exists in `Src/exec.c`. This helper stands in for
9930/// running a function BODY on the live fusevm executor with no scope wrap,
9931/// for callers that already entered `doshfunc` (c:5823).
9932/// Body-only function dispatch (no doshfunc scope wrap) — call as the
9933/// `body_runner` of a direct `doshfunc(...)` invocation to avoid the
9934/// double-wrap of going back through [`dispatch_function_call`]. `None`
9935/// when no executor.
9936pub fn run_function_body(name: &str, args: &[String]) -> Option<i32> {
9937 if let Some(r) =
9938 crate::fusevm_bridge::try_with_executor(|exec| exec.run_function_body_only(name, args))
9939 {
9940 return r;
9941 }
9942 // Session-executor fallback for the no-VM-context exit path (e.g. the
9943 // `zshexit` hook fired by `zexit` from zsh_main). SAFETY per execode.
9944 SESSION_EXECUTOR.with(|c| match c.get() {
9945 Some(ptr) => {
9946 let _ctx = crate::fusevm_bridge::ExecutorContext::enter(unsafe { &mut *ptr });
9947 unsafe { (*ptr).run_function_body_only(name, args) }
9948 }
9949 None => None,
9950 })
9951}
9952
9953/// !!! WARNING: RUST-ONLY HELPER !!!
9954///
9955/// No function of this name exists in `Src/exec.c`. This helper stands in for
9956/// running a source string on the live fusevm executor. The C path is
9957/// `execstring` (c:1228) -> `parse_string` (c:283) -> `execode` (c:1245).
9958/// Run a script source string on the live executor. `Ok(0)` when no
9959/// executor is in scope.
9960pub fn execute_script(src: &str) -> Result<i32, String> {
9961 // c:1228 execstring → execode: no end-of-script hooks (see
9962 // ShellExecutor::execute_string_without_exit_hooks).
9963 if let Some(r) =
9964 crate::fusevm_bridge::try_with_executor(|exec| exec.execute_string_without_exit_hooks(src))
9965 {
9966 return r;
9967 }
9968 // No active VM context: the loop()/zsh_main exit path where `zexit`
9969 // runs a `trap '...' EXIT` raw body (Src/builtin.c:6043) after `loop()`
9970 // returned. Enter the installed session executor so the trap body runs
9971 // in the shell instead of silently no-op'ing. SAFETY per execode.
9972 SESSION_EXECUTOR.with(|c| match c.get() {
9973 Some(ptr) => {
9974 let _ctx = crate::fusevm_bridge::ExecutorContext::enter(unsafe { &mut *ptr });
9975 unsafe { (*ptr).execute_string_without_exit_hooks(src) }
9976 }
9977 None => Ok(0),
9978 })
9979}
9980
9981/// !!! WARNING: RUST-ONLY HELPER !!!
9982///
9983/// No function of this name exists in `Src/exec.c`. This helper stands in for
9984/// running a source string through the fusevm zsh pipeline. Same C path as
9985/// above: `execstring` (c:1228).
9986/// Run a script source string through the live executor's zsh pipeline.
9987/// `Ok(0)` when no executor is in scope.
9988///
9989/// Executor ladder, identical to [`execute_script`] above and to
9990/// `fusevm_bridge::source_file_per_command`: the innermost ACTIVE executor
9991/// when one is in scope, the installed session executor otherwise, `Ok(0)`
9992/// with neither.
9993///
9994/// The session-executor rung is what makes `execstring` (c:1228) behave like
9995/// C, where the interpreter is plain process globals and the string ALWAYS
9996/// runs. Every caller that fires between commands rather than inside one
9997/// reaches this with `CURRENT_EXECUTOR` unset — `checksched`'s
9998/// `execstring(sch->cmd, 0, 0, "sched")` (c:Src/Builtins/sched.c:122), fired
9999/// from the pre-prompt hook or from the `addtimedfn` deadline in ZLE's read
10000/// loop, and `dotrap`'s re-parse of a non-function trap action
10001/// (signals.rs). Without it, `try_with_executor` returned `None`, the
10002/// `.unwrap_or(Ok(0))` reported success, and the command was dropped on the
10003/// floor: `sched +1 'print hi'` listed the entry, dequeued it on time, and
10004/// printed nothing.
10005pub fn execute_script_zsh_pipeline(src: &str) -> Result<i32, String> {
10006 if let Some(r) =
10007 crate::fusevm_bridge::try_with_executor(|exec| exec.execute_string_without_exit_hooks(src))
10008 {
10009 return r;
10010 }
10011 SESSION_EXECUTOR.with(|c| match c.get() {
10012 // SAFETY: set by install_session_executor to an executor that lives
10013 // for the whole single-threaded interactive session. Reached only
10014 // when `CURRENT_EXECUTOR` is unset, i.e. no chunk is running on this
10015 // thread (`run_chunk` holds an `ExecutorContext` for the whole of
10016 // `vm.run()`), so this cannot alias a live `&mut ShellExecutor`.
10017 Some(ptr) => {
10018 let _ctx = crate::fusevm_bridge::ExecutorContext::enter(unsafe { &mut *ptr });
10019 unsafe { (*ptr).execute_string_without_exit_hooks(src) }
10020 }
10021 None => Ok(0),
10022 })
10023}
10024
10025/// !!! WARNING: RUST-ONLY HELPER !!!
10026///
10027/// No function of this name exists in `Src/exec.c`. This helper stands in for
10028/// running a `$(...)` body in-process on the live fusevm executor. C forks
10029/// and calls `entersubsh(ESUB_PGRP|ESUB_NOMONITOR, NULL)` inside `getoutput`
10030/// (c:4713); the in-process form applies that subshell state with a guard
10031/// instead.
10032/// Run a `$(...)` command substitution on the live executor, returning
10033/// captured stdout. Empty string when no executor is in scope.
10034///
10035/// The body runs IN-PROCESS (no fork), so the subshell state that
10036/// `Src/exec.c:4781`'s `entersubsh(ESUB_PGRP|ESUB_NOMONITOR, NULL)` would
10037/// have installed in the forked child is applied here by `SubshStateGuard`
10038/// and unwound when the substitution returns. Every caller of this funnel —
10039/// `getoutput` above and the p10k custom-segment renderer
10040/// (`extensions/p10k/segments_core.rs:917`, which is `content="$(eval
10041/// $command)"`) — is a genuine command substitution and wants it.
10042pub fn run_command_substitution(cmd: &str) -> String {
10043 // c:4781 — `entersubsh(ESUB_PGRP|ESUB_NOMONITOR, NULL);` in the child
10044 // arm of getoutput. Fork-safe subset only; see SubshStateGuard for the
10045 // applied/skipped breakdown.
10046 let _subsh_state = SubshStateGuard::enter();
10047 if let Some(r) =
10048 crate::fusevm_bridge::try_with_executor(|exec| exec.run_command_substitution(cmd))
10049 {
10050 return r;
10051 }
10052 // Session-executor fallback for no-VM-context callers — the native
10053 // p10k custom_* segments (p10k:1698 `content="$(eval $command)"`)
10054 // render at preprompt time, before any ExecutorContext is entered,
10055 // so try_with_executor alone returned "" and every custom segment
10056 // rendered empty. Same pattern as run_function_body above; SAFETY
10057 // per execode.
10058 SESSION_EXECUTOR.with(|c| match c.get() {
10059 Some(ptr) => {
10060 let _ctx = crate::fusevm_bridge::ExecutorContext::enter(unsafe { &mut *ptr });
10061 unsafe { (*ptr).run_command_substitution(cmd) }
10062 }
10063 None => String::new(),
10064 })
10065}
10066
10067/// !!! WARNING: RUST-ONLY HELPER !!!
10068///
10069/// No function of this name exists in `Src/exec.c`. This helper stands in for
10070/// reading the positional parameters off the live fusevm executor. C reads
10071/// the `pparams` global directly (Src/params.c).
10072/// Positional parameters ($1..$N) from the live executor; empty without
10073/// one.
10074pub fn pparams() -> Vec<String> {
10075 crate::fusevm_bridge::try_with_executor(|exec| exec.pparams()).unwrap_or_default()
10076}
10077
10078/// !!! WARNING: RUST-ONLY HELPER !!!
10079///
10080/// No function of this name exists in `Src/exec.c`. This helper stands in for
10081/// replacing the positional parameters on the live fusevm executor. C assigns
10082/// the `pparams` global directly (Src/params.c).
10083/// Replace the positional parameters in the live executor (no-op
10084/// without one).
10085pub fn set_pparams(v: Vec<String>) {
10086 let _ = crate::fusevm_bridge::try_with_executor(|exec| exec.set_pparams(v));
10087}
10088
10089/// !!! WARNING: RUST-ONLY HELPER !!!
10090///
10091/// No function of this name exists in `Src/exec.c`. This helper stands in for
10092/// dropping a function from the fusevm executor's compiled-chunk and source
10093/// maps. C removes the node from `shfunctab` directly.
10094/// Drop a function from both the compiled-chunk and source maps in the
10095/// live executor. Returns true if either entry existed; false when no
10096/// executor.
10097pub fn unregister_function(name: &str) -> bool {
10098 crate::fusevm_bridge::try_with_executor(|exec| {
10099 let a = exec.functions_compiled.remove(name).is_some();
10100 let b = exec.function_source.remove(name).is_some();
10101 a || b
10102 })
10103 .unwrap_or(false)
10104}
10105
10106/// !!! WARNING: RUST-ONLY HELPER !!!
10107///
10108/// No function of this name exists in `Src/exec.c`. This helper stands in for
10109/// reading the saved outer stdout fd of an in-progress `$(...)` capture. C
10110/// has no such stack: the capture happens in a forked child (`getoutput`,
10111/// c:4713) so the parent fd is never rebound.
10112/// Saved outer stdout fd for an in-progress `$(...)` capture (top of
10113/// the bridge's CMDSUBST_OUTER_FDS stack), or `None` when not inside a
10114/// cmdsub. Used by the trap dispatcher to route a trap body's stdout to
10115/// the parent terminal instead of the cmdsub-bound pipe (Bug #56).
10116pub fn cmdsubst_outer_stdout() -> Option<i32> {
10117 crate::fusevm_bridge::cmdsubst_outer_stdout()
10118}
10119
10120/// Port of `execautofn_basic()` from `Src/exec.c:5608` — C decl `execautofn_basic(Estate state, UNUSED(int do_exec))`.
10121/// Run a pre-loaded autoload function body
10122/// via `execode`, snapshotting `scriptname`/`scriptfilename` around
10123/// the call so `%N` / `%x` reflect the autoload target during
10124/// execution.
10125pub fn execautofn_basic(state: &mut estate, _do_exec: i32) -> i32 {
10126 // c:5608
10127 // c:5613 — `shf = state->prog->shf;`
10128 let shf = match state.prog.shf.as_deref() {
10129 Some(s) => s.clone(),
10130 None => return LASTVAL.load(Ordering::Relaxed),
10131 };
10132
10133 // c:5619-5620 — funcstack filename catch-up. zshrs's funcstack
10134 // top-of-stack tracking is in modules::parameter::FUNCSTACK.
10135 {
10136 let mut stk = crate::ported::modules::parameter::FUNCSTACK.lock().unwrap();
10137 if let Some(top) = stk.last_mut() {
10138 if top.filename.is_none() {
10139 // c:5620 — `funcstack->filename = getshfuncfile(shf);`
10140 top.filename = crate::ported::hashtable::getshfuncfile(&shf.node.nam);
10141 }
10142 }
10143 }
10144
10145 // c:5622-5623 — `oldscriptname/oldscriptfilename = scriptname/scriptfilename;`
10146 let oldscriptname = crate::ported::utils::scriptname_get();
10147 let oldscriptfilename = crate::ported::utils::scriptfilename_get();
10148 // c:5624 — `scriptname = dupstring(shf->node.nam);`
10149 crate::ported::utils::set_scriptname(Some(shf.node.nam.clone()));
10150 // c:5625 — `scriptfilename = getshfuncfile(shf);`
10151 crate::ported::utils::set_scriptfilename(crate::ported::hashtable::getshfuncfile(
10152 &shf.node.nam,
10153 ));
10154 // c:5626 — `execode(shf->funcdef, 1, 0, "loadautofunc");`
10155 if let Some(funcdef) = shf.funcdef.clone() {
10156 execode_wordcode(funcdef, 1, 0, "loadautofunc");
10157 }
10158 // c:5627-5628 — restore.
10159 crate::ported::utils::set_scriptname(oldscriptname);
10160 crate::ported::utils::set_scriptfilename(oldscriptfilename);
10161
10162 LASTVAL.load(Ordering::Relaxed) // c:5630
10163}
10164
10165/// Port of `execautofn()` from `Src/exec.c:5635` — C decl `execautofn(Estate state, UNUSED(int do_exec))`.
10166/// The autoload-aware dispatch entry
10167/// for `WC_AUTOFN`: fault the function body in via `loadautofn`,
10168/// then hand off to `execautofn_basic` to actually run it.
10169///
10170/// C body:
10171/// ```c
10172/// static int
10173/// execautofn(Estate state, UNUSED(int do_exec))
10174/// {
10175/// Shfunc shf;
10176/// if (!(shf = loadautofn(state->prog->shf, 1, 0, 0)))
10177/// return 1;
10178/// state->prog->shf = shf;
10179/// return execautofn_basic(state, 0);
10180/// }
10181/// ```
10182///
10183/// Rust port: `loadautofn` mutates the `shfunc` in place via a raw
10184/// pointer and returns 0/1 (success/failure), so the explicit
10185/// `state->prog->shf = shf` assignment in C is implicit here.
10186pub fn execautofn(state: &mut estate, _do_exec: i32) -> i32 {
10187 // c:5638-5640 — `if (!(shf = loadautofn(state->prog->shf, 1, 0, 0))) return 1;`
10188 let shf_ptr: *mut shfunc = match state.prog.shf.as_mut() {
10189 Some(b) => &mut **b as *mut shfunc,
10190 None => return 1,
10191 };
10192 if loadautofn(shf_ptr, 1, 0, 0) != 0 {
10193 return 1;
10194 }
10195 // c:5643 — `return execautofn_basic(state, 0);`
10196 execautofn_basic(state, 0)
10197}
10198
10199/// Port of `execpline2()` from `Src/exec.c:1991` — C decl `execpline2(Estate state, wordcode pcode, int how, int input, int output, int last1)`.
10200/// Recursive
10201/// multi-stage pipe walker: at each step, analyse the current
10202/// command, fork-into-pipe (if mid-pipeline) or exec directly (if
10203/// WC_PIPE_END), then recurse on the next stage with `pipes[0]` as
10204/// its input fd.
10205pub fn execpline2(
10206 state: &mut estate,
10207 pcode: wordcode,
10208 how: i32,
10209 input: i32,
10210 output: i32,
10211 last1: i32,
10212) {
10213 use crate::ported::builtin::{BREAKS, INEVAL, RETFLAG};
10214 use crate::ported::zsh_h::{
10215 execcmd_params, CS_PIPE, WC_PIPE_END, WC_PIPE_LINENO as ZWC_PIPE_LINENO,
10216 WC_PIPE_TYPE as ZWC_PIPE_TYPE, Z_ASYNC,
10217 };
10218 // c:1991
10219 let mut eparams: execcmd_params = execcmd_params::default(); // c:1994 `struct execcmd_params eparams;`
10220
10221 // c:1996-1997 — `if (breaks || retflag) return;`
10222 if BREAKS.load(Ordering::SeqCst) != 0 || RETFLAG.load(Ordering::SeqCst) != 0 {
10223 return;
10224 }
10225
10226 // c:1999-2001 — `if (!IN_EVAL_TRAP() && !ineval && WC_PIPE_LINENO(pcode))
10227 // lineno = WC_PIPE_LINENO(pcode) - 1;`
10228 if !crate::ported::zsh_h::IN_EVAL_TRAP()
10229 && INEVAL.load(Ordering::SeqCst) == 0
10230 && ZWC_PIPE_LINENO(pcode) != 0
10231 {
10232 let new_lineno = ZWC_PIPE_LINENO(pcode).saturating_sub(1) as usize;
10233 crate::ported::input::lineno.with(|l| l.set(new_lineno));
10234 }
10235
10236 // c:2003-2011 — pline_level == 1 → snapshot to list_pipe_text for `jobs` output.
10237 if pline_level.load(Ordering::Relaxed) == 1 {
10238 // c:2003
10239 if (how & Z_ASYNC as i32) != 0 || sfcontext.load(Ordering::Relaxed) == 0 {
10240 // c:2004 — `(how & Z_ASYNC) || !sfcontext`
10241 // c:2005-2008 — `strcpy(list_pipe_text, getjobtext(state->prog,
10242 // state->pc + (WC_PIPE_TYPE(pcode) == WC_PIPE_END ? 0 : 1)));`
10243 let pc_for_text = state.pc
10244 + if ZWC_PIPE_TYPE(pcode) == WC_PIPE_END {
10245 0
10246 } else {
10247 1
10248 };
10249 let text = crate::ported::text::getjobtext(state.prog.clone(), Some(pc_for_text));
10250 if let Ok(mut lpt) = LIST_PIPE_TEXT.lock() {
10251 *lpt = text;
10252 }
10253 } else {
10254 // c:2010 — `list_pipe_text[0] = '\0';`
10255 if let Ok(mut lpt) = LIST_PIPE_TEXT.lock() {
10256 lpt.clear();
10257 }
10258 }
10259 }
10260
10261 if ZWC_PIPE_TYPE(pcode) == WC_PIPE_END {
10262 // c:2012-2014 — terminal stage: analyse + exec directly.
10263 execcmd_analyse(state, &mut eparams); // c:2013
10264 execcmd_exec(
10265 state,
10266 &mut eparams,
10267 input,
10268 output,
10269 how,
10270 if last1 != 0 { 1 } else { 2 }, // c:2014 `last1 ? 1 : 2`
10271 -1, // c:2014 close_if_forked = -1
10272 );
10273 } else {
10274 // c:2015-2039 — non-terminal stage: pipe + fork + recurse.
10275 let mut pipes: [i32; 2] = [-1, -1]; // c:2016
10276 let old_list_pipe = list_pipe.load(Ordering::Relaxed); // c:2017
10277 // c:2018 — `Wordcode next = state->pc + (*state->pc);`
10278 let next = if state.pc < state.prog.prog.len() {
10279 state.pc + state.prog.prog[state.pc] as usize
10280 } else {
10281 state.pc
10282 };
10283 // c:2020 — `++state->pc;`
10284 if state.pc < state.prog.prog.len() {
10285 state.pc += 1;
10286 }
10287 execcmd_analyse(state, &mut eparams); // c:2021
10288
10289 if mpipe(&mut pipes) < 0 {
10290 // c:2023-2025 — pipe() failure — `/* FIXME */` in C, fall through.
10291 }
10292
10293 // c:2027 — `addfilelist(NULL, pipes[0]);`
10294 // C uses the current thisjob's filelist; Rust port wires through JOBTAB.
10295 if let Some(jt) = JOBTAB.get() {
10296 let mut guard = jt.lock().unwrap();
10297 let tj = {
10298 if let Some(m) = THISJOB.get() {
10299 *m.lock().unwrap()
10300 } else {
10301 -1
10302 }
10303 };
10304 if tj >= 0 {
10305 if let Some(j) = guard.get_mut(tj as usize) {
10306 crate::ported::jobs::addfilelist(j, None, pipes[0]);
10307 }
10308 }
10309 }
10310
10311 // c:2028 — `execcmd_exec(state, &eparams, input, pipes[1], how, 0, pipes[0]);`
10312 execcmd_exec(state, &mut eparams, input, pipes[1], how, 0, pipes[0]);
10313 let _ = zclose(pipes[1]); // c:2029
10314 state.pc = next; // c:2030
10315
10316 // c:2034 — `cmdpush(CS_PIPE);`
10317 cmdpush(CS_PIPE as u8);
10318 // c:2035 — `list_pipe = 1;`
10319 list_pipe.store(1, Ordering::Relaxed);
10320 // c:2036 — `execpline2(state, *state->pc++, how, pipes[0], output, last1);`
10321 let next_pcode = if state.pc < state.prog.prog.len() {
10322 state.prog.prog[state.pc]
10323 } else {
10324 0
10325 };
10326 if state.pc < state.prog.prog.len() {
10327 state.pc += 1;
10328 }
10329 execpline2(state, next_pcode, how, pipes[0], output, last1);
10330 // c:2037 — `list_pipe = old_list_pipe;`
10331 list_pipe.store(old_list_pipe, Ordering::Relaxed);
10332 // c:2038 — `cmdpop();`
10333 cmdpop();
10334 }
10335}
10336
10337/// Port of `execpline()` from `Src/exec.c:1668` — C decl `execpline(Estate state, wordcode slcode, int how, int last1)`.
10338/// Full faithful port: allocates a job-table
10339/// entry via `initjob`, sets up coproc mpipes, drives the whole
10340/// (multi-stage) pipeline through `execpline2` (which performs the real
10341/// per-stage mpipe/fork/exec), then either spawns the job asynchronously
10342/// (`Z_ASYNC` -> `spawnjob`/`deletejob`) or waits synchronously
10343/// (`waitjobs`), including the `list_pipe` stop/continue fork machinery
10344/// (SUBJOB/SUPERJOB linkage) that re-forks the shell to keep an
10345/// interactively-suspended right-hand pipeline stage running.
10346///
10347/// Divergences from C, each forced by the Rust substrate and cited
10348/// inline at the point of use:
10349/// * `initjob` here grows the `Vec`-backed jobtab on demand and does
10350/// not return -1, so C's per-pipeline table-full bailout (c:1756-1760)
10351/// is inert on THIS path. That bailout is zsh's universal recursion
10352/// backstop (`initjob` caps the table at `MAX_MAXJOBS` → `zerr("job
10353/// table full or recursion limit exceeded")`), which bounds recursion
10354/// through paths FUNCNEST does not count (sourced files, `eval` — the
10355/// doshfunc funcnest check at c:5684 counts FS_FUNC frames only). The
10356/// fusevm runtime that actually executes pipelines does not allocate a
10357/// job per pipeline, so that backstop was missing entirely — runaway
10358/// `source`/`eval` recursion overflowed the (large but finite)
10359/// main-thread stack → SIGBUS. It is reinstated inline (same ceiling,
10360/// total FUNCSTACK depth as the proxy for held job slots) at the
10361/// FS_SOURCE re-entry (init.rs::source) and FS_EVAL re-entry
10362/// (builtin.rs eval).
10363/// * `errbrk_saved` / `prev_errflag` / `prev_breaks` (jobs.c:128
10364/// globals) are the jobs.rs statics; `update_job` sets them and the
10365/// `if (errbrk_saved)` restore (c:1998-2003) reads them here.
10366pub fn execpline(state: &mut estate, slcode: wordcode, how: i32, last1: i32) -> i32 {
10367 use crate::ported::builtin::{BREAKS, LOOPS, RETFLAG};
10368 use crate::ported::init::zleentry;
10369 use crate::ported::jobs::stat as jst;
10370 use crate::ported::jobs::{
10371 addproc, clearoldjobtab, deletejob, hasprocs, initjob, makerunning, pipecleanfilelist,
10372 printjob, spawnjob, waitjobs, CURJOB, ERRBRK_SAVED, LASTVAL2, PREVJOB, PREV_BREAKS,
10373 PREV_ERRFLAG,
10374 };
10375 use crate::ported::modules::clone::{coprocin, coprocout};
10376 use crate::ported::signals::killjb;
10377 use crate::ported::signals_h::{
10378 queue_signal_level, queue_signals, restore_queue_signals, unqueue_signals,
10379 };
10380 use crate::ported::utils::read_loop;
10381 use crate::ported::zsh_h::{
10382 jobbing, INTERACTIVE, LONGLISTJOBS, STAT_SUBJOB_ORPHANED, WC_PIPE, WC_PIPE_END,
10383 WC_PIPE_TYPE, WC_SUBLIST_COPROC, WC_SUBLIST_FLAGS, WC_SUBLIST_NOT, ZLE_CMD_TRASH, Z_ASYNC,
10384 Z_DISOWN, Z_TIMED,
10385 };
10386
10387 // c:1731 — `static int lastwj, lpforked;` (persist across calls).
10388 static LASTWJ: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
10389 static LPFORKED: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
10390 // c:465 (exec.c) — `static struct timespec list_pipe_start;`. Used as
10391 // the addproc bgtime for the re-forked super-job leader (c:1841).
10392 static LIST_PIPE_START: std::sync::Mutex<Option<std::time::Instant>> =
10393 std::sync::Mutex::new(None);
10394 // c:128 (jobs.c) — `int prev_errflag, prev_breaks, errbrk_saved;` are the
10395 // jobs.rs statics `update_job` sets (c:598-600).
10396
10397 let jt = JOBTAB.get_or_init(|| std::sync::Mutex::new(Vec::new()));
10398 // Read/write the shared thisjob slot.
10399 let thisjob_set = |v: i32| {
10400 if let Some(m) = THISJOB.get() {
10401 *m.lock().unwrap() = v;
10402 }
10403 };
10404 let thisjob_get = || THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
10405
10406 let old_simple_pline = simple_pline.load(Ordering::Relaxed); // c:1728
10407 let mut slflags = WC_SUBLIST_FLAGS(slcode); // c:1729
10408 let code = state.prog.prog[state.pc]; // c:1730 `wordcode code = *state->pc++;`
10409 state.pc += 1;
10410
10411 // c:1733-1736 — non-pipe, non-timed sublist short-circuits with the
10412 // negated-empty status.
10413 if wc_code(code) != WC_PIPE && (how & Z_TIMED) == 0 {
10414 let r = i32::from((slflags & WC_SUBLIST_NOT) != 0);
10415 LASTVAL.store(r, Ordering::Relaxed);
10416 return r;
10417 }
10418 let mut last1 = last1;
10419 if (slflags & WC_SUBLIST_NOT) != 0 {
10420 last1 = 0; // c:1736
10421 }
10422 let mut how = how;
10423
10424 queue_signals(); // c:1744
10425
10426 let pj = thisjob_get(); // c:1746 — `pj = thisjob;`
10427 let mut ipipe: [i32; 2] = [0, 0]; // c:1747
10428 let mut opipe: [i32; 2] = [0, 0];
10429 child_block(); // c:1748
10430
10431 // c:1755 — `thisjob = newjob = initjob();` (Rust jobtab grows on
10432 // demand, so initjob never fails; the -1 bailout is unreachable).
10433 let newjob = {
10434 let mut g = jt.lock().unwrap();
10435 initjob(&mut g)
10436 };
10437 thisjob_set(newjob as i32);
10438 if (how & Z_TIMED) != 0 {
10439 // c:1760-1761
10440 let mut g = jt.lock().unwrap();
10441 g[newjob].stat |= jst::TIMED;
10442 }
10443
10444 if (slflags & WC_SUBLIST_COPROC) != 0 {
10445 // c:1763-1782
10446 how = Z_ASYNC; // c:1764
10447 if coprocin.load(Ordering::Relaxed) >= 0 {
10448 zclose(coprocin.load(Ordering::Relaxed)); // c:1766
10449 zclose(coprocout.load(Ordering::Relaxed)); // c:1767
10450 }
10451 if mpipe(&mut ipipe) < 0 {
10452 // c:1769-1771
10453 coprocin.store(-1, Ordering::Relaxed);
10454 coprocout.store(-1, Ordering::Relaxed);
10455 slflags &= !WC_SUBLIST_COPROC;
10456 } else if mpipe(&mut opipe) < 0 {
10457 // c:1772-1776
10458 unsafe {
10459 libc::close(ipipe[0]);
10460 libc::close(ipipe[1]);
10461 }
10462 coprocin.store(-1, Ordering::Relaxed);
10463 coprocout.store(-1, Ordering::Relaxed);
10464 slflags &= !WC_SUBLIST_COPROC;
10465 } else {
10466 // c:1777-1781
10467 coprocin.store(ipipe[0], Ordering::Relaxed);
10468 coprocout.store(opipe[1], Ordering::Relaxed);
10469 fdtable_set(ipipe[0], FDT_UNUSED);
10470 fdtable_set(opipe[1], FDT_UNUSED);
10471 }
10472 }
10473
10474 // c:1788-1793 — `if (!pline_level++) { ... }`.
10475 let prev_pline = pline_level.fetch_add(1, Ordering::Relaxed);
10476 if prev_pline == 0 {
10477 list_pipe_pid.store(0, Ordering::Relaxed); // c:1789
10478 nowait.store(0, Ordering::Relaxed); // c:1790
10479 simple_pline.store(
10480 i32::from(WC_PIPE_TYPE(code) == WC_PIPE_END),
10481 Ordering::Relaxed,
10482 ); // c:1791
10483 list_pipe_job.store(newjob as i32, Ordering::Relaxed); // c:1792
10484 }
10485 LASTWJ.store(0, Ordering::Relaxed); // c:1794
10486 LPFORKED.store(0, Ordering::Relaxed);
10487 execpline2(state, code, how, opipe[0], ipipe[1], last1); // c:1795
10488 pline_level.fetch_sub(1, Ordering::Relaxed); // c:1796
10489
10490 if (how & Z_ASYNC) != 0 {
10491 // c:1797-1818
10492 clearoldjobtab(); // c:1798
10493 LASTWJ.store(newjob as i32, Ordering::Relaxed); // c:1799
10494
10495 if thisjob_get() == list_pipe_job.load(Ordering::Relaxed) {
10496 list_pipe_job.store(0, Ordering::Relaxed); // c:1801-1802
10497 }
10498 {
10499 let mut g = jt.lock().unwrap();
10500 let tj = thisjob_get();
10501 if tj >= 0 {
10502 g[tj as usize].stat |= jst::NOSTTY; // c:1803
10503 }
10504 }
10505 if (slflags & WC_SUBLIST_COPROC) != 0 {
10506 zclose(ipipe[1]); // c:1805
10507 zclose(opipe[0]); // c:1806
10508 }
10509 if (how & Z_DISOWN) != 0 {
10510 // c:1808-1812
10511 let tj = thisjob_get();
10512 if tj >= 0 {
10513 let mut g = jt.lock().unwrap();
10514 pipecleanfilelist(&mut g[tj as usize], false); // c:1809
10515 deletejob(&mut g, tj as usize, true); // c:1810
10516 }
10517 thisjob_set(-1); // c:1811
10518 } else {
10519 spawnjob(); // c:1814 (locks JOBTAB internally — no guard held)
10520 }
10521 child_unblock(); // c:1815
10522 unqueue_signals(); // c:1816
10523 LASTVAL.store(0, Ordering::Relaxed); // c:1818 `return lastval = 0;`
10524 return 0;
10525 }
10526
10527 // c:1819-2033 — synchronous branch.
10528 if newjob as i32 != LASTWJ.load(Ordering::Relaxed) {
10529 // c:1820
10530 let mut jn_idx = newjob; // `Job jn = jobtab + newjob;`
10531
10532 // c:1824-1825 — a list_pipe sub-shell child exits here.
10533 if newjob as i32 == list_pipe_job.load(Ordering::Relaxed)
10534 && list_pipe_child.load(Ordering::Relaxed) != 0
10535 {
10536 unsafe { libc::_exit(0) };
10537 }
10538
10539 LASTWJ.store(newjob as i32, Ordering::Relaxed); // c:1826
10540 thisjob_set(newjob as i32);
10541
10542 // c:1828-1830 — suppress the job announcement in nested pipes.
10543 {
10544 let mut g = jt.lock().unwrap();
10545 let noprint = list_pipe.load(Ordering::Relaxed) != 0
10546 || (pline_level.load(Ordering::Relaxed) != 0
10547 && (how & Z_TIMED) == 0
10548 && (g[jn_idx].stat & jst::NOSTTY) == 0);
10549 if noprint {
10550 g[jn_idx].stat |= jst::NOPRINT;
10551 }
10552 }
10553
10554 if nowait.load(Ordering::Relaxed) != 0 {
10555 // c:1832-1882
10556 if pline_level.load(Ordering::Relaxed) == 0 {
10557 // c:1833-1875
10558 *CURJOB
10559 .get_or_init(|| std::sync::Mutex::new(-1))
10560 .lock()
10561 .unwrap() = newjob as i32; // c:1836
10562 // c:1838 — DPUTS(!list_pipe_pid, "invalid list_pipe_pid").
10563 // c:1840-1841 — record the re-forked leader in the super-job.
10564 {
10565 let txt = LIST_PIPE_TEXT.lock().map(|s| s.clone()).unwrap_or_default();
10566 let bgt = *LIST_PIPE_START.lock().unwrap();
10567 let mut g = jt.lock().unwrap();
10568 addproc(
10569 &mut g[jn_idx],
10570 list_pipe_pid.load(Ordering::Relaxed),
10571 &txt,
10572 false,
10573 bgt,
10574 -1,
10575 -1,
10576 );
10577 }
10578 {
10579 let mut g = jt.lock().unwrap();
10580 // c:1845 — `if (!jn->procs->next || lpforked == 2)`.
10581 if g[jn_idx].procs.len() <= 1 || LPFORKED.load(Ordering::Relaxed) == 2 {
10582 g[jn_idx].gleader = list_pipe_pid.load(Ordering::Relaxed); // c:1845
10583 g[jn_idx].stat |= jst::SUBLEADER; // c:1847
10584 // c:1852-1861 — adopt any orphaned subjob; we
10585 // become its super-job.
10586 for jobsub in 1..g.len() {
10587 if (g[jobsub].stat & STAT_SUBJOB_ORPHANED) != 0 {
10588 g[jn_idx].other = jobsub as i32; // c:1855
10589 g[jn_idx].stat |= jst::SUPERJOB; // c:1856
10590 g[jobsub].stat &= !STAT_SUBJOB_ORPHANED; // c:1857
10591 g[jobsub].other = list_pipe_pid.load(Ordering::Relaxed);
10592 // c:1858
10593 }
10594 }
10595 }
10596 // c:1863-1869 — copy a stopped proc status from the
10597 // subjob onto our last proc.
10598 let other = g[jn_idx].other as usize;
10599 let stopped = if other < g.len() {
10600 g[other]
10601 .procs
10602 .iter()
10603 .find(|p| p.is_stopped())
10604 .map(|p| p.status)
10605 } else {
10606 None
10607 };
10608 if let Some(st) = stopped {
10609 if let Some(last) = g[jn_idx].procs.last_mut() {
10610 last.status = st;
10611 }
10612 }
10613 // c:1870-1872
10614 g[jn_idx].stat &= !(jst::DONE | jst::NOPRINT);
10615 g[jn_idx].stat |= jst::STOPPED | jst::CHANGED | jst::LOCKED | jst::INUSE;
10616 }
10617 // c:1875 — printjob(jn, !!isset(LONGLISTJOBS), 1).
10618 {
10619 let mut g = jt.lock().unwrap();
10620 crate::exec_jobs::printjob_synch(
10621 &mut g,
10622 jn_idx,
10623 i32::from(isset(LONGLISTJOBS)),
10624 1,
10625 );
10626 }
10627 } else if newjob as i32 != list_pipe_job.load(Ordering::Relaxed) {
10628 let mut g = jt.lock().unwrap();
10629 deletejob(&mut g, jn_idx, false); // c:1878
10630 } else {
10631 LASTWJ.store(-1, Ordering::Relaxed); // c:1879
10632 }
10633 }
10634
10635 ERRBRK_SAVED.store(0, Ordering::Relaxed); // c:1883
10636 // c:1884-2015 — `for (; !nowait;)` wait / continue-fork loop.
10637 loop {
10638 if nowait.load(Ordering::Relaxed) != 0 {
10639 break;
10640 }
10641 if list_pipe_child.load(Ordering::Relaxed) != 0 {
10642 // c:1886-1887
10643 let mut g = jt.lock().unwrap();
10644 g[jn_idx].stat |= jst::NOPRINT;
10645 makerunning(&mut g, jn_idx);
10646 }
10647 // c:1889-1894 — wait unless the job is LOCKED.
10648 let locked = {
10649 let g = jt.lock().unwrap();
10650 (g[jn_idx].stat & jst::LOCKED) != 0
10651 };
10652 let updated;
10653 if !locked {
10654 let tj = thisjob_get();
10655 {
10656 let g = jt.lock().unwrap();
10657 updated = hasprocs(&g, tj as usize); // c:1890
10658 }
10659 waitjobs(); // c:1891 — no JOBTAB guard held across the wait
10660 child_block(); // c:1892
10661 } else {
10662 updated = false; // c:1894
10663 }
10664 // c:1895-1902 — nudge the signal queue when the LHS job is
10665 // still running but we saw no update.
10666 let lpj = list_pipe_job.load(Ordering::Relaxed);
10667 let nudge = !updated && lpj != 0 && {
10668 let g = jt.lock().unwrap();
10669 (lpj as usize) < g.len()
10670 && hasprocs(&g, lpj as usize)
10671 && (g[lpj as usize].stat & jst::STOPPED) == 0
10672 };
10673 if nudge {
10674 let q = queue_signal_level();
10675 child_unblock();
10676 child_block();
10677 dont_queue_signals();
10678 restore_queue_signals(q);
10679 }
10680 // c:1903-1907 — forward a fatal signal from a done child.
10681 let jn_done = {
10682 let g = jt.lock().unwrap();
10683 (g[jn_idx].stat & jst::DONE) != 0
10684 };
10685 if list_pipe_child.load(Ordering::Relaxed) != 0
10686 && jn_done
10687 && (LASTVAL2.load(Ordering::Relaxed) & 0o200) != 0
10688 {
10689 unsafe {
10690 libc::killpg(
10691 mypgrp.load(Ordering::Relaxed),
10692 LASTVAL2.load(Ordering::Relaxed) & !0o200,
10693 );
10694 }
10695 }
10696 // c:1908-1921 — a pipeline with the shell running the RHS was
10697 // stopped; fork to let it continue.
10698 let stop_fork = list_pipe_child.load(Ordering::Relaxed) == 0
10699 && LPFORKED.load(Ordering::Relaxed) == 0
10700 && subsh.load(Ordering::Relaxed) == 0
10701 && jobbing()
10702 && (list_pipe.load(Ordering::Relaxed) != 0
10703 || last1 != 0
10704 || pline_level.load(Ordering::Relaxed) != 0)
10705 && {
10706 let g = jt.lock().unwrap();
10707 (g[jn_idx].stat & jst::STOPPED) != 0
10708 || (lpj != 0
10709 && pline_level.load(Ordering::Relaxed) != 0
10710 && (lpj as usize) < g.len()
10711 && (g[lpj as usize].stat & jst::STOPPED) != 0)
10712 };
10713 if stop_fork {
10714 let mut synch: [i32; 2] = [0, 0]; // c:1913
10715 let mut bgtime = ZshTimespec {
10716 tv_sec: 0,
10717 tv_nsec: 0,
10718 };
10719 let mut pid: libc::pid_t = 0;
10720 let pipe_failed = unsafe { libc::pipe(synch.as_mut_ptr()) } < 0; // c:1922
10721 if !pipe_failed {
10722 pid = zfork(Some(&mut bgtime)); // c:1922
10723 }
10724 if pipe_failed || pid == -1 {
10725 // c:1923-1935 — failure: can't suspend, resume the job.
10726 if pid < 0 {
10727 unsafe {
10728 libc::close(synch[0]);
10729 libc::close(synch[1]);
10730 }
10731 } else {
10732 zerr(&format!("pipe failed: {}", std::io::Error::last_os_error()));
10733 // c:1926
10734 }
10735 let _ = zleentry(ZLE_CMD_TRASH); // c:1929
10736 eprintln!("zsh: job can't be suspended"); // c:1930
10737 {
10738 let mut g = jt.lock().unwrap();
10739 makerunning(&mut g, jn_idx); // c:1932
10740 }
10741 killjb(jn_idx, libc::SIGCONT); // c:1933
10742 thisjob_set(newjob as i32); // c:1934
10743 } else if pid != 0 {
10744 // c:1936-1973 — parent: job control lives here.
10745 let gl = {
10746 let g = jt.lock().unwrap();
10747 g.get(lpj as usize).map(|j| j.gleader).unwrap_or(0)
10748 };
10749 LPFORKED.store(
10750 if unsafe { libc::killpg(gl, 0) } == -1 {
10751 2
10752 } else {
10753 1
10754 },
10755 Ordering::Relaxed,
10756 ); // c:1951-1952
10757 list_pipe_pid.store(pid, Ordering::Relaxed); // c:1953
10758 *LIST_PIPE_START.lock().unwrap() = Some(std::time::Instant::now()); // c:1954
10759 nowait.store(1, Ordering::Relaxed); // c:1955
10760 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:1956
10761 BREAKS.store(LOOPS.load(Ordering::SeqCst), Ordering::SeqCst); // c:1957
10762 unsafe { libc::close(synch[1]) }; // c:1958
10763 let mut dummy = [0u8; 1];
10764 let _ = read_loop(synch[0], &mut dummy); // c:1959
10765 unsafe { libc::close(synch[0]) }; // c:1960
10766 // c:1962-1970 — link super/sub jobs if we're still live.
10767 let jn_done2 = {
10768 let g = jt.lock().unwrap();
10769 (g[jn_idx].stat & jst::DONE) != 0
10770 };
10771 if !jn_done2 {
10772 let mut g = jt.lock().unwrap();
10773 g[lpj as usize].other = newjob as i32; // c:1964
10774 g[lpj as usize].stat |= jst::SUPERJOB; // c:1965
10775 g[jn_idx].stat |= jst::SUBJOB | jst::NOPRINT; // c:1966
10776 g[jn_idx].other = list_pipe_pid.load(Ordering::Relaxed); // c:1967
10777 if hasprocs(&g, lpj as usize) {
10778 g[jn_idx].gleader = g[lpj as usize].gleader; // c:1968-1969
10779 }
10780 }
10781 // c:1971-1972 — stop the LHS group so the whole pipe
10782 // suspends together.
10783 let (do_kill, gl2) = {
10784 let g = jt.lock().unwrap();
10785 (
10786 (list_pipe.load(Ordering::Relaxed) != 0 || last1 != 0)
10787 && hasprocs(&g, lpj as usize),
10788 g.get(lpj as usize).map(|j| j.gleader).unwrap_or(0),
10789 )
10790 };
10791 if do_kill {
10792 unsafe { libc::killpg(gl2, libc::SIGSTOP) };
10793 }
10794 break; // c:1973
10795 } else {
10796 // c:1975-2004 — child: become our own group, stop, then
10797 // continue as the RHS sub-shell.
10798 unsafe { libc::close(synch[0]) }; // c:1976
10799 entersubsh(esub::ASYNC, None); // c:1977
10800 let mypid = unsafe { libc::getpid() };
10801 mypgrp.store(mypid, Ordering::Relaxed);
10802 unsafe { libc::setpgid(0, mypid) }; // c:1992 setpgrp
10803 unsafe { libc::close(synch[1]) }; // c:1993
10804 unsafe { libc::kill(mypid, libc::SIGSTOP) }; // c:1994
10805 list_pipe.store(0, Ordering::Relaxed); // c:1995
10806 list_pipe_child.store(1, Ordering::Relaxed); // c:1996
10807 dosetopt(INTERACTIVE, 0, 0); // c:1997 `opts[INTERACTIVE] = 0`
10808 if ERRBRK_SAVED.load(Ordering::Relaxed) != 0 {
10809 // c:1998-2003 — restore saved break/errflag state.
10810 errflag.store(
10811 PREV_ERRFLAG.load(Ordering::Relaxed)
10812 | (errflag.load(Ordering::Relaxed) & ERRFLAG_INT),
10813 Ordering::Relaxed,
10814 );
10815 BREAKS.store(PREV_BREAKS.load(Ordering::Relaxed), Ordering::SeqCst);
10816 }
10817 break;
10818 }
10819 } else if subsh.load(Ordering::Relaxed) != 0 && {
10820 let g = jt.lock().unwrap();
10821 (g[jn_idx].stat & jst::STOPPED) != 0
10822 } {
10823 // c:2008-2012
10824 if thisjob_get() == newjob as i32 {
10825 let mut g = jt.lock().unwrap();
10826 makerunning(&mut g, jn_idx);
10827 } else {
10828 thisjob_set(newjob as i32);
10829 }
10830 } else {
10831 break; // c:2015
10832 }
10833 }
10834
10835 child_unblock(); // c:2017
10836 unqueue_signals(); // c:2018
10837
10838 // c:2020-2026 — a signal-killed list_pipe: drop this job and
10839 // forward the signal to the enclosing job's group.
10840 let lastval_now = LASTVAL.load(Ordering::Relaxed);
10841 let drop_and_signal =
10842 list_pipe.load(Ordering::Relaxed) != 0 && (lastval_now & 0o200) != 0 && pj >= 0 && {
10843 let g = jt.lock().unwrap();
10844 (g[jn_idx].stat & jst::INUSE) == 0 || (g[jn_idx].stat & jst::DONE) != 0
10845 };
10846 if drop_and_signal {
10847 {
10848 let mut g = jt.lock().unwrap();
10849 deletejob(&mut g, jn_idx, false); // c:2022
10850 }
10851 jn_idx = pj as usize; // c:2023 `jn = jobtab + pj;`
10852 let gl = {
10853 let g = jt.lock().unwrap();
10854 g[jn_idx].gleader
10855 };
10856 if gl != 0 {
10857 killjb(jn_idx, lastval_now & !0o200); // c:2025
10858 }
10859 }
10860 // c:2027-2030 — final cleanup deletejob for a done/child job.
10861 let final_delete = list_pipe_child.load(Ordering::Relaxed) != 0 || {
10862 let g = jt.lock().unwrap();
10863 (g[jn_idx].stat & jst::DONE) != 0
10864 && (list_pipe.load(Ordering::Relaxed) != 0
10865 || (pline_level.load(Ordering::Relaxed) != 0
10866 && (g[jn_idx].stat & jst::SUBJOB) == 0))
10867 };
10868 if final_delete {
10869 let mut g = jt.lock().unwrap();
10870 deletejob(&mut g, jn_idx, false); // c:2030
10871 }
10872 thisjob_set(pj); // c:2031
10873 } else {
10874 unqueue_signals(); // c:2034
10875 }
10876
10877 // c:2035-2036 — apply `!` negation to the pipeline status.
10878 if (slflags & WC_SUBLIST_NOT) != 0
10879 && errflag.load(Ordering::Relaxed) == 0
10880 && RETFLAG.load(Ordering::SeqCst) == 0
10881 {
10882 let lv = LASTVAL.load(Ordering::Relaxed);
10883 LASTVAL.store(i32::from(lv == 0), Ordering::Relaxed);
10884 }
10885
10886 if pline_level.load(Ordering::Relaxed) == 0 {
10887 simple_pline.store(old_simple_pline, Ordering::Relaxed); // c:2039
10888 }
10889 LASTVAL.load(Ordering::Relaxed) // c:2040 `return lastval;`
10890}
10891
10892// `execcmd_exec`'s wordcode dispatch tail from Src/exec.c:2901-3700 is
10893// inlined at every call site (execsimple, execpline) as the match
10894// expression that selects the right execX function. There's no
10895// separate Rust fn for it because:
10896// - The arg-side `execcmd_exec(args, type_)` at exec.rs:795 already
10897// occupies the canonical name (handling precommand modifiers).
10898// - The C dispatch tail is conceptually `execfuncs[code - WC_CURSH]`,
10899// a table lookup at exec.c:5499 — not a separate function.
10900#[cfg(any())]
10901mod _execcmd_tail_doc_anchor {
10902 // c:2901-3700 — see inlined match in execpline + execsimple above.
10903 // c:5499 — execfuncs[] table inlined as the same match.
10904}
10905
10906
10907/// Port of `execcmd_exec()` from `Src/exec.c:2901` — C decl `execcmd_exec(Estate state, Execcmd_params eparams, int input, int output, int how, int last1, int close_if_forked)`.
10908/// int input, int output, int how, int last1, int close_if_forked)`
10909/// from `Src/exec.c:2900-4404`. Execute a command at the lowest
10910/// level of the hierarchy.
10911///
10912/// Line-by-line port of the full 1500-line C body. Sections:
10913/// c:2904-2916 — locals
10914/// c:2917-2924 — eparams field unpacking
10915/// c:2934-2939 — Z_TIMED + doneps4 reset
10916/// c:2945-2960 — old_lastval + use_cmdoutval + `save[]`/`mfds[]` init
10917/// c:2962-2986 — %job head rewrite + AUTORESUME prefix match
10918/// c:2988-3011 — Z_ASYNC / pipeline-not-last / sh-emulation fork-immediately
10919/// c:3013-3283 — precommand-modifier walk (BINF_PREFIX strip)
10920/// + BINF_COMMAND (-p/-v/-V) + BINF_EXEC (-a/-c/-l)
10921/// c:3285-3307 — prefork substitutions + magic_assign
10922/// c:3309-3406 — empty-command branch (redir / nullexec / BINF_COMMAND)
10923/// c:3409-3466 — main resolution loop (shfunc / builtin / autocd)
10924/// c:3468-3479 — errflag bail-out
10925/// c:3480-3492 — text fetch + setunderscore
10926/// c:3494-3524 — rm * safety prompt
10927/// c:3526-3591 — type-specific dispatch prep (WC_FUNCDEF / is_shfunc / WC_AUTOFN)
10928/// c:3593-3632 — external resolution (cmdnamtab, hashcmd, AUTOCD)
10929/// c:3634-3697 — fork decision
10930/// c:3700-3955 — redir loop + multio + addfd + xpandredir
10931/// c:3957-3961 — multio close (`mfds[i].ct >= 2` → closemn)
10932/// c:3963-3995 — nullexec branch
10933/// c:3996-4327 — main dispatch (entersubsh + execfuncdef / `execcurshtable[]` /
10934/// execbuiltin / execshfunc / execute)
10935/// c:4330-4365 — `err:` label: forked-child fd cleanup, fixfds
10936/// c:4366-4403 — `done:` label: POSIX special-builtin error escalation,
10937/// shelltime stop, newxtrerr close, AUTOCONTINUE restore
10938///
10939/// **Helpers used from elsewhere in exec.rs:**
10940/// - `save_params(state, varspc, restorelist, removelist)` → Src/exec.c:4409
10941/// - `restore_params(restorelist, removelist)` → Src/exec.c:4463
10942/// - `isreallycom(cn)` → Src/exec.c:2670
10943/// - `execerr()` → Src/exec.c:2700 (label-style; converts to errflag set + goto-equivalent)
10944/// - `execautofn_basic(state, do_exec)` → Src/exec.c:5608
10945/// - `ensurefeature(modname, "b:", ...)` → Src/module.c:1654
10946///
10947/// Called from `execpline` (wordcode tree-walker). The fusevm bytecode
10948/// VM uses `execcmd_compile_head` + `compile_simple` instead.
10949#[allow(non_snake_case)]
10950#[allow(clippy::too_many_arguments)]
10951#[allow(clippy::redundant_field_names)]
10952#[allow(unused_assignments)]
10953#[allow(unused_variables)]
10954#[allow(unused_mut)]
10955#[allow(unused_imports)]
10956#[allow(unreachable_code)]
10957#[allow(dead_code)]
10958pub fn execcmd_exec(
10959 state: &mut estate,
10960 eparams: &mut crate::ported::zsh_h::execcmd_params,
10961 input: i32,
10962 output: i32,
10963 mut how: i32,
10964 mut last1: i32,
10965 close_if_forked: i32,
10966) {
10967 use crate::ported::zsh_h::{
10968 Star, ASG_ARRAY, ASG_KEY_VALUE, AUTOCD, AUTOCONTINUE, AUTORESUME, BGNICE,
10969 BINF_ASSIGN as BINF_ASSIGN_FLAG, BINF_BUILTIN, BINF_COMMAND, BINF_EXEC, BINF_MAGICEQUALS,
10970 BINF_NOGLOB, BINF_PREFIX, BINF_PSPECIAL, CSHNULLCMD, ERRFLAG_INT, EXECOPT, FDT_EXTERNAL,
10971 FDT_INTERNAL, FDT_TYPE_MASK, FDT_UNUSED, FDT_XTRACE, HASHCMDS, HFILE_USE_OPTIONS,
10972 IS_APPEND_REDIR, IS_DASH, IS_ERROR_REDIR, MAGICEQUALSUBST, NOTIFY, PM_READONLY, PM_SPECIAL,
10973 POSIXBUILTINS, PREFORK_ASSIGN, PREFORK_KEY_VALUE, PREFORK_SINGLE, PREFORK_TYPESET,
10974 PRINTEXITVALUE, RCS, REDIR_CLOSE, REDIR_HERESTR, REDIR_INPIPE, REDIR_MERGEIN,
10975 REDIR_MERGEOUT, REDIR_OUTPIPE, REDIR_READ, REDIR_READWRITE, RMSTARSILENT, SHINSTDIN,
10976 SHNULLCMD, STAT_BUILTIN, STAT_CURSH, STAT_DONE, STAT_NOPRINT, WC_ASSIGN as ZWC_ASSIGN,
10977 WC_ASSIGN_INC as ZWC_ASSIGN_INC, WC_ASSIGN_NUM as ZWC_ASSIGN_NUM,
10978 WC_ASSIGN_SCALAR as ZWC_ASSIGN_SCALAR, WC_ASSIGN_TYPE as ZWC_ASSIGN_TYPE,
10979 WC_ASSIGN_TYPE2 as ZWC_ASSIGN_TYPE2, WC_AUTOFN, WC_CURSH, WC_FUNCDEF, WC_REDIR, WC_SIMPLE,
10980 WC_SUBSH, WC_TIMED, WC_TYPESET, XTRACE, Z_ASYNC, Z_DISOWN, Z_SYNC, Z_TIMED,
10981 };
10982
10983 // c:2900
10984
10985 // c:2904-2916 — locals.
10986 let mut hn: Option<*mut builtin> = None; // c:2904 HashNode hn = NULL
10987 let mut filelist: Vec<jobfile> = Vec::new(); // c:2905 LinkList filelist = NULL
10988 // c:2906 LinkNode node; (loop locals)
10989 // c:2907 Redir fn; (loop locals)
10990 let mut mfds: [Option<Box<multio>>; 10] = // c:2908 struct multio *mfds[10]
10991 [None, None, None, None, None, None, None, None, None, None];
10992 let mut text: Option<String> = None; // c:2909 char *text
10993 let mut save: [i32; 10] = [-2; 10]; // c:2910 int save[10]
10994 let mut fil: i32; // c:2911 int fil
10995 let mut dfil: i32 = 0; // c:2911 int dfil
10996 let mut is_cursh: i32 = 0; // c:2911 int is_cursh = 0
10997 let mut do_exec: i32 = 0; // c:2911 int do_exec = 0
10998 let mut redir_err: i32 = 0; // c:2911 int redir_err = 0
10999 let mut i: i32; // c:2911 int i
11000 let mut nullexec: i32 = 0; // c:2912 int nullexec = 0
11001 let mut magic_assign: i32 = 0; // c:2912 int magic_assign = 0
11002 let mut forked: i32 = 0; // c:2912 int forked = 0
11003 let mut old_lastval: i32; // c:2912 int old_lastval
11004 let mut is_shfunc: i32 = 0; // c:2913 int is_shfunc = 0
11005 let mut is_builtin: i32 = 0; // c:2913 int is_builtin = 0
11006 let mut is_exec: i32 = 0; // c:2913 int is_exec = 0
11007 let mut use_defpath: i32 = 0; // c:2913 int use_defpath = 0
11008 // c:2914 — `Various flags to the command.`
11009 let mut cflags: u32 = 0; // c:2915 int cflags = 0
11010 let mut orig_cflags: u32 = 0; // c:2915 int orig_cflags = 0
11011 let mut checked: i32 = 0; // c:2915 int checked = 0
11012 let mut oautocont: i32 = -1; // c:2915 int oautocont = -1
11013 // c:2916 — `FILE *oxtrerr = xtrerr, *newxtrerr = NULL;`. newxtrerr
11014 // is only made while xtrerr == stderr (c:3767), so the
11015 // c:4449 restore to oxtrerr is a restore to stderr.
11016 let mut newxtrerr: Option<i32> = None; // c:2916
11017
11018 // c:2917-2924 — eparams field unpacking. `args` / `redir` are
11019 // pulled into mutable locals so the body can mutate them
11020 // independently of the eparams struct.
11021 let mut args: Option<Vec<String>> = eparams.args.take(); // c:2921 LinkList args
11022 let mut redir: Option<Vec<redir>> = eparams.redir.take(); // c:2922 LinkList redir
11023 let varspc: Option<usize> = eparams.varspc; // c:2923 Wordcode varspc
11024 let typ: i32 = eparams.typ; // c:2924 int type
11025 // c:2925-2929 — `preargs comes from expanding the head of the args
11026 // list in order to check for prefix commands.` declared later.
11027
11028 // c:2933-2937 — `for the "time" keyword` — child_times_t shti, chti
11029 // + struct timespec then. Rust port keeps the names so the shelltime
11030 // start+stop calls map directly. Use jobs.rs's existing types.
11031 let mut shti = crate::ported::jobs::timeinfo::default(); // c:2934
11032 let mut chti = crate::ported::jobs::timeinfo::default(); // c:2934
11033 let mut then_ts = std::time::Instant::now(); // c:2935 struct timespec then
11034 if (how & Z_TIMED as i32) != 0 {
11035 // c:2936
11036 crate::ported::jobs::shelltime(Some(&mut shti), Some(&mut chti), Some(&mut then_ts), 0);
11037 // c:2937
11038 }
11039
11040 doneps4.store(0, Ordering::Relaxed); // c:2939
11041
11042 // c:2941-2947 — `If assignment but no command get the status from
11043 // variable assignment.`
11044 old_lastval = LASTVAL.load(Ordering::Relaxed); // c:2945
11045 if args.is_none() && varspc.is_some() {
11046 // c:2946
11047 let ef = errflag.load(Ordering::Relaxed);
11048 LASTVAL.store(
11049 if ef != 0 {
11050 ef
11051 } else {
11052 cmdoutval.load(Ordering::Relaxed)
11053 },
11054 Ordering::Relaxed,
11055 ); // c:2947
11056 }
11057 // c:2948-2954 — `If there are arguments, we should reset the status
11058 // for the command before execution---unless we are using the result
11059 // of a command substitution...`
11060 use_cmdoutval.store(if args.is_none() { 1 } else { 0 }, Ordering::Relaxed); // c:2955
11061
11062 // c:2957-2960 — `for (i = 0; i < 10; i++) { save[i] = -2; mfds[i] = NULL; }`
11063 // Already initialised above via array literals; preserved as
11064 // comment for parity. The C loop maps to a no-op in Rust.
11065
11066 // c:2962-2973 — `%job` head rewrite.
11067 if (typ == WC_SIMPLE as i32 || typ == WC_TYPESET as i32)
11068 && args.as_ref().map(|v| !v.is_empty()).unwrap_or(false)
11069 && args.as_ref().unwrap()[0].starts_with('%')
11070 {
11071 // c:2964-2965
11072 if (how & Z_DISOWN as i32) != 0 {
11073 // c:2966
11074 oautocont = if crate::ported::options::opt_state_get("autocontinue").unwrap_or(false) {
11075 1
11076 } else {
11077 0
11078 }; // c:2967
11079 opt_state_set("autocontinue", true); // c:2968
11080 }
11081 // c:2970-2971 — `pushnode(args, dupstring((how & Z_DISOWN) ? "disown" : (how & Z_ASYNC) ? "bg" : "fg"));`
11082 let head = if (how & Z_DISOWN as i32) != 0 {
11083 "disown".to_string()
11084 } else if (how & Z_ASYNC as i32) != 0 {
11085 "bg".to_string()
11086 } else {
11087 "fg".to_string()
11088 };
11089 if let Some(ref mut v) = args {
11090 v.insert(0, head);
11091 }
11092 how = Z_SYNC as i32; // c:2972
11093 }
11094
11095 // c:2975-2986 — AUTORESUME prefix match against jobtab.
11096 if isset(AUTORESUME)
11097 && typ == WC_SIMPLE as i32
11098 && (how & Z_SYNC as i32) != 0
11099 && args.as_ref().map(|v| !v.is_empty()).unwrap_or(false)
11100 && redir.as_ref().map(|v| v.is_empty()).unwrap_or(true)
11101 && input == 0
11102 && args.as_ref().unwrap().len() == 1
11103 {
11104 // c:2979-2981
11105 if unset(NOTIFY) {
11106 // c:2982 — `scanjobs();` via the canonical port
11107 // (Src/jobs.c:1993).
11108 if let Some(jt) = JOBTAB.get() {
11109 let mut guard = jt.lock().unwrap();
11110 crate::ported::jobs::scanjobs(&mut guard);
11111 }
11112 }
11113 // c:2984 — `if (findjobnam(peekfirst(args)) != -1)`
11114 let head = args.as_ref().unwrap()[0].clone();
11115 let maxjob = JOBTAB
11116 .get()
11117 .map(|m| m.lock().unwrap().len() as i32)
11118 .unwrap_or(0);
11119 let thisjob = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
11120 // c:2982 — `findjobnam(s)`. Canonical port at
11121 // jobs.rs::findjobnam matches against `proc.text`, which is
11122 // the command text actually saved into the job at fork —
11123 // matching C exactly. Returns the job index if any non-
11124 // SUBJOB jobtab entry's first-proc text starts with `s`.
11125 let found = if let Some(jt) = JOBTAB.get() {
11126 let guard = jt.lock().unwrap();
11127 crate::ported::jobs::findjobnam(&head, &guard, maxjob - 1, thisjob).is_some()
11128 } else {
11129 false
11130 };
11131 if found {
11132 // c:2985 — `pushnode(args, dupstring("fg"));`
11133 if let Some(ref mut v) = args {
11134 v.insert(0, "fg".to_string());
11135 }
11136 }
11137 }
11138
11139 // ====================================================================
11140 // Helpers ported elsewhere in exec.rs, imported for the body below.
11141 // ====================================================================
11142 // save_params + restore_params — top-level ports in exec.rs
11143 // (c:4410 / c:4464). Both bridged via `use` below.
11144 use crate::ported::exec::{restore_params, save_params};
11145 // isreallycom — top-level port at exec.rs (c:972). Bridges the
11146 // local shadow that this fn body used pre-port.
11147 use crate::ported::exec::isreallycom;
11148 // execautofn_basic — top-level port at exec.rs (c:5608).
11149 use crate::ported::exec::execautofn_basic;
11150 // C `execerr` macro (c:2700) was a goto-equivalent:
11151 // errflag |= ERRFLAG_ERROR; lastval = 1; goto err;
11152 // Rust expansion: each call site inlines the errflag+LASTVAL set
11153 // and then `break`s out of the enclosing redir loop. The loop's
11154 // post-loop errflag check at c:3949 routes to execcmd_exec_err_path
11155 // for the cleanup tail. No macro needed.
11156
11157 // c:2988-3011 — Z_ASYNC / pipeline-not-last / sh-emulation
11158 // fork-immediately fast path.
11159 if (how & Z_ASYNC as i32) != 0
11160 || output != 0
11161 || (last1 == 2 && input != 0 && {
11162 // c:2989 — `EMULATION(EMULATE_SH)` — emulation==EMULATE_SH.
11163 // EMULATION macro: `(emulation & EMULATE_MASK) == X`. The
11164 // ported `emulation` static at options.rs:1044 holds the
11165 // current bit; compare against EMULATE_SH (zsh_h:2883).
11166 (crate::ported::options::emulation.load(Ordering::Relaxed)
11167 & crate::ported::zsh_h::EMULATE_SH)
11168 != 0
11169 })
11170 {
11171 // c:2988
11172 // c:2999 — `text = getjobtext(state->prog, eparams->beg);`
11173 text = Some(crate::ported::text::getjobtext(
11174 state.prog.clone(),
11175 Some(eparams.beg),
11176 ));
11177 // c:3000-3008 — `switch (execcmd_fork(...)) { -1: goto fatal; 0: break; default: return; }`
11178 let mut filelist_for_fork = filelist.clone();
11179 let pid = execcmd_fork(
11180 state,
11181 how,
11182 typ,
11183 varspc,
11184 &mut filelist_for_fork,
11185 text.as_deref().unwrap_or(""),
11186 oautocont,
11187 close_if_forked,
11188 );
11189 match pid {
11190 -1 => {
11191 // c:3002-3003 — `goto fatal;` — fall through to fatal:
11192 // label at c:4377. We model this with a flag.
11193 redir_err = 1; // pretend redir error to trigger fatal arm
11194 // Continue to done label by setting forked + jumping forward.
11195 // Simplified: just bail with status 1 + fatal handling at
11196 // the bottom of the fn.
11197 return execcmd_exec_done_path(
11198 redir_err,
11199 oautocont,
11200 how,
11201 &mut shti,
11202 &mut chti,
11203 &mut then_ts,
11204 forked,
11205 &mut newxtrerr,
11206 cflags,
11207 orig_cflags,
11208 is_cursh,
11209 do_exec,
11210 );
11211 }
11212 0 => {
11213 // c:3004 — child returned 0; continue with the body.
11214 }
11215 _ => {
11216 // c:3007 — parent: `return;` — but first restore AUTOCONTINUE
11217 // and shelltime stop. Inline the done-tail equivalent.
11218 if oautocont >= 0 {
11219 opt_state_set("autocontinue", oautocont != 0);
11220 }
11221 if (how & Z_TIMED as i32) != 0 {
11222 crate::ported::jobs::shelltime(
11223 Some(&mut shti),
11224 Some(&mut chti),
11225 Some(&mut then_ts),
11226 1,
11227 );
11228 }
11229 return;
11230 }
11231 }
11232 last1 = 1; // c:3009
11233 forked = 1; // c:3009
11234 } else {
11235 // c:3010-3011
11236 text = None;
11237 }
11238
11239 // ====================================================================
11240 // c:3013-3283 — precommand-modifier walk.
11241 //
11242 // The full walk (BINF_PREFIX strip + BINF_COMMAND sub-options +
11243 // BINF_EXEC sub-options) is already ported in `execcmd_compile_head`
11244 // (above this fn). Call into it to keep DRY, then convert the
11245 // returned dispatch struct's fields into the locals C uses
11246 // (cflags, orig_cflags, is_builtin, is_shfunc, use_defpath,
11247 // exec_argv0, precmd_skip).
11248 //
11249 // Per [[feedback_true_port_pattern]] the C function does this
11250 // walk inline. Reusing the existing port is acceptable because
11251 // `execcmd_compile_head`'s body IS the c:3013-3283 walk — the
11252 // citations there match. The C tree-walker and the fusevm
11253 // compile-time walker arrive at identical dispatch decisions
11254 // from the same input.
11255 // ====================================================================
11256 let mut preargs: Vec<String> = Vec::new();
11257 let mut exec_argv0: Option<String> = None;
11258 if (typ == WC_SIMPLE as i32 || typ == WC_TYPESET as i32) && args.is_some() {
11259 // c:3018
11260 let head_args: Vec<String> = args.as_ref().unwrap().clone();
11261 let dispatch = execcmd_compile_head(&head_args, typ as u32);
11262 // Pull fields into local mirror of C state.
11263 cflags = dispatch.cflags;
11264 if dispatch.is_builtin {
11265 is_builtin = 1;
11266 }
11267 if dispatch.is_shfunc {
11268 is_shfunc = 1;
11269 }
11270 if dispatch.use_defpath {
11271 use_defpath = 1;
11272 }
11273 exec_argv0 = dispatch.exec_argv0;
11274 // c:3115 — `orig_cflags |= cflags;` accumulated by the head walk.
11275 orig_cflags = dispatch.orig_cflags;
11276 // c:3030-3086 — strip the precmd-modifier prefix from args.
11277 // In C, the walk pulls one arg at a time from `args` into
11278 // `preargs` via execcmd_getargs, then uremnodes each
11279 // BINF_PREFIX modifier. At loop exit C's `preargs` holds the
11280 // dispatch target (1 element) and `args` holds whatever's
11281 // left; `joinlists(preargs, args)` (c:3305-3306) splices the
11282 // target back onto the head. The net effect is `args` with
11283 // the precmd modifiers stripped. We compute that final shape
11284 // directly and leave `preargs` empty so the joinlists arm
11285 // below is a no-op. Without this, preargs=head_args[skip..]
11286 // plus a non-draining args was double-counting every word
11287 // when both held the same suffix.
11288 if let Some(ref mut v) = args {
11289 v.drain(0..dispatch.precmd_skip);
11290 // c:3154 — `pushnode(preargs, "command");` /* Leave everything
11291 // alone, dispatch to whence. We need to put the name back in
11292 // the list. */ `command -v`/`-V` keeps its whole word list and
11293 // dispatches to the `command` builtin (bin_whence with
11294 // BIN_COMMAND), so the name goes back on the front after the
11295 // precommand strip. `execcmd_compile_head` only REPORTS this
11296 // via `has_command_vv` (the fusevm compiler acts on it); the
11297 // tree-walker has to perform it. Without this the head after
11298 // the strip was `-v`, which resolves to nothing —
11299 // `x=command; $x -v cat` printed nothing.
11300 if dispatch.has_command_vv {
11301 v.insert(0, "command".to_string()); // c:3154
11302 } else if dispatch.use_defpath {
11303 // c:3165 — `if (pnode) uremnode(preargs, pnode);` /* We
11304 // don't need this node as we're not treating "command" as a
11305 // builtin this time. */ The `-p` word is consumed here,
11306 // same reason as above: compile_head drops it from its own
11307 // local list only.
11308 if v.first()
11309 .map(|w| {
11310 let b = w.as_bytes();
11311 b.len() >= 2
11312 && IS_DASH(b[0] as char)
11313 && w[1..].chars().all(|c| c == 'p' || c == 'v' || c == 'V')
11314 })
11315 .unwrap_or(false)
11316 {
11317 v.remove(0); // c:3165
11318 }
11319 // c:3176-3177 — `if (IS_DASH(argdata[0]) && IS_DASH(argdata[1])
11320 // && !argdata[2]) uremnode(preargs, argnode);`
11321 if v.first().map(|w| w == "--").unwrap_or(false) {
11322 v.remove(0); // c:3177
11323 }
11324 }
11325 }
11326 let _ = head_args;
11327 preargs.clear();
11328 // c:3076 — `magic_assign = (hn->flags & BINF_MAGICEQUALS);`
11329 // — surface via cflags check: if a typeset-family builtin
11330 // landed, BINF_MAGICEQUALS is in its flags and dispatch
11331 // surfaces it via cflags.
11332 if (cflags & BINF_MAGICEQUALS) != 0 && typ != WC_TYPESET as i32 {
11333 magic_assign = 1;
11334 }
11335 // c:3056 — C's precmd walk sets `hn = builtintab->getnode(...)`
11336 // for the dispatch target before breaking at c:3064. The
11337 // Rust port's execcmd_compile_head returns is_builtin but
11338 // not the entry pointer, and the second resolution loop
11339 // below short-circuits on `is_builtin != 0` (c:3423-3426)
11340 // without re-resolving. Look up the dispatch target now so
11341 // `hn` is non-null at the execbuiltin call (c:4233 /
11342 // exec.rs:10177); otherwise execbuiltin returns 1 silently
11343 // on a null `bn`.
11344 hn = None;
11345 if dispatch.has_command_vv {
11346 // c:3209 — `hn = &commandbn.node;`. The `command -v`/`-V` form
11347 // dispatches through the dedicated descriptor (bin_whence /
11348 // BIN_COMMAND / optstr "pvV"), NOT through builtintab's
11349 // handler-less `BIN_PREFIX("command", …)` row — looking the name
11350 // up there gave a null handler and `command -v cat` printed
11351 // nothing.
11352 hn = Some(&*commandbn as *const builtin as *mut builtin); // c:3209
11353 } else if is_builtin != 0 {
11354 if let Some(target) = args.as_ref().and_then(|v| v.first()) {
11355 if let Some(entry) = BUILTINS.iter().find(|b| b.node.nam == *target) {
11356 hn = Some(entry as *const builtin as *mut builtin);
11357 }
11358 }
11359 }
11360 } else {
11361 // c:3282-3283 — `else preargs = NULL;`
11362 // We use an empty preargs to model NULL — C's `preargs` is
11363 // only iterated if `nonempty(preargs)` in this branch.
11364 }
11365
11366 // c:3285-3300 — `Do prefork substitutions.` magic_assign handling.
11367 // Sets the file-static `esprefork` (exec.rs:267) so any downstream
11368 // execsubst() call inside this command's expansion uses the same
11369 // prefork flags. Also keep a local copy for the immediate
11370 // prefork(args, esprefork, NULL) below.
11371 let esprefork_v: i32 =
11372 if magic_assign != 0 || (isset(MAGICEQUALSUBST) && typ != WC_TYPESET as i32) {
11373 PREFORK_TYPESET // c:3300
11374 } else {
11375 0
11376 };
11377 esprefork.store(esprefork_v, Ordering::Relaxed); // c:3298 esprefork = ...
11378
11379 // c:3302-3307 — prefork(args, esprefork, NULL) + joinlists(preargs, args).
11380 if args.is_some() && eparams.htok != 0 {
11381 // c:3303-3304 — `if (eparams->htok) prefork(args, esprefork, NULL);`
11382 let mut as_linklist: LinkList<String> = Default::default();
11383 if let Some(ref v) = args {
11384 for s in v {
11385 as_linklist.push_back(s.clone());
11386 }
11387 }
11388 let mut rf = 0i32;
11389 prefork(&mut as_linklist, esprefork_v, &mut rf);
11390 // Move back into args.
11391 let mut out: Vec<String> = Vec::new();
11392 while let Some(s) = as_linklist.pop_front() {
11393 out.push(s);
11394 }
11395 args = Some(out);
11396 }
11397 if !preargs.is_empty() {
11398 // c:3305-3306 — `if (preargs) args = joinlists(preargs, args);`
11399 let mut joined = preargs.clone();
11400 if let Some(ref v) = args {
11401 joined.extend(v.iter().cloned());
11402 }
11403 args = Some(joined);
11404 }
11405
11406 // c:3309-3406 — main resolution loop + empty-command branch.
11407 if typ == WC_SIMPLE as i32 || typ == WC_TYPESET as i32 {
11408 let mut unglobbed: i32 = 0; // c:3310
11409
11410 // c:3312 — `for (;;)` — main resolution loop.
11411 loop {
11412 // c:3315-3318 — globbing or untokenise sweep.
11413 if (cflags & BINF_NOGLOB) == 0 {
11414 while checked == 0
11415 && (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0
11416 && args.as_ref().map(|v| !v.is_empty()).unwrap_or(false)
11417 && crate::ported::lex::has_token(&args.as_ref().unwrap()[0])
11418 {
11419 // c:3318 — `zglob(args, firstnode(args), 0);`
11420 // zglob takes &mut Vec<String>; isolate the head element
11421 // by splitting args into [head] and [tail], then re-merging.
11422 let mut head_vec: Vec<String> = Vec::new();
11423 if let Some(ref mut v) = args {
11424 head_vec.push(v.remove(0));
11425 }
11426 crate::ported::glob::zglob(&mut head_vec, 0usize, 0);
11427 if let Some(ref mut v) = args {
11428 // Re-merge the globbed head ahead of the tail. Inserting
11429 // one element at a time (`v.insert(i, s)`) memmoves the
11430 // whole remaining tail on every element, so a head word
11431 // that globs to K matches costs O(K*n) and a completion
11432 // sweep over a large directory spins. Splicing the batch
11433 // in at position 0 shifts the tail exactly once and is
11434 // order-identical: element i lands at index i.
11435 v.splice(0..0, head_vec);
11436 }
11437 }
11438 } else if unglobbed == 0 {
11439 // c:3319-3322
11440 if let Some(ref mut v) = args {
11441 for s in v.iter_mut() {
11442 *s = untokenize(s); // c:3321
11443 }
11444 }
11445 unglobbed = 1; // c:3322
11446 }
11447
11448 // c:3327-3328 — `if ((cflags & BINF_EXEC) && last1) do_exec = 1;`
11449 if (cflags & BINF_EXEC) != 0 && last1 != 0 {
11450 do_exec = 1; // c:3328
11451 }
11452
11453 // c:3331-3407 — empty-command branch.
11454 if args.as_ref().map(|v| v.is_empty()).unwrap_or(true) {
11455 // c:3331 — `if (!args || empty(args))`
11456 if redir.as_ref().map(|v| !v.is_empty()).unwrap_or(false) {
11457 // c:3332 — `if (redir && nonempty(redir))`
11458 if do_exec != 0 {
11459 // c:3333 — `Was this "exec < foobar"?`
11460 nullexec = 1; // c:3335
11461 break;
11462 } else if varspc.is_some() {
11463 // c:3337
11464 nullexec = 2; // c:3338
11465 break;
11466 } else if {
11467 // c:3340-3341 — `if (!nullcmd || !*nullcmd ||
11468 // opts[CSHNULLCMD] || (cflags & BINF_PREFIX))`
11469 let nc = getsparam("NULLCMD");
11470 let nc_empty = nc.as_deref().map(|s| s.is_empty()).unwrap_or(true);
11471 nc_empty || isset(CSHNULLCMD) || (cflags & BINF_PREFIX) != 0
11472 } {
11473 // c:3342 — `zerr("redirection with no command");`
11474 zerr("redirection with no command");
11475 LASTVAL.store(1, Ordering::Relaxed); // c:3343
11476 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3344
11477 if forked != 0 {
11478 // c:3345-3346
11479 crate::ported::builtin::_realexit();
11480 }
11481 if (how & Z_TIMED as i32) != 0 {
11482 // c:3347-3348
11483 crate::ported::jobs::shelltime(
11484 Some(&mut shti),
11485 Some(&mut chti),
11486 Some(&mut then_ts),
11487 1,
11488 );
11489 }
11490 return; // c:3349
11491 } else if {
11492 // c:3350 — `if (!nullcmd || !*nullcmd || opts[SHNULLCMD])`
11493 let nc = getsparam("NULLCMD");
11494 let nc_empty = nc.as_deref().map(|s| s.is_empty()).unwrap_or(true);
11495 nc_empty || isset(SHNULLCMD)
11496 } {
11497 // c:3351-3353 — `if (!args) args = newlinklist(); addlinknode(args, dupstring(":"));`
11498 if args.is_none() {
11499 args = Some(Vec::new());
11500 }
11501 args.as_mut().unwrap().push(":".to_string()); // c:3353
11502 } else if {
11503 // c:3354-3356 — `readnullcmd && *readnullcmd &&
11504 // peekfirst(redir).type == REDIR_READ &&
11505 // !nextnode(firstnode(redir))`
11506 let rnc = getsparam("READNULLCMD");
11507 let rnc_nonempty = rnc.as_deref().map(|s| !s.is_empty()).unwrap_or(false);
11508 rnc_nonempty
11509 && redir.as_ref().unwrap().len() == 1
11510 && redir.as_ref().unwrap()[0].typ == REDIR_READ
11511 } {
11512 // c:3357-3359
11513 if args.is_none() {
11514 args = Some(Vec::new());
11515 }
11516 let rnc = getsparam("READNULLCMD").unwrap_or_default();
11517 args.as_mut().unwrap().push(rnc); // c:3359
11518 } else {
11519 // c:3360-3364 — default: nullcmd as command.
11520 if args.is_none() {
11521 args = Some(Vec::new());
11522 }
11523 let nc = getsparam("NULLCMD").unwrap_or_default();
11524 args.as_mut().unwrap().push(nc); // c:3363
11525 }
11526 } else if (cflags & BINF_PREFIX) != 0 && (cflags & BINF_COMMAND) != 0 {
11527 // c:3365 — bare `command`: lastval=0, return.
11528 LASTVAL.store(0, Ordering::Relaxed); // c:3366
11529 if forked != 0 {
11530 crate::ported::builtin::_realexit(); // c:3367-3368
11531 }
11532 if (how & Z_TIMED as i32) != 0 {
11533 crate::ported::jobs::shelltime(
11534 Some(&mut shti),
11535 Some(&mut chti),
11536 Some(&mut then_ts),
11537 1,
11538 ); // c:3369-3370
11539 }
11540 return; // c:3371
11541 } else {
11542 // c:3372-3406 — no arguments default arm.
11543 // c:3378-3385 — badcshglob == 1 → no match.
11544 if crate::ported::glob::BADCSHGLOB.load(Ordering::Relaxed) == 1 {
11545 zerr("no match"); // c:3379
11546 LASTVAL.store(1, Ordering::Relaxed); // c:3380
11547 if forked != 0 {
11548 crate::ported::builtin::_realexit(); // c:3381-3382
11549 }
11550 if (how & Z_TIMED as i32) != 0 {
11551 crate::ported::jobs::shelltime(
11552 Some(&mut shti),
11553 Some(&mut chti),
11554 Some(&mut then_ts),
11555 1,
11556 ); // c:3383-3384
11557 }
11558 return; // c:3385
11559 }
11560 // c:3387 — `cmdoutval = use_cmdoutval ? lastval : 0;`
11561 cmdoutval.store(
11562 if use_cmdoutval.load(Ordering::Relaxed) != 0 {
11563 LASTVAL.load(Ordering::Relaxed)
11564 } else {
11565 0
11566 },
11567 Ordering::Relaxed,
11568 );
11569 if varspc.is_some() {
11570 // c:3388-3392 — `lastval = old_lastval; addvars(state, varspc, 0);`
11571 LASTVAL.store(old_lastval, Ordering::Relaxed); // c:3390
11572 addvars(state, varspc.unwrap_or(0), 0); // c:3391
11573 }
11574 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
11575 // c:3393
11576 LASTVAL.store(1, Ordering::Relaxed); // c:3394
11577 } else {
11578 // c:3395-3396
11579 LASTVAL.store(cmdoutval.load(Ordering::Relaxed), Ordering::Relaxed);
11580 }
11581 if isset(XTRACE) {
11582 // c:3397-3400 — `fputc('\n', xtrerr); fflush(xtrerr);`
11583 crate::fusevm_bridge::xtrerr_fputs("\n");
11584 crate::fusevm_bridge::xtrerr_flush();
11585 }
11586 if forked != 0 {
11587 crate::ported::builtin::_realexit(); // c:3401-3402
11588 }
11589 if (how & Z_TIMED as i32) != 0 {
11590 crate::ported::jobs::shelltime(
11591 Some(&mut shti),
11592 Some(&mut chti),
11593 Some(&mut then_ts),
11594 1,
11595 ); // c:3403-3404
11596 }
11597 return; // c:3405
11598 }
11599 }
11600
11601 // c:3423-3426 — `if (errflag || checked || is_builtin ||
11602 // (isset(POSIXBUILTINS) ? (cflags & BINF_EXEC) : (cflags & BINF_COMMAND)))`
11603 // A user interrupt sets ERRFLAG_INT, never ERRFLAG_ERROR (signals.c:457), and
11604 // the C line cited above tests the WHOLE errflag, so masking here let an
11605 // interrupted shell keep going where zsh stops.
11606 if errflag.load(Ordering::Relaxed) != 0
11607 || checked != 0
11608 || is_builtin != 0
11609 || if isset(POSIXBUILTINS) {
11610 (cflags & BINF_EXEC) != 0
11611 } else {
11612 (cflags & BINF_COMMAND) != 0
11613 }
11614 {
11615 // c:3423
11616 break; // c:3426
11617 }
11618
11619 // c:3428 — `cmdarg = (char *) peekfirst(args);`
11620 let cmdarg = args.as_ref().unwrap()[0].clone();
11621
11622 // c:3429-3433 — shfunc lookup.
11623 // c:3485 — `(hn = shfunctab->getnode(shfunctab, cmdarg))`.
11624 // Same misport as the compile-time head walk above: C's
11625 // `shfunctab->getnode` is `gethashnode`
11626 // (Src/hashtable.c:821), an O(1) probe that skips DISABLED
11627 // nodes, not a full-table walk.
11628 if (cflags & (BINF_BUILTIN | BINF_COMMAND)) == 0 {
11629 let in_shfunctab = shfunctab_lock()
11630 .read()
11631 .map(|t| !t.getnode(cmdarg.as_str()).is_null())
11632 .unwrap_or(false);
11633 if in_shfunctab {
11634 is_shfunc = 1; // c:3431
11635 break; // c:3432
11636 }
11637 }
11638 // c:3434-3447 — builtintab lookup.
11639 let builtin_entry: Option<&'static builtin> = BUILTINS
11640 .iter()
11641 .find(|b| b.node.nam.as_str() == cmdarg.as_str());
11642 if builtin_entry.is_none() {
11643 if (cflags & BINF_BUILTIN) != 0 {
11644 // c:3435 — `zwarn("no such builtin: %s", cmdarg);`
11645 zwarn(&format!("no such builtin: {}", cmdarg)); // c:3436
11646 LASTVAL.store(1, Ordering::Relaxed); // c:3437
11647 if oautocont >= 0 {
11648 // c:3438-3439
11649 opt_state_set("autocontinue", oautocont != 0);
11650 }
11651 if forked != 0 {
11652 crate::ported::builtin::_realexit(); // c:3440-3441
11653 }
11654 if (how & Z_TIMED as i32) != 0 {
11655 crate::ported::jobs::shelltime(
11656 Some(&mut shti),
11657 Some(&mut chti),
11658 Some(&mut then_ts),
11659 1,
11660 ); // c:3442-3443
11661 }
11662 return; // c:3444
11663 }
11664 break; // c:3446
11665 }
11666 let entry = builtin_entry.unwrap();
11667 // c:3448-3460 — `if (!(hn->flags & BINF_PREFIX)) { is_builtin = 1; ... }`
11668 if (entry.node.flags as u32 & BINF_PREFIX) == 0 {
11669 is_builtin = 1; // c:3449
11670 // c:3452 — `if (!(hn = resolvebuiltin(cmdarg, hn)))` —
11671 // module autoload check. zshrs's BUILTINS table is
11672 // static and pre-resolved; treat resolvebuiltin as
11673 // pass-through.
11674 hn = Some(entry as *const builtin as *mut builtin);
11675 break; // c:3459
11676 }
11677 // c:3461-3463 — BINF_PREFIX modifier (builtin/command/exec).
11678 cflags &= !(BINF_BUILTIN | BINF_COMMAND);
11679 cflags |= entry.node.flags as u32;
11680 if let Some(ref mut v) = args {
11681 v.remove(0); // c:3463 uremnode(args, firstnode(args))
11682 }
11683 hn = None; // c:3464
11684 }
11685 }
11686
11687 // c:3468-3478 — errflag bail-out.
11688 // A user interrupt sets ERRFLAG_INT, never ERRFLAG_ERROR (signals.c:457), and
11689 // the C line cited above tests the WHOLE errflag, so masking here let an
11690 // interrupted shell keep going where zsh stops.
11691 if errflag.load(Ordering::Relaxed) != 0 {
11692 // c:3468
11693 if LASTVAL.load(Ordering::Relaxed) == 0 {
11694 // c:3469
11695 LASTVAL.store(1, Ordering::Relaxed); // c:3470
11696 }
11697 if oautocont >= 0 {
11698 opt_state_set("autocontinue", oautocont != 0);
11699 // c:3472
11700 }
11701 if forked != 0 {
11702 crate::ported::builtin::_realexit(); // c:3473-3474
11703 }
11704 if (how & Z_TIMED as i32) != 0 {
11705 crate::ported::jobs::shelltime(Some(&mut shti), Some(&mut chti), Some(&mut then_ts), 1);
11706 // c:3475-3476
11707 }
11708 return; // c:3477
11709 }
11710
11711 // c:3480-3483 — `Get the text associated with this command.`
11712 if text.is_none()
11713 && sfcontext.load(Ordering::Relaxed) == 0
11714 && (isset(MONITOR) || (how & Z_TIMED as i32) != 0)
11715 {
11716 // c:3481-3482
11717 text = Some(crate::ported::text::getjobtext(
11718 state.prog.clone(),
11719 Some(eparams.beg),
11720 )); // c:3483
11721 }
11722
11723 // c:3485-3492 — `Set up special parameter $_`.
11724 if typ != WC_FUNCDEF as i32 {
11725 // c:3490
11726 let last_str = args
11727 .as_ref()
11728 .and_then(|v| v.last())
11729 .cloned()
11730 .unwrap_or_default();
11731 setunderscore(&last_str); // c:3491-3492
11732 }
11733
11734 // c:3494-3524 — `Warn about "rm *"`.
11735 if typ == WC_SIMPLE as i32
11736 && crate::ported::zsh_h::interact()
11737 && unset(RMSTARSILENT)
11738 && isset(SHINSTDIN)
11739 && args.as_ref().map(|v| v.len() >= 2).unwrap_or(false)
11740 && args.as_ref().unwrap()[0] == "rm"
11741 {
11742 // c:3495-3497
11743 let args_v = args.as_ref().unwrap().clone();
11744 for s in args_v.iter().skip(1) {
11745 // c:3500
11746 // A user interrupt sets ERRFLAG_INT, never ERRFLAG_ERROR (signals.c:457), and
11747 // the C line cited above tests the WHOLE errflag, so masking here let an
11748 // interrupted shell keep going where zsh stops.
11749 if errflag.load(Ordering::Relaxed) != 0 {
11750 break;
11751 }
11752 let l = s.len();
11753 // c:3505 — `if (s[0] == Star && !s[1])` — bare `*`.
11754 if s.len() == 1 && s.as_bytes()[0] == Star as u8 {
11755 let pwd = getsparam("PWD").unwrap_or_default();
11756 if !crate::ported::utils::checkrmall(&pwd) {
11757 // c:3506
11758 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3507
11759 break; // c:3508
11760 }
11761 } else if l >= 2 {
11762 // c:3510 — `s[l-2] == '/' && s[l-1] == Star`
11763 let bytes = s.as_bytes();
11764 if bytes[l - 2] == b'/' && bytes[l - 1] == Star as u8 {
11765 let prefix = if l == 2 {
11766 "/".to_string()
11767 } else {
11768 String::from_utf8_lossy(&bytes[..l - 2]).into_owned()
11769 };
11770 if !crate::ported::utils::checkrmall(&prefix) {
11771 // c:3518
11772 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3519
11773 break; // c:3520
11774 }
11775 }
11776 }
11777 }
11778 }
11779
11780 // c:3526-3580 — type-specific dispatch prep.
11781 if typ == WC_FUNCDEF as i32 {
11782 // c:3526
11783 if state.prog.prog.get(state.pc).copied().unwrap_or(0) != 0 {
11784 // c:3535 — `Nonymous, don't do redirections here`
11785 redir = None; // c:3537
11786 }
11787 } else if is_shfunc != 0 || typ == WC_AUTOFN as i32 {
11788 // c:3539
11789 // c:3540-3559 — shfunc / autoload preload.
11790 if is_shfunc != 0 {
11791 // c:3541-3542 — `shf = (Shfunc)hn;` — already in hn.
11792 } else {
11793 // c:3543-3559 — autoload preload.
11794 if let Some(ref mut sh) = state.prog.shf {
11795 let shf_ptr: *mut shfunc = sh.as_mut() as *mut shfunc;
11796 let r = loadautofn(shf_ptr, 1, 0, 0);
11797 if r != 0 {
11798 // c:3551 — `lastval = 1;`
11799 LASTVAL.store(1, Ordering::Relaxed);
11800 if oautocont >= 0 {
11801 opt_state_set("autocontinue", oautocont != 0);
11802 }
11803 if forked != 0 {
11804 crate::ported::builtin::_realexit();
11805 }
11806 if (how & Z_TIMED as i32) != 0 {
11807 crate::ported::jobs::shelltime(
11808 Some(&mut shti),
11809 Some(&mut chti),
11810 Some(&mut then_ts),
11811 1,
11812 );
11813 }
11814 return; // c:3558
11815 }
11816 }
11817 }
11818 // c:3561-3579 — shf->redir append: a function definition can
11819 // carry extra redirs (`f() { ... } < file`), captured as a
11820 // separate Eprog in shf->redir. Walk that Eprog with a temp
11821 // estate, extract its redirs with ecgetredirs, then merge
11822 // into the live `redir` list.
11823 // Resolve shfunc by name (hn is *mut builtin so we go through
11824 // shfunctab as in the dispatch site at c:4102).
11825 let shfn_name = args
11826 .as_ref()
11827 .and_then(|v| v.first())
11828 .cloned()
11829 .unwrap_or_default();
11830 let shf_redir_eprog: Option<crate::ported::zsh_h::Eprog> = {
11831 if let Ok(tab) = shfunctab_lock().read() {
11832 tab.get(&shfn_name).and_then(|s| s.redir.clone())
11833 } else {
11834 None
11835 }
11836 };
11837 if let Some(red_eprog) = shf_redir_eprog {
11838 // c:3566-3571 — build temp estate from shf->redir.
11839 let mut tmp_state = estate {
11840 prog: red_eprog.clone(),
11841 pc: 0,
11842 strs: red_eprog.strs.clone(),
11843 strs_offset: 0,
11844 };
11845 // c:3572 — `redir2 = ecgetredirs(&s);`
11846 let redir2 = crate::ported::parse::ecgetredirs(&mut tmp_state);
11847 // c:3573-3578 — merge into existing redir.
11848 if redir.is_none() {
11849 redir = Some(redir2); // c:3574
11850 } else if let Some(ref mut r) = redir {
11851 // c:3576-3577 — append.
11852 for n in redir2 {
11853 r.push(n);
11854 }
11855 }
11856 }
11857 }
11858
11859 // c:3582-3591 — errflag bail-out (2).
11860 // A user interrupt sets ERRFLAG_INT, never ERRFLAG_ERROR (signals.c:457), and
11861 // the C line cited above tests the WHOLE errflag, so masking here let an
11862 // interrupted shell keep going where zsh stops.
11863 if errflag.load(Ordering::Relaxed) != 0 {
11864 // c:3582
11865 LASTVAL.store(1, Ordering::Relaxed); // c:3583
11866 if oautocont >= 0 {
11867 opt_state_set("autocontinue", oautocont != 0);
11868 // c:3584-3585
11869 }
11870 if forked != 0 {
11871 crate::ported::builtin::_realexit(); // c:3586-3587
11872 }
11873 if (how & Z_TIMED as i32) != 0 {
11874 crate::ported::jobs::shelltime(Some(&mut shti), Some(&mut chti), Some(&mut then_ts), 1);
11875 // c:3588-3589
11876 }
11877 return; // c:3590
11878 }
11879
11880 // c:3593-3632 — external resolution + AUTOCD.
11881 if (typ == WC_SIMPLE as i32 || typ == WC_TYPESET as i32) && nullexec == 0 {
11882 // c:3593
11883 // Consumed here so a nested command dispatched from this one
11884 // (a function body) does not inherit it.
11885 let redirs_preapplied = REDIRS_PREAPPLIED.with(|c| c.replace(false));
11886 let trycd = isset(AUTOCD)
11887 && isset(SHINSTDIN)
11888 && redir.as_ref().map(|v| v.is_empty()).unwrap_or(true)
11889 && !redirs_preapplied
11890 && args.as_ref().map(|v| v.len() == 1).unwrap_or(false)
11891 && !args.as_ref().unwrap()[0].is_empty(); // c:3595-3597
11892 let mut external_found = false;
11893 if hn.is_none() {
11894 // c:3600
11895 let cmdarg = args.as_ref().unwrap()[0].clone();
11896 let mut dohashcmd = isset(HASHCMDS); // c:3604
11897 // c:3606 — `hn = cmdnamtab->getnode(cmdnamtab, cmdarg);`
11898 let mut have_cmdnam: Option<cmdnam> = {
11899 let tab = cmdnamtab_lock().read().ok();
11900 tab.and_then(|t| {
11901 t.iter()
11902 .find(|(k, _)| k.as_str() == cmdarg.as_str())
11903 .map(|(_, v)| v.clone())
11904 })
11905 };
11906 if have_cmdnam.is_some() && trycd && !isreallycom(have_cmdnam.as_ref().unwrap()) {
11907 // c:3607
11908 // c:3608-3611 — `if (!(hn->flags & HASHED)) { checkpath = path; dohashcmd = 1; }`
11909 if (have_cmdnam.as_ref().unwrap().node.flags & crate::ported::zsh_h::HASHED) == 0 {
11910 dohashcmd = true;
11911 }
11912 // c:3612-3614 — removenode + freenode; `hn = NULL`.
11913 if let Ok(mut tab) = cmdnamtab_lock().write() {
11914 tab.remove(&cmdarg);
11915 }
11916 have_cmdnam = None;
11917 }
11918 if have_cmdnam.is_none() && dohashcmd && cmdarg != ".." {
11919 // c:3616 — `if (!hn && dohashcmd && strcmp(cmdarg, "..")) `
11920 let has_slash = cmdarg.contains('/'); // c:3617-3618
11921 if !has_slash {
11922 // c:3619 — `hn = (HashNode) hashcmd(cmdarg, checkpath);`
11923 let path_dirs = getsparam("PATH").unwrap_or_default();
11924 let dirs: Vec<String> = path_dirs.split(':').map(String::from).collect();
11925 have_cmdnam = hashcmd(&cmdarg, &dirs);
11926 }
11927 }
11928 // C keeps the resolved command in `hn` (c:3619), which the
11929 // AUTOCD test below reads as "found": `hn` here only carries
11930 // builtins and functions, so the external lookup is carried
11931 // separately. Dropped, a found external such as `mysql` was
11932 // treated as not found, and with AUTOCD + CDABLEVARS
11933 // `${=ZPWR_LEARN_COMMAND} 2>>log` ran `cd -- /var/empty`
11934 // (user mysql's home) and printed `~mysql`.
11935 external_found = have_cmdnam.is_some();
11936 }
11937
11938 // c:3625-3631 — AUTOCD: command not found, try directory.
11939 if hn.is_none() && !external_found && trycd {
11940 let cmdarg = args.as_ref().unwrap()[0].clone();
11941 if let Some(s) = cancd(&cmdarg) {
11942 // c:3625
11943 args.as_mut().unwrap()[0] = s; // c:3626
11944 args.as_mut().unwrap().insert(0, "--".to_string()); // c:3627
11945 args.as_mut().unwrap().insert(0, "cd".to_string()); // c:3628
11946 // c:3629 — `if ((hn = builtintab->getnode(builtintab, "cd")))`
11947 let cd_entry = BUILTINS.iter().find(|b| b.node.nam.as_str() == "cd");
11948 if let Some(cd) = cd_entry {
11949 hn = Some(cd as *const builtin as *mut builtin);
11950 is_builtin = 1; // c:3630
11951 }
11952 }
11953 }
11954 }
11955
11956 // c:3635 — `is_cursh = (is_builtin || is_shfunc || nullexec || type >= WC_CURSH);`
11957 is_cursh =
11958 (is_builtin != 0 || is_shfunc != 0 || nullexec != 0 || typ >= WC_CURSH as i32) as i32;
11959
11960 // c:3659-3697 — fork decision.
11961 if forked == 0 {
11962 // c:3659
11963 if do_exec == 0
11964 && (((is_builtin != 0 || is_shfunc != 0) && output != 0)
11965 || (is_cursh == 0
11966 && (last1 != 1
11967 || crate::ported::signals::nsigtrapped.load(Ordering::Relaxed) != 0
11968 || JOBTAB
11969 .get()
11970 .map(|jt| crate::ported::jobs::havefiles(&jt.lock().unwrap()))
11971 .unwrap_or(false)
11972 || crate::ported::utils::FDTABLE_FLOCKS.load(Ordering::Relaxed) != 0)))
11973 {
11974 // c:3660-3663
11975 let mut filelist_for_fork = filelist.clone();
11976 let pid = execcmd_fork(
11977 state,
11978 how,
11979 typ,
11980 varspc,
11981 &mut filelist_for_fork,
11982 text.as_deref().unwrap_or(""),
11983 oautocont,
11984 close_if_forked,
11985 );
11986 match pid {
11987 -1 => {
11988 // c:3666-3667 — goto fatal.
11989 redir_err = 1;
11990 return execcmd_exec_done_path(
11991 redir_err,
11992 oautocont,
11993 how,
11994 &mut shti,
11995 &mut chti,
11996 &mut then_ts,
11997 forked,
11998 &mut newxtrerr,
11999 cflags,
12000 orig_cflags,
12001 is_cursh,
12002 do_exec,
12003 );
12004 }
12005 0 => {
12006 // c:3668 — child continues.
12007 }
12008 _ => {
12009 // c:3670-3671 — parent returns.
12010 if oautocont >= 0 {
12011 opt_state_set("autocontinue", oautocont != 0);
12012 }
12013 if (how & Z_TIMED as i32) != 0 {
12014 crate::ported::jobs::shelltime(
12015 Some(&mut shti),
12016 Some(&mut chti),
12017 Some(&mut then_ts),
12018 1,
12019 );
12020 }
12021 return;
12022 }
12023 }
12024 forked = 1; // c:3673
12025 } else if is_cursh != 0 {
12026 // c:3674
12027 // c:3678-3682 — set jobtab[thisjob] stat bits.
12028 let thisjob = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
12029 if thisjob >= 0 {
12030 if let Some(jt) = JOBTAB.get() {
12031 let mut guard = jt.lock().unwrap();
12032 if let Some(j) = guard.get_mut(thisjob as usize) {
12033 j.stat |= STAT_CURSH; // c:3678
12034 // c:3679-3680 — `if (!jobtab[thisjob].procs)
12035 // jobtab[thisjob].stat |= STAT_NOPRINT;`
12036 // Suppress the "[N] done" print for jobs that
12037 // never forked a real process (cursh / builtin /
12038 // null exec).
12039 if j.procs.is_empty() {
12040 j.stat |= STAT_NOPRINT; // c:3680
12041 }
12042 if is_builtin != 0 {
12043 j.stat |= STAT_BUILTIN; // c:3682
12044 }
12045 }
12046 }
12047 }
12048 } else {
12049 // c:3683-3697 — external exec (real or fake).
12050 is_exec = 1; // c:3687
12051 // c:3695 — `if (type == WC_SUBSH) forked = 1;`
12052 if typ == WC_SUBSH as i32 {
12053 forked = 1; // c:3696
12054 }
12055 }
12056 }
12057
12058 // c:3700-3704 — `if ((esglob = !(cflags & BINF_NOGLOB)) && args && htok)`
12059 if (cflags & BINF_NOGLOB) == 0 && args.is_some() && eparams.htok != 0 {
12060 // c:3700
12061 let mut oargs: LinkList<String> = Default::default();
12062 if let Some(ref v) = args {
12063 for s in v {
12064 oargs.push_back(s.clone());
12065 }
12066 }
12067 globlist(&mut oargs, 0); // c:3702
12068 let mut out: Vec<String> = Vec::new();
12069 while let Some(s) = oargs.pop_front() {
12070 out.push(s);
12071 }
12072 args = Some(out);
12073 }
12074 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
12075 // c:3705
12076 LASTVAL.store(1, Ordering::Relaxed); // c:3706
12077 return execcmd_exec_err_path(
12078 forked,
12079 &mut save,
12080 &mut mfds,
12081 oautocont,
12082 how,
12083 &mut shti,
12084 &mut chti,
12085 &mut then_ts,
12086 &mut newxtrerr,
12087 cflags,
12088 orig_cflags,
12089 is_cursh,
12090 do_exec,
12091 redir_err,
12092 );
12093 }
12094
12095 // c:3765-3773 — "Make a copy of stderr for xtrace output before
12096 // redirecting". The fusevm runtime reaches the same
12097 // port when it opens a simple command's redirect scope.
12098 crate::fusevm_bridge::xtrerr_flush(); // c:3766 fflush(xtrerr)
12099 if isset(XTRACE)
12100 && crate::ported::utils::xtrerr.load(Ordering::Relaxed) == 2
12101 && (typ < WC_SUBSH as i32 || typ == WC_TIMED as i32)
12102 {
12103 // c:3767-3768
12104 let fd = crate::ported::utils::movefd(unsafe { libc::dup(2) }); // c:3769
12105 if fd >= 0 {
12106 crate::ported::utils::xtrerr.store(fd, Ordering::Relaxed); // c:3770
12107 crate::ported::utils::fdtable_set(fd, FDT_XTRACE); // c:3771
12108 newxtrerr = Some(fd);
12109 }
12110 }
12111
12112 // c:3720-3724 — pipeline input/output to mfds.
12113 if input != 0 {
12114 addfd(forked, &mut save, &mut mfds, 0, input, 0, None); // c:3722
12115 }
12116 if output != 0 {
12117 addfd(forked, &mut save, &mut mfds, 1, output, 1, None); // c:3724
12118 }
12119
12120 // c:3726-3728 — `if (redir) spawnpipes(redir, nullexec);`
12121 if let Some(ref mut r) = redir {
12122 spawnpipes(r.as_mut_slice(), nullexec);
12123 }
12124
12125 // c:3731-3955 — io redirection loop. Faithful per-redir match.
12126 while let Some(redir_list) = redir.as_mut() {
12127 // c:3731 — `while (redir && nonempty(redir))`
12128 if redir_list.is_empty() {
12129 break;
12130 }
12131 let mut fn_ = redir_list.remove(0); // c:3732 `fn = (Redir) ugetnode(redir);`
12132 // c:3734-3735 DPUTS — debug assert REDIR_HEREDOC* gone.
12133 if fn_.typ == REDIR_INPIPE {
12134 // c:3736
12135 if checkclobberparam(&fn_) == 0 || fn_.fd2 == -1 {
12136 // c:3737
12137 if fn_.fd2 != -1 {
12138 let _ = zclose(fn_.fd2); // c:3738-3739
12139 }
12140 closemnodes(&mut mfds); // c:3740
12141 fixfds(&save); // c:3741
12142 {
12143 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
12144 LASTVAL.store(1, Ordering::Relaxed);
12145 } // c:3742
12146 break;
12147 }
12148 // c:3744 — `addfd(forked, save, mfds, fn->fd1, fn->fd2, 0, fn->varid);`
12149 addfd(
12150 forked,
12151 &mut save,
12152 &mut mfds,
12153 fn_.fd1,
12154 fn_.fd2,
12155 0,
12156 fn_.varid.as_deref(),
12157 );
12158 } else if fn_.typ == REDIR_OUTPIPE {
12159 // c:3745
12160 if checkclobberparam(&fn_) == 0 || fn_.fd2 == -1 {
12161 // c:3746
12162 if fn_.fd2 != -1 {
12163 let _ = zclose(fn_.fd2); // c:3747-3748
12164 }
12165 closemnodes(&mut mfds); // c:3749
12166 fixfds(&save); // c:3750
12167 {
12168 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
12169 LASTVAL.store(1, Ordering::Relaxed);
12170 } // c:3751
12171 break;
12172 }
12173 // c:3753
12174 addfd(
12175 forked,
12176 &mut save,
12177 &mut mfds,
12178 fn_.fd1,
12179 fn_.fd2,
12180 1,
12181 fn_.varid.as_deref(),
12182 );
12183 } else {
12184 // c:3754 — non-pipe redir branch.
12185 let mut closed: i32; // c:3755
12186 // c:3756-3757 — `if (fn->type != REDIR_HERESTR && xpandredir(fn, redir)) continue;`
12187 if fn_.typ != REDIR_HERESTR && crate::ported::glob::xpandredir(&mut fn_, redir_list) != 0
12188 {
12189 continue;
12190 }
12191 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
12192 // c:3758
12193 closemnodes(&mut mfds); // c:3759
12194 fixfds(&save); // c:3760
12195 {
12196 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
12197 LASTVAL.store(1, Ordering::Relaxed);
12198 } // c:3761
12199 break;
12200 }
12201 if !isset(EXECOPT) {
12202 // c:3763 — `if (unset(EXECOPT)) continue;`
12203 continue;
12204 }
12205 let fil_local: i32;
12206 match fn_.typ {
12207 t if t == REDIR_HERESTR => {
12208 // c:3766
12209 if checkclobberparam(&fn_) == 0 {
12210 fil_local = -1; // c:3768
12211 } else {
12212 fil_local = getherestr(&fn_); // c:3770
12213 }
12214 if fil_local == -1 {
12215 // c:3771
12216 let e = std::io::Error::last_os_error();
12217 let raw = e.raw_os_error().unwrap_or(0);
12218 if raw != 0 && raw != libc::EINTR {
12219 zwarn(&format!("can't create temp file for here document: {}", e));
12220 // c:3772-3774
12221 }
12222 closemnodes(&mut mfds); // c:3775
12223 fixfds(&save); // c:3776
12224 {
12225 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
12226 LASTVAL.store(1, Ordering::Relaxed);
12227 } // c:3777
12228 break;
12229 }
12230 // c:3779
12231 addfd(
12232 forked,
12233 &mut save,
12234 &mut mfds,
12235 fn_.fd1,
12236 fil_local,
12237 0,
12238 fn_.varid.as_deref(),
12239 );
12240 }
12241 t if t == REDIR_READ || t == REDIR_READWRITE => {
12242 // c:3781-3782
12243 if checkclobberparam(&fn_) == 0 {
12244 fil_local = -1; // c:3784
12245 } else {
12246 let name = fn_.name.clone().unwrap_or_default();
12247 let unmeta_name = unmeta(&name);
12248 let cstr = match std::ffi::CString::new(unmeta_name.as_str()) {
12249 Ok(c) => c,
12250 Err(_) => {
12251 closemnodes(&mut mfds);
12252 fixfds(&save);
12253 {
12254 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
12255 LASTVAL.store(1, Ordering::Relaxed);
12256 }
12257 break;
12258 }
12259 };
12260 if fn_.typ == REDIR_READ {
12261 // c:3786
12262 fil_local = unsafe {
12263 libc::open(cstr.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY)
12264 };
12265 } else {
12266 // c:3788-3789
12267 fil_local = unsafe {
12268 libc::open(
12269 cstr.as_ptr(),
12270 libc::O_RDWR | libc::O_CREAT | libc::O_NOCTTY,
12271 0o666,
12272 )
12273 };
12274 }
12275 }
12276 if fil_local == -1 {
12277 // c:3790
12278 closemnodes(&mut mfds); // c:3791
12279 fixfds(&save); // c:3792
12280 let e = std::io::Error::last_os_error();
12281 if e.raw_os_error().unwrap_or(0) != libc::EINTR {
12282 zwarn(&format!("{}: {}", e, fn_.name.as_deref().unwrap_or("")));
12283 // c:3793-3794
12284 }
12285 {
12286 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
12287 LASTVAL.store(1, Ordering::Relaxed);
12288 } // c:3795
12289 break;
12290 }
12291 // c:3797
12292 addfd(
12293 forked,
12294 &mut save,
12295 &mut mfds,
12296 fn_.fd1,
12297 fil_local,
12298 0,
12299 fn_.varid.as_deref(),
12300 );
12301 // c:3800-3802 — `if (nullexec == 1 && fn->fd1 == 0 && ...) init_io(NULL);`
12302 if nullexec == 1
12303 && fn_.fd1 == 0
12304 && fn_.varid.is_none()
12305 && isset(SHINSTDIN)
12306 && isset(INTERACTIVE)
12307 {
12308 // c:3801 — `!zleactive` check ommitted (zleactive
12309 // accessor lives in zle module; fusevm bypasses ZLE).
12310 crate::ported::init::init_io(None); // c:3802
12311 }
12312 }
12313 t if t == REDIR_CLOSE => {
12314 // c:3804
12315 // c:3805 — `if (fn->varid) { parse fd from variable }`
12316 let mut fd1_local = fn_.fd1;
12317 if let Some(varname) = fn_.varid.as_deref() {
12318 // c:3806-3849 — `{var}>&-`/`{var}<&-` REDIR_CLOSE
12319 // with varid. The C path resolves the named param
12320 // to its integer-string value, parses as base-10
12321 // (or base#NN), and rejects readonly / non-numeric
12322 // / shell-owned-fd values.
12323 //
12324 // bad=1 → "parameter %s does not contain a file descriptor"
12325 // bad=2 → "can't close file descriptor from readonly parameter %s"
12326 // bad=3 → "file descriptor %d used by shell, not closed"
12327 //
12328 // Substrate now available: getsparam for value,
12329 // paramtab read for PM_READONLY, MAX_ZSH_FD +
12330 // fdtable_get for shell-owned guard.
12331 let mut bad: u8 = 0;
12332 let value_opt = getsparam(varname);
12333 let is_ro = paramtab()
12334 .read()
12335 .ok()
12336 .and_then(|t| {
12337 t.get(varname)
12338 .map(|p| (p.node.flags as u32 & PM_READONLY) != 0)
12339 })
12340 .unwrap_or(false);
12341 if value_opt.is_none() {
12342 bad = 1; // c:3811 getvalue failed
12343 } else if is_ro {
12344 bad = 2; // c:3813 PM_READONLY
12345 } else {
12346 let s = value_opt.as_deref().unwrap_or("");
12347 match s.trim().parse::<i32>() {
12348 Ok(n) => {
12349 fd1_local = n;
12350 fn_.fd1 = n;
12351 let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed);
12352 if n >= 10
12353 && n <= max_fd
12354 && (fdtable_get(n) & FDT_TYPE_MASK) == FDT_INTERNAL
12355 {
12356 // c:3835 shell-owned-fd reject
12357 bad = 3;
12358 }
12359 }
12360 Err(_) => {
12361 bad = 1; // c:3823 strtol failure
12362 }
12363 }
12364 }
12365 if bad != 0 {
12366 // c:3840-3849
12367 match bad {
12368 3 => zwarn(&format!(
12369 "file descriptor {} used by shell, not closed",
12370 fn_.fd1
12371 )),
12372 2 => zwarn(&format!(
12373 "can't close file descriptor from readonly parameter {}",
12374 varname
12375 )),
12376 _ => zwarn(&format!(
12377 "parameter {} does not contain a file descriptor",
12378 varname
12379 )),
12380 }
12381 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
12382 LASTVAL.store(1, Ordering::Relaxed);
12383 break;
12384 }
12385 }
12386 // c:3852-3865 — `closed`: optional movefd save.
12387 closed = 0;
12388 if forked == 0 && fd1_local < 10 && save[fd1_local as usize] == -2 {
12389 // c:3856
12390 let mv = movefd(fd1_local); // c:3857
12391 save[fd1_local as usize] = mv;
12392 if mv >= 0 {
12393 closed = 1; // c:3862-3863
12394 }
12395 }
12396 if fd1_local < 10 {
12397 // c:3866
12398 closemn(&mut mfds, fd1_local, REDIR_CLOSE);
12399 // c:3867
12400 }
12401 // c:3873-3876
12402 let _ = &mut fd1_local;
12403 if closed == 0 && zclose(fn_.fd1) < 0 && fn_.varid.is_some() {
12404 zwarn(&format!(
12405 "failed to close file descriptor {}: {}",
12406 fn_.fd1,
12407 std::io::Error::last_os_error()
12408 )); // c:3873-3875
12409 }
12410 }
12411 t if t == REDIR_MERGEIN || t == REDIR_MERGEOUT => {
12412 // c:3878-3879
12413 if fn_.fd2 < 10 {
12414 closemn(&mut mfds, fn_.fd2, fn_.typ); // c:3881
12415 }
12416 if checkclobberparam(&fn_) == 0 {
12417 fil_local = -1; // c:3883
12418 } else if fn_.fd2 > 9 {
12419 // c:3884-3897 — fd table check.
12420 let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed);
12421 let cin = crate::ported::modules::clone::coprocin.load(Ordering::Relaxed);
12422 let cout = crate::ported::modules::clone::coprocout.load(Ordering::Relaxed);
12423 let in_table = if fn_.fd2 <= max_fd {
12424 let kind = fdtable_get(fn_.fd2) & FDT_TYPE_MASK;
12425 kind != FDT_UNUSED && kind != FDT_EXTERNAL
12426 } else {
12427 false
12428 };
12429 if in_table || fn_.fd2 == cin || fn_.fd2 == cout {
12430 fil_local = -1; // c:3896
12431 // Per-platform errno setter (c:3897 `errno = EBADF;`).
12432 #[cfg(target_os = "macos")]
12433 unsafe {
12434 *libc::__error() = libc::EBADF;
12435 }
12436 #[cfg(target_os = "linux")]
12437 unsafe {
12438 *libc::__errno_location() = libc::EBADF;
12439 }
12440 } else {
12441 let fd = if fn_.fd2 == -2 {
12442 // c:3900-3901
12443 if fn_.typ == REDIR_MERGEOUT {
12444 crate::ported::modules::clone::coprocout.load(Ordering::Relaxed)
12445 } else {
12446 crate::ported::modules::clone::coprocin.load(Ordering::Relaxed)
12447 }
12448 } else {
12449 fn_.fd2
12450 };
12451 // c:3902 — `fil = movefd(dup(fd));`
12452 let dup_fd = unsafe { libc::dup(fd) };
12453 fil_local = movefd(dup_fd);
12454 }
12455 } else {
12456 let fd = if fn_.fd2 == -2 {
12457 if fn_.typ == REDIR_MERGEOUT {
12458 crate::ported::modules::clone::coprocout.load(Ordering::Relaxed)
12459 } else {
12460 crate::ported::modules::clone::coprocin.load(Ordering::Relaxed)
12461 }
12462 } else {
12463 fn_.fd2
12464 };
12465 let dup_fd = unsafe { libc::dup(fd) };
12466 fil_local = movefd(dup_fd);
12467 }
12468 if fil_local == -1 {
12469 // c:3904
12470 closemnodes(&mut mfds); // c:3907
12471 fixfds(&save); // c:3908
12472 if std::io::Error::last_os_error().raw_os_error().unwrap_or(0) != 0 {
12473 let desc = if fn_.fd2 == -2 {
12474 "coprocess".to_string()
12475 } else {
12476 format!("{}", fn_.fd2)
12477 };
12478 zwarn(&format!("{}: {}", desc, std::io::Error::last_os_error()));
12479 // c:3911-3913
12480 }
12481 {
12482 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
12483 LASTVAL.store(1, Ordering::Relaxed);
12484 } // c:3914
12485 break;
12486 }
12487 // c:3916-3917
12488 let merge_is_out = if fn_.typ == REDIR_MERGEOUT { 1 } else { 0 };
12489 addfd(
12490 forked,
12491 &mut save,
12492 &mut mfds,
12493 fn_.fd1,
12494 fil_local,
12495 merge_is_out,
12496 fn_.varid.as_deref(),
12497 );
12498 }
12499 _ => {
12500 // c:3919 default — write/append/error_redir.
12501 let mut dfil: i32;
12502 if checkclobberparam(&fn_) == 0 {
12503 fil_local = -1; // c:3921
12504 } else if IS_APPEND_REDIR(fn_.typ) {
12505 // c:3922
12506 let name = fn_.name.clone().unwrap_or_default();
12507 let unmeta_name = unmeta(&name);
12508 let cstr = match std::ffi::CString::new(unmeta_name.as_str()) {
12509 Ok(c) => c,
12510 Err(_) => {
12511 closemnodes(&mut mfds);
12512 fixfds(&save);
12513 {
12514 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
12515 LASTVAL.store(1, Ordering::Relaxed);
12516 }
12517 break;
12518 }
12519 };
12520 // c:3924-3927
12521 let mode = if !isset(CLOBBER)
12522 && !isset(crate::ported::zsh_h::APPENDCREATE)
12523 && !IS_CLOBBER_REDIR(fn_.typ)
12524 {
12525 libc::O_WRONLY | libc::O_APPEND | libc::O_NOCTTY
12526 } else {
12527 libc::O_WRONLY | libc::O_APPEND | libc::O_CREAT | libc::O_NOCTTY
12528 };
12529 fil_local = unsafe { libc::open(cstr.as_ptr(), mode, 0o666) };
12530 } else {
12531 // c:3929
12532 fil_local = clobber_open(&fn_);
12533 }
12534 // c:3930-3933 — error_redir dup.
12535 if fil_local != -1 && IS_ERROR_REDIR(fn_.typ) {
12536 let dup_fd = unsafe { libc::dup(fil_local) };
12537 dfil = movefd(dup_fd); // c:3931
12538 } else {
12539 dfil = 0; // c:3933
12540 }
12541 if fil_local == -1 || dfil == -1 {
12542 // c:3934
12543 if fil_local != -1 {
12544 unsafe { libc::close(fil_local) }; // c:3935-3936
12545 }
12546 closemnodes(&mut mfds); // c:3937
12547 fixfds(&save); // c:3938
12548 let e = std::io::Error::last_os_error();
12549 let raw = e.raw_os_error().unwrap_or(0);
12550 if raw != 0 && raw != libc::EINTR {
12551 zwarn(&format!("{}: {}", e, fn_.name.as_deref().unwrap_or("")));
12552 // c:3939-3940
12553 }
12554 {
12555 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
12556 LASTVAL.store(1, Ordering::Relaxed);
12557 } // c:3941
12558 break;
12559 }
12560 // c:3943
12561 addfd(
12562 forked,
12563 &mut save,
12564 &mut mfds,
12565 fn_.fd1,
12566 fil_local,
12567 1,
12568 fn_.varid.as_deref(),
12569 );
12570 if IS_ERROR_REDIR(fn_.typ) {
12571 // c:3944-3945
12572 addfd(forked, &mut save, &mut mfds, 2, dfil, 1, None);
12573 }
12574 let _ = &mut dfil;
12575 }
12576 }
12577 // c:3948-3952 — addfd errflag check.
12578 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
12579 // c:3949
12580 closemnodes(&mut mfds); // c:3950
12581 fixfds(&save); // c:3951
12582 {
12583 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
12584 LASTVAL.store(1, Ordering::Relaxed);
12585 } // c:3952
12586 break;
12587 }
12588 }
12589 }
12590
12591 // c:3957-3961 — close multios with ct >= 2.
12592 i = 0;
12593 while i < 10 {
12594 // c:3959
12595 if let Some(m) = mfds.get(i as usize).and_then(|o| o.as_ref()) {
12596 if m.ct >= 2 {
12597 closemn(&mut mfds, i, REDIR_CLOSE); // c:3960
12598 }
12599 }
12600 i += 1;
12601 }
12602
12603 // c:3963-3995 — nullexec branch.
12604 if nullexec != 0 {
12605 // c:3963
12606 if let Some(vspc) = varspc {
12607 // c:3969
12608 let mut restorelist: Vec<crate::ported::zsh_h::param> = Vec::new();
12609 let mut removelist: Vec<String> = Vec::new();
12610 if !isset(POSIXBUILTINS) && nullexec != 2 {
12611 // c:3971-3972
12612 save_params(state, vspc, &mut restorelist, &mut removelist);
12613 }
12614 addvars(state, vspc, 0); // c:3973
12615 if !restorelist.is_empty() {
12616 // c:3974
12617 restore_params(restorelist, removelist); // c:3975
12618 }
12619 }
12620 let ef = errflag.load(Ordering::Relaxed);
12621 LASTVAL.store(
12622 if ef != 0 {
12623 ef
12624 } else {
12625 cmdoutval.load(Ordering::Relaxed)
12626 },
12627 Ordering::Relaxed,
12628 ); // c:3977
12629 if nullexec == 1 {
12630 // c:3978
12631 // c:3983-3985 — close save[i].
12632 i = 0;
12633 while i < 10 {
12634 if save[i as usize] != -2 {
12635 let _ = zclose(save[i as usize]); // c:3985
12636 }
12637 i += 1;
12638 }
12639 // c:3988-3989 — `jobtab[thisjob].stat |= STAT_DONE; goto done;`
12640 let thisjob = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
12641 if thisjob >= 0 {
12642 if let Some(jt) = JOBTAB.get() {
12643 let mut guard = jt.lock().unwrap();
12644 if let Some(j) = guard.get_mut(thisjob as usize) {
12645 j.stat |= STAT_DONE; // c:3989
12646 }
12647 }
12648 }
12649 return execcmd_exec_done_path(
12650 redir_err,
12651 oautocont,
12652 how,
12653 &mut shti,
12654 &mut chti,
12655 &mut then_ts,
12656 forked,
12657 &mut newxtrerr,
12658 cflags,
12659 orig_cflags,
12660 is_cursh,
12661 do_exec,
12662 );
12663 }
12664 if isset(XTRACE) {
12665 // c:3992-3994
12666 eprintln!();
12667 }
12668 } else if isset(EXECOPT) && (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
12669 // c:3996 — main dispatch branch.
12670 // c:3997 — `int q = queue_signal_level();`
12671 let _q = 0;
12672 // c:4003-4012 — entersubsh for is_exec.
12673 if is_exec != 0 {
12674 // c:4003
12675 let mut flags: i32 = if (how & Z_ASYNC as i32) != 0 {
12676 esub::ASYNC
12677 } else {
12678 0
12679 } | esub::PGRP
12680 | esub::FAKE; // c:4004-4005
12681 if typ != WC_SUBSH as i32 {
12682 flags |= esub::KEEPTRAP; // c:4007
12683 }
12684 if (do_exec != 0 || (typ >= WC_CURSH as i32 && last1 == 1)) && forked == 0 {
12685 // c:4008-4009
12686 flags |= esub::REVERTPGRP; // c:4010
12687 }
12688 entersubsh(flags, None); // c:4011
12689 }
12690
12691 if typ == WC_FUNCDEF as i32 {
12692 // c:4013
12693 // c:4069-4091 — redirections stored with the new function when
12694 // none were taken from the parsed environment.
12695 let redir_prog: Option<crate::ported::zsh_h::Eprog> = if redir.is_none()
12696 && wc_code(state.prog.prog[eparams.beg]) == WC_REDIR
12697 {
12698 let mut s = estate {
12699 prog: state.prog.clone(), // c:4079
12700 pc: eparams.beg, // c:4080
12701 strs: state.prog.strs.clone(), // c:4081
12702 strs_offset: 0,
12703 };
12704 // The copy uses the wordcode parsing area, so save and
12705 // restore state.
12706 crate::ported::context::zcontext_save(); // c:4087
12707 let rp = crate::ported::parse::eccopyredirs(&mut s); // c:4088
12708 crate::ported::context::zcontext_restore(); // c:4089
12709 rp.map(Box::new)
12710 } else {
12711 None // c:4091
12712 };
12713 // c:4039 — `lastval = execfuncdef(state, redir_prog);`
12714 let lv = execfuncdef(state, redir_prog);
12715 LASTVAL.store(lv, Ordering::Relaxed);
12716 } else if typ >= WC_CURSH as i32 {
12717 // c:4042
12718 if last1 == 1 {
12719 do_exec = 1; // c:4044
12720 }
12721 if typ == WC_AUTOFN as i32 {
12722 // c:4046
12723 let lv = execautofn_basic(state, do_exec); // c:4051
12724 LASTVAL.store(lv, Ordering::Relaxed);
12725 } else {
12726 // c:4053 — `lastval = (execfuncs[type - WC_CURSH])(state, do_exec);`
12727 // dispatch_execfuncs ports the C `execfuncs[]` table
12728 // (Src/exec.c:268) by typ → exec{cursh,for,select,...}
12729 // direct call. See dispatch_execfuncs at end of file.
12730 let lv = dispatch_execfuncs(state, typ, do_exec);
12731 LASTVAL.store(lv, Ordering::Relaxed);
12732 }
12733 } else if is_builtin != 0 || is_shfunc != 0 {
12734 // c:4055
12735 let mut restorelist: Vec<crate::ported::zsh_h::param> = Vec::new();
12736 let mut removelist: Vec<String> = Vec::new();
12737 let mut do_save: i32 = 0; // c:4057
12738
12739 if forked == 0 {
12740 // c:4060
12741 if isset(POSIXBUILTINS) {
12742 // c:4061
12743 if is_shfunc != 0
12744 || (hn.map(|p| unsafe { (*p).node.flags as u32 }).unwrap_or(0)
12745 & (BINF_PSPECIAL | BINF_ASSIGN_FLAG))
12746 != 0
12747 {
12748 // c:4067
12749 do_save = if (orig_cflags & BINF_COMMAND) != 0 {
12750 1
12751 } else {
12752 0
12753 };
12754 } else {
12755 do_save = 1; // c:4070
12756 }
12757 } else {
12758 // c:4071
12759 if (cflags & (BINF_COMMAND | BINF_ASSIGN_FLAG)) != 0 || magic_assign == 0 {
12760 // c:4076
12761 do_save = 1; // c:4077
12762 }
12763 }
12764 if do_save != 0 {
12765 if let Some(vspc) = varspc {
12766 // c:4079
12767 save_params(state, vspc, &mut restorelist, &mut removelist);
12768 }
12769 }
12770 }
12771 if varspc.is_some() {
12772 // c:4082
12773 let mut addflags: i32 = 0; // c:4086
12774 if is_shfunc != 0 {
12775 addflags |= ADDVAR_EXPORT; // c:4088
12776 }
12777 if !restorelist.is_empty() {
12778 addflags |= ADDVAR_RESTORE; // c:4090
12779 }
12780 addvars(state, varspc.unwrap_or(0), addflags); // c:4092
12781 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
12782 // c:4093
12783 if !restorelist.is_empty() {
12784 restore_params(restorelist, removelist); // c:4094-4095
12785 }
12786 LASTVAL.store(1, Ordering::Relaxed); // c:4096
12787 fixfds(&save); // c:4097
12788 return execcmd_exec_done_path(
12789 redir_err,
12790 oautocont,
12791 how,
12792 &mut shti,
12793 &mut chti,
12794 &mut then_ts,
12795 forked,
12796 &mut newxtrerr,
12797 cflags,
12798 orig_cflags,
12799 is_cursh,
12800 do_exec,
12801 );
12802 }
12803 }
12804
12805 if is_shfunc != 0 {
12806 // c:4102-4105
12807 let mut a_vec: Vec<String> = args.clone().unwrap_or_default();
12808 // c:4104 — `execshfunc((Shfunc) hn, args);` C casts
12809 // HashNode hn to Shfunc; zshrs's hn is *mut builtin so
12810 // we re-resolve the shfunc by name from shfunctab and
12811 // dispatch through the top-level execshfunc port at
12812 // exec.rs:4978 (which routes to runshfunc).
12813 let name = args
12814 .as_ref()
12815 .and_then(|v| v.first())
12816 .cloned()
12817 .unwrap_or_default();
12818 let mut shf_clone: Option<shfunc> = if let Ok(tab) = shfunctab_lock().read() {
12819 tab.get(&name).cloned()
12820 } else {
12821 None
12822 };
12823 if let Some(ref mut shf) = shf_clone {
12824 execshfunc(shf, &mut a_vec);
12825 }
12826 // c:4105 — `pipecleanfilelist(filelist, 0);` — clean
12827 // out the proc_subst entries from the current job's
12828 // filelist after the shfunc body ran. Route through
12829 // `JOBTAB[thisjob]`.
12830 if let Some(jt) = JOBTAB.get() {
12831 let mut guard = jt.lock().unwrap();
12832 let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
12833 if tj >= 0 {
12834 if let Some(j) = guard.get_mut(tj as usize) {
12835 crate::ported::jobs::pipecleanfilelist(j, false);
12836 }
12837 }
12838 }
12839 } else {
12840 // c:4107 — builtin path.
12841 let mut assigns: Vec<crate::ported::zsh_h::asgment> = Vec::new(); // c:4108
12842 let postassigns = eparams.postassigns; // c:4109
12843 if forked != 0 {
12844 closem(FDT_INTERNAL, 0); // c:4111
12845 }
12846 if postassigns != 0 {
12847 // c:4112-4230 — typeset post-assignment processing.
12848 use crate::ported::zsh_h::{
12849 ASG_ARRAY, ASG_KEY_VALUE, EC_DUPTOK as ECDUPTOK_LOCAL, PREFORK_ASSIGN,
12850 PREFORK_KEY_VALUE, PREFORK_SINGLE, PREFORK_TYPESET, WC_ASSIGN_INC,
12851 WC_ASSIGN_NUM, WC_ASSIGN_SCALAR, WC_ASSIGN_TYPE, WC_ASSIGN_TYPE2,
12852 };
12853 let opc = state.pc; // c:4113
12854 state.pc = eparams.assignspc.unwrap_or(state.pc); // c:4114
12855 // c:4115 — `assigns = newlinklist();` — already declared above.
12856 let mut pa_remaining = postassigns;
12857 while pa_remaining > 0 {
12858 // c:4116 — `while (postassigns--)`
12859 pa_remaining -= 1;
12860 let mut pa_htok: i32 = 0; // c:4117
12861 if state.pc >= state.prog.prog.len() {
12862 break;
12863 }
12864 let ac = state.prog.prog[state.pc]; // c:4118
12865 state.pc += 1;
12866 let mut name = ecgetstr(state, ECDUPTOK_LOCAL, Some(&mut pa_htok)); // c:4119
12867 // c:4123-4124 DPUTS — debug assertion skipped.
12868 if pa_htok != 0 {
12869 // c:4126 — `init_list1(svl, name);`
12870 let mut svl: LinkList<String> = Default::default();
12871 svl.push_back(name.clone());
12872 // c:4127-4166 — INC-scalar special case (typeset $ass form).
12873 if WC_ASSIGN_TYPE(ac) == WC_ASSIGN_SCALAR
12874 && WC_ASSIGN_TYPE2(ac) == WC_ASSIGN_INC
12875 {
12876 // c:4141 — `(void)ecgetstr(...)` — dummy.
12877 let mut dummy_htok: i32 = 0;
12878 let _ = ecgetstr(state, ECDUPTOK_LOCAL, Some(&mut dummy_htok));
12879 let mut rf = 0i32;
12880 prefork(&mut svl, PREFORK_TYPESET, &mut rf); // c:4142
12881 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
12882 // c:4143
12883 state.pc = opc; // c:4144
12884 break;
12885 }
12886 let mut rf2 = 0i32;
12887 globlist(&mut svl, rf2); // c:4147
12888 let _ = &mut rf2;
12889 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
12890 // c:4148
12891 state.pc = opc; // c:4149
12892 break;
12893 }
12894 // c:4152-4165 — drain svl into assigns.
12895 while let Some(data) = svl.pop_front() {
12896 let (asg_name, asg_val): (String, Option<String>) =
12897 if let Some(eq_pos) = data.find('=') {
12898 // c:4156-4159
12899 (
12900 data[..eq_pos].to_string(),
12901 Some(data[eq_pos + 1..].to_string()),
12902 )
12903 } else {
12904 // c:4161-4162
12905 (data, None)
12906 };
12907 assigns.push(crate::ported::zsh_h::asgment {
12908 node: crate::ported::zsh_h::linknode {
12909 next: None,
12910 prev: None,
12911 dat: 0,
12912 },
12913 name: asg_name,
12914 flags: 0,
12915 scalar: asg_val,
12916 array: None,
12917 });
12918 }
12919 continue; // c:4166
12920 }
12921 // c:4168 — `prefork(&svl, PREFORK_SINGLE, NULL);`
12922 let mut rf = 0i32;
12923 prefork(&mut svl, PREFORK_SINGLE, &mut rf);
12924 // c:4169-4170 — `name = empty(svl) ? "" : firstnode_data;`
12925 name = if svl.is_empty() {
12926 String::new()
12927 } else {
12928 svl.pop_front().unwrap_or_default()
12929 };
12930 }
12931 // c:4172 — `untokenize(name);`
12932 // (untokenize is destructive on bytes; Rust untokenize
12933 // returns a new String — call and rebind.)
12934 name = untokenize(&name);
12935 let mut asg = crate::ported::zsh_h::asgment {
12936 node: crate::ported::zsh_h::linknode {
12937 next: None,
12938 prev: None,
12939 dat: 0,
12940 },
12941 name,
12942 flags: 0,
12943 scalar: None,
12944 array: None,
12945 };
12946 if WC_ASSIGN_TYPE(ac) == WC_ASSIGN_SCALAR {
12947 // c:4175
12948 let mut val_htok: i32 = 0;
12949 let mut val = ecgetstr(state, ECDUPTOK_LOCAL, Some(&mut val_htok)); // c:4176
12950 asg.flags = 0; // c:4177
12951 if WC_ASSIGN_TYPE2(ac) == WC_ASSIGN_INC {
12952 // c:4178-4180 — fake assignment, no value.
12953 asg.scalar = None;
12954 } else {
12955 if val_htok != 0 {
12956 // c:4183
12957 let mut svl: LinkList<String> = Default::default();
12958 svl.push_back(val.clone());
12959 let mut rf = 0i32;
12960 prefork(&mut svl, PREFORK_SINGLE | PREFORK_ASSIGN, &mut rf); // c:4184-4186
12961 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
12962 // c:4187
12963 state.pc = opc; // c:4188
12964 break;
12965 }
12966 // c:4195-4196 — `val = empty(svl) ? "" : firstdata;`
12967 val = if svl.is_empty() {
12968 String::new()
12969 } else {
12970 svl.pop_front().unwrap_or_default()
12971 };
12972 }
12973 // c:4198 — `untokenize(val);`
12974 asg.scalar = Some(untokenize(&val));
12975 }
12976 } else {
12977 // c:4202 — array assignment.
12978 asg.flags = ASG_ARRAY; // c:4202
12979 let mut arr_htok: i32 = 0;
12980 let arr_words = ecgetlist(
12981 state,
12982 WC_ASSIGN_NUM(ac) as usize,
12983 ECDUPTOK_LOCAL,
12984 Some(&mut arr_htok),
12985 ); // c:4204
12986 let mut arr_list: LinkList<String> = Default::default();
12987 for s in arr_words {
12988 arr_list.push_back(s);
12989 }
12990 if !arr_list.is_empty()
12991 && (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0
12992 {
12993 // c:4209 — `int prefork_ret = 0;`
12994 let mut prefork_ret = 0i32;
12995 prefork(&mut arr_list, PREFORK_ASSIGN, &mut prefork_ret); // c:4210-4211
12996 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
12997 // c:4212
12998 state.pc = opc; // c:4213
12999 break;
13000 }
13001 if (prefork_ret & PREFORK_KEY_VALUE) != 0 {
13002 // c:4216
13003 asg.flags |= ASG_KEY_VALUE; // c:4217
13004 }
13005 globlist(&mut arr_list, prefork_ret); // c:4218
13006 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
13007 // c:4220
13008 state.pc = opc; // c:4221
13009 break;
13010 }
13011 }
13012 asg.array = Some(arr_list);
13013 }
13014 // c:4227 — `uaddlinknode(assigns, &asg->node);`
13015 assigns.push(asg);
13016 }
13017 state.pc = opc; // c:4229
13018 }
13019 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
13020 // c:4232
13021 // c:Src/builtin.c:262 — `name = (char *) ugetnode(args);`
13022 // C's execbuiltin consumes args[0] (the command name)
13023 // at entry. zshrs's execbuiltin reads the name from
13024 // `bn->node.nam` instead, so we strip args[0] here
13025 // before the call to match C's post-ugetnode argv
13026 // shape. Without this, e.g. `cmd=pwd; $cmd` reached
13027 // execbuiltin with args=["pwd"] and pwd's
13028 // maxargs=0 check rejected the empty call as
13029 // "too many arguments".
13030 let mut a_vec: Vec<String> = args.clone().unwrap_or_default();
13031 if !a_vec.is_empty() {
13032 a_vec.remove(0);
13033 }
13034 let ret = crate::ported::builtin::execbuiltin(
13035 a_vec,
13036 assigns,
13037 hn.unwrap_or(std::ptr::null_mut()),
13038 ); // c:4233
13039 if (errflag.load(Ordering::Relaxed) & ERRFLAG_INT) == 0 {
13040 // c:4238
13041 LASTVAL.store(ret, Ordering::Relaxed); // c:4239
13042 }
13043 }
13044 if (do_save & BINF_COMMAND as i32) != 0 {
13045 // c:4241
13046 errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed); // c:4242
13047 }
13048 // c:4244 fflush(stdout) — Rust stdio auto-flushes.
13049 // c:4245-4251 — write-error check on save[1].
13050 }
13051 if isset(PRINTEXITVALUE)
13052 && isset(SHINSTDIN)
13053 && LASTVAL.load(Ordering::Relaxed) != 0
13054 && subsh.load(Ordering::Relaxed) == 0
13055 {
13056 // c:4253-4255
13057 eprintln!("zsh: exit {}", LASTVAL.load(Ordering::Relaxed)); // c:4258
13058 }
13059
13060 if do_exec != 0 {
13061 // c:4263
13062 if subsh.load(Ordering::Relaxed) != 0 {
13063 crate::ported::builtin::_realexit(); // c:4264-4265
13064 }
13065 if isset(RCS)
13066 && crate::ported::zsh_h::interact()
13067 && nohistsave.load(Ordering::Relaxed) == 0
13068 {
13069 // c:4269
13070 crate::ported::hist::savehistfile(None, HFILE_USE_OPTIONS as i32);
13071 // c:4270
13072 }
13073 crate::ported::builtin::realexit(); // c:4271
13074 }
13075 if !restorelist.is_empty() {
13076 // c:4273
13077 restore_params(restorelist, removelist); // c:4274
13078 }
13079 } else {
13080 // c:4276 — external command execute.
13081 if subsh.load(Ordering::Relaxed) == 0 {
13082 // c:4277
13083 if forked == 0 {
13084 // c:4280 — `setiparam("SHLVL", --shlvl);`
13085 let cur = getsparam("SHLVL")
13086 .and_then(|s| s.parse::<i64>().ok())
13087 .unwrap_or(1);
13088 setiparam("SHLVL", cur - 1); // c:4281
13089 }
13090 if do_exec != 0
13091 && isset(RCS)
13092 && crate::ported::zsh_h::interact()
13093 && nohistsave.load(Ordering::Relaxed) == 0
13094 {
13095 // c:4285
13096 crate::ported::hist::savehistfile(None, HFILE_USE_OPTIONS as i32);
13097 // c:4286
13098 }
13099 }
13100 if typ == WC_SIMPLE as i32 || typ == WC_TYPESET as i32 {
13101 // c:4288
13102 if varspc.is_some() {
13103 // c:4289
13104 let mut addflags: i32 = ADDVAR_EXPORT; // c:4290
13105 if forked != 0 {
13106 addflags |= ADDVAR_RESTORE; // c:4292
13107 }
13108 addvars(state, varspc.unwrap_or(0), addflags); // c:4293
13109 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
13110 // c:4294
13111 std::process::exit(1); // c:4295
13112 }
13113 }
13114 closem(FDT_INTERNAL, 0); // c:4297
13115 // c:4298-4305 — close coprocin/coprocout.
13116 let cpi = crate::ported::modules::clone::coprocin.load(Ordering::Relaxed);
13117 if cpi != -1 {
13118 let _ = zclose(cpi); // c:4299
13119 crate::ported::modules::clone::coprocin.store(-1, Ordering::Relaxed);
13120 // c:4300
13121 }
13122 let cpo = crate::ported::modules::clone::coprocout.load(Ordering::Relaxed);
13123 if cpo != -1 {
13124 let _ = zclose(cpo); // c:4303
13125 crate::ported::modules::clone::coprocout.store(-1, Ordering::Relaxed);
13126 // c:4304
13127 }
13128 if forked == 0 {
13129 // c:4307
13130 setlimits(""); // c:4308
13131 }
13132 if (how & Z_ASYNC as i32) != 0 {
13133 // c:4310 — `zsfree(STTYval); STTYval = 0;`
13134 let mut guard = STTYval.lock().unwrap();
13135 *guard = None; // c:4311-4312
13136 }
13137 // c:4314 — `execute(args, cflags, use_defpath);`
13138 let mut a_vec: Vec<String> = args.clone().unwrap_or_default();
13139 execute(&mut a_vec, cflags, use_defpath); // c:4314
13140 } else {
13141 // c:4315 — `( ... )` — WC_SUBSH.
13142 list_pipe.store(0, Ordering::Relaxed); // c:4318
13143 // c:4319 — `pipecleanfilelist(filelist, 0);` — clean
13144 // proc-subst entries from the current job's filelist
13145 // before recursing into the subshell body.
13146 if let Some(jt) = JOBTAB.get() {
13147 let mut guard = jt.lock().unwrap();
13148 let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
13149 if tj >= 0 {
13150 if let Some(j) = guard.get_mut(tj as usize) {
13151 crate::ported::jobs::pipecleanfilelist(j, false);
13152 }
13153 }
13154 }
13155 state.pc += 1; // c:4324 — `state->pc++;`
13156 let _ = execlist(state, 0, 1); // c:4325
13157 }
13158 }
13159 }
13160
13161 // c:4330-4404 — err: + done: + fatal:.
13162 return execcmd_exec_done_path(
13163 redir_err,
13164 oautocont,
13165 how,
13166 &mut shti,
13167 &mut chti,
13168 &mut then_ts,
13169 forked,
13170 &mut newxtrerr,
13171 cflags,
13172 orig_cflags,
13173 is_cursh,
13174 do_exec,
13175 );
13176}
13177
13178/// !!! WARNING: RUST-ONLY HELPER !!!
13179///
13180/// No function of this name exists in `Src/exec.c`. This helper stands in for
13181/// the `done:` label tail of `execcmd_exec` (c:4366-4403), which C reaches by
13182/// `goto` and Rust cannot.
13183/// Internal helper modelling the C `done:` label tail of
13184/// `execcmd_exec` at `Src/exec.c:4366-4403`. Handles POSIX special-
13185/// builtin error escalation, AUTOCONTINUE restore, STTYval clear,
13186/// shelltime stop, and newxtrerr close.
13187#[allow(clippy::too_many_arguments)]
13188fn execcmd_exec_done_path(
13189 redir_err: i32,
13190 oautocont: i32,
13191 how: i32,
13192 shti: &mut crate::ported::jobs::timeinfo,
13193 chti: &mut crate::ported::jobs::timeinfo,
13194 then_ts: &mut std::time::Instant,
13195 forked: i32,
13196 newxtrerr: &mut Option<i32>,
13197 cflags: u32,
13198 orig_cflags: u32,
13199 is_cursh: i32,
13200 do_exec: i32,
13201) {
13202 use crate::ported::zsh_h::{
13203 AUTOCONTINUE, BINF_COMMAND, BINF_EXEC, BINF_PSPECIAL, INTERACTIVE, POSIXBUILTINS, Z_TIMED,
13204 };
13205 // c:4366
13206 // c:4367-4386 — POSIX special-builtin error escalation.
13207 if isset(POSIXBUILTINS)
13208 && (cflags & (BINF_PSPECIAL | BINF_EXEC)) != 0
13209 && (orig_cflags & BINF_COMMAND) == 0
13210 {
13211 // c:4367-4369
13212 let _forked_or_subsh = forked | zsh_subshell.load(Ordering::Relaxed); // c:4376
13213 // fatal: label entry point — same handling.
13214 if redir_err != 0 || (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
13215 // c:4378
13216 if !isset(INTERACTIVE) {
13217 // c:4379
13218 if _forked_or_subsh != 0 {
13219 unsafe { libc::_exit(1) }; // c:4381
13220 } else {
13221 std::process::exit(1); // c:4383
13222 }
13223 }
13224 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:4385
13225 }
13226 }
13227 // c:4388-4389 — `if ((is_cursh || do_exec) && (how & Z_TIMED)) shelltime(...);`
13228 if (is_cursh != 0 || do_exec != 0) && (how & Z_TIMED as i32) != 0 {
13229 crate::ported::jobs::shelltime(Some(shti), Some(chti), Some(then_ts), 1);
13230 // c:4389
13231 }
13232 // c:4445-4452 — newxtrerr close.
13233 if let Some(fd) = newxtrerr.take() {
13234 // c:4445
13235 crate::fusevm_bridge::xtrerr_flush(); // c:4448 fclose(newxtrerr)
13236 crate::ported::utils::xtrerr.store(2, Ordering::Relaxed); // c:4449 xtrerr = oxtrerr
13237 let _ = zclose(fd); // c:4451
13238 }
13239 // c:4400-4401 — `zsfree(STTYval); STTYval = 0;`
13240 {
13241 let mut guard = STTYval.lock().unwrap();
13242 *guard = None;
13243 }
13244 // c:4402-4403 — `if (oautocont >= 0) opts[AUTOCONTINUE] = oautocont;`
13245 if oautocont >= 0 {
13246 opt_state_set("autocontinue", oautocont != 0);
13247 }
13248}
13249
13250/// !!! WARNING: RUST-ONLY HELPER !!!
13251///
13252/// No function of this name exists in `Src/exec.c`. This helper stands in for
13253/// the `err:` label tail of `execcmd_exec` (c:4330-4365), which C reaches by
13254/// `goto` and Rust cannot.
13255/// Internal helper modelling the C `err:` label tail of
13256/// `execcmd_exec` at `Src/exec.c:4330-4365`. Forked-child fd cleanup
13257/// + waitjobs + _realexit; non-forked: `fixfds(save)` + fall through
13258/// to done:.
13259#[allow(clippy::too_many_arguments)]
13260fn execcmd_exec_err_path(
13261 forked: i32,
13262 save: &mut [i32; 10],
13263 mfds: &mut [Option<Box<multio>>; 10],
13264 oautocont: i32,
13265 how: i32,
13266 shti: &mut crate::ported::jobs::timeinfo,
13267 chti: &mut crate::ported::jobs::timeinfo,
13268 then_ts: &mut std::time::Instant,
13269 newxtrerr: &mut Option<i32>,
13270 cflags: u32,
13271 orig_cflags: u32,
13272 is_cursh: i32,
13273 do_exec: i32,
13274 redir_err: i32,
13275) {
13276 use crate::ported::zsh_h::FDT_UNUSED;
13277 // c:4330
13278 if forked != 0 {
13279 // c:4331
13280 // c:4356-4358 — close all fds 0..10 whose fdtable entry != FDT_UNUSED.
13281 let mut i: i32 = 0;
13282 while i < 10 {
13283 if fdtable_get(i) != FDT_UNUSED {
13284 unsafe { libc::close(i) }; // c:4358
13285 }
13286 i += 1;
13287 }
13288 // c:4359 — `closem(FDT_UNUSED, 1);`
13289 closem(FDT_UNUSED, 1); // c:4359
13290 // c:4360-4361 — `if (thisjob != -1) waitjobs();`
13291 let thisjob = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
13292 if thisjob != -1 {
13293 crate::ported::jobs::waitjobs(); // c:4361
13294 }
13295 crate::ported::builtin::_realexit(); // c:4362
13296 }
13297 fixfds(save); // c:4364
13298
13299 execcmd_exec_done_path(
13300 redir_err,
13301 oautocont,
13302 how,
13303 shti,
13304 chti,
13305 then_ts,
13306 forked,
13307 newxtrerr,
13308 cflags,
13309 orig_cflags,
13310 is_cursh,
13311 do_exec,
13312 );
13313}
13314
13315/// !!! WARNING: RUST-ONLY HELPER !!!
13316///
13317/// No function of this name exists in `Src/exec.c`. This helper stands in for
13318/// the `execfuncs[type - WC_CURSH]` function-pointer table dispatch (table at
13319/// c:268, call sites at c:1331 and c:4053), written as a `match` because Rust
13320/// has no equivalent table of `int (*)(Estate, int)`.
13321/// Internal helper dispatching `execfuncs[type - WC_CURSH]` from
13322/// `Src/exec.c:268`. Each branch maps to the ported wordcode-
13323/// walker function in `src/ported/exec.rs`.
13324fn dispatch_execfuncs(state: &mut estate, typ: i32, do_exec: i32) -> i32 {
13325 use crate::ported::zsh_h::{
13326 WC_ARITH, WC_AUTOFN, WC_CASE, WC_COND, WC_CURSH, WC_FOR, WC_FUNCDEF, WC_IF, WC_REPEAT,
13327 WC_SELECT, WC_SUBSH, WC_TIMED, WC_TRY, WC_WHILE,
13328 };
13329 // Port of `static int (*const execfuncs[])(Estate, int)` dispatch
13330 // table at `Src/exec.c:268`. C indexes by `(type - WC_CURSH)`;
13331 // Rust matches on the WC_* tag directly.
13332 match typ as wordcode {
13333 x if x == WC_CURSH => execcursh(state, do_exec),
13334 x if x == WC_FOR => execfor(state, do_exec),
13335 x if x == WC_SELECT => execselect(state, do_exec),
13336 x if x == WC_WHILE => execwhile(state, do_exec),
13337 x if x == WC_REPEAT => execrepeat(state, do_exec),
13338 x if x == WC_CASE => execcase(state, do_exec),
13339 x if x == WC_IF => execif(state, do_exec),
13340 x if x == WC_COND => execcond(state, do_exec),
13341 x if x == WC_ARITH => execarith(state, do_exec),
13342 x if x == WC_TRY => exectry(state, do_exec),
13343 x if x == WC_FUNCDEF => execfuncdef(state, None),
13344 // c:272 — execfuncs[] table dispatches `WC_AUTOFN` to
13345 // `execautofn` (the loadautofn-then-basic wrapper), not
13346 // `execautofn_basic` directly.
13347 x if x == WC_AUTOFN => execautofn(state, do_exec),
13348 x if x == WC_TIMED => exectime(state, do_exec),
13349 x if x == WC_SUBSH => execcursh(state, do_exec), // c:269 — same handler.
13350 _ => 0,
13351 }
13352}
13353
13354/// Port of `stripkshdef()` from `Src/exec.c:6292` — C decl `stripkshdef(Eprog prog, char *name)`.
13355/// Given an Eprog read from an autoload
13356/// file plus the function name being defined, check whether the
13357/// file consists of *exactly* one `function NAME { … }` definition
13358/// for that name. If so, return a new Eprog whose `prog`/`strs`/
13359/// `pats` slice out just the function body (so calling code can
13360/// invoke the body directly instead of re-parsing). Otherwise
13361/// return the input untouched.
13362///
13363/// Header word layout consumed (matches C `pc[…]` reads):
13364/// pc[0] = WC_LIST with `Z_SYNC|Z_END|Z_SIMPLE` flags
13365/// pc[1] = (sublist header, skipped)
13366/// pc[2] = WC_FUNCDEF
13367/// pc[3] = 1 (single-name funcdef)
13368/// pc[4] = name-string slot (compared to `name`)
13369/// pc[5] = sbeg (offset into strs table)
13370/// pc[6] = nstrs (bytes of strs to copy)
13371/// pc[7] = npats (number of pattern slots to allocate)
13372/// pc[8] = WC_FUNCDEF_SKIP target (end-of-funcdef pc)
13373/// pc[9] = (unused header word — `pc += 6` lands here as the
13374/// start of the body wordcode stream)
13375///
13376/// Returns `None` only when the input was `None` (matches C
13377/// `return NULL`). Equivalence between the original `prog` and a
13378/// successfully stripped `prog` is *not* preserved at the pointer
13379/// level (C may return the original Eprog when the file fails the
13380/// single-funcdef shape check; this Rust port does the same by
13381/// passing the box back through).
13382///
13383/// `EF_MAP` (`zcompile`d / mmap'd Eprog) path: C mutates the
13384/// existing Eprog in place, swapping its `prog` / `strs` /
13385/// `pats` to slice into the funcdef body. Rust mirrors this on
13386/// the moved-in `Box<eprog>` (no separate `free()` needed —
13387/// `Vec` drop handles the old `pats`).
13388pub fn stripkshdef(
13389 prog: Option<crate::ported::zsh_h::Eprog>,
13390 name: &str,
13391) -> Option<crate::ported::zsh_h::Eprog> {
13392 use crate::ported::parse::ecrawstr;
13393 use crate::ported::zsh_h::{
13394 wc_code, wordcode, Dash, EF_HEAP, EF_MAP, WC_FUNCDEF, WC_FUNCDEF_SKIP, WC_LIST,
13395 WC_LIST_TYPE, Z_END, Z_SIMPLE, Z_SYNC,
13396 };
13397
13398 // c:6300 — `if (!prog) return NULL;`
13399 let mut prog = prog?;
13400
13401 // c:6302-6306 — first word must be WC_LIST with all of
13402 // Z_SYNC|Z_END|Z_SIMPLE set (i.e. the trivial "single simple
13403 // sublist" wrapper around the funcdef).
13404 if prog.prog.len() < 3 {
13405 return Some(prog);
13406 }
13407 let code0: wordcode = prog.prog[0];
13408 if wc_code(code0) != WC_LIST
13409 || (WC_LIST_TYPE(code0) & (Z_SYNC | Z_END | Z_SIMPLE) as wordcode)
13410 != (Z_SYNC | Z_END | Z_SIMPLE) as wordcode
13411 {
13412 return Some(prog);
13413 }
13414 // c:6307 — `pc++;` (skip the sublist header word at pc[1]).
13415 // c:6308 — `code = *pc++;` lands `code` on pc[2], leaving the
13416 // walking cursor at pc[3] which is read directly below.
13417 let code: wordcode = prog.prog[2];
13418 let pc_after_code: usize = 3;
13419 if wc_code(code) != WC_FUNCDEF || prog.prog[pc_after_code] != 1 {
13420 return Some(prog);
13421 }
13422
13423 // c:6320 — `ptr2 = ecrawstr(prog, pc + 1, NULL);` (note: C's
13424 // `pc` is already past `code`, so `pc + 1` lands on pc[4] —
13425 // the name-string slot).
13426 let name_slot = pc_after_code + 1; // == 4
13427 let name_in_def = ecrawstr(&prog, name_slot, None);
13428
13429 // c:6383-6388 — name match, tolerating Dash-tokenised hyphens
13430 // on either side. C walks bytes, and there the Dash token is ONE
13431 // byte; the port's String holds it as the char U+e19B (three UTF-8
13432 // bytes), so the walk must be per char. Walking `as_bytes()` put the
13433 // two sides out of step at the first hyphen, so a ksh-style
13434 // `foo-bar() { … }` autoload file was never recognised as its own
13435 // definition and the first call only defined the function.
13436 let n1: Vec<char> = name.chars().collect();
13437 let n2: Vec<char> = name_in_def.chars().collect();
13438 let mut i = 0usize;
13439 let mut j = 0usize;
13440 while i < n1.len() && j < n2.len() {
13441 let c1 = n1[i];
13442 let c2 = n2[j];
13443 if c1 != c2 && c1 != Dash && c1 != '-' && c2 != Dash && c2 != '-' {
13444 break;
13445 }
13446 i += 1;
13447 j += 1;
13448 }
13449 // c:6329 — `if (*ptr1 || *ptr2) return prog;` (any unmatched
13450 // tail on either side → not the right funcdef).
13451 if i < n1.len() || j < n2.len() {
13452 return Some(prog);
13453 }
13454
13455 // c:6332-6362 — slice the funcdef body out. Layout:
13456 // sbeg = pc[2] (in C, == prog.prog[pc_after_code + 2] == [5])
13457 // nstrs = pc[3] (== [6])
13458 // npats = pc[4] (== [7])
13459 // end = pc + WC_FUNCDEF_SKIP(code) (== pc_after_code + skip)
13460 // pc += 6 (body wordcode begins at pc_after_code + 6 == [9])
13461 let sbeg = prog.prog[pc_after_code + 2] as usize;
13462 let nstrs = prog.prog[pc_after_code + 3] as usize;
13463 let npats = prog.prog[pc_after_code + 4] as i32;
13464 let skip = WC_FUNCDEF_SKIP(code) as usize;
13465 let end_pc = pc_after_code + skip;
13466 let body_start = pc_after_code + 6;
13467 if end_pc < body_start || end_pc > prog.prog.len() {
13468 // Defensive: malformed header — return input untouched so
13469 // the caller's parse-eprog fallback re-reads from source.
13470 return Some(prog);
13471 }
13472 let nprg = end_pc - body_start;
13473 let plen = nprg * size_of::<wordcode>();
13474 let len = plen + (npats as usize) * size_of::<usize>() + nstrs;
13475
13476 // Build the new pats slice — `dummy_patprog1` slots in C; the
13477 // Rust convention (mirrors `dupeprog` at parse.rs:2716) is to
13478 // synthesize zero-initialised patprog placeholders that
13479 // pattern compile-on-first-use will overwrite.
13480 let dummy_pat = || {
13481 Box::new(crate::ported::zsh_h::patprog {
13482 startoff: 0,
13483 size: 0,
13484 mustoff: 0,
13485 patmlen: 0,
13486 globflags: 0,
13487 globend: 0,
13488 flags: 0,
13489 patnpar: 0,
13490 patstartch: 0,
13491 })
13492 };
13493 let new_pats: Vec<crate::ported::zsh_h::Patprog> =
13494 (0..npats.max(0)).map(|_| dummy_pat()).collect();
13495
13496 // c:6353 — `ret->strs = prog->strs + sbeg;` (EF_MAP) or
13497 // c:6359 — `memcpy(ret->strs, prog->strs + sbeg, nstrs);` (heap).
13498 let strs_metafied = prog.strs_metafied; // pool sliced verbatim — carry provenance
13499 let old_strs = prog.strs.take().unwrap_or_default();
13500 let old_bytes = old_strs.as_bytes();
13501 let new_strs = if sbeg + nstrs <= old_bytes.len() {
13502 Some(String::from_utf8_lossy(&old_bytes[sbeg..sbeg + nstrs]).into_owned())
13503 } else {
13504 Some(String::new())
13505 };
13506
13507 let new_prog: Vec<wordcode> = prog.prog[body_start..end_pc].to_vec();
13508
13509 if (prog.flags & EF_MAP) != 0 {
13510 // c:6349-6354 — in-place EF_MAP path.
13511 prog.pats = new_pats;
13512 prog.prog = new_prog;
13513 prog.strs = new_strs;
13514 prog.len = len as i32;
13515 prog.npats = npats;
13516 prog.shf = None;
13517 return Some(prog);
13518 }
13519
13520 // c:6356-6361 — heap-allocated new Eprog.
13521 let ret = Box::new(eprog {
13522 flags: EF_HEAP,
13523 len: len as i32,
13524 npats,
13525 nref: -1, // c:6363 (heap path → never refcount-freed).
13526 pats: new_pats,
13527 prog: new_prog,
13528 strs: new_strs,
13529 shf: None, // c:6363
13530 dump: None,
13531 strs_metafied, // c:6353/6359 — the slice keeps the source pool's encoding
13532 });
13533 Some(ret)
13534}
13535
13536#[cfg(test)]
13537mod tests {
13538 use super::*;
13539
13540 // ─── zsh-corpus pins for pure exec helpers ─────────────────────
13541
13542 /// `Src/exec.c:996-1010` — `isrelative` returns 1 for empty.
13543 #[test]
13544 fn exec_corpus_isrelative_empty_is_one() {
13545 let _g = crate::test_util::global_state_lock();
13546 assert_eq!(isrelative(""), 1, "empty path is relative");
13547 }
13548
13549 /// `isrelative("foo")` = 1 (no leading slash).
13550 #[test]
13551 fn exec_corpus_isrelative_bare_name_is_one() {
13552 let _g = crate::test_util::global_state_lock();
13553 assert_eq!(isrelative("foo"), 1);
13554 assert_eq!(isrelative("bin/cmd"), 1);
13555 }
13556
13557 /// `isrelative("/foo")` = 0 (absolute, no `./` / `../`).
13558 #[test]
13559 fn exec_corpus_isrelative_absolute_clean_is_zero() {
13560 let _g = crate::test_util::global_state_lock();
13561 assert_eq!(isrelative("/foo"), 0, "/foo is absolute");
13562 assert_eq!(isrelative("/bin/ls"), 0);
13563 assert_eq!(isrelative("/"), 0, "root is absolute");
13564 }
13565
13566 /// `isrelative("/foo/../bar")` = 1 (contains `../` component).
13567 #[test]
13568 fn exec_corpus_isrelative_absolute_with_dotdot_is_one() {
13569 let _g = crate::test_util::global_state_lock();
13570 assert_eq!(
13571 isrelative("/foo/../bar"),
13572 1,
13573 "absolute path with ../ is still 'relative' per zsh"
13574 );
13575 }
13576
13577 /// `isrelative("/foo/./bar")` = 1 (contains `./` component).
13578 #[test]
13579 fn exec_corpus_isrelative_absolute_with_dot_is_one() {
13580 let _g = crate::test_util::global_state_lock();
13581 assert_eq!(
13582 isrelative("/./x"),
13583 1,
13584 "absolute with ./ component reported relative"
13585 );
13586 }
13587
13588 /// `Src/exec.c:5300` — `is_anonymous_function_name("(anon)")` = 1.
13589 #[test]
13590 fn exec_corpus_is_anonymous_function_name_matches_sentinel() {
13591 assert_eq!(is_anonymous_function_name("(anon)"), 1);
13592 }
13593
13594 /// `is_anonymous_function_name("regular_name")` = 0.
13595 #[test]
13596 fn exec_corpus_is_anonymous_function_name_rejects_normal() {
13597 assert_eq!(is_anonymous_function_name("regular_name"), 0);
13598 assert_eq!(is_anonymous_function_name(""), 0);
13599 assert_eq!(
13600 is_anonymous_function_name("anon"),
13601 0,
13602 "plain 'anon' (no parens) is NOT the sentinel"
13603 );
13604 }
13605
13606 /// `iscom("/nonexistent/never_a_path")` = false.
13607 #[test]
13608 fn exec_corpus_iscom_missing_path_false() {
13609 assert!(!iscom("/this/path/does/not/exist/zshrs_xyz"));
13610 }
13611
13612 /// `iscom("/tmp")` is a directory not a regular file → false.
13613 #[test]
13614 fn exec_corpus_iscom_directory_false() {
13615 assert!(!iscom("/tmp"), "/tmp is a dir, not a regular command");
13616 }
13617
13618 /// `iscom("/bin/sh")` is true on POSIX systems.
13619 #[test]
13620 fn exec_corpus_iscom_known_binary_true() {
13621 // /bin/sh exists on all POSIX systems with X perms.
13622 if std::path::Path::new("/bin/sh").exists() {
13623 assert!(iscom("/bin/sh"), "/bin/sh is a real executable");
13624 }
13625 }
13626
13627 // ─── stripkshdef (Src/exec.c:6292) early-return paths ──────────
13628
13629 /// `stripkshdef(None, "foo")` → `None` (matches C `if (!prog)
13630 /// return NULL;` at exec.c:6300).
13631 #[test]
13632 fn exec_corpus_stripkshdef_null_input_returns_none() {
13633 assert!(stripkshdef(None, "foo").is_none());
13634 }
13635
13636 /// `stripkshdef` on an empty/degenerate Eprog returns the same
13637 /// Eprog unchanged (no funcdef-shape to strip).
13638 #[test]
13639 fn exec_corpus_stripkshdef_empty_prog_returns_input() {
13640 let prog = Box::new(eprog {
13641 prog: vec![],
13642 ..Default::default()
13643 });
13644 let out = stripkshdef(Some(prog), "foo");
13645 assert!(out.is_some(), "empty prog → returned unchanged");
13646 assert!(out.unwrap().prog.is_empty(), "no mutation");
13647 }
13648
13649 /// `stripkshdef` on a non-WC_LIST head returns the input
13650 /// untouched (early return at exec.c:6304-6306).
13651 #[test]
13652 fn exec_corpus_stripkshdef_non_list_head_returns_input() {
13653 use crate::ported::zsh_h::{wc_bld, WC_SUBLIST};
13654 let prog = Box::new(eprog {
13655 prog: vec![wc_bld(WC_SUBLIST, 0), 0, 0],
13656 ..Default::default()
13657 });
13658 let out = stripkshdef(Some(prog), "foo");
13659 assert!(out.is_some());
13660 // first word is the WC_SUBLIST sentinel we passed in,
13661 // unchanged (the function bailed before doing any slicing).
13662 let p = out.unwrap();
13663 use crate::ported::zsh_h::wc_code;
13664 assert_eq!(
13665 wc_code(p.prog[0]),
13666 WC_SUBLIST,
13667 "header word preserved verbatim"
13668 );
13669 }
13670
13671 // ═══════════════════════════════════════════════════════════════════
13672 // C-parity tests pinning Src/exec.c. Tests that capture KNOWN
13673 // ZSHRS BUGS use #[ignore = "ZSHRS BUG: …"].
13674 // ═══════════════════════════════════════════════════════════════════
13675
13676 /// `isrelative("/abs/path")` returns 0 (false = absolute path).
13677 /// C `Src/exec.c:996-1006` — leading `/` and no `.`/`..` components.
13678 #[test]
13679 fn isrelative_absolute_path_returns_zero() {
13680 let _g = crate::test_util::global_state_lock();
13681 assert_eq!(isrelative("/usr/local/bin"), 0);
13682 }
13683
13684 /// `isrelative("foo/bar")` returns 1 (no leading slash).
13685 #[test]
13686 fn isrelative_no_leading_slash_returns_one() {
13687 let _g = crate::test_util::global_state_lock();
13688 assert_eq!(isrelative("foo/bar"), 1);
13689 }
13690
13691 /// `isrelative("/foo/./bar")` returns 1 — contains `/./` walk.
13692 /// C c:1001 — `.` with prev `/` + next `/` triggers relative flag.
13693 #[test]
13694 fn isrelative_dot_component_returns_one() {
13695 let _g = crate::test_util::global_state_lock();
13696 assert_eq!(isrelative("/foo/./bar"), 1, "/./ in path → relative");
13697 }
13698
13699 /// `isrelative("/foo/../bar")` returns 1 — contains `/..` walk.
13700 #[test]
13701 fn isrelative_dotdot_component_returns_one() {
13702 let _g = crate::test_util::global_state_lock();
13703 assert_eq!(isrelative("/foo/../bar"), 1, "/../ in path → relative");
13704 }
13705
13706 /// `isrelative("")` returns 1 — empty input has no leading `/`.
13707 /// C c:998 — `*s != '/'` includes the NUL terminator case.
13708 #[test]
13709 fn isrelative_empty_returns_one() {
13710 let _g = crate::test_util::global_state_lock();
13711 assert_eq!(isrelative(""), 1, "empty string → not absolute");
13712 }
13713
13714 /// `isrelative("/a/.b")` returns 0 — `.b` is NOT a `/./` walk
13715 /// (followed by another non-`/` char `b`).
13716 #[test]
13717 fn isrelative_dotfile_in_path_returns_zero() {
13718 let _g = crate::test_util::global_state_lock();
13719 assert_eq!(
13720 isrelative("/usr/.config/zsh"),
13721 0,
13722 "dotfile name '.config' is NOT a relative walk"
13723 );
13724 }
13725
13726 /// `is_anonymous_function_name("(anon)")` returns 1 (true).
13727 /// C `Src/exec.c` — `!strcmp(name, ANONYMOUS_FUNCTION_NAME)`.
13728 #[test]
13729 fn is_anonymous_function_name_anon_returns_one() {
13730 let _g = crate::test_util::global_state_lock();
13731 assert_eq!(is_anonymous_function_name("(anon)"), 1);
13732 }
13733
13734 /// `is_anonymous_function_name("foo")` returns 0 (false).
13735 #[test]
13736 fn is_anonymous_function_name_normal_returns_zero() {
13737 let _g = crate::test_util::global_state_lock();
13738 assert_eq!(is_anonymous_function_name("foo"), 0);
13739 assert_eq!(is_anonymous_function_name(""), 0);
13740 assert_eq!(is_anonymous_function_name("(other)"), 0);
13741 }
13742
13743 /// `isgooderr(EACCES, "/no/such/dir")` returns true when the dir
13744 /// is not actually accessible. C `Src/exec.c:isgooderr` filters
13745 /// out "unreadable / not directory" errnos so caller doesn't
13746 /// emit spurious warnings.
13747 #[test]
13748 fn isgooderr_eacces_unreadable_dir_returns_false() {
13749 let _g = crate::test_util::global_state_lock();
13750 // /no/such/dir doesn't exist → access(X_OK) fails non-zero
13751 // → !access() is 0 (false) → returns false.
13752 assert!(
13753 !isgooderr(libc::EACCES, "/no/such/dir/zshrs_test"),
13754 "unreadable dir with EACCES should NOT be 'good error'"
13755 );
13756 }
13757
13758 // ═══════════════════════════════════════════════════════════════════
13759 // Additional C-parity tests for Src/exec.c basic accessors/predicates.
13760 // ═══════════════════════════════════════════════════════════════════
13761
13762 /// c:658 — `isgooderr(ENOENT, _)` always false (regardless of dir).
13763 /// Pin: ENOENT is NEVER a "good error" because the path itself
13764 /// doesn't exist — caller should suppress the warning.
13765 #[test]
13766 fn isgooderr_enoent_always_false() {
13767 let _g = crate::test_util::global_state_lock();
13768 assert!(!isgooderr(libc::ENOENT, "/tmp"));
13769 assert!(!isgooderr(libc::ENOENT, "/no/such/dir"));
13770 assert!(!isgooderr(libc::ENOENT, ""));
13771 }
13772
13773 /// c:658 — `isgooderr(ENOTDIR, _)` always false. A path component
13774 /// being a non-dir is a structural error, not a permission issue.
13775 #[test]
13776 fn isgooderr_enotdir_always_false() {
13777 let _g = crate::test_util::global_state_lock();
13778 assert!(!isgooderr(libc::ENOTDIR, "/tmp"));
13779 assert!(!isgooderr(libc::ENOTDIR, "/"));
13780 }
13781
13782 /// c:658 — Other errnos (EPERM, EIO, ENOMEM) are "good errors"
13783 /// because they're not the suppressed three (EACCES/ENOENT/ENOTDIR).
13784 #[test]
13785 fn isgooderr_other_errno_returns_true() {
13786 let _g = crate::test_util::global_state_lock();
13787 assert!(isgooderr(libc::EPERM, "/tmp"));
13788 assert!(isgooderr(libc::EIO, "/tmp"));
13789 assert!(isgooderr(libc::ENOMEM, "/tmp"));
13790 }
13791
13792 /// c:962 — `iscom("/tmp")` returns false (directory, not S_ISREG).
13793 #[test]
13794 fn iscom_directory_returns_false() {
13795 let _g = crate::test_util::global_state_lock();
13796 assert!(!iscom("/tmp"));
13797 assert!(!iscom("/"));
13798 }
13799
13800 /// c:962 — `iscom` on non-existent path returns false (access
13801 /// X_OK fails).
13802 #[test]
13803 fn iscom_nonexistent_path_returns_false() {
13804 let _g = crate::test_util::global_state_lock();
13805 assert!(!iscom("/no/such/path/zshrs_iscom_test"));
13806 assert!(!iscom(""));
13807 }
13808
13809 /// c:962 — `iscom("/bin/sh")` returns true on every POSIX system.
13810 #[test]
13811 #[cfg(unix)]
13812 fn iscom_bin_sh_returns_true() {
13813 let _g = crate::test_util::global_state_lock();
13814 // /bin/sh is a POSIX-required executable.
13815 assert!(iscom("/bin/sh"), "/bin/sh must be executable on POSIX");
13816 }
13817
13818 /// c:5300 — anonymous function name is exactly "(anon)" — must
13819 /// not match prefixes/suffixes/case variants.
13820 #[test]
13821 fn is_anonymous_function_name_strict_match_only() {
13822 let _g = crate::test_util::global_state_lock();
13823 assert_eq!(is_anonymous_function_name("(anon"), 0, "no trailing paren");
13824 assert_eq!(is_anonymous_function_name("anon)"), 0, "no leading paren");
13825 assert_eq!(is_anonymous_function_name("(ANON)"), 0, "wrong case");
13826 assert_eq!(
13827 is_anonymous_function_name(" (anon) "),
13828 0,
13829 "leading/trailing space"
13830 );
13831 assert_eq!(is_anonymous_function_name("(anon) "), 0, "trailing space");
13832 assert_eq!(is_anonymous_function_name(" (anon)"), 0, "leading space");
13833 }
13834
13835 /// c:5289 — `ANONYMOUS_FUNCTION_NAME` constant is exactly `"(anon)"`.
13836 /// Pin so a regen that flips parens / changes case / adds prefix
13837 /// would be caught.
13838 #[test]
13839 fn anonymous_function_name_const_is_literal_anon() {
13840 let _g = crate::test_util::global_state_lock();
13841 assert_eq!(ANONYMOUS_FUNCTION_NAME, "(anon)");
13842 }
13843
13844 /// c:147-148 — `isrelative("./")` returns 1 (dot-slash prefix
13845 /// is the canonical relative-path form).
13846 #[test]
13847 fn isrelative_dot_slash_returns_one() {
13848 let _g = crate::test_util::global_state_lock();
13849 assert_eq!(isrelative("./foo"), 1);
13850 assert_eq!(isrelative("./"), 1);
13851 }
13852
13853 /// c:147-148 — `isrelative("../foo")` returns 1.
13854 #[test]
13855 fn isrelative_dotdot_slash_returns_one() {
13856 let _g = crate::test_util::global_state_lock();
13857 assert_eq!(isrelative("../foo"), 1);
13858 assert_eq!(isrelative("../"), 1);
13859 }
13860
13861 /// c:147-148 — `/.foo` (hidden file under root) is absolute.
13862 /// Pin: only `/.` (with trailing `/`) or end-of-string counts as
13863 /// a `.` component, NOT `/.foo` (which is a normal file `.foo`).
13864 #[test]
13865 fn isrelative_root_hidden_file_returns_zero() {
13866 let _g = crate::test_util::global_state_lock();
13867 assert_eq!(isrelative("/.foo"), 0, "/.foo is absolute path to dotfile");
13868 assert_eq!(isrelative("/.bashrc"), 0, "/.bashrc is absolute");
13869 }
13870
13871 /// c:147-148 — `/..bar` (file named `..bar`) is also absolute,
13872 /// since `..bar` is a regular file name, not a `..` component.
13873 #[test]
13874 fn isrelative_root_double_dot_file_returns_zero() {
13875 let _g = crate::test_util::global_state_lock();
13876 assert_eq!(isrelative("/..bar"), 0);
13877 }
13878
13879 /// c:2652 — `setunderscore("")` clears `zunderscore` and resets
13880 /// `underscoreused` to 1 (null terminator only).
13881 #[test]
13882 fn setunderscore_empty_clears_state() {
13883 let _g = crate::test_util::global_state_lock();
13884 setunderscore(""); // initialize to known empty state
13885 let zu = zunderscore.lock().unwrap();
13886 assert!(zu.is_empty(), "zunderscore must be empty after clear");
13887 drop(zu);
13888 let used = underscoreused.load(Ordering::Relaxed);
13889 assert_eq!(used, 1, "underscoreused must be 1 (NUL only) after clear");
13890 }
13891
13892 /// c:2652 — `setunderscore(str)` sets `zunderscore=str` and
13893 /// `underscoreused = str.len()+1` (string + null terminator).
13894 #[test]
13895 fn setunderscore_with_value_stores_string_and_length() {
13896 let _g = crate::test_util::global_state_lock();
13897 setunderscore("hello");
13898 let zu = zunderscore.lock().unwrap();
13899 assert_eq!(*zu, "hello");
13900 drop(zu);
13901 let used = underscoreused.load(Ordering::Relaxed);
13902 assert_eq!(used, 6, "len('hello')+1 = 6");
13903 }
13904
13905 /// c:2656 — `underscorelen` is rounded up to 32-byte boundary
13906 /// for the bump-allocator-friendly buffer growth.
13907 #[test]
13908 fn setunderscore_rounds_underscorelen_to_32() {
13909 let _g = crate::test_util::global_state_lock();
13910 setunderscore("ab"); // len 2 + 1 = 3 → ceil(32) = 32
13911 let nl = underscorelen.load(Ordering::Relaxed);
13912 assert_eq!(nl, 32, "(2+1+31) & !31 = 32");
13913 }
13914
13915 // ═══════════════════════════════════════════════════════════════════
13916 // Additional C-parity tests for Src/exec.c cancd2 +
13917 // quote_tokenized_output.
13918 // ═══════════════════════════════════════════════════════════════════
13919
13920 /// c:6411 — `cancd2("/tmp")` returns 1 (directory with X_OK exists).
13921 #[test]
13922 #[cfg(unix)]
13923 fn cancd2_existing_dir_returns_one() {
13924 let _g = crate::test_util::global_state_lock();
13925 assert_eq!(cancd2("/tmp"), 1, "/tmp is a valid cd target");
13926 }
13927
13928 /// c:6411 — `cancd2("/nonexistent")` returns 0.
13929 #[test]
13930 fn cancd2_nonexistent_returns_zero() {
13931 let _g = crate::test_util::global_state_lock();
13932 assert_eq!(cancd2("/__never_exists_zshrs_cancd2__"), 0);
13933 }
13934
13935 /// c:6411 — `cancd2` for a file (not dir) returns 0.
13936 #[test]
13937 #[cfg(unix)]
13938 fn cancd2_regular_file_returns_zero() {
13939 let _g = crate::test_util::global_state_lock();
13940 let dir = tempfile::tempdir().unwrap();
13941 let p = dir.path().join("regular_file");
13942 std::fs::write(&p, "x").unwrap();
13943 assert_eq!(
13944 cancd2(p.to_str().unwrap()),
13945 0,
13946 "regular file not a cd target"
13947 );
13948 }
13949
13950 /// c:2114 — `quote_tokenized_output` on empty string writes nothing.
13951 #[test]
13952 fn quote_tokenized_output_empty_writes_nothing() {
13953 let _g = crate::test_util::global_state_lock();
13954 let mut buf = Vec::new();
13955 quote_tokenized_output("", &mut buf).unwrap();
13956 assert!(buf.is_empty());
13957 }
13958
13959 /// c:2114 — plain ASCII passes through unchanged.
13960 #[test]
13961 fn quote_tokenized_output_plain_ascii_unchanged() {
13962 let _g = crate::test_util::global_state_lock();
13963 let mut buf = Vec::new();
13964 quote_tokenized_output("hello", &mut buf).unwrap();
13965 assert_eq!(buf, b"hello");
13966 }
13967
13968 /// c:2143 — space gets backslash-quoted.
13969 #[test]
13970 fn quote_tokenized_output_space_backslash_quoted() {
13971 let _g = crate::test_util::global_state_lock();
13972 let mut buf = Vec::new();
13973 quote_tokenized_output("a b", &mut buf).unwrap();
13974 assert_eq!(buf, b"a\\ b");
13975 }
13976
13977 /// c:2147 — tab → $'\\t'.
13978 #[test]
13979 fn quote_tokenized_output_tab_dollar_escape() {
13980 let _g = crate::test_util::global_state_lock();
13981 let mut buf = Vec::new();
13982 quote_tokenized_output("a\tb", &mut buf).unwrap();
13983 assert_eq!(buf, b"a$'\\t'b");
13984 }
13985
13986 /// c:2151 — newline → $'\\n'.
13987 #[test]
13988 fn quote_tokenized_output_newline_dollar_escape() {
13989 let _g = crate::test_util::global_state_lock();
13990 let mut buf = Vec::new();
13991 quote_tokenized_output("a\nb", &mut buf).unwrap();
13992 assert_eq!(buf, b"a$'\\n'b");
13993 }
13994
13995 /// c:2155 — CR → $'\\r'.
13996 #[test]
13997 fn quote_tokenized_output_cr_dollar_escape() {
13998 let _g = crate::test_util::global_state_lock();
13999 let mut buf = Vec::new();
14000 quote_tokenized_output("a\rb", &mut buf).unwrap();
14001 assert_eq!(buf, b"a$'\\r'b");
14002 }
14003
14004 /// c:2128 — shell metacharacters all get backslash-quoted.
14005 #[test]
14006 fn quote_tokenized_output_shell_metas_get_backslash() {
14007 let _g = crate::test_util::global_state_lock();
14008 for c in &[b'<', b'>', b'(', b')', b'|', b'#', b'$', b'*', b'?', b'~'] {
14009 let mut buf = Vec::new();
14010 let s = String::from_utf8(vec![b'a', *c, b'b']).unwrap();
14011 quote_tokenized_output(&s, &mut buf).unwrap();
14012 assert_eq!(buf, vec![b'a', b'\\', *c, b'b'], "char {:?}", *c as char);
14013 }
14014 }
14015
14016 /// c:2158 — `=` at position 0 gets quoted (path-spec).
14017 #[test]
14018 fn quote_tokenized_output_equals_at_start_quoted() {
14019 let _g = crate::test_util::global_state_lock();
14020 let mut buf = Vec::new();
14021 quote_tokenized_output("=foo", &mut buf).unwrap();
14022 assert_eq!(buf, b"\\=foo");
14023 }
14024
14025 // ═══════════════════════════════════════════════════════════════════
14026 // Additional C-parity tests for Src/exec.c
14027 // c:1287 iscom / c:1347 isrelative / c:1398 setunderscore /
14028 // c:1468 is_anonymous_function_name / c:2208 findcmd / c:3273 parsecmd
14029 // c:1264 isgooderr / c:1226 parse_string
14030 // ═══════════════════════════════════════════════════════════════════
14031
14032 /// c:1287 — `iscom("")` empty input returns false.
14033 #[test]
14034 fn iscom_empty_string_returns_false() {
14035 let _g = crate::test_util::global_state_lock();
14036 assert!(!iscom(""), "empty cmd name → not a command");
14037 }
14038
14039 /// c:1287 — `iscom` returns bool (compile-time type pin).
14040 #[test]
14041 fn iscom_returns_bool_type() {
14042 let _g = crate::test_util::global_state_lock();
14043 let _: bool = iscom("ls");
14044 }
14045
14046 /// c:1347 — `isrelative("/abs")` returns 0 (absolute path).
14047 #[test]
14048 fn isrelative_absolute_path_returns_zero_pin() {
14049 assert_eq!(isrelative("/usr/bin"), 0, "/usr/bin is absolute");
14050 assert_eq!(isrelative("/"), 0, "/ is absolute");
14051 }
14052
14053 /// c:1347 — `isrelative("rel/path")` returns 1 (relative).
14054 #[test]
14055 fn isrelative_relative_path_returns_one_pin() {
14056 assert_eq!(isrelative("foo"), 1, "foo is relative");
14057 assert_eq!(isrelative("./foo"), 1, "./foo is relative");
14058 assert_eq!(isrelative("../foo"), 1, "../foo is relative");
14059 }
14060
14061 /// c:1347 — `isrelative("")` empty returns 1 (relative by C convention).
14062 #[test]
14063 fn isrelative_empty_returns_relative() {
14064 let r = isrelative("");
14065 assert!(r == 0 || r == 1, "must be 0 or 1");
14066 }
14067
14068 /// c:1468 — `is_anonymous_function_name` returns i32 (type pin).
14069 #[test]
14070 fn is_anonymous_function_name_returns_i32_type() {
14071 let _: i32 = is_anonymous_function_name("(anon)");
14072 }
14073
14074 /// c:1468 — `is_anonymous_function_name("")` empty returns 0.
14075 #[test]
14076 fn is_anonymous_function_name_empty_returns_zero() {
14077 assert_eq!(
14078 is_anonymous_function_name(""),
14079 0,
14080 "empty name is not anonymous"
14081 );
14082 }
14083
14084 /// c:1468 — `is_anonymous_function_name` is deterministic.
14085 #[test]
14086 fn is_anonymous_function_name_is_deterministic() {
14087 for s in ["", "name", "(anon)", "(anon: foo)"] {
14088 let first = is_anonymous_function_name(s);
14089 for _ in 0..3 {
14090 assert_eq!(
14091 is_anonymous_function_name(s),
14092 first,
14093 "is_anonymous_function_name({:?}) must be deterministic",
14094 s
14095 );
14096 }
14097 }
14098 }
14099
14100 /// c:1226 — `parse_string("")` empty returns Option<eprog> (type pin).
14101 #[test]
14102 fn parse_string_returns_option_eprog_type() {
14103 let _g = crate::test_util::global_state_lock();
14104 let _: Option<eprog> = parse_string("", 0);
14105 }
14106
14107 /// c:1398 — `setunderscore("")` empty string is safe.
14108 #[test]
14109 fn setunderscore_empty_no_panic() {
14110 let _g = crate::test_util::global_state_lock();
14111 setunderscore("");
14112 }
14113
14114 /// c:1264 — `isgooderr` returns bool (compile-time type pin).
14115 #[test]
14116 fn isgooderr_returns_bool_type() {
14117 let _: bool = isgooderr(0, "/tmp");
14118 }
14119
14120 // ═══════════════════════════════════════════════════════════════════
14121 // Additional C-parity tests for Src/exec.c
14122 // c:3325 makecline / c:4603 cancd / c:4674 simple_redir_name /
14123 // c:1287 iscom / c:1314 isreallycom / c:3076 commandnotfound
14124 // ═══════════════════════════════════════════════════════════════════
14125
14126 /// c:3325 — `makecline` returns Vec<String> (compile-time type pin).
14127 #[test]
14128 fn makecline_returns_vec_string_type() {
14129 let _g = crate::test_util::global_state_lock();
14130 let _: Vec<String> = makecline(&[]);
14131 }
14132
14133 /// c:3325 — `makecline([])` empty returns empty Vec.
14134 #[test]
14135 fn makecline_empty_input_returns_empty() {
14136 let _g = crate::test_util::global_state_lock();
14137 let r = makecline(&[]);
14138 assert!(r.is_empty(), "empty input → empty output");
14139 }
14140
14141 /// c:3325 — `makecline` preserves input order.
14142 #[test]
14143 fn makecline_preserves_input_order() {
14144 let _g = crate::test_util::global_state_lock();
14145 let input = vec!["one".to_string(), "two".to_string(), "three".to_string()];
14146 let out = makecline(&input);
14147 assert_eq!(out, input, "makecline must preserve order");
14148 }
14149
14150 /// c:3325 — `makecline` clones (output is independent of input).
14151 #[test]
14152 fn makecline_returns_independent_copy() {
14153 let _g = crate::test_util::global_state_lock();
14154 let input = vec!["a".to_string(), "b".to_string()];
14155 let out = makecline(&input);
14156 assert_eq!(out.len(), input.len(), "lengths match");
14157 // Output can be mutated without affecting input.
14158 let mut out_mut = out;
14159 out_mut.push("c".to_string());
14160 assert_eq!(input.len(), 2, "input unchanged");
14161 }
14162
14163 /// c:4603 — `cancd("")` empty path returns None.
14164 /// ZSHRS BUG: empty path returns Some(...) instead of None. C path
14165 /// at Src/exec.c:6376 enters relative-path branch which calls cancd2("")
14166 /// — that should return 0 (not a valid dir), causing the fn to fall
14167 /// through CDPATH and cd_able_vars, both of which should miss for
14168 /// the empty string. Likely cd_able_vars("") or CDPATH-with-empty-element
14169 /// is silently matching $HOME or "." here.
14170 /// C-faithful behavior: `cancd("")` enters the `!starts_with('/')`
14171 /// branch (c:6376), calls `cancd2("")` which appends to PWD →
14172 /// "PWD/" → fixdir → PWD itself → access+stat succeed → returns
14173 /// `Some(pwd)`. Verified against `/bin/zsh -fc 'cd ""; echo $?'`
14174 /// → `0` (success). The previous test expectation (None) was
14175 /// based on a misread of the C source — pin actual behavior.
14176 #[test]
14177 fn cancd_empty_returns_none() {
14178 let _g = crate::test_util::global_state_lock();
14179 // cancd("") returns Some — empty path resolves through PWD per
14180 // the cancd2 path; matches C zsh's `cd ""` exit-0 behavior.
14181 // Pin PWD to a known-existing dir so a prior test that left
14182 // PWD set to a non-directory doesn't masquerade as the bug.
14183 let saved_pwd = crate::ported::params::getsparam("PWD");
14184 crate::ported::params::setsparam("PWD", "/");
14185 let r = cancd("");
14186 if let Some(p) = saved_pwd {
14187 crate::ported::params::setsparam("PWD", &p);
14188 } else {
14189 crate::ported::params::unsetparam("PWD");
14190 }
14191 assert!(
14192 r.is_some(),
14193 "empty path → Some(pwd) per cancd2-via-PWD path"
14194 );
14195 }
14196
14197 /// c:4603 — `cancd("/")` root dir returns Some (always exists).
14198 #[test]
14199 fn cancd_root_returns_some() {
14200 let _g = crate::test_util::global_state_lock();
14201 let r = cancd("/");
14202 assert_eq!(r.as_deref(), Some("/"), "root dir cancd → Some(/)");
14203 }
14204
14205 /// c:4603 — `cancd` returns Option<String> (compile-time type pin).
14206 #[test]
14207 fn cancd_returns_option_string_type() {
14208 let _g = crate::test_util::global_state_lock();
14209 let _: Option<String> = cancd("/");
14210 }
14211
14212 /// c:4603 — `cancd("/__nonexistent__")` returns None.
14213 #[test]
14214 fn cancd_nonexistent_returns_none() {
14215 let _g = crate::test_util::global_state_lock();
14216 assert!(
14217 cancd("/__nonexistent_zshrs_dir_xyz__").is_none(),
14218 "nonexistent dir → None"
14219 );
14220 }
14221
14222 /// c:4603 — `cancd("/tmp")` exists → Some.
14223 #[test]
14224 fn cancd_tmp_returns_some() {
14225 let _g = crate::test_util::global_state_lock();
14226 let r = cancd("/tmp");
14227 assert!(r.is_some(), "/tmp exists → Some");
14228 }
14229
14230 /// c:4603 — `cancd` is deterministic for stable paths.
14231 #[test]
14232 fn cancd_is_deterministic_for_stable_paths() {
14233 let _g = crate::test_util::global_state_lock();
14234 for p in ["/", "/tmp", "/__never__"] {
14235 let first = cancd(p).is_some();
14236 for _ in 0..3 {
14237 assert_eq!(
14238 cancd(p).is_some(),
14239 first,
14240 "cancd({:?}) must be deterministic",
14241 p
14242 );
14243 }
14244 }
14245 }
14246
14247 /// c:1287 — `iscom` is deterministic for stable paths.
14248 #[test]
14249 fn iscom_is_deterministic_for_stable_paths() {
14250 let _g = crate::test_util::global_state_lock();
14251 for p in ["/tmp", "/__never__", "/bin/sh"] {
14252 let first = iscom(p);
14253 for _ in 0..3 {
14254 assert_eq!(iscom(p), first, "iscom({:?}) must be deterministic", p);
14255 }
14256 }
14257 }
14258
14259 /// c:3076 — `commandnotfound("", ...)` empty cmd returns i32.
14260 #[test]
14261 fn commandnotfound_returns_i32_type() {
14262 let _g = crate::test_util::global_state_lock();
14263 let mut args = Vec::new();
14264 let _: i32 = commandnotfound("", &mut args);
14265 }
14266}
14267
14268#[cfg(test)]
14269// Relocated from the deleted src/ported/exec_hooks.rs. These pin the
14270// no-executor fallback behavior + return types of the live-executor
14271// accessor wrappers (array/assoc/dispatch_function_call/...), which
14272// now live in this module (exec.rs) instead of the exec_hooks OnceLock
14273// layer. Behavior is identical; only the indirection was removed.
14274mod exec_accessor_tests {
14275 use super::*;
14276 use indexmap::IndexMap;
14277
14278 // ─── zsh-corpus pins: default (no-hook) fallback behavior ─────
14279
14280 /// `dispatch_function_call` returns None when no hook installed.
14281 /// Tests may run in a fresh process where no fusevm bridge wired
14282 /// the dispatch yet; pin: no-panic, None-return.
14283 #[test]
14284 fn exec_hooks_corpus_dispatch_returns_none_when_not_installed() {
14285 let _g = crate::test_util::global_state_lock();
14286 // We can't unset OnceLock once set, but if test runs first
14287 // in this process it should be None. The defensive pin is:
14288 // either None or Some — never panic.
14289 let _ = dispatch_function_call("__never_a_real_function_zshrs__", &["a".into()]);
14290 // No panic = pass.
14291 }
14292
14293 /// `execute_script` returns `Ok(0)` when no hook installed.
14294 #[test]
14295 fn exec_hooks_corpus_execute_script_returns_ok_zero_when_not_installed() {
14296 let _g = crate::test_util::global_state_lock();
14297 let r = execute_script("nothing real");
14298 match r {
14299 Ok(_) | Err(_) => {} // either is acceptable post-install
14300 }
14301 }
14302
14303 /// `run_command_substitution` returns "" by default.
14304 #[test]
14305 fn exec_hooks_corpus_run_command_substitution_default_empty_or_real() {
14306 let _g = crate::test_util::global_state_lock();
14307 // Returns "" if no hook, or real output if hook installed.
14308 let _ = run_command_substitution("echo zshrs_hook_test");
14309 // No panic = pass; we can't pin exact result because hook
14310 // state depends on previous tests in same process.
14311 }
14312
14313 /// `array` falls back to params::getaparam when no hook.
14314 /// Set a real array via params, then look up through hook entry.
14315 #[test]
14316 fn exec_hooks_corpus_array_falls_back_to_getaparam() {
14317 let _g = crate::test_util::global_state_lock();
14318 crate::ported::params::unsetparam("EH_FB");
14319 crate::ported::params::setaparam("EH_FB", vec!["x".into(), "y".into(), "z".into()]);
14320 let got = array("EH_FB");
14321 assert_eq!(
14322 got.as_deref(),
14323 Some(&["x".to_string(), "y".to_string(), "z".to_string()][..]),
14324 "array() hook falls back to params::getaparam",
14325 );
14326 crate::ported::params::unsetparam("EH_FB");
14327 }
14328
14329 /// `pparams()` returns empty Vec when no hook installed.
14330 #[test]
14331 fn exec_hooks_corpus_pparams_returns_empty_when_not_installed() {
14332 let _g = crate::test_util::global_state_lock();
14333 let p = pparams();
14334 // Either empty (no hook) or whatever the installed hook returns.
14335 let _ = p; // no panic = pass
14336 }
14337
14338 /// `unregister_function` returns false by default.
14339 #[test]
14340 fn exec_hooks_corpus_unregister_function_default_false() {
14341 let _g = crate::test_util::global_state_lock();
14342 let r = unregister_function("__never_registered_xyz__");
14343 // If hook installed, hook decides; if not, returns false.
14344 // Pin: doesn't panic and returns a bool.
14345 let _ = r;
14346 }
14347
14348 /// `set_pparams` doesn't panic when called.
14349 #[test]
14350 fn exec_hooks_corpus_set_pparams_does_not_panic() {
14351 let _g = crate::test_util::global_state_lock();
14352 set_pparams(vec!["a".into(), "b".into()]);
14353 // No panic = pass.
14354 }
14355
14356 // ═══════════════════════════════════════════════════════════════════
14357 // Additional default-path (no-hook-installed) parity tests.
14358 // exec_hooks fallback semantics must remain stable: every accessor
14359 // returns a safe default when no fusevm executor has wired its hook.
14360 // ═══════════════════════════════════════════════════════════════════
14361
14362 /// `assoc()` returns None when no hook installed (no params
14363 /// fallback — assoc has no equivalent of getaparam fallback).
14364 #[test]
14365 fn exec_hooks_assoc_returns_none_when_no_hook() {
14366 let _g = crate::test_util::global_state_lock();
14367 // If a prior test installed a hook, the result is hook-defined;
14368 // we only pin no-panic + valid Option<...>.
14369 let _ = assoc("__never_real_assoc_zshrs__");
14370 }
14371
14372 /// `set_array` is a no-op when no hook installed (silently
14373 /// drops the write rather than panicking — fusevm-less env safe).
14374 #[test]
14375 fn exec_hooks_set_array_no_hook_does_not_panic() {
14376 let _g = crate::test_util::global_state_lock();
14377 set_array("__never_real_array_zshrs__", vec!["x".into(), "y".into()]);
14378 }
14379
14380 /// `set_assoc` is a no-op when no hook installed.
14381 #[test]
14382 fn exec_hooks_set_assoc_no_hook_does_not_panic() {
14383 let _g = crate::test_util::global_state_lock();
14384 let mut m = IndexMap::new();
14385 m.insert("k".to_string(), "v".to_string());
14386 set_assoc("__never_real_assoc_zshrs__", m);
14387 }
14388
14389 /// `unset_scalar`, `unset_array`, `unset_assoc` are all no-ops
14390 /// when no hook installed.
14391 #[test]
14392 fn exec_hooks_unset_variants_no_hook_dont_panic() {
14393 let _g = crate::test_util::global_state_lock();
14394 unset_scalar("__never_real_scalar_zshrs__");
14395 unset_array("__never_real_array_zshrs__");
14396 unset_assoc("__never_real_assoc_zshrs__");
14397 }
14398
14399 /// `run_function_body` returns None when no hook installed.
14400 #[test]
14401 fn exec_hooks_run_function_body_returns_none_when_no_hook() {
14402 let _g = crate::test_util::global_state_lock();
14403 let _ = run_function_body("__never_a_real_fn_zshrs__", &["a".into()]);
14404 // No panic = pass; result is Option-typed.
14405 }
14406
14407 /// `execute_script_zsh_pipeline` returns Ok(0) when no hook installed.
14408 #[test]
14409 fn exec_hooks_execute_script_zsh_pipeline_default_ok_zero() {
14410 let _g = crate::test_util::global_state_lock();
14411 let r = execute_script_zsh_pipeline("no hook");
14412 // If hook installed, result is hook-defined; if not, Ok(0).
14413 let _ = r;
14414 }
14415
14416 /// `array()` returns None when name doesn't exist in params either
14417 /// (no fallback hits).
14418 #[test]
14419 fn exec_hooks_array_returns_none_for_missing_name() {
14420 let _g = crate::test_util::global_state_lock();
14421 crate::ported::params::unsetparam("__no_such_array_zshrs__");
14422 let got = array("__no_such_array_zshrs__");
14423 // Either None (no hook + no param) or hook-returned value.
14424 let _ = got;
14425 }
14426
14427 /// `array()` empty name doesn't panic (some callers pass "" for
14428 /// special parameter probes).
14429 #[test]
14430 fn exec_hooks_array_empty_name_does_not_panic() {
14431 let _g = crate::test_util::global_state_lock();
14432 let _ = array("");
14433 }
14434
14435 /// Idempotent: calling array() twice with same name yields same
14436 /// result (no observable side effect on the fallback path).
14437 #[test]
14438 fn exec_hooks_array_is_idempotent() {
14439 let _g = crate::test_util::global_state_lock();
14440 crate::ported::params::unsetparam("EH_IDEMPOTENT");
14441 crate::ported::params::setaparam("EH_IDEMPOTENT", vec!["a".into(), "b".into()]);
14442 let first = array("EH_IDEMPOTENT");
14443 let second = array("EH_IDEMPOTENT");
14444 assert_eq!(first, second);
14445 crate::ported::params::unsetparam("EH_IDEMPOTENT");
14446 }
14447
14448 /// `pparams()` returns an empty vec (not None) when no hook —
14449 /// callers can iterate without an Option-check.
14450 #[test]
14451 fn exec_hooks_pparams_returns_vec_not_none() {
14452 let _g = crate::test_util::global_state_lock();
14453 // Type assertion: result is Vec<String>, not Option<Vec<String>>.
14454 let p: Vec<String> = pparams();
14455 let _ = p; // either [] or hook-installed value
14456 }
14457
14458 /// Round-trip via params fallback: set then read returns same value.
14459 #[test]
14460 fn exec_hooks_array_set_get_roundtrip_via_params() {
14461 let _g = crate::test_util::global_state_lock();
14462 crate::ported::params::unsetparam("EH_RT");
14463 let vals = vec!["one".to_string(), "two".to_string(), "three".to_string()];
14464 crate::ported::params::setaparam("EH_RT", vals.clone());
14465 let got = array("EH_RT").expect("set then get should hit params fallback");
14466 assert_eq!(got, vals);
14467 crate::ported::params::unsetparam("EH_RT");
14468 }
14469
14470 /// `unregister_function` is consistently a bool — pin the return
14471 /// type so accidental refactor to () would fail the type check.
14472 #[test]
14473 fn exec_hooks_unregister_function_returns_bool() {
14474 let _g = crate::test_util::global_state_lock();
14475 let r: bool = unregister_function("__never_xyz_zshrs__");
14476 let _ = r;
14477 }
14478
14479 // ═══════════════════════════════════════════════════════════════════
14480 // Additional contract-pin tests for exec_hooks default behavior.
14481 // ═══════════════════════════════════════════════════════════════════
14482
14483 /// `run_command_substitution` returns String (never None / never Option).
14484 #[test]
14485 fn exec_hooks_run_command_substitution_returns_string_type() {
14486 let _g = crate::test_util::global_state_lock();
14487 let _: String = run_command_substitution("anything");
14488 }
14489
14490 /// `execute_script` returns Result<i32, String>. Pin signature.
14491 #[test]
14492 fn exec_hooks_execute_script_returns_result_type() {
14493 let _g = crate::test_util::global_state_lock();
14494 let _: Result<i32, String> = execute_script("anything");
14495 }
14496
14497 /// `execute_script_zsh_pipeline` returns Result<i32, String>.
14498 #[test]
14499 fn exec_hooks_execute_script_zsh_pipeline_returns_result_type() {
14500 let _g = crate::test_util::global_state_lock();
14501 let _: Result<i32, String> = execute_script_zsh_pipeline("anything");
14502 }
14503
14504 /// `dispatch_function_call` returns Option<i32>.
14505 #[test]
14506 fn exec_hooks_dispatch_function_call_returns_option_i32() {
14507 let _g = crate::test_util::global_state_lock();
14508 let _: Option<i32> = dispatch_function_call("__never_real__", &[]);
14509 }
14510
14511 /// `run_function_body` returns Option<i32>.
14512 #[test]
14513 fn exec_hooks_run_function_body_returns_option_i32() {
14514 let _g = crate::test_util::global_state_lock();
14515 let _: Option<i32> = run_function_body("__never_real__", &[]);
14516 }
14517
14518 /// `array` returns Option<Vec<String>>.
14519 #[test]
14520 fn exec_hooks_array_returns_option_vec_string() {
14521 let _g = crate::test_util::global_state_lock();
14522 let _: Option<Vec<String>> = array("anything");
14523 }
14524
14525 /// `assoc` returns Option<IndexMap<String, String>>.
14526 #[test]
14527 fn exec_hooks_assoc_returns_option_indexmap() {
14528 let _g = crate::test_util::global_state_lock();
14529 let _: Option<IndexMap<String, String>> = assoc("anything");
14530 }
14531
14532 /// Empty-string args to `dispatch_function_call` doesn't panic.
14533 #[test]
14534 fn exec_hooks_dispatch_empty_args_no_panic() {
14535 let _g = crate::test_util::global_state_lock();
14536 let _ = dispatch_function_call("", &[]);
14537 let _ = dispatch_function_call("name", &[]);
14538 }
14539
14540 /// Empty-string args to `run_function_body` doesn't panic.
14541 #[test]
14542 fn exec_hooks_run_function_body_empty_args_no_panic() {
14543 let _g = crate::test_util::global_state_lock();
14544 let _ = run_function_body("", &[]);
14545 let _ = run_function_body("name", &[]);
14546 }
14547
14548 /// Empty-string name to `execute_script` doesn't panic and returns
14549 /// Result.
14550 #[test]
14551 fn exec_hooks_execute_script_empty_src_no_panic() {
14552 let _g = crate::test_util::global_state_lock();
14553 let _ = execute_script("");
14554 let _ = execute_script_zsh_pipeline("");
14555 }
14556
14557 /// Empty-string cmd to `run_command_substitution` doesn't panic.
14558 #[test]
14559 fn exec_hooks_run_command_substitution_empty_no_panic() {
14560 let _g = crate::test_util::global_state_lock();
14561 let _: String = run_command_substitution("");
14562 }
14563
14564 /// `unregister_function("")` doesn't panic.
14565 #[test]
14566 fn exec_hooks_unregister_function_empty_name_no_panic() {
14567 let _g = crate::test_util::global_state_lock();
14568 let _: bool = unregister_function("");
14569 }
14570
14571 /// `unset_*` with empty name does not panic.
14572 #[test]
14573 fn exec_hooks_unset_with_empty_name_no_panic() {
14574 let _g = crate::test_util::global_state_lock();
14575 unset_scalar("");
14576 unset_array("");
14577 unset_assoc("");
14578 }
14579
14580 // ═══════════════════════════════════════════════════════════════════
14581 // Additional contract-pin tests for exec_hooks fallback semantics
14582 // c:213 pparams / c:217 set_pparams / c:223 unregister_function
14583 // ═══════════════════════════════════════════════════════════════════
14584
14585 /// `pparams()` is deterministic for repeated calls without state changes.
14586 #[test]
14587 fn pparams_deterministic_without_changes() {
14588 let _g = crate::test_util::global_state_lock();
14589 let first = pparams();
14590 for _ in 0..3 {
14591 assert_eq!(
14592 pparams(),
14593 first,
14594 "pparams() must be deterministic across reads"
14595 );
14596 }
14597 }
14598
14599 /// `pparams()` returns Vec<String> (not Option) — pin type contract.
14600 #[test]
14601 fn pparams_returns_vec_string_no_option() {
14602 let _g = crate::test_util::global_state_lock();
14603 let _: Vec<String> = pparams();
14604 }
14605
14606 /// `array(name)` with name containing null-byte doesn't panic.
14607 #[test]
14608 fn array_with_special_chars_in_name_no_panic() {
14609 let _g = crate::test_util::global_state_lock();
14610 let _ = array("name with spaces");
14611 let _ = array("name/with/slashes");
14612 let _ = array("$dollarsigns");
14613 }
14614
14615 /// `assoc(name)` empty name no panic.
14616 #[test]
14617 fn assoc_empty_name_no_panic() {
14618 let _g = crate::test_util::global_state_lock();
14619 let _ = assoc("");
14620 }
14621
14622 /// `set_array(empty, ...)` empty name no panic.
14623 #[test]
14624 fn set_array_empty_name_no_panic() {
14625 let _g = crate::test_util::global_state_lock();
14626 set_array("", vec![]);
14627 }
14628
14629 /// `set_assoc(empty, ...)` empty name no panic.
14630 #[test]
14631 fn set_assoc_empty_name_no_panic() {
14632 let _g = crate::test_util::global_state_lock();
14633 let m = indexmap::IndexMap::new();
14634 set_assoc("", m);
14635 }
14636
14637 /// `set_pparams(empty)` is safe.
14638 #[test]
14639 fn set_pparams_empty_vec_no_panic() {
14640 let _g = crate::test_util::global_state_lock();
14641 set_pparams(vec![]);
14642 }
14643
14644 /// Repeated `pparams()` doesn't allocate growing state.
14645 #[test]
14646 fn pparams_repeated_doesnt_grow_state() {
14647 let _g = crate::test_util::global_state_lock();
14648 let first_len = pparams().len();
14649 for _ in 0..10 {
14650 let n = pparams().len();
14651 assert_eq!(n, first_len, "len must not grow across reads");
14652 }
14653 }
14654
14655 /// `unregister_function` is deterministic for nonexistent name.
14656 #[test]
14657 fn unregister_function_unknown_deterministic() {
14658 let _g = crate::test_util::global_state_lock();
14659 let first = unregister_function("__never_real_xyz__");
14660 for _ in 0..3 {
14661 assert_eq!(unregister_function("__never_real_xyz__"), first);
14662 }
14663 }
14664
14665 /// `array(name)` repeated reads of nonexistent name are deterministic.
14666 #[test]
14667 fn array_unknown_is_deterministic() {
14668 let _g = crate::test_util::global_state_lock();
14669 let first = array("__never_real_array_xyz__").is_none();
14670 for _ in 0..3 {
14671 assert_eq!(array("__never_real_array_xyz__").is_none(), first);
14672 }
14673 }
14674
14675 /// `assoc(name)` repeated reads of nonexistent name are deterministic.
14676 #[test]
14677 fn assoc_unknown_is_deterministic() {
14678 let _g = crate::test_util::global_state_lock();
14679 let first = assoc("__never_real_assoc_xyz__").is_none();
14680 for _ in 0..3 {
14681 assert_eq!(assoc("__never_real_assoc_xyz__").is_none(), first);
14682 }
14683 }
14684
14685 // ═══════════════════════════════════════════════════════════════════
14686 // Additional C-parity tests for src/ported/exec_hooks.rs
14687 // c:134 array / c:147 assoc / c:181 dispatch_function_call /
14688 // c:188 run_function_body / c:192 execute_script /
14689 // c:206 run_command_substitution / c:213 pparams / c:223 unregister_function
14690 // ═══════════════════════════════════════════════════════════════════
14691
14692 /// `array(name)` returns Option<Vec<String>> (compile-time pin).
14693 #[test]
14694 fn array_returns_option_vec_string_type() {
14695 let _g = crate::test_util::global_state_lock();
14696 let _: Option<Vec<String>> = array("any");
14697 }
14698
14699 /// `assoc(name)` returns Option<IndexMap<String,String>> (compile-time pin).
14700 #[test]
14701 fn assoc_returns_option_indexmap_type() {
14702 let _g = crate::test_util::global_state_lock();
14703 let _: Option<indexmap::IndexMap<String, String>> = assoc("any");
14704 }
14705
14706 /// `dispatch_function_call` returns Option<i32> (compile-time pin).
14707 #[test]
14708 fn dispatch_function_call_returns_option_i32_type() {
14709 let _g = crate::test_util::global_state_lock();
14710 let _: Option<i32> = dispatch_function_call("__never__", &[]);
14711 }
14712
14713 /// `run_function_body` returns Option<i32> (compile-time pin).
14714 #[test]
14715 fn run_function_body_returns_option_i32_type() {
14716 let _g = crate::test_util::global_state_lock();
14717 let _: Option<i32> = run_function_body("__never__", &[]);
14718 }
14719
14720 /// `execute_script` returns Result<i32, String> (compile-time pin).
14721 #[test]
14722 fn execute_script_returns_result_type() {
14723 let _g = crate::test_util::global_state_lock();
14724 let _: Result<i32, String> = execute_script("");
14725 }
14726
14727 /// `execute_script_zsh_pipeline` returns Result<i32, String> (compile-time pin).
14728 #[test]
14729 fn execute_script_zsh_pipeline_returns_result_type() {
14730 let _g = crate::test_util::global_state_lock();
14731 let _: Result<i32, String> = execute_script_zsh_pipeline("");
14732 }
14733
14734 /// `run_command_substitution` returns String (compile-time pin).
14735 #[test]
14736 fn run_command_substitution_returns_string_type() {
14737 let _g = crate::test_util::global_state_lock();
14738 let _: String = run_command_substitution("");
14739 }
14740
14741 /// `pparams` returns Vec<String> (compile-time pin).
14742 #[test]
14743 fn pparams_returns_vec_string_type() {
14744 let _g = crate::test_util::global_state_lock();
14745 let _: Vec<String> = pparams();
14746 }
14747
14748 /// `unregister_function` returns bool (compile-time pin).
14749 #[test]
14750 fn unregister_function_returns_bool_type() {
14751 let _g = crate::test_util::global_state_lock();
14752 let _: bool = unregister_function("__never__");
14753 }
14754
14755 /// `unset_scalar`/`unset_array`/`unset_assoc` for nonexistent name safe.
14756 #[test]
14757 fn unset_variants_nonexistent_no_panic() {
14758 let _g = crate::test_util::global_state_lock();
14759 unset_scalar("__never_unset_scalar__");
14760 unset_array("__never_unset_array__");
14761 unset_assoc("__never_unset_assoc__");
14762 }
14763
14764 /// `set_pparams` with no hook installed is a silent no-op
14765 /// (c:217-220 — `if let Some(f) = PPARAMS_SET.get() { f(v); }`).
14766 /// Pin the no-hook contract so a refactor that panics on missing
14767 /// hook gets caught.
14768 #[test]
14769 fn set_pparams_without_hook_is_silent_noop() {
14770 let _g = crate::test_util::global_state_lock();
14771 // No hook installed in test context — must not panic.
14772 set_pparams(vec!["a".into(), "b".into(), "c".into()]);
14773 set_pparams(vec![]);
14774 }
14775
14776 // ═══════════════════════════════════════════════════════════════════
14777 // Additional contract pins for exec_hooks.rs
14778 // No-hook-installed contract: every accessor must be safe + deterministic
14779 // ═══════════════════════════════════════════════════════════════════
14780
14781 /// `array("")` empty name returns deterministic value (no panic).
14782 #[test]
14783 fn array_empty_name_no_panic_deterministic() {
14784 let _g = crate::test_util::global_state_lock();
14785 let a = array("");
14786 let b = array("");
14787 assert_eq!(a, b, "array(\"\") must be deterministic");
14788 }
14789
14790 /// `set_array` then `array` without hook should not panic.
14791 #[test]
14792 fn set_array_then_get_no_hook_safe() {
14793 let _g = crate::test_util::global_state_lock();
14794 set_array("__test_hook_arr__", vec!["a".into(), "b".into()]);
14795 let _ = array("__test_hook_arr__");
14796 }
14797
14798 /// `set_assoc` then `assoc` without hook should not panic.
14799 #[test]
14800 fn set_assoc_then_get_no_hook_safe() {
14801 let _g = crate::test_util::global_state_lock();
14802 let mut m = IndexMap::new();
14803 m.insert("k".to_string(), "v".to_string());
14804 set_assoc("__test_hook_assoc__", m);
14805 let _ = assoc("__test_hook_assoc__");
14806 }
14807
14808 /// `run_function_body` with no hook returns `Option<i32>` (type pin, alt).
14809 #[test]
14810 fn run_function_body_returns_option_i32_type_alt() {
14811 let _g = crate::test_util::global_state_lock();
14812 let _: Option<i32> = run_function_body("foo", &[]);
14813 }
14814
14815 /// `run_function_body` is deterministic for the same input when no hook.
14816 #[test]
14817 fn run_function_body_no_hook_deterministic() {
14818 let _g = crate::test_util::global_state_lock();
14819 let a = run_function_body("__never__", &[]);
14820 let b = run_function_body("__never__", &[]);
14821 assert_eq!(a, b);
14822 }
14823
14824 /// `dispatch_function_call` is deterministic across calls.
14825 #[test]
14826 fn dispatch_function_call_no_hook_deterministic() {
14827 let _g = crate::test_util::global_state_lock();
14828 let a = dispatch_function_call("__never__", &[]);
14829 let b = dispatch_function_call("__never__", &[]);
14830 assert_eq!(a, b);
14831 }
14832
14833 /// `pparams` returns `Vec<String>` (compile-time pin, alt).
14834 #[test]
14835 fn pparams_returns_vec_string_type_alt() {
14836 let _g = crate::test_util::global_state_lock();
14837 let _: Vec<String> = pparams();
14838 }
14839
14840 /// `pparams` is deterministic across repeated reads when no hook installed
14841 /// (this is observably true only if no other test has installed it; pin
14842 /// the no-mutation invariant).
14843 #[test]
14844 fn pparams_repeated_reads_are_observable_type() {
14845 let _g = crate::test_util::global_state_lock();
14846 // Just type pin — value depends on whether another test installed a
14847 // hook between these calls (PPARAMS_GET is OnceLock).
14848 let _a = pparams();
14849 let _b = pparams();
14850 }
14851
14852 /// `run_command_substitution` returns `String` type pin (alt).
14853 #[test]
14854 fn run_command_substitution_returns_string_type_alt() {
14855 let _g = crate::test_util::global_state_lock();
14856 let _: String = run_command_substitution("echo x");
14857 }
14858
14859 /// `run_command_substitution` empty command doesn't panic.
14860 #[test]
14861 fn run_command_substitution_empty_cmd_no_panic() {
14862 let _g = crate::test_util::global_state_lock();
14863 let _ = run_command_substitution("");
14864 }
14865
14866 /// `execute_script` returns `Result<i32, String>` type pin.
14867 #[test]
14868 fn execute_script_returns_result_i32_string_type() {
14869 let _g = crate::test_util::global_state_lock();
14870 let _: Result<i32, String> = execute_script("foo");
14871 }
14872
14873 /// `unregister_function("")` empty name returns bool safely.
14874 #[test]
14875 fn unregister_function_empty_name_returns_bool() {
14876 let _g = crate::test_util::global_state_lock();
14877 let _: bool = unregister_function("");
14878 }
14879
14880 /// c:3115 — `orig_cflags |= cflags` records the modifier flags consumed
14881 /// by the head walk: `builtin print` leaves `BINF_BUILTIN` in it, while a
14882 /// plain command accumulates nothing.
14883 #[test]
14884 fn execcmd_compile_head_accumulates_orig_cflags() {
14885 let _g = crate::test_util::global_state_lock();
14886 let words = |w: &[&str]| w.iter().map(|s| s.to_string()).collect::<Vec<_>>();
14887 let d = execcmd_compile_head(&words(&["builtin", "print", "x"]), WC_SIMPLE as u32);
14888 assert_ne!(d.orig_cflags & BINF_BUILTIN as u32, 0);
14889 let d = execcmd_compile_head(&words(&["print", "x"]), WC_SIMPLE as u32);
14890 assert_eq!(d.orig_cflags, 0);
14891 }
14892}