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