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 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(&[
183 "--stdin",
184 "--stdout",
185 "--force",
186 "--blackholed",
187 "--json",
188 "--value-only",
189 "--keys-only",
190 ]);
191 assert!(a.has("stdin"));
192 assert!(a.has("stdout"));
193 assert!(a.has("force"));
194 assert!(a.has("blackholed"));
195 assert!(a.has("json"));
196 assert!(a.has("value-only"));
197 assert!(a.has("keys-only"));
198 }
199
200 #[test]
201 fn parse_exampleconfig() {
202 let a = args(&["--exampleconfig"]);
203 assert!(a.has("exampleconfig"));
204 }
205
206 #[test]
207 fn parse_short_d_boolean() {
208 let a = args(&["-d"]);
210 assert!(a.has("d"));
211 assert_eq!(a.get("d"), Some("true"));
212 }
213
214 #[test]
215 fn parse_short_d_with_value() {
216 let a = args(&["-d", "file.enc"]);
218 assert_eq!(a.get("d"), Some("file.enc"));
219 }
220
221 #[test]
222 fn parse_daemon_long() {
223 let a = args(&["--daemon"]);
224 assert!(a.has("daemon"));
225 }
226
227 #[test]
228 fn parse_disable_auth() {
229 let a = args(&["--disable-auth"]);
230 assert!(a.has("disable-auth"));
231 }
232
233 #[test]
234 fn parse_help() {
235 let a = args(&["--help"]);
236 assert!(a.has("help"));
237 let a = args(&["-h"]);
238 assert!(a.has("help"));
239 }
240
241 #[test]
242 fn parse_short_p_with_value() {
243 let a = args(&["-p", "8080"]);
245 assert_eq!(a.get("p"), Some("8080"));
246 }
247
248 #[test]
249 fn parse_short_p_boolean() {
250 let a = args(&["-p"]);
252 assert!(a.has("p"));
253 }
254
255 #[test]
256 fn parse_short_x_with_value() {
257 let a = args(&["-x", "abcd1234"]);
259 assert_eq!(a.get("x"), Some("abcd1234"));
260 }
261
262 #[test]
263 fn parse_short_x_boolean() {
264 let a = args(&["-x"]);
266 assert!(a.has("x"));
267 }
268
269 #[test]
270 fn flag_with_value_vs_boolean() {
271 let a = args(&["-s", "rate"]);
273 assert_eq!(a.get("s"), Some("rate"));
274
275 let a = args(&["-s", "-v"]);
277 assert!(a.has("s"));
278 assert_eq!(a.get("s"), Some("true"));
279 assert_eq!(a.verbosity, 1);
280
281 let a = args(&["-m", "5"]);
283 assert_eq!(a.get("m"), Some("5"));
284
285 let a = args(&["-m"]);
287 assert!(a.has("m"));
288
289 let a = args(&["-B", "abcdef1234567890"]);
291 assert_eq!(a.get("B"), Some("abcdef1234567890"));
292
293 let a = args(&["-B"]);
295 assert!(a.has("B"));
296 }
297}