1use std::collections::VecDeque;
4use std::path::Path;
5
6use crate::{error, regex, sys, trace_categories};
7
8#[derive(Clone, Debug)]
10pub(crate) enum PatternPiece {
11 Pattern(String),
13 Literal(String),
15}
16
17impl PatternPiece {
18 pub fn as_str(&self) -> &str {
19 match self {
20 Self::Pattern(s) => s,
21 Self::Literal(s) => s,
22 }
23 }
24}
25
26type PatternWord = Vec<PatternPiece>;
27
28#[derive(Clone, Debug, Default)]
30pub(crate) struct FilenameExpansionOptions {
31 pub require_dot_in_pattern_to_match_dot_files: bool,
32}
33
34#[derive(Debug, Default)]
37pub(crate) enum PatternExpansionResult {
38 #[default]
40 NoGlob,
41 Expanded(Vec<String>),
43}
44
45impl PatternExpansionResult {
46 pub fn into_paths(self) -> Vec<String> {
48 match self {
49 Self::NoGlob => vec![],
50 Self::Expanded(paths) => paths,
51 }
52 }
53
54 pub const fn is_unmatched_glob(&self) -> bool {
56 matches!(self, Self::Expanded(paths) if paths.is_empty())
57 }
58}
59
60#[derive(Clone, Debug)]
62pub struct Pattern {
63 pieces: PatternWord,
64 enable_extended_globbing: bool,
65 multiline: bool,
66 case_insensitive: bool,
67}
68
69impl Default for Pattern {
70 fn default() -> Self {
71 Self {
72 pieces: vec![],
73 enable_extended_globbing: false,
74 multiline: true,
75 case_insensitive: false,
76 }
77 }
78}
79
80impl From<PatternWord> for Pattern {
81 fn from(pieces: PatternWord) -> Self {
82 Self {
83 pieces,
84 ..Default::default()
85 }
86 }
87}
88
89impl From<&PatternWord> for Pattern {
90 fn from(value: &PatternWord) -> Self {
91 Self {
92 pieces: value.clone(),
93 ..Default::default()
94 }
95 }
96}
97
98impl From<&str> for Pattern {
99 fn from(value: &str) -> Self {
100 Self {
101 pieces: vec![PatternPiece::Pattern(value.to_owned())],
102 ..Default::default()
103 }
104 }
105}
106
107impl From<String> for Pattern {
108 fn from(value: String) -> Self {
109 Self {
110 pieces: vec![PatternPiece::Pattern(value)],
111 ..Default::default()
112 }
113 }
114}
115
116impl Pattern {
117 #[must_use]
123 pub const fn set_extended_globbing(mut self, value: bool) -> Self {
124 self.enable_extended_globbing = value;
125 self
126 }
127
128 #[must_use]
134 pub const fn set_multiline(mut self, value: bool) -> Self {
135 self.multiline = value;
136 self
137 }
138
139 #[must_use]
145 pub const fn set_case_insensitive(mut self, value: bool) -> Self {
146 self.case_insensitive = value;
147 self
148 }
149
150 pub fn is_empty(&self) -> bool {
152 self.pieces.iter().all(|p| p.as_str().is_empty())
153 }
154
155 pub(crate) const fn accept_all_expand_filter(_path: &Path) -> bool {
157 true
158 }
159
160 #[expect(clippy::too_many_lines)]
169 pub(crate) fn expand<PF>(
170 &self,
171 working_dir: &Path,
172 path_filter: Option<&PF>,
173 options: &FilenameExpansionOptions,
174 ) -> Result<PatternExpansionResult, error::Error>
175 where
176 PF: Fn(&Path) -> bool,
177 {
178 if self.pieces.is_empty() {
185 return Ok(PatternExpansionResult::NoGlob);
186
187 } else if !self.pieces.iter().any(|piece| {
191 matches!(piece, PatternPiece::Pattern(_))
192 && requires_expansion(piece.as_str(), self.enable_extended_globbing)
193 }) {
194 let concatenated: String = self.pieces.iter().map(|piece| piece.as_str()).collect();
195
196 if let Some(filter) = path_filter
197 && !filter(Path::new(&concatenated))
198 {
199 return Ok(PatternExpansionResult::NoGlob);
203 }
204
205 return Ok(PatternExpansionResult::Expanded(vec![concatenated]));
206 }
207
208 tracing::debug!(target: trace_categories::PATTERN, "expanding pattern: {self:?}");
209
210 let mut components: Vec<PatternWord> = vec![];
211 for piece in &self.pieces {
212 let mut split_result: VecDeque<_> = sys::fs::split_path_for_pattern(piece.as_str())
213 .map(|s| match piece {
214 PatternPiece::Pattern(_) => PatternPiece::Pattern(s.to_owned()),
215 PatternPiece::Literal(_) => PatternPiece::Literal(s.to_owned()),
216 })
217 .collect();
218
219 if let Some(first_piece) = split_result.pop_front() {
220 if let Some(last_component) = components.last_mut() {
221 last_component.push(first_piece);
222 } else {
223 components.push(vec![first_piece]);
224 }
225 }
226
227 while let Some(piece) = split_result.pop_front() {
228 components.push(vec![piece]);
229 }
230 }
231
232 let absolute_root = components.first().and_then(|first_component| {
238 let flattened: String = first_component.iter().map(|p| p.as_str()).collect();
239 sys::fs::pattern_path_root(&flattened)
240 });
241
242 let prefix_to_remove;
243 let mut paths_so_far = if let Some(root) = absolute_root {
244 prefix_to_remove = None;
245 components.remove(0);
247 vec![root]
248 } else {
249 let working_dir_str = working_dir.to_string_lossy();
256 let mut working_dir_str =
257 sys::fs::normalize_path_separators(&working_dir_str).into_owned();
258 if !working_dir_str.ends_with('/') {
259 working_dir_str.push('/');
260 }
261
262 prefix_to_remove = Some(working_dir_str);
263 vec![working_dir.to_path_buf()]
264 };
265
266 for component in components {
267 if !component.iter().any(|piece| {
268 matches!(piece, PatternPiece::Pattern(_))
269 && requires_expansion(piece.as_str(), self.enable_extended_globbing)
270 }) {
271 let flattened = component
272 .iter()
273 .map(|piece| piece.as_str())
274 .collect::<String>();
275 paths_so_far.retain_mut(|p| {
276 sys::fs::push_path_for_pattern(p, &flattened);
277
278 p.symlink_metadata().is_ok()
289 });
290 continue;
291 }
292
293 let current_paths = std::mem::take(&mut paths_so_far);
294 for current_path in current_paths {
295 let subpattern = Self::from(&component)
296 .set_extended_globbing(self.enable_extended_globbing)
297 .set_case_insensitive(self.case_insensitive);
298
299 let subpattern_starts_with_dot = subpattern
300 .pieces
301 .first()
302 .is_some_and(|piece| piece.as_str().starts_with('.'));
303
304 let allow_dot_files = !options.require_dot_in_pattern_to_match_dot_files
305 || subpattern_starts_with_dot;
306
307 let matches_dotfile_policy = |dir_entry: &std::fs::DirEntry| {
308 !dir_entry.file_name().to_string_lossy().starts_with('.') || allow_dot_files
309 };
310
311 let regex = subpattern.to_regex(true, true)?;
312 let matches_regex = |dir_entry: &std::fs::DirEntry| {
313 regex
314 .is_match(dir_entry.file_name().to_string_lossy().as_ref())
315 .unwrap_or(false)
316 };
317
318 let mut matching_paths_in_dir: Vec<_> = current_path
319 .read_dir()
320 .map_or_else(|_| vec![], |dir| dir.into_iter().collect())
321 .into_iter()
322 .filter_map(|result| result.ok())
323 .filter(matches_regex)
324 .filter(matches_dotfile_policy)
325 .map(|entry| entry.path())
326 .collect();
327
328 matching_paths_in_dir.sort();
329
330 paths_so_far.append(&mut matching_paths_in_dir);
331 }
332 }
333
334 let results: Vec<_> = paths_so_far
335 .into_iter()
336 .filter_map(|path| {
337 if let Some(filter) = path_filter
338 && !filter(path.as_path())
339 {
340 return None;
341 }
342
343 let path_str = path.to_string_lossy();
348 let normalized = sys::fs::normalize_path_separators(&path_str);
349 let mut path_ref: &str = normalized.as_ref();
350
351 if let Some(prefix_to_remove) = &prefix_to_remove
352 && let Some(stripped) = path_ref.strip_prefix(prefix_to_remove.as_str())
353 {
354 path_ref = stripped;
355 }
356
357 Some(path_ref.to_string())
358 })
359 .collect();
360
361 tracing::debug!(target: trace_categories::PATTERN, " => results: {results:?}");
362
363 Ok(PatternExpansionResult::Expanded(results))
364 }
365
366 pub(crate) fn to_regex_str(
375 &self,
376 strict_prefix_match: bool,
377 strict_suffix_match: bool,
378 ) -> Result<String, error::Error> {
379 let mut regex_str = String::new();
380
381 if strict_prefix_match {
382 regex_str.push('^');
383 }
384
385 let mut current_pattern = String::new();
386 for piece in &self.pieces {
387 match piece {
388 PatternPiece::Pattern(s) => {
389 current_pattern.push_str(s);
390 }
391 PatternPiece::Literal(s) => {
392 for c in s.chars() {
393 if crate::regex::regex_char_is_special(c) {
394 current_pattern.push('\\');
395 }
396 current_pattern.push(c);
397 }
398 }
399 }
400 }
401
402 let regex_piece =
403 pattern_to_regex_str(current_pattern.as_str(), self.enable_extended_globbing)?;
404 regex_str.push_str(regex_piece.as_str());
405
406 if strict_suffix_match {
407 regex_str.push('$');
408 }
409
410 Ok(regex_str)
411 }
412
413 pub(crate) fn to_regex(
422 &self,
423 strict_prefix_match: bool,
424 strict_suffix_match: bool,
425 ) -> Result<fancy_regex::Regex, error::Error> {
426 let regex_str = self.to_regex_str(strict_prefix_match, strict_suffix_match)?;
427
428 tracing::debug!(target: trace_categories::PATTERN, "pattern: '{self:?}' => regex: '{regex_str}'");
429
430 let re = regex::compile_regex(regex_str, self.case_insensitive, self.multiline)?;
431 Ok(re)
432 }
433
434 pub fn exactly_matches(&self, value: &str) -> Result<bool, error::Error> {
449 let re = self.to_regex(true, true)?;
450 Ok(re.is_match(value)?)
451 }
452}
453
454fn requires_expansion(s: &str, enable_extended_globbing: bool) -> bool {
458 shrub_parser::pattern::pattern_has_glob_metacharacters(s, enable_extended_globbing)
459}
460
461fn pattern_to_regex_str(
462 pattern: &str,
463 enable_extended_globbing: bool,
464) -> Result<String, error::Error> {
465 Ok(shrub_parser::pattern::pattern_to_regex_str(
466 pattern,
467 enable_extended_globbing,
468 )?)
469}
470
471pub(crate) fn remove_largest_matching_prefix<'a>(
479 s: &'a str,
480 pattern: Option<&Pattern>,
481) -> Result<&'a str, error::Error> {
482 if let Some(pattern) = pattern {
483 let re = pattern.to_regex(true, true)?;
484 let indices = s.char_indices().rev();
485 let mut last_idx = s.len();
486
487 #[allow(
488 clippy::string_slice,
489 reason = "because we get the indices from char_indices()"
490 )]
491 for (idx, _) in indices {
492 let prefix = &s[0..last_idx];
493 if re.is_match(prefix)? {
494 return Ok(&s[last_idx..]);
495 }
496
497 last_idx = idx;
498 }
499 }
500 Ok(s)
501}
502
503pub(crate) fn remove_smallest_matching_prefix<'a>(
511 s: &'a str,
512 pattern: Option<&Pattern>,
513) -> Result<&'a str, error::Error> {
514 if let Some(pattern) = pattern {
515 let re = pattern.to_regex(true, true)?;
516 let mut indices = s.char_indices();
517
518 #[allow(
519 clippy::string_slice,
520 reason = "because we get the indices from char_indices()"
521 )]
522 while indices.next().is_some() {
523 let next_index = indices.offset();
524 let prefix = &s[0..next_index];
525 if re.is_match(prefix)? {
526 return Ok(&s[next_index..]);
527 }
528 }
529 }
530 Ok(s)
531}
532
533pub(crate) fn remove_largest_matching_suffix<'a>(
541 s: &'a str,
542 pattern: Option<&Pattern>,
543) -> Result<&'a str, error::Error> {
544 if let Some(pattern) = pattern {
545 let re = pattern.to_regex(true, true)?;
546 #[allow(
547 clippy::string_slice,
548 reason = "because we get the indices from char_indices()"
549 )]
550 for (idx, _) in s.char_indices() {
551 let suffix = &s[idx..];
552 if re.is_match(suffix)? {
553 return Ok(&s[..idx]);
554 }
555 }
556 }
557 Ok(s)
558}
559
560pub(crate) fn remove_smallest_matching_suffix<'a>(
568 s: &'a str,
569 pattern: Option<&Pattern>,
570) -> Result<&'a str, error::Error> {
571 if let Some(pattern) = pattern {
572 let re = pattern.to_regex(true, true)?;
573 #[allow(
574 clippy::string_slice,
575 reason = "because we get the indices from char_indices()"
576 )]
577 for (idx, _) in s.char_indices().rev() {
578 let suffix = &s[idx..];
579 if re.is_match(suffix)? {
580 return Ok(&s[..idx]);
581 }
582 }
583 }
584 Ok(s)
585}
586
587#[cfg(test)]
588#[expect(clippy::panic_in_result_fn)]
589mod tests {
590 use anyhow::Result;
591
592 use super::*;
593
594 fn pattern_to_exact_regex_str<P>(pattern: P) -> Result<String, error::Error>
595 where
596 P: Into<Pattern>,
597 {
598 let pattern: Pattern = pattern
599 .into()
600 .set_extended_globbing(true)
601 .set_multiline(false);
602
603 pattern.to_regex_str(true, true)
604 }
605
606 #[test]
607 fn test_pattern_translation() -> Result<()> {
608 assert_eq!(pattern_to_exact_regex_str("a")?.as_str(), "^a$");
609 assert_eq!(pattern_to_exact_regex_str("a*")?.as_str(), "^a.*$");
610 assert_eq!(pattern_to_exact_regex_str("a?")?.as_str(), "^a.$");
611 assert_eq!(pattern_to_exact_regex_str("a@(b|c)")?.as_str(), "^a(b|c)$");
612 assert_eq!(pattern_to_exact_regex_str("a?(b|c)")?.as_str(), "^a(b|c)?$");
613 assert_eq!(
614 pattern_to_exact_regex_str("a*(ab|ac)")?.as_str(),
615 "^a(ab|ac)*$"
616 );
617 assert_eq!(
618 pattern_to_exact_regex_str("a+(ab|ac)")?.as_str(),
619 "^a(ab|ac)+$"
620 );
621 assert_eq!(pattern_to_exact_regex_str("[ab]")?.as_str(), "^[ab]$");
622 assert_eq!(pattern_to_exact_regex_str("[ab]*")?.as_str(), "^[ab].*$");
623 assert_eq!(
624 pattern_to_exact_regex_str("[<{().[]*")?.as_str(),
625 r"^[<{().\[].*$"
626 );
627 assert_eq!(pattern_to_exact_regex_str("[a-d]")?.as_str(), "^[a-d]$");
628 assert_eq!(pattern_to_exact_regex_str(r"\*")?.as_str(), r"^\*$");
629
630 Ok(())
631 }
632
633 #[test]
634 fn test_pattern_word_translation() -> Result<()> {
635 assert_eq!(
636 pattern_to_exact_regex_str(vec![PatternPiece::Pattern("a*".to_owned())])?.as_str(),
637 "^a.*$"
638 );
639 assert_eq!(
640 pattern_to_exact_regex_str(vec![
641 PatternPiece::Pattern("a*".to_owned()),
642 PatternPiece::Literal("b".to_owned()),
643 ])?
644 .as_str(),
645 "^a.*b$"
646 );
647 assert_eq!(
648 pattern_to_exact_regex_str(vec![
649 PatternPiece::Literal("a*".to_owned()),
650 PatternPiece::Pattern("b".to_owned()),
651 ])?
652 .as_str(),
653 r"^a\*b$"
654 );
655
656 Ok(())
657 }
658
659 #[test]
660 fn test_remove_largest_matching_prefix() -> Result<()> {
661 assert_eq!(
662 remove_largest_matching_prefix("ooof", Some(&Pattern::from("")))?,
663 "ooof"
664 );
665 assert_eq!(
666 remove_largest_matching_prefix("ooof", Some(&Pattern::from("x")))?,
667 "ooof"
668 );
669 assert_eq!(
670 remove_largest_matching_prefix("ooof", Some(&Pattern::from("o")))?,
671 "oof"
672 );
673 assert_eq!(
674 remove_largest_matching_prefix("ooof", Some(&Pattern::from("o*o")))?,
675 "f"
676 );
677 assert_eq!(
678 remove_largest_matching_prefix("ooof", Some(&Pattern::from("o*")))?,
679 ""
680 );
681 assert_eq!(
682 remove_largest_matching_prefix("πππrocket", Some(&Pattern::from("π")))?,
683 "ππrocket"
684 );
685 Ok(())
686 }
687
688 #[test]
689 fn test_remove_smallest_matching_prefix() -> Result<()> {
690 assert_eq!(
691 remove_smallest_matching_prefix("ooof", Some(&Pattern::from("")))?,
692 "ooof"
693 );
694 assert_eq!(
695 remove_smallest_matching_prefix("ooof", Some(&Pattern::from("x")))?,
696 "ooof"
697 );
698 assert_eq!(
699 remove_smallest_matching_prefix("ooof", Some(&Pattern::from("o")))?,
700 "oof"
701 );
702 assert_eq!(
703 remove_smallest_matching_prefix("ooof", Some(&Pattern::from("o*o")))?,
704 "of"
705 );
706 assert_eq!(
707 remove_smallest_matching_prefix("ooof", Some(&Pattern::from("o*")))?,
708 "oof"
709 );
710 assert_eq!(
711 remove_smallest_matching_prefix("ooof", Some(&Pattern::from("ooof")))?,
712 ""
713 );
714 assert_eq!(
715 remove_smallest_matching_prefix("πππrocket", Some(&Pattern::from("π")))?,
716 "ππrocket"
717 );
718 Ok(())
719 }
720
721 #[test]
722 fn test_remove_largest_matching_suffix() -> Result<()> {
723 assert_eq!(
724 remove_largest_matching_suffix("foo", Some(&Pattern::from("")))?,
725 "foo"
726 );
727 assert_eq!(
728 remove_largest_matching_suffix("foo", Some(&Pattern::from("x")))?,
729 "foo"
730 );
731 assert_eq!(
732 remove_largest_matching_suffix("foo", Some(&Pattern::from("o")))?,
733 "fo"
734 );
735 assert_eq!(
736 remove_largest_matching_suffix("foo", Some(&Pattern::from("o*")))?,
737 "f"
738 );
739 assert_eq!(
740 remove_largest_matching_suffix("foo", Some(&Pattern::from("foo")))?,
741 ""
742 );
743 assert_eq!(
744 remove_largest_matching_suffix("rocketπππ", Some(&Pattern::from("π")))?,
745 "rocketππ"
746 );
747 Ok(())
748 }
749
750 #[test]
751 fn test_remove_smallest_matching_suffix() -> Result<()> {
752 assert_eq!(
753 remove_smallest_matching_suffix("fooo", Some(&Pattern::from("")))?,
754 "fooo"
755 );
756 assert_eq!(
757 remove_smallest_matching_suffix("fooo", Some(&Pattern::from("x")))?,
758 "fooo"
759 );
760 assert_eq!(
761 remove_smallest_matching_suffix("fooo", Some(&Pattern::from("o")))?,
762 "foo"
763 );
764 assert_eq!(
765 remove_smallest_matching_suffix("fooo", Some(&Pattern::from("o*o")))?,
766 "fo"
767 );
768 assert_eq!(
769 remove_smallest_matching_suffix("fooo", Some(&Pattern::from("o*")))?,
770 "foo"
771 );
772 assert_eq!(
773 remove_smallest_matching_suffix("fooo", Some(&Pattern::from("fooo")))?,
774 ""
775 );
776 assert_eq!(
777 remove_smallest_matching_suffix("rocketπππ", Some(&Pattern::from("π")))?,
778 "rocketππ"
779 );
780 Ok(())
781 }
782
783 #[test]
784 #[expect(clippy::cognitive_complexity)]
785 fn test_matching() -> Result<()> {
786 assert!(Pattern::from("abc").exactly_matches("abc")?);
787
788 assert!(!Pattern::from("abc").exactly_matches("ABC")?);
789 assert!(!Pattern::from("abc").exactly_matches("xabcx")?);
790 assert!(!Pattern::from("abc").exactly_matches("")?);
791 assert!(!Pattern::from("abc").exactly_matches("abcd")?);
792 assert!(!Pattern::from("abc").exactly_matches("def")?);
793
794 assert!(Pattern::from("*").exactly_matches("")?);
795 assert!(Pattern::from("*").exactly_matches("abc")?);
796 assert!(Pattern::from("*").exactly_matches(" ")?);
797
798 assert!(Pattern::from("a*").exactly_matches("a")?);
799 assert!(Pattern::from("a*").exactly_matches("ab")?);
800 assert!(Pattern::from("a*").exactly_matches("a ")?);
801
802 assert!(!Pattern::from("a*").exactly_matches("A")?);
803 assert!(!Pattern::from("a*").exactly_matches("")?);
804 assert!(!Pattern::from("a*").exactly_matches("bc")?);
805 assert!(!Pattern::from("a*").exactly_matches("xax")?);
806 assert!(!Pattern::from("a*").exactly_matches(" a")?);
807
808 assert!(Pattern::from("*a").exactly_matches("a")?);
809 assert!(Pattern::from("*a").exactly_matches("ba")?);
810 assert!(Pattern::from("*a").exactly_matches("aa")?);
811 assert!(Pattern::from("*a").exactly_matches(" a")?);
812
813 assert!(!Pattern::from("*a").exactly_matches("BA")?);
814 assert!(!Pattern::from("*a").exactly_matches("")?);
815 assert!(!Pattern::from("*a").exactly_matches("ab")?);
816 assert!(!Pattern::from("*a").exactly_matches("xax")?);
817
818 Ok(())
819 }
820
821 fn make_extglob(s: &str) -> Pattern {
822 let pattern = Pattern::from(s).set_extended_globbing(true);
823 let regex_str = pattern.to_regex_str(true, true).unwrap();
824 eprintln!("pattern: '{s}' => regex: '{regex_str}'");
825
826 pattern
827 }
828
829 #[test]
830 fn test_extglob_or_matching() -> Result<()> {
831 assert!(make_extglob("@(a|b)").exactly_matches("a")?);
832 assert!(make_extglob("@(a|b)").exactly_matches("b")?);
833
834 assert!(!make_extglob("@(a|b)").exactly_matches("")?);
835 assert!(!make_extglob("@(a|b)").exactly_matches("c")?);
836 assert!(!make_extglob("@(a|b)").exactly_matches("ab")?);
837
838 assert!(!make_extglob("@(a|b)").exactly_matches("")?);
839 assert!(make_extglob("@(a*b|b)").exactly_matches("ab")?);
840 assert!(make_extglob("@(a*b|b)").exactly_matches("axb")?);
841 assert!(make_extglob("@(a*b|b)").exactly_matches("b")?);
842
843 assert!(!make_extglob("@(a*b|b)").exactly_matches("a")?);
844
845 Ok(())
846 }
847
848 #[test]
849 fn test_extglob_not_matching() -> Result<()> {
850 assert!(make_extglob("!(a)").exactly_matches("")?);
852 assert!(make_extglob("!(a)").exactly_matches(" ")?);
853 assert!(make_extglob("!(a)").exactly_matches("x")?);
854 assert!(make_extglob("!(a)").exactly_matches(" a ")?);
855 assert!(make_extglob("!(a)").exactly_matches("a ")?);
856 assert!(make_extglob("!(a)").exactly_matches("aa")?);
857 assert!(!make_extglob("!(a)").exactly_matches("a")?);
858
859 assert!(make_extglob("a!(a)a").exactly_matches("aa")?);
860 assert!(make_extglob("a!(a)a").exactly_matches("aaaa")?);
861 assert!(make_extglob("a!(a)a").exactly_matches("aba")?);
862 assert!(!make_extglob("a!(a)a").exactly_matches("a")?);
863 assert!(!make_extglob("a!(a)a").exactly_matches("aaa")?);
864 assert!(!make_extglob("a!(a)a").exactly_matches("baaa")?);
865
866 assert!(make_extglob("!(a|b)").exactly_matches("c")?);
868 assert!(make_extglob("!(a|b)").exactly_matches("ab")?);
869 assert!(make_extglob("!(a|b)").exactly_matches("aa")?);
870 assert!(make_extglob("!(a|b)").exactly_matches("bb")?);
871 assert!(!make_extglob("!(a|b)").exactly_matches("a")?);
872 assert!(!make_extglob("!(a|b)").exactly_matches("b")?);
873
874 Ok(())
875 }
876
877 #[test]
878 fn test_extglob_advanced_not_matching() -> Result<()> {
879 assert!(make_extglob("!(a*)").exactly_matches("b")?);
880 assert!(make_extglob("!(a*)").exactly_matches("")?);
881 assert!(!make_extglob("!(a*)").exactly_matches("a")?);
882 assert!(!make_extglob("!(a*)").exactly_matches("abc")?);
883 assert!(!make_extglob("!(a*)").exactly_matches("aabc")?);
884
885 Ok(())
886 }
887
888 #[test]
889 fn test_extglob_not_degenerate_matching() -> Result<()> {
890 assert!(make_extglob("!()").exactly_matches("a")?);
892 assert!(!make_extglob("!()").exactly_matches("")?);
893
894 Ok(())
895 }
896
897 #[test]
898 fn test_extglob_zero_or_more_matching() -> Result<()> {
899 assert!(make_extglob("x*(a)x").exactly_matches("xx")?);
900 assert!(make_extglob("x*(a)x").exactly_matches("xax")?);
901 assert!(make_extglob("x*(a)x").exactly_matches("xaax")?);
902
903 assert!(!make_extglob("x*(a)x").exactly_matches("x")?);
904 assert!(!make_extglob("x*(a)x").exactly_matches("xa")?);
905 assert!(!make_extglob("x*(a)x").exactly_matches("xxx")?);
906
907 assert!(make_extglob("*(a|b)").exactly_matches("")?);
908 assert!(make_extglob("*(a|b)").exactly_matches("a")?);
909 assert!(make_extglob("*(a|b)").exactly_matches("b")?);
910 assert!(make_extglob("*(a|b)").exactly_matches("aba")?);
911 assert!(make_extglob("*(a|b)").exactly_matches("aaa")?);
912
913 assert!(!make_extglob("*(a|b)").exactly_matches("c")?);
914 assert!(!make_extglob("*(a|b)").exactly_matches("ca")?);
915
916 Ok(())
917 }
918
919 #[test]
920 fn test_extglob_one_or_more_matching() -> Result<()> {
921 fn make_extglob(s: &str) -> Pattern {
922 Pattern::from(s).set_extended_globbing(true)
923 }
924
925 assert!(make_extglob("x+(a)x").exactly_matches("xax")?);
926 assert!(make_extglob("x+(a)x").exactly_matches("xaax")?);
927
928 assert!(!make_extglob("x+(a)x").exactly_matches("xx")?);
929 assert!(!make_extglob("x+(a)x").exactly_matches("x")?);
930 assert!(!make_extglob("x+(a)x").exactly_matches("xa")?);
931 assert!(!make_extglob("x+(a)x").exactly_matches("xxx")?);
932
933 assert!(make_extglob("+(a|b)").exactly_matches("a")?);
934 assert!(make_extglob("+(a|b)").exactly_matches("b")?);
935 assert!(make_extglob("+(a|b)").exactly_matches("aba")?);
936 assert!(make_extglob("+(a|b)").exactly_matches("aaa")?);
937
938 assert!(!make_extglob("+(a|b)").exactly_matches("")?);
939 assert!(!make_extglob("+(a|b)").exactly_matches("c")?);
940 assert!(!make_extglob("+(a|b)").exactly_matches("ca")?);
941
942 assert!(make_extglob("+(x+(ab)y)").exactly_matches("xaby")?);
943 assert!(make_extglob("+(x+(ab)y)").exactly_matches("xababy")?);
944 assert!(make_extglob("+(x+(ab)y)").exactly_matches("xabababy")?);
945 assert!(make_extglob("+(x+(ab)y)").exactly_matches("xabababyxabababyxabababy")?);
946
947 assert!(!make_extglob("+(x+(ab)y)").exactly_matches("xy")?);
948 assert!(!make_extglob("+(x+(ab)y)").exactly_matches("xay")?);
949 assert!(!make_extglob("+(x+(ab)y)").exactly_matches("xyxy")?);
950
951 Ok(())
952 }
953
954 #[test]
955 fn test_requires_expansion() {
956 assert!(requires_expansion("*", false));
959 assert!(requires_expansion("[abc]", false));
960 assert!(!requires_expansion("]", false));
961 assert!(!requires_expansion("hello", false));
962 assert!(!requires_expansion("@(a)", false));
963 assert!(requires_expansion("@(a)", true));
964 }
965
966 fn expect_expanded(result: PatternExpansionResult) -> Result<Vec<String>> {
970 let PatternExpansionResult::Expanded(paths) = result else {
971 anyhow::bail!("expected Expanded, got {result:?}");
972 };
973 Ok(paths)
974 }
975
976 #[test]
989 fn test_relative_glob_returns_relative_paths() -> Result<()> {
990 let scratch = tempfile::tempdir()?;
991 let sub = scratch.path().join("sub");
992 std::fs::create_dir_all(&sub)?;
993 std::fs::write(sub.join("a.txt"), "")?;
994 std::fs::write(sub.join("b.txt"), "")?;
995
996 let pattern = Pattern::from("sub/*.txt").set_extended_globbing(false);
997 let result = pattern.expand::<fn(&Path) -> bool>(
998 scratch.path(),
999 None,
1000 &FilenameExpansionOptions::default(),
1001 )?;
1002
1003 let paths = expect_expanded(result)?;
1004
1005 let mut sorted = paths.clone();
1006 sorted.sort();
1007 assert_eq!(
1008 sorted,
1009 vec!["sub/a.txt".to_string(), "sub/b.txt".to_string()]
1010 );
1011
1012 let scratch_str: String = scratch.path().to_string_lossy().into_owned();
1014 for p in &paths {
1015 assert!(
1016 !p.contains(scratch_str.as_str()),
1017 "result {p:?} still contains absolute working-dir prefix {scratch_str:?}"
1018 );
1019 }
1020
1021 Ok(())
1022 }
1023
1024 #[test]
1027 fn test_absolute_glob_returns_absolute_paths() -> Result<()> {
1028 let scratch = tempfile::tempdir()?;
1029 std::fs::write(scratch.path().join("one.log"), "")?;
1030 std::fs::write(scratch.path().join("two.log"), "")?;
1031
1032 let abs_pattern = format!("{}/*.log", scratch.path().to_string_lossy());
1033 let abs_pattern = abs_pattern.replace('\\', "/");
1036
1037 let pattern = Pattern::from(abs_pattern.as_str()).set_extended_globbing(false);
1038 let result = pattern.expand::<fn(&Path) -> bool>(
1039 Path::new("/"),
1040 None,
1041 &FilenameExpansionOptions::default(),
1042 )?;
1043
1044 let paths = expect_expanded(result)?;
1045
1046 assert_eq!(paths.len(), 2, "unexpected results: {paths:?}");
1047 let scratch_normalized: String = scratch.path().to_string_lossy().replace('\\', "/");
1048 for p in &paths {
1049 assert!(p.as_bytes().ends_with(b".log"), "unexpected result {p:?}");
1054 assert!(
1056 p.contains(scratch_normalized.as_str()),
1057 "absolute result {p:?} should contain {scratch_normalized:?}"
1058 );
1059 }
1060
1061 Ok(())
1062 }
1063}