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> {
442 let re = self.to_regex(true, true)?;
443 Ok(re.is_match(value)?)
444 }
445}
446
447fn requires_expansion(s: &str, enable_extended_globbing: bool) -> bool {
451 shrub_parser::pattern::pattern_has_glob_metacharacters(s, enable_extended_globbing)
452}
453
454fn pattern_to_regex_str(
455 pattern: &str,
456 enable_extended_globbing: bool,
457) -> Result<String, error::Error> {
458 Ok(shrub_parser::pattern::pattern_to_regex_str(
459 pattern,
460 enable_extended_globbing,
461 )?)
462}
463
464pub(crate) fn remove_largest_matching_prefix<'a>(
472 s: &'a str,
473 pattern: Option<&Pattern>,
474) -> Result<&'a str, error::Error> {
475 if let Some(pattern) = pattern {
476 let re = pattern.to_regex(true, true)?;
477 let indices = s.char_indices().rev();
478 let mut last_idx = s.len();
479
480 #[allow(
481 clippy::string_slice,
482 reason = "because we get the indices from char_indices()"
483 )]
484 for (idx, _) in indices {
485 let prefix = &s[0..last_idx];
486 if re.is_match(prefix)? {
487 return Ok(&s[last_idx..]);
488 }
489
490 last_idx = idx;
491 }
492 }
493 Ok(s)
494}
495
496pub(crate) fn remove_smallest_matching_prefix<'a>(
504 s: &'a str,
505 pattern: Option<&Pattern>,
506) -> Result<&'a str, error::Error> {
507 if let Some(pattern) = pattern {
508 let re = pattern.to_regex(true, true)?;
509 let mut indices = s.char_indices();
510
511 #[allow(
512 clippy::string_slice,
513 reason = "because we get the indices from char_indices()"
514 )]
515 while indices.next().is_some() {
516 let next_index = indices.offset();
517 let prefix = &s[0..next_index];
518 if re.is_match(prefix)? {
519 return Ok(&s[next_index..]);
520 }
521 }
522 }
523 Ok(s)
524}
525
526pub(crate) fn remove_largest_matching_suffix<'a>(
534 s: &'a str,
535 pattern: Option<&Pattern>,
536) -> Result<&'a str, error::Error> {
537 if let Some(pattern) = pattern {
538 let re = pattern.to_regex(true, true)?;
539 #[allow(
540 clippy::string_slice,
541 reason = "because we get the indices from char_indices()"
542 )]
543 for (idx, _) in s.char_indices() {
544 let suffix = &s[idx..];
545 if re.is_match(suffix)? {
546 return Ok(&s[..idx]);
547 }
548 }
549 }
550 Ok(s)
551}
552
553pub(crate) fn remove_smallest_matching_suffix<'a>(
561 s: &'a str,
562 pattern: Option<&Pattern>,
563) -> Result<&'a str, error::Error> {
564 if let Some(pattern) = pattern {
565 let re = pattern.to_regex(true, true)?;
566 #[allow(
567 clippy::string_slice,
568 reason = "because we get the indices from char_indices()"
569 )]
570 for (idx, _) in s.char_indices().rev() {
571 let suffix = &s[idx..];
572 if re.is_match(suffix)? {
573 return Ok(&s[..idx]);
574 }
575 }
576 }
577 Ok(s)
578}
579
580#[cfg(test)]
581#[expect(clippy::panic_in_result_fn)]
582mod tests {
583 use anyhow::Result;
584
585 use super::*;
586
587 fn pattern_to_exact_regex_str<P>(pattern: P) -> Result<String, error::Error>
588 where
589 P: Into<Pattern>,
590 {
591 let pattern: Pattern = pattern
592 .into()
593 .set_extended_globbing(true)
594 .set_multiline(false);
595
596 pattern.to_regex_str(true, true)
597 }
598
599 #[test]
600 fn test_pattern_translation() -> Result<()> {
601 assert_eq!(pattern_to_exact_regex_str("a")?.as_str(), "^a$");
602 assert_eq!(pattern_to_exact_regex_str("a*")?.as_str(), "^a.*$");
603 assert_eq!(pattern_to_exact_regex_str("a?")?.as_str(), "^a.$");
604 assert_eq!(pattern_to_exact_regex_str("a@(b|c)")?.as_str(), "^a(b|c)$");
605 assert_eq!(pattern_to_exact_regex_str("a?(b|c)")?.as_str(), "^a(b|c)?$");
606 assert_eq!(
607 pattern_to_exact_regex_str("a*(ab|ac)")?.as_str(),
608 "^a(ab|ac)*$"
609 );
610 assert_eq!(
611 pattern_to_exact_regex_str("a+(ab|ac)")?.as_str(),
612 "^a(ab|ac)+$"
613 );
614 assert_eq!(pattern_to_exact_regex_str("[ab]")?.as_str(), "^[ab]$");
615 assert_eq!(pattern_to_exact_regex_str("[ab]*")?.as_str(), "^[ab].*$");
616 assert_eq!(
617 pattern_to_exact_regex_str("[<{().[]*")?.as_str(),
618 r"^[<{().\[].*$"
619 );
620 assert_eq!(pattern_to_exact_regex_str("[a-d]")?.as_str(), "^[a-d]$");
621 assert_eq!(pattern_to_exact_regex_str(r"\*")?.as_str(), r"^\*$");
622
623 Ok(())
624 }
625
626 #[test]
627 fn test_pattern_word_translation() -> Result<()> {
628 assert_eq!(
629 pattern_to_exact_regex_str(vec![PatternPiece::Pattern("a*".to_owned())])?.as_str(),
630 "^a.*$"
631 );
632 assert_eq!(
633 pattern_to_exact_regex_str(vec![
634 PatternPiece::Pattern("a*".to_owned()),
635 PatternPiece::Literal("b".to_owned()),
636 ])?
637 .as_str(),
638 "^a.*b$"
639 );
640 assert_eq!(
641 pattern_to_exact_regex_str(vec![
642 PatternPiece::Literal("a*".to_owned()),
643 PatternPiece::Pattern("b".to_owned()),
644 ])?
645 .as_str(),
646 r"^a\*b$"
647 );
648
649 Ok(())
650 }
651
652 #[test]
653 fn test_remove_largest_matching_prefix() -> Result<()> {
654 assert_eq!(
655 remove_largest_matching_prefix("ooof", Some(&Pattern::from("")))?,
656 "ooof"
657 );
658 assert_eq!(
659 remove_largest_matching_prefix("ooof", Some(&Pattern::from("x")))?,
660 "ooof"
661 );
662 assert_eq!(
663 remove_largest_matching_prefix("ooof", Some(&Pattern::from("o")))?,
664 "oof"
665 );
666 assert_eq!(
667 remove_largest_matching_prefix("ooof", Some(&Pattern::from("o*o")))?,
668 "f"
669 );
670 assert_eq!(
671 remove_largest_matching_prefix("ooof", Some(&Pattern::from("o*")))?,
672 ""
673 );
674 assert_eq!(
675 remove_largest_matching_prefix("πππrocket", Some(&Pattern::from("π")))?,
676 "ππrocket"
677 );
678 Ok(())
679 }
680
681 #[test]
682 fn test_remove_smallest_matching_prefix() -> Result<()> {
683 assert_eq!(
684 remove_smallest_matching_prefix("ooof", Some(&Pattern::from("")))?,
685 "ooof"
686 );
687 assert_eq!(
688 remove_smallest_matching_prefix("ooof", Some(&Pattern::from("x")))?,
689 "ooof"
690 );
691 assert_eq!(
692 remove_smallest_matching_prefix("ooof", Some(&Pattern::from("o")))?,
693 "oof"
694 );
695 assert_eq!(
696 remove_smallest_matching_prefix("ooof", Some(&Pattern::from("o*o")))?,
697 "of"
698 );
699 assert_eq!(
700 remove_smallest_matching_prefix("ooof", Some(&Pattern::from("o*")))?,
701 "oof"
702 );
703 assert_eq!(
704 remove_smallest_matching_prefix("ooof", Some(&Pattern::from("ooof")))?,
705 ""
706 );
707 assert_eq!(
708 remove_smallest_matching_prefix("πππrocket", Some(&Pattern::from("π")))?,
709 "ππrocket"
710 );
711 Ok(())
712 }
713
714 #[test]
715 fn test_remove_largest_matching_suffix() -> Result<()> {
716 assert_eq!(
717 remove_largest_matching_suffix("foo", Some(&Pattern::from("")))?,
718 "foo"
719 );
720 assert_eq!(
721 remove_largest_matching_suffix("foo", Some(&Pattern::from("x")))?,
722 "foo"
723 );
724 assert_eq!(
725 remove_largest_matching_suffix("foo", Some(&Pattern::from("o")))?,
726 "fo"
727 );
728 assert_eq!(
729 remove_largest_matching_suffix("foo", Some(&Pattern::from("o*")))?,
730 "f"
731 );
732 assert_eq!(
733 remove_largest_matching_suffix("foo", Some(&Pattern::from("foo")))?,
734 ""
735 );
736 assert_eq!(
737 remove_largest_matching_suffix("rocketπππ", Some(&Pattern::from("π")))?,
738 "rocketππ"
739 );
740 Ok(())
741 }
742
743 #[test]
744 fn test_remove_smallest_matching_suffix() -> Result<()> {
745 assert_eq!(
746 remove_smallest_matching_suffix("fooo", Some(&Pattern::from("")))?,
747 "fooo"
748 );
749 assert_eq!(
750 remove_smallest_matching_suffix("fooo", Some(&Pattern::from("x")))?,
751 "fooo"
752 );
753 assert_eq!(
754 remove_smallest_matching_suffix("fooo", Some(&Pattern::from("o")))?,
755 "foo"
756 );
757 assert_eq!(
758 remove_smallest_matching_suffix("fooo", Some(&Pattern::from("o*o")))?,
759 "fo"
760 );
761 assert_eq!(
762 remove_smallest_matching_suffix("fooo", Some(&Pattern::from("o*")))?,
763 "foo"
764 );
765 assert_eq!(
766 remove_smallest_matching_suffix("fooo", Some(&Pattern::from("fooo")))?,
767 ""
768 );
769 assert_eq!(
770 remove_smallest_matching_suffix("rocketπππ", Some(&Pattern::from("π")))?,
771 "rocketππ"
772 );
773 Ok(())
774 }
775
776 #[test]
777 #[expect(clippy::cognitive_complexity)]
778 fn test_matching() -> Result<()> {
779 assert!(Pattern::from("abc").exactly_matches("abc")?);
780
781 assert!(!Pattern::from("abc").exactly_matches("ABC")?);
782 assert!(!Pattern::from("abc").exactly_matches("xabcx")?);
783 assert!(!Pattern::from("abc").exactly_matches("")?);
784 assert!(!Pattern::from("abc").exactly_matches("abcd")?);
785 assert!(!Pattern::from("abc").exactly_matches("def")?);
786
787 assert!(Pattern::from("*").exactly_matches("")?);
788 assert!(Pattern::from("*").exactly_matches("abc")?);
789 assert!(Pattern::from("*").exactly_matches(" ")?);
790
791 assert!(Pattern::from("a*").exactly_matches("a")?);
792 assert!(Pattern::from("a*").exactly_matches("ab")?);
793 assert!(Pattern::from("a*").exactly_matches("a ")?);
794
795 assert!(!Pattern::from("a*").exactly_matches("A")?);
796 assert!(!Pattern::from("a*").exactly_matches("")?);
797 assert!(!Pattern::from("a*").exactly_matches("bc")?);
798 assert!(!Pattern::from("a*").exactly_matches("xax")?);
799 assert!(!Pattern::from("a*").exactly_matches(" a")?);
800
801 assert!(Pattern::from("*a").exactly_matches("a")?);
802 assert!(Pattern::from("*a").exactly_matches("ba")?);
803 assert!(Pattern::from("*a").exactly_matches("aa")?);
804 assert!(Pattern::from("*a").exactly_matches(" a")?);
805
806 assert!(!Pattern::from("*a").exactly_matches("BA")?);
807 assert!(!Pattern::from("*a").exactly_matches("")?);
808 assert!(!Pattern::from("*a").exactly_matches("ab")?);
809 assert!(!Pattern::from("*a").exactly_matches("xax")?);
810
811 Ok(())
812 }
813
814 fn make_extglob(s: &str) -> Pattern {
815 let pattern = Pattern::from(s).set_extended_globbing(true);
816 let regex_str = pattern.to_regex_str(true, true).unwrap();
817 eprintln!("pattern: '{s}' => regex: '{regex_str}'");
818
819 pattern
820 }
821
822 #[test]
823 fn test_extglob_or_matching() -> Result<()> {
824 assert!(make_extglob("@(a|b)").exactly_matches("a")?);
825 assert!(make_extglob("@(a|b)").exactly_matches("b")?);
826
827 assert!(!make_extglob("@(a|b)").exactly_matches("")?);
828 assert!(!make_extglob("@(a|b)").exactly_matches("c")?);
829 assert!(!make_extglob("@(a|b)").exactly_matches("ab")?);
830
831 assert!(!make_extglob("@(a|b)").exactly_matches("")?);
832 assert!(make_extglob("@(a*b|b)").exactly_matches("ab")?);
833 assert!(make_extglob("@(a*b|b)").exactly_matches("axb")?);
834 assert!(make_extglob("@(a*b|b)").exactly_matches("b")?);
835
836 assert!(!make_extglob("@(a*b|b)").exactly_matches("a")?);
837
838 Ok(())
839 }
840
841 #[test]
842 fn test_extglob_not_matching() -> Result<()> {
843 assert!(make_extglob("!(a)").exactly_matches("")?);
845 assert!(make_extglob("!(a)").exactly_matches(" ")?);
846 assert!(make_extglob("!(a)").exactly_matches("x")?);
847 assert!(make_extglob("!(a)").exactly_matches(" a ")?);
848 assert!(make_extglob("!(a)").exactly_matches("a ")?);
849 assert!(make_extglob("!(a)").exactly_matches("aa")?);
850 assert!(!make_extglob("!(a)").exactly_matches("a")?);
851
852 assert!(make_extglob("a!(a)a").exactly_matches("aa")?);
853 assert!(make_extglob("a!(a)a").exactly_matches("aaaa")?);
854 assert!(make_extglob("a!(a)a").exactly_matches("aba")?);
855 assert!(!make_extglob("a!(a)a").exactly_matches("a")?);
856 assert!(!make_extglob("a!(a)a").exactly_matches("aaa")?);
857 assert!(!make_extglob("a!(a)a").exactly_matches("baaa")?);
858
859 assert!(make_extglob("!(a|b)").exactly_matches("c")?);
861 assert!(make_extglob("!(a|b)").exactly_matches("ab")?);
862 assert!(make_extglob("!(a|b)").exactly_matches("aa")?);
863 assert!(make_extglob("!(a|b)").exactly_matches("bb")?);
864 assert!(!make_extglob("!(a|b)").exactly_matches("a")?);
865 assert!(!make_extglob("!(a|b)").exactly_matches("b")?);
866
867 Ok(())
868 }
869
870 #[test]
871 fn test_extglob_advanced_not_matching() -> Result<()> {
872 assert!(make_extglob("!(a*)").exactly_matches("b")?);
873 assert!(make_extglob("!(a*)").exactly_matches("")?);
874 assert!(!make_extglob("!(a*)").exactly_matches("a")?);
875 assert!(!make_extglob("!(a*)").exactly_matches("abc")?);
876 assert!(!make_extglob("!(a*)").exactly_matches("aabc")?);
877
878 Ok(())
879 }
880
881 #[test]
882 fn test_extglob_not_degenerate_matching() -> Result<()> {
883 assert!(make_extglob("!()").exactly_matches("a")?);
885 assert!(!make_extglob("!()").exactly_matches("")?);
886
887 Ok(())
888 }
889
890 #[test]
891 fn test_extglob_zero_or_more_matching() -> Result<()> {
892 assert!(make_extglob("x*(a)x").exactly_matches("xx")?);
893 assert!(make_extglob("x*(a)x").exactly_matches("xax")?);
894 assert!(make_extglob("x*(a)x").exactly_matches("xaax")?);
895
896 assert!(!make_extglob("x*(a)x").exactly_matches("x")?);
897 assert!(!make_extglob("x*(a)x").exactly_matches("xa")?);
898 assert!(!make_extglob("x*(a)x").exactly_matches("xxx")?);
899
900 assert!(make_extglob("*(a|b)").exactly_matches("")?);
901 assert!(make_extglob("*(a|b)").exactly_matches("a")?);
902 assert!(make_extglob("*(a|b)").exactly_matches("b")?);
903 assert!(make_extglob("*(a|b)").exactly_matches("aba")?);
904 assert!(make_extglob("*(a|b)").exactly_matches("aaa")?);
905
906 assert!(!make_extglob("*(a|b)").exactly_matches("c")?);
907 assert!(!make_extglob("*(a|b)").exactly_matches("ca")?);
908
909 Ok(())
910 }
911
912 #[test]
913 fn test_extglob_one_or_more_matching() -> Result<()> {
914 fn make_extglob(s: &str) -> Pattern {
915 Pattern::from(s).set_extended_globbing(true)
916 }
917
918 assert!(make_extglob("x+(a)x").exactly_matches("xax")?);
919 assert!(make_extglob("x+(a)x").exactly_matches("xaax")?);
920
921 assert!(!make_extglob("x+(a)x").exactly_matches("xx")?);
922 assert!(!make_extglob("x+(a)x").exactly_matches("x")?);
923 assert!(!make_extglob("x+(a)x").exactly_matches("xa")?);
924 assert!(!make_extglob("x+(a)x").exactly_matches("xxx")?);
925
926 assert!(make_extglob("+(a|b)").exactly_matches("a")?);
927 assert!(make_extglob("+(a|b)").exactly_matches("b")?);
928 assert!(make_extglob("+(a|b)").exactly_matches("aba")?);
929 assert!(make_extglob("+(a|b)").exactly_matches("aaa")?);
930
931 assert!(!make_extglob("+(a|b)").exactly_matches("")?);
932 assert!(!make_extglob("+(a|b)").exactly_matches("c")?);
933 assert!(!make_extglob("+(a|b)").exactly_matches("ca")?);
934
935 assert!(make_extglob("+(x+(ab)y)").exactly_matches("xaby")?);
936 assert!(make_extglob("+(x+(ab)y)").exactly_matches("xababy")?);
937 assert!(make_extglob("+(x+(ab)y)").exactly_matches("xabababy")?);
938 assert!(make_extglob("+(x+(ab)y)").exactly_matches("xabababyxabababyxabababy")?);
939
940 assert!(!make_extglob("+(x+(ab)y)").exactly_matches("xy")?);
941 assert!(!make_extglob("+(x+(ab)y)").exactly_matches("xay")?);
942 assert!(!make_extglob("+(x+(ab)y)").exactly_matches("xyxy")?);
943
944 Ok(())
945 }
946
947 #[test]
948 fn test_requires_expansion() {
949 assert!(requires_expansion("*", false));
952 assert!(requires_expansion("[abc]", false));
953 assert!(!requires_expansion("]", false));
954 assert!(!requires_expansion("hello", false));
955 assert!(!requires_expansion("@(a)", false));
956 assert!(requires_expansion("@(a)", true));
957 }
958
959 fn expect_expanded(result: PatternExpansionResult) -> Result<Vec<String>> {
963 let PatternExpansionResult::Expanded(paths) = result else {
964 anyhow::bail!("expected Expanded, got {result:?}");
965 };
966 Ok(paths)
967 }
968
969 #[test]
982 fn test_relative_glob_returns_relative_paths() -> Result<()> {
983 let scratch = tempfile::tempdir()?;
984 let sub = scratch.path().join("sub");
985 std::fs::create_dir_all(&sub)?;
986 std::fs::write(sub.join("a.txt"), "")?;
987 std::fs::write(sub.join("b.txt"), "")?;
988
989 let pattern = Pattern::from("sub/*.txt").set_extended_globbing(false);
990 let result = pattern.expand::<fn(&Path) -> bool>(
991 scratch.path(),
992 None,
993 &FilenameExpansionOptions::default(),
994 )?;
995
996 let paths = expect_expanded(result)?;
997
998 let mut sorted = paths.clone();
999 sorted.sort();
1000 assert_eq!(
1001 sorted,
1002 vec!["sub/a.txt".to_string(), "sub/b.txt".to_string()]
1003 );
1004
1005 let scratch_str: String = scratch.path().to_string_lossy().into_owned();
1007 for p in &paths {
1008 assert!(
1009 !p.contains(scratch_str.as_str()),
1010 "result {p:?} still contains absolute working-dir prefix {scratch_str:?}"
1011 );
1012 }
1013
1014 Ok(())
1015 }
1016
1017 #[test]
1020 fn test_absolute_glob_returns_absolute_paths() -> Result<()> {
1021 let scratch = tempfile::tempdir()?;
1022 std::fs::write(scratch.path().join("one.log"), "")?;
1023 std::fs::write(scratch.path().join("two.log"), "")?;
1024
1025 let abs_pattern = format!("{}/*.log", scratch.path().to_string_lossy());
1026 let abs_pattern = abs_pattern.replace('\\', "/");
1029
1030 let pattern = Pattern::from(abs_pattern.as_str()).set_extended_globbing(false);
1031 let result = pattern.expand::<fn(&Path) -> bool>(
1032 Path::new("/"),
1033 None,
1034 &FilenameExpansionOptions::default(),
1035 )?;
1036
1037 let paths = expect_expanded(result)?;
1038
1039 assert_eq!(paths.len(), 2, "unexpected results: {paths:?}");
1040 let scratch_normalized: String = scratch.path().to_string_lossy().replace('\\', "/");
1041 for p in &paths {
1042 assert!(p.as_bytes().ends_with(b".log"), "unexpected result {p:?}");
1047 assert!(
1049 p.contains(scratch_normalized.as_str()),
1050 "absolute result {p:?} should contain {scratch_normalized:?}"
1051 );
1052 }
1053
1054 Ok(())
1055 }
1056}