1use crate::error;
4
5pub enum ExtendedGlobKind {
7 Plus,
10 At,
12 Exclamation,
14 Question,
17 Star,
20}
21
22pub fn pattern_to_regex_str(
35 pattern: &str,
36 enable_extended_globbing: bool,
37) -> Result<String, error::WordParseError> {
38 let regex_str = pattern_to_regex_translator::pattern(pattern, enable_extended_globbing)
39 .map_err(|e| error::WordParseError::Pattern(e.into()))?;
40 Ok(regex_str)
41}
42
43fn bracket_range(from: (String, char), to: (String, char)) -> Option<String> {
46 let (from_str, from_c) = from;
47 let (to_str, to_c) = to;
48
49 if from_c <= to_c {
50 Some(std::format!("{from_str}-{to_str}"))
51 } else {
52 None
53 }
54}
55
56peg::parser! {
57 grammar pattern_to_regex_translator(enable_extended_globbing: bool) for str {
58 pub(crate) rule pattern() -> String =
59 pieces:(pattern_piece()*) {
60 pieces.join("")
61 }
62
63 rule pattern_piece() -> String =
64 escape_sequence() /
65 bracket_expression() /
66 extglob_enabled() s:extended_glob_pattern() { s } /
67 wildcard() /
68 [c if regex_char_needs_escaping(c)] {
69 let mut s = '\\'.to_string();
70 s.push(c);
71 s
72 } /
73 [c] { c.to_string() }
74
75 rule escape_sequence() -> String =
76 sequence:$(['\\'] [c if regex_char_needs_escaping(c)]) { sequence.to_owned() } /
77 ['\\'] [c] { c.to_string() }
78
79 rule bracket_expression() -> String =
80 "[" invert:(invert_char()?) leading:(leading_bracket_member()?) members:bracket_member()* "]" {
81 let mut members = leading
82 .into_iter()
83 .chain(members)
84 .flatten()
85 .collect::<Vec<_>>();
86
87 if members.is_empty() {
91 if invert.is_some() {
92 String::from(".")
93 } else {
94 String::from("(?!)")
95 }
96 } else {
97 if invert.is_some() {
98 members.insert(0, String::from("^"));
99 }
100
101 std::format!("[{}]", members.join(""))
102 }
103 }
104
105 rule leading_bracket_member() -> Option<String> =
109 from:close_bracket_member() "-" to:single_char_bracket_member() {
110 bracket_range(from, to)
111 } /
112 from:close_bracket_member() { Some(from.0) }
113
114 rule close_bracket_member() -> (String, char) =
115 "]" { (String::from(r"\]"), ']') }
116
117 rule invert_char() -> bool =
118 ['!' | '^'] { true }
119
120 rule bracket_member() -> Option<String> =
121 e:char_class_expression() { Some(e) } /
122 r:char_range() { r } /
123 m:single_char_bracket_member() {
124 let (char_str, _) = m;
125 Some(char_str)
126 }
127
128 rule char_class_expression() -> String =
129 e:$("[:" char_class() ":]") { e.to_owned() }
130
131 rule char_class() =
132 "alnum" / "alpha" / "blank" / "cntrl" / "digit" / "graph" / "lower" / "print" / "punct" / "space" / "upper"/ "xdigit"
133
134 rule char_range() -> Option<String> =
135 from:single_char_bracket_member() "-" to:single_char_bracket_member() {
136 bracket_range(from, to)
137 }
138
139 rule single_char_bracket_member() -> (String, char) =
140 ['\\'] [c] { (std::format!("\\{c}"), c) } /
142 [c if matches!(c, '[' | '^' | '-')] { (std::format!("\\{c}"), c) } /
146 [c if c != ']'] { (c.to_string(), c) }
148
149 rule wildcard() -> String =
150 "?" { String::from(".") } /
151 "*" { String::from(".*") }
152
153 rule extglob_enabled() -> () =
154 &[_] {? if enable_extended_globbing { Ok(()) } else { Err("extglob disabled") } }
155
156 pub(crate) rule extended_glob_pattern() -> String =
157 kind:extended_glob_prefix() "(" branches:extended_glob_body() ")" {
158 let mut s = String::new();
159
160 if matches!(kind, ExtendedGlobKind::Exclamation) {
162 if !branches.is_empty() {
163 s.push_str("(?:(?!");
164 s.push_str(&branches.join("|"));
165 s.push_str(").*|(?>");
166 s.push_str(&branches.join("|"));
167 s.push_str(").+?|)");
168 } else {
169 s.push_str("(?:.+)");
170 }
171 } else {
172 s.push('(');
173 s.push_str(&branches.join("|"));
174 s.push(')');
175
176 match kind {
177 ExtendedGlobKind::Plus => s.push('+'),
178 ExtendedGlobKind::Question => s.push('?'),
179 ExtendedGlobKind::Star => s.push('*'),
180 ExtendedGlobKind::At | ExtendedGlobKind::Exclamation => (),
181 }
182 }
183
184 s
185 }
186
187 rule extended_glob_prefix() -> ExtendedGlobKind =
188 "+" { ExtendedGlobKind::Plus } /
189 "@" { ExtendedGlobKind::At } /
190 "!" { ExtendedGlobKind::Exclamation } /
191 "?" { ExtendedGlobKind::Question } /
192 "*" { ExtendedGlobKind::Star }
193
194 pub(crate) rule extended_glob_body() -> Vec<String> =
195 &[')'] { vec![] } /
197 extended_glob_branch() ** "|"
199
200 rule extended_glob_branch() -> String =
201 &['|' | ')'] { String::new() } /
203 pieces:(!['|' | ')'] piece:pattern_piece() { piece })+ {
204 pieces.join("")
205 }
206
207 rule glob_piece() =
209 bracket_expression() /
210 extglob_enabled() extended_glob_pattern() /
211 wildcard()
212
213 rule non_glob_piece() =
215 escape_sequence() /
216 !glob_piece() [_]
217
218 pub(crate) rule has_glob_metacharacters() -> bool =
223 non_glob_piece()* glob_piece() [_]* { true }
224 }
225}
226
227pub fn pattern_has_glob_metacharacters(pattern: &str, enable_extended_globbing: bool) -> bool {
238 pattern_to_regex_translator::has_glob_metacharacters(pattern, enable_extended_globbing)
239 .unwrap_or(false)
240}
241
242pub const fn regex_char_needs_escaping(c: char) -> bool {
249 matches!(
250 c,
251 '[' | ']' | '(' | ')' | '{' | '}' | '*' | '?' | '.' | '+' | '^' | '$' | '|' | '\\' | '-'
252 )
253}
254
255#[cfg(test)]
256#[expect(clippy::panic_in_result_fn)]
257mod tests {
258 use anyhow::Result;
259
260 use super::*;
261
262 #[test]
263 fn test_bracket_exprs() -> Result<()> {
264 assert_eq!(pattern_to_regex_str("[a-z]", true)?, "[a-z]");
265 assert_eq!(pattern_to_regex_str("[z-a]", true)?, "(?!)");
266 assert_eq!(pattern_to_regex_str("[+-/]", true)?, "[+-/]");
267 assert_eq!(pattern_to_regex_str(r"[\*-/]", true)?, r"[\*-/]");
268 assert_eq!(pattern_to_regex_str("[abc]", true)?, "[abc]");
269 assert_eq!(pattern_to_regex_str(r"[\(]", true)?, r"[\(]");
270 assert_eq!(pattern_to_regex_str(r"[(]", true)?, "[(]");
271 assert_eq!(pattern_to_regex_str("[[:digit:]]", true)?, "[[:digit:]]");
272 assert_eq!(pattern_to_regex_str(r"[-(),!]*", true)?, r"[\-(),!].*");
273 assert_eq!(
274 pattern_to_regex_str(r"[-\(\),\!]*", true)?,
275 r"[\-\(\),\!].*"
276 );
277 assert_eq!(pattern_to_regex_str(r"[a\-b]", true)?, r"[a\-b]");
278 assert_eq!(pattern_to_regex_str(r"[a\-\*]", true)?, r"[a\-\*]");
279
280 assert_eq!(pattern_to_regex_str("[]]", true)?, r"[\]]");
283 assert_eq!(pattern_to_regex_str("[][]", true)?, r"[\]\[]");
284 assert_eq!(pattern_to_regex_str("[]abc[]", true)?, r"[\]abc\[]");
285 assert_eq!(pattern_to_regex_str("[]]]", true)?, r"[\]]\]");
286 assert_eq!(pattern_to_regex_str("[!]]", true)?, r"[^\]]");
287 assert_eq!(pattern_to_regex_str("[^][]", true)?, r"[^\]\[]");
288
289 assert_eq!(pattern_to_regex_str("[]-a]", true)?, r"[\]-a]");
291 assert_eq!(pattern_to_regex_str("[]-a[]", true)?, r"[\]-a\[]");
292 assert_eq!(pattern_to_regex_str("[]-A]", true)?, "(?!)");
293 assert_eq!(pattern_to_regex_str("[!]-A]", true)?, ".");
294
295 assert_eq!(pattern_to_regex_str("[9-0^a]", true)?, r"[\^a]");
299 assert_eq!(pattern_to_regex_str("[z-a^]", true)?, r"[\^]");
300 assert_eq!(pattern_to_regex_str("[+--]", true)?, r"[+-\-]");
301 assert_eq!(pattern_to_regex_str("[--/]", true)?, r"[\--/]");
302
303 assert_eq!(pattern_to_regex_str("[]", true)?, r"\[\]");
305 assert_eq!(pattern_to_regex_str("[!]", true)?, r"\[!\]");
306
307 Ok(())
308 }
309
310 #[test]
311 fn test_extended_glob() -> Result<()> {
312 assert_eq!(
313 pattern_to_regex_translator::extended_glob_pattern("@(a|b)", true)?,
314 "(a|b)"
315 );
316
317 assert_eq!(
318 pattern_to_regex_translator::extended_glob_pattern("@(|a)", true)?,
319 "(|a)"
320 );
321
322 assert_eq!(
323 pattern_to_regex_translator::extended_glob_pattern("@(|)", true)?,
324 "(|)"
325 );
326
327 assert_eq!(
328 pattern_to_regex_translator::extended_glob_body("ab|ac", true)?,
329 vec!["ab", "ac"],
330 );
331
332 assert_eq!(
333 pattern_to_regex_translator::extended_glob_pattern("*(ab|ac)", true)?,
334 "(ab|ac)*"
335 );
336
337 assert_eq!(
338 pattern_to_regex_translator::extended_glob_body("", true)?,
339 Vec::<String>::new(),
340 );
341
342 Ok(())
343 }
344
345 #[test]
346 fn test_has_glob_metacharacters() {
347 assert!(pattern_has_glob_metacharacters("*", false));
349 assert!(pattern_has_glob_metacharacters("?", false));
350 assert!(pattern_has_glob_metacharacters("a*b", false));
351 assert!(pattern_has_glob_metacharacters("a?b", false));
352
353 assert!(pattern_has_glob_metacharacters("[abc]", false));
355 assert!(pattern_has_glob_metacharacters("[a-z]", false));
356 assert!(pattern_has_glob_metacharacters("[!a]", false));
357
358 assert!(pattern_has_glob_metacharacters("[]]", false));
360 assert!(pattern_has_glob_metacharacters("[][]", false));
361 assert!(pattern_has_glob_metacharacters("[!]]", false));
362
363 assert!(!pattern_has_glob_metacharacters("]", false));
365 assert!(!pattern_has_glob_metacharacters("foo]", false));
366 assert!(!pattern_has_glob_metacharacters("a]b", false));
367
368 assert!(!pattern_has_glob_metacharacters("[", false));
370 assert!(!pattern_has_glob_metacharacters("[]", false));
371 assert!(!pattern_has_glob_metacharacters("[!]", false));
372 assert!(!pattern_has_glob_metacharacters("[abc", false));
373 assert!(!pattern_has_glob_metacharacters("a[b", false));
374
375 assert!(!pattern_has_glob_metacharacters("hello", false));
377 assert!(!pattern_has_glob_metacharacters("", false));
378
379 assert!(!pattern_has_glob_metacharacters(r"\*", false));
381 assert!(!pattern_has_glob_metacharacters(r"\?", false));
382 assert!(!pattern_has_glob_metacharacters(r"\[abc]", false));
383
384 assert!(!pattern_has_glob_metacharacters("@(a)", false));
386 assert!(!pattern_has_glob_metacharacters("!(a)", false));
387 assert!(!pattern_has_glob_metacharacters("+(a)", false));
388
389 assert!(pattern_has_glob_metacharacters("@(a)", true));
391 assert!(pattern_has_glob_metacharacters("!(a)", true));
392 assert!(pattern_has_glob_metacharacters("+(a)", true));
393
394 assert!(pattern_has_glob_metacharacters("*(a)", false));
396 assert!(pattern_has_glob_metacharacters("?(a)", false));
397 }
398}