1use std::collections::BTreeSet;
2
3pub fn parse_ssh_config_hosts(config: &str) -> BTreeSet<String> {
4 let mut hosts = BTreeSet::new();
5 let mut needs_another_line = false;
6 for line in config.lines() {
7 let line = line.trim_start();
8 if let Some(line) = line.strip_prefix("Host") {
9 match line.chars().next() {
10 Some('\\') => {
11 needs_another_line = true;
12 }
13 Some('\n' | '\r') => {
14 needs_another_line = false;
15 }
16 Some(c) if c.is_whitespace() => {
17 parse_hosts_from(line, &mut hosts);
18 }
19 Some(_) | None => {
20 needs_another_line = false;
21 }
22 };
23
24 if needs_another_line {
25 parse_hosts_from(line, &mut hosts);
26 needs_another_line = line.trim_end().ends_with('\\');
27 } else {
28 needs_another_line = false;
29 }
30 } else if needs_another_line {
31 needs_another_line = line.trim_end().ends_with('\\');
32 parse_hosts_from(line, &mut hosts);
33 } else {
34 needs_another_line = false;
35 }
36 }
37
38 hosts
39}
40
41fn parse_hosts_from(line: &str, hosts: &mut BTreeSet<String>) {
42 hosts.extend(
43 line.split_whitespace()
44 .filter(|field| !field.starts_with("!"))
45 .filter(|field| !field.contains("*"))
46 .filter(|field| !field.is_empty())
47 .map(|field| field.to_owned()),
48 );
49}
50
51#[cfg(test)]
52mod tests {
53 use super::*;
54
55 #[test]
56 fn test_thank_you_bjorn3() {
57 let hosts = "
58 Host *
59 AddKeysToAgent yes
60 UseKeychain yes
61 IdentityFile ~/.ssh/id_ed25519
62
63 Host whatever.*
64 User another
65
66 Host !not_this
67 User not_me
68
69 Host something
70 HostName whatever.tld
71
72 Host linux bsd host3
73 User bjorn
74
75 Host rpi
76 user rpi
77 hostname rpi.local
78
79 Host \
80 somehost \
81 anotherhost
82 Hostname 192.168.3.3";
83
84 let expected_hosts = BTreeSet::from_iter([
85 "something".to_owned(),
86 "linux".to_owned(),
87 "host3".to_owned(),
88 "bsd".to_owned(),
89 "rpi".to_owned(),
90 "somehost".to_owned(),
91 "anotherhost".to_owned(),
92 ]);
93
94 assert_eq!(expected_hosts, parse_ssh_config_hosts(hosts));
95 }
96}