Nushell plugin for interacting with D-Bus
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add `dbus list` command for easily finding connections on the bus

+514 -6
+42
README.md
··· 20 20 dbus get - Get a D-Bus property 21 21 dbus get-all - Get all D-Bus property for the given objects 22 22 dbus introspect - Introspect a D-Bus object 23 + dbus list - List all available connection names on the bus 23 24 dbus set - Get all D-Bus property for the given objects 24 25 25 26 Flags: ··· 196 197 Set the volume of Spotify to 50% 197 198 > dbus set --dest=org.mpris.MediaPlayer2.spotify /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player Volume 0.5 198 199 200 + ## `dbus list` 201 + 202 + List all available connection names on the bus 203 + 204 + These can be used as arguments for --dest on any of the other commands. 205 + 206 + Search terms: dbus 207 + 208 + Usage: 209 + > dbus list {flags} (pattern) 210 + 211 + Flags: 212 + -h, --help - Display the help message for this command 213 + --session - Send to the session message bus (default) 214 + --system - Send to the system message bus 215 + --started - Send to the bus that started this process, if applicable 216 + --bus <String> - Send to the bus server at the given address 217 + --peer <String> - Send to a non-bus D-Bus server at the given address. Will not call the Hello method on initialization. 218 + --timeout <Duration> - How long to wait for a response 219 + 220 + Parameters: 221 + pattern <string>: An optional glob-like pattern to filter the result by (optional) 222 + 223 + Examples: 224 + List all names available on the bus 225 + > dbus list 226 + 227 + List top-level freedesktop.org names on the bus (e.g. matches `org.freedesktop.PowerManagement`, but not `org.freedesktop.Management.Inhibit`) 228 + > dbus list org.freedesktop.* 229 + ╭───┬───────────────────────────────╮ 230 + │ 0 │ org.freedesktop.DBus │ 231 + │ 1 │ org.freedesktop.Flatpak │ 232 + │ 2 │ org.freedesktop.Notifications │ 233 + ╰───┴───────────────────────────────╯ 234 + 235 + List all MPRIS2 media players on the bus 236 + > dbus list org.mpris.MediaPlayer2.** 237 + ╭───┬────────────────────────────────────────────────╮ 238 + │ 0 │ org.mpris.MediaPlayer2.spotify │ 239 + │ 1 │ org.mpris.MediaPlayer2.kdeconnect.mpris_000001 │ 240 + ╰───┴────────────────────────────────────────────────╯
+27 -1
src/client.rs
··· 2 2 use nu_plugin::LabeledError; 3 3 use nu_protocol::{Spanned, Value}; 4 4 5 - use crate::{config::{DbusClientConfig, DbusBusChoice}, dbus_type::DbusType, convert::to_message_item, introspection::Node}; 5 + use crate::{config::{DbusClientConfig, DbusBusChoice}, dbus_type::DbusType, convert::to_message_item, introspection::Node, pattern::Pattern}; 6 6 7 7 /// Executes D-Bus actions on a connection, handling nushell types 8 8 pub struct DbusClient { ··· 317 317 .map_err(|err| self.error(err, context))?; 318 318 319 319 Ok(()) 320 + } 321 + 322 + pub fn list(&self, pattern: Option<&Pattern>) 323 + -> Result<Vec<String>, LabeledError> 324 + { 325 + let context = "while listing D-Bus connection names"; 326 + 327 + let message = Message::new_method_call( 328 + "org.freedesktop.DBus", 329 + "/org/freedesktop/DBus", 330 + "org.freedesktop.DBus", 331 + "ListNames" 332 + ).map_err(|err| self.error(err, context))?; 333 + 334 + self.conn.send_with_reply_and_block(message, self.config.timeout.item) 335 + .map_err(|err| self.error(err, context)) 336 + .and_then(|reply| reply.read1().map_err(|err| self.error(err, context))) 337 + .map(|names: Vec<String>| { 338 + // Filter the names by the pattern 339 + if let Some(pattern) = pattern { 340 + eprintln!("pattern: {:?}", pattern); 341 + names.into_iter().filter(|name| pattern.is_match(name)).collect() 342 + } else { 343 + names 344 + } 345 + }) 320 346 } 321 347 }
+58 -5
src/main.rs
··· 6 6 mod convert; 7 7 mod dbus_type; 8 8 mod introspection; 9 + mod pattern; 9 10 10 11 use config::*; 11 12 use client::*; 13 + 14 + use crate::pattern::Pattern; 12 15 13 16 fn main() { 14 17 serve_plugin(&mut NuPluginDbus, MsgPackSerializer) ··· 29 32 PluginSignature::build("dbus introspect") 30 33 .is_dbus_command() 31 34 .accepts_dbus_client_options() 35 + .accepts_timeout() 32 36 .usage("Introspect a D-Bus object") 33 37 .extra_usage("Returns information about available nodes, interfaces, methods, \ 34 38 signals, and properties on the given object path") 35 - .named("timeout", SyntaxShape::Duration, "How long to wait for a response", None) 36 39 .required_named("dest", SyntaxShape::String, 37 40 "The name of the connection that owns the object", 38 41 None) ··· 63 66 PluginSignature::build("dbus call") 64 67 .is_dbus_command() 65 68 .accepts_dbus_client_options() 69 + .accepts_timeout() 66 70 .usage("Call a method and get its response") 67 71 .extra_usage("Returns an array if the method call returns more than one value.") 68 - .named("timeout", SyntaxShape::Duration, "How long to wait for a response", None) 69 72 .named("signature", SyntaxShape::String, 70 73 "Signature of the arguments to send, in D-Bus format.\n \ 71 74 If not provided, they will be determined from introspection.\n \ ··· 105 108 PluginSignature::build("dbus get") 106 109 .is_dbus_command() 107 110 .accepts_dbus_client_options() 111 + .accepts_timeout() 108 112 .usage("Get a D-Bus property") 109 - .named("timeout", SyntaxShape::Duration, "How long to wait for a response", None) 110 113 .required_named("dest", SyntaxShape::String, 111 114 "The name of the connection to read the property from", 112 115 None) ··· 135 138 PluginSignature::build("dbus get-all") 136 139 .is_dbus_command() 137 140 .accepts_dbus_client_options() 141 + .accepts_timeout() 138 142 .usage("Get all D-Bus property for the given objects") 139 - .named("timeout", SyntaxShape::Duration, "How long to wait for a response", None) 140 143 .required_named("dest", SyntaxShape::String, 141 144 "The name of the connection to read the property from", 142 145 None) ··· 160 163 PluginSignature::build("dbus set") 161 164 .is_dbus_command() 162 165 .accepts_dbus_client_options() 166 + .accepts_timeout() 163 167 .usage("Get all D-Bus property for the given objects") 164 - .named("timeout", SyntaxShape::Duration, "How long to wait for a response", None) 165 168 .named("signature", SyntaxShape::String, 166 169 "Signature of the value to set, in D-Bus format.\n \ 167 170 If not provided, it will be determined from introspection.\n \ ··· 187 190 result: None, 188 191 }, 189 192 ]), 193 + PluginSignature::build("dbus list") 194 + .is_dbus_command() 195 + .accepts_dbus_client_options() 196 + .accepts_timeout() 197 + .usage("List all available connection names on the bus") 198 + .extra_usage("These can be used as arguments for --dest on any of the other commands.") 199 + .optional("pattern", SyntaxShape::String, 200 + "An optional glob-like pattern to filter the result by") 201 + .plugin_examples(vec![ 202 + PluginExample { 203 + example: "dbus list".into(), 204 + description: "List all names available on the bus".into(), 205 + result: None, 206 + }, 207 + PluginExample { 208 + example: "dbus list org.freedesktop.*".into(), 209 + description: "List top-level freedesktop.org names on the bus \ 210 + (e.g. matches `org.freedesktop.PowerManagement`, \ 211 + but not `org.freedesktop.Management.Inhibit`)".into(), 212 + result: Some(Value::list(vec![ 213 + str!("org.freedesktop.DBus"), 214 + str!("org.freedesktop.Flatpak"), 215 + str!("org.freedesktop.Notifications"), 216 + ], Span::unknown())), 217 + }, 218 + PluginExample { 219 + example: "dbus list org.mpris.MediaPlayer2.**".into(), 220 + description: "List all MPRIS2 media players on the bus".into(), 221 + result: Some(Value::list(vec![ 222 + str!("org.mpris.MediaPlayer2.spotify"), 223 + str!("org.mpris.MediaPlayer2.kdeconnect.mpris_000001"), 224 + ], Span::unknown())), 225 + }, 226 + ]) 190 227 ] 191 228 } 192 229 ··· 208 245 "dbus get" => self.get(call), 209 246 "dbus get-all" => self.get_all(call), 210 247 "dbus set" => self.set(call), 248 + "dbus list" => self.list(call), 211 249 212 250 _ => Err(LabeledError { 213 251 label: "Plugin invoked with unknown command name".into(), ··· 222 260 trait DbusSignatureUtilExt { 223 261 fn is_dbus_command(self) -> Self; 224 262 fn accepts_dbus_client_options(self) -> Self; 263 + fn accepts_timeout(self) -> Self; 225 264 } 226 265 227 266 impl DbusSignatureUtilExt for PluginSignature { ··· 239 278 "Send to a non-bus D-Bus server at the given address. \ 240 279 Will not call the Hello method on initialization.", 241 280 None) 281 + } 282 + 283 + fn accepts_timeout(self) -> Self { 284 + self.named("timeout", SyntaxShape::Duration, "How long to wait for a response", None) 242 285 } 243 286 } 244 287 ··· 309 352 &call.req(3)?, 310 353 )?; 311 354 Ok(Value::nothing(call.head)) 355 + } 356 + 357 + fn list(&self, call: &EvaluatedCall) -> Result<Value, LabeledError> { 358 + let config = DbusClientConfig::try_from(call)?; 359 + let dbus = DbusClient::new(config)?; 360 + let pattern = call.opt::<String>(0)?.map(|pat| Pattern::new(&pat, Some('.'))); 361 + let result = dbus.list(pattern.as_ref())?; 362 + Ok(Value::list( 363 + result.into_iter().map(|s| Value::string(s, call.head)).collect(), 364 + call.head)) 312 365 } 313 366 }
+387
src/pattern.rs
··· 1 + #[derive(Debug, Clone, PartialEq, Eq)] 2 + pub struct Pattern { 3 + separator: Option<char>, 4 + tokens: Vec<PatternToken>, 5 + } 6 + 7 + #[derive(Debug, Clone, PartialEq, Eq)] 8 + enum PatternToken { 9 + Exact(String), 10 + OneWildcard, 11 + ManyWildcard, 12 + AnyChar, 13 + } 14 + 15 + impl 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] 129 + fn 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] 243 + fn 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] 251 + fn 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] 262 + fn 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] 279 + fn 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] 294 + fn 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] 309 + fn 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] 327 + fn 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] 345 + fn 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] 360 + fn 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] 375 + fn 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 + }