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" | "stdin" | "stdout" | "force"
45 | "blackholed" | "daemon" | "disable-auth" => {
46 flags.insert(key, "true".into());
47 }
48 _ => {
49 if let Some(val) = iter.next() {
51 flags.insert(key, val);
52 } else {
53 flags.insert(key, "true".into());
54 }
55 }
56 }
57 }
58 } else if arg.starts_with('-') && arg.len() > 1 {
59 let chars: Vec<char> = arg[1..].chars().collect();
61 for &c in &chars {
62 match c {
63 'v' => verbosity = verbosity.saturating_add(1),
64 'q' => quiet = quiet.saturating_add(1),
65 'a' | 'r' | 'j' | 'P' | 'D' | 'l' | 'f' | 'A' => {
66 flags.insert(c.to_string(), "true".into());
67 }
68 'h' => {
69 flags.insert("help".into(), "true".into());
70 }
71 _ => {
72 if chars.len() == 1 {
75 let next_is_value = iter
76 .as_slice()
77 .first()
78 .map(|s| !s.starts_with('-') || s == "-")
79 .unwrap_or(false);
80 if next_is_value {
81 let val = iter.next().unwrap();
82 flags.insert(c.to_string(), val);
83 } else {
84 flags.insert(c.to_string(), "true".into());
85 }
86 } else {
87 flags.insert(c.to_string(), "true".into());
88 }
89 }
90 }
91 }
92 } else {
93 positional.push(arg);
94 }
95 }
96
97 Args {
98 flags,
99 positional,
100 verbosity,
101 quiet,
102 }
103 }
104
105 pub fn get(&self, key: &str) -> Option<&str> {
107 self.flags.get(key).map(|s| s.as_str())
108 }
109
110 pub fn has(&self, key: &str) -> bool {
112 self.flags.contains_key(key)
113 }
114
115 pub fn config_path(&self) -> Option<&str> {
117 self.get("config").or_else(|| self.get("c"))
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 fn args(s: &[&str]) -> Args {
126 Args::parse_from(s.iter().map(|s| s.to_string()).collect())
127 }
128
129 #[test]
130 fn parse_config_and_verbose() {
131 let a = args(&["--config", "/path/to/config", "-vv", "-s"]);
132 assert_eq!(a.config_path(), Some("/path/to/config"));
133 assert_eq!(a.verbosity, 2);
134 assert!(a.has("s"));
135 }
136
137 #[test]
138 fn parse_version() {
139 let a = args(&["--version"]);
140 assert!(a.has("version"));
141 }
142
143 #[test]
144 fn parse_short_flag_with_value() {
145 let a = args(&["-t", "abcd1234"]);
147 assert_eq!(a.get("t"), Some("abcd1234"));
148 }
149
150 #[test]
151 fn parse_short_flag_boolean() {
152 let a = args(&["-t"]);
154 assert!(a.has("t"));
155 assert_eq!(a.get("t"), Some("true"));
156 }
157
158 #[test]
159 fn parse_short_config() {
160 let a = args(&["-c", "/my/config"]);
161 assert_eq!(a.config_path(), Some("/my/config"));
162 }
163
164 #[test]
165 fn parse_quiet() {
166 let a = args(&["-qq"]);
167 assert_eq!(a.quiet, 2);
168 }
169
170 #[test]
171 fn parse_new_boolean_flags() {
172 let a = args(&["-l", "-f", "-m", "-A"]);
173 assert!(a.has("l"));
174 assert!(a.has("f"));
175 assert!(a.has("m"));
176 assert!(a.has("A"));
177 }
178
179 #[test]
180 fn parse_long_boolean_flags() {
181 let a = args(&["--stdin", "--stdout", "--force", "--blackholed"]);
182 assert!(a.has("stdin"));
183 assert!(a.has("stdout"));
184 assert!(a.has("force"));
185 assert!(a.has("blackholed"));
186 }
187
188 #[test]
189 fn parse_exampleconfig() {
190 let a = args(&["--exampleconfig"]);
191 assert!(a.has("exampleconfig"));
192 }
193
194 #[test]
195 fn parse_short_d_boolean() {
196 let a = args(&["-d"]);
198 assert!(a.has("d"));
199 assert_eq!(a.get("d"), Some("true"));
200 }
201
202 #[test]
203 fn parse_short_d_with_value() {
204 let a = args(&["-d", "file.enc"]);
206 assert_eq!(a.get("d"), Some("file.enc"));
207 }
208
209 #[test]
210 fn parse_daemon_long() {
211 let a = args(&["--daemon"]);
212 assert!(a.has("daemon"));
213 }
214
215 #[test]
216 fn parse_disable_auth() {
217 let a = args(&["--disable-auth"]);
218 assert!(a.has("disable-auth"));
219 }
220
221 #[test]
222 fn parse_help() {
223 let a = args(&["--help"]);
224 assert!(a.has("help"));
225 let a = args(&["-h"]);
226 assert!(a.has("help"));
227 }
228
229 #[test]
230 fn parse_short_p_with_value() {
231 let a = args(&["-p", "8080"]);
233 assert_eq!(a.get("p"), Some("8080"));
234 }
235
236 #[test]
237 fn parse_short_p_boolean() {
238 let a = args(&["-p"]);
240 assert!(a.has("p"));
241 }
242
243 #[test]
244 fn parse_short_x_with_value() {
245 let a = args(&["-x", "abcd1234"]);
247 assert_eq!(a.get("x"), Some("abcd1234"));
248 }
249
250 #[test]
251 fn parse_short_x_boolean() {
252 let a = args(&["-x"]);
254 assert!(a.has("x"));
255 }
256
257 #[test]
258 fn flag_with_value_vs_boolean() {
259 let a = args(&["-s", "rate"]);
261 assert_eq!(a.get("s"), Some("rate"));
262
263 let a = args(&["-s", "-v"]);
265 assert!(a.has("s"));
266 assert_eq!(a.get("s"), Some("true"));
267 assert_eq!(a.verbosity, 1);
268
269 let a = args(&["-m", "5"]);
271 assert_eq!(a.get("m"), Some("5"));
272
273 let a = args(&["-m"]);
275 assert!(a.has("m"));
276
277 let a = args(&["-B", "abcdef1234567890"]);
279 assert_eq!(a.get("B"), Some("abcdef1234567890"));
280
281 let a = args(&["-B"]);
283 assert!(a.has("B"));
284 }
285}