1use std::collections::HashMap;
7
8pub struct Args {
10 pub flags: HashMap<String, String>,
11 pub positional: Vec<String>,
12 pub verbosity: u8,
13 pub quiet: u8,
14}
15
16impl Args {
17 pub fn parse() -> Self {
19 Self::parse_from(std::env::args().skip(1).collect())
20 }
21
22 pub fn parse_from(args: Vec<String>) -> Self {
24 let mut flags = HashMap::new();
25 let mut positional = Vec::new();
26 let mut verbosity: u8 = 0;
27 let mut quiet: u8 = 0;
28 let mut iter = args.into_iter();
29
30 while let Some(arg) = iter.next() {
31 if arg == "--" {
32 positional.extend(iter);
34 break;
35 } else if arg.starts_with("--") {
36 let key = arg[2..].to_string();
37 if let Some(eq_pos) = key.find('=') {
39 let (k, v) = key.split_at(eq_pos);
40 flags.insert(k.to_string(), v[1..].to_string());
41 } else {
42 match key.as_str() {
44 "version" | "exampleconfig" | "help"
45 | "stdin" | "stdout" | "force"
46 | "blackholed"
47 | "daemon" | "disable-auth" => {
48 flags.insert(key, "true".into());
49 }
50 _ => {
51 if let Some(val) = iter.next() {
53 flags.insert(key, val);
54 } else {
55 flags.insert(key, "true".into());
56 }
57 }
58 }
59 }
60 } else if arg.starts_with('-') && arg.len() > 1 {
61 let chars: Vec<char> = arg[1..].chars().collect();
63 for &c in &chars {
64 match c {
65 'v' => verbosity = verbosity.saturating_add(1),
66 'q' => quiet = quiet.saturating_add(1),
67 'a' | 'r' | 'j' | 'P' | 'D'
68 | 'l' | 'f' | 'A' => {
69 flags.insert(c.to_string(), "true".into());
70 }
71 'h' => {
72 flags.insert("help".into(), "true".into());
73 }
74 _ => {
75 if chars.len() == 1 {
78 let next_is_value = iter.as_slice().first()
79 .map(|s| !s.starts_with('-') || s == "-")
80 .unwrap_or(false);
81 if next_is_value {
82 let val = iter.next().unwrap();
83 flags.insert(c.to_string(), val);
84 } else {
85 flags.insert(c.to_string(), "true".into());
86 }
87 } else {
88 flags.insert(c.to_string(), "true".into());
89 }
90 }
91 }
92 }
93 } else {
94 positional.push(arg);
95 }
96 }
97
98 Args {
99 flags,
100 positional,
101 verbosity,
102 quiet,
103 }
104 }
105
106 pub fn get(&self, key: &str) -> Option<&str> {
108 self.flags.get(key).map(|s| s.as_str())
109 }
110
111 pub fn has(&self, key: &str) -> bool {
113 self.flags.contains_key(key)
114 }
115
116 pub fn config_path(&self) -> Option<&str> {
118 self.get("config").or_else(|| self.get("c"))
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 fn args(s: &[&str]) -> Args {
127 Args::parse_from(s.iter().map(|s| s.to_string()).collect())
128 }
129
130 #[test]
131 fn parse_config_and_verbose() {
132 let a = args(&["--config", "/path/to/config", "-vv", "-s"]);
133 assert_eq!(a.config_path(), Some("/path/to/config"));
134 assert_eq!(a.verbosity, 2);
135 assert!(a.has("s"));
136 }
137
138 #[test]
139 fn parse_version() {
140 let a = args(&["--version"]);
141 assert!(a.has("version"));
142 }
143
144 #[test]
145 fn parse_short_flag_with_value() {
146 let a = args(&["-t", "abcd1234"]);
148 assert_eq!(a.get("t"), Some("abcd1234"));
149 }
150
151 #[test]
152 fn parse_short_flag_boolean() {
153 let a = args(&["-t"]);
155 assert!(a.has("t"));
156 assert_eq!(a.get("t"), Some("true"));
157 }
158
159 #[test]
160 fn parse_short_config() {
161 let a = args(&["-c", "/my/config"]);
162 assert_eq!(a.config_path(), Some("/my/config"));
163 }
164
165 #[test]
166 fn parse_quiet() {
167 let a = args(&["-qq"]);
168 assert_eq!(a.quiet, 2);
169 }
170
171 #[test]
172 fn parse_new_boolean_flags() {
173 let a = args(&["-l", "-f", "-m", "-A"]);
174 assert!(a.has("l"));
175 assert!(a.has("f"));
176 assert!(a.has("m"));
177 assert!(a.has("A"));
178 }
179
180 #[test]
181 fn parse_long_boolean_flags() {
182 let a = args(&["--stdin", "--stdout", "--force", "--blackholed"]);
183 assert!(a.has("stdin"));
184 assert!(a.has("stdout"));
185 assert!(a.has("force"));
186 assert!(a.has("blackholed"));
187 }
188
189 #[test]
190 fn parse_exampleconfig() {
191 let a = args(&["--exampleconfig"]);
192 assert!(a.has("exampleconfig"));
193 }
194
195 #[test]
196 fn parse_short_d_boolean() {
197 let a = args(&["-d"]);
199 assert!(a.has("d"));
200 assert_eq!(a.get("d"), Some("true"));
201 }
202
203 #[test]
204 fn parse_short_d_with_value() {
205 let a = args(&["-d", "file.enc"]);
207 assert_eq!(a.get("d"), Some("file.enc"));
208 }
209
210 #[test]
211 fn parse_daemon_long() {
212 let a = args(&["--daemon"]);
213 assert!(a.has("daemon"));
214 }
215
216 #[test]
217 fn parse_disable_auth() {
218 let a = args(&["--disable-auth"]);
219 assert!(a.has("disable-auth"));
220 }
221
222 #[test]
223 fn parse_help() {
224 let a = args(&["--help"]);
225 assert!(a.has("help"));
226 let a = args(&["-h"]);
227 assert!(a.has("help"));
228 }
229
230 #[test]
231 fn parse_short_p_with_value() {
232 let a = args(&["-p", "8080"]);
234 assert_eq!(a.get("p"), Some("8080"));
235 }
236
237 #[test]
238 fn parse_short_p_boolean() {
239 let a = args(&["-p"]);
241 assert!(a.has("p"));
242 }
243
244 #[test]
245 fn parse_short_x_with_value() {
246 let a = args(&["-x", "abcd1234"]);
248 assert_eq!(a.get("x"), Some("abcd1234"));
249 }
250
251 #[test]
252 fn parse_short_x_boolean() {
253 let a = args(&["-x"]);
255 assert!(a.has("x"));
256 }
257
258 #[test]
259 fn flag_with_value_vs_boolean() {
260 let a = args(&["-s", "rate"]);
262 assert_eq!(a.get("s"), Some("rate"));
263
264 let a = args(&["-s", "-v"]);
266 assert!(a.has("s"));
267 assert_eq!(a.get("s"), Some("true"));
268 assert_eq!(a.verbosity, 1);
269
270 let a = args(&["-m", "5"]);
272 assert_eq!(a.get("m"), Some("5"));
273
274 let a = args(&["-m"]);
276 assert!(a.has("m"));
277
278 let a = args(&["-B", "abcdef1234567890"]);
280 assert_eq!(a.get("B"), Some("abcdef1234567890"));
281
282 let a = args(&["-B"]);
284 assert!(a.has("B"));
285 }
286}