Skip to main content

zsh/
parameter.rs

1//! Parameter interface to shell internals - port of Modules/parameter.c
2//!
3//! Provides special parameters: $commands, $functions, $aliases, $builtins,
4//! $modules, $dirstack, $history, $historywords, $options, $nameddirs, $userdirs
5
6use std::collections::HashMap;
7use std::path::PathBuf;
8
9/// Parameter type flags
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ParamType {
12    Scalar,
13    Integer,
14    Float,
15    Array,
16    Associative,
17    Nameref,
18}
19
20impl ParamType {
21    pub fn name(&self) -> &'static str {
22        match self {
23            ParamType::Scalar => "scalar",
24            ParamType::Integer => "integer",
25            ParamType::Float => "float",
26            ParamType::Array => "array",
27            ParamType::Associative => "association",
28            ParamType::Nameref => "nameref",
29        }
30    }
31}
32
33/// Parameter attributes
34#[derive(Debug, Clone, Default)]
35pub struct ParamFlags {
36    pub local: bool,
37    pub left_justify: bool,
38    pub right_blanks: bool,
39    pub right_zeros: bool,
40    pub lower: bool,
41    pub upper: bool,
42    pub readonly: bool,
43    pub tagged: bool,
44    pub tied: bool,
45    pub exported: bool,
46    pub unique: bool,
47    pub hide: bool,
48    pub hideval: bool,
49    pub special: bool,
50}
51
52/// Generate parameter type string (like "scalar-local-export")
53pub fn param_type_str(ptype: ParamType, flags: &ParamFlags) -> String {
54    let mut parts = vec![ptype.name().to_string()];
55
56    if flags.local {
57        parts.push("local".to_string());
58    }
59    if flags.left_justify {
60        parts.push("left".to_string());
61    }
62    if flags.right_blanks {
63        parts.push("right_blanks".to_string());
64    }
65    if flags.right_zeros {
66        parts.push("right_zeros".to_string());
67    }
68    if flags.lower {
69        parts.push("lower".to_string());
70    }
71    if flags.upper {
72        parts.push("upper".to_string());
73    }
74    if flags.readonly {
75        parts.push("readonly".to_string());
76    }
77    if flags.tagged {
78        parts.push("tag".to_string());
79    }
80    if flags.tied {
81        parts.push("tied".to_string());
82    }
83    if flags.exported {
84        parts.push("export".to_string());
85    }
86    if flags.unique {
87        parts.push("unique".to_string());
88    }
89    if flags.hide {
90        parts.push("hide".to_string());
91    }
92    if flags.hideval {
93        parts.push("hideval".to_string());
94    }
95    if flags.special {
96        parts.push("special".to_string());
97    }
98
99    parts.join("-")
100}
101
102/// Commands hash table ($commands)
103#[derive(Debug, Default)]
104pub struct CommandsTable {
105    hashed: HashMap<String, PathBuf>,
106}
107
108impl CommandsTable {
109    pub fn new() -> Self {
110        Self::default()
111    }
112
113    pub fn get(&self, name: &str) -> Option<&PathBuf> {
114        self.hashed.get(name)
115    }
116
117    pub fn set(&mut self, name: &str, path: PathBuf) {
118        self.hashed.insert(name.to_string(), path);
119    }
120
121    pub fn unset(&mut self, name: &str) {
122        self.hashed.remove(name);
123    }
124
125    pub fn clear(&mut self) {
126        self.hashed.clear();
127    }
128
129    pub fn iter(&self) -> impl Iterator<Item = (&String, &PathBuf)> {
130        self.hashed.iter()
131    }
132
133    pub fn len(&self) -> usize {
134        self.hashed.len()
135    }
136
137    pub fn is_empty(&self) -> bool {
138        self.hashed.is_empty()
139    }
140
141    pub fn rehash(&mut self, path_dirs: &[PathBuf]) {
142        self.hashed.clear();
143        for dir in path_dirs {
144            if let Ok(entries) = std::fs::read_dir(dir) {
145                for entry in entries.flatten() {
146                    if let Ok(ft) = entry.file_type() {
147                        if ft.is_file() || ft.is_symlink() {
148                            if let Some(name) = entry.file_name().to_str() {
149                                self.hashed.insert(name.to_string(), entry.path());
150                            }
151                        }
152                    }
153                }
154            }
155        }
156    }
157}
158
159/// Functions hash table ($functions)
160#[derive(Debug, Clone)]
161pub struct FunctionDef {
162    pub body: String,
163    pub flags: u32,
164    pub autoload: bool,
165}
166
167#[derive(Debug, Default)]
168pub struct FunctionsTable {
169    functions: HashMap<String, FunctionDef>,
170    disabled: HashMap<String, FunctionDef>,
171}
172
173impl FunctionsTable {
174    pub fn new() -> Self {
175        Self::default()
176    }
177
178    pub fn get(&self, name: &str) -> Option<&FunctionDef> {
179        self.functions.get(name)
180    }
181
182    pub fn get_disabled(&self, name: &str) -> Option<&FunctionDef> {
183        self.disabled.get(name)
184    }
185
186    pub fn set(&mut self, name: &str, def: FunctionDef) {
187        self.functions.insert(name.to_string(), def);
188    }
189
190    pub fn unset(&mut self, name: &str) {
191        self.functions.remove(name);
192    }
193
194    pub fn disable(&mut self, name: &str) {
195        if let Some(def) = self.functions.remove(name) {
196            self.disabled.insert(name.to_string(), def);
197        }
198    }
199
200    pub fn enable(&mut self, name: &str) {
201        if let Some(def) = self.disabled.remove(name) {
202            self.functions.insert(name.to_string(), def);
203        }
204    }
205
206    pub fn iter(&self) -> impl Iterator<Item = (&String, &FunctionDef)> {
207        self.functions.iter()
208    }
209
210    pub fn iter_disabled(&self) -> impl Iterator<Item = (&String, &FunctionDef)> {
211        self.disabled.iter()
212    }
213}
214
215/// Aliases hash table ($aliases)
216#[derive(Debug, Clone)]
217pub struct AliasDef {
218    pub value: String,
219    pub global: bool,
220    pub suffix: bool,
221}
222
223#[derive(Debug, Default)]
224pub struct AliasesTable {
225    aliases: HashMap<String, AliasDef>,
226    disabled: HashMap<String, AliasDef>,
227    global_aliases: HashMap<String, AliasDef>,
228    suffix_aliases: HashMap<String, AliasDef>,
229}
230
231impl AliasesTable {
232    pub fn new() -> Self {
233        Self::default()
234    }
235
236    pub fn get(&self, name: &str) -> Option<&AliasDef> {
237        self.aliases.get(name)
238    }
239
240    pub fn get_global(&self, name: &str) -> Option<&AliasDef> {
241        self.global_aliases.get(name)
242    }
243
244    pub fn get_suffix(&self, suffix: &str) -> Option<&AliasDef> {
245        self.suffix_aliases.get(suffix)
246    }
247
248    pub fn set(&mut self, name: &str, def: AliasDef) {
249        if def.global {
250            self.global_aliases.insert(name.to_string(), def);
251        } else if def.suffix {
252            self.suffix_aliases.insert(name.to_string(), def);
253        } else {
254            self.aliases.insert(name.to_string(), def);
255        }
256    }
257
258    pub fn unset(&mut self, name: &str) {
259        self.aliases.remove(name);
260        self.global_aliases.remove(name);
261        self.suffix_aliases.remove(name);
262    }
263
264    pub fn disable(&mut self, name: &str) {
265        if let Some(def) = self.aliases.remove(name) {
266            self.disabled.insert(name.to_string(), def);
267        }
268    }
269
270    pub fn enable(&mut self, name: &str) {
271        if let Some(def) = self.disabled.remove(name) {
272            self.aliases.insert(name.to_string(), def);
273        }
274    }
275
276    pub fn iter(&self) -> impl Iterator<Item = (&String, &AliasDef)> {
277        self.aliases.iter()
278    }
279
280    pub fn iter_global(&self) -> impl Iterator<Item = (&String, &AliasDef)> {
281        self.global_aliases.iter()
282    }
283
284    pub fn iter_suffix(&self) -> impl Iterator<Item = (&String, &AliasDef)> {
285        self.suffix_aliases.iter()
286    }
287}
288
289/// Builtins list ($builtins)
290#[derive(Debug, Default)]
291pub struct BuiltinsTable {
292    builtins: HashMap<String, bool>,
293    disabled: HashMap<String, bool>,
294}
295
296impl BuiltinsTable {
297    pub fn new() -> Self {
298        Self::default()
299    }
300
301    pub fn register(&mut self, name: &str) {
302        self.builtins.insert(name.to_string(), true);
303    }
304
305    pub fn is_builtin(&self, name: &str) -> bool {
306        self.builtins.contains_key(name)
307    }
308
309    pub fn disable(&mut self, name: &str) {
310        if self.builtins.remove(name).is_some() {
311            self.disabled.insert(name.to_string(), true);
312        }
313    }
314
315    pub fn enable(&mut self, name: &str) {
316        if self.disabled.remove(name).is_some() {
317            self.builtins.insert(name.to_string(), true);
318        }
319    }
320
321    pub fn list(&self) -> Vec<&str> {
322        self.builtins.keys().map(|s| s.as_str()).collect()
323    }
324
325    pub fn list_disabled(&self) -> Vec<&str> {
326        self.disabled.keys().map(|s| s.as_str()).collect()
327    }
328}
329
330/// Directory stack ($dirstack)
331#[derive(Debug, Default)]
332pub struct DirStack {
333    stack: Vec<PathBuf>,
334}
335
336impl DirStack {
337    pub fn new() -> Self {
338        Self::default()
339    }
340
341    pub fn push(&mut self, dir: PathBuf) {
342        self.stack.push(dir);
343    }
344
345    pub fn pop(&mut self) -> Option<PathBuf> {
346        self.stack.pop()
347    }
348
349    pub fn get(&self, index: usize) -> Option<&PathBuf> {
350        self.stack.get(index)
351    }
352
353    pub fn set(&mut self, stack: Vec<PathBuf>) {
354        self.stack = stack;
355    }
356
357    pub fn len(&self) -> usize {
358        self.stack.len()
359    }
360
361    pub fn is_empty(&self) -> bool {
362        self.stack.is_empty()
363    }
364
365    pub fn iter(&self) -> impl Iterator<Item = &PathBuf> {
366        self.stack.iter()
367    }
368
369    pub fn to_array(&self) -> Vec<String> {
370        self.stack
371            .iter()
372            .map(|p| p.to_string_lossy().to_string())
373            .collect()
374    }
375}
376
377/// Options special parameter ($options)
378#[derive(Debug, Default)]
379pub struct OptionsTable {
380    options: HashMap<String, bool>,
381}
382
383impl OptionsTable {
384    pub fn new() -> Self {
385        Self::default()
386    }
387
388    pub fn set(&mut self, name: &str, value: bool) {
389        self.options.insert(name.to_lowercase(), value);
390    }
391
392    pub fn get(&self, name: &str) -> Option<bool> {
393        self.options.get(&name.to_lowercase()).copied()
394    }
395
396    pub fn is_set(&self, name: &str) -> bool {
397        self.options
398            .get(&name.to_lowercase())
399            .copied()
400            .unwrap_or(false)
401    }
402
403    pub fn iter(&self) -> impl Iterator<Item = (&String, &bool)> {
404        self.options.iter()
405    }
406
407    pub fn to_hash(&self) -> HashMap<String, String> {
408        self.options
409            .iter()
410            .map(|(k, v)| {
411                (
412                    k.clone(),
413                    if *v {
414                        "on".to_string()
415                    } else {
416                        "off".to_string()
417                    },
418                )
419            })
420            .collect()
421    }
422}
423
424/// Named directories ($nameddirs, $userdirs)
425#[derive(Debug, Default)]
426pub struct NamedDirsTable {
427    dirs: HashMap<String, PathBuf>,
428}
429
430impl NamedDirsTable {
431    pub fn new() -> Self {
432        Self::default()
433    }
434
435    pub fn set(&mut self, name: &str, path: PathBuf) {
436        self.dirs.insert(name.to_string(), path);
437    }
438
439    pub fn get(&self, name: &str) -> Option<&PathBuf> {
440        self.dirs.get(name)
441    }
442
443    pub fn unset(&mut self, name: &str) {
444        self.dirs.remove(name);
445    }
446
447    pub fn find_name(&self, path: &PathBuf) -> Option<&str> {
448        self.dirs
449            .iter()
450            .find(|(_, p)| *p == path)
451            .map(|(n, _)| n.as_str())
452    }
453
454    pub fn iter(&self) -> impl Iterator<Item = (&String, &PathBuf)> {
455        self.dirs.iter()
456    }
457}
458
459/// Job states ($jobstates)
460#[derive(Debug, Clone)]
461pub struct JobState {
462    pub running: bool,
463    pub suspended: bool,
464    pub done: bool,
465}
466
467impl JobState {
468    pub fn as_str(&self) -> &'static str {
469        if self.done {
470            "done"
471        } else if self.suspended {
472            "suspended"
473        } else if self.running {
474            "running"
475        } else {
476            "unknown"
477        }
478    }
479}
480
481/// Job texts ($jobtexts)
482#[derive(Debug, Default)]
483pub struct JobsTable {
484    jobs: HashMap<i32, (JobState, String)>,
485}
486
487impl JobsTable {
488    pub fn new() -> Self {
489        Self::default()
490    }
491
492    pub fn add(&mut self, id: i32, state: JobState, text: String) {
493        self.jobs.insert(id, (state, text));
494    }
495
496    pub fn remove(&mut self, id: i32) {
497        self.jobs.remove(&id);
498    }
499
500    pub fn get_state(&self, id: i32) -> Option<&JobState> {
501        self.jobs.get(&id).map(|(s, _)| s)
502    }
503
504    pub fn get_text(&self, id: i32) -> Option<&str> {
505        self.jobs.get(&id).map(|(_, t)| t.as_str())
506    }
507
508    pub fn states(&self) -> HashMap<String, String> {
509        self.jobs
510            .iter()
511            .map(|(id, (state, _))| (id.to_string(), state.as_str().to_string()))
512            .collect()
513    }
514
515    pub fn texts(&self) -> HashMap<String, String> {
516        self.jobs
517            .iter()
518            .map(|(id, (_, text))| (id.to_string(), text.clone()))
519            .collect()
520    }
521}
522
523/// Modules table ($modules)
524#[derive(Debug, Clone)]
525pub struct ModuleInfo {
526    pub loaded: bool,
527    pub autoload: bool,
528}
529
530#[derive(Debug, Default)]
531pub struct ModulesTable {
532    modules: HashMap<String, ModuleInfo>,
533}
534
535impl ModulesTable {
536    pub fn new() -> Self {
537        Self::default()
538    }
539
540    pub fn register(&mut self, name: &str, info: ModuleInfo) {
541        self.modules.insert(name.to_string(), info);
542    }
543
544    pub fn get(&self, name: &str) -> Option<&ModuleInfo> {
545        self.modules.get(name)
546    }
547
548    pub fn is_loaded(&self, name: &str) -> bool {
549        self.modules.get(name).map(|m| m.loaded).unwrap_or(false)
550    }
551
552    pub fn iter(&self) -> impl Iterator<Item = (&String, &ModuleInfo)> {
553        self.modules.iter()
554    }
555
556    pub fn to_hash(&self) -> HashMap<String, String> {
557        self.modules
558            .iter()
559            .map(|(k, v)| {
560                let status = if v.loaded {
561                    "loaded"
562                } else if v.autoload {
563                    "autoload"
564                } else {
565                    "unloaded"
566                };
567                (k.clone(), status.to_string())
568            })
569            .collect()
570    }
571}
572
573#[cfg(test)]
574mod tests {
575    use super::*;
576
577    #[test]
578    fn test_param_type_str() {
579        let flags = ParamFlags::default();
580        assert_eq!(param_type_str(ParamType::Scalar, &flags), "scalar");
581
582        let flags = ParamFlags {
583            local: true,
584            exported: true,
585            ..Default::default()
586        };
587        assert_eq!(
588            param_type_str(ParamType::Array, &flags),
589            "array-local-export"
590        );
591    }
592
593    #[test]
594    fn test_commands_table() {
595        let mut table = CommandsTable::new();
596        table.set("ls", PathBuf::from("/bin/ls"));
597
598        assert_eq!(table.get("ls"), Some(&PathBuf::from("/bin/ls")));
599        assert!(table.get("nonexistent").is_none());
600
601        table.unset("ls");
602        assert!(table.get("ls").is_none());
603    }
604
605    #[test]
606    fn test_functions_table() {
607        let mut table = FunctionsTable::new();
608        table.set(
609            "myfunc",
610            FunctionDef {
611                body: "echo hello".to_string(),
612                flags: 0,
613                autoload: false,
614            },
615        );
616
617        assert!(table.get("myfunc").is_some());
618
619        table.disable("myfunc");
620        assert!(table.get("myfunc").is_none());
621        assert!(table.get_disabled("myfunc").is_some());
622
623        table.enable("myfunc");
624        assert!(table.get("myfunc").is_some());
625    }
626
627    #[test]
628    fn test_aliases_table() {
629        let mut table = AliasesTable::new();
630        table.set(
631            "ll",
632            AliasDef {
633                value: "ls -l".to_string(),
634                global: false,
635                suffix: false,
636            },
637        );
638
639        assert!(table.get("ll").is_some());
640        assert_eq!(table.get("ll").unwrap().value, "ls -l");
641    }
642
643    #[test]
644    fn test_builtins_table() {
645        let mut table = BuiltinsTable::new();
646        table.register("echo");
647        table.register("cd");
648
649        assert!(table.is_builtin("echo"));
650        assert!(!table.is_builtin("nonexistent"));
651
652        table.disable("echo");
653        assert!(!table.is_builtin("echo"));
654    }
655
656    #[test]
657    fn test_dir_stack() {
658        let mut stack = DirStack::new();
659        stack.push(PathBuf::from("/home"));
660        stack.push(PathBuf::from("/tmp"));
661
662        assert_eq!(stack.len(), 2);
663        assert_eq!(stack.pop(), Some(PathBuf::from("/tmp")));
664        assert_eq!(stack.len(), 1);
665    }
666
667    #[test]
668    fn test_options_table() {
669        let mut table = OptionsTable::new();
670        table.set("autocd", true);
671        table.set("EXTENDEDGLOB", true);
672
673        assert!(table.is_set("autocd"));
674        assert!(table.is_set("extendedglob")); // case insensitive
675    }
676
677    #[test]
678    fn test_named_dirs() {
679        let mut table = NamedDirsTable::new();
680        table.set("proj", PathBuf::from("/home/user/projects"));
681
682        assert_eq!(
683            table.get("proj"),
684            Some(&PathBuf::from("/home/user/projects"))
685        );
686        assert_eq!(
687            table.find_name(&PathBuf::from("/home/user/projects")),
688            Some("proj")
689        );
690    }
691
692    #[test]
693    fn test_jobs_table() {
694        let mut table = JobsTable::new();
695        table.add(
696            1,
697            JobState {
698                running: true,
699                suspended: false,
700                done: false,
701            },
702            "vim file.txt".to_string(),
703        );
704
705        assert_eq!(table.get_state(1).unwrap().as_str(), "running");
706        assert_eq!(table.get_text(1), Some("vim file.txt"));
707    }
708
709    #[test]
710    fn test_modules_table() {
711        let mut table = ModulesTable::new();
712        table.register(
713            "zsh/datetime",
714            ModuleInfo {
715                loaded: true,
716                autoload: false,
717            },
718        );
719
720        assert!(table.is_loaded("zsh/datetime"));
721        assert!(!table.is_loaded("nonexistent"));
722    }
723}