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