1use std::cmp::Ordering;
14use std::collections::HashSet;
15use std::fs::{self, Metadata};
16use std::os::unix::fs::{MetadataExt, PermissionsExt};
17use std::path::{Path, PathBuf};
18use std::time::{SystemTime, UNIX_EPOCH};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum GlobSort {
23 Name,
24 Depth,
25 Size,
26 Atime,
27 Mtime,
28 Ctime,
29 Links,
30 None,
31 Exec(usize), }
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum SortOrder {
37 Ascending,
38 Descending,
39}
40
41#[derive(Debug, Clone)]
43pub struct SortSpec {
44 pub sort_type: GlobSort,
45 pub order: SortOrder,
46 pub follow_links: bool,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum TimeUnit {
52 Seconds,
53 Minutes,
54 Hours,
55 Days,
56 Weeks,
57 Months,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum SizeUnit {
63 Bytes,
64 PosixBlocks,
65 Kilobytes,
66 Megabytes,
67 Gigabytes,
68 Terabytes,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum RangeOp {
74 Less,
75 Equal,
76 Greater,
77}
78
79#[derive(Debug, Clone)]
81pub enum Qualifier {
82 IsRegular,
84 IsDirectory,
85 IsSymlink,
86 IsSocket,
87 IsFifo,
88 IsBlockDev,
89 IsCharDev,
90 IsDevice,
91 IsExecutable,
92
93 Readable,
95 Writable,
96 Executable,
97 WorldReadable,
98 WorldWritable,
99 WorldExecutable,
100 GroupReadable,
101 GroupWritable,
102 GroupExecutable,
103 Setuid,
104 Setgid,
105 Sticky,
106
107 OwnedByEuid,
109 OwnedByEgid,
110 OwnedByUid(u32),
111 OwnedByGid(u32),
112
113 Size {
115 value: u64,
116 unit: SizeUnit,
117 op: RangeOp,
118 },
119 Links {
120 value: u64,
121 op: RangeOp,
122 },
123 Atime {
124 value: i64,
125 unit: TimeUnit,
126 op: RangeOp,
127 },
128 Mtime {
129 value: i64,
130 unit: TimeUnit,
131 op: RangeOp,
132 },
133 Ctime {
134 value: i64,
135 unit: TimeUnit,
136 op: RangeOp,
137 },
138
139 Mode {
141 yes: u32,
142 no: u32,
143 },
144
145 Device(u64),
147
148 NonEmptyDir,
150
151 Eval(String),
153}
154
155#[derive(Debug, Clone)]
157pub struct GlobMatch {
158 pub name: String,
159 pub path: PathBuf,
160 pub size: u64,
161 pub atime: i64,
162 pub mtime: i64,
163 pub ctime: i64,
164 pub links: u64,
165 pub mode: u32,
166 pub uid: u32,
167 pub gid: u32,
168 pub dev: u64,
169 pub ino: u64,
170 pub target_size: u64,
172 pub target_atime: i64,
173 pub target_mtime: i64,
174 pub target_ctime: i64,
175 pub target_links: u64,
176 pub sort_strings: Vec<String>,
178}
179
180impl GlobMatch {
181 pub fn from_path(path: &Path) -> Option<Self> {
182 let meta = fs::symlink_metadata(path).ok()?;
183 let name = path.file_name()?.to_string_lossy().to_string();
184
185 let (target_size, target_atime, target_mtime, target_ctime, target_links) =
186 if meta.file_type().is_symlink() {
187 if let Ok(target_meta) = fs::metadata(path) {
188 (
189 target_meta.size(),
190 target_meta.atime(),
191 target_meta.mtime(),
192 target_meta.ctime(),
193 target_meta.nlink(),
194 )
195 } else {
196 (
197 meta.size(),
198 meta.atime(),
199 meta.mtime(),
200 meta.ctime(),
201 meta.nlink(),
202 )
203 }
204 } else {
205 (
206 meta.size(),
207 meta.atime(),
208 meta.mtime(),
209 meta.ctime(),
210 meta.nlink(),
211 )
212 };
213
214 Some(GlobMatch {
215 name,
216 path: path.to_path_buf(),
217 size: meta.size(),
218 atime: meta.atime(),
219 mtime: meta.mtime(),
220 ctime: meta.ctime(),
221 links: meta.nlink(),
222 mode: meta.mode(),
223 uid: meta.uid(),
224 gid: meta.gid(),
225 dev: meta.dev(),
226 ino: meta.ino(),
227 target_size,
228 target_atime,
229 target_mtime,
230 target_ctime,
231 target_links,
232 sort_strings: Vec::new(),
233 })
234 }
235
236 pub fn compare(&self, other: &Self, specs: &[SortSpec], numeric_sort: bool) -> Ordering {
237 for spec in specs {
238 let cmp = match spec.sort_type {
239 GlobSort::Name => {
240 if numeric_sort {
241 numeric_string_cmp(&self.name, &other.name)
242 } else {
243 self.name.cmp(&other.name)
244 }
245 }
246 GlobSort::Depth => {
247 let self_depth = self.path.components().count();
248 let other_depth = other.path.components().count();
249 self_depth.cmp(&other_depth)
250 }
251 GlobSort::Size => {
252 if spec.follow_links {
253 self.target_size.cmp(&other.target_size)
254 } else {
255 self.size.cmp(&other.size)
256 }
257 }
258 GlobSort::Atime => {
259 if spec.follow_links {
260 other.target_atime.cmp(&self.target_atime)
261 } else {
262 other.atime.cmp(&self.atime)
263 }
264 }
265 GlobSort::Mtime => {
266 if spec.follow_links {
267 other.target_mtime.cmp(&self.target_mtime)
268 } else {
269 other.mtime.cmp(&self.mtime)
270 }
271 }
272 GlobSort::Ctime => {
273 if spec.follow_links {
274 other.target_ctime.cmp(&self.target_ctime)
275 } else {
276 other.ctime.cmp(&self.ctime)
277 }
278 }
279 GlobSort::Links => {
280 if spec.follow_links {
281 other.target_links.cmp(&self.target_links)
282 } else {
283 other.links.cmp(&self.links)
284 }
285 }
286 GlobSort::None => Ordering::Equal,
287 GlobSort::Exec(idx) => {
288 let a = self.sort_strings.get(idx).map(|s| s.as_str()).unwrap_or("");
289 let b = other
290 .sort_strings
291 .get(idx)
292 .map(|s| s.as_str())
293 .unwrap_or("");
294 if numeric_sort {
295 numeric_string_cmp(a, b)
296 } else {
297 a.cmp(b)
298 }
299 }
300 };
301
302 if cmp != Ordering::Equal {
303 return match spec.order {
304 SortOrder::Ascending => cmp,
305 SortOrder::Descending => cmp.reverse(),
306 };
307 }
308 }
309 Ordering::Equal
310 }
311}
312
313fn numeric_string_cmp(a: &str, b: &str) -> Ordering {
315 let mut ai = a.chars().peekable();
316 let mut bi = b.chars().peekable();
317
318 loop {
319 match (ai.peek(), bi.peek()) {
320 (None, None) => return Ordering::Equal,
321 (None, Some(_)) => return Ordering::Less,
322 (Some(_), None) => return Ordering::Greater,
323 (Some(&ac), Some(&bc)) => {
324 if ac.is_ascii_digit() && bc.is_ascii_digit() {
325 let mut an = String::new();
327 let mut bn = String::new();
328 while let Some(&c) = ai.peek() {
329 if c.is_ascii_digit() {
330 an.push(c);
331 ai.next();
332 } else {
333 break;
334 }
335 }
336 while let Some(&c) = bi.peek() {
337 if c.is_ascii_digit() {
338 bn.push(c);
339 bi.next();
340 } else {
341 break;
342 }
343 }
344 let av: u64 = an.parse().unwrap_or(0);
345 let bv: u64 = bn.parse().unwrap_or(0);
346 match av.cmp(&bv) {
347 Ordering::Equal => continue,
348 other => return other,
349 }
350 } else {
351 match ac.cmp(&bc) {
352 Ordering::Equal => {
353 ai.next();
354 bi.next();
355 }
356 other => return other,
357 }
358 }
359 }
360 }
361 }
362}
363
364#[derive(Debug, Clone, Default)]
366pub struct GlobOptions {
367 pub null_glob: bool,
368 pub mark_dirs: bool,
369 pub no_glob_dots: bool,
370 pub list_types: bool,
371 pub numeric_sort: bool,
372 pub follow_links: bool,
373 pub extended_glob: bool,
374 pub case_glob: bool,
375 pub glob_star_short: bool,
376 pub bare_glob_qual: bool,
377 pub brace_ccl: bool,
378}
379
380#[derive(Debug, Clone, Default)]
382pub struct QualifierSet {
383 pub qualifiers: Vec<Qualifier>,
384 pub alternatives: Vec<Vec<Qualifier>>,
385 pub negated: bool,
386 pub follow_links: bool,
387 pub sorts: Vec<SortSpec>,
388 pub first: Option<i32>,
389 pub last: Option<i32>,
390 pub colon_mods: Option<String>,
391 pub pre_words: Vec<String>,
392 pub post_words: Vec<String>,
393}
394
395pub struct GlobState {
397 pub options: GlobOptions,
398 pub matches: Vec<GlobMatch>,
399 pub qualifiers: Option<QualifierSet>,
400 pathbuf: String,
401 pathpos: usize,
402}
403
404impl GlobState {
405 pub fn new(options: GlobOptions) -> Self {
406 GlobState {
407 options,
408 matches: Vec::new(),
409 qualifiers: None,
410 pathbuf: String::with_capacity(4096),
411 pathpos: 0,
412 }
413 }
414
415 pub fn glob(&mut self, pattern: &str) -> Vec<String> {
417 self.matches.clear();
418 self.pathbuf.clear();
419 self.pathpos = 0;
420
421 if !has_wildcards(pattern) {
423 return vec![pattern.to_string()];
424 }
425
426 let (pat, quals) = self.parse_qualifiers(pattern);
428 self.qualifiers = quals;
429
430 if let Some(complist) = self.parse_pattern(&pat) {
432 if pat.starts_with('/') {
434 self.pathbuf.push('/');
435 self.pathpos = 1;
436 }
437
438 self.scanner(&complist, 0);
440 }
441
442 self.sort_matches();
444
445 self.apply_selection();
447
448 let mut results: Vec<String> = self
450 .matches
451 .iter()
452 .map(|m| {
453 let mut s = m.path.to_string_lossy().to_string();
454 if self.options.mark_dirs || self.options.list_types {
455 if let Ok(meta) = fs::symlink_metadata(&m.path) {
456 let ch = file_type_char(meta.mode());
457 if self.options.list_types || (self.options.mark_dirs && ch == '/') {
458 s.push(ch);
459 }
460 }
461 }
462 s
463 })
464 .collect();
465
466 if results.is_empty() && !self.options.null_glob {
468 results.push(pattern.to_string());
469 }
470
471 results
472 }
473
474 fn parse_qualifiers(&self, pattern: &str) -> (String, Option<QualifierSet>) {
475 if !pattern.ends_with(')') {
476 return (pattern.to_string(), None);
477 }
478
479 let bytes = pattern.as_bytes();
481 let mut depth = 0;
482 let mut qual_start = None;
483
484 for i in (0..bytes.len()).rev() {
485 match bytes[i] {
486 b')' => depth += 1,
487 b'(' => {
488 depth -= 1;
489 if depth == 0 {
490 qual_start = Some(i);
491 break;
492 }
493 }
494 _ => {}
495 }
496 }
497
498 let start = match qual_start {
499 Some(s) => s,
500 None => return (pattern.to_string(), None),
501 };
502
503 let qual_str = &pattern[start + 1..pattern.len() - 1];
505 let (is_explicit, qual_content) = if qual_str.starts_with("#q") {
506 (true, &qual_str[2..])
507 } else if self.options.bare_glob_qual {
508 (false, qual_str)
509 } else {
510 return (pattern.to_string(), None);
511 };
512
513 if !is_explicit && (qual_content.contains('|') || qual_content.contains('~')) {
515 return (pattern.to_string(), None);
516 }
517
518 let qs = self.parse_qualifier_string(qual_content);
520 (pattern[..start].to_string(), Some(qs))
521 }
522
523 fn parse_qualifier_string(&self, s: &str) -> QualifierSet {
524 let mut qs = QualifierSet::default();
525 let mut chars = s.chars().peekable();
526 let mut negated = false;
527 let mut follow = false;
528
529 while let Some(c) = chars.next() {
530 match c {
531 '^' => negated = !negated,
532 '-' => follow = !follow,
533 ',' => {
534 if !qs.qualifiers.is_empty() {
536 qs.alternatives.push(std::mem::take(&mut qs.qualifiers));
537 }
538 negated = false;
539 follow = false;
540 }
541 ':' => {
542 let rest: String = chars.collect();
544 qs.colon_mods = Some(format!(":{}", rest));
545 break;
546 }
547 '/' => qs.qualifiers.push(Qualifier::IsDirectory),
549 '.' => qs.qualifiers.push(Qualifier::IsRegular),
550 '@' => qs.qualifiers.push(Qualifier::IsSymlink),
551 '=' => qs.qualifiers.push(Qualifier::IsSocket),
552 'p' => qs.qualifiers.push(Qualifier::IsFifo),
553 '%' => match chars.peek() {
554 Some('b') => {
555 chars.next();
556 qs.qualifiers.push(Qualifier::IsBlockDev);
557 }
558 Some('c') => {
559 chars.next();
560 qs.qualifiers.push(Qualifier::IsCharDev);
561 }
562 _ => qs.qualifiers.push(Qualifier::IsDevice),
563 },
564 '*' => qs.qualifiers.push(Qualifier::IsExecutable),
565 'r' => qs.qualifiers.push(Qualifier::Readable),
567 'w' => qs.qualifiers.push(Qualifier::Writable),
568 'x' => qs.qualifiers.push(Qualifier::Executable),
569 'R' => qs.qualifiers.push(Qualifier::WorldReadable),
570 'W' => qs.qualifiers.push(Qualifier::WorldWritable),
571 'X' => qs.qualifiers.push(Qualifier::WorldExecutable),
572 'A' => qs.qualifiers.push(Qualifier::GroupReadable),
573 'I' => qs.qualifiers.push(Qualifier::GroupWritable),
574 'E' => qs.qualifiers.push(Qualifier::GroupExecutable),
575 's' => qs.qualifiers.push(Qualifier::Setuid),
576 'S' => qs.qualifiers.push(Qualifier::Setgid),
577 't' => qs.qualifiers.push(Qualifier::Sticky),
578 'U' => qs.qualifiers.push(Qualifier::OwnedByEuid),
580 'G' => qs.qualifiers.push(Qualifier::OwnedByEgid),
581 'u' => {
582 let uid = self.parse_uid_gid(&mut chars);
583 qs.qualifiers.push(Qualifier::OwnedByUid(uid));
584 }
585 'g' => {
586 let gid = self.parse_uid_gid(&mut chars);
587 qs.qualifiers.push(Qualifier::OwnedByGid(gid));
588 }
589 'L' => {
591 let (unit, op, val) = self.parse_size_spec(&mut chars);
592 qs.qualifiers.push(Qualifier::Size {
593 value: val,
594 unit,
595 op,
596 });
597 }
598 'l' => {
600 let (op, val) = self.parse_range_spec(&mut chars);
601 qs.qualifiers.push(Qualifier::Links { value: val, op });
602 }
603 'a' => {
605 let (unit, op, val) = self.parse_time_spec(&mut chars);
606 qs.qualifiers.push(Qualifier::Atime {
607 value: val as i64,
608 unit,
609 op,
610 });
611 }
612 'm' => {
613 let (unit, op, val) = self.parse_time_spec(&mut chars);
614 qs.qualifiers.push(Qualifier::Mtime {
615 value: val as i64,
616 unit,
617 op,
618 });
619 }
620 'c' => {
621 let (unit, op, val) = self.parse_time_spec(&mut chars);
622 qs.qualifiers.push(Qualifier::Ctime {
623 value: val as i64,
624 unit,
625 op,
626 });
627 }
628 'o' | 'O' => {
630 let desc = c == 'O';
631 if let Some(&sc) = chars.peek() {
632 let sort_type = match sc {
633 'n' => {
634 chars.next();
635 GlobSort::Name
636 }
637 'L' => {
638 chars.next();
639 GlobSort::Size
640 }
641 'l' => {
642 chars.next();
643 GlobSort::Links
644 }
645 'a' => {
646 chars.next();
647 GlobSort::Atime
648 }
649 'm' => {
650 chars.next();
651 GlobSort::Mtime
652 }
653 'c' => {
654 chars.next();
655 GlobSort::Ctime
656 }
657 'd' => {
658 chars.next();
659 GlobSort::Depth
660 }
661 'N' => {
662 chars.next();
663 GlobSort::None
664 }
665 _ => GlobSort::Name,
666 };
667 qs.sorts.push(SortSpec {
668 sort_type,
669 order: if desc {
670 SortOrder::Descending
671 } else {
672 SortOrder::Ascending
673 },
674 follow_links: follow,
675 });
676 }
677 }
678 'N' => { }
680 'D' => { }
681 'n' => { }
682 'M' => { }
683 'T' => { }
684 'F' => qs.qualifiers.push(Qualifier::NonEmptyDir),
685 '[' => {
687 let (first, last) = self.parse_subscript(&mut chars);
688 qs.first = first;
689 qs.last = last;
690 }
691 _ => {}
692 }
693 }
694
695 if !qs.qualifiers.is_empty() {
696 qs.alternatives.push(std::mem::take(&mut qs.qualifiers));
697 }
698
699 qs.negated = negated;
700 qs.follow_links = follow;
701 qs
702 }
703
704 fn parse_uid_gid(&self, chars: &mut std::iter::Peekable<std::str::Chars>) -> u32 {
705 if chars.peek().map(|c| c.is_ascii_digit()).unwrap_or(false) {
707 let mut num = String::new();
708 while let Some(&c) = chars.peek() {
709 if c.is_ascii_digit() {
710 num.push(c);
711 chars.next();
712 } else {
713 break;
714 }
715 }
716 num.parse().unwrap_or(0)
717 } else {
718 0
720 }
721 }
722
723 fn parse_size_spec(
724 &self,
725 chars: &mut std::iter::Peekable<std::str::Chars>,
726 ) -> (SizeUnit, RangeOp, u64) {
727 let unit = match chars.peek() {
728 Some('p') | Some('P') => {
729 chars.next();
730 SizeUnit::PosixBlocks
731 }
732 Some('k') | Some('K') => {
733 chars.next();
734 SizeUnit::Kilobytes
735 }
736 Some('m') | Some('M') => {
737 chars.next();
738 SizeUnit::Megabytes
739 }
740 Some('g') | Some('G') => {
741 chars.next();
742 SizeUnit::Gigabytes
743 }
744 Some('t') | Some('T') => {
745 chars.next();
746 SizeUnit::Terabytes
747 }
748 _ => SizeUnit::Bytes,
749 };
750 let (op, val) = self.parse_range_spec(chars);
751 (unit, op, val)
752 }
753
754 fn parse_time_spec(
755 &self,
756 chars: &mut std::iter::Peekable<std::str::Chars>,
757 ) -> (TimeUnit, RangeOp, u64) {
758 let unit = match chars.peek() {
759 Some('s') => {
760 chars.next();
761 TimeUnit::Seconds
762 }
763 Some('m') => {
764 chars.next();
765 TimeUnit::Minutes
766 }
767 Some('h') => {
768 chars.next();
769 TimeUnit::Hours
770 }
771 Some('d') => {
772 chars.next();
773 TimeUnit::Days
774 }
775 Some('w') => {
776 chars.next();
777 TimeUnit::Weeks
778 }
779 Some('M') => {
780 chars.next();
781 TimeUnit::Months
782 }
783 _ => TimeUnit::Days,
784 };
785 let (op, val) = self.parse_range_spec(chars);
786 (unit, op, val)
787 }
788
789 fn parse_range_spec(&self, chars: &mut std::iter::Peekable<std::str::Chars>) -> (RangeOp, u64) {
790 let op = match chars.peek() {
791 Some('+') => {
792 chars.next();
793 RangeOp::Greater
794 }
795 Some('-') => {
796 chars.next();
797 RangeOp::Less
798 }
799 _ => RangeOp::Equal,
800 };
801 let mut num = String::new();
802 while let Some(&c) = chars.peek() {
803 if c.is_ascii_digit() {
804 num.push(c);
805 chars.next();
806 } else {
807 break;
808 }
809 }
810 let val = num.parse().unwrap_or(0);
811 (op, val)
812 }
813
814 fn parse_subscript(
815 &self,
816 chars: &mut std::iter::Peekable<std::str::Chars>,
817 ) -> (Option<i32>, Option<i32>) {
818 let mut first_str = String::new();
819 let mut last_str = String::new();
820 let mut in_last = false;
821
822 while let Some(&c) = chars.peek() {
823 chars.next();
824 if c == ']' {
825 break;
826 } else if c == ',' {
827 in_last = true;
828 } else if in_last {
829 last_str.push(c);
830 } else {
831 first_str.push(c);
832 }
833 }
834
835 let first = first_str.parse().ok();
836 let last = if in_last {
837 last_str.parse().ok()
838 } else {
839 first
840 };
841 (first, last)
842 }
843
844 fn parse_pattern(&self, pattern: &str) -> Option<Vec<PatternComponent>> {
845 let mut components = Vec::new();
846 let mut current = String::new();
847 let mut chars = pattern.chars().peekable();
848 let mut in_bracket = false;
849
850 if chars.peek() == Some(&'/') {
852 chars.next();
853 }
854
855 while let Some(c) = chars.next() {
856 match c {
857 '/' if !in_bracket => {
858 if !current.is_empty() {
859 components.push(PatternComponent::Pattern(current.clone()));
860 current.clear();
861 }
862 }
863 '[' => {
864 in_bracket = true;
865 current.push(c);
866 }
867 ']' => {
868 in_bracket = false;
869 current.push(c);
870 }
871 '*' if !in_bracket && chars.peek() == Some(&'*') => {
872 chars.next();
873 let follow = chars.peek() == Some(&'*');
875 if follow {
876 chars.next();
877 }
878 if chars.peek() == Some(&'/') {
880 chars.next();
881 }
882 if !current.is_empty() {
883 components.push(PatternComponent::Pattern(current.clone()));
884 current.clear();
885 }
886 components.push(PatternComponent::Recursive {
887 follow_links: follow,
888 });
889 }
890 _ => current.push(c),
891 }
892 }
893
894 if !current.is_empty() {
895 components.push(PatternComponent::Pattern(current));
896 }
897
898 if components.is_empty() {
899 None
900 } else {
901 Some(components)
902 }
903 }
904
905 fn scanner(&mut self, components: &[PatternComponent], depth: usize) {
906 if components.is_empty() {
907 return;
908 }
909
910 let base_path = if self.pathbuf.is_empty() {
911 ".".to_string()
912 } else {
913 self.pathbuf.clone()
914 };
915
916 match &components[0] {
917 PatternComponent::Pattern(pat) => {
918 self.scan_pattern(&base_path, pat, &components[1..], depth);
919 }
920 PatternComponent::Recursive { follow_links } => {
921 self.scanner(&components[1..], depth);
923 self.scan_recursive(&base_path, &components[1..], *follow_links, depth);
925 }
926 }
927 }
928
929 fn scan_pattern(&mut self, base: &str, pattern: &str, rest: &[PatternComponent], depth: usize) {
930 let dir = match fs::read_dir(base) {
931 Ok(d) => d,
932 Err(_) => return,
933 };
934
935 for entry in dir.flatten() {
936 let name = entry.file_name().to_string_lossy().to_string();
937
938 if self.options.no_glob_dots && name.starts_with('.') && !pattern.starts_with('.') {
940 continue;
941 }
942
943 if pattern_match(
944 pattern,
945 &name,
946 self.options.extended_glob,
947 self.options.case_glob,
948 ) {
949 let path = entry.path();
950
951 if rest.is_empty() {
952 if self.check_qualifiers(&path) {
954 if let Some(m) = GlobMatch::from_path(&path) {
955 self.matches.push(m);
956 }
957 }
958 } else {
959 if path.is_dir() {
961 let old_pos = self.pathbuf.len();
962 if !self.pathbuf.is_empty() && !self.pathbuf.ends_with('/') {
963 self.pathbuf.push('/');
964 }
965 self.pathbuf.push_str(&name);
966 self.scanner(rest, depth + 1);
967 self.pathbuf.truncate(old_pos);
968 }
969 }
970 }
971 }
972 }
973
974 fn scan_recursive(
975 &mut self,
976 base: &str,
977 rest: &[PatternComponent],
978 follow_links: bool,
979 depth: usize,
980 ) {
981 let dir = match fs::read_dir(base) {
982 Ok(d) => d,
983 Err(_) => return,
984 };
985
986 for entry in dir.flatten() {
987 let name = entry.file_name().to_string_lossy().to_string();
988
989 if self.options.no_glob_dots && name.starts_with('.') {
991 continue;
992 }
993
994 let path = entry.path();
995 let is_dir = if follow_links {
996 path.is_dir()
997 } else {
998 entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false)
999 };
1000
1001 if is_dir {
1002 let old_pos = self.pathbuf.len();
1003 if !self.pathbuf.is_empty() && !self.pathbuf.ends_with('/') {
1004 self.pathbuf.push('/');
1005 }
1006 self.pathbuf.push_str(&name);
1007
1008 self.scanner(rest, depth + 1);
1010
1011 self.scan_recursive(&self.pathbuf.clone(), rest, follow_links, depth + 1);
1013
1014 self.pathbuf.truncate(old_pos);
1015 }
1016 }
1017 }
1018
1019 fn check_qualifiers(&self, path: &Path) -> bool {
1020 let qs = match &self.qualifiers {
1021 Some(q) => q,
1022 None => return true,
1023 };
1024
1025 if qs.alternatives.is_empty() {
1026 return true;
1027 }
1028
1029 let meta = match if qs.follow_links {
1030 fs::metadata(path)
1031 } else {
1032 fs::symlink_metadata(path)
1033 } {
1034 Ok(m) => m,
1035 Err(_) => return false,
1036 };
1037
1038 for alt in &qs.alternatives {
1040 if self.check_qualifier_list(alt, path, &meta) {
1041 return !qs.negated;
1042 }
1043 }
1044
1045 qs.negated
1046 }
1047
1048 fn check_qualifier_list(&self, quals: &[Qualifier], path: &Path, meta: &Metadata) -> bool {
1049 for q in quals {
1050 if !self.check_single_qualifier(q, path, meta) {
1051 return false;
1052 }
1053 }
1054 true
1055 }
1056
1057 fn check_single_qualifier(&self, qual: &Qualifier, path: &Path, meta: &Metadata) -> bool {
1058 let mode = meta.mode();
1059 let ft = meta.file_type();
1060
1061 match qual {
1062 Qualifier::IsRegular => ft.is_file(),
1063 Qualifier::IsDirectory => ft.is_dir(),
1064 Qualifier::IsSymlink => ft.is_symlink(),
1065 Qualifier::IsSocket => mode & libc::S_IFMT as u32 == libc::S_IFSOCK as u32,
1066 Qualifier::IsFifo => mode & libc::S_IFMT as u32 == libc::S_IFIFO as u32,
1067 Qualifier::IsBlockDev => mode & libc::S_IFMT as u32 == libc::S_IFBLK as u32,
1068 Qualifier::IsCharDev => mode & libc::S_IFMT as u32 == libc::S_IFCHR as u32,
1069 Qualifier::IsDevice => {
1070 let fmt = mode & libc::S_IFMT as u32;
1071 fmt == libc::S_IFBLK as u32 || fmt == libc::S_IFCHR as u32
1072 }
1073 Qualifier::IsExecutable => ft.is_file() && (mode & 0o111 != 0),
1074 Qualifier::Readable => mode & 0o400 != 0,
1075 Qualifier::Writable => mode & 0o200 != 0,
1076 Qualifier::Executable => mode & 0o100 != 0,
1077 Qualifier::WorldReadable => mode & 0o004 != 0,
1078 Qualifier::WorldWritable => mode & 0o002 != 0,
1079 Qualifier::WorldExecutable => mode & 0o001 != 0,
1080 Qualifier::GroupReadable => mode & 0o040 != 0,
1081 Qualifier::GroupWritable => mode & 0o020 != 0,
1082 Qualifier::GroupExecutable => mode & 0o010 != 0,
1083 Qualifier::Setuid => mode & libc::S_ISUID as u32 != 0,
1084 Qualifier::Setgid => mode & libc::S_ISGID as u32 != 0,
1085 Qualifier::Sticky => mode & libc::S_ISVTX as u32 != 0,
1086 Qualifier::OwnedByEuid => meta.uid() == unsafe { libc::geteuid() },
1087 Qualifier::OwnedByEgid => meta.gid() == unsafe { libc::getegid() },
1088 Qualifier::OwnedByUid(uid) => meta.uid() == *uid,
1089 Qualifier::OwnedByGid(gid) => meta.gid() == *gid,
1090 Qualifier::Size { value, unit, op } => {
1091 let size = meta.size();
1092 let scaled = scale_size(size, *unit);
1093 compare_range(scaled, *value, *op)
1094 }
1095 Qualifier::Links { value, op } => compare_range(meta.nlink(), *value, *op),
1096 Qualifier::Atime { value, unit, op } => {
1097 let now = SystemTime::now()
1098 .duration_since(UNIX_EPOCH)
1099 .unwrap()
1100 .as_secs() as i64;
1101 let diff = now - meta.atime();
1102 let scaled = scale_time(diff, *unit);
1103 compare_range(scaled as u64, *value as u64, *op)
1104 }
1105 Qualifier::Mtime { value, unit, op } => {
1106 let now = SystemTime::now()
1107 .duration_since(UNIX_EPOCH)
1108 .unwrap()
1109 .as_secs() as i64;
1110 let diff = now - meta.mtime();
1111 let scaled = scale_time(diff, *unit);
1112 compare_range(scaled as u64, *value as u64, *op)
1113 }
1114 Qualifier::Ctime { value, unit, op } => {
1115 let now = SystemTime::now()
1116 .duration_since(UNIX_EPOCH)
1117 .unwrap()
1118 .as_secs() as i64;
1119 let diff = now - meta.ctime();
1120 let scaled = scale_time(diff, *unit);
1121 compare_range(scaled as u64, *value as u64, *op)
1122 }
1123 Qualifier::Mode { yes, no } => {
1124 let m = mode & 0o7777;
1125 (m & yes) == *yes && (m & no) == 0
1126 }
1127 Qualifier::Device(dev) => meta.dev() == *dev,
1128 Qualifier::NonEmptyDir => {
1129 if !ft.is_dir() {
1130 return false;
1131 }
1132 if let Ok(mut entries) = fs::read_dir(path) {
1133 entries.any(|e| {
1134 e.ok()
1135 .map(|e| {
1136 let name = e.file_name();
1137 name != "." && name != ".."
1138 })
1139 .unwrap_or(false)
1140 })
1141 } else {
1142 false
1143 }
1144 }
1145 Qualifier::Eval(_) => true, }
1147 }
1148
1149 fn sort_matches(&mut self) {
1150 let specs = self
1151 .qualifiers
1152 .as_ref()
1153 .map(|q| q.sorts.clone())
1154 .unwrap_or_else(|| {
1155 vec![SortSpec {
1156 sort_type: GlobSort::Name,
1157 order: SortOrder::Ascending,
1158 follow_links: false,
1159 }]
1160 });
1161
1162 if specs.iter().any(|s| s.sort_type == GlobSort::None) {
1163 return;
1164 }
1165
1166 let numeric = self.options.numeric_sort;
1167 self.matches.sort_by(|a, b| a.compare(b, &specs, numeric));
1168 }
1169
1170 fn apply_selection(&mut self) {
1171 let (first, last) = match &self.qualifiers {
1172 Some(q) => (q.first, q.last),
1173 None => return,
1174 };
1175
1176 let len = self.matches.len() as i32;
1177 if len == 0 {
1178 return;
1179 }
1180
1181 let start = match first {
1182 Some(f) if f < 0 => (len + f).max(0) as usize,
1183 Some(f) => (f - 1).max(0) as usize,
1184 None => 0,
1185 };
1186
1187 let end = match last {
1188 Some(l) if l < 0 => (len + l + 1).max(0) as usize,
1189 Some(l) => l.min(len) as usize,
1190 None => len as usize,
1191 };
1192
1193 if start < end && start < self.matches.len() {
1194 self.matches = self.matches[start..end.min(self.matches.len())].to_vec();
1195 } else {
1196 self.matches.clear();
1197 }
1198 }
1199}
1200
1201#[derive(Debug, Clone)]
1203enum PatternComponent {
1204 Pattern(String),
1205 Recursive { follow_links: bool },
1206}
1207
1208pub fn has_wildcards(s: &str) -> bool {
1210 let mut in_bracket = false;
1211 let mut escape = false;
1212
1213 for c in s.chars() {
1214 if escape {
1215 escape = false;
1216 continue;
1217 }
1218 match c {
1219 '\\' => escape = true,
1220 '[' => {
1221 in_bracket = true;
1222 return true; }
1224 ']' => in_bracket = false,
1225 '*' | '?' if !in_bracket => return true,
1226 '#' | '^' | '~' if !in_bracket => return true,
1227 _ => {}
1228 }
1229 }
1230 false
1231}
1232
1233pub fn pattern_match(pattern: &str, text: &str, extended: bool, case_sensitive: bool) -> bool {
1235 let pat = if case_sensitive {
1236 pattern.to_string()
1237 } else {
1238 pattern.to_lowercase()
1239 };
1240 let txt = if case_sensitive {
1241 text.to_string()
1242 } else {
1243 text.to_lowercase()
1244 };
1245
1246 glob_match_impl(&pat, &txt, extended)
1247}
1248
1249fn glob_match_impl(pattern: &str, text: &str, extended: bool) -> bool {
1250 let mut pi = pattern.chars().peekable();
1251 let mut ti = text.chars().peekable();
1252
1253 while let Some(pc) = pi.next() {
1254 match pc {
1255 '*' => {
1256 if pi.peek().is_none() {
1258 return true; }
1260 let rest: String = pi.collect();
1262 let mut pos = 0;
1263 for (i, _) in text
1264 .char_indices()
1265 .skip(ti.clone().count().saturating_sub(text.len()))
1266 {
1267 if i >= pos {
1268 if glob_match_impl(&rest, &text[i..], extended) {
1269 return true;
1270 }
1271 pos = i + 1;
1272 }
1273 }
1274 return glob_match_impl(&rest, "", extended);
1276 }
1277 '?' => {
1278 if ti.next().is_none() {
1279 return false;
1280 }
1281 }
1282 '[' => {
1283 let tc = match ti.next() {
1284 Some(c) => c,
1285 None => return false,
1286 };
1287 if !match_bracket_expr(&mut pi, tc) {
1288 return false;
1289 }
1290 }
1291 '#' if extended => {
1292 continue;
1294 }
1295 '^' if extended => {
1296 continue;
1298 }
1299 '~' if extended => {
1300 continue;
1302 }
1303 '\\' => {
1304 let escaped = pi.next();
1305 let tc = ti.next();
1306 if escaped != tc {
1307 return false;
1308 }
1309 }
1310 _ => {
1311 if ti.next() != Some(pc) {
1312 return false;
1313 }
1314 }
1315 }
1316 }
1317
1318 ti.peek().is_none()
1319}
1320
1321fn match_bracket_expr(pi: &mut std::iter::Peekable<std::str::Chars>, tc: char) -> bool {
1322 let mut chars_in_class = Vec::new();
1323 let mut negate = false;
1324 let mut first = true;
1325
1326 while let Some(c) = pi.next() {
1327 if first && (c == '!' || c == '^') {
1328 negate = true;
1329 first = false;
1330 continue;
1331 }
1332 first = false;
1333
1334 if c == ']' && !chars_in_class.is_empty() {
1335 break;
1336 }
1337
1338 if pi.peek() == Some(&'-') {
1339 pi.next();
1340 if let Some(&end) = pi.peek() {
1341 if end != ']' {
1342 pi.next();
1343 for ch in c..=end {
1344 chars_in_class.push(ch);
1345 }
1346 continue;
1347 }
1348 }
1349 chars_in_class.push(c);
1351 chars_in_class.push('-');
1352 continue;
1353 }
1354
1355 chars_in_class.push(c);
1356 }
1357
1358 let matched = chars_in_class.contains(&tc);
1359 if negate {
1360 !matched
1361 } else {
1362 matched
1363 }
1364}
1365
1366pub fn file_type_char(mode: u32) -> char {
1368 let fmt = mode & libc::S_IFMT as u32;
1369 if fmt == libc::S_IFBLK as u32 {
1370 '#'
1371 } else if fmt == libc::S_IFCHR as u32 {
1372 '%'
1373 } else if fmt == libc::S_IFDIR as u32 {
1374 '/'
1375 } else if fmt == libc::S_IFIFO as u32 {
1376 '|'
1377 } else if fmt == libc::S_IFLNK as u32 {
1378 '@'
1379 } else if fmt == libc::S_IFREG as u32 {
1380 if mode & 0o111 != 0 {
1381 '*'
1382 } else {
1383 ' '
1384 }
1385 } else if fmt == libc::S_IFSOCK as u32 {
1386 '='
1387 } else {
1388 '?'
1389 }
1390}
1391
1392fn scale_size(bytes: u64, unit: SizeUnit) -> u64 {
1393 match unit {
1394 SizeUnit::Bytes => bytes,
1395 SizeUnit::PosixBlocks => (bytes + 511) / 512,
1396 SizeUnit::Kilobytes => (bytes + 1023) / 1024,
1397 SizeUnit::Megabytes => (bytes + 1048575) / 1048576,
1398 SizeUnit::Gigabytes => (bytes + 1073741823) / 1073741824,
1399 SizeUnit::Terabytes => (bytes + 1099511627775) / 1099511627776,
1400 }
1401}
1402
1403fn scale_time(secs: i64, unit: TimeUnit) -> i64 {
1404 match unit {
1405 TimeUnit::Seconds => secs,
1406 TimeUnit::Minutes => secs / 60,
1407 TimeUnit::Hours => secs / 3600,
1408 TimeUnit::Days => secs / 86400,
1409 TimeUnit::Weeks => secs / 604800,
1410 TimeUnit::Months => secs / 2592000,
1411 }
1412}
1413
1414fn compare_range(value: u64, target: u64, op: RangeOp) -> bool {
1415 match op {
1416 RangeOp::Less => value < target,
1417 RangeOp::Equal => value == target,
1418 RangeOp::Greater => value > target,
1419 }
1420}
1421
1422pub fn has_braces(s: &str, brace_ccl: bool) -> bool {
1428 let mut depth = 0;
1429 let mut has_comma = false;
1430 let mut has_dotdot = false;
1431
1432 let chars: Vec<char> = s.chars().collect();
1433 let len = chars.len();
1434
1435 for i in 0..len {
1436 match chars[i] {
1437 '{' => {
1438 if brace_ccl && depth == 0 {
1439 if i + 2 < len && chars[i + 2] == '}' {
1441 return true;
1442 }
1443 }
1444 depth += 1;
1445 }
1446 '}' => {
1447 if depth > 0 {
1448 depth -= 1;
1449 if depth == 0 && (has_comma || has_dotdot) {
1450 return true;
1451 }
1452 }
1453 }
1454 ',' if depth == 1 => has_comma = true,
1455 '.' if depth == 1 && i + 1 < len && chars[i + 1] == '.' => has_dotdot = true,
1456 _ => {}
1457 }
1458 }
1459
1460 false
1461}
1462
1463pub fn expand_braces(s: &str, brace_ccl: bool) -> Vec<String> {
1465 if !has_braces(s, brace_ccl) {
1466 return vec![s.to_string()];
1467 }
1468
1469 let mut results = vec![s.to_string()];
1470 let mut changed = true;
1471
1472 while changed {
1473 changed = false;
1474 let mut new_results = Vec::new();
1475
1476 for item in &results {
1477 if let Some(expanded) = expand_single_brace(item, brace_ccl) {
1478 new_results.extend(expanded);
1479 changed = true;
1480 } else {
1481 new_results.push(item.clone());
1482 }
1483 }
1484
1485 results = new_results;
1486 }
1487
1488 results
1489}
1490
1491fn expand_single_brace(s: &str, brace_ccl: bool) -> Option<Vec<String>> {
1492 let chars: Vec<char> = s.chars().collect();
1493 let len = chars.len();
1494
1495 let mut brace_start = None;
1497 for i in 0..len {
1498 if chars[i] == '{' {
1499 brace_start = Some(i);
1500 break;
1501 }
1502 }
1503
1504 let start = brace_start?;
1505
1506 let mut depth = 1;
1508 let mut comma_positions = Vec::new();
1509 let mut dotdot_pos = None;
1510
1511 for i in (start + 1)..len {
1512 match chars[i] {
1513 '{' => depth += 1,
1514 '}' => {
1515 depth -= 1;
1516 if depth == 0 {
1517 let prefix: String = chars[..start].iter().collect();
1518 let suffix: String = chars[i + 1..].iter().collect();
1519 let content: String = chars[start + 1..i].iter().collect();
1520
1521 if let Some(dp) = dotdot_pos {
1523 if comma_positions.is_empty() {
1524 return expand_range(&prefix, &content, dp, &suffix);
1525 }
1526 }
1527
1528 if !comma_positions.is_empty() {
1530 return expand_comma(&prefix, &content, &comma_positions, &suffix);
1531 }
1532
1533 if brace_ccl && content.len() > 0 {
1535 return expand_ccl(&prefix, &content, &suffix);
1536 }
1537
1538 return None;
1539 }
1540 }
1541 ',' if depth == 1 => comma_positions.push(i - start - 1),
1542 '.' if depth == 1 && i + 1 < len && chars[i + 1] == '.' => {
1543 if dotdot_pos.is_none() {
1544 dotdot_pos = Some(i - start - 1);
1545 }
1546 }
1547 _ => {}
1548 }
1549 }
1550
1551 None
1552}
1553
1554fn expand_range(
1555 prefix: &str,
1556 content: &str,
1557 dotdot_pos: usize,
1558 suffix: &str,
1559) -> Option<Vec<String>> {
1560 let left = &content[..dotdot_pos];
1561 let right_start = dotdot_pos + 2;
1562
1563 let (right, incr) = if let Some(pos) = content[right_start..].find("..") {
1565 let r = &content[right_start..right_start + pos];
1566 let i: i64 = content[right_start + pos + 2..].parse().unwrap_or(1);
1567 (r, i.abs() as u64)
1568 } else {
1569 (&content[right_start..], 1u64)
1570 };
1571
1572 if let (Ok(start), Ok(end)) = (left.parse::<i64>(), right.parse::<i64>()) {
1574 let mut results = Vec::new();
1575 let (start, end, reverse) = if start <= end {
1576 (start, end, false)
1577 } else {
1578 (end, start, true)
1579 };
1580
1581 let width = left.len().max(right.len());
1583 let pad = left.starts_with('0') || right.starts_with('0');
1584
1585 let mut vals: Vec<i64> = (start..=end).step_by(incr as usize).collect();
1586 if reverse {
1587 vals.reverse();
1588 }
1589
1590 for v in vals {
1591 let s = if pad {
1592 format!("{}{:0>width$}{}", prefix, v, suffix, width = width)
1593 } else {
1594 format!("{}{}{}", prefix, v, suffix)
1595 };
1596 results.push(s);
1597 }
1598 return Some(results);
1599 }
1600
1601 if left.len() == 1 && right.len() == 1 {
1603 let start = left.chars().next()?;
1604 let end = right.chars().next()?;
1605 let (start, end, reverse) = if start <= end {
1606 (start, end, false)
1607 } else {
1608 (end, start, true)
1609 };
1610
1611 let mut results = Vec::new();
1612 let mut chars: Vec<char> = (start..=end).collect();
1613 if reverse {
1614 chars.reverse();
1615 }
1616
1617 for c in chars {
1618 results.push(format!("{}{}{}", prefix, c, suffix));
1619 }
1620 return Some(results);
1621 }
1622
1623 None
1624}
1625
1626fn expand_comma(
1627 prefix: &str,
1628 content: &str,
1629 positions: &[usize],
1630 suffix: &str,
1631) -> Option<Vec<String>> {
1632 let mut results = Vec::new();
1633 let mut last = 0;
1634
1635 for &pos in positions {
1636 let part = &content[last..pos];
1637 results.push(format!("{}{}{}", prefix, part, suffix));
1638 last = pos + 1;
1639 }
1640 results.push(format!("{}{}{}", prefix, &content[last..], suffix));
1641
1642 Some(results)
1643}
1644
1645fn expand_ccl(prefix: &str, content: &str, suffix: &str) -> Option<Vec<String>> {
1646 let mut chars_set = HashSet::new();
1647 let chars: Vec<char> = content.chars().collect();
1648 let mut i = 0;
1649
1650 while i < chars.len() {
1651 if i + 2 < chars.len() && chars[i + 1] == '-' {
1652 let start = chars[i];
1653 let end = chars[i + 2];
1654 for c in start..=end {
1655 chars_set.insert(c);
1656 }
1657 i += 3;
1658 } else {
1659 chars_set.insert(chars[i]);
1660 i += 1;
1661 }
1662 }
1663
1664 let mut results: Vec<String> = chars_set
1665 .iter()
1666 .map(|c| format!("{}{}{}", prefix, c, suffix))
1667 .collect();
1668 results.sort();
1669 Some(results)
1670}
1671
1672pub fn glob(pattern: &str) -> Vec<String> {
1678 let mut state = GlobState::new(GlobOptions {
1679 null_glob: false,
1680 mark_dirs: false,
1681 no_glob_dots: true,
1682 list_types: false,
1683 numeric_sort: false,
1684 follow_links: false,
1685 extended_glob: true,
1686 case_glob: true,
1687 glob_star_short: false,
1688 bare_glob_qual: true,
1689 brace_ccl: false,
1690 });
1691 state.glob(pattern)
1692}
1693
1694pub fn glob_with_options(pattern: &str, options: GlobOptions) -> Vec<String> {
1696 let mut state = GlobState::new(options);
1697 state.glob(pattern)
1698}
1699
1700pub fn addpath(buf: &mut String, component: &str) {
1702 buf.push_str(component);
1703 if !buf.ends_with('/') {
1704 buf.push('/');
1705 }
1706}
1707
1708pub fn statfullpath(pathbuf: &str, name: &str, follow: bool) -> Option<std::fs::Metadata> {
1710 let full = if name.is_empty() {
1711 if pathbuf.is_empty() {
1712 ".".to_string()
1713 } else {
1714 pathbuf.to_string()
1715 }
1716 } else {
1717 format!("{}{}", pathbuf, name)
1718 };
1719
1720 if follow {
1721 std::fs::metadata(&full).ok()
1722 } else {
1723 std::fs::symlink_metadata(&full).ok()
1724 }
1725}
1726
1727pub fn is_directory(path: &str) -> bool {
1729 std::fs::metadata(path).map(|m| m.is_dir()).unwrap_or(false)
1730}
1731
1732pub fn is_symlink(path: &str) -> bool {
1734 std::fs::symlink_metadata(path)
1735 .map(|m| m.file_type().is_symlink())
1736 .unwrap_or(false)
1737}
1738
1739pub fn mindist(dir: &str, name: &str, best: &mut String, exact: bool) -> usize {
1741 let Ok(entries) = std::fs::read_dir(dir) else {
1742 return usize::MAX;
1743 };
1744
1745 let mut min_dist = usize::MAX;
1746
1747 for entry in entries.flatten() {
1748 let entry_name = entry.file_name().to_string_lossy().to_string();
1749 if exact && entry_name == name {
1750 *best = entry_name;
1751 return 0;
1752 }
1753
1754 let dist = crate::utils::spdist(name, &entry_name, min_dist);
1755 if dist < min_dist {
1756 min_dist = dist;
1757 *best = entry_name.clone();
1758 }
1759 }
1760
1761 min_dist
1762}
1763
1764pub fn qgetnum(s: &str) -> Option<(i64, &str)> {
1766 let end = s.find(|c: char| !c.is_ascii_digit()).unwrap_or(s.len());
1767 if end == 0 {
1768 return None;
1769 }
1770 let num = s[..end].parse::<i64>().ok()?;
1771 Some((num, &s[end..]))
1772}
1773
1774pub fn qualtime(s: &str, units: char) -> Option<(i64, &str)> {
1776 let (mut num, rest) = qgetnum(s)?;
1777
1778 match units {
1779 'h' => num *= 3600,
1780 'd' => num *= 86400,
1781 'w' => num *= 604800,
1782 'M' => num *= 2592000,
1783 _ => {}
1784 }
1785
1786 Some((num, rest))
1787}
1788
1789pub fn qualsize(s: &str, units: char) -> Option<(i64, &str)> {
1791 let (mut num, rest) = qgetnum(s)?;
1792
1793 match units {
1794 'k' | 'K' => num *= 1024,
1795 'm' | 'M' => num *= 1024 * 1024,
1796 'g' | 'G' => num *= 1024 * 1024 * 1024,
1797 't' | 'T' => num *= 1024 * 1024 * 1024 * 1024,
1798 'p' | 'P' => num *= 512,
1799 _ => {}
1800 }
1801
1802 Some((num, rest))
1803}
1804
1805pub fn sort_matches_by_type(matches: &mut [String], sort_type: GlobSort, reverse: bool) {
1807 match sort_type {
1808 GlobSort::Name => {
1809 matches.sort();
1810 }
1811 GlobSort::Size => {
1812 matches.sort_by(|a, b| {
1813 let size_a = std::fs::metadata(a).map(|m| m.len()).unwrap_or(0);
1814 let size_b = std::fs::metadata(b).map(|m| m.len()).unwrap_or(0);
1815 size_a.cmp(&size_b)
1816 });
1817 }
1818 GlobSort::Mtime => {
1819 matches.sort_by(|a, b| {
1820 let time_a = std::fs::metadata(a).and_then(|m| m.modified()).ok();
1821 let time_b = std::fs::metadata(b).and_then(|m| m.modified()).ok();
1822 time_a.cmp(&time_b)
1823 });
1824 }
1825 GlobSort::Atime => {
1826 matches.sort_by(|a, b| {
1827 let time_a = std::fs::metadata(a).and_then(|m| m.accessed()).ok();
1828 let time_b = std::fs::metadata(b).and_then(|m| m.accessed()).ok();
1829 time_a.cmp(&time_b)
1830 });
1831 }
1832 GlobSort::Depth => {
1833 matches.sort_by(|a, b| {
1834 let depth_a = a.matches('/').count();
1835 let depth_b = b.matches('/').count();
1836 depth_a.cmp(&depth_b)
1837 });
1838 }
1839 GlobSort::Links => {
1840 matches.sort_by(|a, b| {
1841 let links_a = std::fs::metadata(a).map(|m| m.nlink()).unwrap_or(0);
1842 let links_b = std::fs::metadata(b).map(|m| m.nlink()).unwrap_or(0);
1843 links_a.cmp(&links_b)
1844 });
1845 }
1846 _ => {}
1847 }
1848
1849 if reverse {
1850 matches.reverse();
1851 }
1852}
1853
1854pub mod qualifiers {
1856 use std::os::unix::fs::MetadataExt;
1857 use std::os::unix::fs::PermissionsExt;
1858
1859 pub fn is_regular(path: &str) -> bool {
1860 std::fs::metadata(path)
1861 .map(|m| m.is_file())
1862 .unwrap_or(false)
1863 }
1864
1865 pub fn is_directory(path: &str) -> bool {
1866 std::fs::metadata(path).map(|m| m.is_dir()).unwrap_or(false)
1867 }
1868
1869 pub fn is_symlink(path: &str) -> bool {
1870 std::fs::symlink_metadata(path)
1871 .map(|m| m.file_type().is_symlink())
1872 .unwrap_or(false)
1873 }
1874
1875 pub fn is_fifo(path: &str) -> bool {
1876 std::fs::metadata(path)
1877 .map(|m| (m.mode() & libc::S_IFMT as u32) == libc::S_IFIFO as u32)
1878 .unwrap_or(false)
1879 }
1880
1881 pub fn is_socket(path: &str) -> bool {
1882 std::fs::metadata(path)
1883 .map(|m| (m.mode() & libc::S_IFMT as u32) == libc::S_IFSOCK as u32)
1884 .unwrap_or(false)
1885 }
1886
1887 pub fn is_block_device(path: &str) -> bool {
1888 std::fs::metadata(path)
1889 .map(|m| (m.mode() & libc::S_IFMT as u32) == libc::S_IFBLK as u32)
1890 .unwrap_or(false)
1891 }
1892
1893 pub fn is_char_device(path: &str) -> bool {
1894 std::fs::metadata(path)
1895 .map(|m| (m.mode() & libc::S_IFMT as u32) == libc::S_IFCHR as u32)
1896 .unwrap_or(false)
1897 }
1898
1899 pub fn is_setuid(path: &str) -> bool {
1900 std::fs::metadata(path)
1901 .map(|m| (m.mode() & libc::S_ISUID as u32) != 0)
1902 .unwrap_or(false)
1903 }
1904
1905 pub fn is_setgid(path: &str) -> bool {
1906 std::fs::metadata(path)
1907 .map(|m| (m.mode() & libc::S_ISGID as u32) != 0)
1908 .unwrap_or(false)
1909 }
1910
1911 pub fn is_sticky(path: &str) -> bool {
1912 std::fs::metadata(path)
1913 .map(|m| (m.mode() & libc::S_ISVTX as u32) != 0)
1914 .unwrap_or(false)
1915 }
1916
1917 pub fn is_readable(path: &str) -> bool {
1918 std::fs::metadata(path).is_ok() && std::fs::File::open(path).is_ok()
1919 }
1920
1921 pub fn is_writable(path: &str) -> bool {
1922 std::fs::OpenOptions::new().write(true).open(path).is_ok()
1923 }
1924
1925 pub fn is_executable(path: &str) -> bool {
1926 std::fs::metadata(path)
1927 .map(|m| (m.mode() & 0o111) != 0)
1928 .unwrap_or(false)
1929 }
1930
1931 pub fn size_matches(path: &str, size: u64, cmp: std::cmp::Ordering) -> bool {
1932 std::fs::metadata(path)
1933 .map(|m| m.len().cmp(&size) == cmp)
1934 .unwrap_or(false)
1935 }
1936
1937 pub fn mtime_matches(path: &str, secs: i64, cmp: std::cmp::Ordering) -> bool {
1938 std::fs::metadata(path)
1939 .and_then(|m| m.modified())
1940 .map(|t| {
1941 let elapsed = t.elapsed().map(|d| d.as_secs() as i64).unwrap_or(0);
1942 elapsed.cmp(&secs) == cmp
1943 })
1944 .unwrap_or(false)
1945 }
1946
1947 pub fn uid_matches(path: &str, uid: u32) -> bool {
1948 std::fs::metadata(path)
1949 .map(|m| m.uid() == uid)
1950 .unwrap_or(false)
1951 }
1952
1953 pub fn gid_matches(path: &str, gid: u32) -> bool {
1954 std::fs::metadata(path)
1955 .map(|m| m.gid() == gid)
1956 .unwrap_or(false)
1957 }
1958
1959 pub fn nlinks_matches(path: &str, nlinks: u64, cmp: std::cmp::Ordering) -> bool {
1960 std::fs::metadata(path)
1961 .map(|m| m.nlink().cmp(&nlinks) == cmp)
1962 .unwrap_or(false)
1963 }
1964
1965 pub fn is_command(path: &str) -> bool {
1967 let meta = match std::fs::metadata(path) {
1968 Ok(m) => m,
1969 Err(_) => return false,
1970 };
1971
1972 if !meta.is_file() {
1973 return false;
1974 }
1975
1976 let mode = meta.mode();
1978 if mode & 0o111 == 0 {
1979 return false;
1980 }
1981
1982 true
1985 }
1986}
1987
1988#[derive(Debug, Clone, Copy)]
1994pub struct MatchFlags {
1995 pub anchored_start: bool,
1997 pub anchored_end: bool,
1999 pub shortest: bool,
2001 pub subexpr: bool,
2003}
2004
2005impl Default for MatchFlags {
2006 fn default() -> Self {
2007 MatchFlags {
2008 anchored_start: false,
2009 anchored_end: false,
2010 shortest: false,
2011 subexpr: false,
2012 }
2013 }
2014}
2015
2016#[derive(Debug, Clone)]
2018pub struct MatchData {
2019 pub str: String,
2020 pub pattern: String,
2021 pub match_start: usize,
2022 pub match_end: usize,
2023 pub replacement: Option<String>,
2024}
2025
2026pub fn get_match_ret(data: &MatchData, start: usize, end: usize) -> String {
2028 if start >= end || start >= data.str.len() {
2029 return String::new();
2030 }
2031
2032 let end = end.min(data.str.len());
2033 data.str[start..end].to_string()
2034}
2035
2036pub fn compgetmatch(pat: &str) -> Option<(String, MatchFlags)> {
2038 let mut flags = MatchFlags::default();
2039 let mut pattern = pat.to_string();
2040
2041 if pattern.starts_with('#') {
2043 flags.anchored_start = true;
2044 pattern = pattern[1..].to_string();
2045 }
2046 if pattern.starts_with("##") {
2047 flags.anchored_start = true;
2048 flags.shortest = false;
2049 pattern = pattern[2..].to_string();
2050 }
2051 if pattern.ends_with('%') {
2052 flags.anchored_end = true;
2053 pattern.pop();
2054 }
2055 if pattern.ends_with("%%") {
2056 flags.anchored_end = true;
2057 flags.shortest = false;
2058 pattern.truncate(pattern.len().saturating_sub(2));
2059 }
2060
2061 Some((pattern, flags))
2062}
2063
2064pub fn getmatch(s: &str, pat: &str, flags: MatchFlags, n: i32, replstr: Option<&str>) -> String {
2069 let chars: Vec<char> = s.chars().collect();
2070 let len = chars.len();
2071
2072 if len == 0 {
2073 return s.to_string();
2074 }
2075
2076 let (match_start, match_end) = if flags.anchored_start && flags.anchored_end {
2078 if pattern_match(pat, s, true, true) {
2080 (0, len)
2081 } else {
2082 return s.to_string();
2083 }
2084 } else if flags.anchored_start {
2085 let mut best_end = 0;
2087 for end in 1..=len {
2088 let substr: String = chars[..end].iter().collect();
2089 if pattern_match(pat, &substr, true, true) {
2090 if flags.shortest {
2091 return match replstr {
2092 Some(r) => format!("{}{}", r, chars[end..].iter().collect::<String>()),
2093 None => chars[end..].iter().collect(),
2094 };
2095 }
2096 best_end = end;
2097 }
2098 }
2099 if best_end > 0 {
2100 (0, best_end)
2101 } else {
2102 return s.to_string();
2103 }
2104 } else if flags.anchored_end {
2105 let mut best_start = len;
2107 for start in (0..len).rev() {
2108 let substr: String = chars[start..].iter().collect();
2109 if pattern_match(pat, &substr, true, true) {
2110 if flags.shortest {
2111 return match replstr {
2112 Some(r) => format!("{}{}", chars[..start].iter().collect::<String>(), r),
2113 None => chars[..start].iter().collect(),
2114 };
2115 }
2116 best_start = start;
2117 }
2118 }
2119 if best_start < len {
2120 (best_start, len)
2121 } else {
2122 return s.to_string();
2123 }
2124 } else {
2125 for start in 0..len {
2127 for end in (start + 1)..=len {
2128 let substr: String = chars[start..end].iter().collect();
2129 if pattern_match(pat, &substr, true, true) {
2130 let prefix: String = chars[..start].iter().collect();
2131 let suffix: String = chars[end..].iter().collect();
2132 return match replstr {
2133 Some(r) => format!("{}{}{}", prefix, r, suffix),
2134 None => format!("{}{}", prefix, suffix),
2135 };
2136 }
2137 }
2138 }
2139 return s.to_string();
2140 };
2141
2142 let prefix: String = chars[..match_start].iter().collect();
2144 let suffix: String = chars[match_end..].iter().collect();
2145
2146 match replstr {
2147 Some(r) => format!("{}{}{}", prefix, r, suffix),
2148 None => format!("{}{}", prefix, suffix),
2149 }
2150}
2151
2152pub fn getmatcharr(
2154 arr: &[String],
2155 pat: &str,
2156 flags: MatchFlags,
2157 n: i32,
2158 replstr: Option<&str>,
2159) -> Vec<String> {
2160 arr.iter()
2161 .map(|s| getmatch(s, pat, flags, n, replstr))
2162 .collect()
2163}
2164
2165pub fn getmatchlist(s: &str, pat: &str) -> Vec<(usize, usize)> {
2167 let mut matches = Vec::new();
2168 let chars: Vec<char> = s.chars().collect();
2169 let len = chars.len();
2170
2171 let mut pos = 0;
2172 while pos < len {
2173 for end in (pos + 1)..=len {
2174 let substr: String = chars[pos..end].iter().collect();
2175 if pattern_match(pat, &substr, true, true) {
2176 matches.push((pos, end));
2177 pos = end;
2178 break;
2179 }
2180 }
2181 if matches.last().map(|&(_, e)| e) != Some(pos) {
2182 pos += 1;
2183 }
2184 }
2185
2186 matches
2187}
2188
2189pub fn set_pat_start(pattern: &str, offset: usize) -> String {
2191 if offset == 0 || offset >= pattern.len() {
2192 return pattern.to_string();
2193 }
2194 pattern[offset..].to_string()
2195}
2196
2197pub fn set_pat_end(pattern: &str, end: usize) -> String {
2199 if end >= pattern.len() {
2200 return pattern.to_string();
2201 }
2202 pattern[..end].to_string()
2203}
2204
2205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2211pub enum GlobToken {
2212 Literal(char),
2213 Star, Question, BracketOpen, BracketClose, ParenOpen, ParenClose, Pipe, Hash, Tilde, Caret, BraceOpen, BraceClose, Comma, Range, }
2228
2229pub fn tokenize(s: &str) -> Vec<GlobToken> {
2231 let mut tokens = Vec::new();
2232 let mut chars = s.chars().peekable();
2233
2234 while let Some(c) = chars.next() {
2235 let token = match c {
2236 '\\' => {
2237 if let Some(next) = chars.next() {
2239 GlobToken::Literal(next)
2240 } else {
2241 GlobToken::Literal('\\')
2242 }
2243 }
2244 '*' => GlobToken::Star,
2245 '?' => GlobToken::Question,
2246 '[' => GlobToken::BracketOpen,
2247 ']' => GlobToken::BracketClose,
2248 '(' => GlobToken::ParenOpen,
2249 ')' => GlobToken::ParenClose,
2250 '|' => GlobToken::Pipe,
2251 '#' => GlobToken::Hash,
2252 '~' => GlobToken::Tilde,
2253 '^' => GlobToken::Caret,
2254 '{' => GlobToken::BraceOpen,
2255 '}' => GlobToken::BraceClose,
2256 ',' => GlobToken::Comma,
2257 '.' if chars.peek() == Some(&'.') => {
2258 chars.next();
2259 GlobToken::Range
2260 }
2261 _ => GlobToken::Literal(c),
2262 };
2263 tokens.push(token);
2264 }
2265
2266 tokens
2267}
2268
2269pub fn shtokenize(s: &str) -> Vec<GlobToken> {
2272 let mut tokens = Vec::new();
2273 let mut chars = s.chars().peekable();
2274 let mut in_single_quote = false;
2275 let mut in_double_quote = false;
2276
2277 while let Some(c) = chars.next() {
2278 if in_single_quote {
2279 if c == '\'' {
2280 in_single_quote = false;
2281 } else {
2282 tokens.push(GlobToken::Literal(c));
2283 }
2284 continue;
2285 }
2286
2287 if in_double_quote {
2288 if c == '"' {
2289 in_double_quote = false;
2290 } else if c == '\\' {
2291 if let Some(next) = chars.next() {
2292 tokens.push(GlobToken::Literal(next));
2293 }
2294 } else {
2295 tokens.push(GlobToken::Literal(c));
2296 }
2297 continue;
2298 }
2299
2300 match c {
2301 '\'' => in_single_quote = true,
2302 '"' => in_double_quote = true,
2303 '\\' => {
2304 if let Some(next) = chars.next() {
2305 tokens.push(GlobToken::Literal(next));
2306 }
2307 }
2308 '*' => tokens.push(GlobToken::Star),
2309 '?' => tokens.push(GlobToken::Question),
2310 '[' => tokens.push(GlobToken::BracketOpen),
2311 ']' => tokens.push(GlobToken::BracketClose),
2312 _ => tokens.push(GlobToken::Literal(c)),
2313 }
2314 }
2315
2316 tokens
2317}
2318
2319pub fn zshtokenize(s: &str, extended_glob: bool, sh_glob: bool) -> Vec<GlobToken> {
2321 let mut tokens = Vec::new();
2322 let mut chars = s.chars().peekable();
2323
2324 while let Some(c) = chars.next() {
2325 let token = match c {
2326 '\\' => {
2327 if let Some(next) = chars.next() {
2328 GlobToken::Literal(next)
2329 } else {
2330 GlobToken::Literal('\\')
2331 }
2332 }
2333 '*' => GlobToken::Star,
2334 '?' => GlobToken::Question,
2335 '[' => GlobToken::BracketOpen,
2336 ']' => GlobToken::BracketClose,
2337 '#' if extended_glob => GlobToken::Hash,
2338 '^' if extended_glob => GlobToken::Caret,
2339 '~' if extended_glob => GlobToken::Tilde,
2340 '(' if extended_glob => GlobToken::ParenOpen,
2341 ')' if extended_glob => GlobToken::ParenClose,
2342 '|' if extended_glob => GlobToken::Pipe,
2343 '{' if !sh_glob => GlobToken::BraceOpen,
2344 '}' if !sh_glob => GlobToken::BraceClose,
2345 ',' if !sh_glob => GlobToken::Comma,
2346 _ => GlobToken::Literal(c),
2347 };
2348 tokens.push(token);
2349 }
2350
2351 tokens
2352}
2353
2354pub fn remnulargs(tokens: &mut Vec<GlobToken>) {
2356 tokens.retain(|t| {
2357 if let GlobToken::Literal(c) = t {
2358 *c != '\0'
2359 } else {
2360 true
2361 }
2362 });
2363}
2364
2365#[derive(Debug, Clone, Copy, Default)]
2371pub struct ModeSpec {
2372 pub who: u32, pub op: char, pub perm: u32, }
2376
2377pub fn qgetmodespec(s: &str) -> Option<(ModeSpec, &str)> {
2380 let mut chars = s.chars().peekable();
2381 let mut spec = ModeSpec::default();
2382
2383 if chars.peek().map(|c| c.is_ascii_digit()).unwrap_or(false) {
2385 let mut mode_str = String::new();
2386 while let Some(&c) = chars.peek() {
2387 if c.is_ascii_digit() && c < '8' {
2388 mode_str.push(c);
2389 chars.next();
2390 } else {
2391 break;
2392 }
2393 }
2394 if let Ok(mode) = u32::from_str_radix(&mode_str, 8) {
2395 spec.perm = mode;
2396 spec.op = '=';
2397 spec.who = 0o7777;
2398 let rest_pos = s.len() - chars.collect::<String>().len();
2399 return Some((spec, &s[rest_pos..]));
2400 }
2401 return None;
2402 }
2403
2404 let mut who = 0u32;
2407 while let Some(&c) = chars.peek() {
2408 match c {
2409 'u' => {
2410 who |= 0o4700;
2411 chars.next();
2412 }
2413 'g' => {
2414 who |= 0o2070;
2415 chars.next();
2416 }
2417 'o' => {
2418 who |= 0o1007;
2419 chars.next();
2420 }
2421 'a' => {
2422 who |= 0o7777;
2423 chars.next();
2424 }
2425 _ => break,
2426 }
2427 }
2428 if who == 0 {
2429 who = 0o7777; }
2431 spec.who = who;
2432
2433 spec.op = match chars.next() {
2435 Some('+') => '+',
2436 Some('-') => '-',
2437 Some('=') => '=',
2438 _ => return None,
2439 };
2440
2441 let mut perm = 0u32;
2443 while let Some(&c) = chars.peek() {
2444 match c {
2445 'r' => {
2446 perm |= 0o444;
2447 chars.next();
2448 }
2449 'w' => {
2450 perm |= 0o222;
2451 chars.next();
2452 }
2453 'x' => {
2454 perm |= 0o111;
2455 chars.next();
2456 }
2457 'X' => {
2458 perm |= 0o111;
2459 chars.next();
2460 } 's' => {
2462 perm |= 0o6000;
2463 chars.next();
2464 }
2465 't' => {
2466 perm |= 0o1000;
2467 chars.next();
2468 }
2469 _ => break,
2470 }
2471 }
2472 spec.perm = perm & who;
2473
2474 let rest_pos = s.len() - chars.collect::<String>().len();
2475 Some((spec, &s[rest_pos..]))
2476}
2477
2478pub fn apply_modespec(mode: u32, spec: &ModeSpec) -> u32 {
2480 match spec.op {
2481 '+' => mode | spec.perm,
2482 '-' => mode & !spec.perm,
2483 '=' => (mode & !spec.who) | spec.perm,
2484 _ => mode,
2485 }
2486}
2487
2488pub fn bracechardots(s: &str) -> Option<(char, char, i32)> {
2494 let chars: Vec<char> = s.chars().collect();
2495
2496 if chars.len() < 4 {
2498 return None;
2499 }
2500
2501 let dotdot_pos = s.find("..")?;
2503 if dotdot_pos == 0 {
2504 return None;
2505 }
2506
2507 let left = &s[..dotdot_pos];
2508 let right = &s[dotdot_pos + 2..];
2509
2510 let (end_str, incr) = if let Some(pos) = right.find("..") {
2512 let end = &right[..pos];
2513 let inc: i32 = right[pos + 2..].parse().unwrap_or(1);
2514 (end, inc)
2515 } else {
2516 (right, 1)
2517 };
2518
2519 if left.chars().count() == 1 && end_str.chars().count() == 1 {
2521 let c1 = left.chars().next()?;
2522 let c2 = end_str.chars().next()?;
2523 return Some((c1, c2, incr));
2524 }
2525
2526 None
2527}
2528
2529#[derive(Debug, Clone)]
2535pub struct Redirect {
2536 pub fd: i32,
2537 pub target: String,
2538 pub rtype: RedirectType,
2539}
2540
2541#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2542pub enum RedirectType {
2543 Read, Write, Append, ReadWrite, Clobber, Here, HereStr, Dup, Pipe, }
2553
2554pub fn xpandredir(redir: &Redirect, options: &GlobOptions) -> Vec<Redirect> {
2556 if !has_wildcards(&redir.target) {
2558 return vec![redir.clone()];
2559 }
2560
2561 let mut state = GlobState::new(options.clone());
2563 let matches = state.glob(&redir.target);
2564
2565 if matches.is_empty() {
2566 return vec![redir.clone()];
2567 }
2568
2569 if matches.len() > 1 {
2571 return vec![redir.clone()];
2573 }
2574
2575 vec![Redirect {
2576 fd: redir.fd,
2577 target: matches[0].clone(),
2578 rtype: redir.rtype,
2579 }]
2580}
2581
2582pub fn glob_exec_string(cmd: &str, filename: &str) -> Option<String> {
2589 use std::process::Command;
2590
2591 let cmd = cmd.replace("$REPLY", filename).replace("{}", filename);
2593
2594 let output = Command::new("sh").arg("-c").arg(&cmd).output().ok()?;
2595
2596 if output.status.success() {
2597 Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
2598 } else {
2599 None
2600 }
2601}
2602
2603pub fn qualsheval(filename: &str, expr: &str) -> bool {
2605 use std::process::Command;
2606
2607 let script = format!("REPLY='{}'; {}", filename.replace("'", "'\\''"), expr);
2609
2610 Command::new("sh")
2611 .arg("-c")
2612 .arg(&script)
2613 .status()
2614 .map(|s| s.success())
2615 .unwrap_or(false)
2616}
2617
2618#[cfg(test)]
2619mod tests {
2620 use super::*;
2621 use std::fs::{self, File};
2622 use std::io::Write;
2623 use tempfile::TempDir;
2624
2625 fn setup_test_dir() -> TempDir {
2626 let dir = TempDir::new().unwrap();
2627 let base = dir.path();
2628
2629 File::create(base.join("file1.txt")).unwrap();
2631 File::create(base.join("file2.txt")).unwrap();
2632 File::create(base.join("file3.rs")).unwrap();
2633 File::create(base.join(".hidden")).unwrap();
2634
2635 fs::create_dir(base.join("subdir")).unwrap();
2637 File::create(base.join("subdir/nested.txt")).unwrap();
2638
2639 dir
2640 }
2641
2642 #[test]
2643 fn test_has_wildcards() {
2644 assert!(has_wildcards("*.txt"));
2645 assert!(has_wildcards("file?.txt"));
2646 assert!(has_wildcards("file[12].txt"));
2647 assert!(!has_wildcards("file.txt"));
2648 assert!(!has_wildcards("path/to/file.txt"));
2649 }
2650
2651 #[test]
2652 fn test_pattern_match() {
2653 assert!(pattern_match("*.txt", "file.txt", false, true));
2654 assert!(pattern_match("file?.txt", "file1.txt", false, true));
2655 assert!(!pattern_match("*.txt", "file.rs", false, true));
2656 assert!(pattern_match("file[12].txt", "file1.txt", false, true));
2657 assert!(!pattern_match("file[12].txt", "file3.txt", false, true));
2658 }
2659
2660 #[test]
2661 fn test_brace_expansion() {
2662 let result = expand_braces("{a,b,c}", false);
2663 assert_eq!(result, vec!["a", "b", "c"]);
2664
2665 let result = expand_braces("file{1,2,3}.txt", false);
2666 assert_eq!(result, vec!["file1.txt", "file2.txt", "file3.txt"]);
2667
2668 let result = expand_braces("{1..5}", false);
2669 assert_eq!(result, vec!["1", "2", "3", "4", "5"]);
2670
2671 let result = expand_braces("{a..e}", false);
2672 assert_eq!(result, vec!["a", "b", "c", "d", "e"]);
2673 }
2674
2675 #[test]
2676 fn test_glob_simple() {
2677 let dir = setup_test_dir();
2678 let pattern = format!("{}/*.txt", dir.path().display());
2679
2680 let mut state = GlobState::new(GlobOptions::default());
2681 let results = state.glob(&pattern);
2682
2683 assert_eq!(results.len(), 2);
2684 assert!(results.iter().any(|s| s.ends_with("file1.txt")));
2685 assert!(results.iter().any(|s| s.ends_with("file2.txt")));
2686 }
2687
2688 #[test]
2689 fn test_glob_hidden() {
2690 let dir = setup_test_dir();
2691 let pattern = format!("{}/*", dir.path().display());
2692
2693 let mut state = GlobState::new(GlobOptions {
2695 no_glob_dots: true,
2696 ..Default::default()
2697 });
2698 let results = state.glob(&pattern);
2699 assert!(!results.iter().any(|s| s.contains(".hidden")));
2700
2701 let mut state = GlobState::new(GlobOptions {
2703 no_glob_dots: false,
2704 ..Default::default()
2705 });
2706 let results = state.glob(&pattern);
2707 assert!(results.iter().any(|s| s.contains(".hidden")));
2708 }
2709
2710 #[test]
2711 fn test_file_type_char() {
2712 assert_eq!(file_type_char(libc::S_IFDIR as u32), '/');
2713 assert_eq!(file_type_char(libc::S_IFREG as u32), ' ');
2714 assert_eq!(file_type_char(libc::S_IFREG as u32 | 0o111), '*');
2715 assert_eq!(file_type_char(libc::S_IFLNK as u32), '@');
2716 }
2717
2718 #[test]
2719 fn test_numeric_string_cmp() {
2720 assert_eq!(numeric_string_cmp("file1", "file2"), Ordering::Less);
2721 assert_eq!(numeric_string_cmp("file10", "file2"), Ordering::Greater);
2722 assert_eq!(numeric_string_cmp("file10", "file10"), Ordering::Equal);
2723 }
2724}