zsh/ported/modules/zutil.rs
1//! Zsh utility builtins - port of Modules/zutil.c
2//!
3//! Style stuff. // c:82
4//! Hash table of styles and associated functions. // c:104
5//! Format stuff. // c:800
6//! Zregexparse stuff. // c:1091
7//!
8//! Provides zstyle, zformat, zparseopts builtins.
9
10use crate::ported::builtin::PPARAMS;
11use crate::ported::glob::tokenize;
12use crate::ported::mem::{popheap, pushheap};
13use crate::ported::options::opt_state_set;
14use crate::ported::params::{
15 assignaparam, getaparam, getsparam, paramtab, setaparam, sethparam, setsparam, unsetparam,
16};
17use crate::ported::pattern::{patcompile, pattry};
18use crate::ported::signals_h::{queue_signals, unqueue_signals};
19use crate::ported::utils::{errflag, zwarnnam};
20use crate::ported::zsh_h::PAT_HEAPDUP;
21use crate::ported::zsh_h::{
22 eprog, features, hashnode, hashtable, isset, module, opt_name, options, param, Eprog, HashNode,
23 HashTable, Param, Patprog, ERRFLAG_INT, EXTENDEDGLOB, MAX_OPS, OPT_ARG, OPT_ISSET, PAT_STATIC, PM_ARRAY,
24};
25use std::collections::HashMap;
26use std::io::Write;
27use std::sync::atomic::Ordering;
28use std::sync::{Mutex, OnceLock};
29
30/// Port of `savematch(MatchData *m)` from Src/Modules/zutil.c:40.
31/// C: `static void savematch(MatchData *m)` — snapshot $match/$mbegin/
32/// $mend into the MatchData struct.
33#[allow(non_snake_case)]
34pub fn savematch(m: &mut MatchData) {
35 // c:40
36 queue_signals(); // c:44
37 // c:45-50 — three `a = getaparam("X"); m->X = a ? zarrdup(a) : NULL`
38 // captures. The previous Rust port hardcoded `a = None` for all
39 // three because the fabricated `getaparam(Option<&mut value>)` sig
40 // couldn't take a name string. Now that `getaparam(&str)` matches
41 // C, real reads from paramtab work end-to-end.
42 m.r#match = getaparam("match"); // c:45-46
43 m.mbegin = getaparam("mbegin"); // c:47-48
44 m.mend = getaparam("mend"); // c:49-50
45 unqueue_signals(); // c:51
46}
47
48/// Port of `static void restorematch(MatchData *m)` from
49/// `Src/Modules/zutil.c:55`.
50///
51/// C body (c:57-68):
52/// ```c
53/// if (m->match) setaparam("match", m->match);
54/// else unsetparam("match");
55/// if (m->mbegin) setaparam("mbegin", m->mbegin);
56/// else unsetparam("mbegin");
57/// if (m->mend) setaparam("mend", m->mend);
58/// else unsetparam("mend");
59/// ```
60///
61/// Restores `$match`/`$mbegin`/`$mend` from a snapshot. Critical:
62/// when the saved field is NULL/None, C **unsets** the param. The
63/// previous Rust port left it alone — comment claimed "the Rust
64/// paramtab API doesn't yet expose unsetparam-by-string" but
65/// `unsetparam` HAS been ported (`params::unsetparam` at
66/// params.rs:4731). Skipping the unset means a regex callout that
67/// set `$match` from an originally-unset state would leave `$match`
68/// set after restorematch — the OPPOSITE of the documented contract.
69pub fn restorematch(m: &MatchData) {
70 // c:55
71 // c:57-68 — C uses `setaparam` which routes through `assignaparam`
72 // with `ASSPM_WARN` (params.c:3766). The warn flag tells
73 // assignaparam to emit "scalar parameter X created globally in
74 // function" / "read-only variable" diagnostics on write attempts.
75 //
76 // Prior Rust port called `assignaparam` directly with `flags = 0`,
77 // dropping the WARN bit — a user who pinned `typeset -r match`
78 // upstream would see the readonly assignment silently swallowed,
79 // where C zsh prints `zsh: read-only variable: match` and bails.
80 // Route through `setaparam` (port at params.rs:6023 already does
81 // the `assignaparam(name, val, ASSPM_WARN)` wrap) so the
82 // diagnostic surface matches C bit-for-bit.
83 if let Some(v) = m.r#match.as_ref() {
84 setaparam("match", v.clone()); // c:58
85 } else {
86 unsetparam("match"); // c:60
87 }
88 if let Some(v) = m.mbegin.as_ref() {
89 setaparam("mbegin", v.clone()); // c:62
90 } else {
91 unsetparam("mbegin"); // c:64
92 }
93 if let Some(v) = m.mend.as_ref() {
94 setaparam("mend", v.clone()); // c:66
95 } else {
96 unsetparam("mend"); // c:68
97 }
98}
99
100/// Port of `freematch(Cmatch m, int nbeg, int nend)` from Src/Modules/zutil.c:72.
101/// C: `static void freematch(MatchData *m)` — drops the captured arrays.
102#[allow(non_snake_case)]
103pub fn freematch(m: &mut MatchData) {
104 // c:72
105 // c:72
106 // c:74-81 — freearray(m->match/mbegin/mend) when non-NULL. Rust
107 // path: take() drops the inner Vec, mirroring freearray + NULL set.
108 m.r#match.take();
109 m.mbegin.take();
110 m.mend.take();
111}
112// `MatchData` is defined above (line 23) — Option<Vec<String>> per field
113// matches the C `char **match`/`mbegin`/`mend` semantics where NULL means
114// the variable was unset. The savematch/restorematch/freematch ports
115// below operate on that existing struct.
116
117/// `Stypat` mirroring Src/Modules/zutil.c:97-104.
118///
119/// `Clone` is a RUST-ONLY addition (C moves pointers): `(...)` subshells
120/// are in-process in zshrs, so `subshell_begin` has to deep-copy the
121/// zstyle table the way `fork()` copies it for C — see the
122/// `SubshellSnapshot::zstyles` field in fusevm_bridge.rs.
123#[allow(non_camel_case_types)]
124#[derive(Clone)]
125pub struct stypat {
126 pub next: Option<Box<stypat>>, // c:98 Stypat next
127 pub pat: String, // c:99 char *pat
128 pub prog: Option<crate::ported::pattern::Patprog>, // c:100 Patprog prog (compiled)
129 pub weight: u64, // c:101 zulong weight
130 pub eval: Option<Eprog>, // c:102 Eprog eval
131 pub vals: Vec<String>, // c:103 char **vals
132}
133/// `Stypat` type alias.
134pub type Stypat = Box<stypat>;
135
136/// `Style` mirroring Src/Modules/zutil.c:91-94.
137#[allow(non_camel_case_types)]
138pub struct style {
139 pub node: hashnode, // c:92 struct hashnode node
140 pub pats: Option<Stypat>, // c:93 Stypat pats (sorted by weight)
141}
142/// `Style` type alias.
143pub type Style = Box<style>;
144
145/// Global `zstyletab` mirror — port of the static
146/// `static HashTable zstyletab` in Src/Modules/zutil.c:209.
147/// C allocates this via `newzstyletable()` (c:270) during
148/// module setup; the Rust port uses a `LazyLock<Mutex<>>`
149/// since the table is process-global and `bin_zstyle` /
150/// `lookupstyle` / `testforstyle` all need to share it.
151#[allow(non_upper_case_globals)]
152pub static zstyletab: std::sync::LazyLock<Mutex<style_table>> =
153 std::sync::LazyLock::new(|| Mutex::new(style_table::new())); // c:209
154
155/// Port of `freestylepatnode(Stypat p)` from Src/Modules/zutil.c:111.
156/// C: `static void freestylepatnode(Stypat p)` — drops pat/prog/vals/eval.
157#[allow(non_snake_case)]
158pub fn freestylepatnode(p: Stypat) {
159 // c:111
160 // c:111 zsfree(p->pat) — String drop
161 // c:114 freepatprog(p->prog) — Option<()> drop
162 // c:115-116 if (p->vals) freearray(p->vals) — Vec<String> drop
163 // c:117-118 if (p->eval) freeeprog(p->eval) — Option<()> drop
164 // c:119 zfree(p, sizeof(*p)) — Box<stypat> drop
165 drop(p);
166}
167
168/// Port of `freestylenode(HashNode hn)` from Src/Modules/zutil.c:123.
169/// C: `static void freestylenode(HashNode hn)` — walk pats list freeing
170/// each via freestylepatnode, then free node name + Style.
171#[allow(non_snake_case)]
172pub fn freestylenode(hn: HashNode) {
173 // c:123
174 // c:123 — Style s = (Style) hn; (C uses hashnode-prefix
175 // inheritance; the Rust HashNode and Style are separate Boxes so
176 // the cast collapses to dropping hn — its underlying style.pats
177 // chain drops with it.)
178 let s: HashNode = hn;
179 // c:111 — Stypat p, pn;
180 // c:111-133 — while (p) { pn = p->next; freestylepatnode(p); p = pn; }
181 // Rust: dropping s drops style.pats recursively.
182 drop(s);
183 // c:135 zsfree(s->node.nam) + c:136 zfree(s) — Rust Drop handles.
184}
185
186/// Port of `freestypat(Stypat p, Style s, Stypat prev)` from Src/Modules/zutil.c:151.
187/// C: `static void freestypat(Stypat p, Style s, Stypat prev)` — unlink
188/// from style.pats list, then freestylepatnode. If style empties,
189/// remove from zstyletab too.
190#[allow(non_snake_case)]
191pub fn freestypat(mut p: Stypat, s: Option<&mut style>, prev: Option<&mut stypat>) {
192 // c:151
193 // c:151-158 — relink prev->next to p->next (or s->pats if no prev).
194 // Use Option::take() to move the chain pointer out of p, since
195 // stypat doesn't derive Clone (matching C's pointer-move semantics).
196 let next = p.next.take(); // c:155 capture p->next
197 let s_has_some = s.is_some();
198 if let Some(s_ref) = s {
199 // c:153
200 if let Some(prev_ref) = prev {
201 // c:154
202 prev_ref.next = next; // c:155 prev->next = p->next
203 } else {
204 s_ref.pats = next; // c:157 s->pats = p->next
205 }
206 }
207 // c:160 — freestylepatnode(p);
208 freestylepatnode(p);
209 // c:162-167 — if (s && !s->pats) { zstyletab->removenode + zsfree(name) + zfree(s) }
210 // Static-link path: zstyletab access lives outside src/ported; the
211 // removal is a no-op until the style table accessor is wired.
212 let _ = s_has_some;
213}
214
215impl style_table {
216 /// WARNING: NOT IN ZUTIL.C — method on Rust-only `style_table` wrapper.
217 /// C inlines this pattern at every callsite; Rust factors it onto the wrapper.
218 pub fn new() -> Self {
219 Self::default()
220 }
221
222 /// Port of `setstypat(Style s, char *pat, Patprog prog, char **vals, int eval)` from `Src/Modules/zutil.c:295`.
223 /// Insert or replace a pattern→values mapping for a style.
224 /// Mirrors Src/Modules/zutil.c:295 `setstypat` + c:403 `addstyle`
225 /// — find or create the style's pats list, replace if pattern
226 /// already present, else insert in weight-descending order.
227 pub fn set(
228 &mut self,
229 pattern: &str,
230 style: &str,
231 values: Vec<String>,
232 eval_prog: Option<Eprog>,
233 ) {
234 if let Ok(m) = self.memo.get_mut() {
235 m.clear(); // the patterns change: drop memoized matches
236 }
237 let style_patterns = self.styles.entry(style.to_string()).or_default();
238 // c:319-333 — Exists → replace.
239 if let Some(existing) = style_patterns.iter_mut().find(|p| p.pat == pattern) {
240 existing.vals = values; // c:328
241 existing.eval = eval_prog; // c:329 p->eval = eprog (the parsed program)
242 return;
243 }
244 // c:344-385 — Calculate weight: high 32 bits = colon-component
245 // count, low 32 bits = sum of per-component specificity (0/1/2).
246 //
247 // Scoring per component:
248 // `*` (alone in component, must be followed by NUL or `:`) → 0
249 // contains a pattern metachar (`( | * [ < ? # ^`) → 1
250 // plain literal → 2
251 //
252 // Prior Rust port omitted the c:365 lookahead `(!str[1] ||
253 // str[1] == ':')` on the wildcard-component check, so patterns
254 // like `*foo:bar` mis-scored their first component as a bare
255 // wildcard (0) instead of a metachar-containing pattern (1).
256 // The miscount produced incorrect zstyle ordering — more-
257 // specific patterns lost out to less-specific siblings.
258 let mut weight: u64 = 0;
259 let mut tmp: u64 = 2;
260 let mut first = true;
261 let bytes = pattern.as_bytes();
262 let mut i = 0;
263 while i < bytes.len() {
264 let ch = bytes[i] as char;
265 let next: Option<u8> = bytes.get(i + 1).copied();
266 // c:365 — `if (first && *str == '*' && (!str[1] || str[1] == ':'))`
267 // "alone-star component": star is the first char AND the
268 // next char ends the component (NUL or `:`).
269 if first && ch == '*' && (next.is_none() || next == Some(b':')) {
270 tmp = 0;
271 i += 1;
272 continue;
273 }
274 first = false; // c:370
275 if matches!(ch, '(' | '|' | '*' | '[' | '<' | '?' | '#' | '^') {
276 // c:372
277 tmp = 1;
278 }
279 if ch == ':' {
280 // c:377
281 weight += 1u64 << 32; // c:379
282 first = true; // c:381
283 weight += tmp; // c:382
284 tmp = 2; // c:383
285 }
286 i += 1;
287 }
288 weight += tmp; // c:386
289 // c:337-342 — New pattern: build stypat.
290 // c:339 — `p->prog = prog;`. bin_zstyle compiles the pattern
291 // once, when the style is set (c:525-527 `tokenize(pat)`,
292 // `patcompile(pat, PAT_ZDUP, NULL)`), and every lookup reuses
293 // it. Compiling at lookup time instead re-ran patcompile for
294 // every pattern of a style on every zstyle query.
295 let prog = {
296 let mut tok = pattern.to_string();
297 crate::ported::glob::tokenize(&mut tok); // c:526
298 patcompile(&tok, crate::ported::zsh_h::PAT_ZDUP, None) // c:527
299 };
300 let sp = stypat {
301 next: None, // c:342
302 pat: pattern.to_string(), // c:338
303 prog, // c:339
304 weight, // c:386
305 eval: eval_prog, // c:341 p->eval = eprog (the parsed program)
306 vals: values, // c:340
307 };
308 // c:388-396 — insert q in weight-descending order (highest first).
309 let pos = style_patterns
310 .iter()
311 .position(|p| p.weight < weight)
312 .unwrap_or(style_patterns.len());
313 style_patterns.insert(pos, sp);
314 }
315
316 /// Port of `bin_zstyle(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))` from `Src/Modules/zutil.c:487`.
317 /// Look up the values for (context, style). Mirrors
318 /// Src/Modules/zutil.c:443 `lookupstyle` — walk the style's pats
319 /// list, return values from the first weight-sorted entry whose
320 /// pat matches the context.
321 pub fn get(&self, context: &str, style: &str) -> Option<&[String]> {
322 let patterns = self.styles.get(style)?;
323 self.matched_pat(patterns, context, style)
324 .map(|i| patterns[i].vals.as_slice())
325 }
326
327 /// c:Src/Modules/zutil.c:768-779 — `bin_zstyle -g` retrieval. Unlike
328 /// `lookupstyle`/`get` (which `pattry`-match the CONTEXT against every
329 /// stored pattern), `-g` does an EXACT pattern-string compare
330 /// (`if (!strcmp(args[2], p->pat))`). So after `zstyle ':s:*' k v`,
331 /// `zstyle -g out ':s:sub' k` returns NOTHING (":s:sub" != ":s:*"),
332 /// whereas `zstyle -s ':s:sub' k out` matches and yields "v".
333 pub fn get_exact(&self, pattern: &str, style: &str) -> Option<&[String]> {
334 self.styles.get(style).and_then(|pats| {
335 pats.iter()
336 .find(|p| p.pat == pattern)
337 .map(|p| p.vals.as_slice())
338 })
339 }
340
341 /// WARNING: NOT IN ZUTIL.C — method on the Rust-only `style_table`
342 /// wrapper. Same best-pattern-match walk as `get`, but returns the
343 /// matched entry's values AND whether it is an `-e` (eval) style, so
344 /// `lookupstyle` can decide to execute the body (C reads the matched
345 /// `Stypat`'s `eval` field inline; the wrapper keeps the map private).
346 pub fn get_match(&self, context: &str, style: &str) -> Option<(Vec<String>, bool)> {
347 let patterns = self.styles.get(style)?;
348 self.matched_pat(patterns, context, style)
349 .map(|i| (patterns[i].vals.clone(), patterns[i].eval.is_some()))
350 }
351
352 /// WARNING: NOT IN ZUTIL.C — method on the Rust-only `style_table`
353 /// wrapper. The c:452-458 walk shared by `get` and `get_match`: the index
354 /// of the first pattern, in weight order, whose prog matches `context`
355 /// (c:455 `pattry(p->prog, context)`, on the prog compiled when the style
356 /// was set). Answers are memoized per (style, context); see `memo`.
357 fn matched_pat(&self, patterns: &[stypat], context: &str, style: &str) -> Option<usize> {
358 if let Some(hit) = self
359 .memo
360 .lock()
361 .ok()
362 .and_then(|m| m.get(style).and_then(|c| c.get(context)).copied())
363 {
364 return hit;
365 }
366 let found = patterns
367 .iter()
368 .position(|p| p.pat == "*" || p.prog.as_ref().is_some_and(|prog| pattry(prog, context)));
369 if let Ok(mut m) = self.memo.lock() {
370 m.entry(style.to_string())
371 .or_default()
372 .insert(context.to_string(), found);
373 }
374 found
375 }
376
377 /// WARNING: NOT IN ZUTIL.C — method on Rust-only `style_table` wrapper.
378 /// C inlines this pattern at every callsite; Rust factors it onto the wrapper.
379 /// Remove style/pattern entries from the table. Mirrors the
380 /// `-d` dispatch arms of `bin_zstyle` (Src/Modules/zutil.c:487).
381 pub fn delete(&mut self, pattern: Option<&str>, style: Option<&str>) {
382 if let Ok(m) = self.memo.get_mut() {
383 m.clear(); // the patterns change: drop memoized matches
384 }
385 match (pattern, style) {
386 (None, None) => self.styles.clear(),
387 (Some(pat), None) => {
388 for patterns in self.styles.values_mut() {
389 patterns.retain(|p| p.pat != pat);
390 }
391 self.styles.retain(|_, v| !v.is_empty());
392 }
393 (Some(pat), Some(sty)) => {
394 if let Some(patterns) = self.styles.get_mut(sty) {
395 patterns.retain(|p| p.pat != pat);
396 if patterns.is_empty() {
397 self.styles.remove(sty);
398 }
399 }
400 }
401 (None, Some(sty)) => {
402 self.styles.remove(sty);
403 }
404 }
405 }
406
407 /// Port of `setstypat(Style s, char *pat, Patprog prog, char **vals, int eval)` from `Src/Modules/zutil.c:295`.
408 /// Return `(pattern, style, values)` triples for `zstyle -L` /
409 /// `zstyle -a` listing. Mirrors bin_zstyle list dispatch
410 /// (Src/Modules/zutil.c:487 -L/-a arms).
411 pub fn list(&self, context: Option<&str>) -> Vec<(String, String, Vec<String>)> {
412 let mut result = Vec::new();
413 // c:Src/Modules/zutil.c:558 / :751 / :756 — every listing consumer
414 // (`printstyle` for the plain/`-L` listing, `scanpatstyles` for both
415 // `-g` shapes) calls `scanhashtable(zstyletab, 1, …)`, and that
416 // leading `1` is the SORTED flag: the style table is walked in
417 // strcmp order of the style NAME. `zstyle -g out` therefore returns
418 // its contexts in a stable order in zsh, while iterating the Rust
419 // HashMap directly produced a different order on every run.
420 let mut style_names: Vec<&String> = self.styles.keys().collect();
421 style_names.sort(); // c:hashtable.c scanhashtable sorted arm
422 for style in style_names {
423 let patterns = &self.styles[style];
424 for pat in patterns {
425 if let Some(ctx) = context {
426 let matches =
427 pat.pat == "*" || pat.prog.as_ref().is_some_and(|prog| pattry(prog, ctx));
428 if !matches {
429 continue;
430 }
431 }
432 result.push((pat.pat.clone(), style.clone(), pat.vals.clone()));
433 }
434 }
435 result
436 }
437
438 /// WARNING: NOT IN ZUTIL.C — method on Rust-only `style_table` wrapper.
439 /// Every entry as `(pattern, style, values, is_eval)`, in [`list`]'s
440 /// order (style name sorted, each style's patterns by weight) — what
441 /// `zstyle -L` prints, `-e` included (c:Src/Modules/zutil.c:201
442 /// `printf("zstyle %s", (p->eval ? "-e " : ""))`). The recorder snapshots it so a
443 /// replay can call `setstypat` with the same arguments.
444 ///
445 /// [`list`]: style_table::list
446 pub fn entries(&self) -> Vec<(String, String, Vec<String>, bool)> {
447 let mut style_names: Vec<&String> = self.styles.keys().collect();
448 style_names.sort();
449 style_names
450 .into_iter()
451 .flat_map(|style| {
452 self.styles[style]
453 .iter()
454 .map(move |p| (p.pat.clone(), style.clone(), p.vals.clone(), p.eval.is_some()))
455 })
456 .collect()
457 }
458
459 /// WARNING: NOT IN ZUTIL.C — method on Rust-only `style_table` wrapper.
460 /// C inlines this pattern at every callsite; Rust factors it onto the wrapper.
461 /// List all registered style names (bin_zstyle -g without args).
462 pub fn list_styles(&self) -> Vec<&str> {
463 self.styles.keys().map(|s| s.as_str()).collect()
464 }
465
466 /// WARNING: NOT IN ZUTIL.C — method on Rust-only `style_table` wrapper.
467 /// C inlines this pattern at every callsite; Rust factors it onto the wrapper.
468 /// List all distinct patterns across every style (bin_zstyle -g
469 /// with a single pattern arg).
470 pub fn list_patterns(&self) -> Vec<&str> {
471 let mut patterns = Vec::new();
472 for pats in self.styles.values() {
473 for pat in pats {
474 if !patterns.contains(&pat.pat.as_str()) {
475 patterns.push(pat.pat.as_str());
476 }
477 }
478 }
479 patterns
480 }
481
482 /// WARNING: NOT IN ZUTIL.C — method on Rust-only `style_table` wrapper.
483 /// C inlines this pattern at every callsite; Rust factors it onto the wrapper.
484 /// Boolean-truthy `zstyle -T` / `zstyle -t` check.
485 /// Mirrors bin_zstyle -t / -T arms in Src/Modules/zutil.c:487.
486 pub fn test(&self, context: &str, style: &str, values: Option<&[&str]>) -> bool {
487 if let Some(found) = self.get(context, style) {
488 if let Some(test_vals) = values {
489 test_vals.iter().any(|v| found.contains(&v.to_string()))
490 } else {
491 matches!(
492 found.first().map(|s| s.as_str()),
493 Some("true" | "yes" | "on" | "1")
494 )
495 }
496 } else {
497 false
498 }
499 }
500
501 /// WARNING: NOT IN ZUTIL.C — method on Rust-only `style_table` wrapper.
502 /// C inlines this pattern at every callsite; Rust factors it onto the wrapper.
503 /// Single-value "yes/no" interrogation of a style. The `bin_zstyle`
504 /// -b arm of Src/Modules/zutil.c:487.
505 pub fn test_bool(&self, context: &str, style: &str) -> Option<bool> {
506 self.get(context, style).and_then(|vals| {
507 if vals.len() == 1 {
508 match vals[0].as_str() {
509 "yes" | "true" | "on" | "1" => Some(true),
510 "no" | "false" | "off" | "0" => Some(false),
511 _ => None,
512 }
513 } else {
514 None
515 }
516 })
517 }
518}
519
520/// Port of `static Patprog zstyle_contprog;` from Src/Modules/zutil.c:178 —
521/// the pattern `zstyle -L [context]` filters printed style patterns with.
522thread_local! {
523 static ZSTYLE_CONTPROG: std::cell::RefCell<Option<crate::ported::pattern::Patprog>> = const { std::cell::RefCell::new(None) }; // c:178
524}
525
526/// Port of `printstylenode(HashNode hn, int printflags)` from Src/Modules/zutil.c:184.
527/// C: `static void printstylenode(HashNode hn, int printflags)` — emit
528/// `zstyle -L` / basic-list output for one style entry. The context filter
529/// is the file-static `zstyle_contprog`, set by `bin_zstyle` before the scan
530/// (c:562-570), exactly as in C.
531#[allow(non_snake_case)]
532pub fn printstylenode(hn: &HashNode, printflags: i32) {
533 // c:184
534 let nam: String = hn.nam.clone();
535 let mut stdout = std::io::stdout().lock();
536 use crate::ported::utils::quotedzputs;
537 let t = match zstyletab.lock() {
538 Ok(t) => t,
539 Err(_) => return,
540 };
541 let patterns = match t.styles.get(&nam) {
542 Some(p) => p,
543 None => return,
544 };
545 if printflags == 1 {
546 // c:190-193 — ZSLIST_BASIC header: the style name on its own line.
547 // Only emitted when at least one pattern will survive the filter,
548 // matching C (the header prints unconditionally, but the contprog
549 // filter path is only reached via the syntax listing; bare `zstyle`
550 // passes no context so every pattern survives here).
551 let _ = writeln!(stdout, "{}", quotedzputs(&nam)); // c:191-192
552 }
553 for p in patterns {
554 // c:196-197 — skip patterns that don't match the context filter.
555 if ZSTYLE_CONTPROG.with(|c| {
556 c.borrow()
557 .as_ref()
558 .is_some_and(|prog| !pattry(prog, &p.pat))
559 }) {
560 continue;
561 }
562 let is_eval = p.eval.is_some();
563 if printflags == 1 {
564 // c:198-199 — `printf("%s %s", eval ? "(eval)" : " ", p->pat);`
565 let prefix = if is_eval { "(eval)" } else { " " };
566 let _ = write!(stdout, "{} {}", prefix, p.pat); // c:199
567 } else {
568 // c:201-204 — `printf("zstyle %s", eval ? "-e " : "");
569 // quotedzputs(p->pat); putchar(' '); quotedzputs(style);`
570 let eflag = if is_eval { "-e " } else { "" };
571 let _ = write!(stdout, "zstyle {}", eflag); // c:201
572 let _ = write!(stdout, "{}", quotedzputs(&p.pat)); // c:202
573 let _ = write!(stdout, " "); // c:203
574 let _ = write!(stdout, "{}", quotedzputs(&nam)); // c:204
575 }
576 // c:206-209 — per-value: ` `, quotedzputs(v).
577 for v in &p.vals {
578 let _ = write!(stdout, " {}", quotedzputs(v)); // c:207-208
579 }
580 let _ = writeln!(stdout); // c:210
581 }
582}
583
584/// Port of `scanpatstyles(HashNode hn, int spatflags)` from Src/Modules/zutil.c:229.
585/// C: `static void scanpatstyles(HashNode hn, int spatflags)` — iterate
586/// every pattern of `hn`'s style, switching on `spatflags` (ZSPAT_NAME /
587/// ZSPAT_PAT / ZSPAT_REMOVE).
588#[allow(non_snake_case)]
589pub fn scanpatstyles(hn: HashNode, spatflags: i32) {
590 // c:229
591 // c:229 — Style s = (Style)hn;
592 let _s: HashNode = hn;
593 // c:232 — Stypat p, q;
594 // c:233 — LinkNode n;
595 // c:235-265 — for (q = NULL, p = s->pats; p; q = p, p = p->next)
596 // walks the pattern list and dispatches on spatflags. Rust port:
597 // the HashNode→Style cast doesn't yield the pats list directly
598 // (separate Boxes), so the body switches on spatflags and exits
599 // each branch without traversal until the cast is wired.
600 match spatflags {
601 // c:236
602 0 => { // c:237 ZSPAT_NAME
603 // c:238-241 — if pat matches zstyle_patname, addlinknode + return
604 }
605 1 => { // c:244 ZSPAT_PAT
606 // c:246-251 — addlinknode unless already present
607 }
608 2 => { // c:253 ZSPAT_REMOVE
609 // c:254-262 — if pat matches, freestypat(p, s, q) + return
610 }
611 _ => {}
612 }
613}
614
615impl ZFormat {
616 /// Recursive walker for zformat. Returns the index of the
617 /// terminator (`endchar`). idx is mutated in place.
618 /// Direct port of `zformat_substring()` from Src/Modules/zutil.c:814 —
619 /// the recursive descent over the format string with `%c` substitution
620 /// and `%(?...)` ternary blocks.
621 fn substring(
622 bytes: &[char],
623 idx: &mut usize,
624 out: &mut String,
625 endchar: char,
626 specs: &HashMap<char, String>,
627 presence: bool,
628 quote: bool,
629 qspecs: &HashMap<char, usize>,
630 skip: bool,
631 ) -> Option<()> {
632 while *idx < bytes.len() {
633 let c = bytes[*idx];
634 // Stop at endchar (zutil.c:820 `*s != endchar`).
635 if endchar != '\0' && c == endchar {
636 return Some(());
637 }
638 if c != '%' {
639 // Plain text — emit unless skipping (zutil.c:937-948).
640 if !skip {
641 out.push(c);
642 }
643 *idx += 1;
644 continue;
645 }
646 // `%` — parse the spec.
647 let start = *idx;
648 *idx += 1;
649 // Optional `-` for right-align (zutil.c:825-826).
650 let mut right = false;
651 if *idx < bytes.len() && bytes[*idx] == '-' {
652 right = true;
653 *idx += 1;
654 }
655 // Optional digit run for min (zutil.c:828-831).
656 let mut min: Option<i64> = None;
657 if *idx < bytes.len() && bytes[*idx].is_ascii_digit() {
658 let mut n: i64 = 0;
659 while *idx < bytes.len() && bytes[*idx].is_ascii_digit() {
660 n = n * 10 + bytes[*idx].to_digit(10).unwrap() as i64;
661 *idx += 1;
662 }
663 min = Some(n);
664 }
665 // Ternary detection: `(` at this position (zutil.c:834-840).
666 let testit = *idx < bytes.len() && bytes[*idx] == '(';
667 // `%(-...` allows leading `-` after the paren (zutil.c:835-840).
668 if testit && *idx + 1 < bytes.len() && bytes[*idx + 1] == '-' {
669 right = true;
670 *idx += 1;
671 }
672 // Optional `.MAX` or just `.` after (zutil.c:841-845).
673 let mut max: Option<i64> = None;
674 if *idx < bytes.len()
675 && (bytes[*idx] == '.' || testit)
676 && *idx + 1 < bytes.len()
677 && bytes[*idx + 1].is_ascii_digit()
678 {
679 *idx += 1; // skip `.` or `(`
680 let mut n: i64 = 0;
681 while *idx < bytes.len() && bytes[*idx].is_ascii_digit() {
682 n = n * 10 + bytes[*idx].to_digit(10).unwrap() as i64;
683 *idx += 1;
684 }
685 max = Some(n);
686 } else if *idx < bytes.len() && (bytes[*idx] == '.' || testit) {
687 *idx += 1;
688 }
689
690 // c:849-856 — "next char isn't a legal spec char -- unwind, treat
691 // the sequence literally":
692 // if (!testit && (!*s || *s == '%' || *s == ')' || *s == '-' || *s == '.')) {
693 // if (!quote) start += (s - start == 1 && (*s == '%' || *s == ')'));
694 // s = start;
695 // }
696 // After the unwind `*s` is `%` (or `)`), never a registered spec, so
697 // the raw-copy arm below emits exactly one character.
698 let mut start = start;
699 if !testit {
700 let at = bytes.get(*idx).copied();
701 if matches!(at, None | Some('%') | Some(')') | Some('-') | Some('.')) {
702 // c:853 — `-q` keeps `%%` and `%)` intact for the later
703 // prompt expansion the quoted output is destined for.
704 if !quote && *idx - start == 1 && matches!(at, Some('%') | Some(')')) {
705 start += 1; // c:854
706 }
707 *idx = start; // c:855
708 }
709 }
710
711 if testit && *idx < bytes.len() {
712 // Ternary expression — zutil.c:858-902.
713 let testval: i64 = min.or(max).unwrap_or(0);
714 let spec_char = bytes[*idx];
715 let actval: bool;
716 let spec_val = specs.get(&spec_char);
717 if let Some(sv) = spec_val.filter(|s| !s.is_empty()) {
718 if presence {
719 let cmp_val: i64 = if testval != 0 {
720 sv.chars().count() as i64
721 } else {
722 1
723 };
724 // c:875-876 — "don't count extra %s from quoting when
725 // testing this"; applied even to the bare-presence
726 // value 1, so a `-q` spec holding a `%` with no test
727 // width tests as absent, as in C.
728 let cmp_val = cmp_val - qspecs.get(&spec_char).copied().unwrap_or(0) as i64;
729 actval = if right {
730 testval < cmp_val
731 } else {
732 testval >= cmp_val
733 };
734 } else {
735 let signed_test = if right { -testval } else { testval };
736 // c:882 — `actval = (int) mathevali(specs[(unsigned
737 // char) *s]) - testval;`. The spec's VALUE is an
738 // arithmetic expression, not a bare integer: the
739 // documented `%18(s.math.)` with `s:6*3` is true.
740 // The previous `sv.parse()` returned 0 for any
741 // non-literal, so every arithmetic test was false.
742 //
743 // C's `mathevali` REPORTS a bad expression on the way
744 // out (checkunary -> zerr) and leaves `errflag` set,
745 // yielding 0 (c:Src/math.c:1505-1509); `bin_zformat`
746 // never checks it, so the shell aborts on the flag.
747 // Rust's `mathevali` returns that message in `Err`
748 // instead, and the port discarded it with
749 // `.unwrap_or(0)` — so `zformat -f R '%(%.yes.no)'`
750 // printed `yes` where zsh 5.9.2 prints `bad math
751 // expression: operand expected at `%'` and aborts.
752 // Surface it the way bin_let (builtin.rs:17151) and
753 // paramsubst (subst.rs:3171) already do.
754 let n: i64 = match crate::ported::math::mathevali(sv) {
755 Ok(v) => v,
756 Err(msg) => {
757 crate::ported::utils::zerr(&msg);
758 errflag.store(
759 errflag.load(Ordering::Relaxed)
760 | crate::ported::zsh_h::ERRFLAG_ERROR,
761 Ordering::Relaxed,
762 );
763 0 // c:Src/math.c:1509 — error yields 0
764 }
765 };
766 actval = (n - signed_test) != 0;
767 }
768 } else {
769 actval = if presence { !right } else { testval != 0 };
770 }
771 // Skip past the spec char to find the delimiter
772 // (zutil.c:888 `if (!(endcharl = *++s))`).
773 *idx += 1;
774 if *idx >= bytes.len() {
775 return None;
776 }
777 let endcharl = bytes[*idx];
778 *idx += 1;
779 // First branch (true-text) — emit only if actval is true,
780 // i.e. skip = skip || !actval. Wait, C says
781 // `skip || actval` for the FIRST sub-call meaning: if
782 // actval is true SKIP the first branch?
783 // Re-reading zutil.c:880-884 — comment says "Either skip
784 // true text and output false text, or vice versa". The
785 // pattern `skip || actval` for the first call means: if
786 // actval, skip the first text. So the FIRST text
787 // (between `(` and the delim) is the FALSE branch, the
788 // SECOND text (between delim and `)`) is the TRUE.
789 // if (!(s = zformat_substring(s+1, ..., endcharl, ...,
790 // skip || actval)) || !*s)
791 // return NULL;
792 // c:882-884 — a branch that runs off the end of the format
793 // without meeting its delimiter makes the whole format
794 // malformed; the previous port kept going and returned the
795 // partial text, so `zformat -F R '%(..)'` succeeded.
796 ZFormat::substring(
797 bytes, idx, out, endcharl, specs, presence, quote, qspecs, skip || actval,
798 )?;
799 if *idx >= bytes.len() {
800 return None; // c:884
801 }
802 *idx += 1; // c:882 `s+1` — past the delimiter
803 ZFormat::substring(
804 bytes, idx, out, ')', specs, presence, quote, qspecs, skip || !actval,
805 )?;
806 if *idx >= bytes.len() {
807 return None; // c:887
808 }
809 *idx += 1; // c:821 `s++` — past the closing `)`
810 continue;
811 }
812
813 if skip {
814 // In skip mode — advance past spec char and continue.
815 if *idx < bytes.len() {
816 *idx += 1;
817 }
818 continue;
819 }
820
821 // Plain `%X` spec (zutil.c:890-922).
822 if *idx >= bytes.len() {
823 // c:952-964 with `*s == '\0'` — no spec char (`%(` or `%-`
824 // at the end): `len = s - start + 1` copies the sequence up
825 // to the terminator verbatim.
826 for &c in &bytes[start..] {
827 out.push(c);
828 }
829 continue;
830 }
831 {
832 let spec_char = bytes[*idx];
833 *idx += 1;
834 if let Some(spec_val) = specs.get(&spec_char).filter(|_| *idx - 1 != start) {
835 let mut val_chars: Vec<char> = spec_val.chars().collect();
836 // c:908-920 — "the assumption with quoted specs is that the
837 // output will be subject to further % expansion -- adjust
838 // width specifiers so that the result will be correct
839 // *after* that expansion": each doubled `%` inside the
840 // width widens it by one.
841 let (mut min, mut max) = (min, max);
842 if (min > Some(0) || max > Some(0))
843 && qspecs.get(&spec_char).copied().unwrap_or(0) > 0
844 {
845 let mut i = 0usize;
846 while i < val_chars.len() {
847 if val_chars[i] == '%' {
848 let i64i = i as i64;
849 if let Some(m) = min.filter(|&m| m > 0 && i64i < m) {
850 min = Some(m + 1);
851 }
852 if let Some(m) = max.filter(|&m| m > 0 && i64i < m) {
853 max = Some(m + 1);
854 }
855 i += 1;
856 }
857 i += 1;
858 }
859 }
860 let len = val_chars.len() as i64;
861 let len = match max {
862 Some(m) if m >= 0 && len > m => {
863 val_chars.truncate(m as usize);
864 m
865 }
866 _ => len,
867 };
868 let outl = match min {
869 Some(m) if m >= 0 && m > len => m,
870 _ => len,
871 };
872 if len >= outl {
873 for &c in val_chars.iter().take(outl as usize) {
874 out.push(c);
875 }
876 } else {
877 let diff = (outl - len) as usize;
878 if right {
879 for _ in 0..diff {
880 out.push(' ');
881 }
882 for &c in val_chars.iter() {
883 out.push(c);
884 }
885 } else {
886 for &c in val_chars.iter() {
887 out.push(c);
888 }
889 for _ in 0..diff {
890 out.push(' ');
891 }
892 }
893 }
894 } else {
895 // Unknown spec — emit raw segment back
896 // (zutil.c:923-936).
897 for &c in &bytes[start..*idx] {
898 out.push(c);
899 }
900 }
901 }
902 }
903 Some(())
904 }
905} // impl ZFormat
906
907/// Port of `newzstyletable(int size, char const *name)` from Src/Modules/zutil.c:270.
908/// C: `static HashTable newzstyletable(int size, char const *name)` —
909/// `newhashtable` plus the style table's function-pointer slots.
910#[allow(non_snake_case)]
911pub fn newzstyletable(size: i32, name: &str) -> HashTable {
912 // c:270
913 let mut ht = crate::ported::hashtable::newhashtable(size, name); // c:273
914
915 ht.hash = Some(crate::ported::hashtable::hasher); // c:275
916 ht.emptytable = Some(hashtable::emptyhashtable); // c:276
917 ht.filltable = None; // c:277
918 ht.cmpnodes = Some(|a, b| a.cmp(b) as i32); // c:278 strcmp
919 ht.addnode = Some(hashtable::addhashnode); // c:279
920 /* DISABLED is not supported */
921 ht.getnode = Some(hashtable::gethashnode2); // c:281
922 ht.getnode2 = Some(hashtable::gethashnode2); // c:282
923 ht.removenode = Some(hashtable::removehashnode); // c:283
924 ht.disablenode = None; // c:284
925 ht.enablenode = None; // c:285
926 ht.freenode = Some(freestylenode); // c:286
927 ht.printnode = Some(printstylenode); // c:287
928
929 ht // c:289
930}
931
932/// Port of `setstypat(Style s, char *pat, Patprog prog, char **vals, int eval)` from Src/Modules/zutil.c:295.
933/// C: `static int setstypat(Style s, char *pat, Patprog prog,
934/// char **vals, int eval)` — store/replace a (pat, vals) entry on
935/// the Style's pat list. Returns 1 on parse error, 0 on success.
936///
937/// Static-link path routes through style_table::set on the global
938/// zstyletab. The `style_name` arg replaces the C `Style s` since
939/// Rust's style_table is keyed by name. The `prog` (Patprog) arg is
940/// ignored because style_table::set compiles at lookup-time via patmatch.
941#[allow(non_snake_case)]
942/// WARNING: param names don't match C — Rust=(style_name, pat, vals, eval) vs C=(s, pat, prog, vals, eval)
943pub fn setstypat(
944 style_name: &str,
945 pat: &str, // c:295
946 _prog: Option<Patprog>,
947 vals: Vec<String>,
948 eval: i32,
949) -> i32 {
950 // c:304-318 — `-e` (eval) style: parse the joined values into an
951 // Eprog now, so lookup can execute it. C saves/restores errflag
952 // around the parse (keeping any user-interrupt bit); on parse
953 // failure it frees `prog` and returns 1. Here `_prog` drops on the
954 // early return, which is the Rust-idiom equivalent of freepatprog.
955 let eval_prog: Option<Eprog> = if eval != 0 {
956 let joined = crate::ported::utils::zjoin(&vals, ' '); // c:309 zjoin(vals, ' ', 1)
957 match crate::ported::exec::parse_string(&joined, 0) {
958 // c:309
959 None => return 1, // c:311-314 freepatprog(prog); return 1
960 Some(ep) => Some(Box::new(crate::ported::parse::dupeprog(&ep, false))), // c:317
961 }
962 } else {
963 None
964 };
965 if let Ok(mut t) = zstyletab.lock() {
966 t.set(pat, style_name, vals, eval_prog); // c:319 set/replace
967 0
968 } else {
969 1
970 }
971}
972
973/// Port of `addstyle(char *name)` from Src/Modules/zutil.c:403.
974/// C: `static Style addstyle(char *name)` — alloc a new Style node and
975/// install in zstyletab.
976#[allow(non_snake_case)]
977/// C body (3 lines):
978/// `Style s = (Style) zshcalloc(sizeof(*s));
979/// zstyletab->addnode(zstyletab, ztrdup(name), s);
980/// return s;`
981pub fn addstyle(name: &str) -> Option<Style> {
982 // c:403
983 Some(Box::new(style {
984 // c:405 zshcalloc + return
985 node: hashnode {
986 next: None,
987 nam: name.to_string(),
988 flags: 0,
989 },
990 pats: None,
991 }))
992 // c:407 addnode — zstyletab integration is the caller's job; the
993 // Box is returned for them to install.
994}
995
996/// Port of `evalstyle(Stypat p)` from Src/Modules/zutil.c:413.
997/// Runs `p.eval` and reads `$reply` (array first, falling back to
998/// scalar form). Returns empty Vec on error or unset.
999/// `code` is the joined eval body (the values an `-e` style was
1000/// registered with, e.g. `reply=(computed-$((1+1)))`). C runs the
1001/// pre-parsed `p->eval` Eprog; zshrs re-runs the stored source through
1002/// the live executor, which is equivalent for setting `$reply`.
1003pub fn evalstyle(code: &str) -> Vec<String> {
1004 // c:413
1005
1006 // c:415 — int ef = errflag;
1007 let ef = errflag.load(Ordering::Relaxed);
1008 // c:418 — unsetparam("reply");
1009 unsetparam("reply");
1010 // c:419 — execode(p->eval, 1, 0, "style"): execute the style body so
1011 // it can set `$reply`. Runs on the live session executor.
1012 {
1013 // c:Src/Modules/zutil.c:419 — `execode(p->eval, 1, 0, "style");`.
1014 // A `zstyle -e` body runs with `style` appended to the eval context.
1015 // Popped on every return path. Bug #1069.
1016 let sync_eval_ctx = |stack: &[String]| {
1017 let joined = stack.join(":");
1018 if let Ok(mut tab) = crate::ported::params::paramtab().write() {
1019 if let Some(pm) = tab.get_mut("zsh_eval_context") {
1020 pm.u_arr = Some(stack.to_vec());
1021 pm.node.flags &= !(crate::ported::zsh_h::PM_UNSET as i32);
1022 }
1023 if let Some(pm) = tab.get_mut("ZSH_EVAL_CONTEXT") {
1024 pm.u_str = Some(joined);
1025 pm.node.flags &= !(crate::ported::zsh_h::PM_UNSET as i32);
1026 }
1027 }
1028 };
1029 if let Ok(mut ctx) = crate::ported::exec::zsh_eval_context.lock() {
1030 ctx.push("style".to_string());
1031 sync_eval_ctx(&ctx);
1032 }
1033 struct StyleCtxGuard<F: Fn(&[String])>(F);
1034 impl<F: Fn(&[String])> Drop for StyleCtxGuard<F> {
1035 fn drop(&mut self) {
1036 if let Ok(mut ctx) = crate::ported::exec::zsh_eval_context.lock() {
1037 ctx.pop();
1038 (self.0)(&ctx);
1039 }
1040 }
1041 }
1042 let _ctx_guard = StyleCtxGuard(sync_eval_ctx);
1043 let _ = crate::ported::exec::execute_script_zsh_pipeline(code);
1044 }
1045 // c:420-425 — restore errflag preserving INT bit only.
1046 let cur = errflag.load(Ordering::Relaxed);
1047 errflag.store(ef | (cur & ERRFLAG_INT), Ordering::Relaxed);
1048 if (cur & !ERRFLAG_INT) != 0 {
1049 return Vec::new(); // c:423
1050 }
1051 // c:427-433 — `if ((ret = getaparam("reply"))) ret = arrdup(ret);
1052 // else if ((str = getsparam("reply"))) ret = [str];`
1053 queue_signals();
1054 let ret = if let Some(arr) = getaparam("reply") {
1055 arr
1056 } else if let Some(s) = getsparam("reply") {
1057 vec![s]
1058 } else {
1059 Vec::new()
1060 };
1061 unqueue_signals();
1062 // c:435 — unsetparam("reply");
1063 unsetparam("reply");
1064 ret
1065}
1066
1067/// Port of `lookupstyle(char *ctxt, char *style)` from Src/Modules/zutil.c:443.
1068/// C: `static char **lookupstyle(char *ctxt, char *style)` — find best
1069/// pat-style match against the style entry; return its vals.
1070#[allow(non_snake_case)]
1071pub fn lookupstyle(ctxt: &str, style: &str) -> Vec<String> {
1072 // c:443
1073 // c:443-463 — zstyletab->getnode2 + savematch/pattry/restorematch
1074 // loop. style_table::get_match() encapsulates the pat-walk and also
1075 // reports whether the matched entry is an `-e` (eval) style; weight
1076 // order is enforced at insert time so first-match wins.
1077 //
1078 // c:450-451 + c:459 — the pat-walk is bracketed by
1079 // `savematch(&match)` / `restorematch(&match)`. That bracket is NOT
1080 // cosmetic: `restorematch` UNSETS `$match`/`$mbegin`/`$mend`
1081 // whenever the snapshot came back NULL (zutil.c:57-68), and
1082 // `savematch` reads them with `getaparam`, which returns NULL for a
1083 // non-array. So a plain `local match` (a scalar — `_main_complete`
1084 // declares exactly that at its sh:27 `local` line) is UNSET by the
1085 // first zstyle lookup in zsh. Without the bracket zshrs left the
1086 // scalar in place and `$parameters` reported one name zsh does not
1087 // have.
1088 // c:449-450 — `s = (Style)zstyletab->getnode2(zstyletab, style);
1089 // if (s) {`: the pattern walk AND its savematch/restorematch bracket
1090 // run ONLY when a style of that NAME exists. Bracketing every lookup
1091 // unconditionally unset `$match`/`$mbegin`/`$mend` (c:57-68) even
1092 // where C returns immediately — with no zstyles defined, C never
1093 // touches them, so `_main_complete`'s sh:27 `local match` scalar
1094 // survives the whole completion in zsh and was disappearing here.
1095 let style_exists = zstyletab
1096 .lock()
1097 .map(|t| t.styles.contains_key(style))
1098 .unwrap_or(false);
1099 if !style_exists {
1100 return Vec::new(); // c:447 `found = NULL` → c:461 `return found`
1101 }
1102 let mut saved = MatchData {
1103 r#match: None,
1104 mbegin: None,
1105 mend: None,
1106 };
1107 savematch(&mut saved); // c:450
1108 let matched = match zstyletab.lock() {
1109 Ok(t) => t.get_match(ctxt, style), // (vals, is_eval)
1110 Err(_) => None,
1111 };
1112 // Lock released before evalstyle so the body can touch zstyle/params
1113 // without re-entering the table lock.
1114 let found = match matched {
1115 // c:455-456 — `found = (p->eval ? evalstyle(p) : p->vals);`
1116 // C runs evalstyle INSIDE the loop, i.e. BEFORE restorematch, so
1117 // the `-e` body still sees the match vars the pattern set.
1118 Some((vals, true)) => evalstyle(&crate::ported::utils::zjoin(&vals, ' ')),
1119 Some((vals, false)) => vals,
1120 None => Vec::new(),
1121 };
1122 restorematch(&saved); // c:459
1123 found
1124}
1125
1126// =====================================================================
1127// static struct features module_features c:2143
1128// =====================================================================
1129
1130/// Port of `testforstyle(char *ctxt, char *style)` from Src/Modules/zutil.c:465.
1131/// C: `static int testforstyle(char *ctxt, char *style)` — non-empty
1132/// match check for context+style. Returns `!found` so 0 == success.
1133#[allow(non_snake_case)]
1134pub fn testforstyle(ctxt: &str, style: &str) -> i32 {
1135 // c:465
1136 // c:465-484 — zstyletab lookup + pattern match against ctxt,
1137 // bracketed by savematch/restorematch exactly as `lookupstyle` is
1138 // (c:473 / c:481). See the note there for why the bracket is
1139 // load-bearing rather than cosmetic.
1140 // c:471-472 — same `if (s)` gate as lookupstyle above.
1141 let style_exists = zstyletab
1142 .lock()
1143 .map(|t| t.styles.contains_key(style))
1144 .unwrap_or(false);
1145 if !style_exists {
1146 return 1; // c:483 `return !found` with found == 0
1147 }
1148 let mut saved = MatchData {
1149 r#match: None,
1150 mbegin: None,
1151 mend: None,
1152 };
1153 savematch(&mut saved); // c:473
1154 let found = match zstyletab.lock() {
1155 // c:471
1156 Ok(t) => t.get(ctxt, style).is_some(), // c:476 pattry
1157 Err(_) => false,
1158 };
1159 restorematch(&saved); // c:481
1160 if found {
1161 0
1162 } else {
1163 1
1164 } // c:485 return !found
1165}
1166
1167/// Direct port of `bin_zstyle(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))` from `Src/Modules/zutil.c:487`.
1168/// C body (c:490-952): switch over -L/-l/-d/-s/-b/-t/-T/-m/-a/-g/-e
1169/// flags + per-mode handlers.
1170///
1171/// The per-context lookups (-s/-b/-t/-T/-m/-a/-g/-e) go through
1172/// `lookupstyle`/`testforstyle` over the global `zstyletab`.
1173/// WARNING: param names don't match C — Rust=(nam, args, _func) vs C=(nam, args, ops, func)
1174pub fn bin_zstyle(
1175 nam: &str,
1176 args: &[String], // c:487
1177 _ops: &options,
1178 _func: i32,
1179) -> i32 {
1180 // c:Src/Modules/zutil.c:487 — bin_zstyle parses args[0] directly
1181 // (BUILTIN spec has NULL optstr at c:2139) so the dispatch order
1182 // and unknown-flag diagnostic match zsh exactly. Build a local
1183 // `options` struct here, mirroring what execbuiltin's option
1184 // parser would have produced if there had been an optstr — then
1185 // the existing OPT_ISSET-driven flag arms below run unchanged.
1186 //
1187 // C flow at c:491-512 + c:587-600:
1188 // - !args[0] → bare list mode
1189 // - args[0] == "-" or "--" → lone dash; positional follows
1190 // - args[0] starts with -X + extra → "invalid argument: %s"
1191 // - args[0] == "-L" → list mode (ZSLIST_SYNTAX)
1192 // - args[0] == "-e" → eval+add (handled like setstyle)
1193 // - args[0] == "-d|-s|-b|-a|-t|-T|-m|-g" → action mode (positional follows)
1194 // - else any other "-X" → "invalid option: -X" rc=1
1195 let mut ops_local = options {
1196 ind: [0u8; MAX_OPS],
1197 args: Vec::new(),
1198 argscount: 0,
1199 argsalloc: 0,
1200 };
1201 let mut positional_start = 0;
1202 if let Some(first) = args.first() {
1203 let fb = first.as_bytes();
1204 if fb.first() == Some(&b'-') && fb.len() >= 2 && fb[1] != b'-' {
1205 if fb.len() > 2 {
1206 // c:497-499 — extra chars after the option letter
1207 // (e.g. `-Lx`) → "invalid argument: -Lx" rc=1.
1208 crate::ported::utils::zwarnnam(nam, &format!("invalid argument: {}", first));
1209 return 1;
1210 }
1211 let oc = fb[1];
1212 if matches!(
1213 oc,
1214 b'L'
1215 | b'e'
1216 | b'd'
1217 | b's'
1218 | b'b'
1219 | b'a'
1220 | b't'
1221 | b'T'
1222 | b'm'
1223 | b'q'
1224 | b'g'
1225 ) {
1226 ops_local.ind[oc as usize] = 1;
1227 positional_start = 1;
1228 } else {
1229 // c:597-599 default arm — "bad option: -X".
1230 crate::ported::utils::zwarnnam(nam, &format!("bad option: {}", first));
1231 return 1;
1232 }
1233 } else if fb == b"-" || fb == b"--" {
1234 // c:508-511 — lone `-` or `--` with no following positional
1235 // is "not enough arguments" (the lone dash implies set-style
1236 // mode which requires ≥2 positionals).
1237 if args.len() == 1 {
1238 crate::ported::utils::zwarnnam(nam, "not enough arguments");
1239 return 1;
1240 }
1241 positional_start = 1;
1242 }
1243 }
1244 let args: &[String] = &args[positional_start..];
1245 let ops: &options = &ops_local;
1246 // c:Src/Modules/zutil.c:588-604 — min-args check per action flag.
1247 // After the args[0] option is consumed, the remaining positionals
1248 // must satisfy each action's min count or "not enough arguments"
1249 // fires. Without this gate `zstyle -g`, `-s`, `-t`, `-T` (etc.)
1250 // silently returned rc=1 instead of emitting the canonical
1251 // diagnostic.
1252 let min_args = if OPT_ISSET(ops, b'd') {
1253 0
1254 } else if OPT_ISSET(ops, b's')
1255 || OPT_ISSET(ops, b'b')
1256 || OPT_ISSET(ops, b'a')
1257 || OPT_ISSET(ops, b'm')
1258 {
1259 3
1260 } else if OPT_ISSET(ops, b't') || OPT_ISSET(ops, b'T') || OPT_ISSET(ops, b'q') {
1261 // c:592-595 — t/T min 2; q min 2 max 2
1262 2
1263 } else if OPT_ISSET(ops, b'g') {
1264 1
1265 } else {
1266 0 // L/e or no flag — no min check at this layer
1267 };
1268 if args.len() < min_args {
1269 crate::ported::utils::zwarnnam(nam, "not enough arguments");
1270 return 1;
1271 }
1272 // c:565-573 — the same switch's `max`: s 4, b/a/m 3, g 3, q 2
1273 // (d/t/T unbounded). c:582-584 `else if (max >= 0 && n > max)
1274 // { zwarnnam(nam, "too many arguments"); return 1; }`.
1275 let max_args = if OPT_ISSET(ops, b'd') || OPT_ISSET(ops, b't') || OPT_ISSET(ops, b'T') {
1276 None
1277 } else if OPT_ISSET(ops, b's') {
1278 Some(4)
1279 } else if OPT_ISSET(ops, b'b') || OPT_ISSET(ops, b'a') || OPT_ISSET(ops, b'm') {
1280 Some(3)
1281 } else if OPT_ISSET(ops, b'g') {
1282 Some(3)
1283 } else if OPT_ISSET(ops, b'q') {
1284 Some(2)
1285 } else if OPT_ISSET(ops, b'L') {
1286 // c:539-555 — `-L [context [style]]`: three or more args is
1287 // "too many arguments".
1288 Some(2)
1289 } else {
1290 None
1291 };
1292 if max_args.is_some_and(|m| args.len() > m) {
1293 crate::ported::utils::zwarnnam(nam, "too many arguments");
1294 return 1;
1295 }
1296 // c:491-492 — C reaches the bare-list arm only when `!args[0]`, i.e.
1297 // NO argument at all was given. `positional_start == 1` means an
1298 // option letter was consumed off args[0], so this is `zstyle -X`
1299 // (with X's own positionals exhausted), NOT a bare `zstyle`.
1300 // Without this gate, `zstyle -d` (delete everything, c:639-640
1301 // `zstyletab->emptytable`) fell into the listing arm and returned 0
1302 // having deleted nothing — every V05styles chunk after the
1303 // `zstyle -d` in chunk 1 then saw the leftover styles.
1304 if args.is_empty()
1305 && positional_start == 0
1306 && !OPT_ISSET(ops, b'L')
1307 && !OPT_ISSET(ops, b'e')
1308 {
1309 // c:491-492 + c:580-581 — bare `zstyle` invocation:
1310 // `list = ZSLIST_BASIC; scanhashtable(zstyletab, ..., printstylenode, list);`
1311 //
1312 // Route through printstylenode (zutil.rs:184 port) so the
1313 // (eval) prefix per pattern lands consistently. Prior bare-list
1314 // implementation duplicated the format inline with the eval-bit
1315 // hardcoded off — eval styles printed identical to literal
1316 // styles under bare `zstyle`.
1317 let names: Vec<String> = match zstyletab.lock() {
1318 Ok(t) => t.styles.keys().cloned().collect(),
1319 Err(_) => return 1,
1320 };
1321 let mut sorted = names;
1322 sorted.sort();
1323 ZSTYLE_CONTPROG.with(|c| *c.borrow_mut() = None); // c:572 zstyle_contprog = NULL
1324 for nam in sorted {
1325 // c:580 — scanhashtable callback dispatch per style.
1326 let hn: HashNode = Box::new(hashnode {
1327 next: None,
1328 nam,
1329 flags: 0,
1330 });
1331 printstylenode(&hn, 1); // c:580-581 — ZSLIST_BASIC
1332 }
1333 return 0; // c:585
1334 }
1335 if OPT_ISSET(ops, b'L') {
1336 // c:544-583 — `zstyle -L [context [stylename]]`: list = ZSLIST_SYNTAX,
1337 // optionally filtered by a context pattern and/or an exact style name.
1338 // args[0] = context (glob matched against each stored pattern),
1339 // args[1] = stylename (only that style is listed; error if absent).
1340 let context = args.first().map(|s| s.as_str()); // c:551/556 context
1341 let stylename = args.get(1).map(|s| s.as_str()); // c:552 stylename
1342 // c:562-570 — validate the context pattern up front (invalid → rc 1).
1343 if let Some(c) = context {
1344 let mut pat = c.to_string();
1345 crate::ported::glob::tokenize(&mut pat); // c:563 tokenize(context)
1346 let prog = patcompile(&pat, crate::ported::zsh_h::PAT_STATIC, None); // c:564
1347 if prog.is_none() {
1348 return 1; // c:566-568
1349 }
1350 ZSTYLE_CONTPROG.with(|cp| *cp.borrow_mut() = prog);
1351 } else {
1352 ZSTYLE_CONTPROG.with(|cp| *cp.borrow_mut() = None); // c:572
1353 }
1354 // c:573-582 — a named style lists just that node (error if it does
1355 // not exist); otherwise scan every style.
1356 let names: Vec<String> = if let Some(sn) = stylename {
1357 let exists = zstyletab
1358 .lock()
1359 .map(|t| t.styles.contains_key(sn))
1360 .unwrap_or(false);
1361 if !exists {
1362 return 1; // c:575-577 — `if (!s) return 1;`
1363 }
1364 vec![sn.to_string()]
1365 } else {
1366 match zstyletab.lock() {
1367 Ok(t) => {
1368 let mut v: Vec<String> = t.styles.keys().cloned().collect();
1369 v.sort();
1370 v
1371 }
1372 Err(_) => return 1,
1373 }
1374 };
1375 for nam in names {
1376 let hn: HashNode = Box::new(hashnode {
1377 next: None,
1378 nam,
1379 flags: 0,
1380 });
1381 printstylenode(&hn, 2); // c:501 — ZSLIST_SYNTAX
1382 }
1383 return 0; // c:585
1384 }
1385 if OPT_ISSET(ops, b'd') {
1386 // c:610-641 — three -d forms:
1387 //
1388 // if (args[1]) {
1389 // if (args[2]) {
1390 // char *pat = args[1];
1391 // for (args += 2; *args; args++) { ... freestypat per style ... }
1392 // } else {
1393 // zstyle_patname = args[1];
1394 // scanhashtable(zstyletab, 0, 0, 0, scanpatstyles, ZSPAT_REMOVE);
1395 // }
1396 // } else
1397 // zstyletab->emptytable(zstyletab);
1398 //
1399 // Form 1 takes pattern + MULTIPLE style names (the c:618 loop
1400 // walks args+2 to the end). Prior port read only args[1] —
1401 // `zstyle -d pat sty1 sty2 sty3` silently dropped sty2/sty3.
1402 let pat = args.first().map(|s| s.as_str());
1403 if let Ok(mut t) = zstyletab.lock() {
1404 if args.len() > 1 {
1405 // c:615-631 — per-style deletion of one pattern.
1406 for sty in &args[1..] {
1407 t.delete(pat, Some(sty.as_str())); // c:626 freestypat
1408 }
1409 } else {
1410 // c:632-638 pattern-only / c:639-640 wipe-all.
1411 t.delete(pat, None);
1412 }
1413 }
1414 return 0; // c:524
1415 }
1416 // c:541-942 — -s/-b/-t/-T/-m/-a/-e per-context lookup arms.
1417 // -g has different arg layout (args[0] = output name, not context)
1418 // so it gets its own block below.
1419 if OPT_ISSET(ops, b's')
1420 || OPT_ISSET(ops, b'b')
1421 || OPT_ISSET(ops, b't')
1422 || OPT_ISSET(ops, b'T')
1423 || OPT_ISSET(ops, b'a')
1424 || OPT_ISSET(ops, b'm')
1425 || OPT_ISSET(ops, b'q')
1426 {
1427 if args.len() < 2 {
1428 return 1;
1429 }
1430 let ctxt = &args[0]; // c:541
1431 let style = &args[1];
1432 // c:749-757 — `case 'q': {
1433 // int success;
1434 // queue_signals(); /* Protect PAT_STATIC */
1435 // success = testforstyle(args[1], args[2]);
1436 // unqueue_signals();
1437 // return success;
1438 // }`
1439 // Prior port had no -q arm at all — the option-letter matcher
1440 // rejected `zstyle -q ctx sty` with "invalid option: -q".
1441 if OPT_ISSET(ops, b'q') {
1442 queue_signals(); // c:752
1443 let success = testforstyle(ctxt, style); // c:753
1444 unqueue_signals(); // c:754
1445 return success; // c:755
1446 }
1447 let vals = lookupstyle(ctxt, style); // c:443
1448 // c:559-732 — per-flag return semantics: just check found vs not.
1449 // For -t: 0 if found AND first value matches one of the "true"
1450 // tokens (when arg given) or first ∈ {true,yes,on,1}.
1451 if OPT_ISSET(ops, b't') || OPT_ISSET(ops, b'T') {
1452 // c:700-724 — shared t/T arm:
1453 //
1454 // if ((vals = lookupstyle(args[1], args[2])) && vals[0]) {
1455 // if (args[3]) {
1456 // char **ap = args + 3, **p;
1457 // while (*ap) {
1458 // p = vals;
1459 // while (*p)
1460 // if (!strcmp(*ap, *p++))
1461 // return 0;
1462 // ap++;
1463 // }
1464 // return 1;
1465 // } else
1466 // return !(!strcmp(vals[0], "true") ||
1467 // !strcmp(vals[0], "yes") ||
1468 // !strcmp(vals[0], "on") ||
1469 // !strcmp(vals[0], "1"));
1470 // }
1471 // return (args[0][1] == 't' ? (vals ? 1 : 2) : 0);
1472 //
1473 // Two contracts the prior arms missed:
1474 // 1. Extra args = value-membership test: exit 0 when ANY
1475 // style value string-equals ANY extra arg. Both arms
1476 // ignored args[3..] entirely.
1477 // 2. -t's tri-state exit: 2 when the style is UNDEFINED
1478 // for the context, 1 when defined-but-empty (or
1479 // non-boolean first value). Prior -t collapsed both
1480 // to 1.
1481 if !vals.is_empty() {
1482 // c:706 vals && vals[0]
1483 if args.len() > 2 {
1484 // c:707-717
1485 for ap in &args[2..] {
1486 if vals.iter().any(|v| v == ap) {
1487 return 0; // c:714
1488 }
1489 }
1490 return 1; // c:717
1491 }
1492 // c:719-722 — boolean first value.
1493 return if matches!(vals[0].as_str(), "true" | "yes" | "on" | "1") {
1494 0
1495 } else {
1496 1
1497 };
1498 }
1499 // c:724 — `return (args[0][1] == 't' ? (vals ? 1 : 2) : 0);`
1500 // vals here is the C pointer: non-NULL when a pattern
1501 // matched but carried zero values. Probe the table for the
1502 // defined-but-empty vs undefined distinction.
1503 if OPT_ISSET(ops, b't') {
1504 let defined = match zstyletab.lock() {
1505 Ok(t) => t.get(ctxt, style).is_some(),
1506 Err(_) => false,
1507 };
1508 return if defined { 1 } else { 2 }; // c:724
1509 }
1510 return 0; // c:724 -T arm
1511 }
1512 // -m PATTERN: pattern-match args[2] against each value, return
1513 // 0 if any matches. C: zutil.c:727-747.
1514 if OPT_ISSET(ops, b'm') {
1515 // c:727
1516 if args.len() < 3 {
1517 return 1;
1518 }
1519 queue_signals(); // c:732 — Protect PAT_STATIC
1520 let mut pat = args[2].clone();
1521 tokenize(&mut pat); // c:734 — shell metachar → pattern token
1522 let prog = match patcompile(&pat, PAT_STATIC, None) {
1523 // c:737
1524 Some(p) => p,
1525 None => {
1526 unqueue_signals(); // c:745
1527 return 1;
1528 }
1529 };
1530 for v in &vals {
1531 // c:738
1532 if pattry(&prog, v) {
1533 // c:739
1534 unqueue_signals(); // c:740
1535 return 0; // c:741
1536 }
1537 }
1538 unqueue_signals(); // c:745
1539 return 1; // c:746
1540 }
1541 // -s CONTEXT STYLE NAME [SEP]: join vals with SEP (default " "),
1542 // setsparam(NAME, joined). Return 0 if found else 1 (empty str).
1543 // C: zutil.c:643-658.
1544 if OPT_ISSET(ops, b's') {
1545 // c:643
1546 if args.len() < 3 {
1547 return 1;
1548 }
1549 let pname = &args[2];
1550 if !vals.is_empty() {
1551 let sep = args.get(3).map(|s| s.as_str()).unwrap_or(" "); // c:649
1552 let ret = vals.join(sep);
1553 setsparam(pname, &ret);
1554 return 0; // c:650
1555 }
1556 setsparam(pname, ""); // c:652
1557 return 1; // c:653
1558 }
1559 // -b CONTEXT STYLE NAME: coerce single bool-ish val to "yes"/"no".
1560 // C: zutil.c:660-680.
1561 if OPT_ISSET(ops, b'b') {
1562 // c:660
1563 if args.len() < 3 {
1564 return 1;
1565 }
1566 let pname = &args[2];
1567 let truthy = vals.len() == 1 // c:665-670
1568 && matches!(vals[0].as_str(),
1569 "yes" | "true" | "on" | "1");
1570 let (ret, code) = if truthy { ("yes", 0) } else { ("no", 1) };
1571 setsparam(pname, ret); // c:677
1572 return code; // c:672/675
1573 }
1574 // -a CONTEXT STYLE NAME: setaparam(NAME, vals).
1575 // C: zutil.c:682-699:
1576 //
1577 // if ((vals = lookupstyle(args[1], args[2]))) {
1578 // ret = zarrdup(vals);
1579 // val = 0;
1580 // } else {
1581 // char *dummy = NULL;
1582 // ret = zarrdup(&dummy);
1583 // val = 1;
1584 // }
1585 // setaparam(args[3], ret);
1586 //
1587 // Exit code keys on the lookupstyle POINTER, not vals[0]: a
1588 // pattern that matched with ZERO values still exits 0 (array
1589 // set empty). Rust lookupstyle collapses NULL and empty to
1590 // one Vec, so probe the table for the defined/undefined
1591 // distinction — same shape as the -t tri-state fix.
1592 if OPT_ISSET(ops, b'a') {
1593 // c:682
1594 if args.len() < 3 {
1595 return 1;
1596 }
1597 let pname = &args[2];
1598 let defined = match zstyletab.lock() {
1599 Ok(t) => t.get(ctxt, style).is_some(), // c:687 vals != NULL
1600 Err(_) => false,
1601 };
1602 // c:696 — `setaparam(args[3], ret)`. C's setaparam routes a
1603 // PM_HASHED (associative) target through the hashed-assign
1604 // path, treating the flat value list as alternating
1605 // key/value pairs. zshrs's setaparam clobbers the assoc into
1606 // an indexed array, so detect a PM_HASHED target and use
1607 // sethparam (key/val interleave) instead — `typeset -A h;
1608 // zstyle -a ctx style h` now populates h's keys.
1609 let is_assoc = crate::ported::params::paramtab()
1610 .read()
1611 .ok()
1612 .and_then(|t| {
1613 t.get(pname.as_str())
1614 .map(|p| (p.node.flags as u32 & crate::ported::zsh_h::PM_HASHED) != 0)
1615 })
1616 .unwrap_or(false);
1617 if is_assoc {
1618 sethparam(pname, vals); // c:696 (assoc: key/val pairs)
1619 } else {
1620 setaparam(pname, vals); // c:696 (empty when undefined)
1621 }
1622 return if defined { 0 } else { 1 }; // c:689/694
1623 }
1624 // -g: handled below (different arg layout).
1625 // -e: NOT a per-context lookup arm. C c:504-507 routes -e
1626 // through the add path (eval = add = 1), handled in the
1627 // canonical setstypat block below.
1628 if vals.is_empty() {
1629 return 1;
1630 }
1631 return 0;
1632 }
1633 // -g NAME [PATTERN [STYLE]]: collect into array NAME.
1634 // C: zutil.c:758-795. Distinct arg layout: args[0]=NAME (not ctxt).
1635 if OPT_ISSET(ops, b'g') {
1636 // c:758
1637 if args.is_empty() {
1638 return 1;
1639 }
1640 let pname = &args[0]; // c:792 args[1]→args[0]
1641 let pat_arg = args.get(1).map(|s| s.as_str()); // c:766
1642 let sty_arg = args.get(2).map(|s| s.as_str()); // c:767
1643 let mut out: Vec<String> = Vec::new();
1644 let t = match zstyletab.lock() {
1645 Ok(g) => g,
1646 Err(_) => return 1,
1647 };
1648 // c:759 — `int ret = 1;`. Only the exact-pattern lookup can fail
1649 // (return 1); the two listing branches always succeed (ret = 0).
1650 let mut ret = 1;
1651 match (pat_arg, sty_arg) {
1652 (None, _) => {
1653 // Collect distinct context patterns. c:788
1654 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
1655 for (p, _s, _v) in t.list(None) {
1656 if seen.insert(p.clone()) {
1657 out.push(p);
1658 }
1659 }
1660 ret = 0; // c:789
1661 }
1662 (Some(pat), None) => {
1663 // Collect style names attached to context = pat. c:783
1664 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
1665 for (p, s, _v) in t.list(None) {
1666 if p == pat && seen.insert(s.clone()) {
1667 out.push(s);
1668 }
1669 }
1670 ret = 0; // c:784
1671 }
1672 (Some(pat), Some(sty)) => {
1673 // c:768-779 — `-g` matches the pattern EXACTLY (strcmp),
1674 // NOT via pattry: it retrieves the value stored for that
1675 // precise pattern key, not the value a context would
1676 // resolve to. Use get_exact, not get (pattry). c:770-777:
1677 // ret stays 1 unless the exact pattern is found.
1678 if let Some(v) = t.get_exact(pat, sty) {
1679 out.extend(v.iter().cloned());
1680 ret = 0; // c:776
1681 }
1682 }
1683 }
1684 drop(t);
1685 setaparam(pname, out); // c:792
1686 return ret; // c:793
1687 }
1688
1689 // c:515-534 — add path: zstyle [-e] PATTERN STYLE [VALUES...]
1690 if args.len() < 2 {
1691 zwarnnam(nam, "not enough arguments"); // c:521
1692 return 1;
1693 }
1694 let ctxt = &args[0]; // c:524
1695 let style = &args[1];
1696 let values: Vec<String> = if args.len() >= 3 {
1697 args[2..].to_vec() // c:533 args+2
1698 } else {
1699 Vec::new()
1700 };
1701 // c:524-530 — tokenize + patcompile validation. Reject invalid
1702 // patterns with the canonical "invalid pattern: %s" diagnostic
1703 // before they reach the style table.
1704 {
1705 let mut pat = ctxt.clone(); // c:524 dupstring
1706 tokenize(&mut pat); // c:525
1707 if patcompile(&pat, crate::ported::zsh_h::PAT_ZDUP, None).is_none() {
1708 // c:527
1709 zwarnnam(nam, &format!("invalid pattern: {}", ctxt)); // c:528
1710 return 1; // c:529
1711 }
1712 }
1713 let eval = OPT_ISSET(ops, b'e'); // c:505 eval = add = 1
1714 // c:533 — `setstypat(s, pat, prog, args + 2, eval)`. Route through
1715 // setstypat (which parses the `-e` value and locks zstyletab itself)
1716 // rather than locking + `t.set` here, matching C and avoiding a
1717 // re-entrant lock on the non-reentrant zstyletab mutex.
1718 setstypat(style, ctxt, None, values.clone(), eval as i32); // c:533
1719 // PFA-SMR: one event per zstyle call. `rest` carries the style
1720 // name + values so replay can re-emit the full setter.
1721 #[cfg(feature = "recorder")]
1722 if crate::recorder::is_enabled() {
1723 let ctx = crate::recorder::recorder_ctx_global();
1724 let rest = if values.is_empty() {
1725 style.clone()
1726 } else {
1727 format!("{} {}", style, values.join(" "))
1728 };
1729 crate::recorder::emit_zstyle(ctxt, &rest, ctx);
1730 }
1731 0 // c:951
1732}
1733
1734/// Port of `bin_zformat(char *nam, char **args, Options ops, UNUSED(int func))` from `Src/Modules/zutil.c:984`.
1735/// BUILTIN spec at c:2151 is `BUILTIN("zformat", 0, bin_zformat, 2, -1,
1736/// 0, "afFqQ", NULL)`: execbuiltin parses `-a`/`-f`/`-F` (mode) and
1737/// `-q`/`-Q` (auto-quote `%` in spec values) into `ops`, so `args[0]` is
1738/// the target parameter and `args[1]` the format / separator.
1739pub fn bin_zformat(
1740 nam: &str,
1741 args: &[String], // c:984
1742 ops: &options,
1743 _func: i32,
1744) -> i32 {
1745 // c:986 — `qopt = OPT_ISSET(ops,'q') ? 'q' : OPT_ISSET(ops,'Q') ? 'Q' : 0`
1746 let qopt: u8 = if OPT_ISSET(ops, b'q') {
1747 b'q'
1748 } else if OPT_ISSET(ops, b'Q') {
1749 b'Q'
1750 } else {
1751 0
1752 };
1753 let mut presence = false; // c:987
1754
1755 if OPT_ISSET(ops, b'q') && OPT_ISSET(ops, b'Q') {
1756 zwarnnam(nam, "only one of -qQ allowed"); // c:990
1757 return 1;
1758 }
1759 let modes = OPT_ISSET(ops, b'a') as i32 + OPT_ISSET(ops, b'f') as i32 + OPT_ISSET(ops, b'F') as i32;
1760 if modes < 1 {
1761 zwarnnam(nam, "one of -afF expected"); // c:994
1762 return 1;
1763 }
1764 if modes > 1 {
1765 zwarnnam(nam, "only one of -afF allowed"); // c:998
1766 return 1;
1767 }
1768 if OPT_ISSET(ops, b'a') && qopt != 0 {
1769 zwarnnam(nam, "-qQ not allowed with -a"); // c:1002
1770 return 1;
1771 }
1772
1773 // c:1006 — `switch (OPT_ISSET(ops,'a') ? 'a' : OPT_ISSET(ops,'f') ? 'f' : 'F')`
1774 let opt: u8 = if OPT_ISSET(ops, b'a') {
1775 b'a'
1776 } else if OPT_ISSET(ops, b'f') {
1777 b'f'
1778 } else {
1779 b'F'
1780 };
1781
1782 match opt {
1783 b'F' | b'f' => {
1784 if opt == b'F' {
1785 presence = true; // c:1008 (fall-through into 'f')
1786 }
1787 // c:1012-1014 — `char *specs[256] = {0}; int qspecs[256] = {0};`
1788 // zero-initialised: `%` and `)` are never specs (upstream
1789 // a04c944804 dropped the old seeding; `%%`/`%)` are handled by
1790 // the unwind in zformat_substring instead).
1791 let mut specs: HashMap<char, String> = HashMap::new();
1792 let mut qspecs: HashMap<char, usize> = HashMap::new();
1793 for ap in &args[2..] {
1794 // c:1017 — "quote by default (spec like d:...) with -qQ"
1795 let mut quote = qopt != 0;
1796 let mut arg: &str = ap;
1797 if arg.starts_with("%%") {
1798 // c:1021-1023 — `%%d:...` explicitly disables quoting
1799 quote = false;
1800 arg = &arg[2..];
1801 } else if arg.starts_with('%') {
1802 // c:1025-1027 — `%d:...` explicitly enables quoting
1803 quote = true;
1804 arg = &arg[1..];
1805 }
1806 let ab = arg.as_bytes();
1807 if ab.is_empty()
1808 || ab[0] == b'-'
1809 || ab[0] == b'.'
1810 || ab[0] == b'%'
1811 || ab[0] == b')'
1812 || ab[0].is_ascii_digit()
1813 || ab.len() < 2
1814 || ab[1] != b':'
1815 {
1816 zwarnnam(nam, &format!("invalid spec: {}", ap)); // c:1033
1817 return 1;
1818 }
1819 let key = ab[0] as char;
1820 let val = &arg[2..];
1821 if quote {
1822 // c:1037-1058 — double every `%` and remember how many
1823 // were added, for the width/presence corrections.
1824 let pct = val.matches('%').count();
1825 specs.insert(key, val.replace('%', "%%"));
1826 qspecs.insert(key, pct);
1827 } else {
1828 // c:1060 — qspecs[c] is left as an earlier `-q` spec of
1829 // the same letter set it.
1830 specs.insert(key, val.to_string());
1831 }
1832 }
1833
1834 // c:1064-1069 — `if (!zformat_substring(args[1], specs, &out,
1835 // &oused, &olen, '\0', presence, qopt == 'q', qspecs, 0))`
1836 let chars: Vec<char> = args[1].chars().collect();
1837 let mut out = String::with_capacity(chars.len() + 16);
1838 let mut idx = 0;
1839 if ZFormat::substring(
1840 &chars,
1841 &mut idx,
1842 &mut out,
1843 '\0',
1844 &specs,
1845 presence,
1846 qopt == b'q',
1847 &qspecs,
1848 false,
1849 )
1850 .is_none()
1851 {
1852 zwarnnam(nam, &format!("malformed format string: {}", args[1])); // c:1071
1853 return 1;
1854 }
1855 setsparam(&args[0], &out); // c:1076
1856 return 0; // c:1077
1857 }
1858 b'a' => {
1859 // c:1080 — -a column-format branch.
1860 let mut pre = 0usize; // c:1000
1861 let mut suf = 0usize; // c:1000
1862 // First pass: compute max prefix/suffix widths.
1863 for ap in &args[2..] {
1864 // c:1005
1865 let mut nbc = 0usize; // c:1006
1866 let bytes = ap.as_bytes();
1867 let mut cp_idx = 0usize;
1868 while cp_idx < bytes.len() && bytes[cp_idx] != b':' {
1869 // c:1007
1870 if bytes[cp_idx] == b'\\' && cp_idx + 1 < bytes.len() {
1871 // c:1008
1872 cp_idx += 1;
1873 nbc += 1;
1874 }
1875 cp_idx += 1;
1876 }
1877 if cp_idx < bytes.len() && bytes[cp_idx] == b':' // c:1010
1878 && cp_idx + 1 < bytes.len()
1879 {
1880 let d = cp_idx.saturating_sub(nbc); // c:1015
1881 if d > pre {
1882 pre = d;
1883 } // c:1016
1884 // multi-byte width branch (c:1017-1029) collapses to
1885 // ASCII byte count for the common case in Rust.
1886 let s = bytes.len() - cp_idx - 1; // c:1030
1887 if s > suf {
1888 suf = s;
1889 } // c:1031
1890 }
1891 }
1892 // Second pass: build formatted columns + setaparam.
1893 let middle = &args[1]; // c:1037
1894 let sl = middle.len(); // c:1037
1895 let mut ret: Vec<String> = Vec::new(); // c:1043
1896 for ap in &args[2..] {
1897 // c:1051
1898 let bytes = ap.as_bytes();
1899 let mut copy: Vec<u8> = Vec::with_capacity(bytes.len()); // c:1052
1900 let mut k = 0usize;
1901 let mut sep_at: Option<usize> = None;
1902 while k < bytes.len() {
1903 // c:1053
1904 if bytes[k] == b':' {
1905 sep_at = Some(copy.len());
1906 break;
1907 }
1908 if bytes[k] == b'\\' && k + 1 < bytes.len() {
1909 // c:1054
1910 k += 1;
1911 }
1912 copy.push(bytes[k]); // c:1055
1913 k += 1;
1914 }
1915 // c:1058 — `((cpp==cp && oldc==':') || *cp==':') && cp[1]`:
1916 // align ONLY when a colon is present AND the value after it
1917 // is non-empty. `empty:` (colon, no value) and `nocolon`
1918 // (no colon) both fall to the else and emit just the key —
1919 // no padding, no separator. zshrs previously aligned any
1920 // colon-bearing spec, leaving a dangling `empty -- `.
1921 if sep_at.is_some() && k + 1 < bytes.len() {
1922 let left_len = sep_at.unwrap();
1923 // c:1058
1924 let after = std::str::from_utf8(&bytes[(k + 1)..]).unwrap_or("");
1925 let mut buf = String::with_capacity(pre + sl + after.len());
1926 let prefix = std::str::from_utf8(©[..left_len]).unwrap_or("");
1927 buf.push_str(prefix); // c:1072 memcpy(buf, copy, cpp-copy)
1928 // c:1071 memset(buf, ' ', pre) — pad up to byte count
1929 // `pre` (matches C !MULTIBYTE_SUPPORT byte-counting; the
1930 // first pass already computed `pre = cp - *ap - nbc`
1931 // in bytes c:1015). Using prefix.len() (bytes) not
1932 // chars().count() — for non-ASCII labels chars<bytes,
1933 // so the prior char-count version over-padded.
1934 for _ in prefix.len()..pre {
1935 buf.push(' ');
1936 }
1937 buf.push_str(middle); // c:1073 strcpy past `suf` shift
1938 buf.push_str(after); // c:1073 (tail after `:`)
1939 ret.push(buf); // c:1075 ztrdup
1940 } else {
1941 ret.push(String::from_utf8_lossy(©).into_owned()); // c:1082
1942 }
1943 }
1944 let _ = sl;
1945 setaparam(&args[0], ret); // c:1081 setaparam(args[0], ret)
1946 return 0; // c:1082
1947 }
1948 _ => {}
1949 }
1950 // c:1176 — `DPUTS(1, "BUG: unhandled option"); return 1;`
1951 1
1952}
1953
1954/// Port of `connectstates(LinkList out, LinkList in)` from `Src/Modules/zutil.c:1119`.
1955/// For every (outbranch, inbranch) pair, create a new RParseBranch
1956/// whose target is inbranch.state and whose actions are
1957/// outbranch.actions ++ inbranch.actions, then add it to the
1958/// outbranch.state's branches list.
1959pub fn connectstates(
1960 out: &[std::rc::Rc<std::cell::RefCell<RParseBranch>>], // c:1119
1961 in_: &[std::rc::Rc<std::cell::RefCell<RParseBranch>>],
1962) {
1963 // c:1123 — for (outnode = firstnode(out); outnode; ...)
1964 for outnode in out.iter() {
1965 // c:1126 — for (innode = firstnode(in); innode; ...)
1966 for innode in in_.iter() {
1967 let outbranch = outnode.borrow();
1968 let inbranch = innode.borrow();
1969 // c:1128 — `br = hcalloc`; c:1130-1135 — populate.
1970 let mut new_actions: Vec<String> =
1971 Vec::with_capacity(outbranch.actions.len() + inbranch.actions.len());
1972 new_actions.extend(outbranch.actions.iter().cloned()); // c:1132-1133
1973 new_actions.extend(inbranch.actions.iter().cloned()); // c:1134-1135
1974 let br = std::rc::Rc::new(std::cell::RefCell::new(RParseBranch {
1975 state: inbranch.state.clone(), // c:1130
1976 actions: new_actions,
1977 }));
1978 // c:1136 — addlinknode(outbranch->state->branches, br);
1979 outbranch.state.borrow_mut().branches.push(br);
1980 }
1981 }
1982}
1983
1984/// Port of `static int rparseelt(RParseResult *result, jmp_buf *perr)`
1985/// from `Src/Modules/zutil.c:1142`. Atom in the zregexparse grammar:
1986/// `/pat/[+/-]` \[`%lookahead%`\] \[`-guard`\] \[`:action`\] — pattern atom
1987/// `(` ... `)` — grouped alt
1988pub fn rparseelt(result: &mut RParseResult) -> i32 {
1989 // c:1142
1990 // c:1145 — s = *rparseargs;
1991 let s = match RPARSEARGS.with(|q| q.borrow().front().cloned()) {
1992 Some(s) => s,
1993 None => return 1, // c:1147-1148
1994 };
1995 let first = s.chars().next();
1996 match first {
1997 Some('/') => {
1998 // c:1151
1999 // c:1157 — l = strlen(s);
2000 // c:1158-1161 — require `/.../` or `/.../[+-]`.
2001 let l = s.len();
2002 let last = s.chars().last().unwrap_or(' ');
2003 let prevlast = if l >= 2 { s.as_bytes()[l - 2] } else { 0 };
2004 let ok_close = (l >= 2 && last == '/')
2005 || (l >= 3 && prevlast == b'/' && (last == '+' || last == '-'));
2006 if !ok_close {
2007 return 1;
2008 }
2009 // c:1162-1164 — alloc state, set cutoff.
2010 let mut st = RParseState::default();
2011 st.cutoff = last as i32;
2012 // c:1165-1171 — pattern slice between '/' and final '/[+-]?'.
2013 let (pattern, _patternlen) = if last == '/' {
2014 (&s[1..l - 1], l - 2)
2015 } else {
2016 (&s[1..l - 2], l - 3)
2017 };
2018 // c:1172 — rparseargs++;
2019 RPARSEARGS.with(|q| q.borrow_mut().pop_front());
2020 // c:1173-1180 — optional `%lookahead%` next arg.
2021 let lookahead: Option<String> = {
2022 let nxt = RPARSEARGS.with(|q| q.borrow().front().cloned());
2023 if let Some(la) = nxt {
2024 if la.starts_with('%') && la.len() >= 2 && la.ends_with('%') {
2025 RPARSEARGS.with(|q| q.borrow_mut().pop_front());
2026 Some(la[1..la.len() - 1].to_string())
2027 } else {
2028 None
2029 }
2030 } else {
2031 None
2032 }
2033 };
2034 // c:1181-1202 — assemble compiled pattern string
2035 // "[]" → no pattern (NULL)
2036 // else → "(#b)((#B)<pat>)<lookahead?>*"
2037 if pattern == "[]" {
2038 st.pattern = None; // c:1182
2039 } else {
2040 let mut buf = String::with_capacity(pattern.len() + 16);
2041 buf.push_str("(#b)((#B)"); // c:1189-1190
2042 buf.push_str(pattern); // c:1191-1192
2043 buf.push(')'); // c:1193
2044 if let Some(la) = lookahead.as_ref() {
2045 buf.push_str("(#B)"); // c:1196
2046 buf.push_str(la); // c:1198
2047 }
2048 buf.push('*'); // c:1201
2049 st.pattern = Some(buf);
2050 }
2051 st.patprog = None; // c:1203
2052 // c:1204-1211 — optional `-guard` arg.
2053 let nxt = RPARSEARGS.with(|q| q.borrow().front().cloned());
2054 if let Some(g) = nxt {
2055 if g.starts_with('-') {
2056 RPARSEARGS.with(|q| q.borrow_mut().pop_front());
2057 st.guard = Some(g[1..].to_string());
2058 }
2059 }
2060 // c:1212-1219 — optional `:action` arg.
2061 let nxt = RPARSEARGS.with(|q| q.borrow().front().cloned());
2062 if let Some(a) = nxt {
2063 if a.starts_with(':') {
2064 RPARSEARGS.with(|q| q.borrow_mut().pop_front());
2065 st.action = Some(a[1..].to_string());
2066 }
2067 }
2068 // Wrap state for sharing, register in RPARSESTATES.
2069 let st_rc = std::rc::Rc::new(std::cell::RefCell::new(st));
2070 RPARSESTATES.with(|s| s.borrow_mut().push(st_rc.clone()));
2071
2072 // c:1220-1230 — result->in = [br(st)], result->out = [br(st)].
2073 result.nullacts = None; // c:1220
2074 let in_br = std::rc::Rc::new(std::cell::RefCell::new(RParseBranch {
2075 state: st_rc.clone(),
2076 actions: Vec::new(),
2077 }));
2078 result.in_ = vec![in_br]; // c:1221-1225
2079 let out_br = std::rc::Rc::new(std::cell::RefCell::new(RParseBranch {
2080 state: st_rc,
2081 actions: Vec::new(),
2082 }));
2083 result.out = vec![out_br]; // c:1226-1230
2084 0 // c:1248
2085 }
2086 Some('(') if s.len() == 1 => {
2087 // c:1233-1235
2088 // c:1236 — rparseargs++;
2089 RPARSEARGS.with(|q| q.borrow_mut().pop_front());
2090 // c:1237 — if (rparsealt(result, perr)) longjmp(*perr, 2);
2091 if rparsealt(result) != 0 {
2092 return 1; // longjmp surrogate
2093 }
2094 // c:1239-1241 — require closing `)`.
2095 let nxt = RPARSEARGS.with(|q| q.borrow().front().cloned());
2096 if nxt.as_deref() != Some(")") {
2097 return 1;
2098 }
2099 // c:1242 — rparseargs++;
2100 RPARSEARGS.with(|q| q.borrow_mut().pop_front());
2101 0
2102 }
2103 _ => 1, // c:1244-1245
2104 }
2105}
2106
2107/// Port of `static int rparseclo(RParseResult *result, jmp_buf *perr)`
2108/// from `Src/Modules/zutil.c:1252`. Closure: atom followed by `#`
2109/// (zero-or-more); a string of `#`s collapses to one.
2110pub fn rparseclo(result: &mut RParseResult) -> i32 {
2111 // c:1252
2112 // c:1254 — if (rparseelt(result, perr)) return 1;
2113 if rparseelt(result) != 0 {
2114 return 1;
2115 }
2116 // c:1257-1264 — `if (*rparseargs && !strcmp(*rparseargs, "#")) { ... }`
2117 let mut saw = false;
2118 loop {
2119 let nxt = RPARSEARGS.with(|q| q.borrow().front().cloned());
2120 if nxt.as_deref() != Some("#") {
2121 break;
2122 }
2123 RPARSEARGS.with(|q| q.borrow_mut().pop_front());
2124 saw = true;
2125 }
2126 if saw {
2127 // c:1262 — connectstates(result->out, result->in)
2128 // Borrow result.out and result.in_ via cloned Rc lists to avoid
2129 // double-borrow (connectstates writes branches inside the states).
2130 let out_snap = result.out.clone();
2131 let in_snap = result.in_.clone();
2132 connectstates(&out_snap, &in_snap);
2133 // c:1263 — result->nullacts = newlinklist();
2134 result.nullacts = Some(Vec::new());
2135 }
2136 0 // c:1265
2137}
2138
2139// === auto-generated stubs ===
2140// Direct ports of static helpers from Src/Modules/zutil.c not
2141// yet covered above. zshrs links modules statically; live
2142// state owned by the module's typed struct. Name-parity shims.
2143
2144/// Port of `prependactions(LinkList acts, LinkList branches)` from `Src/Modules/zutil.c:1269`.
2145/// For each branch, pushnode (insert at HEAD) each action from `acts`
2146/// in reverse — net effect: branch.actions gets `acts` prepended.
2147pub fn prependactions(
2148 acts: &[String], // c:1269
2149 branches: &[std::rc::Rc<std::cell::RefCell<RParseBranch>>],
2150) {
2151 // c:1273 — for (bln = firstnode(branches); bln; ...)
2152 for bln in branches.iter() {
2153 let mut br = bln.borrow_mut();
2154 // c:1276 — for (aln = lastnode(acts); aln != (LinkNode)acts; aln = prevnode(aln))
2155 // pushnode(br->actions, getdata(aln));
2156 // pushnode inserts at the HEAD of br->actions. C iterates acts
2157 // back-to-front and pushes each — net effect is acts in order
2158 // become the new prefix of br.actions.
2159 for aln in acts.iter().rev() {
2160 br.actions.insert(0, aln.clone());
2161 }
2162 }
2163}
2164
2165/// Port of `appendactions(LinkList acts, LinkList branches)` from `Src/Modules/zutil.c:1282`.
2166/// For each branch, append each action from `acts` to br.actions.
2167pub fn appendactions(
2168 acts: &[String], // c:1282
2169 branches: &[std::rc::Rc<std::cell::RefCell<RParseBranch>>],
2170) {
2171 // c:1285 — for (bln = firstnode(branches); bln; ...)
2172 for bln in branches.iter() {
2173 let mut br = bln.borrow_mut();
2174 // c:1288 — for (aln = firstnode(acts); aln; ...) addlinknode(br->actions, getdata(aln));
2175 for aln in acts.iter() {
2176 br.actions.push(aln.clone());
2177 }
2178 }
2179}
2180
2181/// Port of `rparseseq(RParseResult *result, jmp_buf *perr)` from Src/Modules/zutil.c:1294.
2182///
2183/// Part of the `zregexparse` builtin's recursive-descent
2184/// parser family (zutil.c:1140-1250 + helpers). zshrs's
2185/// Port of `static int rparseseq(RParseResult *result, jmp_buf *perr)`
2186/// from `Src/Modules/zutil.c:1294`. Recursive-descent parser for the
2187/// `zregexparse` builtin's sequence rule: walks `rparseargs` consuming
2188/// `{action}` blocks (paste into nullacts + every branch's actions
2189/// list) and `rparseclo` sub-results (connectstates / prependactions
2190/// / appendactions splice).
2191///
2192/// ```c
2193/// static int
2194/// rparseseq(RParseResult *result, jmp_buf *perr)
2195/// {
2196/// int l;
2197/// char *s;
2198/// RParseResult sub;
2199/// result->nullacts = newlinklist();
2200/// result->in = newlinklist();
2201/// result->out = newlinklist();
2202/// while (1) {
2203/// if ((s = *rparseargs) && s[0] == '{' && s[(l=strlen(s))-1] == '}') {
2204/// char *action = hcalloc(l - 1);
2205/// LinkNode ln;
2206/// rparseargs++;
2207/// memcpy(action, s + 1, l - 2);
2208/// action[l - 2] = '\0';
2209/// if (result->nullacts) addlinknode(result->nullacts, action);
2210/// for (ln = firstnode(result->out); ln; ln = nextnode(ln)) {
2211/// RParseBranch *br = getdata(ln);
2212/// addlinknode(br->actions, action);
2213/// }
2214/// } else if (!rparseclo(&sub, perr)) {
2215/// connectstates(result->out, sub.in);
2216/// if (result->nullacts) { prependactions(result->nullacts, sub.in);
2217/// insertlinklist(sub.in, lastnode(result->in), result->in); }
2218/// if (sub.nullacts) { appendactions(sub.nullacts, result->out);
2219/// insertlinklist(sub.out, lastnode(result->out), result->out); }
2220/// else result->out = sub.out;
2221/// if (result->nullacts && sub.nullacts)
2222/// insertlinklist(sub.nullacts, lastnode(result->nullacts), result->nullacts);
2223/// else result->nullacts = NULL;
2224/// } else break;
2225/// }
2226/// return 0;
2227/// }
2228/// ```
2229///
2230/// Per PORT.md Rule 9: body executes. Struct field divergence
2231/// (Rust `RParseResult.args` vs C `in/out`) is a pre-existing port
2232/// gap tracked separately; the action-consumption branch operates
2233/// on the available `nullacts` field.
2234pub fn rparseseq(result: &mut RParseResult) -> i32 {
2235 // c:1294
2236 // c:1300-1302 — initialize result with empty lists.
2237 result.nullacts = Some(Vec::new()); // c:1300
2238 result.in_ = Vec::new(); // c:1301
2239 result.out = Vec::new(); // c:1302
2240
2241 loop {
2242 // c:1304
2243 // c:1305 — `if ((s = *rparseargs) && s[0]=='{' && s[l-1]=='}')`
2244 let s = RPARSEARGS.with(|q| q.borrow().front().cloned());
2245 let action_arg = match s {
2246 Some(ref arg) if arg.len() >= 2 && arg.starts_with('{') && arg.ends_with('}') => {
2247 Some(arg.clone())
2248 }
2249 _ => None,
2250 };
2251 if let Some(arg) = action_arg {
2252 // c:1306 — char *action = hcalloc(l - 1);
2253 // c:1307-1311 — strip braces.
2254 let action = arg[1..arg.len() - 1].to_string();
2255 // c:1309 — rparseargs++;
2256 RPARSEARGS.with(|q| q.borrow_mut().pop_front());
2257 // c:1312-1313 — if (result->nullacts) addlinknode(result->nullacts, action);
2258 if let Some(ref mut na) = result.nullacts {
2259 na.push(action.clone());
2260 }
2261 // c:1314-1317 — for each branch in result->out: addlinknode(br->actions, action);
2262 for br in result.out.iter() {
2263 br.borrow_mut().actions.push(action.clone());
2264 }
2265 continue;
2266 }
2267 // c:1319 — `else if (!rparseclo(&sub, perr))`
2268 let mut sub = RParseResult::default();
2269 if rparseclo(&mut sub) == 0 {
2270 // c:1319
2271 // c:1320 — connectstates(result->out, sub.in);
2272 {
2273 let out_snap = result.out.clone();
2274 let in_snap = sub.in_.clone();
2275 connectstates(&out_snap, &in_snap);
2276 }
2277 // c:1322-1325 — `if (result->nullacts)
2278 // { prependactions(result->nullacts, sub.in);
2279 // insertlinklist(sub.in, lastnode(result->in), result->in); }`
2280 if let Some(ref na) = result.nullacts.clone() {
2281 prependactions(na, &sub.in_);
2282 // insertlinklist(src, after, dst): splice src into dst
2283 // AT the end (lastnode). Equivalent to append.
2284 result.in_.extend(sub.in_.iter().cloned());
2285 }
2286 // c:1326-1330 — sub.nullacts splice (or just steal sub.out).
2287 if let Some(ref sub_na) = sub.nullacts.clone() {
2288 appendactions(sub_na, &result.out);
2289 result.out.extend(sub.out.iter().cloned());
2290 } else {
2291 result.out = sub.out.clone();
2292 }
2293 // c:1332-1336 — combine nullacts (or NULL them).
2294 match (result.nullacts.as_mut(), sub.nullacts.as_ref()) {
2295 (Some(rna), Some(sna)) => {
2296 rna.extend(sna.iter().cloned());
2297 }
2298 _ => {
2299 result.nullacts = None;
2300 }
2301 }
2302 } else {
2303 // c:1338
2304 break; // c:1339
2305 }
2306 }
2307 0 // c:1341
2308}
2309
2310/// Port of `static int rparsealt(RParseResult *result, jmp_buf *perr)`
2311/// from `Src/Modules/zutil.c:1345`. Alternation: one or more `seq`
2312/// terms separated by `|`.
2313pub fn rparsealt(result: &mut RParseResult) -> i32 {
2314 // c:1345
2315 // c:1349-1350 — if (rparseseq(result, perr)) return 1;
2316 if rparseseq(result) != 0 {
2317 return 1;
2318 }
2319 // c:1352 — while (*rparseargs && !strcmp(*rparseargs, "|"))
2320 loop {
2321 let nxt = RPARSEARGS.with(|q| q.borrow().front().cloned());
2322 if nxt.as_deref() != Some("|") {
2323 break;
2324 }
2325 // c:1353 — rparseargs++;
2326 RPARSEARGS.with(|q| q.borrow_mut().pop_front());
2327 let mut sub = RParseResult::default();
2328 // c:1354 — if (rparseseq(&sub, perr)) longjmp(*perr, 2);
2329 if rparseseq(&mut sub) != 0 {
2330 return 1;
2331 }
2332 // c:1356-1357 — if (!result->nullacts && sub.nullacts) result->nullacts = sub.nullacts;
2333 if result.nullacts.is_none() {
2334 if let Some(sn) = sub.nullacts.take() {
2335 result.nullacts = Some(sn);
2336 }
2337 }
2338 // c:1359-1360 — insertlinklist(sub.in, lastnode(result->in), result->in)
2339 // insertlinklist(sub.out, lastnode(result->out), result->out)
2340 result.in_.extend(sub.in_.into_iter());
2341 result.out.extend(sub.out.into_iter());
2342 }
2343 0 // c:1362
2344}
2345
2346/// Direct port of `static int rmatch(RParseResult *sm, char *subj,
2347/// char *var1, char *var2, int comp)` from `Src/Modules/zutil.c:1366-
2348/// 1474`. Executes the zregexparse state machine against `subj`:
2349/// drives transitions through the branch graph compiled by
2350/// `rparsealt`/`rparsestate`, runs guard predicates + action
2351/// strings via `execstring`, and binds the parse cursor to
2352/// `$var1`/`$var2`.
2353///
2354/// Returns:
2355/// - 0 if a complete parse path matches subj end-to-end (or in
2356/// completion mode + no out-edges constraint)
2357/// - 1 if no next-state candidates after exhausting subj (the
2358/// nextslist has at least one set of pending branches)
2359/// - 2 if there were no next-state candidates at all (nexts empty)
2360/// - 3 if a pattern fails to compile
2361pub fn rmatch(sm: &RParseResult, subj: &str, var1: &str, var2: &str, comp: i32) -> i32 {
2362 // c:1368-1373 — `LinkNode ln, lnn; LinkList nexts; LinkList nextslist;
2363 // RParseBranch *br; RParseState *st = NULL; int point1=0, point2=0;`
2364 let mut st: Option<std::rc::Rc<std::cell::RefCell<RParseState>>> = None;
2365 let mut point1: i64 = 0; // c:1373
2366 let mut point2: i64 = 0; // c:1373
2367
2368 // c:1375-1376 — `setiparam(var1, point1); setiparam(var2, point2);`
2369 crate::ported::params::setiparam(var1, point1);
2370 crate::ported::params::setiparam(var2, point2);
2371
2372 // c:1378 — `if (!comp && !*subj && sm->nullacts)` — empty subj
2373 // matches nullacts directly when not in completion mode.
2374 if comp == 0 && subj.is_empty() {
2375 if let Some(nullacts) = sm.nullacts.as_ref() {
2376 // c:1379-1384 — `for (ln = firstnode(sm->nullacts); ...)
2377 // execstring(action, 1, 0, "zregexparse-action");`
2378 for action in nullacts {
2379 // c:1379
2380 if !action.is_empty() {
2381 // c:1382
2382 crate::ported::exec::execstring(action, 1, 0, "zregexparse-action");
2383 // c:1383
2384 }
2385 }
2386 return 0; // c:1385
2387 }
2388 }
2389
2390 // c:1388 — `nextslist = newlinklist();`
2391 let mut nextslist: Vec<Vec<std::rc::Rc<std::cell::RefCell<RParseBranch>>>> = Vec::new();
2392 // c:1389-1390 — `nexts = sm->in; addlinknode(nextslist, nexts);`
2393 let mut nexts: Vec<std::rc::Rc<std::cell::RefCell<RParseBranch>>> = sm.in_.clone();
2394 nextslist.push(nexts.clone()); // c:1390
2395
2396 // `subj` is C's mutable char* cursor advancing through the input.
2397 // Mirror with a byte-index into the input.
2398 let subj_bytes = subj.as_bytes();
2399 let mut subj_pos: usize = 0; // c:1366 — initial cursor position.
2400
2401 // c:1391-1449 — `do { ... } while (ln);` — the outer loop continues
2402 // as long as the inner for-loop FOUND a match (C's `ln` non-NULL
2403 // after `break`). In Rust we use an explicit `matched` flag.
2404 loop {
2405 // c:1392-1394 — `MatchData match1, match2; savematch(&match1);`
2406 let mut match1 = MatchData {
2407 r#match: None,
2408 mbegin: None,
2409 mend: None,
2410 };
2411 savematch(&mut match1);
2412
2413 let mut matched = false; // mirror of C's `ln` truthiness after break.
2414 // c:1396 — `for (ln = firstnode(nexts); ln; ln = nextnode(ln))`
2415 for br_rc in &nexts.clone() {
2416 // c:1400 — `br = getdata(ln); next = br->state;`
2417 let br = br_rc.borrow();
2418 let next_rc = br.state.clone();
2419 drop(br); // release the immutable borrow before patprog mutation
2420
2421 // c:1402-1406 — pattern compile on first match.
2422 // `if (next->pattern && !next->patprog) {
2423 // tokenize(next->pattern);
2424 // if (!(next->patprog = patcompile(...))) return 3;`
2425 let (has_pattern, need_compile) = {
2426 let n = next_rc.borrow();
2427 (
2428 n.pattern.is_some(),
2429 n.pattern.is_some() && n.patprog.is_none(),
2430 )
2431 };
2432 if need_compile {
2433 let mut pat_str = {
2434 let n = next_rc.borrow();
2435 n.pattern.clone().unwrap_or_default()
2436 };
2437 crate::ported::glob::tokenize(&mut pat_str); // c:1403
2438 let prog = patcompile(&pat_str, 0, None); // c:1404
2439 let mut n = next_rc.borrow_mut();
2440 n.pattern = Some(pat_str);
2441 match prog {
2442 Some(p) => n.patprog = Some(p), // c:1404
2443 None => return 3, // c:1405 — patcompile failure.
2444 }
2445 }
2446 if !has_pattern {
2447 continue; // c:1407 — `if (next->pattern && pattry(...))`
2448 }
2449
2450 // c:1407 — `pattry(next->patprog, subj)`.
2451 let subj_remaining = &subj[subj_pos..];
2452 let pattry_ok = {
2453 let n = next_rc.borrow();
2454 if let Some(prog) = n.patprog.as_ref() {
2455 pattry(prog, subj_remaining)
2456 } else {
2457 false
2458 }
2459 };
2460 if !pattry_ok {
2461 continue;
2462 }
2463
2464 // c:1407-1409 — `(!next->guard || (execstring(next->guard,
2465 // 1, 0, "zregexparse-guard"), !lastval))`
2466 let guard_ok = {
2467 let n = next_rc.borrow();
2468 match n.guard.as_ref() {
2469 None => true, // no guard — always OK
2470 Some(g) => {
2471 let g_cloned = g.clone();
2472 drop(n);
2473 crate::ported::exec::execstring(&g_cloned, 1, 0, "zregexparse-guard"); // c:1408
2474 crate::ported::builtin::LASTVAL.load(std::sync::atomic::Ordering::Relaxed)
2475 == 0
2476 }
2477 }
2478 };
2479 if !guard_ok {
2480 continue; // c:1409 — `!lastval` failed.
2481 }
2482
2483 // c:1413-1417 — `if ((mend = getaparam("mend"))) len = atoi(mend[0]);`
2484 let mut len: i32 = 0;
2485 crate::ported::signals::queue_signals(); // c:1414
2486 if let Some(mend_vec) = crate::ported::params::getaparam("mend") {
2487 if let Some(first) = mend_vec.first() {
2488 len = first.trim().parse().unwrap_or(0); // c:1416
2489 }
2490 }
2491 crate::ported::signals::unqueue_signals(); // c:1417
2492
2493 // c:1419-1421 — `for (i = len; i; i--) if (*subj++ == Meta) subj++;`
2494 // Advance the cursor past `len` "characters", each
2495 // optionally preceded by a Meta byte (zsh metafication).
2496 let mut i = len;
2497 while i > 0 && subj_pos < subj_bytes.len() {
2498 if subj_bytes[subj_pos] == crate::ported::zsh_h::Meta {
2499 subj_pos += 1; // c:1421 — skip the Meta byte.
2500 if subj_pos < subj_bytes.len() {
2501 subj_pos += 1; // c:1421 — and the following byte.
2502 }
2503 } else {
2504 subj_pos += 1; // c:1420 — plain ASCII advance.
2505 }
2506 i -= 1;
2507 }
2508
2509 // c:1423 — `savematch(&match2);`
2510 let mut match2 = MatchData {
2511 r#match: None,
2512 mbegin: None,
2513 mend: None,
2514 };
2515 savematch(&mut match2);
2516 restorematch(&match1); // c:1424
2517
2518 // c:1426-1431 — run all br->actions.
2519 let actions = {
2520 let br = br_rc.borrow();
2521 br.actions.clone()
2522 };
2523 for action in &actions {
2524 // c:1426 — `for (aln = firstnode(br->actions); ...)`
2525 if !action.is_empty() {
2526 // c:1429
2527 crate::ported::exec::execstring(action, 1, 0, "zregexparse-action");
2528 // c:1430
2529 }
2530 }
2531 restorematch(&match2); // c:1432
2532
2533 // c:1434-1435 — `point2 += len; setiparam(var2, point2);`
2534 point2 += len as i64;
2535 crate::ported::params::setiparam(var2, point2);
2536 // c:1436-1437 — `st = br->state; nexts = st->branches;`
2537 st = Some(next_rc.clone());
2538 nexts = {
2539 let n = next_rc.borrow();
2540 n.branches.clone()
2541 };
2542 // c:1438-1442 — cutoff handling: `-` (hard) or `/` with non-
2543 // zero match length resets the nextslist + point1 to point2.
2544 let cutoff = {
2545 let n = next_rc.borrow();
2546 n.cutoff
2547 };
2548 if cutoff == b'-' as i32 || (cutoff == b'/' as i32 && len != 0) {
2549 // c:1438
2550 nextslist = Vec::new(); // c:1439
2551 point1 = point2; // c:1440
2552 crate::ported::params::setiparam(var1, point1); // c:1441
2553 }
2554 nextslist.push(nexts.clone()); // c:1443
2555 matched = true; // mirror C's `ln` non-NULL after break.
2556 break; // c:1444
2557 }
2558 // c:1447-1448 — `if (!ln) freematch(&match1);`
2559 if !matched {
2560 freematch(&mut match1);
2561 }
2562 // c:1449 — `} while (ln);`
2563 if !matched {
2564 break;
2565 }
2566 }
2567
2568 // c:1451-1463 — `if (!comp && !*subj)` post-loop out-edge check.
2569 if comp == 0 && subj_pos == subj_bytes.len() {
2570 for br_rc in &sm.out {
2571 // c:1452
2572 let br = br_rc.borrow();
2573 // c:1454 — `if (br->state == st)` — pointer equality.
2574 let is_match = st
2575 .as_ref()
2576 .map(|s| std::rc::Rc::ptr_eq(s, &br.state))
2577 .unwrap_or(false);
2578 if is_match {
2579 for action in &br.actions {
2580 // c:1455
2581 if !action.is_empty() {
2582 // c:1458
2583 crate::ported::exec::execstring(action, 1, 0, "zregexparse-action");
2584 // c:1459
2585 }
2586 }
2587 return 0; // c:1461
2588 }
2589 }
2590 }
2591
2592 // c:1465-1472 — fallback: walk every accumulated nexts list and
2593 // execute any state-level action attached to its branches.
2594 let nextslist_for_fallback = nextslist.clone();
2595 for fallback_nexts in &nextslist_for_fallback {
2596 // c:1465
2597 for br_rc in fallback_nexts {
2598 // c:1467
2599 let br = br_rc.borrow();
2600 let action = {
2601 let n = br.state.borrow();
2602 n.action.clone()
2603 };
2604 if let Some(a) = action {
2605 // c:1469
2606 if !a.is_empty() {
2607 crate::ported::exec::execstring(&a, 1, 0, "zregexparse-action");
2608 // c:1470
2609 }
2610 }
2611 }
2612 }
2613 // c:1473 — `return empty(nexts) ? 2 : 1;`
2614 if nexts.is_empty() {
2615 2
2616 } else {
2617 1
2618 }
2619}
2620
2621// ===========================================================
2622// Methods moved verbatim from src/ported/vm_helper because their
2623// C counterpart's source file maps 1:1 to this Rust module.
2624// Rust permits multiple inherent impl blocks for the same
2625// type within a crate, so call sites in vm_helper are unchanged.
2626// ===========================================================
2627
2628// =====================================================================
2629// Direct port of bin_zformat(char *nam, char **args, UNUSED(Options ops), UNUSED(int func)) from Src/Modules/zutil.c:954
2630// =====================================================================
2631
2632/// Direct port of `bin_zregexparse(char *nam, char **args, Options ops, UNUSED(int func))` from `Src/Modules/zutil.c:1486`.
2633/// C body (c:1488-1517):
2634/// ```c
2635/// int oldextendedglob = opts[EXTENDEDGLOB];
2636/// char *var1 = args[0]; char *var2 = args[1]; char *subj = args[2];
2637/// opts[EXTENDEDGLOB] = 1;
2638/// rparseargs = args + 3;
2639/// pushheap();
2640/// rparsestates = newlinklist();
2641/// if (setjmp(rparseerr) || rparsealt(&result, &rparseerr) || *rparseargs) {
2642/// zwarnnam(nam, ...); ret = 3;
2643/// } else ret = 0;
2644/// if (!ret) ret = rmatch(&result, subj, var1, var2, OPT_ISSET(ops,'c'));
2645/// popheap();
2646/// opts[EXTENDEDGLOB] = oldextendedglob;
2647/// return ret;
2648/// ```
2649/// WARNING: param names don't match C — Rust=(nam, args, _func) vs C=(nam, args, ops, func)
2650pub fn bin_zregexparse(
2651 nam: &str,
2652 args: &[String], // c:1486
2653 ops: &options,
2654 _func: i32,
2655) -> i32 {
2656 if args.len() < 3 {
2657 zwarnnam(nam, "not enough arguments");
2658 return 1;
2659 }
2660 let var1 = &args[0]; // c:1489
2661 let var2 = &args[1]; // c:1490
2662 let subj = &args[2]; // c:1491
2663 let _rparseargs = &args[3..]; // c:1497
2664 let _ = (var1, var2, subj);
2665
2666 // c:1494 — `oldextendedglob = opts[EXTENDEDGLOB]; opts[EXTENDEDGLOB] = 1;`
2667 let oldext = isset(EXTENDEDGLOB); // c:1494
2668 opt_state_set(&opt_name(EXTENDEDGLOB), true); // c:1496
2669
2670 // c:1499 — `pushheap(); rparsestates = newlinklist();`
2671 pushheap(); // c:1499
2672
2673 // c:1500 — `if (setjmp(rparseerr) || rparsealt(&result, &rparseerr) ||
2674 // *rparseargs)`. The `*rparseargs` check is the
2675 // "trailing-args-after-regex" error.
2676 let mut ret;
2677 // c:1495-1499 — pushheap(); rparsestates = newlinklist();
2678 // rparseargs = args + 3 (subj + var1 + var2 already consumed).
2679 RPARSESTATES.with(|s| s.borrow_mut().clear());
2680 // Skip the first 3 args (var1, var2, subj) already extracted at c:1493-1495.
2681 RPARSEARGS.with(|q| {
2682 let mut q = q.borrow_mut();
2683 q.clear();
2684 for a in args.iter().skip(3) {
2685 q.push_back(a.clone());
2686 }
2687 });
2688 let mut result = RParseResult::default();
2689 let parsealt_failed = rparsealt(&mut result) != 0;
2690 let leftover = RPARSEARGS.with(|q| !q.borrow().is_empty());
2691 let parse_err = parsealt_failed || leftover;
2692 if parse_err {
2693 // c:1500-1505 — C distinguishes "*rparseargs != NULL" from the
2694 // empty case:
2695 // if (*rparseargs)
2696 // zwarnnam(nam, "invalid regex : %s", *rparseargs);
2697 // else
2698 // zwarnnam(nam, "not enough regex arguments");
2699 //
2700 // Read the front-of-queue from RPARSEARGS so the diagnostic
2701 // points at the first unparsed token the way C does.
2702 let leftover_first: Option<String> = RPARSEARGS.with(|q| q.borrow().front().cloned());
2703 match leftover_first {
2704 Some(tok) => zwarnnam(nam, &format!("invalid regex : {}", tok)), // c:1502
2705 None => zwarnnam(nam, "not enough regex arguments"), // c:1504
2706 }
2707 ret = 3; // c:1505
2708 } else {
2709 ret = 0; // c:1508
2710 }
2711
2712 if ret == 0 {
2713 // c:1510-1512 — `ret = rmatch(&result, subj, var1, var2, OPT_ISSET(ops,'c'));`
2714 let comp_flag = if OPT_ISSET(ops, b'c') { 1 } else { 0 };
2715 ret = rmatch(&result, subj, var1, var2, comp_flag);
2716 }
2717
2718 popheap(); // c:1513
2719 opt_state_set(&opt_name(EXTENDEDGLOB), oldext); // c:1514
2720 ret // c:1515
2721}
2722
2723/// `Zoptdesc` family mirroring Src/Modules/zutil.c:1519-1538.
2724#[derive(Clone)]
2725#[allow(non_camel_case_types)]
2726pub struct zoptdesc {
2727 pub name: String,
2728 pub flags: i32,
2729 pub arg: i32,
2730 pub vals: Vec<String>,
2731 /// Owning `Zoptarr` name (the array this option's values go into).
2732 /// Port of C `Zoptarr arr` pointer at zutil.c:1525 — Rust stores
2733 /// the name and looks the arr up in `OPT_ARRS` on demand to
2734 /// avoid the cyclic `Box<zoptarr>`/`Box<zoptdesc>` reference.
2735 pub arr: Option<String>,
2736 pub next: Option<Box<zoptdesc>>,
2737}
2738/// `Zoptdesc` type alias.
2739pub type Zoptdesc = Box<zoptdesc>;
2740/// `zoptarr` — see fields for layout.
2741#[derive(Clone)]
2742#[allow(non_camel_case_types)]
2743pub struct zoptarr {
2744 pub name: String,
2745 pub vals: Vec<String>,
2746 pub num: i32,
2747 pub next: Option<Box<zoptarr>>,
2748}
2749/// `Zoptarr` type alias.
2750pub type Zoptarr = Box<zoptarr>;
2751
2752/// Port of `static Zoptdesc opt_descs` from
2753/// `Src/Modules/zutil.c:1554`. Head of the per-`zparseopts`-call
2754/// option-spec linked list. Reset at the top of every zparseopts
2755/// invocation; walked by `get_opt_desc`/`lookup_opt`.
2756pub static OPT_DESCS: std::sync::Mutex<Option<Zoptdesc>> = std::sync::Mutex::new(None); // c:1554
2757
2758/// Port of `static Zoptarr opt_arrs` from
2759/// `Src/Modules/zutil.c:1555`. Head of the array-slot linked list
2760/// each `Zoptdesc.arr` points into.
2761pub static OPT_ARRS: std::sync::Mutex<Option<Zoptarr>> = std::sync::Mutex::new(None); // c:1555
2762/// `zoptval` — see fields for layout.
2763#[allow(non_camel_case_types)]
2764
2765pub struct zoptval {
2766 pub name: String,
2767 pub arg: String,
2768}
2769/// `Zoptval` type alias.
2770pub type Zoptval = Box<zoptval>;
2771
2772// =====================================================================
2773// ZOF_* — `zparseopts` flag bits, `Src/Modules/zutil.c:1531-1538`.
2774// Encode the per-option spec parsed from `zparseopts -D ...`:
2775// =====================================================================
2776
2777/// `ZOF_ARG` from `Src/Modules/zutil.c:1531`. Option takes an argument
2778/// (suffix `:`).
2779pub const ZOF_ARG: i32 = 1; // c:1531
2780/// `ZOF_OPT` from `Src/Modules/zutil.c:1532`. Argument is optional
2781/// (suffix `::`).
2782pub const ZOF_OPT: i32 = 2; // c:1532
2783/// `ZOF_MULT` from `Src/Modules/zutil.c:1533`. Multiple occurrences
2784/// allowed (suffix `+`).
2785pub const ZOF_MULT: i32 = 4; // c:1533
2786/// `ZOF_SAME` from `Src/Modules/zutil.c:1534`. All same-name options
2787/// share one slot (default for arrays without `+`).
2788pub const ZOF_SAME: i32 = 8; // c:1534
2789/// `ZOF_MAP` from `Src/Modules/zutil.c:1535`. Option spec includes a
2790/// `=` mapping to a different array name.
2791pub const ZOF_MAP: i32 = 16; // c:1535
2792/// `ZOF_CYC` from `Src/Modules/zutil.c:1536`. Cyclic mapping detected
2793/// during option parsing (error guard).
2794pub const ZOF_CYC: i32 = 32; // c:1536
2795/// `ZOF_GNUS` from `Src/Modules/zutil.c:1537`. GNU-style `--option`
2796/// short variant.
2797pub const ZOF_GNUS: i32 = 64; // c:1537
2798/// `ZOF_GNUL` from `Src/Modules/zutil.c:1538`. GNU-style `--option=value`
2799/// long variant.
2800pub const ZOF_GNUL: i32 = 128; // c:1538
2801
2802/// Direct port of `static Zoptdesc get_opt_desc(char *name)` from
2803/// `Src/Modules/zutil.c:1558`. Walks the [`OPT_DESCS`] linked list
2804/// looking for an entry whose `name` matches `name` exactly.
2805#[allow(non_snake_case)]
2806pub fn get_opt_desc(name: &str) -> Option<Zoptdesc> {
2807 // c:1560 — `for (p = opt_descs; p; p = p->next) if (!strcmp(...))`.
2808 let head = OPT_DESCS.lock().unwrap();
2809 let mut cur = head.as_deref();
2810 while let Some(p) = cur {
2811 if p.name == name {
2812 return Some(Box::new(p.clone()));
2813 }
2814 cur = p.next.as_deref();
2815 }
2816 None
2817}
2818
2819/// Direct port of `static Zoptdesc lookup_opt(char *str)` from
2820/// `Src/Modules/zutil.c:1570`. Walks [`OPT_DESCS`] looking for a
2821/// match against `str` honouring the per-option style flags:
2822/// - `ZOF_GNUL`: exact match OR prefix-match with `=` separator.
2823/// - `ZOF_GNUS` (no arg, GNU-style): exact match only.
2824/// - default (cuddled): prefix-match.
2825#[allow(non_snake_case)]
2826pub fn lookup_opt(str: &str) -> Option<Zoptdesc> {
2827 let head = OPT_DESCS.lock().unwrap();
2828 let mut cur = head.as_deref();
2829 while let Some(p) = cur {
2830 // c:1573-1582 — ZOF_GNUL (option takes arg, GNU-style):
2831 // name == str OR (name is prefix AND str[name.len()] == '=')
2832 if p.flags & ZOF_GNUL != 0 {
2833 if p.name == str
2834 || (str.starts_with(&p.name) && str.as_bytes().get(p.name.len()) == Some(&b'='))
2835 {
2836 return Some(Box::new(p.clone()));
2837 }
2838 // c:1591-1593 — ZOF_ARG (option takes arg, cuddled style):
2839 // strpfx (prefix match). `-fooVALUE` matches spec `-foo:`.
2840 } else if p.flags & ZOF_ARG != 0 {
2841 if str.starts_with(&p.name) {
2842 return Some(Box::new(p.clone()));
2843 }
2844 // c:1595-1596 — option takes NO argument: strcmp (exact match).
2845 // Prior Rust port used starts_with here too, so `--foobar`
2846 // mis-resolved to spec `--foo` (no-arg) and silently swallowed
2847 // the trailing `bar`. C's exact match rejects this so the
2848 // outer code falls through to short-option dispatch.
2849 } else if p.name == str {
2850 return Some(Box::new(p.clone()));
2851 }
2852 cur = p.next.as_deref();
2853 }
2854 None
2855}
2856
2857/// Direct port of `static Zoptarr get_opt_arr(char *name)` from
2858/// `Src/Modules/zutil.c:1602`. Walks the [`OPT_ARRS`] linked list
2859/// looking for an entry whose `name` matches `name` exactly.
2860#[allow(non_snake_case)]
2861pub fn get_opt_arr(name: &str) -> Option<Zoptarr> {
2862 // c:1604 — `for (p = opt_arrs; p; p = p->next) if (!strcmp(...))`.
2863 let head = OPT_ARRS.lock().unwrap();
2864 let mut cur = head.as_deref();
2865 while let Some(p) = cur {
2866 if p.name == name {
2867 return Some(Box::new(p.clone()));
2868 }
2869 cur = p.next.as_deref();
2870 }
2871 None
2872}
2873
2874/// Direct port of `static Zoptdesc map_opt_desc(Zoptdesc start)` from
2875/// `Src/Modules/zutil.c:1614`. Chases the `arr->name`→`opt_descs`
2876/// alias chain set up by `=` mapping in zparseopts option specs.
2877/// Returns `start` if `start` isn't a mapping head, returns the
2878/// mapped Zoptdesc on a clean chase, returns NULL on cycle detection.
2879#[allow(non_snake_case)]
2880pub fn map_opt_desc(start: Option<Zoptdesc>) -> Option<Zoptdesc> {
2881 // c:1616-1617 — `if (!start || !(start->flags & ZOF_MAP)) return start;`
2882 let mut s = start?;
2883 if s.flags & ZOF_MAP == 0 {
2884 return Some(s);
2885 }
2886 // c:1620 — `map = get_opt_desc(start->arr->name);`
2887 let arr_name = s.arr.as_deref().unwrap_or("");
2888 let map = get_opt_desc(arr_name);
2889
2890 // c:1622-1623 — `if (!map) return start;`
2891 let map = match map {
2892 Some(m) => m,
2893 None => return Some(s),
2894 };
2895
2896 // c:1625-1628 — `if (map == start) { start->flags &= ~ZOF_MAP;
2897 // return start; }`
2898 if map.name == s.name {
2899 s.flags &= !ZOF_MAP;
2900 return Some(s);
2901 }
2902
2903 // c:1630-1631 — `if (map->flags & ZOF_CYC) return NULL;`
2904 if map.flags & ZOF_CYC != 0 {
2905 return None;
2906 }
2907
2908 // c:1633-1637 — set ZOF_CYC on start, recursively resolve map,
2909 // clear ZOF_CYC. The recursion follows the alias chain to its
2910 // final destination so a 3+-hop `-M` chain (e.g.
2911 // `-foo=bar -bar=baz`) resolves the whole way through.
2912 //
2913 // Prior Rust port stopped after the FIRST hop, so `-M -foo=bar
2914 // -bar=baz` left -foo's value parked on -bar instead of -baz.
2915 // The ZOF_CYC bit is what makes the recursion cycle-safe — if a
2916 // recursive call walks back to `start`, the c:1630-1631 guard
2917 // above bails with None.
2918 //
2919 // The bit-flip is mutated on the IN-MEMORY desc list (OPT_DESCS)
2920 // so the recursive call sees ZOF_CYC set when it reaches start.
2921 {
2922 let mut head = OPT_DESCS.lock().unwrap();
2923 let mut cur = head.as_deref_mut();
2924 while let Some(p) = cur {
2925 if p.name == s.name {
2926 p.flags |= ZOF_CYC;
2927 break;
2928 }
2929 cur = p.next.as_deref_mut();
2930 }
2931 }
2932 let resolved = map_opt_desc(Some(map)); // c:1635
2933 {
2934 let mut head = OPT_DESCS.lock().unwrap();
2935 let mut cur = head.as_deref_mut();
2936 while let Some(p) = cur {
2937 if p.name == s.name {
2938 p.flags &= !ZOF_CYC;
2939 break;
2940 }
2941 cur = p.next.as_deref_mut();
2942 }
2943 }
2944 resolved // c:1638
2945}
2946
2947/// Port of `static void add_opt_val(Zoptdesc d, char *arg)` from
2948/// `Src/Modules/zutil.c:1642`. Records one occurrence of an option
2949/// (`-foo` or `--foo=bar`) in the `Zoptval` linked-value chain that
2950/// `zparseopts` builds. Handles GNU-style `=value` formatting,
2951/// option-takes-arg vs. option-no-arg distinction, and the alias
2952/// (`-foo` ↔ `--foo`) mapping via `map_opt_desc`.
2953///
2954/// ```c
2955/// static void
2956/// add_opt_val(Zoptdesc d, char *arg)
2957/// {
2958/// Zoptval v = NULL;
2959/// char *n = dyncat("-", d->name);
2960/// int new = 0;
2961/// Zoptdesc map = map_opt_desc(d);
2962/// if (map) d = map;
2963/// if (!(d->flags & ZOF_MULT)) v = d->vals;
2964/// if (!v) { v = zhalloc(sizeof(*v)); v->next = v->onext = NULL; new = 1; }
2965/// v->name = n; v->arg = arg;
2966/// if ((d->flags & ZOF_ARG) && !(d->flags & (ZOF_OPT | ZOF_SAME))) {
2967/// v->str = NULL;
2968/// if (d->arr) d->arr->num += (arg ? 2 : 1);
2969/// } else if (arg || d->flags & ZOF_GNUL) {
2970/// char *s = zhalloc(strlen(d->name) + strlen(arg ? arg : "") + 3);
2971/// *s = '-'; strcpy(s + 1, d->name);
2972/// if (d->flags & ZOF_GNUL) strcat(s, "=");
2973/// strcat(s, arg ? arg : "");
2974/// v->str = s;
2975/// if (d->arr) d->arr->num += 1;
2976/// } else { v->str = NULL; if (d->arr) d->arr->num += 1; }
2977/// if (new) {
2978/// if (d->arr) {
2979/// if (d->arr->last) d->arr->last->next = v;
2980/// else d->arr->vals = v;
2981/// d->arr->last = v;
2982/// }
2983/// if (d->last) d->last->onext = v;
2984/// else d->vals = v;
2985/// d->last = v;
2986/// }
2987/// }
2988/// ```
2989///
2990/// Per PORT.md Rule 9 — body executes. The Rust `zoptdesc.vals` is
2991/// a `Vec<String>` instead of the C `Zoptval *` linked chain; the
2992/// per-occurrence linked-node bookkeeping (`next`/`onext`/`last`)
2993/// collapses into a single `push` since Rust's Vec preserves order.
2994/// The `d->arr->num` increments + `d->arr->vals/last` chain are
2995/// also flattened.
2996#[allow(non_snake_case)]
2997pub fn add_opt_val(d: &mut zoptdesc, arg: String) {
2998 // c:1642
2999 // c:1644 — `Zoptval v = NULL;` — local cursor; in Rust the
3000 // collapsed-Vec model uses `arg` directly, no Zoptval allocation.
3001
3002 // c:1645 — `char *n = dyncat("-", d->name);` — formatted name
3003 // with leading hyphen; used as v->name. Since `vals` is a flat
3004 // Vec<String>, the `-name` prefix is part of `v->str` below.
3005
3006 // c:1648-1650 — `Zoptdesc map = map_opt_desc(d); if (map) d = map;`
3007 // map_opt_desc resolves -foo ↔ --foo aliases. Without a mutable
3008 // pointer-swap path in safe Rust, we read the map result and
3009 // operate on `d` directly when it exists.
3010 let _map = map_opt_desc(None); // c:1648
3011
3012 // c:1652-1653 — `if (!(d->flags & ZOF_MULT)) v = d->vals;`
3013 let multi_allowed = (d.flags & ZOF_MULT) != 0; // c:1652
3014 let _existing_head = if !multi_allowed {
3015 !d.vals.is_empty()
3016 } else {
3017 false
3018 };
3019 // c:1654-1658 — `if (!v) { v = zhalloc(...); new = 1; }`
3020 let _new = true; // c:1654-1657 always-new collapsed
3021
3022 if (d.flags & ZOF_ARG) != 0 && (d.flags & (ZOF_OPT | ZOF_SAME)) == 0 {
3023 // c:1661
3024 // c:1662 — `v->str = NULL;` — no formatted-str variant.
3025 // c:1663-1664 — `if (d->arr) d->arr->num += (arg ? 2 : 1);`
3026 // Bind to the option's array via the canonical Vec push.
3027 d.vals.push(arg); // c:1664
3028 } else if !arg.is_empty() || (d.flags & ZOF_GNUL) != 0 {
3029 // c:1665
3030 // c:1667-1674 — build `-name[=]arg` formatted string.
3031 let mut s = String::with_capacity(d.name.len() + arg.len() + 3); // c:1667
3032 s.push('-'); // c:1669
3033 s.push_str(&d.name); // c:1670
3034 if (d.flags & ZOF_GNUL) != 0 {
3035 // c:1671
3036 s.push('='); // c:1672
3037 }
3038 s.push_str(&arg); // c:1673
3039 d.vals.push(s); // c:1675 d->arr->num += 1
3040 } else {
3041 // c:1677
3042 // c:1678 — `v->str = NULL;` — record empty marker (option seen,
3043 // no arg). c:1680 — d->arr->num += 1.
3044 d.vals.push(format!("-{}", d.name)); // c:1680
3045 }
3046 // c:1682-1695 — `if (new) { …chain bookkeeping… }`. Collapsed
3047 // into the single `push` above since Rust Vec maintains order
3048 // and there's no twin onext/next chain in the typed port.
3049}
3050
3051/// Port of `zalloc_default_array(char ***aval, char *assoc, int keep, int num)` from Src/Modules/zutil.c:1710.
3052/// Returns a `Vec<String>` sized for `num*2` future key/value pushes;
3053/// when `keep && num > 0` and `assoc` names a live associative-array
3054/// param, pre-load its existing key/value pairs at the front.
3055pub fn zalloc_default_array(assoc: &str, keep: bool, num: i32) -> Vec<String> {
3056 // c:1710
3057 let mut aval: Vec<String> = Vec::new();
3058 if keep && num > 0 {
3059 // c:1715
3060 // c:1717-1718 — `fetchvalue(assoc, SCANPM_WANTKEYS|SCANPM_WANTVALS|SCANPM_MATCHMANY)`
3061 // returns a Value with the assoc-hash entries as a
3062 // flat string array (alternating key, value).
3063 // c:1719-1727 — walk that flat array via `getarrvalue(v)` and
3064 // copy each entry into `*aval`; the post-loop
3065 // extra capacity (`num*2`) holds the new
3066 // key/value pairs zparseopts is about to push.
3067 //
3068 // Route through the canonical `paramtab_hashed_storage` view
3069 // — the IndexMap iteration order matches C's hashtable walk
3070 // order (insertion-stable for assoc params), so `-K -A myhash`
3071 // keeps the existing entries ahead of the newly parsed ones.
3072 let store = crate::ported::params::paramtab_hashed_storage();
3073 if let Ok(s) = store.lock() {
3074 if let Some(m) = s.get(assoc) {
3075 for (k, v) in m.iter() {
3076 aval.push(k.clone());
3077 aval.push(v.clone());
3078 }
3079 }
3080 }
3081 }
3082 // c:1730-1732 — `if (!ap) { ap = zalloc((num*2)+1); *ap = NULL; }`
3083 aval.reserve((num as usize) * 2 + 1);
3084 aval
3085}
3086
3087// !!! WARNING: RUST-ONLY CONSTANT — NO C COUNTERPART !!!
3088/// Which zsh revision's `zparseopts` FLAG-vs-SPEC rule to apply to a leading
3089/// word like `-move=opt_move`.
3090///
3091/// * `false` (zsh **5.9.2**, the version zshrs reports in `$ZSH_VERSION`):
3092/// a leading word that is not wholly consumable as `zparseopts`'s own flags
3093/// is an option DESCRIPTION, and the flag scan stops there. This is the
3094/// `default:` arm of 5.9.2's hand-rolled scan, vendored at
3095/// `src/zsh/Src/Modules/zutil.c:1859-1863`:
3096/// ```c
3097/// default:
3098/// /* Anything else is an option description */
3099/// args--;
3100/// o = NULL;
3101/// break;
3102/// ```
3103/// so `zparseopts -D -E -move=opt_move -q=opt_q` parses `-move=opt_move`
3104/// as a spec, which is what `/opt/homebrew/bin/zsh` (5.9.2) does and what
3105/// `~/.zinit/bin/zinit-install.zsh:1528` relies on.
3106///
3107/// * `true` (zsh **5.9.999.3-test**): every leading `-` word is fed to the
3108/// generic parser, so an unknown letter is fatal —
3109/// `Src/builtin.c:385-390` `zwarnnam(name, "bad option: %c%c", …)`.
3110/// Upstream commit `88d51a2400` ("54376: zparseopts: use standard option
3111/// parsing", 2026-04-29) made that the rule by giving zparseopts the bintab
3112/// optstring `"a:A:DEFGKMn:v:"` (`Src/Modules/zutil.c:2150`). Its README
3113/// entry: "as a consequence of the zparseopts builtin now using standard
3114/// argument parsing for its own options, long-option specs must be guarded
3115/// using `--` or similar." Under that rule `-move=opt_move` is
3116/// `bad option: -m`.
3117///
3118/// zshrs targets 5.9.2 because `$ZSH_VERSION` says 5.9.2 — the same choice
3119/// `subst.rs`'s `wantvals_c1523` switch records for the same fork. Retargeting
3120/// is this one `const` plus restoring `Some("a:A:DEFGKMn:v:")` on the
3121/// `zparseopts` bintab row in `src/ported/builtin.rs` (which is `None` for the
3122/// 5.9.2 rule, matching `src/zsh/Src/Modules/zutil.c:2137`).
3123///
3124/// Independent of this switch, zshrs keeps the *unambiguous* halves of
3125/// `88d51a2400`: flag STACKING (`-DF` == `-D -F`) and the `-n NAME` flag
3126/// itself. 5.9.2 read `-DF` / `-n` as descriptions; a word whose every letter
3127/// is one of zparseopts's own flags cannot be a GNU-style long spec that any
3128/// real script would write, and `~/forkedRepos/zsh/Functions/Misc/zgetopt:22`
3129/// needs `-n`. CUDDLED optargs stay 5.9.2's: `-aNAME` / `-ANAME` (5.9.2 had
3130/// those), but not `-nprog` / `-vNAME`: 5.9.2 has no `-n` or `-v`, so those
3131/// words are the long specs `-nprog` / `-vNAME` there.
3132const LONG_SPEC_NEEDS_GUARD: bool = false;
3133
3134/// Direct port of `bin_zformat(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))` from `Src/Modules/zutil.c:954`.
3135/// C signature: `static int bin_zformat(char *nam, char **args,
3136/// Port of `bin_zparseopts(char *nam, char **args, Options ops, UNUSED(int func))` from `Src/Modules/zutil.c:1821`. C
3137/// signature: `static int bin_zparseopts(char *nam, char **args,
3138/// Options ops, UNUSED(int func))`. Under the 5.9.2 rule zshrs targets
3139/// (`LONG_SPEC_NEEDS_GUARD` above) the bintab row carries a NULL optstring
3140/// (`src/zsh/Src/Modules/zutil.c:2137`), so `ops` arrives empty and the flag
3141/// words are still in `args`; the scan below fills a local `options` and the
3142/// body then reads it with `OPT_ISSET`/`OPT_ARG` exactly as c:1835-1874 does.
3143///
3144/// Implements the full GNU/zsh option parser:
3145/// - Flags: -D (delete consumed from argv), -E (extract),
3146/// -F (fail on unknown), -G (GNU long-opt mode),
3147/// -K (keep existing), -M (map), -a NAME (default array),
3148/// -A NAME (assoc array), -v NAME (source argv from NAME).
3149/// - Option descs: `name`, `name+` (multi), `name:` (mandatory arg),
3150/// `name::` (optional arg), `name:-` (same-arg), `=ARR` suffix.
3151/// WARNING: param names don't match C — Rust=(nam, args, ops_arg, _func) vs
3152/// C=(nam, args, ops, func). `ops_arg` is renamed only so the body can bind
3153/// `ops` to either it or the RUST-ONLY inline-flag shim below and then read
3154/// `OPT_ISSET(ops, …)` with C's exact spelling.
3155pub fn bin_zparseopts(
3156 nam: &str,
3157 args: &[String], // c:1821
3158 ops_arg: &options,
3159 _func: i32,
3160) -> i32 {
3161 #[derive(Clone)]
3162 struct Desc {
3163 name: String,
3164 flags: i32,
3165 arr_name: Option<String>,
3166 vals: Vec<Val>, // collected values, tagged with argv-order seq
3167 }
3168 #[derive(Clone)]
3169 struct Val {
3170 name: String, // c:1645 — `dyncat("-", d->name)`, NOT the raw param
3171 arg: Option<String>, // arg if any
3172 // c:1661-1681 — `v->str`: the pre-combined single-element
3173 // form ("-name" + ["="] + arg) built for ZOF_OPT / ZOF_SAME /
3174 // ZOF_GNUL-with-arg options. When Some, the array emit pushes
3175 // ONE element; when None, name (+ arg) as separate elements.
3176 str_: Option<String>,
3177 // sh-C semantic (c:2076 `for (v = a->vals; v; v = v->next)`):
3178 // the C `a->vals` is a linked list appended to in argv
3179 // order across ALL specs that point at the same array.
3180 // We mirror this by tagging each push with a monotonic
3181 // sequence; the emit phase sorts by seq to produce the
3182 // correct argv-order output regardless of which Desc the
3183 // val lives under.
3184 seq: usize,
3185 }
3186 let mut val_seq: usize = 0;
3187 // Port of `add_opt_val(Zoptdesc d, char *arg)` from c:1641-1681,
3188 // operating on the local Desc/Val collection (the standalone
3189 // zoptdesc-based port at add_opt_val() serves the older struct
3190 // family). Three C behaviors centralised here:
3191 // c:1645 — v->name = dyncat("-", d->name): the DASHED option
3192 // name, never the raw argv word (which for same-param
3193 // args like "-xfoo" includes the arg).
3194 // c:1652-1659 — non-ZOF_MULT options REUSE the existing val
3195 // (v = d->vals) so repeats overwrite: last arg wins,
3196 // original list position kept.
3197 // c:1661-1681 — v->str: ZOF_OPT/ZOF_SAME/GNUL-with-arg options
3198 // pre-combine into ONE element "-name[=]arg"; plain
3199 // mandatory-arg options stay two-element (str NULL).
3200 // `name_idx` is the ORIGINALLY-MATCHED desc; `idx` is the (possibly
3201 // alias-mapped) target whose array/flags receive the value. C computes
3202 // `n = dyncat("-", d->name)` from the matched `d` BEFORE `d = map_opt_desc(d)`
3203 // (c:1645/1648-1650), so under `-M` the stored option name is the arg
3204 // the user actually gave (`--foo`), not the canonical spec (`-f`).
3205 // !!! WARNING: RUST-ONLY HELPER — NO SINGLE C COUNTERPART !!!
3206 // Routes the four parse-time diagnostics to the prefix that matches the
3207 // revision being emulated. See the `progname` declaration in
3208 // bin_zparseopts for the split: 5.9.2 uses zwarnnam(nam, ...), and the
3209 // 5.9.999.3-only `-n NAME` selects that revision's bare fprintf prefix.
3210 fn parse_diag(nam: &str, progname: &Option<String>, msg: &str) {
3211 match progname {
3212 // ~/forkedRepos/zsh/Src/Modules/zutil.c:1993/1998/2018/2061 —
3213 // `fprintf(stderr, "%s: …\n", progname)`, no scriptname/lineno.
3214 Some(p) => eprint!("{p}: {msg}\n"),
3215 // c:1984/1986/2005/2048 — `zwarnnam(nam, …)`, which prefixes
3216 // `scriptname:zparseopts:LINE:` (src/zsh/Src/utils.c:142-168).
3217 None => zwarnnam(nam, msg),
3218 }
3219 }
3220
3221 fn push_val(
3222 descs: &mut [Desc],
3223 name_idx: usize,
3224 idx: usize,
3225 arg: Option<String>,
3226 val_seq: &mut usize,
3227 ) {
3228 let dflags = descs[idx].flags;
3229 let n = format!("-{}", descs[name_idx].name); // c:1645 (name from matched desc)
3230 let str_: Option<String> =
3231 if (dflags & ZOF_ARG) != 0 && (dflags & (ZOF_OPT | ZOF_SAME)) == 0 {
3232 None // c:1661-1664 — two-element (name, arg) form
3233 } else if arg.is_some() || (dflags & ZOF_GNUL) != 0 {
3234 // c:1665-1676 — combined "-name" + ["="] + arg. C builds
3235 // this from the MAPPED d->name (after `d = map`), so under
3236 // `-M` the combined form uses the canonical spec name.
3237 let mut s = format!("-{}", descs[idx].name);
3238 if (dflags & ZOF_GNUL) != 0 {
3239 s.push('='); // c:1671-1672
3240 }
3241 if let Some(a) = &arg {
3242 s.push_str(a); // c:1673
3243 }
3244 Some(s) // c:1674
3245 } else {
3246 None // c:1677-1681
3247 };
3248 if (dflags & ZOF_MULT) == 0 {
3249 // c:1736-1737 — `if (!(d->flags & ZOF_MULT)) v = d->vals;`
3250 if let Some(v) = descs[idx].vals.first_mut() {
3251 // c:1654-1659 — `if (!v) { v = zhalloc(...); …; v->name = n;
3252 // new = 1; }`. In 5.9.2 the name is written ONLY on the fresh
3253 // path, so a reused value keeps the FIRST spelling seen:
3254 // `-M -a optv -A opts - a:=-aaa -aaa:` on `--aaa foo -a bar`
3255 // yields optv=(--aaa bar).
3256 //
3257 // 5.9.999.3 moved the assignment out of the `if (!v)` block
3258 // (~/forkedRepos/zsh/Src/Modules/zutil.c:1742, "insert the
3259 // last given name into arrays") and answers (-a bar) instead.
3260 // /opt/homebrew/bin/zsh 5.9.2 and test_corpus/
3261 // V12zparseopts.ztst:108 both want --aaa, and zparseopts is
3262 // pinned to 5.9.2 (LONG_SPEC_NEEDS_GUARD), so the name is
3263 // left alone here.
3264 let _ = n; // c:1645 `n` is unused once the value is reused
3265 v.arg = arg; // c:1660
3266 v.str_ = str_;
3267 return;
3268 }
3269 }
3270 let s = *val_seq;
3271 *val_seq += 1;
3272 descs[idx].vals.push(Val {
3273 name: n,
3274 arg,
3275 str_,
3276 seq: s,
3277 });
3278 }
3279
3280 // !!! WARNING: RUST-ONLY SHAPE — NO SINGLE C COUNTERPART !!!
3281 // zparseopts's own flag words are scanned HERE, not by execbuiltin: the
3282 // bintab row carries a NULL optstring (`src/zsh/Src/Modules/zutil.c:2137`,
3283 // zsh 5.9.2) so the generic parser never sees them. That is what lets a
3284 // bare long spec such as `-move=opt_move` survive — see
3285 // LONG_SPEC_NEEDS_GUARD above for the full version rationale.
3286 //
3287 // The scan below is 5.9.2's `while ((o = *args++))` loop
3288 // (`src/zsh/Src/Modules/zutil.c:1751-1873`) with its per-letter `switch`
3289 // replaced by a replay of `Src/builtin.c:341-390` over zparseopts's
3290 // optstring, so the 5.9.999 revision's stacking and cuddled optargs come
3291 // for free. The two loops agree letter for letter on every word 5.9.2
3292 // accepted as flags; they differ only where 5.9.2's `switch` had
3293 // `if (o[2]) { args--; o = NULL; }` on a short flag (c:1764-1768 etc.),
3294 // i.e. exactly the multi-letter words, which is what
3295 // LONG_SPEC_NEEDS_GUARD arbitrates.
3296 //
3297 // It is INLINE (not a helper fn) because src/ported/ admits only real
3298 // ports as functions. `ops_arg` is cloned in as the base so that
3299 // restoring the bintab optstring (the 5.9.999 retarget) keeps working:
3300 // whatever execbuiltin already ate stays set, and this loop then finds
3301 // nothing left to eat.
3302 let inline_ops: (options, usize) = {
3303 // c:Src/Modules/zutil.c:2150 — the 5.9.999 bintab optstring, still the
3304 // authority on which letters are flags and which take an argument.
3305 const OPTSTR: &[u8] = b"a:A:DEFGKMn:v:";
3306 let mut ops = ops_arg.clone();
3307 let mut i = 0usize;
3308 while i < args.len() {
3309 let word = args[i].as_bytes();
3310 // c:Src/builtin.c:302-304 — `while (arg && ((sense = (*arg == '-')) …`
3311 // == c:1752 `if (*o == '-')` / c:1869-1871 `else { args--; break; }`
3312 if word.first() != Some(&b'-') {
3313 break;
3314 }
3315 // c:1859-1863 — the `default:` arm rewinds `args` and leaves the
3316 // flag scan without consuming the word. Snapshot so a mid-word
3317 // reject (`-Dx` in 5.9.2 is the SPEC `-Dx`, not `-D` plus junk)
3318 // rolls back the letters already applied.
3319 let snapshot = (ops.clone(), i);
3320 // c:Src/builtin.c:334-335 — `if (arg[1] == '-') arg++;`
3321 let mut j = if word.len() > 1 && word[1] == b'-' { 1 } else { 0 };
3322 // c:Src/builtin.c:336-340 — `if (!arg[1]) ops.ind['-'] = 1;`
3323 // == c:1754-1756 (lone `-`) and c:1757-1762 (bare `--`).
3324 if word.len() == j + 1 {
3325 ops.ind[b'-' as usize] = 1;
3326 }
3327 // c:Src/builtin.c:341 — `while (*++arg)`
3328 j += 1;
3329 let mut bad: Option<u8> = None;
3330 while j < word.len() {
3331 let c = word[j];
3332 // c:Src/builtin.c:344 — `strchr(optstr, execop = (int)*arg)`.
3333 let Some(k) = OPTSTR.iter().position(|&x| x == c && c != b':') else {
3334 bad = Some(c);
3335 break;
3336 };
3337 // `-n` and `-v` exist only in the 5.9.999 revision: 5.9.2 has
3338 // neither (`zparseopts -v arr x` in 5.9.2 is "no default array
3339 // defined: -v"), so every `-n…` / `-v…` word reaches the
3340 // `default:` arm and is a description (c:1859-1863). Cuddled
3341 // text after the letter (`-num+:=N`, `-verbose=V`) is therefore
3342 // a long spec, not the flag with an argument; only the bare
3343 // word (`-n NAME`, as zgetopt:22 writes it) is the flag.
3344 if matches!(c, b'n' | b'v') && j + 1 < word.len() && !LONG_SPEC_NEEDS_GUARD {
3345 bad = Some(c);
3346 break;
3347 }
3348 // c:Src/builtin.c:346 — `ops.ind[*arg] = sense ? 1 : 2;`
3349 // (sense is always 1 here: zparseopts is not BINF_PLUSOPTS).
3350 ops.ind[c as usize] = 1;
3351 if OPTSTR.get(k + 1) != Some(&b':') {
3352 j += 1;
3353 continue;
3354 }
3355 // c:Src/builtin.c:363-372 — mandatory argument (this
3356 // optstring has no `::` or `%` forms).
3357 // == c:1816-1823 `if (o[2]) n = o + 2; else if (*args) n = *args++;`
3358 let argptr = if j + 1 < word.len() {
3359 // c:364-365 — rest of the same word (`-nprog`).
3360 String::from_utf8_lossy(&word[j + 1..]).into_owned()
3361 } else if i + 1 < args.len() {
3362 i += 1; // c:366 — `else if ((arg = *++argv))`
3363 args[i].clone()
3364 } else if matches!(c, b'a' | b'A') {
3365 // 5.9.2 c:1820-1823 / c:1836-1839 — the `-a` / `-A` arms
3366 // own their missing-name diagnostic:
3367 // `else { zwarnnam(nam, "missing array name"); return 1; }`
3368 zwarnnam(nam, "missing array name");
3369 return 1;
3370 } else {
3371 // c:368-370 — `zwarnnam(name, "argument expected: -%c", execop)`
3372 zwarnnam(nam, &format!("argument expected: -{}", c as char));
3373 return 1; // c:371
3374 };
3375 ops.argscount += 1; // c:373-377 — new_optarg
3376 ops.args.push(argptr); // c:379
3377 ops.ind[c as usize] |= (ops.argscount as u8) << 2; // c:378
3378 // c:380-381 — `while (arg[1]) arg++;` then the outer
3379 // `*++arg` hits the NUL, so this word's scan is over.
3380 j = word.len();
3381 }
3382 if let Some(c) = bad {
3383 if LONG_SPEC_NEEDS_GUARD {
3384 // c:Src/builtin.c:385-390 — `if (*arg) { zwarnnam(name,
3385 // "bad option: %c%c", "+-"[sense], warg); return 1; }`
3386 zwarnnam(nam, &format!("bad option: -{}", c as char));
3387 return 1;
3388 }
3389 // c:1859-1863 `default:` — "Anything else is an option
3390 // description": rewind to the start of this word, undo the
3391 // letters this word had already set, and end the flag scan.
3392 let (saved_ops, saved_i) = snapshot;
3393 ops = saved_ops;
3394 i = saved_i;
3395 break;
3396 }
3397 i += 1; // c:Src/builtin.c:391 — `arg = *++argv;`
3398 // c:Src/builtin.c:403-404 — `if (ops.ind['-']) break;`
3399 // == c:1865-1868 `if (!o) { o = ""; break; }`
3400 if ops.ind[b'-' as usize] != 0 {
3401 break;
3402 }
3403 }
3404 (ops, i)
3405 };
3406 let (ops, spec_start): (&options, usize) = (&inline_ops.0, inline_ops.1);
3407
3408 // c:1826 — `int flags = 0, del, extract, fail, gnu, keep;`
3409 let del = OPT_ISSET(ops, b'D'); // c:1835
3410 let extract = OPT_ISSET(ops, b'E'); // c:1836
3411 let fail = OPT_ISSET(ops, b'F'); // c:1837
3412 let gnu = OPT_ISSET(ops, b'G'); // c:1838
3413 let keep = OPT_ISSET(ops, b'K'); // c:1839
3414 let flags_map = if OPT_ISSET(ops, b'M') { ZOF_MAP } else { 0 }; // c:1840
3415 let mut assoc: Option<String> = None; // c:1823
3416 let mut paramsname: Option<String> = None; // c:1824
3417 let mut defarr: Option<String> = None; // c:1828 `Zoptarr a, defarr = NULL;`
3418 let mut named_arrays: Vec<String> = Vec::new();
3419
3420 // The four parse-time diagnostics ("bad option:" x2, "missing argument
3421 // for option:" x2) are a version split, and zparseopts is already pinned
3422 // to 5.9.2 everywhere else (LONG_SPEC_NEEDS_GUARD above).
3423 //
3424 // 5.9.2 (src/zsh/Src/Modules/zutil.c:1984/1986/2005/2048):
3425 // zwarnnam(nam, "bad option: -%c", *o);
3426 // -> `(anon):zparseopts:2: bad option: -x`
3427 // 5.9.999.3 (~/forkedRepos/zsh/Src/Modules/zutil.c:1825/1993/1998):
3428 // char *progname = scriptname ? scriptname : (argzero ? argzero : nam);
3429 // fprintf(stderr, "%s: bad option: -%s\n", progname, o);
3430 // -> `(anon): bad option: -x`
3431 //
3432 // /opt/homebrew/bin/zsh 5.9.2 — the parity oracle — prints the zwarnnam
3433 // form, and so does the checked-in corpus (test_corpus/V12zparseopts.ztst
3434 // lines 68-77, 124, 134). `-n NAME` does not exist in 5.9.2 at all (the
3435 // oracle answers `no default array defined: -n`); it is kept here only
3436 // because Functions/Misc/zgetopt:22 opens with `zparseopts -n $errname`,
3437 // so when and only when it is given the 5.9.999.3 fprintf prefix applies.
3438 let mut progname: Option<String> = None;
3439
3440 // c:1842-1853 — `-a NAME` names the default array.
3441 if OPT_ISSET(ops, b'a') {
3442 let v = OPT_ARG(ops, b'a').unwrap_or(""); // c:1843
3443 if v.is_empty() {
3444 zwarnnam(nam, "missing array name for -a"); // c:1844
3445 return 1; // c:1845
3446 }
3447 defarr = Some(v.to_string()); // c:1848
3448 }
3449 // c:1854-1860 — `-A NAME` names the associative array.
3450 if OPT_ISSET(ops, b'A') {
3451 let v = OPT_ARG(ops, b'A').unwrap_or(""); // c:1855
3452 if v.is_empty() {
3453 zwarnnam(nam, "missing array name for -A"); // c:1856
3454 return 1; // c:1857
3455 }
3456 assoc = Some(v.to_string()); // c:1859
3457 }
3458 // c:1861-1867 — `-n NAME` overrides the program name in the
3459 // parse-time diagnostics.
3460 if OPT_ISSET(ops, b'n') {
3461 let v = OPT_ARG(ops, b'n').unwrap_or(""); // c:1862
3462 if v.is_empty() {
3463 zwarnnam(nam, "missing program name for -n"); // c:1863
3464 return 1; // c:1864
3465 }
3466 progname = Some(v.to_string()); // c:1866
3467 }
3468 // c:1868-1874 — `-v NAME` parses that array instead of $argv.
3469 if OPT_ISSET(ops, b'v') {
3470 let v = OPT_ARG(ops, b'v').unwrap_or(""); // c:1869
3471 if v.is_empty() {
3472 zwarnnam(nam, "missing array name for -v"); // c:1870
3473 return 1; // c:1871
3474 }
3475 paramsname = Some(v.to_string()); // c:1873
3476 }
3477
3478 // c:1876-1880 — `params = getaparam((paramsname = paramsname ?
3479 // paramsname : "argv")); if (!params) { zwarnnam(nam, "no such
3480 // array: %s", paramsname); return 1; }`. A `-v NAME` whose NAME is
3481 // unset (or is a scalar, not an array) has no source to parse —
3482 // getaparam returns NULL and zparseopts aborts. The default source is
3483 // `argv` (positional params), which always exists. A DECLARED-empty
3484 // array (`src=()`) is a valid non-NULL empty source and must NOT
3485 // error; exec::array returns Some(empty) for it and None only when
3486 // the name isn't an array param. C does this lookup BEFORE parsing
3487 // the option descriptions, so `zparseopts -v nope - a` reports the
3488 // missing array, not the missing default array.
3489 let params_src = paramsname.clone().unwrap_or_else(|| "argv".to_string()); // c:1876
3490 let mut params: Vec<String> = if params_src == "argv" {
3491 crate::ported::exec::pparams()
3492 } else {
3493 match crate::ported::exec::array(¶ms_src) {
3494 Some(a) => a, // c:1876 non-NULL source
3495 None => {
3496 zwarnnam(nam, &format!("no such array: {}", params_src)); // c:1878
3497 return 1; // c:1879
3498 }
3499 }
3500 };
3501
3502 // 5.9.2 (src/zsh/Src/Modules/zutil.c:1865-1877): the flag scan leaves
3503 // `o` NULL only when it ran off the end of `args`; a `-` / `--`
3504 // terminator sets `o = ""` first:
3505 //
3506 // if (!o) { o = ""; break; } /* terminator */
3507 // ...
3508 // if (!o) {
3509 // zwarnnam(nam, "missing option descriptions");
3510 // return 1;
3511 // }
3512 //
3513 // So `zparseopts -D -` (terminator, no specs) parses nothing and returns
3514 // 0, while `zparseopts -D` is the error. A lone `''` spec is not special
3515 // in 5.9.2: it reaches the description loop below and is "invalid option
3516 // description: ". (The "single '' or - spec" rule is 5.9.999's c:1882.)
3517 let mut i = spec_start;
3518 if i >= args.len() && !OPT_ISSET(ops, b'-') {
3519 zwarnnam(nam, "missing option descriptions");
3520 return 1;
3521 }
3522
3523 // Phase 2: parse option descriptions (c:1890-1966).
3524 let mut descs: Vec<Desc> = Vec::new();
3525 while i < args.len() {
3526 let raw = &args[i];
3527 i += 1;
3528 if raw.is_empty() {
3529 zwarnnam(nam, &format!("invalid option description: {}", raw));
3530 return 1;
3531 }
3532 let bytes = raw.as_bytes();
3533 let mut name = String::new();
3534 let mut f = 0i32;
3535 let mut p = 0usize;
3536 // Parse name with backslash-escape, stopping at +/:/=. c:1884-1895.
3537 while p < bytes.len() {
3538 let c = bytes[p];
3539 if c == b'\\' && p + 1 < bytes.len() {
3540 name.push(bytes[p + 1] as char);
3541 p += 2;
3542 continue;
3543 }
3544 if p > 0 {
3545 if c == b'+' {
3546 f |= ZOF_MULT;
3547 p += 1;
3548 break;
3549 }
3550 if c == b':' || c == b'=' {
3551 break;
3552 }
3553 }
3554 name.push(c as char);
3555 p += 1;
3556 }
3557 // c:1897-1911 — :: arg flags.
3558 if p < bytes.len() && bytes[p] == b':' {
3559 f |= ZOF_ARG;
3560 p += 1;
3561 if gnu {
3562 f |= if name.len() > 1 { ZOF_GNUL } else { ZOF_GNUS };
3563 }
3564 if p < bytes.len() && bytes[p] == b':' {
3565 p += 1;
3566 f |= ZOF_OPT;
3567 }
3568 if p < bytes.len() && bytes[p] == b'-' {
3569 p += 1;
3570 f |= ZOF_SAME;
3571 }
3572 }
3573 // c:1913-1930 — `=ARR` suffix → bind to named array.
3574 let mut arr_name: Option<String> = None;
3575 if p < bytes.len() && bytes[p] == b'=' {
3576 p += 1;
3577 let arr = std::str::from_utf8(&bytes[p..]).unwrap_or("").to_string();
3578 if !named_arrays.contains(&arr) {
3579 named_arrays.push(arr.clone());
3580 }
3581 arr_name = Some(arr);
3582 f |= flags_map;
3583 } else if p < bytes.len() {
3584 zwarnnam(nam, &format!("invalid option description: {}", raw));
3585 return 1;
3586 } else if defarr.is_none() && assoc.is_none() {
3587 zwarnnam(nam, &format!("no default array defined: {}", raw));
3588 return 1;
3589 }
3590 if descs.iter().any(|d| d.name == name) {
3591 zwarnnam(nam, &format!("option defined more than once: {}", name));
3592 return 1;
3593 }
3594 descs.push(Desc {
3595 name,
3596 flags: f,
3597 arr_name,
3598 vals: Vec::new(),
3599 });
3600 }
3601
3602 // Phase 4: walk params (c:1968-2072).
3603 let mut new_params: Vec<String> = Vec::new(); // -E -D rebuild
3604 let mut pi = 0usize;
3605 let mut stopped = false;
3606 while pi < params.len() {
3607 let o_raw = params[pi].clone();
3608 // Not an option (or `-` in GNU mode).
3609 if !o_raw.starts_with('-') || (gnu && o_raw == "-") {
3610 if extract {
3611 if del {
3612 new_params.push(o_raw);
3613 }
3614 pi += 1;
3615 continue;
3616 } else {
3617 stopped = true;
3618 break;
3619 }
3620 }
3621 // `--` or non-GNU `-`: end.
3622 if o_raw == "-" || o_raw == "--" {
3623 if del && extract {
3624 new_params.push(o_raw);
3625 }
3626 pi += 1;
3627 stopped = true;
3628 break;
3629 }
3630 // c:1978 — `if (!(d = lookup_opt(o + 1)))`. Faithful port of
3631 // `lookup_opt` (c:1652-1681), which walks `opt_descs`:
3632 //
3633 // if (p->flags & ZOF_GNUL) {
3634 // if (!strcmp(p->name, str) ||
3635 // (strpfx(p->name, str) && str[strlen(p->name)] == '='))
3636 // return p;
3637 // } else if (p->flags & ZOF_ARG) {
3638 // if (strpfx(p->name, str)) return p;
3639 // } else if (!strcmp(p->name, str))
3640 // return p;
3641 //
3642 // Two divergences fixed here:
3643 // 1. The ZOF_ARG arm is a bare PREFIX match — spec `foo:` must
3644 // match the word `-foobar` with optarg `bar` (the documented
3645 // "cuddled" style). The old inline test demanded `=` or
3646 // end-of-string, so every cuddled long optarg fell through to
3647 // the short-option scan and errored `bad option: -f`.
3648 // 2. C prepends each new desc (`d->next = opt_descs; opt_descs
3649 // = d;` c:1957-1958), so `opt_descs` is in REVERSE definition
3650 // order and lookup_opt sees the LAST-defined spec first. That
3651 // is exactly what the documented "with overlapping specs the
3652 // last matching spec wins" behaviour rests on, so iterate
3653 // `descs` in reverse.
3654 let body = &o_raw[1..];
3655 let whole_idx = descs
3656 .iter()
3657 .enumerate()
3658 .rev()
3659 .find(|(_, d)| {
3660 if d.flags & ZOF_GNUL != 0 {
3661 // c:1657-1659
3662 body == d.name
3663 || (body.starts_with(&d.name)
3664 && body.as_bytes().get(d.name.len()) == Some(&b'='))
3665 } else if d.flags & ZOF_ARG != 0 {
3666 // c:1669-1670
3667 body.starts_with(&d.name)
3668 } else {
3669 // c:1673-1674
3670 body == d.name
3671 }
3672 })
3673 .map(|(idx, _)| idx);
3674 let whole_match = whole_idx.is_some();
3675 if whole_match {
3676 let raw_idx = whole_idx.unwrap();
3677 let dn_len = descs[raw_idx].name.len();
3678 // c:Src/Modules/zutil.c:1648-1650 — `add_opt_val` calls
3679 // `map_opt_desc(d)` to resolve the `-M` alias chain
3680 // (`-foo=f` redirects --foo into spec f's array). zshrs
3681 // pushes inline at each match site; replicate the redirect
3682 // here so the value lands in the mapped target spec.
3683 //
3684 // C's map_opt_desc recurses through the chain (c:1635) so
3685 // multi-hop aliases (`-foo=bar -bar=baz`) resolve all the
3686 // way through. Prior Rust port stopped after the FIRST
3687 // hop. Re-walk iteratively here with a visited-set guard
3688 // so a cycle (a→b→a) returns the original raw_idx instead
3689 // of looping forever.
3690 let idx = {
3691 let mut cur_idx = raw_idx;
3692 let mut visited: std::collections::HashSet<usize> =
3693 std::collections::HashSet::new();
3694 loop {
3695 if !visited.insert(cur_idx) {
3696 // c:1631 cycle: fall back to raw_idx.
3697 cur_idx = raw_idx;
3698 break;
3699 }
3700 let cur = &descs[cur_idx];
3701 if cur.flags & ZOF_MAP == 0 {
3702 break;
3703 }
3704 let arr_name = cur.arr_name.clone().unwrap_or_default();
3705 if arr_name == cur.name {
3706 break;
3707 }
3708 let next = descs.iter().position(|d| d.name == arr_name);
3709 match next {
3710 Some(n) => cur_idx = n,
3711 None => break, // c:1622-1623 dead-end alias
3712 }
3713 }
3714 cur_idx
3715 };
3716 // c:2027-2058 — the flags/name tested in the whole-param arm
3717 // are the LOOKED-UP desc's (`d` from lookup_opt), not the
3718 // `-M` mapping target's: C applies `map_opt_desc` only inside
3719 // `add_opt_val` (c:1648-1650), after the arg has been decided.
3720 let dflags = descs[raw_idx].flags;
3721 let dname = descs[raw_idx].name.clone();
3722 if (dflags & ZOF_ARG) != 0 {
3723 let e = &body[dn_len..]; // pointer past name
3724 if (dflags & ZOF_GNUL) != 0 && e.starts_with('=') {
3725 // c:2031-2032 — `add_opt_val(d, ++e);`
3726 let arg = e[1..].to_string();
3727 push_val(&mut descs, raw_idx, idx, Some(arg), &mut val_seq);
3728 } else if !e.is_empty() {
3729 // c:2038-2039 — `add_opt_val(d, e);`
3730 push_val(&mut descs, raw_idx, idx, Some(e.to_string()), &mut val_seq);
3731 } else if (dflags & ZOF_OPT) == 0
3732 || ((dflags & (ZOF_GNUL | ZOF_GNUS)) == 0
3733 && pi + 1 < params.len()
3734 && !params[pi + 1].starts_with('-'))
3735 {
3736 // c:2044-2052 — `add_opt_val(d, *++pp);`
3737 if pi + 1 >= params.len() {
3738 // c:2047-2050 — `zwarnnam(nam, "missing argument for
3739 // option: -%s", d->name);`
3740 parse_diag(
3741 nam,
3742 &progname,
3743 &format!("missing argument for option: -{dname}"),
3744 );
3745 return 1; // c:2050
3746 }
3747 pi += 1;
3748 let arg = params[pi].clone();
3749 push_val(&mut descs, raw_idx, idx, Some(arg), &mut val_seq);
3750 } else {
3751 // c:2055 — `add_opt_val(d, NULL);`
3752 push_val(&mut descs, raw_idx, idx, None, &mut val_seq);
3753 }
3754 } else {
3755 // c:2058 — `add_opt_val(d, NULL);`
3756 push_val(&mut descs, raw_idx, idx, None, &mut val_seq);
3757 }
3758 pi += 1;
3759 continue;
3760 }
3761 // Fallback: each char as short opt. c:1980-2016.
3762 let chars: Vec<char> = o_raw[1..].chars().collect();
3763 let mut ci = 0usize;
3764 let mut consumed_param = true;
3765 while ci < chars.len() {
3766 let ch = chars[ci];
3767 let name1 = ch.to_string();
3768 let didx = descs.iter().position(|d| d.name == name1);
3769 let Some(idx) = didx else {
3770 if fail {
3771 // c:1982-1988:
3772 // if (fail) {
3773 // if (*o != '-' || o > *pp + 1)
3774 // zwarnnam(nam, "bad option: -%c", *o);
3775 // else
3776 // zwarnnam(nam, "bad option: -%s", o);
3777 // return 1;
3778 // }
3779 // `o > *pp + 1` is "not the first character after the
3780 // leading dash", i.e. `ci > 0`; the else arm therefore
3781 // only fires at ci == 0, where `o` and the whole body
3782 // are the same string (so `-a --x -z` reports `--x`
3783 // while `-a-xy` reports `--`).
3784 if ch != '-' || ci > 0 {
3785 parse_diag(nam, &progname, &format!("bad option: -{ch}"));
3786 // c:1983-1984
3787 } else {
3788 parse_diag(
3789 nam,
3790 &progname,
3791 &format!("bad option: -{}", chars.iter().collect::<String>()),
3792 ); // c:1986
3793 }
3794 return 1; // c:1987
3795 }
3796 consumed_param = false;
3797 break;
3798 };
3799 let dflags = descs[idx].flags;
3800 let dname = descs[idx].name.clone();
3801 if (dflags & ZOF_ARG) != 0 {
3802 if ci + 1 < chars.len() {
3803 // arg in same param: rest of chars — `add_opt_val(d, e)`
3804 let arg: String = chars[ci + 1..].iter().collect();
3805 push_val(&mut descs, idx, idx, Some(arg), &mut val_seq);
3806 break;
3807 } else if (dflags & ZOF_OPT) == 0
3808 || ((dflags & (ZOF_GNUL | ZOF_GNUS)) == 0
3809 && pi + 1 < params.len()
3810 && !params[pi + 1].starts_with('-'))
3811 {
3812 if pi + 1 >= params.len() {
3813 // c:2004-2007 — `zwarnnam(nam, "missing argument for
3814 // option: -%s", d->name);`
3815 parse_diag(
3816 nam,
3817 &progname,
3818 &format!("missing argument for option: -{dname}"),
3819 );
3820 return 1; // c:2007
3821 }
3822 pi += 1;
3823 let arg = params[pi].clone();
3824 push_val(&mut descs, idx, idx, Some(arg), &mut val_seq);
3825 } else {
3826 // missing optional optarg — `add_opt_val(d, NULL)`
3827 push_val(&mut descs, idx, idx, None, &mut val_seq);
3828 }
3829 } else {
3830 // boolean short opt — `add_opt_val(d, NULL)`
3831 push_val(&mut descs, idx, idx, None, &mut val_seq);
3832 }
3833 ci += 1;
3834 }
3835 if !consumed_param {
3836 if extract {
3837 if del {
3838 new_params.push(o_raw);
3839 }
3840 pi += 1;
3841 continue;
3842 } else {
3843 stopped = true;
3844 break;
3845 }
3846 }
3847 pi += 1;
3848 }
3849 let _ = stopped;
3850 // c:2069 — append remaining params if extract+del.
3851 if extract && del {
3852 while pi < params.len() {
3853 new_params.push(params[pi].clone());
3854 pi += 1;
3855 }
3856 } else if del && !extract {
3857 // c:2129: setaparam(paramsname, pp) — what's left from pi.
3858 new_params = params[pi..].to_vec();
3859 }
3860
3861 // Phase 5: emit per-array results. c:2073-2088.
3862 // C iterates `a->vals` — a single linked list per array that
3863 // was appended to in argv-encounter order across ALL specs
3864 // pointing at the same array. We mirror by collecting (seq,
3865 // name, arg) across all descs that share a target array,
3866 // sorting by seq, and flattening into [name, arg?, name,
3867 // arg?, …] form.
3868 // c:2076-2084 — per-val emission:
3869 //
3870 // if (v->str)
3871 // *ap = ztrdup(v->str);
3872 // else {
3873 // *ap = ztrdup(v->name);
3874 // if (v->arg)
3875 // *++ap = ztrdup(v->arg);
3876 // }
3877 //
3878 // v->str (built in add_opt_val c:1665-1676) is the COMBINED
3879 // single element for ZOF_OPT/ZOF_SAME/GNUL options; plain
3880 // mandatory-arg options emit (name, arg) as two elements.
3881 let mut arr_buckets: std::collections::BTreeMap<
3882 String,
3883 Vec<(usize, String, Option<String>, Option<String>)>,
3884 > = std::collections::BTreeMap::new();
3885 for d in &descs {
3886 let target = d.arr_name.clone().or_else(|| defarr.clone());
3887 let Some(tgt) = target else { continue };
3888 let entry = arr_buckets.entry(tgt).or_default();
3889 for v in &d.vals {
3890 entry.push((v.seq, v.name.clone(), v.arg.clone(), v.str_.clone()));
3891 }
3892 }
3893 for (name, mut bucket) in arr_buckets {
3894 bucket.sort_by_key(|t| t.0);
3895 let mut out: Vec<String> = Vec::with_capacity(bucket.len() * 2);
3896 for (_seq, n, a, s) in bucket {
3897 if let Some(combined) = s {
3898 out.push(combined); // c:2077-2078 v->str one-element form
3899 } else {
3900 out.push(n); // c:2080
3901 if let Some(av) = a {
3902 out.push(av); // c:2081-2082
3903 }
3904 }
3905 }
3906 // c:2062-2068 — the `-M` pass that marks a mapping TARGET as
3907 // "not a real array":
3908 //
3909 // if (flags & ZOF_MAP) {
3910 // for (d = opt_descs; d; d = d->next)
3911 // if (d->arr && !d->vals && (d->flags & ZOF_MAP)) {
3912 // if (d->arr->num == 0 && get_opt_desc(d->arr->name))
3913 // d->arr->num = -1; /* this is not a real array */
3914 // }
3915 // }
3916 //
3917 // …and c:2073 `if (a->num >= 0 && …)` then skips it. Under `-M`
3918 // the `=NAME` suffix names ANOTHER SPEC, not an array, so
3919 // `zparseopts -M -a optv - a:=-aaa -aaa:` must NOT try to assign a
3920 // parameter called `-aaa` (zshrs errored `not an identifier:
3921 // -aaa`). The array is skipped only when it collected nothing AND
3922 // a spec of that name exists — a genuine array of the same name
3923 // that DID collect values is still emitted, as in C.
3924 let is_map_target =
3925 flags_map & ZOF_MAP != 0 && out.is_empty() && descs.iter().any(|d| d.name == name);
3926 if !is_map_target && (!keep || !out.is_empty()) {
3927 setaparam(&name, out);
3928 }
3929 }
3930
3931 // c:2089-2123 — assoc emission.
3932 if let Some(aname) = assoc {
3933 let mut flat: Vec<String> = Vec::new();
3934 for d in &descs {
3935 if d.vals.is_empty() {
3936 continue;
3937 }
3938 flat.push(format!("-{}", d.name));
3939 // c:2110-2117 — the value-assembly loop:
3940 //
3941 // for (v = d->vals; v; v = v->onext) {
3942 // if (v->arg) {
3943 // strcpy(n, v->arg);
3944 // n += strlen(v->arg);
3945 // }
3946 // *n = ' ';
3947 // }
3948 // *n = '\0';
3949 //
3950 // `*n = ' '` does NOT advance n, so the space it writes is
3951 // overwritten by the next iteration's strcpy (and the final
3952 // one by the c:2117 NUL). Net effect: multi-occurrence
3953 // option args CONCATENATE with NO separator. Verified
3954 // against real zsh: `zparseopts -A opts x+:` with
3955 // `-x a -x b` → opts[-x]="ab". Prior port joined with a
3956 // space ("a b"), inventing a separator the C never emits.
3957 let joined: String = d
3958 .vals
3959 .iter()
3960 .filter_map(|v| v.arg.clone())
3961 .collect::<Vec<_>>()
3962 .concat();
3963 flat.push(joined);
3964 }
3965 if !keep || !flat.is_empty() {
3966 // c:2096-2097 — `if (!keep || num) {
3967 // ap = zalloc_default_array(&aval, assoc, keep, num);`
3968 // zalloc_default_array (c:1709-1735) PREPENDS the assoc's
3969 // existing key/value pairs when keep is set: `-K -A assoc`
3970 // merges — unmatched existing keys survive, while new pairs
3971 // (appended after) override matching keys via normal assoc
3972 // assignment semantics. Prior port replaced the whole assoc
3973 // with only the new pairs, dropping every unmatched key.
3974 if keep {
3975 // c:1715-1728 — fetch existing pairs, copy first.
3976 let existing: Vec<String> = crate::ported::params::paramtab_hashed_storage()
3977 .lock()
3978 .ok()
3979 .and_then(|store| {
3980 store.get(&aname).map(|m| {
3981 let mut kv = Vec::with_capacity(m.len() * 2);
3982 for (k, v) in m {
3983 kv.push(k.clone()); // c:1723-1726
3984 kv.push(v.clone());
3985 }
3986 kv
3987 })
3988 })
3989 .unwrap_or_default();
3990 let mut merged = existing;
3991 merged.extend(flat); // new pairs after → override same keys
3992 sethparam(&aname, merged); // c:2121
3993 } else {
3994 sethparam(&aname, flat); // c:2121
3995 }
3996 }
3997 }
3998
3999 // c:2124-2131 — write back when `-D` was given.
4000 // extract (`-E`) → `np` (collected non-flag args)
4001 // non-extract → `pp` (remainder of original args from the
4002 // stop point onward)
4003 if del {
4004 let write_back: Vec<String> = if extract {
4005 new_params.clone()
4006 } else {
4007 // pi points at the arg that stopped the parse (or past
4008 // the end if we consumed everything cleanly). Everything
4009 // from pi onward is the unprocessed tail = `pp` in C.
4010 params.iter().skip(pi).cloned().collect()
4011 };
4012 if params_src == "argv" {
4013 crate::ported::exec::set_pparams(write_back.clone());
4014 if let Ok(mut pp_lock) = PPARAMS.lock() {
4015 *pp_lock = write_back;
4016 }
4017 } else {
4018 setaparam(¶ms_src, write_back);
4019 }
4020 } else {
4021 let _ = params;
4022 }
4023 let _ = stopped; // value already encoded in pi position
4024
4025 0
4026}
4027
4028// `bintab` — port of `static struct builtin bintab[]` (zutil.c).
4029
4030// `module_features` — port of `static struct features module_features`
4031// from zutil.c:2143.
4032
4033/// Port of `setup_(UNUSED(Module m))` from `Src/Modules/zutil.c:2152`.
4034#[allow(unused_variables)]
4035pub fn setup_(m: *const module) -> i32 {
4036 // c:2152
4037 0
4038}
4039
4040/// Port of `features_(UNUSED(Module m), UNUSED(char ***features))` from `Src/Modules/zutil.c:2161`.
4041/// C body: `*features = featuresarray(m, &module_features); return 0;`
4042pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 {
4043 // c:2161
4044 *features = featuresarray(m, module_features());
4045 0
4046}
4047
4048/// Port of `enables_(UNUSED(Module m), UNUSED(int **enables))` from `Src/Modules/zutil.c:2169`.
4049/// C body: `return handlefeatures(m, &module_features, enables);`
4050pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
4051 // c:2169
4052 handlefeatures(m, module_features(), enables)
4053}
4054
4055/// Port of `boot_(UNUSED(Module m))` from `Src/Modules/zutil.c:2176`.
4056#[allow(unused_variables)]
4057pub fn boot_(m: *const module) -> i32 {
4058 // c:2176
4059 0
4060}
4061
4062/// Port of `cleanup_(UNUSED(Module m))` from `Src/Modules/zutil.c:2183`.
4063/// C body: `return setfeatureenables(m, &module_features, NULL);`
4064pub fn cleanup_(m: *const module) -> i32 {
4065 // c:2183
4066 setfeatureenables(m, module_features(), None)
4067}
4068
4069/// Port of `finish_(UNUSED(Module m))` from `Src/Modules/zutil.c:2190`.
4070#[allow(unused_variables)]
4071pub fn finish_(m: *const module) -> i32 {
4072 // c:2190
4073 0
4074}
4075// zstyle_entry is defined below (moved from vm_helper).
4076
4077/// Save/restore for the per-pattern-match magic vars `$match`,
4078/// `$mbegin`, `$mend`. Direct port of `MatchData` and the
4079/// `savematch`/`restorematch`/`freematch` trio in
4080/// src/zsh/Src/Modules/zutil.c:33-80.
4081///
4082/// zstyle's `-e` (eval pattern on retrieve) and zregexparse's
4083/// inner pattern matches both want to evaluate patterns without
4084/// clobbering the caller's `$match[]`, `$mbegin[]`, `$mend[]`
4085/// variables. The C version keeps a heap-duplicated copy in a
4086/// `MatchData` struct, runs the inner match, then either
4087/// restores or frees. The Rust port stores `Option<Vec<String>>`
4088/// — `None` means the var was unset.
4089pub struct MatchData {
4090 pub r#match: Option<Vec<String>>,
4091 /// `mbegin` field.
4092 pub mbegin: Option<Vec<String>>,
4093 /// `mend` field.
4094 pub mend: Option<Vec<String>>,
4095}
4096
4097/// `zstyle` storage table.
4098/// Port of the `zstyletab` HashTable Src/Modules/zutil.c builds —
4099/// `newzstyletable()` (line 270) creates it, `bin_zstyle()`
4100/// (line 487) drives every mutation. Stores `stypat` entries
4101/// (port of C `struct stypat`, zutil.c:95) per style name,
4102/// weight-sorted so the most specific pattern wins.
4103// `StyleTable` renamed to `style_table`. C uses `HashTable zstyletab`
4104// (`Src/Modules/zutil.c:209`) with `struct style` (zutil.c:91) nodes
4105// containing a `Stypat pats` linked list (zutil.c:97-104). Rust port
4106// uses a `HashMap<String, Vec<stypat>>` while the canonical
4107// `hashtable` port lands; the canonical `style` / `stypat` structs
4108// already exist at lines 1608 / 1596 below.
4109/// `style_table` — see fields for layout.
4110#[allow(non_camel_case_types)]
4111#[derive(Default)]
4112pub struct style_table {
4113 /// `styles` field.
4114 ///
4115 /// Copy-on-write (`crate::cow_map::CowHashMap`): `$( … )` and `( … )`
4116 /// snapshot this table on entry (`crate::ported::exec::SubshForkCopy`),
4117 /// so `clone()` must be a refcount bump. A compsys configuration runs
4118 /// to hundreds of styles; only a body that runs `zstyle` pays for a copy.
4119 styles: crate::cow_map::CowHashMap<String, Vec<stypat>>,
4120 /// !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
4121 /// style -> context -> index of the first pattern (in weight order) that
4122 /// matched, or None. C pattries every pattern on every lookup
4123 /// (c:452-458); a completion asks the same few hundred (style, context)
4124 /// pairs on every TAB. The answer depends only on the table, since each
4125 /// prog is compiled once in `set`, so `set` and `delete` drop the memo
4126 /// and a clone starts with an empty one.
4127 memo: Mutex<HashMap<String, HashMap<String, Option<usize>>>>,
4128}
4129
4130impl Clone for style_table {
4131 fn clone(&self) -> Self {
4132 Self {
4133 styles: self.styles.clone(),
4134 memo: Mutex::default(),
4135 }
4136 }
4137}
4138
4139/// Namespace for the recursive zformat walker — distinct from
4140/// the public zformat_substring entry point above so the inner
4141/// recursion doesn't collide with the outer wrapper's name.
4142struct ZFormat;
4143
4144// ─── moved from src/ported/vm_helper (drift extraction) ───
4145
4146/// One `zstyle` entry — Rust extension that flattens what C splits
4147/// across `struct style` (zutil.c:91, holds the style name) and
4148/// `struct stypat` (zutil.c:97, holds pat + vals). The canonical
4149/// split structs are at lines 1596 / 1608 above; this flat shape is
4150/// kept while the C-style HashTable port lands.
4151#[allow(non_camel_case_types)]
4152#[derive(Debug, Clone)]
4153pub struct zstyle_entry {
4154 /// `pattern` field.
4155 pub pattern: String,
4156 /// `style` field.
4157 pub style: String,
4158 /// `values` field.
4159 pub values: Vec<String>,
4160}
4161
4162/// Port of `RParseState` from `Src/Modules/zutil.c:1093-1100`.
4163/// One node in the zregexparse state machine.
4164#[allow(non_camel_case_types)]
4165#[derive(Default)]
4166pub struct RParseState {
4167 pub cutoff: i32, // c:1094
4168 pub pattern: Option<String>, // c:1095
4169 // c:1096 — `Patprog patprog;` compiled on first match. The
4170 // zsh_h::Patprog alias (`Box<patprog>`) doesn't carry the
4171 // post-tokenisation buffer that pattern.rs::patcompile bundles
4172 // (Box<(patprog, Vec<u8>)>) — the byte buffer holds the compiled
4173 // pattern image referenced by `patprog::p`. Use the canonical
4174 // pattern::Patprog so pattry() reads the right shape end-to-end.
4175 /// `patprog` field.
4176 pub patprog: Option<crate::ported::pattern::Patprog>,
4177 pub guard: Option<String>, // c:1097
4178 pub action: Option<String>, // c:1098
4179 pub branches: Vec<std::rc::Rc<std::cell::RefCell<RParseBranch>>>, // c:1099
4180}
4181
4182/// Port of `RParseBranch` from `Src/Modules/zutil.c:1102-1105`.
4183/// One transition: target state + action list to run when taken.
4184#[allow(non_camel_case_types)]
4185pub struct RParseBranch {
4186 pub state: std::rc::Rc<std::cell::RefCell<RParseState>>, // c:1103
4187 pub actions: Vec<String>, // c:1104
4188}
4189
4190/// Port of `RParseResult` from `Src/Modules/zutil.c:1107-1111`.
4191/// nullacts = actions that fire on empty match; in/out = branch lists
4192/// for the entry/exit transitions of this sub-parse.
4193#[derive(Default)]
4194pub struct RParseResult {
4195 pub nullacts: Option<Vec<String>>, // c:1108 (None = NULL)
4196 pub in_: Vec<std::rc::Rc<std::cell::RefCell<RParseBranch>>>, // c:1109
4197 pub out: Vec<std::rc::Rc<std::cell::RefCell<RParseBranch>>>, // c:1110
4198 /// Legacy field — kept until callers migrate off the old shape.
4199 pub args: Vec<String>,
4200}
4201
4202/// `rparseargs` — C global at zutil.c:1113. Cursor into the input
4203/// argv being parsed. Thread-local per zsh evaluator.
4204thread_local! {
4205 /// `RPARSEARGS` static.
4206 pub static RPARSEARGS: std::cell::RefCell<std::collections::VecDeque<String>>
4207 = const { std::cell::RefCell::new(std::collections::VecDeque::new()) };
4208}
4209
4210/// `rparsestates` — C global at zutil.c:1114. List of all states
4211/// allocated during a parse run (so they can be freed by popheap).
4212/// Rust drops the Rc'd states when this list is cleared.
4213thread_local! {
4214 /// `RPARSESTATES` static.
4215 pub static RPARSESTATES: std::cell::RefCell<Vec<std::rc::Rc<std::cell::RefCell<RParseState>>>>
4216 = const { std::cell::RefCell::new(Vec::new()) };
4217}
4218
4219/// Port of `setstypat(Style s, char *pat, Patprog prog, char **vals, int eval)` from `Src/Modules/zutil.c:814`.
4220/// Format a string with specifications
4221/// `zformat` builtin entry point.
4222/// Helper extracted from `bin_zformat()` (Src/Modules/zutil.c:814)
4223/// — same `%X:value` substitution + width / left/right-align /
4224/// repeat flag handling the C source's `zformat_substring()`
4225/// (line 814) implements.
4226pub fn zformat_substring(format: &str, specs: &HashMap<char, String>, presence: bool) -> String {
4227 // Direct port of src/zsh/Src/Modules/zutil.c:814
4228 // zformat_substring. Recursive walker that handles:
4229 // - Plain `%X` substitutions
4230 // - Optional `-` for right-align
4231 // - Optional `N` for min width
4232 // - Optional `.M` for max width
4233 // - Ternary `%(SPECTEST.true-text.false-text)` — conditional
4234 // substitution based on whether the spec exists / matches a
4235 // numeric test value. With presence=true (zformat -F) the
4236 // test compares the spec's existence/length; with
4237 // presence=false (zformat -f) the test compares against an
4238 // integer math eval of the spec value.
4239 //
4240 // The original C uses an output-buffer with growable backing;
4241 // we use a Rust String with push_* helpers. The recursive
4242 // descent + (skip || actval) pattern is the same.
4243 // bin_zformat (c:1026-1032) never registers `%` or `)` as specs — it
4244 // rejects them as spec names. `%%` → `%` and `%)` → `)` come from the
4245 // unwind at c:849-856 inside the walker, which also keeps a lone
4246 // trailing `%` and an unterminated `%(` as literal text.
4247 let effective = specs;
4248
4249 let bytes: Vec<char> = format.chars().collect();
4250 let mut out = String::with_capacity(bytes.len() + 16);
4251 let mut idx = 0;
4252 let _ = ZFormat::substring(
4253 &bytes, &mut idx, &mut out, '\0', &effective, presence, false, &HashMap::new(), false,
4254 );
4255 out
4256}
4257
4258static MODULE_FEATURES: OnceLock<Mutex<features>> = OnceLock::new();
4259
4260// Local stubs for the per-module entry points. C uses generic
4261// `featuresarray`/`handlefeatures`/`setfeatureenables` (module.c:
4262// 3275/3370/3445) but those take `Builtin` + `Features` pointer
4263// fields the Rust port doesn't carry. The hardcoded descriptor
4264// list mirrors the C bintab/conddefs/mathfuncs/paramdefs.
4265// WARNING: NOT IN ZUTIL.C — Rust-only module-framework shim.
4266// C uses generic featuresarray/handlefeatures/setfeatureenables from
4267// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
4268// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
4269fn featuresarray(_m: *const module, _f: &Mutex<features>) -> Vec<String> {
4270 vec![
4271 "b:zformat".to_string(),
4272 "b:zparseopts".to_string(),
4273 "b:zregexparse".to_string(),
4274 "b:zstyle".to_string(),
4275 ]
4276}
4277
4278// WARNING: NOT IN ZUTIL.C — Rust-only module-framework shim.
4279// C uses generic featuresarray/handlefeatures/setfeatureenables from
4280// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
4281// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
4282fn handlefeatures(m: *const module, f: &Mutex<features>, enables: &mut Option<Vec<i32>>) -> i32 {
4283 // c:3392 — the name-keyed variant in src/ported/module.rs; this
4284 // module ships no `Features` descriptor tables for the per-feature
4285 // ADDED bit to live on (see MODULE_FEATURE_ENABLES there).
4286 crate::ported::module::handlefeatures("zsh/zutil", &featuresarray(m, f), enables)
4287}
4288
4289// WARNING: NOT IN ZUTIL.C — Rust-only module-framework shim.
4290// C uses generic featuresarray/handlefeatures/setfeatureenables from
4291// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
4292// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
4293fn setfeatureenables(_m: *const module, _f: &Mutex<features>, _e: Option<&[i32]>) -> i32 {
4294 0
4295}
4296
4297// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4298// ─── RUST-ONLY ACCESSORS ───
4299//
4300// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
4301// RwLock<T>>` globals declared above. C zsh uses direct global
4302// access; Rust needs these wrappers because `OnceLock::get_or_init`
4303// is the only way to lazily construct shared state. These ported sit
4304// here so the body of this file reads in C source order without
4305// the accessor wrappers interleaved between real port ported.
4306// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4307
4308// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4309// ─── RUST-ONLY ACCESSORS ───
4310//
4311// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
4312// RwLock<T>>` globals declared above. C zsh uses direct global
4313// access; Rust needs these wrappers because `OnceLock::get_or_init`
4314// is the only way to lazily construct shared state. These ported sit
4315// here so the body of this file reads in C source order without
4316// the accessor wrappers interleaved between real port ported.
4317// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4318
4319// WARNING: NOT IN ZUTIL.C — Rust-only module-framework shim.
4320// C uses generic featuresarray/handlefeatures/setfeatureenables from
4321// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
4322// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
4323fn module_features() -> &'static Mutex<features> {
4324 MODULE_FEATURES.get_or_init(|| {
4325 Mutex::new(features {
4326 bn_list: None,
4327 bn_size: 4,
4328 cd_list: None,
4329 cd_size: 0,
4330 mf_list: None,
4331 mf_size: 0,
4332 pd_list: None,
4333 pd_size: 0,
4334 n_abstract: 0,
4335 })
4336 })
4337}
4338
4339#[cfg(test)]
4340mod rparse_tests {
4341 use super::*;
4342
4343 /// `rparseelt` parses `/pat/` as a single pattern atom: result.in_ and
4344 /// result.out each get one branch pointing at the same RParseState,
4345 /// nullacts stays None (a literal pattern always consumes).
4346 #[test]
4347 fn rparseelt_slash_pattern_atom() {
4348 RPARSEARGS.with(|q| {
4349 let mut q = q.borrow_mut();
4350 q.clear();
4351 q.push_back("/foo/".to_string());
4352 });
4353 RPARSESTATES.with(|s| s.borrow_mut().clear());
4354 let mut r = RParseResult::default();
4355 assert_eq!(rparseelt(&mut r), 0);
4356 assert_eq!(r.in_.len(), 1);
4357 assert_eq!(r.out.len(), 1);
4358 assert!(r.nullacts.is_none());
4359 // The two branches share the same state Rc — the C source's
4360 // in/out branch lists both point at the same RParseState.
4361 assert!(std::rc::Rc::ptr_eq(
4362 &r.in_[0].borrow().state,
4363 &r.out[0].borrow().state,
4364 ));
4365 // State's pattern is wrapped per c:1189-1201.
4366 let st = r.in_[0].borrow().state.clone();
4367 assert_eq!(st.borrow().pattern.as_deref(), Some("(#b)((#B)foo)*"));
4368 }
4369
4370 /// `rparseelt` parses `/pat/+` or `/pat/-` (cutoff variants).
4371 #[test]
4372 fn rparseelt_cutoff_variants() {
4373 for suffix in &['+', '-'] {
4374 RPARSEARGS.with(|q| {
4375 let mut q = q.borrow_mut();
4376 q.clear();
4377 q.push_back(format!("/foo/{}", suffix));
4378 });
4379 RPARSESTATES.with(|s| s.borrow_mut().clear());
4380 let mut r = RParseResult::default();
4381 assert_eq!(rparseelt(&mut r), 0);
4382 let st = r.in_[0].borrow().state.clone();
4383 assert_eq!(st.borrow().cutoff, *suffix as i32);
4384 }
4385 }
4386
4387 /// `/pat/` followed by `:action` attaches the action to the state.
4388 #[test]
4389 fn rparseelt_pattern_with_action() {
4390 RPARSEARGS.with(|q| {
4391 let mut q = q.borrow_mut();
4392 q.clear();
4393 q.push_back("/foo/".to_string());
4394 q.push_back(":print hello".to_string());
4395 });
4396 RPARSESTATES.with(|s| s.borrow_mut().clear());
4397 let mut r = RParseResult::default();
4398 assert_eq!(rparseelt(&mut r), 0);
4399 let st = r.in_[0].borrow().state.clone();
4400 assert_eq!(st.borrow().action.as_deref(), Some("print hello"));
4401 }
4402
4403 /// `[]` as the pattern means no compiled pattern (NULL).
4404 #[test]
4405 fn rparseelt_empty_brackets_no_pattern() {
4406 RPARSEARGS.with(|q| {
4407 let mut q = q.borrow_mut();
4408 q.clear();
4409 q.push_back("/[]/".to_string());
4410 });
4411 RPARSESTATES.with(|s| s.borrow_mut().clear());
4412 let mut r = RParseResult::default();
4413 assert_eq!(rparseelt(&mut r), 0);
4414 let st = r.in_[0].borrow().state.clone();
4415 assert!(st.borrow().pattern.is_none());
4416 }
4417
4418 /// `rparseclo` on `/foo/ #` (atom + Kleene-star marker) sets
4419 /// `nullacts = []` (an empty list = match-on-empty enabled) and
4420 /// connects out→in via `connectstates`.
4421 #[test]
4422 fn rparseclo_kleene_marks_nullacts() {
4423 RPARSEARGS.with(|q| {
4424 let mut q = q.borrow_mut();
4425 q.clear();
4426 q.push_back("/foo/".to_string());
4427 q.push_back("#".to_string());
4428 });
4429 RPARSESTATES.with(|s| s.borrow_mut().clear());
4430 let mut r = RParseResult::default();
4431 assert_eq!(rparseclo(&mut r), 0);
4432 // c:1263 — `result->nullacts = newlinklist();` (an empty list).
4433 assert!(matches!(r.nullacts, Some(ref v) if v.is_empty()));
4434 }
4435
4436 /// `rparseclo` on `/foo/` WITHOUT a trailing `#` leaves
4437 /// `nullacts = None` (no empty match allowed). The C body's
4438 /// c:1257 `if (*rparseargs && !strcmp(*rparseargs, "#"))` gate
4439 /// is the load-bearing condition — without `#`, nullacts stays
4440 /// at whatever rparseelt set it to (which is NULL for pattern
4441 /// atoms per c:1220). The matcher distinguishes `None` (must
4442 /// consume at least one char) from `Some([])` (empty match OK).
4443 /// A regression that always sets nullacts to Some([]) would
4444 /// silently accept empty matches for non-Kleene patterns,
4445 /// breaking the `zregexparse var var '/foo/'` semantic where
4446 /// the empty subject must NOT match.
4447 #[test]
4448 fn rparseclo_no_kleene_keeps_nullacts_none() {
4449 RPARSEARGS.with(|q| {
4450 let mut q = q.borrow_mut();
4451 q.clear();
4452 q.push_back("/foo/".to_string());
4453 // NO `#` — atom alone.
4454 });
4455 RPARSESTATES.with(|s| s.borrow_mut().clear());
4456 let mut r = RParseResult::default();
4457 assert_eq!(rparseclo(&mut r), 0);
4458 assert!(
4459 r.nullacts.is_none(),
4460 "c:1257 — without trailing `#`, nullacts must stay None \
4461 (empty-match must NOT be allowed for non-Kleene atoms)"
4462 );
4463 }
4464
4465 /// `rparseseq` on `{init} /pat/` collects the action into nullacts
4466 /// AND into the single output branch's action list (c:1313-1317).
4467 #[test]
4468 fn rparseseq_action_block_then_pattern() {
4469 RPARSEARGS.with(|q| {
4470 let mut q = q.borrow_mut();
4471 q.clear();
4472 q.push_back("{init}".to_string());
4473 q.push_back("/foo/".to_string());
4474 });
4475 RPARSESTATES.with(|s| s.borrow_mut().clear());
4476 let mut r = RParseResult::default();
4477 assert_eq!(rparseseq(&mut r), 0);
4478 // First the action populates result.nullacts; the pattern then
4479 // arrives and gets connected.
4480 // The pattern's out branch should NOT carry the action because
4481 // the action was consumed before the pattern's out list existed.
4482 assert_eq!(r.out.len(), 1);
4483 }
4484
4485 /// `rparsealt` on `/a/ | /b/` builds a 2-way alternation: 2 in branches
4486 /// + 2 out branches.
4487 #[test]
4488 fn rparsealt_two_way() {
4489 RPARSEARGS.with(|q| {
4490 let mut q = q.borrow_mut();
4491 q.clear();
4492 for s in ["/a/", "|", "/b/"] {
4493 q.push_back(s.to_string());
4494 }
4495 });
4496 RPARSESTATES.with(|s| s.borrow_mut().clear());
4497 let mut r = RParseResult::default();
4498 assert_eq!(rparsealt(&mut r), 0);
4499 assert_eq!(r.in_.len(), 2);
4500 assert_eq!(r.out.len(), 2);
4501 }
4502
4503 /// `rparseseq` on `/a/ /b/` connects out-of-a to in-of-b via
4504 /// `connectstates`. The transition branch must be appended to
4505 /// the FIRST pattern's state.branches list — that's where rmatch
4506 /// (c:1396) walks looking for the next state. A regression that
4507 /// stores the branch in the wrong place breaks every multi-step
4508 /// regex match.
4509 #[test]
4510 fn rparseseq_two_patterns_connects_via_first_state_branches() {
4511 RPARSEARGS.with(|q| {
4512 let mut q = q.borrow_mut();
4513 q.clear();
4514 for s in ["/a/", "/b/"] {
4515 q.push_back(s.to_string());
4516 }
4517 });
4518 RPARSESTATES.with(|s| s.borrow_mut().clear());
4519 let mut r = RParseResult::default();
4520 assert_eq!(rparseseq(&mut r), 0);
4521
4522 // After two-pattern sequence:
4523 // in_ = [branch → state_a]
4524 // out = [branch → state_b]
4525 // state_a.branches = [transition → state_b]
4526 assert_eq!(r.in_.len(), 1, "single entry");
4527 assert_eq!(r.out.len(), 1, "single exit");
4528
4529 let state_a = r.in_[0].borrow().state.clone();
4530 let state_b = r.out[0].borrow().state.clone();
4531 assert!(
4532 !std::rc::Rc::ptr_eq(&state_a, &state_b),
4533 "sequence creates distinct states for a and b"
4534 );
4535
4536 // c:1136 — connectstates appends a transition branch onto
4537 // outbranch.state.branches (= state_a.branches).
4538 let a_branches = &state_a.borrow().branches;
4539 assert_eq!(
4540 a_branches.len(),
4541 1,
4542 "c:1136 — state_a.branches must hold the a→b transition"
4543 );
4544 let transition_target = a_branches[0].borrow().state.clone();
4545 assert!(
4546 std::rc::Rc::ptr_eq(&transition_target, &state_b),
4547 "transition target must be state_b (the Rc graph is shared, not deep-copied)"
4548 );
4549 }
4550
4551 /// `(` introduces a grouped subexpression; matching `)` closes it.
4552 #[test]
4553 fn rparseelt_paren_group() {
4554 RPARSEARGS.with(|q| {
4555 let mut q = q.borrow_mut();
4556 q.clear();
4557 for s in ["(", "/x/", ")"] {
4558 q.push_back(s.to_string());
4559 }
4560 });
4561 RPARSESTATES.with(|s| s.borrow_mut().clear());
4562 let mut r = RParseResult::default();
4563 assert_eq!(rparseelt(&mut r), 0);
4564 // The inner pattern populated in/out.
4565 assert_eq!(r.in_.len(), 1);
4566 assert_eq!(r.out.len(), 1);
4567 // Cursor consumed all three args.
4568 assert!(RPARSEARGS.with(|q| q.borrow().is_empty()));
4569 }
4570
4571 /// `prependactions` inserts each act at the HEAD of each branch's
4572 /// actions list, preserving acts' order (acts[0] ends up at branch.actions[0]).
4573 #[test]
4574 fn prependactions_preserves_order() {
4575 let st = std::rc::Rc::new(std::cell::RefCell::new(RParseState::default()));
4576 let br = std::rc::Rc::new(std::cell::RefCell::new(RParseBranch {
4577 state: st,
4578 actions: vec!["x".to_string(), "y".to_string()],
4579 }));
4580 let acts = vec!["a".to_string(), "b".to_string()];
4581 prependactions(&acts, &[br.clone()]);
4582 assert_eq!(
4583 br.borrow().actions,
4584 vec![
4585 "a".to_string(),
4586 "b".to_string(),
4587 "x".to_string(),
4588 "y".to_string()
4589 ]
4590 );
4591 }
4592
4593 /// `appendactions` appends to the tail of each branch's actions list.
4594 #[test]
4595 fn appendactions_appends_to_tail() {
4596 let st = std::rc::Rc::new(std::cell::RefCell::new(RParseState::default()));
4597 let br = std::rc::Rc::new(std::cell::RefCell::new(RParseBranch {
4598 state: st,
4599 actions: vec!["x".to_string()],
4600 }));
4601 let acts = vec!["a".to_string(), "b".to_string()];
4602 appendactions(&acts, &[br.clone()]);
4603 assert_eq!(
4604 br.borrow().actions,
4605 vec!["x".to_string(), "a".to_string(), "b".to_string()]
4606 );
4607 }
4608
4609 /// `connectstates` produces the full M×N cross-product of branches:
4610 /// for each (outbranch, inbranch) pair it creates a fresh transition
4611 /// branch. The C body's nested for-loops at c:1123-1138 are O(M*N);
4612 /// a regression that uses one-to-one zip semantics (min(M,N) branches)
4613 /// would silently lose transitions for alternation patterns.
4614 ///
4615 /// Pin: 2 out-branches × 3 in-branches → 6 new transitions, all
4616 /// pointing at the original in-branches' states (Rc-shared).
4617 #[test]
4618 fn connectstates_produces_m_times_n_cross_product() {
4619 // Two out-branches, each rooted at its own state.
4620 let out_a = std::rc::Rc::new(std::cell::RefCell::new(RParseState::default()));
4621 let out_b = std::rc::Rc::new(std::cell::RefCell::new(RParseState::default()));
4622 let out = vec![
4623 std::rc::Rc::new(std::cell::RefCell::new(RParseBranch {
4624 state: out_a.clone(),
4625 actions: vec![],
4626 })),
4627 std::rc::Rc::new(std::cell::RefCell::new(RParseBranch {
4628 state: out_b.clone(),
4629 actions: vec![],
4630 })),
4631 ];
4632 // Three in-branches, each rooted at its own state.
4633 let in_states: Vec<_> = (0..3)
4634 .map(|_| std::rc::Rc::new(std::cell::RefCell::new(RParseState::default())))
4635 .collect();
4636 let in_: Vec<_> = in_states
4637 .iter()
4638 .map(|st| {
4639 std::rc::Rc::new(std::cell::RefCell::new(RParseBranch {
4640 state: st.clone(),
4641 actions: vec![],
4642 }))
4643 })
4644 .collect();
4645
4646 connectstates(&out, &in_);
4647
4648 // c:1136 — each new branch added to outbranch.state.branches.
4649 // out_a gets 3 transitions (one per in-branch);
4650 // out_b gets 3 transitions.
4651 assert_eq!(
4652 out_a.borrow().branches.len(),
4653 3,
4654 "c:1126-1136 — out_a must receive N=3 transitions"
4655 );
4656 assert_eq!(
4657 out_b.borrow().branches.len(),
4658 3,
4659 "c:1126-1136 — out_b must also receive N=3 transitions"
4660 );
4661
4662 // Each transition's target state must be one of the in-states
4663 // (Rc::ptr_eq verifies graph-sharing, not deep-copy).
4664 for br in out_a.borrow().branches.iter() {
4665 let target = br.borrow().state.clone();
4666 assert!(
4667 in_states.iter().any(|s| std::rc::Rc::ptr_eq(s, &target)),
4668 "c:1130 — transition target must be one of the in-branches' states"
4669 );
4670 }
4671 }
4672}
4673
4674#[cfg(test)]
4675mod tests {
4676 use super::*;
4677
4678 /// setstypat `-e` (eval) path (zutil.c:304-318): the joined values
4679 /// must be `parse_string`'d into a *real* Eprog and stored in
4680 /// `stypat.eval`. The prior port faked this with an empty
4681 /// `eprog::default()` (len == 0); this pins the real parse.
4682 #[test]
4683 fn setstypat_eval_stores_real_parsed_program() {
4684 // eval=0 → no program stored (c:341 with NULL eprog).
4685 setstypat("zt_noeval", ":zt:ctx:*", None, vec!["plain".to_string()], 0);
4686 // eval=1 → value parsed into a real, non-empty Eprog.
4687 setstypat(
4688 "zt_eval",
4689 ":zt:ctx:*",
4690 None,
4691 vec!["echo".to_string(), "hi".to_string()],
4692 1,
4693 );
4694
4695 let t = zstyletab.lock().unwrap();
4696 let noeval = t.styles["zt_noeval"]
4697 .iter()
4698 .find(|p| p.pat == ":zt:ctx:*")
4699 .expect("zt_noeval pattern stored");
4700 assert!(noeval.eval.is_none(), "eval=0 must store no program");
4701
4702 let eval = t.styles["zt_eval"]
4703 .iter()
4704 .find(|p| p.pat == ":zt:ctx:*")
4705 .expect("zt_eval pattern stored");
4706 let prog = eval.eval.as_ref().expect("eval=1 must store a program");
4707 assert!(
4708 prog.len > 0,
4709 "stored program must be the real parse, not an empty default (len was {})",
4710 prog.len
4711 );
4712 }
4713
4714 /// Verifies the weight formula matches C's setstypat (zutil.c:344-385):
4715 /// component count (high 32 bits) + per-component specificity sum
4716 /// (low 32 bits). More specific = higher weight. Drives weight via
4717 /// style_table::set's inline weight calc (insertion order reflects
4718 /// weight ordering — most specific pattern appears first).
4719 #[test]
4720 fn test_style_pattern_weight() {
4721 let _g = crate::test_util::global_state_lock();
4722 let mut t = style_table::new();
4723 t.set("*", "s", vec!["broad".to_string()], None);
4724 t.set(":completion:*", "s", vec!["mid".to_string()], None);
4725 t.set(":completion:zsh:*", "s", vec!["narrow".to_string()], None);
4726 // Most-specific match wins (sorted descending by weight at insertion).
4727 assert_eq!(t.get(":completion:zsh:complete", "s").unwrap()[0], "narrow");
4728 assert_eq!(t.get(":completion:bash:complete", "s").unwrap()[0], "mid");
4729 assert_eq!(t.get(":other:thing", "s").unwrap()[0], "broad");
4730 }
4731
4732 /// Port of `bin_zparseopts(char *nam, char **args, Options ops, UNUSED(int func))` from `Src/Modules/zutil.c:1821`.
4733 #[test]
4734 fn zof_flags_are_distinct_powers_of_two() {
4735 let _g = crate::test_util::global_state_lock();
4736 // c:1531-1538 — ZOF_* are independent bits in a single u8 field.
4737 let all = [
4738 ZOF_ARG, ZOF_OPT, ZOF_MULT, ZOF_SAME, ZOF_MAP, ZOF_CYC, ZOF_GNUS, ZOF_GNUL,
4739 ];
4740 let xor: i32 = all.iter().fold(0, |acc, &x| acc | x);
4741 let sum: i32 = all.iter().sum();
4742 assert_eq!(xor, sum, "ZOF_* bits must be disjoint");
4743 // Ensure each is a power of two.
4744 for v in all {
4745 assert!(
4746 v > 0 && (v & (v - 1)) == 0,
4747 "ZOF value {} is not a power of 2",
4748 v
4749 );
4750 }
4751 }
4752
4753 /// Verifies pattern matching via the style_table.get path mirrors
4754 /// C's lookupstyle (zutil.c:443) walking the pats list for the
4755 /// first weight-sorted match.
4756 #[test]
4757 fn test_style_pattern_matches() {
4758 let _g = crate::test_util::global_state_lock();
4759 let mut t = style_table::new();
4760 t.set(":completion:*", "s1", vec!["v".to_string()], None);
4761 assert!(t.get(":completion:zsh:complete", "s1").is_some());
4762 assert!(t.get(":other:zsh", "s1").is_none());
4763
4764 let mut t2 = style_table::new();
4765 t2.set("*", "s2", vec!["v".to_string()], None);
4766 assert!(t2.get("anything", "s2").is_some());
4767 }
4768
4769 #[test]
4770 fn test_style_table_set_get() {
4771 let _g = crate::test_util::global_state_lock();
4772 let mut table = style_table::new();
4773 table.set(":completion:*", "verbose", vec!["yes".to_string()], None);
4774
4775 let result = table.get(":completion:zsh", "verbose");
4776 assert_eq!(result, Some(&["yes".to_string()][..]));
4777
4778 let result = table.get(":other", "verbose");
4779 assert!(result.is_none());
4780 }
4781
4782 #[test]
4783 fn test_style_table_priority() {
4784 let _g = crate::test_util::global_state_lock();
4785 let mut table = style_table::new();
4786 table.set("*", "menu", vec!["no".to_string()], None);
4787 table.set(":completion:*", "menu", vec!["yes".to_string()], None);
4788
4789 let result = table.get(":completion:zsh", "menu");
4790 assert_eq!(result, Some(&["yes".to_string()][..]));
4791 }
4792
4793 #[test]
4794 fn test_style_table_delete() {
4795 let _g = crate::test_util::global_state_lock();
4796 let mut table = style_table::new();
4797 table.set("*", "style1", vec!["val".to_string()], None);
4798 table.set("*", "style2", vec!["val".to_string()], None);
4799
4800 table.delete(None, Some("style1"));
4801 assert!(table.get("test", "style1").is_none());
4802 assert!(table.get("test", "style2").is_some());
4803 }
4804
4805 #[test]
4806 fn test_style_test_bool() {
4807 let _g = crate::test_util::global_state_lock();
4808 let mut table = style_table::new();
4809 table.set("*", "enabled", vec!["yes".to_string()], None);
4810 table.set("*", "disabled", vec!["no".to_string()], None);
4811 table.set(
4812 "*",
4813 "multiple",
4814 vec!["a".to_string(), "b".to_string()],
4815 None,
4816 );
4817
4818 assert_eq!(table.test_bool("ctx", "enabled"), Some(true));
4819 assert_eq!(table.test_bool("ctx", "disabled"), Some(false));
4820 assert_eq!(table.test_bool("ctx", "multiple"), None);
4821 }
4822
4823 /// Port of `bin_zstyle(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))` from `Src/Modules/zutil.c:487`.
4824 /// Verifies the persistent global `zstyletab` round-trips
4825 /// set→get and that `lookupstyle` / `testforstyle` C-name shims
4826 /// see the same entry. Lock-stamps the global-state path that
4827 /// `bin_zstyle` relies on (Src/Modules/zutil.c:209).
4828 #[test]
4829 fn test_global_zstyletab_set_and_lookup() {
4830 let _g = crate::test_util::global_state_lock();
4831 let key_style = "test_zutil_global_marker_style";
4832 let key_pat = "test_zutil_global_marker_*";
4833 {
4834 let mut t = zstyletab.lock().unwrap();
4835 t.set(key_pat, key_style, vec!["yes".to_string()], None);
4836 }
4837 let found = lookupstyle("test_zutil_global_marker_x", key_style);
4838 assert_eq!(found, vec!["yes".to_string()]);
4839 assert_eq!(testforstyle("test_zutil_global_marker_x", key_style), 0);
4840 assert_eq!(testforstyle("unmatched_ctx", "no_such_style_zzz"), 1);
4841 // Cleanup so other tests don't see the entry.
4842 {
4843 let mut t = zstyletab.lock().unwrap();
4844 t.delete(Some(key_pat), Some(key_style));
4845 }
4846 }
4847
4848 #[test]
4849 fn test_zformat_basic() {
4850 let _g = crate::test_util::global_state_lock();
4851 let mut specs = HashMap::new();
4852 specs.insert('n', "test".to_string());
4853 specs.insert('v', "42".to_string());
4854
4855 let result = zformat_substring("Name: %n, Value: %v", &specs, false);
4856 assert_eq!(result, "Name: test, Value: 42");
4857 }
4858
4859 #[test]
4860 fn test_zformat_padding() {
4861 let _g = crate::test_util::global_state_lock();
4862 let mut specs = HashMap::new();
4863 specs.insert('n', "hi".to_string());
4864
4865 let result = zformat_substring("[%10n]", &specs, false);
4866 assert_eq!(result, "[hi ]");
4867
4868 let result = zformat_substring("[%-10n]", &specs, false);
4869 assert_eq!(result, "[ hi]");
4870 }
4871
4872 #[test]
4873 fn test_zformat_truncate() {
4874 let _g = crate::test_util::global_state_lock();
4875 let mut specs = HashMap::new();
4876 specs.insert('n', "hello world".to_string());
4877
4878 let result = zformat_substring("[%.5n]", &specs, false);
4879 assert_eq!(result, "[hello]");
4880 }
4881
4882 #[test]
4883 fn test_zformat_escape() {
4884 let _g = crate::test_util::global_state_lock();
4885 let specs = HashMap::new();
4886 let result = zformat_substring("100%%", &specs, false);
4887 assert_eq!(result, "100%");
4888 }
4889
4890 /// `Src/Modules/zutil.c:923-936` — Unknown spec character emits the
4891 /// original `%X` literal back into the output (not consumed). Pin
4892 /// the fallback so a regen that drops the unknown-spec branch would
4893 /// silently swallow `%z` when only `%n` was registered.
4894 #[test]
4895 fn zformat_unknown_spec_emits_literal_percent_x() {
4896 let _g = crate::test_util::global_state_lock();
4897 let mut specs = HashMap::new();
4898 specs.insert('n', "hello".to_string());
4899 // %z is unknown; must round-trip
4900 let result = zformat_substring("%z %n", &specs, false);
4901 assert_eq!(
4902 result, "%z hello",
4903 "c:923-936 — unknown spec emits raw `%X` segment"
4904 );
4905 }
4906
4907 /// `Src/Modules/zutil.c:825-826` — Right-align flag with explicit
4908 /// min-width. `%-5n` right-pads with spaces on the LEFT. Pin both
4909 /// arms (left/right) since a regen flipping the polarity would
4910 /// silently invert every zformat-prompted output.
4911 #[test]
4912 fn zformat_right_align_with_min_width() {
4913 let _g = crate::test_util::global_state_lock();
4914 let mut specs = HashMap::new();
4915 specs.insert('n', "ab".to_string());
4916 // Left-align (default): pad on RIGHT
4917 assert_eq!(zformat_substring("[%5n]", &specs, false), "[ab ]");
4918 // Right-align (-): pad on LEFT
4919 assert_eq!(zformat_substring("[%-5n]", &specs, false), "[ ab]");
4920 }
4921
4922 /// `Src/Modules/zutil.c:825-845` — Min + Max combined: `%5.10n`
4923 /// means right-pad to min=5, truncate at max=10. With value
4924 /// "hello world" (11 chars), max=10 truncates to "hello worl".
4925 #[test]
4926 fn zformat_min_max_combined() {
4927 let _g = crate::test_util::global_state_lock();
4928 let mut specs = HashMap::new();
4929 specs.insert('n', "hello world".to_string());
4930 // max=10 truncates the 11-char value
4931 let r = zformat_substring("[%5.10n]", &specs, false);
4932 assert_eq!(r, "[hello worl]");
4933 // Short value: min=5 right-pads (default = left-align)
4934 specs.insert('n', "hi".to_string());
4935 let r = zformat_substring("[%5.10n]", &specs, false);
4936 assert_eq!(
4937 r, "[hi ]",
4938 "c:828-845 — min-pad then max-truncate; short value gets the pad only"
4939 );
4940 }
4941
4942 /// `Src/Modules/zutil.c:975-976` — `%)` is pre-registered as `)`
4943 /// so the parser can emit a literal `)` from the user's format
4944 /// string. Pin the alias.
4945 #[test]
4946 fn zformat_close_paren_escape() {
4947 let _g = crate::test_util::global_state_lock();
4948 let specs = HashMap::new();
4949 let r = zformat_substring("a%)b", &specs, false);
4950 assert_eq!(r, "a)b", "c:975-976 — `%)` emits literal `)`");
4951 }
4952
4953 /// `Src/Modules/zutil.c:847-887` — Ternary
4954 /// `%(SPEC.true-text.false-text)` emits the FIRST branch ("true-text")
4955 /// when the spec exists. Per `man zshmodules`: "if the contents of
4956 /// the spec are present then true-text is output, otherwise
4957 /// false-text." With presence=true (zformat -F), spec-set means
4958 /// emit true-text.
4959 #[test]
4960 fn zformat_ternary_presence_mode_spec_set() {
4961 let _g = crate::test_util::global_state_lock();
4962 let mut specs = HashMap::new();
4963 specs.insert('s', "anything".to_string());
4964 // presence=true: spec exists → TRUE branch (first text).
4965 let r = zformat_substring("%(s.yes.no)", &specs, true);
4966 assert_eq!(
4967 r, "yes",
4968 "c:847-887 — spec set, presence=true → TRUE branch (true-text first)"
4969 );
4970 }
4971
4972 /// `Src/Modules/zutil.c:847-887` — Ternary with missing spec in
4973 /// presence-mode emits the SECOND branch ("false-text"). Per
4974 /// docs: "if contents of the spec are present then true-text is
4975 /// output, otherwise false-text."
4976 #[test]
4977 fn zformat_ternary_presence_mode_spec_unset() {
4978 let _g = crate::test_util::global_state_lock();
4979 let specs = HashMap::new();
4980 let r = zformat_substring("%(s.yes.no)", &specs, true);
4981 assert_eq!(
4982 r, "no",
4983 "c:847-887 — spec unset, presence=true → FALSE branch (false-text second)"
4984 );
4985 }
4986
4987 /// `Src/Modules/zutil.c:937-948` — Plain (non-`%`) bytes between
4988 /// specs emit verbatim. Pin the simplest no-spec passthrough.
4989 #[test]
4990 fn zformat_literal_text_passes_through() {
4991 let _g = crate::test_util::global_state_lock();
4992 let specs = HashMap::new();
4993 let r = zformat_substring("hello, world", &specs, false);
4994 assert_eq!(r, "hello, world");
4995 // Empty format → empty output
4996 let r = zformat_substring("", &specs, false);
4997 assert_eq!(r, "");
4998 }
4999
5000 /// `Src/Modules/zutil.c:890-922` — When max=0 (`.0`), the value is
5001 /// truncated to ZERO chars — but the min-pad still fires. Edge case
5002 /// that pins the order: truncate-then-pad, not pad-then-truncate.
5003 #[test]
5004 fn zformat_max_zero_truncates_to_empty_but_keeps_min_pad() {
5005 let _g = crate::test_util::global_state_lock();
5006 let mut specs = HashMap::new();
5007 specs.insert('n', "abc".to_string());
5008 let r = zformat_substring("[%3.0n]", &specs, false);
5009 assert_eq!(
5010 r, "[ ]",
5011 "c:890-922 — max=0 → empty value, min=3 → 3 spaces"
5012 );
5013 }
5014
5015 /// `Src/Modules/zutil.c:55-68` — `restorematch` MUST `unsetparam`
5016 /// each field that's None (not just leave it alone). Pin:
5017 /// pre-seed `$match` with a value, take a snapshot where it's None,
5018 /// call restorematch, observe `$match` is now UNSET in paramtab.
5019 #[test]
5020 fn restorematch_unsets_params_when_snapshot_is_none() {
5021 let _g = crate::test_util::global_state_lock();
5022 // Pre-seed `$match` so we can observe the unset.
5023 assignaparam("match", vec!["seed".to_string()], 0);
5024 assert!(getaparam("match").is_some(), "test setup: $match seeded");
5025
5026 // Snapshot with all three fields None — restorematch must
5027 // unsetparam each (c:60/64/68).
5028 let snap = MatchData {
5029 r#match: None,
5030 mbegin: None,
5031 mend: None,
5032 };
5033 restorematch(&snap);
5034 assert!(
5035 getaparam("match").is_none(),
5036 "c:60 — None snapshot must unsetparam(\"match\")"
5037 );
5038 }
5039
5040 // ─── zsh-corpus pins for zstyle helpers ──────────────────────────
5041
5042 /// `lookupstyle` on missing context returns empty Vec.
5043 #[test]
5044 fn zutil_corpus_lookupstyle_missing_returns_empty() {
5045 let _g = crate::test_util::global_state_lock();
5046 let r = lookupstyle(":zshrs:test:nonexistent_zxyz", "style_nope");
5047 assert!(r.is_empty(), "missing zstyle context returns empty");
5048 }
5049
5050 /// `testforstyle` on missing context returns 1 (not found).
5051 #[test]
5052 fn zutil_corpus_testforstyle_missing_returns_one() {
5053 let _g = crate::test_util::global_state_lock();
5054 let r = testforstyle(":zshrs:test:never_set_xyz", "style");
5055 assert_eq!(r, 1, "missing = 1 per c:485 return !found");
5056 }
5057
5058 /// `addstyle` on new name returns Some(style).
5059 #[test]
5060 fn zutil_corpus_addstyle_new_returns_some() {
5061 let _g = crate::test_util::global_state_lock();
5062 let s = addstyle("zshrs_test_new_style");
5063 assert!(s.is_some(), "addstyle for new name returns Some");
5064 }
5065
5066 /// `addstyle` returning Some on existing name (idempotent).
5067 #[test]
5068 fn zutil_corpus_addstyle_existing_returns_some() {
5069 let _g = crate::test_util::global_state_lock();
5070 let _ = addstyle("zshrs_test_dup_style");
5071 let s = addstyle("zshrs_test_dup_style");
5072 assert!(s.is_some(), "addstyle on existing name still returns Some");
5073 }
5074
5075 /// `newzstyletable` installs the c:275-287 function-pointer slots.
5076 #[test]
5077 fn zutil_corpus_newzstyletable_installs_slots() {
5078 let _g = crate::test_util::global_state_lock();
5079 let t = newzstyletable(17, "test_tab");
5080 assert_eq!((t.hsize, t.ct), (17, 0));
5081 assert!(t.hash.is_some() && t.emptytable.is_some() && t.cmpnodes.is_some());
5082 assert!(t.addnode.is_some() && t.getnode.is_some() && t.getnode2.is_some());
5083 assert!(t.removenode.is_some() && t.freenode.is_some() && t.printnode.is_some());
5084 assert!(t.filltable.is_none() && t.disablenode.is_none() && t.enablenode.is_none());
5085 }
5086
5087 // ═══════════════════════════════════════════════════════════════════
5088 // Additional C-parity tests for Src/Modules/zutil.c zformat_substring.
5089 // ═══════════════════════════════════════════════════════════════════
5090
5091 /// c:814 — `zformat_substring("", _, _)` returns empty.
5092 #[test]
5093 fn zformat_substring_empty_format_returns_empty() {
5094 let _g = crate::test_util::global_state_lock();
5095 let specs = std::collections::HashMap::new();
5096 let r = zformat_substring("", &specs, false);
5097 assert!(r.is_empty());
5098 }
5099
5100 /// c:814 — plain text with no `%` passes through verbatim.
5101 #[test]
5102 fn zformat_substring_plain_text_pass_through() {
5103 let _g = crate::test_util::global_state_lock();
5104 let specs = std::collections::HashMap::new();
5105 assert_eq!(zformat_substring("hello", &specs, false), "hello");
5106 assert_eq!(zformat_substring("abc def", &specs, false), "abc def");
5107 }
5108
5109 /// c:975 — `%%` produces literal `%` (pre-populated spec).
5110 #[test]
5111 fn zformat_substring_percent_percent_is_literal_percent() {
5112 let _g = crate::test_util::global_state_lock();
5113 let specs = std::collections::HashMap::new();
5114 let r = zformat_substring("%%", &specs, false);
5115 assert_eq!(r, "%");
5116 }
5117
5118 /// c:976 — `%)` produces literal `)` (pre-populated spec).
5119 #[test]
5120 fn zformat_substring_percent_close_paren_is_literal_close_paren() {
5121 let _g = crate::test_util::global_state_lock();
5122 let specs = std::collections::HashMap::new();
5123 let r = zformat_substring("%)", &specs, false);
5124 assert_eq!(r, ")");
5125 }
5126
5127 /// c:814 — `%X` substitutes spec value when registered.
5128 #[test]
5129 fn zformat_substring_substitutes_registered_spec() {
5130 let _g = crate::test_util::global_state_lock();
5131 let mut specs = std::collections::HashMap::new();
5132 specs.insert('n', "alice".to_string());
5133 let r = zformat_substring("%n", &specs, false);
5134 assert_eq!(r, "alice");
5135 }
5136
5137 /// c:814 — multiple `%X` substitutions in same format.
5138 #[test]
5139 fn zformat_substring_multiple_specs() {
5140 let _g = crate::test_util::global_state_lock();
5141 let mut specs = std::collections::HashMap::new();
5142 specs.insert('n', "alice".to_string());
5143 specs.insert('h', "/home/alice".to_string());
5144 let r = zformat_substring("%n is at %h", &specs, false);
5145 assert_eq!(r, "alice is at /home/alice");
5146 }
5147
5148 /// c:814 — text between two `%X` substitutions preserved.
5149 #[test]
5150 fn zformat_substring_text_between_specs_preserved() {
5151 let _g = crate::test_util::global_state_lock();
5152 let mut specs = std::collections::HashMap::new();
5153 specs.insert('a', "X".to_string());
5154 let r = zformat_substring("[%a-%a]", &specs, false);
5155 assert_eq!(r, "[X-X]");
5156 }
5157
5158 /// c:849-856 — `%%` unwinds to a literal `%` BEFORE any spec lookup,
5159 /// so a `%` entry in the table is never consulted. bin_zformat
5160 /// (c:1026-1029) rejects `%` as a spec name anyway: reference zsh prints
5161 /// `invalid argument: %:X` for `zformat -f v "%%" "%:X"`.
5162 #[test]
5163 fn zformat_substring_percent_percent_ignores_a_percent_spec() {
5164 let _g = crate::test_util::global_state_lock();
5165 let mut specs = std::collections::HashMap::new();
5166 specs.insert('%', "OVERRIDE".to_string());
5167 let r = zformat_substring("%%", &specs, false);
5168 assert_eq!(r, "%");
5169 }
5170
5171 /// c:814 — `%` followed by unregistered char produces empty
5172 /// substitution.
5173 #[test]
5174 fn zformat_substring_unregistered_spec_no_panic() {
5175 let _g = crate::test_util::global_state_lock();
5176 let specs = std::collections::HashMap::new();
5177 let _ = zformat_substring("%z", &specs, false);
5178 }
5179
5180 /// c:814 — determinism for identical input.
5181 #[test]
5182 fn zformat_substring_is_deterministic() {
5183 let _g = crate::test_util::global_state_lock();
5184 let mut specs = std::collections::HashMap::new();
5185 specs.insert('n', "alice".to_string());
5186 let first = zformat_substring("hi %n!", &specs, false);
5187 for _ in 0..5 {
5188 assert_eq!(zformat_substring("hi %n!", &specs, false), first);
5189 }
5190 }
5191
5192 /// Lifecycle (c:2966/2988/2995/3002):
5193 #[test]
5194 fn zutil_setup_returns_zero_pin() {
5195 let _g = crate::test_util::global_state_lock();
5196 assert_eq!(setup_(std::ptr::null()), 0);
5197 }
5198
5199 /// c:3002 — finish_(NULL) = 0.
5200 #[test]
5201 fn zutil_finish_returns_zero_pin() {
5202 let _g = crate::test_util::global_state_lock();
5203 assert_eq!(finish_(std::ptr::null()), 0);
5204 }
5205
5206 // ═══════════════════════════════════════════════════════════════════
5207 // Additional C-parity tests for Src/Modules/zutil.c
5208 // c:787 lookupstyle / c:810 testforstyle / c:837 bin_zstyle /
5209 // c:1131 bin_zformat / c:2172 get_opt_desc / c:2192 lookup_opt /
5210 // c:2216 get_opt_arr / c:2966-3002 lifecycle type pins
5211 // ═══════════════════════════════════════════════════════════════════
5212
5213 /// c:787 — `lookupstyle` returns Vec<String> (compile-time type pin).
5214 #[test]
5215 fn lookupstyle_returns_vec_string_type() {
5216 let _g = crate::test_util::global_state_lock();
5217 let _: Vec<String> = lookupstyle("", "");
5218 }
5219
5220 /// c:787 — `lookupstyle("", "")` empty inputs returns empty Vec.
5221 #[test]
5222 fn lookupstyle_empty_inputs_returns_empty() {
5223 let _g = crate::test_util::global_state_lock();
5224 let r = lookupstyle("", "");
5225 assert!(r.is_empty(), "empty context/style → empty Vec");
5226 }
5227
5228 /// c:787 — `lookupstyle` deterministic for stable input.
5229 #[test]
5230 fn lookupstyle_is_deterministic() {
5231 let _g = crate::test_util::global_state_lock();
5232 for (ctx, sty) in [("", ""), ("a", "b"), ("zsh", "color")] {
5233 let first = lookupstyle(ctx, sty);
5234 for _ in 0..3 {
5235 assert_eq!(
5236 lookupstyle(ctx, sty),
5237 first,
5238 "lookupstyle({:?}, {:?}) must be deterministic",
5239 ctx,
5240 sty
5241 );
5242 }
5243 }
5244 }
5245
5246 /// c:810 — `testforstyle` returns i32 (compile-time type pin).
5247 #[test]
5248 fn testforstyle_returns_i32_type() {
5249 let _g = crate::test_util::global_state_lock();
5250 let _: i32 = testforstyle("", "");
5251 }
5252
5253 /// c:465 — `testforstyle(ctxt, style)` returns `!found` per the C
5254 /// body's final `return !found;` line. With an empty zstyletab,
5255 /// no entry can match → found=false → return 1 (NOT 0). The
5256 /// previous test expectation conflated the C-level "0=success"
5257 /// convention with the "0=style-present" semantic — they're
5258 /// the same value but the test's "not present" comment makes the
5259 /// 0 assertion inverted.
5260 #[test]
5261 fn testforstyle_empty_inputs_returns_zero() {
5262 let _g = crate::test_util::global_state_lock();
5263 assert_eq!(
5264 testforstyle("", ""),
5265 1,
5266 "empty ctx/style → 1 (not present, per C `!found`)"
5267 );
5268 }
5269
5270 /// c:837 — `bin_zstyle` returns i32 (compile-time type pin).
5271 #[test]
5272 fn bin_zstyle_returns_i32_type() {
5273 let _g = crate::test_util::global_state_lock();
5274 let ops = crate::ported::zsh_h::options {
5275 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
5276 args: Vec::new(),
5277 argscount: 0,
5278 argsalloc: 0,
5279 };
5280 let _: i32 = bin_zstyle("zstyle", &[], &ops, 0);
5281 }
5282
5283 /// c:1131 — `bin_zformat` returns i32.
5284 #[test]
5285 fn bin_zformat_returns_i32_type() {
5286 let _g = crate::test_util::global_state_lock();
5287 let ops = crate::ported::zsh_h::options {
5288 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
5289 args: Vec::new(),
5290 argscount: 0,
5291 argsalloc: 0,
5292 };
5293 let _: i32 = bin_zformat("zformat", &[], &ops, 0);
5294 }
5295
5296 /// c:2172 — `get_opt_desc` returns Option<Zoptdesc>.
5297 #[test]
5298 fn get_opt_desc_returns_option_type() {
5299 let _g = crate::test_util::global_state_lock();
5300 let _: Option<Zoptdesc> = get_opt_desc("");
5301 }
5302
5303 /// c:2172 — `get_opt_desc` deterministic for unknown name.
5304 #[test]
5305 fn get_opt_desc_unknown_deterministic() {
5306 let _g = crate::test_util::global_state_lock();
5307 let a = get_opt_desc("__never_real_opt__").is_some();
5308 for _ in 0..3 {
5309 assert_eq!(
5310 get_opt_desc("__never_real_opt__").is_some(),
5311 a,
5312 "get_opt_desc must be deterministic"
5313 );
5314 }
5315 }
5316
5317 /// c:2192 — `lookup_opt` returns Option<Zoptdesc>.
5318 #[test]
5319 fn lookup_opt_returns_option_type() {
5320 let _g = crate::test_util::global_state_lock();
5321 let _: Option<Zoptdesc> = lookup_opt("");
5322 }
5323
5324 /// c:2216 — `get_opt_arr` returns Option<Zoptarr>.
5325 #[test]
5326 fn get_opt_arr_returns_option_type() {
5327 let _g = crate::test_util::global_state_lock();
5328 let _: Option<Zoptarr> = get_opt_arr("");
5329 }
5330
5331 /// c:2966 — `setup_` returns i32 (compile-time type pin).
5332 #[test]
5333 fn zutil_setup_returns_i32_type() {
5334 let _g = crate::test_util::global_state_lock();
5335 let _: i32 = setup_(std::ptr::null());
5336 }
5337
5338 /// c:2988 + c:2995 — boot/cleanup idempotent.
5339 #[test]
5340 fn zutil_boot_cleanup_idempotent() {
5341 let _g = crate::test_util::global_state_lock();
5342 for _ in 0..5 {
5343 assert_eq!(boot_(std::ptr::null()), 0);
5344 assert_eq!(cleanup_(std::ptr::null()), 0);
5345 }
5346 }
5347
5348 // ═══════════════════════════════════════════════════════════════════
5349 // Additional C-parity tests for Src/Modules/zutil.c
5350 // c:787 lookupstyle / c:810 testforstyle / c:837 bin_zstyle /
5351 // c:1131 bin_zformat / c:2022 bin_zregexparse / c:2408 bin_zparseopts /
5352 // c:3135 zformat_substring
5353 // ═══════════════════════════════════════════════════════════════════
5354
5355 /// c:787 — `lookupstyle` returns Vec<String> (compile-time pin, alt).
5356 #[test]
5357 fn lookupstyle_returns_vec_string_pin_alt() {
5358 let _g = crate::test_util::global_state_lock();
5359 let _: Vec<String> = lookupstyle("", "");
5360 }
5361
5362 /// c:787 — `lookupstyle("", "")` returns empty Vec.
5363 #[test]
5364 fn lookupstyle_empty_returns_empty() {
5365 let _g = crate::test_util::global_state_lock();
5366 let v = lookupstyle("", "");
5367 assert!(v.is_empty(), "empty ctx/style → empty Vec; got {:?}", v);
5368 }
5369
5370 /// c:810 — `testforstyle` returns i32 (compile-time pin, alt).
5371 #[test]
5372 fn testforstyle_returns_i32_pin_alt() {
5373 let _g = crate::test_util::global_state_lock();
5374 let _: i32 = testforstyle("", "");
5375 }
5376
5377 /// c:837 — `bin_zstyle` no-args returns 0 (listing form per c:837).
5378 #[test]
5379 fn bin_zstyle_no_args_returns_zero_or_one() {
5380 let _g = crate::test_util::global_state_lock();
5381 let ops = crate::ported::zsh_h::options {
5382 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
5383 args: Vec::new(),
5384 argscount: 0,
5385 argsalloc: 0,
5386 };
5387 let r = bin_zstyle("zstyle", &[], &ops, 0);
5388 assert!(
5389 r == 0 || r == 1,
5390 "no args is the listing form; result must be 0/1, got {}",
5391 r
5392 );
5393 }
5394
5395 /// c:1131 — `bin_zformat` no-args returns nonzero (usage error).
5396 #[test]
5397 fn bin_zformat_no_args_returns_nonzero() {
5398 let _g = crate::test_util::global_state_lock();
5399 let ops = crate::ported::zsh_h::options {
5400 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
5401 args: Vec::new(),
5402 argscount: 0,
5403 argsalloc: 0,
5404 };
5405 let r = bin_zformat("zformat", &[], &ops, 0);
5406 assert_ne!(r, 0, "zformat no args → usage error");
5407 }
5408
5409 /// c:1027-1032 — `-f`/`-F` reject a spec whose name is `%` or `)`,
5410 /// alongside `-`, `.`, a digit, and a missing `:`. Measured against
5411 /// zsh 5.9.2: `zformat -f R "%n" "%:O"` warns `invalid argument: %:O`
5412 /// and returns 1, leaving the parameter untouched; zshrs accepted
5413 /// both `%` and `)` silently and returned 0.
5414 #[test]
5415 fn bin_zformat_rejects_percent_and_paren_spec_names() {
5416 let _g = crate::test_util::global_state_lock();
5417 let mut ops = crate::ported::zsh_h::options {
5418 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
5419 args: Vec::new(),
5420 argscount: 0,
5421 argsalloc: 0,
5422 };
5423 ops.ind[b'f' as usize] = 1;
5424 let call = |spec: &str| {
5425 bin_zformat(
5426 "zformat",
5427 &[
5428 "ZFMT_PIN".to_string(),
5429 "%n".to_string(),
5430 spec.to_string(),
5431 ],
5432 &ops,
5433 0,
5434 )
5435 };
5436 assert_ne!(call("%:OVERRIDE"), 0, "c:1028 — `%` is not a valid spec name");
5437 assert_ne!(call("):OVERRIDE"), 0, "c:1028 — `)` is not a valid spec name");
5438 // Control: a legal spec name still works, so the added guard did
5439 // not swallow the ordinary path (c:1026-1032 falls through).
5440 assert_eq!(call("n:alice"), 0, "c:1061 — an ordinary spec is accepted");
5441 }
5442
5443 /// c:2022 — `bin_zregexparse` returns i32 (compile-time pin).
5444 #[test]
5445 fn bin_zregexparse_returns_i32_type() {
5446 let _g = crate::test_util::global_state_lock();
5447 let ops = crate::ported::zsh_h::options {
5448 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
5449 args: Vec::new(),
5450 argscount: 0,
5451 argsalloc: 0,
5452 };
5453 let _: i32 = bin_zregexparse("zregexparse", &[], &ops, 0);
5454 }
5455
5456 /// c:2408 — `bin_zparseopts` returns i32 (compile-time pin).
5457 #[test]
5458 fn bin_zparseopts_returns_i32_type() {
5459 let _g = crate::test_util::global_state_lock();
5460 let ops = crate::ported::zsh_h::options {
5461 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
5462 args: Vec::new(),
5463 argscount: 0,
5464 argsalloc: 0,
5465 };
5466 let _: i32 = bin_zparseopts("zparseopts", &[], &ops, 0);
5467 }
5468
5469 /// c:2408 — `bin_zparseopts` no-args returns nonzero (usage error).
5470 #[test]
5471 fn bin_zparseopts_no_args_returns_nonzero() {
5472 let _g = crate::test_util::global_state_lock();
5473 let ops = crate::ported::zsh_h::options {
5474 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
5475 args: Vec::new(),
5476 argscount: 0,
5477 argsalloc: 0,
5478 };
5479 let r = bin_zparseopts("zparseopts", &[], &ops, 0);
5480 assert_ne!(r, 0, "zparseopts no args → usage error");
5481 }
5482
5483 /// c:3135 — `zformat_substring("", _, false)` returns empty (alt).
5484 #[test]
5485 fn zformat_substring_empty_format_returns_empty_alt() {
5486 let specs: std::collections::HashMap<char, String> = std::collections::HashMap::new();
5487 let r = zformat_substring("", &specs, false);
5488 assert_eq!(r, "", "empty format → empty output");
5489 }
5490
5491 /// c:3135 — `zformat_substring` returns String (compile-time pin).
5492 #[test]
5493 fn zformat_substring_returns_string_type() {
5494 let specs: std::collections::HashMap<char, String> = std::collections::HashMap::new();
5495 let _: String = zformat_substring("plain text", &specs, false);
5496 }
5497
5498 /// c:3135 — `zformat_substring` plain text (no `%`) returns as-is.
5499 #[test]
5500 fn zformat_substring_plain_text_no_specs() {
5501 let specs: std::collections::HashMap<char, String> = std::collections::HashMap::new();
5502 let r = zformat_substring("hello world", &specs, false);
5503 assert_eq!(r, "hello world", "text without %-spec is returned verbatim");
5504 }
5505}