zsh/ported/init.rs
1//! init.c - main loop and initialization routines
2//!
3//! Port of Src/init.c
4
5use std::sync::atomic::{AtomicI32, AtomicU32, AtomicUsize, Ordering};
6use std::sync::Mutex;
7
8use crate::ported::builtin::{realexit, LASTVAL, RETFLAG, STOPMSG};
9use crate::ported::context::zcontext_restore;
10use crate::ported::hist::{curhist, curline, hbegin, hend, hist_ring, histlinect, stophist};
11use crate::ported::lex::{set_tok, tok, ENDINPUT};
12use crate::ported::mem::popheap;
13use crate::ported::options::{dosetopt, emulation};
14use crate::ported::params::{getsparam, TERMFLAGS};
15use crate::ported::signals::{
16 dotrap, install_handler, intr, queue_signals, signal_ignore, sigtrapped, unqueue_signals,
17};
18use crate::ported::signals_h::dont_queue_signals;
19use crate::ported::text::getpermtext;
20use crate::ported::utils::{callhookfunc, errflag, movefd, unmeta, ERRFLAG_ERROR};
21use crate::ported::zsh_h::{
22 eprog, hookdef, interact, islogin, isset, jobbing, CONTINUEONERROR, EMULATE_KSH,
23 EMULATE_SH, GLOBALRCS, HISTBEEP, HISTIGNOREDUPS, HIST_DUP, HIST_TMPSTORE, HOOKF_ALL,
24 HOOK_SUFFIX, HUP, IGNOREEOF, INTERACTIVE, LEXERR, PRIVILEGED, RCS, SHINSTDIN, SINGLECOMMAND,
25 TERM_BAD, TERM_NOUP, TERM_UNKNOWN, ZEXIT_NORMAL, ZLE_CMD_POSTEXEC, ZLE_CMD_PREEXEC,
26};
27// =========================================================================
28// File-scope globals from init.c
29// =========================================================================
30
31/// Port of `int noexitct` from Src/init.c:44.
32pub static noexitct: AtomicI32 = AtomicI32::new(0); // c:44
33
34// buffer for $_ and its length // c:46
35
36/// Port of `char *zunderscore` from Src/init.c:49.
37pub static zunderscore: Mutex<String> = Mutex::new(String::new()); // c:49
38
39/// Port of `size_t underscorelen` from Src/init.c:52.
40pub static underscorelen: AtomicUsize = AtomicUsize::new(0); // c:52
41
42/// Port of `int underscoreused` from Src/init.c:55.
43pub static underscoreused: AtomicI32 = AtomicI32::new(0); // c:55
44
45// what level of sourcing we are at // c:57
46
47/// Port of `int sourcelevel` from Src/init.c:60.
48pub static sourcelevel: AtomicI32 = AtomicI32::new(0); // c:60
49
50// the shell tty fd // c:62
51
52/// Port of `mod_export int SHTTY` from Src/init.c:65.
53pub static SHTTY: AtomicI32 = AtomicI32::new(-1); // c:65
54
55// the FILE attached to the shell tty // c:67
56// `mod_export FILE *shout;` (c:70). zshrs writes the tty through
57// `crate::shout` on SHTTY rather than a stdio stream, so this holds only
58// what C tests the pointer for: 0 is NULL, otherwise the fd the stream
59// was opened on, plus one (`shout = stderr` stores 3).
60pub static shout: Mutex<usize> = Mutex::new(0); // c:70
61
62// termcap strings // c:72
63
64/// Port of `mod_export char *tcstr[TC_COUNT]` from Src/init.c:75.
65pub static tcstr: Mutex<[String; 39]> = Mutex::new([
66 // c:75
67 String::new(),
68 String::new(),
69 String::new(),
70 String::new(),
71 String::new(),
72 String::new(),
73 String::new(),
74 String::new(),
75 String::new(),
76 String::new(),
77 String::new(),
78 String::new(),
79 String::new(),
80 String::new(),
81 String::new(),
82 String::new(),
83 String::new(),
84 String::new(),
85 String::new(),
86 String::new(),
87 String::new(),
88 String::new(),
89 String::new(),
90 String::new(),
91 String::new(),
92 String::new(),
93 String::new(),
94 String::new(),
95 String::new(),
96 String::new(),
97 String::new(),
98 String::new(),
99 String::new(),
100 String::new(),
101 String::new(),
102 String::new(),
103 String::new(),
104 String::new(),
105 String::new(),
106]);
107
108// lengths of each termcap string // c:78
109
110/// Port of `mod_export int tclen[TC_COUNT]` from Src/init.c:81.
111pub static tclen: Mutex<[i32; 39]> = Mutex::new([0; 39]); // c:81
112
113// Values of the li, co and am entries // c:82
114
115/// Port of `int tclines` from Src/init.c:85.
116pub static tclines: AtomicI32 = AtomicI32::new(0); // c:85
117
118/// Port of `int tccolumns` from Src/init.c:85.
119pub static tccolumns: AtomicI32 = AtomicI32::new(0); // c:85
120
121/// Port of `mod_export int hasam` from Src/init.c:87.
122pub static hasam: AtomicI32 = AtomicI32::new(0); // c:87
123
124/// Port of `int hasxn` from Src/init.c:89.
125pub static hasxn: AtomicI32 = AtomicI32::new(0); // c:89
126
127// Value of the Co (max_colors) entry: may not be set // c:91
128
129/// Port of `mod_export int tccolours` from Src/init.c:94.
130pub static tccolours: AtomicI32 = AtomicI32::new(0); // c:94
131
132// SIGCHLD mask // c:96
133// `mod_export sigset_t sigchld_mask;` — owned by signals layer. // c:99
134
135/// Port of `struct hookdef zshhooks[]` from `Src/init.c:101-106`:
136/// ```c
137/// struct hookdef zshhooks[] = {
138/// HOOKDEF("exit", NULL, HOOKF_ALL),
139/// HOOKDEF("before_trap", NULL, HOOKF_ALL),
140/// HOOKDEF("after_trap", NULL, HOOKF_ALL),
141/// HOOKDEF("get_color_attr", NULL, HOOKF_ALL),
142/// };
143/// ```
144/// Stored as `AtomicPtr<hookdef>` holding the base pointer of a
145/// heap-leaked `[hookdef; 4]` so that `addhookdefs(NULL, zshhooks,
146/// 4)` at `setupvals()` (c:1085) can walk the array via `h++` exactly
147/// as the C call does. The leak is intentional — C's static-storage
148/// `zshhooks[]` has program lifetime, and the registered hookdef
149/// pointers must stay valid for any later `runhookdef` dispatch.
150pub static zshhooks: once_cell::sync::Lazy<
151 // c:101
152 std::sync::atomic::AtomicPtr<hookdef>,
153> = once_cell::sync::Lazy::new(|| {
154 let arr: Box<[hookdef; 4]> = Box::new([
155 hookdef {
156 // c:102
157 next: std::ptr::null_mut(),
158 name: "exit".to_string(),
159 def: None,
160 flags: HOOKF_ALL,
161 funcs: std::ptr::null_mut(),
162 },
163 hookdef {
164 // c:103
165 next: std::ptr::null_mut(),
166 name: "before_trap".to_string(),
167 def: None,
168 flags: HOOKF_ALL,
169 funcs: std::ptr::null_mut(),
170 },
171 hookdef {
172 // c:104
173 next: std::ptr::null_mut(),
174 name: "after_trap".to_string(),
175 def: None,
176 flags: HOOKF_ALL,
177 funcs: std::ptr::null_mut(),
178 },
179 hookdef {
180 // c:105
181 next: std::ptr::null_mut(),
182 name: "get_color_attr".to_string(),
183 def: None,
184 flags: HOOKF_ALL,
185 funcs: std::ptr::null_mut(),
186 },
187 ]);
188 let base = Box::into_raw(arr) as *mut hookdef;
189 std::sync::atomic::AtomicPtr::new(base)
190});
191
192/// !!! RUST-ONLY — NO C COUNTERPART !!!
193/// C registers `zshhooks` exactly once, from setupvals (c:1085). zshrs
194/// has two entry routes to that step: the `-c` route never runs
195/// setupvals, so ShellExecutor::new performs it; the stdin / script /
196/// interactive route runs ShellExecutor::new AND then zsh_main →
197/// setupvals. Both go through this guard so the second attempt does not
198/// re-add the same static hookdefs, which addhookdefs reports as
199/// `name clash when adding hook` (module.c:889).
200pub static ZSHHOOKS_ADDED: std::sync::Once = std::sync::Once::new();
201
202// original argv[0]. This is already metafied // c:258
203
204/// Port of `static char *argv0` from Src/init.c:259.
205static argv0: Mutex<String> = Mutex::new(String::new()); // c:259
206
207/// Port of `mod_export ZleEntryPoint zle_entry_ptr` from Src/init.c:1730.
208/// Stored as a usize representing a fn pointer (0 == NULL).
209pub static zle_entry_ptr: AtomicUsize = AtomicUsize::new(0); // c:1730
210
211/// Port of `mod_export int zle_load_state` from Src/init.c:1739.
212pub static zle_load_state: AtomicI32 = AtomicI32::new(0); // c:1739
213
214/// Port of `mod_export CompctlReadFn compctlreadptr` from Src/init.c:1831.
215pub static compctlreadptr: AtomicUsize = AtomicUsize::new(0); // c:1831
216
217/// Port of `mod_export int use_exit_printed` from Src/init.c:1846.
218pub static use_exit_printed: AtomicI32 = AtomicI32::new(0); // c:1846
219
220// =========================================================================
221// Static arrays from init.c
222// =========================================================================
223
224/// Port of `static char *tccapnams[TC_COUNT]` from Src/init.c:747.
225const tccapnams: [&str; 39] = [
226 // c:747
227 "cl", "le", "LE", "nd", "RI", "up", "UP", "do", "DO", "dc", "DC", "ic", "IC", "cd", "ce", "al",
228 "dl", "ta", "md", "mh", "so", "us", "ZH", "me", "se", "ue", "ZR", "ch", "ku", "kd", "kl", "kr",
229 "sc", "rc", "bc", "AF", "AB", "vi", "ve",
230];
231
232/// Bookkeeping for C's `2` option value (Src/init.c:365-372): INTERACTIVE,
233/// MONITOR and HASHDIRS start at 2, "on by default", so that `opts[X] &= 1`
234/// and `if (opts[X] == 2)` can tell a default from an explicit `-i`/`-m`.
235/// Option storage here is boolean; a set bit means the option still holds its
236/// default-on sentinel. Bit 0 INTERACTIVE, bit 1 MONITOR, bit 2 HASHDIRS.
237static OPT_DEFAULT_ON: AtomicU32 = AtomicU32::new(0);
238
239/// Clears the default-on sentinel of INTERACTIVE/MONITOR/HASHDIRS once
240/// `parseopts` sets the option explicitly (C stores 0 or 1 over the 2).
241macro_rules! note_explicit_opt {
242 ($optno:expr) => {
243 match ($optno as i32).abs() {
244 crate::ported::zsh_h::INTERACTIVE => {
245 OPT_DEFAULT_ON.fetch_and(!1, Ordering::SeqCst);
246 }
247 crate::ported::zsh_h::MONITOR => {
248 OPT_DEFAULT_ON.fetch_and(!2, Ordering::SeqCst);
249 }
250 crate::ported::zsh_h::HASHDIRS => {
251 OPT_DEFAULT_ON.fetch_and(!4, Ordering::SeqCst);
252 }
253 _ => {}
254 }
255 };
256}
257
258/// Port of `static void parseargs(...)` from Src/init.c:263.
259fn parseargs(
260 zsh_name: &str,
261 argv: &mut Vec<String>, // c:263
262 runscript: &mut Option<String>,
263 cmdptr: &mut Option<String>,
264) {
265 let mut idx: usize = 0; // c:265
266 let flags: i32 = 1; /* PARSEARGS_TOPLEVEL */
267 // c:267
268 let flags = if argv.first().map(|s| s.starts_with('-')).unwrap_or(false)
269 // c:268-269
270 {
271 flags | 2 /* PARSEARGS_LOGIN */
272 } else {
273 flags
274 };
275
276 // c:282 — `argv0 = argzero = posixzero = *argv++;` — all THREE are
277 // seeded from the kernel-supplied argv[0]. Only `argv0` was; `argzero`
278 // and `posixzero` stayed unset for an interactive shell, so `$0` read
279 // empty at the prompt where zsh reads `/bin/zsh`, and the empty value
280 // cascaded into `doshfunc`'s funcstack push: with `argzero` NULL,
281 // `funcsave->argv0` (c:6011) is None, so the outermost frame's
282 // `fstack.caller` fell through to a re-read of `argzero` — which
283 // doshfunc had just overwritten with the callee's own name (c:5986) —
284 // and `$functrace[-1]` reported `_main_complete:2` instead of
285 // `/bin/zsh:2`. The same None also skipped the c:6116 restore
286 // (`if let Some(saved) = funcsave_argv0`), leaking the completer's name
287 // into the interactive shell's `$0` after every TAB.
288 // A `-c` name argument (c:299) and a runscript (c:1402) both overwrite
289 // this later, exactly as in C.
290 *argv0.lock().unwrap() = argv[idx].clone(); // c:282
291 crate::ported::utils::set_argzero(Some(argv[idx].clone())); // c:282
292 crate::ported::utils::set_posixzero(Some(argv[idx].clone())); // c:282
293 idx += 1;
294 // SHIN = 0; // c:272
295
296 // parseopts(zsh_name, &argv, opts, cmdptr, NULL, flags) // c:280
297 let _ = parseopts(zsh_name, argv, &mut idx, cmdptr, flags);
298
299 // c:291-292 — `if (opts[SHINSTDIN]) opts[USEZLE] = opts[USEZLE] &&
300 // isatty(0);`. SHINSTDIN starts unset here (set below), so this is
301 // normally a no-op; honor it for an explicitly-set SHINSTDIN.
302 if isset(SHINSTDIN) {
303 let usezle = isset(crate::ported::zsh_h::USEZLE) && unsafe { libc::isatty(0) != 0 };
304 // USEZLE's canonical option name is `zle` (zsh_h.rs:4145
305 // `opt_name(USEZLE) == "zle"`); `isset(USEZLE)` reads the `zle`
306 // key, so the write MUST use `zle` too. The prior `"usezle"` key
307 // was never read → this `opts[USEZLE] = opts[USEZLE] && isatty(0)`
308 // downgrade silently did nothing.
309 crate::ported::options::opt_state_set("zle", usezle); // c:292
310 }
311
312 // c:294 — `paramlist = znewlinklist();`
313 let mut paramlist: Vec<String> = Vec::new();
314 if idx < argv.len() {
315 // c:295 — there's a non-option argument.
316 // c:296 — `if (unset(SHINSTDIN))` — a positional arg is the
317 // script to run (or $0 under -c) ONLY when not already forced to
318 // read stdin.
319 if !isset(SHINSTDIN) {
320 // c:297 — `posixzero = *argv;`
321 crate::ported::utils::set_posixzero(Some(argv[idx].clone()));
322 if cmdptr.is_some() {
323 // c:299 — `argzero = *argv;` ($0 under -c)
324 crate::ported::utils::set_argzero(Some(argv[idx].clone()));
325 } else {
326 // c:301 — `*runscript = *argv;`
327 *runscript = Some(argv[idx].clone());
328 }
329 // c:302 — `opts[INTERACTIVE] &= 1;`. A script source makes the
330 // shell non-interactive unless `-i` forced it on.
331 if OPT_DEFAULT_ON.fetch_and(!1, Ordering::SeqCst) & 1 != 0 {
332 crate::ported::options::opt_state_set("interactive", false);
333 }
334 idx += 1;
335 }
336 // c:305-306 — remaining args become positional parameters.
337 while idx < argv.len() {
338 paramlist.push(argv[idx].clone());
339 idx += 1;
340 }
341 } else if cmdptr.is_none() {
342 // c:307-308 — `else if (!*cmdptr) opts[SHINSTDIN] = 1;` — no
343 // script and no `-c`: read commands from stdin.
344 crate::ported::options::opt_state_set("shinstdin", true);
345 }
346 // c:309-310 — `if (isset(SINGLECOMMAND)) opts[INTERACTIVE] &= 1;`
347 if isset(SINGLECOMMAND) && OPT_DEFAULT_ON.fetch_and(!1, Ordering::SeqCst) & 1 != 0 {
348 crate::ported::options::opt_state_set("interactive", false);
349 }
350 // c:311 — `opts[INTERACTIVE] = !!opts[INTERACTIVE];` is a no-op for
351 // the bool port.
352 // c:312-315 — `if (opts[MONITOR] == 2) opts[MONITOR] = opts[INTERACTIVE];`
353 // and the same for HASHDIRS: only a still-default value follows INTERACTIVE.
354 let interactive = isset(INTERACTIVE);
355 let still_default = OPT_DEFAULT_ON.load(Ordering::SeqCst);
356 if still_default & 2 != 0 {
357 crate::ported::options::opt_state_set("monitor", interactive);
358 }
359 if still_default & 4 != 0 {
360 crate::ported::options::opt_state_set("hashdirs", interactive);
361 }
362 // c:316 — `pparams = paramlist;`
363 if !paramlist.is_empty() {
364 if let Ok(mut p) = crate::ported::builtin::PPARAMS.lock() {
365 *p = paramlist;
366 }
367 }
368}
369
370/// Port of `static void parseopts_insert(...)` from Src/init.c:328.
371///
372/// Insert into list in order of pointer value.
373fn parseopts_insert(optlist: &mut Vec<usize>, base: usize, optno: i32) {
374 // c:328
375 let ptr = base + (if optno < 0 { -optno } else { optno }) as usize; // c:348
376 for (i, &node) in optlist.iter().enumerate() {
377 // c:348
378 if ptr < node {
379 // c:348
380 optlist.insert(i, ptr); // c:348
381 return; // c:348
382 }
383 }
384 optlist.push(ptr); // c:348
385}
386
387/// zshrs-only long flags that take a following word as their argument.
388/// They are handled by the binary front-end; `parseopts` only needs to
389/// skip BOTH words so the argument is not mistaken for a script operand.
390const ZSHRS_LONG_FLAGS_WITH_ARG: &[&str] = &["emulate", "rcfile", "init-file", "docs", "out"];
391
392/// Port of `mod_export int parseopts(...)` from Src/init.c:390.
393/// Rust idiom replacement: index-walk over `argv` Vec covers the C
394/// argv pointer-advance; the `emulate_required` / `toplevel` state
395/// tracking mirrors the C source's local flags. The long-option
396/// table lookups happen against the shared options.rs registry.
397pub fn parseopts(
398 _nam: &str,
399 argv: &mut Vec<String>,
400 idx: &mut usize, // c:390
401 cmdp: &mut Option<String>,
402 flags: i32,
403) -> i32 {
404 let toplevel = (flags & 1) != 0; // c:396
405 let mut emulate_required = toplevel; // c:397
406 *cmdp = None; // c:400
407
408 while *idx < argv.len() {
409 // c:418
410 let arg = argv[*idx].clone();
411 if !(arg.starts_with('-') || arg.starts_with('+')) {
412 break;
413 }
414 if arg == "--version" {
415 // c:434
416 println!("zshrs (C-port)"); // c:435-436
417 if toplevel {
418 std::process::exit(0);
419 } // c:437
420 }
421 if arg == "--help" {
422 // c:439
423 printhelp(); // c:440
424 if toplevel {
425 std::process::exit(0);
426 } // c:441
427 }
428 if arg == "-c" {
429 // c:470
430 if emulate_required {
431 // c:471
432 parseopts_setemulate(_nam, flags); // c:472
433 emulate_required = false; // c:473
434 }
435 *idx += 1;
436 *cmdp = argv.get(*idx).cloned(); // c:476
437 // c:495 — `new_opts[INTERACTIVE] &= 1;`: a default-on INTERACTIVE is
438 // cleared by `-c`, an explicit `-i` is kept.
439 if OPT_DEFAULT_ON.fetch_and(!1, Ordering::SeqCst) & 1 != 0 {
440 crate::ported::options::opt_state_set("interactive", false);
441 }
442 *idx += 1;
443 continue;
444 }
445 // c:Src/init.c:478-490 — `-o NAME` / `+o NAME`: a long-name
446 // option whose name follows (`-o nullglob`). `optlookup` resolves
447 // the name; `dosetopt` applies it with the sign as the on/off
448 // action (`-` sets, `+` unsets).
449 if arg == "-o" || arg == "+o" {
450 if emulate_required {
451 parseopts_setemulate(_nam, flags); // c:481-483
452 emulate_required = false;
453 }
454 let action = arg.starts_with('-'); // c:420
455 *idx += 1;
456 if let Some(name) = argv.get(*idx).cloned() {
457 let optno = crate::ported::options::optlookup(&name); // c:493
458 if optno != crate::ported::zsh_h::OPT_INVALID {
459 // c:501 — dosetopt(optno, action, toplevel, new_opts)
460 crate::ported::options::dosetopt(optno, action as i32, toplevel as i32);
461 note_explicit_opt!(optno);
462 }
463 }
464 *idx += 1;
465 continue;
466 }
467 // c:Src/init.c:425-429 — the pseudo-option `--` ends option
468 // processing; everything after it is an operand even if it looks
469 // like a flag.
470 if arg == "--" {
471 *idx += 1;
472 break;
473 }
474 // c:Src/init.c:432-460 — GNU-style long options. C rewrites `-`
475 // to `_` in the name and falls into the shared `longoptions:`
476 // label, so `--no-rcs` is exactly `-o no_rcs`.
477 //
478 // zshrs-specific long flags (`--zsh`, `--bash`, `--dap`, …) are
479 // NOT zsh options; the bin's front-end consumes them before
480 // zsh_main and c:492's `optlookup` would reject them with "no
481 // such option", so an unrecognised `--NAME` is still skipped
482 // here. That is the deliberate divergence. Skipping ALL of them
483 // was too broad: a long flag that IS a real option never reached
484 // the option table, so `zshrs --no-rcs` left RCS set and an
485 // interactive shell still sourced .zshenv/.zshrc, while the
486 // equivalent `-f` and `-o norcs` both suppressed them (the rc
487 // gate is `isset(RCS)` at init.rs:1546/1568).
488 if let Some(long) = arg.strip_prefix("--") {
489 let name = long.replace('-', "_"); // c:456-459
490 let optno = crate::ported::options::optlookup(&name); // c:492
491 if optno != crate::ported::zsh_h::OPT_INVALID {
492 if emulate_required {
493 parseopts_setemulate(_nam, flags); // c:488-490
494 emulate_required = false;
495 }
496 // c:498 — `dosetopt(optno, action, toplevel, new_opts)`.
497 // `action` is true for a `-`-introduced word (c:420);
498 // optlookup answers a NEGATIVE optno for a `no…`
499 // spelling and dosetopt inverts on that, which is what
500 // the `-o` arm above already relies on.
501 crate::ported::options::dosetopt(optno, 1, toplevel as i32);
502 note_explicit_opt!(optno);
503 }
504 // A zshrs-only long flag. Those that take a following word
505 // must consume it too, or the word is mistaken for the
506 // script operand and the shell tries to RUN the user's
507 // `--rcfile` argument.
508 *idx += 1;
509 if ZSHRS_LONG_FLAGS_WITH_ARG.contains(&&arg[2..]) {
510 *idx += 1;
511 }
512 continue;
513 }
514 // c:Src/init.c:516-534 — a cluster of single option letters
515 // (`-x`, `-v`, `-xv`, `+x`). Each char maps via `optlookupc` to a
516 // (possibly negated, for inverted-sense letters like `f`) option
517 // number; `dosetopt` applies it with the sign as the action.
518 // OPT_INVALID letters are skipped rather than erroring as C does
519 // at c:517 — zshrs accepts some non-zsh single-dash flags the C
520 // table doesn't know.
521 if emulate_required {
522 parseopts_setemulate(_nam, flags); // c:516-519
523 emulate_required = false;
524 }
525 let action = arg.starts_with('-'); // c:420
526 for c in arg[1..].chars() {
527 let optno = crate::ported::options::optlookupc(c); // c:520
528 if optno != crate::ported::zsh_h::OPT_INVALID {
529 // c:526 — dosetopt(optno, action, toplevel, new_opts)
530 crate::ported::options::dosetopt(optno, action as i32, toplevel as i32);
531 note_explicit_opt!(optno);
532 }
533 }
534 *idx += 1;
535 }
536 if emulate_required {
537 // c:557
538 parseopts_setemulate(_nam, flags); // c:557
539 }
540 0 // c:557
541}
542
543/// Port of `static void printhelp(void)` from Src/init.c:557.
544fn printhelp() {
545 // c:557
546 let argz = argv0.lock().unwrap().clone(); // c:557
547 println!("Usage: {} [<options>] [<argument> ...]", argz); // c:559
548 println!(); // c:560
549 println!("Special options:"); // c:560
550 println!(" --help show this message, then exit"); // c:561
551 println!(" --version show zsh version number, then exit"); // c:562
552 println!(" -b end option processing, like --"); // c:564
553 println!(" -c take first argument as a command to execute"); // c:577
554 println!(" -o OPTION set an option by name (see below)"); // c:577
555 println!(); // c:577
556 println!("Normal options are named. An option may be turned on by"); // c:577
557 println!("`-o OPTION', `--OPTION', `+o no_OPTION' or `+-no-OPTION'. An"); // c:577
558 println!("option may be turned off by `-o no_OPTION', `--no-OPTION',"); // c:577
559 println!("`+o OPTION' or `+-OPTION'. Options are listed below only in"); // c:577
560 println!("`--OPTION' or `--no-OPTION' form."); // c:577
561 // printoptionlist(); // c:577
562}
563
564/// Port of `mod_export void init_io(char *cmd)` from Src/init.c:577.
565pub fn init_io(_cmd: Option<&str>) {
566 // c:577
567 // stdout, stderr fully buffered // c:577
568 // setvbuf(stdout, outbuf, _IOFBF, BUFSIZ); setvbuf(stderr, ...) // c:587-591
569 // (Rust's stdout/stderr are line/block buffered by default)
570
571 // Close any existing shout // c:605-614
572 *shout.lock().unwrap() = 0;
573 if SHTTY.load(Ordering::SeqCst) != -1 {
574 // c:615
575 // c:616 — `zclose(SHTTY);` — fdtable-aware close. SHTTY was
576 // registered as FDT_INTERNAL by movefd at one of the open
577 // sites below (c:627 ttyname/O_RDWR open, c:658 dup(0), c:662
578 // dup(1), c:668 /dev/tty open — all routed through movefd
579 // which sets fdtable[fd] = FDT_INTERNAL at utils.rs:2243).
580 // Prior port used raw libc::close which skipped the
581 // fdtable_set(fd, FDT_UNUSED) clear that zclose does at
582 // utils.rs:2402. Same leak shape as random.rs finish_
583 // (b3107b5a46), tcp.rs tcp_close (9b4dae375a), and
584 // zpty.rs deleteptycmd (c37083d09f) — stale FDT_INTERNAL
585 // marker survives the close → kernel-reused fd inherits the
586 // module-owned classification → closem(FDT_UNUSED, 0) calls
587 // from sibling builtins skip closing it.
588 let _ = crate::ported::utils::zclose(SHTTY.load(Ordering::SeqCst));
589 SHTTY.store(-1, Ordering::SeqCst); // c:617
590 }
591
592 // xtrerr = stderr; // c:621
593
594 // Make sure the tty is opened read/write. // c:623
595 //
596 // C uses `if (isatty(0))` — the truthy test, NOT a strict
597 // `== 1`. POSIX guarantees isatty returns 0 on false but
598 // "non-zero" on true with the exact value implementation-
599 // defined. macOS/glibc/musl all return 1 today but the
600 // strict `== 1` check is brittle — match C's truthy test
601 // so a future libc that returns any non-zero value still
602 // hits the SHTTY-open path.
603 // ttystrname is the canonical "name of the controlling tty" global
604 // (init.c declares it; clone.c reads it for $TTY after fork). The
605 // C `init_io` keeps it in lockstep with `SHTTY`: every open-site
606 // and every dup-fallback either replaces it with `ttyname(fd)` of
607 // the new SHTTY or resets to "" / "/dev/tty" on failure. Mirror
608 // every store here so $TTY isn't empty after init.
609 let set_ttystrname = |s: String| {
610 // c:625 zsfree(ttystrname); ttystrname = ztrdup(...)
611 *crate::ported::modules::clone::ttystrname.lock().unwrap() = s;
612 };
613 #[cfg(unix)]
614 let ttyname_of = |fd: i32| -> Option<String> {
615 unsafe {
616 let p = libc::ttyname(fd);
617 if p.is_null() {
618 None
619 } else {
620 std::ffi::CStr::from_ptr(p)
621 .to_str()
622 .ok()
623 .map(|s| s.to_string())
624 }
625 }
626 };
627 #[cfg(unix)]
628 unsafe {
629 if libc::isatty(0) != 0 {
630 // c:624
631 let name_ptr = libc::ttyname(0); // c:626
632 if !name_ptr.is_null() {
633 let name = std::ffi::CStr::from_ptr(name_ptr);
634 let cstr = std::ffi::CString::new(name.to_bytes()).unwrap();
635 // c:626 ttystrname = ztrdup(ttyname(0))
636 set_ttystrname(name.to_string_lossy().into_owned());
637 let fd = libc::open(
638 cstr.as_ptr(), // c:627
639 libc::O_RDWR | libc::O_NOCTTY,
640 );
641 SHTTY.store(movefd(fd), Ordering::SeqCst);
642 }
643 if SHTTY.load(Ordering::SeqCst) == -1 {
644 // c:658
645 SHTTY.store(movefd(libc::dup(0)), Ordering::SeqCst);
646 // c:659
647 }
648 }
649 if SHTTY.load(Ordering::SeqCst) == -1 && libc::isatty(1) != 0 {
650 // c:662
651 SHTTY.store(movefd(libc::dup(1)), Ordering::SeqCst);
652 // c:663
653 // c:664-665 — zsfree(ttystrname); ttystrname = ztrdup(ttyname(1));
654 if let Some(n) = ttyname_of(1) {
655 set_ttystrname(n);
656 }
657 }
658 if SHTTY.load(Ordering::SeqCst) == -1 {
659 // c:667
660 let dev_tty = std::ffi::CString::new("/dev/tty").unwrap();
661 let fd = libc::open(dev_tty.as_ptr(), libc::O_RDWR | libc::O_NOCTTY); // c:668
662 SHTTY.store(movefd(fd), Ordering::SeqCst);
663 if SHTTY.load(Ordering::SeqCst) != -1 {
664 // c:669-670 — ttystrname = ztrdup(ttyname(SHTTY));
665 if let Some(n) = ttyname_of(SHTTY.load(Ordering::SeqCst)) {
666 set_ttystrname(n);
667 }
668 }
669 }
670 if SHTTY.load(Ordering::SeqCst) == -1 {
671 // c:672-674 — failed all opens: ttystrname = "";
672 set_ttystrname(String::new());
673 } else {
674 // c:675
675 let fdflags = libc::fcntl(SHTTY.load(Ordering::SeqCst), libc::F_GETFD, 0); // c:677
676 if fdflags != -1 {
677 // c:678
678 libc::fcntl(
679 SHTTY.load(Ordering::SeqCst),
680 libc::F_SETFD, // c:680
681 fdflags | libc::FD_CLOEXEC,
682 );
683 }
684 // c:683-684 — if (!ttystrname) ttystrname = ztrdup("/dev/tty");
685 // Rust mirrors NULL-check via empty-string check on the Mutex.
686 let mut guard = crate::ported::modules::clone::ttystrname.lock().unwrap();
687 if guard.is_empty() {
688 *guard = "/dev/tty".to_string();
689 }
690 }
691 }
692
693 // c:689-694 — set up terminal output only for an interactive shell;
694 // disable the line editor (USEZLE → the special `zle` option) when the
695 // shell isn't interactive, or is interactive without a real tty.
696 if interact() {
697 // c:689
698 init_shout(); // c:690
699 // c:691-692 — `if (!SHTTY || !shout) opts[USEZLE] = 0;`. zshrs has
700 // no `shout` FILE* (it writes the tty via SHTTY / fd 2), so the C
701 // `!shout` arm collapses into the SHTTY check. The option's
702 // canonical name is `zle` (options.rs:92), NOT `usezle`.
703 if SHTTY.load(Ordering::SeqCst) == -1 {
704 crate::ported::options::opt_state_set("zle", false); // c:692 opts[USEZLE]=0
705 }
706 } else {
707 // c:693-694 — `} else opts[USEZLE] = 0;`
708 crate::ported::options::opt_state_set("zle", false); // c:694
709 }
710
711 // c:699 — `mypid = (zlong)getpid();` — no zshrs global; getpid() is
712 // called where needed (acquire_pgrp computes it locally).
713 // c:700-707 — if interactive, make sure the shell is in the foreground
714 // and is the process-group leader. Gating on MONITOR + the one-shot
715 // `!origpgrp` guard matches C exactly; the prior port called
716 // acquire_pgrp unconditionally and never recorded origpgrp, so
717 // release_pgrp at exit had no group to hand the tty back to.
718 if isset(crate::ported::zsh_h::MONITOR) {
719 // c:700
720 if SHTTY.load(Ordering::SeqCst) == -1 {
721 // c:701
722 crate::ported::options::opt_state_set("monitor", false); // c:702
723 } else {
724 // c:703 — `else if (!origpgrp)`: only acquire the first time.
725 let origpgrp_recorded = *crate::ported::jobs::ORIGPGRP
726 .get_or_init(|| std::sync::Mutex::new(0))
727 .lock()
728 .unwrap()
729 != 0;
730 if !origpgrp_recorded {
731 // c:704 — `origpgrp = GETPGRP();`
732 *crate::ported::jobs::ORIGPGRP
733 .get_or_init(|| std::sync::Mutex::new(0))
734 .lock()
735 .unwrap() = unsafe { libc::getpgrp() };
736 // c:705 — `acquire_pgrp();` (might also clear opts[MONITOR]).
737 let _ = crate::ported::jobs::acquire_pgrp();
738 }
739 }
740 }
741}
742
743/// Port of `mod_export void init_shout(void)` from Src/init.c:728.
744pub fn init_shout() {
745 // c:728
746 let fd = SHTTY.load(Ordering::SeqCst);
747 if fd == -1 {
748 // c:735-739 — "Since we're interactive, it's nice to have
749 // somewhere to write."
750 *shout.lock().unwrap() = 2 + 1; // c:738 shout = stderr
751 return; // c:739
752 }
753 // c:747-752 — `shout = fdopen(SHTTY, "w")` + setvbuf; the buffering
754 // lives in `crate::shout`.
755 *shout.lock().unwrap() = fd as usize + 1; // c:748
756 if let Some(ti) = crate::ported::utils::gettyinfo() {
757 // c:754 gettyinfo(&shttyinfo)
758 *crate::ported::utils::SHTTYINFO.lock().unwrap() = Some(ti);
759 }
760}
761
762/// Port of `mod_export char *tccap_get_name(int cap)` from Src/init.c:756.
763pub fn tccap_get_name(cap: usize) -> &'static str {
764 // c:756
765 if cap >= 39
766 /* TC_COUNT */
767 {
768 // c:771
769 return ""; // c:771
770 }
771 tccapnams[cap] // c:771
772}
773
774/// Port of `mod_export int init_term(void)` from Src/init.c:771.
775///
776/// Reads `$TERM` from the param table, and on a recognised term
777/// populates `tcstr[]`/`tclen[]` from the system termcap/terminfo
778/// DB via `tgetent` + `tgetstr` over `tccapnams[]`, exactly like C.
779/// This is the substrate every `tcmultout` / `tc_leftcurs` /
780/// `tsetcap` call site reads.
781///
782/// ncurses is already linked (build.rs `rustc-link-lib=ncurses`, the
783/// same lib the zsh/terminfo module's `tigetstr` externs use), so
784/// the termcap-emulation entry points are available. The previous
785/// Rust body hardcoded ANSI/VT100 escapes — under `TERM=screen-*` /
786/// `tmux-*` that diverged from zsh on standout (`so` is `\e[3m`
787/// there, not the hardcoded `\e[7m`).
788pub fn init_term() -> i32 {
789 // c:766 — the termcap emulation entry points. These used to be an
790 // `extern "C"` block resolved against ncurses; they are now
791 // `crate::terminfo_db`, which reads the compiled terminfo database
792 // directly. That removed the last reason the binary linked a C
793 // terminal library, and with it `libtinfo.so.6` as an install
794 // dependency on Ubuntu. Verified identical to ncurses over the whole
795 // reference database — 2819 entries x 497 capabilities.
796 use crate::terminfo_db::{tgetent, tgetflag, tgetnum, tgetstr};
797 use crate::ported::zsh_h::{
798 TCBACKSPACE, TCCLEARSCREEN, TCDOWN, TCFAINTBEG, TCITALICSBEG, TCITALICSEND, TCLEFT,
799 TCRESTRCURSOR, TCSAVECURSOR, TCUP, TC_COUNT,
800 };
801
802 // c:776-779 — `if (!*term) { termflags |= TERM_UNKNOWN; return 0; }`
803 let term = getsparam("TERM").unwrap_or_default();
804 if term.is_empty() {
805 TERMFLAGS.fetch_or(TERM_UNKNOWN, Ordering::SeqCst);
806 return 0;
807 }
808
809 // c:782-783 — `if (!strcmp(term, "emacs")) opts[USEZLE] = 0;`
810 // C proceeds to tgetent afterwards; zshrs keeps the USEZLE-off
811 // via the option layer.
812 if term == "emacs" {
813 crate::ported::options::opt_state_set("zle", false); // c:783 opts[USEZLE] = 0
814 }
815
816 // An embedded NUL can never name a terminfo entry.
817 if term.contains('\0') {
818 TERMFLAGS.fetch_or(TERM_BAD, Ordering::SeqCst);
819 return 0;
820 }
821 // c:785-797 — `if (tgetent(termbuf, term) != TGETENT_SUCCESS) {
822 // zerr(...); errflag &= ~ERRFLAG_ERROR; termflags |= TERM_BAD;
823 // return 0; }`. ncurses tgetent accepts NULL (the
824 // TGETENT_ACCEPTS_NULL arm at c:786-787).
825 let ent = tgetent(&term);
826 if ent != 1 {
827 // c:791 — `if (interact) zerr("can't find terminal definition
828 // for %s", term);` — interact gate keeps -fc scripts quiet.
829 if crate::ported::zsh_h::isset(crate::ported::zsh_h::INTERACTIVE) {
830 crate::ported::utils::zerr(&format!("can't find terminal definition for {}", term));
831 // c:792
832 }
833 crate::ported::utils::errflag
834 .fetch_and(!crate::ported::utils::ERRFLAG_ERROR, Ordering::Relaxed); // c:793
835 TERMFLAGS.fetch_or(TERM_BAD, Ordering::SeqCst); // c:794
836 return 0; // c:795
837 }
838
839 // c:801-802 — `termflags &= ~TERM_BAD; termflags &= ~TERM_UNKNOWN;`
840 TERMFLAGS.fetch_and(!(TERM_BAD | TERM_UNKNOWN), Ordering::SeqCst);
841
842 // c:803-815 — `for (t0 = 0; t0 != TC_COUNT; t0++) { ... tgetstr
843 // (tccapnams[t0], &pp) ... }` — fetch every capability string.
844 {
845 let mut s = tcstr.lock().unwrap();
846 let mut l = tclen.lock().unwrap();
847 // c:798 `char tbuf[1024]` — element type must be c_char: it is
848 // i8 on macOS/x86_64-linux but u8 on aarch64-linux.
849 for t0 in 0..TC_COUNT as usize {
850 match tgetstr(tccapnams[t0]) {
851 // c:811-813 — dup the cap string + record length.
852 Some(bytes) => {
853 s[t0] = String::from_utf8_lossy(&bytes).into_owned();
854 l[t0] = bytes.len() as i32;
855 }
856 // c:809 — `tcstr[t0] = NULL, tclen[t0] = 0;`
857 None => {
858 s[t0] = String::new();
859 l[t0] = 0;
860 }
861 }
862 }
863 }
864
865 // c:817-818 — automargin / newline-glitch flags.
866 let flag = tgetflag;
867 let num = tgetnum;
868 hasam.store(flag("am"), Ordering::SeqCst); // c:818 `hasam = tgetflag("am");`
869 hasxn.store(flag("xn"), Ordering::SeqCst); // c:819 `hasxn = tgetflag("xn");`
870 tclines.store(num("li"), Ordering::SeqCst); // c:821 `tclines = tgetnum("li");`
871 tccolumns.store(num("co"), Ordering::SeqCst); // c:822 `tccolumns = tgetnum("co");`
872 tccolours.store(num("Co"), Ordering::SeqCst); // c:823 `tccolours = tgetnum("Co");`
873
874 // Post-fetch fixups — all operate on tcstr/tclen under one lock.
875 {
876 let mut s = tcstr.lock().unwrap();
877 let mut l = tclen.lock().unwrap();
878 let can = |l: &[i32; 39], cap: i32| l[cap as usize] != 0; // tccan()
879
880 // c:825-833 — no cursor-up cap → single-line mode (TERM_NOUP).
881 if can(&l, TCUP) {
882 TERMFLAGS.fetch_and(!TERM_NOUP, Ordering::SeqCst); // c:829
883 } else {
884 s[TCUP as usize] = String::new(); // c:831-832
885 l[TCUP as usize] = 0;
886 TERMFLAGS.fetch_or(TERM_NOUP, Ordering::SeqCst); // c:833
887 }
888
889 // c:836-840 — most termcaps don't define "bc"; default `\b`.
890 if !can(&l, TCBACKSPACE) {
891 s[TCBACKSPACE as usize] = "\u{8}".to_string(); // c:838
892 l[TCBACKSPACE as usize] = 1; // c:839
893 }
894
895 // c:843-847 — no cursor-left cap → use backspace.
896 if !can(&l, TCLEFT) {
897 s[TCLEFT as usize] = s[TCBACKSPACE as usize].clone(); // c:845
898 l[TCLEFT as usize] = l[TCBACKSPACE as usize]; // c:846
899 }
900
901 // c:849-853 — save-cursor without restore-cursor is useless.
902 if can(&l, TCSAVECURSOR) && !can(&l, TCRESTRCURSOR) {
903 l[TCSAVECURSOR as usize] = 0; // c:850
904 s[TCSAVECURSOR as usize] = String::new(); // c:851-852
905 }
906
907 // c:856-860 — if the down cap is `\n`, don't use it.
908 if can(&l, TCDOWN) && s[TCDOWN as usize].starts_with('\n') {
909 l[TCDOWN as usize] = 0; // c:857
910 s[TCDOWN as usize] = String::new(); // c:858-859
911 }
912
913 // c:863-867 — no clear cap → ^L.
914 if !can(&l, TCCLEARSCREEN) {
915 s[TCCLEARSCREEN as usize] = "\u{c}".to_string(); // c:865
916 l[TCCLEARSCREEN as usize] = 1; // c:866
917 }
918
919 // c:868 — `rprompt_indent = 1;` lives in the params layer
920 // (rprompt_indent_unsetfn keeps it there); no-op here.
921
922 // c:876-884 — no italics caps → CSI 3 m / CSI 23 m.
923 if !can(&l, TCITALICSBEG) {
924 s[TCITALICSBEG as usize] = "\x1b[3m".to_string(); // c:878
925 l[TCITALICSBEG as usize] = 4; // c:879
926 }
927 if !can(&l, TCITALICSEND) {
928 s[TCITALICSEND as usize] = "\x1b[23m".to_string(); // c:882
929 l[TCITALICSEND as usize] = 5; // c:883
930 }
931 // c:885-888 — no faint cap → CSI 2 m.
932 if !can(&l, TCFAINTBEG) {
933 s[TCFAINTBEG as usize] = "\x1b[2m".to_string(); // c:887
934 l[TCFAINTBEG as usize] = 4; // c:888
935 }
936 }
937
938 1 // c:890 `return 1;`
939}
940
941/// Port of `static char *getmypath(const char *name, const char *cwd)` from Src/init.c:909.
942pub fn getmypath(name: Option<&str>, cwd: Option<&str>) -> Option<String> {
943 // c:909
944 #[cfg(target_os = "macos")]
945 unsafe {
946 // c:914
947 let mut buf = vec![0u8; libc::PATH_MAX as usize]; // c:918
948 let mut n: u32 = libc::PATH_MAX as u32; // c:916
949 let ret = libc::_NSGetExecutablePath(
950 buf.as_mut_ptr() as *mut i8, // c:919
951 &mut n,
952 );
953 if ret < 0 {
954 // c:919
955 buf.resize(n as usize, 0); // c:921
956 let ret2 = libc::_NSGetExecutablePath(buf.as_mut_ptr() as *mut i8, &mut n); // c:922
957 if ret2 == 0 {
958 let s = std::ffi::CStr::from_ptr(buf.as_ptr() as *const i8);
959 let lossy = s.to_string_lossy().into_owned();
960 if !lossy.is_empty() {
961 return Some(lossy);
962 }
963 }
964 } else if ret == 0 {
965 // c:924
966 let s = std::ffi::CStr::from_ptr(buf.as_ptr() as *const i8);
967 let lossy = s.to_string_lossy().into_owned();
968 if !lossy.is_empty() {
969 return Some(lossy);
970 } // c:925
971 }
972 }
973 #[cfg(target_os = "linux")]
974 {
975 if let Ok(p) = std::fs::read_link("/proc/self/exe") {
976 // c:946
977 return Some(p.to_string_lossy().into_owned()); // c:949
978 }
979 }
980
981 let name = name?; // c:956-957
982 let name = if name.starts_with('-') {
983 &name[1..]
984 } else {
985 name
986 }; // c:958-959
987 let namelen = name.len(); // c:960
988 if namelen == 0 {
989 return None;
990 } // c:960-961
991 if name.ends_with('/') {
992 return None;
993 } // c:963-964
994 if name.starts_with('/') {
995 // c:965
996 return Some(name.to_string()); // c:967
997 }
998 if name.contains('/') {
999 // c:969
1000 let cwd = cwd?; // c:971-972
1001 return Some(format!("{}/{}", cwd, name)); // c:974
1002 }
1003 let path = std::env::var("PATH").ok()?; // c:984
1004 if path.is_empty() {
1005 return None;
1006 } // c:985-986
1007 for dir in path.split(':') {
1008 // c:990-1000
1009 let candidate = if dir.is_empty() {
1010 std::path::PathBuf::from(name)
1011 } else {
1012 std::path::PathBuf::from(format!("{}/{}", dir, name))
1013 };
1014 if let Ok(real) = std::fs::canonicalize(&candidate) {
1015 // c:1014
1016 if real.is_file() {
1017 return Some(real.to_string_lossy().into_owned());
1018 }
1019 }
1020 }
1021 None // c:1014
1022}
1023
1024/// Bootstrap `module_path` / `MODULE_PATH` per `Src/init.c:1176`:
1025/// `module_path = mkarray(ztrdup(MODULE_DIR))` plus the matching
1026/// PM_TIED scalar from `Src/params.c:404` IPDEF8.
1027///
1028/// MODULE_DIR is a build-time `#define` from `Src/zshpaths.h`
1029/// (e.g. `/opt/homebrew/Cellar/zsh/5.9.1/lib`) resolved during
1030/// zsh's `./configure`. zshrs ships modules statically linked so
1031/// there is no equivalent build-time anchor. Probe the running
1032/// system's zsh once (cached via OnceLock) so `${module_path[1]}`
1033/// / `${(j.:.)module_path}` / `${#module_path}` agree between
1034/// `zshrs --zsh` and the system zsh that parity tests compare
1035/// against. Falls back to an empty array when no system zsh is
1036/// installed (this also makes paramtab carry an empty
1037/// `module_path` array rather than no entry at all, matching the
1038/// `mkarray(NULL)` shape the C code produces for an empty
1039/// MODULE_DIR build).
1040///
1041/// Called from `setupvals` (the canonical c:1014 entry point) and
1042/// from `ShellExecutor::new` (the bin entry that skips full
1043/// `setupvals` per the init_bltinmods comment at vm_helper.rs).
1044///
1045/// **Extension** — no direct C analog. The C source inlines this
1046/// at `Src/init.c:1176` inside `setupvals`. Allowlisted in
1047/// `tests/data/fake_fn_allowlist.txt` so ShellExecutor can call
1048/// just the module_path-init subset of setupvals without invoking
1049/// the full body (which would conflict with the bin entry's
1050/// piecewise paramtab init).
1051pub fn module_path_init() {
1052 use crate::ported::params::*;
1053 use crate::ported::zsh_h::*;
1054 static MODULE_DIR_CACHE: std::sync::OnceLock<Vec<String>> = std::sync::OnceLock::new();
1055 let module_dir: Vec<String> = MODULE_DIR_CACHE
1056 .get_or_init(|| {
1057 // c:Src/init.c:1176 — `module_path = mkarray(ztrdup(MODULE_DIR))`,
1058 // where `MODULE_DIR` is the C build's configure-time install
1059 // prefix: the directory holding the `.so`/`.bundle` files that
1060 // `try_load_module` (c:Src/module.c:1583) dlopens.
1061 //
1062 // zshrs's module directory is ITS OWN — `$ZSHRS_HOME/modules`,
1063 // default `~/.zshrs/modules`, the single-directory rule every
1064 // other zshrs artifact follows. It is emphatically NOT any zsh C
1065 // installation's `lib`: zshrs links its modules statically (the
1066 // port's `try_load_module` answers from `module_linked`, never
1067 // from a path search) and its runtime-loadable modules are
1068 // `znative` cdylibs reached through `zmodload -R`, which takes an
1069 // explicit path. Pointing this parameter at a foreign zsh's
1070 // bundle directory named files zshrs can never load, made the
1071 // value depend on which zsh happened to be installed, and — in
1072 // the shape this replaces — spent 7.3 ms of every single startup
1073 // forking `zsh -fc 'print -r ${module_path[1]}'` to ask.
1074 //
1075 // Consequence, stated plainly: `$module_path` does not match the
1076 // system zsh's, the same way `$0` does not (both are properties
1077 // of the running binary's own installation).
1078 let home = std::env::var_os("ZSHRS_HOME")
1079 .map(std::path::PathBuf::from)
1080 .or_else(|| {
1081 std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join(".zshrs"))
1082 });
1083 match home {
1084 Some(h) => vec![h.join("modules").to_string_lossy().into_owned()],
1085 // No $HOME and no $ZSHRS_HOME: C leaves `module_path` a
1086 // one-element array whatever MODULE_DIR was, so keep the
1087 // shape rather than inventing a path.
1088 None => vec![String::new()],
1089 }
1090 })
1091 .clone();
1092 let scalar_join = module_dir.join(":");
1093 let mut tab = paramtab().write().unwrap();
1094 // c:Src/init.c:1176 — module_path = PM_ARRAY tied to MODULE_PATH.
1095 let mp = Box::new(param {
1096 node: hashnode {
1097 next: None,
1098 nam: "module_path".to_string(),
1099 flags: (PM_ARRAY | PM_SPECIAL | PM_TIED) as i32,
1100 },
1101 u_data: 0,
1102 u_tied: None,
1103 u_arr: Some(module_dir),
1104 u_str: None,
1105 u_val: 0,
1106 u_dval: 0.0,
1107 u_hash: None,
1108 gsu_s: None,
1109 gsu_i: None,
1110 gsu_f: None,
1111 gsu_a: None,
1112 gsu_h: None,
1113 base: 0,
1114 width: 0,
1115 env: None,
1116 ename: Some("MODULE_PATH".to_string()),
1117 old: None,
1118 level: 0,
1119 });
1120 tab.insert("module_path".to_string(), mp);
1121 // c:Src/params.c:404 IPDEF8 — PM_TIED scalar mirroring
1122 // module_path with `:` join.
1123 let mps = Box::new(param {
1124 node: hashnode {
1125 next: None,
1126 nam: "MODULE_PATH".to_string(),
1127 flags: (PM_SCALAR | PM_SPECIAL | PM_TIED | PM_DONTIMPORT) as i32,
1128 },
1129 u_data: 0,
1130 u_tied: None,
1131 u_arr: None,
1132 u_str: Some(scalar_join),
1133 u_val: 0,
1134 u_dval: 0.0,
1135 u_hash: None,
1136 gsu_s: None,
1137 gsu_i: None,
1138 gsu_f: None,
1139 gsu_a: None,
1140 gsu_h: None,
1141 base: 0,
1142 width: 0,
1143 env: None,
1144 ename: Some("module_path".to_string()),
1145 old: None,
1146 level: 0,
1147 });
1148 tab.insert("MODULE_PATH".to_string(), mps);
1149}
1150
1151/// Port of `void setupvals(char *cmd, char *runscript, char *zsh_name)` from Src/init.c:1014.
1152///
1153/// Initialize lots of global variables and hash tables. // c:1014
1154pub fn setupvals(cmd: Option<&str>, runscript: Option<&str>, zsh_name: &str) {
1155 // c:1014
1156 let mut close_fds = [0i32; 10]; // c:1043
1157 let mut tmppipe = [-1i32; 2]; // c:1043
1158
1159 // Workaround NIS grabbing fd's 0-9 // c:1045
1160 #[cfg(unix)]
1161 unsafe {
1162 if libc::pipe(tmppipe.as_mut_ptr()) == 0 {
1163 // c:1053
1164 let mut i: i32 = -1; // c:1060
1165 while i < 9 {
1166 // c:1061
1167 let j: i32;
1168 if i < tmppipe[0] {
1169 // c:1063
1170 j = tmppipe[0]; // c:1064
1171 } else if i < tmppipe[1] {
1172 // c:1065
1173 j = tmppipe[1]; // c:1066
1174 } else {
1175 j = libc::dup(0); // c:1068
1176 if j == -1 {
1177 break;
1178 } // c:1069-1070
1179 }
1180 if j < 10 {
1181 // c:1072
1182 close_fds[j as usize] = 1; // c:1073
1183 } else {
1184 libc::close(j); // c:1075
1185 }
1186 if i < j {
1187 i = j;
1188 } // c:1076-1077
1189 }
1190 if i < tmppipe[0] {
1191 libc::close(tmppipe[0]);
1192 } // c:1079-1080
1193 if i < tmppipe[1] {
1194 libc::close(tmppipe[1]);
1195 } // c:1081-1082
1196 }
1197 }
1198
1199 // c:1085 — `(void)addhookdefs(NULL, zshhooks, sizeof(zshhooks)/sizeof(*zshhooks));`
1200 // Registers the four well-known hookdefs (exit, before_trap,
1201 // after_trap, get_color_attr) into the global `hooktab` chain.
1202 ZSHHOOKS_ADDED.call_once(|| {
1203 let base = zshhooks.load(std::sync::atomic::Ordering::SeqCst);
1204 let _ = crate::ported::module::addhookdefs(std::ptr::null(), base, 4);
1205 });
1206 // In C the `zsh/zle` module's boot_ (zle_main.c:2301) registers the
1207 // before_trap/after_trap hookfuncs AND the comphooks[] hookdefs
1208 // (insert_match, menu_start, compctl_make, compctl_cleanup,
1209 // comp_list_matches with def=ilistmatches). zshrs static-links ZLE, and
1210 // `zmodload zsh/zle` routes to features_, not boot_, so that
1211 // registration never happened — leaving `comp_list_matches` unregistered
1212 // so `list_matches` fell straight to the plain uncolored `ilistmatches`
1213 // and `zmodload zsh/complist`'s `addhookfunc` had no hookdef to attach
1214 // `complistmatches` to (every list-colors/group-colors style dropped).
1215 // Run the ZLE module boot once here at startup; addhookdef's duplicate
1216 // guard makes a later real boot_ idempotent.
1217 let _ = crate::ported::zle::zle_main::boot_(std::ptr::null());
1218 // C's zsh/complete module boot_ (complete.c:1758) attaches the funcs to
1219 // the ZLE hookdefs registered just above: complete/before_complete/
1220 // after_complete/list_matches/invalidate_list. Like zle_main::boot_ it
1221 // never ran (zmodload routes to features_), so after_complete's hook had
1222 // no func and menu-completion's menu_start (→ domenuselect) never fired.
1223 let _ = crate::ported::zle::complete::boot_(std::ptr::null());
1224 // init_eprog(); // c:1087
1225 // zero_mnumber.type = MN_INTEGER; zero_mnumber.u.l = 0; // c:1089-1090
1226
1227 // noeval = 0; // c:1092
1228 // curhist = 0; histsiz = DEFAULT_HISTSIZE; inithist(); // c:1093-1095
1229 let _ = crate::ported::hist::inithist();
1230 // cmdstack = zalloc(CMDSTACKSZ); cmdsp = 0; // c:1097-1098
1231 // bangchar = '!'; hashchar = '#'; hatchar = '^'; // c:1100-1102
1232 // termflags = TERM_UNKNOWN; // c:1103
1233 TERMFLAGS.store(TERM_UNKNOWN, Ordering::SeqCst);
1234 // curjob = prevjob = coprocin = coprocout = -1; // c:1104
1235 // c:1121 — `zgettime_monotonic_if_available(&shtimer); /* init
1236 // $SECONDS */`. C stamps `shtimer` at STARTUP. zshrs's analog
1237 // (`params::shtimer_lock()`) is a lazily-initialised OnceLock, so
1238 // without this it got stamped on its FIRST READ instead — making every
1239 // elapsed-since-shell-start measurement come out ~0. Visible in bare
1240 // `time`, whose real-time column is `now - shtimer`
1241 // (c:Src/jobs.c:1961 `dtime_ts(&dtimespec, &shtimer, &now)`): it
1242 // printed `0.000 total` and a nonsense `1684500% cpu`. Stamped with the
1243 // same wall-clock basis the consumers use (`SystemTime` since the
1244 // epoch), not CLOCK_MONOTONIC, so the subtraction below stays valid.
1245 // TOUCH, not assign: the lock's initialiser stamps "now" on first
1246 // access, so touching it primes it exactly once. Re-assigning here
1247 // would RE-stamp it on the `zsh_main` path, resetting $SECONDS after
1248 // the process-entry stamp in `bins/zshrs.rs::main` already ran.
1249 let _ = crate::ported::params::shtimer_lock(); // c:1121
1250 // srand((unsigned)(shtimer.tv_sec + shtimer.tv_nsec)); // c:1122
1251 #[cfg(unix)]
1252 unsafe {
1253 let mut ts: libc::timespec = std::mem::zeroed();
1254 libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut ts);
1255 libc::srand((ts.tv_sec as u32).wrapping_add(ts.tv_nsec as u32));
1256 }
1257
1258 // Set default path // c:1108
1259 // path = ["/bin", "/usr/bin", "/usr/ucb", "/usr/local/bin", NULL]; // c:1109-1114
1260 std::env::set_var(
1261 "PATH", // c:1109
1262 std::env::var("PATH")
1263 .unwrap_or_else(|_| "/bin:/usr/bin:/usr/ucb:/usr/local/bin".to_string()),
1264 );
1265
1266 // cdpath, manpath, fignore = mkarray(NULL) // c:1116-1118
1267 // fpath = ... // c:1132-1172
1268 // mailpath, psvar = mkarray(...) // c:1174-1175
1269 // modulestab = newmoduletable(17, "modules"); // c:1177
1270 // linkedmodules = znewlinklist(); // c:1178
1271
1272 module_path_init();
1273
1274 // Set default prompts // c:1180
1275 // prompt, prompt2, prompt3, prompt4, sprompt = ztrdup(...) // c:1181-1194
1276
1277 // ifs = EMULATION(KSH|SH) ? DEFAULT_IFS_SH : DEFAULT_IFS // c:1196-1197
1278 // wordchars = ztrdup(DEFAULT_WORDCHARS); postedit = ztrdup(""); // c:1198-1199
1279
1280 // If _ is set in environment then initialize our $_ by copying it // c:1200
1281 let underscore_val = std::env::var("_").unwrap_or_default(); // c:1201
1282 let metafied = crate::ported::utils::metafy(&underscore_val); // c:1202
1283 *zunderscore.lock().unwrap() = metafied.clone(); // c:1202
1284 underscoreused.store((metafied.len() + 1) as i32, Ordering::SeqCst); // c:1203
1285 let ulen = (metafied.len() + 1 + 31) & !31; // c:1204
1286 underscorelen.store(ulen, Ordering::SeqCst); // c:1205
1287 // zunderscore = zrealloc(zunderscore, underscorelen); // c:1205
1288
1289 // zoptarg = ""; zoptind = 1; // c:1207-1208
1290
1291 crate::ported::params::ppid.store(unsafe { libc::getppid() } as i64, Ordering::Relaxed); // c:1226
1292 crate::ported::params::mypid.store(unsafe { libc::getpid() } as i64, Ordering::Relaxed); // c:1227
1293 // term = ztrdup(""); // c:1212
1294
1295 // nullcmd = "cat"; readnullcmd = DEFAULT_READNULLCMD; // c:1214-1215
1296
1297 // cached_uid = getuid(); // c:1219
1298 // pswd = getpwuid(cached_uid); home = pswd->pw_dir; ... // c:1222-1234
1299 if let Ok(home) = std::env::var("HOME") {
1300 // c:1225
1301 let _ = home;
1302 }
1303
1304 // PWD/OLDPWD initialization // c:1236-1259
1305 let cwd = crate::ported::compat::zgetcwd(); // c:1252
1306 std::env::set_var("PWD", &cwd); // c:1253
1307 if std::env::var("OLDPWD").is_err() {
1308 // c:1255-1257
1309 std::env::set_var("OLDPWD", &cwd); // c:1256
1310 }
1311
1312 // c:1261 — `inittyptab();`. This is the FIRST call C makes, and it runs
1313 // here for a reason: `parseargs` has already set INTERACTIVE/SHINSTDIN, so
1314 // the one-shot `ZTF_INIT` latch inside (utils.c:4160-4164) sees the real
1315 // values and can raise `ZTF_INTERACT`. That bit is the head of the chain
1316 // c:4257 → `ZTF_BANGCHAR` → c:4291 → `bangchar` is `ISPECIAL`.
1317 //
1318 // zshrs reaches `inittyptab` EARLIER than C does — `ShellExecutor::new`
1319 // (vm_helper.rs, C's `createparamtable`, which C runs at c:1286, AFTER
1320 // this) and `lex_init` both seed the type table defensively because their
1321 // callers need `isident`/`iblank` working. Those calls happen before
1322 // options are parsed, so the latch was being decided with
1323 // `interact=false, shinstdin=false` (measured) and `ZTF_INTERACT` could
1324 // never be set — `!` was not a special character in ANY interactive
1325 // zshrs, and `${(q)v}` on `v='a!b'` gave `a!b` where zsh gives `a\!b`.
1326 //
1327 // Re-arm the latch so it is decided HERE, at C's own call site, with C's
1328 // inputs. The observable semantics are C's: computed once, from
1329 // `interact && isset(SHINSTDIN)`, and never revisited afterwards.
1330 crate::ported::ztype_h::TYPTAB_FLAGS.fetch_and(
1331 !crate::ported::ztype_h::ZTF_INIT,
1332 std::sync::atomic::Ordering::Relaxed,
1333 );
1334 crate::ported::utils::inittyptab(); // c:1261
1335 crate::ported::lex::initlextabs(); // c:1262
1336
1337 crate::ported::hashtable::createreswdtable(); // c:1264
1338 crate::ported::hashtable::createaliastables(); // c:1265
1339 crate::ported::hashtable::createcmdnamtable(); // c:1266
1340 crate::ported::hashtable::createshfunctable(); // c:1267
1341 let _ = crate::ported::builtin::createbuiltintable(); // c:1268
1342 crate::ported::hashnameddir::createnameddirtable(); // c:1269
1343 crate::ported::params::createparamtable(); // c:1270
1344
1345 // c:Src/init.c:1274-1276 — `#ifdef TIOCGWINSZ / adjustwinsize(0);`, the
1346 // first thing after createparamtable. Probes the tty via TIOCGWINSZ and
1347 // publishes the geometry to $COLUMNS/$LINES.
1348 //
1349 // zshrs never made this call, so it never asked the tty: $COLUMNS/$LINES
1350 // read 0 in every shell with a terminal (zsh reports 97/24 in a 97x24 pty).
1351 // Everything width-derived was then computed against 0 — prompt `%<<`
1352 // truncation, select lists, completion listing widths.
1353 //
1354 // Ordering is C's and it is load-bearing in both directions:
1355 // - It must follow init_io (c:1908), which is what sets SHTTY; while
1356 // SHTTY is -1 adjustwinsize early-returns (c:1900-1901) and does
1357 // nothing. That is also the correct behaviour with no terminal at all:
1358 // $COLUMNS keeps whatever the environment supplied, or 0.
1359 // - It must follow the environ import (createparamtable, c:1270) because
1360 // the tty geometry OVERRIDES an inherited COLUMNS: a 97-column terminal
1361 // reports 97 even when COLUMNS=10 was exported in. (C reaches that via
1362 // the c:1906-1907 "Signal missed while a job owned the tty?" promotion
1363 // of from=0 to from=1, which makes adjustcolumns take the signalled
1364 // path and overwrite zterm_columns with ws_col.)
1365 //
1366 // Note SHTTY does not require stdin/stdout to be a terminal: init_io's last
1367 // resort is `open("/dev/tty")` (c:667-670), so a piped-but-still-attached
1368 // shell gets the real width too — `zsh -fc 'print $COLUMNS | cat'` in a
1369 // 97-column terminal prints 97.
1370 let _ = crate::ported::utils::adjustwinsize(0); // c:1276
1371
1372 // c:Src/init.c:1180-1194 — default prompts. C sets the `prompt`/
1373 // `prompt2`/`prompt3`/`prompt4`/`sprompt` globals (which IPDEF7 binds
1374 // to PS1/PS2/PS3/PS4/SPROMPT) BEFORE createparamtable; zshrs creates
1375 // those special scalars empty, so set the defaults here, right after.
1376 // - prompt/prompt2 are gated on INTERACTIVE (c:1181): a
1377 // non-interactive shell keeps empty PS1/PS2. zshrs's INTERACTIVE
1378 // is isatty-based and is NOT downgraded for `-c`/script, so also
1379 // require reading from stdin (no `-c`, no runscript) — the
1380 // SHINSTDIN condition — to match zsh's "interactive" gate.
1381 // - prompt3/prompt4/sprompt are set unconditionally (c:1191-1194).
1382 // Each default only applies when the param is still empty, so an
1383 // env-imported value (e.g. `PS4` exported) wins, mirroring C where
1384 // importenv overrides the compiled defaults.
1385 {
1386 let ksh_sh = crate::ported::zsh_h::EMULATION(EMULATE_KSH | EMULATE_SH);
1387 let set_default = |name: &str, val: &str| {
1388 // C sets these defaults at c:1196-1206, i.e. BEFORE
1389 // `createparamtable()` (c:1270) walks `environ` and assigns
1390 // over them (c:919-924 `assignsparam(iname, ..,
1391 // ASSPM_ENV_IMPORT)`), so an inherited value ALWAYS wins —
1392 // including an inherited EMPTY one. zshrs runs the import
1393 // first and the defaults second, so "already set" has to be
1394 // decided the way C decides it: was the name in the
1395 // environment? Testing `getsparam().is_empty()` conflated
1396 // "unset" with "exported as empty" and re-seeded the default
1397 // over an explicit `export PS1=` — the exact idiom
1398 // Test/W02jobs.ztst's `zpty_start` uses to silence the prompt,
1399 // which is why every zpty chunk's expected output was prefixed
1400 // with a live `%m%# ` prompt.
1401 if std::env::var_os(name).is_some() {
1402 return;
1403 }
1404 if getsparam(name).map_or(true, |v| v.is_empty()) {
1405 crate::ported::params::setsparam(name, val);
1406 }
1407 };
1408 // c:1181 — interactive shell reading from stdin gets the
1409 // hostname prompt; ksh/sh emulation gets the bare `$`/`#`.
1410 if isset(INTERACTIVE) && cmd.is_none() && runscript.is_none() {
1411 if ksh_sh {
1412 set_default(
1413 "PS1",
1414 if crate::ported::utils::privasserted() {
1415 "# "
1416 } else {
1417 "$ "
1418 },
1419 ); // c:1185
1420 set_default("PS2", "> "); // c:1186
1421 } else {
1422 set_default("PS1", "%m%# "); // c:1188
1423 set_default("PS2", "%_> "); // c:1189
1424 }
1425 }
1426 set_default("PS3", "?# "); // c:1191
1427 set_default("PS4", if ksh_sh { "+ " } else { "+%N:%i> " }); // c:1192-1193
1428 set_default("SPROMPT", "zsh: correct '%R' to '%r' [nyae]? "); // c:1194
1429 }
1430
1431 // condtab = NULL; wrappers = NULL; // c:1272-1273
1432
1433 crate::ported::utils::adjustwinsize(0); // c:1276
1434
1435 // getrlimit loop // c:1286-1289
1436
1437 // breaks = contflag = loops = ancestor_loops = 0; (c:1328; statics start at 0)
1438 // lastmailcheck = zmonotime(NULL); // c:1293
1439 // locallevel = sourcelevel = 0; // c:1294
1440 sourcelevel.store(0, Ordering::SeqCst); // c:1294
1441 // sfcontext = SFC_NONE; trap_return = 0; // c:1295-1296
1442 // trap_state = TRAP_STATE_INACTIVE; // c:1297
1443 // noerrexit = NOERREXIT_EXIT|RETURN|SIGNAL; // c:1298
1444 crate::ported::exec::nohistsave.store(1, Ordering::SeqCst); // c:1332
1445 // dirstack = znewlinklist(); bufstack = znewlinklist(); // c:1300-1301
1446 // hsubl = hsubr = NULL; lastpid = 0; // c:1302-1303
1447
1448 // get_usage(); // c:1305
1449
1450 // Close fd's we opened to block 0-9 // c:1307
1451 #[cfg(unix)]
1452 for i in 0..10 {
1453 // c:1308
1454 if close_fds[i] != 0 {
1455 // c:1309
1456 unsafe {
1457 libc::close(i as i32);
1458 } // c:1310
1459 }
1460 }
1461
1462 crate::ported::prompt::set_default_colour_sequences(); // c:1313
1463
1464 // c:1315-1326 — `/* ZSH_EXEPATH */`: the absolute path of the running
1465 // executable, from argv[0] resolved against the start-up cwd. zshrs
1466 // keeps C's `pwd` global in $PWD.
1467 {
1468 let exename = argv0.lock().unwrap().clone(); // c:1318
1469 let cwd = crate::ported::params::getsparam("PWD"); // c:1319
1470 if let Some(mypath) = getmypath(Some(&exename), cwd.as_deref()) {
1471 // c:1320-1323
1472 crate::ported::params::setsparam("ZSH_EXEPATH", &mypath); // c:1324
1473 }
1474 }
1475
1476 // These are PARAMETERS (`setsparam`), not environment entries. The port
1477 // used `std::env::set_var`, which put ZSH_EXEPATH / ZSH_SCRIPT /
1478 // ZSH_EXECUTION_STRING / ZSH_NAME into the environ every external
1479 // command inherits; under zsh a child sees none of them.
1480 if let Some(cmd) = cmd {
1481 // c:1182
1482 crate::ported::params::setsparam("ZSH_EXECUTION_STRING", cmd); // c:1183
1483 }
1484 if let Some(rs) = runscript {
1485 // c:1184
1486 crate::ported::params::setsparam("ZSH_SCRIPT", rs); // c:1185
1487 }
1488 // c:1186 — `setsparam("ZSH_NAME", ztrdup(zsh_name));`. Not repeated
1489 // here: ShellExecutor::new (vm_helper.rs, `setsparam("ZSH_NAME", "zsh")`)
1490 // has already created the param with the literal "zsh", and writing the
1491 // basename of argv[0] ("zshrs") over it would change $ZSH_NAME.
1492 let _ = zsh_name;
1493}
1494
1495/// Port of `static void setupshin(char *runscript)` from Src/init.c:1340.
1496fn setupshin(runscript: Option<&str>) {
1497 // c:1340
1498 if let Some(script) = runscript {
1499 // c:1340
1500 let funmeta = unmeta(script); // c:1346
1501 let mut sfname: Option<String> = None; // c:1343
1502 if std::path::Path::new(&funmeta).is_file() {
1503 // c:1350-1352
1504 sfname = Some(script.to_string()); // c:1353
1505 } else if crate::ported::zsh_h::isset(crate::ported::zsh_h::PATHSCRIPT)
1506 && !script.contains('/')
1507 {
1508 // c:1354-1360 — with PATHSCRIPT, search $PATH when the script
1509 // name has no directory part.
1510 sfname = crate::ported::utils::pathprog(script)
1511 .map(|p| p.to_string_lossy().into_owned()); // c:1359
1512 }
1513 if sfname.is_none() {
1514 // c:1361
1515 crate::ported::utils::zerr(&format!(
1516 // c:1364
1517 "can't open input file: {}",
1518 script
1519 ));
1520 std::process::exit(127); // c:1365
1521 }
1522 }
1523 // lineno = 1; // c:1394
1524 // shinbufalloc(); // c:1394
1525}
1526
1527/// Port of `void init_signals(void)` from Src/init.c:1394.
1528pub fn init_signals() {
1529 // c:1394
1530 // c:1398-1399 — `sigtrapped = hcalloc(TRAPCOUNT * sizeof(int));`
1531 // and `siglists = hcalloc(TRAPCOUNT * sizeof(Eprog));`. Both tables
1532 // are statics in signals.rs, sized at their declaration.
1533
1534 // c:1401-1406 — `if (interact) { signal_setmask(signal_mask(0));
1535 // for (i=0; i<NSIG; ++i) signal_default(i); }`. Reset to default
1536 // dispositions on every signal at startup so a parent's stale
1537 // handlers don't leak in.
1538 #[cfg(unix)]
1539 if interact() {
1540 let empty = crate::ported::signals::signal_mask(0);
1541 let _ = crate::ported::signals::signal_setmask(&empty);
1542 // c:1404 — `for (i=0; i<NSIG; ++i) signal_default(i);`. NSIG
1543 // is `<signal.h>`-provided in C; libc-rs doesn't re-export it
1544 // directly. `signals_h::SIGCOUNT` is the canonical port of
1545 // NSIG-1 (Linux=64, macOS=31).
1546 //
1547 // The previous Rust port used `1..64i32` — hardcoded, missing
1548 // signal 64 on Linux (where NSIG=65) AND iterating PAST the
1549 // valid range on macOS (where signals 32..63 don't exist).
1550 // Use `1..=SIGCOUNT` so the loop iterates exactly NSIG-1
1551 // signals on each platform, skipping signal 0 (C iterates
1552 // it but signal_default(0) is implementation-defined).
1553 for i in 1..=crate::ported::signals_h::SIGCOUNT {
1554 let _ = crate::ported::signals::signal_default(i);
1555 }
1556 }
1557
1558 // c:1407 — `sigchld_mask = signal_mask(SIGCHLD);`. The mask is a pure
1559 // function of SIGCHLD, so `child_block`/`child_unblock` (signals_h.rs)
1560 // derive it with `signal_mask` at each use instead of reading a cached copy.
1561
1562 intr(); // c:1409
1563
1564 #[cfg(unix)]
1565 {
1566 // c:1444-1445 — detect a parent-installed SIG_IGN on SIGQUIT and
1567 // record it as an ignored trap. The body lives in
1568 // `extensions::startup_signals` because `bins/zshrs.rs`'s `-c` and
1569 // script-file dispatch bypass this function entirely and need the
1570 // same two lines; keeping ONE implementation avoids the two paths
1571 // drifting apart.
1572 crate::startup_signals::record_inherited_sigquit_ignore();
1573 // c:1414-1416 — `#ifndef QDEBUG signal_ignore(SIGQUIT)`.
1574 signal_ignore(libc::SIGQUIT);
1575
1576 // c:1418-1421 — SIGHUP: if parent installed SIG_IGN, clear
1577 // the HUP option; otherwise install our handler.
1578 if signal_ignore(libc::SIGHUP) == libc::SIG_IGN {
1579 dosetopt(HUP, 0, 0); // c:1419
1580 } else {
1581 install_handler(libc::SIGHUP); // c:1421
1582 }
1583 install_handler(libc::SIGCHLD); // c:1422
1584 #[cfg(not(target_os = "haiku"))]
1585 {
1586 install_handler(libc::SIGWINCH); // c:1424
1587 // c:1425 — `winch_block(); /* See utils.c:preprompt() */`
1588 //
1589 // The standing block is the whole delivery policy for
1590 // SIGWINCH: from here on the handler runs ONLY inside an
1591 // explicit unblock window — `preprompt` (c:Src/utils.c:1540),
1592 // `raw_getbyte`'s poll/read (c:Src/Zle/zle_main.c:588/850,
1593 // ported at zle/zle_main.rs:504/648/757) and `readoutput`
1594 // (c:Src/input.c:277). C's remaining unblocks
1595 // (c:Src/exec.c:533/571/576/586/590/632) are not windows at
1596 // all: they sit in the forked child immediately before
1597 // `execve`, and only decide the mask the new program
1598 // inherits. Everywhere else — a widget body, and
1599 // so the whole of completion — a resize stays PENDING and
1600 // `zterm_columns`/`zterm_lines` hold the geometry the
1601 // current redraw started with.
1602 //
1603 // Without it the block existed only as a SIDE EFFECT of
1604 // `raw_getbyte` re-blocking after each read
1605 // (zle_main.rs:507/654/762): every unblock happened to be
1606 // paired, so the mask happened to be right. An UNPAIRED
1607 // unblock then leaves SIGWINCH live until the next
1608 // `raw_getbyte` re-block — i.e. for the rest of the
1609 // current command, a completion included.
1610 // `zexecve_recover` had four of them (vm_helper.rs,
1611 // c:565/570/580/584/626, child-side in C).
1612 //
1613 // Whenever the handler does run inside a widget,
1614 // `adjustwinsize(1)` lands in the middle of `calclist` and
1615 // display strings built against 80 columns get counted
1616 // against the new 60. Measured that way, `git <TAB>`
1617 // across a 24x80 -> 24x60 resize asks "see all 164
1618 // possibilities (251 lines)?" for 164 one-line matches
1619 // where zsh asks 153/153.
1620 crate::ported::signals_h::winch_block(); // c:1425
1621 }
1622
1623 // c:1427-1431 — interactive-only handlers.
1624 if interact() {
1625 install_handler(libc::SIGPIPE); // c:1428
1626 install_handler(libc::SIGALRM); // c:1429
1627 signal_ignore(libc::SIGTERM); // c:1430
1628 }
1629
1630 // c:1432-1436 — `if (jobbing)` job-control signal ignores.
1631 if jobbing() {
1632 signal_ignore(libc::SIGTTOU); // c:1433
1633 signal_ignore(libc::SIGTSTP); // c:1434
1634 signal_ignore(libc::SIGTTIN); // c:1435
1635 }
1636 }
1637}
1638
1639/// Port of `void run_init_scripts(void)` from Src/init.c:1445.
1640/// Runs the standard zsh init scripts (or KSH/SH-compat scripts when
1641/// emulating). Each guard mirrors the C predicate set: islogin, RCS,
1642/// GLOBALRCS, PRIVILEGED, INTERACTIVE.
1643pub fn run_init_scripts() {
1644 // c:1445
1645
1646 // c:1447 — noerrexit = NOERREXIT_EXIT | NOERREXIT_RETURN | NOERREXIT_SIGNAL;
1647 crate::ported::exec::noerrexit.store(
1648 crate::ported::zsh_h::NOERREXIT_EXIT
1649 | crate::ported::zsh_h::NOERREXIT_RETURN
1650 | crate::ported::zsh_h::NOERREXIT_SIGNAL,
1651 Ordering::SeqCst,
1652 );
1653
1654 // c:1449 — if (EMULATION(EMULATE_KSH|EMULATE_SH)) { ... }
1655 let emul = emulation.load(Ordering::SeqCst);
1656 let is_posix = (emul & (EMULATE_KSH | EMULATE_SH) as i32) != 0;
1657
1658 let is_login = islogin();
1659 let interact = isset(INTERACTIVE);
1660 let privileged = isset(PRIVILEGED);
1661
1662 // ZSHRS-ONLY, no C counterpart. C models two startup-file sets — zsh's
1663 // and one lumped Bourne one — because `emulate sh`/`ksh` is all it
1664 // offers. zshrs ships drop-ins for eight shells, and each reads its
1665 // own files: `--bash` wants `~/.bashrc` and the `~/.bash_profile`
1666 // chain, `--ksh` wants `$ENV` defaulted to `~/.kshrc`, `--mksh`
1667 // `~/.mkshrc`, `--csh` `~/.cshrc` + `~/.login`. Routed through
1668 // `extensions::emulation_startup`, which owns that table. Only a
1669 // selected DROP-IN takes this path; a runtime `emulate sh` keeps the
1670 // faithful branches below.
1671 if crate::extensions::emulation_startup::overrides_zsh_startup() {
1672 crate::extensions::emulation_startup::run_init_scripts();
1673 crate::ported::exec::noerrexit.store(0, Ordering::SeqCst); // c:1549
1674 crate::ported::exec::nohistsave.store(0, Ordering::SeqCst); // c:1550
1675 return;
1676 }
1677
1678 if is_posix {
1679 // c:1450-1451 — if (islogin) source("/etc/profile");
1680 if is_login {
1681 let _ = source("/etc/profile");
1682 }
1683 if !privileged {
1684 // c:1452
1685 // c:1454 — if (islogin) sourcehome(".profile");
1686 if is_login {
1687 sourcehome(".profile");
1688 }
1689 // c:1456-1468 — if (interact) { … getsparam("ENV"); source(s); }
1690 if interact {
1691 if let Some(s) = getsparam("ENV") {
1692 let _ = source(&s);
1693 }
1694 }
1695 } else {
1696 // c:1470 — source("/etc/suid_profile");
1697 let _ = source("/etc/suid_profile");
1698 }
1699 } else {
1700 // !!! RUST-ONLY — no C counterpart. A `zshrs-recorder` shard
1701 // (~/.zshrs/images/*-recorder.rkyv) holds the end-state of this
1702 // whole chain; with `[shell].skip_configs` not "off", replay it
1703 // and source NONE of the files below, /etc/zshenv included.
1704 // `-f` (RCS unset), PRIVILEGED and the `--zsh` parity mode
1705 // (`emulating()`) keep the faithful C chain. A shard that
1706 // applies zero rows falls through to sourcing.
1707 if isset(RCS)
1708 && !privileged
1709 && !crate::extensions::emulation_startup::emulating()
1710 && crate::canonical_apply::replay_startup()
1711 {
1712 crate::ported::exec::noerrexit.store(0, Ordering::SeqCst); // c:1549
1713 crate::ported::exec::nohistsave.store(0, Ordering::SeqCst); // c:1550
1714 return;
1715 }
1716 // c:1473 — source(GLOBAL_ZSHENV);
1717 let _ = source(&crate::extensions::global_rc::global_rc_path(
1718 crate::ported::config_h::GLOBAL_ZSHENV,
1719 ));
1720
1721 // c:1476-1490 — if (isset(RCS) && unset(PRIVILEGED))
1722 // { newuser-probe; sourcehome(".zshenv"); }
1723 if isset(RCS) && !privileged {
1724 sourcehome(".zshenv");
1725 }
1726 // c:1491-1498 — if (islogin) { GLOBAL_ZPROFILE? + .zprofile }
1727 if is_login {
1728 if isset(RCS) && isset(GLOBALRCS) {
1729 let _ = source(&crate::extensions::global_rc::global_rc_path(
1730 crate::ported::config_h::GLOBAL_ZPROFILE,
1731 ));
1732 }
1733 if isset(RCS) && !privileged {
1734 sourcehome(".zprofile");
1735 }
1736 }
1737 // c:1499-1506 — if (interact) { GLOBAL_ZSHRC? + .zshrc }
1738 if interact {
1739 if isset(RCS) && isset(GLOBALRCS) {
1740 let _ = source(&crate::extensions::global_rc::global_rc_path(
1741 crate::ported::config_h::GLOBAL_ZSHRC,
1742 ));
1743 }
1744 if isset(RCS) && !privileged {
1745 sourcehome(".zshrc");
1746 }
1747 }
1748 // c:1507-1514 — if (islogin) { GLOBAL_ZLOGIN? + .zlogin }
1749 if is_login {
1750 if isset(RCS) && isset(GLOBALRCS) {
1751 let _ = source(&crate::extensions::global_rc::global_rc_path(
1752 crate::ported::config_h::GLOBAL_ZLOGIN,
1753 ));
1754 }
1755 if isset(RCS) && !privileged {
1756 sourcehome(".zlogin");
1757 }
1758 }
1759 }
1760 crate::ported::exec::noerrexit.store(0, Ordering::SeqCst); // c:1549
1761 crate::ported::exec::nohistsave.store(0, Ordering::SeqCst); // c:1550
1762}
1763
1764/// Port of `void init_misc(char *cmd, char *zsh_name)` from Src/init.c:1524.
1765pub fn init_misc(cmd: Option<&str>, zsh_name: &str) {
1766 // c:1524
1767 if zsh_name.starts_with('r') {
1768 // c:1524
1769 crate::ported::utils::zerrnam(
1770 zsh_name, // c:1527
1771 "no support for restricted mode",
1772 );
1773 std::process::exit(1); // c:1528
1774 }
1775 if let Some(cmdstr) = cmd {
1776 // c:1530
1777 // if (SHIN >= 10) close(SHIN); // c:1531-1532
1778 // SHIN = movefd(open("/dev/null", O_RDONLY|O_NOCTTY)); // c:1551
1779 // shinbufreset(); // c:1551
1780 // execstring(cmd, 0, 1, "cmdarg"); // c:1551
1781 let _ = cmdstr;
1782 // stopmsg = 1; zexit(...); // c:1551-1537
1783 std::process::exit(0);
1784 }
1785
1786 // c:1573-1574 — `if (interact && isset(RCS))
1787 // readhistfile(NULL, 0, HFILE_USE_OPTIONS);`
1788 // — THE startup read of $HISTFILE into the history ring. Without
1789 // it an interactive shell starts with empty history (up-arrow /
1790 // fc -l see nothing from previous sessions).
1791 if crate::ported::zsh_h::interact() && isset(RCS) {
1792 crate::ported::hist::readhistfile(None, 0, crate::ported::zsh_h::HFILE_USE_OPTIONS as i32);
1793 // c:1574
1794 }
1795}
1796
1797/// Port of `mod_export enum source_return source(char *s)` from Src/init.c:1551.
1798///
1799/// Body dispatches through `with_executor` to the fusevm bytecode
1800/// pipeline (`execute_script_zsh_pipeline`), matching the path
1801/// `bins/zshrs.rs::source_from_memory` takes. `scriptname` /
1802/// `scriptfilename` / `sourcelevel` are saved+restored around the
1803/// nested execution per the C save/restore at c:1572-1670.
1804pub fn source(s: &str) -> i32 {
1805 // c:1551
1806 let us = unmeta(s); // c:1551
1807 let path = std::path::Path::new(&us);
1808 // c:1565-1568 — `if (!s || (!(prog = try_source_file(us)) &&
1809 // (tempfd = ... open(us, ...)) == -1)) return SOURCE_NOT_FOUND;`
1810 // — a loadable `<file>.zwc` rescues a missing/unreadable plain
1811 // file; only BOTH failing is NOT_FOUND. The compiled prog itself
1812 // is fetched below (the Rust port defers it to the contents
1813 // read so the state save/restore stays in one place).
1814 let zwc_prog = crate::ported::parse::try_source_file(&us);
1815 if zwc_prog.is_none() && !path.exists() {
1816 return 1; /* SOURCE_NOT_FOUND */
1817 }
1818
1819 // c:1571-1581 — save shell state.
1820 let old_scriptname = crate::ported::utils::scriptname_get(); // c:1573
1821 let old_scriptfilename = crate::ported::utils::scriptfilename_get(); // c:1574
1822 crate::ported::utils::set_scriptname(Some(us.clone())); // c:1572
1823 crate::ported::utils::set_scriptfilename(Some(us.clone()));
1824
1825 // c:1627-1630 — `if (isset(SOURCETRACE)) { printprompt4();
1826 // fprintf(xtrerr ? xtrerr : stderr, "<sourcetrace>\n"); }`
1827 if isset(crate::ported::zsh_h::SOURCETRACE) {
1828 // c:1621 — `lineno = 1;` precedes the trace, so PS4 `%i` reads 1.
1829 // The file's own statements set it again as they run; put the
1830 // caller's value back for the frame push below.
1831 let oldlineno = crate::ported::params::getsparam("LINENO")
1832 .and_then(|s| s.parse::<i64>().ok())
1833 .unwrap_or(0);
1834 crate::fusevm_bridge::set_lineno_impl(1); // c:1621
1835 crate::ported::utils::printprompt4(); // c:1628
1836 crate::fusevm_bridge::xtrerr_fputs("<sourcetrace>\n"); // c:1629
1837 crate::fusevm_bridge::xtrerr_flush();
1838 crate::fusevm_bridge::set_lineno_impl(oldlineno);
1839 }
1840
1841 sourcelevel.fetch_add(1, Ordering::SeqCst); // c:1606
1842
1843 // c:1610-1618 — push an FS_SOURCE funcstack frame so `$funcstack`,
1844 // `$functrace` and `$funcfiletrace` include the sourced file (the
1845 // readers at parameter.rs walk FUNCSTACK and special-case
1846 // `tp == FS_SOURCE`). FUNCSTACK is a Vec stack: the last element is
1847 // the top, so `prev` is left None (the index encodes the link, same
1848 // convention as the FS_FUNC push in exec.rs::doshfunc:5821).
1849 let oldlineno = crate::ported::lex::lineno() as i64; // c:1576 oldlineno = lineno
1850 {
1851 // c:1612-1613 — caller: the current funcstack top, else the
1852 // previous script filename, else "zsh".
1853 let caller = {
1854 let stk = crate::ported::modules::parameter::FUNCSTACK
1855 .lock()
1856 .unwrap_or_else(|e| e.into_inner());
1857 stk.last().map(|f| f.name.clone())
1858 }
1859 .or_else(|| old_scriptfilename.clone())
1860 .or_else(|| Some("zsh".to_string()));
1861 let frame = crate::ported::zsh_h::funcstack {
1862 prev: None, // c:1617 (Vec-stack index encodes link)
1863 name: us.clone(), // c:1611 fstack.name = scriptfilename
1864 filename: Some(us.clone()), // c:1616
1865 caller, // c:1612
1866 flineno: 0, // c:1614
1867 lineno: oldlineno, // c:1615
1868 tp: crate::ported::zsh_h::FS_SOURCE, // c:1618
1869 };
1870 crate::ported::modules::parameter::FUNCSTACK
1871 .lock()
1872 .unwrap_or_else(|e| e.into_inner())
1873 .push(frame); // c:1618 funcstack = &fstack
1874 }
1875
1876 // c:1618-1642 — parse-and-execute loop. Route through the
1877 // fusevm executor for the actual parse+exec; if no executor
1878 // context (out-of-band call), fall back to the partial
1879 // read-for-side-effects path so errors still surface.
1880 //
1881 // c:1566 — `try_source_file(us)` runs FIRST: a sibling
1882 // `<file>.zwc` newer than the file (or `s` itself being a
1883 // `.zwc`) supplies the compiled wordcode instead of the plain
1884 // read. Bridge wordcode → text via getpermtext (same as the
1885 // `.zwc` autoload path in exec.rs::loadautofn).
1886 let from_zwc = zwc_prog.is_some();
1887 let contents = match zwc_prog {
1888 Some(prog) => Ok(crate::ported::text::getpermtext(Box::new(prog), None, 0)),
1889 // c:1566/1626 — source() reads the file as raw bytes; a
1890 // non-UTF-8 byte is metafied, not an error (Src/utils.c:4856).
1891 None => crate::script_bytes::read_script_file(path),
1892 };
1893 if let Ok(body) = contents {
1894 // c:Src/jobs.c:1878-1884 — zsh runs the sourced list through
1895 // execpline, whose per-pipeline `initjob()` caps recursion at
1896 // MAX_MAXJOBS: `zerr("job table full or recursion limit exceeded")`
1897 // then bails. The fusevm pipeline path below doesn't allocate a job
1898 // per pipeline, so without this guard runaway `. self`-style
1899 // recursion (invisible to FUNCNEST, which counts FS_FUNC frames
1900 // only) overflowed the 256 MB main-thread stack → uncatchable
1901 // SIGBUS. Reproduce the ceiling: total FUNCSTACK depth is the proxy
1902 // for zsh's concurrently-held job slots. At/over the ceiling, raise
1903 // the zsh-identical error (zerr sets ERRFLAG_ERROR so the outer
1904 // sourced lists unwind) and refuse the deeper body — the FS_SOURCE
1905 // frame just pushed is popped normally below.
1906 let over_limit = crate::ported::modules::parameter::FUNCSTACK
1907 .lock()
1908 .map(|s| s.len())
1909 .unwrap_or(0)
1910 >= crate::ported::jobs::MAX_MAXJOBS;
1911 if over_limit {
1912 crate::ported::utils::zerr("job table full or recursion limit exceeded");
1913 crate::ported::builtin::LASTVAL.store(1, Ordering::Relaxed);
1914 } else if from_zwc {
1915 // c:1621 — `execode(prog, 1, 0, "filecode")`. The deparse of an
1916 // already-compiled program is re-lexed here, so it needs the
1917 // lexer pinned to the spelling `untokenize` writes; see
1918 // ShellExecutor::execute_zwc_program.
1919 let _ = crate::fusevm_bridge::execute_zwc_program(&body);
1920 } else {
1921 let _ = crate::ported::exec::execute_script_zsh_pipeline(&body);
1922 }
1923 }
1924
1925 sourcelevel.fetch_sub(1, Ordering::SeqCst); // c:1644
1926
1927 // c:1664 — `funcstack = fstack.prev;` — pop our FS_SOURCE frame.
1928 crate::ported::modules::parameter::FUNCSTACK
1929 .lock()
1930 .unwrap_or_else(|e| e.into_inner())
1931 .pop();
1932
1933 // c:1646-1670 — restore shell state.
1934 crate::ported::utils::set_scriptname(old_scriptname);
1935 crate::ported::utils::set_scriptfilename(old_scriptfilename);
1936 0 /* SOURCE_OK */ // c:1679
1937}
1938
1939/// Port of `void sourcehome(char *s)` from Src/init.c:1679.
1940pub fn sourcehome(s: &str) {
1941 // c:1679
1942 queue_signals(); // c:1679
1943 let emul = emulation.load(Ordering::SeqCst);
1944 let is_posix = (emul & 6) != 0;
1945 // c:1684 — `h = is_posix ? getsparam("HOME") : (getsparam("ZDOTDIR")
1946 // ?: getsparam("HOME"))`
1947 // paramtab read; was OS env.
1948 let h = if is_posix {
1949 getsparam("HOME")
1950 } else {
1951 getsparam("ZDOTDIR").or_else(|| getsparam("HOME"))
1952 };
1953 let h = match h {
1954 // c:1685-1689
1955 Some(h) => h,
1956 None => {
1957 unqueue_signals();
1958 return;
1959 }
1960 };
1961 let buf = format!("{}/{}", h, s); // c:1713
1962 unqueue_signals(); // c:1713
1963 source(&buf); // c:1713
1964}
1965
1966/// Port of `void init_bltinmods(void)` from Src/init.c:1703.
1967pub fn init_bltinmods() {
1968 // c:1703
1969 // #include "bltinmods.list" // c:1705
1970 // load_module("zsh/main", NULL, 0); // c:1706
1971 //
1972 // C's bltinmods.list is autotools-generated and contains a
1973 // `register_module(name, setup_, ...)` line per statically-linked
1974 // module — which seeds MODULESTAB and runs each module's setup_
1975 // (and later boot_) entry points. Our equivalent is the
1976 // register_builtin_modules() call inside `modulestab::new()`, which
1977 // is reached the first time MODULESTAB is observed. Force that
1978 // observation here so default-loaded modules
1979 // (zsh/parameter, zsh/main, zsh/watch, …) register their
1980 // builtins/params/preprompt hooks before user code runs.
1981 drop(crate::ported::module::MODULESTAB.lock().unwrap());
1982}
1983
1984/// Port of `mod_export void noop_function(void)` from Src/init.c:1713.
1985pub fn noop_function() { // c:1713
1986 /* do nothing */ // c:1720
1987}
1988
1989/// Port of `mod_export void noop_function_int(int nothing)` from Src/init.c:1720.
1990pub fn noop_function_int(_nothing: i32) { // c:1720
1991 /* do nothing */ // c:1720
1992}
1993
1994/// Once-guard for the `zleentry` module autoload (c:1755 `case 0:`).
1995///
1996/// In C the guard IS `zle_load_state == 0`: ZLE is dlopened lazily by
1997/// the first `zleentry` call, and that same call loads `zsh/compctl`
1998/// alongside it (c:1764-1765). zshrs links ZLE in statically and
1999/// initialises it earlier, in `zsh_main`, which already sets
2000/// `zle_load_state = 1` — so the C gate can never fire at the first ZLE
2001/// read. This flag restores the one-shot semantics for the
2002/// module-registration half of c:1755-1773, which still has to happen
2003/// at the first zleentry-equivalent call and not before.
2004///
2005/// The observable effect of that half: an interactive shell has
2006/// `zsh/zle`, `zsh/complete` (a `zsh/compctl` dependency) and
2007/// `zsh/compctl` marked loaded from its first line read onwards, while
2008/// `-c` / script runs — which never reach a ZLE read — leave all three
2009/// merely `autoloaded`. `_default` (Completion/Zsh/Context/_default)
2010/// branches on `zmodload -e zsh/compctl` to decide whether the legacy
2011/// `compcall` engine is available, so this is load-bearing for
2012/// completion parity, not just for `zmodload` listings.
2013pub static zle_modules_loaded: std::sync::atomic::AtomicBool =
2014 std::sync::atomic::AtomicBool::new(false);
2015
2016/// Port of `mod_export char *zleentry(...)` from Src/init.c:1743.
2017pub fn zleentry(cmd: i32) -> Option<String> {
2018 // c:1743
2019 let mut cmd = cmd;
2020 match zle_load_state.load(Ordering::SeqCst) {
2021 // c:1755
2022 0 => {
2023 // c:1756
2024 // c:1761-1762 — `if (cmd != ZLE_CMD_TRASH &&
2025 // cmd != ZLE_CMD_RESET_PROMPT && cmd != ZLE_CMD_REFRESH)`
2026 if cmd != crate::ported::zsh_h::ZLE_CMD_TRASH
2027 && cmd != crate::ported::zsh_h::ZLE_CMD_RESET_PROMPT
2028 && cmd != crate::ported::zsh_h::ZLE_CMD_REFRESH
2029 {
2030 // c:1764 — `if (load_module("zsh/zle", NULL, 0) != 1)`
2031 // then c:1765 `(void)load_module("zsh/compctl", NULL, 0)`.
2032 // `zsh/compctl` pulls in `zsh/complete` through the
2033 // compctl.mdd moddeps edge seeded in
2034 // register_builtin_modules.
2035 if !zle_modules_loaded.swap(true, Ordering::SeqCst) {
2036 let mut tab = crate::ported::module::MODULESTAB.lock().unwrap();
2037 if tab.load_module("zsh/zle", None, false) != 1 {
2038 tab.load_module("zsh/compctl", None, false); // c:1765
2039 }
2040 }
2041 zle_load_state.store(2, Ordering::SeqCst); // c:1770
2042 }
2043 }
2044 1 => {
2045 // c:1776
2046 // c:1777 — `ret = zle_entry_ptr(cmd, ap);`. The pointer is
2047 // set at Src/Zle/zle_main.c:2248 to `zle_main_entry`, so a
2048 // LOADED zle dispatches the command there; the fallback
2049 // switch below is skipped (`cmd = -1`).
2050 //
2051 // This call was MISSING: the arm set `cmd = -1` and returned
2052 // None, making `zleentry(ZLE_CMD_TRASH)` a silent no-op. C
2053 // relies on it in `zwarning` (Src/utils.c:144-145 `if
2054 // (isatty(2)) zleentry(ZLE_CMD_TRASH);`) to park the cursor
2055 // past the ZLE display before a diagnostic lands on stderr
2056 // and to set `resetneeded` so the editor line is repainted
2057 // afterwards. Without it, a warning emitted mid-ZLE (e.g.
2058 // `compdescribe`'s "invalid argument" during `pr<TAB>`) was
2059 // written INTO the prompt line and the completion list was
2060 // then drawn without the prompt being redrawn — 5 rows off
2061 // zsh in the comptab parity grid.
2062 //
2063 // Rust deviation (no C counterpart): C's `zleentry` is
2064 // variadic, so it forwards every command's `va_list`
2065 // straight through. The Rust signature carries no varargs,
2066 // so only the ARGLESS commands can be reconstructed here;
2067 // the arg-carrying ones (READ / GET_LINE / ADD_TO_LINE /
2068 // SET_KEYMAP / GET_KEY / SET_HIST_LINE) are called through
2069 // their own typed paths and still fall through unchanged.
2070 use crate::ported::zle::zle_main::zle_main_entry_args;
2071 let mut args = match cmd {
2072 x if x == crate::ported::zsh_h::ZLE_CMD_TRASH => {
2073 Some(zle_main_entry_args::Trash) // c:2152
2074 }
2075 x if x == crate::ported::zsh_h::ZLE_CMD_RESET_PROMPT => {
2076 Some(zle_main_entry_args::ResetPrompt) // c:2156
2077 }
2078 x if x == crate::ported::zsh_h::ZLE_CMD_REFRESH => {
2079 Some(zle_main_entry_args::Refresh) // c:2160
2080 }
2081 x if x == crate::ported::zsh_h::ZLE_CMD_PREEXEC => {
2082 Some(zle_main_entry_args::Preexec) // c:2187
2083 }
2084 x if x == crate::ported::zsh_h::ZLE_CMD_POSTEXEC => {
2085 Some(zle_main_entry_args::Postexec) // c:2191
2086 }
2087 x if x == crate::ported::zsh_h::ZLE_CMD_CHPWD => {
2088 Some(zle_main_entry_args::Chpwd) // c:2195
2089 }
2090 _ => None,
2091 };
2092 let ret = args
2093 .as_mut()
2094 .and_then(|a| crate::ported::zle::zle_main::zle_main_entry(cmd, a)); // c:1777
2095 cmd = -1; // c:1779
2096 if ret.is_some() {
2097 return ret; // c:1777
2098 }
2099 }
2100 2 => { /* fallback */ } // c:1782
2101 _ => {}
2102 }
2103 match cmd {
2104 // c:1788
2105 // ZLE_CMD_READ // c:1796
2106 4 => {
2107 // c:1796
2108 let _line = String::new(); // c:1808
2109 return Some(String::new());
2110 }
2111 // ZLE_CMD_GET_LINE // c:1812
2112 5 => {
2113 // c:1812
2114 return Some(String::new()); // c:1835
2115 }
2116 _ => {}
2117 }
2118 None // c:1855
2119}
2120
2121/// Port of `mod_export int fallback_compctlread(...)` from Src/init.c:1835.
2122pub fn fallback_compctlread(name: &str) -> i32 {
2123 // c:1835
2124 crate::ported::utils::zwarnnam(
2125 name, // c:1855
2126 "no loaded module provides read for completion context",
2127 );
2128 1 // c:1855
2129}
2130
2131/// Port of `mod_export int zsh_main(int argc, char **argv)` from Src/init.c:1855.
2132pub fn zsh_main(_argc: i32, argv: &[String]) -> i32 {
2133 // c:1855
2134 #[cfg(unix)]
2135 unsafe {
2136 let empty = std::ffi::CString::new("").unwrap();
2137 libc::setlocale(libc::LC_ALL, empty.as_ptr()); // c:1861
2138 }
2139 // The process locale is now the environment's, so consume the lazy
2140 // fallback that library consumers (unit tests, embedded entry points)
2141 // rely on. Doing it here means it can never fire later and undo an
2142 // `LC_*` parameter assignment made from an rc file.
2143 let _ = *crate::ported::utils::MB_LOCALE_READY;
2144
2145 // init_jobs(argv, environ); // c:1864
2146 let env: Vec<String> = std::env::vars()
2147 .map(|(k, v)| format!("{}={}", k, v))
2148 .collect();
2149 let _ = crate::ported::jobs::init_jobs(argv, &env);
2150
2151 // typtab[...] |= IMETA; // c:1871-1875
2152
2153 // Metafy each argv (already strings in Rust) // c:1877
2154
2155 let mut zsh_name = argv.first().cloned().unwrap_or_default(); // c:1879
2156 loop {
2157 // c:1880
2158 let arg0 = zsh_name.clone(); // c:1881
2159 zsh_name = match arg0.rfind('/') {
2160 // c:1882
2161 None => arg0.clone(), // c:1883
2162 Some(i) => arg0[i + 1..].to_string(), // c:1885
2163 };
2164 if zsh_name.starts_with('-') {
2165 // c:1886
2166 zsh_name = zsh_name[1..].to_string(); // c:1887
2167 }
2168 if zsh_name == "su" {
2169 // c:1888
2170 if let Ok(sh) = std::env::var("SHELL") {
2171 // c:1889
2172 if !sh.is_empty() && arg0 != sh {
2173 // c:1890
2174 zsh_name = sh; // c:1891
2175 continue; // c:1892
2176 }
2177 }
2178 break; // c:1893
2179 }
2180 break; // c:1895
2181 }
2182
2183 // fdtable_size = zopenmax(); fdtable[0..2] = FDT_EXTERNAL; // c:1898-1900
2184 let _ = crate::ported::compat::zopenmax();
2185 // c:1900 — `fdtable[0] = fdtable[1] = fdtable[2] = FDT_EXTERNAL;`.
2186 // stdin/stdout/stderr belong to whoever invoked the shell, so they
2187 // are marked as somebody else's: c:Src/exec.c:3892-3894 lets `>&N` /
2188 // `<&N` duplicate an `FDT_EXTERNAL` descriptor and refuses every
2189 // other classified one. The port called `zopenmax()` and stopped,
2190 // leaving all three reading `FDT_UNUSED`.
2191 for fd in 0..=2 {
2192 crate::ported::utils::check_fd_table(fd);
2193 crate::ported::utils::fdtable_set(fd, crate::ported::zsh_h::FDT_EXTERNAL);
2194 }
2195
2196 crate::ported::options::createoptiontable(); // c:1902
2197
2198 // parseargs(zsh_name, argv, &runscript, &cmd); // c:1905
2199 let mut argv_v = argv.to_vec();
2200 let mut runscript: Option<String> = None; // c:1857
2201 let mut cmd: Option<String> = None; // c:1858
2202 parseargs(&zsh_name, &mut argv_v, &mut runscript, &mut cmd);
2203
2204 SHTTY.store(-1, Ordering::SeqCst); // c:1907
2205 init_io(cmd.as_deref()); // c:1908
2206 crate::startup_trace::mark("init_io");
2207 setupvals(cmd.as_deref(), runscript.as_deref(), &zsh_name); // c:1909
2208 // c:Src/params.c:893-988 — createparamtable imports `environ` and, for
2209 // an IPDEF8 PM_TIED colon-array, installs BOTH sides: the scalar and
2210 // the array split on ':'. `setupvals` above re-seeds the specials from
2211 // the static table, which leaves each tied array EMPTY; without the
2212 // split, an interactive shell reached its first prompt with
2213 // typeset -aT FPATH fpath=( )
2214 // while `$FPATH` still held the full value.
2215 //
2216 // `path` masked this: `PATH` is always exported, so a later env import
2217 // refilled it. zsh never exports `FPATH`, so nothing refilled that one,
2218 // and `-c` never runs this path at all -- which is why only an
2219 // interactive shell lost `$fpath`. A `.zshrc` doing the standard
2220 // `fpath=( mydir $fpath )` then appended to nothing and the shell kept
2221 // only what the rc file added.
2222 for (arr, scalar) in crate::ported::params::TIED_COLON_ARRAYS {
2223 let empty = crate::ported::params::getaparam(arr)
2224 .map(|v| v.is_empty())
2225 .unwrap_or(true);
2226 if !empty {
2227 continue;
2228 }
2229 let joined = crate::ported::params::getsparam(scalar).unwrap_or_default();
2230 if joined.is_empty() {
2231 continue;
2232 }
2233 let split: Vec<String> = joined.split(':').map(str::to_string).collect();
2234 crate::ported::params::setaparam(arr, split);
2235 }
2236 // The bundled tree has to survive the re-seed too, and the split above
2237 // does not restore it: with an INHERITED FPATH the array comes back
2238 // non-empty (the env import refilled it), so the split is skipped --
2239 // and the entry `ShellExecutor::new` appended lives only in the param
2240 // it just overwrote. The result was that `~/.zshrs/functions` was
2241 // present with FPATH unset and missing with FPATH set, in interactive
2242 // shells only. Append it here, last, exactly as the constructor does.
2243 if let Some(d) = crate::bundled_functions::functions_dir() {
2244 if d.is_dir() {
2245 let dir = d.to_string_lossy().into_owned();
2246 let mut arr = crate::ported::params::getaparam("fpath").unwrap_or_default();
2247 if !arr.iter().any(|e| *e == dir) {
2248 arr.push(dir);
2249 crate::ported::params::setaparam("fpath", arr);
2250 }
2251 }
2252 }
2253 crate::startup_trace::mark("setupvals");
2254
2255 init_signals(); // c:1911
2256 crate::startup_trace::mark("init_signals");
2257 init_bltinmods(); // c:1912
2258 crate::startup_trace::mark("init_bltinmods");
2259 crate::ported::builtin::init_builtins(); // c:1913
2260 crate::startup_trace::mark("init_builtins");
2261
2262 // `setupvals` above is what opens the shell's own long-lived files —
2263 // the log, the history database, the plugin cache — and none of them
2264 // arrives through `movefd`, so none of them registered itself the way
2265 // c:Src/utils.c:2007-2010 does. Sweep once here, after those opens
2266 // and before a single line of user code runs, so the gates that read
2267 // the fdtable (c:Src/exec.c:3830-3835 for `exec N>&-`,
2268 // c:Src/exec.c:3884-3897 for `>&N` / `<&N`) see the shell's own
2269 // descriptors as the shell's.
2270 crate::lowfd::register_internal_fds();
2271
2272 // Initialize the ZLE line editor for interactive sessions BEFORE the
2273 // rc files are sourced, so a user's `bindkey` in .zshrc/.zshenv
2274 // layers onto the populated emacs/main keymap instead of being wiped
2275 // by a later default_bindings(). C loads `zsh/zle` during init
2276 // (init.c via the module system); zshrs links ZLE statically, so the
2277 // equivalent setup — register the built-in widgets (init_thingies),
2278 // create the keymap name table, and build + select the default
2279 // keymaps (default_bindings sets curkeymap="main") — runs here once.
2280 // Guarded by zle_load_state so it never re-runs and clobbers user
2281 // bindings. Gated on `interact` so non-interactive `-c` shells skip
2282 // ZLE entirely (they read via shingetline, not the editor).
2283 //
2284 // Gated on the SAME predicate C uses to decide whether a line is read
2285 // through the editor at all — `interact && isset(SHINSTDIN) && SHTTY
2286 // != -1 && isset(USEZLE)` (c:Src/input.c:385). In C the zsh/zle module
2287 // is dlopened lazily by that read, so a shell started with `+Z`
2288 // (NO_ZLE, as Test/W02jobs.ztst and W03jobparameters.ztst launch it:
2289 // `zsh -fiV +Z`) never loads it and `zle_load_state` stays 0.
2290 // zshrs links ZLE in statically and initialises it eagerly, and the
2291 // gate here was `interact()` alone — so `+Z` still set
2292 // `zle_load_state = 1`. That flag is what arms the shell-integration
2293 // markers: `init.c:227/235` fire `zleentry(ZLE_CMD_PREEXEC /
2294 // POSTEXEC)` only when it is 1, and those write the OSC 133;C / 133;D
2295 // "start/end of output" sequences (Src/Zle/termquery.c:759-765
2296 // `mark_output`). With `+Z` honoured they are silent, as in zsh.
2297 // `zle_load_state` is still set lazily on the first real ZLE read
2298 // (input.rs, the `use_zle` branch), so `setopt zle` from an rc file
2299 // re-arms the editor exactly like C's lazy `load_module`.
2300 if interact()
2301 && isset(SHINSTDIN)
2302 && SHTTY.load(Ordering::SeqCst) != -1
2303 && isset(crate::ported::zsh_h::USEZLE)
2304 && zle_load_state.load(Ordering::SeqCst) == 0
2305 {
2306 crate::ported::zle::zle_thingy::init_thingies();
2307 crate::ported::zle::zle_keymap::createkeymapnamtab();
2308 crate::ported::zle::zle_keymap::default_bindings();
2309 zle_load_state.store(1, Ordering::SeqCst); // c:1739 — ZLE loaded
2310 }
2311
2312 // c:1914 — run_init_scripts() sources .zshenv/.zshrc/.zlogin via
2313 // source(). That runs BEFORE the loop's first execode establishes a
2314 // VM execution context, so the sourced bodies must execute under an
2315 // explicit SESSION_EXECUTOR context or they silently no-op (rc files
2316 // ignored). Scope it to JUST this call — the loop below enters its
2317 // own per-command context, and a broad/global executor scope would
2318 // re-enter on nested command substitution and hang.
2319 crate::fusevm_bridge::with_session_context(run_init_scripts); // c:1914
2320 crate::startup_trace::mark("run_init_scripts");
2321 setupshin(runscript.as_deref()); // c:1915
2322 crate::startup_trace::mark("setupshin");
2323 init_misc(cmd.as_deref(), &zsh_name); // c:1916
2324 crate::startup_trace::mark("init_misc");
2325
2326 loop {
2327 // c:1918
2328 let mut errexit = 0; // c:1924
2329 // maybeshrinkjobtab(); // c:1925
2330 // c:1927-1935 — `do { retflag = 0; loop(1,0); if (errflag &&
2331 // !interact && !isset(CONTINUEONERROR)) { errexit = 1; break; } }
2332 // while (tok != ENDINPUT && (tok != LEXERR || isset(SHINSTDIN)));`
2333 loop {
2334 // c:1927 do
2335 RETFLAG.store(0, Ordering::SeqCst); // c:1929 retflag = 0
2336 let _ = r#loop(1, 0); // c:1930
2337 if errflag.load(Ordering::SeqCst) != 0 && !interact() && !isset(CONTINUEONERROR) {
2338 // c:1931
2339 errexit = 1; // c:1932
2340 break; // c:1933
2341 }
2342 let tok_v = tok(); // c:1935
2343 if !(tok_v != ENDINPUT && (tok_v != LEXERR || isset(SHINSTDIN))) {
2344 break; // c:1935 while-cond false → exit do-while
2345 }
2346 }
2347 if tok() == LEXERR || errexit != 0 {
2348 // c:1936
2349 // c:1938-1939 — `if (!lastval) lastval = 1;` (fatal error → nonzero)
2350 if LASTVAL.load(Ordering::SeqCst) == 0 {
2351 LASTVAL.store(1, Ordering::SeqCst);
2352 }
2353 STOPMSG.store(1, Ordering::SeqCst); // c:1940 stopmsg = 1
2354 crate::ported::builtin::zexit(LASTVAL.load(Ordering::SeqCst), ZEXIT_NORMAL);
2355 // c:1941
2356 }
2357 if !(isset(IGNOREEOF) && interact()) {
2358 // c:1943
2359 // c:1944-1947 — interactive "logout\n"/"exit\n" echo is `#if 0`'d
2360 // out in the C source, so nothing to port there.
2361 crate::ported::builtin::zexit(LASTVAL.load(Ordering::SeqCst), ZEXIT_NORMAL); // c:1948
2362 continue; // c:1949 — only reached if zexit DEFERRED (running jobs)
2363 }
2364 noexitct.fetch_add(1, Ordering::SeqCst); // c:1951 noexitct++
2365 if noexitct.load(Ordering::SeqCst) >= 10 {
2366 // c:1952
2367 STOPMSG.store(1, Ordering::SeqCst); // c:1953 stopmsg = 1
2368 crate::ported::builtin::zexit(LASTVAL.load(Ordering::SeqCst), ZEXIT_NORMAL);
2369 // c:1954
2370 }
2371 // c:1961-1963 — IGNOREEOF interactive nudge.
2372 if use_exit_printed.load(Ordering::SeqCst) == 0 {
2373 crate::ported::utils::zerrnam(
2374 "zsh",
2375 if !islogin() {
2376 "use 'exit' to exit."
2377 } else {
2378 "use 'logout' to logout."
2379 },
2380 );
2381 }
2382 }
2383}
2384
2385// =========================================================================
2386// Functions from init.c
2387// =========================================================================
2388
2389/// Port of `enum loop_return loop(int toplevel, int justonce)` from Src/init.c:113.
2390///
2391/// Keep executing lists until EOF found. // c:109
2392///
2393/// ```c
2394/// enum loop_return
2395/// loop(int toplevel, int justonce)
2396/// {
2397/// Eprog prog;
2398/// int err, non_empty = 0;
2399/// queue_signals();
2400/// pushheap();
2401/// if (!toplevel)
2402/// zcontext_save();
2403/// for (;;) {
2404/// freeheap();
2405/// if (stophist == 3) hend(NULL);
2406/// hbegin(1);
2407/// if (isset(SHINSTDIN)) {
2408/// setblock_stdin();
2409/// if (interact && toplevel) { ... preprompt() ... }
2410/// }
2411/// use_exit_printed = 0;
2412/// intr();
2413/// lexinit();
2414/// if (!(prog = parse_event(ENDINPUT))) { ... }
2415/// if (hend(prog)) { ... preexec ... execode ... }
2416/// if (ferror(stderr)) { ... }
2417/// if (subsh) realexit();
2418/// if (((!interact || sourcelevel) && errflag) || retflag) break;
2419/// if (isset(SINGLECOMMAND) && toplevel) { dotrap(SIGEXIT); realexit(); }
2420/// if (justonce) break;
2421/// }
2422/// err = errflag;
2423/// if (!toplevel) zcontext_restore();
2424/// popheap();
2425/// unqueue_signals();
2426/// if (err) return LOOP_ERROR;
2427/// if (!non_empty) return LOOP_EMPTY;
2428/// return LOOP_OK;
2429/// }
2430/// ```
2431pub fn r#loop(toplevel: i32, justonce: i32) -> i32 {
2432 // c:113
2433
2434 // c:114 — `int subsh = subsh;` — read the global at exec.rs:160
2435 // (port of `int subsh;` from Src/exec.c). Non-zero when current
2436 // shell is a forked subshell (set by entersubsh c:1083).
2437 let subsh: i32 = crate::ported::exec::subsh.load(Ordering::Relaxed);
2438
2439 let mut prog: Option<crate::ported::parse::ZshProgram>; // c:115
2440 let err: i32; // c:116
2441 let mut non_empty: i32 = 0; // c:116
2442
2443 queue_signals(); // c:118
2444 crate::ported::mem::pushheap(); // c:119
2445 if toplevel == 0 {
2446 // c:120 !toplevel
2447 crate::ported::context::zcontext_save(); // c:121
2448 }
2449 // One HISTORY UNIT per accepted editor line. C's par_event recurses
2450 // until ENDINPUT (parse.c:635), so a pasted multi-command buffer parses
2451 // into ONE prog and hend stores ONE entry ("cmd1\ncmd2"). zshrs's
2452 // single-event interleave (parse_event returns per logical command; see
2453 // parse.rs:766-786) would cycle hbegin/hend per command and SPLIT the
2454 // pasted line into separate history entries — up-arrow then recalled
2455 // only the last command. While the previous accepted line still has
2456 // unconsumed input buffered, keep the history entry OPEN: skip the
2457 // hend/hbegin cycle (and preprompt) until the line is drained.
2458 let mut hist_open_across_events = false;
2459 loop {
2460 // c:122
2461 crate::ported::mem::freeheap(); // c:123
2462 if !hist_open_across_events {
2463 if stophist.load(Ordering::SeqCst) == 3 {
2464 // c:124 stophist == 3
2465 hend(None); // c:125 hend(NULL)
2466 }
2467 // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
2468 // A `@` line run by stryke from `inputline` reached the lexer as
2469 // an empty line; its event is closed now, so record its text
2470 // before the next event takes a number.
2471 crate::stryke_history_flush();
2472 hbegin(1); // c:126 hbegin(1)
2473 }
2474 if isset(SHINSTDIN) && !hist_open_across_events {
2475 // c:127 isset(SHINSTDIN)
2476 crate::ported::utils::setblock_stdin(); // c:128
2477 if isset(INTERACTIVE) && toplevel != 0 {
2478 // c:129 interact && toplevel
2479 let hstop = stophist.load(Ordering::SeqCst); // c:130
2480 stophist.store(3, Ordering::SeqCst); // c:131
2481 // c:146-148 — drain any signals queued during the previous read
2482 // WITHOUT changing the queueing nesting level: snapshot the level,
2483 // dont_queue_signals() (whose run_queued_signals() reaps bg-job
2484 // SIGCHLDs that were queued while queueing_enabled stayed at its
2485 // baseline 1 across the ZLE read), then restore the level. This
2486 // port previously jumped straight to preprompt(), so every queued
2487 // SIGCHLD went undrained → children never reaped (zombies) and the
2488 // shell hung at the prompt. Port of Src/init.c:146-148.
2489 let q = crate::ported::signals_h::queue_signal_level();
2490 dont_queue_signals();
2491 crate::ported::signals_h::restore_queue_signals(q);
2492 // c:133-138 — reset errflag for preprompt
2493 errflag.store(0, Ordering::SeqCst); // c:139 errflag = 0
2494 crate::ported::utils::preprompt(); // c:140
2495 if stophist.load(Ordering::SeqCst) != 3 {
2496 // c:141
2497 hbegin(1); // c:142
2498 } else {
2499 // c:143
2500 stophist.store(hstop, Ordering::SeqCst); // c:144
2501 }
2502 // c:146-149 — reset errflag again
2503 errflag.store(0, Ordering::SeqCst); // c:150
2504 }
2505 }
2506 use_exit_printed.store(0, Ordering::SeqCst); // c:153
2507 intr(); // c:154
2508 crate::ported::lex::lexinit(); // c:155
2509 // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
2510 // C's parse_event builds the event's wordcode Eprog, and the preexec
2511 // call below renders `$2`/`$3` from it (c:210-211 getjobtext /
2512 // getpermtext). zshrs's parse_event returns the fusevm AST, which the
2513 // text renderers cannot walk, so record the event's source as the
2514 // lexer consumes it (the same echo buffer funcdef bodies use) and
2515 // compile it to an Eprog only if a preexec hook is present.
2516 let event_mark = (toplevel != 0).then(crate::funcdef_capture::body_mark_begin);
2517 prog = crate::ported::parse::parse_event(ENDINPUT as i32); // c:156
2518 let event_src = event_mark.and_then(crate::funcdef_capture::event_text);
2519 if prog.is_none() {
2520 // c:156
2521 hend(None); // c:158
2522 // A parse failure closes any deferred same-line history unit —
2523 // the next iteration must run its own hbegin again.
2524 hist_open_across_events = false;
2525 // c:159-161 — break on clean EOF / non-toplevel LEXERR / justonce
2526 let tok_v = tok(); // c:159 tok
2527 let errflag_v = errflag.load(Ordering::SeqCst);
2528 let lexerr_break = tok_v == LEXERR && (!isset(SHINSTDIN) || toplevel == 0);
2529 let endinput_break = tok_v == ENDINPUT && errflag_v == 0;
2530 if endinput_break || lexerr_break || justonce != 0 {
2531 // c:159-161
2532 break; // c:162
2533 }
2534 if crate::ported::hist::exit_pending.load(Ordering::SeqCst) {
2535 // c:163 exit_pending
2536 STOPMSG.store(1, Ordering::SeqCst); // c:169 stopmsg = 1
2537 crate::ported::builtin::zexit(
2538 // c:170 zexit(exit_val, ZEXIT_NORMAL)
2539 crate::ported::builtin::EXIT_VAL.load(Ordering::SeqCst),
2540 ZEXIT_NORMAL,
2541 );
2542 }
2543 if tok_v == LEXERR && LASTVAL.load(Ordering::SeqCst) == 0
2544 // c:172
2545 {
2546 LASTVAL.store(1, Ordering::SeqCst); // c:173 lastval = 1
2547 }
2548 continue; // c:174
2549 }
2550 let prog_inner = prog.take().unwrap();
2551 // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
2552 // The accepted line is about to run at top level, where commands
2553 // stamp `locallevel` without opening a scope (a bare `typeset X=1`
2554 // takes the current level). An `async_precmd` batch fired by the
2555 // preprompt() above may still be running hook functions on a worker,
2556 // with that counter raised by its own scope — the typeset would then
2557 // belong to the worker's scope and vanish when the hook returned.
2558 // C runs precmd hooks synchronously (c:140 preprompt), so nothing
2559 // else can hold a scope here; wait for the batch, or withdraw it.
2560 crate::async_precmd::quiesce();
2561 // c:176 — `if (hend(prog))`: passing the program in commits the
2562 // history line. zshrs's `hend` takes Option<&[u8]>; sentinel
2563 // non-empty slice signals "program present" to the commit path.
2564 //
2565 // Same-accepted-line continuation (see hist_open_across_events at
2566 // the loop head): while the input buffer still holds unconsumed
2567 // chars of the line being edited, DEFER hend — chline keeps
2568 // accumulating so the whole pasted buffer commits as one entry,
2569 // exactly as C's to-ENDINPUT par_event produces.
2570 hist_open_across_events = isset(INTERACTIVE)
2571 && toplevel != 0
2572 && crate::ported::input::inbufct.with(|c| c.get()) > 0;
2573 let hend_ret = if hist_open_across_events {
2574 1 // execute; the history commit happens when the line drains
2575 } else {
2576 let prog_bytes: Vec<u8> = format!("{:?}", prog_inner).into_bytes();
2577 hend(Some(&prog_bytes)) // c:176
2578 };
2579 if hend_ret != 0 {
2580 let _toksav = tok(); // c:177
2581 non_empty = 1; // c:179
2582 // c:180-215 — preexec hook + ZLE_CMD_PREEXEC.
2583 if toplevel != 0 {
2584 // c:180-215 — the preexec hook, shared with the script-file
2585 // event loop (vm_helper::run_events_per_command).
2586 crate::vm_helper::run_preexec_hook(event_src.as_deref());
2587 }
2588 if toplevel != 0 // c:216
2589 && zle_load_state.load(Ordering::SeqCst) == 1
2590 {
2591 let _ = zleentry(
2592 // c:217 ZLE_CMD_PREEXEC
2593 ZLE_CMD_PREEXEC,
2594 );
2595 }
2596 if STOPMSG.load(Ordering::SeqCst) != 0 {
2597 // c:218
2598 STOPMSG.fetch_sub(1, Ordering::SeqCst); // c:219
2599 }
2600 // c:220 — `execode(prog, 0, 0, toplevel ? "toplevel" : "file");`
2601 let exec_label = if toplevel != 0 { "toplevel" } else { "file" };
2602 // Save the lexer's line counter across execution. Each
2603 // statement's `SET_LINENO` (c:Src/exec.c execpline WC_PIPE_LINENO)
2604 // overwrites `lineno` to that statement's line while running;
2605 // C's execlist brackets this with `oldlineno = lineno` /
2606 // `lineno = oldlineno` (exec.c:28/292) so the value is restored
2607 // on exit. Without it, the faithful single-event loop (which
2608 // interleaves parse + execode) had the NEXT event parse from the
2609 // just-executed statement's line — `$LINENO` froze at 1 across a
2610 // piped/interactive script instead of advancing 1,2,3.
2611 let saved_lex_lineno = crate::ported::lex::LEX_LINENO.with(|c| c.get());
2612 crate::ported::exec::execode(&prog_inner, 0, 0, exec_label); // c:220
2613 crate::ported::lex::LEX_LINENO.with(|c| c.set(saved_lex_lineno));
2614 // c:Src/exec.c:1611-1618 — errexit (`set -e`) fires a shell
2615 // exit FROM execlist (`if (errexit) { errflag = 0; if
2616 // (sigtrapped[SIGEXIT]) dotrap(SIGEXIT); realexit(); }`).
2617 // zshrs's fusevm can't realexit mid-VM, so BUILTIN_ERREXIT_CHECK
2618 // defers it via EXIT_PENDING + a jump to chunk-end. Whole-program
2619 // mode then skips the rest of the single chunk via that jump, but
2620 // the faithful single-event loop runs each event as its OWN chunk
2621 // — the jump only ends the failed event, and without honoring the
2622 // pending exit here the NEXT event would still run. Mirror C's
2623 // execlist realexit at the execode boundary: zexit fires the EXIT
2624 // trap (its trap-body dispatch reaches the session executor) and
2625 // realexits with EXIT_VAL. Guarded to top level + no subshell so
2626 // a deferred subshell/cmdsub exit still propagates via its own
2627 // unwind (fusevm_bridge) instead of killing the parent here.
2628 if toplevel != 0
2629 && crate::ported::builtin::SUBSHELL_DEPTH.load(Ordering::SeqCst) == 0
2630 && crate::ported::builtin::EXIT_PENDING.load(Ordering::SeqCst) != 0
2631 {
2632 crate::ported::builtin::zexit(
2633 crate::ported::builtin::EXIT_VAL.load(Ordering::SeqCst),
2634 ZEXIT_NORMAL,
2635 );
2636 }
2637 // c:221 — `tok = toksav;` restore
2638 set_tok(_toksav);
2639 if toplevel != 0 {
2640 // c:222
2641 noexitct.store(0, Ordering::SeqCst); // c:223
2642 if zle_load_state.load(Ordering::SeqCst) == 1 {
2643 // c:224
2644 let _ = zleentry(
2645 // c:225 ZLE_CMD_POSTEXEC
2646 ZLE_CMD_POSTEXEC,
2647 );
2648 }
2649 }
2650 }
2651 // c:228-231 — `if (ferror(stderr))` write-error path. Mirror as a
2652 // best-effort flush; Rust panic on broken pipe handled upstream.
2653 // c:232 — `if (subsh) realexit();`
2654 if subsh != 0 {
2655 // c:232
2656 realexit(); // c:233
2657 }
2658 // c:234 — `if (((!interact || sourcelevel) && errflag) || retflag) break;`
2659 let errflag_v = errflag.load(Ordering::SeqCst);
2660 let interact_v = isset(INTERACTIVE);
2661 let srclvl = sourcelevel.load(Ordering::SeqCst);
2662 let retflag_v = RETFLAG.load(Ordering::SeqCst);
2663 if ((!interact_v || srclvl != 0) && errflag_v != 0) || retflag_v != 0 {
2664 // c:234
2665 break; // c:235
2666 }
2667 if isset(SINGLECOMMAND) && toplevel != 0 {
2668 // c:236
2669 dont_queue_signals(); // c:237
2670 // c:238 — sigtrapped[SIGEXIT] != 0 → dotrap(SIGEXIT)
2671 let tr = sigtrapped.lock().unwrap();
2672 let sigexit = libc::SIGINT as usize; // SIGEXIT = 0 (zsh-internal)
2673 if tr.get(sigexit).copied().unwrap_or(0) != 0 {
2674 // c:238
2675 drop(tr);
2676 let _ = dotrap(0 /* SIGEXIT */); // c:239
2677 }
2678 realexit(); // c:240
2679 }
2680 if justonce != 0 {
2681 // c:242
2682 break; // c:243
2683 }
2684 let _ = histlinect.load(Ordering::SeqCst); // silence unused alias
2685 }
2686 err = errflag.load(Ordering::SeqCst); // c:245 err = errflag
2687 if toplevel == 0 {
2688 // c:246 !toplevel
2689 zcontext_restore(); // c:247
2690 }
2691 popheap(); // c:248
2692 unqueue_signals(); // c:249
2693
2694 if err != 0 {
2695 return 2; /* LOOP_ERROR */
2696 } // c:251-252
2697 if non_empty == 0 {
2698 return 1; /* LOOP_EMPTY */
2699 } // c:253-254
2700 0 /* LOOP_OK */ // c:255
2701}
2702
2703/// Port of `static void parseopts_setemulate(...)` from Src/init.c:348.
2704fn parseopts_setemulate(nam: &str, flags: i32) {
2705 // c:350 — `emulate(nam, 1, &emulation, opts);` initialises most
2706 // options. Strip leading `-` (login-shell marker) and any path
2707 // components so the name passed to `emulate` is just the basename
2708 // (`ksh` / `sh` / `csh` / `zsh`).
2709 let bare = nam.trim_start_matches('-');
2710 let basename = std::path::Path::new(bare)
2711 .file_name()
2712 .and_then(|n| n.to_str())
2713 .unwrap_or(bare);
2714 // ZSHRS-ONLY. In C, `argv[0]` IS the personality selector, so
2715 // re-deriving the emulation from it here is free. zshrs adds `--MODE`
2716 // flags C has no equivalent of, and `argv[0]` is always the zshrs
2717 // binary — so this call used to reset a `--ksh` / `--sh` / `--dash`
2718 // shell back to zsh emulation on the interactive path, which reaches
2719 // `zsh_main` (the `-c` and script-file paths in bins/zshrs.rs do not).
2720 // `zshrs --sh -i` ran with `shwordsplit` OFF as a result. The
2721 // CLI-selected personality wins when there is one.
2722 let name = crate::extensions::emulation_startup::selected_emulate_name().unwrap_or(basename);
2723 crate::ported::options::emulate(name, true); // c:350
2724 // ZSHRS-ONLY. `emulate()` resets the option table wholesale, so the
2725 // drop-in's own deltas (bash's brace expansion, `$BASH_REMATCH`, the
2726 // prompt options) have to be re-applied on top of the preset. Without
2727 // this they survived only on the `-c` / script paths, which do not
2728 // reach this function — an interactive `--bash`, i.e. a login shell,
2729 // ran without any of them.
2730 crate::extensions::emulation_startup::apply_personality_option_deltas();
2731
2732 // c:351 — `opts[LOGINSHELL] = ((flags & PARSEARGS_LOGIN) != 0);`
2733 const PARSEARGS_LOGIN: i32 = 2;
2734 crate::ported::options::opt_state_set("loginshell", (flags & PARSEARGS_LOGIN) != 0);
2735
2736 // c:352 — `opts[PRIVILEGED] = (getuid() != geteuid() || …);`
2737 let priv_on = unsafe { libc::getuid() != libc::geteuid() || libc::getgid() != libc::getegid() };
2738 crate::ported::options::opt_state_set("privileged", priv_on);
2739
2740 // c:361 — `opts[INTERACTIVE] = isatty(0) ? 2 : 0;`. The "2"
2741 // sentinel means "default-on, may be downgraded by SHINSTDIN
2742 // handling below". Rust port stores as bool — we only retain
2743 // the on/off distinction; the downgrade logic at c:end-of-fn
2744 // is a no-op once we collapse to bool.
2745 let interactive_default = unsafe { libc::isatty(0) != 0 };
2746 crate::ported::options::opt_state_set("interactive", interactive_default);
2747 // MONITOR and HASHDIRS start at 2 as well (c:366-367).
2748 OPT_DEFAULT_ON.store(i32::from(interactive_default) as u32 | 2 | 4, Ordering::SeqCst);
2749
2750 // c:366-368 — `opts[MONITOR] = 2; opts[HASHDIRS] = 2; opts[USEZLE] = 1;`
2751 crate::ported::options::opt_state_set("monitor", true);
2752 crate::ported::options::opt_state_set("hashdirs", true);
2753 // USEZLE's canonical name is `zle` (zsh_h.rs:4145); the prior
2754 // `"usezle"` key was never read by `isset(USEZLE)` → this default was a
2755 // silent no-op. init_io (c:691-694) downgrades it for non-tty shells.
2756 crate::ported::options::opt_state_set("zle", true); // c:368 opts[USEZLE]=1
2757 // c:369-370 — `opts[SHINSTDIN] = 0; opts[SINGLECOMMAND] = 0;`
2758 crate::ported::options::opt_state_set("shinstdin", false);
2759 crate::ported::options::opt_state_set("singlecommand", false);
2760}
2761
2762#[cfg(test)]
2763mod tests {
2764 use super::*;
2765
2766 /// source() pushes an FS_SOURCE funcstack frame (init.c:1610-1618)
2767 /// and pops it on exit (c:1664). Verify the push is balanced (no
2768 /// leak) and that a readable file returns SOURCE_OK.
2769 #[test]
2770 fn source_funcstack_push_is_balanced() {
2771 use crate::ported::modules::parameter::FUNCSTACK;
2772 let depth_before = FUNCSTACK.lock().unwrap().len();
2773
2774 let mut path = std::env::temp_dir();
2775 path.push(format!("zshrs_source_balance_{}.zsh", std::process::id()));
2776 std::fs::write(&path, ": # no-op sourced file\n").unwrap();
2777
2778 let rc = source(path.to_str().unwrap());
2779
2780 let depth_after = FUNCSTACK.lock().unwrap().len();
2781 std::fs::remove_file(&path).ok();
2782
2783 assert_eq!(rc, 0, "source of a readable file returns SOURCE_OK");
2784 assert_eq!(
2785 depth_before, depth_after,
2786 "FS_SOURCE push must be matched by a pop (no funcstack leak)"
2787 );
2788
2789 // A non-existent file is SOURCE_NOT_FOUND and pushes nothing.
2790 let missing = "/nonexistent/zshrs_source_xyz_should_not_exist";
2791 assert_ne!(source(missing), 0, "missing file returns SOURCE_NOT_FOUND");
2792 assert_eq!(
2793 FUNCSTACK.lock().unwrap().len(),
2794 depth_before,
2795 "NOT_FOUND path must not touch the funcstack"
2796 );
2797 }
2798
2799 /// `fallback_compctlread` is the dispatch target when the zle
2800 /// module isn't loaded — `compctl -K read` paths need SOMETHING
2801 /// callable. C returns 1 + zwarnnam to signal "no provider". A
2802 /// regression returning 0 would make completers believe a read
2803 /// happened when nothing did (silent data corruption in completion).
2804 #[test]
2805 fn fallback_compctlread_signals_failure() {
2806 let _g = crate::test_util::global_state_lock();
2807 assert_eq!(fallback_compctlread("test"), 1);
2808 }
2809
2810 /// c:418 — `parseopts` advances `idx` past every consumed option.
2811 /// `-c CMD` consumes 2 slots and stores CMD in cmdp. Regression
2812 /// that doesn't advance idx would loop forever or skip args.
2813 #[test]
2814 fn parseopts_dash_c_captures_command_string() {
2815 let _g = crate::test_util::global_state_lock();
2816 let mut argv = vec!["-c".to_string(), "echo hi".to_string()];
2817 let mut idx = 0usize;
2818 let mut cmd: Option<String> = None;
2819 let r = parseopts("zsh", &mut argv, &mut idx, &mut cmd, 0);
2820 assert_eq!(r, 0);
2821 assert_eq!(
2822 cmd.as_deref(),
2823 Some("echo hi"),
2824 "-c must capture the next arg as the command"
2825 );
2826 assert_eq!(idx, 2, "idx must advance past both -c AND its arg");
2827 }
2828
2829 /// c:Src/init.c:456-460 — a GNU-style `--LONG` option is rewritten
2830 /// (`-` becomes `_`) and applied through optlookup + dosetopt, so
2831 /// `--no-rcs` is exactly `-o no_rcs`.
2832 ///
2833 /// This arm used to skip EVERY `--NAME`, so a long-spelled option
2834 /// never reached the option table: `zshrs --no-rcs -i` left RCS set
2835 /// and still sourced .zshenv/.zshrc (the gate is `isset(RCS)` in
2836 /// `run_init_scripts`), while `-f` and `-o norcs` both suppressed
2837 /// them. Non-interactive `-c` masked it because nothing reads .zshrc
2838 /// there.
2839 #[test]
2840 fn parseopts_long_option_reaches_the_option_table() {
2841 let _g = crate::test_util::global_state_lock();
2842 for spelling in ["--no-rcs", "--norcs", "--no_rcs"] {
2843 crate::ported::options::opt_state_set("rcs", true);
2844 assert!(isset(RCS), "precondition for {}: RCS starts on", spelling);
2845 let mut argv = vec![spelling.to_string()];
2846 let mut idx = 0usize;
2847 let mut cmd: Option<String> = None;
2848 let r = parseopts("zsh", &mut argv, &mut idx, &mut cmd, 1);
2849 assert_eq!(r, 0, "{} must parse cleanly", spelling);
2850 assert!(!isset(RCS), "{} must clear RCS, like -f does", spelling);
2851 assert_eq!(idx, 1, "{} consumes exactly one slot", spelling);
2852 }
2853 crate::ported::options::opt_state_set("rcs", true);
2854 }
2855
2856 /// The deliberate divergence from c:492 must hold: zshrs-only long
2857 /// flags (`--zsh`, `--dap`, …) are NOT zsh options, are consumed by
2858 /// the bin front-end, and must be skipped here rather than rejected
2859 /// with "no such option". Guards the fix above from over-reaching.
2860 #[test]
2861 fn parseopts_skips_zshrs_specific_long_flags() {
2862 let _g = crate::test_util::global_state_lock();
2863 for flag in ["--zsh", "--bash", "--zsh-compat", "--dap"] {
2864 let mut argv = vec![flag.to_string()];
2865 let mut idx = 0usize;
2866 let mut cmd: Option<String> = None;
2867 let r = parseopts("zsh", &mut argv, &mut idx, &mut cmd, 1);
2868 assert_eq!(r, 0, "{} must not be rejected", flag);
2869 assert_eq!(idx, 1, "{} must still be consumed", flag);
2870 }
2871 }
2872
2873 /// c:418 — `parseopts` stops at the first non-option positional.
2874 /// Regression that keeps walking past `--` or first bareword would
2875 /// silently consume the script name as an option.
2876 #[test]
2877 fn parseopts_stops_at_first_positional() {
2878 let _g = crate::test_util::global_state_lock();
2879 let mut argv = vec!["script.sh".to_string(), "arg1".to_string()];
2880 let mut idx = 0usize;
2881 let mut cmd: Option<String> = None;
2882 parseopts("zsh", &mut argv, &mut idx, &mut cmd, 0);
2883 assert_eq!(idx, 0, "must stop AT the first bareword (no advance)");
2884 assert!(cmd.is_none(), "no -c → cmd stays None");
2885 }
2886
2887 /// c:418 — `parseopts` on empty argv exits cleanly with idx=0
2888 /// and cmd=None. Regression panicking on empty would crash
2889 /// startup with weird args.
2890 #[test]
2891 fn parseopts_empty_argv_is_safe() {
2892 let _g = crate::test_util::global_state_lock();
2893 let mut argv: Vec<String> = Vec::new();
2894 let mut idx = 0usize;
2895 let mut cmd: Option<String> = None;
2896 assert_eq!(parseopts("zsh", &mut argv, &mut idx, &mut cmd, 0), 0);
2897 assert_eq!(idx, 0);
2898 assert!(cmd.is_none());
2899 }
2900
2901 /// c:756 — `tccap_get_name` indexes into the terminal-capability
2902 /// name array. Out-of-range MUST NOT panic — it returns "" so
2903 /// downstream tigetstr calls fail-soft. Regression panicking
2904 /// would crash the prompt subsystem on tiny terminals.
2905 #[test]
2906 fn tccap_get_name_out_of_range_returns_empty() {
2907 let _g = crate::test_util::global_state_lock();
2908 // index 9999 is well past the cap table (~39 entries per
2909 // init.c:75). Must return safely.
2910 let n = tccap_get_name(9999);
2911 assert!(
2912 n.is_empty(),
2913 "out-of-range index must return empty (got {n:?})"
2914 );
2915 }
2916
2917 /// c:418 — `parseopts` consumes `-x` (xtrace) as a flag without
2918 /// an argument. Pin the no-arg-required path; a regression that
2919 /// always reads `argv[idx+1]` would OOB-index when -x is the
2920 /// last arg.
2921 #[test]
2922 fn parseopts_dash_x_is_single_slot_flag() {
2923 let _g = crate::test_util::global_state_lock();
2924 let mut argv = vec!["-x".to_string()];
2925 let mut idx = 0usize;
2926 let mut cmd: Option<String> = None;
2927 let r = parseopts("zsh", &mut argv, &mut idx, &mut cmd, 0);
2928 assert_eq!(r, 0);
2929 assert_eq!(idx, 1, "-x consumes exactly 1 arg slot");
2930 assert!(cmd.is_none(), "-x must NOT capture a command");
2931 }
2932
2933 /// c:418 — `parseopts` -c with NO following argument. The
2934 /// contract is "no valid command captured" so the caller can
2935 /// surface a usage error.
2936 #[test]
2937 fn parseopts_dash_c_without_argument_does_not_capture_command() {
2938 let _g = crate::test_util::global_state_lock();
2939 let mut argv = vec!["-c".to_string()];
2940 let mut idx = 0usize;
2941 let mut cmd: Option<String> = None;
2942 let _ = parseopts("zsh", &mut argv, &mut idx, &mut cmd, 0);
2943 assert!(
2944 cmd.is_none() || cmd.as_deref() == Some(""),
2945 "-c without arg must NOT capture a usable command"
2946 );
2947 }
2948
2949 /// c:418 — `parseopts` consumes multiple flags in sequence.
2950 /// Pin the per-arg loop so a regen that early-exits after the
2951 /// first flag silently drops -v.
2952 #[test]
2953 fn parseopts_consumes_multiple_flags() {
2954 let _g = crate::test_util::global_state_lock();
2955 let mut argv = vec!["-x".to_string(), "-v".to_string()];
2956 let mut idx = 0usize;
2957 let mut cmd: Option<String> = None;
2958 let r = parseopts("zsh", &mut argv, &mut idx, &mut cmd, 0);
2959 assert_eq!(r, 0);
2960 assert_eq!(idx, 2, "both flags must be consumed");
2961 }
2962
2963 /// c:756 — `tccap_get_name(0)` returns the first cap name. The
2964 /// table is non-empty per init.c:75; index 0 must be a valid
2965 /// stem. Pin the boundary so a regen that adds an "off-by-one
2966 /// slot 0" mistake gets caught.
2967 #[test]
2968 fn tccap_get_name_index_zero_is_nonempty() {
2969 let _g = crate::test_util::global_state_lock();
2970 let n = tccap_get_name(0);
2971 assert!(
2972 !n.is_empty(),
2973 "index 0 must yield a real cap name; got {:?}",
2974 n
2975 );
2976 for c in n.chars() {
2977 assert!(
2978 c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_',
2979 "cap name {:?} contains non-termcap char {:?}",
2980 n,
2981 c
2982 );
2983 }
2984 }
2985
2986 /// c:1713 — `noop_function` MUST be safe to call without panics.
2987 /// C body is empty; the Rust port mirrors it. Re-entry test
2988 /// catches a regen that adds stateful side-effects.
2989 #[test]
2990 fn noop_function_is_safe_to_call_multiple_times() {
2991 let _g = crate::test_util::global_state_lock();
2992 noop_function();
2993 noop_function();
2994 noop_function();
2995 }
2996
2997 /// c:1720 — `noop_function_int(n)` ignores its argument across
2998 /// the full i32 range. Pin no-side-effect contract.
2999 #[test]
3000 fn noop_function_int_ignores_argument() {
3001 let _g = crate::test_util::global_state_lock();
3002 noop_function_int(0);
3003 noop_function_int(42);
3004 noop_function_int(-1);
3005 noop_function_int(i32::MAX);
3006 noop_function_int(i32::MIN);
3007 }
3008
3009 /// c:1743 — `zleentry(cmd)` for an unknown cmd code returns
3010 /// None (matching the C "no zle" branch). Every call site
3011 /// assumes None means "no ZLE active".
3012 #[test]
3013 fn zleentry_unknown_command_returns_none() {
3014 let _g = crate::test_util::global_state_lock();
3015 assert!(
3016 zleentry(99999).is_none(),
3017 "unknown zle command must return None, not a default string"
3018 );
3019 }
3020
3021 /// c:1551 — `source("")` on empty path must NOT segfault. The
3022 /// C source opens("") which fails with ENOENT, returning non-0.
3023 #[test]
3024 fn source_empty_path_returns_nonzero() {
3025 let _g = crate::test_util::global_state_lock();
3026 let r = source("");
3027 assert_ne!(r, 0, "source of empty path must report failure");
3028 }
3029
3030 // ─── zsh-corpus pins for init helpers ───────────────────────────
3031
3032 /// `tccap_get_name(0)` returns a non-empty cap name.
3033 #[test]
3034 fn init_corpus_tccap_index_zero_nonempty() {
3035 let _g = crate::test_util::global_state_lock();
3036 let name = tccap_get_name(0);
3037 assert!(!name.is_empty(), "cap[0] has a name, got {name:?}");
3038 }
3039
3040 /// `tccap_get_name` for index TC_COUNT or higher returns "".
3041 #[test]
3042 fn init_corpus_tccap_out_of_range_returns_empty() {
3043 let _g = crate::test_util::global_state_lock();
3044 assert_eq!(tccap_get_name(39), "", "TC_COUNT = empty");
3045 assert_eq!(tccap_get_name(999), "", "way out of range = empty");
3046 }
3047
3048 /// All cap indexes 0..TC_COUNT return non-empty distinct names.
3049 #[test]
3050 fn init_corpus_tccap_indexes_all_named() {
3051 let _g = crate::test_util::global_state_lock();
3052 for i in 0..39 {
3053 let n = tccap_get_name(i);
3054 assert!(!n.is_empty(), "cap {i} should have a name");
3055 }
3056 }
3057
3058 /// `source("/nonexistent/path/zshrs_test_xyz")` returns non-zero.
3059 #[test]
3060 fn init_corpus_source_nonexistent_returns_nonzero() {
3061 let _g = crate::test_util::global_state_lock();
3062 assert_ne!(source("/nonexistent/path/zshrs_test_xyz_zzz"), 0);
3063 }
3064
3065 /// `noop_function()` doesn't panic.
3066 #[test]
3067 fn init_corpus_noop_function_does_not_panic() {
3068 let _g = crate::test_util::global_state_lock();
3069 noop_function();
3070 }
3071
3072 /// `noop_function_int(0)` doesn't panic.
3073 #[test]
3074 fn init_corpus_noop_function_int_zero_no_panic() {
3075 let _g = crate::test_util::global_state_lock();
3076 noop_function_int(0);
3077 }
3078
3079 /// `zleentry` with negative cmd code returns None.
3080 #[test]
3081 fn init_corpus_zleentry_negative_returns_none() {
3082 let _g = crate::test_util::global_state_lock();
3083 assert!(zleentry(-1).is_none());
3084 }
3085
3086 // ═══════════════════════════════════════════════════════════════════
3087 // Additional C-parity tests for Src/init.c tccap_get_name / init_term
3088 // / noop helpers.
3089 // ═══════════════════════════════════════════════════════════════════
3090
3091 /// c:756 — `tccap_get_name(cap)` for cap >= 39 (TC_COUNT) returns
3092 /// empty string (Rust port out-of-range guard, c:771).
3093 #[test]
3094 fn tccap_get_name_out_of_range_returns_empty_pin() {
3095 let _g = crate::test_util::global_state_lock();
3096 assert_eq!(tccap_get_name(39), "", "TC_COUNT boundary returns ''");
3097 assert_eq!(tccap_get_name(100), "");
3098 assert_eq!(tccap_get_name(usize::MAX), "");
3099 }
3100
3101 /// c:756 — `tccap_get_name(0)` returns a non-empty cap name
3102 /// (first entry of tccapnams table).
3103 #[test]
3104 fn tccap_get_name_first_entry_is_nonempty() {
3105 let _g = crate::test_util::global_state_lock();
3106 let n = tccap_get_name(0);
3107 assert!(!n.is_empty(), "tccapnams[0] must be a valid cap name");
3108 }
3109
3110 /// c:756 — `tccap_get_name` is deterministic across calls.
3111 #[test]
3112 fn tccap_get_name_is_deterministic() {
3113 let _g = crate::test_util::global_state_lock();
3114 for i in 0..39 {
3115 let first = tccap_get_name(i);
3116 for _ in 0..5 {
3117 assert_eq!(tccap_get_name(i), first, "tccap_get_name({}) impure", i);
3118 }
3119 }
3120 }
3121
3122 /// c:756 — every in-range cap name fits in ASCII and has
3123 /// non-control content (termcap caps are 2-char ASCII codes).
3124 #[test]
3125 fn tccap_get_name_in_range_returns_printable_ascii() {
3126 let _g = crate::test_util::global_state_lock();
3127 for i in 0..39 {
3128 let n = tccap_get_name(i);
3129 if n.is_empty() {
3130 continue;
3131 }
3132 for c in n.chars() {
3133 assert!(
3134 c.is_ascii() && !c.is_control(),
3135 "tccap_get_name({}) = {:?} has non-printable char {:?}",
3136 i,
3137 n,
3138 c
3139 );
3140 }
3141 }
3142 }
3143
3144 /// c:1713 — `noop_function` returns nothing, has no observable effect.
3145 /// Repeated calls don't accumulate state.
3146 #[test]
3147 fn noop_function_is_idempotent() {
3148 let _g = crate::test_util::global_state_lock();
3149 for _ in 0..100 {
3150 noop_function();
3151 }
3152 }
3153
3154 /// c:1720 — `noop_function_int` accepts any i32 value without panic.
3155 #[test]
3156 fn noop_function_int_accepts_any_value() {
3157 let _g = crate::test_util::global_state_lock();
3158 noop_function_int(0);
3159 noop_function_int(-1);
3160 noop_function_int(i32::MAX);
3161 noop_function_int(i32::MIN);
3162 }
3163
3164 /// `zleentry` with very large positive cmd code returns None
3165 /// (out-of-range cmd dispatch).
3166 #[test]
3167 fn zleentry_unknown_cmd_returns_none() {
3168 let _g = crate::test_util::global_state_lock();
3169 assert!(zleentry(99999).is_none(), "unknown cmd → None");
3170 }
3171
3172 /// c:1209 — `init_bltinmods` is idempotent (safe to call multiple times).
3173 #[test]
3174 fn init_bltinmods_is_idempotent() {
3175 let _g = crate::test_util::global_state_lock();
3176 init_bltinmods();
3177 init_bltinmods();
3178 }
3179
3180 // ═══════════════════════════════════════════════════════════════════
3181 // Additional C-parity tests for Src/init.c
3182 // c:747 tccapnams / c:756 tccap_get_name / c:1713 noop_function /
3183 // c:1199 fallback_compctlread.
3184 // ═══════════════════════════════════════════════════════════════════
3185
3186 /// c:747 — `tccapnams` table has exactly 39 entries (TC_COUNT).
3187 #[test]
3188 fn tccapnams_table_size_is_tc_count() {
3189 assert_eq!(tccapnams.len(), 39, "TC_COUNT must be 39");
3190 }
3191
3192 /// c:747 — every tccapnams entry is exactly 2 ASCII chars
3193 /// (termcap 2-letter capability code convention).
3194 #[test]
3195 fn tccapnams_entries_are_two_ascii_chars() {
3196 for (i, &n) in tccapnams.iter().enumerate() {
3197 assert_eq!(n.len(), 2, "tccapnams[{}] = {:?} must be 2 chars", i, n);
3198 assert!(
3199 n.chars().all(|c| c.is_ascii_alphabetic()),
3200 "tccapnams[{}] = {:?} must be ASCII alpha",
3201 i,
3202 n
3203 );
3204 }
3205 }
3206
3207 /// c:747 — no duplicates in tccapnams.
3208 #[test]
3209 fn tccapnams_has_no_duplicates() {
3210 let mut seen = std::collections::HashSet::new();
3211 for &n in tccapnams.iter() {
3212 assert!(seen.insert(n), "duplicate tccap name: {:?}", n);
3213 }
3214 }
3215
3216 /// c:756 — `tccap_get_name(39)` is at TC_COUNT boundary → empty.
3217 #[test]
3218 fn tccap_get_name_at_tc_count_boundary_returns_empty() {
3219 assert_eq!(tccap_get_name(39), "", "TC_COUNT bound returns empty");
3220 }
3221
3222 /// c:756 — `tccap_get_name(38)` is last valid index → non-empty.
3223 #[test]
3224 fn tccap_get_name_last_valid_index_returns_nonempty() {
3225 let n = tccap_get_name(38);
3226 assert!(!n.is_empty(), "tccap_get_name(38) must be non-empty");
3227 assert_eq!(n, tccapnams[38], "matches table");
3228 }
3229
3230 /// c:756 — `tccap_get_name(usize::MAX)` is far out of range → empty.
3231 #[test]
3232 fn tccap_get_name_usize_max_returns_empty() {
3233 assert_eq!(tccap_get_name(usize::MAX), "");
3234 }
3235
3236 /// c:756 — `tccap_get_name` is a pure function.
3237 #[test]
3238 fn tccap_get_name_is_pure() {
3239 for i in [0usize, 1, 5, 10, 38, 39, 100] {
3240 let a = tccap_get_name(i);
3241 let b = tccap_get_name(i);
3242 assert_eq!(a, b, "tccap_get_name({}) must be deterministic", i);
3243 }
3244 }
3245
3246 /// c:1199 — `fallback_compctlread` returns 1 (failure) without args.
3247 #[test]
3248 fn fallback_compctlread_returns_one_pin() {
3249 let _g = crate::test_util::global_state_lock();
3250 assert_eq!(fallback_compctlread(""), 1, "fallback always fails");
3251 assert_eq!(fallback_compctlread("anything"), 1);
3252 }
3253
3254 /// c:1713 — `noop_function` is a true no-op (no panic, no state change).
3255 #[test]
3256 fn noop_function_does_nothing_observable() {
3257 for _ in 0..100 {
3258 noop_function();
3259 }
3260 }
3261
3262 /// c:1720 — `noop_function_int` accepts any i32 without panic.
3263 #[test]
3264 fn noop_function_int_accepts_full_i32_range() {
3265 for v in [i32::MIN, -1, 0, 1, 42, i32::MAX] {
3266 noop_function_int(v);
3267 }
3268 }
3269
3270 /// c:1159 — `zleentry(0)` returns None (no live ZLE for cmd=0).
3271 #[test]
3272 fn zleentry_zero_cmd_returns_none() {
3273 let _g = crate::test_util::global_state_lock();
3274 assert_eq!(zleentry(0), None, "cmd=0 unwired without live ZLE");
3275 }
3276
3277 // ═══════════════════════════════════════════════════════════════════
3278 // Additional C-parity tests for Src/init.c
3279 // c:464 tccap_get_name / c:1081 source / c:1116 sourcehome /
3280 // c:1159 zleentry / c:1199 fallback_compctlread / c:1149 noop_function /
3281 // c:1154 noop_function_int / c:1143 init_bltinmods
3282 // ═══════════════════════════════════════════════════════════════════
3283
3284 /// c:464 — `tccap_get_name` returns &'static str (compile-time type pin).
3285 #[test]
3286 fn tccap_get_name_returns_static_str_type() {
3287 let _: &'static str = tccap_get_name(0);
3288 }
3289
3290 /// c:1081 — `source("")` empty path returns i32 (compile-time pin).
3291 #[test]
3292 fn source_returns_i32_type() {
3293 let _g = crate::test_util::global_state_lock();
3294 let _: i32 = source("");
3295 }
3296
3297 /// c:1081 — `source` is deterministic for the same nonexistent path.
3298 #[test]
3299 fn source_nonexistent_is_deterministic() {
3300 let _g = crate::test_util::global_state_lock();
3301 let first = source("/__never_exists_zshrs_source__");
3302 for _ in 0..3 {
3303 assert_eq!(
3304 source("/__never_exists_zshrs_source__"),
3305 first,
3306 "source must be deterministic"
3307 );
3308 }
3309 }
3310
3311 /// c:1116 — `sourcehome("")` empty arg is safe.
3312 #[test]
3313 fn sourcehome_empty_no_panic() {
3314 let _g = crate::test_util::global_state_lock();
3315 sourcehome("");
3316 }
3317
3318 /// c:1159 — `zleentry` returns Option<String> (compile-time type pin).
3319 #[test]
3320 fn zleentry_returns_option_string_type() {
3321 let _g = crate::test_util::global_state_lock();
3322 let _: Option<String> = zleentry(0);
3323 }
3324
3325 /// c:1199 — `fallback_compctlread` returns i32.
3326 #[test]
3327 fn fallback_compctlread_returns_i32_type() {
3328 let _g = crate::test_util::global_state_lock();
3329 let _: i32 = fallback_compctlread("");
3330 }
3331
3332 /// c:1199 — `fallback_compctlread` is deterministic for any name.
3333 #[test]
3334 fn fallback_compctlread_is_deterministic() {
3335 let _g = crate::test_util::global_state_lock();
3336 for n in ["", "name", "anything"] {
3337 let first = fallback_compctlread(n);
3338 for _ in 0..3 {
3339 assert_eq!(
3340 fallback_compctlread(n),
3341 first,
3342 "fallback_compctlread({:?}) must be deterministic",
3343 n
3344 );
3345 }
3346 }
3347 }
3348
3349 /// c:1143 — `init_bltinmods` returns void (no panic check).
3350 #[test]
3351 fn init_bltinmods_no_panic_full_sweep() {
3352 let _g = crate::test_util::global_state_lock();
3353 for _ in 0..5 {
3354 init_bltinmods();
3355 }
3356 }
3357
3358 /// c:1149 — `noop_function` returns void (compile-time void type).
3359 #[test]
3360 fn noop_function_signature_void() {
3361 let _: () = noop_function();
3362 }
3363
3364 /// c:1154 — `noop_function_int(N)` returns void.
3365 #[test]
3366 fn noop_function_int_signature_void() {
3367 let _: () = noop_function_int(0);
3368 }
3369
3370 /// c:1116 — `sourcehome("/")` root dir safe.
3371 #[test]
3372 fn sourcehome_root_dir_no_panic() {
3373 let _g = crate::test_util::global_state_lock();
3374 sourcehome("/");
3375 }
3376
3377 // ═══════════════════════════════════════════════════════════════════
3378 // Additional C-parity tests for Src/init.c
3379 // c:464 tccap_get_name / c:491 init_term / c:1081 source /
3380 // c:1159 zleentry / c:1199 fallback_compctlread + lifecycle pins
3381 // ═══════════════════════════════════════════════════════════════════
3382
3383 /// c:464 — `tccap_get_name` returns `&'static str` (compile-time pin, alt).
3384 #[test]
3385 fn tccap_get_name_returns_static_str_pin_alt() {
3386 let _: &'static str = tccap_get_name(0);
3387 }
3388
3389 /// c:464 — `tccap_get_name(TC_COUNT)` and beyond return empty.
3390 /// C source: `if (cap >= 39 /* TC_COUNT */) return "";`.
3391 #[test]
3392 fn tccap_get_name_oob_returns_empty() {
3393 assert_eq!(tccap_get_name(39), "", "TC_COUNT itself returns empty");
3394 assert_eq!(tccap_get_name(100), "", "way past TC_COUNT returns empty");
3395 assert_eq!(
3396 tccap_get_name(usize::MAX),
3397 "",
3398 "MAX returns empty (no panic)"
3399 );
3400 }
3401
3402 /// c:464 — `tccap_get_name` is deterministic for in-range indices.
3403 #[test]
3404 fn tccap_get_name_deterministic_for_valid_indices() {
3405 for cap in 0..39usize {
3406 let first = tccap_get_name(cap);
3407 for _ in 0..3 {
3408 assert_eq!(
3409 tccap_get_name(cap),
3410 first,
3411 "tccap_get_name({}) must be pure",
3412 cap
3413 );
3414 }
3415 }
3416 }
3417
3418 /// c:464 — every in-range cap returns non-empty (zsh ships a name
3419 /// for every TC_* slot; an empty entry would be a corpus drift).
3420 #[test]
3421 fn tccap_get_name_all_in_range_caps_non_empty() {
3422 for cap in 0..39usize {
3423 assert!(
3424 !tccap_get_name(cap).is_empty(),
3425 "TC_{} (cap idx {}) must have a non-empty cap name",
3426 cap,
3427 cap
3428 );
3429 }
3430 }
3431
3432 /// c:491 — `init_term` returns i32 (compile-time pin).
3433 #[test]
3434 fn init_term_returns_i32_type() {
3435 let _g = crate::test_util::global_state_lock();
3436 let _: i32 = init_term();
3437 }
3438
3439 /// c:1081 — `source(empty)` returns i32 (compile-time pin, alt).
3440 #[test]
3441 fn source_returns_i32_pin_alt() {
3442 let _g = crate::test_util::global_state_lock();
3443 let _: i32 = source("");
3444 }
3445
3446 /// c:1081 — `source("/__nonexistent_xyz__")` is non-fatal (returns).
3447 #[test]
3448 fn source_nonexistent_file_no_panic() {
3449 let _g = crate::test_util::global_state_lock();
3450 let _ = source("/__definitely_not_a_file_zshrs_xyz__");
3451 }
3452
3453 /// c:1159 — `zleentry` is deterministic (pure dispatch).
3454 #[test]
3455 fn zleentry_is_deterministic() {
3456 let _g = crate::test_util::global_state_lock();
3457 for cmd in 0..5i32 {
3458 let first = zleentry(cmd);
3459 for _ in 0..3 {
3460 assert_eq!(
3461 zleentry(cmd),
3462 first,
3463 "zleentry({}) must be deterministic",
3464 cmd
3465 );
3466 }
3467 }
3468 }
3469
3470 /// c:1199 — `fallback_compctlread("")` is safe (empty name no-op).
3471 #[test]
3472 fn fallback_compctlread_empty_name_safe() {
3473 let _g = crate::test_util::global_state_lock();
3474 let _ = fallback_compctlread("");
3475 }
3476
3477 /// c:1149 — `noop_function` called repeatedly is safe.
3478 #[test]
3479 fn noop_function_repeated_calls_safe() {
3480 for _ in 0..100 {
3481 noop_function();
3482 }
3483 }
3484
3485 /// c:1154 — `noop_function_int` for various ints is safe + void.
3486 #[test]
3487 fn noop_function_int_various_inputs_safe() {
3488 for n in [-1, 0, 1, i32::MIN, i32::MAX] {
3489 let _: () = noop_function_int(n);
3490 }
3491 }
3492
3493 /// c:1116 — `sourcehome("nonexistent_file_xyz")` safe (no-op when
3494 /// the home-relative path doesn't exist).
3495 #[test]
3496 fn sourcehome_nonexistent_file_safe() {
3497 let _g = crate::test_util::global_state_lock();
3498 sourcehome("__definitely_no_such_zshrc_xyz__");
3499 }
3500
3501 // ═══════════════════════════════════════════════════════════════════
3502 // Additional C-parity pins for Src/init.c
3503 // c:464 tccap_get_name / c:491 init_term / c:1081 source /
3504 // c:1116 sourcehome / c:1143 init_bltinmods / c:1149 noop_function /
3505 // c:1159 zleentry / c:1199 fallback_compctlread
3506 // ═══════════════════════════════════════════════════════════════════
3507
3508 /// c:1143 — `init_bltinmods` is idempotent (safe to call repeatedly).
3509 #[test]
3510 fn init_bltinmods_idempotent_repeated_calls() {
3511 let _g = crate::test_util::global_state_lock();
3512 for _ in 0..10 {
3513 init_bltinmods();
3514 }
3515 }
3516
3517 /// c:464 — `tccap_get_name` for index 0 returns non-empty (first cap).
3518 #[test]
3519 fn tccap_get_name_index_zero_non_empty() {
3520 let _g = crate::test_util::global_state_lock();
3521 let _ = tccap_get_name(0);
3522 }
3523
3524 /// c:464 — `tccap_get_name(usize::MAX)` doesn't panic.
3525 #[test]
3526 fn tccap_get_name_usize_max_no_panic() {
3527 let _g = crate::test_util::global_state_lock();
3528 let r = tccap_get_name(usize::MAX);
3529 // OOB returns empty per the OOB test that already exists.
3530 assert!(r.is_empty(), "OOB must return empty, got {:?}", r);
3531 }
3532
3533 /// c:1116 — `sourcehome("")` empty name safe (no-op when path empty).
3534 #[test]
3535 fn sourcehome_empty_name_no_panic() {
3536 let _g = crate::test_util::global_state_lock();
3537 sourcehome("");
3538 }
3539
3540 /// c:1081 — `source` return type i32 (compile-time pin).
3541 #[test]
3542 fn source_returns_i32_type_compile_pin() {
3543 let _g = crate::test_util::global_state_lock();
3544 let _: i32 = source("/dev/null");
3545 }
3546
3547 /// c:1081 — `source("")` empty path doesn't panic.
3548 #[test]
3549 fn source_empty_path_no_panic() {
3550 let _g = crate::test_util::global_state_lock();
3551 let _ = source("");
3552 }
3553
3554 /// c:1159 — `zleentry` return type Option<String> (compile-time pin, alt).
3555 #[test]
3556 fn zleentry_returns_option_string_type_alt() {
3557 let _g = crate::test_util::global_state_lock();
3558 let _: Option<String> = zleentry(0);
3559 }
3560
3561 /// c:1159 — `zleentry(-1)` (invalid cmd) doesn't panic.
3562 #[test]
3563 fn zleentry_negative_cmd_no_panic() {
3564 let _g = crate::test_util::global_state_lock();
3565 let _ = zleentry(-1);
3566 }
3567
3568 /// c:1199 — `fallback_compctlread` returns i32 (compile-time pin, alt).
3569 #[test]
3570 fn fallback_compctlread_returns_i32_type_alt() {
3571 let _g = crate::test_util::global_state_lock();
3572 let _: i32 = fallback_compctlread("test");
3573 }
3574
3575 /// c:1149 — `noop_function` returns void (compile-time pin).
3576 #[test]
3577 fn noop_function_returns_void_type() {
3578 let _g = crate::test_util::global_state_lock();
3579 let _: () = noop_function();
3580 }
3581
3582 /// c:1154 — `noop_function_int` returns void (compile-time pin).
3583 #[test]
3584 fn noop_function_int_returns_void_type() {
3585 let _g = crate::test_util::global_state_lock();
3586 let _: () = noop_function_int(42);
3587 }
3588
3589 /// c:464 — `tccap_get_name` is deterministic for OOB (same empty value
3590 /// across calls).
3591 #[test]
3592 fn tccap_get_name_oob_deterministic() {
3593 let _g = crate::test_util::global_state_lock();
3594 let a = tccap_get_name(99_999);
3595 let b = tccap_get_name(99_999);
3596 assert_eq!(a, b, "OOB must be deterministic");
3597 }
3598}