brush_core/
patterns.rs

1//! Shell patterns
2
3use crate::{error, regex, trace_categories};
4use std::{
5    collections::VecDeque,
6    path::{Path, PathBuf},
7};
8
9/// Represents a piece of a shell pattern.
10#[derive(Clone, Debug)]
11pub(crate) enum PatternPiece {
12    /// A pattern that should be interpreted as a shell pattern.
13    Pattern(String),
14    /// A literal string that should be matched exactly.
15    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/// Options for filename expansion.
30#[derive(Clone, Debug, Default)]
31pub(crate) struct FilenameExpansionOptions {
32    pub require_dot_in_pattern_to_match_dot_files: bool,
33}
34
35/// Encapsulates a shell pattern.
36#[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    /// Enables (or disables) extended globbing support for this pattern.
93    ///
94    /// # Arguments
95    ///
96    /// * `value` - Whether or not to enable extended globbing (extglob).
97    #[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    /// Enables (or disables) multiline support for this pattern.
104    ///
105    /// # Arguments
106    ///
107    /// * `value` - Whether or not to enable multiline matching.
108    #[must_use]
109    pub const fn set_multiline(mut self, value: bool) -> Self {
110        self.multiline = value;
111        self
112    }
113
114    /// Enables (or disables) case-insensitive matching for this pattern.
115    ///
116    /// # Arguments
117    ///
118    /// * `value` - Whether or not to enable case-insensitive matching.
119    #[must_use]
120    pub const fn set_case_insensitive(mut self, value: bool) -> Self {
121        self.case_insensitive = value;
122        self
123    }
124
125    /// Returns whether or not the pattern is empty.
126    pub fn is_empty(&self) -> bool {
127        self.pieces.iter().all(|p| p.as_str().is_empty())
128    }
129
130    /// Placeholder function that always returns true.
131    pub(crate) const fn accept_all_expand_filter(_path: &Path) -> bool {
132        true
133    }
134
135    /// Expands the pattern into a list of matching file paths.
136    ///
137    /// # Arguments
138    ///
139    /// * `working_dir` - The current working directory, used for relative paths.
140    /// * `path_filter` - Optionally provides a function that filters paths after expansion.
141    #[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 the pattern is completely empty, then short-circuit the function; there's
153        // no reason to proceed onward when we know there's no expansions.
154        if self.is_empty() {
155            return Ok(vec![]);
156
157        // Similarly, if we're *confident* the pattern doesn't require expansion, then we
158        // know there's a single expansion (before filtering).
159        } 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        // Check if the path appears to be absolute.
200        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            // TODO: Figure out appropriate thing to do on non-Unix platforms.
212            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    /// Converts the pattern to a regular expression string.
305    ///
306    /// # Arguments
307    ///
308    /// * `strict_prefix_match` - Whether or not the pattern should strictly match the beginning of
309    ///   the string.
310    /// * `strict_suffix_match` - Whether or not the pattern should strictly match the end of the
311    ///   string.
312    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    /// Converts the pattern to a regular expression.
350    ///
351    /// # Arguments
352    ///
353    /// * `strict_prefix_match` - Whether or not the pattern should strictly match the beginning of
354    ///   the string.
355    /// * `strict_suffix_match` - Whether or not the pattern should strictly match the end of the
356    ///   string.
357    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    /// Checks if the pattern exactly matches the given string. An error result
371    /// is returned if the pattern is found to be invalid or malformed
372    /// during processing.
373    ///
374    /// # Arguments
375    ///
376    /// * `value` - The string to check for a match.
377    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    // TODO: Make this more accurate.
385    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/// Removes the largest matching prefix from a string that matches the given pattern.
399///
400/// # Arguments
401///
402/// * `s` - The string to remove the prefix from.
403/// * `pattern` - The pattern to match.
404#[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/// Removes the smallest matching prefix from a string that matches the given pattern.
430///
431/// # Arguments
432///
433/// * `s` - The string to remove the prefix from.
434/// * `pattern` - The pattern to match.
435#[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/// Removes the largest matching suffix from a string that matches the given pattern.
459///
460/// # Arguments
461///
462/// * `s` - The string to remove the suffix from.
463/// * `pattern` - The pattern to match.
464#[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/// Removes the smallest matching suffix from a string that matches the given pattern.
485///
486/// # Arguments
487///
488/// * `s` - The string to remove the suffix from.
489/// * `pattern` - The pattern to match.
490#[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        // Basic cases.
773        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        // Alternates.
789        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        // Degenerate case.
813        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}