Skip to main content

zsh/
hashtable.rs

1//! Hash table implementations - port of hashtable.c
2//!
3//! Provides hash tables for commands, shell functions, reserved words, aliases,
4//! and history. Uses Rust's HashMap internally but maintains zsh-compatible APIs.
5
6use std::collections::HashMap;
7use std::fs;
8use std::path::{Path, PathBuf};
9
10/// Flags for hash nodes
11pub mod flags {
12    pub const DISABLED: u32 = 1 << 0;
13    pub const HASHED: u32 = 1 << 1;
14    pub const ALIAS_GLOBAL: u32 = 1 << 2;
15    pub const ALIAS_SUFFIX: u32 = 1 << 3;
16    pub const PM_UNDEFINED: u32 = 1 << 4;
17    pub const PM_TAGGED: u32 = 1 << 5;
18    pub const PM_TAGGED_LOCAL: u32 = 1 << 6;
19    pub const PM_LOADDIR: u32 = 1 << 7;
20    pub const PM_UNALIASED: u32 = 1 << 8;
21    pub const PM_KSHSTORED: u32 = 1 << 9;
22    pub const PM_ZSHSTORED: u32 = 1 << 10;
23    pub const PM_CUR_FPATH: u32 = 1 << 11;
24}
25
26/// Generic hash function (zsh's hasher)
27pub fn hasher(s: &str) -> u32 {
28    let mut hashval: u32 = 0;
29    for c in s.bytes() {
30        hashval = hashval.wrapping_add(hashval.wrapping_shl(5).wrapping_add(c as u32));
31    }
32    hashval
33}
34
35/// History-specific hash function (normalizes whitespace)
36pub fn hist_hasher(s: &str) -> u32 {
37    let mut hashval: u32 = 0;
38    let mut chars = s.chars().peekable();
39
40    while let Some(&c) = chars.peek() {
41        if c.is_whitespace() {
42            chars.next();
43        } else {
44            break;
45        }
46    }
47
48    while let Some(c) = chars.next() {
49        if c.is_whitespace() {
50            while let Some(&next) = chars.peek() {
51                if next.is_whitespace() {
52                    chars.next();
53                } else {
54                    break;
55                }
56            }
57            if chars.peek().is_some() {
58                hashval = hashval.wrapping_add(hashval.wrapping_shl(5).wrapping_add(' ' as u32));
59            }
60        } else {
61            hashval = hashval.wrapping_add(hashval.wrapping_shl(5).wrapping_add(c as u32));
62        }
63    }
64    hashval
65}
66
67/// Compare strings with normalized whitespace (for history)
68/// Multiple whitespace sequences are treated as equivalent to single spaces.
69/// Trailing whitespace is ignored when comparing.
70pub fn hist_strcmp(s1: &str, s2: &str, reduce_blanks: bool) -> std::cmp::Ordering {
71    let s1 = s1.trim_start();
72    let s2 = s2.trim_start();
73
74    if reduce_blanks {
75        return s1.cmp(s2);
76    }
77
78    let mut c1 = s1.chars().peekable();
79    let mut c2 = s2.chars().peekable();
80
81    loop {
82        let ch1 = c1.peek().copied();
83        let ch2 = c2.peek().copied();
84
85        match (ch1, ch2) {
86            (None, None) => return std::cmp::Ordering::Equal,
87            (None, Some(c)) => {
88                if c.is_whitespace() {
89                    while c2.peek().map(|c| c.is_whitespace()).unwrap_or(false) {
90                        c2.next();
91                    }
92                    if c2.peek().is_none() {
93                        return std::cmp::Ordering::Equal;
94                    }
95                }
96                return std::cmp::Ordering::Less;
97            }
98            (Some(c), None) => {
99                if c.is_whitespace() {
100                    while c1.peek().map(|c| c.is_whitespace()).unwrap_or(false) {
101                        c1.next();
102                    }
103                    if c1.peek().is_none() {
104                        return std::cmp::Ordering::Equal;
105                    }
106                }
107                return std::cmp::Ordering::Greater;
108            }
109            (Some(ch1), Some(ch2)) => {
110                let ws1 = ch1.is_whitespace();
111                let ws2 = ch2.is_whitespace();
112
113                if ws1 && ws2 {
114                    while c1.peek().map(|c| c.is_whitespace()).unwrap_or(false) {
115                        c1.next();
116                    }
117                    while c2.peek().map(|c| c.is_whitespace()).unwrap_or(false) {
118                        c2.next();
119                    }
120                } else if ws1 {
121                    while c1.peek().map(|c| c.is_whitespace()).unwrap_or(false) {
122                        c1.next();
123                    }
124                    if c1.peek().is_none() {
125                        return std::cmp::Ordering::Less;
126                    }
127                    return std::cmp::Ordering::Less;
128                } else if ws2 {
129                    while c2.peek().map(|c| c.is_whitespace()).unwrap_or(false) {
130                        c2.next();
131                    }
132                    if c2.peek().is_none() {
133                        return std::cmp::Ordering::Greater;
134                    }
135                    return std::cmp::Ordering::Greater;
136                } else if ch1 != ch2 {
137                    return ch1.cmp(&ch2);
138                } else {
139                    c1.next();
140                    c2.next();
141                }
142            }
143        }
144    }
145}
146
147/// Command name entry
148#[derive(Debug, Clone)]
149pub struct CmdName {
150    pub name: String,
151    pub flags: u32,
152    pub path: Option<PathBuf>,
153    pub dir_index: Option<usize>,
154}
155
156impl CmdName {
157    pub fn new(name: &str) -> Self {
158        Self {
159            name: name.to_string(),
160            flags: 0,
161            path: None,
162            dir_index: None,
163        }
164    }
165
166    pub fn with_path(name: &str, path: PathBuf) -> Self {
167        Self {
168            name: name.to_string(),
169            flags: flags::HASHED,
170            path: Some(path),
171            dir_index: None,
172        }
173    }
174
175    pub fn with_dir_index(name: &str, dir_index: usize) -> Self {
176        Self {
177            name: name.to_string(),
178            flags: 0,
179            path: None,
180            dir_index: Some(dir_index),
181        }
182    }
183
184    pub fn is_disabled(&self) -> bool {
185        self.flags & flags::DISABLED != 0
186    }
187
188    pub fn is_hashed(&self) -> bool {
189        self.flags & flags::HASHED != 0
190    }
191}
192
193/// Command name hash table
194#[derive(Debug)]
195pub struct CmdNameTable {
196    table: HashMap<String, CmdName>,
197    path_checked_index: usize,
198    path: Vec<String>,
199    hash_executables_only: bool,
200}
201
202impl CmdNameTable {
203    pub fn new() -> Self {
204        Self {
205            table: HashMap::new(),
206            path_checked_index: 0,
207            path: Vec::new(),
208            hash_executables_only: false,
209        }
210    }
211
212    pub fn set_path(&mut self, path: Vec<String>) {
213        self.path = path;
214        self.path_checked_index = 0;
215    }
216
217    pub fn set_hash_executables_only(&mut self, value: bool) {
218        self.hash_executables_only = value;
219    }
220
221    pub fn add(&mut self, cmd: CmdName) {
222        self.table.insert(cmd.name.clone(), cmd);
223    }
224
225    pub fn get(&self, name: &str) -> Option<&CmdName> {
226        self.table.get(name).filter(|c| !c.is_disabled())
227    }
228
229    pub fn get_including_disabled(&self, name: &str) -> Option<&CmdName> {
230        self.table.get(name)
231    }
232
233    pub fn remove(&mut self, name: &str) -> Option<CmdName> {
234        self.table.remove(name)
235    }
236
237    pub fn clear(&mut self) {
238        self.table.clear();
239        self.path_checked_index = 0;
240    }
241
242    pub fn len(&self) -> usize {
243        self.table.len()
244    }
245
246    pub fn is_empty(&self) -> bool {
247        self.table.is_empty()
248    }
249
250    /// Hash all commands in a directory
251    pub fn hash_dir(&mut self, dir: &str, dir_index: usize) {
252        if dir.starts_with('.') || dir.is_empty() {
253            return;
254        }
255
256        let Ok(entries) = fs::read_dir(dir) else {
257            return;
258        };
259
260        for entry in entries.flatten() {
261            let Ok(name) = entry.file_name().into_string() else {
262                continue;
263            };
264
265            if self.table.contains_key(&name) {
266                continue;
267            }
268
269            let path = entry.path();
270            let should_add = if self.hash_executables_only {
271                is_executable(&path)
272            } else {
273                true
274            };
275
276            if should_add {
277                self.table
278                    .insert(name.clone(), CmdName::with_dir_index(&name, dir_index));
279            }
280        }
281    }
282
283    /// Fill table from PATH
284    pub fn fill(&mut self) {
285        for i in self.path_checked_index..self.path.len() {
286            let dir = self.path[i].clone();
287            self.hash_dir(&dir, i);
288        }
289        self.path_checked_index = self.path.len();
290    }
291
292    /// Iterate over all entries
293    pub fn iter(&self) -> impl Iterator<Item = (&String, &CmdName)> {
294        self.table.iter()
295    }
296
297    /// Get full path for a command
298    pub fn get_full_path(&self, name: &str) -> Option<PathBuf> {
299        let cmd = self.table.get(name)?;
300        if cmd.is_disabled() {
301            return None;
302        }
303
304        if let Some(ref path) = cmd.path {
305            return Some(path.clone());
306        }
307
308        if let Some(idx) = cmd.dir_index {
309            if idx < self.path.len() {
310                let mut path = PathBuf::from(&self.path[idx]);
311                path.push(name);
312                return Some(path);
313            }
314        }
315
316        None
317    }
318}
319
320impl Default for CmdNameTable {
321    fn default() -> Self {
322        Self::new()
323    }
324}
325
326/// Check if a path is executable
327#[cfg(unix)]
328fn is_executable(path: &Path) -> bool {
329    use std::os::unix::fs::PermissionsExt;
330
331    if let Ok(meta) = path.metadata() {
332        if !meta.is_file() {
333            return false;
334        }
335        let mode = meta.permissions().mode();
336        mode & 0o111 != 0
337    } else {
338        false
339    }
340}
341
342#[cfg(not(unix))]
343fn is_executable(path: &Path) -> bool {
344    path.is_file()
345}
346
347/// Shell function entry
348#[derive(Debug, Clone)]
349pub struct ShFunc {
350    pub name: String,
351    pub flags: u32,
352    pub filename: Option<String>,
353    pub body: Option<String>,
354}
355
356impl ShFunc {
357    pub fn new(name: &str) -> Self {
358        Self {
359            name: name.to_string(),
360            flags: 0,
361            filename: None,
362            body: None,
363        }
364    }
365
366    pub fn autoload(name: &str) -> Self {
367        Self {
368            name: name.to_string(),
369            flags: flags::PM_UNDEFINED,
370            filename: None,
371            body: None,
372        }
373    }
374
375    pub fn with_body(name: &str, body: &str) -> Self {
376        Self {
377            name: name.to_string(),
378            flags: 0,
379            filename: None,
380            body: Some(body.to_string()),
381        }
382    }
383
384    pub fn is_disabled(&self) -> bool {
385        self.flags & flags::DISABLED != 0
386    }
387
388    pub fn is_autoload(&self) -> bool {
389        self.flags & flags::PM_UNDEFINED != 0
390    }
391
392    pub fn is_traced(&self) -> bool {
393        self.flags & (flags::PM_TAGGED | flags::PM_TAGGED_LOCAL) != 0
394    }
395}
396
397/// Shell function hash table
398#[derive(Debug)]
399pub struct ShFuncTable {
400    table: HashMap<String, ShFunc>,
401}
402
403impl ShFuncTable {
404    pub fn new() -> Self {
405        Self {
406            table: HashMap::new(),
407        }
408    }
409
410    pub fn add(&mut self, func: ShFunc) -> Option<ShFunc> {
411        self.table.insert(func.name.clone(), func)
412    }
413
414    pub fn get(&self, name: &str) -> Option<&ShFunc> {
415        self.table.get(name).filter(|f| !f.is_disabled())
416    }
417
418    pub fn get_including_disabled(&self, name: &str) -> Option<&ShFunc> {
419        self.table.get(name)
420    }
421
422    pub fn get_mut(&mut self, name: &str) -> Option<&mut ShFunc> {
423        self.table.get_mut(name).filter(|f| !f.is_disabled())
424    }
425
426    pub fn remove(&mut self, name: &str) -> Option<ShFunc> {
427        self.table.remove(name)
428    }
429
430    pub fn disable(&mut self, name: &str) -> bool {
431        if let Some(func) = self.table.get_mut(name) {
432            func.flags |= flags::DISABLED;
433            true
434        } else {
435            false
436        }
437    }
438
439    pub fn enable(&mut self, name: &str) -> bool {
440        if let Some(func) = self.table.get_mut(name) {
441            func.flags &= !flags::DISABLED;
442            true
443        } else {
444            false
445        }
446    }
447
448    pub fn len(&self) -> usize {
449        self.table.len()
450    }
451
452    pub fn is_empty(&self) -> bool {
453        self.table.is_empty()
454    }
455
456    pub fn iter(&self) -> impl Iterator<Item = (&String, &ShFunc)> {
457        self.table.iter()
458    }
459
460    pub fn iter_sorted(&self) -> Vec<(&String, &ShFunc)> {
461        let mut entries: Vec<_> = self.table.iter().collect();
462        entries.sort_by(|a, b| a.0.cmp(b.0));
463        entries
464    }
465
466    pub fn clear(&mut self) {
467        self.table.clear();
468    }
469}
470
471impl Default for ShFuncTable {
472    fn default() -> Self {
473        Self::new()
474    }
475}
476
477/// Reserved word token types
478#[derive(Debug, Clone, Copy, PartialEq, Eq)]
479#[repr(u8)]
480pub enum ReswdToken {
481    Bang,
482    DinBrack,
483    InBrace,
484    OutBrace,
485    Case,
486    Coproc,
487    Typeset,
488    DoLoop,
489    Done,
490    Elif,
491    Else,
492    Zend,
493    Esac,
494    Fi,
495    For,
496    Foreach,
497    Func,
498    If,
499    Nocorrect,
500    Repeat,
501    Select,
502    Then,
503    Time,
504    Until,
505    While,
506}
507
508/// Reserved word entry
509#[derive(Debug, Clone)]
510pub struct Reswd {
511    pub name: String,
512    pub flags: u32,
513    pub token: ReswdToken,
514}
515
516impl Reswd {
517    pub fn new(name: &str, token: ReswdToken) -> Self {
518        Self {
519            name: name.to_string(),
520            flags: 0,
521            token,
522        }
523    }
524
525    pub fn is_disabled(&self) -> bool {
526        self.flags & flags::DISABLED != 0
527    }
528}
529
530/// Reserved word hash table
531#[derive(Debug)]
532pub struct ReswdTable {
533    table: HashMap<String, Reswd>,
534}
535
536impl ReswdTable {
537    pub fn new() -> Self {
538        let mut table = HashMap::new();
539
540        let words = [
541            ("!", ReswdToken::Bang),
542            ("[[", ReswdToken::DinBrack),
543            ("{", ReswdToken::InBrace),
544            ("}", ReswdToken::OutBrace),
545            ("case", ReswdToken::Case),
546            ("coproc", ReswdToken::Coproc),
547            ("declare", ReswdToken::Typeset),
548            ("do", ReswdToken::DoLoop),
549            ("done", ReswdToken::Done),
550            ("elif", ReswdToken::Elif),
551            ("else", ReswdToken::Else),
552            ("end", ReswdToken::Zend),
553            ("esac", ReswdToken::Esac),
554            ("export", ReswdToken::Typeset),
555            ("fi", ReswdToken::Fi),
556            ("float", ReswdToken::Typeset),
557            ("for", ReswdToken::For),
558            ("foreach", ReswdToken::Foreach),
559            ("function", ReswdToken::Func),
560            ("if", ReswdToken::If),
561            ("integer", ReswdToken::Typeset),
562            ("local", ReswdToken::Typeset),
563            ("nocorrect", ReswdToken::Nocorrect),
564            ("readonly", ReswdToken::Typeset),
565            ("repeat", ReswdToken::Repeat),
566            ("select", ReswdToken::Select),
567            ("then", ReswdToken::Then),
568            ("time", ReswdToken::Time),
569            ("typeset", ReswdToken::Typeset),
570            ("until", ReswdToken::Until),
571            ("while", ReswdToken::While),
572        ];
573
574        for (name, token) in words {
575            table.insert(name.to_string(), Reswd::new(name, token));
576        }
577
578        Self { table }
579    }
580
581    pub fn get(&self, name: &str) -> Option<&Reswd> {
582        self.table.get(name).filter(|r| !r.is_disabled())
583    }
584
585    pub fn get_including_disabled(&self, name: &str) -> Option<&Reswd> {
586        self.table.get(name)
587    }
588
589    pub fn disable(&mut self, name: &str) -> bool {
590        if let Some(rw) = self.table.get_mut(name) {
591            rw.flags |= flags::DISABLED;
592            true
593        } else {
594            false
595        }
596    }
597
598    pub fn enable(&mut self, name: &str) -> bool {
599        if let Some(rw) = self.table.get_mut(name) {
600            rw.flags &= !flags::DISABLED;
601            true
602        } else {
603            false
604        }
605    }
606
607    pub fn is_reserved(&self, name: &str) -> bool {
608        self.get(name).is_some()
609    }
610
611    pub fn iter(&self) -> impl Iterator<Item = (&String, &Reswd)> {
612        self.table.iter()
613    }
614}
615
616impl Default for ReswdTable {
617    fn default() -> Self {
618        Self::new()
619    }
620}
621
622/// Alias entry
623#[derive(Debug, Clone)]
624pub struct Alias {
625    pub name: String,
626    pub flags: u32,
627    pub text: String,
628    pub inuse: i32,
629}
630
631impl Alias {
632    pub fn new(name: &str, text: &str) -> Self {
633        Self {
634            name: name.to_string(),
635            flags: 0,
636            text: text.to_string(),
637            inuse: 0,
638        }
639    }
640
641    pub fn global(name: &str, text: &str) -> Self {
642        Self {
643            name: name.to_string(),
644            flags: flags::ALIAS_GLOBAL,
645            text: text.to_string(),
646            inuse: 0,
647        }
648    }
649
650    pub fn suffix(name: &str, text: &str) -> Self {
651        Self {
652            name: name.to_string(),
653            flags: flags::ALIAS_SUFFIX,
654            text: text.to_string(),
655            inuse: 0,
656        }
657    }
658
659    pub fn is_disabled(&self) -> bool {
660        self.flags & flags::DISABLED != 0
661    }
662
663    pub fn is_global(&self) -> bool {
664        self.flags & flags::ALIAS_GLOBAL != 0
665    }
666
667    pub fn is_suffix(&self) -> bool {
668        self.flags & flags::ALIAS_SUFFIX != 0
669    }
670}
671
672/// Alias hash table
673#[derive(Debug)]
674pub struct AliasTable {
675    table: HashMap<String, Alias>,
676}
677
678impl AliasTable {
679    pub fn new() -> Self {
680        Self {
681            table: HashMap::new(),
682        }
683    }
684
685    pub fn with_defaults() -> Self {
686        let mut table = Self::new();
687        table.add(Alias::new("run-help", "man"));
688        table.add(Alias::new("which-command", "whence"));
689        table
690    }
691
692    pub fn add(&mut self, alias: Alias) -> Option<Alias> {
693        self.table.insert(alias.name.clone(), alias)
694    }
695
696    pub fn get(&self, name: &str) -> Option<&Alias> {
697        self.table.get(name).filter(|a| !a.is_disabled())
698    }
699
700    pub fn get_including_disabled(&self, name: &str) -> Option<&Alias> {
701        self.table.get(name)
702    }
703
704    pub fn get_mut(&mut self, name: &str) -> Option<&mut Alias> {
705        self.table.get_mut(name).filter(|a| !a.is_disabled())
706    }
707
708    pub fn remove(&mut self, name: &str) -> Option<Alias> {
709        self.table.remove(name)
710    }
711
712    pub fn disable(&mut self, name: &str) -> bool {
713        if let Some(alias) = self.table.get_mut(name) {
714            alias.flags |= flags::DISABLED;
715            true
716        } else {
717            false
718        }
719    }
720
721    pub fn enable(&mut self, name: &str) -> bool {
722        if let Some(alias) = self.table.get_mut(name) {
723            alias.flags &= !flags::DISABLED;
724            true
725        } else {
726            false
727        }
728    }
729
730    pub fn len(&self) -> usize {
731        self.table.len()
732    }
733
734    pub fn is_empty(&self) -> bool {
735        self.table.is_empty()
736    }
737
738    pub fn clear(&mut self) {
739        self.table.clear();
740    }
741
742    pub fn iter(&self) -> impl Iterator<Item = (&String, &Alias)> {
743        self.table.iter()
744    }
745
746    pub fn iter_sorted(&self) -> Vec<(&String, &Alias)> {
747        let mut entries: Vec<_> = self.table.iter().collect();
748        entries.sort_by(|a, b| a.0.cmp(b.0));
749        entries
750    }
751}
752
753impl Default for AliasTable {
754    fn default() -> Self {
755        Self::new()
756    }
757}
758
759/// Suffix alias table (separate from regular aliases)
760pub type SuffixAliasTable = AliasTable;
761
762/// Directory cache entry for function filenames
763#[derive(Debug, Clone)]
764struct DirCacheEntry {
765    name: String,
766    refs: usize,
767}
768
769/// Directory cache for efficient storage of function directories
770#[derive(Debug)]
771pub struct DirCache {
772    entries: Vec<DirCacheEntry>,
773    last_entry: Option<usize>,
774}
775
776impl DirCache {
777    pub fn new() -> Self {
778        Self {
779            entries: Vec::new(),
780            last_entry: None,
781        }
782    }
783
784    /// Get or create a cached directory string
785    pub fn get_or_insert(&mut self, value: &str) -> String {
786        if let Some(idx) = self.last_entry {
787            if self.entries[idx].name == value {
788                self.entries[idx].refs += 1;
789                return self.entries[idx].name.clone();
790            }
791        }
792
793        for (i, entry) in self.entries.iter_mut().enumerate() {
794            if entry.name == value {
795                entry.refs += 1;
796                self.last_entry = Some(i);
797                return entry.name.clone();
798            }
799        }
800
801        let idx = self.entries.len();
802        self.entries.push(DirCacheEntry {
803            name: value.to_string(),
804            refs: 1,
805        });
806        self.last_entry = Some(idx);
807        self.entries[idx].name.clone()
808    }
809
810    /// Release a reference to a cached directory
811    pub fn release(&mut self, value: &str) {
812        for i in 0..self.entries.len() {
813            if self.entries[i].name == value {
814                self.entries[i].refs -= 1;
815                if self.entries[i].refs == 0 {
816                    self.entries.remove(i);
817                    if self.last_entry == Some(i) {
818                        self.last_entry = None;
819                    } else if let Some(ref mut last) = self.last_entry {
820                        if *last > i {
821                            *last -= 1;
822                        }
823                    }
824                }
825                return;
826            }
827        }
828    }
829
830    pub fn len(&self) -> usize {
831        self.entries.len()
832    }
833
834    pub fn is_empty(&self) -> bool {
835        self.entries.is_empty()
836    }
837}
838
839impl Default for DirCache {
840    fn default() -> Self {
841        Self::new()
842    }
843}
844
845/// Print flags for whence/type commands
846pub mod print_flags {
847    pub const NAMEONLY: u32 = 1 << 0;
848    pub const WHENCE_WORD: u32 = 1 << 1;
849    pub const WHENCE_SIMPLE: u32 = 1 << 2;
850    pub const WHENCE_CSH: u32 = 1 << 3;
851    pub const WHENCE_VERBOSE: u32 = 1 << 4;
852    pub const WHENCE_FUNCDEF: u32 = 1 << 5;
853    pub const LIST: u32 = 1 << 6;
854}
855
856/// Format a command name entry for output
857pub fn format_cmdnam(cmd: &CmdName, path: &[String], print_flags: u32) -> String {
858    let name = &cmd.name;
859
860    if print_flags & print_flags::WHENCE_WORD != 0 {
861        let kind = if cmd.is_hashed() { "hashed" } else { "command" };
862        return format!("{}: {}\n", name, kind);
863    }
864
865    if print_flags & (print_flags::WHENCE_CSH | print_flags::WHENCE_SIMPLE) != 0 {
866        if cmd.is_hashed() {
867            if let Some(ref p) = cmd.path {
868                return format!("{}\n", p.display());
869            }
870        } else if let Some(idx) = cmd.dir_index {
871            if idx < path.len() {
872                return format!("{}/{}\n", path[idx], name);
873            }
874        }
875        return format!("{}\n", name);
876    }
877
878    if print_flags & print_flags::WHENCE_VERBOSE != 0 {
879        if cmd.is_hashed() {
880            if let Some(ref p) = cmd.path {
881                return format!("{} is hashed to {}\n", name, p.display());
882            }
883        } else if let Some(idx) = cmd.dir_index {
884            if idx < path.len() {
885                return format!("{} is {}/{}\n", name, path[idx], name);
886            }
887        }
888        return format!("{} is {}\n", name, name);
889    }
890
891    if print_flags & print_flags::LIST != 0 {
892        let prefix = if name.starts_with('-') {
893            "hash -- "
894        } else {
895            "hash "
896        };
897
898        if cmd.is_hashed() {
899            if let Some(ref p) = cmd.path {
900                return format!("{}{}={}\n", prefix, name, p.display());
901            }
902        } else if let Some(idx) = cmd.dir_index {
903            if idx < path.len() {
904                return format!("{}{}={}/{}\n", prefix, name, path[idx], name);
905            }
906        }
907    }
908
909    if cmd.is_hashed() {
910        if let Some(ref p) = cmd.path {
911            return format!("{}={}\n", name, p.display());
912        }
913    } else if let Some(idx) = cmd.dir_index {
914        if idx < path.len() {
915            return format!("{}={}/{}\n", name, path[idx], name);
916        }
917    }
918
919    format!("{}={}\n", name, name)
920}
921
922/// Format a shell function for output
923pub fn format_shfunc(func: &ShFunc, print_flags: u32) -> String {
924    let name = &func.name;
925
926    if print_flags & print_flags::NAMEONLY != 0
927        || (print_flags & print_flags::WHENCE_SIMPLE != 0
928            && print_flags & print_flags::WHENCE_FUNCDEF == 0)
929    {
930        return format!("{}\n", name);
931    }
932
933    if print_flags & (print_flags::WHENCE_VERBOSE | print_flags::WHENCE_WORD) != 0
934        && print_flags & print_flags::WHENCE_FUNCDEF == 0
935    {
936        if print_flags & print_flags::WHENCE_WORD != 0 {
937            return format!("{}: function\n", name);
938        }
939
940        let kind = if func.is_autoload() {
941            "is an autoload shell function"
942        } else {
943            "is a shell function"
944        };
945
946        let mut result = format!("{} {}", name, kind);
947        if let Some(ref filename) = func.filename {
948            result.push_str(&format!(" from {}", filename));
949        }
950        result.push('\n');
951        return result;
952    }
953
954    let mut result = format!("{} () {{\n", name);
955
956    if func.is_autoload() {
957        result.push_str("\t# undefined\n");
958        if func.is_traced() {
959            result.push_str("\t# traced\n");
960        }
961        result.push_str("\tbuiltin autoload -X");
962        if let Some(ref filename) = func.filename {
963            if func.flags & flags::PM_LOADDIR != 0 {
964                result.push_str(&format!(" {}", filename));
965            }
966        }
967    } else if let Some(ref body) = func.body {
968        if func.is_traced() {
969            result.push_str("\t# traced\n");
970        }
971        for line in body.lines() {
972            result.push_str(&format!("\t{}\n", line));
973        }
974    }
975
976    result.push_str("}\n");
977    result
978}
979
980/// Format a reserved word for output
981pub fn format_reswd(rw: &Reswd, print_flags: u32) -> String {
982    let name = &rw.name;
983
984    if print_flags & print_flags::WHENCE_WORD != 0 {
985        return format!("{}: reserved\n", name);
986    }
987
988    if print_flags & print_flags::WHENCE_CSH != 0 {
989        return format!("{}: shell reserved word\n", name);
990    }
991
992    if print_flags & print_flags::WHENCE_VERBOSE != 0 {
993        return format!("{} is a reserved word\n", name);
994    }
995
996    format!("{}\n", name)
997}
998
999/// Format an alias for output
1000pub fn format_alias(alias: &Alias, print_flags: u32) -> String {
1001    let name = &alias.name;
1002    let text = &alias.text;
1003
1004    if print_flags & print_flags::NAMEONLY != 0 {
1005        return format!("{}\n", name);
1006    }
1007
1008    if print_flags & print_flags::WHENCE_WORD != 0 {
1009        let kind = if alias.is_suffix() {
1010            "suffix alias"
1011        } else if alias.is_global() {
1012            "global alias"
1013        } else {
1014            "alias"
1015        };
1016        return format!("{}: {}\n", name, kind);
1017    }
1018
1019    if print_flags & print_flags::WHENCE_SIMPLE != 0 {
1020        return format!("{}\n", text);
1021    }
1022
1023    if print_flags & print_flags::WHENCE_CSH != 0 {
1024        let kind = if alias.is_suffix() {
1025            "suffix "
1026        } else if alias.is_global() {
1027            "globally "
1028        } else {
1029            ""
1030        };
1031        return format!("{}: {}aliased to {}\n", name, kind, text);
1032    }
1033
1034    if print_flags & print_flags::WHENCE_VERBOSE != 0 {
1035        let kind = if alias.is_suffix() {
1036            " suffix"
1037        } else if alias.is_global() {
1038            " global"
1039        } else {
1040            "n"
1041        };
1042        return format!("{} is a{} alias for {}\n", name, kind, text);
1043    }
1044
1045    if print_flags & print_flags::LIST != 0 {
1046        if name.contains('=') {
1047            return format!("# invalid alias '{}'\n", name);
1048        }
1049
1050        let mut result = String::from("alias ");
1051        if alias.is_suffix() {
1052            result.push_str("-s ");
1053        } else if alias.is_global() {
1054            result.push_str("-g ");
1055        }
1056
1057        if name.starts_with('-') || name.starts_with('+') {
1058            result.push_str("-- ");
1059        }
1060
1061        result.push_str(&format!("{}={}\n", shell_quote(name), shell_quote(text)));
1062        return result;
1063    }
1064
1065    format!("{}={}\n", shell_quote(name), shell_quote(text))
1066}
1067
1068/// Quote a string for shell output
1069fn shell_quote(s: &str) -> String {
1070    if s.chars()
1071        .all(|c| c.is_alphanumeric() || c == '_' || c == '-' || c == '/' || c == '.')
1072    {
1073        s.to_string()
1074    } else {
1075        format!("'{}'", s.replace('\'', "'\\''"))
1076    }
1077}
1078
1079#[cfg(test)]
1080mod tests {
1081    use super::*;
1082
1083    #[test]
1084    fn test_hasher() {
1085        assert_eq!(hasher(""), 0);
1086        assert!(hasher("test") != 0);
1087        assert_eq!(hasher("test"), hasher("test"));
1088        assert_ne!(hasher("test"), hasher("Test"));
1089    }
1090
1091    #[test]
1092    fn test_hist_hasher() {
1093        assert_eq!(hist_hasher("  hello  world  "), hist_hasher("hello world"));
1094        assert_ne!(hist_hasher("hello world"), hist_hasher("helloworld"));
1095    }
1096
1097    #[test]
1098    fn test_hist_strcmp() {
1099        assert_eq!(
1100            hist_strcmp("  hello  world  ", "hello world", false),
1101            std::cmp::Ordering::Equal
1102        );
1103        assert_eq!(
1104            hist_strcmp("hello world", "hello world", true),
1105            std::cmp::Ordering::Equal
1106        );
1107    }
1108
1109    #[test]
1110    fn test_cmdnam_table() {
1111        let mut table = CmdNameTable::new();
1112        table.add(CmdName::with_path("ls", PathBuf::from("/bin/ls")));
1113
1114        assert!(table.get("ls").is_some());
1115        assert!(table.get("nonexistent").is_none());
1116
1117        let ls = table.get("ls").unwrap();
1118        assert!(ls.is_hashed());
1119        assert!(!ls.is_disabled());
1120    }
1121
1122    #[test]
1123    fn test_shfunc_table() {
1124        let mut table = ShFuncTable::new();
1125        table.add(ShFunc::with_body("myfunc", "echo hello"));
1126        table.add(ShFunc::autoload("lazy"));
1127
1128        assert!(table.get("myfunc").is_some());
1129        assert!(!table.get("myfunc").unwrap().is_autoload());
1130        assert!(table.get("lazy").unwrap().is_autoload());
1131
1132        table.disable("myfunc");
1133        assert!(table.get("myfunc").is_none());
1134        assert!(table.get_including_disabled("myfunc").is_some());
1135
1136        table.enable("myfunc");
1137        assert!(table.get("myfunc").is_some());
1138    }
1139
1140    #[test]
1141    fn test_reswd_table() {
1142        let table = ReswdTable::new();
1143
1144        assert!(table.is_reserved("if"));
1145        assert!(table.is_reserved("while"));
1146        assert!(table.is_reserved("[["));
1147        assert!(!table.is_reserved("notreserved"));
1148
1149        let if_rw = table.get("if").unwrap();
1150        assert_eq!(if_rw.token, ReswdToken::If);
1151    }
1152
1153    #[test]
1154    fn test_alias_table() {
1155        let mut table = AliasTable::with_defaults();
1156
1157        assert!(table.get("run-help").is_some());
1158        assert_eq!(table.get("run-help").unwrap().text, "man");
1159
1160        table.add(Alias::global("G", "| grep"));
1161        assert!(table.get("G").unwrap().is_global());
1162
1163        table.add(Alias::suffix("pdf", "zathura"));
1164        assert!(table.get("pdf").unwrap().is_suffix());
1165
1166        table.disable("G");
1167        assert!(table.get("G").is_none());
1168    }
1169
1170    #[test]
1171    fn test_dir_cache() {
1172        let mut cache = DirCache::new();
1173
1174        let d1 = cache.get_or_insert("/usr/share/zsh");
1175        let d2 = cache.get_or_insert("/usr/share/zsh");
1176        assert_eq!(d1, d2);
1177        assert_eq!(cache.len(), 1);
1178
1179        let d3 = cache.get_or_insert("/home/user/.zsh");
1180        assert_ne!(d1, d3);
1181        assert_eq!(cache.len(), 2);
1182
1183        cache.release("/usr/share/zsh");
1184        assert_eq!(cache.len(), 2);
1185
1186        cache.release("/usr/share/zsh");
1187        assert_eq!(cache.len(), 1);
1188    }
1189
1190    #[test]
1191    fn test_format_alias() {
1192        let alias = Alias::new("ll", "ls -l");
1193        let output = format_alias(&alias, print_flags::WHENCE_VERBOSE);
1194        assert!(output.contains("is an alias for"));
1195
1196        let global = Alias::global("G", "| grep");
1197        let output = format_alias(&global, print_flags::WHENCE_WORD);
1198        assert!(output.contains("global alias"));
1199    }
1200
1201    #[test]
1202    fn test_format_reswd() {
1203        let table = ReswdTable::new();
1204        let if_rw = table.get("if").unwrap();
1205
1206        let output = format_reswd(if_rw, print_flags::WHENCE_VERBOSE);
1207        assert!(output.contains("is a reserved word"));
1208
1209        let output = format_reswd(if_rw, print_flags::WHENCE_WORD);
1210        assert!(output.contains("reserved"));
1211    }
1212}