Skip to main content

lerna/
glob.rs

1// Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
2//! Glob pattern matching utilities
3
4/// A glob pattern for filtering names
5#[derive(Clone, Debug, Default, PartialEq, Eq)]
6pub struct Glob {
7    /// Patterns to include
8    pub include: Vec<String>,
9    /// Patterns to exclude
10    pub exclude: Vec<String>,
11}
12
13impl Glob {
14    pub fn new() -> Self {
15        Self::default()
16    }
17
18    pub fn with_include(mut self, patterns: Vec<String>) -> Self {
19        self.include = patterns;
20        self
21    }
22
23    pub fn with_exclude(mut self, patterns: Vec<String>) -> Self {
24        self.exclude = patterns;
25        self
26    }
27
28    /// Filter a list of names based on include and exclude patterns
29    pub fn filter(&self, names: &[String]) -> Vec<String> {
30        names
31            .iter()
32            .filter(|name| {
33                self.matches_any(name, &self.include) && !self.matches_any(name, &self.exclude)
34            })
35            .cloned()
36            .collect()
37    }
38
39    /// Check if a name matches any of the given glob patterns
40    fn matches_any(&self, name: &str, patterns: &[String]) -> bool {
41        for pattern in patterns {
42            if glob_match(pattern, name) {
43                return true;
44            }
45        }
46        false
47    }
48}
49
50/// Simple glob pattern matching (supports * and ?)
51fn glob_match(pattern: &str, text: &str) -> bool {
52    let pattern_chars = pattern.chars().peekable();
53    let text_chars = text.chars().peekable();
54
55    glob_match_impl(
56        &mut pattern_chars.collect::<Vec<_>>(),
57        &text_chars.collect::<Vec<_>>(),
58    )
59}
60
61fn glob_match_impl(pattern: &[char], text: &[char]) -> bool {
62    let mut pi = 0;
63    let mut ti = 0;
64    let mut star_pi: Option<usize> = None;
65    let mut star_ti: Option<usize> = None;
66
67    while ti < text.len() {
68        if pi < pattern.len() && (pattern[pi] == '?' || pattern[pi] == text[ti]) {
69            // Characters match or ? matches any single character
70            pi += 1;
71            ti += 1;
72        } else if pi < pattern.len() && pattern[pi] == '*' {
73            // * matches zero or more characters
74            star_pi = Some(pi);
75            star_ti = Some(ti);
76            pi += 1;
77        } else if let Some(spi) = star_pi {
78            // Mismatch, but we have a previous * - backtrack
79            pi = spi + 1;
80            star_ti = Some(star_ti.unwrap() + 1);
81            ti = star_ti.unwrap();
82        } else {
83            // No match
84            return false;
85        }
86    }
87
88    // Check remaining pattern characters (should all be *)
89    while pi < pattern.len() && pattern[pi] == '*' {
90        pi += 1;
91    }
92
93    pi == pattern.len()
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn test_glob_match_exact() {
102        assert!(glob_match("abc", "abc"));
103        assert!(!glob_match("abc", "abd"));
104        assert!(!glob_match("abc", "ab"));
105        assert!(!glob_match("abc", "abcd"));
106    }
107
108    #[test]
109    fn test_glob_match_star() {
110        assert!(glob_match("*", "anything"));
111        assert!(glob_match("*", ""));
112        assert!(glob_match("a*", "abc"));
113        assert!(glob_match("*c", "abc"));
114        assert!(glob_match("a*c", "abc"));
115        assert!(glob_match("a*c", "ac"));
116        assert!(glob_match("a*c", "aXYZc"));
117        assert!(!glob_match("a*c", "ab"));
118    }
119
120    #[test]
121    fn test_glob_match_question() {
122        assert!(glob_match("?", "a"));
123        assert!(!glob_match("?", ""));
124        assert!(!glob_match("?", "ab"));
125        assert!(glob_match("a?c", "abc"));
126        assert!(!glob_match("a?c", "ac"));
127        assert!(!glob_match("a?c", "abbc"));
128    }
129
130    #[test]
131    fn test_glob_match_combined() {
132        assert!(glob_match("a*b?c", "aXXXbYc"));
133        assert!(glob_match("*.txt", "file.txt"));
134        assert!(!glob_match("*.txt", "file.py"));
135        assert!(glob_match("test_*", "test_foo"));
136        assert!(glob_match("test_*", "test_"));
137    }
138
139    #[test]
140    fn test_glob_filter() {
141        let glob = Glob::new()
142            .with_include(vec!["*.py".to_string(), "*.txt".to_string()])
143            .with_exclude(vec!["test_*".to_string()]);
144
145        let names = vec![
146            "main.py".to_string(),
147            "test_main.py".to_string(),
148            "readme.txt".to_string(),
149            "config.yaml".to_string(),
150        ];
151
152        let filtered = glob.filter(&names);
153        assert_eq!(
154            filtered,
155            vec!["main.py".to_string(), "readme.txt".to_string()]
156        );
157    }
158
159    #[test]
160    fn test_glob_filter_empty() {
161        let glob = Glob::new();
162        let names = vec!["a".to_string(), "b".to_string()];
163        // With no include patterns, nothing matches
164        let filtered = glob.filter(&names);
165        assert!(filtered.is_empty());
166    }
167
168    #[test]
169    fn test_glob_filter_include_all() {
170        let glob = Glob::new().with_include(vec!["*".to_string()]);
171        let names = vec!["a".to_string(), "b".to_string(), "c".to_string()];
172        let filtered = glob.filter(&names);
173        assert_eq!(filtered, names);
174    }
175}