1use std::fmt;
2use std::str::FromStr;
3
4use thiserror::Error;
5
6#[derive(Debug, Error)]
8pub enum GlobParseError {
9 #[error("No wildcard found: {src:?}")]
10 NoWildcard { src: String },
11 #[error("Multiple wildcards found: {src:?}")]
12 MultipleWildcards { src: String },
13 #[error("'**' appeared without '*': {src:?}")]
14 StrayRecursiveWildcard { src: String },
15}
16
17#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct GlobPattern {
20 branches: Vec<GlobBranch>,
21}
22
23impl GlobPattern {
24 pub fn new(src: &str) -> Self {
26 src.parse().unwrap()
27 }
28
29 pub fn do_match<'a>(&self, file_name: &'a str) -> Vec<&'a str> {
31 let mut matches = Vec::new();
32 for branch in &self.branches {
33 if let Some(m) = branch.do_match(file_name) {
34 matches.push(m);
35 }
36 }
37 matches
38 }
39
40 pub fn subst(&self, stem: &str) -> Vec<String> {
42 self.branches
43 .iter()
44 .filter_map(|branch| branch.subst(stem))
45 .collect::<Vec<_>>()
46 }
47
48 pub fn prefixes(&self) -> Vec<String> {
50 self.branches
51 .iter()
52 .map(|branch| branch.prefix.clone())
53 .collect::<Vec<_>>()
54 }
55}
56
57impl FromStr for GlobPattern {
58 type Err = GlobParseError;
59 fn from_str(src: &str) -> Result<Self, Self::Err> {
60 let branches = src
61 .split(",")
62 .map(|branch| branch.parse::<GlobBranch>())
63 .collect::<Result<Vec<_>, _>>()?;
64 Ok(Self { branches })
65 }
66}
67
68impl fmt::Display for GlobPattern {
69 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
70 for (i, branch) in self.branches.iter().enumerate() {
71 if i > 0 {
72 f.write_str(",")?;
73 }
74 write!(f, "{}", branch)?;
75 }
76 Ok(())
77 }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
81struct GlobBranch {
82 prefix: String,
83 wildcard: Wildcard,
84 suffix: String,
85}
86
87impl GlobBranch {
88 fn do_match<'a>(&self, file_name: &'a str) -> Option<&'a str> {
89 if file_name.starts_with(&self.prefix) && file_name.ends_with(&self.suffix) {
90 let stem = &file_name[self.prefix.len()..file_name.len() - self.suffix.len()];
91 if self.wildcard == Wildcard::Recursive || !stem.contains('/') {
92 return Some(stem);
93 }
94 }
95 None
96 }
97
98 fn subst(&self, stem: &str) -> Option<String> {
99 if self.wildcard == Wildcard::Recursive || !stem.contains('/') {
100 Some(format!("{}{}{}", self.prefix, stem, self.suffix))
101 } else {
102 None
103 }
104 }
105}
106
107impl FromStr for GlobBranch {
108 type Err = GlobParseError;
109 fn from_str(src: &str) -> Result<Self, Self::Err> {
110 let pos = src.find('*').ok_or_else(|| GlobParseError::NoWildcard {
111 src: src.to_owned(),
112 })?;
113 let (pos2, glob_type) = if src[pos..].starts_with("**/*") {
114 (pos + 4, Wildcard::Recursive)
115 } else if src[pos..].starts_with("**") {
116 return Err(GlobParseError::StrayRecursiveWildcard {
117 src: src.to_owned(),
118 });
119 } else {
120 (pos + 1, Wildcard::Single)
121 };
122 let suffix = &src[pos2..];
123 if suffix.contains('*') {
124 return Err(GlobParseError::MultipleWildcards {
125 src: src.to_owned(),
126 });
127 }
128 Ok(Self {
129 prefix: src[..pos].to_owned(),
130 wildcard: glob_type,
131 suffix: suffix.to_owned(),
132 })
133 }
134}
135
136impl fmt::Display for GlobBranch {
137 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
138 write!(f, "{}{}{}", self.prefix, self.wildcard, self.suffix)
139 }
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143enum Wildcard {
144 Recursive,
146 Single,
148}
149
150impl fmt::Display for Wildcard {
151 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
152 use Wildcard::*;
153 match self {
154 Recursive => f.write_str("**/*"),
155 Single => f.write_str("*"),
156 }
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn test_parse() {
166 assert_eq!(
167 GlobPattern::new("tests/fixtures/**/*-in.txt"),
168 GlobPattern {
169 branches: vec![GlobBranch {
170 prefix: "tests/fixtures/".to_owned(),
171 wildcard: Wildcard::Recursive,
172 suffix: "-in.txt".to_owned(),
173 }]
174 }
175 );
176
177 assert_eq!(
178 GlobPattern::new("tests/fixtures/*-out.txt"),
179 GlobPattern {
180 branches: vec![GlobBranch {
181 prefix: "tests/fixtures/".to_owned(),
182 wildcard: Wildcard::Single,
183 suffix: "-out.txt".to_owned(),
184 }]
185 }
186 );
187
188 assert_eq!(
189 GlobPattern::new("foo/*.txt,bar/*.txt"),
190 GlobPattern {
191 branches: vec![
192 GlobBranch {
193 prefix: "foo/".to_owned(),
194 wildcard: Wildcard::Single,
195 suffix: ".txt".to_owned(),
196 },
197 GlobBranch {
198 prefix: "bar/".to_owned(),
199 wildcard: Wildcard::Single,
200 suffix: ".txt".to_owned(),
201 }
202 ]
203 }
204 );
205 }
206
207 #[test]
208 fn test_parse_error() {
209 let e = "tests/fixtures/in.txt".parse::<GlobPattern>().unwrap_err();
210 assert_eq!(
211 e.to_string(),
212 "No wildcard found: \"tests/fixtures/in.txt\""
213 );
214
215 let e = "tests/fixtures/*/*/in.txt"
216 .parse::<GlobPattern>()
217 .unwrap_err();
218 assert_eq!(
219 e.to_string(),
220 "Multiple wildcards found: \"tests/fixtures/*/*/in.txt\""
221 );
222
223 let e = "tests/fixtures/**/in.txt"
224 .parse::<GlobPattern>()
225 .unwrap_err();
226 assert_eq!(
227 e.to_string(),
228 "'**' appeared without '*': \"tests/fixtures/**/in.txt\""
229 );
230 }
231
232 #[test]
233 fn test_stringify() {
234 let cases = [
235 "tests/fixtures/**/*-in.txt",
236 "tests/fixtures/*-out.txt",
237 "*.rs",
238 "tests/fixtures/**/*",
239 "foo/*.txt,bar/*.rs",
240 ];
241 for &case in &cases {
242 assert_eq!(GlobPattern::new(case).to_string(), case);
243 }
244 }
245
246 #[test]
247 fn test_match() {
248 let empty: Vec<&str> = vec![];
249
250 let pat = GlobPattern::new("tests/fixtures/**/*-in.txt");
251 assert_eq!(pat.do_match("tests/fixtures/foo-in.txt"), vec!["foo"]);
252 assert_eq!(
253 pat.do_match("tests/fixtures/foo/bar-in.txt"),
254 vec!["foo/bar"]
255 );
256 assert_eq!(pat.do_match("tests/fixtures/foo-out.txt"), empty);
257
258 let pat = GlobPattern::new("tests/fixtures/*-in.txt");
259 assert_eq!(pat.do_match("tests/fixtures/foo-in.txt"), vec!["foo"]);
260 assert_eq!(pat.do_match("tests/fixtures/foo/bar-in.txt"), empty);
261 assert_eq!(pat.do_match("tests/fixtures/foo-out.txt"), empty);
262
263 let pat = GlobPattern::new("foo/**/*.txt,foo/bar/**/*.txt");
264 assert_eq!(pat.do_match("foo/a.txt"), vec!["a"]);
265 assert_eq!(pat.do_match("foo/bar/a.txt"), vec!["bar/a", "a"]);
266 }
267
268 #[test]
269 fn test_subst() {
270 let empty: Vec<&str> = vec![];
271
272 let pat = GlobPattern::new("tests/fixtures/**/*-in.txt");
273 assert_eq!(
274 pat.subst("foo"),
275 vec!["tests/fixtures/foo-in.txt".to_owned()]
276 );
277 assert_eq!(
278 pat.subst("foo/bar"),
279 vec!["tests/fixtures/foo/bar-in.txt".to_owned()]
280 );
281
282 let pat = GlobPattern::new("tests/fixtures/*-in.txt");
283 assert_eq!(
284 pat.subst("foo"),
285 vec!["tests/fixtures/foo-in.txt".to_owned()]
286 );
287 assert_eq!(pat.subst("foo/bar"), empty);
288
289 let pat = GlobPattern::new("foo/*.txt,bar/*.rs");
290 assert_eq!(
291 pat.subst("a"),
292 vec!["foo/a.txt".to_owned(), "bar/a.rs".to_owned()]
293 );
294 assert_eq!(pat.subst("foo/bar"), empty);
295 }
296}