Skip to main content

zsh/
zutil.rs

1//! Zsh utility builtins - port of Modules/zutil.c
2//!
3//! Provides zstyle, zformat, zparseopts builtins.
4
5use regex::Regex;
6use std::collections::HashMap;
7
8/// Style pattern with associated values
9#[derive(Debug, Clone)]
10pub struct StylePattern {
11    pub pattern: String,
12    pub weight: u64,
13    pub values: Vec<String>,
14    pub eval: bool,
15}
16
17impl StylePattern {
18    pub fn new(pattern: &str, values: Vec<String>, eval: bool) -> Self {
19        let weight = Self::calculate_weight(pattern);
20        Self {
21            pattern: pattern.to_string(),
22            weight,
23            values,
24            eval,
25        }
26    }
27
28    fn calculate_weight(pattern: &str) -> u64 {
29        let mut weight: u64 = 0;
30        let mut tmp = 2u64;
31        let mut first = true;
32
33        for ch in pattern.chars() {
34            if first && ch == '*' {
35                tmp = 0;
36                continue;
37            }
38            first = false;
39
40            if ch == '('
41                || ch == '|'
42                || ch == '*'
43                || ch == '['
44                || ch == '<'
45                || ch == '?'
46                || ch == '#'
47                || ch == '^'
48            {
49                tmp = 1;
50            }
51
52            if ch == ':' {
53                weight += 1 << 32;
54                first = true;
55                weight += tmp;
56                tmp = 2;
57            }
58        }
59        weight + tmp
60    }
61
62    pub fn matches(&self, context: &str) -> bool {
63        if self.pattern == "*" {
64            return true;
65        }
66
67        let regex_pattern = glob_to_regex(&self.pattern);
68        if let Ok(re) = Regex::new(&regex_pattern) {
69            re.is_match(context)
70        } else {
71            self.pattern == context
72        }
73    }
74}
75
76fn glob_to_regex(pattern: &str) -> String {
77    let mut result = String::from("^");
78    for ch in pattern.chars() {
79        match ch {
80            '*' => result.push_str(".*"),
81            '?' => result.push('.'),
82            '.' | '+' | '(' | ')' | '[' | ']' | '{' | '}' | '^' | '$' | '|' | '\\' => {
83                result.push('\\');
84                result.push(ch);
85            }
86            _ => result.push(ch),
87        }
88    }
89    result.push('$');
90    result
91}
92
93/// Style storage - maps style names to patterns
94#[derive(Debug, Default)]
95pub struct StyleTable {
96    styles: HashMap<String, Vec<StylePattern>>,
97}
98
99impl StyleTable {
100    pub fn new() -> Self {
101        Self::default()
102    }
103
104    pub fn set(&mut self, pattern: &str, style: &str, values: Vec<String>, eval: bool) {
105        let style_patterns = self.styles.entry(style.to_string()).or_default();
106
107        if let Some(existing) = style_patterns.iter_mut().find(|p| p.pattern == pattern) {
108            existing.values = values;
109            existing.eval = eval;
110        } else {
111            let sp = StylePattern::new(pattern, values, eval);
112            let weight = sp.weight;
113            let pos = style_patterns
114                .iter()
115                .position(|p| p.weight < weight)
116                .unwrap_or(style_patterns.len());
117            style_patterns.insert(pos, sp);
118        }
119    }
120
121    pub fn get(&self, context: &str, style: &str) -> Option<&[String]> {
122        self.styles.get(style).and_then(|patterns| {
123            patterns
124                .iter()
125                .find(|p| p.matches(context))
126                .map(|p| p.values.as_slice())
127        })
128    }
129
130    pub fn delete(&mut self, pattern: Option<&str>, style: Option<&str>) {
131        match (pattern, style) {
132            (None, None) => self.styles.clear(),
133            (Some(pat), None) => {
134                for patterns in self.styles.values_mut() {
135                    patterns.retain(|p| p.pattern != pat);
136                }
137                self.styles.retain(|_, v| !v.is_empty());
138            }
139            (Some(pat), Some(sty)) => {
140                if let Some(patterns) = self.styles.get_mut(sty) {
141                    patterns.retain(|p| p.pattern != pat);
142                    if patterns.is_empty() {
143                        self.styles.remove(sty);
144                    }
145                }
146            }
147            (None, Some(sty)) => {
148                self.styles.remove(sty);
149            }
150        }
151    }
152
153    pub fn list(&self, context: Option<&str>) -> Vec<(String, String, Vec<String>)> {
154        let mut result = Vec::new();
155        for (style, patterns) in &self.styles {
156            for pat in patterns {
157                if let Some(ctx) = context {
158                    if !pat.matches(ctx) {
159                        continue;
160                    }
161                }
162                result.push((style.clone(), pat.pattern.clone(), pat.values.clone()));
163            }
164        }
165        result
166    }
167
168    pub fn list_styles(&self) -> Vec<&str> {
169        self.styles.keys().map(|s| s.as_str()).collect()
170    }
171
172    pub fn list_patterns(&self) -> Vec<&str> {
173        let mut patterns = Vec::new();
174        for pats in self.styles.values() {
175            for pat in pats {
176                if !patterns.contains(&pat.pattern.as_str()) {
177                    patterns.push(pat.pattern.as_str());
178                }
179            }
180        }
181        patterns
182    }
183
184    pub fn test(&self, context: &str, style: &str, values: Option<&[&str]>) -> bool {
185        if let Some(found) = self.get(context, style) {
186            if let Some(test_vals) = values {
187                test_vals.iter().any(|v| found.contains(&v.to_string()))
188            } else {
189                matches!(
190                    found.first().map(|s| s.as_str()),
191                    Some("true" | "yes" | "on" | "1")
192                )
193            }
194        } else {
195            false
196        }
197    }
198
199    pub fn test_bool(&self, context: &str, style: &str) -> Option<bool> {
200        self.get(context, style).and_then(|vals| {
201            if vals.len() == 1 {
202                match vals[0].as_str() {
203                    "yes" | "true" | "on" | "1" => Some(true),
204                    "no" | "false" | "off" | "0" => Some(false),
205                    _ => None,
206                }
207            } else {
208                None
209            }
210        })
211    }
212}
213
214/// Format a string with specifications
215pub fn zformat(format: &str, specs: &HashMap<char, String>, _presence: bool) -> String {
216    let mut result = String::new();
217    let mut chars = format.chars().peekable();
218
219    while let Some(ch) = chars.next() {
220        if ch == '%' {
221            let mut right = false;
222            let mut min: Option<usize> = None;
223            let mut max: Option<usize> = None;
224
225            if chars.peek() == Some(&'-') {
226                right = true;
227                chars.next();
228            }
229
230            let mut num_str = String::new();
231            while let Some(&c) = chars.peek() {
232                if c.is_ascii_digit() {
233                    num_str.push(c);
234                    chars.next();
235                } else {
236                    break;
237                }
238            }
239            if !num_str.is_empty() {
240                min = num_str.parse().ok();
241            }
242
243            if chars.peek() == Some(&'.') || chars.peek() == Some(&'(') {
244                let is_ternary = chars.peek() == Some(&'(');
245                if !is_ternary {
246                    chars.next();
247                }
248
249                let mut max_str = String::new();
250                while let Some(&c) = chars.peek() {
251                    if c.is_ascii_digit() {
252                        max_str.push(c);
253                        chars.next();
254                    } else {
255                        break;
256                    }
257                }
258                if !max_str.is_empty() {
259                    max = max_str.parse().ok();
260                }
261            }
262
263            if let Some(&spec_char) = chars.peek() {
264                chars.next();
265
266                if spec_char == '(' {
267                    continue;
268                }
269
270                if let Some(spec_val) = specs.get(&spec_char) {
271                    let mut val = spec_val.clone();
272
273                    if let Some(m) = max {
274                        if val.len() > m {
275                            val.truncate(m);
276                        }
277                    }
278
279                    let out_len = min.map(|m| m.max(val.len())).unwrap_or(val.len());
280
281                    if val.len() >= out_len {
282                        result.push_str(&val[..out_len]);
283                    } else {
284                        let padding = out_len - val.len();
285                        if right {
286                            result.push_str(&" ".repeat(padding));
287                            result.push_str(&val);
288                        } else {
289                            result.push_str(&val);
290                            result.push_str(&" ".repeat(padding));
291                        }
292                    }
293                } else if spec_char == '%' {
294                    result.push('%');
295                }
296            }
297        } else {
298            result.push(ch);
299        }
300    }
301
302    result
303}
304
305/// Option description for zparseopts
306#[derive(Debug, Clone)]
307pub struct OptDesc {
308    pub name: String,
309    pub takes_arg: bool,
310    pub optional_arg: bool,
311    pub multiple: bool,
312    pub array_name: Option<String>,
313}
314
315impl OptDesc {
316    pub fn parse(spec: &str) -> Option<Self> {
317        if spec.is_empty() {
318            return None;
319        }
320
321        let mut name = String::new();
322        let mut takes_arg = false;
323        let mut optional_arg = false;
324        let mut multiple = false;
325        let mut array_name = None;
326        let mut chars = spec.chars().peekable();
327
328        while let Some(&ch) = chars.peek() {
329            if ch == '+' {
330                multiple = true;
331                chars.next();
332                break;
333            } else if ch == ':' || ch == '=' {
334                break;
335            } else if ch == '\\' {
336                chars.next();
337                if let Some(c) = chars.next() {
338                    name.push(c);
339                }
340            } else {
341                name.push(ch);
342                chars.next();
343            }
344        }
345
346        if name.is_empty() {
347            return None;
348        }
349
350        if chars.peek() == Some(&':') {
351            takes_arg = true;
352            chars.next();
353            if chars.peek() == Some(&':') {
354                optional_arg = true;
355                chars.next();
356            }
357        }
358
359        if chars.peek() == Some(&'=') {
360            chars.next();
361            array_name = Some(chars.collect());
362        }
363
364        Some(Self {
365            name,
366            takes_arg,
367            optional_arg,
368            multiple,
369            array_name,
370        })
371    }
372}
373
374/// Parse options from arguments
375pub fn zparseopts(
376    args: &[String],
377    specs: &[OptDesc],
378    delete: bool,
379    extract: bool,
380) -> Result<(HashMap<String, Vec<String>>, Vec<String>), String> {
381    let mut results: HashMap<String, Vec<String>> = HashMap::new();
382    let mut remaining = Vec::new();
383    let mut i = 0;
384
385    let short_opts: HashMap<char, &OptDesc> = specs
386        .iter()
387        .filter(|s| s.name.len() == 1)
388        .map(|s| (s.name.chars().next().unwrap(), s))
389        .collect();
390
391    let long_opts: HashMap<&str, &OptDesc> = specs
392        .iter()
393        .filter(|s| s.name.len() > 1)
394        .map(|s| (s.name.as_str(), s))
395        .collect();
396
397    while i < args.len() {
398        let arg = &args[i];
399
400        if !arg.starts_with('-') || arg == "-" {
401            if extract {
402                if !delete {
403                    remaining.push(arg.clone());
404                }
405                i += 1;
406                continue;
407            } else {
408                remaining.extend(args[i..].iter().cloned());
409                break;
410            }
411        }
412
413        if arg == "--" {
414            i += 1;
415            remaining.extend(args[i..].iter().cloned());
416            break;
417        }
418
419        let opt_str = &arg[1..];
420
421        if let Some(desc) = long_opts.get(opt_str) {
422            let key = format!("-{}", desc.name);
423            let entry = results.entry(key).or_default();
424
425            if desc.takes_arg {
426                if i + 1 < args.len() && !desc.optional_arg {
427                    i += 1;
428                    entry.push(args[i].clone());
429                } else if desc.optional_arg {
430                    entry.push(String::new());
431                } else {
432                    return Err(format!("missing argument for option: -{}", desc.name));
433                }
434            } else {
435                entry.push(String::new());
436            }
437        } else if opt_str.starts_with('-') {
438            let long_name = &opt_str[1..];
439            if let Some((name, value)) = long_name.split_once('=') {
440                if let Some(desc) = long_opts.get(name) {
441                    let key = format!("-{}", desc.name);
442                    results.entry(key).or_default().push(value.to_string());
443                } else {
444                    if !extract {
445                        remaining.extend(args[i..].iter().cloned());
446                        break;
447                    }
448                    remaining.push(arg.clone());
449                }
450            } else if let Some(desc) = long_opts.get(long_name) {
451                let key = format!("-{}", desc.name);
452                let entry = results.entry(key).or_default();
453
454                if desc.takes_arg {
455                    if i + 1 < args.len() && !desc.optional_arg {
456                        i += 1;
457                        entry.push(args[i].clone());
458                    } else if desc.optional_arg {
459                        entry.push(String::new());
460                    } else {
461                        return Err(format!("missing argument for option: --{}", desc.name));
462                    }
463                } else {
464                    entry.push(String::new());
465                }
466            } else {
467                if !extract {
468                    remaining.extend(args[i..].iter().cloned());
469                    break;
470                }
471                remaining.push(arg.clone());
472            }
473        } else {
474            let mut j = 0;
475            let chars: Vec<char> = opt_str.chars().collect();
476
477            while j < chars.len() {
478                let ch = chars[j];
479                if let Some(desc) = short_opts.get(&ch) {
480                    let key = format!("-{}", desc.name);
481                    let entry = results.entry(key).or_default();
482
483                    if desc.takes_arg {
484                        if j + 1 < chars.len() {
485                            entry.push(chars[j + 1..].iter().collect());
486                            break;
487                        } else if i + 1 < args.len() && !desc.optional_arg {
488                            i += 1;
489                            entry.push(args[i].clone());
490                        } else if desc.optional_arg {
491                            entry.push(String::new());
492                        } else {
493                            return Err(format!("missing argument for option: -{}", desc.name));
494                        }
495                    } else {
496                        entry.push(String::new());
497                    }
498                } else {
499                    if !extract {
500                        remaining.push(arg.clone());
501                        remaining.extend(args[i + 1..].iter().cloned());
502                        return Ok((results, remaining));
503                    }
504                    break;
505                }
506                j += 1;
507            }
508        }
509        i += 1;
510    }
511
512    if !delete && !extract {
513        remaining = args[i..].to_vec();
514    }
515
516    Ok((results, remaining))
517}
518
519/// Align array values with a separator
520pub fn zformat_align(sep: &str, values: &[&str]) -> Vec<String> {
521    let mut max_pre = 0;
522
523    for value in values {
524        if let Some(pos) = value.find(':') {
525            let pre_len = value[..pos].chars().filter(|c| *c != '\\').count();
526            if pre_len > max_pre {
527                max_pre = pre_len;
528            }
529        }
530    }
531
532    let mut result = Vec::new();
533    for value in values {
534        if let Some(pos) = value.find(':') {
535            let pre = &value[..pos];
536            let post = &value[pos + 1..];
537            let pre_len = pre.chars().filter(|c| *c != '\\').count();
538            let padding = max_pre - pre_len;
539
540            let clean_pre: String = pre.chars().filter(|c| *c != '\\').collect();
541
542            result.push(format!(
543                "{}{}{}{}",
544                clean_pre,
545                " ".repeat(padding),
546                sep,
547                post
548            ));
549        } else {
550            result.push(value.to_string());
551        }
552    }
553
554    result
555}
556
557#[cfg(test)]
558mod tests {
559    use super::*;
560
561    #[test]
562    fn test_style_pattern_weight() {
563        let p1 = StylePattern::new("*", vec![], false);
564        let p2 = StylePattern::new(":completion:*", vec![], false);
565        let p3 = StylePattern::new(":completion:zsh:*", vec![], false);
566
567        assert!(p3.weight > p2.weight);
568        assert!(p2.weight > p1.weight);
569    }
570
571    #[test]
572    fn test_style_pattern_matches() {
573        let p = StylePattern::new(":completion:*", vec![], false);
574        assert!(p.matches(":completion:zsh:complete"));
575        assert!(!p.matches(":other:zsh"));
576
577        let p2 = StylePattern::new("*", vec![], false);
578        assert!(p2.matches("anything"));
579    }
580
581    #[test]
582    fn test_style_table_set_get() {
583        let mut table = StyleTable::new();
584        table.set(":completion:*", "verbose", vec!["yes".to_string()], false);
585
586        let result = table.get(":completion:zsh", "verbose");
587        assert_eq!(result, Some(&["yes".to_string()][..]));
588
589        let result = table.get(":other", "verbose");
590        assert!(result.is_none());
591    }
592
593    #[test]
594    fn test_style_table_priority() {
595        let mut table = StyleTable::new();
596        table.set("*", "menu", vec!["no".to_string()], false);
597        table.set(":completion:*", "menu", vec!["yes".to_string()], false);
598
599        let result = table.get(":completion:zsh", "menu");
600        assert_eq!(result, Some(&["yes".to_string()][..]));
601    }
602
603    #[test]
604    fn test_style_table_delete() {
605        let mut table = StyleTable::new();
606        table.set("*", "style1", vec!["val".to_string()], false);
607        table.set("*", "style2", vec!["val".to_string()], false);
608
609        table.delete(None, Some("style1"));
610        assert!(table.get("test", "style1").is_none());
611        assert!(table.get("test", "style2").is_some());
612    }
613
614    #[test]
615    fn test_style_test_bool() {
616        let mut table = StyleTable::new();
617        table.set("*", "enabled", vec!["yes".to_string()], false);
618        table.set("*", "disabled", vec!["no".to_string()], false);
619        table.set(
620            "*",
621            "multiple",
622            vec!["a".to_string(), "b".to_string()],
623            false,
624        );
625
626        assert_eq!(table.test_bool("ctx", "enabled"), Some(true));
627        assert_eq!(table.test_bool("ctx", "disabled"), Some(false));
628        assert_eq!(table.test_bool("ctx", "multiple"), None);
629    }
630
631    #[test]
632    fn test_zformat_basic() {
633        let mut specs = HashMap::new();
634        specs.insert('n', "test".to_string());
635        specs.insert('v', "42".to_string());
636
637        let result = zformat("Name: %n, Value: %v", &specs, false);
638        assert_eq!(result, "Name: test, Value: 42");
639    }
640
641    #[test]
642    fn test_zformat_padding() {
643        let mut specs = HashMap::new();
644        specs.insert('n', "hi".to_string());
645
646        let result = zformat("[%10n]", &specs, false);
647        assert_eq!(result, "[hi        ]");
648
649        let result = zformat("[%-10n]", &specs, false);
650        assert_eq!(result, "[        hi]");
651    }
652
653    #[test]
654    fn test_zformat_truncate() {
655        let mut specs = HashMap::new();
656        specs.insert('n', "hello world".to_string());
657
658        let result = zformat("[%.5n]", &specs, false);
659        assert_eq!(result, "[hello]");
660    }
661
662    #[test]
663    fn test_zformat_escape() {
664        let specs = HashMap::new();
665        let result = zformat("100%%", &specs, false);
666        assert_eq!(result, "100%");
667    }
668
669    #[test]
670    fn test_opt_desc_parse() {
671        let desc = OptDesc::parse("v").unwrap();
672        assert_eq!(desc.name, "v");
673        assert!(!desc.takes_arg);
674
675        let desc = OptDesc::parse("o:").unwrap();
676        assert_eq!(desc.name, "o");
677        assert!(desc.takes_arg);
678        assert!(!desc.optional_arg);
679
680        let desc = OptDesc::parse("o::").unwrap();
681        assert!(desc.optional_arg);
682
683        let desc = OptDesc::parse("v+").unwrap();
684        assert!(desc.multiple);
685
686        let desc = OptDesc::parse("a:=myarray").unwrap();
687        assert_eq!(desc.array_name, Some("myarray".to_string()));
688    }
689
690    #[test]
691    fn test_zparseopts_basic() {
692        let specs = vec![OptDesc::parse("v").unwrap(), OptDesc::parse("o:").unwrap()];
693
694        let args: Vec<String> = vec!["-v", "-o", "value", "rest"]
695            .into_iter()
696            .map(String::from)
697            .collect();
698
699        let (opts, remaining) = zparseopts(&args, &specs, false, false).unwrap();
700
701        assert!(opts.contains_key("-v"));
702        assert_eq!(opts.get("-o"), Some(&vec!["value".to_string()]));
703        assert_eq!(remaining, vec!["rest"]);
704    }
705
706    #[test]
707    fn test_zparseopts_combined() {
708        let specs = vec![
709            OptDesc::parse("a").unwrap(),
710            OptDesc::parse("b").unwrap(),
711            OptDesc::parse("c:").unwrap(),
712        ];
713
714        let args: Vec<String> = vec!["-abc", "val"].into_iter().map(String::from).collect();
715
716        let (opts, _) = zparseopts(&args, &specs, false, false).unwrap();
717
718        assert!(opts.contains_key("-a"));
719        assert!(opts.contains_key("-b"));
720        assert_eq!(opts.get("-c"), Some(&vec!["val".to_string()]));
721    }
722
723    #[test]
724    fn test_zparseopts_long() {
725        let specs = vec![
726            OptDesc::parse("verbose").unwrap(),
727            OptDesc::parse("output:").unwrap(),
728        ];
729
730        let args: Vec<String> = vec!["--verbose", "--output", "file.txt"]
731            .into_iter()
732            .map(String::from)
733            .collect();
734
735        let (opts, _) = zparseopts(&args, &specs, false, false).unwrap();
736
737        assert!(opts.contains_key("-verbose"));
738        assert_eq!(opts.get("-output"), Some(&vec!["file.txt".to_string()]));
739    }
740
741    #[test]
742    fn test_zformat_align() {
743        let values = vec!["short:desc1", "verylongname:desc2", "med:desc3"];
744        let result = zformat_align(" -- ", &values);
745
746        assert_eq!(result[0], "short        -- desc1");
747        assert_eq!(result[1], "verylongname -- desc2");
748        assert_eq!(result[2], "med          -- desc3");
749    }
750}