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}