rainier_console/
arguments.rs1use std::collections::{HashMap, HashSet};
19
20#[derive(Debug, Clone, Default, PartialEq, Eq)]
22pub struct Arguments {
23 command: String,
24 positional: Vec<String>,
25 options: HashMap<String, String>,
26 flags: HashSet<String>,
27}
28
29impl Arguments {
30 pub fn parse<I, S>(argv: I) -> Self
34 where
35 I: IntoIterator<Item = S>,
36 S: Into<String>,
37 {
38 let mut parsed = Arguments::default();
39 let mut only_positional = false;
40
41 for raw in argv {
42 let argument: String = raw.into();
43
44 if only_positional {
45 parsed.push_positional(argument);
46 continue;
47 }
48
49 if argument == "--" {
50 only_positional = true;
51 continue;
52 }
53
54 if let Some(rest) = argument.strip_prefix("--") {
55 match rest.split_once('=') {
56 Some((name, value)) => {
57 parsed.options.insert(name.to_string(), value.to_string());
58 }
59 None => {
60 parsed.flags.insert(rest.to_string());
61 }
62 }
63 continue;
64 }
65
66 if let Some(rest) = argument.strip_prefix('-') {
67 if !rest.is_empty() && !rest.starts_with(|c: char| c.is_ascii_digit()) {
68 for flag in rest.chars() {
71 parsed.flags.insert(flag.to_string());
72 }
73 continue;
74 }
75 }
76
77 parsed.push_positional(argument);
78 }
79
80 parsed
81 }
82
83 fn push_positional(&mut self, argument: String) {
84 if self.command.is_empty() {
85 self.command = argument;
86 } else {
87 self.positional.push(argument);
88 }
89 }
90
91 pub fn command(&self) -> &str {
93 &self.command
94 }
95
96 pub fn is_empty(&self) -> bool {
98 self.command.is_empty()
99 }
100
101 pub fn positional(&self) -> &[String] {
103 &self.positional
104 }
105
106 pub fn argument(&self, index: usize) -> Option<&str> {
108 self.positional.get(index).map(String::as_str)
109 }
110
111 pub fn option(&self, name: &str) -> Option<&str> {
123 let value = self.options.get(name).map(String::as_str);
124
125 if value.is_none() && self.flags.contains(name) {
126 tracing::warn!(
127 "`--{name}` was given with no value; write `--{name}=value`. Ignoring it."
128 );
129 }
130 value
131 }
132
133 pub fn option_or<'a>(&'a self, name: &str, default: &'a str) -> &'a str {
135 self.option(name).unwrap_or(default)
136 }
137
138 pub fn parsed_or<T: std::str::FromStr>(&self, name: &str, default: T) -> T {
140 self.option(name).and_then(|value| value.parse().ok()).unwrap_or(default)
141 }
142
143 pub fn flag(&self, name: &str) -> bool {
145 self.flags.contains(name)
146 }
147
148 pub fn is_valueless(&self, name: &str) -> bool {
153 self.flags.contains(name) && !self.options.contains_key(name)
154 }
155
156 pub fn wants_help(&self) -> bool {
158 self.flag("help") || self.flag("h") || self.command == "help"
159 }
160
161 pub fn options(&self) -> &HashMap<String, String> {
163 &self.options
164 }
165
166 pub fn flags(&self) -> &HashSet<String> {
168 &self.flags
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 fn parse(line: &str) -> Arguments {
177 Arguments::parse(line.split_whitespace())
178 }
179
180 #[test]
181 fn the_first_word_is_the_command() {
182 let args = parse("route:list");
183 assert_eq!(args.command(), "route:list");
184 assert!(args.positional().is_empty());
185 assert!(!args.is_empty());
186 }
187
188 #[test]
189 fn an_empty_line_has_no_command() {
190 assert!(Arguments::parse(Vec::<String>::new()).is_empty());
191 }
192
193 #[test]
194 fn later_words_are_positional() {
195 let args = parse("make:controller PostController extra");
196 assert_eq!(args.command(), "make:controller");
197 assert_eq!(args.argument(0), Some("PostController"));
198 assert_eq!(args.argument(1), Some("extra"));
199 assert_eq!(args.argument(2), None);
200 }
201
202 #[test]
203 fn long_options_carry_a_value() {
204 let args = parse("serve --port=3000 --host=0.0.0.0");
205 assert_eq!(args.option("port"), Some("3000"));
206 assert_eq!(args.option("host"), Some("0.0.0.0"));
207 assert_eq!(args.parsed_or("port", 8000u16), 3000);
208 }
209
210 #[test]
211 fn a_missing_or_unparsable_option_falls_back() {
212 let args = parse("serve --port=nonsense");
213 assert_eq!(args.parsed_or("port", 8000u16), 8000);
214 assert_eq!(args.parsed_or("timeout", 30u32), 30);
215 assert_eq!(args.option_or("host", "127.0.0.1"), "127.0.0.1");
216 }
217
218 #[test]
219 fn a_valueless_long_argument_is_a_flag() {
220 let args = parse("queue:work --once --quiet");
221 assert!(args.flag("once"));
222 assert!(args.flag("quiet"));
223 assert!(!args.flag("verbose"));
224 assert!(args.option("once").is_none());
225 }
226
227 #[test]
228 fn short_flags_can_be_bundled() {
229 let args = parse("migrate -vf");
230 assert!(args.flag("v"));
231 assert!(args.flag("f"));
232 assert_eq!(args.flags().len(), 2);
233 }
234
235 #[test]
236 fn a_separated_value_is_positional_not_an_option() {
237 let args = parse("serve --port 3000");
240 assert!(args.flag("port"));
241 assert_eq!(args.option("port"), None);
242 assert_eq!(args.argument(0), Some("3000"));
243 }
244
245 #[test]
246 fn a_double_dash_ends_option_parsing() {
247 let args = parse("run -- --not-a-flag positional");
248 assert_eq!(args.command(), "run");
249 assert_eq!(args.positional(), ["--not-a-flag", "positional"]);
250 assert!(args.flags().is_empty());
251 }
252
253 #[test]
254 fn a_negative_number_is_not_a_flag_bundle() {
255 let args = parse("offset -5");
256 assert_eq!(args.argument(0), Some("-5"));
257 assert!(args.flags().is_empty());
258 }
259
260 #[test]
261 fn an_empty_option_value_is_kept() {
262 let args = parse("serve --host=");
263 assert_eq!(args.option("host"), Some(""));
264 }
265
266 #[test]
267 fn help_is_recognised_however_it_is_spelled() {
268 assert!(parse("serve --help").wants_help());
269 assert!(parse("serve -h").wants_help());
270 assert!(parse("help").wants_help());
271 assert!(!parse("serve").wants_help());
272 }
273
274 #[test]
275 fn an_option_given_without_a_value_is_not_silently_a_flag() {
276 let args = Arguments::parse(["serve", "--port", "8091"]);
280
281 assert!(args.is_valueless("port"));
282 assert_eq!(args.option("port"), None);
283 assert_eq!(args.argument(0), Some("8091"), "the value became a positional");
284
285 let args = Arguments::parse(["serve", "--port=8091"]);
287 assert_eq!(args.option("port"), Some("8091"));
288 assert!(!args.is_valueless("port"));
289 }
290}