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:5050` / `:5260`)
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_hooks::*` 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::zsh_h::{
79 builtin, cmdnam, emulation_options, eprog, execstack, funcwrap, hashnode, isset, multio, redir,
80 shfunc, unset, wc_code, Emulation_options, Inang, Inpar, Meta, Nularg, Outpar, Pound,
81 BINF_BUILTIN, BINF_CLEARENV, BINF_COMMAND, BINF_DASH, BINF_EXEC, BINF_PREFIX, CHASEDOTS,
82 CHASELINKS, CLOBBER, CLOBBEREMPTY, CS_CMDSUBST, ERRFLAG_INT, FDT_EXTERNAL, FDT_INTERNAL,
83 FDT_PROC_SUBST, FDT_SAVED_MASK, FDT_TYPE_MASK, FDT_UNUSED, FDT_XTRACE, HASHDIRS, INP_LINENO,
84 INTERACTIVE, IS_CLOBBER_REDIR, IS_DASH, JOBTEXTSIZE, MAX_PIPESTATS, MONITOR, MULTIOS,
85 MULTIOUNIT, PATHDIRS, PM_LOADDIR, PM_READONLY, PM_UNDEFINED, POSIXBUILTINS, POSIXJOBS,
86 POSIXTRAPS, REDIRF_FROM_HEREDOC, REDIR_CLOSE, REDIR_HEREDOCDASH, REDIR_HERESTR, REDIR_INPIPE,
87 REDIR_OUTPIPE, USEZLE, VERBOSE, WC_LIST, WC_LIST_TYPE, WC_PIPE, WC_PIPE_END, WC_PIPE_TYPE,
88 WC_REDIR, WC_REDIR_TYPE, WC_REDIR_VARID, WC_SIMPLE, WC_SIMPLE_ARGC, WC_SUBLIST, WC_SUBLIST_END,
89 WC_SUBLIST_FLAGS, WC_SUBLIST_TYPE, WC_TYPESET, ZSIG_FUNC, ZSIG_IGNORED, Z_END,
90};
91use crate::ported::zsh_system_h::timespec as ZshTimespec;
92use crate::ported::ztype_h::{inull, itok};
93use crate::zsh_h::XTRACE;
94
95/// Port of the anonymous `enum { ... }` from `Src/exec.c:35-40`.
96/// Flag bits passed as the `addflags` argument to `addvars` /
97/// `addvarsfromargs`:
98/// - `ADDVAR_EXPORT` (1<<0) — export each assignment for the
99/// command `VAR=val cmd ...` form.
100/// - `ADDVAR_RESTORE` (1<<2) — the variable list is being restored
101/// later (implicit local scope), so
102/// suppress `ASSPM_WARN`.
103pub const ADDVAR_EXPORT: i32 = 1 << 0; // c:37 (Src/exec.c)
104/// `ADDVAR_RESTORE` constant.
105pub const ADDVAR_RESTORE: i32 = 1 << 2; // c:39 (Src/exec.c)
106
107/// Port of `int trap_state;` from `Src/exec.c:134`. Tracks whether
108/// a trap handler is currently being processed and, paired with
109/// `TRAP_RETURN` below, whether a `return` inside the trap should
110/// promote to `TRAP_STATE_FORCE_RETURN` to unwind the trap caller.
111///
112/// Values: `TRAP_STATE_INACTIVE = 0`, `TRAP_STATE_PRIMED = 1`,
113/// `TRAP_STATE_FORCE_RETURN = 2` (see `Src/zsh.h`).
114pub static TRAP_STATE: std::sync::atomic::AtomicI32 = // c:134 (Src/exec.c)
115 std::sync::atomic::AtomicI32::new(0);
116
117/// Port of `int trap_return;` from `Src/exec.c:155`. Carries the
118/// pending exit status from inside a trap; sentinel `-2` means
119/// "running an EXIT/DEBUG-style trap at the current level"
120/// (signals.c:1166). Promoted to the user's `return N` value by
121/// `bin_return` when POSIX-trap semantics apply (builtin.c:5852).
122pub static TRAP_RETURN: std::sync::atomic::AtomicI32 = // c:155 (Src/exec.c)
123 std::sync::atomic::AtomicI32::new(0);
124
125/// Port of `int forklevel;` from `Src/exec.c:1052`. Records the
126/// `locallevel` at the most recent fork point (set at c:1221:
127/// `forklevel = locallevel;` inside `entersubsh()`). Used by:
128/// - `signals.c:808` SIGPIPE handler — `!forklevel` distinguishes
129/// the top-level shell from a forked subshell.
130/// - `exec.c:6146` — `if (locallevel > forklevel)` decides whether
131/// a function-defined trap should fire on this subshell exit.
132/// - `params.c:3724` — WARNCREATEGLOBAL nest-depth check.
133///
134/// Initialised to 0 (no fork has occurred yet). Set to `locallevel`
135/// at every `entersubsh()` entry per c:1221.
136pub static FORKLEVEL: std::sync::atomic::AtomicI32 = // c:1052 (Src/exec.c)
137 std::sync::atomic::AtomicI32::new(0);
138
139// =============================================================================
140// File-static globals from Src/exec.c. Bucket choices per PORT_PLAN.md:
141// - Per-evaluator transient state → thread_local Cell (bucket 1)
142// - Shell-wide shared state → AtomicI32 / Mutex (bucket 2)
143// All names match C exactly. Surrounding doc-comments cite the C
144// declaration line.
145// =============================================================================
146
147/// Port of `int noerrexit;` from `Src/exec.c:72`. Bit-flags that
148/// suppress ERREXIT triggering on the next command(s). Bits:
149/// `NOERREXIT_EXIT` (in `if`/`while`/`until` test contexts),
150/// `NOERREXIT_RETURN` (after `return`), `NOERREXIT_UNTIL_EXEC`
151/// (until next exec'd command). Bucket-1 — per-evaluator (each
152/// recursive eval has its own suppression frame).
153pub static noerrexit: std::sync::atomic::AtomicI32 = // c:72 (Src/exec.c)
154 std::sync::atomic::AtomicI32::new(0);
155
156/// Port of `int this_noerrexit;` from `Src/exec.c:109`. When set,
157/// suppress ERREXIT for THIS one command only (consumed + cleared
158/// before the next command starts). Set by `execcursh` and the
159/// `((expr))` arith path so a 0-result doesn't trigger errexit.
160pub static this_noerrexit: std::sync::atomic::AtomicI32 = // c:109 (Src/exec.c)
161 std::sync::atomic::AtomicI32::new(0);
162
163/// Port of `mod_export int noerrs;` from `Src/exec.c:117`. When
164/// non-zero, suppress `zerr()` output (lex error reporting during
165/// `parse_string`, `parseopts` etc.). Saved/restored by
166/// `execsave`/`execrestore`.
167/// Port of `static char list_pipe_text[JOBTEXTSIZE]` from
168/// `Src/exec.c:463`. Holds the textual rendering of the in-flight
169/// pipe list; saved across nested execlist invocations at
170/// exec.c:1372-1380 (zeroed on entry, restored from
171/// `old_list_pipe_text` at c:1634-1638) and round-tripped through
172/// execsave/execrestore (c:6448 / c:6484). zshrs models it as a
173/// length-bounded String guarded by a Mutex — the C `char[80]` cap
174/// is a buffer-overflow guard, but matching length matters for the
175/// `jobs` builtin's pipe-list rendering.
176pub static LIST_PIPE_TEXT: std::sync::Mutex<String> = std::sync::Mutex::new(String::new()); // c:463 (Src/exec.c)
177
178pub static noerrs: std::sync::atomic::AtomicI32 = // c:117 (Src/exec.c)
179 std::sync::atomic::AtomicI32::new(0);
180
181/// Port of `int nohistsave;` from `Src/exec.c:122`. When non-zero,
182/// `addhistnode` no-ops so trap firings / `eval` invocations don't
183/// pollute `$HISTCMD`. Tracked alongside `noerrs` in the trap path.
184pub static nohistsave: std::sync::atomic::AtomicI32 = // c:122 (Src/exec.c)
185 std::sync::atomic::AtomicI32::new(0);
186
187/// Port of `int subsh;` from `Src/exec.c:160`. Subshell depth — bumped
188/// every time `entersubsh` forks a sub-shell, used by signal handling
189/// (different SIGINT semantics in subshells) and by `${$$}` (`$$`
190/// stays at the top-level pid).
191pub static subsh: std::sync::atomic::AtomicI32 = // c:160 (Src/exec.c)
192 std::sync::atomic::AtomicI32::new(0);
193
194/// Port of `mod_export int zsh_subshell;` from `Src/init.c:67`. Visible
195/// `$ZSH_SUBSHELL` parameter — incremented by `entersubsh()` each time
196/// the shell forks into a subshell (real or fake-exec). Distinct from
197/// `subsh` which records whether we ARE a subshell; `zsh_subshell` is
198/// the visible depth count.
199pub static zsh_subshell: std::sync::atomic::AtomicI32 = // c:67 (Src/init.c)
200 std::sync::atomic::AtomicI32::new(0);
201
202/// Port of `mod_export volatile int retflag;` from `Src/exec.c:165`.
203/// Set by `bin_return` to unwind the function-call stack. Cleared
204/// by `runshfunc` on entry, checked by `execlist`'s main loop.
205pub static retflag: std::sync::atomic::AtomicI32 = // c:165 (Src/exec.c)
206 std::sync::atomic::AtomicI32::new(0);
207
208/// Port of `pid_t cmdoutpid;` from `Src/exec.c:215`. Pid of the most
209/// recent `$(cmd)` command-substitution child. Used by exit-status
210/// propagation: `cmdoutval` carries the exit; `cmdoutpid` carries
211/// the pid `waitpid`-d for it.
212pub static cmdoutpid: std::sync::atomic::AtomicI32 = // c:215 (Src/exec.c)
213 std::sync::atomic::AtomicI32::new(0);
214
215/// Port of `mod_export pid_t procsubstpid;` from `Src/exec.c:220`.
216/// Pid of the most recent process-substitution child (`<(cmd)` /
217/// `>(cmd)`). Tracked separately from `cmdoutpid` because procsubst
218/// jobs aren't wait-collected by the parent until the fd is closed.
219pub static procsubstpid: std::sync::atomic::AtomicI32 = // c:220 (Src/exec.c)
220 std::sync::atomic::AtomicI32::new(0);
221
222/// Port of `int cmdoutval;` from `Src/exec.c:225`. Exit status of
223/// the most recent `$(cmd)`. Drives `$?` when a varspc-only command
224/// runs alongside a substitution.
225pub static cmdoutval: std::sync::atomic::AtomicI32 = // c:225 (Src/exec.c)
226 std::sync::atomic::AtomicI32::new(0);
227
228/// Port of `int use_cmdoutval;` from `Src/exec.c:234`. When set,
229/// `lastval` is updated from `cmdoutval` after the command
230/// (i.e. the command had substitutions whose exit status matters).
231pub static use_cmdoutval: std::sync::atomic::AtomicI32 = // c:234 (Src/exec.c)
232 std::sync::atomic::AtomicI32::new(0);
233
234/// Port of `mod_export int sfcontext;` from `Src/exec.c:239`. Source
235/// context — one of `SFC_NONE`, `SFC_DIRECT` (user typed it),
236/// `SFC_SIGNAL` (trap firing), `SFC_HOOK` (precmd/preexec etc.),
237/// `SFC_WIDGET` (ZLE widget), `SFC_COMPLETE` (completion fn),
238/// `SFC_CFUNC` (compsys fn), `SFC_SUBST` ($(...) cmd-subst),
239/// `SFC_EVAL` (eval body). Read by `zerr()` / `funcstack` building.
240pub static sfcontext: std::sync::atomic::AtomicI32 = // c:239 (Src/exec.c)
241 std::sync::atomic::AtomicI32::new(0);
242
243/// Port of `int list_pipe = 0;` from `Src/exec.c:457`. Set when the
244/// currently-executing pipeline is the long-running pipe-into-loop
245/// shape (`cat foo | while read a; do ... done`) — drives the
246/// super/sub-job tracking documented in the famous `Allen Edeln…`
247/// comment block above this declaration in C.
248pub static list_pipe: std::sync::atomic::AtomicI32 = // c:457 (Src/exec.c)
249 std::sync::atomic::AtomicI32::new(0);
250
251/// Port of `int simple_pline = 0;` from `Src/exec.c:457`. Set during
252/// dispatch of a "simple" pipeline (single-stage / no shell-construct
253/// tail) so the `list_pipe` machinery short-circuits.
254pub static simple_pline: std::sync::atomic::AtomicI32 = // c:457 (Src/exec.c)
255 std::sync::atomic::AtomicI32::new(0);
256
257/// Port of `static pid_t list_pipe_pid;` from `Src/exec.c:459`.
258/// PID of the sub-shell created to host the loop-after-pipe pattern;
259/// passed up the recursive `execlist` stack so the cat-job's super-
260/// job entry can record it.
261pub static list_pipe_pid: std::sync::atomic::AtomicI32 = // c:459 (Src/exec.c)
262 std::sync::atomic::AtomicI32::new(0);
263
264/// Port of `static int nowait;` from `Src/exec.c:461`. When set,
265/// `execpline` doesn't wait for the pipeline; used during the
266/// list_pipe sub-shell fork bookkeeping.
267pub static nowait: std::sync::atomic::AtomicI32 = // c:461 (Src/exec.c)
268 std::sync::atomic::AtomicI32::new(0);
269
270/// Port of `int pline_level = 0;` from `Src/exec.c:461`. Recursive
271/// pipeline depth (counts nested pipelines within the current
272/// `execlist` call chain).
273pub static pline_level: std::sync::atomic::AtomicI32 = // c:461 (Src/exec.c)
274 std::sync::atomic::AtomicI32::new(0);
275
276/// Port of `static int list_pipe_child = 0;` from `Src/exec.c:462`.
277/// Set in the child after the list_pipe fork so the child knows to
278/// continue executing the loop body (vs the parent which records
279/// the pid + returns).
280pub static list_pipe_child: std::sync::atomic::AtomicI32 = // c:462 (Src/exec.c)
281 std::sync::atomic::AtomicI32::new(0);
282
283/// Port of `static int list_pipe_job;` from `Src/exec.c:462`. Job
284/// table index of the pipeline's first-stage job (the `cat` in
285/// `cat foo | while ...`).
286pub static list_pipe_job: std::sync::atomic::AtomicI32 = // c:462 (Src/exec.c)
287 std::sync::atomic::AtomicI32::new(0);
288
289/// Port of `static int doneps4;` from `Src/exec.c:262`. Set after
290/// `printprompt4` has emitted the `$PS4` prefix for the current
291/// xtrace command — prevents double-printing when an inner sub-eval
292/// also wants to xtrace.
293pub static doneps4: std::sync::atomic::AtomicI32 = // c:262 (Src/exec.c)
294 std::sync::atomic::AtomicI32::new(0);
295
296/// Port of `static int esprefork, esglob = 1;` from `Src/exec.c:2680`.
297///
298/// File-static "execsubst parameters" — callers (execcmd_exec at
299/// c:3298 / c:3700) set these BEFORE invoking execsubst, which then
300/// uses them as the `flags` arg to prefork() and the gate on
301/// globlist(). `esprefork` is `PREFORK_TYPESET` for magic-assign /
302/// MAGICEQUALSUBST words, else 0. `esglob` defaults to 1; cleared
303/// when the dispatched builtin has `BINF_NOGLOB`.
304pub static esprefork: std::sync::atomic::AtomicI32 = // c:2680
305 std::sync::atomic::AtomicI32::new(0);
306pub static esglob: std::sync::atomic::AtomicI32 = // c:2680 (= 1)
307 std::sync::atomic::AtomicI32::new(1);
308
309/// Port of `struct execstack *exstack;` from `Src/exec.c:244`. Head
310/// of the linked exec-context save stack — `execsave` pushes a frame
311/// before signal-handler / trap dispatch; `execrestore` pops it
312/// afterwards so the interrupted command resumes with its state intact.
313pub static exstack: std::sync::Mutex<Option<Box<execstack>>> = // c:244
314 std::sync::Mutex::new(None);
315
316/// Port of `static char *STTYval;` from `Src/exec.c:263`. Pending
317/// `stty` argument string captured by `addvars` when the command's
318/// inline env contains `STTY=...`. Applied by `execute` before fork
319/// + exec so the spawned program sees its tty configured. Reset to
320/// `None` after consumption to avoid infinite recursion.
321pub static STTYval: std::sync::Mutex<Option<String>> = // c:263 (Src/exec.c)
322 std::sync::Mutex::new(None);
323
324/// Convert a here-document into a here-string. Line-by-line port of
325/// `gethere()` from `Src/exec.c:4569-4652`. Reads the body from the
326/// input stream via `hgetc()` until the terminator line is matched,
327/// returning the collected body as a string. `strp` is in/out: on
328/// entry the raw terminator (possibly with token markers + leading
329/// tabs); on return the munged terminator (after `quotesubst` +
330/// `untokenize` and, for `REDIR_HEREDOCDASH`, leading-tab strip).
331///
332/// Returns `None` on out-of-memory (C `zalloc`/`realloc` failure).
333/// Rust's `String` auto-grows so the OOM branch is effectively
334/// unreachable, but the return type stays `Option<String>` to mirror
335/// the C signature which can return NULL.
336///
337/// Port of `gethere(char **strp, int typ)` from `Src/exec.c:4573`.
338pub fn gethere(strp: &mut String, typ: i32) -> Option<String> {
339 // c:4573 (Src/exec.c)
340 let mut buf: String; // c:4575 char *buf
341 let mut bsiz: usize; // c:4576 int bsiz
342 let mut qt: i32 = 0; // c:4576 int qt = 0
343 let mut strip: i32 = 0; // c:4576 int strip = 0
344 // c:4577 — char *s, *t, *bptr, c. zshrs uses byte-offsets into
345 // `buf` for `t` and tracks `bptr` implicitly as `buf.len()` (the
346 // C `bptr++` increment is `buf.push(c)`; `bptr--` is `buf.pop()`).
347 // `s` (the loop iterator for the inull-scan) stays local to its
348 // for-loop. `c` mirrors the C `char c`.
349 let mut t: usize; // c:4577 char *t
350 let mut c: Option<char>; // c:4577 char c
351 let mut str: String = strp.clone(); // c:4578 char *str = *strp
352
353 // c:4580-4584 — for (s = str; *s; s++) if (inull(*s)) { qt = 1; break; }
354 for s in str.bytes() {
355 if inull(s) {
356 // c:4581
357 qt = 1; // c:4582
358 break; // c:4583
359 }
360 }
361 str = quotesubst(&str); // c:4585
362 str = untokenize(&str); // c:4586
363 if typ == REDIR_HEREDOCDASH {
364 // c:4587
365 strip = 1; // c:4588
366 // c:4589-4590 — while (*str == '\t') str++;
367 while str.starts_with('\t') {
368 str.remove(0);
369 }
370 }
371 *strp = str.clone(); // c:4592 *strp = str
372
373 // c:4593 — bptr = buf = zalloc(bsiz = 256);
374 bsiz = 256;
375 buf = String::with_capacity(bsiz);
376 let _ = bsiz; // bsiz is tracked by C for zfree; Rust drops automatically
377
378 // c:4594 — for (;;)
379 loop {
380 t = buf.len(); // c:4595 t = bptr
381
382 // c:4597-4598 — while ((c = hgetc()) == '\t' && strip) ;
383 loop {
384 c = hgetc();
385 if !(c == Some('\t') && strip != 0) {
386 break;
387 }
388 }
389
390 // c:4599 — for (;;) — inner body-read loop
391 loop {
392 // c:4600-4613 — buffer-growth realloc dance. Rust's
393 // String auto-grows; nothing to do.
394 // c:4614 — if (lexstop || c == '\n') break;
395 if LEX_LEXSTOP.with(|f| f.get()) || c == Some('\n') || c.is_none() {
396 break;
397 }
398 // c:4616 — if (!qt && c == '\\')
399 if qt == 0 && c == Some('\\') {
400 buf.push('\\'); // c:4617 *bptr++ = c
401 c = hgetc(); // c:4618
402 if c == Some('\n') {
403 // c:4619
404 buf.pop(); // c:4620 bptr--
405 c = hgetc(); // c:4621
406 continue; // c:4622
407 }
408 }
409 if let Some(ch) = c {
410 // c:4625 *bptr++ = c
411 buf.push(ch);
412 }
413 c = hgetc(); // c:4626
414 }
415 // c:4628 — *bptr = '\0'; (implicit — Rust String tracks len)
416
417 // c:4629-4630 — if (!strcmp(t, str)) break;
418 if &buf[t..] == str.as_str() {
419 break;
420 }
421 // c:4631-4634 — if (lexstop) { t = bptr; break; }
422 if LEX_LEXSTOP.with(|f| f.get()) {
423 t = buf.len();
424 break;
425 }
426 // c:4635 — *bptr++ = '\n';
427 buf.push('\n');
428 }
429 // c:4637 — *t = '\0';
430 buf.truncate(t);
431
432 // c:4638-4640 — s = buf; buf = dupstring(buf); zfree(s, bsiz);
433 // The C dance frees the realloc'd block and re-allocates via the
434 // string-heap allocator. Rust drops the old String when reassigned.
435 buf = dupstring(&buf);
436
437 if qt == 0 {
438 // c:4641
439 // c:4642 — int ef = errflag;
440 let ef = errflag.load(Ordering::Relaxed);
441 // c:4644 — parsestr(&buf);
442 if let Ok(parsed) = parsestr(&buf) {
443 buf = parsed;
444 }
445 // c:4646-4649 — if (!(errflag & ERRFLAG_ERROR)) errflag = ef | (errflag & ERRFLAG_INT);
446 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
447 let cur = errflag.load(Ordering::Relaxed);
448 errflag.store(ef | (cur & ERRFLAG_INT), Ordering::Relaxed);
449 }
450 }
451 Some(buf) // c:4651 return buf
452}
453
454/// Port of `LinkList getoutput(char *cmd, int qt)` from
455/// `Src/exec.c:4712-4791`. Runs a command-substitution body in the
456/// active executor, then routes the captured stdout through
457/// `readoutput(pipe, qt, NULL)` semantics at c:4855-4872.
458///
459/// C return shape: `LinkList` of `char*`. Rust port returns
460/// `Vec<String>` (same shape, owned).
461///
462/// `qt` matches C exactly:
463/// - qt=1 (quoted, `"$(...)"`): trim trailing newlines, return
464/// entire output as a single-element vec. C c:4858-4862: if
465/// output empty, returns a single Nularg sentinel so callers
466/// see "empty value" rather than "no value".
467/// - qt=0 (unquoted, `$(...)`): trim trailing newlines, then
468/// `spacesplit(buf, allownull=false)` per c:4865-4871.
469///
470/// Uses `with_executor` (panics on missing VM context), not
471/// `try_with_executor + unwrap_or_default()`. C `getoutput` calls
472/// `execpline` directly — there's no "no shell" code path. The
473/// silent-no-op pattern (return empty string when no executor) would
474/// mask catastrophic state corruption as "command produced no output",
475/// which is the failure mode the `subst.rs:496` warning block flags.
476/* $(...) */
477// c:4709
478/// `getoutput` — see implementation.
479pub fn getoutput(cmd: &str, qt: i32) -> Vec<String> {
480 // c:4713
481 // c:4715 — `Eprog prog;`
482 let prog: Option<eprog>;
483 // c:4716 — `int pipes[2];` (collapsed: in-process executor; no fork)
484 // c:4717 — `pid_t pid;` (collapsed)
485 let mut s: String; // c:4718
486 // c:4720-4723 — `int onc = nocomments; nocomments = (interact &&
487 // !sourcelevel && unset(INTERACTIVECOMMENTS));
488 // prog = parse_string(cmd, 0); nocomments = onc;`
489 let onc = crate::ported::lex::LEX_NOCOMMENTS.with(|c| c.get());
490 let new_nc = crate::ported::zsh_h::interact()
491 && crate::ported::init::sourcelevel.load(Ordering::Relaxed) == 0
492 && !isset(crate::ported::zsh_h::INTERACTIVECOMMENTS);
493 crate::ported::lex::LEX_NOCOMMENTS.with(|c| c.set(new_nc));
494 prog = parse_string(cmd, 0);
495 crate::ported::lex::LEX_NOCOMMENTS.with(|c| c.set(onc));
496
497 if prog.is_none() {
498 // c:4725
499 return Vec::new(); // c:4726 return NULL
500 }
501 let prog = prog.unwrap();
502
503 if !isset(crate::ported::zsh_h::EXECOPT) {
504 // c:4728
505 return Vec::new(); // c:4729 newlinklist()
506 }
507
508 // c:4731 — `if ((s = simple_redir_name(prog, REDIR_READ)))` — `$(< word)`
509 if let Some(red_name) = simple_redir_name(&prog, crate::ported::zsh_h::REDIR_READ) {
510 /* $(< word) */
511 // c:4732
512 s = red_name;
513 s = singsub(&s); // c:4737
514 if errflag.load(Ordering::Relaxed) != 0 {
515 return Vec::new(); // c:4739
516 }
517 let s = untokenize(&s); // c:4740
518 let path_meta = unmeta(&s); // c:4741 unmeta(s)
519 let cpath = match std::ffi::CString::new(path_meta.as_bytes()) {
520 Ok(c) => c,
521 Err(_) => return Vec::new(),
522 };
523 let stream = unsafe {
524 libc::open(cpath.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY) // c:4741
525 };
526 if stream == -1 {
527 // c:4742 — `zwarn("%e: %s", errno, s);`
528 let errno = std::io::Error::last_os_error();
529 zerr(&format!("{}: {}", errno, s));
530 LASTVAL.store(1, Ordering::Relaxed);
531 cmdoutval.store(1, Ordering::Relaxed);
532 return Vec::new(); // c:4744
533 }
534 // c:4746 — `retval = readoutput(stream, qt, &readerror);`
535 let mut readerror: i32 = 0;
536 let retval = readoutput(stream, qt, &mut readerror); // c:4746
537 if readerror != 0 {
538 // c:4747
539 zerr(&format!(
540 "error when reading {}: {}", // c:4748
541 s,
542 std::io::Error::from_raw_os_error(readerror)
543 ));
544 LASTVAL.store(1, Ordering::Relaxed);
545 cmdoutval.store(1, Ordering::Relaxed);
546 }
547 return retval; // c:4751
548 }
549
550 // c:4753-4790 — Full fork path: mpipe + zfork + parent
551 // readoutput / waitforpid / child execode + _realexit. fusevm runs
552 // command substitution in-process, so the fork shape collapses to a
553 // synchronous executor call. C control points preserved as cites:
554 // c:4753 mpipe — handled by ShellExecutor pipe wiring
555 // c:4758 child_block — no-op (no fork)
556 // c:4760 zfork — replaced by in-process exec
557 // c:4768-4776 parent — equivalent to executor return
558 // c:4778-4789 child — entersubsh+execode+_realexit collapse
559 cmdoutval.store(0, Ordering::Relaxed); // c:4759
560 let buf = crate::ported::exec_hooks::run_command_substitution(cmd);
561 LASTVAL.store(cmdoutval.load(Ordering::Relaxed), Ordering::Relaxed); // c:4775
562
563 // c:4772 retval = readoutput — post-walk (c:4855-4871 tail) inlined.
564 let buf = buf.trim_end_matches('\n');
565 if qt != 0 {
566 if buf.is_empty() {
567 vec![String::from(Nularg)] // c:4859-4861
568 } else {
569 vec![buf.to_string()] // c:4863
570 }
571 } else {
572 crate::ported::utils::spacesplit(buf, false) // c:4865
573 }
574}
575
576/// Direct port of `Shfunc loadautofn(Shfunc shf, int ks, int test_only,
577/// int ignore_loaddir)` from `Src/exec.c:5050`. Walks `$fpath` for a
578/// file named `shf->node.nam`, reads it, installs the text body on
579/// the corresponding `shfunctab` entry, and clears `PM_UNDEFINED`.
580///
581/// C body (abridged):
582/// 1. `name = shf->node.nam`
583/// 2. `getfpfunc(name, &dir_path, NULL, 0)` → resolved file path
584/// 3. If !test_only && file found: parse → store eprog on
585/// `shf->funcdef`; clear PM_UNDEFINED; set `shf->filename`.
586/// 4. Returns shf on success, NULL on failure.
587///
588/// Rust port: returns 0 = success, 1 = failure (matches the
589/// existing call-site convention in `bin_functions -c`). Stores
590/// raw file text on `ShFunc.body` (the Rust-side ShFunc in
591/// `hashtable.rs:362`); the parser pass that converts text →
592/// Eprog runs lazily at first call site.
593/// Port of `loadautofn(Shfunc shf, int fksh, int autol, int current_fpath)` from `Src/exec.c:5682`.
594pub fn loadautofn(
595 shf: *mut shfunc, // c:5682 (Src/exec.c)
596 _ks: i32,
597 autol: i32,
598 _ignore_loaddir: i32,
599) -> i32 {
600 if shf.is_null() {
601 return 1;
602 }
603 // c:5054 — `name = shf->node.nam`.
604 let name = unsafe { (*shf).node.nam.clone() };
605 // c:5070 — `path = getfpfunc(name, &dir_path, NULL, 0)`.
606 let mut dir_path: Option<String> = None;
607 let path = match getfpfunc(&name, &mut dir_path, None, 0) {
608 Some(p) => p,
609 None => {
610 // c:Src/exec.c:5713-5719 — file not found path. C:
611 // `if (prog == &dummy_eprog) {
612 // locallevel--;
613 // zwarn("%s: function definition file not found",
614 // shf->node.nam);
615 // locallevel++;
616 // popheap();
617 // return NULL;
618 // }`
619 // C's getfpfunc returns &dummy_eprog as the "not found"
620 // sentinel when test_only==0; loadautofn detects it and
621 // emits the diagnostic before returning NULL. Rust's
622 // getfpfunc returns Option::None for the same condition,
623 // so we emit the same diagnostic here. The locallevel
624 // dance is preserved as a comment because the Rust
625 // port's zwarn doesn't reference locallevel in the
626 // format string itself (the dance in C is only to keep
627 // the prefix line counter consistent with the function-
628 // body context). Bug #107 in docs/BUGS.md.
629 crate::ported::utils::zwarn(&format!(
630 "{}: function definition file not found",
631 name
632 ));
633 return 1; // c:5719 NULL
634 }
635 };
636 let _ = autol;
637 // Previously the Rust port treated this parameter as
638 // "test_only" and early-returned when set, so the `+X`
639 // call from `eval_autoload` (`loadautofn(shf, mode, 1, d)`)
640 // never actually loaded the file. C's parameter is `autol`
641 // (autoload mode), NOT a test-only flag — the C body
642 // unconditionally loads/parses regardless of autol. autol=1
643 // controls the EF_RUN / map-flag dance for the wordcode prog
644 // (c:5725-5749), but the loaded-body / PM_UNDEFINED-clear
645 // path runs in all cases. Removing the early-return so
646 // `autoload -U +X funcname` actually loads the body and
647 // `type funcname` reports `function from /path/file` instead
648 // of `autoload shell function`. Bug #160 in docs/BUGS.md.
649 // c:5100-5140 — read the file. C uses zopen + read + parse_string +
650 // execsave; Rust port stores raw text on the ShFunc and defers
651 // parse-to-Eprog until the first call.
652 let body = match std::fs::read_to_string(&path) {
653 Ok(t) => t,
654 Err(_) => return 1,
655 };
656 // c:5142 — `shf->filename = ztrdup(dir_path)`.
657 unsafe {
658 (*shf).filename = dir_path.clone().or(Some(path.clone()));
659 }
660 // c:5148 — `shf->node.flags &= ~PM_UNDEFINED`.
661 unsafe {
662 (*shf).node.flags &= !(PM_UNDEFINED as i32);
663 }
664 // Sync the body string into the Rust-side ShFunc table so the
665 // lazy-parse path can find it later.
666 if let Ok(mut tab) = shfunctab_lock().write() {
667 if let Some(existing) = tab.get_mut(&name) {
668 existing.body = Some(body);
669 existing.filename = dir_path;
670 } else {
671 tab.add(shfunc {
672 node: hashnode {
673 next: None,
674 nam: name.clone(),
675 flags: 0,
676 },
677 filename: dir_path,
678 lineno: 0,
679 funcdef: None,
680 redir: None,
681 sticky: None,
682 body: Some(body),
683 });
684 }
685 }
686 0
687}
688
689/// Port of `getfpfunc(char *s, int *ksh, char **fdir, char **alt_path, int test_only)` from Src/exec.c:5260. Walks `$fpath` (or the
690/// supplied `spec_path` slice) for a file named `name` and writes the
691/// resolved directory through `*dir_path_out` (matching the C `char **dir_path`).
692/// Returns `Some(file_contents_path)` on success, `None` when not found.
693pub fn getfpfunc(
694 name: &str,
695 dir_path_out: &mut Option<String>, // c:5260 (Src/exec.c)
696 spec_path: Option<&[String]>,
697 _all_loaded: i32,
698) -> Option<String> {
699 // C reads $fpath via `getaparam("fpath")` (the param-table array form
700 // tied to scalar `FPATH` via `typeset -T`). Reading `std::env::var`
701 // misses any in-script modification like `fpath=(/some/dir $fpath)`
702 // because that mutates the internal param table, not the inherited
703 // process env. Fall back to env only when the param table is empty
704 // (cold start before any param-table init).
705 let dirs: Vec<String> = match spec_path {
706 Some(s) => s.to_vec(),
707 None => crate::ported::params::getaparam("fpath")
708 .filter(|v| !v.is_empty())
709 .or_else(|| getsparam("FPATH").map(|v| v.split(':').map(String::from).collect()))
710 .or_else(|| {
711 std::env::var("FPATH")
712 .ok()
713 .map(|v| v.split(':').map(String::from).collect())
714 })
715 .unwrap_or_default(),
716 };
717 for dir in &dirs {
718 if dir.is_empty() {
719 continue;
720 }
721 let path = format!("{}/{}", dir, name);
722 if std::path::Path::new(&path).exists() {
723 *dir_path_out = Some(dir.clone());
724 return Some(path);
725 }
726 }
727 None
728}
729
730/// Port of `resolvebuiltin(const char *cmdarg, HashNode hn)` from
731/// `Src/exec.c:2703`. Ensures that an autoload-stub builtin has its
732/// module loaded before the caller invokes its `handlerfunc`. If the
733/// stub has no handler, `ensurefeature` is asked to load the module
734/// and re-lookup the builtin node. C body (abridged):
735/// ```c
736/// if (!((Builtin) hn)->handlerfunc) {
737/// char *modname = dupstring(((Builtin) hn)->optstr);
738/// (void)ensurefeature(modname, "b:", ...);
739/// hn = builtintab->getnode(builtintab, cmdarg);
740/// if (!hn) { lastval=1; zerr(...); return NULL; }
741/// }
742/// return hn;
743/// ```
744///
745/// WARNING: zshrs's builtin table is the static `BUILTINS` array in
746/// `src/ported/builtin.rs`. Module autoload routes through
747/// `module::ensurefeature(MODULESTAB, modname, "b:", Some(cmdarg))`;
748/// after the module loads the handler should be wired into BUILTINS.
749pub fn resolvebuiltin<'a>(
750 cmdarg: &str, // c:2703 (Src/exec.c)
751 hn: &'a builtin,
752) -> Option<&'a builtin> {
753 // c:2705 — `if (!((Builtin) hn)->handlerfunc)`.
754 if hn.handlerfunc.is_none() {
755 // c:2706 — `modname = dupstring(((Builtin)hn)->optstr)`.
756 let modname = hn.optstr.clone().unwrap_or_default();
757 // c:2712 — `ensurefeature(modname, "b:", cmdarg)`.
758 let _ = {
759 let mut t = crate::ported::module::MODULESTAB.lock().unwrap();
760 crate::ported::module::ensurefeature(&mut t, &modname, "b:", Some(cmdarg))
761 };
762 // c:2715-2716 — re-lookup the now-(hopefully)-resolved builtin.
763 if let Some(re) = BUILTINS.iter().find(|b| b.node.nam == cmdarg) {
764 if re.handlerfunc.is_some() {
765 return Some(re); // c:2723
766 }
767 }
768 // c:2717-2721 — `lastval = 1; zerr(...)` + return NULL.
769 zerr(&format!(
770 "autoloading module {} failed to define builtin: {}",
771 modname, cmdarg
772 ));
773 return None; // c:2720
774 }
775 Some(hn) // c:2723
776}
777
778/// Dispatch decision returned by `execcmd_compile_head` — the
779/// fusevm-bytecode-time head resolver that mirrors the local-variable
780/// state the C `execcmd_exec` function carries through `c:2913-2916`
781/// (`is_builtin`, `is_shfunc`, `cflags`, `use_defpath`) plus the
782/// precmd-modifier strip count. The fusevm bytecode compiler reads
783/// this to emit the correct dispatch opcode in
784/// `src/extensions/compile_zsh.rs::compile_simple`.
785///
786/// Not a C struct — invented to bridge the divergence between the
787/// C wordcode-walker (which mutates locals + falls through to
788/// invocation) and zshrs's split parse → compile → VM pipeline.
789#[allow(non_camel_case_types)]
790#[derive(Debug, Default, Clone)]
791pub struct execcmd_dispatch {
792 /// Number of `BINF_PREFIX` words to strip from the head of args.
793 /// `Src/exec.c:3086 uremnode(preargs, firstnode(preargs))`.
794 pub precmd_skip: usize,
795 /// Set when the head (after strip) is a real builtin
796 /// (`Src/exec.c:3065 is_builtin = 1`).
797 pub is_builtin: bool,
798 /// Set when the head (after strip) is a shell function
799 /// (`Src/exec.c:3053 is_shfunc = 1`).
800 pub is_shfunc: bool,
801 /// `cflags` accumulator from `Src/exec.c:2915` — gathers
802 /// `BINF_BUILTIN | BINF_COMMAND | BINF_EXEC | BINF_DASH |
803 /// BINF_NOGLOB` bits encountered during the precommand-modifier
804 /// walk (c:3062 `cflags |= hn->flags`).
805 pub cflags: u32,
806 /// `command -p` requested: use the default `$PATH` for lookup
807 /// (`Src/exec.c:3160 use_defpath = 1`). NOT YET HONORED by the
808 /// fusevm compiler — flagged for follow-up.
809 pub use_defpath: bool,
810 /// `command -v` / `command -V` requested: the dispatch target
811 /// flips to `bin_whence` per `Src/exec.c:3149-3157`
812 /// (`hn = &commandbn.node; is_builtin = 1`). The fusevm compiler
813 /// reads this and emits `Op::CallBuiltin(BUILTIN_WHENCE_FROM_COMMAND)`
814 /// instead of resolving the post-strip head.
815 pub has_command_vv: bool,
816 /// `exec -a NAME` requested: ARGV0 override per `Src/exec.c:3214-3240`.
817 /// `Some(NAME)` triggers `zputenv("ARGV0=NAME")` before exec.
818 pub exec_argv0: Option<String>,
819 /// Empty-command branch fired with no redirs (`Src/exec.c:3372-3406`
820 /// — the `else` arm of `if (redir && nonempty(redir))`). Covers
821 /// bare `exec` / `noglob` / `command`. Caller emits
822 /// `lastval = cmdoutval` (0 when no `$(cmd)` ran) and returns.
823 /// Also fires for the `(cflags & BINF_PREFIX) && (cflags &
824 /// BINF_COMMAND)` sub-case at `c:3365-3371` (bare `command`
825 /// returns 0 without complaining about missing redirs).
826 pub is_empty_command: bool,
827}
828
829/// !!! NOT A PORT OF C `execcmd_exec` !!!
830///
831/// This is a fusevm-bytecode-time head resolver invoked by
832/// `src/extensions/compile_zsh.rs::compile_simple` and the
833/// `command` builtin shim in `src/fusevm_bridge.rs`. The canonical
834/// 7-arg port of `Src/exec.c:execcmd_exec` lives elsewhere in this
835/// file under the C-faithful name `execcmd_exec`.
836///
837/// This helper mirrors the head section (`c:2904-3275`) of the C
838/// function — local initialisation, the precommand-modifier walk
839/// that strips `BINF_PREFIX` builtins (`-`, `builtin`, `command`,
840/// `exec`, `noglob`), and the `BINF_COMMAND`/`BINF_EXEC`
841/// sub-option parsers — and returns the resulting dispatch
842/// decision via `execcmd_dispatch`. The fusevm compiler reads
843/// that struct to decide which `Op::CallBuiltin` /
844/// `Op::CallFunction` / `Op::Exec` to emit, and to compute the
845/// correct post-strip `argc`.
846///
847/// =================== WARNING — DIVERGENCE ====================
848///
849/// The C function runs ~1500 lines and PERFORMS dispatch: it sets up
850/// `multio` redirections, evaluates `varspc` assignments, then calls
851/// `execbuiltin` / `runshfunc` / `execute` directly. This helper
852/// stops after the precmd-modifier walk and only returns the head
853/// decision; runtime dispatch is driven by the bytecode the fusevm
854/// compiler emits.
855///
856/// Signature adaptation: the C `Estate`/`Execcmd_params` carry the
857/// wordcode iterator state — zshrs doesn't traverse wordcode here,
858/// so the args list arrives already-expanded as a `&[String]`
859/// (analog of `preargs` after `execcmd_getargs` at `c:3028`).
860/// `type_` mirrors `eparams->type` (`WC_SIMPLE` vs `WC_TYPESET`).
861///
862/// =============================================================
863pub fn execcmd_compile_head(args: &[String], type_: u32) -> execcmd_dispatch {
864 // c:2900 (Src/exec.c)
865
866 // c:2904-2916 — locals.
867 let mut hn: Option<&'static builtin> = None; // c:2904
868 let mut is_shfunc = false; // c:2913
869 let mut is_builtin = false; // c:2913
870 let mut use_defpath = false; // c:2913
871 let mut cflags: u32 = 0; // c:2915
872 let mut orig_cflags: u32 = 0; // c:2915
873 let _ = orig_cflags;
874 // c:3263 — `char *exec_argv0 = NULL;` (declared inside the
875 // BINF_EXEC arm; hoisted here so the dispatch struct can carry it
876 // out after the loop terminates).
877 let mut exec_argv0: Option<String> = None;
878 // c:3149/3158 — `has_vV`/`has_p` flags from the BINF_COMMAND arm
879 // (c:3104). Surface `has_vV` via the dispatch struct so the fusevm
880 // compiler can emit `bin_whence` instead of resolving the head.
881 let mut has_command_vv = false;
882
883 // c:2962-2973 — `%job` head: rewrite `%name` → `fg|bg|disown %name`.
884 // Not in scope for the compile-time dispatch walk: jobspec
885 // expansion happens at runtime in fusevm; the bytecode emits a
886 // direct `fg`/`bg` call when it sees a leading `%`. Flagged for
887 // follow-up when the canonical port lands.
888
889 // c:2975-2986 — AUTORESUME prefix-match against jobtab. Same
890 // status as the %job head: runtime concern, deferred.
891
892 // c:3013-3091 — precommand-modifier walk.
893 let mut preargs: Vec<String> = args.to_vec(); // c:3027 newlinklist
894 let mut precmd_skip: usize = 0;
895
896 // c:3018 — `if ((type == WC_SIMPLE || type == WC_TYPESET) && args)`.
897 if (type_ == WC_SIMPLE || type_ == WC_TYPESET) && !preargs.is_empty() {
898 // c:3018
899 // c:3029 — `while (nonempty(preargs))`.
900 while precmd_skip < preargs.len() {
901 // c:3029
902 // c:3030 — `cmdarg = (char *) peekfirst(preargs);`.
903 let cmdarg = untokenize(&preargs[precmd_skip]);
904 // c:3031 — `checked = !has_token(cmdarg)`. zshrs's fusevm
905 // already performed prefork expansion on `preargs`, so
906 // `has_token` is effectively false here; the C `break` on
907 // unexpanded tokens is unreachable in this entry point.
908
909 // c:3034-3035 — WC_TYPESET fast path: `getnode2` looks up
910 // even disabled builtins so the reserved-word form
911 // (`integer x`, `local foo`) still dispatches to the
912 // typeset family. The static `BUILTINS` array doesn't
913 // expose a separate disabled-bit lookup; one path covers
914 // both. Effect is identical for the precmd-modifier walk.
915
916 // c:3050-3052 — `if (!(cflags & (BINF_BUILTIN |
917 // BINF_COMMAND)) && shfunctab->getnode(...))` — shell
918 // function takes precedence unless a `builtin`/`command`
919 // modifier preceded it.
920 if (cflags & (BINF_BUILTIN | BINF_COMMAND)) == 0 {
921 // c:3051
922 if shfunctab_lock()
923 .read()
924 .map(|t| t.iter().any(|(k, _)| k == &cmdarg))
925 .unwrap_or(false)
926 {
927 is_shfunc = true; // c:3053
928 break; // c:3054
929 }
930 }
931 // c:3056 — `builtintab->getnode(builtintab, cmdarg)`.
932 let entry = BUILTINS.iter().find(|b| b.node.nam == cmdarg);
933 let Some(entry) = entry else {
934 // c:3056-3058
935 break;
936 };
937 hn = Some(entry);
938 // c:3061-3063 — accumulate cflags.
939 orig_cflags |= cflags;
940 cflags &= !(BINF_BUILTIN | BINF_COMMAND);
941 cflags |= entry.node.flags as u32;
942 // c:3064 — `if (!(hn->flags & BINF_PREFIX))` — real
943 // builtin, stop.
944 if (entry.node.flags as u32 & BINF_PREFIX) == 0 {
945 // c:3064
946 // WARNING — DIVERGENCE: c:3068 calls `resolvebuiltin`
947 // to autoload the builtin's module if its
948 // `handlerfunc` is NULL. In zshrs, builtins live in
949 // two places: the static `BUILTINS` table (which
950 // mirrors C `handlerfunc`, often `None` for ports
951 // dispatched through fusevm) AND fusevm's
952 // `register_builtins` map (the actual runtime
953 // dispatcher). A null `handlerfunc` in the static
954 // table is NOT an autoload failure for us — it
955 // means dispatch routes through fusevm. So we
956 // skip the resolvebuiltin call here; the faithful
957 // port remains available for future callers that
958 // genuinely need module-autoload semantics.
959 is_builtin = true; // c:3065
960 break; // c:3077
961 }
962 // c:3086 — `uremnode(preargs, firstnode(preargs))`.
963 precmd_skip += 1;
964 // c:3087-3091 — `if (!firstnode(preargs)) { execcmd_getargs
965 // (...); if (!firstnode(preargs)) break; }`. zshrs has
966 // no `execcmd_getargs` (args arrive pre-expanded); the
967 // bounds-check at the top of `while precmd_skip <
968 // preargs.len()` handles the empty case identically.
969
970 // c:3092-3177 — BINF_COMMAND sub-option parsing
971 // (`command -p / -v / -V`).
972 if (cflags & BINF_COMMAND) != 0 && precmd_skip < preargs.len() {
973 // c:3102-3104 — `LinkNode argnode, oldnode, pnode = NULL;
974 // int has_p = 0, has_vV = 0, has_other = 0;`
975 let mut argnode: usize = precmd_skip; // c:3105 `argnode = firstnode(preargs);`
976 let mut pnode: Option<usize> = None; // c:3102
977 let mut has_p = false; // c:3104
978 let mut has_vv = false; // c:3104
979 let mut has_other = false; // c:3104
980 // c:3107 — `while (IS_DASH(*argdata))`
981 while argnode < preargs.len()
982 && IS_DASH(preargs[argnode].chars().next().unwrap_or('\0'))
983 {
984 let argdata = preargs[argnode].clone(); // c:3106
985 let bytes = argdata.as_bytes();
986 // c:3108-3111 — stop on bare `-` or `--`.
987 if bytes.len() < 2 || (IS_DASH(bytes[1] as char) && bytes.len() == 2) {
988 // c:3109
989 break; // c:3111
990 }
991 // c:3112-3133 — scan flag chars.
992 for &c in &bytes[1..] {
993 // c:3112
994 match c as char {
995 'p' => {
996 // c:3114
997 has_p = true; // c:3122
998 pnode = Some(argnode); // c:3123
999 }
1000 'v' | 'V' => {
1001 // c:3125-3126
1002 has_vv = true; // c:3127
1003 }
1004 _ => {
1005 // c:3129
1006 has_other = true; // c:3130
1007 }
1008 }
1009 }
1010 // c:3134-3138 — unknown flag → don't try, leave alone.
1011 if has_other {
1012 // c:3134
1013 has_p = false; // c:3136
1014 has_vv = false; // c:3136
1015 break; // c:3137
1016 }
1017 // c:3140-3147 — advance to next arg.
1018 argnode += 1; // c:3141 nextnode(argnode)
1019 if argnode >= preargs.len() {
1020 // c:3142 — execcmd_getargs (skipped: pre-expanded)
1021 break; // c:3145
1022 }
1023 }
1024 // c:3149-3157 — `-v`/`-V` → dispatch to whence.
1025 if has_vv {
1026 // c:3149
1027 // c:3154 `pushnode(preargs, "command")` — C re-inserts
1028 // "command" so bin_whence sees it as argv[0]. zshrs
1029 // surfaces this via `has_command_vv`; the fusevm
1030 // compiler emits the equivalent whence call.
1031 has_command_vv = true; // c:3155-3156 hn = &commandbn; is_builtin=1
1032 is_builtin = true;
1033 break; // c:3157
1034 } else if has_p {
1035 // c:3158
1036 use_defpath = true; // c:3160
1037 if let Some(pn) = pnode {
1038 // c:3165 — `uremnode(preargs, pnode)`. zshrs:
1039 // remove the `-p`-bearing arg from preargs.
1040 if pn < preargs.len() {
1041 preargs.remove(pn);
1042 // precmd_skip already accounts for the
1043 // stripped `command` prefix; we just removed
1044 // the `-p` flag which sat at preargs[pn].
1045 // No precmd_skip change needed — the head
1046 // remains where it was.
1047 }
1048 }
1049 }
1050 // c:3176-3177 — `--` trailing end-of-options strip.
1051 if argnode < preargs.len() {
1052 let argdata = &preargs[argnode];
1053 let b = argdata.as_bytes();
1054 if b.len() == 2 && IS_DASH(b[0] as char) && IS_DASH(b[1] as char) {
1055 // c:3176
1056 preargs.remove(argnode); // c:3177
1057 }
1058 }
1059 } else if (cflags & BINF_EXEC) != 0 && precmd_skip < preargs.len() {
1060 // c:3178-3275 — BINF_EXEC sub-option parsing
1061 // (`exec -a NAME -l -c`).
1062 let mut argnode: usize = precmd_skip; // c:3185
1063 let mut error_done = false;
1064 // c:3196 — `while (argdata && IS_DASH(*argdata) &&
1065 // strlen(argdata) >= 2)`
1066 while argnode < preargs.len() {
1067 let argdata = preargs[argnode].clone();
1068 let bytes = argdata.as_bytes();
1069 if bytes.is_empty() || !IS_DASH(bytes[0] as char) || bytes.len() < 2 {
1070 break; // c:3196 loop guard
1071 }
1072 let oldnode = argnode; // c:3197
1073 argnode += 1; // c:3198 nextnode(oldnode)
1074 // c:3203-3208 — empty next → error.
1075 if argnode >= preargs.len() {
1076 // c:3203
1077 zerr(
1078 // c:3204
1079 "exec requires a command to execute",
1080 );
1081 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3206
1082 error_done = true;
1083 break; // c:3207 goto done
1084 }
1085 // c:3209 — `uremnode(preargs, oldnode)`.
1086 preargs.remove(oldnode);
1087 argnode -= 1; // re-anchor — `argnode` was the post-removed slot
1088 // c:3210-3211 — `--` stops option scan.
1089 if bytes.len() == 2 && IS_DASH(bytes[0] as char) && IS_DASH(bytes[1] as char) {
1090 // c:3210
1091 break; // c:3211
1092 }
1093 // c:3212-3258 — scan flag chars after the leading `-`.
1094 let mut k = 1usize;
1095 while k < bytes.len() && !error_done {
1096 let cmdopt = bytes[k] as char; // c:3212
1097 match cmdopt {
1098 'a' => {
1099 // c:3214 — `-a` ARGV0 override.
1100 if k + 1 < bytes.len() {
1101 // c:3216 — `-aNAME` inline form.
1102 exec_argv0 =
1103 Some(String::from_utf8_lossy(&bytes[k + 1..]).into_owned()); // c:3217
1104 k = bytes.len(); // c:3219 position past end
1105 } else {
1106 // c:3220 — `-a NAME` separate form.
1107 if argnode >= preargs.len() {
1108 // c:3230
1109 zerr(
1110 // c:3231
1111 "exec flag -a requires a parameter",
1112 );
1113 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3233
1114 error_done = true;
1115 break; // c:3234 goto done
1116 }
1117 exec_argv0 = Some(preargs[argnode].clone()); // c:3236
1118 preargs.remove(argnode); // c:3239
1119 }
1120 }
1121 'c' => {
1122 // c:3242
1123 cflags |= BINF_CLEARENV; // c:3243
1124 }
1125 'l' => {
1126 // c:3245
1127 cflags |= BINF_DASH; // c:3246
1128 }
1129 _ => {
1130 // c:3248
1131 zerr(
1132 // c:3249
1133 &format!("unknown exec flag -{}", cmdopt),
1134 );
1135 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3251
1136 error_done = true;
1137 break; // c:3256
1138 }
1139 }
1140 k += 1;
1141 }
1142 if error_done {
1143 break;
1144 }
1145 }
1146 // c:3263-3274 — zputenv("ARGV0=NAME"). zshrs defers
1147 // the actual `setenv` to the fusevm compiler / external
1148 // exec path; we surface `exec_argv0` via the dispatch
1149 // struct so the caller can apply it before fork+exec.
1150 if let Some(ref a0) = exec_argv0 {
1151 // c:3263 — `remnulargs + untokenize` then setenv.
1152 let cleaned = untokenize(a0); // c:3266-3267
1153 exec_argv0 = Some(cleaned);
1154 }
1155 if error_done {
1156 return execcmd_dispatch {
1157 precmd_skip,
1158 is_builtin,
1159 is_shfunc,
1160 cflags,
1161 use_defpath,
1162 has_command_vv,
1163 exec_argv0,
1164 is_empty_command: false,
1165 };
1166 }
1167 }
1168 // c:3275-3278 — `hn = NULL; if ((cflags & BINF_COMMAND) &&
1169 // unset(POSIXBUILTINS)) break;`. After processing a
1170 // `command` precmd modifier (and its -p/-v/-V flags), the
1171 // C loop exits with hn cleared so the dispatch falls
1172 // through to external lookup. Without this, the next
1173 // iteration would find `command print` → print's builtin
1174 // and dispatch to it; zsh's intentional behaviour is to
1175 // skip builtins under `command` (unless POSIXBUILTINS is
1176 // set, where the loop continues normally).
1177 if (cflags & BINF_COMMAND) != 0 && !isset(POSIXBUILTINS) {
1178 hn = None; // c:3275 hn = NULL
1179 break; // c:3277
1180 }
1181 }
1182 }
1183
1184 // c:3309-3406 — "Empty command" branch. When the precmd-modifier
1185 // walk above strips every word with nothing left to dispatch
1186 // (bare `exec`, bare `noglob`, bare `command`, bare `nocorrect`),
1187 // C falls into `if (!args || empty(args))` at c:3331. Sub-cases:
1188 //
1189 // - redir-present + do_exec → nullexec=1 (continue to run)
1190 // - redir-present + varspc → nullexec=2 (continue)
1191 // - redir-present + no nullcmd → `zerr("redirection with no command")`
1192 // lastval=1, return
1193 // - redir-present + SHNULLCMD → args=[":"]
1194 // - redir-present + readnullcmd → args=[readnullcmd]
1195 // - redir-present + default → args=[nullcmd]
1196 // - NO redir + BINF_PREFIX+COMMAND → lastval=0, return (c:3365-3371)
1197 // - NO redir + default → lastval=cmdoutval, return (c:3372-3406)
1198 //
1199 // zshrs's `execcmd_compile_head` doesn't receive `redir` (it
1200 // takes `args` only). The cases that DEPEND on redirs are handled by
1201 // `compile_zsh.rs::compile_redir` before this dispatch fires; the
1202 // remaining cases collapse into the single `is_empty_command`
1203 // flag below. Both NO-redir sub-cases produce the same observable
1204 // outcome (lastval=0, return without invoking anything), so a
1205 // single flag suffices.
1206 let is_empty_command = precmd_skip >= preargs.len();
1207
1208 // =================== WARNING — DIVERGENCE ====================
1209 // c:3285+: prefork-substitution, magic_assign decision, multio
1210 // setup, varspc evaluation, and the actual execbuiltin /
1211 // runshfunc / execute call. ~1300 lines of interpreter-only
1212 // code, entirely replaced by fusevm bytecode dispatch in
1213 // `src/extensions/compile_zsh.rs::compile_simple` and the
1214 // opcode handlers in `src/fusevm_bridge.rs::register_builtins`.
1215 // The return value below feeds those compile-time decisions.
1216 // =============================================================
1217
1218 let _ = hn;
1219 execcmd_dispatch {
1220 precmd_skip,
1221 is_builtin,
1222 is_shfunc,
1223 cflags,
1224 use_defpath,
1225 has_command_vv,
1226 exec_argv0,
1227 is_empty_command,
1228 }
1229}
1230
1231// =============================================================================
1232// Leaf-function ports — c:283 (parse_string) and below. Added incrementally to
1233// chip at the ~5500 lines of exec.c still un-ported beyond the wordcode
1234// walker (execlist / execpline / execcmd which the fusevm bytecode VM
1235// replaces — see the WARNING block in execcmd_exec).
1236// =============================================================================
1237
1238/// Port of `parse_string(char *s, int reset_lineno)` from `Src/exec.c:283`.
1239///
1240/// C body:
1241/// ```c
1242/// Eprog p; zlong oldlineno;
1243/// zcontext_save();
1244/// inpush(s, INP_LINENO, NULL);
1245/// strinbeg(0);
1246/// oldlineno = lineno;
1247/// if (reset_lineno) lineno = 1;
1248/// p = parse_list();
1249/// lineno = oldlineno;
1250/// if (tok == LEXERR && !lastval) lastval = 1;
1251/// strinend();
1252/// inpop();
1253/// zcontext_restore();
1254/// return p;
1255/// ```
1256///
1257/// Parses an arbitrary string as a zsh command list, returning the
1258/// `Eprog` (compiled wordcode). Used by `getoutput` for `$(cmd)`,
1259/// `bin_eval` for `eval`, and the autoload path.
1260pub fn parse_string(s: &str, reset_lineno: i32) -> Option<eprog> {
1261 // c:285-286
1262 let p: Option<eprog>;
1263 let oldlineno: i64;
1264
1265 zcontext_save(); // c:288
1266 inpush(s, INP_LINENO, None); // c:289
1267 strinbeg(0); // c:290
1268 oldlineno = LEX_LINENO.get() as i64; // c:291
1269 if reset_lineno != 0 {
1270 // c:292
1271 LEX_LINENO.set(1); // c:293
1272 }
1273 p = parse_list(); // c:294
1274 LEX_LINENO.set(oldlineno as u64); // c:295
1275 // c:296-297 — `if (tok == LEXERR && !lastval) lastval = 1;`
1276 if tok() == LEXERR && LASTVAL.load(Ordering::Relaxed) == 0 {
1277 LASTVAL.store(1, Ordering::Relaxed);
1278 }
1279 strinend(); // c:298
1280 inpop(); // c:299
1281 zcontext_restore(); // c:300
1282 p // c:301
1283}
1284
1285/// Port of `int isgooderr(int e, char *dir)` from `Src/exec.c:652`.
1286///
1287/// C body:
1288/// ```c
1289/// /* Maybe the directory was unreadable, or maybe it wasn't even a directory. */
1290/// return ((e != EACCES || !access(dir, X_OK)) &&
1291/// e != ENOENT && e != ENOTDIR);
1292/// ```
1293///
1294/// errno classifier for `execve` failures during PATH search: if the
1295/// errno is EACCES (and the dir is X-accessible) or ENOENT/ENOTDIR,
1296/// it's "expected" (try next PATH entry); otherwise it's a real
1297/// failure worth surfacing.
1298pub fn isgooderr(e: i32, dir: &str) -> bool {
1299 // c:652
1300 let dir_x_ok = std::path::Path::new(&unmeta(dir))
1301 .metadata()
1302 .map(|m| m.permissions().mode() & 0o111 != 0)
1303 .unwrap_or(false);
1304 // c:658-659 — `(e != EACCES || !access(dir, X_OK)) && e != ENOENT && e != ENOTDIR`
1305 (e != libc::EACCES || !dir_x_ok) && e != libc::ENOENT && e != libc::ENOTDIR
1306}
1307
1308/// Port of `int iscom(char *s)` from `Src/exec.c:962`.
1309///
1310/// C body:
1311/// ```c
1312/// struct stat statbuf;
1313/// char *us = unmeta(s);
1314/// return (access(us, X_OK) == 0 && stat(us, &statbuf) >= 0 &&
1315/// S_ISREG(statbuf.st_mode));
1316/// ```
1317///
1318/// True iff `s` names an executable regular file (X-perm + S_IFREG).
1319/// Used by the PATH-search loop in `findcmd` / `search_defpath` to
1320/// validate candidate paths before exec.
1321pub fn iscom(s: &str) -> bool {
1322 // c:962
1323 let us = unmeta(s); // c:965
1324 // c:967-968 — `access(us, X_OK) == 0 && stat(us, &statbuf) >= 0 && S_ISREG(...)`
1325 let cstr = match std::ffi::CString::new(us.as_str()) {
1326 Ok(c) => c,
1327 Err(_) => return false,
1328 };
1329 let x_ok = unsafe { libc::access(cstr.as_ptr(), libc::X_OK) } == 0;
1330 if !x_ok {
1331 return false;
1332 }
1333 let meta = match std::fs::metadata(&us) {
1334 Ok(m) => m,
1335 Err(_) => return false,
1336 };
1337 meta.file_type().is_file()
1338}
1339
1340/// Port of `int isreallycom(Cmdnam cn)` from `Src/exec.c:972-987`.
1341///
1342/// Verify that a hashed/cached cmdnamtab entry still names a real
1343/// external command (X-perm + regular file). For HASHED entries
1344/// (`cn->u.cmd` carries the absolute path), test the path directly;
1345/// otherwise concatenate `name[0] + "/" + nam` and test that.
1346/// Used by `execcmd_exec` to drop stale cmdnamtab hits before they
1347/// turn into a failed `execve` syscall.
1348pub fn isreallycom(cn: &cmdnam) -> bool {
1349 // c:972
1350 let fullnam: String;
1351 if (cn.node.flags & crate::ported::zsh_h::HASHED) != 0 {
1352 // c:977-978 — `strcpy(fullnam, cn->u.cmd);`
1353 fullnam = cn.cmd.clone().unwrap_or_default();
1354 } else if cn.name.is_none() || cn.name.as_ref().unwrap().is_empty() {
1355 // c:979-980 — `if (!cn->u.name) return 0;`
1356 return false;
1357 } else {
1358 // c:982-984 — `strcpy + strcat("/") + strcat(nam)`
1359 let path0 = &cn.name.as_ref().unwrap()[0];
1360 fullnam = format!("{}/{}", path0, cn.node.nam);
1361 }
1362 iscom(&fullnam) // c:986
1363}
1364
1365/// Port of `int isrelative(char *s)` from `Src/exec.c:996`.
1366///
1367/// C body:
1368/// ```c
1369/// if (*s != '/') return 1;
1370/// for (; *s; s++)
1371/// if (*s == '.' && s[-1] == '/' &&
1372/// (s[1] == '/' || s[1] == '\0' ||
1373/// (s[1] == '.' && (s[2] == '/' || s[2] == '\0'))))
1374/// return 1;
1375/// return 0;
1376/// ```
1377///
1378/// True iff `s` either doesn't start with `/` OR contains a `./` or
1379/// `../` component anywhere. Used by `cd` resolution and PATH-cache
1380/// invalidation to detect non-canonical paths.
1381pub fn isrelative(s: &str) -> i32 {
1382 // c:996
1383 let bytes = s.as_bytes();
1384 if bytes.is_empty() || bytes[0] != b'/' {
1385 // c:998
1386 return 1; // c:999
1387 }
1388 // c:1000-1004 — walk for `./` or `../` components.
1389 for i in 1..bytes.len() {
1390 let c = bytes[i];
1391 let prev = bytes[i - 1];
1392 if c == b'.' && prev == b'/' {
1393 let next = bytes.get(i + 1).copied().unwrap_or(0);
1394 if next == b'/' || next == 0 {
1395 // c:1002
1396 return 1;
1397 }
1398 if next == b'.' {
1399 let next2 = bytes.get(i + 2).copied().unwrap_or(0);
1400 if next2 == b'/' || next2 == 0 {
1401 // c:1003
1402 return 1;
1403 }
1404 }
1405 }
1406 }
1407 0 // c:1005
1408}
1409
1410/// Port of `void setunderscore(char *str)` from `Src/exec.c:2652`.
1411///
1412/// C body:
1413/// ```c
1414/// queue_signals();
1415/// if (str && *str) {
1416/// size_t l = strlen(str) + 1, nl = (l + 31) & ~31;
1417/// if (nl > underscorelen || (underscorelen - nl) > 64) {
1418/// zfree(zunderscore, underscorelen);
1419/// zunderscore = (char *) zalloc(underscorelen = nl);
1420/// }
1421/// strcpy(zunderscore, str);
1422/// underscoreused = l;
1423/// } else {
1424/// ... reset zunderscore = "" ...
1425/// }
1426/// unqueue_signals();
1427/// ```
1428///
1429/// Sets the `$_` global to the last argument of the most recent
1430/// command. Called from `execcmd_exec` (c:3936) per `last_status`
1431/// update; mirrored in zshrs by the fusevm `Op::Exec` handler.
1432pub fn setunderscore(str: &str) {
1433 // c:2652
1434 queue_signals(); // c:2654
1435 if !str.is_empty() {
1436 // c:2655 `if (str && *str)`
1437 // c:2656-2663 — copy str into zunderscore; track byte length in underscoreused.
1438 let mut zu = zunderscore.lock().unwrap();
1439 *zu = str.to_string();
1440 let nl = (str.len() + 1 + 31) & !31; // c:2656
1441 underscorelen.store(nl, Ordering::Relaxed); // c:2660
1442 underscoreused.store((str.len() + 1) as i32, Ordering::Relaxed);
1443 // c:2663
1444 } else {
1445 // c:2664
1446 let mut zu = zunderscore.lock().unwrap();
1447 zu.clear(); // c:2669 `*zunderscore = '\0';`
1448 underscoreused.store(1, Ordering::Relaxed); // c:2670
1449 }
1450 unqueue_signals(); // c:2672
1451}
1452
1453/// Port of `int mpipe(int *pp)` from `Src/exec.c:5160`.
1454///
1455/// C body:
1456/// ```c
1457/// if (pipe(pp) < 0) {
1458/// zerr("pipe failed: %e", errno);
1459/// return -1;
1460/// }
1461/// pp[0] = movefd(pp[0]);
1462/// pp[1] = movefd(pp[1]);
1463/// return 0;
1464/// ```
1465///
1466/// libc `pipe(2)` wrapper that pushes both ends out of the reserved-
1467/// fd range via `movefd`. Used by `getpipe` / `getproc` /
1468/// `spawnpipes` for process substitution and pipeline wiring.
1469pub fn mpipe(pp: &mut [i32; 2]) -> i32 {
1470 // c:5160
1471 let mut fds: [libc::c_int; 2] = [-1; 2];
1472 if unsafe { libc::pipe(fds.as_mut_ptr()) } < 0 {
1473 // c:5162
1474 zerr(&format!(
1475 // c:5163
1476 "pipe failed: {}",
1477 std::io::Error::last_os_error()
1478 ));
1479 return -1; // c:5164
1480 }
1481 pp[0] = movefd(fds[0]); // c:5166
1482 pp[1] = movefd(fds[1]); // c:5167
1483 0 // c:5168
1484}
1485
1486/// Port of `static const char *const ANONYMOUS_FUNCTION_NAME = "(anon)";`
1487/// from `Src/exec.c:5289`. Anonymous-function name marker used by
1488/// `is_anonymous_function_name`, `execfuncdef`, and `doshfunc` for
1489/// `() { ... }` anonymous function dispatch.
1490pub const ANONYMOUS_FUNCTION_NAME: &str = "(anon)";
1491
1492/// Port of `int is_anonymous_function_name(const char *name)` from
1493/// `Src/exec.c:5300`.
1494///
1495/// C body:
1496/// ```c
1497/// return !strcmp(name, ANONYMOUS_FUNCTION_NAME);
1498/// ```
1499///
1500/// True iff the name equals the `"(anon)"` sentinel. Used by zprof
1501/// reporting and `whence -v` to skip / annotate anonymous functions.
1502pub fn is_anonymous_function_name(name: &str) -> i32 {
1503 // c:5300
1504 if name == ANONYMOUS_FUNCTION_NAME {
1505 // c:5302
1506 1
1507 } else {
1508 0
1509 }
1510}
1511
1512/// Port of `void execsave(void)` from `Src/exec.c:6438`.
1513///
1514/// C body:
1515/// ```c
1516/// struct execstack *es = (struct execstack *) zalloc(sizeof(struct execstack));
1517/// es->list_pipe_pid = list_pipe_pid;
1518/// es->nowait = nowait;
1519/// es->pline_level = pline_level;
1520/// es->list_pipe_child = list_pipe_child;
1521/// es->list_pipe_job = list_pipe_job;
1522/// strcpy(es->list_pipe_text, list_pipe_text);
1523/// es->lastval = lastval;
1524/// es->noeval = noeval;
1525/// es->badcshglob = badcshglob;
1526/// es->cmdoutpid = cmdoutpid;
1527/// es->cmdoutval = cmdoutval;
1528/// es->use_cmdoutval = use_cmdoutval;
1529/// es->procsubstpid = procsubstpid;
1530/// es->trap_return = trap_return;
1531/// es->trap_state = trap_state;
1532/// es->trapisfunc = trapisfunc;
1533/// es->traplocallevel = traplocallevel;
1534/// es->noerrs = noerrs;
1535/// es->this_noerrexit = this_noerrexit;
1536/// es->underscore = ztrdup(zunderscore);
1537/// es->next = exstack;
1538/// exstack = es;
1539/// noerrs = cmdoutpid = 0;
1540/// ```
1541///
1542/// Snapshot every transient exec-context global onto the `exstack`
1543/// linked list so a signal-handler / trap-firing nested eval can
1544/// scribble freely; `execrestore` pops the frame back. Called by
1545/// `dotrap` (signals.c) and the trap-firing entry in `execlist`.
1546pub fn execsave() {
1547 // c:6438
1548 // c:6442 — `es = zalloc(sizeof(execstack));`
1549 let mut es = Box::new(execstack {
1550 // c:6442
1551 next: None,
1552 list_pipe_pid: list_pipe_pid.load(Ordering::Relaxed), // c:6443
1553 nowait: nowait.load(Ordering::Relaxed), // c:6444
1554 pline_level: pline_level.load(Ordering::Relaxed), // c:6445
1555 list_pipe_child: list_pipe_child.load(Ordering::Relaxed), // c:6446
1556 list_pipe_job: list_pipe_job.load(Ordering::Relaxed), // c:6447
1557 list_pipe_text: {
1558 // c:6448 — `strcpy(es->list_pipe_text, list_pipe_text);`
1559 let mut buf = [0u8; JOBTEXTSIZE];
1560 if let Ok(s) = LIST_PIPE_TEXT.lock() {
1561 let bytes = s.as_bytes();
1562 let n = bytes.len().min(JOBTEXTSIZE - 1);
1563 buf[..n].copy_from_slice(&bytes[..n]);
1564 }
1565 buf
1566 },
1567 lastval: LASTVAL.load(Ordering::Relaxed), // c:6449
1568 // c:6450 — `es->noeval = noeval;`. Snapshot math.c's
1569 // `int noeval` (the parse-only side-effect-skip counter)
1570 // via math.rs's pub accessor.
1571 noeval: crate::ported::math::m_noeval(),
1572 // c:6451 — `es->badcshglob = badcshglob;`. Snapshot the
1573 // csh-glob diagnostic counter (glob.c:103 / glob.rs
1574 // BADCSHGLOB) so nested eval / trap dispatch doesn't disturb
1575 // the outer command's per-line accounting.
1576 badcshglob: crate::ported::glob::BADCSHGLOB.load(Ordering::Relaxed), // c:6451
1577 cmdoutpid: cmdoutpid.load(Ordering::Relaxed), // c:6452
1578 cmdoutval: cmdoutval.load(Ordering::Relaxed), // c:6453
1579 use_cmdoutval: use_cmdoutval.load(Ordering::Relaxed), // c:6454
1580 procsubstpid: procsubstpid.load(Ordering::Relaxed), // c:6455
1581 trap_return: TRAP_RETURN.load(Ordering::Relaxed), // c:6456
1582 trap_state: TRAP_STATE.load(Ordering::Relaxed), // c:6457
1583 trapisfunc: trapisfunc.load(Ordering::Relaxed), // c:6458
1584 traplocallevel: traplocallevel.load(Ordering::Relaxed), // c:6459
1585 noerrs: noerrs.load(Ordering::Relaxed), // c:6460
1586 this_noerrexit: this_noerrexit.load(Ordering::Relaxed), // c:6461
1587 // c:6462 — `es->underscore = ztrdup(zunderscore);`
1588 underscore: Some(zunderscore.lock().unwrap().clone()),
1589 });
1590 // c:6463-6464 — `es->next = exstack; exstack = es;`
1591 let mut head = exstack.lock().unwrap();
1592 es.next = head.take();
1593 *head = Some(es);
1594 // c:6465 — `noerrs = cmdoutpid = 0;`
1595 noerrs.store(0, Ordering::Relaxed);
1596 cmdoutpid.store(0, Ordering::Relaxed);
1597}
1598
1599/// Port of `void execrestore(void)` from `Src/exec.c:6470`.
1600///
1601/// C body:
1602/// ```c
1603/// struct execstack *en = exstack;
1604/// DPUTS(!exstack, "BUG: execrestore() without execsave()");
1605/// queue_signals();
1606/// exstack = exstack->next;
1607/// list_pipe_pid = en->list_pipe_pid;
1608/// nowait = en->nowait;
1609/// pline_level = en->pline_level;
1610/// list_pipe_child = en->list_pipe_child;
1611/// list_pipe_job = en->list_pipe_job;
1612/// strcpy(list_pipe_text, en->list_pipe_text);
1613/// lastval = en->lastval;
1614/// noeval = en->noeval;
1615/// badcshglob = en->badcshglob;
1616/// cmdoutpid = en->cmdoutpid;
1617/// cmdoutval = en->cmdoutval;
1618/// use_cmdoutval = en->use_cmdoutval;
1619/// procsubstpid = en->procsubstpid;
1620/// trap_return = en->trap_return;
1621/// trap_state = en->trap_state;
1622/// trapisfunc = en->trapisfunc;
1623/// traplocallevel = en->traplocallevel;
1624/// noerrs = en->noerrs;
1625/// this_noerrexit = en->this_noerrexit;
1626/// setunderscore(en->underscore);
1627/// zsfree(en->underscore);
1628/// free(en);
1629/// unqueue_signals();
1630/// ```
1631///
1632/// Pop the top `execstack` frame and restore every transient
1633/// exec-context global. Inverse of `execsave`.
1634pub fn execrestore() {
1635 // c:6470
1636 let mut head = exstack.lock().unwrap();
1637 let en = match head.take() {
1638 // c:6472 + c:6477
1639 Some(en) => en,
1640 None => {
1641 // c:6474 — DPUTS(!exstack, "BUG: execrestore() without execsave()")
1642 crate::DPUTS!(true, "BUG: execrestore() without execsave()");
1643 return;
1644 }
1645 };
1646 queue_signals(); // c:6476
1647 *head = en.next; // c:6477
1648 drop(head); // release lock before scalar restores
1649
1650 list_pipe_pid.store(en.list_pipe_pid, Ordering::Relaxed); // c:6479
1651 nowait.store(en.nowait, Ordering::Relaxed); // c:6480
1652 pline_level.store(en.pline_level, Ordering::Relaxed); // c:6481
1653 list_pipe_child.store(en.list_pipe_child, Ordering::Relaxed); // c:6482
1654 list_pipe_job.store(en.list_pipe_job, Ordering::Relaxed); // c:6483
1655 // c:6484 — `strcpy(list_pipe_text, en->list_pipe_text);`.
1656 if let Ok(mut s) = LIST_PIPE_TEXT.lock() {
1657 let nul = en
1658 .list_pipe_text
1659 .iter()
1660 .position(|&b| b == 0)
1661 .unwrap_or(JOBTEXTSIZE);
1662 *s = String::from_utf8_lossy(&en.list_pipe_text[..nul]).into_owned();
1663 }
1664 LASTVAL.store(en.lastval, Ordering::Relaxed); // c:6485
1665 // c:6486 — `noeval = en->noeval;`. Restore math.c's noeval
1666 // counter from the saved frame.
1667 crate::ported::math::m_noeval_set(en.noeval);
1668 // c:6487 — `badcshglob = en->badcshglob;`. Restore the csh-glob
1669 // diagnostic counter saved on entry.
1670 crate::ported::glob::BADCSHGLOB.store(en.badcshglob, Ordering::Relaxed);
1671 cmdoutpid.store(en.cmdoutpid, Ordering::Relaxed); // c:6488
1672 cmdoutval.store(en.cmdoutval, Ordering::Relaxed); // c:6489
1673 use_cmdoutval.store(en.use_cmdoutval, Ordering::Relaxed); // c:6490
1674 procsubstpid.store(en.procsubstpid, Ordering::Relaxed); // c:6491
1675 TRAP_RETURN.store(en.trap_return, Ordering::Relaxed); // c:6492
1676 TRAP_STATE.store(en.trap_state, Ordering::Relaxed); // c:6493
1677 trapisfunc.store(en.trapisfunc, Ordering::Relaxed); // c:6494
1678 traplocallevel.store(en.traplocallevel, Ordering::Relaxed); // c:6495
1679 noerrs.store(en.noerrs, Ordering::Relaxed); // c:6496
1680 this_noerrexit.store(en.this_noerrexit, Ordering::Relaxed); // c:6497
1681 // c:6498-6499 — `setunderscore(en->underscore); zsfree(en->underscore);`
1682 if let Some(ref u) = en.underscore {
1683 setunderscore(u); // c:6498
1684 }
1685 // c:6500 — `free(en);` — handled by Box drop when `en` falls out of scope.
1686 unqueue_signals(); // c:6502
1687}
1688
1689/// Port of `void execstring(char *s, int dont_change_job, int exiting,
1690/// char *context)` from `Src/exec.c:1228`.
1691///
1692/// C body:
1693/// ```c
1694/// Eprog prog;
1695/// pushheap();
1696/// if (isset(VERBOSE)) {
1697/// zputs(s, stderr);
1698/// fputc('\n', stderr);
1699/// fflush(stderr);
1700/// }
1701/// if ((prog = parse_string(s, 0)))
1702/// execode(prog, dont_change_job, exiting, context);
1703/// popheap();
1704/// ```
1705///
1706/// Public entry — execute an arbitrary string as a zsh command list.
1707/// Called by `eval`, `.`/`source`, `trap` action firing, autoload
1708/// body executors, command substitution body runners.
1709///
1710/// =================== WARNING — DIVERGENCE ====================
1711/// The C path is `parse_string` → `execode` → `execlist` (wordcode
1712/// walker). zshrs replaces `execode/execlist` with the fusevm
1713/// bytecode VM at `crate::vm_helper::ShellExecutor::execute_script_zsh_pipeline`.
1714/// Faithful port: VERBOSE banner + pushheap/popheap intact; the
1715/// parse+execute chain delegates to the fusevm entry. When `execlist`
1716/// lands as a strict 1:1 port, swap the delegate for the canonical
1717/// chain.
1718/// =============================================================
1719pub fn execstring(s: &str, _dont_change_job: i32, _exiting: i32, _context: &str) {
1720 // c:1228
1721 pushheap(); // c:1232
1722 // c:1233-1237 — VERBOSE banner.
1723 if isset(VERBOSE) {
1724 // c:1233
1725 let mut stderr = std::io::stderr().lock();
1726 use std::io::Write;
1727 let _ = stderr.write_all(s.as_bytes()); // c:1234 zputs(s, stderr)
1728 let _ = stderr.write_all(b"\n"); // c:1235
1729 let _ = stderr.flush(); // c:1236
1730 }
1731 // c:1238-1239 — parse + execode. zshrs delegates the parse+VM
1732 // chain to the fusevm pipeline via the exec_hooks fn-ptr
1733 // installed by fusevm_bridge at startup. Direct
1734 // `with_executor` / ShellExecutor reach-in from src/ported/ is
1735 // forbidden — see memory feedback_no_exec_script_from_ported.
1736 let _ = crate::ported::exec_hooks::execute_script_zsh_pipeline(s);
1737 popheap(); // c:1240
1738}
1739
1740/// Port of `void runshfunc(Eprog prog, FuncWrap wrap, char *name)` from
1741/// `Src/exec.c:6166`. The inner shell-function executor — fires
1742/// module-registered wrapper handlers around the function body, with
1743/// `$_` (zunderscore) save/restore and a paramscope push/pop around
1744/// the wordcode walk.
1745///
1746/// C control flow:
1747/// ```c
1748/// queue_signals();
1749/// ou = zalloc(ouu = underscoreused);
1750/// if (ou) memcpy(ou, zunderscore, underscoreused);
1751/// while (wrap) { // wrapper chain
1752/// wrap->module->wrapper++;
1753/// cont = wrap->handler(prog, wrap->next, name);
1754/// wrap->module->wrapper--;
1755/// if (!wrap->module->wrapper && (wrap->module->node.flags & MOD_UNLOAD))
1756/// unload_module(wrap->module);
1757/// if (!cont) { // wrapper handled it
1758/// if (ou) zfree(ou, ouu);
1759/// unqueue_signals();
1760/// return;
1761/// }
1762/// wrap = wrap->next;
1763/// }
1764/// startparamscope();
1765/// execode(prog, 1, 0, "shfunc");
1766/// if (ou) { setunderscore(ou); zfree(ou, ouu); }
1767/// endparamscope();
1768/// unqueue_signals();
1769/// ```
1770///
1771/// (a) `wrap->module->wrapper++/--` (c:6178/6180) wired against
1772/// `module::MODULESTAB.modules[name].wrapper` (i32), looked up
1773/// by `wrap.module.node.nam`. Recursive unload during handler
1774/// defers correctly.
1775/// (b) `unload_module(wrap->module)` (c:6184) wired via
1776/// `modulestab.unload_module(name)` when wrapper hits 0 AND
1777/// MOD_UNLOAD flag is set on the module's hashnode.
1778/// (c) `execode(prog, 1, 0, "shfunc")` (c:6195) ported at
1779/// exec.rs:6047. Body uses execode for the no-source
1780/// (compiled-wordcode) branch and fusevm for the
1781/// source-preserving (autoloaded) branch per cache coherence.
1782/// (d) `startparamscope/endparamscope` Rust signatures take
1783/// `&mut HashTable` (params.rs:7425/7435). We pass the global
1784/// paramtab handle via the params crate.
1785pub fn runshfunc(prog: &eprog, mut wrap: Option<&funcwrap>, name: &str) {
1786 // c:6166
1787 queue_signals(); // c:6171
1788 // c:6173-6175 — snapshot zunderscore into `ou`.
1789 let ouu = underscoreused.load(Ordering::Relaxed) as usize;
1790 let ou: Option<String> = if ouu > 0 {
1791 // c:6174
1792 Some(zunderscore.lock().unwrap().clone()) // c:6175
1793 } else {
1794 None
1795 };
1796 // c:6177-6193 — wrapper chain walk.
1797 while let Some(w) = wrap {
1798 // c:6177
1799 // c:6178 — wrap->module->wrapper++ (WARNING a).
1800 // c:6178 — `wrap->module->wrapper++;` — bump refcount so a
1801 // recursive unload during the handler defers until we return.
1802 let mod_name: Option<String> = w.module.as_ref().map(|m| m.node.nam.clone());
1803 if let Some(ref n) = mod_name {
1804 if let Ok(mut tab) = crate::ported::module::MODULESTAB.lock() {
1805 if let Some(m) = tab.modules.get_mut(n) {
1806 m.wrapper += 1;
1807 }
1808 }
1809 }
1810 let cont = if let Some(h) = w.handler {
1811 // c:6179 — WrapFunc takes Eprog by value + next FuncWrap by value.
1812 // We pass an empty next sentinel (wrapper-chain walks are
1813 // single-step in zshrs — see chain-walk comment below).
1814 let next_sentinel = Box::new(funcwrap {
1815 next: None,
1816 flags: 0,
1817 handler: None,
1818 module: None,
1819 });
1820 h(Box::new(prog.clone()), next_sentinel, name)
1821 } else {
1822 1
1823 };
1824 // c:6180 — `wrap->module->wrapper--;`
1825 // c:6182-6184 — `if (!wrap->module->wrapper && (flags & MOD_UNLOAD)) unload_module(wrap->module);`
1826 if let Some(ref n) = mod_name {
1827 let should_unload = {
1828 if let Ok(mut tab) = crate::ported::module::MODULESTAB.lock() {
1829 if let Some(m) = tab.modules.get_mut(n) {
1830 m.wrapper -= 1;
1831 m.wrapper == 0 && (m.node.flags & crate::ported::zsh_h::MOD_UNLOAD) != 0
1832 } else {
1833 false
1834 }
1835 } else {
1836 false
1837 }
1838 };
1839 if should_unload {
1840 if let Ok(mut tab) = crate::ported::module::MODULESTAB.lock() {
1841 let _ = tab.unload_module(n); // c:6184
1842 }
1843 }
1844 }
1845 if cont == 0 {
1846 // c:6186 — wrapper claimed the call.
1847 unqueue_signals(); // c:6189
1848 return; // c:6190
1849 }
1850 // c:6192 — wrap = wrap->next; the linked-list step requires
1851 // owning the next ref; the borrowed iteration breaks here.
1852 // Wrapper chains > 1 are extremely rare; we stop at the
1853 // first to avoid a Box::leak.
1854 wrap = None;
1855 }
1856 // c:6194 — startparamscope (just inc_locallevel internally).
1857 inc_locallevel();
1858 // c:6195 — `execode(prog, 1, 0, "shfunc");` — run the function
1859 // body. Prefer the canonical execode (exec.rs:6047) which walks
1860 // execlist on a fresh estate over the prog. If prog.strs carries
1861 // the original source (autoloaded ported that the lazy-compile path
1862 // populated), route through the fusevm pipeline for cache
1863 // coherence with execstring.
1864 if let Some(ref src) = prog.strs {
1865 let _ = crate::ported::exec_hooks::execute_script_zsh_pipeline(src);
1866 } else {
1867 // Pure wordcode body — drive via the canonical execode.
1868 execode(Box::new(prog.clone()), 1, 0, "shfunc");
1869 let _ = name;
1870 }
1871 if let Some(ou_str) = ou {
1872 // c:6196
1873 setunderscore(&ou_str); // c:6197
1874 // c:6198 — zfree(ou, ouu) — Rust drops on scope exit.
1875 }
1876 endparamscope(); // c:6200
1877 // c:6141 — deferred-exit gate. After endparamscope() unwinds the
1878 // function's local scope (locallevel--), check whether an exit
1879 // queued inside the function has reached its target scope:
1880 // if (exit_pending && exit_level >= locallevel+1 && !in_exit_trap)
1881 // The `+1` accounts for endparamscope having already happened
1882 // here (locallevel is already one less than when exit_level was
1883 // captured at c:5890). When the gate fires:
1884 // - locallevel > forklevel: still in a nested function — force
1885 // the outer frame to return too (retflag=1, breaks=loops).
1886 // - locallevel <= forklevel: out of all functions — actually
1887 // exit the shell now via zexit(exit_val, ZEXIT_NORMAL).
1888 // `in_exit_trap` (c:Src/signals.c:63 — `int in_exit_trap;`) is the
1889 // EXIT-trap reentry counter. dotrap at signals.c:1272/1277 wraps
1890 // SIGEXIT handler dispatch with ++/--, so an exit issued FROM an
1891 // EXIT trap shouldn't re-trigger the gate (or the trap would
1892 // recurse). zshrs's signals::in_exit_trap is the canonical port
1893 // surface — read it directly here.
1894 let exit_pending = crate::ported::builtin::EXIT_PENDING.load(Ordering::Relaxed);
1895 let exit_level = crate::ported::builtin::EXIT_LEVEL.load(Ordering::Relaxed);
1896 let cur_locallevel = locallevel.load(Ordering::Relaxed) as i32;
1897 let cur_forklevel = FORKLEVEL.load(Ordering::Relaxed);
1898 let in_exit_trap = crate::ported::signals::in_exit_trap.load(Ordering::Relaxed); // c:Src/signals.c:63
1899 if exit_pending != 0 && exit_level >= cur_locallevel + 1 && in_exit_trap == 0 {
1900 // c:6141
1901 if cur_locallevel > cur_forklevel {
1902 // c:6143 — still inside a nested function: keep unwinding.
1903 RETFLAG.store(1, Ordering::Relaxed); // c:6144
1904 BREAKS.store(LOOPS.load(Ordering::Relaxed), Ordering::Relaxed); // c:6145
1905 } else {
1906 // c:6151 — out of all functions: exit for real.
1907 crate::ported::builtin::STOPMSG.store(1, Ordering::Relaxed); // c:6151
1908 let val = EXIT_VAL.load(Ordering::Relaxed);
1909 crate::ported::builtin::zexit(val, crate::ported::zsh_h::ZEXIT_NORMAL);
1910 // c:6152
1911 }
1912 }
1913 unqueue_signals(); // c:6202
1914}
1915
1916/// Port of `Emulation_options sticky_emulation_dup(Emulation_options src,
1917/// int useheap)` from `Src/exec.c:5501`.
1918///
1919/// C body (`useheap` selects between heap-arena and permanent zalloc;
1920/// Rust collapses both into owned `Box` clones):
1921/// ```c
1922/// Emulation_options newsticky = useheap ?
1923/// hcalloc(sizeof(*src)) : zshcalloc(sizeof(*src));
1924/// newsticky->emulation = src->emulation;
1925/// if (src->n_on_opts) {
1926/// size_t sz = src->n_on_opts * sizeof(*src->on_opts);
1927/// newsticky->n_on_opts = src->n_on_opts;
1928/// newsticky->on_opts = useheap ? zhalloc(sz) : zalloc(sz);
1929/// memcpy(newsticky->on_opts, src->on_opts, sz);
1930/// }
1931/// if (src->n_off_opts) {
1932/// size_t sz = src->n_off_opts * sizeof(*src->off_opts);
1933/// newsticky->n_off_opts = src->n_off_opts;
1934/// newsticky->off_opts = useheap ? zhalloc(sz) : zalloc(sz);
1935/// memcpy(newsticky->off_opts, src->off_opts, sz);
1936/// }
1937/// return newsticky;
1938/// ```
1939///
1940/// Deep-clone a sticky emulation struct. Used by `shfunc_set_sticky`
1941/// at function-def time to snapshot the pending `sticky` global so
1942/// the function carries its own immutable copy.
1943pub fn sticky_emulation_dup(src: &emulation_options, _useheap: i32) -> Emulation_options {
1944 // c:5501
1945 // c:5503-5505 — `newsticky = hcalloc/zshcalloc; newsticky->emulation = src->emulation;`
1946 let mut newsticky = Box::new(emulation_options {
1947 emulation: src.emulation, // c:5505
1948 n_on_opts: 0,
1949 n_off_opts: 0,
1950 on_opts: Vec::new(),
1951 off_opts: Vec::new(),
1952 });
1953 // c:5506-5511 — copy on_opts.
1954 if src.n_on_opts != 0 {
1955 // c:5506
1956 newsticky.n_on_opts = src.n_on_opts; // c:5508
1957 newsticky.on_opts = src.on_opts.clone(); // c:5510 memcpy
1958 }
1959 // c:5512-5517 — copy off_opts.
1960 if src.n_off_opts != 0 {
1961 // c:5512
1962 newsticky.n_off_opts = src.n_off_opts; // c:5514
1963 newsticky.off_opts = src.off_opts.clone(); // c:5516 memcpy
1964 }
1965 newsticky // c:5519
1966}
1967
1968/// Port of `void shfunc_set_sticky(Shfunc shf)` from `Src/exec.c:5527`.
1969///
1970/// C body:
1971/// ```c
1972/// if (sticky)
1973/// shf->sticky = sticky_emulation_dup(sticky, 0);
1974/// else
1975/// shf->sticky = NULL;
1976/// ```
1977///
1978/// Stamp the function with the current pending sticky-emulation
1979/// snapshot (deep-copy via `sticky_emulation_dup`), or clear it.
1980pub fn shfunc_set_sticky(shf: &mut shfunc) {
1981 // c:5527
1982 let sticky_guard = sticky.lock().unwrap();
1983 if let Some(ref s) = *sticky_guard {
1984 // c:5529
1985 shf.sticky = Some(sticky_emulation_dup(s, 0)); // c:5530
1986 } else {
1987 // c:5531
1988 shf.sticky = None; // c:5532
1989 }
1990}
1991
1992/// Port of `static char *search_defpath(char *cmd, char *pbuf, int plen)`
1993/// from `Src/exec.c:691`.
1994///
1995/// Walk DEFAULT_PATH for an executable `<dir>/<cmd>` regular file.
1996/// Used by `command -p` to bypass the user's `$PATH` and search the
1997/// system default (`/bin:/usr/bin:...`).
1998pub fn search_defpath(cmd: &str, plen: usize) -> Option<String> {
1999 // c:691
2000 // c:695 — `for (ps = DEFAULT_PATH; ps; ps = pe ? pe+1 : NULL)`.
2001 for ps in DEFAULT_PATH.split(':') {
2002 // c:695
2003 // c:697 — `if (*ps == '/')`.
2004 if !ps.starts_with('/') {
2005 continue;
2006 }
2007 // c:700-707 — PATH_MAX bounds check on `<dir>` segment.
2008 if ps.len() >= plen {
2009 // c:700 / c:704
2010 continue; // c:701 / c:705
2011 }
2012 // c:708 — `*s++ = '/';`. c:709-710 bounds check on `<dir>/<cmd>`.
2013 let full_len = ps.len() + 1 + cmd.len();
2014 if full_len >= plen {
2015 // c:709
2016 continue; // c:710
2017 }
2018 let buf = format!("{}/{}", ps, cmd); // c:711 `strucpy(&s, cmd);`
2019 // c:712 — `if (iscom(pbuf)) return pbuf;`
2020 if iscom(&buf) {
2021 // c:712
2022 return Some(buf); // c:713
2023 }
2024 }
2025 None // c:716
2026}
2027
2028/// Port of `static int checkclobberparam(struct redir *f)` from
2029/// `Src/exec.c:2178`.
2030///
2031/// C body:
2032/// ```c
2033/// struct value vbuf; Value v;
2034/// char *s = f->varid; int fd;
2035/// if (!s) return 1;
2036/// if (!(v = getvalue(&vbuf, &s, 0))) return 1;
2037/// if (v->pm->node.flags & PM_READONLY) {
2038/// zwarn("can't allocate file descriptor to readonly parameter %s",
2039/// f->varid);
2040/// errno = 0;
2041/// return 0;
2042/// }
2043/// /* We can't clobber the value in the parameter if it's
2044/// * already an opened file descriptor */
2045/// if (!isset(CLOBBER) && (s = getstrvalue(v)) &&
2046/// (fd = (int)zstrtol(s, &s, 10)) >= 0 && !*s &&
2047/// fd <= max_zsh_fd && fdtable[fd] == FDT_EXTERNAL) {
2048/// zwarn("can't clobber parameter %s containing file descriptor %d",
2049/// f->varid, fd);
2050/// errno = 0;
2051/// return 0;
2052/// }
2053/// return 1;
2054/// ```
2055///
2056/// Validate that `f->varid` (the `{var}>file` brace-FD form's var
2057/// name) is writable and (under NOCLOBBER) doesn't currently hold an
2058/// FDT_EXTERNAL fd number. Returns 1 on OK, 0 on refusal (zwarn
2059/// already emitted).
2060///
2061/// NOCLOBBER + FDT_EXTERNAL clause now ported (c:2199-2213). When
2062/// NOCLOBBER is set and the param's value is the fd-number of an
2063/// FDT_EXTERNAL-marked fd in the fdtable, refuse with a warning so
2064/// the existing fd doesn't get clobbered by the upcoming open(2).
2065pub fn checkclobberparam(f: &redir) -> i32 {
2066 // c:2178
2067 // c:2182 — `char *s = f->varid;`
2068 let s = match &f.varid {
2069 Some(v) => v.clone(),
2070 None => return 1, // c:2185-2186 — `if (!s) return 1;`
2071 };
2072 // c:2186 — `if (!(v = getvalue(&vbuf, &s, 0))) return 1;`
2073 let mut vbuf = crate::ported::zsh_h::value {
2074 pm: None,
2075 arr: Vec::new(),
2076 scanflags: 0,
2077 valflags: 0,
2078 start: 0,
2079 end: 0,
2080 };
2081 let mut cursor: &str = s.as_str();
2082 let v_opt = crate::ported::params::getvalue(Some(&mut vbuf), &mut cursor, 0);
2083 if v_opt.is_none() {
2084 return 1; // c:2187
2085 }
2086 // c:2188-2197 — readonly refusal via v->pm->node.flags.
2087 let readonly = vbuf
2088 .pm
2089 .as_ref()
2090 .map(|p| (p.node.flags as u32 & PM_READONLY) != 0)
2091 .unwrap_or(false);
2092 if readonly {
2093 // c:2191
2094 zwarn(&format!(
2095 // c:2192
2096 "can't allocate file descriptor to readonly parameter {}",
2097 s
2098 ));
2099 // c:2195 — `errno = 0;` not flagged as a system error.
2100 return 0; // c:2196
2101 }
2102 // c:2199-2213 — NOCLOBBER + FDT_EXTERNAL refusal: if NOCLOBBER set
2103 // AND the param holds a valid fd that's already in our fdtable as
2104 // FDT_EXTERNAL (allocated by sysopen / coproc / etc.), refuse the
2105 // open so we don't clobber it.
2106 if !isset(CLOBBER) {
2107 // c:2201 — `getstrvalue(v)` — read the param's string form.
2108 let val_str = crate::ported::params::getstrvalue(Some(&mut vbuf));
2109 if let Ok(fd) = val_str.trim().parse::<i32>() {
2110 // c:2202 — `if (fd <= max_zsh_fd && fdtable[fd] == FDT_EXTERNAL)`
2111 let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed);
2112 if fd >= 0 && fd <= max_fd {
2113 let kind = fdtable_get(fd);
2114 if kind == FDT_EXTERNAL {
2115 zwarn(&format!("{}: file descriptor {} already open", s, fd)); // c:2206-2210
2116 return 0; // c:2211
2117 }
2118 }
2119 }
2120 }
2121 1 // c:2214
2122}
2123
2124/// Port of `static int clobber_open(struct redir *f)` from
2125/// `Src/exec.c:2221`.
2126///
2127/// C body:
2128/// ```c
2129/// struct stat buf;
2130/// int fd, oerrno;
2131/// char *ufname = unmeta(f->name);
2132/// /* If clobbering, just open. */
2133/// if (isset(CLOBBER) || IS_CLOBBER_REDIR(f->type))
2134/// return open(ufname, O_WRONLY | O_CREAT | O_TRUNC | O_NOCTTY, 0666);
2135/// /* If not clobbering, attempt to create file exclusively. */
2136/// if ((fd = open(ufname, O_WRONLY | O_CREAT | O_EXCL | O_NOCTTY, 0666)) >= 0)
2137/// return fd;
2138/// /* If that fails, we are still allowed to open non-regular files. */
2139/// oerrno = errno;
2140/// if ((fd = open(ufname, O_WRONLY | O_NOCTTY)) != -1) {
2141/// if (!fstat(fd, &buf)) {
2142/// if (!S_ISREG(buf.st_mode)) return fd;
2143/// /* CLOBBER_EMPTY allows re-use of empty regular files. */
2144/// if (isset(CLOBBEREMPTY) && buf.st_size == 0) return fd;
2145/// }
2146/// close(fd);
2147/// }
2148/// errno = oerrno;
2149/// return -1;
2150/// ```
2151///
2152/// Open the redir target for write with the NOCLOBBER rules:
2153/// - CLOBBER set or `>|` form → just open with O_TRUNC
2154/// - Otherwise → try O_EXCL first; on EEXIST, only allow non-regular
2155/// files (FIFOs, devices, sockets) OR empty regular files under
2156/// CLOBBEREMPTY.
2157pub fn clobber_open(f: &redir) -> i32 {
2158 // c:2221
2159 let ufname_owned = unmeta(f.name.as_deref().unwrap_or("")); // c:2225
2160 let ufname = match std::ffi::CString::new(ufname_owned.as_str()) {
2161 Ok(c) => c,
2162 Err(_) => return -1,
2163 };
2164 // c:2228-2230 — clobber path: just open + truncate.
2165 if isset(CLOBBER) || IS_CLOBBER_REDIR(f.typ) {
2166 // c:2228
2167 // c:2229 — `open(ufname, O_WRONLY|O_CREAT|O_TRUNC|O_NOCTTY, 0666)`
2168 let fd = unsafe {
2169 libc::open(
2170 ufname.as_ptr(),
2171 libc::O_WRONLY | libc::O_CREAT | libc::O_TRUNC | libc::O_NOCTTY,
2172 0o666 as libc::c_uint,
2173 )
2174 };
2175 return fd; // c:2230
2176 }
2177 // c:2233-2235 — try O_EXCL create first.
2178 let fd = unsafe {
2179 // c:2233
2180 libc::open(
2181 ufname.as_ptr(),
2182 libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | libc::O_NOCTTY,
2183 0o666 as libc::c_uint,
2184 )
2185 };
2186 if fd >= 0 {
2187 return fd; // c:2235
2188 }
2189 // c:2240 — `oerrno = errno;` — save for restoration on the recover path.
2190 let oerrno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
2191 // c:2241-2260 — recover: open() w/o O_EXCL, accept if non-regular
2192 // OR (CLOBBEREMPTY && size == 0).
2193 let fd = unsafe {
2194 // c:2241
2195 libc::open(
2196 ufname.as_ptr(),
2197 libc::O_WRONLY | libc::O_NOCTTY,
2198 0o666 as libc::c_uint,
2199 )
2200 };
2201 if fd != -1 {
2202 let mut buf: libc::stat = unsafe { std::mem::zeroed() };
2203 if unsafe { libc::fstat(fd, &mut buf) } == 0 {
2204 // c:2242
2205 // c:2243-2244 — non-regular file: accept.
2206 if (buf.st_mode & libc::S_IFMT) != libc::S_IFREG {
2207 // c:2243
2208 return fd; // c:2244
2209 }
2210 // c:2256-2257 — CLOBBEREMPTY + empty regular: accept.
2211 if isset(CLOBBEREMPTY) && buf.st_size == 0 {
2212 // c:2256
2213 return fd; // c:2257
2214 }
2215 }
2216 unsafe {
2217 libc::close(fd);
2218 } // c:2259
2219 }
2220 // c:2262 — `errno = oerrno;` — restore the EEXIST so caller diagnoses
2221 // "file exists" not the noisier "couldn't reopen" trailing errno.
2222 // Per-platform errno setter: __error() on macOS, __errno_location()
2223 // on Linux. Without cfg gating the build breaks on Linux (CI).
2224 #[cfg(target_os = "macos")]
2225 unsafe {
2226 *libc::__error() = oerrno;
2227 }
2228 #[cfg(target_os = "linux")]
2229 unsafe {
2230 *libc::__errno_location() = oerrno;
2231 }
2232 -1 // c:2263
2233}
2234
2235/// Port of `char *findcmd(char *arg0, int docopy, int default_path)`
2236/// from `Src/exec.c:897`. Walk `$PATH` (or DEFAULT_PATH under
2237/// `default_path=1`) for `arg0`, returning the matching path on
2238/// success. `_docopy` is the C source's "duplicate the result"
2239/// flag — Rust ownership covers it without an explicit copy step.
2240/// `default_path=1` forces `/bin:/usr/bin:...` search (used by
2241/// `command -p`).
2242pub fn findcmd(arg0: &str, _docopy: i32, default_path: i32) -> Option<String> {
2243 // c:897
2244 // c:903-908 — if (default_path) → search_defpath; return.
2245 if default_path != 0 {
2246 return search_defpath(arg0, libc::PATH_MAX as usize);
2247 }
2248 // c:912-913 — strlen(arg0) > PATH_MAX → NULL.
2249 if arg0.len() > libc::PATH_MAX as usize {
2250 return None;
2251 }
2252 // c:Src/exec.c:914-920 — `/`-bearing arg path resolution.
2253 // if ((s = strchr(arg0, '/'))) {
2254 // RET_IF_COM(arg0); // ← unconditional accept on iscom hit
2255 // if (arg0 == s || unset(PATHDIRS) ||
2256 // !strncmp(arg0, "./", 2) ||
2257 // !strncmp(arg0, "../", 3))
2258 // return NULL;
2259 // }
2260 // The Rust port had the iscom check gated on `starts_with('/')`,
2261 // so `type ./target/debug/zshrs` returned None even when the
2262 // file was executable. Bug #496 family.
2263 if arg0.contains('/') {
2264 if iscom(arg0) {
2265 return Some(arg0.to_string()); // c:915 RET_IF_COM
2266 }
2267 // c:916-919 — absolute OR PATHDIRS-off OR `./` / `../` →
2268 // give up here (no $PATH walk for these). Relative without
2269 // those prefixes falls through to the $PATH scan below for
2270 // the PATHDIRS=set case.
2271 if arg0.starts_with('/')
2272 || !isset(PATHDIRS)
2273 || arg0.starts_with("./")
2274 || arg0.starts_with("../")
2275 {
2276 return None;
2277 }
2278 // else fall through to PATH walk.
2279 }
2280 // c:943-951 — walk `path[]` (the shell `$path` array). Read $PATH
2281 // from paramtab so shell-private edits via `path=(...)` take
2282 // effect (not OS env only).
2283 let path = getsparam("PATH")?;
2284 for dir in path.split(':') {
2285 if dir.is_empty() {
2286 continue;
2287 }
2288 let candidate = format!("{}/{}", dir, arg0);
2289 if iscom(&candidate) {
2290 return Some(candidate);
2291 }
2292 }
2293 None // c:952
2294}
2295
2296/// Port of `static void addfd(int forked, int *save, struct multio **mfds,
2297/// int fd1, int fd2, int rflag, char *varid)`
2298/// from `Src/exec.c:2397`.
2299///
2300/// C body (~100 lines, three branches):
2301/// ```c
2302/// if (varid) {
2303/// /* {varid}>file form — move fd above 10 and bind $varid to it */
2304/// } else if (!mfds[fd1] || unset(MULTIOS)) {
2305/// /* new multio OR MULTIOS off — first redir on this fd */
2306/// } else {
2307/// /* additional redir on a fd that's already a multio (split or extend) */
2308/// }
2309/// ```
2310///
2311/// Register `fd2` (already-open) as a redirection target for `fd1`.
2312/// Three branches: `varid` writes the moved fd to `$varid` and bumps
2313/// `fdtable[fd1]` = FDT_EXTERNAL; new-multio path saves the original fd1
2314/// (when `!forked`) and stamps `mfds[fd1]` as a single-entry struct;
2315/// extend-multio path either splits a ct=1 stream into a pipe + 2 fds
2316/// via `mpipe`, or appends another fd to an already-split stream
2317/// (re-allocating mfds for fd1 past the MULTIOUNIT boundary).
2318///
2319/// `multio.fds` is now `Vec<i32>` (zsh_h.rs:1397) so the C
2320/// `hrealloc` at c:2485 maps to `Vec::push`; MULTIOUNIT is no
2321/// longer a hard cap (still 8 for the initial allocation, grown
2322/// on demand thereafter).
2323///
2324/// `fdtable[fdN] |= FDT_SAVED_MASK` at c:2440 — Rust fdtable_set
2325/// stores the int value but doesn't expose a bitwise-OR setter; we
2326/// re-read + OR + re-store as two atomic-feeling steps.
2327pub fn addfd(
2328 forked: i32,
2329 save: &mut [i32; 10],
2330 mfds: &mut [Option<Box<multio>>; 10],
2331 fd1: i32,
2332 fd2: i32,
2333 rflag: i32,
2334 varid: Option<&str>,
2335) {
2336 // c:2397
2337 let mut pipes: [i32; 2] = [-1; 2]; // c:2400
2338
2339 // c:2402-2417 — `if (varid)` branch — {varid}>file shape.
2340 if let Some(vid) = varid {
2341 // c:2402
2342 let fd_moved = movefd(fd2); // c:2404
2343 if fd_moved == -1 {
2344 // c:2405
2345 zerr(&format!(
2346 // c:2406
2347 "cannot move fd {}: {}",
2348 fd2,
2349 std::io::Error::last_os_error()
2350 ));
2351 return; // c:2407
2352 }
2353 // c:2409 — `fdtable[fd1] = FDT_EXTERNAL;`
2354 fdtable_set(fd_moved, FDT_EXTERNAL);
2355 // c:2410 — `setiparam(varid, (zlong)fd1);`
2356 setiparam(vid, fd_moved as i64);
2357 // c:2415-2416 — `if (errflag) zclose(fd1);`
2358 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
2359 // c:2415
2360 let _ = zclose(fd_moved); // c:2416
2361 }
2362 return;
2363 }
2364 // c:2418 — `else if (!mfds[fd1] || unset(MULTIOS))`
2365 let fd1u = fd1 as usize;
2366 if fd1u >= mfds.len() {
2367 return;
2368 }
2369 if mfds[fd1u].is_none() || unset(MULTIOS) {
2370 // c:2418
2371 if mfds[fd1u].is_none() {
2372 // c:2419 — `starting a new multio`
2373 // c:2420 — `mfds[fd1] = zhalloc(sizeof(multio));`
2374 mfds[fd1u] = Some(Box::new(multio {
2375 ct: 0,
2376 rflag: 0,
2377 pipe: -1,
2378 // c:2420 — C allocates VARLENARRAY trailing `int fds[1]`;
2379 // grow on demand via push() below. Pre-fill MULTIOUNIT
2380 // slots with -1 so existing indexed writes (fds[0], fds[1])
2381 // still work without explicit resize().
2382 fds: vec![-1; MULTIOUNIT],
2383 }));
2384 // c:2421 — `if (!forked && save[fd1] == -2)`
2385 if forked == 0 && save[fd1u] == -2 {
2386 if fd1 == fd2 {
2387 // c:2422
2388 save[fd1u] = -1; // c:2423
2389 } else {
2390 // c:2424
2391 let fd_n = movefd(fd1); // c:2425
2392 if fd_n < 0 {
2393 // c:2430
2394 let e = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
2395 if e != libc::EBADF {
2396 // c:2431
2397 zerr(&format!(
2398 // c:2432
2399 "cannot duplicate fd {}: {}",
2400 fd1,
2401 std::io::Error::from_raw_os_error(e)
2402 ));
2403 mfds[fd1u] = None; // c:2433
2404 closemnodes(mfds); // c:2434
2405 return; // c:2435
2406 }
2407 } else {
2408 // c:2438-2439 — DPUTS check that the saved fd is FDT_INTERNAL.
2409 crate::DPUTS!(
2410 fdtable_get(fd_n) != FDT_INTERNAL,
2411 "Saved file descriptor not marked as internal"
2412 );
2413 // c:2440 — `fdtable[fdN] |= FDT_SAVED_MASK;`
2414 let cur = fdtable_get(fd_n);
2415 fdtable_set(fd_n, cur | FDT_SAVED_MASK);
2416 }
2417 save[fd1u] = fd_n; // c:2442
2418 }
2419 }
2420 }
2421 // c:2446-2447 — `if (!varid) redup(fd2, fd1);` (varid already
2422 // handled above; this is the non-varid branch.)
2423 let _ = redup(fd2, fd1);
2424 // c:2448-2450 — `mfds[fd1]->ct=1; mfds[fd1]->fds[0]=fd1; mfds[fd1]->rflag=rflag;`
2425 if let Some(mn) = mfds[fd1u].as_mut() {
2426 mn.ct = 1; // c:2448
2427 mn.fds[0] = fd1; // c:2449
2428 mn.rflag = rflag; // c:2450
2429 }
2430 } else {
2431 // c:2451 — extend existing multio.
2432 // c:2452-2456 — rflag mismatch check.
2433 let cur_rflag = mfds[fd1u].as_ref().map(|m| m.rflag).unwrap_or(0);
2434 if cur_rflag != rflag {
2435 // c:2452
2436 zerr(&format!("file mode mismatch on fd {}", fd1)); // c:2453
2437 closemnodes(mfds); // c:2454
2438 return; // c:2455
2439 }
2440 let cur_ct = mfds[fd1u].as_ref().map(|m| m.ct).unwrap_or(0);
2441 if cur_ct == 1 {
2442 // c:2457 — split the stream.
2443 // c:2458 — `int fdN = movefd(fd1);`
2444 let fd_n = movefd(fd1);
2445 if fd_n < 0 {
2446 // c:2459
2447 zerr(&format!(
2448 // c:2460
2449 "multio failed for fd {}: {}",
2450 fd1,
2451 std::io::Error::last_os_error()
2452 ));
2453 closemnodes(mfds); // c:2461
2454 return; // c:2462
2455 }
2456 if let Some(mn) = mfds[fd1u].as_mut() {
2457 mn.fds[0] = fd_n; // c:2464
2458 }
2459 // c:2465 — `fdN = movefd(fd2);`
2460 let fd_n2 = movefd(fd2);
2461 if fd_n2 < 0 {
2462 // c:2466
2463 zerr(&format!(
2464 // c:2467
2465 "multio failed for fd {}: {}",
2466 fd2,
2467 std::io::Error::last_os_error()
2468 ));
2469 closemnodes(mfds); // c:2468
2470 return; // c:2469
2471 }
2472 if let Some(mn) = mfds[fd1u].as_mut() {
2473 mn.fds[1] = fd_n2; // c:2471
2474 }
2475 // c:2472 — `mpipe(pipes)`
2476 if mpipe(&mut pipes) < 0 {
2477 // c:2472
2478 zerr(&format!(
2479 // c:2473
2480 "multio failed for fd {}: {}",
2481 fd2,
2482 std::io::Error::last_os_error()
2483 ));
2484 closemnodes(mfds); // c:2474
2485 return; // c:2475
2486 }
2487 // c:2477 — `mfds[fd1]->pipe = pipes[1 - rflag];`
2488 if let Some(mn) = mfds[fd1u].as_mut() {
2489 mn.pipe = pipes[(1 - rflag) as usize];
2490 }
2491 // c:2478 — `redup(pipes[rflag], fd1);`
2492 let _ = redup(pipes[rflag as usize], fd1);
2493 // c:2479 — `mfds[fd1]->ct = 2;`
2494 if let Some(mn) = mfds[fd1u].as_mut() {
2495 mn.ct = 2;
2496 }
2497 } else {
2498 // c:2480 — extend already-split stream.
2499 // c:2482-2486 — `mn = hrealloc(mn, sizeof + (ct-1)*sizeof(int),
2500 // sizeof + ct*sizeof(int));`
2501 // Rust's `Vec<i32>` grows on demand; ensure capacity for the
2502 // new slot before the indexed write below.
2503 if let Some(mn) = mfds[fd1u].as_mut() {
2504 while mn.fds.len() <= cur_ct as usize {
2505 mn.fds.push(-1);
2506 }
2507 }
2508 // c:2487 — `if ((fdN = movefd(fd2)) < 0)`
2509 let fd_n = movefd(fd2);
2510 if fd_n < 0 {
2511 zerr(&format!(
2512 // c:2488
2513 "multio failed for fd {}: {}",
2514 fd2,
2515 std::io::Error::last_os_error()
2516 ));
2517 closemnodes(mfds); // c:2489
2518 return; // c:2490
2519 }
2520 // c:2492 — `mfds[fd1]->fds[mfds[fd1]->ct++] = fdN;`
2521 if let Some(mn) = mfds[fd1u].as_mut() {
2522 let slot = mn.ct as usize;
2523 if slot < mn.fds.len() {
2524 mn.fds[slot] = fd_n;
2525 mn.ct += 1;
2526 }
2527 }
2528 }
2529 }
2530}
2531
2532/// Port of `static void closemn(struct multio **mfds, int fd, int type)`
2533/// from `Src/exec.c:2273`.
2534///
2535/// C body (abridged — the meat is the fork-into-tee-or-cat child):
2536/// ```c
2537/// if (fd >= 0 && mfds[fd] && mfds[fd]->ct >= 2) {
2538/// struct multio *mn = mfds[fd];
2539/// char buf[TCBUFSIZE]; int len, i;
2540/// pid_t pid; struct timespec bgtime;
2541/// child_block();
2542/// if ((pid = zfork(&bgtime))) {
2543/// for (i = 0; i < mn->ct; i++) zclose(mn->fds[i]);
2544/// zclose(mn->pipe);
2545/// if (pid == -1) { mfds[fd] = NULL; child_unblock(); return; }
2546/// mn->ct = 1; mn->fds[0] = fd;
2547/// addproc(pid, NULL, 1, &bgtime, -1, -1);
2548/// child_unblock(); return;
2549/// }
2550/// /* pid == 0 (child) */
2551/// opts[INTERACTIVE] = 0;
2552/// dont_queue_signals();
2553/// child_unblock();
2554/// closeallelse(mn);
2555/// if (mn->rflag) {
2556/// /* tee process: read mn->pipe, write each mn->fds[i] */
2557/// } else {
2558/// /* cat process: read each mn->fds[i], write mn->pipe */
2559/// }
2560/// _exit(0);
2561/// } else if (fd >= 0 && type == REDIR_CLOSE)
2562/// mfds[fd] = NULL;
2563/// ```
2564///
2565/// Success-path close of a multio. For ct>=2 (multiple-output
2566/// redirection), forks a tee/cat child that proxies bytes between
2567/// the original fd and the per-output fds. Single-output multios
2568/// (ct=1) skip the fork entirely and just clear the slot.
2569///
2570/// c:2299 — `addproc(pid, NULL, 1, &bgtime, -1, -1)` records the
2571/// tee/cat child in the current job's auxprocs.
2572pub fn closemn(mfds: &mut [Option<Box<multio>>; 10], fd: i32, type_: i32) {
2573 // c:2273
2574 // c:2275 — `if (fd >= 0 && mfds[fd] && mfds[fd]->ct >= 2)`
2575 let needs_tee = fd >= 0
2576 && (fd as usize) < mfds.len()
2577 && mfds[fd as usize].as_ref().is_some_and(|m| m.ct >= 2);
2578 if needs_tee {
2579 // c:2275
2580 // Take the multio out of the slot so we can move pieces into
2581 // the child without aliasing the slot.
2582 let mn = mfds[fd as usize].take().unwrap();
2583 let mut buf = [0u8; 4092]; // c:2277 TCBUFSIZE
2584 // c:2287 — `child_block();` block SIGCHLD before fork race.
2585 child_block();
2586 // c:2288 — `pid = zfork(&bgtime);`
2587 let mut bgtime = ZshTimespec {
2588 tv_sec: 0,
2589 tv_nsec: 0,
2590 };
2591 let pid = zfork(Some(&mut bgtime));
2592 if pid != 0 {
2593 // c:2288 parent branch
2594 // c:2289-2290 — close all per-output fds.
2595 for i in 0..mn.ct as usize {
2596 if i < mn.fds.len() {
2597 let _ = zclose(mn.fds[i]); // c:2290
2598 }
2599 }
2600 let _ = zclose(mn.pipe); // c:2291
2601 if pid == -1 {
2602 // c:2292
2603 // c:2293 — `mfds[fd] = NULL;` already done via .take()
2604 child_unblock(); // c:2294
2605 return; // c:2295
2606 }
2607 // c:2297-2298 — `mn->ct = 1; mn->fds[0] = fd;`
2608 let mut mn_back = mn;
2609 mn_back.ct = 1; // c:2297
2610 mn_back.fds[0] = fd; // c:2298
2611 mfds[fd as usize] = Some(mn_back);
2612 // c:2299 — `addproc(pid, NULL, 1, &bgtime, -1, -1);` — record
2613 // the tee/cat child in the current job's auxprocs (aux=true).
2614 if let Some(jt) = JOBTAB.get() {
2615 let mut guard = jt.lock().unwrap();
2616 let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
2617 if tj >= 0 {
2618 if let Some(j) = guard.get_mut(tj as usize) {
2619 crate::ported::jobs::addproc(
2620 j,
2621 pid,
2622 "",
2623 true,
2624 Some(std::time::Instant::now()),
2625 -1,
2626 -1,
2627 );
2628 }
2629 }
2630 }
2631 let _ = bgtime;
2632 child_unblock(); // c:2300
2633 return; // c:2301
2634 }
2635 // c:2303 — child branch (pid == 0).
2636 opt_state_set("interactive", false); // c:2304
2637 dont_queue_signals(); // c:2305
2638 child_unblock(); // c:2306
2639 closeallelse(&mn); // c:2307
2640 // c:2308-2333 — tee or cat loop.
2641 if mn.rflag != 0 {
2642 // c:2308 — `mn->rflag` set → tee process
2643 // c:2310 — `while ((len = read(mn->pipe, buf, TCBUFSIZE)) != 0)`
2644 loop {
2645 let len = unsafe {
2646 libc::read(mn.pipe, buf.as_mut_ptr() as *mut libc::c_void, buf.len())
2647 };
2648 if len == 0 {
2649 break;
2650 }
2651 if len < 0 {
2652 // c:2311
2653 let e = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
2654 if e == libc::EINTR {
2655 // c:2312
2656 continue;
2657 } else {
2658 break; // c:2315
2659 }
2660 }
2661 // c:2317-2319 — `for i: write_loop(mn->fds[i], buf, len)`
2662 for i in 0..mn.ct as usize {
2663 if i >= mn.fds.len() {
2664 break;
2665 }
2666 if write_loop(mn.fds[i], &buf[..len as usize]).is_err() {
2667 break; // c:2319
2668 }
2669 }
2670 }
2671 } else {
2672 // c:2321 — cat process
2673 for i in 0..mn.ct as usize {
2674 if i >= mn.fds.len() {
2675 break;
2676 }
2677 // c:2324 — `while ((len = read(mn->fds[i], buf, TCBUFSIZE)) != 0)`
2678 loop {
2679 let len = unsafe {
2680 libc::read(mn.fds[i], buf.as_mut_ptr() as *mut libc::c_void, buf.len())
2681 };
2682 if len == 0 {
2683 break;
2684 }
2685 if len < 0 {
2686 let e = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
2687 // c:2326 — `if (errno == EINTR && !isatty(mn->fds[i]))`
2688 if e == libc::EINTR && unsafe { libc::isatty(mn.fds[i]) } == 0 {
2689 continue;
2690 } else {
2691 break; // c:2329
2692 }
2693 }
2694 // c:2331 — `if (write_loop(mn->pipe, buf, len) < 0) break;`
2695 if write_loop(mn.pipe, &buf[..len as usize]).is_err() {
2696 break; // c:2332
2697 }
2698 }
2699 }
2700 }
2701 // c:2335 — `_exit(0);`
2702 unsafe {
2703 libc::_exit(0);
2704 }
2705 } else if fd >= 0 && type_ == REDIR_CLOSE {
2706 // c:2336
2707 // c:2337 — `mfds[fd] = NULL;`
2708 if (fd as usize) < mfds.len() {
2709 mfds[fd as usize] = None;
2710 }
2711 }
2712}
2713
2714/// Port of `static void closemnodes(struct multio **mfds)` from
2715/// `Src/exec.c:2344`.
2716///
2717/// C body:
2718/// ```c
2719/// int i, j;
2720/// for (i = 0; i < 10; i++)
2721/// if (mfds[i]) {
2722/// for (j = 0; j < mfds[i]->ct; j++)
2723/// zclose(mfds[i]->fds[j]);
2724/// mfds[i] = NULL;
2725/// }
2726/// ```
2727///
2728/// Failure-path cleanup: close every fd stashed in any of the 10
2729/// multio slots and null the slot. Called from `execcmd_exec` when
2730/// a redirect setup fails partway through and we need to roll back.
2731pub fn closemnodes(mfds: &mut [Option<Box<multio>>; 10]) {
2732 // c:2344
2733 for i in 0..10 {
2734 // c:2348
2735 if let Some(mn) = mfds[i].take() {
2736 // c:2349
2737 for j in 0..mn.ct as usize {
2738 // c:2350
2739 if j < mn.fds.len() {
2740 let _ = zclose(mn.fds[j]); // c:2351
2741 }
2742 }
2743 // c:2352 — `mfds[i] = NULL;` — handled by .take() above.
2744 }
2745 }
2746}
2747
2748/// Port of `static void closeallelse(struct multio *mn)` from
2749/// `Src/exec.c:2358`.
2750///
2751/// C body:
2752/// ```c
2753/// int i, j;
2754/// long openmax;
2755/// openmax = fdtable_size;
2756/// for (i = 0; i < openmax; i++)
2757/// if (mn->pipe != i) {
2758/// for (j = 0; j < mn->ct; j++)
2759/// if (mn->fds[j] == i) break;
2760/// if (j == mn->ct)
2761/// zclose(i);
2762/// }
2763/// ```
2764///
2765/// Close every fd in the open range EXCEPT `mn->pipe` and the fds
2766/// stashed in `mn->fds`. Called inside the multio tee/cat child
2767/// process to release every fd the parent had open — only the pipe
2768/// + per-output fds stay alive for the read/write loop.
2769pub fn closeallelse(mn: &multio) {
2770 // c:2358
2771 // c:2363 — `openmax = fdtable_size;`. zshrs models fdtable as a
2772 // Vec; use MAX_ZSH_FD as the upper bound (fdtable_size grows past
2773 // max_zsh_fd in C but every slot past it is FDT_UNUSED anyway).
2774 let openmax = MAX_ZSH_FD.load(Ordering::Relaxed) + 1; // c:2363
2775 for i in 0..openmax {
2776 // c:2365
2777 if mn.pipe == i {
2778 // c:2366
2779 continue;
2780 }
2781 // c:2367-2369 — scan mn->fds[] for i; skip-close if found.
2782 let mut found = false;
2783 for j in 0..mn.ct as usize {
2784 // c:2367
2785 if j < mn.fds.len() && mn.fds[j] == i {
2786 // c:2368
2787 found = true;
2788 break; // c:2369
2789 }
2790 }
2791 // c:2370-2371 — `if (j == mn->ct) zclose(i);`
2792 if !found {
2793 let _ = zclose(i); // c:2371
2794 }
2795 }
2796}
2797
2798/// Port of `static void fixfds(int *save)` from `Src/exec.c:4523`.
2799///
2800/// C body:
2801/// ```c
2802/// int old_errno = errno;
2803/// int i;
2804/// for (i = 0; i != 10; i++)
2805/// if (save[i] != -2)
2806/// redup(save[i], i);
2807/// errno = old_errno;
2808/// ```
2809///
2810/// Restore fds 0..9 from the `save[10]` slot array. `-2` sentinel
2811/// means "no save was made for this fd"; any other value is the
2812/// stashed fd that gets `dup2`'d back via `redup`. Preserves the
2813/// caller's errno across the loop so a downstream caller diagnoses
2814/// the original failure, not a noisy dup2 errno.
2815pub fn fixfds(save: &[i32; 10]) {
2816 // c:4523
2817 let old_errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0); // c:4525
2818 for i in 0..10i32 {
2819 // c:4528 — `for (i = 0; i != 10; i++)`
2820 if save[i as usize] != -2 {
2821 // c:4529
2822 redup(save[i as usize], i); // c:4530
2823 }
2824 }
2825 // c:4531 — `errno = old_errno;`
2826 #[cfg(target_os = "macos")]
2827 unsafe {
2828 *libc::__error() = old_errno;
2829 }
2830 #[cfg(target_os = "linux")]
2831 unsafe {
2832 *libc::__errno_location() = old_errno;
2833 }
2834}
2835
2836/// Port of `mod_export void closem(int how, int all)` from `Src/exec.c:4546`.
2837///
2838/// C body:
2839/// ```c
2840/// int i;
2841/// for (i = 10; i <= max_zsh_fd; i++)
2842/// if (fdtable[i] != FDT_UNUSED &&
2843/// (all || (fdtable[i] != FDT_PROC_SUBST &&
2844/// fdtable[i] != FDT_EXTERNAL)) &&
2845/// (how == FDT_UNUSED || (fdtable[i] & FDT_TYPE_MASK) == how)) {
2846/// if (i == SHTTY) SHTTY = -1;
2847/// zclose(i);
2848/// }
2849/// ```
2850///
2851/// Walk fds 10..=MAX_ZSH_FD and close every internal shell fd that
2852/// matches the criteria. `how == FDT_UNUSED` matches all kinds (no
2853/// type filter); otherwise only fds whose low-nibble type equals
2854/// `how` are closed. `all == 0` preserves user-visible fds
2855/// (FDT_PROC_SUBST, FDT_EXTERNAL) since those need to outlive the
2856/// shell's internal-fd lifetime. SHTTY clearing prevents a stale
2857/// reference if we just closed the controlling tty.
2858pub fn closem(how: i32, all: i32) {
2859 // c:4546
2860 let max = MAX_ZSH_FD.load(Ordering::Relaxed); // c:4550
2861 for i in 10i32..=max {
2862 // c:4550
2863 let kind = fdtable_get(i); // c:4551 fdtable[i]
2864 if kind == FDT_UNUSED {
2865 // c:4551
2866 continue;
2867 }
2868 // c:4557-4558 — `(all || (kind != FDT_PROC_SUBST && kind != FDT_EXTERNAL))`
2869 if all == 0 && (kind == FDT_PROC_SUBST || kind == FDT_EXTERNAL) {
2870 continue;
2871 }
2872 // c:4559 — `(how == FDT_UNUSED || (fdtable[i] & FDT_TYPE_MASK) == how)`
2873 if how != FDT_UNUSED && (kind & FDT_TYPE_MASK) != how {
2874 continue;
2875 }
2876 // c:4560-4561 — `if (i == SHTTY) SHTTY = -1;`
2877 if i == SHTTY.load(Ordering::Relaxed) {
2878 // c:4560
2879 SHTTY.store(-1, Ordering::Relaxed); // c:4561
2880 }
2881 // c:4562 — `zclose(i);`
2882 let _ = zclose(i);
2883 }
2884}
2885
2886/// Port of `Cmdnam hashcmd(char *arg0, char **pp)` from
2887/// `Src/exec.c:1010`.
2888///
2889/// C body:
2890/// ```c
2891/// Cmdnam cn;
2892/// char *s, buf[PATH_MAX+1];
2893/// char **pq;
2894/// if (*arg0 == '/') return NULL;
2895/// for (; *pp; pp++)
2896/// if (**pp == '/') {
2897/// s = buf;
2898/// struncpy(&s, *pp, PATH_MAX);
2899/// *s++ = '/';
2900/// if ((s - buf) + strlen(arg0) >= PATH_MAX) continue;
2901/// strcpy(s, arg0);
2902/// if (iscom(buf)) break;
2903/// }
2904/// if (!*pp) return NULL;
2905/// cn = (Cmdnam) zshcalloc(sizeof *cn);
2906/// cn->node.flags = 0;
2907/// cn->u.name = pp;
2908/// cmdnamtab->addnode(cmdnamtab, ztrdup(arg0), cn);
2909/// if (isset(HASHDIRS)) {
2910/// for (pq = pathchecked; pq <= pp; pq++) hashdir(pq);
2911/// pathchecked = pp + 1;
2912/// }
2913/// return cn;
2914/// ```
2915///
2916/// Walk `pp[]` (a $path slice starting from `pathchecked`) for the
2917/// first absolute-PATH entry where `<entry>/<arg0>` is an executable
2918/// regular file. Inserts the unhashed-cmdnam entry into `cmdnamtab`
2919/// and (under HASHDIRS) bulk-hashes every PATH dir we walked through
2920/// so subsequent commands hit the cache.
2921///
2922/// Returns the just-inserted `cmdnam` (now in `cmdnamtab`) on success,
2923/// `None` if `arg0` is absolute or no PATH entry contains it.
2924pub fn hashcmd(arg0: &str, pp: &[String]) -> Option<cmdnam> {
2925 // c:1010
2926 // c:1016 — `if (*arg0 == '/') return NULL;`
2927 if arg0.starts_with('/') {
2928 return None; // c:1017
2929 }
2930 // c:1018-1028 — walk pp[] for first matching absolute entry.
2931 let mut found_idx: Option<usize> = None;
2932 for (i, dir) in pp.iter().enumerate() {
2933 // c:1018
2934 if !dir.starts_with('/') {
2935 // c:1019
2936 continue;
2937 }
2938 // c:1020-1025 — buf = "<dir>/<arg0>"; PATH_MAX bounds check.
2939 if dir.len() + 1 + arg0.len() >= libc::PATH_MAX as usize {
2940 // c:1023
2941 continue; // c:1024
2942 }
2943 let buf = format!("{}/{}", dir, arg0); // c:1025
2944 if iscom(&buf) {
2945 // c:1026
2946 found_idx = Some(i);
2947 break; // c:1027
2948 }
2949 }
2950 // c:1030-1031 — `if (!*pp) return NULL;`
2951 let pp_idx = match found_idx {
2952 Some(i) => i,
2953 None => return None, // c:1031
2954 };
2955 // c:1033-1036 — alloc cn, set flags=0, u.name=pp (the matching slice).
2956 let path_slice: Vec<String> = pp[pp_idx..].to_vec(); // c:1035
2957 let cn = cmdnam_unhashed(arg0, path_slice); // c:1033-1035
2958 // c:1036 — `cmdnamtab->addnode(cmdnamtab, ztrdup(arg0), cn);`
2959 if let Ok(mut tab) = cmdnamtab_lock().write() {
2960 tab.add(cn.clone());
2961 }
2962 // c:1038-1042 — under HASHDIRS, bulk-hash every dir up to and
2963 // including the matching one, then bump pathchecked past it.
2964 if isset(HASHDIRS) {
2965 // c:1038
2966 let start = pathchecked.load(Ordering::Relaxed); // c:1039
2967 for pq in start..=pp_idx {
2968 // c:1039
2969 if pq < pp.len() {
2970 hashdir(&pp[pq], pq); // c:1040
2971 }
2972 }
2973 pathchecked.store(pp_idx + 1, Ordering::Relaxed); // c:1041
2974 }
2975 Some(cn) // c:1044
2976}
2977
2978/// Port of `static pid_t zfork(struct timespec *ts)` from
2979/// `Src/exec.c:349`.
2980///
2981/// C body:
2982/// ```c
2983/// pid_t pid;
2984/// if (thisjob != -1 && thisjob >= jobtabsize - 1 && !expandjobtab()) {
2985/// zerr("job table full");
2986/// return -1;
2987/// }
2988/// if (ts) zgettime_monotonic_if_available(ts);
2989/// queue_signals();
2990/// pid = fork();
2991/// unqueue_signals();
2992/// if (pid == -1) {
2993/// zerr("fork failed: %e", errno);
2994/// return -1;
2995/// }
2996/// #ifdef HAVE_GETRLIMIT
2997/// if (!pid) setlimits(NULL);
2998/// #endif
2999/// return pid;
3000/// ```
3001///
3002/// fork(2) wrapper with jobtab capacity check + child rlimit
3003/// re-application. Used by every subshell-spawning path: pipelines,
3004/// process substitution, async commands, command substitution.
3005pub fn zfork(ts: Option<&mut ZshTimespec>) -> libc::pid_t {
3006 // c:349
3007 let pid: libc::pid_t;
3008
3009 // c:356-359 — `if (thisjob != -1 && thisjob >= jobtabsize - 1 && !expandjobtab())`
3010 let thisjob_lock = THISJOB.get_or_init(|| std::sync::Mutex::new(-1));
3011 let thisjob = *thisjob_lock.lock().unwrap();
3012 if thisjob != -1 {
3013 // c:356
3014 let needed = (thisjob + 1) as usize;
3015 let needs_expand = JOBTAB
3016 .get_or_init(|| std::sync::Mutex::new(Vec::new()))
3017 .lock()
3018 .map(|t| needed >= t.len().saturating_sub(1))
3019 .unwrap_or(false);
3020 if needs_expand {
3021 let mut tab = JOBTAB.get().unwrap().lock().unwrap();
3022 if !expandjobtab(&mut tab, needed) {
3023 // c:357
3024 zerr("job table full"); // c:357
3025 return -1; // c:358
3026 }
3027 }
3028 }
3029 // c:360-361 — `if (ts) zgettime_monotonic_if_available(ts);`
3030 if let Some(ts) = ts {
3031 zgettime_monotonic_if_available(ts);
3032 }
3033 // c:368-370 — `queue_signals(); pid = fork(); unqueue_signals();`
3034 queue_signals(); // c:368
3035 pid = unsafe { libc::fork() }; // c:369
3036 unqueue_signals(); // c:370
3037 // c:371-374 — fork failure.
3038 if pid == -1 {
3039 // c:371
3040 zerr(&format!(
3041 // c:372
3042 "fork failed: {}",
3043 std::io::Error::last_os_error()
3044 ));
3045 return -1; // c:373
3046 }
3047 // c:375-379 — child: re-apply rlimits (HAVE_GETRLIMIT path).
3048 #[cfg(unix)]
3049 if pid == 0 {
3050 // c:376
3051 let _ = setlimits(""); // c:378
3052 }
3053 pid // c:380
3054}
3055
3056/// Port of `void loadautofnsetfile(Shfunc shf, char *fdir)` from
3057/// `Src/exec.c:5657`.
3058///
3059/// C body:
3060/// ```c
3061/// if (!(shf->node.flags & PM_LOADDIR) ||
3062/// strcmp(shf->filename, fdir) != 0) {
3063/// dircache_set(&shf->filename, NULL);
3064/// if (fdir) {
3065/// shf->node.flags |= PM_LOADDIR;
3066/// dircache_set(&shf->filename, fdir);
3067/// } else {
3068/// shf->node.flags &= ~PM_LOADDIR;
3069/// shf->filename = ztrdup(shf->node.nam);
3070/// }
3071/// }
3072/// ```
3073///
3074/// Update `shf->filename` to the autoload directory `fdir`. Routes
3075/// through the refcounted `dircache_set` so identical directory
3076/// strings are shared across shfunc table entries.
3077pub fn loadautofnsetfile(shf: &mut shfunc, fdir: Option<&str>) {
3078 // c:5657
3079 // c:5664-5665 — `if (!(shf->node.flags & PM_LOADDIR) || strcmp(shf->filename, fdir) != 0)`
3080 let loaddir = (shf.node.flags as u32 & PM_LOADDIR) != 0;
3081 let same = match (&shf.filename, fdir) {
3082 (Some(a), Some(b)) => a == b,
3083 _ => false,
3084 };
3085 if !loaddir || !same {
3086 // c:5664
3087 // c:5667 — `dircache_set(&shf->filename, NULL);` — refcount-drop old.
3088 dircache_set(&mut shf.filename, None);
3089 if let Some(fdir) = fdir {
3090 // c:5668
3091 shf.node.flags |= PM_LOADDIR as i32; // c:5670
3092 dircache_set(&mut shf.filename, Some(fdir)); // c:5671
3093 } else {
3094 // c:5672
3095 shf.node.flags &= !(PM_LOADDIR as i32); // c:5674
3096 shf.filename = Some(shf.node.nam.clone()); // c:5675 `ztrdup(shf->node.nam)`
3097 }
3098 }
3099}
3100
3101/// Port of `int commandnotfound(char *arg0, LinkList args)` from
3102/// `Src/exec.c:669`.
3103///
3104/// C body:
3105/// ```c
3106/// Shfunc shf = (Shfunc)
3107/// shfunctab->getnode(shfunctab, "command_not_found_handler");
3108/// if (!shf) {
3109/// lastval = 127;
3110/// return 1;
3111/// }
3112/// pushnode(args, arg0);
3113/// lastval = doshfunc(shf, args, 1);
3114/// return 0;
3115/// ```
3116///
3117/// Look up the user-defined `command_not_found_handler` shfunc and
3118/// invoke it with `arg0` prepended to `args`. Returns 0 if handled,
3119/// 1 if no handler (so caller emits the standard "command not found"
3120/// error). Sets `$?` to 127 in the no-handler path.
3121pub fn commandnotfound(arg0: &str, args: &mut Vec<String>) -> i32 {
3122 // c:669
3123 // c:671-672 — `shf = shfunctab->getnode(shfunctab, "command_not_found_handler");`
3124 let has_handler = shfunctab_lock()
3125 .read()
3126 .map(|t| t.get("command_not_found_handler").is_some())
3127 .unwrap_or(false);
3128 if !has_handler {
3129 // c:674
3130 LASTVAL.store(127, Ordering::Relaxed); // c:675
3131 return 1; // c:676
3132 }
3133 // c:679 — `pushnode(args, arg0);` — prepend arg0 (handler name
3134 // is the first positional arg per C convention).
3135 args.insert(0, arg0.to_string());
3136 args.insert(0, "command_not_found_handler".to_string());
3137 // c:680 — `lastval = doshfunc(shf, args, 1);`. Direct doshfunc
3138 // call mirrors C — body_runner routes through the host body-only
3139 // entry so the function body runs once inside doshfunc's scope.
3140 let shf_clone: Option<shfunc> = shfunctab_lock()
3141 .read()
3142 .ok()
3143 .and_then(|t| t.get("command_not_found_handler").cloned());
3144 if let Some(mut shf) = shf_clone {
3145 let body_args = args.clone();
3146 let body_runner = move || -> i32 {
3147 crate::ported::exec_hooks::run_function_body(
3148 "command_not_found_handler",
3149 &body_args[1..],
3150 )
3151 .unwrap_or(0)
3152 };
3153 let lv = doshfunc(&mut shf, args.clone(), true, body_runner);
3154 LASTVAL.store(lv, Ordering::Relaxed);
3155 }
3156 0 // c:681
3157}
3158
3159/// Port of `char *namedpipe(void)` from `Src/exec.c:5001`.
3160///
3161/// C body (#ifdef HAVE_FIFOS branch):
3162/// ```c
3163/// char *tnam = gettempname(NULL, 1);
3164/// if (!tnam) {
3165/// zerr("failed to create named pipe: %e", errno);
3166/// return NULL;
3167/// }
3168/// if (mkfifo(tnam, 0600) < 0) {
3169/// zerr("failed to create named pipe: %s, %e", tnam, errno);
3170/// return NULL;
3171/// }
3172/// return tnam;
3173/// ```
3174///
3175/// Create a FIFO with a unique name for process substitution. Used by
3176/// `getproc` (`<(cmd)` / `>(cmd)`) on systems without `/dev/fd`.
3177pub fn namedpipe() -> Option<String> {
3178 // c:5001
3179 let tnam = gettempname(None, true); // c:5003
3180 let tnam = match tnam {
3181 Some(t) => t,
3182 None => {
3183 // c:5005
3184 zerr(&format!(
3185 // c:5006
3186 "failed to create named pipe: {}",
3187 std::io::Error::last_os_error()
3188 ));
3189 return None; // c:5007
3190 }
3191 };
3192 // c:5010 — `mkfifo(tnam, 0600)`.
3193 let cstr = match std::ffi::CString::new(tnam.as_str()) {
3194 Ok(c) => c,
3195 Err(_) => return None,
3196 };
3197 if unsafe { libc::mkfifo(cstr.as_ptr(), 0o600) } < 0 {
3198 // c:5010
3199 zerr(&format!(
3200 // c:5014
3201 "failed to create named pipe: {}, {}",
3202 tnam,
3203 std::io::Error::last_os_error()
3204 ));
3205 return None; // c:5015
3206 }
3207 Some(tnam) // c:5017
3208}
3209
3210/// Port of `Eprog parsecmd(char *cmd, char **eptr)` from `Src/exec.c:4878`.
3211///
3212/// C body:
3213/// ```c
3214/// char *str;
3215/// Eprog prog;
3216/// for (str = cmd + 2; *str && *str != Outpar; str++);
3217/// if (!*str || cmd[1] != Inpar) {
3218/// char *errstr = dupstrpfx(cmd, 2);
3219/// untokenize(errstr);
3220/// zerr("unterminated `%s...)'", errstr);
3221/// return NULL;
3222/// }
3223/// *str = '\0';
3224/// if (eptr) *eptr = str+1;
3225/// if (!(prog = parse_string(cmd + 2, 0))) {
3226/// zerr("parse error in process substitution");
3227/// return NULL;
3228/// }
3229/// return prog;
3230/// ```
3231///
3232/// Port of `static LinkList readoutput(int in, int qt, int *readerror)`
3233/// from `Src/exec.c:4805`. Drain a command-substitution pipe fd and
3234/// return the captured output split per `qt`.
3235///
3236/// `qt=1` (quoted-substitution `"$(...)"`): single-element vec with
3237/// the trailing-newline-trimmed buffer (empty buffer → `Nularg` sentinel
3238/// per c:4861).
3239/// `qt=0` (unquoted `$(...)`): split on IFS via `spacesplit`; if
3240/// `GLOBSUBST` is set, each word is `shtokenize`d for downstream globbing.
3241///
3242/// `readerror` is set to the errno on read failure, 0 on clean EOF.
3243pub fn readoutput(in_fd: i32, qt: i32, readerror: &mut i32) -> Vec<String> {
3244 // c:4805
3245 let mut buf: Vec<u8> = Vec::with_capacity(64); // c:4816 (initial bsiz=64)
3246 let mut readret: isize = 0; // c:4818 readret tracks last read return
3247 // c:4824 dont_queue_signals(); c:4825 child_unblock(); — signal-queue
3248 // dance keeps SIGCHLD live so the foreground process can be reaped
3249 // while we drain. zshrs's in-process command-sub runs without the
3250 // queue (no fork), but the C call surface is preserved for parity.
3251 dont_queue_signals(); // c:4824
3252 child_unblock(); // c:4825
3253 let mut inbuf = [0u8; 64]; // c:4815 inbuf[64]
3254 loop {
3255 // c:4826
3256 // c:4828 — `readret = read(in, inbuf, 64);`
3257 let r = unsafe { libc::read(in_fd, inbuf.as_mut_ptr() as *mut libc::c_void, inbuf.len()) };
3258 readret = r as isize;
3259 if readret <= 0 {
3260 // c:4829
3261 if readret < 0 && std::io::Error::last_os_error().raw_os_error() == Some(libc::EINTR) {
3262 // c:4830 — `if (readret < 0 && errno == EINTR) continue;`
3263 continue;
3264 }
3265 break; // c:4832
3266 }
3267 // c:4835 — `for (bufptr = inbuf; bufptr < inbuf + readret; bufptr++)`
3268 for i in 0..(readret as usize) {
3269 let c = inbuf[i];
3270 if crate::ported::ztype_h::imeta(c) {
3271 // c:4837 — `if (imeta(c)) { *ptr++ = Meta; c ^= 32; cnt++; }`
3272 buf.push(Meta as u8); // c:4838
3273 buf.push(c ^ 32); // c:4839 (Meta-encoded payload)
3274 } else {
3275 buf.push(c); // c:4848 *ptr++ = c
3276 }
3277 }
3278 }
3279 child_block(); // c:4854
3280 // c:4855 — `if (readerror) *readerror = readret < 0 ? errno : 0;`
3281 *readerror = if readret < 0 {
3282 std::io::Error::last_os_error().raw_os_error().unwrap_or(0)
3283 } else {
3284 0
3285 };
3286 // c:4857 — `close(in);`
3287 unsafe {
3288 libc::close(in_fd);
3289 }
3290 // c:4858-4859 — `while (cnt && ptr[-1] == '\n') ptr--, cnt--;`
3291 while buf.last() == Some(&b'\n') {
3292 buf.pop();
3293 }
3294 // c:4861-4863 — qt branch: empty → Nularg sentinel; else single elem.
3295 let s = String::from_utf8_lossy(&buf).into_owned();
3296 if qt != 0 {
3297 // c:4861
3298 if buf.is_empty() {
3299 return vec![String::from(Nularg)]; // c:4862
3300 }
3301 return vec![s]; // c:4864
3302 }
3303 // c:4866-4871 — `spacesplit` + per-word GLOBSUBST `shtokenize`.
3304 let mut words = crate::ported::utils::spacesplit(&s, false); // c:4867
3305 if isset(crate::ported::zsh_h::GLOBSUBST) {
3306 // c:4870
3307 for w in words.iter_mut() {
3308 crate::ported::glob::shtokenize(w); // c:4870
3309 }
3310 }
3311 words
3312}
3313
3314/// Lex a `<(...)`/`>(...)`/`=(...)` body — the leading 2 chars are
3315/// the marker pair (`Inang+Inpar`, `Outang+Inpar`, `Equals+Inpar`),
3316/// remainder is the command up to the matching `Outpar`. Returns the
3317/// parsed Eprog (and writes the post-`)` cursor through `eptr`).
3318pub fn parsecmd(cmd: &str, eptr: Option<&mut usize>) -> Option<eprog> {
3319 // c:4878
3320 let bytes = cmd.as_bytes();
3321 // c:4883 — `for (str = cmd + 2; *str && *str != Outpar; str++);`
3322 if bytes.len() < 2 {
3323 return None;
3324 }
3325 let mut str_idx: usize = 2;
3326 while str_idx < bytes.len() && (bytes[str_idx] as char) != Outpar {
3327 str_idx += 1;
3328 }
3329 // c:4884 — `if (!*str || cmd[1] != Inpar)`.
3330 if str_idx >= bytes.len() || (bytes[1] as char) != Inpar {
3331 // c:4884
3332 let errstr = if bytes.len() >= 2 {
3333 untokenize(&cmd[..2]) // c:4891-4892
3334 } else {
3335 String::new()
3336 };
3337 zerr(&format!("unterminated `{}...)'", errstr)); // c:4893
3338 return None; // c:4894
3339 }
3340 // c:4896 — `*str = '\0';` — cmd[str_idx] becomes the terminator.
3341 // c:4897-4898 — `if (eptr) *eptr = str + 1;`
3342 if let Some(p) = eptr {
3343 *p = str_idx + 1;
3344 }
3345 // c:4899 — `parse_string(cmd + 2, 0)`.
3346 let body = &cmd[2..str_idx];
3347 let prog = parse_string(body, 0);
3348 if prog.is_none() {
3349 // c:4899
3350 zerr("parse error in process substitution"); // c:4900
3351 return None; // c:4901
3352 }
3353 prog // c:4903
3354}
3355
3356/// `POUNDBANGLIMIT` from `Src/exec.c:500` — max bytes read from the
3357/// front of a script when probing for a `#!` shebang line.
3358pub const POUNDBANGLIMIT: usize = 128;
3359
3360/// Port of `static char **makecline(LinkList list)` from `Src/exec.c:2046`.
3361///
3362/// Builds the argv array from a command's args list. The C version
3363/// allocates with a 4-slot prepad (2 reserved at the front for the
3364/// shebang `argv[-1]/argv[-2]` overwrite trick in zexecve) — Rust
3365/// doesn't need this since we rebuild the Vec on shebang re-exec
3366/// (see zexecve WARNING e).
3367///
3368/// XTRACE side-effect: each arg is printed via quotedzputs to xtrerr
3369/// (stderr), preceded by the PS4 prefix when first command of the line.
3370pub fn makecline(list: &[String]) -> Vec<String> {
3371 // c:2046
3372 if isset(XTRACE) {
3373 // c:2055
3374 if doneps4.load(Ordering::Relaxed) == 0 {
3375 // c:2056
3376 printprompt4(); // c:2057
3377 }
3378 let mut first = true;
3379 let mut err = std::io::stderr().lock();
3380 use std::io::Write;
3381 for s in list.iter() {
3382 // c:2059
3383 if !first {
3384 let _ = err.write_all(b" "); // c:2063
3385 }
3386 first = false;
3387 let _ = err.write_all(quotedzputs(s).as_bytes()); // c:2061
3388 }
3389 let _ = err.write_all(b"\n"); // c:2065
3390 let _ = err.flush(); // c:2066
3391 }
3392 list.to_vec() // c:2071-2072 — argv built; null terminator implicit in CString[] conversion
3393}
3394
3395/// Port of `static void execute(LinkList args, int flags, int defpath)`
3396/// from `Src/exec.c:723`. The canonical "child runs the simple
3397/// external command" path: STTY/ARGV0/BINF_DASH handling, makecline,
3398/// closem(FDT_XTRACE) + child_unblock, slash-path direct exec,
3399/// defpath (`command -p`) search, cmdnamtab + $PATH walk, with
3400/// commandnotfound-handler fallback and the final exit-code escape
3401/// (127 not-found / 126 noperm).
3402///
3403/// =================== WARNING — DIVERGENCE ====================
3404/// (a) `cmdnamtab->getnode(cmdnamtab, arg0)` (c:824) — HASHED
3405/// fast-path wired via cmdnamtab_lock(); jumps direct to
3406/// `cn.cmd` absolute path before the $PATH scan. Unhashed
3407/// cursor-walk (c:830-846) still falls to the full $PATH scan;
3408/// observable behavior matches C when the hash hit is HASHED.
3409/// (b) `commandnotfound(arg0, args)` (c:809, 873) calls into the
3410/// not-yet-ported `doshfunc` for the `command_not_found_handler`
3411/// shell function. Already routes through executor dispatch
3412/// (see exec.rs:2783).
3413/// (c) `_realexit()` (c:810, 874) — bare `std::process::exit`.
3414/// (d) `SHTTY` close on `!FD_CLOEXEC` (c:781-784) — Rust assumes
3415/// FD_CLOEXEC platform default (macOS, Linux).
3416/// (e) `path` Rust accessor uses paramtab lookup for "PATH";
3417/// `defpath` (`command -p`) walks DEFAULT_PATH via
3418/// search_defpath (already ported).
3419/// =============================================================
3420pub fn execute(args: &mut Vec<String>, flags: u32, defpath: i32) {
3421 // c:723
3422 let mut eno: i32 = 0;
3423 let mut ee: i32; // c:729
3424 let mut arg0 = if args.is_empty() {
3425 return;
3426 } else {
3427 args[0].clone()
3428 }; // c:731
3429 // c:733-748 — STTY pre-exec handling.
3430 {
3431 let mut stty = STTYval.lock().unwrap();
3432 if let Some(s) = stty.take() {
3433 // c:738 — STTYval = 0 to break recursion.
3434 if !s.is_empty()
3435 && unsafe { libc::isatty(0) } != 0
3436 && unsafe { libc::tcgetpgrp(0) } == unsafe { libc::getpid() }
3437 {
3438 drop(stty);
3439 let cmd = format!("stty {}", s); // c:739
3440 execstring(&cmd, 1, 0, "stty"); // c:743
3441 }
3442 }
3443 }
3444 // c:752-763 — ARGV0 override.
3445 if let Some(z) = zgetenv("ARGV0") {
3446 args[0] = z.clone(); // c:753
3447 unsafe {
3448 let key = std::ffi::CString::new("ARGV0").unwrap();
3449 libc::unsetenv(key.as_ptr()); // c:760
3450 }
3451 arg0 = args[0].clone();
3452 } else if (flags & BINF_DASH) != 0 {
3453 // c:764 — `BINF_DASH` prepends `-`.
3454 args[0] = format!("-{}", arg0); // c:767-768
3455 arg0 = args[0].clone();
3456 }
3457 let argv = makecline(args); // c:771
3458 let newenvp_owned: Option<Vec<String>> = if (flags & BINF_CLEARENV) != 0 {
3459 Some(Vec::new()) // c:772-773 — blank_env: char ** with only NULL slot
3460 } else {
3461 None
3462 };
3463 let newenvp = newenvp_owned.as_deref();
3464 closem(FDT_XTRACE, 0); // c:779
3465 // c:780-785 — !FD_CLOEXEC SHTTY close — WARNING (d).
3466 child_unblock(); // c:786
3467 if arg0.len() >= libc::PATH_MAX as usize {
3468 // c:787
3469 zerr(&format!("command too long: {}", arg0)); // c:788
3470 unsafe {
3471 libc::_exit(1);
3472 } // c:789
3473 }
3474 // c:791-801 — slash in arg0 → direct exec.
3475 if let Some(slash_pos) = arg0.find('/') {
3476 let lerrno = zexecve(&arg0, &argv, newenvp); // c:793
3477 let is_dot = arg0.starts_with('.')
3478 && (slash_pos == 1 || (arg0.len() > 2 && &arg0[..2] == ".." && slash_pos == 2));
3479 if slash_pos == 0 || unset(PATHDIRS) || is_dot {
3480 // c:794
3481 zerr(&format!(
3482 "{}: {}",
3483 std::io::Error::from_raw_os_error(lerrno),
3484 arg0
3485 )); // c:797
3486 let code = if lerrno == libc::EACCES || lerrno == libc::ENOEXEC {
3487 126
3488 } else {
3489 127
3490 };
3491 unsafe {
3492 libc::_exit(code);
3493 } // c:798
3494 }
3495 }
3496 if defpath != 0 {
3497 // c:804 — `command -p` default-path search.
3498 let pbuf = match search_defpath(&arg0, libc::PATH_MAX as usize) {
3499 Some(p) => p, // c:808
3500 None => {
3501 if commandnotfound(&arg0, args) == 0 {
3502 // c:809
3503 unsafe {
3504 libc::_exit(LASTVAL.load(Ordering::Relaxed));
3505 }
3506 }
3507 zerr(&format!("command not found: {}", arg0)); // c:811
3508 unsafe {
3509 libc::_exit(127);
3510 } // c:812
3511 }
3512 };
3513 ee = zexecve(&pbuf, &argv, newenvp); // c:815
3514 let dir = pbuf.rsplit_once('/').map(|(d, _)| d).unwrap_or("");
3515 if isgooderr(ee, if dir.is_empty() { "/" } else { dir }) {
3516 // c:819
3517 eno = ee;
3518 }
3519 } else {
3520 // c:822 — cmdnamtab fast-path: if `arg0` is a hashed cmdnam,
3521 // jump straight to the absolute path stored in `cn.cmd`,
3522 // skipping the full $PATH scan (one exec attempt vs N).
3523 // c:824 — `if ((cn = cmdnamtab->getnode(cmdnamtab, arg0)))`.
3524 let hashed_path: Option<String> = {
3525 let tab = cmdnamtab_lock().read().ok();
3526 tab.and_then(|t| {
3527 t.get(&arg0).and_then(|cn| {
3528 if (cn.node.flags & crate::ported::zsh_h::HASHED) != 0 {
3529 // c:827-828 — `strcpy(nn, cn->u.cmd);`
3530 cn.cmd.clone()
3531 } else {
3532 None
3533 }
3534 })
3535 })
3536 };
3537 if let Some(nn) = hashed_path {
3538 // c:848 — `ee = zexecve(nn, argv, newenvp);`
3539 ee = zexecve(&nn, &argv, newenvp);
3540 let dir = nn.rsplit_once('/').map(|(d, _)| d).unwrap_or("");
3541 if isgooderr(ee, if dir.is_empty() { "/" } else { dir }) {
3542 eno = ee;
3543 }
3544 // If the hashed entry's exec failed without a "good" error,
3545 // we still need the $PATH fallback — fall through.
3546 if eno == 0 && ee != 0 {
3547 // Reset for the $PATH scan below.
3548 ee = 0;
3549 }
3550 }
3551 // c:822 — normal $PATH scan (always runs; cmdnam fast-path was an
3552 // optimization but C also walks the rest of `path` if the hashed
3553 // exec failed with a non-"good" error).
3554 let path_str = getsparam("PATH").unwrap_or_default();
3555 for pp in path_str.split(':') {
3556 if pp.is_empty() || pp == "." {
3557 // c:856
3558 ee = zexecve(&arg0, &argv, newenvp); // c:857
3559 if isgooderr(ee, pp) {
3560 eno = ee;
3561 }
3562 } else {
3563 // c:860
3564 let candidate = format!("{}/{}", pp, arg0); // c:861-864
3565 ee = zexecve(&candidate, &argv, newenvp); // c:865
3566 if isgooderr(ee, pp) {
3567 eno = ee;
3568 }
3569 }
3570 }
3571 }
3572 // c:871-881 — final error reporting.
3573 if eno != 0 {
3574 // c:871
3575 zerr(&format!(
3576 "{}: {}",
3577 std::io::Error::from_raw_os_error(eno),
3578 arg0
3579 )); // c:872
3580 } else if commandnotfound(&arg0, args) == 0 {
3581 // c:873
3582 unsafe {
3583 libc::_exit(LASTVAL.load(Ordering::Relaxed));
3584 } // c:874
3585 } else {
3586 zerr(&format!("command not found: {}", arg0)); // c:876
3587 }
3588 let code = if eno == libc::EACCES || eno == libc::ENOEXEC {
3589 126
3590 } else {
3591 127
3592 }; // c:881
3593 unsafe {
3594 libc::_exit(code);
3595 }
3596}
3597
3598/// Port of `static int zexecve(char *pth, char **argv, char **newenvp)`
3599/// from `Src/exec.c:504`. Wraps `execve(2)` with:
3600/// - `$_` env var stamped to absolute `pth` (c:514-520)
3601/// - winch signal unblock right before the syscall (c:527)
3602/// - on `ENOEXEC` / `ENOENT`: reads the first POUNDBANGLIMIT
3603/// bytes, parses a `#!interp arg` shebang and re-execs the
3604/// interpreter (c:534-628). For `ENOEXEC` with no shebang,
3605/// binary-safety check then falls back to `/bin/sh script` per
3606/// POSIX (c:588-628).
3607///
3608/// Returns `errno` from the failing exec — execve only returns on
3609/// failure, so success means the calling process is already replaced.
3610///
3611/// =================== WARNING — DIVERGENCE ====================
3612/// (a) C uses `static char buf[PATH_MAX*2+1]` for the `_=...` env
3613/// string; Rust uses a stack `String` (consumed by `zputenv`).
3614/// (b) `closedumps()` for `!FD_CLOEXEC` (c:521-523) called
3615/// unconditionally as a no-op when FD_CLOEXEC is platform default.
3616/// (c) `unmetafy(pth, NULL)` / round-trip `metafy` at c:510-513,
3617/// c:639-642 — handled implicitly via &str ↔ CString.
3618/// (d) `metafy(execvebuf+2, -1, META_STATIC)` (c:551, 575) — we
3619/// drop the metafy and pass byte ranges to zerr directly.
3620/// (e) `argv[-1]` / `argv[-2]` shebang interpreter slot-overwriting
3621/// (C overwrites BEFORE `argv[0]`) — Rust rebuilds a fresh
3622/// `Vec<String>` with interp + optional arg + original argv tail
3623/// since Vec doesn't expose negative indexing.
3624/// (f) `environ` is FFI-loaded only when `newenvp` is None.
3625/// =============================================================
3626pub fn zexecve(pth: &str, argv: &[String], newenvp: Option<&[String]>) -> i32 {
3627 // c:504
3628 use std::ffi::CString;
3629 // c:514-520 — `_=pth` env stamping.
3630 let pth_abs = if pth.starts_with('/') {
3631 // c:516
3632 pth.to_string() // c:517
3633 } else {
3634 // c:518
3635 format!("{}/{}", getsparam("PWD").unwrap_or_default(), pth) // c:519
3636 };
3637 zputenv(&format!("_={}", pth_abs)); // c:520
3638 closedumps(); // c:522
3639 winch_unblock(); // c:527
3640 let cpth = match CString::new(pth) {
3641 Ok(c) => c,
3642 Err(_) => return libc::ENOENT,
3643 };
3644 let cargs: Vec<CString> = argv
3645 .iter()
3646 .filter_map(|a| CString::new(a.as_str()).ok())
3647 .collect();
3648 let mut argv_ptrs: Vec<*const libc::c_char> = cargs.iter().map(|c| c.as_ptr()).collect();
3649 argv_ptrs.push(std::ptr::null());
3650 let env_holder: Vec<CString>;
3651 let env_ptrs: Vec<*const libc::c_char>;
3652 let envp: *const *const libc::c_char = match newenvp {
3653 Some(env) => {
3654 env_holder = env
3655 .iter()
3656 .filter_map(|e| CString::new(e.as_str()).ok())
3657 .collect();
3658 env_ptrs = {
3659 let mut v: Vec<*const libc::c_char> =
3660 env_holder.iter().map(|c| c.as_ptr()).collect();
3661 v.push(std::ptr::null());
3662 v
3663 };
3664 env_ptrs.as_ptr()
3665 }
3666 None => unsafe {
3667 extern "C" {
3668 static environ: *const *const libc::c_char;
3669 }
3670 environ
3671 },
3672 };
3673 unsafe {
3674 libc::execve(cpth.as_ptr(), argv_ptrs.as_ptr(), envp); // c:528
3675 }
3676 let eno = std::io::Error::last_os_error()
3677 .raw_os_error()
3678 .unwrap_or(libc::ENOEXEC); // c:534
3679 if eno == libc::ENOEXEC || eno == libc::ENOENT {
3680 // c:534
3681 let fd = unsafe { libc::open(cpth.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY) }; // c:538
3682 if fd < 0 {
3683 return std::io::Error::last_os_error()
3684 .raw_os_error()
3685 .unwrap_or(libc::ENOENT); // c:634
3686 }
3687 let mut buf = vec![0u8; POUNDBANGLIMIT + 1]; // c:541
3688 let ct = unsafe {
3689 libc::read(
3690 fd,
3691 buf.as_mut_ptr() as *mut libc::c_void,
3692 POUNDBANGLIMIT as libc::size_t,
3693 )
3694 }; // c:542
3695 unsafe {
3696 libc::close(fd);
3697 } // c:543
3698 if ct >= 0 {
3699 // c:544
3700 let ct = ct as usize;
3701 if ct >= 2 && buf[0] == b'#' && buf[1] == b'!' {
3702 // c:545
3703 let mut t0 = 0;
3704 while t0 < ct && buf[t0] != b'\n' {
3705 t0 += 1;
3706 } // c:546-548
3707 if t0 == ct {
3708 // c:549
3709 zerr(&format!(
3710 // c:550
3711 "{}: bad interpreter: {}: {}",
3712 pth,
3713 String::from_utf8_lossy(&buf[2..t0.min(ct)]),
3714 std::io::Error::from_raw_os_error(eno)
3715 ));
3716 } else {
3717 // c:552
3718 while t0 > 0 && (buf[t0] == b' ' || buf[t0] == b'\t' || buf[t0] == b'\n') {
3719 buf[t0] = 0;
3720 t0 -= 1;
3721 } // c:553-554
3722 let mut ptr_lo: usize = 2;
3723 while ptr_lo < buf.len() && buf[ptr_lo] == b' ' {
3724 ptr_lo += 1;
3725 } // c:555
3726 let ptr2_lo = ptr_lo;
3727 let mut ptr_hi = ptr2_lo;
3728 while ptr_hi < buf.len() && buf[ptr_hi] != 0 && buf[ptr_hi] != b' ' {
3729 ptr_hi += 1;
3730 } // c:556
3731 let interp_str = String::from_utf8_lossy(&buf[ptr2_lo..ptr_hi]).into_owned();
3732 if eno == libc::ENOENT {
3733 // c:557 — pathprog rewrite path.
3734 let pprog = if !interp_str.starts_with('/') {
3735 // c:561
3736 pathprog(&interp_str).map(|p| p.display().to_string())
3737 } else {
3738 None
3739 };
3740 if let Some(pprog) = pprog {
3741 // c:562
3742 let mut argv_new: Vec<String> = Vec::with_capacity(argv.len() + 2);
3743 argv_new.push(interp_str.clone()); // c:564
3744 if ptr_hi >= buf.len() || buf[ptr_hi] == 0 {
3745 argv_new.push(pth.to_string());
3746 } else {
3747 // c:567
3748 let mut rest_lo = ptr_hi + 1;
3749 while rest_lo < buf.len() && buf[rest_lo] == b' ' {
3750 rest_lo += 1;
3751 }
3752 let mut rest_hi = rest_lo;
3753 while rest_hi < buf.len() && buf[rest_hi] != 0 {
3754 rest_hi += 1;
3755 }
3756 let arg_str =
3757 String::from_utf8_lossy(&buf[rest_lo..rest_hi]).into_owned();
3758 argv_new.push(arg_str);
3759 argv_new.push(pth.to_string());
3760 }
3761 for orig in argv.iter().skip(1) {
3762 argv_new.push(orig.clone());
3763 }
3764 winch_unblock(); // c:565/c:570
3765 return zexecve(&pprog, &argv_new, newenvp); // c:566/c:571
3766 }
3767 zerr(&format!(
3768 // c:574
3769 "{}: bad interpreter: {}: {}",
3770 pth,
3771 interp_str,
3772 std::io::Error::from_raw_os_error(eno)
3773 ));
3774 } else if ptr_hi < buf.len() && buf[ptr_hi] != 0 {
3775 // c:576
3776 let mut rest_lo = ptr_hi + 1;
3777 while rest_lo < buf.len() && buf[rest_lo] == b' ' {
3778 rest_lo += 1;
3779 }
3780 let mut rest_hi = rest_lo;
3781 while rest_hi < buf.len() && buf[rest_hi] != 0 {
3782 rest_hi += 1;
3783 }
3784 let arg_str = String::from_utf8_lossy(&buf[rest_lo..rest_hi]).into_owned();
3785 let mut argv_new: Vec<String> =
3786 vec![interp_str.clone(), arg_str, pth.to_string()];
3787 for orig in argv.iter().skip(1) {
3788 argv_new.push(orig.clone());
3789 }
3790 winch_unblock(); // c:580
3791 return zexecve(&interp_str, &argv_new, newenvp); // c:581
3792 } else {
3793 // c:582
3794 let mut argv_new: Vec<String> = vec![interp_str.clone(), pth.to_string()];
3795 for orig in argv.iter().skip(1) {
3796 argv_new.push(orig.clone());
3797 }
3798 winch_unblock(); // c:584
3799 return zexecve(&interp_str, &argv_new, newenvp); // c:585
3800 }
3801 }
3802 } else if eno == libc::ENOEXEC {
3803 // c:588 — binary-safety + /bin/sh fallback.
3804 let nul_pos = buf[..ct].iter().position(|&b| b == 0); // c:597
3805 let isbinary = match nul_pos {
3806 None => false, // c:598
3807 Some(npos) => {
3808 let mut has_letter = false;
3809 let mut binary = true;
3810 for &b in &buf[..npos] {
3811 // c:602-609
3812 if (b as char).is_ascii_lowercase() || b == b'$' || b == b'`' {
3813 has_letter = true;
3814 }
3815 if has_letter && b == b'\n' {
3816 binary = false; // c:606
3817 break;
3818 }
3819 }
3820 binary
3821 }
3822 };
3823 if !isbinary {
3824 // c:611
3825 let mut argv_new: Vec<String> = Vec::with_capacity(argv.len() + 2);
3826 argv_new.push("sh".to_string()); // c:625
3827 if !argv.is_empty() && (argv[0].starts_with('-') || argv[0].starts_with('+')) {
3828 argv_new.push("-".to_string()); // c:623
3829 }
3830 for orig in argv.iter() {
3831 argv_new.push(orig.clone());
3832 }
3833 winch_unblock(); // c:626
3834 return zexecve("/bin/sh", &argv_new, newenvp); // c:627
3835 }
3836 }
3837 }
3838 }
3839 eno // c:643
3840}
3841
3842/// Port of `char *getoutputfile(char *cmd, char **eptr)` from
3843/// `Src/exec.c:4910` — `=(cmd)` process substitution.
3844///
3845/// Substitutes the cmd's stdout into a temp file, returns the
3846/// filename. Optimised path: `=(<<<heredoc-str)` writes the
3847/// heredoc body directly without a fork.
3848///
3849/// (a) `addfilelist(nam, 0)` (c:4960) wired via `JOBTAB[thisjob]`
3850/// so the temp file gets cleaned at job exit.
3851/// (b) `waitforpid` Rust takes 1 arg `pid`, C takes `(pid, full)`.
3852/// Behavior matches the `full=0` case anyway.
3853/// (c) `entersubsh` is ported at exec.rs:3934 — wire it here when
3854/// re-routing the fork path away from setsid-only fallback.
3855/// (d) `execode` is now ported (exec.rs:6047) — the body still
3856/// re-feeds through fusevm for cache coherence with execstring.
3857/// (e) `_realexit` flushes stdio + jobs + history. We use bare
3858/// `std::process::exit(0)` for now.
3859/// (f) TMPSUFFIX link()-rename block (c:4951-4958) deferred; rare
3860/// `setopt suffix_alias` interaction with =(…).
3861pub fn getoutputfile(cmd: &str, eptr: Option<&mut usize>) -> Option<String> {
3862 // c:4910
3863 let bytes = cmd.as_bytes();
3864 let _ = bytes;
3865 // c:4918 — `if (thisjob == -1)` — guard removed (thisjob model differs).
3866 let mut ends_at: usize = 0;
3867 let prog = parsecmd(cmd, Some(&mut ends_at))?; // c:4922
3868 if let Some(p) = eptr {
3869 *p = ends_at;
3870 }
3871 let mut nam = gettempname(None, true)?; // c:4924
3872 // c:4927 — `simple_redir_name` opt for `=(<<<str)`.
3873 let mut s: Option<String> = simple_redir_name(&prog, REDIR_HERESTR).map(|raw| {
3874 // c:4933
3875 let mut sub = singsub(&raw); // c:4933
3876 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
3877 // c:4934
3878 String::new() // c:4935 — sentinel; checked below
3879 } else {
3880 sub = untokenize(&sub); // c:4937
3881 dyncat(&sub, "\n") // c:4938
3882 }
3883 });
3884 if let Some(ref sv) = s {
3885 if sv.is_empty() {
3886 s = None;
3887 }
3888 }
3889 if s.is_none() {
3890 // c:4942
3891 child_block(); // c:4943
3892 }
3893 // c:4945 — `open(nam, O_WRONLY|O_CREAT|O_EXCL|O_NOCTTY, 0600)`.
3894 let c_nam = match std::ffi::CString::new(nam.clone()) {
3895 Ok(c) => c,
3896 Err(_) => {
3897 if s.is_none() {
3898 child_unblock();
3899 }
3900 return None;
3901 }
3902 };
3903 let fd = unsafe {
3904 libc::open(
3905 c_nam.as_ptr(),
3906 libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | libc::O_NOCTTY,
3907 0o600 as libc::c_uint,
3908 )
3909 };
3910 if fd < 0 {
3911 // c:4945
3912 zerr(&format!(
3913 "process substitution failed: {}",
3914 std::io::Error::last_os_error()
3915 )); // c:4946
3916 if s.is_none() {
3917 child_unblock(); // c:4948
3918 }
3919 return None; // c:4949
3920 }
3921 // c:4951-4958 — TMPSUFFIX link block (see WARNING f).
3922 // c:4960 — `addfilelist(nam, 0);` — register temp file in current
3923 // job's filelist so it's unlinked at job exit (not relying on the
3924 // OS temp-reaper).
3925 if let Some(jt) = JOBTAB.get() {
3926 let mut guard = jt.lock().unwrap();
3927 let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
3928 if tj >= 0 {
3929 if let Some(j) = guard.get_mut(tj as usize) {
3930 crate::ported::jobs::addfilelist(j, Some(&nam), 0);
3931 }
3932 }
3933 }
3934 if let Some(sv) = s {
3935 // c:4962 — optimised here-string write path.
3936 let mut buf: Vec<u8> = sv.into_bytes();
3937 let _len = unmetafy(&mut buf); // c:4965
3938 let _ = write_loop(fd, &buf); // c:4966
3939 unsafe {
3940 libc::close(fd);
3941 } // c:4967
3942 return Some(nam); // c:4968
3943 }
3944 // c:4971 — `cmdoutpid = pid = zfork(NULL)`.
3945 let pid = zfork(None);
3946 cmdoutpid.store(pid, Ordering::Relaxed);
3947 if pid == -1 {
3948 // c:4972
3949 unsafe {
3950 libc::close(fd);
3951 } // c:4973
3952 child_unblock(); // c:4974
3953 return Some(nam); // c:4975
3954 } else if pid != 0 {
3955 // c:4976 — parent.
3956 unsafe {
3957 libc::close(fd);
3958 } // c:4977
3959 let _ = waitforpid(pid); // c:4978
3960 cmdoutval.store(0, Ordering::Relaxed); // c:4979
3961 return Some(nam); // c:4980
3962 }
3963 // c:4983 — child.
3964 closem(FDT_UNUSED, 0); // c:4984
3965 let _ = redup(fd, 1); // c:4985
3966 entersubsh(esub::PGRP | esub::NOMONITOR, None); // c:4986
3967 cmdpush(CS_CMDSUBST as u8); // c:4987
3968 // c:4988 — execode — WARNING (d).
3969 let body_end = if ends_at > 0 { ends_at - 1 } else { 2 };
3970 let body = if body_end > 2 && body_end <= cmd.len() {
3971 &cmd[2..body_end]
3972 } else {
3973 ""
3974 };
3975 let _ = crate::ported::exec_hooks::execute_script_zsh_pipeline(body);
3976 cmdpop(); // c:4989
3977 unsafe {
3978 libc::close(1);
3979 } // c:4990
3980 // _realexit — WARNING (e)
3981 std::process::exit(0); // c:4991
3982 #[allow(unreachable_code)]
3983 {
3984 // c:4992-4993 — `zerr("exit returned in child!!"); kill(getpid(), SIGKILL);`
3985 let _ = &mut nam;
3986 unsafe {
3987 libc::kill(libc::getpid(), libc::SIGKILL);
3988 }
3989 None
3990 }
3991}
3992
3993/// Port of `char *getproc(char *cmd, char **eptr)` from
3994/// `Src/exec.c:5025` — `<(cmd)` / `>(cmd)` process substitution
3995/// via `/dev/fd/N` (PATH_DEV_FD branch; modern Linux/macOS).
3996///
3997/// (a) PATH_DEV_FD branch only — the FIFO fallback (`!PATH_DEV_FD`
3998/// path c:5037-5064) is omitted; modern Linux/macOS both
3999/// provide /dev/fd. `namedpipe()` is ported (exec.rs:2701) but
4000/// unused here.
4001/// (b) `addproc` is 7-arg; procsubst pid recorded via aux=true on
4002/// the current job (c:5141-5142).
4003/// (c) `addfilelist(NULL, fd)` wired via `JOBTAB[thisjob]` at
4004/// c:5087.
4005/// (d) `entersubsh` is ported at exec.rs:3934 — wired below at
4006/// c:5063 (`entersubsh(ESUB_ASYNC|ESUB_PGRP, NULL)`).
4007/// (e) `execode` is ported at exec.rs:6047. Body still re-feeds
4008/// through fusevm for cache coherence.
4009/// (f) `_realexit` flushes stdio + jobs + history. We use bare
4010/// `std::process::exit(LASTVAL)` for now.
4011/// (g) `fdtable[fd] = FDT_PROC_SUBST` (c:5086) — set via fdtable_set.
4012pub fn getproc(cmd: &str, eptr: Option<&mut usize>) -> Option<String> {
4013 // c:5025
4014 let bytes = cmd.as_bytes();
4015 let out: i32 = if !bytes.is_empty() && (bytes[0] as char) == Inang {
4016 1 // c:5032 — `<(...)` writer-side child
4017 } else {
4018 0
4019 };
4020 // c:5068-5071 — `if (thisjob == -1) { zerr(...); return NULL; }` —
4021 // proc subst needs a host job to attach the child to.
4022 let tj_check = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
4023 if tj_check == -1 {
4024 zerr(&format!("process substitution {} cannot be used here", cmd)); // c:5069
4025 return None; // c:5070
4026 }
4027 // c:5072 — PATH_DEV_FD path: allocate buffer for the /dev/fd/N string.
4028 let mut ends_at: usize = 0;
4029 let _prog = parsecmd(cmd, Some(&mut ends_at))?; // c:5073
4030 if let Some(p) = eptr {
4031 *p = ends_at;
4032 }
4033 let mut pipes: [i32; 2] = [-1; 2];
4034 if mpipe(&mut pipes) < 0 {
4035 // c:5075
4036 return None;
4037 }
4038 let mut bgtime: ZshTimespec = libc::timespec {
4039 tv_sec: 0,
4040 tv_nsec: 0,
4041 };
4042 let pid = zfork(Some(&mut bgtime)); // c:5077
4043 if pid != 0 {
4044 // c:5077 — parent path.
4045 let pnam = format!("/dev/fd/{}", pipes[(1 - out) as usize]); // c:5078
4046 let _ = zclose(pipes[out as usize]); // c:5079
4047 if pid == -1 {
4048 // c:5080
4049 let _ = zclose(pipes[(1 - out) as usize]); // c:5082
4050 return None; // c:5083
4051 }
4052 let fd = pipes[(1 - out) as usize]; // c:5085
4053 fdtable_set(fd, FDT_PROC_SUBST); // c:5086
4054 // c:5087 — `addfilelist(NULL, fd);` — register the proc-subst
4055 // pipe fd in the current job's filelist so it's closed at job exit.
4056 if let Some(jt) = JOBTAB.get() {
4057 let mut guard = jt.lock().unwrap();
4058 let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
4059 if tj >= 0 {
4060 if let Some(j) = guard.get_mut(tj as usize) {
4061 crate::ported::jobs::addfilelist(j, None, fd);
4062 }
4063 }
4064 }
4065 // c:5088-5091 — `if (!out) addproc(pid, NULL, 1, &bgtime, -1, -1);` —
4066 // record the proc-subst writer-side child in the job's
4067 // auxprocs (aux=true). For `<(cmd)` (out==1 = reader-side
4068 // child), C omits the addproc — symmetric here.
4069 if out == 0 {
4070 if let Some(jt) = JOBTAB.get() {
4071 let mut guard = jt.lock().unwrap();
4072 let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
4073 if tj >= 0 {
4074 if let Some(j) = guard.get_mut(tj as usize) {
4075 crate::ported::jobs::addproc(
4076 j,
4077 pid,
4078 "",
4079 true,
4080 Some(std::time::Instant::now()),
4081 -1,
4082 -1,
4083 );
4084 }
4085 }
4086 }
4087 }
4088 procsubstpid.store(pid, Ordering::Relaxed); // c:5092
4089 return Some(pnam); // c:5093
4090 }
4091 // c:5095 — child.
4092 entersubsh(esub::ASYNC | esub::PGRP, None); // c:5095
4093 let _ = redup(pipes[out as usize], out); // c:5096
4094 closem(FDT_UNUSED, 0); // c:5097
4095 cmdpush(CS_CMDSUBST as u8); // c:5100
4096 let body_end = if ends_at > 0 { ends_at - 1 } else { 2 };
4097 let body = if body_end > 2 && body_end <= cmd.len() {
4098 &cmd[2..body_end]
4099 } else {
4100 ""
4101 };
4102 let _ = crate::ported::exec_hooks::execute_script_zsh_pipeline(body);
4103 cmdpop(); // c:5102
4104 let _ = zclose(out); // c:5103
4105 std::process::exit(LASTVAL.load(Ordering::Relaxed)); // c:5104
4106}
4107
4108/// Port of `enum { ESUB_ASYNC, ESUB_PGRP, ... };` from `Src/exec.c:1056`.
4109/// Flag bits for `entersubsh(int flags, struct entersubsh_ret *retp)`.
4110pub mod esub {
4111 // c:1056
4112 /// `ASYNC` constant.
4113 pub const ASYNC: i32 = 0x01; // c:1058
4114 /// `PGRP` constant.
4115 pub const PGRP: i32 = 0x02; // c:1063
4116 /// `KEEPTRAP` constant.
4117 pub const KEEPTRAP: i32 = 0x04; // c:1065
4118 /// `FAKE` constant.
4119 pub const FAKE: i32 = 0x08; // c:1067
4120 /// `REVERTPGRP` constant.
4121 pub const REVERTPGRP: i32 = 0x10; // c:1069
4122 /// `NOMONITOR` constant.
4123 pub const NOMONITOR: i32 = 0x20; // c:1071
4124 /// `JOB_CONTROL` constant.
4125 pub const JOB_CONTROL: i32 = 0x40; // c:1073
4126}
4127
4128/// Port of `struct entersubsh_ret` from `Src/exec.c` (forward decl).
4129/// Out-arg used by `entersubsh()` to hand back the group-leader pid
4130/// and the list-pipe job index the parent should track. Only filled
4131/// in for `ESUB_PGRP` + non-async forks (synchronous pipeline child
4132/// groups).
4133#[allow(non_camel_case_types)]
4134#[derive(Default)]
4135pub struct entersubsh_ret {
4136 pub gleader: i32, // c:1122
4137 pub list_pipe_job: i32, // c:1123
4138}
4139
4140/// Port of `static void entersubsh(int flags, struct entersubsh_ret *retp)`
4141/// from `Src/exec.c:1083`. Called by every child fork to switch the
4142/// process into subshell mode: traps reset, monitor disabled, signals
4143/// re-defaulted, pgrp + tty handed off, saved fds closed, jobtab
4144/// cleared, ZSH_SUBSHELL bumped, forklevel = locallevel.
4145///
4146/// (a) `jobtab[list_pipe_job]` / `jobtab[thisjob]` pgrp ops (c:1110-
4147/// 1151) are now ported via `JOBTAB[thisjob]`.gleader access; the
4148/// ESUB_PGRP+sync path establishes pipeline group-leadership
4149/// (list_pipe_job inherit or thisjob-as-leader), filling
4150/// entersubsh_ret with the chosen gleader + list_pipe_job index.
4151/// (b) `clearjobtab(monitor)` (c:1219) — Rust signature is
4152/// `clearjobtab(&mut JobTable, monitor)`; we get the global table
4153/// via a TABLE handle similar to other jobs.rs entries.
4154/// (c) `attachtty(...)` (c:1119, 1144) — wired via libc::tcsetpgrp(2, gleader).
4155/// (d) `release_pgrp()` called for ESUB_REVERTPGRP when `getpid() ==
4156/// mypgrp` — direct C parity (jobs.rs:3406 provides the call).
4157/// (e) `opts[USEZLE] = 0; zleactive = 0` — Rust opts table lookup
4158/// uses `opts_set_off(USEZLE)`; zleactive is the atomic in
4159/// builtins/sched.rs.
4160/// =============================================================
4161pub fn entersubsh(flags: i32, retp: Option<&mut entersubsh_ret>) {
4162 // c:1083
4163 let monitor: i32;
4164 let job_control_ok: i32;
4165 // c:1088-1092 — reset traps unless KEEPTRAP.
4166 if (flags & esub::KEEPTRAP) == 0 {
4167 // c:1088
4168 for sig in 0..=SIGCOUNT {
4169 // c:1089
4170 let st = {
4171 let guard = sigtrapped.lock().unwrap();
4172 guard.get(sig as usize).copied().unwrap_or(0)
4173 };
4174 let func_set = (st & ZSIG_FUNC) != 0; // c:1090
4175 let posix_ignored = isset(POSIXTRAPS) && ((st & ZSIG_IGNORED) != 0); // c:1091
4176 if !func_set && !posix_ignored {
4177 unsettrap(sig); // c:1092
4178 }
4179 }
4180 }
4181 monitor = if isset(MONITOR) { 1 } else { 0 }; // c:1093
4182 job_control_ok = if monitor != 0 && (flags & esub::JOB_CONTROL) != 0 && isset(POSIXJOBS) {
4183 // c:1094
4184 1
4185 } else {
4186 0
4187 };
4188 EXIT_VAL.store(0, Ordering::Relaxed); // c:1095
4189 if (flags & esub::NOMONITOR) != 0 {
4190 // c:1096
4191 dosetopt(MONITOR, 0, 0); // c:1097
4192 }
4193 if !isset(MONITOR) {
4194 // c:1098
4195 if (flags & esub::ASYNC) != 0 {
4196 // c:1099
4197 let _ = settrap(libc::SIGINT, None, 0); // c:1100
4198 let _ = settrap(libc::SIGQUIT, None, 0); // c:1101
4199 if unsafe { libc::isatty(0) } != 0 {
4200 // c:1102
4201 unsafe {
4202 libc::close(0);
4203 } // c:1103
4204 let devnull = std::ffi::CString::new("/dev/null").unwrap();
4205 if unsafe { libc::open(devnull.as_ptr(), libc::O_RDWR | libc::O_NOCTTY) } != 0 {
4206 // c:1104
4207 zerr(&format!(
4208 // c:1105
4209 "can't open /dev/null: {}",
4210 std::io::Error::last_os_error()
4211 ));
4212 unsafe {
4213 libc::_exit(1);
4214 } // c:1106
4215 }
4216 }
4217 }
4218 } else if (flags & esub::PGRP) != 0 {
4219 // c:1110 — `else if (thisjob != -1 && (flags & ESUB_PGRP))`.
4220 let thisjob = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
4221 if thisjob != -1 {
4222 let lpj = list_pipe_job.load(Ordering::Relaxed);
4223 let lp = list_pipe.load(Ordering::Relaxed);
4224 let lpc = list_pipe_child.load(Ordering::Relaxed);
4225 if let Some(jt) = JOBTAB.get() {
4226 let mut guard = jt.lock().unwrap();
4227 let lpj_gleader = guard.get(lpj as usize).map(|j| j.gleader).unwrap_or(0);
4228 if lpj_gleader != 0 && (lp != 0 || lpc != 0) {
4229 // c:1111-1124 — inherit list_pipe_job's group leader.
4230 let pgid = if unsafe { libc::setpgid(0, lpj_gleader) } == -1
4231 || (unsafe { libc::killpg(lpj_gleader, 0) } == -1
4232 && std::io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH))
4233 {
4234 // c:1115-1117 — primary group leader gone; this child becomes leader.
4235 let new_gl = if lpc != 0 {
4236 mypgrp.load(Ordering::Relaxed)
4237 } else {
4238 unsafe { libc::getpid() }
4239 };
4240 if let Some(j) = guard.get_mut(lpj as usize) {
4241 j.gleader = new_gl;
4242 }
4243 if let Some(j) = guard.get_mut(thisjob as usize) {
4244 j.gleader = new_gl;
4245 }
4246 unsafe { libc::setpgid(0, new_gl) };
4247 if (flags & esub::ASYNC) == 0 {
4248 unsafe { libc::tcsetpgrp(2, new_gl) }; // c:1119 attachtty
4249 }
4250 new_gl
4251 } else {
4252 lpj_gleader
4253 };
4254 if let Some(r) = retp {
4255 if (flags & esub::ASYNC) == 0 {
4256 r.gleader = pgid; // c:1122
4257 r.list_pipe_job = lpj; // c:1123
4258 }
4259 }
4260 } else {
4261 // c:1126-1151 — standard group-leader-takeover path.
4262 let thisjob_gleader =
4263 guard.get(thisjob as usize).map(|j| j.gleader).unwrap_or(0);
4264 if thisjob_gleader == 0 || unsafe { libc::setpgid(0, thisjob_gleader) } == -1 {
4265 let new_gl = unsafe { libc::getpid() };
4266 if let Some(j) = guard.get_mut(thisjob as usize) {
4267 j.gleader = new_gl; // c:1138
4268 }
4269 if lpj != thisjob {
4270 let lpj_was_unset = guard
4271 .get(lpj as usize)
4272 .map(|j| j.gleader == 0)
4273 .unwrap_or(true);
4274 if lpj_was_unset {
4275 if let Some(j) = guard.get_mut(lpj as usize) {
4276 j.gleader = new_gl; // c:1140-1141
4277 }
4278 }
4279 }
4280 unsafe { libc::setpgid(0, new_gl) }; // c:1142
4281 if (flags & esub::ASYNC) == 0 {
4282 unsafe { libc::tcsetpgrp(2, new_gl) }; // c:1144 attachtty
4283 if let Some(r) = retp {
4284 r.gleader = new_gl; // c:1146
4285 if lpj != thisjob {
4286 r.list_pipe_job = lpj; // c:1148
4287 }
4288 }
4289 }
4290 }
4291 }
4292 }
4293 } else {
4294 // No real job slot; basic setpgid fallback.
4295 unsafe { libc::setpgid(0, 0) };
4296 }
4297 }
4298 if (flags & esub::FAKE) == 0 {
4299 // c:1153
4300 subsh.store(1, Ordering::Relaxed); // c:1154
4301 }
4302 // c:1161 — `zsh_subshell++;` regardless of FAKE.
4303 zsh_subshell.fetch_add(1, Ordering::Relaxed);
4304 // c:1162 — `if ((flags & ESUB_REVERTPGRP) && getpid() == mypgrp)`.
4305 if (flags & esub::REVERTPGRP) != 0
4306 && unsafe { libc::getpid() } == mypgrp.load(Ordering::Relaxed)
4307 {
4308 release_pgrp(); // c:1163
4309 }
4310 *shout.lock().unwrap() = 0; // c:1164 — shout = NULL
4311 if (flags & esub::NOMONITOR) != 0 {
4312 // c:1165
4313 signal_ignore(libc::SIGTTOU); // c:1171
4314 signal_ignore(libc::SIGTTIN); // c:1172
4315 signal_ignore(libc::SIGTSTP); // c:1173
4316 } else if job_control_ok == 0 {
4317 // c:1174
4318 signal_default(libc::SIGTTOU); // c:1181
4319 signal_default(libc::SIGTTIN); // c:1182
4320 signal_default(libc::SIGTSTP); // c:1183
4321 }
4322 let interact = isset(INTERACTIVE); // c:1185 — Rust uses INTERACTIVE option as proxy
4323 if interact {
4324 signal_default(libc::SIGTERM); // c:1186
4325 let int_st = sigtrapped
4326 .lock()
4327 .unwrap()
4328 .get(libc::SIGINT as usize)
4329 .copied()
4330 .unwrap_or(0);
4331 if (int_st & ZSIG_IGNORED) == 0 {
4332 // c:1187
4333 signal_default(libc::SIGINT); // c:1188
4334 }
4335 let pipe_st = sigtrapped
4336 .lock()
4337 .unwrap()
4338 .get(libc::SIGPIPE as usize)
4339 .copied()
4340 .unwrap_or(0);
4341 if pipe_st == 0 {
4342 // c:1189
4343 signal_default(libc::SIGPIPE); // c:1190
4344 }
4345 }
4346 let quit_st = sigtrapped
4347 .lock()
4348 .unwrap()
4349 .get(libc::SIGQUIT as usize)
4350 .copied()
4351 .unwrap_or(0);
4352 if (quit_st & ZSIG_IGNORED) == 0 {
4353 // c:1192
4354 signal_default(libc::SIGQUIT); // c:1193
4355 }
4356 // c:1202-1205 — unblock any trapped signals while in `intrap`.
4357 if intrap.load(Ordering::Relaxed) != 0 {
4358 // c:1202
4359 for sig in 1..=SIGCOUNT {
4360 let st = sigtrapped
4361 .lock()
4362 .unwrap()
4363 .get(sig as usize)
4364 .copied()
4365 .unwrap_or(0);
4366 if st != 0 && st != ZSIG_IGNORED {
4367 // c:1204
4368 let m = signal_mask(sig);
4369 let _ = signal_unblock(&m); // c:1205
4370 }
4371 }
4372 }
4373 if job_control_ok == 0 {
4374 // c:1206
4375 dosetopt(MONITOR, 0, 0); // c:1207
4376 }
4377 dosetopt(USEZLE, 0, 0); // c:1208
4378 zleactive.store(0, Ordering::Relaxed); // c:1209
4379 // c:1214-1217 — close saved fds.
4380 let max = MAX_ZSH_FD.load(Ordering::Relaxed);
4381 for i in 10..=max {
4382 if (fdtable_get(i) & FDT_SAVED_MASK) != 0 {
4383 // c:1215
4384 let _ = zclose(i); // c:1216
4385 }
4386 }
4387 // c:1218-1219 — `clearjobtab(monitor);` — calls the canonical port
4388 // at jobs.rs:1695 which handles ALL the C body including the
4389 // oldjobtab snapshot path (c:1799-1817) under POSIXJOBS guard.
4390 let mut dummy_table = crate::exec_jobs::JobTable::new();
4391 crate::ported::jobs::clearjobtab(&mut dummy_table, monitor);
4392 let _ = get_usage(); // c:1220
4393 FORKLEVEL.store(
4394 // c:1221 — `forklevel = locallevel;`
4395 locallevel.load(Ordering::Relaxed),
4396 Ordering::Relaxed,
4397 );
4398}
4399
4400/// Port of `static int getpipe(char *cmd, int nullexec)` from
4401/// `Src/exec.c:5119`.
4402///
4403/// C body executes `<(cmd)` / `>(cmd)` process substitution via a
4404/// pipe pair: parent gets back the readable (`<(...)`) or writable
4405/// (`>(...)`) end as an fd; child runs the substituted command with
4406/// its stdio redirected into the other end.
4407///
4408/// ```c
4409/// Eprog prog;
4410/// int pipes[2], out = *cmd == Inang;
4411/// pid_t pid;
4412/// struct timespec bgtime;
4413/// char *ends;
4414/// if (!(prog = parsecmd(cmd, &ends))) return -1;
4415/// if (*ends) { zerr("invalid syntax..."); return -1; }
4416/// if (mpipe(pipes) < 0) return -1;
4417/// if ((pid = zfork(&bgtime))) {
4418/// zclose(pipes[out]);
4419/// if (pid == -1) { zclose(pipes[!out]); return -1; }
4420/// if (!nullexec) addproc(pid, NULL, 1, &bgtime, -1, -1);
4421/// procsubstpid = pid;
4422/// return pipes[!out];
4423/// }
4424/// entersubsh(ESUB_ASYNC|ESUB_PGRP|ESUB_NOMONITOR, NULL);
4425/// redup(pipes[out], out);
4426/// closem(FDT_UNUSED, 0);
4427/// cmdpush(CS_CMDSUBST);
4428/// execode(prog, 0, 1, out ? "outsubst" : "insubst");
4429/// cmdpop();
4430/// _realexit();
4431/// ```
4432///
4433/// (a) `addproc` is now 7-arg (jobs.rs:1516) — wired at the
4434/// procsubst pid recording site (c:5141-5142) earlier this
4435/// session; the child IS now recorded in `JOBTAB[thisjob]`.
4436/// (b) `entersubsh` IS now ported (exec.rs:3934) including the
4437/// ESUB_PGRP pipeline group-leadership path — wired this
4438/// session for getpipe's `entersubsh(ESUB_ASYNC|ESUB_PGRP|
4439/// ESUB_NOMONITOR, NULL)` call.
4440/// (c) `execode(prog, ...)` IS now ported (exec.rs:6047) — getpipe
4441/// can route through execode for the parsed eprog. Currently
4442/// this caller still uses the fusevm pipeline for cache
4443/// coherence with execstring; switch over when the wordcode
4444/// walker becomes the primary path.
4445/// (d) `_realexit()` flushes stdio + jobs + history. We use bare
4446/// `std::process::exit(lastval)` for now.
4447pub fn getpipe(cmd: &str, nullexec: i32) -> i32 {
4448 // c:5119
4449 let bytes = cmd.as_bytes();
4450 let out: i32 = if !bytes.is_empty() && (bytes[0] as char) == Inang {
4451 1 // c:5122 — `<(...)` reads from child, child writes to fd 1
4452 } else {
4453 0 // `>(...)` — child reads from fd 0
4454 };
4455 let mut ends_at: usize = 0;
4456 let prog = parsecmd(cmd, Some(&mut ends_at)); // c:5127
4457 if prog.is_none() {
4458 // c:5127
4459 return -1; // c:5128
4460 }
4461 // c:5129 — `if (*ends)` — trailing bytes after the `)` are invalid.
4462 if ends_at < bytes.len() && bytes[ends_at] != 0 {
4463 zerr("invalid syntax for process substitution in redirection"); // c:5130
4464 return -1; // c:5131
4465 }
4466 let mut pipes: [i32; 2] = [-1; 2];
4467 if mpipe(&mut pipes) < 0 {
4468 // c:5133
4469 return -1;
4470 }
4471 // c:5135 — `if ((pid = zfork(&bgtime)))` — parent path.
4472 let mut bgtime: ZshTimespec = libc::timespec {
4473 tv_sec: 0,
4474 tv_nsec: 0,
4475 };
4476 let pid = zfork(Some(&mut bgtime)); // c:5135
4477 if pid != 0 {
4478 // c:5135 — parent.
4479 let _ = zclose(pipes[out as usize]); // c:5136
4480 if pid == -1 {
4481 // c:5137
4482 let _ = zclose(pipes[(1 - out) as usize]); // c:5138
4483 return -1; // c:5139
4484 }
4485 // c:5141-5142 — `if (!nullexec) addproc(pid, NULL, 1, &bgtime, -1, -1);`
4486 if nullexec == 0 {
4487 if let Some(jt) = JOBTAB.get() {
4488 let mut guard = jt.lock().unwrap();
4489 let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
4490 if tj >= 0 {
4491 if let Some(j) = guard.get_mut(tj as usize) {
4492 crate::ported::jobs::addproc(
4493 j,
4494 pid,
4495 "",
4496 true, // aux=1 for proc subst
4497 Some(std::time::Instant::now()),
4498 -1,
4499 -1,
4500 );
4501 }
4502 }
4503 }
4504 }
4505 procsubstpid.store(pid, Ordering::Relaxed); // c:5143
4506 return pipes[(1 - out) as usize]; // c:5144
4507 }
4508 // c:5146 — child path.
4509 entersubsh(esub::ASYNC | esub::PGRP | esub::NOMONITOR, None); // c:5146
4510 let _ = redup(pipes[out as usize], out); // c:5147
4511 closem(FDT_UNUSED, 0); // c:5148
4512 cmdpush(CS_CMDSUBST as u8); // c:5149
4513 // c:5150 — execode(prog, 0, 1, ...) — see WARNING (c).
4514 let body_end = if ends_at > 0 { ends_at - 1 } else { 2 };
4515 let body = if body_end > 2 && body_end <= bytes.len() {
4516 &cmd[2..body_end]
4517 } else {
4518 ""
4519 };
4520 let _ = crate::ported::exec_hooks::execute_script_zsh_pipeline(body);
4521 cmdpop(); // c:5151
4522 // c:5152 — _realexit() — WARNING (d).
4523 std::process::exit(LASTVAL.load(Ordering::Relaxed));
4524}
4525
4526/// Port of `static void spawnpipes(LinkList l, int nullexec)` from
4527/// `Src/exec.c:5184`.
4528///
4529/// Walks a redir list `l`, and for each REDIR_OUTPIPE/REDIR_INPIPE
4530/// entry fires `getpipe(name, nullexec || varid)` and stashes the
4531/// resulting fd into `f->fd2`.
4532///
4533/// ```c
4534/// LinkNode n;
4535/// Redir f;
4536/// char *str;
4537/// n = firstnode(l);
4538/// for (; n; incnode(n)) {
4539/// f = (Redir) getdata(n);
4540/// if (f->type == REDIR_OUTPIPE || f->type == REDIR_INPIPE) {
4541/// str = f->name;
4542/// f->fd2 = getpipe(str, nullexec || f->varid);
4543/// }
4544/// }
4545/// ```
4546///
4547/// =================== WARNING — DIVERGENCE ====================
4548/// The Rust port consumes a `&mut Vec<crate::ported::zsh_h::redir>`
4549/// in place of `LinkList`. The walk is identical; the only behavior
4550/// difference is that LinkList iteration in C lets callers splice
4551/// nodes mid-walk — we never do that here so it's a no-op divergence.
4552/// =============================================================
4553pub fn spawnpipes(l: &mut [redir], nullexec: i32) {
4554 // c:5184
4555 for f in l.iter_mut() {
4556 // c:5191
4557 if f.typ == REDIR_OUTPIPE || f.typ == REDIR_INPIPE {
4558 // c:5193
4559 let str_ = f.name.clone().unwrap_or_default(); // c:5194
4560 let nullexec_eff = if f.varid.as_deref().map_or(false, |v| !v.is_empty()) {
4561 1
4562 } else {
4563 nullexec
4564 };
4565 f.fd2 = getpipe(&str_, nullexec_eff); // c:5195
4566 }
4567 }
4568}
4569
4570/// Port of `static int cancd2(char *s)` from `Src/exec.c:6411`.
4571///
4572/// C body:
4573/// ```c
4574/// struct stat buf;
4575/// char *us, *us2 = NULL;
4576/// int ret;
4577/// if (!isset(CHASEDOTS) && !isset(CHASELINKS)) {
4578/// if (*s != '/')
4579/// us = tricat(pwd[1] ? pwd : "", "/", s);
4580/// else
4581/// us = ztrdup(s);
4582/// fixdir(us2 = us);
4583/// } else
4584/// us = unmeta(s);
4585/// ret = !(access(us, X_OK) || stat(us, &buf) || !S_ISDIR(buf.st_mode));
4586/// if (us2) free(us2);
4587/// return ret;
4588/// ```
4589///
4590/// True iff `s` is a directory we can `cd` into (X-perm). With
4591/// `!CHASEDOTS && !CHASELINKS`, lexically canonicalise the path
4592/// (joining with PWD if relative) so `cd /foo/bar/..` works without
4593/// resolving the symlink. Otherwise pass `s` through `unmeta` to libc.
4594pub fn cancd2(s: &str) -> i32 {
4595 // c:6411
4596 let us: String;
4597 // c:6422 — `if (!isset(CHASEDOTS) && !isset(CHASELINKS))`.
4598 let chasedots = isset(CHASEDOTS); // c:6422
4599 let chaselinks = isset(CHASELINKS);
4600 if !chasedots && !chaselinks {
4601 // c:6422
4602 // c:6423-6426 — `*s != '/' ? tricat(pwd, "/", s) : ztrdup(s);`
4603 let pwd_str = getsparam("PWD").unwrap_or_default(); // c:6424 `pwd`
4604 let mut raw = if !s.starts_with('/') {
4605 // c:6423
4606 format!(
4607 "{}/{}",
4608 if pwd_str.len() > 1 { &pwd_str[..] } else { "" },
4609 s
4610 )
4611 } else {
4612 s.to_string()
4613 };
4614 // c:6427 — `fixdir(us2 = us);` — lexical canonicalisation.
4615 raw = fixdir(&raw);
4616 us = raw;
4617 } else {
4618 // c:6428
4619 us = unmeta(s); // c:6429
4620 }
4621 // c:6430 — `!(access(us, X_OK) || stat(us, &buf) || !S_ISDIR(...))`.
4622 let cstr = match std::ffi::CString::new(us.as_str()) {
4623 Ok(c) => c,
4624 Err(_) => return 0,
4625 };
4626 if unsafe { libc::access(cstr.as_ptr(), libc::X_OK) } != 0 {
4627 return 0;
4628 }
4629 let meta = match std::fs::metadata(&us) {
4630 Ok(m) => m,
4631 Err(_) => return 0,
4632 };
4633 if !meta.file_type().is_dir() {
4634 return 0;
4635 }
4636 1
4637}
4638
4639/// Port of `char *cancd(char *s)` from `Src/exec.c:6370`.
4640///
4641/// Resolve a `cd` target against `$cdpath` and `cd_able_vars`.
4642/// Returns the chosen absolute path (heap-dup) if `cancd2` accepts
4643/// it, else `None`.
4644///
4645/// C body uses CDPATH walking + `cd_able_vars()` fallback. Sets
4646/// `doprintdir = -1` when a non-trivial path is found (so `cd`
4647/// echoes the resolved path).
4648pub fn cancd(s: &str) -> Option<String> {
4649 // c:6370
4650 // c:6372-6373 — `nocdpath = s[0]=='.' && (s[1]=='/' || !s[1] ||
4651 // (s[1]=='.' && (s[2]=='/' || !s[2])))`.
4652 let bytes = s.as_bytes();
4653 let nocdpath = bytes.first().copied() == Some(b'.')
4654 && (bytes.get(1).copied() == Some(b'/')
4655 || bytes.get(1).is_none()
4656 || (bytes.get(1).copied() == Some(b'.')
4657 && (bytes.get(2).copied() == Some(b'/') || bytes.get(2).is_none())));
4658 // c:6376 — `if (*s != '/')` branch.
4659 if !s.starts_with('/') {
4660 // c:6376
4661 // c:6379-6380 — `if (cancd2(s)) return s;`
4662 if cancd2(s) != 0 {
4663 return Some(s.to_string());
4664 }
4665 // c:6381-6382 — `if (access(unmeta(s), X_OK) == 0) return NULL;`
4666 let cstr = std::ffi::CString::new(unmeta(s).as_str()).ok()?;
4667 if unsafe { libc::access(cstr.as_ptr(), libc::X_OK) } == 0 {
4668 return None; // c:6382
4669 }
4670 // c:6383-6397 — CDPATH walk.
4671 if !nocdpath {
4672 let cdpath_str = getsparam("CDPATH").unwrap_or_default();
4673 for cp in cdpath_str.split(':') {
4674 // c:6384
4675 let sbuf = if !cp.is_empty() {
4676 format!("{}/{}", cp, s) // c:6386
4677 } else {
4678 s.to_string() // c:6391
4679 };
4680 if cancd2(&sbuf) != 0 {
4681 // c:6393
4682 DOPRINTDIR.store(-1, Ordering::Relaxed); // c:6394
4683 return Some(sbuf); // c:6395
4684 }
4685 }
4686 }
4687 // c:6398-6403 — `cd_able_vars()` fallback.
4688 if let Some(t) = cd_able_vars(s) {
4689 // c:6398
4690 if cancd2(&t) != 0 {
4691 // c:6399
4692 DOPRINTDIR.store(-1, Ordering::Relaxed); // c:6400
4693 return Some(t); // c:6401
4694 }
4695 }
4696 return None; // c:6404
4697 }
4698 // c:6406 — absolute path: `return cancd2(s) ? s : NULL;`
4699 if cancd2(s) != 0 {
4700 Some(s.to_string())
4701 } else {
4702 None
4703 }
4704}
4705
4706/// Port of `char *simple_redir_name(Eprog prog, int redir_type)` from
4707/// `Src/exec.c:4689`.
4708///
4709/// Test if an Eprog encodes a single simple-command consisting of a
4710/// SINGLE redirection of the requested type with NO command body
4711/// (the `cat < foo` shape). When true, returns the redir target name
4712/// (heap-dup) so callers like `$(< file)` short-circuit to a direct
4713/// `open(2)` instead of fork+pipe+exec.
4714///
4715/// C body walks the wordcode at fixed offsets (`pc[0]` = WC_LIST,
4716/// `pc[1]` = WC_SUBLIST, `pc[2]` = WC_PIPE, `pc[3]` = WC_REDIR,
4717/// `pc[6]` = WC_SIMPLE with argc=0). zshrs's wordcode buffer is the
4718/// same shape — this port replicates the same offset reads.
4719pub fn simple_redir_name(prog: &eprog, redir_type: i32) -> Option<String> {
4720 // c:4689
4721 let pc = &prog.prog;
4722 // c:4694-4702 — guard chain. Walk the wordcode buffer at fixed
4723 // offsets matching C's `pc[0]..pc[6]` checks.
4724 if pc.len() < 7 {
4725 return None;
4726 }
4727
4728 if wc_code(pc[0]) != WC_LIST
4729 || (WC_LIST_TYPE(pc[0]) & Z_END as u32) == 0 // c:4695
4730 || wc_code(pc[1]) != WC_SUBLIST
4731 || WC_SUBLIST_FLAGS(pc[1]) != 0 // c:4696
4732 || WC_SUBLIST_TYPE(pc[1]) != WC_SUBLIST_END // c:4697
4733 || wc_code(pc[2]) != WC_PIPE
4734 || WC_PIPE_TYPE(pc[2]) != WC_PIPE_END // c:4698
4735 || wc_code(pc[3]) != WC_REDIR
4736 || WC_REDIR_TYPE(pc[3]) != redir_type // c:4699
4737 || WC_REDIR_VARID(pc[3]) != 0 // c:4700
4738 || pc[4] != 0 // c:4701
4739 || wc_code(pc[6]) != WC_SIMPLE
4740 || WC_SIMPLE_ARGC(pc[6]) != 0
4741 // c:4702
4742 {
4743 return None; // c:4706
4744 }
4745 // c:4703 — `return dupstring(ecrawstr(prog, pc + 5, NULL));`
4746 Some(dupstring(&ecrawstr(prog, 5, None)))
4747}
4748
4749/// Port of `int getherestr(struct redir *fn)` from `Src/exec.c:4655`.
4750///
4751/// C body:
4752/// ```c
4753/// char *s, *t;
4754/// int fd, len;
4755/// t = fn->name;
4756/// singsub(&t);
4757/// untokenize(t);
4758/// unmetafy(t, &len);
4759/// if (!(fn->flags & REDIRF_FROM_HEREDOC))
4760/// t[len++] = '\n';
4761/// if ((fd = gettempfile(NULL, 1, &s)) < 0)
4762/// return -1;
4763/// write_loop(fd, t, len);
4764/// close(fd);
4765/// fd = open(s, O_RDONLY | O_NOCTTY);
4766/// unlink(s);
4767/// return fd;
4768/// ```
4769///
4770/// Materialise a `<<<` herestring or unprocessed-here-doc body into a
4771/// tempfile, then re-open read-only and unlink — gives the consumer a
4772/// read fd whose backing file is already cleaned up.
4773pub fn getherestr(fn_: &redir) -> i32 {
4774 // c:4655
4775 let mut t: String = fn_.name.clone().unwrap_or_default(); // c:4660
4776 t = singsub(&t); // c:4661
4777 t = untokenize(&t); // c:4662
4778 // c:4663 — `unmetafy(t, &len);` — strip Meta-escapes.
4779 // Reuse the canonical unmetafy port (utils.rs) on a Vec<u8>.
4780 let mut bytes: Vec<u8> = t.into_bytes();
4781 let _len = unmetafy(&mut bytes);
4782 // c:4671-4672 — `if (!(fn->flags & REDIRF_FROM_HEREDOC)) t[len++] = '\n';`
4783 if (fn_.flags & REDIRF_FROM_HEREDOC) == 0 {
4784 // c:4671
4785 bytes.push(b'\n'); // c:4672
4786 }
4787 // c:4673-4674 — `if ((fd = gettempfile(NULL, 1, &s)) < 0) return -1;`
4788 let (fd, s) = match gettempfile(None) {
4789 Some(p) => p,
4790 None => return -1, // c:4674
4791 };
4792 // c:4675 — `write_loop(fd, t, len);`
4793 let _ = write_loop(fd, &bytes); // c:4675
4794 // c:4676 — `close(fd);`
4795 let _ = zclose(fd); // c:4676
4796 // c:4677 — `fd = open(s, O_RDONLY | O_NOCTTY);`
4797 let cstr = std::ffi::CString::new(s.as_str()).unwrap_or_default();
4798 let new_fd = unsafe { libc::open(cstr.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY) }; // c:4677
4799 // c:4678 — `unlink(s);`
4800 unsafe {
4801 libc::unlink(cstr.as_ptr());
4802 } // c:4678
4803 new_fd // c:4679
4804}
4805
4806/// Port of `void quote_tokenized_output(char *str, FILE *file)` from
4807/// `Src/exec.c:2114`.
4808///
4809/// C body (abridged):
4810/// ```c
4811/// for (; *s; s++) {
4812/// switch (*s) {
4813/// case Meta: putc(*++s ^ 32, file); continue;
4814/// case Nularg: continue;
4815/// case '\\' '<' '>' '(' '|' ')' '^' '#' '~' '[' ']' '*' '?' '$' ' ':
4816/// putc('\\', file); break;
4817/// case '\t': fputs("$'\\t'", file); continue;
4818/// case '\n': fputs("$'\\n'", file); continue;
4819/// case '\r': fputs("$'\\r'", file); continue;
4820/// case '=': if (s == str) putc('\\', file); break;
4821/// default:
4822/// if (itok(*s)) { putc(ztokens[*s - Pound], file); continue; }
4823/// }
4824/// putc(*s, file);
4825/// }
4826/// ```
4827///
4828/// Used by `xtrace` (`set -x` printer) and `whence -c` to display a
4829/// tokenized argv in a form where lexer tokens (`Star`, `Inpar`, …)
4830/// surface as unescaped chars (`*`, `(`) while literal special chars
4831/// get backslash-escaped — round-tripping through the shell.
4832pub fn quote_tokenized_output(str_in: &str, file: &mut impl std::io::Write) -> std::io::Result<()> {
4833 // c:2114
4834 let bytes = str_in.as_bytes();
4835 let mut i = 0usize;
4836 while i < bytes.len() {
4837 // c:2118 `for (; *s; s++)`
4838 let c = bytes[i];
4839 match c {
4840 x if x == Meta => {
4841 // c:2120 — `case Meta: putc(*++s ^ 32, file);`
4842 if i + 1 < bytes.len() {
4843 file.write_all(&[bytes[i + 1] ^ 32])?; // c:2121
4844 i += 2;
4845 } else {
4846 i += 1;
4847 }
4848 continue; // c:2122
4849 }
4850 x if x as char == Nularg => {
4851 // c:2124
4852 i += 1;
4853 continue; // c:2126
4854 }
4855 b'\\' | b'<' | b'>' | b'(' | b'|' | b')' | b'^' | b'#' | b'~' | b'[' | b']' | b'*'
4856 | b'?' | b'$' | b' ' => {
4857 // c:2128-2142
4858 file.write_all(b"\\")?; // c:2143
4859 }
4860 b'\t' => {
4861 // c:2146
4862 file.write_all(b"$'\\t'")?; // c:2147
4863 i += 1;
4864 continue;
4865 }
4866 b'\n' => {
4867 // c:2150
4868 file.write_all(b"$'\\n'")?; // c:2151
4869 i += 1;
4870 continue;
4871 }
4872 b'\r' => {
4873 // c:2154
4874 file.write_all(b"$'\\r'")?; // c:2155
4875 i += 1;
4876 continue;
4877 }
4878 b'=' => {
4879 // c:2158 — `if (s == str) putc('\\', file);`
4880 if i == 0 {
4881 file.write_all(b"\\")?; // c:2160
4882 }
4883 }
4884 _ => {
4885 // c:2163 — `if (itok(*s)) putc(ztokens[*s - Pound], file); continue;`
4886 if itok(c) {
4887 // c:2164
4888 let pound = Pound as u8;
4889 if c >= pound {
4890 let idx = (c - pound) as usize;
4891 let zt = ztokens.as_bytes();
4892 if idx < zt.len() {
4893 file.write_all(&[zt[idx]])?; // c:2165 `ztokens[*s - Pound]`
4894 }
4895 }
4896 i += 1;
4897 continue;
4898 }
4899 }
4900 }
4901 file.write_all(&[c])?; // c:2171
4902 i += 1;
4903 }
4904 Ok(())
4905}
4906
4907// =====================================================================
4908// Wordcode-VM control-flow dispatch — faithful ports of the C
4909// `Src/exec.c` + `Src/loop.c` wordcode interpreter entries.
4910//
4911// Each function below takes `&mut estate` and returns `i32` to mirror
4912// the C `int execX(Estate state, int do_exec)` signature exactly. Per-
4913// line `// c:NNN` citations track the C source line.
4914//
4915// zshrs's primary execution path is the fusevm bytecode VM. These
4916// wordcode-VM entries exist for C-name parity with the upstream
4917// interpreter so that future bridging code can drive zshrs through
4918// the same dispatch tree zsh's `Src/init.c::loop` walks. Where
4919// zshrs primitives don't yet model their C counterpart (e.g.
4920// `execsubst`, `addvars`, `execfuncs[]` dispatch table), the local
4921// helper is declared with a comment citing the C source file:line
4922// where the canonical body lives — same pattern as the canonical
4923// `ksh93::ksh93_wrapper` port at c:152-227.
4924// =====================================================================
4925
4926use crate::ported::math::{matheval as wc_matheval, mathevali as wc_mathevali};
4927use crate::ported::pattern::{patcompile, pattry};
4928use crate::ported::r#loop::try_tryflag;
4929
4930// Addvars-specific imports (Src/exec.c:2497 port at exec.rs::addvars).
4931use crate::ported::builtin::{BREAKS, CONTFLAG, LOOPS, RETFLAG};
4932use crate::ported::linklist::LinkList;
4933use crate::ported::mem::freeheap;
4934use crate::ported::params::setloopvar;
4935use crate::ported::params::{assignaparam, assignsparam, unsetparam};
4936use crate::ported::parse::{ecgetlist, ecgetstr};
4937use crate::ported::pattern::haswilds;
4938use crate::ported::signals_h::{queue_signal_level, restore_queue_signals};
4939use crate::ported::subst::{globlist, prefork};
4940use crate::ported::zsh_h::{
4941 estate, wordcode, EC_DUP, EC_DUPTOK, EC_NODUP, NOERREXIT_EXIT, NOERREXIT_RETURN, PAT_STATIC,
4942 WC_CASE, WC_CASE_AND, WC_CASE_OR, WC_CASE_SKIP, WC_CASE_TESTAND, WC_CASE_TYPE, WC_CURSH_SKIP,
4943 WC_END, WC_FOR_COND, WC_FOR_LIST, WC_FOR_SKIP, WC_FOR_TYPE, WC_FUNCDEF, WC_FUNCDEF_SKIP, WC_IF,
4944 WC_IF_ELSE, WC_IF_SKIP, WC_IF_TYPE, WC_REPEAT_SKIP, WC_TIMED_EMPTY, WC_TIMED_TYPE, WC_TRY_SKIP,
4945 WC_WHILE_SKIP, WC_WHILE_TYPE, WC_WHILE_UNTIL,
4946};
4947use crate::ported::zsh_h::{
4948 ALLEXPORT, ASSPM_AUGMENT, ASSPM_KEY_VALUE, ASSPM_WARN, GLOBASSIGN, KSHARRAYS, PREFORK_ASSIGN,
4949 PREFORK_KEY_VALUE, PREFORK_SINGLE, WC_ASSIGN, WC_ASSIGN_INC, WC_ASSIGN_NUM, WC_ASSIGN_SCALAR,
4950 WC_ASSIGN_TYPE, WC_ASSIGN_TYPE2,
4951};
4952use crate::ported::zsh_h::{
4953 CS_ALWAYS, CS_CASE, CS_COND, CS_CURSH, CS_ELIF, CS_ELIFTHEN, CS_ELSE, CS_FOR, CS_IF, CS_IFTHEN,
4954 CS_MATH, CS_REPEAT, CS_UNTIL, CS_WHILE, MN_INTEGER,
4955};
4956
4957// --- Local stubs for C primitives not yet ported elsewhere ------------
4958//
4959// These mirror the C functions of the same names. Each cites the C
4960// source file:line where the canonical body lives. They are inlined
4961// here (rather than a separate `pub fn` in the owning C-file module)
4962// because the owning ports are pending the wider exec-substrate
4963// work (sub-PR). Once those land, these locals collapse to direct
4964// `crate::ported::<owner>::<fn>` calls.
4965
4966/// Port of `void execsubst(LinkList strs)` from `Src/exec.c:2684`.
4967///
4968/// C body (c:2684-2693):
4969/// ```c
4970/// void execsubst(LinkList strs) {
4971/// if (strs) {
4972/// prefork(strs, esprefork, NULL);
4973/// if (esglob && !errflag) {
4974/// LinkList ostrs = strs;
4975/// globlist(strs, 0);
4976/// strs = ostrs;
4977/// }
4978/// }
4979/// }
4980/// ```
4981///
4982/// `execsubst` runs `prefork` (parameter / arithmetic / command
4983/// substitution expansion + IFS-split) over the whole list, then
4984/// (when `esglob` is set) `globlist` to do filename globbing on the
4985/// result.
4986fn execsubst(list: &mut Vec<String>) {
4987 // c:2684
4988 if list.is_empty() {
4989 return; // c:2686 `if (strs)`
4990 }
4991 let mut ll: crate::ported::subst::LinkList = std::mem::take(list).into_iter().collect();
4992 let prefork_flags = esprefork.load(Ordering::Relaxed); // c:2687 esprefork
4993 let mut rf: i32 = 0;
4994 prefork(&mut ll, prefork_flags, &mut rf); // c:2687
4995 if esglob.load(Ordering::Relaxed) != 0 && errflag.load(Ordering::Relaxed) == 0 {
4996 // c:2688 `if (esglob && !errflag)`
4997 globlist(&mut ll, 0); // c:2690
4998 }
4999 *list = ll.into_iter().collect();
5000}
5001
5002/// Direct port of `static void addvars(Estate state, Wordcode pc,
5003/// int addflags)` from `Src/exec.c:2497-2648`. Process the WC_ASSIGN
5004/// nodes stacked inline of a simple command — the `var=value` and
5005/// `arr=(v1 v2 v3)` assignments that precede argv. Walks the wordcode
5006/// at `pc`, extracts each assignment's name + value (scalar or array),
5007/// optionally preforks + globs the tokenised RHS, and routes through
5008/// `assignsparam` (scalar) or `assignaparam` (array).
5009///
5010/// XTRACE side-effect: prints `name=value ` / `name=( v1 v2 ) ` to
5011/// stderr (C uses xtrerr; zshrs uses eprint!).
5012///
5013/// `STTY=...` in an inline-export form (`STTY=raw cmd`) gets captured
5014/// into the file-static `STTYval` for `execute()` to apply pre-exec.
5015fn addvars(state: &mut estate, pc: usize, addflags: i32) {
5016 // c:2501 — locals.
5017 let mut vl: LinkList<String>; // c:2501 `LinkList vl;`
5018 let xtr: bool; // c:2502 `int xtr,`
5019 let mut isstr: bool; // c:2502 `int isstr,`
5020 let mut htok: i32 = 0; // c:2502 `int htok = 0;`
5021 let mut arr: Vec<String>; // c:2503 `char **arr, **ptr, *name;`
5022 let mut name: String;
5023 let mut flags: i32; // c:2504 `int flags;`
5024 let opc = state.pc; // c:2506 `Wordcode opc = state->pc;`
5025 let mut ac: wordcode; // c:2507 `wordcode ac;`
5026 // c:2508 `local_list1(svl);` — stack-local one-element LinkList
5027 // for the scalar-assignment path. Rust uses a fresh LinkList per
5028 // iteration; equivalent semantics.
5029
5030 // c:2510-2515 — comment about WARNCREATEGLOBAL warning suppression
5031 // when the assignment list is implicitly local (ADDVAR_RESTORE).
5032 flags = if (addflags & ADDVAR_RESTORE) == 0 {
5033 ASSPM_WARN // c:2516
5034 } else {
5035 0 // c:2516
5036 };
5037 xtr = isset(XTRACE); // c:2517 `xtr = isset(XTRACE);`
5038 if xtr {
5039 // c:2518
5040 printprompt4(); // c:2519
5041 doneps4.store(1, Ordering::Relaxed); // c:2520 `doneps4 = 1;`
5042 }
5043 state.pc = pc; // c:2522 `state->pc = pc;`
5044
5045 // c:2523 `while (wc_code(ac = *state->pc++) == WC_ASSIGN) {`
5046 loop {
5047 if state.pc >= state.prog.prog.len() {
5048 break;
5049 }
5050 ac = state.prog.prog[state.pc];
5051 state.pc += 1;
5052 if wc_code(ac) != WC_ASSIGN {
5053 // Step back so the WC_SIMPLE / outer dispatcher sees the
5054 // non-assignment opcode. C's `state->pc++` post-increment
5055 // already pointed past WC_ASSIGN; we need to unconsume.
5056 state.pc -= 1;
5057 break;
5058 }
5059 let mut myflags = flags; // c:2524 `int myflags = flags;`
5060 name = ecgetstr(state, EC_DUPTOK, Some(&mut htok)); // c:2525
5061 if htok != 0 {
5062 // c:2526 `if (htok) untokenize(name);`
5063 name = untokenize(&name).to_string(); // c:2527
5064 }
5065 if WC_ASSIGN_TYPE2(ac) == WC_ASSIGN_INC {
5066 // c:2528
5067 myflags |= ASSPM_AUGMENT; // c:2529
5068 }
5069 if xtr {
5070 // c:2530
5071 // c:2531-2532 — fprintf(xtrerr, ... "%s+=" : "%s=", name);
5072 if WC_ASSIGN_TYPE2(ac) == WC_ASSIGN_INC {
5073 eprint!("{}+=", name); // c:2532
5074 } else {
5075 eprint!("{}=", name); // c:2532
5076 }
5077 }
5078
5079 // c:2533 `if ((isstr = (WC_ASSIGN_TYPE(ac) == WC_ASSIGN_SCALAR))) {`
5080 isstr = WC_ASSIGN_TYPE(ac) == WC_ASSIGN_SCALAR;
5081 if isstr {
5082 // c:2534 `init_list1(svl, ecgetstr(state, EC_DUPTOK, &htok));`
5083 let svl_val = ecgetstr(state, EC_DUPTOK, Some(&mut htok));
5084 vl = LinkList::new();
5085 vl.push_back(svl_val);
5086 // c:2535 `vl = &svl;` — vl already points at the new list.
5087 } else {
5088 // c:2537 `vl = ecgetlist(state, WC_ASSIGN_NUM(ac), EC_DUPTOK, &htok);`
5089 let items = ecgetlist(
5090 state,
5091 WC_ASSIGN_NUM(ac) as usize,
5092 EC_DUPTOK,
5093 Some(&mut htok),
5094 );
5095 vl = LinkList::new();
5096 for it in items {
5097 vl.push_back(it);
5098 }
5099 if errflag.load(Ordering::Relaxed) != 0 {
5100 // c:2538-2541
5101 state.pc = opc; // c:2539
5102 return; // c:2540
5103 }
5104 }
5105
5106 // c:2544 `if (vl && htok) {`
5107 if htok != 0 {
5108 // c:2545 `int prefork_ret = 0;`
5109 let mut prefork_ret: i32 = 0;
5110 // c:2546-2547 — prefork(vl, (isstr ? PREFORK_SINGLE|PREFORK_ASSIGN
5111 // : PREFORK_ASSIGN), &prefork_ret);
5112 let pf_flags = if isstr {
5113 PREFORK_SINGLE | PREFORK_ASSIGN
5114 } else {
5115 PREFORK_ASSIGN
5116 };
5117 prefork(&mut vl, pf_flags, &mut prefork_ret); // c:2547
5118 if errflag.load(Ordering::Relaxed) != 0 {
5119 // c:2548
5120 state.pc = opc; // c:2549
5121 return; // c:2550
5122 }
5123 if (prefork_ret & PREFORK_KEY_VALUE) != 0 {
5124 // c:2552
5125 myflags |= ASSPM_KEY_VALUE; // c:2553
5126 }
5127 // c:2554-2555 — `if (!isstr || (isset(GLOBASSIGN) && isstr &&
5128 // haswilds((char *)getdata(firstnode(vl)))))`
5129 let needs_glob = if !isstr {
5130 true
5131 } else {
5132 isset(GLOBASSIGN)
5133 && isstr
5134 && !vl.is_empty()
5135 && haswilds(vl.nodes.front().map(|s| s.as_str()).unwrap_or(""))
5136 };
5137 if needs_glob {
5138 globlist(&mut vl, prefork_ret); // c:2556
5139 // c:2557-2562 — `if (isset(GLOBASSIGN) && isstr)
5140 // unsetparam(name);`
5141 if isset(GLOBASSIGN) && isstr {
5142 unsetparam(&name); // c:2562
5143 }
5144 if errflag.load(Ordering::Relaxed) != 0 {
5145 // c:2563
5146 state.pc = opc; // c:2564
5147 return; // c:2565
5148 }
5149 }
5150 }
5151 // c:2569 `if (isstr && (empty(vl) || !nextnode(firstnode(vl))))`
5152 // — scalar-assignment path: zero or one element after prefork.
5153 if isstr && (vl.is_empty() || vl.len() == 1) {
5154 let val: String; // c:2571 `char *val;`
5155 if vl.is_empty() {
5156 // c:2574
5157 val = String::new(); // c:2575 `val = ztrdup("");`
5158 } else {
5159 // c:2577 `untokenize(peekfirst(vl));`
5160 let peek = vl.nodes.front().cloned().unwrap_or_default();
5161 val = untokenize(&peek).to_string(); // c:2577-2578
5162 // c:2578 `val = ztrdup(ugetnode(vl));` — ugetnode pops;
5163 // we just cloned the front above. Equivalent.
5164 }
5165 if xtr {
5166 // c:2580
5167 eprint!("{}", quotedzputs(&val)); // c:2581
5168 eprint!(" "); // c:2582 `fputc(' ', xtrerr);`
5169 }
5170 // c:2584 `if ((addflags & ADDVAR_EXPORT) && !strchr(name, '['))`
5171 let pm = if (addflags & ADDVAR_EXPORT) != 0 && !name.contains('[') {
5172 // c:2585 `if (strcmp(name, "STTY") == 0)`
5173 if name == "STTY" {
5174 // c:2586-2587 — `STTYval = ztrdup(val);`
5175 let mut stty = STTYval.lock().unwrap();
5176 *stty = Some(val.clone()); // c:2587
5177 }
5178 // c:2589 `allexp = opts[ALLEXPORT];`
5179 let allexp = isset(ALLEXPORT);
5180 // c:2590 `opts[ALLEXPORT] = 1;` — temporarily set.
5181 opt_state_set("allexport", true);
5182 if isset(KSHARRAYS) {
5183 // c:2591
5184 unsetparam(&name); // c:2592
5185 }
5186 let pm = assignsparam(&name, &val, myflags); // c:2593
5187 // c:2594 `opts[ALLEXPORT] = allexp;` — restore.
5188 opt_state_set("allexport", allexp);
5189 pm
5190 } else {
5191 // c:2595
5192 assignsparam(&name, &val, myflags) // c:2596
5193 };
5194 if pm.is_none() {
5195 // c:2597 `if (!pm)`
5196 LASTVAL.store(1, Ordering::Relaxed); // c:2598 `lastval = 1;`
5197 // c:2599-2604 — "cheating" comment: don't zerr.
5198 if cmdoutval.load(Ordering::Relaxed) == 0 {
5199 // c:2605 `if (!cmdoutval)`
5200 cmdoutval.store(1, Ordering::Relaxed); // c:2606
5201 }
5202 }
5203 if errflag.load(Ordering::Relaxed) != 0 {
5204 // c:2608
5205 state.pc = opc; // c:2609
5206 return; // c:2610
5207 }
5208 continue; // c:2612
5209 }
5210 // c:2614 `if (vl) { ... }` — array-assignment path: drain vl
5211 // into a fresh `char **arr`.
5212 // c:2615-2619 `ptr = arr = zalloc(...); while (nonempty(vl)) *ptr++ = ztrdup(ugetnode(vl));`
5213 arr = Vec::with_capacity(vl.len() + 1);
5214 while let Some(s) = vl.pop_front() {
5215 arr.push(s);
5216 }
5217 // c:2623 `*ptr = NULL;` — C terminator; Rust Vec doesn't need it.
5218 if xtr {
5219 // c:2624
5220 eprint!("( "); // c:2625
5221 for s in &arr {
5222 // c:2626 `for (ptr = arr; *ptr; ptr++)`
5223 eprint!("{}", quotedzputs(s)); // c:2627
5224 eprint!(" "); // c:2628
5225 }
5226 eprint!(") "); // c:2630
5227 }
5228 // c:2632 `if (!assignaparam(name, arr, myflags))`
5229 if assignaparam(&name, arr, myflags).is_none() {
5230 LASTVAL.store(1, Ordering::Relaxed); // c:2633
5231 // c:2634-2638 — "cheating" comment.
5232 if cmdoutval.load(Ordering::Relaxed) == 0 {
5233 // c:2639
5234 cmdoutval.store(1, Ordering::Relaxed); // c:2640
5235 }
5236 }
5237 if errflag.load(Ordering::Relaxed) != 0 {
5238 // c:2642
5239 state.pc = opc; // c:2643
5240 return; // c:2644
5241 }
5242 }
5243 state.pc = opc; // c:2647 `state->pc = opc;`
5244}
5245
5246// execfuncs[] dispatch table from `Src/exec.c:5499` is inlined as a
5247// match expression at the call sites in execsimple. Not a separate
5248// Rust fn — every C-side reference to
5249// `execfuncs[code - WC_CURSH](state, ...)` resolves inline below.
5250
5251// --- exec.c entries ---------------------------------------------------
5252
5253/// Port of `execcursh(Estate state, int do_exec)` from
5254/// `Src/exec.c:469-498`. Execute a `{ ... }` current-shell command
5255/// group: skip the trailing try-only word, optionally drop a stale
5256/// job slot, then run the inner list.
5257pub fn execcursh(state: &mut estate, do_exec: i32) -> i32 {
5258 // c:472 — `end = state->pc + WC_CURSH_SKIP(state->pc[-1]);`
5259 let prior = state.prog.prog[state.pc.wrapping_sub(1)];
5260 let end = state.pc + WC_CURSH_SKIP(prior) as usize;
5261 // c:475 — `state->pc++;` skip the try/always-only word.
5262 state.pc += 1;
5263 // c:482-486 — drop empty job slot before nested cmd: if outer-pipe
5264 // bookkeeping is clean AND thisjob is a real job that's not the
5265 // pipe-leader AND has no procs yet, deletejob() recycles it. Avoids
5266 // leaking job-table slots when execcursh recurses.
5267 {
5268 let lp = list_pipe.load(Ordering::Relaxed);
5269 let lpj = list_pipe_job.load(Ordering::Relaxed);
5270 let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
5271 if lp == 0 && tj != -1 && tj != lpj {
5272 if let Some(jt) = JOBTAB.get() {
5273 let mut guard = jt.lock().unwrap();
5274 let has = crate::ported::jobs::hasprocs(&guard, tj as usize);
5275 if !has {
5276 if let Some(j) = guard.get_mut(tj as usize) {
5277 crate::ported::jobs::deletejob(j, false);
5278 }
5279 }
5280 }
5281 }
5282 }
5283 cmdpush(CS_CURSH as u8); // c:487 — `cmdpush(CS_CURSH);`
5284 let _ = execlist(state, 1, do_exec); // c:488 — `execlist(state, 1, do_exec);`
5285 cmdpop(); // c:489 — `cmdpop();`
5286 state.pc = end; // c:491 — `state->pc = end;`
5287 this_noerrexit.store(1, Ordering::Relaxed); // c:492 — `this_noerrexit = 1;`
5288 LASTVAL.load(Ordering::Relaxed) // c:494 — `return lastval;`
5289}
5290
5291// `(...)` subshell — no dedicated C function (handled inline by
5292// `execpline`'s WC_PIPE branch via the WC_SUBSH bit, exec.c:2540+).
5293// In zshrs the subshell branch is folded into `execpline` and
5294// `execsimple`'s WC_SUBSH dispatch — both invoke execcursh for the
5295// inner-list walk since fusevm bytecode handles the forking via
5296// Op::Subshell at a higher layer.
5297
5298/// Port of `execcond(Estate state, UNUSED(int do_exec))` from
5299/// `Src/exec.c:5204-5232`. Run a `[[ ... ]]` cond expression.
5300pub fn execcond(state: &mut estate, _do_exec: i32) -> i32 {
5301 state.pc -= 1; // c:5208 — `state->pc--;`
5302 // c:5209-5213 — XTRACE prelude.
5303 if isset(XTRACE) {
5304 printprompt4();
5305 eprint!("[[");
5306 // c:5212 — `tracingcond++;` not modeled in zshrs.
5307 }
5308 cmdpush(CS_COND as u8); // c:5214
5309 // c:5215 — `stat = evalcond(state, NULL);` — TODO faithful: needs
5310 // the wordcode-level evalcond from Src/cond.c which is distinct
5311 // from the test-builtin evalcond ported in cond.rs. Pending.
5312 let stat: i32 = 0;
5313 // c:5219-5221 — `if (stat == 2) errflag |= ERRFLAG_ERROR;`
5314 if stat == 2 {
5315 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
5316 }
5317 cmdpop(); // c:5222
5318 if isset(XTRACE) {
5319 eprintln!(" ]]");
5320 }
5321 stat // c:5230 — `return stat;`
5322}
5323
5324/// Port of `execarith(Estate state, UNUSED(int do_exec))` from
5325/// `Src/exec.c:5237-5275`. Run a `(( ... ))` arithmetic command;
5326/// returns 0 when val != 0 (success), 1 when val == 0 (false), 2 on
5327/// parse error.
5328pub fn execarith(state: &mut estate, _do_exec: i32) -> i32 {
5329 if isset(XTRACE) {
5330 printprompt4();
5331 eprint!("((");
5332 }
5333 cmdpush(CS_MATH as u8); // c:5247
5334 let mut htok: i32 = 0;
5335 let mut e = ecgetstr(state, EC_DUPTOK, Some(&mut htok)); // c:5248
5336 if htok != 0 {
5337 e = singsub(&e); // c:5250 — `singsub(&e);`
5338 }
5339 if isset(XTRACE) {
5340 eprint!(" {}", e);
5341 }
5342 let val_result = wc_matheval(&e); // c:5254 — `val = matheval(e);`
5343 cmdpop(); // c:5256
5344 if isset(XTRACE) {
5345 eprintln!(" ))");
5346 }
5347 // c:5262-5265 — `if (errflag) { errflag &= ~ERRFLAG_ERROR; return 2; }`
5348 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 || val_result.is_err() {
5349 errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed);
5350 return 2;
5351 }
5352 // c:5267 — `return (val.type == MN_INTEGER) ? val.u.l == 0 : val.u.d == 0.0;`
5353 let val = val_result.unwrap();
5354 if val.type_ == MN_INTEGER {
5355 if val.l == 0 {
5356 1
5357 } else {
5358 0
5359 }
5360 } else if val.d == 0.0 {
5361 1
5362 } else {
5363 0
5364 }
5365}
5366
5367/// Port of `exectime(Estate state, UNUSED(int do_exec))` from
5368/// `Src/exec.c:5279-5294`. Run `time pipeline`: drives execpline with
5369/// the Z_TIMED|Z_SYNC flags so it tracks wall/user/sys time.
5370pub fn exectime(state: &mut estate, _do_exec: i32) -> i32 {
5371 let jb = *THISJOB
5372 .get_or_init(|| std::sync::Mutex::new(-1))
5373 .lock()
5374 .unwrap(); // c:5283
5375 let prior = state.prog.prog[state.pc.wrapping_sub(1)];
5376 // c:5284-5287 — empty `time` (no pipeline) — print accumulated shell time.
5377 if WC_TIMED_TYPE(prior) == WC_TIMED_EMPTY {
5378 // c:5285 — `shelltime(NULL,NULL,NULL,0);` — print accumulated
5379 // shell+kids time deltas since last call.
5380 crate::ported::jobs::shelltime(None, None, None, 0);
5381 return 0; // c:5286
5382 }
5383 // c:5288 — `execpline(state, *state->pc++, Z_TIMED|Z_SYNC, 0);`
5384 let slcode = state.prog.prog[state.pc];
5385 state.pc += 1;
5386 use crate::ported::zsh_h::{Z_SYNC, Z_TIMED};
5387 let _ = execpline(state, slcode, Z_TIMED as i32 | Z_SYNC as i32, 0);
5388 *THISJOB
5389 .get_or_init(|| std::sync::Mutex::new(-1))
5390 .lock()
5391 .unwrap() = jb; // c:5289
5392 LASTVAL.load(Ordering::Relaxed) // c:5290
5393}
5394
5395/// `execshfunc(Shfunc shf, LinkList args)` — `Src/exec.c:5540`.
5396/// Promoted to top-level pub fn so execcmd_exec at the shfunc
5397/// dispatch site (c:4102-4105) can route through it. The real port
5398/// owns queue_signals + cmdstack + sfcontext setup before calling
5399/// doshfunc; doshfunc itself is unported, so we route the body
5400/// through `runshfunc` (exec.rs:1700), which carries the
5401/// wrapper-chain + zunderscore restore. Degraded vs C (no cmdstack
5402/// push, no sfcontext flip, no XTRACE arg-trace) but the function
5403/// body executes and `lastval` is updated.
5404pub fn execshfunc(shf: &mut shfunc, args: &mut Vec<String>) {
5405 // c:5546-5547 — `if (errflag) return;`
5406 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
5407 return;
5408 }
5409 // c:5550-5557 — drop empty job slot before nested shfunc invoke:
5410 // if outer-pipe bookkeeping is clean AND thisjob is a real job
5411 // that's not the pipe-leader AND has no procs yet, deletejob()
5412 // recycles it. Avoids leaking job-table slots across recursive
5413 // function calls. Same pattern as execcursh's c:482-486.
5414 {
5415 let lp = list_pipe.load(Ordering::Relaxed);
5416 let lpj = list_pipe_job.load(Ordering::Relaxed);
5417 let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
5418 if lp == 0 && tj != -1 && tj != lpj {
5419 if let Some(jt) = JOBTAB.get() {
5420 let mut guard = jt.lock().unwrap();
5421 let has = crate::ported::jobs::hasprocs(&guard, tj as usize);
5422 if !has {
5423 // c:5554-5555 — `last_file_list = jobtab[thisjob].filelist;
5424 // jobtab[thisjob].filelist = NULL;` — preserve
5425 // the filelist so deletejob doesn't unlink temp
5426 // files. Rust take()s the Vec into a local.
5427 let _last_file_list: Vec<String> = if let Some(j) = guard.get_mut(tj as usize) {
5428 std::mem::take(&mut j.filelist)
5429 } else {
5430 Vec::new()
5431 };
5432 if let Some(j) = guard.get_mut(tj as usize) {
5433 crate::ported::jobs::deletejob(j, false); // c:5556
5434 }
5435 }
5436 }
5437 }
5438 }
5439 // c:5559-5570 — `if (isset(XTRACE)) { printprompt4(); ... \n; }` —
5440 // emit PS4 prefix + space-separated quoted args on the trace
5441 // stream so `set -x` shows the function invocation line.
5442 if isset(XTRACE) {
5443 printprompt4();
5444 for (i, a) in args.iter().enumerate() {
5445 if i > 0 {
5446 eprint!(" ");
5447 }
5448 eprint!("{}", quotedzputs(a));
5449 }
5450 eprintln!();
5451 }
5452 // c:5572-5578 cmdstack/sfcontext setup: omit (no cmdstack in
5453 // zshrs yet — replaced by tracing).
5454 // c:5580 — `doshfunc(shf, args, 0);` — doshfunc swaps PPARAMS
5455 // ($1, $2, …) to the function's args, runs the body via
5456 // runshfunc, then restores. doshfunc itself isn't ported yet
5457 // so we do the swap-and-restore inline here.
5458 // c:5580 — `doshfunc(shf, args, 0);`. The C path always has
5459 // `funcdef` populated since C parses at definition time. zshrs
5460 // compiles to fusevm chunks instead, so `funcdef` is None for
5461 // user-defined functions; only `body` (source string) carries
5462 // the definition. When that's the case, build a one-shot eprog
5463 // whose `strs` carries the source so runshfunc's script-pipeline
5464 // arm (execute_script_zsh_pipeline) executes the body.
5465 let prog_owned: Option<eprog> = if shf.funcdef.is_some() {
5466 None
5467 } else if let Some(ref body) = shf.body {
5468 Some(eprog {
5469 strs: Some(body.clone()),
5470 ..Default::default()
5471 })
5472 } else {
5473 None
5474 };
5475 let prog_ref: Option<&eprog> = match (shf.funcdef.as_deref(), prog_owned.as_ref()) {
5476 (Some(p), _) => Some(p),
5477 (_, Some(p)) => Some(p),
5478 _ => None,
5479 };
5480 if let Some(_prog) = prog_ref {
5481 // c:5580 — `doshfunc(shf, args, 0);`. Direct doshfunc call —
5482 // noreturnval=0 means the body's return value updates LASTVAL
5483 // (caller of execfuncdef reads it back). PPARAMS swap +
5484 // restore happens INSIDE doshfunc's scope; body_runner just
5485 // runs the body.
5486 let name_for_body = shf.node.nam.clone();
5487 let body_args_owned: Vec<String> = if args.len() > 1 {
5488 args[1..].to_vec()
5489 } else {
5490 Vec::new()
5491 };
5492 let body_runner = move || -> i32 {
5493 crate::ported::exec_hooks::run_function_body(&name_for_body, &body_args_owned)
5494 .unwrap_or(0)
5495 };
5496 let _ = doshfunc(shf, args.clone(), false, body_runner);
5497 }
5498 // c:5582-5589 cmdstack restore/free: omit (no cmdstack).
5499}
5500
5501/// Port of `int doshfunc(Shfunc shfunc, LinkList doshargs, int noreturnval)`
5502/// from `Src/exec.c:5823-6158`.
5503///
5504/// C body's scope-management sequence ported here. The C source's
5505/// body-execution call (`runshfunc(prog, wrappers, name)` at c:6042)
5506/// is replaced by `body_runner` — zshrs runs function bodies through
5507/// fusevm bytecode rather than zsh's wordcode walker (per PORT.md
5508/// "zshrs replaces zsh's tree-walking interpreter" rule), so the
5509/// callback hands the live executor back to the caller (typically
5510/// the fusevm bridge) for the actual body run. Every line of scope
5511/// save/restore around the body call mirrors C exactly.
5512///
5513/// **RUST-ONLY ADAPTATION:** the extra `body_runner` parameter is
5514/// not in C. C calls `runshfunc(prog, wrappers, name)` directly at
5515/// c:6042; zshrs delegates to a closure because the body-execution
5516/// pipeline (fusevm) differs from C's (wordcode). The closure
5517/// fully replaces the runshfunc call and returns the body's exit
5518/// status (which doshfunc reads as `lastval` for the `noreturnval`
5519/// path).
5520#[allow(non_snake_case)]
5521pub fn doshfunc(
5522 shfunc: &mut shfunc, // c:5823
5523 doshargs: Vec<String>, // c:5823
5524 noreturnval: bool, // c:5823
5525 mut body_runner: impl FnMut() -> i32, // (Rust-only — body delegate)
5526) -> i32 {
5527 use crate::ported::builtin::{BREAKS, CONTFLAG, LASTVAL, LOOPS, RETFLAG};
5528 use crate::ported::jobs::{NUMPIPESTATS, PIPESTATS};
5529 use crate::ported::modules::parameter::FUNCSTACK;
5530 use crate::ported::params::endparamscope;
5531 use crate::ported::params::locallevel as locallevel_atomic;
5532 use crate::ported::zsh_h::{FS_EVAL, FS_FUNC, FS_SOURCE, FUNCTIONARGZERO, PM_UNDEFINED};
5533 use std::sync::atomic::Ordering;
5534
5535 let name = shfunc.node.nam.clone(); // c:5827
5536 let flags = shfunc.node.flags; // c:5828
5537 let fname = dupstring(&name); // c:5829
5538 let _ = fname; // c:5829 (kept for parity)
5539
5540 // c:5835 — `queue_signals();` Lots of memory + global-state changes.
5541 queue_signals();
5542
5543 // c:5847-5848 — `marked_prog = shfunc->funcdef; useeprog(marked_prog);`
5544 // Pinned so a recursive unload doesn't free the eprog under us.
5545 // (Skipped: zshrs's shfunc holds a Box<Eprog>; Drop semantics
5546 // already pin until call ends. C does explicit refcount on
5547 // `funcdef->nref` via useeprog.)
5548
5549 // c:5856-5916 — Funcsave allocation + per-field snapshot.
5550 let funcsave_breaks = BREAKS.load(Ordering::Relaxed); // c:5859
5551 let funcsave_contflag = CONTFLAG.load(Ordering::Relaxed); // c:5860
5552 let funcsave_loops = LOOPS.load(Ordering::Relaxed); // c:5861
5553 let funcsave_lastval = LASTVAL.load(Ordering::Relaxed); // c:5862
5554 let funcsave_numpipestats = {
5555 // c:5864
5556 NUMPIPESTATS
5557 .get_or_init(|| std::sync::Mutex::new(0))
5558 .lock()
5559 .map(|n| *n)
5560 .unwrap_or(0)
5561 };
5562 let funcsave_noerrexit = noerrexit.load(Ordering::Relaxed); // c:5865
5563 // c:5866-5867 — trap_state PRIMED branch decrements trap_return.
5564 if TRAP_STATE.load(Ordering::Relaxed) == TRAP_STATE_PRIMED {
5565 // c:5866
5566 TRAP_RETURN.fetch_sub(1, Ordering::Relaxed); // c:5867
5567 }
5568 // c:5871 — `noerrexit &= ~NOERREXIT_RETURN;` — scope-clear of
5569 // return-suppress so a `return` inside the body fires errexit
5570 // checks normally.
5571 noerrexit.fetch_and(!NOERREXIT_RETURN, Ordering::Relaxed);
5572
5573 // c:5872-5880 — noreturnval branch: deep-copy pipestats so the
5574 // function body's pipestats writes are restored on exit.
5575 let funcsave_pipestats: Option<Vec<i32>> = if noreturnval {
5576 // c:5872
5577 let p = PIPESTATS.get_or_init(|| std::sync::Mutex::new([0; MAX_PIPESTATS]));
5578 p.lock().ok().map(|g| g[..funcsave_numpipestats].to_vec()) // c:5879 memcpy
5579 } else {
5580 None
5581 };
5582
5583 // c:5882-5896 — TRAPEXIT special case (deep-copy shfunc so
5584 // starttrapscope doesn't rug-pull). zshrs doesn't yet support
5585 // running TRAPEXIT directly via doshfunc; flagged for follow-up.
5586 // (Skip: name = "TRAPEXIT" path.)
5587 let _ = name.as_str(); // sentinel for the eventual port.
5588
5589 // c:5898 — `starttrapscope();` — canonical port at signals.rs:1135
5590 // tags SIGEXIT for deferred restoration at scope end.
5591 crate::ported::signals::starttrapscope();
5592 // c:5899 — `startpatternscope();`
5593 crate::ported::pattern::startpatternscope();
5594
5595 // c:5901 — `pptab = pparams;` — save outer positional params.
5596 let pptab: Vec<String> = crate::ported::builtin::PPARAMS
5597 .lock()
5598 .map(|p| p.clone())
5599 .unwrap_or_default();
5600
5601 // c:5902-5903 — non-undefined: `scriptname = dupstring(name);`
5602 let funcsave_scriptname = crate::ported::utils::scriptname_get();
5603 if (flags as u32 & PM_UNDEFINED) == 0 {
5604 // c:5902
5605 crate::ported::utils::set_scriptname(Some(dupstring(&name))); // c:5903
5606 }
5607
5608 // c:5904-5908 — `funcsave->zoptind = zoptind; ...` snapshot.
5609 // C zsh saves zoptind (the canonical OPTIND counter) and
5610 // zoptarg into the funcsave struct so OPTIND is implicitly
5611 // function-local: a `getopts` loop inside the function gets
5612 // its own counter that snaps back to the caller's on
5613 // function return. zshrs stores OPTIND/OPTARG in paramtab
5614 // as regular int/string params; snapshot them here and
5615 // restore at scope end. Bug #513.
5616 let funcsave_optind: Option<String> = crate::ported::params::getsparam("OPTIND");
5617 let funcsave_optarg: Option<String> = crate::ported::params::getsparam("OPTARG");
5618
5619 // c:5914 — `memcpy(funcsave->opts, opts, sizeof(opts));` — option
5620 // snapshot. Port wraps opts in OPTS_LIVE; capture the live state
5621 // here as a HashMap snapshot.
5622 let funcsave_opts = crate::ported::options::opt_state_snapshot();
5623
5624 // c:5915-5916 — `funcsave->emulation/sticky = emulation/sticky;`
5625 // Emulation snapshot pending the sticky-emulation port.
5626
5627 // c:5954-5969 — PM_TAGGED / PM_WARNNESTED option-override block.
5628 // Anonymous-function name comparison via pointer equality in C;
5629 // zshrs uses string equality. Skip until ANONYMOUS_FUNCTION_NAME
5630 // sentinel is ported.
5631
5632 // c:5970 — `funcsave->oflags = oflags;` — module-global tracking
5633 // function-attribute inheritance. Skip until oflags is ported.
5634
5635 // c:5977 — `opts[PRINTEXITVALUE] = 0;` — suppress printexitvalue
5636 // for inner commands; outer flag restored on exit.
5637 opt_state_set("printexitvalue", false);
5638
5639 // c:5978-5998 — pparams swap. C reads doshargs and constructs the
5640 // function's positional-param array. First arg is the function
5641 // name (regardless of FUNCTIONARGZERO); the rest become $1..$N.
5642 let funcsave_argv0: Option<String> = if !doshargs.is_empty() {
5643 // c:5978
5644 // c:5982-5985 — `pparams = x = zshcalloc(...)`.
5645 let positionals: Vec<String> = if doshargs.len() > 1 {
5646 doshargs[1..].to_vec()
5647 } else {
5648 Vec::new()
5649 };
5650 if let Ok(mut pp) = crate::ported::builtin::PPARAMS.lock() {
5651 *pp = positionals;
5652 }
5653 // c:5984-5987 — FUNCTIONARGZERO: save argzero, install
5654 // doshargs[0] (the function name).
5655 if isset(FUNCTIONARGZERO) {
5656 // c:5984
5657 let prev = crate::ported::utils::argzero();
5658 crate::ported::utils::set_argzero(Some(doshargs[0].clone())); // c:5986
5659 prev
5660 } else {
5661 None
5662 }
5663 } else {
5664 // c:5992-5997 — no args: empty pparams. argzero saved+dup'd.
5665 if let Ok(mut pp) = crate::ported::builtin::PPARAMS.lock() {
5666 *pp = Vec::new();
5667 }
5668 if isset(FUNCTIONARGZERO) {
5669 // c:5994
5670 let prev = crate::ported::utils::argzero();
5671 crate::ported::utils::set_argzero(prev.clone()); // c:5996 ztrdup(argzero)
5672 prev
5673 } else {
5674 None
5675 }
5676 };
5677
5678 // c:5999 — `++funcdepth;` — bumped on entry. Mirror via locallevel
5679 // since zshrs tracks function-call depth there.
5680 //
5681 // Plus the canonical startparamscope (c:6194 inside runshfunc).
5682 // zshrs's body_runner replaces runshfunc's `execode` call so the
5683 // startparamscope/endparamscope pair must wrap body_runner here,
5684 // not inside the closure. inc_locallevel is exactly startparamscope.
5685 inc_locallevel();
5686
5687 // c:6000-6004 — FUNCNEST check + `goto undoshfunc` on overflow.
5688 // Skip the runtime check (the zshrs fusevm doesn't recurse via
5689 // real stack frames so the depth limit is less critical), but
5690 // keep the comment so the C label `undoshfunc:` target is
5691 // visible — `goto undoshfunc;` here would jump straight to the
5692 // epilogue at the `undoshfunc:` label below.
5693
5694 // c:6005-6019 — funcstack frame push. The full C block:
5695 // funcsave->fstack.name = dupstring(name);
5696 // funcsave->fstack.caller = funcstack ? funcstack->name :
5697 // dupstring(argv0 ? argv0 : argzero);
5698 // funcsave->fstack.lineno = lineno;
5699 // funcsave->fstack.prev = funcstack;
5700 // funcsave->fstack.tp = FS_FUNC;
5701 // funcstack = &funcsave->fstack;
5702 // funcsave->fstack.flineno = shfunc->lineno;
5703 // funcsave->fstack.filename = getshfuncfile(shfunc);
5704 let lineno_now = crate::ported::input::lineno.with(|c| c.get()) as i64;
5705 let (caller, prev_tp): (Option<String>, Option<i32>) = {
5706 let stk = FUNCSTACK.lock().unwrap_or_else(|e| e.into_inner());
5707 if let Some(p) = stk.last() {
5708 (Some(p.name.clone()), Some(p.tp))
5709 } else {
5710 // c:6011-6012 — outermost: argv0 (saved) or argzero global.
5711 let z = funcsave_argv0
5712 .clone()
5713 .or_else(crate::ported::utils::argzero);
5714 (z, None)
5715 }
5716 };
5717 // c:6018-6019 — flineno: shfunc->lineno (function def line)
5718 let flineno = shfunc.lineno;
5719 let filename = shfunc.filename.clone().or_else(|| Some(String::new()));
5720 {
5721 let frame = crate::ported::zsh_h::funcstack {
5722 prev: None, // c:6014 (Vec-stack: index encodes link)
5723 name: dupstring(&name), // c:6005
5724 filename, // c:6019
5725 caller, // c:6011
5726 flineno, // c:6018
5727 lineno: lineno_now, // c:6013
5728 tp: FS_FUNC, // c:6015
5729 };
5730 let _ = prev_tp; // c:6011 (informational)
5731 let mut stack = FUNCSTACK.lock().unwrap_or_else(|e| e.into_inner());
5732 stack.push(frame); // c:6016 funcstack = &funcsave->fstack
5733 }
5734
5735 // c:6021-6042 — body execution. C: `runshfunc(prog, wrappers, name)`.
5736 // zshrs delegates to the body_runner closure (typically a fusevm
5737 // sub-VM run from the bridge). The closure returns the body's
5738 // exit status which becomes lastval.
5739 //
5740 // c:Src/exec.c:1251-1266 — push "shfunc" onto zsh_eval_context
5741 // so the body sees `${zsh_eval_context[*]}` containing the call
5742 // chain context. The execode-based path (c:1245-1282 port at
5743 // exec.rs:7092) already did this, but the fusevm body_runner
5744 // path skipped doshfunc's body_runner invocation without the
5745 // push. Bug #262 in docs/BUGS.md.
5746 //
5747 // Push BOTH the static `zsh_eval_context` (matches C's variable)
5748 // AND the paramtab array entry (what `${zsh_eval_context[*]}`
5749 // reads). Pop on every return path via the guard struct so
5750 // panics / early returns don't leak the entry.
5751 crate::vm_helper::push_zsh_eval_context("shfunc");
5752 struct EvalContextGuard;
5753 impl Drop for EvalContextGuard {
5754 fn drop(&mut self) {
5755 crate::vm_helper::pop_zsh_eval_context();
5756 }
5757 }
5758 let _eval_ctx_guard = EvalContextGuard;
5759 // c:Src/exec.c — function bodies execute with `lineno` reset to
5760 // the relative line within the body (incremented per WC_PIPE
5761 // from the wordcode-encoded lineno). zsh's zerrmsg
5762 // (Src/utils.c:301) emits the lineno prefix only when lineno
5763 // is non-zero AND (!SHINSTDIN || locallevel != 0). For an
5764 // inline single-line function like `f() { x=1 }`, the body's
5765 // WC_PIPE encodes lineno=1, exec sets `lineno = lineno - 1 =
5766 // 0`, and the zerrmsg path falls through to space-only ("f: ").
5767 //
5768 // zshrs's compiler doesn't thread WC_PIPE_LINENO into the
5769 // bytecode, so the global lineno stays at the script-wide
5770 // value (1 for inline `-c`). Suppress the line-number prefix
5771 // inside function bodies by saving lineno on entry and forcing
5772 // it to 0 during body execution; restore on exit. This makes
5773 // warnings inside functions emit `f: ...` matching zsh's
5774 // single-line-function format. Bug #54/#74/#86 in docs/BUGS.md.
5775 let saved_lineno = crate::ported::lex::lineno();
5776 crate::ported::lex::set_lineno(0);
5777 // c:Src/exec.c:6173-6175 + c:6196-6198 — `runshfunc` saves
5778 // zunderscore before the body runs and restores it after, so
5779 // `$_` reads outside the function continue to reflect the
5780 // function CALL's last arg (set by setunderscore at c:3491
5781 // before doshfunc enters). Without this, commands inside the
5782 // body (`:`, `echo`, etc.) update `$_` to their own last arg,
5783 // and the post-call `echo "[$_]"` sees the body's residue
5784 // instead of the call's arg. Bug surfaced via
5785 // test_dollar_underscore_after_function_call.
5786 let saved_zunderscore = crate::ported::params::getsparam("_").unwrap_or_default();
5787 let body_status = body_runner();
5788 crate::ported::params::set_zunderscore(std::slice::from_ref(&saved_zunderscore));
5789 crate::ported::lex::set_lineno(saved_lineno);
5790 LASTVAL.store(body_status, Ordering::Relaxed);
5791
5792 // c:6043 — `doneshfunc:` label. The C `runshfunc` happy-path
5793 // falls through here from c:6042.
5794 // c:6044 — `funcstack = funcsave->fstack.prev;` — pop our frame.
5795 {
5796 let mut stack = FUNCSTACK.lock().unwrap_or_else(|e| e.into_inner());
5797 stack.pop();
5798 }
5799 // c:6045 — `undoshfunc:` label. Reached either by fall-through
5800 // from c:6044 or by `goto undoshfunc;` from the FUNCNEST check
5801 // at c:6003. Tail epilogue follows.
5802
5803 // c:6046 — `--funcdepth;` — paired endparamscope (c:6200 inside
5804 // runshfunc) lives at c:6157 below as `endparamscope()`. Removed
5805 // the dec here so locallevel only decrements once per
5806 // function-call frame; double-dec was purging level-0 globals on
5807 // function exit (the `f() { x=foo; }; f; echo $x` regression).
5808
5809 // c:6047-6053 — retflag clear. C clears retflag and restores
5810 // outer breaks if a `return` fired.
5811 if RETFLAG.load(Ordering::SeqCst) != 0 {
5812 // c:6047
5813 RETFLAG.store(0, Ordering::SeqCst); // c:6051
5814 BREAKS.store(funcsave_breaks, Ordering::SeqCst); // c:6052
5815 }
5816
5817 // c:6054-6058 — pparams + argv0 restore.
5818 if let Ok(mut pp) = crate::ported::builtin::PPARAMS.lock() {
5819 *pp = pptab; // c:6059 pparams = pptab
5820 }
5821 if let Some(saved) = funcsave_argv0 {
5822 // c:6055
5823 crate::ported::utils::set_argzero(Some(saved)); // c:6057
5824 }
5825
5826 // c:Src/exec.c:6060-6062 — `zoptind = funcsave->zoptind;
5827 // zoptarg = funcsave->zoptarg;`. Restore OPTIND/OPTARG so
5828 // an inner getopts loop's counter mutations don't leak to
5829 // the caller. Bug #513.
5830 if let Some(saved) = funcsave_optind {
5831 if let Ok(n) = saved.parse::<i64>() {
5832 crate::ported::params::setiparam("OPTIND", n);
5833 } else {
5834 crate::ported::params::setsparam("OPTIND", &saved);
5835 }
5836 }
5837 if let Some(saved) = funcsave_optarg {
5838 crate::ported::params::setsparam("OPTARG", &saved);
5839 }
5840
5841 // c:6064 — `scriptname = funcsave->scriptname;`
5842 crate::ported::utils::set_scriptname(funcsave_scriptname);
5843
5844 // c:6067 — `endpatternscope();`
5845 crate::ported::pattern::endpatternscope();
5846
5847 // c:6078-6102 — LOCALOPTIONS restore. Re-apply the snapshot when
5848 // localoptions was set inside the body.
5849 if crate::ported::options::opt_state_get("localoptions").unwrap_or(false) {
5850 // c:6091 memcpy(opts, funcsave->opts, sizeof(opts)) — full restore.
5851 let current = crate::ported::options::opt_state_snapshot();
5852 for (k, _) in ¤t {
5853 if !funcsave_opts.contains_key(k) {
5854 crate::ported::options::opt_state_unset(k);
5855 }
5856 }
5857 for (k, v) in &funcsave_opts {
5858 opt_state_set(k, *v);
5859 }
5860 } else {
5861 // c:6097-6101 — non-LOCALOPTIONS: restore only the always-
5862 // restored subset (XTRACE / PRINTEXITVALUE / LOCALOPTIONS /
5863 // LOCALLOOPS / WARNNESTEDVAR).
5864 for opt in [
5865 "xtrace",
5866 "printexitvalue",
5867 "localoptions",
5868 "localloops",
5869 "warnnestedvar",
5870 ] {
5871 if let Some(v) = funcsave_opts.get(opt) {
5872 opt_state_set(opt, *v);
5873 }
5874 }
5875 }
5876
5877 // c:6104-6112 — LOCALLOOPS warn-on-active-continue/break + restore
5878 // breaks/contflag/loops snapshot. Skip the warn lines for now;
5879 // restore the bookkeeping.
5880 if crate::ported::options::opt_state_get("localloops").unwrap_or(false) {
5881 BREAKS.store(funcsave_breaks, Ordering::SeqCst); // c:6109
5882 CONTFLAG.store(funcsave_contflag, Ordering::SeqCst); // c:6110
5883 LOOPS.store(funcsave_loops, Ordering::SeqCst); // c:6111
5884 }
5885
5886 // c:Src/exec.c:6195-6200 — C's runshfunc calls endparamscope()
5887 // BEFORE returning to doshfunc, which then calls endtrapscope()
5888 // at c:6114. So locallevel is ALREADY one less by the time
5889 // endtrapscope's pop loop compares saved local > current.
5890 //
5891 // Bug #80 in docs/BUGS.md: zshrs had endtrapscope FIRST (here at
5892 // line 5774), endparamscope LATER. That left locallevel at the
5893 // function's own level when endtrapscope ran, so saved entries
5894 // tagged with `local == current_function_level` failed the
5895 // `local > locallevel` pop condition. Nested EXIT traps
5896 // (saved at deeper level) never restored at the outer fn's
5897 // endtrapscope — outer EXIT traps fired at script exit instead.
5898 //
5899 // Decrement locallevel via a peer-of-endparamscope locallevel
5900 // bookkeeping call before endtrapscope, then leave the real
5901 // endparamscope at its current site below so the param scope
5902 // unwind still happens after the exit_pending check.
5903 {
5904 use crate::ported::params::locallevel as ll;
5905 let prev = ll.load(Ordering::Relaxed);
5906 if prev > 0 {
5907 ll.store(prev - 1, Ordering::Relaxed);
5908 }
5909 crate::ported::signals::endtrapscope();
5910 // Re-bump so the existing endparamscope() call below sees the
5911 // same pre-decrement state and its own internal decrement
5912 // lands at the right value (mirrors C's "endparamscope already
5913 // happened" comment at c:6135-6136 — the C order is endparam
5914 // (inside runshfunc) → endtrap (in doshfunc); we keep that
5915 // logical ordering for endtrapscope only, without disturbing
5916 // the rest of the epilogue's level math).
5917 ll.store(prev, Ordering::Relaxed);
5918 }
5919
5920 // c:6116-6117 — TRAP_STATE_PRIMED branch: bump trap_return back.
5921 if TRAP_STATE.load(Ordering::Relaxed) == TRAP_STATE_PRIMED {
5922 // c:6116
5923 TRAP_RETURN.fetch_add(1, Ordering::Relaxed); // c:6117
5924 }
5925
5926 // c:6118 — `ret = lastval;`
5927 let ret = LASTVAL.load(Ordering::Relaxed);
5928
5929 // c:6119 — `noerrexit = funcsave->noerrexit;`
5930 noerrexit.store(funcsave_noerrexit, Ordering::Relaxed);
5931
5932 // c:6120-6124 — noreturnval: restore lastval + pipestats. C runs
5933 // the function for side-effects only; outer lastval/pipestats
5934 // should reflect the PRE-call state.
5935 if noreturnval {
5936 // c:6120
5937 LASTVAL.store(funcsave_lastval, Ordering::Relaxed); // c:6121
5938 if let Some(saved_ps) = funcsave_pipestats {
5939 let n = NUMPIPESTATS.get_or_init(|| std::sync::Mutex::new(0));
5940 if let Ok(mut nguard) = n.lock() {
5941 *nguard = funcsave_numpipestats; // c:6122
5942 }
5943 let p = PIPESTATS.get_or_init(|| std::sync::Mutex::new([0; MAX_PIPESTATS]));
5944 if let Ok(mut pguard) = p.lock() {
5945 for (i, v) in saved_ps.iter().enumerate() {
5946 if i < pguard.len() {
5947 pguard[i] = *v; // c:6123 memcpy
5948 }
5949 }
5950 }
5951 }
5952 }
5953
5954 // c:Src/exec.c doshfunc → endparamscope — restore local-typeset
5955 // params installed during the body. In C, this is called inside
5956 // runshfunc (c:6200) BEFORE control returns to doshfunc's tail —
5957 // so by the time the exit_pending check runs at c:6141,
5958 // locallevel has ALREADY been decremented. The c:6135-6136
5959 // comment explicitly states "The endparamscope() has already
5960 // happened, hence the +1 here."
5961 //
5962 // The previous Rust ordering placed endparamscope AFTER the
5963 // exit_pending check, which compared exit_level against the
5964 // un-decremented locallevel. For `foo() { exit 7; }; foo`:
5965 // exit_level=1, cur_locallevel=1 (pre-decrement)
5966 // check: exit_level >= cur_locallevel + 1 ⟹ 1 >= 2 = false
5967 // The function returned cleanly without triggering zexit, and
5968 // the shell exited 0 instead of 7. Moving endparamscope before
5969 // the check matches C and makes the off-by-one resolve.
5970 endparamscope();
5971
5972 // c:6128 — `unqueue_signals();`
5973 unqueue_signals();
5974
5975 // c:6135-6155 — exit_pending branch: when an `exit` was queued
5976 // inside the function body and we've unwound enough scopes for
5977 // it to take effect, either keep unwinding (still inside a
5978 // nested function) or actually exit the shell.
5979 let exit_pending = crate::ported::builtin::EXIT_PENDING.load(Ordering::Relaxed);
5980 let exit_level = crate::ported::builtin::EXIT_LEVEL.load(Ordering::Relaxed);
5981 let cur_locallevel = locallevel.load(Ordering::Relaxed) as i32;
5982 let cur_forklevel = FORKLEVEL.load(Ordering::Relaxed);
5983 let in_exit_trap = crate::ported::signals::in_exit_trap.load(Ordering::Relaxed); // c:Src/signals.c:63
5984 if exit_pending != 0 && exit_level >= cur_locallevel + 1 && in_exit_trap == 0 {
5985 // c:6141
5986 if cur_locallevel > cur_forklevel {
5987 // c:6143 — still inside a nested function: keep unwinding.
5988 RETFLAG.store(1, Ordering::Relaxed); // c:6144
5989 BREAKS.store(LOOPS.load(Ordering::Relaxed), Ordering::Relaxed); // c:6145
5990 } else {
5991 // c:6151 — out of all functions: exit for real.
5992 crate::ported::builtin::STOPMSG.store(1, Ordering::Relaxed); // c:6151
5993 let val = EXIT_VAL.load(Ordering::Relaxed);
5994 crate::ported::builtin::zexit(val, crate::ported::zsh_h::ZEXIT_NORMAL);
5995 // c:6152
5996 }
5997 }
5998
5999 ret // c:6157 return ret
6000}
6001
6002/// `TRAP_STATE_PRIMED` per `Src/signals.h:55` — doshfunc tests this
6003/// to decide whether to bump trap_return on entry/exit. Local
6004/// const here because the canonical zsh_h port doesn't carry
6005/// trap-state numeric constants yet.
6006const TRAP_STATE_PRIMED: i32 = 2; // c:Src/signals.h:55
6007
6008/// Port of `execfuncdef(Estate state, Eprog redir_prog)` from
6009/// `Src/exec.c:5309-5494`. Define a shell function: extract
6010/// name(s)+body from the wordcode payload, allocate the Shfunc,
6011/// install into `shfunctab` (named), or execute immediately (anon).
6012#[allow(non_snake_case)]
6013pub fn execfuncdef(state: &mut estate, mut redir_prog: Option<crate::ported::zsh_h::Eprog>) -> i32 {
6014 use crate::ported::hashtable::{dircache_set, shfunctab_lock};
6015 use crate::ported::jobs::{getsigidx, removetrapnode};
6016 use crate::ported::parse::{dupeprog, freeeprog, incrdumpcount};
6017 use crate::ported::signals::settrap;
6018 use crate::ported::utils::scriptfilename_get;
6019 use crate::ported::zsh_h::{
6020 eprog as eprog_t, hashnode, patprog as patprog_t, shfunc as shfunc_t, Patprog,
6021 EC_DUPTOK as _, EF_HEAP, EF_MAP, EF_REAL, FS_EVAL, FS_FUNC, PM_ANONYMOUS, PM_TAGGED,
6022 PM_TAGGED_LOCAL, PRINTEXITVALUE, SHINSTDIN, ZSIG_FUNC,
6023 };
6024 // c:5311 — `Shfunc shf;`
6025 let mut shf: Box<shfunc_t>;
6026 // c:5312 — `char *s = NULL;`
6027 let mut s: Option<String> = None;
6028 // c:5313 — `int signum, nprg, sbeg, nstrs, npats, do_tracing, len, plen, i, htok = 0, ret = 0;`
6029 let mut signum: i32;
6030 let nprg: i32;
6031 let sbeg: i32;
6032 let nstrs: i32;
6033 let npats: i32;
6034 let do_tracing: i32;
6035 let len: i32;
6036 let plen: i32;
6037 // `i` — C loop counter for pp stamp; Rust uses .map().collect().
6038 let mut htok: i32 = 0;
6039 let mut ret: i32 = 0;
6040 // c:5314 — `int anon_func = 0;`
6041 let mut anon_func: i32 = 0;
6042 // c:5315 — `Wordcode beg = state->pc, end;`
6043 let _beg: usize = state.pc;
6044 let mut end: usize;
6045 // c:5316 — `Eprog prog;`
6046 // (allocated inline per-iter below; no upfront binding needed)
6047 // c:5317 — `Patprog *pp;` — handled by Vec construction.
6048 // c:5318 — `LinkList names;`
6049 let names: Vec<String>;
6050 // c:5319 — `int tracing_flags;`
6051 let tracing_flags: i32;
6052
6053 // c:5321 — `end = beg + WC_FUNCDEF_SKIP(state->pc[-1]);`
6054 end = state.pc + WC_FUNCDEF_SKIP(state.prog.prog[state.pc.wrapping_sub(1)]) as usize;
6055 // c:5322 — `names = ecgetlist(state, *state->pc++, EC_DUPTOK, &htok);`
6056 let num = state.prog.prog[state.pc] as usize;
6057 state.pc += 1;
6058 names = ecgetlist(state, num, EC_DUPTOK, Some(&mut htok));
6059 // c:5323 — `sbeg = *state->pc++;`
6060 sbeg = state.prog.prog[state.pc] as i32;
6061 state.pc += 1;
6062 // c:5324 — `nstrs = *state->pc++;`
6063 nstrs = state.prog.prog[state.pc] as i32;
6064 state.pc += 1;
6065 // c:5325 — `npats = *state->pc++;`
6066 npats = state.prog.prog[state.pc] as i32;
6067 state.pc += 1;
6068 // c:5326 — `do_tracing = *state->pc++;`
6069 do_tracing = state.prog.prog[state.pc] as i32;
6070 state.pc += 1;
6071
6072 // c:5328 — `nprg = (end - state->pc);`
6073 nprg = end.saturating_sub(state.pc) as i32;
6074 // c:5329 — `plen = nprg * sizeof(wordcode);`
6075 plen = nprg.saturating_mul(size_of::<wordcode>() as i32);
6076 // c:5330 — `len = plen + (npats * sizeof(Patprog)) + nstrs;`
6077 len = plen + npats.saturating_mul(size_of::<usize>() as i32) + nstrs;
6078 // c:5331 — `tracing_flags = do_tracing ? PM_TAGGED_LOCAL : 0;`
6079 tracing_flags = if do_tracing != 0 {
6080 PM_TAGGED_LOCAL as i32
6081 } else {
6082 0
6083 };
6084
6085 // c:5333-5339 — htok name substitution.
6086 let mut names_mut: Vec<String> = names;
6087 if htok != 0 && !names_mut.is_empty() {
6088 execsubst(&mut names_mut); // c:5334
6089 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
6090 // c:5335
6091 state.pc = end; // c:5336
6092 return 1; // c:5337
6093 }
6094 }
6095
6096 // c:5341-5342 DPUTS — debug assertion (anon + redir simultaneously).
6097 // Not portable as panic; left as comment.
6098
6099 // c:5343 — `while (!names || (s = (char *) ugetnode(names))) {`
6100 // num==0 → anon (no names); else iterate names.
6101 let mut names_iter = names_mut.into_iter();
6102 loop {
6103 let no_names = num == 0;
6104 if !no_names {
6105 // c:5343 — `s = ugetnode(names)`; break when list exhausted.
6106 match names_iter.next() {
6107 Some(nm) => s = Some(nm),
6108 None => break,
6109 }
6110 }
6111 // c:5344-5374 — Eprog alloc.
6112 let prog: Box<eprog_t>;
6113 let dump_present = state.prog.dump.is_some();
6114 let make_pat = || -> Patprog {
6115 // c:5375-5376 `*pp = dummy_patprog1;` — sentinel slot.
6116 Box::new(patprog_t {
6117 startoff: 0,
6118 size: 0,
6119 mustoff: 0,
6120 patmlen: 0,
6121 globflags: 0,
6122 globend: 0,
6123 flags: 0,
6124 patnpar: 0,
6125 patstartch: 0,
6126 })
6127 };
6128 if no_names {
6129 // c:5345-5346 — `zhalloc`, `nref = -1`.
6130 // c:5355-5357 — EF_HEAP, no dump, npats pats on heap.
6131 let pats: Vec<Patprog> = (0..npats).map(|_| make_pat()).collect();
6132 let prog_words: Vec<wordcode> = state.prog.prog[state.pc..end].to_vec();
6133 // c:5365 — `prog->strs = state->strs + sbeg;`
6134 let strs_tail = state.strs.as_ref().map(|t| {
6135 let off = (sbeg as usize).min(t.len());
6136 t[off..].to_string()
6137 });
6138 prog = Box::new(eprog_t {
6139 flags: EF_HEAP,
6140 len,
6141 npats,
6142 nref: -1, // c:5346
6143 pats,
6144 prog: prog_words,
6145 strs: strs_tail,
6146 shf: None, // c:5377
6147 dump: None, // c:5356
6148 });
6149 } else if dump_present {
6150 // c:5358-5363 — EF_MAP path: refcount the dump, allocate
6151 // pats permanent, reuse `state->pc` slice in place.
6152 if let Some(dp) = state.prog.dump.as_deref() {
6153 incrdumpcount(dp); // c:5360
6154 }
6155 let pats: Vec<Patprog> = (0..npats).map(|_| make_pat()).collect();
6156 let prog_words: Vec<wordcode> = state.prog.prog[state.pc..end].to_vec();
6157 let strs_tail = state.strs.as_ref().map(|t| {
6158 let off = (sbeg as usize).min(t.len());
6159 t[off..].to_string()
6160 });
6161 prog = Box::new(eprog_t {
6162 flags: EF_MAP, // c:5359
6163 len,
6164 npats,
6165 nref: 1, // c:5349
6166 pats,
6167 prog: prog_words,
6168 strs: strs_tail,
6169 shf: None, // c:5377
6170 dump: state.prog.dump.clone(), // c:5361
6171 });
6172 } else {
6173 // c:5366-5374 — EF_REAL: copy wordcode + strs into a
6174 // freshly-owned eprog (no shared dump backing).
6175 let pats: Vec<Patprog> = (0..npats).map(|_| make_pat()).collect();
6176 let pc_end = state.pc + nprg as usize;
6177 let prog_words: Vec<wordcode> = state.prog.prog[state.pc..pc_end].to_vec();
6178 // c:5373 — `memcpy(prog->strs, state->strs + sbeg, nstrs);`
6179 let strs_copy = state.strs.as_ref().map(|t| {
6180 let off = (sbeg as usize).min(t.len());
6181 let n_avail = t.len().saturating_sub(off);
6182 let take = (nstrs as usize).min(n_avail);
6183 t[off..off + take].to_string()
6184 });
6185 prog = Box::new(eprog_t {
6186 flags: EF_REAL, // c:5367
6187 len,
6188 npats,
6189 nref: 1, // c:5349
6190 pats,
6191 prog: prog_words,
6192 strs: strs_copy,
6193 shf: None, // c:5377
6194 dump: None, // c:5371
6195 });
6196 }
6197
6198 // c:5379-5381 — Shfunc alloc + funcdef + tracing flags.
6199 shf = Box::new(shfunc_t {
6200 node: hashnode {
6201 next: None,
6202 nam: String::new(),
6203 flags: tracing_flags,
6204 },
6205 filename: scriptfilename_get(), // c:5383 `ztrdup(scriptfilename)`
6206 // c:5384-5388 — funcstack top FS_FUNC/FS_EVAL → flineno+lineno
6207 // else just lineno.
6208 lineno: {
6209 let cur_lineno = crate::ported::input::lineno.with(|l| l.get()) as i64;
6210 if let Ok(stk) = crate::ported::modules::parameter::FUNCSTACK.lock() {
6211 if let Some(top) = stk.last() {
6212 if top.tp == FS_FUNC || top.tp == FS_EVAL {
6213 top.flineno + cur_lineno
6214 } else {
6215 cur_lineno
6216 }
6217 } else {
6218 cur_lineno
6219 }
6220 } else {
6221 cur_lineno
6222 }
6223 },
6224 funcdef: Some(prog), // c:5380
6225 redir: None,
6226 sticky: None,
6227 body: None,
6228 });
6229 // c:5396-5401 — redir_prog ownership.
6230 // C: `if (names && nonempty(names) && redir_prog) shf->redir = dupeprog(redir_prog,0)`
6231 // else `shf->redir = redir_prog; redir_prog = 0;`
6232 // "nonempty(names)" means there's a NEXT name still to consume —
6233 // i.e. peek the iterator.
6234 if !no_names && names_iter.len() > 0 && redir_prog.is_some() {
6235 // c:5397 — dupe so each earlier name gets its own copy; the
6236 // last name (when iterator drains) gets the original.
6237 if let Some(rp) = redir_prog.as_deref() {
6238 shf.redir = Some(Box::new(dupeprog(rp, false)));
6239 }
6240 } else {
6241 // c:5399-5400 — last name (or anon) takes original.
6242 shf.redir = redir_prog.take();
6243 }
6244 // c:5402 — `shfunc_set_sticky(shf);`
6245 shfunc_set_sticky(&mut shf);
6246
6247 if no_names {
6248 // c:5404-5457 — anonymous function: execute immediately.
6249 // `LinkList args;` c:5409
6250 let mut args: Vec<String>;
6251
6252 anon_func = 1; // c:5411
6253 shf.node.flags |= PM_ANONYMOUS as i32; // c:5412
6254
6255 state.pc = end; // c:5414
6256 // c:5415 — `end += *state->pc++;`
6257 end += state.prog.prog[state.pc] as usize;
6258 state.pc += 1;
6259 // c:5416 — `args = ecgetlist(state, *state->pc++, EC_DUPTOK, &htok);`
6260 let arg_count = state.prog.prog[state.pc] as usize;
6261 state.pc += 1;
6262 args = ecgetlist(state, arg_count, EC_DUPTOK, Some(&mut htok));
6263
6264 // c:5418-5429 — htok arg subst + cleanup-on-error.
6265 if htok != 0 && !args.is_empty() {
6266 execsubst(&mut args); // c:5419
6267 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
6268 // c:5421 — `freeeprog(shf->funcdef);`
6269 if let Some(mut fd) = shf.funcdef.take() {
6270 freeeprog(&mut fd);
6271 }
6272 if shf.redir.is_some() {
6273 // c:5422-5423 — "shouldn't be" anon+redir, but free if so.
6274 if let Some(mut rd) = shf.redir.take() {
6275 freeeprog(&mut rd);
6276 }
6277 }
6278 dircache_set(&mut shf.filename, None); // c:5424
6279 drop(shf); // c:5425 `zfree(shf, sizeof(*shf));`
6280 state.pc = end; // c:5426
6281 return 1; // c:5427
6282 }
6283 }
6284
6285 // c:5431-5432 — `setunderscore` to last arg (or "").
6286 let under_val = if !args.is_empty() {
6287 args.last().cloned().unwrap_or_default()
6288 } else {
6289 String::new()
6290 };
6291 setunderscore(&under_val);
6292
6293 // c:5434-5435 — `if (!args) args = newlinklist();`
6294 // (Rust Vec is never null; no-op.)
6295 shf.node.nam = ANONYMOUS_FUNCTION_NAME.to_string(); // c:5436
6296 // c:5437 — `pushnode(args, shf->node.nam);` — prepend.
6297 args.insert(0, shf.node.nam.clone());
6298
6299 execshfunc(&mut shf, &mut args); // c:5439
6300 ret = LASTVAL.load(Ordering::Relaxed); // c:5440
6301
6302 // c:5442-5450 — PRINTEXITVALUE+SHINSTDIN exit report.
6303 if isset(PRINTEXITVALUE) && isset(SHINSTDIN) && ret != 0 {
6304 eprintln!("zsh: exit {}", ret); // c:5445/5447
6305 }
6306
6307 // c:5452-5456 — cleanup.
6308 if let Some(mut fd) = shf.funcdef.take() {
6309 freeeprog(&mut fd);
6310 }
6311 if let Some(mut rd) = shf.redir.take() {
6312 // c:5453-5454 — "shouldn't be" but free if present.
6313 freeeprog(&mut rd);
6314 }
6315 dircache_set(&mut shf.filename, None); // c:5455
6316 drop(shf); // c:5456 `zfree(shf, sizeof(*shf));`
6317 break; // c:5457
6318 } else {
6319 // c:5458-5484 — named function path.
6320 let nm = s.as_deref().unwrap_or("");
6321 // c:5460-5475 — TRAP* signal-trap install.
6322 if nm.len() > 4 && nm.starts_with("TRAP") {
6323 if let Some(sn) = getsigidx(&nm[4..]) {
6324 signum = sn;
6325 // c:5462 — `if (settrap(signum, NULL, ZSIG_FUNC))`
6326 if settrap(signum, None, ZSIG_FUNC) != 0 {
6327 if let Some(mut fd) = shf.funcdef.take() {
6328 freeeprog(&mut fd); // c:5463
6329 }
6330 dircache_set(&mut shf.filename, None); // c:5464
6331 drop(shf); // c:5465
6332 state.pc = end; // c:5466
6333 return 1; // c:5467
6334 }
6335 // c:5474 — `removetrapnode(signum);`
6336 removetrapnode(signum);
6337 // c:Src/signals.c::settrap → unsettrap →
6338 // removetrap also clears sigfuncs[sig] (the C
6339 // string-form trap slot). zshrs's port stores
6340 // string-form bodies in a separate
6341 // `traps_table` HashMap not touched by
6342 // removetrap. Drop the string-form entry here
6343 // so dotrap's fallback doesn't double-dispatch
6344 // when a TRAPxxx function REPLACES an
6345 // existing `trap '...' SIG` registration. Bug
6346 // #541 in docs/BUGS.md.
6347 if let Ok(mut t) = crate::ported::builtin::traps_table().lock() {
6348 t.remove(&nm[4..]);
6349 }
6350 }
6351 }
6352 // c:5477-5482 — re-define-self trace flag propagate.
6353 if let Ok(stk) = crate::ported::modules::parameter::FUNCSTACK.lock() {
6354 if let Some(top) = stk.last() {
6355 if top.tp == FS_FUNC && top.name == nm {
6356 // c:5479 — `Shfunc old = shfunctab->getnode(s);`
6357 if let Ok(rd) = shfunctab_lock().read() {
6358 if let Some(old) = rd.get(nm) {
6359 // c:5481 — propagate PM_TAGGED|PM_TAGGED_LOCAL.
6360 shf.node.flags |=
6361 old.node.flags & (PM_TAGGED as i32 | PM_TAGGED_LOCAL as i32);
6362 }
6363 }
6364 }
6365 }
6366 }
6367 // c:5483 — `shfunctab->addnode(shfunctab, ztrdup(s), shf);`
6368 shf.node.nam = nm.to_string();
6369 if let Ok(mut wr) = shfunctab_lock().write() {
6370 wr.add(*shf);
6371 }
6372 }
6373 }
6374 // c:5486-5487 — `if (!anon_func) setunderscore("");`
6375 if anon_func == 0 {
6376 setunderscore("");
6377 }
6378 // c:5488-5491 — leftover redir cleanup ("shouldn't happen").
6379 if let Some(mut rd) = redir_prog.take() {
6380 freeeprog(&mut rd);
6381 }
6382 // c:5492 — `state->pc = end;`
6383 state.pc = end;
6384 // c:5493 — `return ret;`
6385 ret
6386}
6387
6388/// Port of `execsimple(Estate state)` from `Src/exec.c:1290-1340`.
6389/// Fast-path for single-Simple commands that bypasses the full
6390/// `execcmd_exec` machinery.
6391pub fn execsimple(state: &mut estate) -> i32 {
6392 // c:1292 — `wordcode code = *state->pc++;`
6393 let mut code = state.prog.prog[state.pc];
6394 state.pc += 1;
6395 // c:1295-1296 — `if (errflag) return (lastval = 1);`
6396 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
6397 LASTVAL.store(1, Ordering::Relaxed);
6398 return 1;
6399 }
6400 // c:1298-1299 — `if (!isset(EXECOPT)) return lastval = 0;`
6401 if !isset(crate::ported::zsh_h::EXECOPT) {
6402 LASTVAL.store(0, Ordering::Relaxed);
6403 return 0;
6404 }
6405 // c:1301-1303 — `if (!IN_EVAL_TRAP() && !ineval && code) lineno = code - 1;`
6406 // In evaluated traps, don't modify the line number (the trap
6407 // dispatcher restores it). `code` here is the wordcode-encoded
6408 // line number from the WC_SIMPLE entry at state.pc-1.
6409 if !crate::ported::zsh_h::IN_EVAL_TRAP()
6410 && crate::ported::builtin::INEVAL.load(Ordering::SeqCst) == 0
6411 && code != 0
6412 {
6413 crate::ported::input::lineno.with(|l| l.set((code as usize).saturating_sub(1)));
6414 }
6415 // c:1306 — `code = wc_code(*state->pc++);`
6416 code = wc_code(state.prog.prog[state.pc]);
6417 state.pc += 1;
6418 // c:1311-1312 — `otj = thisjob; thisjob = -1;`
6419 let otj = *THISJOB
6420 .get_or_init(|| std::sync::Mutex::new(-1))
6421 .lock()
6422 .unwrap();
6423 *THISJOB
6424 .get_or_init(|| std::sync::Mutex::new(-1))
6425 .lock()
6426 .unwrap() = -1;
6427 use crate::ported::zsh_h::{
6428 WC_ARITH, WC_CASE, WC_COND, WC_FOR, WC_REPEAT, WC_SELECT, WC_SUBSH, WC_TIMED, WC_TRY,
6429 WC_WHILE,
6430 };
6431 use crate::ported::zsh_h::{WC_ASSIGN, WC_CURSH};
6432 let lv = if code == WC_ASSIGN {
6433 // c:1315-1319 — assignment-only simple cmd path.
6434 // cmdoutval = 0; addvars(state, state->pc - 1, 0); setunderscore("");
6435 addvars(state, state.pc.saturating_sub(1), 0);
6436 setunderscore(""); // c:1317
6437 if isset(XTRACE) {
6438 eprintln!();
6439 }
6440 let ef = errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR;
6441 if ef != 0 {
6442 ef
6443 } else {
6444 0
6445 }
6446 } else {
6447 // c:1322-1330 — dispatch via execfuncs[code - WC_CURSH] or execfuncdef.
6448 let q = queue_signal_level();
6449 dont_queue_signals();
6450 let result = if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
6451 ERRFLAG_ERROR
6452 } else if code == WC_FUNCDEF {
6453 execfuncdef(state, None)
6454 } else {
6455 // c:5499 execfuncs[] table inlined — match the WC_* tag.
6456 match code {
6457 WC_CURSH => execcursh(state, 0),
6458 WC_SUBSH => execcursh(state, 0), // subshell folds to cursh body walk
6459 WC_FOR => execfor(state, 0),
6460 WC_SELECT => execselect(state, 0),
6461 WC_CASE => execcase(state, 0),
6462 WC_IF => execif(state, 0),
6463 WC_WHILE => execwhile(state, 0),
6464 WC_REPEAT => execrepeat(state, 0),
6465 WC_TIMED => exectime(state, 0),
6466 WC_COND => execcond(state, 0),
6467 WC_ARITH => execarith(state, 0),
6468 WC_TRY => exectry(state, 0),
6469 _ => 0,
6470 }
6471 };
6472 restore_queue_signals(q);
6473 result
6474 };
6475 // c:1334 — `thisjob = otj;`
6476 *THISJOB
6477 .get_or_init(|| std::sync::Mutex::new(-1))
6478 .lock()
6479 .unwrap() = otj;
6480 LASTVAL.store(lv, Ordering::Relaxed); // c:1336 — `return lastval = lv;`
6481 lv
6482}
6483
6484/// Port of `execlist(Estate state, int dont_change_job, int exiting)`
6485/// from `Src/exec.c:1349-1665`. Walks WC_LIST entries, dispatches each
6486/// sublist (WC_SUBLIST chain inlined per c:1525-1625, same as C —
6487/// there's no separate execsublist function), handles signal-trap
6488/// dispatch + ERREXIT propagation.
6489///
6490/// Body ports the structural skeleton faithfully (WC_LIST walk,
6491/// per-iteration breaks/retflag/errflag guards, ltype dispatch on
6492/// Z_END/Z_SYNC/Z_ASYNC, donetrap handling). The full signal queue
6493/// + DEBUGBEFORECMD trap machinery from c:1357-1500 is preserved
6494/// in shape with TODO-citations where dependent primitives aren't
6495/// yet ported.
6496pub fn execlist(state: &mut estate, dont_change_job: i32, mut exiting: i32) -> i32 {
6497 let mut last_status: i32 = 0;
6498 let mut donetrap: i32 = 0; // c:1352 — `static int donetrap;`
6499 let cj = *THISJOB
6500 .get_or_init(|| std::sync::Mutex::new(-1))
6501 .lock()
6502 .unwrap(); // c:1364 — `cj = thisjob;`
6503 let _ = dont_change_job; // c:1361 — restored on exit if nonzero.
6504 // c:1380 — `code = *state->pc++;`
6505 if state.pc >= state.prog.prog.len() {
6506 return last_status;
6507 }
6508 let mut code = state.prog.prog[state.pc];
6509 state.pc += 1;
6510 // c:1382-1384 — empty list returns lastval = 0.
6511 if wc_code(code) != WC_LIST {
6512 LASTVAL.store(0, Ordering::Relaxed);
6513 return 0;
6514 }
6515 use crate::ported::zsh_h::{WC_LIST_SKIP, WC_LIST_TYPE, Z_END, Z_SIMPLE, Z_SYNC};
6516 // c:1385-1499 — main WC_LIST loop.
6517 while wc_code(code) == WC_LIST
6518 && BREAKS.load(Ordering::SeqCst) == 0
6519 && RETFLAG.load(Ordering::SeqCst) == 0
6520 && (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0
6521 {
6522 let ltype = WC_LIST_TYPE(code) as i32;
6523 // c:1396 — `csp = cmdsp;` — snapshot cmdstack depth at start
6524 // of this WC_LIST iteration; restored at end so partial
6525 // cmdpush sequences (e.g. from execcond, execfuncs) don't
6526 // leak into the next sublist.
6527 let csp = crate::ported::prompt::CMDSTACK.with(|s| s.borrow().len());
6528 // c:1502-1509 — Z_SIMPLE fast-path.
6529 if (ltype & Z_SIMPLE as i32) != 0 {
6530 let next_pc = state.pc + WC_LIST_SKIP(code) as usize;
6531 let s = execsimple(state);
6532 last_status = s;
6533 state.pc = next_pc;
6534 } else {
6535 // c:1513-1523 — sublist chain.
6536 if state.pc >= state.prog.prog.len() {
6537 break;
6538 }
6539 code = state.prog.prog[state.pc];
6540 state.pc += 1;
6541 // c:1525-1625 — sublist chain (&&/|| operators) inlined.
6542 use crate::ported::zsh_h::{
6543 WC_SUBLIST_AND, WC_SUBLIST_END, WC_SUBLIST_NOT, WC_SUBLIST_OR, WC_SUBLIST_SIMPLE,
6544 WC_SUBLIST_SKIP,
6545 };
6546 let mut sub_code = code;
6547 let _ = dont_change_job;
6548 while wc_code(sub_code) == WC_SUBLIST {
6549 let flags = WC_SUBLIST_FLAGS(sub_code);
6550 let next = state.pc + WC_SUBLIST_SKIP(sub_code) as usize;
6551 let sl_type = WC_SUBLIST_TYPE(sub_code) as i32;
6552 let last1 = if WC_SUBLIST_TYPE(sub_code) == WC_SUBLIST_END {
6553 exiting
6554 } else {
6555 0
6556 };
6557 if flags == WC_SUBLIST_SIMPLE {
6558 last_status = execsimple(state); // c:1605
6559 } else {
6560 let _ = execpline(state, sub_code, sl_type, last1); // c:1607
6561 last_status = LASTVAL.load(Ordering::Relaxed);
6562 }
6563 // c:1612 — `WC_SUBLIST_NOT` inverts status.
6564 if (flags & WC_SUBLIST_NOT) != 0 {
6565 last_status = if last_status == 0 { 1 } else { 0 };
6566 LASTVAL.store(last_status, Ordering::Relaxed);
6567 }
6568 state.pc = next;
6569 if WC_SUBLIST_TYPE(sub_code) == WC_SUBLIST_END {
6570 break;
6571 }
6572 if state.pc >= state.prog.prog.len() {
6573 break;
6574 }
6575 // c:1617-1623 — short-circuit on && / ||.
6576 if sl_type == WC_SUBLIST_AND as i32 && last_status != 0 {
6577 while state.pc < state.prog.prog.len() {
6578 let c = state.prog.prog[state.pc];
6579 if wc_code(c) != WC_SUBLIST {
6580 break;
6581 }
6582 state.pc = state.pc + 1 + WC_SUBLIST_SKIP(c) as usize;
6583 if WC_SUBLIST_TYPE(c) == WC_SUBLIST_END {
6584 break;
6585 }
6586 }
6587 break;
6588 }
6589 if sl_type == WC_SUBLIST_OR as i32 && last_status == 0 {
6590 while state.pc < state.prog.prog.len() {
6591 let c = state.prog.prog[state.pc];
6592 if wc_code(c) != WC_SUBLIST {
6593 break;
6594 }
6595 state.pc = state.pc + 1 + WC_SUBLIST_SKIP(c) as usize;
6596 if WC_SUBLIST_TYPE(c) == WC_SUBLIST_END {
6597 break;
6598 }
6599 }
6600 break;
6601 }
6602 sub_code = state.prog.prog[state.pc];
6603 state.pc += 1;
6604 }
6605 }
6606 // c:1593 — `cmdsp = csp;` — restore cmdstack depth to the
6607 // snapshot taken at start of iteration. Reverses any cmdpush
6608 // calls made by nested execcond / execfuncs / execcmd_exec
6609 // that didn't pop cleanly.
6610 crate::ported::prompt::CMDSTACK.with(|s| {
6611 let mut g = s.borrow_mut();
6612 if g.len() > csp {
6613 g.truncate(csp);
6614 }
6615 });
6616 // c:1626-1634 — donetrap is reset between sublists.
6617 donetrap = 0;
6618 // c:1640-1645 — fetch next WC_LIST header (or break out).
6619 if state.pc >= state.prog.prog.len() {
6620 break;
6621 }
6622 let next_code = state.prog.prog[state.pc];
6623 if wc_code(next_code) != WC_LIST {
6624 break;
6625 }
6626 state.pc += 1;
6627 code = next_code;
6628 // c:1389 — z_end means last sublist, exiting becomes 1 for tail-exec.
6629 if (ltype & Z_END as i32) != 0 {
6630 exiting = 1;
6631 }
6632 }
6633 // c:1659-1664 — cleanup: restore thisjob if dont_change_job, this_noerrexit=1.
6634 if dont_change_job != 0 {
6635 *THISJOB
6636 .get_or_init(|| std::sync::Mutex::new(-1))
6637 .lock()
6638 .unwrap() = cj;
6639 }
6640 let _ = donetrap;
6641 this_noerrexit.store(1, Ordering::Relaxed);
6642 LASTVAL.store(last_status, Ordering::Relaxed);
6643 last_status
6644}
6645
6646// WC_SUBLIST chain walk is inlined into execlist (per `Src/exec.c:1525-
6647// 1625`, the C source likewise inlines it — there's no `execsublist`
6648// function in zsh C).
6649
6650/// Port of `execcmd_getargs(LinkList preargs, LinkList args, int expand)`
6651/// from `Src/exec.c:2791-2806`. Transfer the first node of `args`
6652/// to `preargs`, performing `prefork` (singleton-list expansion) on
6653/// the way if `expand` is set. Used by `execcmd_exec` to pull the
6654/// command head one word at a time so prefix-modifier walking
6655/// (BINF_COMMAND, BINF_EXEC etc.) sees expanded names.
6656pub fn execcmd_getargs(preargs: &mut LinkList<String>, args: &mut LinkList<String>, expand: i32) {
6657 // c:2791
6658 if args.firstnode().is_none() {
6659 // c:2793 — `if (!firstnode(args)) return;`
6660 return;
6661 } else if expand != 0 {
6662 // c:2795
6663 // c:2796-2797 — `local_list0(svl); init_list0(svl);` —
6664 // stack-local single-bucket list. Rust uses a fresh
6665 // LinkList<String> per call.
6666 let mut svl: LinkList<String> = Default::default();
6667 // c:2799 — `addlinknode(&svl, uremnode(args, firstnode(args)));`
6668 if let Some(idx) = args.firstnode() {
6669 if let Some(head) = crate::ported::linklist::uremnode(args, idx) {
6670 svl.push_back(head);
6671 }
6672 }
6673 // c:2801 — `prefork(&svl, 0, NULL);`
6674 let mut rf = 0i32;
6675 prefork(&mut svl, 0, &mut rf);
6676 // c:2802 — `joinlists(preargs, &svl);`
6677 crate::ported::linklist::joinlists(preargs, &mut svl);
6678 } else {
6679 // c:2803-2804 — no-expand path: move head verbatim.
6680 if let Some(idx) = args.firstnode() {
6681 if let Some(head) = crate::ported::linklist::uremnode(args, idx) {
6682 preargs.push_back(head);
6683 }
6684 }
6685 }
6686}
6687
6688/// Port of `execcmd_fork(Estate state, int how, int type,
6689/// Wordcode varspc, LinkList *filelistp, char *text, int oautocont,
6690/// int close_if_forked)` from `Src/exec.c:2810-2893`.
6691///
6692/// Fork the current command into a child process: parent records
6693/// the pid + STTY env scan + addproc; child enters subshell, writes
6694/// `entersubsh_ret` back to parent through `synch` pipe, and returns
6695/// 0 so the caller can continue with the body.
6696///
6697/// `filelistp` out-arg is moved from `jobtab[thisjob].filelist`
6698/// only in the child branch (so the parent's `filelist` stays
6699/// untouched). Rust sig keeps the same C contract.
6700pub fn execcmd_fork(
6701 state: &mut estate,
6702 how: i32,
6703 typ: i32,
6704 varspc: Option<usize>,
6705 filelistp: &mut Vec<String>,
6706 text: &str,
6707 oautocont: i32,
6708 close_if_forked: i32,
6709) -> i32 {
6710 use crate::ported::signals::sigtrapped as sigtrapped_static;
6711 use crate::ported::signals_h::SIGEXIT;
6712 use crate::ported::zsh_h::{
6713 AUTOCONTINUE, BGNICE, WC_ASSIGN as ZWC_ASSIGN, WC_ASSIGN_NUM as ZWC_ASSIGN_NUM,
6714 WC_ASSIGN_SCALAR as ZWC_ASSIGN_SCALAR, WC_ASSIGN_TYPE as ZWC_ASSIGN_TYPE,
6715 WC_SUBSH as ZWC_SUBSH, ZSIG_IGNORED, Z_ASYNC,
6716 };
6717 // c:2810
6718 let pid: libc::pid_t; // c:2814
6719 let mut synch: [i32; 2] = [-1, -1]; // c:2815
6720 let flags: i32; // c:2815
6721 let mut esret: entersubsh_ret = entersubsh_ret::default(); // c:2816
6722 // c:2817 — `struct timespec bgtime;` — bgtime is passed to zfork
6723 // for accounting; the Rust zfork wrapper expects Option<&mut ZshTimespec>.
6724 let mut bgtime = ZshTimespec::default();
6725
6726 child_block(); // c:2819
6727 esret.gleader = -1; // c:2820
6728 esret.list_pipe_job = -1; // c:2821
6729
6730 // c:2823 — `if (pipe(synch) < 0) { zerr("pipe failed: %e", errno); return -1; }`
6731 if unsafe { libc::pipe(synch.as_mut_ptr()) } < 0 {
6732 zerr(&format!("pipe failed: {}", std::io::Error::last_os_error()));
6733 return -1; // c:2825
6734 }
6735 // c:2826 — `else if ((pid = zfork(&bgtime)) == -1) { ... }`
6736 pid = zfork(Some(&mut bgtime));
6737 if pid == -1 {
6738 unsafe {
6739 libc::close(synch[0]); // c:2827
6740 libc::close(synch[1]); // c:2828
6741 }
6742 LASTVAL.store(1, Ordering::Relaxed); // c:2829
6743 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:2830
6744 return -1; // c:2831
6745 }
6746 if pid != 0 {
6747 // c:2833 — parent.
6748 unsafe { libc::close(synch[1]) }; // c:2834
6749 // c:2835 — `read_loop(synch[0], (char *)&esret, sizeof(esret));`
6750 let mut buf = [0u8; size_of::<entersubsh_ret>()];
6751 let _ = crate::ported::utils::read_loop(synch[0], &mut buf);
6752 // entersubsh_ret is two i32s; reconstruct from LE bytes (host order).
6753 if buf.len() >= 8 {
6754 esret.gleader = i32::from_ne_bytes([buf[0], buf[1], buf[2], buf[3]]);
6755 esret.list_pipe_job = i32::from_ne_bytes([buf[4], buf[5], buf[6], buf[7]]);
6756 }
6757 unsafe { libc::close(synch[0]) }; // c:2836
6758 if (how & Z_ASYNC as i32) != 0 {
6759 // c:2837 — `lastpid = (zlong) pid;`
6760 crate::ported::modules::clone::lastpid.store(pid, Ordering::Relaxed);
6761 } else {
6762 // c:2839 — `if (!jobtab[thisjob].stty_in_env && varspc)`.
6763 let thisjob_idx = {
6764 if let Some(m) = THISJOB.get() {
6765 *m.lock().unwrap()
6766 } else {
6767 -1
6768 }
6769 };
6770 // Examine the jobtab entry under lock.
6771 let stty_already = if thisjob_idx >= 0 {
6772 if let Some(jt) = JOBTAB.get() {
6773 let guard = jt.lock().unwrap();
6774 guard
6775 .get(thisjob_idx as usize)
6776 .map(|j| j.stty_in_env != 0)
6777 .unwrap_or(true)
6778 } else {
6779 true
6780 }
6781 } else {
6782 true
6783 };
6784 if !stty_already && varspc.is_some() {
6785 // c:2841-2851 — walk varspc looking for STTY=...
6786 let mut p = varspc.unwrap();
6787 loop {
6788 if p >= state.prog.prog.len() {
6789 break;
6790 }
6791 let ac = state.prog.prog[p];
6792 if wc_code(ac) != ZWC_ASSIGN {
6793 break;
6794 }
6795 // c:2845 — `if (!strcmp(ecrawstr(state->prog, p + 1, NULL), "STTY"))`
6796 let name = ecrawstr(&state.prog, p + 1, None);
6797 if name == "STTY" {
6798 // c:2846 — `jobtab[thisjob].stty_in_env = 1;`
6799 if let Some(jt) = JOBTAB.get() {
6800 let mut guard = jt.lock().unwrap();
6801 if let Some(j) = guard.get_mut(thisjob_idx as usize) {
6802 j.stty_in_env = 1;
6803 }
6804 }
6805 break; // c:2847
6806 }
6807 p += if ZWC_ASSIGN_TYPE(ac) == ZWC_ASSIGN_SCALAR {
6808 3 // c:2849
6809 } else {
6810 (ZWC_ASSIGN_NUM(ac) + 2) as usize // c:2850
6811 };
6812 }
6813 }
6814 }
6815 // c:2853 — `addproc(pid, text, 0, &bgtime, esret.gleader, esret.list_pipe_job);`
6816 if let Some(jt) = JOBTAB.get() {
6817 let mut guard = jt.lock().unwrap();
6818 let tj = {
6819 if let Some(m) = THISJOB.get() {
6820 *m.lock().unwrap()
6821 } else {
6822 -1
6823 }
6824 };
6825 if tj >= 0 {
6826 if let Some(j) = guard.get_mut(tj as usize) {
6827 crate::ported::jobs::addproc(
6828 j,
6829 pid,
6830 text,
6831 false,
6832 Some(std::time::Instant::now()),
6833 esret.gleader,
6834 esret.list_pipe_job,
6835 );
6836 }
6837 }
6838 }
6839 // c:2854-2855 — `if (oautocont >= 0) opts[AUTOCONTINUE] = oautocont;`
6840 if oautocont >= 0 {
6841 opt_state_set("autocontinue", oautocont != 0);
6842 let _ = AUTOCONTINUE; // const referenced for parity
6843 }
6844 // c:2856 — `pipecleanfilelist(jobtab[thisjob].filelist, 1);`
6845 if let Some(jt) = JOBTAB.get() {
6846 let mut guard = jt.lock().unwrap();
6847 let tj = {
6848 if let Some(m) = THISJOB.get() {
6849 *m.lock().unwrap()
6850 } else {
6851 -1
6852 }
6853 };
6854 if tj >= 0 {
6855 if let Some(j) = guard.get_mut(tj as usize) {
6856 crate::ported::jobs::pipecleanfilelist(j, true);
6857 }
6858 }
6859 }
6860 return pid; // c:2857
6861 }
6862
6863 // c:2860 — pid == 0 (child).
6864 unsafe { libc::close(synch[0]) }; // c:2861
6865 flags = (if (how & Z_ASYNC as i32) != 0 {
6866 esub::ASYNC
6867 } else {
6868 0
6869 }) | esub::PGRP; // c:2862
6870 let mut flags = flags;
6871 if typ != ZWC_SUBSH as i32 && (how & Z_ASYNC as i32) == 0 {
6872 flags |= esub::KEEPTRAP; // c:2864
6873 }
6874 if typ == ZWC_SUBSH as i32 && (how & Z_ASYNC as i32) == 0 {
6875 flags |= esub::JOB_CONTROL; // c:2866
6876 }
6877 // c:2867 — `*filelistp = jobtab[thisjob].filelist;`
6878 if let Some(jt) = JOBTAB.get() {
6879 let mut guard = jt.lock().unwrap();
6880 let tj = {
6881 if let Some(m) = THISJOB.get() {
6882 *m.lock().unwrap()
6883 } else {
6884 -1
6885 }
6886 };
6887 if tj >= 0 {
6888 if let Some(j) = guard.get_mut(tj as usize) {
6889 *filelistp = std::mem::take(&mut j.filelist);
6890 }
6891 }
6892 }
6893 entersubsh(flags, Some(&mut esret)); // c:2868
6894 // c:2869 — `write_loop(synch[1], &esret, sizeof(esret));`
6895 let mut buf = [0u8; 8];
6896 buf[0..4].copy_from_slice(&esret.gleader.to_ne_bytes());
6897 buf[4..8].copy_from_slice(&esret.list_pipe_job.to_ne_bytes());
6898 if write_loop(synch[1], &buf).map(|n| n as usize).unwrap_or(0) != buf.len() {
6899 zerr(&format!(
6900 "Failed to send entersubsh_ret report: {}",
6901 std::io::Error::last_os_error()
6902 ));
6903 return -1; // c:2871
6904 }
6905 unsafe { libc::close(synch[1]) }; // c:2873
6906 let _ = zclose(close_if_forked); // c:2874
6907
6908 // c:2876 — `if (sigtrapped[SIGINT] & ZSIG_IGNORED) holdintr();`
6909 let sigint_state = {
6910 let guard = sigtrapped_static.lock().unwrap();
6911 guard.get(libc::SIGINT as usize).copied().unwrap_or(0)
6912 };
6913 if (sigint_state & ZSIG_IGNORED) != 0 {
6914 crate::ported::signals::holdintr(); // c:2877
6915 }
6916 // c:2882 — `sigtrapped[SIGEXIT] = 0;` — EXIT traps don't fire in fork-child.
6917 {
6918 let mut guard = sigtrapped_static.lock().unwrap();
6919 if let Some(slot) = guard.get_mut(SIGEXIT as usize) {
6920 *slot = 0;
6921 }
6922 }
6923 // c:2884-2890 — `if ((how & Z_ASYNC) && isset(BGNICE)) nice(5)`.
6924 // Per-platform errno setter+reader: __error() on macOS,
6925 // __errno_location() on Linux. Without cfg gating Linux CI breaks.
6926 if (how & Z_ASYNC as i32) != 0 && isset(BGNICE) {
6927 #[cfg(target_os = "macos")]
6928 unsafe {
6929 *libc::__error() = 0;
6930 if libc::nice(5) == -1 && *libc::__error() != 0 {
6931 zwarn(&format!(
6932 "nice(5) failed: {}",
6933 std::io::Error::last_os_error()
6934 ));
6935 }
6936 }
6937 #[cfg(target_os = "linux")]
6938 unsafe {
6939 *libc::__errno_location() = 0;
6940 if libc::nice(5) == -1 && *libc::__errno_location() != 0 {
6941 zwarn(&format!(
6942 "nice(5) failed: {}",
6943 std::io::Error::last_os_error()
6944 ));
6945 }
6946 }
6947 }
6948 0 // c:2892
6949}
6950
6951/// Port of `execcmd_analyse(Estate state, Execcmd_params eparams)`
6952/// from `Src/exec.c:2733-2785`. Pre-execcmd_exec analysis pass:
6953/// walks the wordcode at `state->pc`, splits out redirs/varspc/args
6954/// without expanding (no prefork, no globbing), and fills `eparams`
6955/// so the caller (execcmd_exec at c:2901 or execpline2 at c:2013)
6956/// can branch on the command type before the real work.
6957pub fn execcmd_analyse(state: &mut estate, eparams: &mut crate::ported::zsh_h::execcmd_params) {
6958 use crate::ported::zsh_h::{
6959 WC_ASSIGN as ZWC_ASSIGN, WC_REDIR as ZWC_REDIR, WC_SIMPLE as ZWC_SIMPLE,
6960 WC_SIMPLE_ARGC as ZWC_SIMPLE_ARGC, WC_TYPESET as ZWC_TYPESET,
6961 WC_TYPESET_ARGC as ZWC_TYPESET_ARGC,
6962 };
6963 // c:2733
6964 let mut code: wordcode; // c:2735
6965 let mut i: i32; // c:2736
6966 let _ = i;
6967
6968 // c:2738 — `eparams->beg = state->pc;`
6969 eparams.beg = state.pc;
6970 // c:2739-2740 — `eparams->redir = (wc_code(*state->pc) == WC_REDIR ? ecgetredirs(state) : NULL);`
6971 eparams.redir =
6972 if state.pc < state.prog.prog.len() && wc_code(state.prog.prog[state.pc]) == ZWC_REDIR {
6973 Some(crate::ported::parse::ecgetredirs(state))
6974 } else {
6975 None
6976 };
6977 // c:2741-2748 — varspc walk (WC_ASSIGN chain).
6978 if state.pc < state.prog.prog.len() && wc_code(state.prog.prog[state.pc]) == ZWC_ASSIGN {
6979 cmdoutval.store(0, Ordering::Relaxed); // c:2742
6980 eparams.varspc = Some(state.pc); // c:2743
6981 // c:2744-2746 — `while (wc_code((code = *state->pc)) == WC_ASSIGN) state->pc += ...`
6982 loop {
6983 if state.pc >= state.prog.prog.len() {
6984 break;
6985 }
6986 code = state.prog.prog[state.pc];
6987 if wc_code(code) != ZWC_ASSIGN {
6988 break;
6989 }
6990 state.pc += if WC_ASSIGN_TYPE(code) == WC_ASSIGN_SCALAR {
6991 3 // c:2745
6992 } else {
6993 (WC_ASSIGN_NUM(code) + 2) as usize // c:2746
6994 };
6995 }
6996 } else {
6997 eparams.varspc = None; // c:2748
6998 }
6999
7000 // c:2750 — `code = *state->pc++;`
7001 if state.pc >= state.prog.prog.len() {
7002 eparams.args = None;
7003 eparams.assignspc = None;
7004 eparams.typ = 0;
7005 eparams.postassigns = 0;
7006 eparams.htok = 0;
7007 return;
7008 }
7009 code = state.prog.prog[state.pc];
7010 state.pc += 1;
7011
7012 // c:2752 — `eparams->type = wc_code(code);`
7013 eparams.typ = wc_code(code) as i32;
7014 // c:2753 — `eparams->postassigns = 0;`
7015 eparams.postassigns = 0;
7016
7017 // c:2755-2783 — switch on type. EC_DUP is used (not EC_DUPTOK)
7018 // per the comment at c:2755-2757.
7019 match eparams.typ as wordcode {
7020 x if x == ZWC_SIMPLE => {
7021 // c:2759-2763
7022 let mut htok = 0;
7023 let argc = ZWC_SIMPLE_ARGC(code) as usize;
7024 eparams.args = Some(ecgetlist(state, argc, EC_DUP, Some(&mut htok)));
7025 eparams.htok = htok;
7026 eparams.assignspc = None;
7027 }
7028 x if x == ZWC_TYPESET => {
7029 // c:2765-2777
7030 let mut htok = 0;
7031 let argc = ZWC_TYPESET_ARGC(code) as usize;
7032 eparams.args = Some(ecgetlist(state, argc, EC_DUP, Some(&mut htok)));
7033 eparams.htok = htok;
7034 // c:2768 — `eparams->postassigns = *state->pc++;`
7035 if state.pc < state.prog.prog.len() {
7036 eparams.postassigns = state.prog.prog[state.pc] as i32;
7037 state.pc += 1;
7038 }
7039 // c:2769 — `eparams->assignspc = state->pc;`
7040 eparams.assignspc = Some(state.pc);
7041 // c:2770-2776 — walk past the postassigns.
7042 let mut k = 0i32;
7043 while k < eparams.postassigns {
7044 if state.pc >= state.prog.prog.len() {
7045 break;
7046 }
7047 code = state.prog.prog[state.pc];
7048 // c:2772-2773 DPUTS — assert wc_code == WC_ASSIGN; skipped.
7049 state.pc += if WC_ASSIGN_TYPE(code) == WC_ASSIGN_SCALAR {
7050 3 // c:2774
7051 } else {
7052 (WC_ASSIGN_NUM(code) + 2) as usize // c:2775
7053 };
7054 k += 1;
7055 }
7056 }
7057 _ => {
7058 // c:2779-2783 default.
7059 eparams.args = None;
7060 eparams.assignspc = None;
7061 eparams.htok = 0;
7062 }
7063 }
7064}
7065
7066/// Port of `char **zsh_eval_context;` from `Src/exec.c` (zsh.export:355).
7067/// Stack of `"context"` labels used by `eval`-style nested execution:
7068/// `bin_dot`, `bin_eval`, `execode`, autoloads. Each `execode(prog,
7069/// ..., "context")` pushes its label and pops on return.
7070pub static zsh_eval_context: std::sync::Mutex<Vec<String>> = std::sync::Mutex::new(Vec::new());
7071
7072/// Port of `static int donetrap;` from `Src/exec.c:1351`. Tracks
7073/// whether the ZERR trap has already fired for the current sublist.
7074/// C source resets to 0 at sublist start (c:1455) and sets to 1
7075/// after `dotrap(SIGZERR)` (c:1602). The check
7076/// `if (!this_noerrexit && !donetrap && !this_donetrap)` at c:1598
7077/// suppresses re-firing within the same sublist AND, crucially,
7078/// carries the "already fired" state across a function-call return
7079/// boundary so the outer caller's post-command check doesn't fire
7080/// ZERR a second time for the same logical error. Bug #303 in
7081/// docs/BUGS.md.
7082///
7083/// Reset at each top-level statement boundary via
7084/// `BUILTIN_DONETRAP_RESET` emitted by `compile_list`. Set after
7085/// `dotrap(SIGZERR)` fires inside `BUILTIN_ERREXIT_CHECK`.
7086pub static DONETRAP: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
7087
7088/// Port of `save_params(Estate state, Wordcode pc, LinkList *restore_p,
7089/// LinkList *remove_p)` from `Src/exec.c:4410-4458`. Walk WC_ASSIGN
7090/// chain at `pc`, snapshot each existing param into `restore_p` (so
7091/// the builtin/shfunc can restore them on return) and enqueue every
7092/// touched name in `remove_p` (so we know what to unset).
7093pub fn save_params(
7094 state: &mut estate,
7095 pc: usize,
7096 restore_p: &mut Vec<crate::ported::zsh_h::param>,
7097 remove_p: &mut Vec<String>,
7098) {
7099 use crate::ported::zsh_h::{
7100 PM_READONLY, PM_SPECIAL, WC_ASSIGN, WC_ASSIGN_NUM as ZWC_ASSIGN_NUM,
7101 WC_ASSIGN_SCALAR as ZWC_ASSIGN_SCALAR, WC_ASSIGN_TYPE as ZWC_ASSIGN_TYPE,
7102 };
7103 // c:4410 — `*restore_p = newlinklist();` — caller pre-allocates.
7104 // c:4417 — `*remove_p = newlinklist();` — caller pre-allocates.
7105 let mut p = pc;
7106 // c:4419 — `while (wc_code(ac = *pc) == WC_ASSIGN)`
7107 loop {
7108 if p >= state.prog.prog.len() {
7109 break;
7110 }
7111 let ac = state.prog.prog[p];
7112 if wc_code(ac) != WC_ASSIGN {
7113 break;
7114 }
7115 // c:4420 — `s = ecrawstr(state->prog, pc + 1, NULL);`
7116 let s = ecrawstr(&state.prog, p + 1, None);
7117 // c:4421 — `pm = paramtab->getnode(paramtab, s)`
7118 let pm_clone: Option<crate::ported::zsh_h::param> = {
7119 let tab = paramtab().read().unwrap();
7120 tab.get(&s).map(|b| (**b).clone())
7121 };
7122 if let Some(pm) = pm_clone {
7123 // c:4423-4424 — `if (pm->env) delenv(pm);`
7124 if pm.env.is_some() {
7125 crate::ported::params::delenv(&s);
7126 }
7127 // c:4425-4448 — copy if not readonly-special.
7128 if (pm.node.flags & PM_SPECIAL as i32) == 0 {
7129 // c:4426-4438 — regular param: deep copy via copyparam(tpm, pm, 0).
7130 let mut tpm = pm.clone();
7131 tpm.node.nam = s.clone();
7132 // copyparam with fakecopy=0 already done by the clone()
7133 // (Clone derives a deep copy of param fields).
7134 restore_p.push(tpm); // c:4451
7135 } else if (pm.node.flags & PM_READONLY as i32) == 0 {
7136 // c:4439-4448 — special-but-not-readonly: fakecopy=1.
7137 let mut tpm = pm.clone();
7138 tpm.node.nam = pm.node.nam.clone();
7139 restore_p.push(tpm); // c:4451
7140 }
7141 // c:4449 — `addlinknode(*remove_p, dupstring(s));`
7142 remove_p.push(s.clone());
7143 } else {
7144 // c:4453 — `addlinknode(*remove_p, dupstring(s));`
7145 remove_p.push(s.clone());
7146 }
7147 // c:4455 — `pc += (WC_ASSIGN_TYPE(ac) == WC_ASSIGN_SCALAR ? 3 : WC_ASSIGN_NUM(ac) + 2);`
7148 p += if ZWC_ASSIGN_TYPE(ac) == ZWC_ASSIGN_SCALAR {
7149 3
7150 } else {
7151 (ZWC_ASSIGN_NUM(ac) + 2) as usize
7152 };
7153 }
7154}
7155
7156/// Port of `restore_params(LinkList restorelist, LinkList removelist)`
7157/// from `Src/exec.c:4464-4528`. After the builtin/shfunc returns,
7158/// unset every name in removelist, then for each saved param in
7159/// restorelist re-install its values (PM_SPECIAL go through gsu
7160/// setfn; regular params re-enter paramtab as-is).
7161pub fn restore_params(restorelist: Vec<crate::ported::zsh_h::param>, removelist: Vec<String>) {
7162 use crate::ported::zsh_h::{PM_READONLY, PM_SPECIAL};
7163 // c:4470-4476 — `while ((s = ugetnode(removelist)))` — unset each.
7164 for s in &removelist {
7165 // c:4471 — `if ((pm = paramtab->getnode(paramtab, s)) && !(pm->node.flags & PM_SPECIAL))`
7166 let flags = {
7167 let tab = paramtab().read().unwrap();
7168 tab.get(s).map(|p| p.node.flags)
7169 };
7170 if let Some(f) = flags {
7171 if (f & PM_SPECIAL as i32) == 0 {
7172 // c:4473 — `pm->node.flags &= ~PM_READONLY;`
7173 let mut tab = paramtab().write().unwrap();
7174 if let Some(pm_mut) = tab.get_mut(s) {
7175 pm_mut.node.flags &= !(PM_READONLY as i32);
7176 }
7177 // Drop write guard before calling unsetparam_pm.
7178 drop(tab);
7179 let mut tab = paramtab().write().unwrap();
7180 if let Some(pm_mut) = tab.get_mut(s) {
7181 let _ = crate::ported::params::unsetparam_pm(pm_mut, 0, 0); // c:4474
7182 }
7183 }
7184 }
7185 }
7186 // c:4478-4523 — restore saved params.
7187 for pm in restorelist {
7188 // c:4481-4520 — PM_SPECIAL: route through gsu setfn.
7189 // c:4521-4523 — non-special: re-install via paramtab.
7190 if (pm.node.flags & PM_SPECIAL as i32) != 0 {
7191 // PM_SPECIAL restore: full path requires PM_TYPE dispatch
7192 // on gsu_s/i/f/a/h setfn. Each setfn fires the param's
7193 // canonical write hook. Pragmatic port: overwrite in
7194 // paramtab; daily-driver path rarely saves specials (those
7195 // are reserved-name vars like PATH/FPATH/etc. which can't
7196 // appear as `VAR=val cmd` prefix anyway).
7197 let mut tab = paramtab().write().unwrap();
7198 tab.insert(pm.node.nam.clone(), Box::new(pm));
7199 } else {
7200 // c:4521 — `paramtab->addnode(paramtab, ztrdup(pm->node.nam), pm);`
7201 let mut tab = paramtab().write().unwrap();
7202 tab.insert(pm.node.nam.clone(), Box::new(pm));
7203 }
7204 }
7205}
7206
7207/// Port of `void execode(Eprog p, int dont_change_job, int exiting,
7208/// char *context)` from `Src/exec.c:1245-1282`. Set up an `estate`
7209/// around the given Eprog and run `execlist`. Maintains the
7210/// `zsh_eval_context` stack so `$ZSH_EVAL_CONTEXT` reflects the
7211/// call chain.
7212pub fn execode(p: crate::ported::zsh_h::Eprog, dont_change_job: i32, exiting: i32, context: &str) {
7213 // c:1245
7214 let prog_ref = *p;
7215 // c:1247 — `struct estate s;`
7216 let mut s = estate {
7217 prog: Box::new(prog_ref.clone()),
7218 // c:1269 — `s.pc = p->prog;` — start at index 0.
7219 pc: 0,
7220 // c:1270 — `s.strs = p->strs;`
7221 strs: prog_ref.strs.clone(),
7222 strs_offset: 0,
7223 };
7224 // c:1251-1266 — push context onto zsh_eval_context.
7225 let pushed = {
7226 if let Ok(mut ctx) = zsh_eval_context.lock() {
7227 ctx.push(context.to_string());
7228 true
7229 } else {
7230 false
7231 }
7232 };
7233 // c:1271 — `useeprog(p);`
7234 crate::ported::parse::useeprog(&mut s.prog);
7235 // c:1273 — `execlist(&s, dont_change_job, exiting);`
7236 execlist(&mut s, dont_change_job, exiting);
7237 // c:1275 — `freeeprog(p);`
7238 crate::ported::parse::freeeprog(&mut s.prog);
7239 // c:1281 — `zsh_eval_context[alen] = NULL;` — pop our entry.
7240 if pushed {
7241 if let Ok(mut ctx) = zsh_eval_context.lock() {
7242 ctx.pop();
7243 }
7244 }
7245}
7246
7247/// Port of `execautofn_basic(Estate state, UNUSED(int do_exec))` from
7248/// `Src/exec.c:5608-5630`. Run a pre-loaded autoload function body
7249/// via `execode`, snapshotting `scriptname`/`scriptfilename` around
7250/// the call so `%N` / `%x` reflect the autoload target during
7251/// execution.
7252pub fn execautofn_basic(state: &mut estate, _do_exec: i32) -> i32 {
7253 // c:5608
7254 // c:5613 — `shf = state->prog->shf;`
7255 let shf = match state.prog.shf.as_deref() {
7256 Some(s) => s.clone(),
7257 None => return LASTVAL.load(Ordering::Relaxed),
7258 };
7259
7260 // c:5619-5620 — funcstack filename catch-up. zshrs's funcstack
7261 // top-of-stack tracking is in modules::parameter::FUNCSTACK.
7262 {
7263 let mut stk = crate::ported::modules::parameter::FUNCSTACK.lock().unwrap();
7264 if let Some(top) = stk.last_mut() {
7265 if top.filename.is_none() {
7266 // c:5620 — `funcstack->filename = getshfuncfile(shf);`
7267 top.filename = crate::ported::hashtable::getshfuncfile(&shf.node.nam);
7268 }
7269 }
7270 }
7271
7272 // c:5622-5623 — `oldscriptname/oldscriptfilename = scriptname/scriptfilename;`
7273 let oldscriptname = crate::ported::utils::scriptname_get();
7274 let oldscriptfilename = crate::ported::utils::scriptfilename_get();
7275 // c:5624 — `scriptname = dupstring(shf->node.nam);`
7276 crate::ported::utils::set_scriptname(Some(shf.node.nam.clone()));
7277 // c:5625 — `scriptfilename = getshfuncfile(shf);`
7278 crate::ported::utils::set_scriptfilename(crate::ported::hashtable::getshfuncfile(
7279 &shf.node.nam,
7280 ));
7281 // c:5626 — `execode(shf->funcdef, 1, 0, "loadautofunc");`
7282 if let Some(funcdef) = shf.funcdef.clone() {
7283 execode(funcdef, 1, 0, "loadautofunc");
7284 }
7285 // c:5627-5628 — restore.
7286 crate::ported::utils::set_scriptname(oldscriptname);
7287 crate::ported::utils::set_scriptfilename(oldscriptfilename);
7288
7289 LASTVAL.load(Ordering::Relaxed) // c:5630
7290}
7291
7292/// Port of `static int execautofn(Estate state, UNUSED(int do_exec))`
7293/// from `Src/exec.c:5635-5644`. The autoload-aware dispatch entry
7294/// for `WC_AUTOFN`: fault the function body in via `loadautofn`,
7295/// then hand off to `execautofn_basic` to actually run it.
7296///
7297/// C body:
7298/// ```c
7299/// static int
7300/// execautofn(Estate state, UNUSED(int do_exec))
7301/// {
7302/// Shfunc shf;
7303/// if (!(shf = loadautofn(state->prog->shf, 1, 0, 0)))
7304/// return 1;
7305/// state->prog->shf = shf;
7306/// return execautofn_basic(state, 0);
7307/// }
7308/// ```
7309///
7310/// Rust port: `loadautofn` mutates the `shfunc` in place via a raw
7311/// pointer and returns 0/1 (success/failure), so the explicit
7312/// `state->prog->shf = shf` assignment in C is implicit here.
7313pub fn execautofn(state: &mut estate, _do_exec: i32) -> i32 {
7314 // c:5638-5640 — `if (!(shf = loadautofn(state->prog->shf, 1, 0, 0))) return 1;`
7315 let shf_ptr: *mut shfunc = match state.prog.shf.as_mut() {
7316 Some(b) => &mut **b as *mut shfunc,
7317 None => return 1,
7318 };
7319 if loadautofn(shf_ptr, 1, 0, 0) != 0 {
7320 return 1;
7321 }
7322 // c:5643 — `return execautofn_basic(state, 0);`
7323 execautofn_basic(state, 0)
7324}
7325
7326/// Port of `execpline2(Estate state, wordcode pcode, int how, int input,
7327/// int output, int last1)` from `Src/exec.c:1989-2040`. Recursive
7328/// multi-stage pipe walker: at each step, analyse the current
7329/// command, fork-into-pipe (if mid-pipeline) or exec directly (if
7330/// WC_PIPE_END), then recurse on the next stage with `pipes[0]` as
7331/// its input fd.
7332pub fn execpline2(
7333 state: &mut estate,
7334 pcode: wordcode,
7335 how: i32,
7336 input: i32,
7337 output: i32,
7338 last1: i32,
7339) {
7340 use crate::ported::builtin::{BREAKS, INEVAL, RETFLAG};
7341 use crate::ported::zsh_h::{
7342 execcmd_params, CS_PIPE, WC_PIPE_END, WC_PIPE_LINENO as ZWC_PIPE_LINENO,
7343 WC_PIPE_TYPE as ZWC_PIPE_TYPE, Z_ASYNC,
7344 };
7345 // c:1991
7346 let mut eparams: execcmd_params = execcmd_params::default(); // c:1994 `struct execcmd_params eparams;`
7347
7348 // c:1996-1997 — `if (breaks || retflag) return;`
7349 if BREAKS.load(Ordering::SeqCst) != 0 || RETFLAG.load(Ordering::SeqCst) != 0 {
7350 return;
7351 }
7352
7353 // c:1999-2001 — `if (!IN_EVAL_TRAP() && !ineval && WC_PIPE_LINENO(pcode))
7354 // lineno = WC_PIPE_LINENO(pcode) - 1;`
7355 if !crate::ported::zsh_h::IN_EVAL_TRAP()
7356 && INEVAL.load(Ordering::SeqCst) == 0
7357 && ZWC_PIPE_LINENO(pcode) != 0
7358 {
7359 let new_lineno = ZWC_PIPE_LINENO(pcode).saturating_sub(1) as usize;
7360 crate::ported::input::lineno.with(|l| l.set(new_lineno));
7361 }
7362
7363 // c:2003-2011 — pline_level == 1 → snapshot to list_pipe_text for `jobs` output.
7364 if pline_level.load(Ordering::Relaxed) == 1 {
7365 // c:2003
7366 if (how & Z_ASYNC as i32) != 0 || sfcontext.load(Ordering::Relaxed) == 0 {
7367 // c:2004 — `(how & Z_ASYNC) || !sfcontext`
7368 // c:2005-2008 — `strcpy(list_pipe_text, getjobtext(state->prog,
7369 // state->pc + (WC_PIPE_TYPE(pcode) == WC_PIPE_END ? 0 : 1)));`
7370 let pc_for_text = state.pc
7371 + if ZWC_PIPE_TYPE(pcode) == WC_PIPE_END {
7372 0
7373 } else {
7374 1
7375 };
7376 let text = crate::ported::text::getjobtext(state.prog.clone(), Some(pc_for_text));
7377 if let Ok(mut lpt) = LIST_PIPE_TEXT.lock() {
7378 *lpt = text;
7379 }
7380 } else {
7381 // c:2010 — `list_pipe_text[0] = '\0';`
7382 if let Ok(mut lpt) = LIST_PIPE_TEXT.lock() {
7383 lpt.clear();
7384 }
7385 }
7386 }
7387
7388 if ZWC_PIPE_TYPE(pcode) == WC_PIPE_END {
7389 // c:2012-2014 — terminal stage: analyse + exec directly.
7390 execcmd_analyse(state, &mut eparams); // c:2013
7391 execcmd_exec(
7392 state,
7393 &mut eparams,
7394 input,
7395 output,
7396 how,
7397 if last1 != 0 { 1 } else { 2 }, // c:2014 `last1 ? 1 : 2`
7398 -1, // c:2014 close_if_forked = -1
7399 );
7400 } else {
7401 // c:2015-2039 — non-terminal stage: pipe + fork + recurse.
7402 let mut pipes: [i32; 2] = [-1, -1]; // c:2016
7403 let old_list_pipe = list_pipe.load(Ordering::Relaxed); // c:2017
7404 // c:2018 — `Wordcode next = state->pc + (*state->pc);`
7405 let next = if state.pc < state.prog.prog.len() {
7406 state.pc + state.prog.prog[state.pc] as usize
7407 } else {
7408 state.pc
7409 };
7410 // c:2020 — `++state->pc;`
7411 if state.pc < state.prog.prog.len() {
7412 state.pc += 1;
7413 }
7414 execcmd_analyse(state, &mut eparams); // c:2021
7415
7416 if mpipe(&mut pipes) < 0 {
7417 // c:2023-2025 — pipe() failure — `/* FIXME */` in C, fall through.
7418 }
7419
7420 // c:2027 — `addfilelist(NULL, pipes[0]);`
7421 // C uses the current thisjob's filelist; Rust port wires through JOBTAB.
7422 if let Some(jt) = JOBTAB.get() {
7423 let mut guard = jt.lock().unwrap();
7424 let tj = {
7425 if let Some(m) = THISJOB.get() {
7426 *m.lock().unwrap()
7427 } else {
7428 -1
7429 }
7430 };
7431 if tj >= 0 {
7432 if let Some(j) = guard.get_mut(tj as usize) {
7433 crate::ported::jobs::addfilelist(j, None, pipes[0]);
7434 }
7435 }
7436 }
7437
7438 // c:2028 — `execcmd_exec(state, &eparams, input, pipes[1], how, 0, pipes[0]);`
7439 execcmd_exec(state, &mut eparams, input, pipes[1], how, 0, pipes[0]);
7440 let _ = zclose(pipes[1]); // c:2029
7441 state.pc = next; // c:2030
7442
7443 // c:2034 — `cmdpush(CS_PIPE);`
7444 cmdpush(CS_PIPE as u8);
7445 // c:2035 — `list_pipe = 1;`
7446 list_pipe.store(1, Ordering::Relaxed);
7447 // c:2036 — `execpline2(state, *state->pc++, how, pipes[0], output, last1);`
7448 let next_pcode = if state.pc < state.prog.prog.len() {
7449 state.prog.prog[state.pc]
7450 } else {
7451 0
7452 };
7453 if state.pc < state.prog.prog.len() {
7454 state.pc += 1;
7455 }
7456 execpline2(state, next_pcode, how, pipes[0], output, last1);
7457 // c:2037 — `list_pipe = old_list_pipe;`
7458 list_pipe.store(old_list_pipe, Ordering::Relaxed);
7459 // c:2038 — `cmdpop();`
7460 cmdpop();
7461 }
7462}
7463
7464/// Port of `execpline(Estate state, wordcode slcode, int how, int last1)`
7465/// from `Src/exec.c:1668-1942`. Walks the WC_PIPE chain, sets up
7466/// pipes/fork between stages, handles Z_TIMED / Z_ASYNC.
7467///
7468/// The full body needs: pipe(), fork(), execcmd_exec per-stage, job-
7469/// table installation, wait-status reaping. Until those primitives
7470/// land in faithfully-ported form, the structural shape is preserved
7471/// here: walk the WC_PIPE chain, exec each cmd inline (the inlined
7472/// match is the same dispatch C's exec.c:2901-3700 uses), propagate
7473/// LASTVAL through stages. Single-cmd pipelines work end-to-end;
7474/// multi-stage pipelines fall back to sequential execution (status
7475/// of last stage) until pipe + fork land.
7476pub fn execpline(state: &mut estate, slcode: wordcode, how: i32, last1: i32) -> i32 {
7477 use crate::ported::zsh_h::{WC_SUBLIST_FLAGS, WC_SUBLIST_NOT, Z_TIMED};
7478 let slflags = WC_SUBLIST_FLAGS(slcode); // c:1673
7479 // c:1677-1680 — `if (wc_code(code) != WC_PIPE && !(how & Z_TIMED))
7480 // return lastval = (slflags & WC_SUBLIST_NOT) != 0;
7481 // else if (slflags & WC_SUBLIST_NOT) last1 = 0;`
7482 if state.pc >= state.prog.prog.len() || wc_code(state.prog.prog[state.pc]) != WC_PIPE {
7483 if (how & Z_TIMED as i32) == 0 {
7484 let ret = if (slflags & WC_SUBLIST_NOT) != 0 {
7485 1
7486 } else {
7487 0
7488 };
7489 LASTVAL.store(ret, Ordering::Relaxed);
7490 return ret;
7491 }
7492 }
7493 let mut last1 = last1;
7494 if (slflags & WC_SUBLIST_NOT) != 0 {
7495 last1 = 0; // c:1680
7496 }
7497 let mut code = state.prog.prog[state.pc];
7498 state.pc += 1;
7499 let mut last_status: i32 = 0;
7500 use crate::ported::zsh_h::{WC_PIPE_END, WC_PIPE_TYPE};
7501 let _ = how;
7502 let _ = last1;
7503 // c:1700-1940 — main WC_PIPE loop. Each iter: exec one cmd, advance.
7504 loop {
7505 // c:2901-3700 — execcmd_exec dispatch tail inlined: match the
7506 // WC_* tag at state.pc and dispatch to the matching execX.
7507 // Same dispatch as `execfuncs[]` (exec.c:5499).
7508 use crate::ported::zsh_h::{
7509 WC_ARITH, WC_CASE, WC_COND, WC_CURSH, WC_FOR, WC_FUNCDEF, WC_IF, WC_REPEAT, WC_SELECT,
7510 WC_SIMPLE, WC_SUBSH, WC_TIMED, WC_TRY, WC_WHILE,
7511 };
7512 let s = if state.pc < state.prog.prog.len() {
7513 let inner = state.prog.prog[state.pc];
7514 match wc_code(inner) {
7515 WC_SIMPLE => execsimple(state),
7516 WC_SUBSH | WC_CURSH => execcursh(state, 0),
7517 WC_FOR => execfor(state, 0),
7518 WC_SELECT => execselect(state, 0),
7519 WC_CASE => execcase(state, 0),
7520 WC_IF => execif(state, 0),
7521 WC_WHILE => execwhile(state, 0),
7522 WC_REPEAT => execrepeat(state, 0),
7523 WC_FUNCDEF => execfuncdef(state, None),
7524 WC_TIMED => exectime(state, 0),
7525 WC_COND => execcond(state, 0),
7526 WC_ARITH => execarith(state, 0),
7527 WC_TRY => exectry(state, 0),
7528 _ => {
7529 state.pc += 1;
7530 0
7531 }
7532 }
7533 } else {
7534 0
7535 };
7536 last_status = s;
7537 // c:1885-1893 — last pipe stage check.
7538 if WC_PIPE_TYPE(code) == WC_PIPE_END {
7539 break;
7540 }
7541 // c:1897-1900 — fetch next WC_PIPE header for the next stage.
7542 if state.pc >= state.prog.prog.len() {
7543 break;
7544 }
7545 let next_code = state.prog.prog[state.pc];
7546 if wc_code(next_code) != WC_PIPE {
7547 break;
7548 }
7549 state.pc += 1;
7550 code = next_code;
7551 // Multi-stage pipe() + fork() per cmd is now ported via
7552 // `execpline2` (c:1991-2040). Callers wanting full pipeline
7553 // isolation route through that path; this inline dispatch
7554 // serves the single-process simple-command tree-walker used
7555 // by the fusevm bytecode shim, which does its own
7556 // pipe/fork via `OpPipeCreate`/`OpFork` ops.
7557 }
7558 LASTVAL.store(last_status, Ordering::Relaxed);
7559 last_status
7560}
7561
7562// `execcmd_exec`'s wordcode dispatch tail from Src/exec.c:2901-3700 is
7563// inlined at every call site (execsimple, execpline) as the match
7564// expression that selects the right execX function. There's no
7565// separate Rust fn for it because:
7566// - The arg-side `execcmd_exec(args, type_)` at exec.rs:795 already
7567// occupies the canonical name (handling precommand modifiers).
7568// - The C dispatch tail is conceptually `execfuncs[code - WC_CURSH]`,
7569// a table lookup at exec.c:5499 — not a separate function.
7570#[cfg(any())]
7571mod _execcmd_tail_doc_anchor {
7572 // c:2901-3700 — see inlined match in execpline + execsimple above.
7573 // c:5499 — execfuncs[] table inlined as the same match.
7574}
7575
7576// --- loop.c entries ---------------------------------------------------
7577
7578/// Port of `execfor(Estate state, int do_exec)` from `Src/loop.c:50-202`.
7579/// `for var in args; do body; done` and the C-style `for ((init;cond;adv))`
7580/// variant. WC_FOR_TYPE distinguishes PPARAM (use $@) / LIST (explicit
7581/// words) / COND (C-style).
7582pub fn execfor(state: &mut estate, do_exec: i32) -> i32 {
7583 use crate::ported::zsh_h::Z_END;
7584 let code = state.prog.prog[state.pc.wrapping_sub(1)]; // c:54
7585 let iscond = WC_FOR_TYPE(code) == WC_FOR_COND; // c:55
7586 let mut last_iter = false; // c:57 — `int last = 0;`
7587 let mut val: i64 = 0; // c:59
7588 let mut vars: Vec<String> = Vec::new();
7589 let mut args: Vec<String> = Vec::new();
7590 let mut cond_expr: String = String::new();
7591 let mut advance_expr: String = String::new();
7592 let old_simple_pline = simple_pline.swap(1, Ordering::Relaxed); // c:62-63
7593 let end_pc = state.pc + WC_FOR_SKIP(code) as usize; // c:65
7594 let mut ctok = 0i32;
7595 let mut atok = 0i32;
7596 if iscond {
7597 // c:68-82 — C-style for: init expr at top, then cond/advance.
7598 let init = ecgetstr(state, EC_NODUP, None); // c:68
7599 let init_sub = singsub(&init); // c:69
7600 if isset(XTRACE) {
7601 // c:70-75
7602 let init_show = untokenize(&init_sub);
7603 printprompt4();
7604 eprintln!("{}", init_show);
7605 }
7606 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
7607 let _ = wc_matheval(&init_sub); // c:77 — `matheval(str);`
7608 }
7609 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
7610 // c:79-82
7611 state.pc = end_pc;
7612 simple_pline.store(old_simple_pline, Ordering::Relaxed);
7613 return 1;
7614 }
7615 cond_expr = ecgetstr(state, EC_NODUP, Some(&mut ctok)); // c:83
7616 advance_expr = ecgetstr(state, EC_NODUP, Some(&mut atok)); // c:84
7617 } else {
7618 // c:86 — `vars = ecgetlist(state, *state->pc++, EC_NODUP, NULL);`
7619 let count = state.prog.prog[state.pc] as usize;
7620 state.pc += 1;
7621 vars = ecgetlist(state, count, EC_NODUP, None);
7622 if WC_FOR_TYPE(code) == WC_FOR_LIST {
7623 // c:88-100 — explicit `for var in words`
7624 let mut htok = 0i32;
7625 let arg_count = state.prog.prog[state.pc] as usize;
7626 state.pc += 1;
7627 args = ecgetlist(state, arg_count, EC_DUPTOK, Some(&mut htok));
7628 if args.is_empty() {
7629 state.pc = end_pc;
7630 simple_pline.store(old_simple_pline, Ordering::Relaxed);
7631 return 0;
7632 }
7633 if htok != 0 {
7634 execsubst(&mut args); // c:96
7635 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
7636 state.pc = end_pc;
7637 simple_pline.store(old_simple_pline, Ordering::Relaxed);
7638 return 1;
7639 }
7640 }
7641 } else {
7642 // c:102-107 — implicit `for var` (no `in` clause) uses
7643 // the positional params $@ from PPARAMS (params.rs Mutex).
7644 args = crate::ported::builtin::PPARAMS
7645 .lock()
7646 .map(|p| p.clone())
7647 .unwrap_or_default();
7648 }
7649 }
7650 // c:111-112 — empty args ⇒ lastval = 0.
7651 if !iscond && args.is_empty() {
7652 LASTVAL.store(0, Ordering::Relaxed);
7653 }
7654 LOOPS.fetch_add(1, Ordering::SeqCst); // c:114 — `loops++;`
7655 pushheap(); // c:115
7656 cmdpush(CS_FOR as u8); // c:116
7657 let loop_pc = state.pc; // c:117
7658 let mut args_iter = args.into_iter();
7659 while !last_iter {
7660 if iscond {
7661 // c:119-138 — eval cond expression.
7662 let mut cs = cond_expr.clone();
7663 if ctok != 0 {
7664 cs = singsub(&cs);
7665 }
7666 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
7667 let trimmed = cs.trim_start();
7668 if !trimmed.is_empty() {
7669 if isset(XTRACE) {
7670 printprompt4();
7671 eprintln!("{}", trimmed);
7672 }
7673 val = wc_mathevali(trimmed).unwrap_or(0);
7674 } else {
7675 val = 1;
7676 }
7677 }
7678 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
7679 if BREAKS.load(Ordering::SeqCst) > 0 {
7680 BREAKS.fetch_sub(1, Ordering::SeqCst);
7681 }
7682 LASTVAL.store(1, Ordering::Relaxed);
7683 break;
7684 }
7685 if val == 0 {
7686 break;
7687 }
7688 } else {
7689 // c:140-162 — for var binding from args.
7690 let mut count = 0;
7691 for name in &vars {
7692 let value = match args_iter.next() {
7693 Some(v) => v,
7694 None => {
7695 if count != 0 {
7696 last_iter = true;
7697 String::new()
7698 } else {
7699 break;
7700 }
7701 }
7702 };
7703 if isset(XTRACE) {
7704 printprompt4();
7705 eprintln!("{}={}", name, value);
7706 }
7707 setloopvar(name, &value);
7708 count += 1;
7709 }
7710 if count == 0 {
7711 break;
7712 }
7713 }
7714 state.pc = loop_pc; // c:163
7715 let _do_exec_now = do_exec != 0 && !args_iter.clone().any(|_| true); // c:164 — `do_exec && args && empty(args)`
7716 let _ = execlist(state, 1, if _do_exec_now { 1 } else { 0 });
7717 // c:166-169 — breaks/continue handling.
7718 if BREAKS.load(Ordering::SeqCst) > 0 {
7719 let prev = BREAKS.fetch_sub(1, Ordering::SeqCst);
7720 if prev - 1 > 0 || CONTFLAG.load(Ordering::SeqCst) == 0 {
7721 break;
7722 }
7723 CONTFLAG.store(0, Ordering::SeqCst);
7724 }
7725 if RETFLAG.load(Ordering::SeqCst) != 0 {
7726 break;
7727 }
7728 // c:170-178 — C-style advance step.
7729 if iscond && (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
7730 let mut adv = advance_expr.clone();
7731 if atok != 0 {
7732 adv = singsub(&adv);
7733 }
7734 if isset(XTRACE) {
7735 printprompt4();
7736 eprintln!("{}", adv);
7737 }
7738 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
7739 let _ = wc_matheval(&adv);
7740 }
7741 }
7742 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
7743 if BREAKS.load(Ordering::SeqCst) > 0 {
7744 BREAKS.fetch_sub(1, Ordering::SeqCst);
7745 }
7746 LASTVAL.store(1, Ordering::Relaxed);
7747 break;
7748 }
7749 freeheap(); // c:184
7750 }
7751 popheap(); // c:186
7752 cmdpop(); // c:187
7753 LOOPS.fetch_sub(1, Ordering::SeqCst); // c:188
7754 simple_pline.store(old_simple_pline, Ordering::Relaxed);
7755 state.pc = end_pc;
7756 this_noerrexit.store(1, Ordering::Relaxed);
7757 let _ = Z_END;
7758 LASTVAL.load(Ordering::Relaxed)
7759}
7760
7761/// Port of `execselect(Estate state, UNUSED(int do_exec))` from
7762/// `Src/loop.c:217-410`. `select var in words; do body; done` REPL.
7763pub fn execselect(state: &mut estate, _do_exec: i32) -> i32 {
7764 // The full select body manages a REPL prompt, terminal columns,
7765 // selectlist redraw, etc. The `selectlist` helper at loop.rs:130
7766 // already ports c:347 (menu display). Structural execselect:
7767 // c:225-410 — read vars + words like execfor, then loop on stdin
7768 // input prompting via PROMPT3, set var=word, run body.
7769 let code = state.prog.prog[state.pc.wrapping_sub(1)];
7770 let end_pc = state.pc + WC_FOR_SKIP(code) as usize;
7771 // c:228-237 — read var name + words. Skip body and use existing
7772 // bridge handler at BUILTIN_RUN_SELECT for actual REPL until full
7773 // wordcode driver lands.
7774 state.pc = end_pc;
7775 this_noerrexit.store(1, Ordering::Relaxed);
7776 LASTVAL.load(Ordering::Relaxed)
7777}
7778
7779/// Port of `execwhile(Estate state, UNUSED(int do_exec))` from
7780/// `Src/loop.c:413-498`. `while/until cond; do body; done`.
7781pub fn execwhile(state: &mut estate, _do_exec: i32) -> i32 {
7782 let code = state.prog.prog[state.pc.wrapping_sub(1)]; // c:417
7783 let isuntil = WC_WHILE_TYPE(code) == WC_WHILE_UNTIL; // c:419
7784 let end_pc = state.pc + WC_WHILE_SKIP(code) as usize; // c:422
7785 let olderrexit = noerrexit.load(Ordering::Relaxed); // c:423
7786 let mut oldval: i32 = 0; // c:424
7787 pushheap(); // c:425
7788 cmdpush(if isuntil {
7789 CS_UNTIL as u8
7790 } else {
7791 CS_WHILE as u8
7792 }); // c:426
7793 LOOPS.fetch_add(1, Ordering::SeqCst); // c:427
7794 let loop_pc = state.pc; // c:428
7795 let old_simple_pline = simple_pline.load(Ordering::Relaxed); // c:419
7796 // c:430-456 — empty-loop fast path. If loop body is two WC_ENDs,
7797 // sit in a tight signal-wait loop until ^C breaks us.
7798 if state.prog.prog.get(loop_pc) == Some(&WC_END)
7799 && state.prog.prog.get(loop_pc + 1) == Some(&WC_END)
7800 {
7801 simple_pline.store(1, Ordering::Relaxed);
7802 // c:438-439 — spin until breaks.
7803 while BREAKS.load(Ordering::SeqCst) == 0 {
7804 std::thread::yield_now();
7805 }
7806 BREAKS.fetch_sub(1, Ordering::SeqCst);
7807 simple_pline.store(old_simple_pline, Ordering::Relaxed);
7808 } else {
7809 // c:441-485 — normal loop.
7810 loop {
7811 state.pc = loop_pc; // c:442
7812 noerrexit.fetch_or(NOERREXIT_EXIT | NOERREXIT_RETURN, Ordering::Relaxed); // c:443
7813 simple_pline.store(1, Ordering::Relaxed); // c:446
7814 let _ = execlist(state, 1, 0); // c:448 — exec cond.
7815 simple_pline.store(old_simple_pline, Ordering::Relaxed);
7816 noerrexit.store(olderrexit, Ordering::Relaxed); // c:451
7817 let cond_status = LASTVAL.load(Ordering::Relaxed); // c:452
7818 // c:453-460 — `if (!((lastval == 0) ^ isuntil)) break;`
7819 let cond_passed = (cond_status == 0) ^ isuntil;
7820 if !cond_passed {
7821 if BREAKS.load(Ordering::SeqCst) > 0 {
7822 BREAKS.fetch_sub(1, Ordering::SeqCst);
7823 }
7824 if RETFLAG.load(Ordering::SeqCst) == 0 {
7825 LASTVAL.store(oldval, Ordering::Relaxed);
7826 }
7827 break;
7828 }
7829 if RETFLAG.load(Ordering::SeqCst) != 0 {
7830 // c:461
7831 if BREAKS.load(Ordering::SeqCst) > 0 {
7832 BREAKS.fetch_sub(1, Ordering::SeqCst);
7833 }
7834 break;
7835 }
7836 simple_pline.store(1, Ordering::Relaxed); // c:468
7837 let _ = execlist(state, 1, 0); // c:470 — exec body.
7838 simple_pline.store(old_simple_pline, Ordering::Relaxed);
7839 // c:472-477 — breaks/continue handling.
7840 if BREAKS.load(Ordering::SeqCst) > 0 {
7841 let prev = BREAKS.fetch_sub(1, Ordering::SeqCst);
7842 if prev - 1 > 0 || CONTFLAG.load(Ordering::SeqCst) == 0 {
7843 break;
7844 }
7845 CONTFLAG.store(0, Ordering::SeqCst);
7846 }
7847 // c:478-481 — errflag bail.
7848 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
7849 LASTVAL.store(1, Ordering::Relaxed);
7850 break;
7851 }
7852 // c:482-483 — retflag bail.
7853 if RETFLAG.load(Ordering::SeqCst) != 0 {
7854 break;
7855 }
7856 freeheap(); // c:484
7857 oldval = LASTVAL.load(Ordering::Relaxed); // c:485
7858 }
7859 }
7860 cmdpop(); // c:489
7861 popheap(); // c:490
7862 LOOPS.fetch_sub(1, Ordering::SeqCst); // c:491
7863 state.pc = end_pc; // c:492
7864 this_noerrexit.store(1, Ordering::Relaxed); // c:493
7865 LASTVAL.load(Ordering::Relaxed)
7866}
7867
7868/// Port of `execrepeat(Estate state, UNUSED(int do_exec))` from
7869/// `Src/loop.c:499-551`. `repeat N; do body; done`.
7870pub fn execrepeat(state: &mut estate, _do_exec: i32) -> i32 {
7871 let code = state.prog.prog[state.pc.wrapping_sub(1)]; // c:503
7872 let old_simple_pline = simple_pline.swap(1, Ordering::Relaxed); // c:507
7873 let end_pc = state.pc + WC_REPEAT_SKIP(code) as usize; // c:510
7874 let mut htok = 0i32;
7875 let mut tmp = ecgetstr(state, EC_DUPTOK, Some(&mut htok)); // c:512
7876 if htok != 0 {
7877 tmp = singsub(&tmp); // c:514
7878 tmp = untokenize(&tmp); // c:515
7879 }
7880 let count = wc_mathevali(&tmp).unwrap_or(0); // c:517
7881 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
7882 simple_pline.store(old_simple_pline, Ordering::Relaxed);
7883 return 1;
7884 }
7885 LASTVAL.store(0, Ordering::Relaxed); // c:520
7886 pushheap(); // c:521
7887 cmdpush(CS_REPEAT as u8); // c:522
7888 LOOPS.fetch_add(1, Ordering::SeqCst); // c:523
7889 let loop_pc = state.pc; // c:524
7890 let mut remaining = count;
7891 while remaining > 0 {
7892 // c:525
7893 remaining -= 1;
7894 state.pc = loop_pc;
7895 let _ = execlist(state, 1, 0); // c:527
7896 freeheap(); // c:528
7897 // c:529-534 — breaks/continue handling.
7898 if BREAKS.load(Ordering::SeqCst) > 0 {
7899 let prev = BREAKS.fetch_sub(1, Ordering::SeqCst);
7900 if prev - 1 > 0 || CONTFLAG.load(Ordering::SeqCst) == 0 {
7901 break;
7902 }
7903 CONTFLAG.store(0, Ordering::SeqCst);
7904 }
7905 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
7906 // c:536-538
7907 LASTVAL.store(1, Ordering::Relaxed);
7908 break;
7909 }
7910 if RETFLAG.load(Ordering::SeqCst) != 0 {
7911 // c:540
7912 break;
7913 }
7914 }
7915 cmdpop(); // c:544
7916 popheap(); // c:545
7917 LOOPS.fetch_sub(1, Ordering::SeqCst); // c:546
7918 simple_pline.store(old_simple_pline, Ordering::Relaxed);
7919 state.pc = end_pc; // c:548
7920 this_noerrexit.store(1, Ordering::Relaxed); // c:549
7921 LASTVAL.load(Ordering::Relaxed)
7922}
7923
7924/// Port of `execif(Estate state, int do_exec)` from `Src/loop.c:553-598`.
7925/// `if cond; then body; elif ...; else ...; fi`.
7926pub fn execif(state: &mut estate, do_exec: i32) -> i32 {
7927 let code0 = state.prog.prog[state.pc.wrapping_sub(1)]; // c:558
7928 let olderrexit = noerrexit.load(Ordering::Relaxed); // c:559
7929 let end_pc = state.pc + WC_IF_SKIP(code0) as usize; // c:560
7930 noerrexit.fetch_or(NOERREXIT_EXIT | NOERREXIT_RETURN, Ordering::Relaxed); // c:562
7931 let mut s = 0i32; // c:557 — `s = 0`
7932 let mut run = 0i32; // c:557 — `run = 0`
7933 while state.pc < end_pc {
7934 // c:563
7935 let code = state.prog.prog[state.pc];
7936 state.pc += 1;
7937 // c:565-571 — non-IF, or IF_ELSE: break out.
7938 if wc_code(code) != WC_IF || WC_IF_TYPE(code) == WC_IF_ELSE {
7939 run = if wc_code(code) == WC_IF && WC_IF_TYPE(code) == WC_IF_ELSE {
7940 2
7941 } else {
7942 1
7943 };
7944 if run == 1 {
7945 state.pc -= 1; // back up onto the body header
7946 }
7947 break;
7948 }
7949 let next_pc = state.pc + WC_IF_SKIP(code) as usize; // c:572
7950 cmdpush(if s != 0 { CS_ELIF as u8 } else { CS_IF as u8 }); // c:573
7951 let _ = execlist(state, 1, 0); // c:574
7952 cmdpop(); // c:575
7953 // c:576-579 — selected branch: lastval == 0.
7954 if LASTVAL.load(Ordering::Relaxed) == 0 {
7955 run = 1;
7956 break;
7957 }
7958 if RETFLAG.load(Ordering::SeqCst) != 0 {
7959 // c:580
7960 break;
7961 }
7962 s = 1;
7963 state.pc = next_pc;
7964 }
7965 noerrexit.store(olderrexit, Ordering::Relaxed); // c:584
7966 // c:585-591 — run selected branch.
7967 if run != 0 {
7968 cmdpush(if run == 2 {
7969 CS_ELSE as u8
7970 } else if s != 0 {
7971 CS_ELIFTHEN as u8
7972 } else {
7973 CS_IFTHEN as u8
7974 });
7975 let _ = execlist(state, 1, do_exec);
7976 cmdpop();
7977 } else if RETFLAG.load(Ordering::SeqCst) == 0
7978 && (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0
7979 {
7980 LASTVAL.store(0, Ordering::Relaxed); // c:592
7981 }
7982 state.pc = end_pc; // c:594
7983 this_noerrexit.store(1, Ordering::Relaxed); // c:595
7984 LASTVAL.load(Ordering::Relaxed)
7985}
7986
7987/// Port of `execcase(Estate state, int do_exec)` from `Src/loop.c:600-733`.
7988/// `case word in pat) body ;; ... esac` with `;;`/`;&`/`;|` separators.
7989pub fn execcase(state: &mut estate, do_exec: i32) -> i32 {
7990 let code0 = state.prog.prog[state.pc.wrapping_sub(1)]; // c:603
7991 let end_pc = state.pc + WC_CASE_SKIP(code0) as usize; // c:607
7992 // c:609-611 — read & expand the case-word.
7993 let raw_word = ecgetstr(state, EC_DUP, None);
7994 let word_sub = singsub(&raw_word);
7995 let word = untokenize(&word_sub);
7996 let mut anypatok = false; // c:613
7997 cmdpush(CS_CASE as u8); // c:615
7998 let mut code = 0u32;
7999 while state.pc < end_pc {
8000 // c:616
8001 code = state.prog.prog[state.pc];
8002 state.pc += 1;
8003 if wc_code(code) != WC_CASE {
8004 break;
8005 }
8006 let next_pc = state.pc + WC_CASE_SKIP(code) as usize; // c:621
8007 let nalts = state.prog.prog[state.pc] as i32; // c:622
8008 state.pc += 1;
8009 let mut patok = false;
8010 let mut nalts_remaining = nalts;
8011 while !patok && nalts_remaining > 0 {
8012 // c:629-672 — try each alternative pattern.
8013 // c:631-633 — `npat = state->pc[1]; spprog = state->prog->pats + npat;`
8014 // zshrs's pat-compile-on-demand path: extract raw pat text + try patcompile/pattry.
8015 queue_signals(); // c:636
8016 let mut htok = 0i32;
8017 let pat_raw = ecrawstr(&state.prog, state.pc, Some(&mut htok));
8018 let pat = if htok != 0 {
8019 singsub(&pat_raw)
8020 } else {
8021 pat_raw
8022 };
8023 if let Some(pprog) = patcompile(&pat, PAT_STATIC, None) {
8024 // c:660 — `if (pprog && pattry(pprog, word)) patok = anypatok = 1;`
8025 if pattry(&pprog, &word) {
8026 patok = true;
8027 anypatok = true;
8028 }
8029 } else {
8030 zerr(&format!("bad pattern: {}", pat)); // c:657
8031 }
8032 state.pc += 2; // c:664 — `state->pc += 2;`
8033 nalts_remaining -= 1;
8034 unqueue_signals(); // c:666
8035 }
8036 state.pc += (2 * nalts_remaining) as usize; // c:668
8037 if patok {
8038 // c:672-684 — run selected arm body.
8039 let _ = execlist(
8040 state,
8041 1,
8042 ((WC_CASE_TYPE(code) == WC_CASE_OR) as i32) & do_exec,
8043 );
8044 // c:675-682 — chain into ;& and ;| siblings.
8045 while RETFLAG.load(Ordering::SeqCst) == 0
8046 && wc_code(code) == WC_CASE
8047 && WC_CASE_TYPE(code) == WC_CASE_AND
8048 && state.pc < end_pc
8049 {
8050 state.pc = next_pc;
8051 code = state.prog.prog[state.pc];
8052 state.pc += 1;
8053 let inner_next = state.pc + WC_CASE_SKIP(code) as usize;
8054 let inner_nalts = state.prog.prog[state.pc] as usize;
8055 state.pc += 1 + 2 * inner_nalts;
8056 let _ = execlist(
8057 state,
8058 1,
8059 ((WC_CASE_TYPE(code) == WC_CASE_OR) as i32) & do_exec,
8060 );
8061 let _ = inner_next;
8062 }
8063 if WC_CASE_TYPE(code) != WC_CASE_TESTAND {
8064 break;
8065 }
8066 }
8067 state.pc = next_pc; // c:687
8068 }
8069 cmdpop(); // c:691
8070 state.pc = end_pc; // c:693
8071 if !anypatok {
8072 // c:695-696
8073 LASTVAL.store(0, Ordering::Relaxed);
8074 }
8075 this_noerrexit.store(1, Ordering::Relaxed); // c:697
8076 LASTVAL.load(Ordering::Relaxed)
8077}
8078
8079/// Port of `exectry(Estate state, int do_exec)` from `Src/loop.c:735-798`.
8080/// `{ try } always { finally }`: capture errflag/retflag/breaks/contflag
8081/// from the try-clause, reset them around the always-clause, then
8082/// restore if always-clause didn't override.
8083pub fn exectry(state: &mut estate, _do_exec: i32) -> i32 {
8084 let header = state.prog.prog[state.pc.wrapping_sub(1)]; // c:741
8085 let end_pc = state.pc + WC_TRY_SKIP(header) as usize; // c:742
8086 let try_inner = state.prog.prog[state.pc]; // c:743
8087 let always_pc = state.pc + 1 + WC_TRY_SKIP(try_inner) as usize; // c:743
8088 state.pc += 1; // c:744
8089 pushheap(); // c:745
8090 cmdpush(CS_CURSH as u8); // c:746
8091 try_tryflag.fetch_add(1, Ordering::SeqCst); // c:749
8092 let _ = execlist(state, 1, 0); // c:750
8093 try_tryflag.fetch_sub(1, Ordering::SeqCst); // c:751
8094 let try_status = LASTVAL.load(Ordering::Relaxed);
8095 let endval = if try_status != 0 {
8096 // c:754
8097 try_status
8098 } else {
8099 (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) as i32
8100 };
8101 freeheap(); // c:756
8102 cmdpop(); // c:758
8103 cmdpush(CS_ALWAYS as u8); // c:759
8104 // c:762-763 — save try_errflag / try_interrupt.
8105 let saved_err = errflag.load(Ordering::Relaxed);
8106 let save_try_err = (saved_err & ERRFLAG_ERROR) != 0;
8107 let save_try_int = (saved_err & ERRFLAG_INT) != 0;
8108 // c:768 — `errflag = 0;` (clear both bits).
8109 errflag.fetch_and(!(ERRFLAG_ERROR | ERRFLAG_INT), Ordering::Relaxed);
8110 // c:769-774 — save retflag/breaks/contflag.
8111 let save_retflag = RETFLAG.swap(0, Ordering::SeqCst);
8112 let save_breaks = BREAKS.swap(0, Ordering::SeqCst);
8113 let save_contflag = CONTFLAG.swap(0, Ordering::SeqCst);
8114 state.pc = always_pc; // c:776
8115 let _ = execlist(state, 1, 0); // c:777
8116 // c:779-786 — restore errflag bits.
8117 if save_try_err {
8118 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
8119 } else {
8120 errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed);
8121 }
8122 if save_try_int {
8123 errflag.fetch_or(ERRFLAG_INT, Ordering::Relaxed);
8124 } else {
8125 errflag.fetch_and(!ERRFLAG_INT, Ordering::Relaxed);
8126 }
8127 // c:789-794 — re-arm retflag/breaks/contflag only if always didn't override.
8128 if RETFLAG.load(Ordering::SeqCst) == 0 {
8129 RETFLAG.store(save_retflag, Ordering::SeqCst);
8130 }
8131 if BREAKS.load(Ordering::SeqCst) == 0 {
8132 BREAKS.store(save_breaks, Ordering::SeqCst);
8133 }
8134 if CONTFLAG.load(Ordering::SeqCst) == 0 {
8135 CONTFLAG.store(save_contflag, Ordering::SeqCst);
8136 }
8137 cmdpop(); // c:796
8138 popheap(); // c:797
8139 state.pc = end_pc; // c:798
8140 this_noerrexit.store(1, Ordering::Relaxed); // c:799
8141 endval
8142}
8143
8144/// Port of `execcmd_exec(Estate state, Execcmd_params eparams,
8145/// int input, int output, int how, int last1, int close_if_forked)`
8146/// from `Src/exec.c:2900-4404`. Execute a command at the lowest
8147/// level of the hierarchy.
8148///
8149/// Line-by-line port of the full 1500-line C body. Sections:
8150/// c:2904-2916 — locals
8151/// c:2917-2924 — eparams field unpacking
8152/// c:2934-2939 — Z_TIMED + doneps4 reset
8153/// c:2945-2960 — old_lastval + use_cmdoutval + `save[]`/`mfds[]` init
8154/// c:2962-2986 — %job head rewrite + AUTORESUME prefix match
8155/// c:2988-3011 — Z_ASYNC / pipeline-not-last / sh-emulation fork-immediately
8156/// c:3013-3283 — precommand-modifier walk (BINF_PREFIX strip)
8157/// + BINF_COMMAND (-p/-v/-V) + BINF_EXEC (-a/-c/-l)
8158/// c:3285-3307 — prefork substitutions + magic_assign
8159/// c:3309-3406 — empty-command branch (redir / nullexec / BINF_COMMAND)
8160/// c:3409-3466 — main resolution loop (shfunc / builtin / autocd)
8161/// c:3468-3479 — errflag bail-out
8162/// c:3480-3492 — text fetch + setunderscore
8163/// c:3494-3524 — rm * safety prompt
8164/// c:3526-3591 — type-specific dispatch prep (WC_FUNCDEF / is_shfunc / WC_AUTOFN)
8165/// c:3593-3632 — external resolution (cmdnamtab, hashcmd, AUTOCD)
8166/// c:3634-3697 — fork decision
8167/// c:3700-3955 — redir loop + multio + addfd + xpandredir
8168/// c:3957-3961 — multio close (`mfds[i].ct >= 2` → closemn)
8169/// c:3963-3995 — nullexec branch
8170/// c:3996-4327 — main dispatch (entersubsh + execfuncdef / `execcurshtable[]` /
8171/// execbuiltin / execshfunc / execute)
8172/// c:4330-4365 — `err:` label: forked-child fd cleanup, fixfds
8173/// c:4366-4403 — `done:` label: POSIX special-builtin error escalation,
8174/// shelltime stop, newxtrerr close, AUTOCONTINUE restore
8175///
8176/// **Substrate stubs (declared inside this fn citing home C file):**
8177/// - `save_params(state, varspc, restorelist, removelist)` → Src/exec.c:4409
8178/// - `restore_params(restorelist, removelist)` → Src/exec.c:4463
8179/// - `isreallycom(cn)` → Src/exec.c:2670
8180/// - `execerr()` → Src/exec.c:2700 (label-style; converts to errflag set + goto-equivalent)
8181/// - `execautofn_basic(state, do_exec)` → Src/exec.c:5050
8182/// - `ensurefeature(modname, "b:", ...)` → Src/module.c:1654
8183///
8184/// **NOT routed through fusevm.** This canonical port targets the
8185/// tree-walker dispatcher; the fusevm bytecode VM uses
8186/// `execcmd_compile_head` + `compile_simple` instead. No call
8187/// site yet — the port closes the substrate gap so future
8188/// wordcode-walker code can use it.
8189#[allow(non_snake_case)]
8190#[allow(clippy::too_many_arguments)]
8191#[allow(clippy::redundant_field_names)]
8192#[allow(unused_assignments)]
8193#[allow(unused_variables)]
8194#[allow(unused_mut)]
8195#[allow(unused_imports)]
8196#[allow(unreachable_code)]
8197#[allow(dead_code)]
8198pub fn execcmd_exec(
8199 state: &mut estate,
8200 eparams: &mut crate::ported::zsh_h::execcmd_params,
8201 input: i32,
8202 output: i32,
8203 mut how: i32,
8204 mut last1: i32,
8205 close_if_forked: i32,
8206) {
8207 use crate::ported::zsh_h::{
8208 Star, ASG_ARRAY, ASG_KEY_VALUE, AUTOCD, AUTOCONTINUE, AUTORESUME, BGNICE,
8209 BINF_ASSIGN as BINF_ASSIGN_FLAG, BINF_BUILTIN, BINF_COMMAND, BINF_EXEC, BINF_MAGICEQUALS,
8210 BINF_NOGLOB, BINF_PREFIX, BINF_PSPECIAL, CSHNULLCMD, ERRFLAG_INT, EXECOPT, FDT_EXTERNAL,
8211 FDT_INTERNAL, FDT_TYPE_MASK, FDT_UNUSED, FDT_XTRACE, HASHCMDS, HFILE_USE_OPTIONS,
8212 IS_APPEND_REDIR, IS_DASH, IS_ERROR_REDIR, MAGICEQUALSUBST, NOTIFY, PM_READONLY, PM_SPECIAL,
8213 POSIXBUILTINS, PREFORK_ASSIGN, PREFORK_KEY_VALUE, PREFORK_SINGLE, PREFORK_TYPESET,
8214 PRINTEXITVALUE, RCS, REDIR_CLOSE, REDIR_HERESTR, REDIR_INPIPE, REDIR_MERGEIN,
8215 REDIR_MERGEOUT, REDIR_OUTPIPE, REDIR_READ, REDIR_READWRITE, RMSTARSILENT, SHINSTDIN,
8216 SHNULLCMD, STAT_BUILTIN, STAT_CURSH, STAT_DONE, STAT_NOPRINT, WC_ASSIGN as ZWC_ASSIGN,
8217 WC_ASSIGN_INC as ZWC_ASSIGN_INC, WC_ASSIGN_NUM as ZWC_ASSIGN_NUM,
8218 WC_ASSIGN_SCALAR as ZWC_ASSIGN_SCALAR, WC_ASSIGN_TYPE as ZWC_ASSIGN_TYPE,
8219 WC_ASSIGN_TYPE2 as ZWC_ASSIGN_TYPE2, WC_AUTOFN, WC_CURSH, WC_FUNCDEF, WC_REDIR, WC_SIMPLE,
8220 WC_SUBSH, WC_TIMED, WC_TYPESET, XTRACE, Z_ASYNC, Z_DISOWN, Z_SYNC, Z_TIMED,
8221 };
8222
8223 // c:2900
8224
8225 // c:2904-2916 — locals.
8226 let mut hn: Option<*mut builtin> = None; // c:2904 HashNode hn = NULL
8227 let mut filelist: Vec<String> = Vec::new(); // c:2905 LinkList filelist = NULL
8228 // c:2906 LinkNode node; (loop locals)
8229 // c:2907 Redir fn; (loop locals)
8230 let mut mfds: [Option<Box<multio>>; 10] = // c:2908 struct multio *mfds[10]
8231 [None, None, None, None, None, None, None, None, None, None];
8232 let mut text: Option<String> = None; // c:2909 char *text
8233 let mut save: [i32; 10] = [-2; 10]; // c:2910 int save[10]
8234 let mut fil: i32; // c:2911 int fil
8235 let mut dfil: i32 = 0; // c:2911 int dfil
8236 let mut is_cursh: i32 = 0; // c:2911 int is_cursh = 0
8237 let mut do_exec: i32 = 0; // c:2911 int do_exec = 0
8238 let mut redir_err: i32 = 0; // c:2911 int redir_err = 0
8239 let mut i: i32; // c:2911 int i
8240 let mut nullexec: i32 = 0; // c:2912 int nullexec = 0
8241 let mut magic_assign: i32 = 0; // c:2912 int magic_assign = 0
8242 let mut forked: i32 = 0; // c:2912 int forked = 0
8243 let mut old_lastval: i32; // c:2912 int old_lastval
8244 let mut is_shfunc: i32 = 0; // c:2913 int is_shfunc = 0
8245 let mut is_builtin: i32 = 0; // c:2913 int is_builtin = 0
8246 let mut is_exec: i32 = 0; // c:2913 int is_exec = 0
8247 let mut use_defpath: i32 = 0; // c:2913 int use_defpath = 0
8248 // c:2914 — `Various flags to the command.`
8249 let mut cflags: u32 = 0; // c:2915 int cflags = 0
8250 let mut orig_cflags: u32 = 0; // c:2915 int orig_cflags = 0
8251 let mut checked: i32 = 0; // c:2915 int checked = 0
8252 let mut oautocont: i32 = -1; // c:2915 int oautocont = -1
8253 // c:2916 — `FILE *oxtrerr = xtrerr, *newxtrerr = NULL;` — xtrerr
8254 // accessor is stub; track newxtrerr state via Option<RawFd>.
8255 let mut newxtrerr: Option<i32> = None; // c:2916
8256
8257 // c:2917-2924 — eparams field unpacking. `args` / `redir` are
8258 // pulled into mutable locals so the body can mutate them
8259 // independently of the eparams struct.
8260 let mut args: Option<Vec<String>> = eparams.args.take(); // c:2921 LinkList args
8261 let mut redir: Option<Vec<redir>> = eparams.redir.take(); // c:2922 LinkList redir
8262 let varspc: Option<usize> = eparams.varspc; // c:2923 Wordcode varspc
8263 let typ: i32 = eparams.typ; // c:2924 int type
8264 // c:2925-2929 — `preargs comes from expanding the head of the args
8265 // list in order to check for prefix commands.` declared later.
8266
8267 // c:2933-2937 — `for the "time" keyword` — child_times_t shti, chti
8268 // + struct timespec then. Rust port keeps the names so the shelltime
8269 // start+stop calls map directly. Use jobs.rs's existing types.
8270 let mut shti = crate::ported::jobs::timeinfo::default(); // c:2934
8271 let mut chti = crate::ported::jobs::timeinfo::default(); // c:2934
8272 let mut then_ts = std::time::Instant::now(); // c:2935 struct timespec then
8273 if (how & Z_TIMED as i32) != 0 {
8274 // c:2936
8275 crate::ported::jobs::shelltime(Some(&mut shti), Some(&mut chti), Some(&mut then_ts), 0);
8276 // c:2937
8277 }
8278
8279 doneps4.store(0, Ordering::Relaxed); // c:2939
8280
8281 // c:2941-2947 — `If assignment but no command get the status from
8282 // variable assignment.`
8283 old_lastval = LASTVAL.load(Ordering::Relaxed); // c:2945
8284 if args.is_none() && varspc.is_some() {
8285 // c:2946
8286 let ef = errflag.load(Ordering::Relaxed);
8287 LASTVAL.store(
8288 if ef != 0 {
8289 ef
8290 } else {
8291 cmdoutval.load(Ordering::Relaxed)
8292 },
8293 Ordering::Relaxed,
8294 ); // c:2947
8295 }
8296 // c:2948-2954 — `If there are arguments, we should reset the status
8297 // for the command before execution---unless we are using the result
8298 // of a command substitution...`
8299 use_cmdoutval.store(if args.is_none() { 1 } else { 0 }, Ordering::Relaxed); // c:2955
8300
8301 // c:2957-2960 — `for (i = 0; i < 10; i++) { save[i] = -2; mfds[i] = NULL; }`
8302 // Already initialised above via array literals; preserved as
8303 // comment for parity. The C loop maps to a no-op in Rust.
8304
8305 // c:2962-2973 — `%job` head rewrite.
8306 if (typ == WC_SIMPLE as i32 || typ == WC_TYPESET as i32)
8307 && args.as_ref().map(|v| !v.is_empty()).unwrap_or(false)
8308 && args.as_ref().unwrap()[0].starts_with('%')
8309 {
8310 // c:2964-2965
8311 if (how & Z_DISOWN as i32) != 0 {
8312 // c:2966
8313 oautocont = if crate::ported::options::opt_state_get("autocontinue").unwrap_or(false) {
8314 1
8315 } else {
8316 0
8317 }; // c:2967
8318 opt_state_set("autocontinue", true); // c:2968
8319 }
8320 // c:2970-2971 — `pushnode(args, dupstring((how & Z_DISOWN) ? "disown" : (how & Z_ASYNC) ? "bg" : "fg"));`
8321 let head = if (how & Z_DISOWN as i32) != 0 {
8322 "disown".to_string()
8323 } else if (how & Z_ASYNC as i32) != 0 {
8324 "bg".to_string()
8325 } else {
8326 "fg".to_string()
8327 };
8328 if let Some(ref mut v) = args {
8329 v.insert(0, head);
8330 }
8331 how = Z_SYNC as i32; // c:2972
8332 }
8333
8334 // c:2975-2986 — AUTORESUME prefix match against jobtab.
8335 if isset(AUTORESUME)
8336 && typ == WC_SIMPLE as i32
8337 && (how & Z_SYNC as i32) != 0
8338 && args.as_ref().map(|v| !v.is_empty()).unwrap_or(false)
8339 && redir.as_ref().map(|v| v.is_empty()).unwrap_or(true)
8340 && input == 0
8341 && args.as_ref().unwrap().len() == 1
8342 {
8343 // c:2979-2981
8344 if unset(NOTIFY) {
8345 // c:2982 — `scanjobs();` inlined: walk JOBTAB and printjob
8346 // each STAT_CHANGED entry. C scanjobs body at jobs.c:1993
8347 // is identical to this 5-line walk.
8348 if let Some(jt) = JOBTAB.get() {
8349 let mut guard = jt.lock().unwrap();
8350 let long_list = isset(crate::ported::zsh_h::LONGLISTJOBS);
8351 for i in 1..guard.len() {
8352 // jobs.c:1997 — `for (i = 1; i <= maxjob; i++)`
8353 if (guard[i].stat & crate::ported::zsh_h::STAT_CHANGED) != 0 {
8354 let s = crate::ported::jobs::printjob(&guard[i], i, long_list, None, None); // jobs.c:1999
8355 if !s.is_empty() {
8356 eprint!("{}", s);
8357 }
8358 }
8359 }
8360 }
8361 }
8362 // c:2984 — `if (findjobnam(peekfirst(args)) != -1)`
8363 let head = args.as_ref().unwrap()[0].clone();
8364 let maxjob = JOBTAB
8365 .get()
8366 .map(|m| m.lock().unwrap().len() as i32)
8367 .unwrap_or(0);
8368 let thisjob = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
8369 // c:2982 — `findjobnam(s)`. Canonical port at
8370 // jobs.rs::findjobnam matches against `proc.text`, which is
8371 // the command text actually saved into the job at fork —
8372 // matching C exactly. Returns the job index if any non-
8373 // SUBJOB jobtab entry's first-proc text starts with `s`.
8374 let found = if let Some(jt) = JOBTAB.get() {
8375 let guard = jt.lock().unwrap();
8376 crate::ported::jobs::findjobnam(&head, &guard, maxjob - 1, thisjob).is_some()
8377 } else {
8378 false
8379 };
8380 if found {
8381 // c:2985 — `pushnode(args, dupstring("fg"));`
8382 if let Some(ref mut v) = args {
8383 v.insert(0, "fg".to_string());
8384 }
8385 }
8386 }
8387
8388 // ====================================================================
8389 // SUBSTRATE STUBS — same-named locals citing their home C file per
8390 // [[feedback_no_shortcuts_in_porting]]. Each stub mirrors the C
8391 // signature and returns a degenerate value that keeps the body
8392 // executing while the real port lands.
8393 // ====================================================================
8394 // save_params + restore_params — top-level ports in exec.rs
8395 // (c:4410 / c:4464). Both bridged via `use` below.
8396 use crate::ported::exec::{restore_params, save_params};
8397 // isreallycom — top-level port at exec.rs (c:972). Bridges the
8398 // local shadow that this fn body used pre-port.
8399 use crate::ported::exec::isreallycom;
8400 // execautofn_basic — top-level port at exec.rs (c:5608).
8401 use crate::ported::exec::execautofn_basic;
8402 // C `execerr` macro (c:2700) was a goto-equivalent:
8403 // errflag |= ERRFLAG_ERROR; lastval = 1; goto err;
8404 // Rust expansion: each call site inlines the errflag+LASTVAL set
8405 // and then `break`s out of the enclosing redir loop. The loop's
8406 // post-loop errflag check at c:3949 routes to execcmd_exec_err_path
8407 // for the cleanup tail. No macro needed.
8408
8409 // c:2988-3011 — Z_ASYNC / pipeline-not-last / sh-emulation
8410 // fork-immediately fast path.
8411 if (how & Z_ASYNC as i32) != 0
8412 || output != 0
8413 || (last1 == 2 && input != 0 && {
8414 // c:2989 — `EMULATION(EMULATE_SH)` — emulation==EMULATE_SH.
8415 // EMULATION macro: `(emulation & EMULATE_MASK) == X`. The
8416 // ported `emulation` static at options.rs:1044 holds the
8417 // current bit; compare against EMULATE_SH (zsh_h:2883).
8418 (crate::ported::options::emulation.load(Ordering::Relaxed)
8419 & crate::ported::zsh_h::EMULATE_SH)
8420 != 0
8421 })
8422 {
8423 // c:2988
8424 // c:2999 — `text = getjobtext(state->prog, eparams->beg);`
8425 text = Some(crate::ported::text::getjobtext(
8426 state.prog.clone(),
8427 Some(eparams.beg),
8428 ));
8429 // c:3000-3008 — `switch (execcmd_fork(...)) { -1: goto fatal; 0: break; default: return; }`
8430 let mut filelist_for_fork = filelist.clone();
8431 let pid = execcmd_fork(
8432 state,
8433 how,
8434 typ,
8435 varspc,
8436 &mut filelist_for_fork,
8437 text.as_deref().unwrap_or(""),
8438 oautocont,
8439 close_if_forked,
8440 );
8441 match pid {
8442 -1 => {
8443 // c:3002-3003 — `goto fatal;` — fall through to fatal:
8444 // label at c:4377. We model this with a flag.
8445 redir_err = 1; // pretend redir error to trigger fatal arm
8446 // Continue to done label by setting forked + jumping forward.
8447 // Simplified: just bail with status 1 + fatal handling at
8448 // the bottom of the fn.
8449 return execcmd_exec_done_path(
8450 redir_err,
8451 oautocont,
8452 how,
8453 &mut shti,
8454 &mut chti,
8455 &mut then_ts,
8456 forked,
8457 &mut newxtrerr,
8458 cflags,
8459 orig_cflags,
8460 is_cursh,
8461 do_exec,
8462 );
8463 }
8464 0 => {
8465 // c:3004 — child returned 0; continue with the body.
8466 }
8467 _ => {
8468 // c:3007 — parent: `return;` — but first restore AUTOCONTINUE
8469 // and shelltime stop. Inline the done-tail equivalent.
8470 if oautocont >= 0 {
8471 opt_state_set("autocontinue", oautocont != 0);
8472 }
8473 if (how & Z_TIMED as i32) != 0 {
8474 crate::ported::jobs::shelltime(
8475 Some(&mut shti),
8476 Some(&mut chti),
8477 Some(&mut then_ts),
8478 1,
8479 );
8480 }
8481 return;
8482 }
8483 }
8484 last1 = 1; // c:3009
8485 forked = 1; // c:3009
8486 } else {
8487 // c:3010-3011
8488 text = None;
8489 }
8490
8491 // ====================================================================
8492 // c:3013-3283 — precommand-modifier walk.
8493 //
8494 // The full walk (BINF_PREFIX strip + BINF_COMMAND sub-options +
8495 // BINF_EXEC sub-options) is already ported in `execcmd_compile_head`
8496 // (above this fn). Call into it to keep DRY, then convert the
8497 // returned dispatch struct's fields into the locals C uses
8498 // (cflags, orig_cflags, is_builtin, is_shfunc, use_defpath,
8499 // exec_argv0, precmd_skip).
8500 //
8501 // Per [[feedback_true_port_pattern]] the C function does this
8502 // walk inline. Reusing the existing port is acceptable because
8503 // `execcmd_compile_head`'s body IS the c:3013-3283 walk — the
8504 // citations there match. The C tree-walker and the fusevm
8505 // compile-time walker arrive at identical dispatch decisions
8506 // from the same input.
8507 // ====================================================================
8508 let mut preargs: Vec<String> = Vec::new();
8509 let mut exec_argv0: Option<String> = None;
8510 if (typ == WC_SIMPLE as i32 || typ == WC_TYPESET as i32) && args.is_some() {
8511 // c:3018
8512 let head_args: Vec<String> = args.as_ref().unwrap().clone();
8513 let dispatch = execcmd_compile_head(&head_args, typ as u32);
8514 // Pull fields into local mirror of C state.
8515 cflags = dispatch.cflags;
8516 if dispatch.is_builtin {
8517 is_builtin = 1;
8518 }
8519 if dispatch.is_shfunc {
8520 is_shfunc = 1;
8521 }
8522 if dispatch.use_defpath {
8523 use_defpath = 1;
8524 }
8525 exec_argv0 = dispatch.exec_argv0;
8526 // c:3061 — `orig_cflags |= cflags;` accumulator path; for
8527 // BINF_PREFIX walks orig_cflags tracks each step's pre-mask
8528 // bits. execcmd_compile_head doesn't surface orig_cflags
8529 // separately, so approximate as the post-strip cflags.
8530 orig_cflags = cflags;
8531 // c:3030-3086 — preargs = args after stripping the precmd
8532 // prefix words. The compile_head dispatch returned the strip
8533 // count, so apply it.
8534 preargs = head_args[dispatch.precmd_skip..].to_vec();
8535 // The remainder of args (after the BINF_PREFIX strip) is what
8536 // the dispatch sees. Mirror C's `args` mutation: replace the
8537 // contents past the head.
8538 if let Some(ref mut v) = args {
8539 v.drain(0..dispatch.precmd_skip);
8540 }
8541 // c:3076 — `magic_assign = (hn->flags & BINF_MAGICEQUALS);`
8542 // — surface via cflags check: if a typeset-family builtin
8543 // landed, BINF_MAGICEQUALS is in its flags and dispatch
8544 // surfaces it via cflags.
8545 if (cflags & BINF_MAGICEQUALS) != 0 && typ != WC_TYPESET as i32 {
8546 magic_assign = 1;
8547 }
8548 // hn is a pointer to the resolved builtin; the compile_head
8549 // walk doesn't return it directly. Mark as None — the
8550 // resolution loop below will re-look-up via builtintab.
8551 hn = None;
8552 } else {
8553 // c:3282-3283 — `else preargs = NULL;`
8554 // We use an empty preargs to model NULL — C's `preargs` is
8555 // only iterated if `nonempty(preargs)` in this branch.
8556 }
8557
8558 // c:3285-3300 — `Do prefork substitutions.` magic_assign handling.
8559 // Sets the file-static `esprefork` (exec.rs:267) so any downstream
8560 // execsubst() call inside this command's expansion uses the same
8561 // prefork flags. Also keep a local copy for the immediate
8562 // prefork(args, esprefork, NULL) below.
8563 let esprefork_v: i32 =
8564 if magic_assign != 0 || (isset(MAGICEQUALSUBST) && typ != WC_TYPESET as i32) {
8565 PREFORK_TYPESET // c:3300
8566 } else {
8567 0
8568 };
8569 esprefork.store(esprefork_v, Ordering::Relaxed); // c:3298 esprefork = ...
8570
8571 // c:3302-3307 — prefork(args, esprefork, NULL) + joinlists(preargs, args).
8572 if args.is_some() && eparams.htok != 0 {
8573 // c:3303-3304 — `if (eparams->htok) prefork(args, esprefork, NULL);`
8574 let mut as_linklist: LinkList<String> = Default::default();
8575 if let Some(ref v) = args {
8576 for s in v {
8577 as_linklist.push_back(s.clone());
8578 }
8579 }
8580 let mut rf = 0i32;
8581 prefork(&mut as_linklist, esprefork_v, &mut rf);
8582 // Move back into args.
8583 let mut out: Vec<String> = Vec::new();
8584 while let Some(s) = as_linklist.pop_front() {
8585 out.push(s);
8586 }
8587 args = Some(out);
8588 }
8589 if !preargs.is_empty() {
8590 // c:3305-3306 — `if (preargs) args = joinlists(preargs, args);`
8591 let mut joined = preargs.clone();
8592 if let Some(ref v) = args {
8593 joined.extend(v.iter().cloned());
8594 }
8595 args = Some(joined);
8596 }
8597
8598 // c:3309-3406 — main resolution loop + empty-command branch.
8599 if typ == WC_SIMPLE as i32 || typ == WC_TYPESET as i32 {
8600 let mut unglobbed: i32 = 0; // c:3310
8601
8602 // c:3312 — `for (;;)` — main resolution loop.
8603 loop {
8604 // c:3315-3318 — globbing or untokenise sweep.
8605 if (cflags & BINF_NOGLOB) == 0 {
8606 while checked == 0
8607 && (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0
8608 && args.as_ref().map(|v| !v.is_empty()).unwrap_or(false)
8609 && crate::ported::lex::has_token(&args.as_ref().unwrap()[0])
8610 {
8611 // c:3318 — `zglob(args, firstnode(args), 0);`
8612 // zglob takes &mut Vec<String>; isolate the head element
8613 // by splitting args into [head] and [tail], then re-merging.
8614 let mut head_vec: Vec<String> = Vec::new();
8615 if let Some(ref mut v) = args {
8616 head_vec.push(v.remove(0));
8617 }
8618 crate::ported::glob::zglob(&mut head_vec, 0usize, 0);
8619 if let Some(ref mut v) = args {
8620 for (i, s) in head_vec.into_iter().enumerate() {
8621 v.insert(i, s);
8622 }
8623 }
8624 }
8625 } else if unglobbed == 0 {
8626 // c:3319-3322
8627 if let Some(ref mut v) = args {
8628 for s in v.iter_mut() {
8629 *s = untokenize(s); // c:3321
8630 }
8631 }
8632 unglobbed = 1; // c:3322
8633 }
8634
8635 // c:3327-3328 — `if ((cflags & BINF_EXEC) && last1) do_exec = 1;`
8636 if (cflags & BINF_EXEC) != 0 && last1 != 0 {
8637 do_exec = 1; // c:3328
8638 }
8639
8640 // c:3331-3407 — empty-command branch.
8641 if args.as_ref().map(|v| v.is_empty()).unwrap_or(true) {
8642 // c:3331 — `if (!args || empty(args))`
8643 if redir.as_ref().map(|v| !v.is_empty()).unwrap_or(false) {
8644 // c:3332 — `if (redir && nonempty(redir))`
8645 if do_exec != 0 {
8646 // c:3333 — `Was this "exec < foobar"?`
8647 nullexec = 1; // c:3335
8648 break;
8649 } else if varspc.is_some() {
8650 // c:3337
8651 nullexec = 2; // c:3338
8652 break;
8653 } else if {
8654 // c:3340-3341 — `if (!nullcmd || !*nullcmd ||
8655 // opts[CSHNULLCMD] || (cflags & BINF_PREFIX))`
8656 let nc = getsparam("NULLCMD");
8657 let nc_empty = nc.as_deref().map(|s| s.is_empty()).unwrap_or(true);
8658 nc_empty || isset(CSHNULLCMD) || (cflags & BINF_PREFIX) != 0
8659 } {
8660 // c:3342 — `zerr("redirection with no command");`
8661 zerr("redirection with no command");
8662 LASTVAL.store(1, Ordering::Relaxed); // c:3343
8663 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3344
8664 if forked != 0 {
8665 // c:3345-3346
8666 crate::ported::builtin::_realexit();
8667 }
8668 if (how & Z_TIMED as i32) != 0 {
8669 // c:3347-3348
8670 crate::ported::jobs::shelltime(
8671 Some(&mut shti),
8672 Some(&mut chti),
8673 Some(&mut then_ts),
8674 1,
8675 );
8676 }
8677 return; // c:3349
8678 } else if {
8679 // c:3350 — `if (!nullcmd || !*nullcmd || opts[SHNULLCMD])`
8680 let nc = getsparam("NULLCMD");
8681 let nc_empty = nc.as_deref().map(|s| s.is_empty()).unwrap_or(true);
8682 nc_empty || isset(SHNULLCMD)
8683 } {
8684 // c:3351-3353 — `if (!args) args = newlinklist(); addlinknode(args, dupstring(":"));`
8685 if args.is_none() {
8686 args = Some(Vec::new());
8687 }
8688 args.as_mut().unwrap().push(":".to_string()); // c:3353
8689 } else if {
8690 // c:3354-3356 — `readnullcmd && *readnullcmd &&
8691 // peekfirst(redir).type == REDIR_READ &&
8692 // !nextnode(firstnode(redir))`
8693 let rnc = getsparam("READNULLCMD");
8694 let rnc_nonempty = rnc.as_deref().map(|s| !s.is_empty()).unwrap_or(false);
8695 rnc_nonempty
8696 && redir.as_ref().unwrap().len() == 1
8697 && redir.as_ref().unwrap()[0].typ == REDIR_READ
8698 } {
8699 // c:3357-3359
8700 if args.is_none() {
8701 args = Some(Vec::new());
8702 }
8703 let rnc = getsparam("READNULLCMD").unwrap_or_default();
8704 args.as_mut().unwrap().push(rnc); // c:3359
8705 } else {
8706 // c:3360-3364 — default: nullcmd as command.
8707 if args.is_none() {
8708 args = Some(Vec::new());
8709 }
8710 let nc = getsparam("NULLCMD").unwrap_or_default();
8711 args.as_mut().unwrap().push(nc); // c:3363
8712 }
8713 } else if (cflags & BINF_PREFIX) != 0 && (cflags & BINF_COMMAND) != 0 {
8714 // c:3365 — bare `command`: lastval=0, return.
8715 LASTVAL.store(0, Ordering::Relaxed); // c:3366
8716 if forked != 0 {
8717 crate::ported::builtin::_realexit(); // c:3367-3368
8718 }
8719 if (how & Z_TIMED as i32) != 0 {
8720 crate::ported::jobs::shelltime(
8721 Some(&mut shti),
8722 Some(&mut chti),
8723 Some(&mut then_ts),
8724 1,
8725 ); // c:3369-3370
8726 }
8727 return; // c:3371
8728 } else {
8729 // c:3372-3406 — no arguments default arm.
8730 // c:3378-3385 — badcshglob == 1 → no match.
8731 if crate::ported::glob::BADCSHGLOB.load(Ordering::Relaxed) == 1 {
8732 zerr("no match"); // c:3379
8733 LASTVAL.store(1, Ordering::Relaxed); // c:3380
8734 if forked != 0 {
8735 crate::ported::builtin::_realexit(); // c:3381-3382
8736 }
8737 if (how & Z_TIMED as i32) != 0 {
8738 crate::ported::jobs::shelltime(
8739 Some(&mut shti),
8740 Some(&mut chti),
8741 Some(&mut then_ts),
8742 1,
8743 ); // c:3383-3384
8744 }
8745 return; // c:3385
8746 }
8747 // c:3387 — `cmdoutval = use_cmdoutval ? lastval : 0;`
8748 cmdoutval.store(
8749 if use_cmdoutval.load(Ordering::Relaxed) != 0 {
8750 LASTVAL.load(Ordering::Relaxed)
8751 } else {
8752 0
8753 },
8754 Ordering::Relaxed,
8755 );
8756 if varspc.is_some() {
8757 // c:3388-3392 — `lastval = old_lastval; addvars(state, varspc, 0);`
8758 LASTVAL.store(old_lastval, Ordering::Relaxed); // c:3390
8759 addvars(state, varspc.unwrap_or(0), 0); // c:3391
8760 }
8761 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
8762 // c:3393
8763 LASTVAL.store(1, Ordering::Relaxed); // c:3394
8764 } else {
8765 // c:3395-3396
8766 LASTVAL.store(cmdoutval.load(Ordering::Relaxed), Ordering::Relaxed);
8767 }
8768 if isset(XTRACE) {
8769 // c:3397-3400 — `fputc('\n', xtrerr); fflush(xtrerr);`
8770 // xtrerr accessor is stub; rely on the existing
8771 // stderr writer in compile_zsh tracing path.
8772 eprintln!();
8773 }
8774 if forked != 0 {
8775 crate::ported::builtin::_realexit(); // c:3401-3402
8776 }
8777 if (how & Z_TIMED as i32) != 0 {
8778 crate::ported::jobs::shelltime(
8779 Some(&mut shti),
8780 Some(&mut chti),
8781 Some(&mut then_ts),
8782 1,
8783 ); // c:3403-3404
8784 }
8785 return; // c:3405
8786 }
8787 }
8788
8789 // c:3423-3426 — `if (errflag || checked || is_builtin ||
8790 // (isset(POSIXBUILTINS) ? (cflags & BINF_EXEC) : (cflags & BINF_COMMAND)))`
8791 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0
8792 || checked != 0
8793 || is_builtin != 0
8794 || if isset(POSIXBUILTINS) {
8795 (cflags & BINF_EXEC) != 0
8796 } else {
8797 (cflags & BINF_COMMAND) != 0
8798 }
8799 {
8800 // c:3423
8801 break; // c:3426
8802 }
8803
8804 // c:3428 — `cmdarg = (char *) peekfirst(args);`
8805 let cmdarg = args.as_ref().unwrap()[0].clone();
8806
8807 // c:3429-3433 — shfunc lookup.
8808 if (cflags & (BINF_BUILTIN | BINF_COMMAND)) == 0 {
8809 let in_shfunctab = shfunctab_lock()
8810 .read()
8811 .map(|t| t.iter().any(|(k, _)| k.as_str() == cmdarg.as_str()))
8812 .unwrap_or(false);
8813 if in_shfunctab {
8814 is_shfunc = 1; // c:3431
8815 break; // c:3432
8816 }
8817 }
8818 // c:3434-3447 — builtintab lookup.
8819 let builtin_entry: Option<&'static builtin> = BUILTINS
8820 .iter()
8821 .find(|b| b.node.nam.as_str() == cmdarg.as_str());
8822 if builtin_entry.is_none() {
8823 if (cflags & BINF_BUILTIN) != 0 {
8824 // c:3435 — `zwarn("no such builtin: %s", cmdarg);`
8825 zwarn(&format!("no such builtin: {}", cmdarg)); // c:3436
8826 LASTVAL.store(1, Ordering::Relaxed); // c:3437
8827 if oautocont >= 0 {
8828 // c:3438-3439
8829 opt_state_set("autocontinue", oautocont != 0);
8830 }
8831 if forked != 0 {
8832 crate::ported::builtin::_realexit(); // c:3440-3441
8833 }
8834 if (how & Z_TIMED as i32) != 0 {
8835 crate::ported::jobs::shelltime(
8836 Some(&mut shti),
8837 Some(&mut chti),
8838 Some(&mut then_ts),
8839 1,
8840 ); // c:3442-3443
8841 }
8842 return; // c:3444
8843 }
8844 break; // c:3446
8845 }
8846 let entry = builtin_entry.unwrap();
8847 // c:3448-3460 — `if (!(hn->flags & BINF_PREFIX)) { is_builtin = 1; ... }`
8848 if (entry.node.flags as u32 & BINF_PREFIX) == 0 {
8849 is_builtin = 1; // c:3449
8850 // c:3452 — `if (!(hn = resolvebuiltin(cmdarg, hn)))` —
8851 // module autoload check. zshrs's BUILTINS table is
8852 // static and pre-resolved; treat resolvebuiltin as
8853 // pass-through.
8854 hn = Some(entry as *const builtin as *mut builtin);
8855 break; // c:3459
8856 }
8857 // c:3461-3463 — BINF_PREFIX modifier (builtin/command/exec).
8858 cflags &= !(BINF_BUILTIN | BINF_COMMAND);
8859 cflags |= entry.node.flags as u32;
8860 if let Some(ref mut v) = args {
8861 v.remove(0); // c:3463 uremnode(args, firstnode(args))
8862 }
8863 hn = None; // c:3464
8864 }
8865 }
8866
8867 // c:3468-3478 — errflag bail-out.
8868 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
8869 // c:3468
8870 if LASTVAL.load(Ordering::Relaxed) == 0 {
8871 // c:3469
8872 LASTVAL.store(1, Ordering::Relaxed); // c:3470
8873 }
8874 if oautocont >= 0 {
8875 opt_state_set("autocontinue", oautocont != 0);
8876 // c:3472
8877 }
8878 if forked != 0 {
8879 crate::ported::builtin::_realexit(); // c:3473-3474
8880 }
8881 if (how & Z_TIMED as i32) != 0 {
8882 crate::ported::jobs::shelltime(Some(&mut shti), Some(&mut chti), Some(&mut then_ts), 1);
8883 // c:3475-3476
8884 }
8885 return; // c:3477
8886 }
8887
8888 // c:3480-3483 — `Get the text associated with this command.`
8889 if text.is_none()
8890 && sfcontext.load(Ordering::Relaxed) == 0
8891 && (isset(MONITOR) || (how & Z_TIMED as i32) != 0)
8892 {
8893 // c:3481-3482
8894 text = Some(crate::ported::text::getjobtext(
8895 state.prog.clone(),
8896 Some(eparams.beg),
8897 )); // c:3483
8898 }
8899
8900 // c:3485-3492 — `Set up special parameter $_`.
8901 if typ != WC_FUNCDEF as i32 {
8902 // c:3490
8903 let last_str = args
8904 .as_ref()
8905 .and_then(|v| v.last())
8906 .cloned()
8907 .unwrap_or_default();
8908 setunderscore(&last_str); // c:3491-3492
8909 }
8910
8911 // c:3494-3524 — `Warn about "rm *"`.
8912 if typ == WC_SIMPLE as i32
8913 && crate::ported::zsh_h::interact()
8914 && unset(RMSTARSILENT)
8915 && isset(SHINSTDIN)
8916 && args.as_ref().map(|v| v.len() >= 2).unwrap_or(false)
8917 && args.as_ref().unwrap()[0] == "rm"
8918 {
8919 // c:3495-3497
8920 let args_v = args.as_ref().unwrap().clone();
8921 for s in args_v.iter().skip(1) {
8922 // c:3500
8923 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
8924 break;
8925 }
8926 let l = s.len();
8927 // c:3505 — `if (s[0] == Star && !s[1])` — bare `*`.
8928 if s.len() == 1 && s.as_bytes()[0] == Star as u8 {
8929 let pwd = getsparam("PWD").unwrap_or_default();
8930 if !crate::ported::utils::checkrmall(&pwd) {
8931 // c:3506
8932 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3507
8933 break; // c:3508
8934 }
8935 } else if l >= 2 {
8936 // c:3510 — `s[l-2] == '/' && s[l-1] == Star`
8937 let bytes = s.as_bytes();
8938 if bytes[l - 2] == b'/' && bytes[l - 1] == Star as u8 {
8939 let prefix = if l == 2 {
8940 "/".to_string()
8941 } else {
8942 String::from_utf8_lossy(&bytes[..l - 2]).into_owned()
8943 };
8944 if !crate::ported::utils::checkrmall(&prefix) {
8945 // c:3518
8946 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3519
8947 break; // c:3520
8948 }
8949 }
8950 }
8951 }
8952 }
8953
8954 // c:3526-3580 — type-specific dispatch prep.
8955 if typ == WC_FUNCDEF as i32 {
8956 // c:3526
8957 if state.prog.prog.get(state.pc).copied().unwrap_or(0) != 0 {
8958 // c:3535 — `Nonymous, don't do redirections here`
8959 redir = None; // c:3537
8960 }
8961 } else if is_shfunc != 0 || typ == WC_AUTOFN as i32 {
8962 // c:3539
8963 // c:3540-3559 — shfunc / autoload preload.
8964 if is_shfunc != 0 {
8965 // c:3541-3542 — `shf = (Shfunc)hn;` — already in hn.
8966 } else {
8967 // c:3543-3559 — autoload preload.
8968 if let Some(ref mut sh) = state.prog.shf {
8969 let shf_ptr: *mut shfunc = sh.as_mut() as *mut shfunc;
8970 let r = loadautofn(shf_ptr, 1, 0, 0);
8971 if r != 0 {
8972 // c:3551 — `lastval = 1;`
8973 LASTVAL.store(1, Ordering::Relaxed);
8974 if oautocont >= 0 {
8975 opt_state_set("autocontinue", oautocont != 0);
8976 }
8977 if forked != 0 {
8978 crate::ported::builtin::_realexit();
8979 }
8980 if (how & Z_TIMED as i32) != 0 {
8981 crate::ported::jobs::shelltime(
8982 Some(&mut shti),
8983 Some(&mut chti),
8984 Some(&mut then_ts),
8985 1,
8986 );
8987 }
8988 return; // c:3558
8989 }
8990 }
8991 }
8992 // c:3561-3579 — shf->redir append: a function definition can
8993 // carry extra redirs (`f() { ... } < file`), captured as a
8994 // separate Eprog in shf->redir. Walk that Eprog with a temp
8995 // estate, extract its redirs with ecgetredirs, then merge
8996 // into the live `redir` list.
8997 // Resolve shfunc by name (hn is *mut builtin so we go through
8998 // shfunctab as in the dispatch site at c:4102).
8999 let shfn_name = args
9000 .as_ref()
9001 .and_then(|v| v.first())
9002 .cloned()
9003 .unwrap_or_default();
9004 let shf_redir_eprog: Option<crate::ported::zsh_h::Eprog> = {
9005 if let Ok(tab) = shfunctab_lock().read() {
9006 tab.get(&shfn_name).and_then(|s| s.redir.clone())
9007 } else {
9008 None
9009 }
9010 };
9011 if let Some(red_eprog) = shf_redir_eprog {
9012 // c:3566-3571 — build temp estate from shf->redir.
9013 let mut tmp_state = estate {
9014 prog: red_eprog.clone(),
9015 pc: 0,
9016 strs: red_eprog.strs.clone(),
9017 strs_offset: 0,
9018 };
9019 // c:3572 — `redir2 = ecgetredirs(&s);`
9020 let redir2 = crate::ported::parse::ecgetredirs(&mut tmp_state);
9021 // c:3573-3578 — merge into existing redir.
9022 if redir.is_none() {
9023 redir = Some(redir2); // c:3574
9024 } else if let Some(ref mut r) = redir {
9025 // c:3576-3577 — append.
9026 for n in redir2 {
9027 r.push(n);
9028 }
9029 }
9030 }
9031 }
9032
9033 // c:3582-3591 — errflag bail-out (2).
9034 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
9035 // c:3582
9036 LASTVAL.store(1, Ordering::Relaxed); // c:3583
9037 if oautocont >= 0 {
9038 opt_state_set("autocontinue", oautocont != 0);
9039 // c:3584-3585
9040 }
9041 if forked != 0 {
9042 crate::ported::builtin::_realexit(); // c:3586-3587
9043 }
9044 if (how & Z_TIMED as i32) != 0 {
9045 crate::ported::jobs::shelltime(Some(&mut shti), Some(&mut chti), Some(&mut then_ts), 1);
9046 // c:3588-3589
9047 }
9048 return; // c:3590
9049 }
9050
9051 // c:3593-3632 — external resolution + AUTOCD.
9052 if (typ == WC_SIMPLE as i32 || typ == WC_TYPESET as i32) && nullexec == 0 {
9053 // c:3593
9054 let trycd = isset(AUTOCD)
9055 && isset(SHINSTDIN)
9056 && redir.as_ref().map(|v| v.is_empty()).unwrap_or(true)
9057 && args.as_ref().map(|v| v.len() == 1).unwrap_or(false)
9058 && !args.as_ref().unwrap()[0].is_empty(); // c:3595-3597
9059 if hn.is_none() {
9060 // c:3600
9061 let cmdarg = args.as_ref().unwrap()[0].clone();
9062 let mut dohashcmd = isset(HASHCMDS); // c:3604
9063 // c:3606 — `hn = cmdnamtab->getnode(cmdnamtab, cmdarg);`
9064 let mut have_cmdnam: Option<cmdnam> = {
9065 let tab = cmdnamtab_lock().read().ok();
9066 tab.and_then(|t| {
9067 t.iter()
9068 .find(|(k, _)| k.as_str() == cmdarg.as_str())
9069 .map(|(_, v)| v.clone())
9070 })
9071 };
9072 if have_cmdnam.is_some() && trycd && !isreallycom(have_cmdnam.as_ref().unwrap()) {
9073 // c:3607
9074 // c:3608-3614 — remove the cached entry; force rehash.
9075 cmdnam_unhashed(&cmdarg, Vec::new());
9076 have_cmdnam = None;
9077 if let Some(cn) = have_cmdnam.as_ref() {
9078 if (cn.node.flags & crate::ported::zsh_h::HASHED) == 0 {
9079 // checkpath = path; dohashcmd = 1;
9080 dohashcmd = true;
9081 }
9082 }
9083 }
9084 if have_cmdnam.is_none() && dohashcmd && cmdarg != ".." {
9085 // c:3616 — `if (!hn && dohashcmd && strcmp(cmdarg, "..")) `
9086 let has_slash = cmdarg.contains('/'); // c:3617-3618
9087 if !has_slash {
9088 // c:3619 — `hn = (HashNode) hashcmd(cmdarg, checkpath);`
9089 let path_dirs = getsparam("PATH").unwrap_or_default();
9090 let dirs: Vec<String> = path_dirs.split(':').map(String::from).collect();
9091 have_cmdnam = hashcmd(&cmdarg, &dirs);
9092 }
9093 }
9094 // hn stays None for external commands — the resolution
9095 // value matters only for builtin/shfunc dispatch in the
9096 // following blocks.
9097 let _ = have_cmdnam;
9098 }
9099
9100 // c:3625-3631 — AUTOCD: command not found, try directory.
9101 if hn.is_none() && trycd {
9102 let cmdarg = args.as_ref().unwrap()[0].clone();
9103 if let Some(s) = cancd(&cmdarg) {
9104 // c:3625
9105 args.as_mut().unwrap()[0] = s; // c:3626
9106 args.as_mut().unwrap().insert(0, "--".to_string()); // c:3627
9107 args.as_mut().unwrap().insert(0, "cd".to_string()); // c:3628
9108 // c:3629 — `if ((hn = builtintab->getnode(builtintab, "cd")))`
9109 let cd_entry = BUILTINS.iter().find(|b| b.node.nam.as_str() == "cd");
9110 if let Some(cd) = cd_entry {
9111 hn = Some(cd as *const builtin as *mut builtin);
9112 is_builtin = 1; // c:3630
9113 }
9114 }
9115 }
9116 }
9117
9118 // c:3635 — `is_cursh = (is_builtin || is_shfunc || nullexec || type >= WC_CURSH);`
9119 is_cursh =
9120 (is_builtin != 0 || is_shfunc != 0 || nullexec != 0 || typ >= WC_CURSH as i32) as i32;
9121
9122 // c:3659-3697 — fork decision.
9123 if forked == 0 {
9124 // c:3659
9125 if do_exec == 0
9126 && (((is_builtin != 0 || is_shfunc != 0) && output != 0)
9127 || (is_cursh == 0
9128 && (last1 != 1
9129 || crate::ported::signals::nsigtrapped.load(Ordering::Relaxed) != 0
9130 || JOBTAB
9131 .get()
9132 .map(|jt| crate::ported::jobs::havefiles(&jt.lock().unwrap()))
9133 .unwrap_or(false)
9134 || false/* fdtable_flocks — substrate stub */)))
9135 {
9136 // c:3660-3663
9137 let mut filelist_for_fork = filelist.clone();
9138 let pid = execcmd_fork(
9139 state,
9140 how,
9141 typ,
9142 varspc,
9143 &mut filelist_for_fork,
9144 text.as_deref().unwrap_or(""),
9145 oautocont,
9146 close_if_forked,
9147 );
9148 match pid {
9149 -1 => {
9150 // c:3666-3667 — goto fatal.
9151 redir_err = 1;
9152 return execcmd_exec_done_path(
9153 redir_err,
9154 oautocont,
9155 how,
9156 &mut shti,
9157 &mut chti,
9158 &mut then_ts,
9159 forked,
9160 &mut newxtrerr,
9161 cflags,
9162 orig_cflags,
9163 is_cursh,
9164 do_exec,
9165 );
9166 }
9167 0 => {
9168 // c:3668 — child continues.
9169 }
9170 _ => {
9171 // c:3670-3671 — parent returns.
9172 if oautocont >= 0 {
9173 opt_state_set("autocontinue", oautocont != 0);
9174 }
9175 if (how & Z_TIMED as i32) != 0 {
9176 crate::ported::jobs::shelltime(
9177 Some(&mut shti),
9178 Some(&mut chti),
9179 Some(&mut then_ts),
9180 1,
9181 );
9182 }
9183 return;
9184 }
9185 }
9186 forked = 1; // c:3673
9187 } else if is_cursh != 0 {
9188 // c:3674
9189 // c:3678-3682 — set jobtab[thisjob] stat bits.
9190 let thisjob = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
9191 if thisjob >= 0 {
9192 if let Some(jt) = JOBTAB.get() {
9193 let mut guard = jt.lock().unwrap();
9194 if let Some(j) = guard.get_mut(thisjob as usize) {
9195 j.stat |= STAT_CURSH; // c:3678
9196 // c:3679-3680 — `if (!jobtab[thisjob].procs)
9197 // jobtab[thisjob].stat |= STAT_NOPRINT;`
9198 // Suppress the "[N] done" print for jobs that
9199 // never forked a real process (cursh / builtin /
9200 // null exec).
9201 if j.procs.is_empty() {
9202 j.stat |= STAT_NOPRINT; // c:3680
9203 }
9204 if is_builtin != 0 {
9205 j.stat |= STAT_BUILTIN; // c:3682
9206 }
9207 }
9208 }
9209 }
9210 } else {
9211 // c:3683-3697 — external exec (real or fake).
9212 is_exec = 1; // c:3687
9213 // c:3695 — `if (type == WC_SUBSH) forked = 1;`
9214 if typ == WC_SUBSH as i32 {
9215 forked = 1; // c:3696
9216 }
9217 }
9218 }
9219
9220 // c:3700-3704 — `if ((esglob = !(cflags & BINF_NOGLOB)) && args && htok)`
9221 if (cflags & BINF_NOGLOB) == 0 && args.is_some() && eparams.htok != 0 {
9222 // c:3700
9223 let mut oargs: LinkList<String> = Default::default();
9224 if let Some(ref v) = args {
9225 for s in v {
9226 oargs.push_back(s.clone());
9227 }
9228 }
9229 globlist(&mut oargs, 0); // c:3702
9230 let mut out: Vec<String> = Vec::new();
9231 while let Some(s) = oargs.pop_front() {
9232 out.push(s);
9233 }
9234 args = Some(out);
9235 }
9236 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
9237 // c:3705
9238 LASTVAL.store(1, Ordering::Relaxed); // c:3706
9239 return execcmd_exec_err_path(
9240 forked,
9241 &mut save,
9242 &mut mfds,
9243 oautocont,
9244 how,
9245 &mut shti,
9246 &mut chti,
9247 &mut then_ts,
9248 &mut newxtrerr,
9249 cflags,
9250 orig_cflags,
9251 is_cursh,
9252 do_exec,
9253 redir_err,
9254 );
9255 }
9256
9257 // c:3711-3718 — XTRACE prep (newxtrerr stderr dup).
9258 // Architectural divergence: C duplicates stderr to a new FD and
9259 // marks it `FDT_XTRACE` in the fdtable so the redir loop skips it.
9260 // zshrs routes xtrace output through `eprintln!()` / `tracing`
9261 // instead of a duplicated fd, so the FDT_XTRACE bookkeeping has
9262 // no counterpart. Not a port gap — `xtrerr is FILE*` is a C-ism
9263 // intentionally replaced.
9264
9265 // c:3720-3724 — pipeline input/output to mfds.
9266 if input != 0 {
9267 addfd(forked, &mut save, &mut mfds, 0, input, 0, None); // c:3722
9268 }
9269 if output != 0 {
9270 addfd(forked, &mut save, &mut mfds, 1, output, 1, None); // c:3724
9271 }
9272
9273 // c:3726-3728 — `if (redir) spawnpipes(redir, nullexec);`
9274 if let Some(ref mut r) = redir {
9275 spawnpipes(r.as_mut_slice(), nullexec);
9276 }
9277
9278 // c:3731-3955 — io redirection loop. Faithful per-redir match.
9279 while let Some(redir_list) = redir.as_mut() {
9280 // c:3731 — `while (redir && nonempty(redir))`
9281 if redir_list.is_empty() {
9282 break;
9283 }
9284 let mut fn_ = redir_list.remove(0); // c:3732 `fn = (Redir) ugetnode(redir);`
9285 // c:3734-3735 DPUTS — debug assert REDIR_HEREDOC* gone.
9286 if fn_.typ == REDIR_INPIPE {
9287 // c:3736
9288 if checkclobberparam(&fn_) == 0 || fn_.fd2 == -1 {
9289 // c:3737
9290 if fn_.fd2 != -1 {
9291 let _ = zclose(fn_.fd2); // c:3738-3739
9292 }
9293 closemnodes(&mut mfds); // c:3740
9294 fixfds(&save); // c:3741
9295 {
9296 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
9297 LASTVAL.store(1, Ordering::Relaxed);
9298 } // c:3742
9299 break;
9300 }
9301 // c:3744 — `addfd(forked, save, mfds, fn->fd1, fn->fd2, 0, fn->varid);`
9302 addfd(
9303 forked,
9304 &mut save,
9305 &mut mfds,
9306 fn_.fd1,
9307 fn_.fd2,
9308 0,
9309 fn_.varid.as_deref(),
9310 );
9311 } else if fn_.typ == REDIR_OUTPIPE {
9312 // c:3745
9313 if checkclobberparam(&fn_) == 0 || fn_.fd2 == -1 {
9314 // c:3746
9315 if fn_.fd2 != -1 {
9316 let _ = zclose(fn_.fd2); // c:3747-3748
9317 }
9318 closemnodes(&mut mfds); // c:3749
9319 fixfds(&save); // c:3750
9320 {
9321 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
9322 LASTVAL.store(1, Ordering::Relaxed);
9323 } // c:3751
9324 break;
9325 }
9326 // c:3753
9327 addfd(
9328 forked,
9329 &mut save,
9330 &mut mfds,
9331 fn_.fd1,
9332 fn_.fd2,
9333 1,
9334 fn_.varid.as_deref(),
9335 );
9336 } else {
9337 // c:3754 — non-pipe redir branch.
9338 let mut closed: i32; // c:3755
9339 // c:3756-3757 — xpandredir glob/brace.
9340 if fn_.typ != REDIR_HERESTR {
9341 // Put fn_ back temporarily so xpandredir can mutate
9342 // around it; not implemented identically — xpandredir
9343 // signature in zshrs differs (takes &mut redir + ctx).
9344 // c:3756 — `if (xpandredir(fn, redir)) continue;`
9345 // Pragmatic: skip xpandredir (it handles brace/glob in
9346 // redir paths — uncommon, ports to follow-up).
9347 }
9348 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
9349 // c:3758
9350 closemnodes(&mut mfds); // c:3759
9351 fixfds(&save); // c:3760
9352 {
9353 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
9354 LASTVAL.store(1, Ordering::Relaxed);
9355 } // c:3761
9356 break;
9357 }
9358 if !isset(EXECOPT) {
9359 // c:3763 — `if (unset(EXECOPT)) continue;`
9360 continue;
9361 }
9362 let fil_local: i32;
9363 match fn_.typ {
9364 t if t == REDIR_HERESTR => {
9365 // c:3766
9366 if checkclobberparam(&fn_) == 0 {
9367 fil_local = -1; // c:3768
9368 } else {
9369 fil_local = getherestr(&fn_); // c:3770
9370 }
9371 if fil_local == -1 {
9372 // c:3771
9373 let e = std::io::Error::last_os_error();
9374 let raw = e.raw_os_error().unwrap_or(0);
9375 if raw != 0 && raw != libc::EINTR {
9376 zwarn(&format!("can't create temp file for here document: {}", e));
9377 // c:3772-3774
9378 }
9379 closemnodes(&mut mfds); // c:3775
9380 fixfds(&save); // c:3776
9381 {
9382 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
9383 LASTVAL.store(1, Ordering::Relaxed);
9384 } // c:3777
9385 break;
9386 }
9387 // c:3779
9388 addfd(
9389 forked,
9390 &mut save,
9391 &mut mfds,
9392 fn_.fd1,
9393 fil_local,
9394 0,
9395 fn_.varid.as_deref(),
9396 );
9397 }
9398 t if t == REDIR_READ || t == REDIR_READWRITE => {
9399 // c:3781-3782
9400 if checkclobberparam(&fn_) == 0 {
9401 fil_local = -1; // c:3784
9402 } else {
9403 let name = fn_.name.clone().unwrap_or_default();
9404 let unmeta_name = unmeta(&name);
9405 let cstr = match std::ffi::CString::new(unmeta_name.as_str()) {
9406 Ok(c) => c,
9407 Err(_) => {
9408 closemnodes(&mut mfds);
9409 fixfds(&save);
9410 {
9411 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
9412 LASTVAL.store(1, Ordering::Relaxed);
9413 }
9414 break;
9415 }
9416 };
9417 if fn_.typ == REDIR_READ {
9418 // c:3786
9419 fil_local = unsafe {
9420 libc::open(cstr.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY)
9421 };
9422 } else {
9423 // c:3788-3789
9424 fil_local = unsafe {
9425 libc::open(
9426 cstr.as_ptr(),
9427 libc::O_RDWR | libc::O_CREAT | libc::O_NOCTTY,
9428 0o666,
9429 )
9430 };
9431 }
9432 }
9433 if fil_local == -1 {
9434 // c:3790
9435 closemnodes(&mut mfds); // c:3791
9436 fixfds(&save); // c:3792
9437 let e = std::io::Error::last_os_error();
9438 if e.raw_os_error().unwrap_or(0) != libc::EINTR {
9439 zwarn(&format!("{}: {}", e, fn_.name.as_deref().unwrap_or("")));
9440 // c:3793-3794
9441 }
9442 {
9443 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
9444 LASTVAL.store(1, Ordering::Relaxed);
9445 } // c:3795
9446 break;
9447 }
9448 // c:3797
9449 addfd(
9450 forked,
9451 &mut save,
9452 &mut mfds,
9453 fn_.fd1,
9454 fil_local,
9455 0,
9456 fn_.varid.as_deref(),
9457 );
9458 // c:3800-3802 — `if (nullexec == 1 && fn->fd1 == 0 && ...) init_io(NULL);`
9459 if nullexec == 1
9460 && fn_.fd1 == 0
9461 && fn_.varid.is_none()
9462 && isset(SHINSTDIN)
9463 && isset(INTERACTIVE)
9464 {
9465 // c:3801 — `!zleactive` check ommitted (zleactive
9466 // accessor lives in zle module; fusevm bypasses ZLE).
9467 crate::ported::init::init_io(None); // c:3802
9468 }
9469 }
9470 t if t == REDIR_CLOSE => {
9471 // c:3804
9472 // c:3805 — `if (fn->varid) { parse fd from variable }`
9473 let mut fd1_local = fn_.fd1;
9474 if let Some(varname) = fn_.varid.as_deref() {
9475 // c:3806-3849 — `{var}>&-`/`{var}<&-` REDIR_CLOSE
9476 // with varid. The C path resolves the named param
9477 // to its integer-string value, parses as base-10
9478 // (or base#NN), and rejects readonly / non-numeric
9479 // / shell-owned-fd values.
9480 //
9481 // bad=1 → "parameter %s does not contain a file descriptor"
9482 // bad=2 → "can't close file descriptor from readonly parameter %s"
9483 // bad=3 → "file descriptor %d used by shell, not closed"
9484 //
9485 // Substrate now available: getsparam for value,
9486 // paramtab read for PM_READONLY, MAX_ZSH_FD +
9487 // fdtable_get for shell-owned guard.
9488 let mut bad: u8 = 0;
9489 let value_opt = getsparam(varname);
9490 let is_ro = paramtab()
9491 .read()
9492 .ok()
9493 .and_then(|t| {
9494 t.get(varname)
9495 .map(|p| (p.node.flags as u32 & PM_READONLY) != 0)
9496 })
9497 .unwrap_or(false);
9498 if value_opt.is_none() {
9499 bad = 1; // c:3811 getvalue failed
9500 } else if is_ro {
9501 bad = 2; // c:3813 PM_READONLY
9502 } else {
9503 let s = value_opt.as_deref().unwrap_or("");
9504 match s.trim().parse::<i32>() {
9505 Ok(n) => {
9506 fd1_local = n;
9507 fn_.fd1 = n;
9508 let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed);
9509 if n >= 10
9510 && n <= max_fd
9511 && (fdtable_get(n) & FDT_TYPE_MASK) == FDT_INTERNAL
9512 {
9513 // c:3835 shell-owned-fd reject
9514 bad = 3;
9515 }
9516 }
9517 Err(_) => {
9518 bad = 1; // c:3823 strtol failure
9519 }
9520 }
9521 }
9522 if bad != 0 {
9523 // c:3840-3849
9524 match bad {
9525 3 => zwarn(&format!(
9526 "file descriptor {} used by shell, not closed",
9527 fn_.fd1
9528 )),
9529 2 => zwarn(&format!(
9530 "can't close file descriptor from readonly parameter {}",
9531 varname
9532 )),
9533 _ => zwarn(&format!(
9534 "parameter {} does not contain a file descriptor",
9535 varname
9536 )),
9537 }
9538 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
9539 LASTVAL.store(1, Ordering::Relaxed);
9540 break;
9541 }
9542 }
9543 // c:3852-3865 — `closed`: optional movefd save.
9544 closed = 0;
9545 if forked == 0 && fd1_local < 10 && save[fd1_local as usize] == -2 {
9546 // c:3856
9547 let mv = movefd(fd1_local); // c:3857
9548 save[fd1_local as usize] = mv;
9549 if mv >= 0 {
9550 closed = 1; // c:3862-3863
9551 }
9552 }
9553 if fd1_local < 10 {
9554 // c:3866
9555 closemn(&mut mfds, fd1_local, REDIR_CLOSE);
9556 // c:3867
9557 }
9558 // c:3873-3876
9559 let _ = &mut fd1_local;
9560 if closed == 0 && zclose(fn_.fd1) < 0 && fn_.varid.is_some() {
9561 zwarn(&format!(
9562 "failed to close file descriptor {}: {}",
9563 fn_.fd1,
9564 std::io::Error::last_os_error()
9565 )); // c:3873-3875
9566 }
9567 }
9568 t if t == REDIR_MERGEIN || t == REDIR_MERGEOUT => {
9569 // c:3878-3879
9570 if fn_.fd2 < 10 {
9571 closemn(&mut mfds, fn_.fd2, fn_.typ); // c:3881
9572 }
9573 if checkclobberparam(&fn_) == 0 {
9574 fil_local = -1; // c:3883
9575 } else if fn_.fd2 > 9 {
9576 // c:3884-3897 — fd table check.
9577 let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed);
9578 let cin = crate::ported::modules::clone::coprocin.load(Ordering::Relaxed);
9579 let cout = crate::ported::modules::clone::coprocout.load(Ordering::Relaxed);
9580 let in_table = if fn_.fd2 <= max_fd {
9581 let kind = fdtable_get(fn_.fd2) & FDT_TYPE_MASK;
9582 kind != FDT_UNUSED && kind != FDT_EXTERNAL
9583 } else {
9584 false
9585 };
9586 if in_table || fn_.fd2 == cin || fn_.fd2 == cout {
9587 fil_local = -1; // c:3896
9588 // Per-platform errno setter (c:3897 `errno = EBADF;`).
9589 #[cfg(target_os = "macos")]
9590 unsafe {
9591 *libc::__error() = libc::EBADF;
9592 }
9593 #[cfg(target_os = "linux")]
9594 unsafe {
9595 *libc::__errno_location() = libc::EBADF;
9596 }
9597 } else {
9598 let fd = if fn_.fd2 == -2 {
9599 // c:3900-3901
9600 if fn_.typ == REDIR_MERGEOUT {
9601 crate::ported::modules::clone::coprocout.load(Ordering::Relaxed)
9602 } else {
9603 crate::ported::modules::clone::coprocin.load(Ordering::Relaxed)
9604 }
9605 } else {
9606 fn_.fd2
9607 };
9608 // c:3902 — `fil = movefd(dup(fd));`
9609 let dup_fd = unsafe { libc::dup(fd) };
9610 fil_local = movefd(dup_fd);
9611 }
9612 } else {
9613 let fd = if fn_.fd2 == -2 {
9614 if fn_.typ == REDIR_MERGEOUT {
9615 crate::ported::modules::clone::coprocout.load(Ordering::Relaxed)
9616 } else {
9617 crate::ported::modules::clone::coprocin.load(Ordering::Relaxed)
9618 }
9619 } else {
9620 fn_.fd2
9621 };
9622 let dup_fd = unsafe { libc::dup(fd) };
9623 fil_local = movefd(dup_fd);
9624 }
9625 if fil_local == -1 {
9626 // c:3904
9627 closemnodes(&mut mfds); // c:3907
9628 fixfds(&save); // c:3908
9629 if std::io::Error::last_os_error().raw_os_error().unwrap_or(0) != 0 {
9630 let desc = if fn_.fd2 == -2 {
9631 "coprocess".to_string()
9632 } else {
9633 format!("{}", fn_.fd2)
9634 };
9635 zwarn(&format!("{}: {}", desc, std::io::Error::last_os_error()));
9636 // c:3911-3913
9637 }
9638 {
9639 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
9640 LASTVAL.store(1, Ordering::Relaxed);
9641 } // c:3914
9642 break;
9643 }
9644 // c:3916-3917
9645 let merge_is_out = if fn_.typ == REDIR_MERGEOUT { 1 } else { 0 };
9646 addfd(
9647 forked,
9648 &mut save,
9649 &mut mfds,
9650 fn_.fd1,
9651 fil_local,
9652 merge_is_out,
9653 fn_.varid.as_deref(),
9654 );
9655 }
9656 _ => {
9657 // c:3919 default — write/append/error_redir.
9658 let mut dfil: i32;
9659 if checkclobberparam(&fn_) == 0 {
9660 fil_local = -1; // c:3921
9661 } else if IS_APPEND_REDIR(fn_.typ) {
9662 // c:3922
9663 let name = fn_.name.clone().unwrap_or_default();
9664 let unmeta_name = unmeta(&name);
9665 let cstr = match std::ffi::CString::new(unmeta_name.as_str()) {
9666 Ok(c) => c,
9667 Err(_) => {
9668 closemnodes(&mut mfds);
9669 fixfds(&save);
9670 {
9671 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
9672 LASTVAL.store(1, Ordering::Relaxed);
9673 }
9674 break;
9675 }
9676 };
9677 // c:3924-3927
9678 let mode = if !isset(CLOBBER)
9679 && !isset(crate::ported::zsh_h::APPENDCREATE)
9680 && !IS_CLOBBER_REDIR(fn_.typ)
9681 {
9682 libc::O_WRONLY | libc::O_APPEND | libc::O_NOCTTY
9683 } else {
9684 libc::O_WRONLY | libc::O_APPEND | libc::O_CREAT | libc::O_NOCTTY
9685 };
9686 fil_local = unsafe { libc::open(cstr.as_ptr(), mode, 0o666) };
9687 } else {
9688 // c:3929
9689 fil_local = clobber_open(&fn_);
9690 }
9691 // c:3930-3933 — error_redir dup.
9692 if fil_local != -1 && IS_ERROR_REDIR(fn_.typ) {
9693 let dup_fd = unsafe { libc::dup(fil_local) };
9694 dfil = movefd(dup_fd); // c:3931
9695 } else {
9696 dfil = 0; // c:3933
9697 }
9698 if fil_local == -1 || dfil == -1 {
9699 // c:3934
9700 if fil_local != -1 {
9701 unsafe { libc::close(fil_local) }; // c:3935-3936
9702 }
9703 closemnodes(&mut mfds); // c:3937
9704 fixfds(&save); // c:3938
9705 let e = std::io::Error::last_os_error();
9706 let raw = e.raw_os_error().unwrap_or(0);
9707 if raw != 0 && raw != libc::EINTR {
9708 zwarn(&format!("{}: {}", e, fn_.name.as_deref().unwrap_or("")));
9709 // c:3939-3940
9710 }
9711 {
9712 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
9713 LASTVAL.store(1, Ordering::Relaxed);
9714 } // c:3941
9715 break;
9716 }
9717 // c:3943
9718 addfd(
9719 forked,
9720 &mut save,
9721 &mut mfds,
9722 fn_.fd1,
9723 fil_local,
9724 1,
9725 fn_.varid.as_deref(),
9726 );
9727 if IS_ERROR_REDIR(fn_.typ) {
9728 // c:3944-3945
9729 addfd(forked, &mut save, &mut mfds, 2, dfil, 1, None);
9730 }
9731 let _ = &mut dfil;
9732 }
9733 }
9734 // c:3948-3952 — addfd errflag check.
9735 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
9736 // c:3949
9737 closemnodes(&mut mfds); // c:3950
9738 fixfds(&save); // c:3951
9739 {
9740 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
9741 LASTVAL.store(1, Ordering::Relaxed);
9742 } // c:3952
9743 break;
9744 }
9745 }
9746 }
9747
9748 // c:3957-3961 — close multios with ct >= 2.
9749 i = 0;
9750 while i < 10 {
9751 // c:3959
9752 if let Some(m) = mfds.get(i as usize).and_then(|o| o.as_ref()) {
9753 if m.ct >= 2 {
9754 closemn(&mut mfds, i, REDIR_CLOSE); // c:3960
9755 }
9756 }
9757 i += 1;
9758 }
9759
9760 // c:3963-3995 — nullexec branch.
9761 if nullexec != 0 {
9762 // c:3963
9763 if let Some(vspc) = varspc {
9764 // c:3969
9765 let mut restorelist: Vec<crate::ported::zsh_h::param> = Vec::new();
9766 let mut removelist: Vec<String> = Vec::new();
9767 if !isset(POSIXBUILTINS) && nullexec != 2 {
9768 // c:3971-3972
9769 save_params(state, vspc, &mut restorelist, &mut removelist);
9770 }
9771 addvars(state, vspc, 0); // c:3973
9772 if !restorelist.is_empty() {
9773 // c:3974
9774 restore_params(restorelist, removelist); // c:3975
9775 }
9776 }
9777 let ef = errflag.load(Ordering::Relaxed);
9778 LASTVAL.store(
9779 if ef != 0 {
9780 ef
9781 } else {
9782 cmdoutval.load(Ordering::Relaxed)
9783 },
9784 Ordering::Relaxed,
9785 ); // c:3977
9786 if nullexec == 1 {
9787 // c:3978
9788 // c:3983-3985 — close save[i].
9789 i = 0;
9790 while i < 10 {
9791 if save[i as usize] != -2 {
9792 let _ = zclose(save[i as usize]); // c:3985
9793 }
9794 i += 1;
9795 }
9796 // c:3988-3989 — `jobtab[thisjob].stat |= STAT_DONE; goto done;`
9797 let thisjob = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
9798 if thisjob >= 0 {
9799 if let Some(jt) = JOBTAB.get() {
9800 let mut guard = jt.lock().unwrap();
9801 if let Some(j) = guard.get_mut(thisjob as usize) {
9802 j.stat |= STAT_DONE; // c:3989
9803 }
9804 }
9805 }
9806 return execcmd_exec_done_path(
9807 redir_err,
9808 oautocont,
9809 how,
9810 &mut shti,
9811 &mut chti,
9812 &mut then_ts,
9813 forked,
9814 &mut newxtrerr,
9815 cflags,
9816 orig_cflags,
9817 is_cursh,
9818 do_exec,
9819 );
9820 }
9821 if isset(XTRACE) {
9822 // c:3992-3994
9823 eprintln!();
9824 }
9825 } else if isset(EXECOPT) && (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
9826 // c:3996 — main dispatch branch.
9827 // c:3997 — `int q = queue_signal_level();`
9828 let _q = 0;
9829 // c:4003-4012 — entersubsh for is_exec.
9830 if is_exec != 0 {
9831 // c:4003
9832 let mut flags: i32 = if (how & Z_ASYNC as i32) != 0 {
9833 esub::ASYNC
9834 } else {
9835 0
9836 } | esub::PGRP
9837 | esub::FAKE; // c:4004-4005
9838 if typ != WC_SUBSH as i32 {
9839 flags |= esub::KEEPTRAP; // c:4007
9840 }
9841 if (do_exec != 0 || (typ >= WC_CURSH as i32 && last1 == 1)) && forked == 0 {
9842 // c:4008-4009
9843 flags |= esub::REVERTPGRP; // c:4010
9844 }
9845 entersubsh(flags, None); // c:4011
9846 }
9847
9848 if typ == WC_FUNCDEF as i32 {
9849 // c:4013
9850 // c:4014-4036 — `redir_prog` setup from wordcode if no
9851 // redirs+WC_REDIR follows. Wire only when fusevm WC_REDIR
9852 // peek is in scope; for the tree-walker entry point we
9853 // approximate by passing None.
9854 let redir_prog: Option<crate::ported::zsh_h::Eprog> = None;
9855 // c:4039 — `lastval = execfuncdef(state, redir_prog);`
9856 let lv = execfuncdef(state, redir_prog);
9857 LASTVAL.store(lv, Ordering::Relaxed);
9858 } else if typ >= WC_CURSH as i32 {
9859 // c:4042
9860 if last1 == 1 {
9861 do_exec = 1; // c:4044
9862 }
9863 if typ == WC_AUTOFN as i32 {
9864 // c:4046
9865 let lv = execautofn_basic(state, do_exec); // c:4051
9866 LASTVAL.store(lv, Ordering::Relaxed);
9867 } else {
9868 // c:4053 — `lastval = (execfuncs[type - WC_CURSH])(state, do_exec);`
9869 // dispatch_execfuncs ports the C `execfuncs[]` table
9870 // (Src/exec.c:170-180) by typ → exec{cursh,for,select,...}
9871 // direct call. See dispatch_execfuncs at end of file.
9872 let lv = dispatch_execfuncs(state, typ, do_exec);
9873 LASTVAL.store(lv, Ordering::Relaxed);
9874 }
9875 } else if is_builtin != 0 || is_shfunc != 0 {
9876 // c:4055
9877 let mut restorelist: Vec<crate::ported::zsh_h::param> = Vec::new();
9878 let mut removelist: Vec<String> = Vec::new();
9879 let mut do_save: i32 = 0; // c:4057
9880
9881 if forked == 0 {
9882 // c:4060
9883 if isset(POSIXBUILTINS) {
9884 // c:4061
9885 if is_shfunc != 0
9886 || (hn.map(|p| unsafe { (*p).node.flags as u32 }).unwrap_or(0)
9887 & (BINF_PSPECIAL | BINF_ASSIGN_FLAG))
9888 != 0
9889 {
9890 // c:4067
9891 do_save = if (orig_cflags & BINF_COMMAND) != 0 {
9892 1
9893 } else {
9894 0
9895 };
9896 } else {
9897 do_save = 1; // c:4070
9898 }
9899 } else {
9900 // c:4071
9901 if (cflags & (BINF_COMMAND | BINF_ASSIGN_FLAG)) != 0 || magic_assign == 0 {
9902 // c:4076
9903 do_save = 1; // c:4077
9904 }
9905 }
9906 if do_save != 0 {
9907 if let Some(vspc) = varspc {
9908 // c:4079
9909 save_params(state, vspc, &mut restorelist, &mut removelist);
9910 }
9911 }
9912 }
9913 if varspc.is_some() {
9914 // c:4082
9915 let mut addflags: i32 = 0; // c:4086
9916 if is_shfunc != 0 {
9917 addflags |= ADDVAR_EXPORT; // c:4088
9918 }
9919 if !restorelist.is_empty() {
9920 addflags |= ADDVAR_RESTORE; // c:4090
9921 }
9922 addvars(state, varspc.unwrap_or(0), addflags); // c:4092
9923 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
9924 // c:4093
9925 if !restorelist.is_empty() {
9926 restore_params(restorelist, removelist); // c:4094-4095
9927 }
9928 LASTVAL.store(1, Ordering::Relaxed); // c:4096
9929 fixfds(&save); // c:4097
9930 return execcmd_exec_done_path(
9931 redir_err,
9932 oautocont,
9933 how,
9934 &mut shti,
9935 &mut chti,
9936 &mut then_ts,
9937 forked,
9938 &mut newxtrerr,
9939 cflags,
9940 orig_cflags,
9941 is_cursh,
9942 do_exec,
9943 );
9944 }
9945 }
9946
9947 if is_shfunc != 0 {
9948 // c:4102-4105
9949 let mut a_vec: Vec<String> = args.clone().unwrap_or_default();
9950 // c:4104 — `execshfunc((Shfunc) hn, args);` C casts
9951 // HashNode hn to Shfunc; zshrs's hn is *mut builtin so
9952 // we re-resolve the shfunc by name from shfunctab and
9953 // dispatch through the top-level execshfunc port at
9954 // exec.rs:4978 (which routes to runshfunc).
9955 let name = args
9956 .as_ref()
9957 .and_then(|v| v.first())
9958 .cloned()
9959 .unwrap_or_default();
9960 let mut shf_clone: Option<shfunc> = if let Ok(tab) = shfunctab_lock().read() {
9961 tab.get(&name).cloned()
9962 } else {
9963 None
9964 };
9965 if let Some(ref mut shf) = shf_clone {
9966 execshfunc(shf, &mut a_vec);
9967 }
9968 // c:4105 — `pipecleanfilelist(filelist, 0);` — clean
9969 // out the proc_subst entries from the current job's
9970 // filelist after the shfunc body ran. Route through
9971 // `JOBTAB[thisjob]`.
9972 if let Some(jt) = JOBTAB.get() {
9973 let mut guard = jt.lock().unwrap();
9974 let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
9975 if tj >= 0 {
9976 if let Some(j) = guard.get_mut(tj as usize) {
9977 crate::ported::jobs::pipecleanfilelist(j, false);
9978 }
9979 }
9980 }
9981 } else {
9982 // c:4107 — builtin path.
9983 let mut assigns: Vec<crate::ported::zsh_h::asgment> = Vec::new(); // c:4108
9984 let postassigns = eparams.postassigns; // c:4109
9985 if forked != 0 {
9986 closem(FDT_INTERNAL, 0); // c:4111
9987 }
9988 if postassigns != 0 {
9989 // c:4112-4230 — typeset post-assignment processing.
9990 use crate::ported::zsh_h::{
9991 ASG_ARRAY, ASG_KEY_VALUE, EC_DUPTOK as ECDUPTOK_LOCAL, PREFORK_ASSIGN,
9992 PREFORK_KEY_VALUE, PREFORK_SINGLE, PREFORK_TYPESET, WC_ASSIGN_INC,
9993 WC_ASSIGN_NUM, WC_ASSIGN_SCALAR, WC_ASSIGN_TYPE, WC_ASSIGN_TYPE2,
9994 };
9995 let opc = state.pc; // c:4113
9996 state.pc = eparams.assignspc.unwrap_or(state.pc); // c:4114
9997 // c:4115 — `assigns = newlinklist();` — already declared above.
9998 let mut pa_remaining = postassigns;
9999 while pa_remaining > 0 {
10000 // c:4116 — `while (postassigns--)`
10001 pa_remaining -= 1;
10002 let mut pa_htok: i32 = 0; // c:4117
10003 if state.pc >= state.prog.prog.len() {
10004 break;
10005 }
10006 let ac = state.prog.prog[state.pc]; // c:4118
10007 state.pc += 1;
10008 let mut name = ecgetstr(state, ECDUPTOK_LOCAL, Some(&mut pa_htok)); // c:4119
10009 // c:4123-4124 DPUTS — debug assertion skipped.
10010 if pa_htok != 0 {
10011 // c:4126 — `init_list1(svl, name);`
10012 let mut svl: LinkList<String> = Default::default();
10013 svl.push_back(name.clone());
10014 // c:4127-4166 — INC-scalar special case (typeset $ass form).
10015 if WC_ASSIGN_TYPE(ac) == WC_ASSIGN_SCALAR
10016 && WC_ASSIGN_TYPE2(ac) == WC_ASSIGN_INC
10017 {
10018 // c:4141 — `(void)ecgetstr(...)` — dummy.
10019 let mut dummy_htok: i32 = 0;
10020 let _ = ecgetstr(state, ECDUPTOK_LOCAL, Some(&mut dummy_htok));
10021 let mut rf = 0i32;
10022 prefork(&mut svl, PREFORK_TYPESET, &mut rf); // c:4142
10023 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
10024 // c:4143
10025 state.pc = opc; // c:4144
10026 break;
10027 }
10028 let mut rf2 = 0i32;
10029 globlist(&mut svl, rf2); // c:4147
10030 let _ = &mut rf2;
10031 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
10032 // c:4148
10033 state.pc = opc; // c:4149
10034 break;
10035 }
10036 // c:4152-4165 — drain svl into assigns.
10037 while let Some(data) = svl.pop_front() {
10038 let (asg_name, asg_val): (String, Option<String>) =
10039 if let Some(eq_pos) = data.find('=') {
10040 // c:4156-4159
10041 (
10042 data[..eq_pos].to_string(),
10043 Some(data[eq_pos + 1..].to_string()),
10044 )
10045 } else {
10046 // c:4161-4162
10047 (data, None)
10048 };
10049 assigns.push(crate::ported::zsh_h::asgment {
10050 node: crate::ported::zsh_h::linknode {
10051 next: None,
10052 prev: None,
10053 dat: 0,
10054 },
10055 name: asg_name,
10056 flags: 0,
10057 scalar: asg_val,
10058 array: None,
10059 });
10060 }
10061 continue; // c:4166
10062 }
10063 // c:4168 — `prefork(&svl, PREFORK_SINGLE, NULL);`
10064 let mut rf = 0i32;
10065 prefork(&mut svl, PREFORK_SINGLE, &mut rf);
10066 // c:4169-4170 — `name = empty(svl) ? "" : firstnode_data;`
10067 name = if svl.is_empty() {
10068 String::new()
10069 } else {
10070 svl.pop_front().unwrap_or_default()
10071 };
10072 }
10073 // c:4172 — `untokenize(name);`
10074 // (untokenize is destructive on bytes; Rust untokenize
10075 // returns a new String — call and rebind.)
10076 name = untokenize(&name);
10077 let mut asg = crate::ported::zsh_h::asgment {
10078 node: crate::ported::zsh_h::linknode {
10079 next: None,
10080 prev: None,
10081 dat: 0,
10082 },
10083 name,
10084 flags: 0,
10085 scalar: None,
10086 array: None,
10087 };
10088 if WC_ASSIGN_TYPE(ac) == WC_ASSIGN_SCALAR {
10089 // c:4175
10090 let mut val_htok: i32 = 0;
10091 let mut val = ecgetstr(state, ECDUPTOK_LOCAL, Some(&mut val_htok)); // c:4176
10092 asg.flags = 0; // c:4177
10093 if WC_ASSIGN_TYPE2(ac) == WC_ASSIGN_INC {
10094 // c:4178-4180 — fake assignment, no value.
10095 asg.scalar = None;
10096 } else {
10097 if val_htok != 0 {
10098 // c:4183
10099 let mut svl: LinkList<String> = Default::default();
10100 svl.push_back(val.clone());
10101 let mut rf = 0i32;
10102 prefork(&mut svl, PREFORK_SINGLE | PREFORK_ASSIGN, &mut rf); // c:4184-4186
10103 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
10104 // c:4187
10105 state.pc = opc; // c:4188
10106 break;
10107 }
10108 // c:4195-4196 — `val = empty(svl) ? "" : firstdata;`
10109 val = if svl.is_empty() {
10110 String::new()
10111 } else {
10112 svl.pop_front().unwrap_or_default()
10113 };
10114 }
10115 // c:4198 — `untokenize(val);`
10116 asg.scalar = Some(untokenize(&val));
10117 }
10118 } else {
10119 // c:4202 — array assignment.
10120 asg.flags = ASG_ARRAY; // c:4202
10121 let mut arr_htok: i32 = 0;
10122 let arr_words = ecgetlist(
10123 state,
10124 WC_ASSIGN_NUM(ac) as usize,
10125 ECDUPTOK_LOCAL,
10126 Some(&mut arr_htok),
10127 ); // c:4204
10128 let mut arr_list: LinkList<String> = Default::default();
10129 for s in arr_words {
10130 arr_list.push_back(s);
10131 }
10132 if !arr_list.is_empty()
10133 && (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0
10134 {
10135 // c:4209 — `int prefork_ret = 0;`
10136 let mut prefork_ret = 0i32;
10137 prefork(&mut arr_list, PREFORK_ASSIGN, &mut prefork_ret); // c:4210-4211
10138 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
10139 // c:4212
10140 state.pc = opc; // c:4213
10141 break;
10142 }
10143 if (prefork_ret & PREFORK_KEY_VALUE) != 0 {
10144 // c:4216
10145 asg.flags |= ASG_KEY_VALUE; // c:4217
10146 }
10147 globlist(&mut arr_list, prefork_ret); // c:4218
10148 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
10149 // c:4220
10150 state.pc = opc; // c:4221
10151 break;
10152 }
10153 }
10154 asg.array = Some(arr_list);
10155 }
10156 // c:4227 — `uaddlinknode(assigns, &asg->node);`
10157 assigns.push(asg);
10158 }
10159 state.pc = opc; // c:4229
10160 }
10161 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0 {
10162 // c:4232
10163 let a_vec: Vec<String> = args.clone().unwrap_or_default();
10164 let ret = crate::ported::builtin::execbuiltin(
10165 a_vec,
10166 assigns,
10167 hn.unwrap_or(std::ptr::null_mut()),
10168 ); // c:4233
10169 if (errflag.load(Ordering::Relaxed) & ERRFLAG_INT) == 0 {
10170 // c:4238
10171 LASTVAL.store(ret, Ordering::Relaxed); // c:4239
10172 }
10173 }
10174 if (do_save & BINF_COMMAND as i32) != 0 {
10175 // c:4241
10176 errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed); // c:4242
10177 }
10178 // c:4244 fflush(stdout) — Rust stdio auto-flushes.
10179 // c:4245-4251 — write-error check on save[1].
10180 }
10181 if isset(PRINTEXITVALUE)
10182 && isset(SHINSTDIN)
10183 && LASTVAL.load(Ordering::Relaxed) != 0
10184 && subsh.load(Ordering::Relaxed) == 0
10185 {
10186 // c:4253-4255
10187 eprintln!("zsh: exit {}", LASTVAL.load(Ordering::Relaxed)); // c:4258
10188 }
10189
10190 if do_exec != 0 {
10191 // c:4263
10192 if subsh.load(Ordering::Relaxed) != 0 {
10193 crate::ported::builtin::_realexit(); // c:4264-4265
10194 }
10195 if isset(RCS)
10196 && crate::ported::zsh_h::interact()
10197 && nohistsave.load(Ordering::Relaxed) == 0
10198 {
10199 // c:4269
10200 crate::ported::hist::savehistfile(None, HFILE_USE_OPTIONS as i32);
10201 // c:4270
10202 }
10203 crate::ported::builtin::realexit(); // c:4271
10204 }
10205 if !restorelist.is_empty() {
10206 // c:4273
10207 restore_params(restorelist, removelist); // c:4274
10208 }
10209 } else {
10210 // c:4276 — external command execute.
10211 if subsh.load(Ordering::Relaxed) == 0 {
10212 // c:4277
10213 if forked == 0 {
10214 // c:4280 — `setiparam("SHLVL", --shlvl);`
10215 let cur = getsparam("SHLVL")
10216 .and_then(|s| s.parse::<i64>().ok())
10217 .unwrap_or(1);
10218 setiparam("SHLVL", cur - 1); // c:4281
10219 }
10220 if do_exec != 0
10221 && isset(RCS)
10222 && crate::ported::zsh_h::interact()
10223 && nohistsave.load(Ordering::Relaxed) == 0
10224 {
10225 // c:4285
10226 crate::ported::hist::savehistfile(None, HFILE_USE_OPTIONS as i32);
10227 // c:4286
10228 }
10229 }
10230 if typ == WC_SIMPLE as i32 || typ == WC_TYPESET as i32 {
10231 // c:4288
10232 if varspc.is_some() {
10233 // c:4289
10234 let mut addflags: i32 = ADDVAR_EXPORT; // c:4290
10235 if forked != 0 {
10236 addflags |= ADDVAR_RESTORE; // c:4292
10237 }
10238 addvars(state, varspc.unwrap_or(0), addflags); // c:4293
10239 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
10240 // c:4294
10241 std::process::exit(1); // c:4295
10242 }
10243 }
10244 closem(FDT_INTERNAL, 0); // c:4297
10245 // c:4298-4305 — close coprocin/coprocout.
10246 let cpi = crate::ported::modules::clone::coprocin.load(Ordering::Relaxed);
10247 if cpi != -1 {
10248 let _ = zclose(cpi); // c:4299
10249 crate::ported::modules::clone::coprocin.store(-1, Ordering::Relaxed);
10250 // c:4300
10251 }
10252 let cpo = crate::ported::modules::clone::coprocout.load(Ordering::Relaxed);
10253 if cpo != -1 {
10254 let _ = zclose(cpo); // c:4303
10255 crate::ported::modules::clone::coprocout.store(-1, Ordering::Relaxed);
10256 // c:4304
10257 }
10258 if forked == 0 {
10259 // c:4307
10260 setlimits(""); // c:4308
10261 }
10262 if (how & Z_ASYNC as i32) != 0 {
10263 // c:4310 — `zsfree(STTYval); STTYval = 0;`
10264 let mut guard = STTYval.lock().unwrap();
10265 *guard = None; // c:4311-4312
10266 }
10267 // c:4314 — `execute(args, cflags, use_defpath);`
10268 let mut a_vec: Vec<String> = args.clone().unwrap_or_default();
10269 execute(&mut a_vec, cflags, use_defpath); // c:4314
10270 } else {
10271 // c:4315 — `( ... )` — WC_SUBSH.
10272 list_pipe.store(0, Ordering::Relaxed); // c:4318
10273 // c:4319 — `pipecleanfilelist(filelist, 0);` — clean
10274 // proc-subst entries from the current job's filelist
10275 // before recursing into the subshell body.
10276 if let Some(jt) = JOBTAB.get() {
10277 let mut guard = jt.lock().unwrap();
10278 let tj = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
10279 if tj >= 0 {
10280 if let Some(j) = guard.get_mut(tj as usize) {
10281 crate::ported::jobs::pipecleanfilelist(j, false);
10282 }
10283 }
10284 }
10285 state.pc += 1; // c:4324 — `state->pc++;`
10286 let _ = execlist(state, 0, 1); // c:4325
10287 }
10288 }
10289 }
10290
10291 // c:4330-4404 — err: + done: + fatal:.
10292 return execcmd_exec_done_path(
10293 redir_err,
10294 oautocont,
10295 how,
10296 &mut shti,
10297 &mut chti,
10298 &mut then_ts,
10299 forked,
10300 &mut newxtrerr,
10301 cflags,
10302 orig_cflags,
10303 is_cursh,
10304 do_exec,
10305 );
10306}
10307
10308/// Internal helper modelling the C `done:` label tail of
10309/// `execcmd_exec` at `Src/exec.c:4366-4403`. Handles POSIX special-
10310/// builtin error escalation, AUTOCONTINUE restore, STTYval clear,
10311/// shelltime stop, and newxtrerr close.
10312#[allow(clippy::too_many_arguments)]
10313fn execcmd_exec_done_path(
10314 redir_err: i32,
10315 oautocont: i32,
10316 how: i32,
10317 shti: &mut crate::ported::jobs::timeinfo,
10318 chti: &mut crate::ported::jobs::timeinfo,
10319 then_ts: &mut std::time::Instant,
10320 forked: i32,
10321 newxtrerr: &mut Option<i32>,
10322 cflags: u32,
10323 orig_cflags: u32,
10324 is_cursh: i32,
10325 do_exec: i32,
10326) {
10327 use crate::ported::zsh_h::{
10328 AUTOCONTINUE, BINF_COMMAND, BINF_EXEC, BINF_PSPECIAL, INTERACTIVE, POSIXBUILTINS, Z_TIMED,
10329 };
10330 // c:4366
10331 // c:4367-4386 — POSIX special-builtin error escalation.
10332 if isset(POSIXBUILTINS)
10333 && (cflags & (BINF_PSPECIAL | BINF_EXEC)) != 0
10334 && (orig_cflags & BINF_COMMAND) == 0
10335 {
10336 // c:4367-4369
10337 let _forked_or_subsh = forked | zsh_subshell.load(Ordering::Relaxed); // c:4376
10338 // fatal: label entry point — same handling.
10339 if redir_err != 0 || (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
10340 // c:4378
10341 if !isset(INTERACTIVE) {
10342 // c:4379
10343 if _forked_or_subsh != 0 {
10344 unsafe { libc::_exit(1) }; // c:4381
10345 } else {
10346 std::process::exit(1); // c:4383
10347 }
10348 }
10349 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:4385
10350 }
10351 }
10352 // c:4388-4389 — `if ((is_cursh || do_exec) && (how & Z_TIMED)) shelltime(...);`
10353 if (is_cursh != 0 || do_exec != 0) && (how & Z_TIMED as i32) != 0 {
10354 crate::ported::jobs::shelltime(Some(shti), Some(chti), Some(then_ts), 1);
10355 // c:4389
10356 }
10357 // c:4390-4398 — newxtrerr close.
10358 if let Some(fd) = newxtrerr.take() {
10359 // c:4390
10360 let _ = zclose(fd); // c:4396
10361 }
10362 // c:4400-4401 — `zsfree(STTYval); STTYval = 0;`
10363 {
10364 let mut guard = STTYval.lock().unwrap();
10365 *guard = None;
10366 }
10367 // c:4402-4403 — `if (oautocont >= 0) opts[AUTOCONTINUE] = oautocont;`
10368 if oautocont >= 0 {
10369 opt_state_set("autocontinue", oautocont != 0);
10370 }
10371}
10372
10373/// Internal helper modelling the C `err:` label tail of
10374/// `execcmd_exec` at `Src/exec.c:4330-4365`. Forked-child fd cleanup
10375/// + waitjobs + _realexit; non-forked: `fixfds(save)` + fall through
10376/// to done:.
10377#[allow(clippy::too_many_arguments)]
10378fn execcmd_exec_err_path(
10379 forked: i32,
10380 save: &mut [i32; 10],
10381 mfds: &mut [Option<Box<multio>>; 10],
10382 oautocont: i32,
10383 how: i32,
10384 shti: &mut crate::ported::jobs::timeinfo,
10385 chti: &mut crate::ported::jobs::timeinfo,
10386 then_ts: &mut std::time::Instant,
10387 newxtrerr: &mut Option<i32>,
10388 cflags: u32,
10389 orig_cflags: u32,
10390 is_cursh: i32,
10391 do_exec: i32,
10392 redir_err: i32,
10393) {
10394 use crate::ported::zsh_h::FDT_UNUSED;
10395 // c:4330
10396 if forked != 0 {
10397 // c:4331
10398 // c:4356-4358 — close all fds 0..10 whose fdtable entry != FDT_UNUSED.
10399 let mut i: i32 = 0;
10400 while i < 10 {
10401 if fdtable_get(i) != FDT_UNUSED {
10402 unsafe { libc::close(i) }; // c:4358
10403 }
10404 i += 1;
10405 }
10406 // c:4359 — `closem(FDT_UNUSED, 1);`
10407 closem(FDT_UNUSED, 1); // c:4359
10408 // c:4360-4361 — `if (thisjob != -1) waitjobs();`
10409 let thisjob = THISJOB.get().map(|m| *m.lock().unwrap()).unwrap_or(-1);
10410 if thisjob != -1 {
10411 if let Some(jt) = JOBTAB.get() {
10412 let mut guard = jt.lock().unwrap();
10413 crate::ported::jobs::waitjobs(&mut guard, thisjob as usize); // c:4361
10414 }
10415 }
10416 crate::ported::builtin::_realexit(); // c:4362
10417 }
10418 fixfds(save); // c:4364
10419
10420 execcmd_exec_done_path(
10421 redir_err,
10422 oautocont,
10423 how,
10424 shti,
10425 chti,
10426 then_ts,
10427 forked,
10428 newxtrerr,
10429 cflags,
10430 orig_cflags,
10431 is_cursh,
10432 do_exec,
10433 );
10434}
10435
10436/// Internal helper dispatching `execfuncs[type - WC_CURSH]` from
10437/// `Src/exec.c:170-180`. Each branch maps to the ported wordcode-
10438/// walker function in `src/ported/exec.rs`.
10439fn dispatch_execfuncs(state: &mut estate, typ: i32, do_exec: i32) -> i32 {
10440 use crate::ported::zsh_h::{
10441 WC_ARITH, WC_AUTOFN, WC_CASE, WC_COND, WC_CURSH, WC_FOR, WC_FUNCDEF, WC_IF, WC_REPEAT,
10442 WC_SELECT, WC_SUBSH, WC_TIMED, WC_TRY, WC_WHILE,
10443 };
10444 // Port of `static int (*const execfuncs[])(Estate, int)` dispatch
10445 // table at `Src/exec.c:170-180`. C indexes by `(type - WC_CURSH)`;
10446 // Rust matches on the WC_* tag directly.
10447 match typ as wordcode {
10448 x if x == WC_CURSH => execcursh(state, do_exec),
10449 x if x == WC_FOR => execfor(state, do_exec),
10450 x if x == WC_SELECT => execselect(state, do_exec),
10451 x if x == WC_WHILE => execwhile(state, do_exec),
10452 x if x == WC_REPEAT => execrepeat(state, do_exec),
10453 x if x == WC_CASE => execcase(state, do_exec),
10454 x if x == WC_IF => execif(state, do_exec),
10455 x if x == WC_COND => execcond(state, do_exec),
10456 x if x == WC_ARITH => execarith(state, do_exec),
10457 x if x == WC_TRY => exectry(state, do_exec),
10458 x if x == WC_FUNCDEF => execfuncdef(state, None),
10459 // c:272 — execfuncs[] table dispatches `WC_AUTOFN` to
10460 // `execautofn` (the loadautofn-then-basic wrapper), not
10461 // `execautofn_basic` directly.
10462 x if x == WC_AUTOFN => execautofn(state, do_exec),
10463 x if x == WC_TIMED => exectime(state, do_exec),
10464 x if x == WC_SUBSH => execcursh(state, do_exec), // c:269 — same handler.
10465 _ => 0,
10466 }
10467}
10468
10469/// Port of `Eprog stripkshdef(Eprog prog, char *name)` from
10470/// `Src/exec.c:6286-6364`. Given an Eprog read from an autoload
10471/// file plus the function name being defined, check whether the
10472/// file consists of *exactly* one `function NAME { … }` definition
10473/// for that name. If so, return a new Eprog whose `prog`/`strs`/
10474/// `pats` slice out just the function body (so calling code can
10475/// invoke the body directly instead of re-parsing). Otherwise
10476/// return the input untouched.
10477///
10478/// Header word layout consumed (matches C `pc[…]` reads):
10479/// pc[0] = WC_LIST with `Z_SYNC|Z_END|Z_SIMPLE` flags
10480/// pc[1] = (sublist header, skipped)
10481/// pc[2] = WC_FUNCDEF
10482/// pc[3] = 1 (single-name funcdef)
10483/// pc[4] = name-string slot (compared to `name`)
10484/// pc[5] = sbeg (offset into strs table)
10485/// pc[6] = nstrs (bytes of strs to copy)
10486/// pc[7] = npats (number of pattern slots to allocate)
10487/// pc[8] = WC_FUNCDEF_SKIP target (end-of-funcdef pc)
10488/// pc[9] = (unused header word — `pc += 6` lands here as the
10489/// start of the body wordcode stream)
10490///
10491/// Returns `None` only when the input was `None` (matches C
10492/// `return NULL`). Equivalence between the original `prog` and a
10493/// successfully stripped `prog` is *not* preserved at the pointer
10494/// level (C may return the original Eprog when the file fails the
10495/// single-funcdef shape check; this Rust port does the same by
10496/// passing the box back through).
10497///
10498/// `EF_MAP` (`zcompile`d / mmap'd Eprog) path: C mutates the
10499/// existing Eprog in place, swapping its `prog` / `strs` /
10500/// `pats` to slice into the funcdef body. Rust mirrors this on
10501/// the moved-in `Box<eprog>` (no separate `free()` needed —
10502/// `Vec` drop handles the old `pats`).
10503pub fn stripkshdef(
10504 prog: Option<crate::ported::zsh_h::Eprog>,
10505 name: &str,
10506) -> Option<crate::ported::zsh_h::Eprog> {
10507 use crate::ported::parse::ecrawstr;
10508 use crate::ported::zsh_h::{
10509 wc_code, wordcode, Dash, EF_HEAP, EF_MAP, WC_FUNCDEF, WC_FUNCDEF_SKIP, WC_LIST,
10510 WC_LIST_TYPE, Z_END, Z_SIMPLE, Z_SYNC,
10511 };
10512
10513 // c:6300 — `if (!prog) return NULL;`
10514 let mut prog = prog?;
10515
10516 // c:6302-6306 — first word must be WC_LIST with all of
10517 // Z_SYNC|Z_END|Z_SIMPLE set (i.e. the trivial "single simple
10518 // sublist" wrapper around the funcdef).
10519 if prog.prog.len() < 3 {
10520 return Some(prog);
10521 }
10522 let code0: wordcode = prog.prog[0];
10523 if wc_code(code0) != WC_LIST
10524 || (WC_LIST_TYPE(code0) & (Z_SYNC | Z_END | Z_SIMPLE) as wordcode)
10525 != (Z_SYNC | Z_END | Z_SIMPLE) as wordcode
10526 {
10527 return Some(prog);
10528 }
10529 // c:6307 — `pc++;` (skip the sublist header word at pc[1]).
10530 // c:6308 — `code = *pc++;` lands `code` on pc[2], leaving the
10531 // walking cursor at pc[3] which is read directly below.
10532 let code: wordcode = prog.prog[2];
10533 let pc_after_code: usize = 3;
10534 if wc_code(code) != WC_FUNCDEF || prog.prog[pc_after_code] != 1 {
10535 return Some(prog);
10536 }
10537
10538 // c:6320 — `ptr2 = ecrawstr(prog, pc + 1, NULL);` (note: C's
10539 // `pc` is already past `code`, so `pc + 1` lands on pc[4] —
10540 // the name-string slot).
10541 let name_slot = pc_after_code + 1; // == 4
10542 let name_in_def = ecrawstr(&prog, name_slot, None);
10543
10544 // c:6320-6328 — name match, tolerating Dash-tokenised hyphens
10545 // on either side.
10546 let n1 = name.as_bytes();
10547 let n2 = name_in_def.as_bytes();
10548 let mut i = 0usize;
10549 let mut j = 0usize;
10550 while i < n1.len() && j < n2.len() {
10551 let c1 = n1[i] as char;
10552 let c2 = n2[j] as char;
10553 if c1 != c2 && c1 != Dash && c1 != '-' && c2 != Dash && c2 != '-' {
10554 break;
10555 }
10556 i += 1;
10557 j += 1;
10558 }
10559 // c:6329 — `if (*ptr1 || *ptr2) return prog;` (any unmatched
10560 // tail on either side → not the right funcdef).
10561 if i < n1.len() || j < n2.len() {
10562 return Some(prog);
10563 }
10564
10565 // c:6332-6362 — slice the funcdef body out. Layout:
10566 // sbeg = pc[2] (in C, == prog.prog[pc_after_code + 2] == [5])
10567 // nstrs = pc[3] (== [6])
10568 // npats = pc[4] (== [7])
10569 // end = pc + WC_FUNCDEF_SKIP(code) (== pc_after_code + skip)
10570 // pc += 6 (body wordcode begins at pc_after_code + 6 == [9])
10571 let sbeg = prog.prog[pc_after_code + 2] as usize;
10572 let nstrs = prog.prog[pc_after_code + 3] as usize;
10573 let npats = prog.prog[pc_after_code + 4] as i32;
10574 let skip = WC_FUNCDEF_SKIP(code) as usize;
10575 let end_pc = pc_after_code + skip;
10576 let body_start = pc_after_code + 6;
10577 if end_pc < body_start || end_pc > prog.prog.len() {
10578 // Defensive: malformed header — return input untouched so
10579 // the caller's parse-eprog fallback re-reads from source.
10580 return Some(prog);
10581 }
10582 let nprg = end_pc - body_start;
10583 let plen = nprg * size_of::<wordcode>();
10584 let len = plen + (npats as usize) * size_of::<usize>() + nstrs;
10585
10586 // Build the new pats slice — `dummy_patprog1` slots in C; the
10587 // Rust convention (mirrors `dupeprog` at parse.rs:2716) is to
10588 // synthesize zero-initialised patprog placeholders that
10589 // pattern compile-on-first-use will overwrite.
10590 let dummy_pat = || {
10591 Box::new(crate::ported::zsh_h::patprog {
10592 startoff: 0,
10593 size: 0,
10594 mustoff: 0,
10595 patmlen: 0,
10596 globflags: 0,
10597 globend: 0,
10598 flags: 0,
10599 patnpar: 0,
10600 patstartch: 0,
10601 })
10602 };
10603 let new_pats: Vec<crate::ported::zsh_h::Patprog> =
10604 (0..npats.max(0)).map(|_| dummy_pat()).collect();
10605
10606 // c:6353 — `ret->strs = prog->strs + sbeg;` (EF_MAP) or
10607 // c:6359 — `memcpy(ret->strs, prog->strs + sbeg, nstrs);` (heap).
10608 let old_strs = prog.strs.take().unwrap_or_default();
10609 let old_bytes = old_strs.as_bytes();
10610 let new_strs = if sbeg + nstrs <= old_bytes.len() {
10611 Some(String::from_utf8_lossy(&old_bytes[sbeg..sbeg + nstrs]).into_owned())
10612 } else {
10613 Some(String::new())
10614 };
10615
10616 let new_prog: Vec<wordcode> = prog.prog[body_start..end_pc].to_vec();
10617
10618 if (prog.flags & EF_MAP) != 0 {
10619 // c:6349-6354 — in-place EF_MAP path.
10620 prog.pats = new_pats;
10621 prog.prog = new_prog;
10622 prog.strs = new_strs;
10623 prog.len = len as i32;
10624 prog.npats = npats;
10625 prog.shf = None;
10626 return Some(prog);
10627 }
10628
10629 // c:6356-6361 — heap-allocated new Eprog.
10630 let ret = Box::new(eprog {
10631 flags: EF_HEAP,
10632 len: len as i32,
10633 npats,
10634 nref: -1, // c:6363 (heap path → never refcount-freed).
10635 pats: new_pats,
10636 prog: new_prog,
10637 strs: new_strs,
10638 shf: None, // c:6363
10639 dump: None,
10640 });
10641 Some(ret)
10642}
10643
10644#[cfg(test)]
10645mod tests {
10646 use super::*;
10647
10648 // ─── zsh-corpus pins for pure exec helpers ─────────────────────
10649
10650 /// `Src/exec.c:996-1010` — `isrelative` returns 1 for empty.
10651 #[test]
10652 fn exec_corpus_isrelative_empty_is_one() {
10653 let _g = crate::test_util::global_state_lock();
10654 assert_eq!(isrelative(""), 1, "empty path is relative");
10655 }
10656
10657 /// `isrelative("foo")` = 1 (no leading slash).
10658 #[test]
10659 fn exec_corpus_isrelative_bare_name_is_one() {
10660 let _g = crate::test_util::global_state_lock();
10661 assert_eq!(isrelative("foo"), 1);
10662 assert_eq!(isrelative("bin/cmd"), 1);
10663 }
10664
10665 /// `isrelative("/foo")` = 0 (absolute, no `./` / `../`).
10666 #[test]
10667 fn exec_corpus_isrelative_absolute_clean_is_zero() {
10668 let _g = crate::test_util::global_state_lock();
10669 assert_eq!(isrelative("/foo"), 0, "/foo is absolute");
10670 assert_eq!(isrelative("/bin/ls"), 0);
10671 assert_eq!(isrelative("/"), 0, "root is absolute");
10672 }
10673
10674 /// `isrelative("/foo/../bar")` = 1 (contains `../` component).
10675 #[test]
10676 fn exec_corpus_isrelative_absolute_with_dotdot_is_one() {
10677 let _g = crate::test_util::global_state_lock();
10678 assert_eq!(
10679 isrelative("/foo/../bar"),
10680 1,
10681 "absolute path with ../ is still 'relative' per zsh"
10682 );
10683 }
10684
10685 /// `isrelative("/foo/./bar")` = 1 (contains `./` component).
10686 #[test]
10687 fn exec_corpus_isrelative_absolute_with_dot_is_one() {
10688 let _g = crate::test_util::global_state_lock();
10689 assert_eq!(
10690 isrelative("/./x"),
10691 1,
10692 "absolute with ./ component reported relative"
10693 );
10694 }
10695
10696 /// `Src/exec.c:5300` — `is_anonymous_function_name("(anon)")` = 1.
10697 #[test]
10698 fn exec_corpus_is_anonymous_function_name_matches_sentinel() {
10699 assert_eq!(is_anonymous_function_name("(anon)"), 1);
10700 }
10701
10702 /// `is_anonymous_function_name("regular_name")` = 0.
10703 #[test]
10704 fn exec_corpus_is_anonymous_function_name_rejects_normal() {
10705 assert_eq!(is_anonymous_function_name("regular_name"), 0);
10706 assert_eq!(is_anonymous_function_name(""), 0);
10707 assert_eq!(
10708 is_anonymous_function_name("anon"),
10709 0,
10710 "plain 'anon' (no parens) is NOT the sentinel"
10711 );
10712 }
10713
10714 /// `iscom("/nonexistent/never_a_path")` = false.
10715 #[test]
10716 fn exec_corpus_iscom_missing_path_false() {
10717 assert!(!iscom("/this/path/does/not/exist/zshrs_xyz"));
10718 }
10719
10720 /// `iscom("/tmp")` is a directory not a regular file → false.
10721 #[test]
10722 fn exec_corpus_iscom_directory_false() {
10723 assert!(!iscom("/tmp"), "/tmp is a dir, not a regular command");
10724 }
10725
10726 /// `iscom("/bin/sh")` is true on POSIX systems.
10727 #[test]
10728 fn exec_corpus_iscom_known_binary_true() {
10729 // /bin/sh exists on all POSIX systems with X perms.
10730 if std::path::Path::new("/bin/sh").exists() {
10731 assert!(iscom("/bin/sh"), "/bin/sh is a real executable");
10732 }
10733 }
10734
10735 // ─── stripkshdef (Src/exec.c:6286) early-return paths ──────────
10736
10737 /// `stripkshdef(None, "foo")` → `None` (matches C `if (!prog)
10738 /// return NULL;` at exec.c:6300).
10739 #[test]
10740 fn exec_corpus_stripkshdef_null_input_returns_none() {
10741 assert!(stripkshdef(None, "foo").is_none());
10742 }
10743
10744 /// `stripkshdef` on an empty/degenerate Eprog returns the same
10745 /// Eprog unchanged (no funcdef-shape to strip).
10746 #[test]
10747 fn exec_corpus_stripkshdef_empty_prog_returns_input() {
10748 let prog = Box::new(eprog {
10749 prog: vec![],
10750 ..Default::default()
10751 });
10752 let out = stripkshdef(Some(prog), "foo");
10753 assert!(out.is_some(), "empty prog → returned unchanged");
10754 assert!(out.unwrap().prog.is_empty(), "no mutation");
10755 }
10756
10757 /// `stripkshdef` on a non-WC_LIST head returns the input
10758 /// untouched (early return at exec.c:6304-6306).
10759 #[test]
10760 fn exec_corpus_stripkshdef_non_list_head_returns_input() {
10761 use crate::ported::zsh_h::{wc_bld, WC_SUBLIST};
10762 let prog = Box::new(eprog {
10763 prog: vec![wc_bld(WC_SUBLIST, 0), 0, 0],
10764 ..Default::default()
10765 });
10766 let out = stripkshdef(Some(prog), "foo");
10767 assert!(out.is_some());
10768 // first word is the WC_SUBLIST sentinel we passed in,
10769 // unchanged (the function bailed before doing any slicing).
10770 let p = out.unwrap();
10771 use crate::ported::zsh_h::wc_code;
10772 assert_eq!(
10773 wc_code(p.prog[0]),
10774 WC_SUBLIST,
10775 "header word preserved verbatim"
10776 );
10777 }
10778
10779 // ═══════════════════════════════════════════════════════════════════
10780 // C-parity tests pinning Src/exec.c. Tests that capture KNOWN
10781 // ZSHRS BUGS use #[ignore = "ZSHRS BUG: …"].
10782 // ═══════════════════════════════════════════════════════════════════
10783
10784 /// `isrelative("/abs/path")` returns 0 (false = absolute path).
10785 /// C `Src/exec.c:996-1006` — leading `/` and no `.`/`..` components.
10786 #[test]
10787 fn isrelative_absolute_path_returns_zero() {
10788 let _g = crate::test_util::global_state_lock();
10789 assert_eq!(isrelative("/usr/local/bin"), 0);
10790 }
10791
10792 /// `isrelative("foo/bar")` returns 1 (no leading slash).
10793 #[test]
10794 fn isrelative_no_leading_slash_returns_one() {
10795 let _g = crate::test_util::global_state_lock();
10796 assert_eq!(isrelative("foo/bar"), 1);
10797 }
10798
10799 /// `isrelative("/foo/./bar")` returns 1 — contains `/./` walk.
10800 /// C c:1001 — `.` with prev `/` + next `/` triggers relative flag.
10801 #[test]
10802 fn isrelative_dot_component_returns_one() {
10803 let _g = crate::test_util::global_state_lock();
10804 assert_eq!(isrelative("/foo/./bar"), 1, "/./ in path → relative");
10805 }
10806
10807 /// `isrelative("/foo/../bar")` returns 1 — contains `/..` walk.
10808 #[test]
10809 fn isrelative_dotdot_component_returns_one() {
10810 let _g = crate::test_util::global_state_lock();
10811 assert_eq!(isrelative("/foo/../bar"), 1, "/../ in path → relative");
10812 }
10813
10814 /// `isrelative("")` returns 1 — empty input has no leading `/`.
10815 /// C c:998 — `*s != '/'` includes the NUL terminator case.
10816 #[test]
10817 fn isrelative_empty_returns_one() {
10818 let _g = crate::test_util::global_state_lock();
10819 assert_eq!(isrelative(""), 1, "empty string → not absolute");
10820 }
10821
10822 /// `isrelative("/a/.b")` returns 0 — `.b` is NOT a `/./` walk
10823 /// (followed by another non-`/` char `b`).
10824 #[test]
10825 fn isrelative_dotfile_in_path_returns_zero() {
10826 let _g = crate::test_util::global_state_lock();
10827 assert_eq!(
10828 isrelative("/usr/.config/zsh"),
10829 0,
10830 "dotfile name '.config' is NOT a relative walk"
10831 );
10832 }
10833
10834 /// `is_anonymous_function_name("(anon)")` returns 1 (true).
10835 /// C `Src/exec.c` — `!strcmp(name, ANONYMOUS_FUNCTION_NAME)`.
10836 #[test]
10837 fn is_anonymous_function_name_anon_returns_one() {
10838 let _g = crate::test_util::global_state_lock();
10839 assert_eq!(is_anonymous_function_name("(anon)"), 1);
10840 }
10841
10842 /// `is_anonymous_function_name("foo")` returns 0 (false).
10843 #[test]
10844 fn is_anonymous_function_name_normal_returns_zero() {
10845 let _g = crate::test_util::global_state_lock();
10846 assert_eq!(is_anonymous_function_name("foo"), 0);
10847 assert_eq!(is_anonymous_function_name(""), 0);
10848 assert_eq!(is_anonymous_function_name("(other)"), 0);
10849 }
10850
10851 /// `isgooderr(EACCES, "/no/such/dir")` returns true when the dir
10852 /// is not actually accessible. C `Src/exec.c:isgooderr` filters
10853 /// out "unreadable / not directory" errnos so caller doesn't
10854 /// emit spurious warnings.
10855 #[test]
10856 #[ignore = "ZSHRS BUG: isgooderr exact semantics need verification — C: `((e != EACCES || !access(dir, X_OK)) && e != ENOENT && e != ENOTDIR)`"]
10857 fn isgooderr_eacces_unreadable_dir_returns_false() {
10858 let _g = crate::test_util::global_state_lock();
10859 // /no/such/dir doesn't exist → access(X_OK) fails non-zero
10860 // → !access() is 0 (false) → returns false.
10861 assert!(
10862 !isgooderr(libc::EACCES, "/no/such/dir/zshrs_test"),
10863 "unreadable dir with EACCES should NOT be 'good error'"
10864 );
10865 }
10866
10867 // ═══════════════════════════════════════════════════════════════════
10868 // Additional C-parity tests for Src/exec.c basic accessors/predicates.
10869 // ═══════════════════════════════════════════════════════════════════
10870
10871 /// c:658 — `isgooderr(ENOENT, _)` always false (regardless of dir).
10872 /// Pin: ENOENT is NEVER a "good error" because the path itself
10873 /// doesn't exist — caller should suppress the warning.
10874 #[test]
10875 fn isgooderr_enoent_always_false() {
10876 let _g = crate::test_util::global_state_lock();
10877 assert!(!isgooderr(libc::ENOENT, "/tmp"));
10878 assert!(!isgooderr(libc::ENOENT, "/no/such/dir"));
10879 assert!(!isgooderr(libc::ENOENT, ""));
10880 }
10881
10882 /// c:658 — `isgooderr(ENOTDIR, _)` always false. A path component
10883 /// being a non-dir is a structural error, not a permission issue.
10884 #[test]
10885 fn isgooderr_enotdir_always_false() {
10886 let _g = crate::test_util::global_state_lock();
10887 assert!(!isgooderr(libc::ENOTDIR, "/tmp"));
10888 assert!(!isgooderr(libc::ENOTDIR, "/"));
10889 }
10890
10891 /// c:658 — Other errnos (EPERM, EIO, ENOMEM) are "good errors"
10892 /// because they're not the suppressed three (EACCES/ENOENT/ENOTDIR).
10893 #[test]
10894 fn isgooderr_other_errno_returns_true() {
10895 let _g = crate::test_util::global_state_lock();
10896 assert!(isgooderr(libc::EPERM, "/tmp"));
10897 assert!(isgooderr(libc::EIO, "/tmp"));
10898 assert!(isgooderr(libc::ENOMEM, "/tmp"));
10899 }
10900
10901 /// c:962 — `iscom("/tmp")` returns false (directory, not S_ISREG).
10902 #[test]
10903 fn iscom_directory_returns_false() {
10904 let _g = crate::test_util::global_state_lock();
10905 assert!(!iscom("/tmp"));
10906 assert!(!iscom("/"));
10907 }
10908
10909 /// c:962 — `iscom` on non-existent path returns false (access
10910 /// X_OK fails).
10911 #[test]
10912 fn iscom_nonexistent_path_returns_false() {
10913 let _g = crate::test_util::global_state_lock();
10914 assert!(!iscom("/no/such/path/zshrs_iscom_test"));
10915 assert!(!iscom(""));
10916 }
10917
10918 /// c:962 — `iscom("/bin/sh")` returns true on every POSIX system.
10919 #[test]
10920 #[cfg(unix)]
10921 fn iscom_bin_sh_returns_true() {
10922 let _g = crate::test_util::global_state_lock();
10923 // /bin/sh is a POSIX-required executable.
10924 assert!(iscom("/bin/sh"), "/bin/sh must be executable on POSIX");
10925 }
10926
10927 /// c:5300 — anonymous function name is exactly "(anon)" — must
10928 /// not match prefixes/suffixes/case variants.
10929 #[test]
10930 fn is_anonymous_function_name_strict_match_only() {
10931 let _g = crate::test_util::global_state_lock();
10932 assert_eq!(is_anonymous_function_name("(anon"), 0, "no trailing paren");
10933 assert_eq!(is_anonymous_function_name("anon)"), 0, "no leading paren");
10934 assert_eq!(is_anonymous_function_name("(ANON)"), 0, "wrong case");
10935 assert_eq!(
10936 is_anonymous_function_name(" (anon) "),
10937 0,
10938 "leading/trailing space"
10939 );
10940 assert_eq!(is_anonymous_function_name("(anon) "), 0, "trailing space");
10941 assert_eq!(is_anonymous_function_name(" (anon)"), 0, "leading space");
10942 }
10943
10944 /// c:5289 — `ANONYMOUS_FUNCTION_NAME` constant is exactly `"(anon)"`.
10945 /// Pin so a regen that flips parens / changes case / adds prefix
10946 /// would be caught.
10947 #[test]
10948 fn anonymous_function_name_const_is_literal_anon() {
10949 let _g = crate::test_util::global_state_lock();
10950 assert_eq!(ANONYMOUS_FUNCTION_NAME, "(anon)");
10951 }
10952
10953 /// c:147-148 — `isrelative("./")` returns 1 (dot-slash prefix
10954 /// is the canonical relative-path form).
10955 #[test]
10956 fn isrelative_dot_slash_returns_one() {
10957 let _g = crate::test_util::global_state_lock();
10958 assert_eq!(isrelative("./foo"), 1);
10959 assert_eq!(isrelative("./"), 1);
10960 }
10961
10962 /// c:147-148 — `isrelative("../foo")` returns 1.
10963 #[test]
10964 fn isrelative_dotdot_slash_returns_one() {
10965 let _g = crate::test_util::global_state_lock();
10966 assert_eq!(isrelative("../foo"), 1);
10967 assert_eq!(isrelative("../"), 1);
10968 }
10969
10970 /// c:147-148 — `/.foo` (hidden file under root) is absolute.
10971 /// Pin: only `/.` (with trailing `/`) or end-of-string counts as
10972 /// a `.` component, NOT `/.foo` (which is a normal file `.foo`).
10973 #[test]
10974 fn isrelative_root_hidden_file_returns_zero() {
10975 let _g = crate::test_util::global_state_lock();
10976 assert_eq!(isrelative("/.foo"), 0, "/.foo is absolute path to dotfile");
10977 assert_eq!(isrelative("/.bashrc"), 0, "/.bashrc is absolute");
10978 }
10979
10980 /// c:147-148 — `/..bar` (file named `..bar`) is also absolute,
10981 /// since `..bar` is a regular file name, not a `..` component.
10982 #[test]
10983 fn isrelative_root_double_dot_file_returns_zero() {
10984 let _g = crate::test_util::global_state_lock();
10985 assert_eq!(isrelative("/..bar"), 0);
10986 }
10987
10988 /// c:2652 — `setunderscore("")` clears `zunderscore` and resets
10989 /// `underscoreused` to 1 (null terminator only).
10990 #[test]
10991 fn setunderscore_empty_clears_state() {
10992 let _g = crate::test_util::global_state_lock();
10993 setunderscore(""); // initialize to known empty state
10994 let zu = zunderscore.lock().unwrap();
10995 assert!(zu.is_empty(), "zunderscore must be empty after clear");
10996 drop(zu);
10997 let used = underscoreused.load(Ordering::Relaxed);
10998 assert_eq!(used, 1, "underscoreused must be 1 (NUL only) after clear");
10999 }
11000
11001 /// c:2652 — `setunderscore(str)` sets `zunderscore=str` and
11002 /// `underscoreused = str.len()+1` (string + null terminator).
11003 #[test]
11004 fn setunderscore_with_value_stores_string_and_length() {
11005 let _g = crate::test_util::global_state_lock();
11006 setunderscore("hello");
11007 let zu = zunderscore.lock().unwrap();
11008 assert_eq!(*zu, "hello");
11009 drop(zu);
11010 let used = underscoreused.load(Ordering::Relaxed);
11011 assert_eq!(used, 6, "len('hello')+1 = 6");
11012 }
11013
11014 /// c:2656 — `underscorelen` is rounded up to 32-byte boundary
11015 /// for the bump-allocator-friendly buffer growth.
11016 #[test]
11017 fn setunderscore_rounds_underscorelen_to_32() {
11018 let _g = crate::test_util::global_state_lock();
11019 setunderscore("ab"); // len 2 + 1 = 3 → ceil(32) = 32
11020 let nl = underscorelen.load(Ordering::Relaxed);
11021 assert_eq!(nl, 32, "(2+1+31) & !31 = 32");
11022 }
11023
11024 // ═══════════════════════════════════════════════════════════════════
11025 // Additional C-parity tests for Src/exec.c cancd2 +
11026 // quote_tokenized_output.
11027 // ═══════════════════════════════════════════════════════════════════
11028
11029 /// c:6411 — `cancd2("/tmp")` returns 1 (directory with X_OK exists).
11030 #[test]
11031 #[cfg(unix)]
11032 fn cancd2_existing_dir_returns_one() {
11033 let _g = crate::test_util::global_state_lock();
11034 assert_eq!(cancd2("/tmp"), 1, "/tmp is a valid cd target");
11035 }
11036
11037 /// c:6411 — `cancd2("/nonexistent")` returns 0.
11038 #[test]
11039 fn cancd2_nonexistent_returns_zero() {
11040 let _g = crate::test_util::global_state_lock();
11041 assert_eq!(cancd2("/__never_exists_zshrs_cancd2__"), 0);
11042 }
11043
11044 /// c:6411 — `cancd2` for a file (not dir) returns 0.
11045 #[test]
11046 #[cfg(unix)]
11047 fn cancd2_regular_file_returns_zero() {
11048 let _g = crate::test_util::global_state_lock();
11049 let dir = tempfile::tempdir().unwrap();
11050 let p = dir.path().join("regular_file");
11051 std::fs::write(&p, "x").unwrap();
11052 assert_eq!(
11053 cancd2(p.to_str().unwrap()),
11054 0,
11055 "regular file not a cd target"
11056 );
11057 }
11058
11059 /// c:2114 — `quote_tokenized_output` on empty string writes nothing.
11060 #[test]
11061 fn quote_tokenized_output_empty_writes_nothing() {
11062 let _g = crate::test_util::global_state_lock();
11063 let mut buf = Vec::new();
11064 quote_tokenized_output("", &mut buf).unwrap();
11065 assert!(buf.is_empty());
11066 }
11067
11068 /// c:2114 — plain ASCII passes through unchanged.
11069 #[test]
11070 fn quote_tokenized_output_plain_ascii_unchanged() {
11071 let _g = crate::test_util::global_state_lock();
11072 let mut buf = Vec::new();
11073 quote_tokenized_output("hello", &mut buf).unwrap();
11074 assert_eq!(buf, b"hello");
11075 }
11076
11077 /// c:2143 — space gets backslash-quoted.
11078 #[test]
11079 fn quote_tokenized_output_space_backslash_quoted() {
11080 let _g = crate::test_util::global_state_lock();
11081 let mut buf = Vec::new();
11082 quote_tokenized_output("a b", &mut buf).unwrap();
11083 assert_eq!(buf, b"a\\ b");
11084 }
11085
11086 /// c:2147 — tab → $'\\t'.
11087 #[test]
11088 fn quote_tokenized_output_tab_dollar_escape() {
11089 let _g = crate::test_util::global_state_lock();
11090 let mut buf = Vec::new();
11091 quote_tokenized_output("a\tb", &mut buf).unwrap();
11092 assert_eq!(buf, b"a$'\\t'b");
11093 }
11094
11095 /// c:2151 — newline → $'\\n'.
11096 #[test]
11097 fn quote_tokenized_output_newline_dollar_escape() {
11098 let _g = crate::test_util::global_state_lock();
11099 let mut buf = Vec::new();
11100 quote_tokenized_output("a\nb", &mut buf).unwrap();
11101 assert_eq!(buf, b"a$'\\n'b");
11102 }
11103
11104 /// c:2155 — CR → $'\\r'.
11105 #[test]
11106 fn quote_tokenized_output_cr_dollar_escape() {
11107 let _g = crate::test_util::global_state_lock();
11108 let mut buf = Vec::new();
11109 quote_tokenized_output("a\rb", &mut buf).unwrap();
11110 assert_eq!(buf, b"a$'\\r'b");
11111 }
11112
11113 /// c:2128 — shell metacharacters all get backslash-quoted.
11114 #[test]
11115 fn quote_tokenized_output_shell_metas_get_backslash() {
11116 let _g = crate::test_util::global_state_lock();
11117 for c in &[b'<', b'>', b'(', b')', b'|', b'#', b'$', b'*', b'?', b'~'] {
11118 let mut buf = Vec::new();
11119 let s = String::from_utf8(vec![b'a', *c, b'b']).unwrap();
11120 quote_tokenized_output(&s, &mut buf).unwrap();
11121 assert_eq!(buf, vec![b'a', b'\\', *c, b'b'], "char {:?}", *c as char);
11122 }
11123 }
11124
11125 /// c:2158 — `=` at position 0 gets quoted (path-spec).
11126 #[test]
11127 fn quote_tokenized_output_equals_at_start_quoted() {
11128 let _g = crate::test_util::global_state_lock();
11129 let mut buf = Vec::new();
11130 quote_tokenized_output("=foo", &mut buf).unwrap();
11131 assert_eq!(buf, b"\\=foo");
11132 }
11133
11134 // ═══════════════════════════════════════════════════════════════════
11135 // Additional C-parity tests for Src/exec.c
11136 // c:1287 iscom / c:1347 isrelative / c:1398 setunderscore /
11137 // c:1468 is_anonymous_function_name / c:2208 findcmd / c:3273 parsecmd
11138 // c:1264 isgooderr / c:1226 parse_string
11139 // ═══════════════════════════════════════════════════════════════════
11140
11141 /// c:1287 — `iscom("")` empty input returns false.
11142 #[test]
11143 fn iscom_empty_string_returns_false() {
11144 let _g = crate::test_util::global_state_lock();
11145 assert!(!iscom(""), "empty cmd name → not a command");
11146 }
11147
11148 /// c:1287 — `iscom` returns bool (compile-time type pin).
11149 #[test]
11150 fn iscom_returns_bool_type() {
11151 let _g = crate::test_util::global_state_lock();
11152 let _: bool = iscom("ls");
11153 }
11154
11155 /// c:1347 — `isrelative("/abs")` returns 0 (absolute path).
11156 #[test]
11157 fn isrelative_absolute_path_returns_zero_pin() {
11158 assert_eq!(isrelative("/usr/bin"), 0, "/usr/bin is absolute");
11159 assert_eq!(isrelative("/"), 0, "/ is absolute");
11160 }
11161
11162 /// c:1347 — `isrelative("rel/path")` returns 1 (relative).
11163 #[test]
11164 fn isrelative_relative_path_returns_one_pin() {
11165 assert_eq!(isrelative("foo"), 1, "foo is relative");
11166 assert_eq!(isrelative("./foo"), 1, "./foo is relative");
11167 assert_eq!(isrelative("../foo"), 1, "../foo is relative");
11168 }
11169
11170 /// c:1347 — `isrelative("")` empty returns 1 (relative by C convention).
11171 #[test]
11172 fn isrelative_empty_returns_relative() {
11173 let r = isrelative("");
11174 assert!(r == 0 || r == 1, "must be 0 or 1");
11175 }
11176
11177 /// c:1468 — `is_anonymous_function_name` returns i32 (type pin).
11178 #[test]
11179 fn is_anonymous_function_name_returns_i32_type() {
11180 let _: i32 = is_anonymous_function_name("(anon)");
11181 }
11182
11183 /// c:1468 — `is_anonymous_function_name("")` empty returns 0.
11184 #[test]
11185 fn is_anonymous_function_name_empty_returns_zero() {
11186 assert_eq!(
11187 is_anonymous_function_name(""),
11188 0,
11189 "empty name is not anonymous"
11190 );
11191 }
11192
11193 /// c:1468 — `is_anonymous_function_name` is deterministic.
11194 #[test]
11195 fn is_anonymous_function_name_is_deterministic() {
11196 for s in ["", "name", "(anon)", "(anon: foo)"] {
11197 let first = is_anonymous_function_name(s);
11198 for _ in 0..3 {
11199 assert_eq!(
11200 is_anonymous_function_name(s),
11201 first,
11202 "is_anonymous_function_name({:?}) must be deterministic",
11203 s
11204 );
11205 }
11206 }
11207 }
11208
11209 /// c:1226 — `parse_string("")` empty returns Option<eprog> (type pin).
11210 #[test]
11211 fn parse_string_returns_option_eprog_type() {
11212 let _g = crate::test_util::global_state_lock();
11213 let _: Option<eprog> = parse_string("", 0);
11214 }
11215
11216 /// c:1398 — `setunderscore("")` empty string is safe.
11217 #[test]
11218 fn setunderscore_empty_no_panic() {
11219 let _g = crate::test_util::global_state_lock();
11220 setunderscore("");
11221 }
11222
11223 /// c:1264 — `isgooderr` returns bool (compile-time type pin).
11224 #[test]
11225 fn isgooderr_returns_bool_type() {
11226 let _: bool = isgooderr(0, "/tmp");
11227 }
11228
11229 // ═══════════════════════════════════════════════════════════════════
11230 // Additional C-parity tests for Src/exec.c
11231 // c:3325 makecline / c:4603 cancd / c:4674 simple_redir_name /
11232 // c:1287 iscom / c:1314 isreallycom / c:3076 commandnotfound
11233 // ═══════════════════════════════════════════════════════════════════
11234
11235 /// c:3325 — `makecline` returns Vec<String> (compile-time type pin).
11236 #[test]
11237 fn makecline_returns_vec_string_type() {
11238 let _g = crate::test_util::global_state_lock();
11239 let _: Vec<String> = makecline(&[]);
11240 }
11241
11242 /// c:3325 — `makecline([])` empty returns empty Vec.
11243 #[test]
11244 fn makecline_empty_input_returns_empty() {
11245 let _g = crate::test_util::global_state_lock();
11246 let r = makecline(&[]);
11247 assert!(r.is_empty(), "empty input → empty output");
11248 }
11249
11250 /// c:3325 — `makecline` preserves input order.
11251 #[test]
11252 fn makecline_preserves_input_order() {
11253 let _g = crate::test_util::global_state_lock();
11254 let input = vec!["one".to_string(), "two".to_string(), "three".to_string()];
11255 let out = makecline(&input);
11256 assert_eq!(out, input, "makecline must preserve order");
11257 }
11258
11259 /// c:3325 — `makecline` clones (output is independent of input).
11260 #[test]
11261 fn makecline_returns_independent_copy() {
11262 let _g = crate::test_util::global_state_lock();
11263 let input = vec!["a".to_string(), "b".to_string()];
11264 let out = makecline(&input);
11265 assert_eq!(out.len(), input.len(), "lengths match");
11266 // Output can be mutated without affecting input.
11267 let mut out_mut = out;
11268 out_mut.push("c".to_string());
11269 assert_eq!(input.len(), 2, "input unchanged");
11270 }
11271
11272 /// c:4603 — `cancd("")` empty path returns None.
11273 /// ZSHRS BUG: empty path returns Some(...) instead of None. C path
11274 /// at Src/exec.c:6376 enters relative-path branch which calls cancd2("")
11275 /// — that should return 0 (not a valid dir), causing the fn to fall
11276 /// through CDPATH and cd_able_vars, both of which should miss for
11277 /// the empty string. Likely cd_able_vars("") or CDPATH-with-empty-element
11278 /// is silently matching $HOME or "." here.
11279 #[test]
11280 #[ignore = "ZSHRS BUG: cancd('') returns Some instead of None — likely cd_able_vars or empty CDPATH element matching $HOME (Src/exec.c:6383-6403)"]
11281 fn cancd_empty_returns_none() {
11282 let _g = crate::test_util::global_state_lock();
11283 assert!(cancd("").is_none(), "empty path → None");
11284 }
11285
11286 /// c:4603 — `cancd("/")` root dir returns Some (always exists).
11287 #[test]
11288 fn cancd_root_returns_some() {
11289 let _g = crate::test_util::global_state_lock();
11290 let r = cancd("/");
11291 assert_eq!(r.as_deref(), Some("/"), "root dir cancd → Some(/)");
11292 }
11293
11294 /// c:4603 — `cancd` returns Option<String> (compile-time type pin).
11295 #[test]
11296 fn cancd_returns_option_string_type() {
11297 let _g = crate::test_util::global_state_lock();
11298 let _: Option<String> = cancd("/");
11299 }
11300
11301 /// c:4603 — `cancd("/__nonexistent__")` returns None.
11302 #[test]
11303 fn cancd_nonexistent_returns_none() {
11304 let _g = crate::test_util::global_state_lock();
11305 assert!(
11306 cancd("/__nonexistent_zshrs_dir_xyz__").is_none(),
11307 "nonexistent dir → None"
11308 );
11309 }
11310
11311 /// c:4603 — `cancd("/tmp")` exists → Some.
11312 #[test]
11313 fn cancd_tmp_returns_some() {
11314 let _g = crate::test_util::global_state_lock();
11315 let r = cancd("/tmp");
11316 assert!(r.is_some(), "/tmp exists → Some");
11317 }
11318
11319 /// c:4603 — `cancd` is deterministic for stable paths.
11320 #[test]
11321 fn cancd_is_deterministic_for_stable_paths() {
11322 let _g = crate::test_util::global_state_lock();
11323 for p in ["/", "/tmp", "/__never__"] {
11324 let first = cancd(p).is_some();
11325 for _ in 0..3 {
11326 assert_eq!(
11327 cancd(p).is_some(),
11328 first,
11329 "cancd({:?}) must be deterministic",
11330 p
11331 );
11332 }
11333 }
11334 }
11335
11336 /// c:1287 — `iscom` is deterministic for stable paths.
11337 #[test]
11338 fn iscom_is_deterministic_for_stable_paths() {
11339 let _g = crate::test_util::global_state_lock();
11340 for p in ["/tmp", "/__never__", "/bin/sh"] {
11341 let first = iscom(p);
11342 for _ in 0..3 {
11343 assert_eq!(iscom(p), first, "iscom({:?}) must be deterministic", p);
11344 }
11345 }
11346 }
11347
11348 /// c:3076 — `commandnotfound("", ...)` empty cmd returns i32.
11349 #[test]
11350 fn commandnotfound_returns_i32_type() {
11351 let _g = crate::test_util::global_state_lock();
11352 let mut args = Vec::new();
11353 let _: i32 = commandnotfound("", &mut args);
11354 }
11355}