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" => {
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' | 't' | 'j' | 'p' | 'P' | 'x' | 'D' | 'l' | 'f' | 'A' => {
66 flags.insert(c.to_string(), "true".into());
67 }
68 _ => {
69 if chars.len() == 1 {
72 let next_is_value = iter
73 .as_slice()
74 .first()
75 .map(|s| !s.starts_with('-') || s == "-")
76 .unwrap_or(false);
77 if next_is_value {
78 let val = iter.next().unwrap();
79 flags.insert(c.to_string(), val);
80 } else {
81 flags.insert(c.to_string(), "true".into());
82 }
83 } else {
84 flags.insert(c.to_string(), "true".into());
85 }
86 }
87 }
88 }
89 } else {
90 positional.push(arg);
91 }
92 }
93
94 Args {
95 flags,
96 positional,
97 verbosity,
98 quiet,
99 }
100 }
101
102 pub fn get(&self, key: &str) -> Option<&str> {
104 self.flags.get(key).map(|s| s.as_str())
105 }
106
107 pub fn has(&self, key: &str) -> bool {
109 self.flags.contains_key(key)
110 }
111
112 pub fn config_path(&self) -> Option<&str> {
114 self.get("config").or_else(|| self.get("c"))
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 fn args(s: &[&str]) -> Args {
123 Args::parse_from(s.iter().map(|s| s.to_string()).collect())
124 }
125
126 #[test]
127 fn parse_config_and_verbose() {
128 let a = args(&["--config", "/path/to/config", "-vv", "-s"]);
129 assert_eq!(a.config_path(), Some("/path/to/config"));
130 assert_eq!(a.verbosity, 2);
131 assert!(a.has("s"));
132 }
133
134 #[test]
135 fn parse_version() {
136 let a = args(&["--version"]);
137 assert!(a.has("version"));
138 }
139
140 #[test]
141 fn parse_positional() {
142 let a = args(&["-t", "abcd1234"]);
143 assert!(a.has("t"));
144 assert_eq!(a.positional, vec!["abcd1234"]);
145 }
146
147 #[test]
148 fn parse_short_config() {
149 let a = args(&["-c", "/my/config"]);
150 assert_eq!(a.config_path(), Some("/my/config"));
151 }
152
153 #[test]
154 fn parse_quiet() {
155 let a = args(&["-qq"]);
156 assert_eq!(a.quiet, 2);
157 }
158
159 #[test]
160 fn parse_new_boolean_flags() {
161 let a = args(&["-l", "-f", "-m", "-A"]);
162 assert!(a.has("l"));
163 assert!(a.has("f"));
164 assert!(a.has("m"));
165 assert!(a.has("A"));
166 }
167
168 #[test]
169 fn parse_long_boolean_flags() {
170 let a = args(&["--stdin", "--stdout", "--force", "--blackholed"]);
171 assert!(a.has("stdin"));
172 assert!(a.has("stdout"));
173 assert!(a.has("force"));
174 assert!(a.has("blackholed"));
175 }
176
177 #[test]
178 fn parse_exampleconfig() {
179 let a = args(&["--exampleconfig"]);
180 assert!(a.has("exampleconfig"));
181 }
182
183 #[test]
184 fn flag_with_value_vs_boolean() {
185 let a = args(&["-s", "rate"]);
187 assert_eq!(a.get("s"), Some("rate"));
188
189 let a = args(&["-s", "-v"]);
191 assert!(a.has("s"));
192 assert_eq!(a.get("s"), Some("true"));
193 assert_eq!(a.verbosity, 1);
194
195 let a = args(&["-m", "5"]);
197 assert_eq!(a.get("m"), Some("5"));
198
199 let a = args(&["-m"]);
201 assert!(a.has("m"));
202
203 let a = args(&["-B", "abcdef1234567890"]);
205 assert_eq!(a.get("B"), Some("abcdef1234567890"));
206
207 let a = args(&["-B"]);
209 assert!(a.has("B"));
210 }
211}