1#[derive(Clone, Debug, Default, PartialEq, Eq)]
6pub struct Glob {
7 pub include: Vec<String>,
9 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 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 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
50fn 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 pi += 1;
71 ti += 1;
72 } else if pi < pattern.len() && pattern[pi] == '*' {
73 star_pi = Some(pi);
75 star_ti = Some(ti);
76 pi += 1;
77 } else if let Some(spi) = star_pi {
78 pi = spi + 1;
80 star_ti = Some(star_ti.unwrap() + 1);
81 ti = star_ti.unwrap();
82 } else {
83 return false;
85 }
86 }
87
88 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 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}