1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
use indexmap::IndexMap;
use std::{fs, io};
pub type HostMap = IndexMap<String, String>;
pub fn load_ssh_config(path: &str) -> Result<HostMap, io::Error> {
parse_ssh_config(&fs::read_to_string(path.replace('~', env!("HOME")))?)
}
pub fn parse_ssh_config<S: AsRef<str>>(config: S) -> Result<HostMap, io::Error> {
let config = config.as_ref();
let mut map = HostMap::new();
let mut token = String::new();
let mut line = vec![];
let mut skip_line = false;
let mut stanza = String::new();
let mut key = true;
for c in config.chars() {
if skip_line {
if c == '\n' {
skip_line = false;
}
continue;
}
if c == '#' {
skip_line = true;
if !token.is_empty() {
line.push(token);
token = String::new();
}
} else if key && (c == ' ' || c == '=') {
if !token.is_empty() {
line.push(token);
token = String::new();
key = false;
}
} else if c == '\n' {
if !token.is_empty() {
line.push(token);
token = String::new();
key = true;
}
if line.is_empty() {
continue;
} else if line.len() != 2 {
return Err(io::Error::new(
io::ErrorKind::Other,
format!("can't parse line: {:?}", line),
));
} else {
match line[0].to_lowercase().as_ref() {
"host" => {
let parsed = &line[1];
if parsed.contains('*')
|| parsed.contains('!')
|| parsed.contains('?')
|| parsed.contains(',')
|| parsed.contains(' ')
{
stanza.clear();
} else {
stanza = parsed.clone();
map.insert(stanza.clone(), stanza.clone());
}
}
"hostname" => {
if !stanza.is_empty() {
map.insert(stanza.clone(), line[1].clone());
stanza.clear();
}
}
_ => {}
}
line.clear();
}
} else if (c == ' ' || c == '=') && token.is_empty() {
continue;
} else {
token.push(c);
}
}
Ok(map)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config() {
let config = load_ssh_config("./tests/test_config").expect("failed to parse config");
assert_eq!(11, config.len());
assert_eq!(
config.keys().cloned().collect::<Vec<_>>(),
vec![
"homework-server",
"nixcraft",
"docker1",
"nas01",
"docker2",
"docker3",
"devserver",
"ec2-some-long-name.amazon.probably.com",
"ec2-some-long-namer.amazon.probably.com",
"torrentz-server",
"midi-files.com",
]
);
assert_eq!("torrentz-r-us.com", config.get("torrentz-server").unwrap());
assert_eq!("docker3.mycloud.net", config.get("docker3").unwrap());
assert_eq!("192.168.1.100", config.get("nas01").unwrap());
assert_eq!("midi-files.com", config.get("midi-files.com").unwrap());
}
}