use std::collections::BTreeSet; pub fn parse_ssh_config_hosts(config: &str) -> BTreeSet { let mut hosts = BTreeSet::new(); let mut needs_another_line = false; for line in config.lines() { let line = line.trim_start(); if let Some(line) = line.strip_prefix("Host") { match line.chars().next() { Some('\\') => { needs_another_line = true; } Some('\n' | '\r') => { needs_another_line = false; } Some(c) if c.is_whitespace() => { parse_hosts_from(line, &mut hosts); } Some(_) | None => { needs_another_line = false; } }; if needs_another_line { parse_hosts_from(line, &mut hosts); needs_another_line = line.trim_end().ends_with('\\'); } else { needs_another_line = false; } } else if needs_another_line { needs_another_line = line.trim_end().ends_with('\\'); parse_hosts_from(line, &mut hosts); } else { needs_another_line = false; } } hosts } fn parse_hosts_from(line: &str, hosts: &mut BTreeSet) { hosts.extend( line.split_whitespace() .filter(|field| !field.starts_with("!")) .filter(|field| !field.contains("*")) .filter(|field| !field.is_empty()) .map(|field| field.to_owned()), ); } #[cfg(test)] mod tests { use super::*; #[test] fn test_thank_you_bjorn3() { let hosts = " Host * AddKeysToAgent yes UseKeychain yes IdentityFile ~/.ssh/id_ed25519 Host whatever.* User another Host !not_this User not_me Host something HostName whatever.tld Host linux bsd host3 User bjorn Host rpi user rpi hostname rpi.local Host \ somehost \ anotherhost Hostname 192.168.3.3"; let expected_hosts = BTreeSet::from_iter([ "something".to_owned(), "linux".to_owned(), "host3".to_owned(), "bsd".to_owned(), "rpi".to_owned(), "somehost".to_owned(), "anotherhost".to_owned(), ]); assert_eq!(expected_hosts, parse_ssh_config_hosts(hosts)); } }