Skip to main content

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::utils::zwarnnam;
11use indexmap::IndexMap;
12use regex::Regex;
13use std::collections::HashMap;
14use crate::ported::zsh_h::OPT_ISSET;
15use std::io::Write;
16use crate::ported::zsh_h::{Param, hashnode, param, PM_ARRAY};
17
18// =====================================================================
19// ZOF_* — `zparseopts` flag bits, `Src/Modules/zutil.c:1531-1538`.
20// Encode the per-option spec parsed from `zparseopts -D ...`:
21// =====================================================================
22
23/// `ZOF_ARG` from `Src/Modules/zutil.c:1531`. Option takes an argument
24/// (suffix `:`).
25pub const ZOF_ARG:  i32 = 1;                                                 // c:1531
26/// `ZOF_OPT` from `Src/Modules/zutil.c:1532`. Argument is optional
27/// (suffix `::`).
28pub const ZOF_OPT:  i32 = 2;                                                 // c:1532
29/// `ZOF_MULT` from `Src/Modules/zutil.c:1533`. Multiple occurrences
30/// allowed (suffix `+`).
31pub const ZOF_MULT: i32 = 4;                                                 // c:1533
32/// `ZOF_SAME` from `Src/Modules/zutil.c:1534`. All same-name options
33/// share one slot (default for arrays without `+`).
34pub const ZOF_SAME: i32 = 8;                                                 // c:1534
35/// `ZOF_MAP` from `Src/Modules/zutil.c:1535`. Option spec includes a
36/// `=` mapping to a different array name.
37pub const ZOF_MAP:  i32 = 16;                                                // c:1535
38/// `ZOF_CYC` from `Src/Modules/zutil.c:1536`. Cyclic mapping detected
39/// during option parsing (error guard).
40pub const ZOF_CYC:  i32 = 32;                                                // c:1536
41/// `ZOF_GNUS` from `Src/Modules/zutil.c:1537`. GNU-style `--option`
42/// short variant.
43pub const ZOF_GNUS: i32 = 64;                                                // c:1537
44/// `ZOF_GNUL` from `Src/Modules/zutil.c:1538`. GNU-style `--option=value`
45/// long variant.
46pub const ZOF_GNUL: i32 = 128;                                               // c:1538
47// zstyle_entry is defined below (moved from exec.rs).
48
49/// Save/restore for the per-pattern-match magic vars `$match`,
50/// `$mbegin`, `$mend`. Direct port of `MatchData` and the
51/// `savematch`/`restorematch`/`freematch` trio in
52/// src/zsh/Src/Modules/zutil.c:33-80.
53///
54/// zstyle's `-e` (eval pattern on retrieve) and zregexparse's
55/// inner pattern matches both want to evaluate patterns without
56/// clobbering the caller's `$match[]`, `$mbegin[]`, `$mend[]`
57/// variables. The C version keeps a heap-duplicated copy in a
58/// `MatchData` struct, runs the inner match, then either
59/// restores or frees. The Rust port stores `Option<Vec<String>>`
60/// — `None` means the var was unset.
61pub struct MatchData {
62    pub r#match: Option<Vec<String>>,
63    pub mbegin: Option<Vec<String>>,
64    pub mend: Option<Vec<String>>,
65}
66
67/// `zstyle` storage table.
68/// Port of the `zstyletab` HashTable Src/Modules/zutil.c builds —
69/// `newzstyletable()` (line 270) creates it, `bin_zstyle()`
70/// (line 487) drives every mutation. Stores `stypat` entries
71/// (port of C `struct stypat`, zutil.c:95) per style name,
72/// weight-sorted so the most specific pattern wins.
73// `StyleTable` renamed to `style_table`. C uses `HashTable zstyletab`
74// (`Src/Modules/zutil.c:209`) with `struct style` (zutil.c:91) nodes
75// containing a `Stypat pats` linked list (zutil.c:97-104). Rust port
76// uses a `HashMap<String, Vec<stypat>>` while the canonical
77// `hashtable` port lands; the canonical `style` / `stypat` structs
78// already exist at lines 1608 / 1596 below.
79#[allow(non_camel_case_types)]
80#[derive(Default)]
81pub struct style_table {
82    styles: HashMap<String, Vec<stypat>>,
83}
84
85/// Global `zstyletab` mirror — port of the static
86/// `static HashTable zstyletab` in Src/Modules/zutil.c:209.
87/// C allocates this via `newzstyletable()` (c:270) during
88/// module setup; the Rust port uses a `LazyLock<Mutex<>>`
89/// since the table is process-global and `bin_zstyle` /
90/// `lookupstyle` / `testforstyle` all need to share it.
91#[allow(non_upper_case_globals)]
92pub static zstyletab: std::sync::LazyLock<std::sync::Mutex<style_table>> =
93    std::sync::LazyLock::new(|| std::sync::Mutex::new(style_table::new())); // c:209
94
95impl style_table {
96    /// WARNING: NOT IN ZUTIL.C — method on Rust-only `style_table` wrapper.
97    /// C inlines this pattern at every callsite; Rust factors it onto the wrapper.
98    pub fn new() -> Self {
99        Self::default()
100    }
101
102    /// Port of `setstypat(Style s, char *pat, Patprog prog, char **vals, int eval)` from `Src/Modules/zutil.c:295`.
103    /// Insert or replace a pattern→values mapping for a style.
104    /// Mirrors Src/Modules/zutil.c:295 `setstypat` + c:403 `addstyle`
105    /// — find or create the style's pats list, replace if pattern
106    /// already present, else insert in weight-descending order.
107    pub fn set(&mut self, pattern: &str, style: &str, values: Vec<String>, eval: bool) {
108        let style_patterns = self.styles.entry(style.to_string()).or_default();
109        // c:319-333 — Exists → replace.
110        if let Some(existing) = style_patterns.iter_mut().find(|p| p.pat == pattern) {
111            existing.vals = values;                                           // c:328
112            existing.eval = if eval {
113                Some(Box::new(crate::ported::zsh_h::eprog::default()))
114            } else { None };                                                  // c:329
115            return;
116        }
117        // c:344-385 — Calculate weight: high 32 bits = colon-component
118        // count, low 32 bits = sum of per-component specificity (0/1/2).
119        let mut weight: u64 = 0;
120        let mut tmp: u64 = 2;
121        let mut first = true;
122        for ch in pattern.chars() {
123            if first && ch == '*' {                                           // c:365
124                tmp = 0;
125                continue;
126            }
127            first = false;
128            if matches!(ch, '(' | '|' | '*' | '[' | '<' | '?' | '#' | '^') {  // c:372
129                tmp = 1;
130            }
131            if ch == ':' {                                                    // c:377
132                weight += 1u64 << 32;                                         // c:379
133                first = true;
134                weight += tmp;
135                tmp = 2;
136            }
137        }
138        weight += tmp;                                                        // c:386
139        // c:337-342 — New pattern: build stypat.
140        // c:339 — p->prog = prog; the C arg comes from patcompile()
141        // before setstypat is called. The style_table::set API takes
142        // pattern as &str and compiles at lookup-time via patmatch,
143        // so we record None here and rely on get() to match.
144        let prog: Option<crate::ported::zsh_h::Patprog> = None;
145        // c:341 — p->eval = eprog; signals "this is an -e style".
146        // Eprog body parsing requires parse_string (unported), so we
147        // record Some(Box<eprog>::default()) as a non-NULL sentinel
148        // when eval=true to preserve the C "is eval?" check semantics,
149        // None otherwise.
150        let eval_eprog: Option<crate::ported::zsh_h::Eprog> = if eval {
151            Some(Box::new(crate::ported::zsh_h::eprog::default()))
152        } else {
153            None
154        };
155        let sp = stypat {
156            next: None,                                                       // c:342
157            pat: pattern.to_string(),                                         // c:338
158            prog,                                                             // c:339
159            weight,                                                           // c:386
160            eval: eval_eprog,                                                 // c:341
161            vals: values,                                                     // c:340
162        };
163        // c:388-396 — insert q in weight-descending order (highest first).
164        let pos = style_patterns
165            .iter()
166            .position(|p| p.weight < weight)
167            .unwrap_or(style_patterns.len());
168        style_patterns.insert(pos, sp);
169    }
170
171    /// Port of `bin_zstyle(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))` from `Src/Modules/zutil.c:487`.
172    /// Look up the values for (context, style). Mirrors
173    /// Src/Modules/zutil.c:443 `lookupstyle` — walk the style's pats
174    /// list, return values from the first weight-sorted entry whose
175    /// pat matches the context.
176    pub fn get(&self, context: &str, style: &str) -> Option<&[String]> {
177        self.styles.get(style).and_then(|patterns| {
178            patterns
179                .iter()
180                .find(|p| {
181                    if p.pat == "*" {
182                        true
183                    } else {
184                        crate::ported::pattern::patmatch(&p.pat, context)
185                    }
186                })
187                .map(|p| p.vals.as_slice())
188        })
189    }
190
191    /// WARNING: NOT IN ZUTIL.C — method on Rust-only `style_table` wrapper.
192    /// C inlines this pattern at every callsite; Rust factors it onto the wrapper.
193    /// Remove style/pattern entries from the table. Mirrors the
194    /// `-d` dispatch arms of `bin_zstyle` (Src/Modules/zutil.c:487).
195    pub fn delete(&mut self, pattern: Option<&str>, style: Option<&str>) {
196        match (pattern, style) {
197            (None, None) => self.styles.clear(),
198            (Some(pat), None) => {
199                for patterns in self.styles.values_mut() {
200                    patterns.retain(|p| p.pat != pat);
201                }
202                self.styles.retain(|_, v| !v.is_empty());
203            }
204            (Some(pat), Some(sty)) => {
205                if let Some(patterns) = self.styles.get_mut(sty) {
206                    patterns.retain(|p| p.pat != pat);
207                    if patterns.is_empty() {
208                        self.styles.remove(sty);
209                    }
210                }
211            }
212            (None, Some(sty)) => {
213                self.styles.remove(sty);
214            }
215        }
216    }
217
218    /// Port of `setstypat(Style s, char *pat, Patprog prog, char **vals, int eval)` from `Src/Modules/zutil.c:295`.
219    /// Return `(pattern, style, values)` triples for `zstyle -L` /
220    /// `zstyle -a` listing. Mirrors bin_zstyle list dispatch
221    /// (Src/Modules/zutil.c:487 -L/-a arms).
222    pub fn list(&self, context: Option<&str>) -> Vec<(String, String, Vec<String>)> {
223        let mut result = Vec::new();
224        for (style, patterns) in &self.styles {
225            for pat in patterns {
226                if let Some(ctx) = context {
227                    let matches = if pat.pat == "*" {
228                        true
229                    } else {
230                        crate::ported::pattern::patmatch(&pat.pat, ctx)
231                    };
232                    if !matches {
233                        continue;
234                    }
235                }
236                result.push((pat.pat.clone(), style.clone(), pat.vals.clone()));
237            }
238        }
239        result
240    }
241
242    /// WARNING: NOT IN ZUTIL.C — method on Rust-only `style_table` wrapper.
243    /// C inlines this pattern at every callsite; Rust factors it onto the wrapper.
244    /// List all registered style names (bin_zstyle -g without args).
245    pub fn list_styles(&self) -> Vec<&str> {
246        self.styles.keys().map(|s| s.as_str()).collect()
247    }
248
249    /// WARNING: NOT IN ZUTIL.C — method on Rust-only `style_table` wrapper.
250    /// C inlines this pattern at every callsite; Rust factors it onto the wrapper.
251    /// List all distinct patterns across every style (bin_zstyle -g
252    /// with a single pattern arg).
253    pub fn list_patterns(&self) -> Vec<&str> {
254        let mut patterns = Vec::new();
255        for pats in self.styles.values() {
256            for pat in pats {
257                if !patterns.contains(&pat.pat.as_str()) {
258                    patterns.push(pat.pat.as_str());
259                }
260            }
261        }
262        patterns
263    }
264
265    /// WARNING: NOT IN ZUTIL.C — method on Rust-only `style_table` wrapper.
266    /// C inlines this pattern at every callsite; Rust factors it onto the wrapper.
267    /// Boolean-truthy `zstyle -T` / `zstyle -t` check.
268    /// Mirrors bin_zstyle -t / -T arms in Src/Modules/zutil.c:487.
269    pub fn test(&self, context: &str, style: &str, values: Option<&[&str]>) -> bool {
270        if let Some(found) = self.get(context, style) {
271            if let Some(test_vals) = values {
272                test_vals.iter().any(|v| found.contains(&v.to_string()))
273            } else {
274                matches!(
275                    found.first().map(|s| s.as_str()),
276                    Some("true" | "yes" | "on" | "1")
277                )
278            }
279        } else {
280            false
281        }
282    }
283
284    /// WARNING: NOT IN ZUTIL.C — method on Rust-only `style_table` wrapper.
285    /// C inlines this pattern at every callsite; Rust factors it onto the wrapper.
286    /// Single-value "yes/no" interrogation of a style. The `bin_zstyle`
287    /// -b arm of Src/Modules/zutil.c:487.
288    pub fn test_bool(&self, context: &str, style: &str) -> Option<bool> {
289        self.get(context, style).and_then(|vals| {
290            if vals.len() == 1 {
291                match vals[0].as_str() {
292                    "yes" | "true" | "on" | "1" => Some(true),
293                    "no" | "false" | "off" | "0" => Some(false),
294                    _ => None,
295                }
296            } else {
297                None
298            }
299        })
300    }
301}
302
303/// Port of `setstypat(Style s, char *pat, Patprog prog, char **vals, int eval)` from `Src/Modules/zutil.c:814`.
304/// Format a string with specifications
305/// `zformat` builtin entry point.
306/// Helper extracted from `bin_zformat()` (Src/Modules/zutil.c:814)
307/// — same `%X:value` substitution + width / left/right-align /
308/// repeat flag handling the C source's `zformat_substring()`
309/// (line 814) implements.
310pub fn zformat_substring(format: &str, specs: &HashMap<char, String>, presence: bool) -> String {
311    // Direct port of src/zsh/Src/Modules/zutil.c:814
312    // zformat_substring. Recursive walker that handles:
313    //   - Plain `%X` substitutions
314    //   - Optional `-` for right-align
315    //   - Optional `N` for min width
316    //   - Optional `.M` for max width
317    //   - Ternary `%(SPECTEST.true-text.false-text)` — conditional
318    //     substitution based on whether the spec exists / matches a
319    //     numeric test value. With presence=true (zformat -F) the
320    //     test compares the spec's existence/length; with
321    //     presence=false (zformat -f) the test compares against an
322    //     integer math eval of the spec value.
323    //
324    // The original C uses an output-buffer with growable backing;
325    // we use a Rust String with push_* helpers. The recursive
326    // descent + (skip || actval) pattern is the same.
327    // Per zsh/Src/Modules/zutil.c::bin_zformat lines 975-976:
328    // `specs['%']` and `specs[')']` are pre-populated to literal "%" and ")"
329    // BEFORE the recursive walk, which is why `%%` produces `%` and
330    // `%)` produces `)` even though no caller registers them. Rebuild
331    // a private copy of the specs map with those defaults injected,
332    // unless the caller explicitly overrode them.
333    let mut effective: HashMap<char, String> = specs.clone();
334    effective.entry('%').or_insert_with(|| "%".to_string());
335    effective.entry(')').or_insert_with(|| ")".to_string());
336
337    let bytes: Vec<char> = format.chars().collect();
338    let mut out = String::with_capacity(bytes.len() + 16);
339    let mut idx = 0;
340    let _ = ZFormat::substring(
341        &bytes, &mut idx, &mut out, '\0', &effective, presence, false,
342    );
343    out
344}
345
346/// Namespace for the recursive zformat walker — distinct from
347/// the public zformat_substring entry point above so the inner
348/// recursion doesn't collide with the outer wrapper's name.
349struct ZFormat;
350
351impl ZFormat {
352    /// Recursive walker for zformat. Returns the index of the
353    /// terminator (`endchar`). idx is mutated in place.
354    /// Direct port of `zformat_substring()` from Src/Modules/zutil.c:814 —
355    /// the recursive descent over the format string with `%c` substitution
356    /// and `%(?...)` ternary blocks.
357    fn substring(
358        bytes: &[char],
359        idx: &mut usize,
360        out: &mut String,
361        endchar: char,
362        specs: &HashMap<char, String>,
363        presence: bool,
364        skip: bool,
365    ) -> Option<()> {
366        while *idx < bytes.len() {
367            let c = bytes[*idx];
368            // Stop at endchar (zutil.c:820 `*s != endchar`).
369            if endchar != '\0' && c == endchar {
370                return Some(());
371            }
372            if c != '%' {
373                // Plain text — emit unless skipping (zutil.c:937-948).
374                if !skip {
375                    out.push(c);
376                }
377                *idx += 1;
378                continue;
379            }
380            // `%` — parse the spec.
381            let start = *idx;
382            *idx += 1;
383            // Optional `-` for right-align (zutil.c:825-826).
384            let mut right = false;
385            if *idx < bytes.len() && bytes[*idx] == '-' {
386                right = true;
387                *idx += 1;
388            }
389            // Optional digit run for min (zutil.c:828-831).
390            let mut min: Option<i64> = None;
391            if *idx < bytes.len() && bytes[*idx].is_ascii_digit() {
392                let mut n: i64 = 0;
393                while *idx < bytes.len() && bytes[*idx].is_ascii_digit() {
394                    n = n * 10 + bytes[*idx].to_digit(10).unwrap() as i64;
395                    *idx += 1;
396                }
397                min = Some(n);
398            }
399            // Ternary detection: `(` at this position (zutil.c:834-840).
400            let testit = *idx < bytes.len() && bytes[*idx] == '(';
401            // `%(-...` allows leading `-` after the paren (zutil.c:835-840).
402            if testit && *idx + 1 < bytes.len() && bytes[*idx + 1] == '-' {
403                right = true;
404                *idx += 1;
405            }
406            // Optional `.MAX` or just `.` after (zutil.c:841-845).
407            let mut max: Option<i64> = None;
408            if *idx < bytes.len()
409                && (bytes[*idx] == '.' || testit)
410                && *idx + 1 < bytes.len()
411                && bytes[*idx + 1].is_ascii_digit()
412            {
413                *idx += 1; // skip `.` or `(`
414                let mut n: i64 = 0;
415                while *idx < bytes.len() && bytes[*idx].is_ascii_digit() {
416                    n = n * 10 + bytes[*idx].to_digit(10).unwrap() as i64;
417                    *idx += 1;
418                }
419                max = Some(n);
420            } else if *idx < bytes.len() && (bytes[*idx] == '.' || testit) {
421                *idx += 1;
422            }
423
424            if testit && *idx < bytes.len() {
425                // Ternary expression — zutil.c:847-887.
426                let testval: i64 = min.or(max).unwrap_or(0);
427                let spec_char = bytes[*idx];
428                let actval: bool;
429                let spec_val = specs.get(&spec_char);
430                if let Some(sv) = spec_val.filter(|s| !s.is_empty()) {
431                    if presence {
432                        let cmp_val: i64 = if testval != 0 {
433                            sv.chars().count() as i64
434                        } else {
435                            1
436                        };
437                        actval = if right {
438                            testval < cmp_val
439                        } else {
440                            testval >= cmp_val
441                        };
442                    } else {
443                        let signed_test = if right { -testval } else { testval };
444                        let n: i64 = sv.parse().unwrap_or(0);
445                        actval = (n - signed_test) != 0;
446                    }
447                } else {
448                    actval = if presence { !right } else { testval != 0 };
449                }
450                // Skip past the spec char to find the delimiter
451                // (zutil.c:874-876 endcharl = *++s).
452                *idx += 1;
453                if *idx >= bytes.len() {
454                    return None;
455                }
456                let endcharl = bytes[*idx];
457                *idx += 1;
458                // First branch (true-text) — emit only if actval is true,
459                // i.e. skip = skip || !actval. Wait, C says
460                // `skip || actval` for the FIRST sub-call meaning: if
461                // actval is true SKIP the first branch?
462                // Re-reading zutil.c:880-884 — comment says "Either skip
463                // true text and output false text, or vice versa". The
464                // pattern `skip || actval` for the first call means: if
465                // actval, skip the first text. So the FIRST text
466                // (between `(` and the delim) is the FALSE branch, the
467                // SECOND text (between delim and `)`) is the TRUE.
468                ZFormat::substring(bytes, idx, out, endcharl, specs, presence, skip || actval)?;
469                // Skip the delimiter
470                if *idx < bytes.len() && bytes[*idx] == endcharl {
471                    *idx += 1;
472                }
473                ZFormat::substring(bytes, idx, out, ')', specs, presence, skip || !actval)?;
474                // Skip the closing `)`
475                if *idx < bytes.len() && bytes[*idx] == ')' {
476                    *idx += 1;
477                }
478                continue;
479            }
480
481            if skip {
482                // In skip mode — advance past spec char and continue.
483                if *idx < bytes.len() {
484                    *idx += 1;
485                }
486                continue;
487            }
488
489            // Plain `%X` spec (zutil.c:890-922).
490            if *idx < bytes.len() {
491                let spec_char = bytes[*idx];
492                *idx += 1;
493                if let Some(spec_val) = specs.get(&spec_char) {
494                    let mut val_chars: Vec<char> = spec_val.chars().collect();
495                    let len = val_chars.len() as i64;
496                    let len = match max {
497                        Some(m) if m >= 0 && len > m => {
498                            val_chars.truncate(m as usize);
499                            m
500                        }
501                        _ => len,
502                    };
503                    let outl = match min {
504                        Some(m) if m >= 0 && m > len => m,
505                        _ => len,
506                    };
507                    if len >= outl {
508                        for &c in val_chars.iter().take(outl as usize) {
509                            out.push(c);
510                        }
511                    } else {
512                        let diff = (outl - len) as usize;
513                        if right {
514                            for _ in 0..diff {
515                                out.push(' ');
516                            }
517                            for &c in val_chars.iter() {
518                                out.push(c);
519                            }
520                        } else {
521                            for &c in val_chars.iter() {
522                                out.push(c);
523                            }
524                            for _ in 0..diff {
525                                out.push(' ');
526                            }
527                        }
528                    }
529                } else {
530                    // Unknown spec — emit raw segment back
531                    // (zutil.c:923-936).
532                    for &c in &bytes[start..*idx] {
533                        out.push(c);
534                    }
535                }
536            }
537        }
538        Some(())
539    }
540} // impl ZFormat
541
542#[cfg(test)]
543mod tests {
544    use super::*;
545
546    /// Verifies the weight formula matches C's setstypat (zutil.c:344-385):
547    /// component count (high 32 bits) + per-component specificity sum
548    /// (low 32 bits). More specific = higher weight. Drives weight via
549    /// style_table::set's inline weight calc (insertion order reflects
550    /// weight ordering — most specific pattern appears first).
551    #[test]
552    fn test_style_pattern_weight() {
553        let mut t = style_table::new();
554        t.set("*",                  "s", vec!["broad".to_string()], false);
555        t.set(":completion:*",      "s", vec!["mid".to_string()],   false);
556        t.set(":completion:zsh:*",  "s", vec!["narrow".to_string()],false);
557        // Most-specific match wins (sorted descending by weight at insertion).
558        assert_eq!(t.get(":completion:zsh:complete", "s").unwrap()[0], "narrow");
559        assert_eq!(t.get(":completion:bash:complete", "s").unwrap()[0], "mid");
560        assert_eq!(t.get(":other:thing", "s").unwrap()[0], "broad");
561    }
562
563    /// Port of `bin_zparseopts(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))` from `Src/Modules/zutil.c:1738`.
564    #[test]
565    fn zof_flags_are_distinct_powers_of_two() {
566        // c:1531-1538 — ZOF_* are independent bits in a single u8 field.
567        let all = [ZOF_ARG, ZOF_OPT, ZOF_MULT, ZOF_SAME, ZOF_MAP, ZOF_CYC, ZOF_GNUS, ZOF_GNUL];
568        let xor: i32 = all.iter().fold(0, |acc, &x| acc | x);
569        let sum: i32 = all.iter().sum();
570        assert_eq!(xor, sum, "ZOF_* bits must be disjoint");
571        // Ensure each is a power of two.
572        for v in all {
573            assert!(v > 0 && (v & (v - 1)) == 0, "ZOF value {} is not a power of 2", v);
574        }
575    }
576
577    /// Verifies pattern matching via the style_table.get path mirrors
578    /// C's lookupstyle (zutil.c:443) walking the pats list for the
579    /// first weight-sorted match.
580    #[test]
581    fn test_style_pattern_matches() {
582        let mut t = style_table::new();
583        t.set(":completion:*", "s1", vec!["v".to_string()], false);
584        assert!(t.get(":completion:zsh:complete", "s1").is_some());
585        assert!(t.get(":other:zsh", "s1").is_none());
586
587        let mut t2 = style_table::new();
588        t2.set("*", "s2", vec!["v".to_string()], false);
589        assert!(t2.get("anything", "s2").is_some());
590    }
591
592    #[test]
593    fn test_style_table_set_get() {
594        let mut table = style_table::new();
595        table.set(":completion:*", "verbose", vec!["yes".to_string()], false);
596
597        let result = table.get(":completion:zsh", "verbose");
598        assert_eq!(result, Some(&["yes".to_string()][..]));
599
600        let result = table.get(":other", "verbose");
601        assert!(result.is_none());
602    }
603
604    #[test]
605    fn test_style_table_priority() {
606        let mut table = style_table::new();
607        table.set("*", "menu", vec!["no".to_string()], false);
608        table.set(":completion:*", "menu", vec!["yes".to_string()], false);
609
610        let result = table.get(":completion:zsh", "menu");
611        assert_eq!(result, Some(&["yes".to_string()][..]));
612    }
613
614    #[test]
615    fn test_style_table_delete() {
616        let mut table = style_table::new();
617        table.set("*", "style1", vec!["val".to_string()], false);
618        table.set("*", "style2", vec!["val".to_string()], false);
619
620        table.delete(None, Some("style1"));
621        assert!(table.get("test", "style1").is_none());
622        assert!(table.get("test", "style2").is_some());
623    }
624
625    #[test]
626    fn test_style_test_bool() {
627        let mut table = style_table::new();
628        table.set("*", "enabled", vec!["yes".to_string()], false);
629        table.set("*", "disabled", vec!["no".to_string()], false);
630        table.set(
631            "*",
632            "multiple",
633            vec!["a".to_string(), "b".to_string()],
634            false,
635        );
636
637        assert_eq!(table.test_bool("ctx", "enabled"), Some(true));
638        assert_eq!(table.test_bool("ctx", "disabled"), Some(false));
639        assert_eq!(table.test_bool("ctx", "multiple"), None);
640    }
641
642    /// Port of `bin_zstyle(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))` from `Src/Modules/zutil.c:487`.
643    /// Verifies the persistent global `zstyletab` round-trips
644    /// set→get and that `lookupstyle` / `testforstyle` C-name shims
645    /// see the same entry. Lock-stamps the global-state path that
646    /// `bin_zstyle` relies on (Src/Modules/zutil.c:209).
647    #[test]
648    fn test_global_zstyletab_set_and_lookup() {
649        let key_style = "test_zutil_global_marker_style";
650        let key_pat = "test_zutil_global_marker_*";
651        {
652            let mut t = zstyletab.lock().unwrap();
653            t.set(key_pat, key_style,
654                  vec!["yes".to_string()], false);
655        }
656        let found = lookupstyle("test_zutil_global_marker_x", key_style);
657        assert_eq!(found, vec!["yes".to_string()]);
658        assert_eq!(testforstyle("test_zutil_global_marker_x", key_style), 0);
659        assert_eq!(testforstyle("unmatched_ctx", "no_such_style_zzz"), 1);
660        // Cleanup so other tests don't see the entry.
661        {
662            let mut t = zstyletab.lock().unwrap();
663            t.delete(Some(key_pat), Some(key_style));
664        }
665    }
666
667    #[test]
668    fn test_zformat_basic() {
669        let mut specs = HashMap::new();
670        specs.insert('n', "test".to_string());
671        specs.insert('v', "42".to_string());
672
673        let result = zformat_substring("Name: %n, Value: %v", &specs, false);
674        assert_eq!(result, "Name: test, Value: 42");
675    }
676
677    #[test]
678    fn test_zformat_padding() {
679        let mut specs = HashMap::new();
680        specs.insert('n', "hi".to_string());
681
682        let result = zformat_substring("[%10n]", &specs, false);
683        assert_eq!(result, "[hi        ]");
684
685        let result = zformat_substring("[%-10n]", &specs, false);
686        assert_eq!(result, "[        hi]");
687    }
688
689    #[test]
690    fn test_zformat_truncate() {
691        let mut specs = HashMap::new();
692        specs.insert('n', "hello world".to_string());
693
694        let result = zformat_substring("[%.5n]", &specs, false);
695        assert_eq!(result, "[hello]");
696    }
697
698    #[test]
699    fn test_zformat_escape() {
700        let specs = HashMap::new();
701        let result = zformat_substring("100%%", &specs, false);
702        assert_eq!(result, "100%");
703    }
704
705}
706
707// ===========================================================
708// Methods moved verbatim from src/ported/exec.rs because their
709// C counterpart's source file maps 1:1 to this Rust module.
710// Rust permits multiple inherent impl blocks for the same
711// type within a crate, so call sites in exec.rs are unchanged.
712// ===========================================================
713
714// =====================================================================
715// Direct port of bin_zformat(char *nam, char **args, UNUSED(Options ops), UNUSED(int func)) from Src/Modules/zutil.c:954
716// =====================================================================
717
718/// Direct port of `bin_zregexparse(char *nam, char **args, Options ops, UNUSED(int func))` from `Src/Modules/zutil.c:1486`.
719/// C body (c:1488-1517):
720/// ```c
721/// int oldextendedglob = opts[EXTENDEDGLOB];
722/// char *var1 = args[0]; char *var2 = args[1]; char *subj = args[2];
723/// opts[EXTENDEDGLOB] = 1;
724/// rparseargs = args + 3;
725/// pushheap();
726/// rparsestates = newlinklist();
727/// if (setjmp(rparseerr) || rparsealt(&result, &rparseerr) || *rparseargs) {
728///     zwarnnam(nam, ...); ret = 3;
729/// } else ret = 0;
730/// if (!ret) ret = rmatch(&result, subj, var1, var2, OPT_ISSET(ops,'c'));
731/// popheap();
732/// opts[EXTENDEDGLOB] = oldextendedglob;
733/// return ret;
734/// ```
735/// WARNING: param names don't match C — Rust=(nam, args, _func) vs C=(nam, args, ops, func)
736pub fn bin_zregexparse(nam: &str, args: &[String],                            // c:1486
737                       ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
738    if args.len() < 3 {
739        zwarnnam(nam, "not enough arguments");
740        return 1;
741    }
742    let var1 = &args[0];                                                     // c:1489
743    let var2 = &args[1];                                                     // c:1490
744    let subj = &args[2];                                                     // c:1491
745    let _rparseargs = &args[3..];                                            // c:1497
746    let _ = (var1, var2, subj);
747
748    // c:1494 — `oldextendedglob = opts[EXTENDEDGLOB]; opts[EXTENDEDGLOB] = 1;`
749    let oldext = crate::ported::zsh_h::isset(crate::ported::zsh_h::EXTENDEDGLOB); // c:1494
750    crate::ported::options::opt_state_set(
751        &crate::ported::zsh_h::opt_name(crate::ported::zsh_h::EXTENDEDGLOB),
752        true,
753    );                                                                       // c:1496
754
755    // c:1499 — `pushheap(); rparsestates = newlinklist();`
756    crate::ported::mem::pushheap();                                          // c:1499
757
758    // c:1500 — `if (setjmp(rparseerr) || rparsealt(&result, &rparseerr) ||
759    // *rparseargs)`. rparsealt is a stub here (the alternation parser
760    // is open work); without it the parse always succeeds vacuously
761    // and we fall straight to rmatch. The `*rparseargs` check is the
762    // "trailing-args-after-regex" error.
763    let mut ret;
764    let mut result = RParseResult { nullacts: Vec::new(), args: Vec::new() };
765    let parse_err = rparsealt(&mut result, std::ptr::null_mut()) != 0;
766    if parse_err {                                                           // c:1500
767        zwarnnam(nam, &format!("invalid regex : {}",                         // c:1502
768            args.last().map(|s| s.as_str()).unwrap_or("")));
769        ret = 3;                                                             // c:1505
770    } else {
771        ret = 0;                                                             // c:1508
772    }
773
774    if ret == 0 {                                                            // c:1510
775        // c:1511 — `rmatch(&result, subj, var1, var2, OPT_ISSET(ops,'c'))`
776        // — match the parsed regex tree against subj, capturing into
777        // var1/var2. The rmatch port is open work; placeholder fall-
778        // through to ret=0 (no match).
779        let _ = OPT_ISSET(ops, b'c');
780        let _ = (var1, var2, subj);
781    }
782
783    crate::ported::mem::popheap();                                           // c:1513
784    crate::ported::options::opt_state_set(
785        &crate::ported::zsh_h::opt_name(crate::ported::zsh_h::EXTENDEDGLOB),
786        oldext,
787    );                                                                       // c:1514
788    ret                                                                      // c:1515
789}
790
791/// Direct port of `bin_zstyle(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))` from `Src/Modules/zutil.c:487`.
792/// C body (c:490-952): switch over -L/-l/-d/-s/-b/-t/-T/-m/-a/-g/-e
793/// flags + per-mode handlers.
794///
795/// **Status**: structural port — the no-flag display path
796/// (matches all zstyle entries) and -L/-l listing path are wired
797/// against the canonical zstyletab walks; -s/-b/-t/-T/-m/-a/-g/-e
798/// per-context lookups depend on the lookupstyle helper which
799/// currently returns Vec::new() (the per-style-flavour matching
800/// engine in zutil.c hasn't landed). Without it, the lookups all
801/// return "no match" (ret=1).
802/// WARNING: param names don't match C — Rust=(nam, args, _func) vs C=(nam, args, ops, func)
803pub fn bin_zstyle(nam: &str, args: &[String],                                 // c:487
804                  ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
805
806    // c:495-540 — flag dispatch backed by the global zstyletab.
807    if args.is_empty() {                                                     // c:495
808        // c:496 — list mode: walk zstyletab printing each entry.
809        let t = match zstyletab.lock() { Ok(g) => g, Err(_) => return 1 };
810        let mut out = std::io::stdout().lock();
811        for (pat, style, vals) in t.list(None) {                             // c:496
812            let _ = write!(out, "{} {}", pat, style);
813            for v in &vals {
814                let _ = write!(out, " {}", v);
815            }
816            let _ = writeln!(out);
817        }
818        return 0;                                                            // c:497
819    }
820    if OPT_ISSET(ops, b'L') || OPT_ISSET(ops, b'l') {                        // c:511
821        // -L: emit as replayable `zstyle` commands.
822        let t = match zstyletab.lock() { Ok(g) => g, Err(_) => return 1 };
823        let mut out = std::io::stdout().lock();
824        for (pat, style, vals) in t.list(None) {                             // c:511
825            let _ = write!(out, "zstyle {} {}", pat, style);
826            for v in &vals {
827                let _ = write!(out, " {}", v);
828            }
829            let _ = writeln!(out);
830        }
831        return 0;                                                            // c:514
832    }
833    if OPT_ISSET(ops, b'd') {                                                // c:520
834        // -d: delete the style. C: `args[0]` is pattern (optional),
835        // `args[1]` is style (optional). With no args → wipe all.
836        let pat = args.first().map(|s| s.as_str());
837        let sty = args.get(1).map(|s| s.as_str());
838        if let Ok(mut t) = zstyletab.lock() {
839            t.delete(pat, sty);                                              // c:521-523
840        }
841        return 0;                                                            // c:524
842    }
843    // c:541-942 — -s/-b/-t/-T/-m/-a/-e per-context lookup arms.
844    // -g has different arg layout (args[0] = output name, not context)
845    // so it gets its own block below.
846    if OPT_ISSET(ops, b's') || OPT_ISSET(ops, b'b') || OPT_ISSET(ops, b't')
847        || OPT_ISSET(ops, b'T') || OPT_ISSET(ops, b'a')
848        || OPT_ISSET(ops, b'e')
849        || OPT_ISSET(ops, b'm')
850    {
851        if args.len() < 2 { return 1; }
852        let ctxt = &args[0];                                                 // c:541
853        let style = &args[1];
854        let vals = lookupstyle(ctxt, style);                                 // c:443
855        // c:559-732 — per-flag return semantics: just check found vs not.
856        // For -t: 0 if found AND first value matches one of the "true"
857        // tokens (when arg given) or first ∈ {true,yes,on,1}.
858        if OPT_ISSET(ops, b't') {                                            // c:660
859            let t = match zstyletab.lock() { Ok(g) => g, Err(_) => return 1 };
860            return if t.test(ctxt, style, None) { 0 } else { 1 };
861        }
862        if OPT_ISSET(ops, b'T') {                                            // c:692
863            // -T: same as -t but missing entries succeed (return 0).
864            let t = match zstyletab.lock() { Ok(g) => g, Err(_) => return 1 };
865            if t.get(ctxt, style).is_some() {
866                return if t.test(ctxt, style, None) { 0 } else { 1 };
867            }
868            return 0;
869        }
870        // -m PATTERN: pattern-match args[2] against each value, return
871        // 0 if any matches. C: zutil.c:727-747.
872        if OPT_ISSET(ops, b'm') {                                            // c:727
873            if args.len() < 3 { return 1; }
874            let pat = &args[2];
875            let prog = match crate::ported::pattern::patcompile(
876                pat,
877                crate::ported::zsh_h::PAT_STATIC,
878                None,
879            ) {
880                Some(p) => p,
881                None => return 1,
882            };
883            for v in &vals {                                                 // c:738
884                if crate::ported::pattern::pattry(&prog, v) {                // c:739
885                    return 0;                                                // c:741
886                }
887            }
888            return 1;                                                        // c:746
889        }
890        // -s CONTEXT STYLE NAME [SEP]: join vals with SEP (default " "),
891        // setsparam(NAME, joined). Return 0 if found else 1 (empty str).
892        // C: zutil.c:643-658.
893        if OPT_ISSET(ops, b's') {                                            // c:643
894            if args.len() < 3 { return 1; }
895            let pname = &args[2];
896            if !vals.is_empty() {
897                let sep = args.get(3).map(|s| s.as_str()).unwrap_or(" ");    // c:649
898                let ret = vals.join(sep);
899                crate::ported::params::setsparam(pname, &ret);
900                return 0;                                                    // c:650
901            }
902            crate::ported::params::setsparam(pname, "");                     // c:652
903            return 1;                                                        // c:653
904        }
905        // -b CONTEXT STYLE NAME: coerce single bool-ish val to "yes"/"no".
906        // C: zutil.c:660-680.
907        if OPT_ISSET(ops, b'b') {                                            // c:660
908            if args.len() < 3 { return 1; }
909            let pname = &args[2];
910            let truthy = vals.len() == 1                                     // c:665-670
911                && matches!(vals[0].as_str(),
912                            "yes" | "true" | "on" | "1");
913            let (ret, code) = if truthy { ("yes", 0) } else { ("no", 1) };
914            crate::ported::params::setsparam(pname, ret);                    // c:677
915            return code;                                                     // c:672/675
916        }
917        // -a CONTEXT STYLE NAME: setaparam(NAME, vals).
918        // C: zutil.c:682-699.
919        if OPT_ISSET(ops, b'a') {                                            // c:682
920            if args.len() < 3 { return 1; }
921            let pname = &args[2];
922            let found = !vals.is_empty();
923            crate::ported::params::setaparam(pname,                          // c:696
924                if found { vals } else { Vec::new() });
925            return if found { 0 } else { 1 };                                // c:689/694
926        }
927        // -e: deferred-eval style lookup. For now: bind joined value.
928        if OPT_ISSET(ops, b'e') {
929            if args.len() < 3 { return 1; }
930            let pname = &args[2];
931            if vals.is_empty() { return 1; }
932            let val = vals.join(" ");
933            crate::ported::params::setsparam(pname, &val);
934            return 0;
935        }
936        // -g: handled below (different arg layout).
937        if vals.is_empty() { return 1; }
938        return 0;
939    }
940    // -g NAME [PATTERN [STYLE]]: collect into array NAME.
941    // C: zutil.c:758-795. Distinct arg layout: args[0]=NAME (not ctxt).
942    if OPT_ISSET(ops, b'g') {                                                // c:758
943        if args.is_empty() { return 1; }
944        let pname = &args[0];                                                // c:792 args[1]→args[0]
945        let pat_arg = args.get(1).map(|s| s.as_str());                       // c:766
946        let sty_arg = args.get(2).map(|s| s.as_str());                       // c:767
947        let mut out: Vec<String> = Vec::new();
948        let t = match zstyletab.lock() { Ok(g) => g, Err(_) => return 1 };
949        match (pat_arg, sty_arg) {
950            (None, _) => {
951                // Collect distinct context patterns. c:788
952                let mut seen: std::collections::HashSet<String> =
953                    std::collections::HashSet::new();
954                for (p, _s, _v) in t.list(None) {
955                    if seen.insert(p.clone()) { out.push(p); }
956                }
957            }
958            (Some(pat), None) => {
959                // Collect style names attached to context = pat. c:783
960                let mut seen: std::collections::HashSet<String> =
961                    std::collections::HashSet::new();
962                for (p, s, _v) in t.list(None) {
963                    if p == pat && seen.insert(s.clone()) { out.push(s); }
964                }
965            }
966            (Some(pat), Some(sty)) => {
967                // Values at context=pat, style=sty. c:768-779
968                if let Some(v) = t.get(pat, sty) {
969                    out.extend(v.iter().cloned());
970                }
971            }
972        }
973        drop(t);
974        crate::ported::params::setaparam(pname, out);                        // c:792
975        return 0;
976    }
977
978    // c:945 — set/replace style: addstyle each value.
979    if args.len() < 3 {
980        zwarnnam(nam, "not enough arguments");                               // c:947
981        return 1;
982    }
983    let ctxt = &args[0];                                                     // c:945
984    let style = &args[1];
985    let values: Vec<String> = args[2..].to_vec();                            // c:949
986    if let Ok(mut t) = zstyletab.lock() {
987        t.set(ctxt, style, values, false);                                   // c:295 setstypat
988    }
989    0                                                                        // c:951
990}
991
992/// Direct port of `bin_zformat(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))` from `Src/Modules/zutil.c:954`.
993/// C signature: `static int bin_zformat(char *nam, char **args,
994/// Port of `bin_zparseopts(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))` from `Src/Modules/zutil.c:1738`. C
995/// signature: `static int bin_zparseopts(char *nam, char **args,
996/// UNUSED(Options ops), UNUSED(int func))`.
997///
998/// Implements the full GNU/zsh option parser:
999///   - Flags: -D (delete consumed from argv), -E (extract),
1000///     -F (fail on unknown), -G (GNU long-opt mode),
1001///     -K (keep existing), -M (map), -a NAME (default array),
1002///     -A NAME (assoc array), -v NAME (source argv from NAME).
1003///   - Option descs: `name`, `name+` (multi), `name:` (mandatory arg),
1004///     `name::` (optional arg), `name:-` (same-arg), `=ARR` suffix.
1005/// WARNING: param names don't match C — Rust=(nam, args, _func) vs C=(nam, args, ops, func)
1006pub fn bin_zparseopts(nam: &str, args: &[String],                             // c:1738
1007                      _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
1008
1009    #[derive(Clone)]
1010    struct Desc {
1011        name: String,
1012        flags: i32,
1013        arr_name: Option<String>,
1014        vals: Vec<Val>,                 // collected values
1015    }
1016    #[derive(Clone)]
1017    struct Val {
1018        name: String,                   // option name as it appeared
1019        arg: Option<String>,            // arg if any
1020    }
1021
1022    let mut del = false;                // c:1742
1023    let mut flags_map = 0i32;           // c:1742
1024    let mut extract = false;
1025    let mut fail = false;
1026    let mut gnu = false;
1027    let mut keep = false;
1028    let mut assoc: Option<String> = None;
1029    let mut paramsname: Option<String> = None;
1030    let mut defarr: Option<String> = None;
1031    let mut named_arrays: Vec<String> = Vec::new();
1032
1033    // Phase 1: parse zparseopts flags (c:1751-1873).
1034    let mut i = 0usize;
1035    while i < args.len() {
1036        let o = &args[i];
1037        if !o.starts_with('-') { break; }
1038        if o.len() == 1 { break; }                                            // "-"
1039        let bytes = o.as_bytes();
1040        match bytes[1] {
1041            b'-' if bytes.len() == 2 => { i += 1; break; }                    // "--"
1042            b'-' => { break; }                                                // "-something"
1043            b'D' if bytes.len() == 2 => { del = true; i += 1; }
1044            b'E' if bytes.len() == 2 => { extract = true; i += 1; }
1045            b'F' if bytes.len() == 2 => { fail = true; i += 1; }
1046            b'G' if bytes.len() == 2 => { gnu = true; i += 1; }
1047            b'K' if bytes.len() == 2 => { keep = true; i += 1; }
1048            b'M' if bytes.len() == 2 => { flags_map |= ZOF_MAP; i += 1; }
1049            b'a' => {
1050                if defarr.is_some() {
1051                    zwarnnam(nam, "default array given more than once");
1052                    return 1;
1053                }
1054                let n = if o.len() > 2 { o[2..].to_string() }
1055                        else if i + 1 < args.len() { i += 1; args[i].clone() }
1056                        else { zwarnnam(nam, "missing array name"); return 1; };
1057                defarr = Some(n);
1058                i += 1;
1059            }
1060            b'A' => {
1061                if assoc.is_some() {
1062                    zwarnnam(nam, "associative array given more than once");
1063                    return 1;
1064                }
1065                let n = if o.len() > 2 { o[2..].to_string() }
1066                        else if i + 1 < args.len() { i += 1; args[i].clone() }
1067                        else { zwarnnam(nam, "missing array name"); return 1; };
1068                assoc = Some(n);
1069                i += 1;
1070            }
1071            b'v' => {
1072                if paramsname.is_some() {
1073                    zwarnnam(nam, "argv array given more than once");
1074                    return 1;
1075                }
1076                let n = if o.len() > 2 { o[2..].to_string() }
1077                        else if i + 1 < args.len() { i += 1; args[i].clone() }
1078                        else { zwarnnam(nam, "missing array name"); return 1; };
1079                paramsname = Some(n);
1080                i += 1;
1081            }
1082            _ => break,                                                       // option-desc
1083        }
1084    }
1085    if i >= args.len() {                                                      // c:1874
1086        zwarnnam(nam, "missing option descriptions");
1087        return 1;
1088    }
1089
1090    // Phase 2: parse option descriptions (c:1878-1954).
1091    let mut descs: Vec<Desc> = Vec::new();
1092    while i < args.len() {
1093        let raw = &args[i];
1094        i += 1;
1095        if raw.is_empty() {
1096            zwarnnam(nam, &format!("invalid option description: {}", raw));
1097            return 1;
1098        }
1099        let bytes = raw.as_bytes();
1100        let mut name = String::new();
1101        let mut f = 0i32;
1102        let mut p = 0usize;
1103        // Parse name with backslash-escape, stopping at +/:/=. c:1884-1895.
1104        while p < bytes.len() {
1105            let c = bytes[p];
1106            if c == b'\\' && p + 1 < bytes.len() {
1107                name.push(bytes[p + 1] as char);
1108                p += 2;
1109                continue;
1110            }
1111            if p > 0 {
1112                if c == b'+' { f |= ZOF_MULT; p += 1; break; }
1113                if c == b':' || c == b'=' { break; }
1114            }
1115            name.push(c as char);
1116            p += 1;
1117        }
1118        // c:1897-1911 — :: arg flags.
1119        if p < bytes.len() && bytes[p] == b':' {
1120            f |= ZOF_ARG;
1121            p += 1;
1122            if gnu {
1123                f |= if name.len() > 1 { ZOF_GNUL } else { ZOF_GNUS };
1124            }
1125            if p < bytes.len() && bytes[p] == b':' { p += 1; f |= ZOF_OPT; }
1126            if p < bytes.len() && bytes[p] == b'-' { p += 1; f |= ZOF_SAME; }
1127        }
1128        // c:1913-1930 — `=ARR` suffix → bind to named array.
1129        let mut arr_name: Option<String> = None;
1130        if p < bytes.len() && bytes[p] == b'=' {
1131            p += 1;
1132            let arr = std::str::from_utf8(&bytes[p..]).unwrap_or("").to_string();
1133            if !named_arrays.contains(&arr) { named_arrays.push(arr.clone()); }
1134            arr_name = Some(arr);
1135            f |= flags_map;
1136        } else if p < bytes.len() {
1137            zwarnnam(nam, &format!("invalid option description: {}", raw));
1138            return 1;
1139        } else if defarr.is_none() && assoc.is_none() {
1140            zwarnnam(nam, &format!("no default array defined: {}", raw));
1141            return 1;
1142        }
1143        if descs.iter().any(|d| d.name == name) {
1144            zwarnnam(nam, &format!("option defined more than once: {}", name));
1145            return 1;
1146        }
1147        descs.push(Desc { name, flags: f, arr_name, vals: Vec::new() });
1148    }
1149
1150    // Phase 3: source params (c:1955-1959).
1151    let params_src = paramsname.clone().unwrap_or_else(|| "argv".to_string());
1152    let mut params: Vec<String> = crate::fusevm_bridge::with_executor(|exec| {
1153        if params_src == "argv" {
1154            exec.pparams()
1155        } else {
1156            exec.array(&params_src).unwrap_or_default()
1157        }
1158    });
1159
1160    // Phase 4: walk params (c:1961-2060).
1161    let mut new_params: Vec<String> = Vec::new();          // -E -D rebuild
1162    let mut pi = 0usize;
1163    let mut stopped = false;
1164    while pi < params.len() {
1165        let o_raw = params[pi].clone();
1166        // Not an option (or `-` in GNU mode).
1167        if !o_raw.starts_with('-') || (gnu && o_raw == "-") {
1168            if extract {
1169                if del { new_params.push(o_raw); }
1170                pi += 1;
1171                continue;
1172            } else { stopped = true; break; }
1173        }
1174        // `--` or non-GNU `-`: end.
1175        if o_raw == "-" || o_raw == "--" {
1176            if del && extract { new_params.push(o_raw); }
1177            pi += 1;
1178            stopped = true;
1179            break;
1180        }
1181        // Try whole-name match. c:1978.
1182        let body = &o_raw[1..];
1183        let whole_idx = descs.iter().position(|d|
1184            body == d.name || body.starts_with(&d.name)
1185                && body.as_bytes().get(d.name.len()).is_some_and(|b| *b == b'=' || *b == 0));
1186        let whole_match = whole_idx.map(|idx| {
1187            let d = &descs[idx];
1188            body == d.name ||
1189                (body.starts_with(&d.name) && (
1190                    body.as_bytes().get(d.name.len()) == Some(&b'=')))
1191        }).unwrap_or(false);
1192        if whole_match {
1193            let idx = whole_idx.unwrap();
1194            let dn_len = descs[idx].name.len();
1195            let dflags = descs[idx].flags;
1196            let dname = descs[idx].name.clone();
1197            if (dflags & ZOF_ARG) != 0 {
1198                let e = &body[dn_len..];                 // pointer past name
1199                if (dflags & ZOF_GNUL) != 0 && e.starts_with('=') {  // c:2031
1200                    let arg = e[1..].to_string();
1201                    descs[idx].vals.push(Val { name: o_raw.clone(), arg: Some(arg) });
1202                } else if !e.is_empty() {                              // c:2038
1203                    descs[idx].vals.push(Val { name: o_raw.clone(), arg: Some(e.to_string()) });
1204                } else if (dflags & ZOF_OPT) == 0
1205                    || ((dflags & (ZOF_GNUL | ZOF_GNUS)) == 0
1206                        && pi + 1 < params.len()
1207                        && !params[pi + 1].starts_with('-'))
1208                {                                                       // c:2044
1209                    if pi + 1 >= params.len() {
1210                        zwarnnam(nam,
1211                            &format!("missing argument for option: -{}", dname));
1212                        return 1;
1213                    }
1214                    pi += 1;
1215                    let arg = params[pi].clone();
1216                    descs[idx].vals.push(Val { name: o_raw.clone(), arg: Some(arg) });
1217                } else {                                                // c:2055
1218                    descs[idx].vals.push(Val { name: o_raw.clone(), arg: None });
1219                }
1220            } else {
1221                descs[idx].vals.push(Val { name: o_raw.clone(), arg: None });
1222            }
1223            pi += 1;
1224            continue;
1225        }
1226        // Fallback: each char as short opt. c:1980-2016.
1227        let chars: Vec<char> = o_raw[1..].chars().collect();
1228        let mut ci = 0usize;
1229        let mut consumed_param = true;
1230        while ci < chars.len() {
1231            let ch = chars[ci];
1232            let name1 = ch.to_string();
1233            let didx = descs.iter().position(|d| d.name == name1);
1234            let Some(idx) = didx else {
1235                if fail {
1236                    if ch != '-' || ci > 0 {
1237                        zwarnnam(nam, &format!("bad option: -{}", ch));
1238                    } else {
1239                        zwarnnam(nam, &format!("bad option: -{}", chars.iter().collect::<String>()));
1240                    }
1241                    return 1;
1242                }
1243                consumed_param = false;
1244                break;
1245            };
1246            let dflags = descs[idx].flags;
1247            let dname = descs[idx].name.clone();
1248            if (dflags & ZOF_ARG) != 0 {
1249                if ci + 1 < chars.len() {
1250                    // arg in same param: rest of chars
1251                    let arg: String = chars[ci + 1..].iter().collect();
1252                    descs[idx].vals.push(Val {
1253                        name: format!("-{}", ch),
1254                        arg: Some(arg),
1255                    });
1256                    break;
1257                } else if (dflags & ZOF_OPT) == 0
1258                    || ((dflags & (ZOF_GNUL | ZOF_GNUS)) == 0
1259                        && pi + 1 < params.len()
1260                        && !params[pi + 1].starts_with('-'))
1261                {
1262                    if pi + 1 >= params.len() {
1263                        zwarnnam(nam, &format!("missing argument for option: -{}", dname));
1264                        return 1;
1265                    }
1266                    pi += 1;
1267                    let arg = params[pi].clone();
1268                    descs[idx].vals.push(Val { name: format!("-{}", ch), arg: Some(arg) });
1269                } else {
1270                    descs[idx].vals.push(Val { name: format!("-{}", ch), arg: None });
1271                }
1272            } else {
1273                descs[idx].vals.push(Val { name: format!("-{}", ch), arg: None });
1274            }
1275            ci += 1;
1276        }
1277        if !consumed_param {
1278            if extract {
1279                if del { new_params.push(o_raw); }
1280                pi += 1;
1281                continue;
1282            } else {
1283                stopped = true;
1284                break;
1285            }
1286        }
1287        pi += 1;
1288    }
1289    let _ = stopped;
1290    // c:2069 — append remaining params if extract+del.
1291    if extract && del {
1292        while pi < params.len() {
1293            new_params.push(params[pi].clone());
1294            pi += 1;
1295        }
1296    } else if del && !extract {
1297        // c:2129: setaparam(paramsname, pp) — what's left from pi.
1298        new_params = params[pi..].to_vec();
1299    }
1300
1301    // Phase 5: emit per-array results. c:2073-2088.
1302    // Group descs by arr_name → array of [name, arg, name, arg, ...].
1303    let mut arr_outputs: std::collections::BTreeMap<String, Vec<String>> =
1304        std::collections::BTreeMap::new();
1305    for d in &descs {
1306        let target = d.arr_name.clone().or_else(|| defarr.clone());
1307        let Some(tgt) = target else { continue };
1308        let entry = arr_outputs.entry(tgt).or_default();
1309        for v in &d.vals {
1310            entry.push(v.name.clone());
1311            if let Some(a) = &v.arg {
1312                entry.push(a.clone());
1313            }
1314        }
1315    }
1316    for (name, vals) in arr_outputs {
1317        if !keep || !vals.is_empty() {
1318            crate::ported::params::setaparam(&name, vals);
1319        }
1320    }
1321
1322    // c:2089-2123 — assoc emission.
1323    if let Some(aname) = assoc {
1324        let mut flat: Vec<String> = Vec::new();
1325        for d in &descs {
1326            if d.vals.is_empty() { continue; }
1327            flat.push(format!("-{}", d.name));
1328            let joined: String = d.vals.iter()
1329                .filter_map(|v| v.arg.clone())
1330                .collect::<Vec<_>>().join(" ");
1331            flat.push(joined);
1332        }
1333        if !keep || !flat.is_empty() {
1334            crate::ported::params::sethparam(&aname, flat);
1335        }
1336    }
1337
1338    // c:2124-2131 — write back consumed argv when -D was given.
1339    if del {
1340        if params_src == "argv" {
1341            crate::fusevm_bridge::with_executor(|exec| {
1342                exec.set_pparams(new_params.clone());
1343            });
1344            if let Ok(mut pp) = crate::ported::builtin::PPARAMS.lock() {
1345                *pp = new_params;
1346            }
1347        } else {
1348            crate::ported::params::setaparam(&params_src, new_params);
1349        }
1350    } else {
1351        let _ = params;
1352    }
1353
1354    0
1355}
1356
1357/// Port of `bin_zformat(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))` from `Src/Modules/zutil.c:955`.
1358/// C signature: `static int bin_zformat(char *nam, char **args,
1359/// UNUSED(Options ops), UNUSED(int func))`.
1360/// BUILTIN spec at zutil.c:2138 takes just two-or-more args (no
1361/// option flags); the first arg is `-f`/`-F`/`-a` (a single letter
1362/// after the dash) selecting the substitution mode.
1363/// WARNING: param names don't match C — Rust=(nam, args, _func) vs C=(nam, args, ops, func)
1364pub fn bin_zformat(nam: &str, args: &[String],                                // c:955
1365                   _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
1366    let mut presence = 0i32;                                                  // c:958
1367    if args.is_empty() {                                                      // c:960
1368        crate::ported::utils::zwarnnam(nam,
1369            &format!("invalid argument: {}", ""));
1370        return 1;
1371    }
1372    let opt_arg = &args[0];
1373    let bytes = opt_arg.as_bytes();
1374    if bytes.is_empty() || bytes[0] != b'-' || bytes.len() != 2 {             // c:960-963
1375        crate::ported::utils::zwarnnam(nam,
1376            &format!("invalid argument: {}", opt_arg));
1377        return 1;                                                             // c:962
1378    }
1379    let opt = bytes[1];                                                       // c:961
1380    let args = &args[1..];                                                    // c:965 args++
1381
1382    match opt {                                                               // c:967
1383        b'F' | b'f' => {                                                      // c:968 / c:971
1384            if opt == b'F' { presence = 1; }                                  // c:969 fall-through
1385            // c:973-994 — -f / -F branch.
1386            if args.len() < 2 {                                               // c:973 args[0]/args[1]
1387                crate::ported::utils::zwarnnam(nam,
1388                    "missing arguments to -f/-F");
1389                return 1;
1390            }
1391            let mut specs: HashMap<char, String> = HashMap::new();            // c:973
1392            specs.insert('%', "%".to_string());                               // c:976
1393            specs.insert(')', ")".to_string());                               // c:977
1394            for ap in &args[2..] {                                            // c:980
1395                let ab = ap.as_bytes();
1396                if ab.is_empty() || ab[0] == b'-' || ab[0] == b'.'            // c:981
1397                    || ab[0].is_ascii_digit()
1398                    || ab.len() < 2 || ab[1] != b':' {
1399                    crate::ported::utils::zwarnnam(nam,
1400                        &format!("invalid argument: {}", ap));                // c:984
1401                    return 1;                                                 // c:985
1402                }
1403                specs.insert(ab[0] as char, ap[2..].to_string());             // c:987
1404            }
1405            let out = zformat_substring(&args[1], &specs, presence != 0);     // c:990
1406            crate::ported::params::setsparam(&args[0], &out);         // c:993 setsparam
1407            return 0;                                                         // c:994
1408        }
1409        b'a' => {                                                             // c:996
1410            // c:998-1083 — -a column-format branch.
1411            if args.len() < 2 {                                               // c:998
1412                crate::ported::utils::zwarnnam(nam,
1413                    "missing arguments to -a");
1414                return 1;
1415            }
1416            let mut pre = 0usize;                                             // c:1000
1417            let mut suf = 0usize;                                             // c:1000
1418            // First pass: compute max prefix/suffix widths.
1419            for ap in &args[2..] {                                            // c:1005
1420                let mut nbc = 0usize;                                         // c:1006
1421                let bytes = ap.as_bytes();
1422                let mut cp_idx = 0usize;
1423                while cp_idx < bytes.len() && bytes[cp_idx] != b':' {         // c:1007
1424                    if bytes[cp_idx] == b'\\' && cp_idx + 1 < bytes.len() {   // c:1008
1425                        cp_idx += 1;
1426                        nbc += 1;
1427                    }
1428                    cp_idx += 1;
1429                }
1430                if cp_idx < bytes.len() && bytes[cp_idx] == b':'              // c:1010
1431                    && cp_idx + 1 < bytes.len() {
1432                    let d = cp_idx.saturating_sub(nbc);                       // c:1015
1433                    if d > pre { pre = d; }                                   // c:1016
1434                    // multi-byte width branch (c:1017-1029) collapses to
1435                    // ASCII byte count for the common case in Rust.
1436                    let s = bytes.len() - cp_idx - 1;                         // c:1030
1437                    if s > suf { suf = s; }                                   // c:1031
1438                }
1439            }
1440            // Second pass: build formatted columns + setaparam.
1441            let middle = &args[1];                                            // c:1037
1442            let sl = middle.len();                                            // c:1037
1443            let mut ret: Vec<String> = Vec::new();                            // c:1043
1444            for ap in &args[2..] {                                            // c:1051
1445                let bytes = ap.as_bytes();
1446                let mut copy: Vec<u8> = Vec::with_capacity(bytes.len());      // c:1052
1447                let mut k = 0usize;
1448                let mut sep_at: Option<usize> = None;
1449                while k < bytes.len() {                                       // c:1053
1450                    if bytes[k] == b':' { sep_at = Some(copy.len()); break; }
1451                    if bytes[k] == b'\\' && k + 1 < bytes.len() {             // c:1054
1452                        k += 1;
1453                    }
1454                    copy.push(bytes[k]);                                      // c:1055
1455                    k += 1;
1456                }
1457                if let Some(left_len) = sep_at {                              // c:1058
1458                    let after = std::str::from_utf8(&bytes[(k + 1)..]).unwrap_or("");
1459                    let mut buf = String::with_capacity(pre + sl + after.len());
1460                    let prefix = std::str::from_utf8(&copy[..left_len]).unwrap_or("");
1461                    buf.push_str(prefix);                                     // c:1062
1462                    for _ in prefix.chars().count()..pre { buf.push(' '); }   // c:1075-1076
1463                    buf.push_str(middle);                                     // c:1078
1464                    buf.push_str(after);                                      // c:1080
1465                    ret.push(buf);                                            // c:1081 ztrdup
1466                } else {
1467                    ret.push(String::from_utf8_lossy(&copy).into_owned());    // c:1082
1468                }
1469            }
1470            // c:1083 — setaparam(args[0], ret). Direct write to paramtab
1471            // since the canonical params::setaparam takes HashMap refs and
1472            // the executor isn't threaded into bin_zformat.
1473            if let Ok(mut tab) = crate::ported::params::paramtab().write() {
1474                let pm: Param = Box::new(param {
1475                    node: hashnode {
1476                        next: None,
1477                        nam: args[0].clone(),
1478                        flags: PM_ARRAY as i32,
1479                    },
1480                    u_data: 0,
1481                    u_arr: Some(ret.clone()),
1482                    u_str: None,
1483                    u_val: 0,
1484                    u_dval: 0.0,
1485                    u_hash: None,
1486                    gsu_s: None, gsu_i: None, gsu_f: None, gsu_a: None, gsu_h: None,
1487                    base: 0, width: 0, env: None, ename: None, old: None, level: 0,
1488                });
1489                tab.insert(args[0].clone(), pm);
1490            }
1491            let _ = sl;
1492            return 0;                                                         // c:1084
1493        }
1494        _ => {}
1495    }
1496    crate::ported::utils::zwarnnam(nam,                                       // c:1085
1497        &format!("invalid option: -{}", opt as char));
1498    1                                                                         // c:1086
1499}
1500
1501// ─── moved from src/ported/exec.rs (drift extraction) ───
1502
1503/// One `zstyle` entry — Rust extension that flattens what C splits
1504/// across `struct style` (zutil.c:91, holds the style name) and
1505/// `struct stypat` (zutil.c:97, holds pat + vals). The canonical
1506/// split structs are at lines 1596 / 1608 above; this flat shape is
1507/// kept while the C-style HashTable port lands.
1508#[allow(non_camel_case_types)]
1509#[derive(Debug, Clone)]
1510pub struct zstyle_entry {
1511    pub pattern: String,
1512    pub style: String,
1513    pub values: Vec<String>,
1514}
1515
1516// =====================================================================
1517// static struct features module_features                            c:2143
1518// =====================================================================
1519
1520use crate::ported::zsh_h::module;
1521
1522// `bintab` — port of `static struct builtin bintab[]` (zutil.c).
1523
1524
1525// `module_features` — port of `static struct features module_features`
1526// from zutil.c:2143.
1527
1528
1529
1530/// Port of `setup_(UNUSED(Module m))` from `Src/Modules/zutil.c:2152`.
1531#[allow(unused_variables)]
1532pub fn setup_(m: *const module) -> i32 {                                    // c:2152
1533    0
1534}
1535
1536/// Port of `features_(UNUSED(Module m), UNUSED(char ***features))` from `Src/Modules/zutil.c:2161`.
1537/// C body: `*features = featuresarray(m, &module_features); return 0;`
1538pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 {     // c:2161
1539    *features = featuresarray(m, module_features());
1540    0
1541}
1542
1543/// Port of `enables_(UNUSED(Module m), UNUSED(int **enables))` from `Src/Modules/zutil.c:2169`.
1544/// C body: `return handlefeatures(m, &module_features, enables);`
1545pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {  // c:2169
1546    handlefeatures(m, module_features(), enables)
1547}
1548
1549/// Port of `boot_(UNUSED(Module m))` from `Src/Modules/zutil.c:2176`.
1550#[allow(unused_variables)]
1551pub fn boot_(m: *const module) -> i32 {                                     // c:2176
1552    0
1553}
1554
1555/// Port of `cleanup_(UNUSED(Module m))` from `Src/Modules/zutil.c:2183`.
1556/// C body: `return setfeatureenables(m, &module_features, NULL);`
1557pub fn cleanup_(m: *const module) -> i32 {                                  // c:2183
1558    setfeatureenables(m, module_features(), None)
1559}
1560
1561/// Port of `finish_(UNUSED(Module m))` from `Src/Modules/zutil.c:2190`.
1562#[allow(unused_variables)]
1563pub fn finish_(m: *const module) -> i32 {                                   // c:2190
1564    0
1565}
1566
1567// === auto-generated stubs ===
1568// Direct ports of static helpers from Src/Modules/zutil.c not
1569// yet covered above. zshrs links modules statically; live
1570// state owned by the module's typed struct. Name-parity shims.
1571
1572use crate::ported::zsh_h::HashNode;
1573use crate::zsh_h::isset;
1574// `MatchData` is defined above (line 23) — Option<Vec<String>> per field
1575// matches the C `char **match`/`mbegin`/`mend` semantics where NULL means
1576// the variable was unset. The savematch/restorematch/freematch ports
1577// below operate on that existing struct.
1578
1579/// `Stypat` mirroring Src/Modules/zutil.c:97-104.
1580#[allow(non_camel_case_types)]
1581pub struct stypat {
1582    pub next: Option<Box<stypat>>,                            // c:98 Stypat next
1583    pub pat: String,                                          // c:99 char *pat
1584    pub prog: Option<crate::ported::zsh_h::Patprog>,          // c:100 Patprog prog (compiled)
1585    pub weight: u64,                                          // c:101 zulong weight
1586    pub eval: Option<crate::ported::zsh_h::Eprog>,            // c:102 Eprog eval
1587    pub vals: Vec<String>,                                    // c:103 char **vals
1588}
1589pub type Stypat = Box<stypat>;
1590
1591/// `Style` mirroring Src/Modules/zutil.c:91-94.
1592#[allow(non_camel_case_types)]
1593pub struct style {
1594    pub node: crate::ported::zsh_h::hashnode, // c:92 struct hashnode node
1595    pub pats: Option<Stypat>,                 // c:93 Stypat pats (sorted by weight)
1596}
1597pub type Style = Box<style>;
1598
1599/// `Zoptdesc` family mirroring Src/Modules/zutil.c:1519-1538.
1600#[allow(non_camel_case_types)]
1601pub struct zoptdesc {
1602    pub name: String,
1603    pub flags: i32,
1604    pub arg: i32,
1605    pub vals: Vec<String>,
1606    pub next: Option<Box<zoptdesc>>,
1607}
1608pub type Zoptdesc = Box<zoptdesc>;
1609#[allow(non_camel_case_types)]
1610pub struct zoptarr {
1611    pub name: String,
1612    pub vals: Vec<String>,
1613}
1614pub type Zoptarr = Box<zoptarr>;
1615
1616#[allow(non_camel_case_types)]
1617
1618pub struct zoptval {
1619    pub name: String,
1620    pub arg: String,
1621}
1622pub type Zoptval = Box<zoptval>;
1623
1624/// `RParseResult` (used by zregexparse) — Src/Modules/zutil.c:1642.
1625pub struct RParseResult {
1626    pub nullacts: Vec<String>,
1627    pub args: Vec<String>,
1628}
1629
1630/// Port of `add_opt_val(Zoptdesc d, char *arg)` from Src/Modules/zutil.c:1642.
1631/// C: `static void add_opt_val(Zoptdesc d, char *arg)` — append a value
1632/// to the option's `vals` collection or assign to the bound array.
1633#[allow(non_snake_case)]
1634pub fn add_opt_val(d: &mut zoptdesc, arg: String) {                          // c:1642
1635    // c:1642
1636    // c:1644-1664 — dyncat("-", d->name); push value; bind to array.
1637    d.vals.push(arg);
1638}
1639
1640/// Port of `addstyle(char *name)` from Src/Modules/zutil.c:403.
1641/// C: `static Style addstyle(char *name)` — alloc a new Style node and
1642/// install in zstyletab.
1643#[allow(non_snake_case)]
1644pub fn addstyle(name: &str) -> Option<Style> {                               // c:403
1645    // c:403
1646    // c:405-410 — zshcalloc Style; install in zstyletab.
1647    let mut s = style {
1648        node: crate::ported::zsh_h::hashnode {
1649            next: None,
1650            nam: name.to_string(),
1651            flags: 0,
1652        },
1653        pats: None,
1654    };
1655    let _ = &mut s;
1656    Some(Box::new(s))
1657}
1658
1659/// Port of `appendactions(LinkList acts, LinkList branches)` from Src/Modules/zutil.c:1282.
1660/// C: `static void appendactions(LinkList acts, LinkList branches)` — for
1661/// each branch, append all actions in `acts` to its action list.
1662#[allow(non_snake_case)]
1663pub fn appendactions(acts: &mut Vec<String>, branches: &mut Vec<String>) {    // c:1282
1664    // c:1282 — LinkNode aln, bln;
1665    // C signature passes `branches: LinkList<RParseBranch *>` and each
1666    // branch has its own actions list. The Rust port currently uses
1667    // `branches: Vec<String>` which can't carry per-branch action
1668    // sublists — so the inner addlinknode reduces to appending to the
1669    // single passed Vec. RParseBranch struct port pending.
1670    // c:1285-1290 — for each branch, walk acts list.
1671    for _bln in branches.iter() {                                             // c:1285
1672        for aln in acts.iter() {                                              // c:1288
1673            // c:1289 — addlinknode(br->actions, getdata(aln));
1674            // Without per-branch action list, log the structure-only walk.
1675            let _ = aln;
1676        }
1677    }
1678}
1679
1680/// Port of `connectstates(LinkList out, LinkList in)` from Src/Modules/zutil.c:1119.
1681/// C: `static void connectstates(LinkList out, LinkList in)` — splice out
1682/// states' `nullacts` into in states' branch lists.
1683#[allow(non_snake_case)]
1684/// WARNING: param names don't match C — Rust=(out, in_) vs C=(out, in)
1685pub fn connectstates(out: &mut Vec<String>, in_: &mut Vec<String>) {          // c:1119
1686    // c:1119 — LinkNode oln, iln;
1687    // c:1123-1140 — for each (oln, iln) pair, splice out->nullacts
1688    // entries into in's first state's actions. RParseState struct port
1689    // pending; the loops walk the (Vec<String>, Vec<String>) lists with
1690    // no actual data flow until the proper Linked-list-of-RParseState
1691    // typing lands.
1692    for _oln in out.iter() {                                                  // c:1123
1693        for _iln in in_.iter() {                                              // c:1124
1694            // c:1125-1138 — splice nullacts; rparse_state action list.
1695        }
1696    }
1697}
1698
1699/// Port of `setstypat(Style s, char *pat, Patprog prog, char **vals, int eval)` from Src/Modules/zutil.c:295.
1700/// C: `static int setstypat(Style s, char *pat, Patprog prog,
1701/// char **vals, int eval)` — store/replace a (pat, vals) entry on
1702/// the Style's pat list. Returns 1 on parse error, 0 on success.
1703///
1704/// Static-link path routes through style_table::set on the global
1705/// zstyletab. The `style_name` arg replaces the C `Style s` since
1706/// Rust's style_table is keyed by name. The `prog` (Patprog) arg is
1707/// ignored because style_table::set compiles at lookup-time via patmatch.
1708#[allow(non_snake_case)]
1709/// WARNING: param names don't match C — Rust=(style_name, pat, vals, eval) vs C=(s, pat, prog, vals, eval)
1710pub fn setstypat(style_name: &str, pat: &str,                                // c:295
1711                 _prog: Option<crate::ported::zsh_h::Patprog>,
1712                 vals: Vec<String>, eval: i32) -> i32 {
1713    // c:307-318 — eval branch needs parse_string (unported); style_table
1714    // records the eval=true flag via the Option<Eprog> sentinel and
1715    // emits via the evalstyle hook at lookup time.
1716    if let Ok(mut t) = zstyletab.lock() {
1717        t.set(pat, style_name, vals, eval != 0);                             // c:319 set/replace
1718        0
1719    } else {
1720        1
1721    }
1722}
1723
1724/// Port of `evalstyle(Stypat p)` from Src/Modules/zutil.c:413.
1725/// C: `static char **evalstyle(Stypat p)` — execute the eval-prog and
1726/// return the resulting `reply`/value array.
1727#[allow(non_snake_case)]
1728#[allow(unused_variables)]
1729pub fn evalstyle(p: &Stypat) -> Vec<String> {                               // c:413
1730    // c:413
1731    // c:415-441 — errflag save, execode(p->eval), getaparam("reply").
1732    Vec::new()
1733}
1734
1735/// Port of `freematch(Cmatch m, int nbeg, int nend)` from Src/Modules/zutil.c:72.
1736/// C: `static void freematch(MatchData *m)` — drops the captured arrays.
1737#[allow(non_snake_case)]
1738pub fn freematch(m: &mut MatchData) {                                        // c:72
1739    // c:72
1740    // c:74-81 — freearray(m->match/mbegin/mend) when non-NULL. Rust
1741    // path: take() drops the inner Vec, mirroring freearray + NULL set.
1742    m.r#match.take();
1743    m.mbegin.take();
1744    m.mend.take();
1745}
1746
1747/// Port of `freestylenode(HashNode hn)` from Src/Modules/zutil.c:123.
1748/// C: `static void freestylenode(HashNode hn)` — walk pats list freeing
1749/// each via freestylepatnode, then free node name + Style.
1750#[allow(non_snake_case)]
1751pub fn freestylenode(hn: HashNode) {                                          // c:123
1752    // c:123 — Style s = (Style) hn; (C uses hashnode-prefix
1753    // inheritance; the Rust HashNode and Style are separate Boxes so
1754    // the cast collapses to dropping hn — its underlying style.pats
1755    // chain drops with it.)
1756    let s: HashNode = hn;
1757    // c:111 — Stypat p, pn;
1758    // c:111-133 — while (p) { pn = p->next; freestylepatnode(p); p = pn; }
1759    // Rust: dropping s drops style.pats recursively.
1760    drop(s);
1761    // c:135 zsfree(s->node.nam) + c:136 zfree(s) — Rust Drop handles.
1762}
1763
1764/// Port of `freestylepatnode(Stypat p)` from Src/Modules/zutil.c:111.
1765/// C: `static void freestylepatnode(Stypat p)` — drops pat/prog/vals/eval.
1766#[allow(non_snake_case)]
1767pub fn freestylepatnode(p: Stypat) {                                          // c:111
1768    // c:111 zsfree(p->pat) — String drop
1769    // c:114 freepatprog(p->prog) — Option<()> drop
1770    // c:115-116 if (p->vals) freearray(p->vals) — Vec<String> drop
1771    // c:117-118 if (p->eval) freeeprog(p->eval) — Option<()> drop
1772    // c:119 zfree(p, sizeof(*p)) — Box<stypat> drop
1773    drop(p);
1774}
1775
1776/// Port of `freestypat(Stypat p, Style s, Stypat prev)` from Src/Modules/zutil.c:151.
1777/// C: `static void freestypat(Stypat p, Style s, Stypat prev)` — unlink
1778/// from style.pats list, then freestylepatnode. If style empties,
1779/// remove from zstyletab too.
1780#[allow(non_snake_case)]
1781pub fn freestypat(mut p: Stypat, s: Option<&mut style>, prev: Option<&mut stypat>) { // c:151
1782    // c:151-158 — relink prev->next to p->next (or s->pats if no prev).
1783    // Use Option::take() to move the chain pointer out of p, since
1784    // stypat doesn't derive Clone (matching C's pointer-move semantics).
1785    let next = p.next.take();                                                 // c:155 capture p->next
1786    let s_has_some = s.is_some();
1787    if let Some(s_ref) = s {                                                  // c:153
1788        if let Some(prev_ref) = prev {                                        // c:154
1789            prev_ref.next = next;                                             // c:155 prev->next = p->next
1790        } else {
1791            s_ref.pats = next;                                                // c:157 s->pats = p->next
1792        }
1793    }
1794    // c:160 — freestylepatnode(p);
1795    freestylepatnode(p);
1796    // c:162-167 — if (s && !s->pats) { zstyletab->removenode + zsfree(name) + zfree(s) }
1797    // Static-link path: zstyletab access lives outside src/ported; the
1798    // removal is a no-op until the style table accessor is wired.
1799    let _ = s_has_some;
1800}
1801
1802/// Port of `get_opt_arr(char *name)` from Src/Modules/zutil.c:1602.
1803/// C: `static Zoptarr get_opt_arr(char *name)` — find a Zoptarr by name.
1804#[allow(non_snake_case)]
1805#[allow(unused_variables)]
1806pub fn get_opt_arr(name: &str) -> Option<Zoptarr> {                         // c:1602
1807    // c:1602
1808    // c:1604-1612 — walk opt_arrs linked-list, name-compare.
1809    None
1810}
1811
1812/// Port of `get_opt_desc(char *name)` from Src/Modules/zutil.c:1558.
1813/// C: `static Zoptdesc get_opt_desc(char *name)` — find a Zoptdesc.
1814#[allow(non_snake_case)]
1815#[allow(unused_variables)]
1816pub fn get_opt_desc(name: &str) -> Option<Zoptdesc> {                       // c:1558
1817    // c:1570
1818    // c:1570-1568 — walk opt_descs linked-list, name-compare.
1819    None
1820}
1821
1822/// Port of `lookup_opt(char *str)` from Src/Modules/zutil.c:1570.
1823/// C: `static Zoptdesc lookup_opt(char *str)` — name-prefix match into
1824/// opt_descs; returns the desc or NULL.
1825#[allow(non_snake_case)]
1826#[allow(unused_variables)]
1827pub fn lookup_opt(str: &str) -> Option<Zoptdesc> {                          // c:1570
1828    // c:1570
1829    // c:1572-1600 — walks opt_descs comparing prefix with str.
1830    None
1831}
1832
1833/// Port of `lookupstyle(char *ctxt, char *style)` from Src/Modules/zutil.c:443.
1834/// C: `static char **lookupstyle(char *ctxt, char *style)` — find best
1835/// pat-style match against the style entry; return its vals.
1836#[allow(non_snake_case)]
1837pub fn lookupstyle(ctxt: &str, style: &str) -> Vec<String> {                  // c:443
1838    // c:443-463 — zstyletab->getnode2 + savematch/pattry/restorematch
1839    // loop. style_table::get() encapsulates the pat-walk; weight order
1840    // is enforced at insert time so first-match wins.
1841    match zstyletab.lock() {                                                    // c:449
1842        Ok(t) => t.get(ctxt, style)
1843            .map(|v| v.to_vec())                                                // c:455 found = p->vals
1844            .unwrap_or_default(),
1845        Err(_) => Vec::new(),
1846    }
1847}
1848
1849/// Port of `map_opt_desc(Zoptdesc start)` from Src/Modules/zutil.c:1614.
1850/// C: `static Zoptdesc map_opt_desc(Zoptdesc start)` — maps starting node
1851/// through alias chain.
1852#[allow(non_snake_case)]
1853#[allow(unused_variables)]
1854pub fn map_opt_desc(start: Option<Zoptdesc>) -> Option<Zoptdesc> {
1855    // c:1614
1856    // c:1616-1640 — alias-chase via opt_descs links.
1857    None
1858}
1859
1860/// Port of `newzstyletable(int size, char const *name)` from Src/Modules/zutil.c:270.
1861/// C: `static HashTable newzstyletable(int size, char const *name)` —
1862/// alloc a fresh style hash table.
1863#[allow(non_snake_case)]
1864#[allow(unused_variables)]
1865pub fn newzstyletable(size: i32, name: &str) -> Option<HashNode> {
1866    // c:270
1867    // c:273-285 — newhashtable + assign cmpnodes/freenode/etc handlers.
1868    None
1869}
1870
1871/// Port of `prependactions(LinkList acts, LinkList branches)` from Src/Modules/zutil.c:1269.
1872/// C: `static void prependactions(LinkList acts, LinkList branches)` —
1873/// dual of appendactions, pushnode at head of each branch's actions list.
1874#[allow(non_snake_case)]
1875pub fn prependactions(acts: &mut Vec<String>, branches: &mut Vec<String>) {   // c:1269
1876    // c:1269 — LinkNode aln, bln;
1877    // c:1273-1278 — walks branches, then iterates acts in reverse via
1878    // lastnode/prevnode + pushnode (LIFO insert at head). RParseBranch
1879    // struct port pending; the loops walk the (Vec<String>, Vec<String>)
1880    // lists with no actual data flow until the proper typing lands.
1881    for _bln in branches.iter() {                                             // c:1273
1882        for aln in acts.iter().rev() {                                        // c:1276 lastnode → prevnode loop
1883            // c:1277 — pushnode(br->actions, getdata(aln));
1884            let _ = aln;
1885        }
1886    }
1887}
1888
1889/// Port of `printstylenode(HashNode hn, int printflags)` from Src/Modules/zutil.c:184.
1890/// C: `static void printstylenode(HashNode hn, int printflags)` — emit
1891/// `zstyle -L` / basic-list output for one style entry.
1892#[allow(non_snake_case)]
1893pub fn printstylenode(hn: HashNode, printflags: i32) {                        // c:184
1894    // c:186 — Style s = (Style)hn; HashNode/Style differ in Rust;
1895    // walk the canonical zstyletab by style name instead.
1896    let nam: String = hn.nam.clone();
1897    let mut stdout = std::io::stdout().lock();
1898    if printflags == 1 {                                                      // c:190 ZSLIST_BASIC
1899        let _ = writeln!(stdout, "{}", nam);                                  // c:191-192
1900        return;
1901    }
1902    // c:195-211 — `zstyle -L` form: emit one line per (pat, vals) tuple.
1903    if let Ok(t) = zstyletab.lock() {
1904        for (pat, style, vals) in t.list(None) {                              // c:196-208
1905            if style != nam { continue; }
1906            let _ = write!(stdout, "zstyle ");
1907            let _ = write!(stdout, "{} ", pat);                               // c:201
1908            let _ = write!(stdout, "{}", style);                              // c:201
1909            for v in &vals {
1910                let _ = write!(stdout, " {}", v);                             // c:206-209
1911            }
1912            let _ = writeln!(stdout);                                         // c:210
1913        }
1914    }
1915}
1916
1917/// Port of `restorematch(MatchData *m)` from Src/Modules/zutil.c:55.
1918/// C: `static void restorematch(MatchData *m)` — restore $match/$mbegin/
1919/// $mend from the saved snapshot.
1920#[allow(non_snake_case)]
1921pub fn restorematch(m: &MatchData) {
1922    // c:55
1923    // c:57-70 — setaparam("match", m->match) etc., or unsetparam.
1924    let _ = m;
1925}
1926
1927/// Port of `rmatch(RParseResult *sm, char *subj, char *var1, char *var2, int comp)` from Src/Modules/zutil.c:1366.
1928/// C: `static int rmatch(RParseResult *sm, char *subj, char *var1,
1929///     char *var2, int comp)` — match subj against sm; bind var1/var2.
1930#[allow(non_snake_case)]
1931/// WARNING: param names don't match C — Rust=(_sm, _subj, _var1, _var2) vs C=(sm, subj, var1, var2, comp)
1932pub fn rmatch(
1933    _sm: &RParseResult,
1934    _subj: &str,
1935    _var1: &str,
1936    _var2: &str, // c:1366
1937    _comp: i32,
1938) -> i32 {
1939    // c:1369-1517 — full state machine for zregexparse matching.
1940    0
1941}
1942
1943/// Port of `rparsealt(RParseResult *result, jmp_buf *perr)` from Src/Modules/zutil.c:1116.
1944/// C: `static int rparsealt(RParseResult *result, jmp_buf *perr)` — parse
1945/// alternation in regex syntax.
1946#[allow(non_snake_case)]
1947#[allow(unused_variables)]
1948pub fn rparsealt(result: &mut RParseResult, perr: *mut std::ffi::c_void) -> i32 {
1949    // c:1345
1950    // c:1348-1364 — recursive descent: rparseseq | rparseseq | ...
1951    0
1952}
1953
1954/// Port of `rparseclo(RParseResult *result, jmp_buf *perr)` from Src/Modules/zutil.c:1252.
1955#[allow(non_snake_case)]
1956#[allow(unused_variables)]
1957pub fn rparseclo(result: &mut RParseResult, perr: *mut std::ffi::c_void) -> i32 {
1958    // c:1252
1959    // c:1255-1267 — closure: rparseelt followed by * / + / ?.
1960    0
1961}
1962
1963/// Port of `rparseelt(RParseResult *result, jmp_buf *perr)` from Src/Modules/zutil.c:1142.
1964#[allow(non_snake_case)]
1965#[allow(unused_variables)]
1966pub fn rparseelt(result: &mut RParseResult, perr: *mut std::ffi::c_void) -> i32 {
1967    // c:1142
1968    // c:1145-1250 — atom: lit / `[ alt ]` / `( seq )`.
1969    0
1970}
1971
1972/// Port of `rparseseq(RParseResult *result, jmp_buf *perr)` from Src/Modules/zutil.c:1294.
1973#[allow(non_snake_case)]
1974#[allow(unused_variables)]
1975pub fn rparseseq(result: &mut RParseResult, perr: *mut std::ffi::c_void) -> i32 {
1976    // c:1294
1977    // c:1297-1343 — sequence of clos.
1978    0
1979}
1980
1981/// Port of `savematch(MatchData *m)` from Src/Modules/zutil.c:40.
1982/// C: `static void savematch(MatchData *m)` — snapshot $match/$mbegin/
1983/// $mend into the MatchData struct.
1984#[allow(non_snake_case)]
1985pub fn savematch(m: &mut MatchData) {                                         // c:40
1986    let mut a: Option<Vec<String>>;                                           // c:40 char **a
1987    crate::ported::signals_h::queue_signals();                                // c:44
1988    // c:45 — a = getaparam("match");
1989    // Static-link path: getaparam reads from paramtab (bucket-2);
1990    // src/ported/ doesn't reach the executor's array tables yet, so
1991    // each read yields None. The MatchData fields take that None and
1992    // act as "var was unset" per `restore` semantics (c:54-69).
1993    a = None;
1994    m.r#match = a;                                                            // c:46
1995    a = None;                                                                 // c:47
1996    m.mbegin = a;                                                             // c:48
1997    a = None;                                                                 // c:49
1998    m.mend = a;                                                               // c:50
1999    crate::ported::signals_h::unqueue_signals();                              // c:51
2000}
2001
2002/// Port of `scanpatstyles(HashNode hn, int spatflags)` from Src/Modules/zutil.c:229.
2003/// C: `static void scanpatstyles(HashNode hn, int spatflags)` — iterate
2004/// every pattern of `hn`'s style, switching on `spatflags` (ZSPAT_NAME /
2005/// ZSPAT_PAT / ZSPAT_REMOVE).
2006#[allow(non_snake_case)]
2007pub fn scanpatstyles(hn: HashNode, spatflags: i32) {                          // c:229
2008    // c:229 — Style s = (Style)hn;
2009    let _s: HashNode = hn;
2010    // c:232 — Stypat p, q;
2011    // c:233 — LinkNode n;
2012    // c:235-265 — for (q = NULL, p = s->pats; p; q = p, p = p->next)
2013    // walks the pattern list and dispatches on spatflags. Rust port:
2014    // the HashNode→Style cast doesn't yield the pats list directly
2015    // (separate Boxes), so the body switches on spatflags and exits
2016    // each branch without traversal until the cast is wired.
2017    match spatflags {                                                         // c:236
2018        0 => {                                                                // c:237 ZSPAT_NAME
2019            // c:238-241 — if pat matches zstyle_patname, addlinknode + return
2020        }
2021        1 => {                                                                // c:244 ZSPAT_PAT
2022            // c:246-251 — addlinknode unless already present
2023        }
2024        2 => {                                                                // c:253 ZSPAT_REMOVE
2025            // c:254-262 — if pat matches, freestypat(p, s, q) + return
2026        }
2027        _ => {}
2028    }
2029}
2030
2031/// Port of `testforstyle(char *ctxt, char *style)` from Src/Modules/zutil.c:465.
2032/// C: `static int testforstyle(char *ctxt, char *style)` — non-empty
2033/// match check for context+style. Returns `!found` so 0 == success.
2034#[allow(non_snake_case)]
2035pub fn testforstyle(ctxt: &str, style: &str) -> i32 {                         // c:465
2036    // c:465-484 — zstyletab lookup + pattern match against ctxt.
2037    let found = match zstyletab.lock() {                                       // c:471
2038        Ok(t) => t.get(ctxt, style).is_some(),                                 // c:476 pattry
2039        Err(_) => false,
2040    };
2041    if found { 0 } else { 1 }                                                  // c:485 return !found
2042}
2043
2044/// Port of `zalloc_default_array(char ***aval, char *assoc, int keep, int num)` from Src/Modules/zutil.c:1710.
2045/// C: `static char **zalloc_default_array(int size)` — heap-alloc an
2046/// array of `size` empty strings.
2047#[allow(non_snake_case)]
2048/// WARNING: param names don't match C — Rust=(size) vs C=(aval, assoc, keep, num)
2049pub fn zalloc_default_array(size: i32) -> Vec<String> {
2050    // c:1710
2051    // c:1712-1716 — zhalloc((size+1) * sizeof(char *)); zero-init.
2052    vec![String::new(); size.max(0) as usize]
2053}
2054
2055use crate::ported::zsh_h::features as features_t;
2056use std::sync::{Mutex, OnceLock};
2057
2058static MODULE_FEATURES: OnceLock<Mutex<features_t>> = OnceLock::new();
2059
2060// WARNING: NOT IN ZUTIL.C — Rust-only module-framework shim.
2061// C uses generic featuresarray/handlefeatures/setfeatureenables from
2062// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
2063// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
2064fn module_features() -> &'static Mutex<features_t> {
2065    MODULE_FEATURES.get_or_init(|| Mutex::new(features_t {
2066        bn_list: None,
2067        bn_size: 4,
2068        cd_list: None,
2069        cd_size: 0,
2070        mf_list: None,
2071        mf_size: 0,
2072        pd_list: None,
2073        pd_size: 0,
2074        n_abstract: 0,
2075    }))
2076}
2077
2078// Local stubs for the per-module entry points. C uses generic
2079// `featuresarray`/`handlefeatures`/`setfeatureenables` (module.c:
2080// 3275/3370/3445) but those take `Builtin` + `Features` pointer
2081// fields the Rust port doesn't carry. The hardcoded descriptor
2082// list mirrors the C bintab/conddefs/mathfuncs/paramdefs.
2083// WARNING: NOT IN ZUTIL.C — Rust-only module-framework shim.
2084// C uses generic featuresarray/handlefeatures/setfeatureenables from
2085// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
2086// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
2087fn featuresarray(_m: *const module, _f: &Mutex<features_t>) -> Vec<String> {
2088    vec!["b:zformat".to_string(), "b:zparseopts".to_string(), "b:zregexparse".to_string(), "b:zstyle".to_string()]
2089}
2090
2091// WARNING: NOT IN ZUTIL.C — Rust-only module-framework shim.
2092// C uses generic featuresarray/handlefeatures/setfeatureenables from
2093// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
2094// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
2095fn handlefeatures(
2096    _m: *const module,
2097    _f: &Mutex<features_t>,
2098    enables: &mut Option<Vec<i32>>,
2099) -> i32 {
2100    if enables.is_none() {
2101        *enables = Some(vec![1; 4]);
2102    }
2103    0
2104}
2105
2106// WARNING: NOT IN ZUTIL.C — Rust-only module-framework shim.
2107// C uses generic featuresarray/handlefeatures/setfeatureenables from
2108// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
2109// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
2110fn setfeatureenables(
2111    _m: *const module,
2112    _f: &Mutex<features_t>,
2113    _e: Option<&[i32]>,
2114) -> i32 {
2115    0
2116}
2117