1use crate::{error, regex, trace_categories};
4use std::{
5 collections::VecDeque,
6 path::{Path, PathBuf},
7};
8
9#[derive(Clone, Debug)]
11pub(crate) enum PatternPiece {
12 Pattern(String),
14 Literal(String),
16}
17
18impl PatternPiece {
19 pub fn as_str(&self) -> &str {
20 match self {
21 Self::Pattern(s) => s,
22 Self::Literal(s) => s,
23 }
24 }
25}
26
27type PatternWord = Vec<PatternPiece>;
28
29#[derive(Clone, Debug, Default)]
31pub(crate) struct FilenameExpansionOptions {
32 pub require_dot_in_pattern_to_match_dot_files: bool,
33}
34
35#[derive(Clone, Debug)]
37pub struct Pattern {
38 pieces: PatternWord,
39 enable_extended_globbing: bool,
40 multiline: bool,
41 case_insensitive: bool,
42}
43
44impl Default for Pattern {
45 fn default() -> Self {
46 Self {
47 pieces: vec![],
48 enable_extended_globbing: false,
49 multiline: true,
50 case_insensitive: false,
51 }
52 }
53}
54
55impl From<PatternWord> for Pattern {
56 fn from(pieces: PatternWord) -> Self {
57 Self {
58 pieces,
59 ..Default::default()
60 }
61 }
62}
63
64impl From<&PatternWord> for Pattern {
65 fn from(value: &PatternWord) -> Self {
66 Self {
67 pieces: value.clone(),
68 ..Default::default()
69 }
70 }
71}
72
73impl From<&str> for Pattern {
74 fn from(value: &str) -> Self {
75 Self {
76 pieces: vec![PatternPiece::Pattern(value.to_owned())],
77 ..Default::default()
78 }
79 }
80}
81
82impl From<String> for Pattern {
83 fn from(value: String) -> Self {
84 Self {
85 pieces: vec![PatternPiece::Pattern(value)],
86 ..Default::default()
87 }
88 }
89}
90
91impl Pattern {
92 #[must_use]
98 pub const fn set_extended_globbing(mut self, value: bool) -> Self {
99 self.enable_extended_globbing = value;
100 self
101 }
102
103 #[must_use]
109 pub const fn set_multiline(mut self, value: bool) -> Self {
110 self.multiline = value;
111 self
112 }
113
114 #[must_use]
120 pub const fn set_case_insensitive(mut self, value: bool) -> Self {
121 self.case_insensitive = value;
122 self
123 }
124
125 pub fn is_empty(&self) -> bool {
127 self.pieces.iter().all(|p| p.as_str().is_empty())
128 }
129
130 pub(crate) const fn accept_all_expand_filter(_path: &Path) -> bool {
132 true
133 }
134
135 #[expect(clippy::too_many_lines)]
142 #[allow(clippy::unwrap_in_result)]
143 pub(crate) fn expand<PF>(
144 &self,
145 working_dir: &Path,
146 path_filter: Option<&PF>,
147 options: &FilenameExpansionOptions,
148 ) -> Result<Vec<String>, error::Error>
149 where
150 PF: Fn(&Path) -> bool,
151 {
152 if self.is_empty() {
155 return Ok(vec![]);
156
157 } else if !self.pieces.iter().any(|piece| {
160 matches!(piece, PatternPiece::Pattern(_)) && requires_expansion(piece.as_str())
161 }) {
162 let concatenated: String = self.pieces.iter().map(|piece| piece.as_str()).collect();
163
164 if let Some(filter) = path_filter {
165 if !filter(Path::new(&concatenated)) {
166 return Ok(vec![]);
167 }
168 }
169
170 return Ok(vec![concatenated]);
171 }
172
173 tracing::debug!(target: trace_categories::PATTERN, "expanding pattern: {self:?}");
174
175 let mut components: Vec<PatternWord> = vec![];
176 for piece in &self.pieces {
177 let mut split_result = piece
178 .as_str()
179 .split(std::path::MAIN_SEPARATOR)
180 .map(|s| match piece {
181 PatternPiece::Pattern(_) => PatternPiece::Pattern(s.to_owned()),
182 PatternPiece::Literal(_) => PatternPiece::Literal(s.to_owned()),
183 })
184 .collect::<VecDeque<_>>();
185
186 if let Some(first_piece) = split_result.pop_front() {
187 if let Some(last_component) = components.last_mut() {
188 last_component.push(first_piece);
189 } else {
190 components.push(vec![first_piece]);
191 }
192 }
193
194 while let Some(piece) = split_result.pop_front() {
195 components.push(vec![piece]);
196 }
197 }
198
199 let is_absolute = if let Some(first_component) = components.first() {
201 first_component
202 .iter()
203 .all(|piece| piece.as_str().is_empty())
204 } else {
205 false
206 };
207
208 let prefix_to_remove;
209 let mut paths_so_far = if is_absolute {
210 prefix_to_remove = None;
211 vec![PathBuf::from(std::path::MAIN_SEPARATOR_STR)]
213 } else {
214 let mut working_dir_str = working_dir.to_string_lossy().to_string();
215
216 if !working_dir_str.ends_with(std::path::MAIN_SEPARATOR) {
217 working_dir_str.push(std::path::MAIN_SEPARATOR);
218 }
219
220 prefix_to_remove = Some(working_dir_str);
221 vec![working_dir.to_path_buf()]
222 };
223
224 for component in components {
225 if !component.iter().any(|piece| {
226 matches!(piece, PatternPiece::Pattern(_)) && requires_expansion(piece.as_str())
227 }) {
228 for p in &mut paths_so_far {
229 let flattened = component
230 .iter()
231 .map(|piece| piece.as_str())
232 .collect::<String>();
233 p.push(flattened);
234 }
235 continue;
236 }
237
238 let current_paths = std::mem::take(&mut paths_so_far);
239 for current_path in current_paths {
240 let subpattern = Self::from(&component)
241 .set_extended_globbing(self.enable_extended_globbing)
242 .set_case_insensitive(self.case_insensitive);
243
244 let subpattern_starts_with_dot = subpattern
245 .pieces
246 .first()
247 .is_some_and(|piece| piece.as_str().starts_with('.'));
248
249 let allow_dot_files = !options.require_dot_in_pattern_to_match_dot_files
250 || subpattern_starts_with_dot;
251
252 let matches_dotfile_policy = |dir_entry: &std::fs::DirEntry| {
253 !dir_entry.file_name().to_string_lossy().starts_with('.') || allow_dot_files
254 };
255
256 let regex = subpattern.to_regex(true, true)?;
257 let matches_regex = |dir_entry: &std::fs::DirEntry| {
258 regex
259 .is_match(dir_entry.file_name().to_string_lossy().as_ref())
260 .unwrap_or(false)
261 };
262
263 let mut matching_paths_in_dir: Vec<_> = current_path
264 .read_dir()
265 .map_or_else(|_| vec![], |dir| dir.into_iter().collect())
266 .into_iter()
267 .filter_map(|result| result.ok())
268 .filter(matches_regex)
269 .filter(matches_dotfile_policy)
270 .map(|entry| entry.path())
271 .collect();
272
273 matching_paths_in_dir.sort();
274
275 paths_so_far.append(&mut matching_paths_in_dir);
276 }
277 }
278
279 let results: Vec<_> = paths_so_far
280 .into_iter()
281 .filter_map(|path| {
282 if let Some(filter) = path_filter {
283 if !filter(path.as_path()) {
284 return None;
285 }
286 }
287
288 let path_str = path.to_string_lossy();
289 let mut path_ref = path_str.as_ref();
290
291 if let Some(prefix_to_remove) = &prefix_to_remove {
292 path_ref = path_ref.strip_prefix(prefix_to_remove).unwrap();
293 }
294
295 Some(path_ref.to_string())
296 })
297 .collect();
298
299 tracing::debug!(target: trace_categories::PATTERN, " => results: {results:?}");
300
301 Ok(results)
302 }
303
304 pub(crate) fn to_regex_str(
313 &self,
314 strict_prefix_match: bool,
315 strict_suffix_match: bool,
316 ) -> Result<String, error::Error> {
317 let mut regex_str = String::new();
318
319 if strict_prefix_match {
320 regex_str.push('^');
321 }
322
323 let mut current_pattern = String::new();
324 for piece in &self.pieces {
325 match piece {
326 PatternPiece::Pattern(s) => {
327 current_pattern.push_str(s);
328 }
329 PatternPiece::Literal(s) => {
330 for c in s.chars() {
331 current_pattern.push('\\');
332 current_pattern.push(c);
333 }
334 }
335 }
336 }
337
338 let regex_piece =
339 pattern_to_regex_str(current_pattern.as_str(), self.enable_extended_globbing)?;
340 regex_str.push_str(regex_piece.as_str());
341
342 if strict_suffix_match {
343 regex_str.push('$');
344 }
345
346 Ok(regex_str)
347 }
348
349 pub(crate) fn to_regex(
358 &self,
359 strict_prefix_match: bool,
360 strict_suffix_match: bool,
361 ) -> Result<fancy_regex::Regex, error::Error> {
362 let regex_str = self.to_regex_str(strict_prefix_match, strict_suffix_match)?;
363
364 tracing::debug!(target: trace_categories::PATTERN, "pattern: '{self:?}' => regex: '{regex_str}'");
365
366 let re = regex::compile_regex(regex_str, self.case_insensitive, self.multiline)?;
367 Ok(re)
368 }
369
370 pub fn exactly_matches(&self, value: &str) -> Result<bool, error::Error> {
378 let re = self.to_regex(true, true)?;
379 Ok(re.is_match(value)?)
380 }
381}
382
383fn requires_expansion(s: &str) -> bool {
384 s.contains(['*', '?', '[', ']', '(', ')'])
386}
387
388fn pattern_to_regex_str(
389 pattern: &str,
390 enable_extended_globbing: bool,
391) -> Result<String, error::Error> {
392 Ok(brush_parser::pattern::pattern_to_regex_str(
393 pattern,
394 enable_extended_globbing,
395 )?)
396}
397
398#[expect(clippy::ref_option)]
405pub(crate) fn remove_largest_matching_prefix<'a>(
406 s: &'a str,
407 pattern: &Option<Pattern>,
408) -> Result<&'a str, error::Error> {
409 if let Some(pattern) = pattern {
410 let indices = s.char_indices().rev();
411 let mut last_idx = s.len();
412
413 #[allow(
414 clippy::string_slice,
415 reason = "because we get the indices from char_indices()"
416 )]
417 for (idx, _) in indices {
418 let prefix = &s[0..last_idx];
419 if pattern.exactly_matches(prefix)? {
420 return Ok(&s[last_idx..]);
421 }
422
423 last_idx = idx;
424 }
425 }
426 Ok(s)
427}
428
429#[expect(clippy::ref_option)]
436pub(crate) fn remove_smallest_matching_prefix<'a>(
437 s: &'a str,
438 pattern: &Option<Pattern>,
439) -> Result<&'a str, error::Error> {
440 if let Some(pattern) = pattern {
441 let mut indices = s.char_indices();
442
443 #[allow(
444 clippy::string_slice,
445 reason = "because we get the indices from char_indices()"
446 )]
447 while indices.next().is_some() {
448 let next_index = indices.offset();
449 let prefix = &s[0..next_index];
450 if pattern.exactly_matches(prefix)? {
451 return Ok(&s[next_index..]);
452 }
453 }
454 }
455 Ok(s)
456}
457
458#[expect(clippy::ref_option)]
465pub(crate) fn remove_largest_matching_suffix<'a>(
466 s: &'a str,
467 pattern: &Option<Pattern>,
468) -> Result<&'a str, error::Error> {
469 if let Some(pattern) = pattern {
470 #[allow(
471 clippy::string_slice,
472 reason = "because we get the indices from char_indices()"
473 )]
474 for (idx, _) in s.char_indices() {
475 let suffix = &s[idx..];
476 if pattern.exactly_matches(suffix)? {
477 return Ok(&s[..idx]);
478 }
479 }
480 }
481 Ok(s)
482}
483
484#[expect(clippy::ref_option)]
491pub(crate) fn remove_smallest_matching_suffix<'a>(
492 s: &'a str,
493 pattern: &Option<Pattern>,
494) -> Result<&'a str, error::Error> {
495 if let Some(pattern) = pattern {
496 #[allow(
497 clippy::string_slice,
498 reason = "because we get the indices from char_indices()"
499 )]
500 for (idx, _) in s.char_indices().rev() {
501 let suffix = &s[idx..];
502 if pattern.exactly_matches(suffix)? {
503 return Ok(&s[..idx]);
504 }
505 }
506 }
507 Ok(s)
508}
509
510#[cfg(test)]
511#[expect(clippy::panic_in_result_fn)]
512mod tests {
513 use super::*;
514 use anyhow::Result;
515
516 fn pattern_to_exact_regex_str<P>(pattern: P) -> Result<String, error::Error>
517 where
518 P: Into<Pattern>,
519 {
520 let pattern: Pattern = pattern
521 .into()
522 .set_extended_globbing(true)
523 .set_multiline(false);
524
525 pattern.to_regex_str(true, true)
526 }
527
528 #[test]
529 fn test_pattern_translation() -> Result<()> {
530 assert_eq!(pattern_to_exact_regex_str("a")?.as_str(), "^a$");
531 assert_eq!(pattern_to_exact_regex_str("a*")?.as_str(), "^a.*$");
532 assert_eq!(pattern_to_exact_regex_str("a?")?.as_str(), "^a.$");
533 assert_eq!(pattern_to_exact_regex_str("a@(b|c)")?.as_str(), "^a(b|c)$");
534 assert_eq!(pattern_to_exact_regex_str("a?(b|c)")?.as_str(), "^a(b|c)?$");
535 assert_eq!(
536 pattern_to_exact_regex_str("a*(ab|ac)")?.as_str(),
537 "^a(ab|ac)*$"
538 );
539 assert_eq!(
540 pattern_to_exact_regex_str("a+(ab|ac)")?.as_str(),
541 "^a(ab|ac)+$"
542 );
543 assert_eq!(pattern_to_exact_regex_str("[ab]")?.as_str(), "^[ab]$");
544 assert_eq!(pattern_to_exact_regex_str("[ab]*")?.as_str(), "^[ab].*$");
545 assert_eq!(
546 pattern_to_exact_regex_str("[<{().[]*")?.as_str(),
547 r"^[<{().\[].*$"
548 );
549 assert_eq!(pattern_to_exact_regex_str("[a-d]")?.as_str(), "^[a-d]$");
550 assert_eq!(pattern_to_exact_regex_str(r"\*")?.as_str(), r"^\*$");
551
552 Ok(())
553 }
554
555 #[test]
556 fn test_pattern_word_translation() -> Result<()> {
557 assert_eq!(
558 pattern_to_exact_regex_str(vec![PatternPiece::Pattern("a*".to_owned())])?.as_str(),
559 "^a.*$"
560 );
561 assert_eq!(
562 pattern_to_exact_regex_str(vec![
563 PatternPiece::Pattern("a*".to_owned()),
564 PatternPiece::Literal("b".to_owned()),
565 ])?
566 .as_str(),
567 "^a.*b$"
568 );
569 assert_eq!(
570 pattern_to_exact_regex_str(vec![
571 PatternPiece::Literal("a*".to_owned()),
572 PatternPiece::Pattern("b".to_owned()),
573 ])?
574 .as_str(),
575 r"^a\*b$"
576 );
577
578 Ok(())
579 }
580
581 #[test]
582 fn test_remove_largest_matching_prefix() -> Result<()> {
583 assert_eq!(
584 remove_largest_matching_prefix("ooof", &Some(Pattern::from("")))?,
585 "ooof"
586 );
587 assert_eq!(
588 remove_largest_matching_prefix("ooof", &Some(Pattern::from("x")))?,
589 "ooof"
590 );
591 assert_eq!(
592 remove_largest_matching_prefix("ooof", &Some(Pattern::from("o")))?,
593 "oof"
594 );
595 assert_eq!(
596 remove_largest_matching_prefix("ooof", &Some(Pattern::from("o*o")))?,
597 "f"
598 );
599 assert_eq!(
600 remove_largest_matching_prefix("ooof", &Some(Pattern::from("o*")))?,
601 ""
602 );
603 assert_eq!(
604 remove_largest_matching_prefix("🚀🚀🚀rocket", &Some(Pattern::from("🚀")))?,
605 "🚀🚀rocket"
606 );
607 Ok(())
608 }
609
610 #[test]
611 fn test_remove_smallest_matching_prefix() -> Result<()> {
612 assert_eq!(
613 remove_smallest_matching_prefix("ooof", &Some(Pattern::from("")))?,
614 "ooof"
615 );
616 assert_eq!(
617 remove_smallest_matching_prefix("ooof", &Some(Pattern::from("x")))?,
618 "ooof"
619 );
620 assert_eq!(
621 remove_smallest_matching_prefix("ooof", &Some(Pattern::from("o")))?,
622 "oof"
623 );
624 assert_eq!(
625 remove_smallest_matching_prefix("ooof", &Some(Pattern::from("o*o")))?,
626 "of"
627 );
628 assert_eq!(
629 remove_smallest_matching_prefix("ooof", &Some(Pattern::from("o*")))?,
630 "oof"
631 );
632 assert_eq!(
633 remove_smallest_matching_prefix("ooof", &Some(Pattern::from("ooof")))?,
634 ""
635 );
636 assert_eq!(
637 remove_smallest_matching_prefix("🚀🚀🚀rocket", &Some(Pattern::from("🚀")))?,
638 "🚀🚀rocket"
639 );
640 Ok(())
641 }
642
643 #[test]
644 fn test_remove_largest_matching_suffix() -> Result<()> {
645 assert_eq!(
646 remove_largest_matching_suffix("foo", &Some(Pattern::from("")))?,
647 "foo"
648 );
649 assert_eq!(
650 remove_largest_matching_suffix("foo", &Some(Pattern::from("x")))?,
651 "foo"
652 );
653 assert_eq!(
654 remove_largest_matching_suffix("foo", &Some(Pattern::from("o")))?,
655 "fo"
656 );
657 assert_eq!(
658 remove_largest_matching_suffix("foo", &Some(Pattern::from("o*")))?,
659 "f"
660 );
661 assert_eq!(
662 remove_largest_matching_suffix("foo", &Some(Pattern::from("foo")))?,
663 ""
664 );
665 assert_eq!(
666 remove_largest_matching_suffix("rocket🚀🚀🚀", &Some(Pattern::from("🚀")))?,
667 "rocket🚀🚀"
668 );
669 Ok(())
670 }
671
672 #[test]
673 fn test_remove_smallest_matching_suffix() -> Result<()> {
674 assert_eq!(
675 remove_smallest_matching_suffix("fooo", &Some(Pattern::from("")))?,
676 "fooo"
677 );
678 assert_eq!(
679 remove_smallest_matching_suffix("fooo", &Some(Pattern::from("x")))?,
680 "fooo"
681 );
682 assert_eq!(
683 remove_smallest_matching_suffix("fooo", &Some(Pattern::from("o")))?,
684 "foo"
685 );
686 assert_eq!(
687 remove_smallest_matching_suffix("fooo", &Some(Pattern::from("o*o")))?,
688 "fo"
689 );
690 assert_eq!(
691 remove_smallest_matching_suffix("fooo", &Some(Pattern::from("o*")))?,
692 "foo"
693 );
694 assert_eq!(
695 remove_smallest_matching_suffix("fooo", &Some(Pattern::from("fooo")))?,
696 ""
697 );
698 assert_eq!(
699 remove_smallest_matching_suffix("rocket🚀🚀🚀", &Some(Pattern::from("🚀")))?,
700 "rocket🚀🚀"
701 );
702 Ok(())
703 }
704
705 #[test]
706 #[expect(clippy::cognitive_complexity)]
707 fn test_matching() -> Result<()> {
708 assert!(Pattern::from("abc").exactly_matches("abc")?);
709
710 assert!(!Pattern::from("abc").exactly_matches("ABC")?);
711 assert!(!Pattern::from("abc").exactly_matches("xabcx")?);
712 assert!(!Pattern::from("abc").exactly_matches("")?);
713 assert!(!Pattern::from("abc").exactly_matches("abcd")?);
714 assert!(!Pattern::from("abc").exactly_matches("def")?);
715
716 assert!(Pattern::from("*").exactly_matches("")?);
717 assert!(Pattern::from("*").exactly_matches("abc")?);
718 assert!(Pattern::from("*").exactly_matches(" ")?);
719
720 assert!(Pattern::from("a*").exactly_matches("a")?);
721 assert!(Pattern::from("a*").exactly_matches("ab")?);
722 assert!(Pattern::from("a*").exactly_matches("a ")?);
723
724 assert!(!Pattern::from("a*").exactly_matches("A")?);
725 assert!(!Pattern::from("a*").exactly_matches("")?);
726 assert!(!Pattern::from("a*").exactly_matches("bc")?);
727 assert!(!Pattern::from("a*").exactly_matches("xax")?);
728 assert!(!Pattern::from("a*").exactly_matches(" a")?);
729
730 assert!(Pattern::from("*a").exactly_matches("a")?);
731 assert!(Pattern::from("*a").exactly_matches("ba")?);
732 assert!(Pattern::from("*a").exactly_matches("aa")?);
733 assert!(Pattern::from("*a").exactly_matches(" a")?);
734
735 assert!(!Pattern::from("*a").exactly_matches("BA")?);
736 assert!(!Pattern::from("*a").exactly_matches("")?);
737 assert!(!Pattern::from("*a").exactly_matches("ab")?);
738 assert!(!Pattern::from("*a").exactly_matches("xax")?);
739
740 Ok(())
741 }
742
743 fn make_extglob(s: &str) -> Pattern {
744 let pattern = Pattern::from(s).set_extended_globbing(true);
745 let regex_str = pattern.to_regex_str(true, true).unwrap();
746 eprintln!("pattern: '{s}' => regex: '{regex_str}'");
747
748 pattern
749 }
750
751 #[test]
752 fn test_extglob_or_matching() -> Result<()> {
753 assert!(make_extglob("@(a|b)").exactly_matches("a")?);
754 assert!(make_extglob("@(a|b)").exactly_matches("b")?);
755
756 assert!(!make_extglob("@(a|b)").exactly_matches("")?);
757 assert!(!make_extglob("@(a|b)").exactly_matches("c")?);
758 assert!(!make_extglob("@(a|b)").exactly_matches("ab")?);
759
760 assert!(!make_extglob("@(a|b)").exactly_matches("")?);
761 assert!(make_extglob("@(a*b|b)").exactly_matches("ab")?);
762 assert!(make_extglob("@(a*b|b)").exactly_matches("axb")?);
763 assert!(make_extglob("@(a*b|b)").exactly_matches("b")?);
764
765 assert!(!make_extglob("@(a*b|b)").exactly_matches("a")?);
766
767 Ok(())
768 }
769
770 #[test]
771 fn test_extglob_not_matching() -> Result<()> {
772 assert!(make_extglob("!(a)").exactly_matches("")?);
774 assert!(make_extglob("!(a)").exactly_matches(" ")?);
775 assert!(make_extglob("!(a)").exactly_matches("x")?);
776 assert!(make_extglob("!(a)").exactly_matches(" a ")?);
777 assert!(make_extglob("!(a)").exactly_matches("a ")?);
778 assert!(make_extglob("!(a)").exactly_matches("aa")?);
779 assert!(!make_extglob("!(a)").exactly_matches("a")?);
780
781 assert!(make_extglob("a!(a)a").exactly_matches("aa")?);
782 assert!(make_extglob("a!(a)a").exactly_matches("aaaa")?);
783 assert!(make_extglob("a!(a)a").exactly_matches("aba")?);
784 assert!(!make_extglob("a!(a)a").exactly_matches("a")?);
785 assert!(!make_extglob("a!(a)a").exactly_matches("aaa")?);
786 assert!(!make_extglob("a!(a)a").exactly_matches("baaa")?);
787
788 assert!(make_extglob("!(a|b)").exactly_matches("c")?);
790 assert!(make_extglob("!(a|b)").exactly_matches("ab")?);
791 assert!(make_extglob("!(a|b)").exactly_matches("aa")?);
792 assert!(make_extglob("!(a|b)").exactly_matches("bb")?);
793 assert!(!make_extglob("!(a|b)").exactly_matches("a")?);
794 assert!(!make_extglob("!(a|b)").exactly_matches("b")?);
795
796 Ok(())
797 }
798
799 #[test]
800 fn test_extglob_advanced_not_matching() -> Result<()> {
801 assert!(make_extglob("!(a*)").exactly_matches("b")?);
802 assert!(make_extglob("!(a*)").exactly_matches("")?);
803 assert!(!make_extglob("!(a*)").exactly_matches("a")?);
804 assert!(!make_extglob("!(a*)").exactly_matches("abc")?);
805 assert!(!make_extglob("!(a*)").exactly_matches("aabc")?);
806
807 Ok(())
808 }
809
810 #[test]
811 fn test_extglob_not_degenerate_matching() -> Result<()> {
812 assert!(make_extglob("!()").exactly_matches("a")?);
814 assert!(!make_extglob("!()").exactly_matches("")?);
815
816 Ok(())
817 }
818
819 #[test]
820 fn test_extglob_zero_or_more_matching() -> Result<()> {
821 assert!(make_extglob("x*(a)x").exactly_matches("xx")?);
822 assert!(make_extglob("x*(a)x").exactly_matches("xax")?);
823 assert!(make_extglob("x*(a)x").exactly_matches("xaax")?);
824
825 assert!(!make_extglob("x*(a)x").exactly_matches("x")?);
826 assert!(!make_extglob("x*(a)x").exactly_matches("xa")?);
827 assert!(!make_extglob("x*(a)x").exactly_matches("xxx")?);
828
829 assert!(make_extglob("*(a|b)").exactly_matches("")?);
830 assert!(make_extglob("*(a|b)").exactly_matches("a")?);
831 assert!(make_extglob("*(a|b)").exactly_matches("b")?);
832 assert!(make_extglob("*(a|b)").exactly_matches("aba")?);
833 assert!(make_extglob("*(a|b)").exactly_matches("aaa")?);
834
835 assert!(!make_extglob("*(a|b)").exactly_matches("c")?);
836 assert!(!make_extglob("*(a|b)").exactly_matches("ca")?);
837
838 Ok(())
839 }
840
841 #[test]
842 fn test_extglob_one_or_more_matching() -> Result<()> {
843 fn make_extglob(s: &str) -> Pattern {
844 Pattern::from(s).set_extended_globbing(true)
845 }
846
847 assert!(make_extglob("x+(a)x").exactly_matches("xax")?);
848 assert!(make_extglob("x+(a)x").exactly_matches("xaax")?);
849
850 assert!(!make_extglob("x+(a)x").exactly_matches("xx")?);
851 assert!(!make_extglob("x+(a)x").exactly_matches("x")?);
852 assert!(!make_extglob("x+(a)x").exactly_matches("xa")?);
853 assert!(!make_extglob("x+(a)x").exactly_matches("xxx")?);
854
855 assert!(make_extglob("+(a|b)").exactly_matches("a")?);
856 assert!(make_extglob("+(a|b)").exactly_matches("b")?);
857 assert!(make_extglob("+(a|b)").exactly_matches("aba")?);
858 assert!(make_extglob("+(a|b)").exactly_matches("aaa")?);
859
860 assert!(!make_extglob("+(a|b)").exactly_matches("")?);
861 assert!(!make_extglob("+(a|b)").exactly_matches("c")?);
862 assert!(!make_extglob("+(a|b)").exactly_matches("ca")?);
863
864 assert!(make_extglob("+(x+(ab)y)").exactly_matches("xaby")?);
865 assert!(make_extglob("+(x+(ab)y)").exactly_matches("xababy")?);
866 assert!(make_extglob("+(x+(ab)y)").exactly_matches("xabababy")?);
867 assert!(make_extglob("+(x+(ab)y)").exactly_matches("xabababyxabababyxabababy")?);
868
869 assert!(!make_extglob("+(x+(ab)y)").exactly_matches("xy")?);
870 assert!(!make_extglob("+(x+(ab)y)").exactly_matches("xay")?);
871 assert!(!make_extglob("+(x+(ab)y)").exactly_matches("xyxy")?);
872
873 Ok(())
874 }
875}