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