Nushell plugin for interacting with D-Bus
1#[derive(Debug, Clone, PartialEq, Eq)]
2pub struct Pattern {
3 separator: Option<char>,
4 tokens: Vec<PatternToken>,
5}
6
7#[derive(Debug, Clone, PartialEq, Eq)]
8enum PatternToken {
9 Exact(String),
10 OneWildcard,
11 ManyWildcard,
12 AnyChar,
13}
14
15impl Pattern {
16 pub fn new(pattern: &str, separator: Option<char>) -> Pattern {
17 let mut tokens = vec![];
18 for ch in pattern.chars() {
19 match ch {
20 '*' => {
21 if tokens.last() == Some(&PatternToken::OneWildcard) {
22 *tokens.last_mut().unwrap() = PatternToken::ManyWildcard;
23 } else {
24 tokens.push(PatternToken::OneWildcard);
25 }
26 }
27 '?' => tokens.push(PatternToken::AnyChar),
28 _ => match tokens.last_mut() {
29 Some(PatternToken::Exact(s)) => s.push(ch),
30 _ => tokens.push(PatternToken::Exact(ch.into())),
31 },
32 }
33 }
34 Pattern { separator, tokens }
35 }
36
37 pub fn is_match(&self, string: &str) -> bool {
38 #[derive(Debug)]
39 enum MatchState {
40 Precise,
41 ScanAhead { stop_at_separator: bool },
42 }
43 let mut state = MatchState::Precise;
44 let mut tokens = &self.tokens[..];
45 let mut search_str = string;
46 while !tokens.is_empty() {
47 match tokens.first().unwrap() {
48 PatternToken::Exact(s) => {
49 if search_str.starts_with(s) {
50 // Exact match passed. Consume the token and string and continue
51 tokens = &tokens[1..];
52 search_str = &search_str[s.len()..];
53 state = MatchState::Precise;
54 } else {
55 match state {
56 MatchState::Precise => {
57 // Can't possibly match
58 return false;
59 }
60 MatchState::ScanAhead { stop_at_separator } => {
61 if search_str.is_empty() {
62 // End of input, can't match
63 return false;
64 }
65 if stop_at_separator
66 && self
67 .separator
68 .is_some_and(|sep| search_str.starts_with(sep))
69 {
70 // Found the separator. Consume a char and revert to precise
71 // mode
72 search_str = &search_str[1..];
73 state = MatchState::Precise;
74 } else {
75 // Skip the non-matching char and continue
76 search_str = &search_str[1..];
77 }
78 }
79 }
80 }
81 }
82 PatternToken::OneWildcard => {
83 // Set the mode to ScanAhead, stopping at separator
84 state = MatchState::ScanAhead {
85 stop_at_separator: true,
86 };
87 tokens = &tokens[1..];
88 }
89 PatternToken::ManyWildcard => {
90 // Set the mode to ScanAhead, ignoring separator
91 state = MatchState::ScanAhead {
92 stop_at_separator: false,
93 };
94 tokens = &tokens[1..];
95 }
96 PatternToken::AnyChar => {
97 if !search_str.is_empty() {
98 // Take a char from the search str and continue
99 search_str = &search_str[1..];
100 tokens = &tokens[1..];
101 } else {
102 // End of input
103 return false;
104 }
105 }
106 }
107 }
108 #[cfg(test)]
109 {
110 println!(
111 "end, state={:?}, search_str={:?}, tokens={:?}",
112 state, search_str, tokens
113 );
114 }
115 if !search_str.is_empty() {
116 // If the search str is not empty at the end
117 match state {
118 // We didn't end with a wildcard, so this is a fail
119 MatchState::Precise => false,
120 // This could be a match as long as the separator isn't contained in the remainder
121 MatchState::ScanAhead {
122 stop_at_separator: true,
123 } => {
124 if let Some(separator) = self.separator {
125 !search_str.contains(separator)
126 } else {
127 // No separator specified, so this is a success
128 true
129 }
130 }
131 // Always a success, no matter what remains
132 MatchState::ScanAhead {
133 stop_at_separator: false,
134 } => true,
135 }
136 } else {
137 // The match has succeeded - there is nothing more to match
138 true
139 }
140 }
141}
142
143#[test]
144fn test_pattern_new() {
145 assert_eq!(
146 Pattern::new("", Some('/')),
147 Pattern {
148 separator: Some('/'),
149 tokens: vec![]
150 }
151 );
152 assert_eq!(
153 Pattern::new("", None),
154 Pattern {
155 separator: None,
156 tokens: vec![]
157 }
158 );
159 assert_eq!(
160 Pattern::new("org.freedesktop.DBus", Some('.')),
161 Pattern {
162 separator: Some('.'),
163 tokens: vec![PatternToken::Exact("org.freedesktop.DBus".into()),]
164 }
165 );
166 assert_eq!(
167 Pattern::new("*", Some('.')),
168 Pattern {
169 separator: Some('.'),
170 tokens: vec![PatternToken::OneWildcard,]
171 }
172 );
173 assert_eq!(
174 Pattern::new("**", Some('.')),
175 Pattern {
176 separator: Some('.'),
177 tokens: vec![PatternToken::ManyWildcard,]
178 }
179 );
180 assert_eq!(
181 Pattern::new("?", Some('.')),
182 Pattern {
183 separator: Some('.'),
184 tokens: vec![PatternToken::AnyChar,]
185 }
186 );
187 assert_eq!(
188 Pattern::new("org.freedesktop.*", Some('.')),
189 Pattern {
190 separator: Some('.'),
191 tokens: vec![
192 PatternToken::Exact("org.freedesktop.".into()),
193 PatternToken::OneWildcard,
194 ]
195 }
196 );
197 assert_eq!(
198 Pattern::new("org.freedesktop.**", Some('.')),
199 Pattern {
200 separator: Some('.'),
201 tokens: vec![
202 PatternToken::Exact("org.freedesktop.".into()),
203 PatternToken::ManyWildcard,
204 ]
205 }
206 );
207 assert_eq!(
208 Pattern::new("org.*.DBus", Some('.')),
209 Pattern {
210 separator: Some('.'),
211 tokens: vec![
212 PatternToken::Exact("org.".into()),
213 PatternToken::OneWildcard,
214 PatternToken::Exact(".DBus".into()),
215 ]
216 }
217 );
218 assert_eq!(
219 Pattern::new("org.**.DBus", Some('.')),
220 Pattern {
221 separator: Some('.'),
222 tokens: vec![
223 PatternToken::Exact("org.".into()),
224 PatternToken::ManyWildcard,
225 PatternToken::Exact(".DBus".into()),
226 ]
227 }
228 );
229 assert_eq!(
230 Pattern::new("org.**.?Bus", Some('.')),
231 Pattern {
232 separator: Some('.'),
233 tokens: vec![
234 PatternToken::Exact("org.".into()),
235 PatternToken::ManyWildcard,
236 PatternToken::Exact(".".into()),
237 PatternToken::AnyChar,
238 PatternToken::Exact("Bus".into()),
239 ]
240 }
241 );
242 assert_eq!(
243 Pattern::new("org.free*top", Some('.')),
244 Pattern {
245 separator: Some('.'),
246 tokens: vec![
247 PatternToken::Exact("org.free".into()),
248 PatternToken::OneWildcard,
249 PatternToken::Exact("top".into()),
250 ]
251 }
252 );
253 assert_eq!(
254 Pattern::new("org.free**top", Some('.')),
255 Pattern {
256 separator: Some('.'),
257 tokens: vec![
258 PatternToken::Exact("org.free".into()),
259 PatternToken::ManyWildcard,
260 PatternToken::Exact("top".into()),
261 ]
262 }
263 );
264 assert_eq!(
265 Pattern::new("org.**top", Some('.')),
266 Pattern {
267 separator: Some('.'),
268 tokens: vec![
269 PatternToken::Exact("org.".into()),
270 PatternToken::ManyWildcard,
271 PatternToken::Exact("top".into()),
272 ]
273 }
274 );
275 assert_eq!(
276 Pattern::new("**top", Some('.')),
277 Pattern {
278 separator: Some('.'),
279 tokens: vec![
280 PatternToken::ManyWildcard,
281 PatternToken::Exact("top".into()),
282 ]
283 }
284 );
285 assert_eq!(
286 Pattern::new("org.free**", Some('.')),
287 Pattern {
288 separator: Some('.'),
289 tokens: vec![
290 PatternToken::Exact("org.free".into()),
291 PatternToken::ManyWildcard,
292 ]
293 }
294 );
295}
296
297#[test]
298fn test_pattern_is_match_empty() {
299 let pat = Pattern {
300 separator: Some('.'),
301 tokens: vec![],
302 };
303 assert!(pat.is_match(""));
304 assert!(!pat.is_match("anystring"));
305 assert!(!pat.is_match("anystring.anyotherstring"));
306}
307
308#[test]
309fn test_pattern_is_match_exact() {
310 let pat = Pattern {
311 separator: Some('.'),
312 tokens: vec![PatternToken::Exact("specific".into())],
313 };
314 assert!(pat.is_match("specific"));
315 assert!(!pat.is_match(""));
316 assert!(!pat.is_match("specifi"));
317 assert!(!pat.is_match("specifica"));
318}
319
320#[test]
321fn test_pattern_is_match_one_wildcard() {
322 let pat = Pattern {
323 separator: Some('.'),
324 tokens: vec![
325 PatternToken::Exact("foo.".into()),
326 PatternToken::OneWildcard,
327 PatternToken::Exact(".baz".into()),
328 ],
329 };
330 assert!(pat.is_match("foo.bar.baz"));
331 assert!(pat.is_match("foo.grok.baz"));
332 assert!(pat.is_match("foo..baz"));
333 assert!(!pat.is_match("foo.ono.notmatch.baz"));
334 assert!(!pat.is_match(""));
335 assert!(!pat.is_match("specifi"));
336 assert!(!pat.is_match("specifica.baz"));
337 assert!(!pat.is_match("foo.specifica"));
338}
339
340#[test]
341fn test_pattern_is_match_one_wildcard_at_end() {
342 let pat = Pattern {
343 separator: Some('.'),
344 tokens: vec![
345 PatternToken::Exact("foo.".into()),
346 PatternToken::OneWildcard,
347 ],
348 };
349 assert!(pat.is_match("foo.bar"));
350 assert!(pat.is_match("foo.grok"));
351 assert!(pat.is_match("foo."));
352 assert!(!pat.is_match("foo.ono.notmatch.baz"));
353 assert!(!pat.is_match(""));
354 assert!(!pat.is_match("specifi"));
355 assert!(!pat.is_match("specifica.baz"));
356}
357
358#[test]
359fn test_pattern_is_match_one_wildcard_at_start() {
360 let pat = Pattern {
361 separator: Some('.'),
362 tokens: vec![
363 PatternToken::OneWildcard,
364 PatternToken::Exact(".bar".into()),
365 ],
366 };
367 assert!(pat.is_match("foo.bar"));
368 assert!(pat.is_match("grok.bar"));
369 assert!(pat.is_match(".bar"));
370 assert!(!pat.is_match("foo.ono.notmatch.bar"));
371 assert!(!pat.is_match(""));
372 assert!(!pat.is_match("specifi"));
373 assert!(!pat.is_match("specifica.baz"));
374}
375
376#[test]
377fn test_pattern_is_match_one_wildcard_no_separator() {
378 let pat = Pattern {
379 separator: None,
380 tokens: vec![
381 PatternToken::Exact("foo.".into()),
382 PatternToken::OneWildcard,
383 PatternToken::Exact(".baz".into()),
384 ],
385 };
386 assert!(pat.is_match("foo.bar.baz"));
387 assert!(pat.is_match("foo.grok.baz"));
388 assert!(pat.is_match("foo..baz"));
389 assert!(pat.is_match("foo.this.shouldmatch.baz"));
390 assert!(pat.is_match("foo.this.should.match.baz"));
391 assert!(!pat.is_match(""));
392 assert!(!pat.is_match("specifi"));
393 assert!(!pat.is_match("specifica.baz"));
394 assert!(!pat.is_match("foo.specifica"));
395}
396
397#[test]
398fn test_pattern_is_match_many_wildcard() {
399 let pat = Pattern {
400 separator: Some('.'),
401 tokens: vec![
402 PatternToken::Exact("foo.".into()),
403 PatternToken::ManyWildcard,
404 PatternToken::Exact(".baz".into()),
405 ],
406 };
407 assert!(pat.is_match("foo.bar.baz"));
408 assert!(pat.is_match("foo.grok.baz"));
409 assert!(pat.is_match("foo..baz"));
410 assert!(pat.is_match("foo.this.shouldmatch.baz"));
411 assert!(pat.is_match("foo.this.should.match.baz"));
412 assert!(!pat.is_match(""));
413 assert!(!pat.is_match("specifi"));
414 assert!(!pat.is_match("specifica.baz"));
415 assert!(!pat.is_match("foo.specifica"));
416}
417
418#[test]
419fn test_pattern_is_match_many_wildcard_at_end() {
420 let pat = Pattern {
421 separator: Some('.'),
422 tokens: vec![
423 PatternToken::Exact("foo.".into()),
424 PatternToken::ManyWildcard,
425 ],
426 };
427 assert!(pat.is_match("foo.bar"));
428 assert!(pat.is_match("foo.grok"));
429 assert!(pat.is_match("foo."));
430 assert!(pat.is_match("foo.this.should.match"));
431 assert!(!pat.is_match(""));
432 assert!(!pat.is_match("specifi"));
433 assert!(!pat.is_match("specifica.baz"));
434}
435
436#[test]
437fn test_pattern_is_match_many_wildcard_at_start() {
438 let pat = Pattern {
439 separator: Some('.'),
440 tokens: vec![
441 PatternToken::ManyWildcard,
442 PatternToken::Exact(".bar".into()),
443 ],
444 };
445 assert!(pat.is_match("foo.bar"));
446 assert!(pat.is_match("grok.bar"));
447 assert!(pat.is_match("should.match.bar"));
448 assert!(pat.is_match(".bar"));
449 assert!(!pat.is_match(""));
450 assert!(!pat.is_match("specifi"));
451 assert!(!pat.is_match("specifica.baz"));
452}
453
454#[test]
455fn test_pattern_is_match_any_char() {
456 let pat = Pattern {
457 separator: Some('.'),
458 tokens: vec![
459 PatternToken::Exact("fo".into()),
460 PatternToken::AnyChar,
461 PatternToken::Exact(".baz".into()),
462 ],
463 };
464 assert!(pat.is_match("foo.baz"));
465 assert!(pat.is_match("foe.baz"));
466 assert!(pat.is_match("foi.baz"));
467 assert!(!pat.is_match(""));
468 assert!(!pat.is_match("fooo.baz"));
469 assert!(!pat.is_match("fo.baz"));
470}