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(¶ms_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(¶ms_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(©[..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(©).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