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