1use std::collections::HashMap;
7use std::fs;
8use std::path::{Path, PathBuf};
9
10pub 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
26pub 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
35pub 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
67pub 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#[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#[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 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 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 pub fn iter(&self) -> impl Iterator<Item = (&String, &CmdName)> {
294 self.table.iter()
295 }
296
297 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#[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#[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#[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#[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#[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#[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#[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#[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
759pub type SuffixAliasTable = AliasTable;
761
762#[derive(Debug, Clone)]
764struct DirCacheEntry {
765 name: String,
766 refs: usize,
767}
768
769#[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 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 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
845pub 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
856pub 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
922pub 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
980pub 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
999pub 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
1068fn 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}