1use std::collections::HashMap;
7use std::env;
8
9pub struct App {
15 name: String,
16 version: String,
17 about: String,
18 commands: Vec<Command>,
19 global_args: Vec<Arg>,
20}
21
22impl App {
23 pub fn new(name: impl Into<String>) -> Self {
24 Self {
25 name: name.into(),
26 version: "0.2.0".to_string(),
27 about: String::new(),
28 commands: Vec::new(),
29 global_args: Vec::new(),
30 }
31 }
32
33 pub fn version(mut self, version: impl Into<String>) -> Self {
34 self.version = version.into();
35 self
36 }
37
38 pub fn about(mut self, about: impl Into<String>) -> Self {
39 self.about = about.into();
40 self
41 }
42
43 pub fn command(mut self, cmd: Command) -> Self {
44 self.commands.push(cmd);
45 self
46 }
47
48 pub fn arg(mut self, arg: Arg) -> Self {
49 self.global_args.push(arg);
50 self
51 }
52
53 pub fn parse(self) -> Matches {
54 let args: Vec<String> = env::args().skip(1).collect();
55 self.parse_args(&args)
56 }
57
58 fn parse_args(self, args: &[String]) -> Matches {
59 let mut matches = Matches {
60 command: None,
61 args: HashMap::new(),
62 values: Vec::new(),
63 };
64
65 if args.is_empty() {
66 return matches;
67 }
68
69 if args[0] == "--help" || args[0] == "-h" {
71 self.print_help();
72 std::process::exit(0);
73 }
74 if args[0] == "--version" || args[0] == "-V" {
75 println!("{} {}", self.name, self.version);
76 std::process::exit(0);
77 }
78
79 if let Some(cmd) = self.commands.iter().find(|c| c.name == args[0]) {
81 matches.command = Some(args[0].clone());
82 matches.parse_command_args(cmd, &args[1..]);
83 } else {
84 matches.parse_args_list(&self.global_args, args);
85 }
86
87 matches
88 }
89
90 fn print_help(&self) {
91 println!("{}", self.name);
92 if !self.about.is_empty() {
93 println!("{}\n", self.about);
94 }
95 println!("Usage: {} [OPTIONS] [COMMAND]\n", self.name.to_lowercase());
96
97 if !self.commands.is_empty() {
98 println!("Commands:");
99 for cmd in &self.commands {
100 println!(" {:<12} {}", cmd.name, cmd.about);
101 }
102 println!();
103 }
104
105 println!("Options:");
106 println!(" -h, --help Print help");
107 println!(" -V, --version Print version");
108
109 for arg in &self.global_args {
110 let short = arg.short.as_ref().map(|s| format!("-{}, ", s)).unwrap_or_default();
111 println!(" {}{:<12} {}", short, format!("--{}", arg.long), arg.help);
112 }
113 }
114}
115
116pub struct Command {
121 name: String,
122 about: String,
123 args: Vec<Arg>,
124}
125
126impl Command {
127 pub fn new(name: impl Into<String>) -> Self {
128 Self {
129 name: name.into(),
130 about: String::new(),
131 args: Vec::new(),
132 }
133 }
134
135 pub fn about(mut self, about: impl Into<String>) -> Self {
136 self.about = about.into();
137 self
138 }
139
140 pub fn arg(mut self, arg: Arg) -> Self {
141 self.args.push(arg);
142 self
143 }
144}
145
146pub struct Arg {
151 name: String,
152 long: String,
153 short: Option<String>,
154 help: String,
155 takes_value: bool,
156 required: bool,
157}
158
159impl Arg {
160 pub fn new(name: impl Into<String>) -> Self {
161 let name = name.into();
162 Self {
163 long: name.clone(),
164 name,
165 short: None,
166 help: String::new(),
167 takes_value: false,
168 required: false,
169 }
170 }
171
172 pub fn long(mut self, long: impl Into<String>) -> Self {
173 self.long = long.into();
174 self
175 }
176
177 pub fn short(mut self, short: char) -> Self {
178 self.short = Some(short.to_string());
179 self
180 }
181
182 pub fn help(mut self, help: impl Into<String>) -> Self {
183 self.help = help.into();
184 self
185 }
186
187 pub fn takes_value(mut self, takes: bool) -> Self {
188 self.takes_value = takes;
189 self
190 }
191
192 pub fn required(mut self, req: bool) -> Self {
193 self.required = req;
194 self
195 }
196}
197
198pub struct Matches {
203 command: Option<String>,
204 args: HashMap<String, Option<String>>,
205 values: Vec<String>,
206}
207
208impl Matches {
209 pub fn subcommand(&self) -> Option<&str> {
210 self.command.as_deref()
211 }
212
213 pub fn is_present(&self, name: &str) -> bool {
214 self.args.contains_key(name)
215 }
216
217 pub fn value_of(&self, name: &str) -> Option<&str> {
218 self.args.get(name)?.as_deref()
219 }
220
221 pub fn values(&self) -> &[String] {
222 &self.values
223 }
224
225 pub fn value_as<T>(&self, name: &str) -> Option<T>
232 where
233 T: std::str::FromStr,
234 {
235 self.value_of(name)?.parse().ok()
236 }
237
238 pub fn any_present(&self, names: &[&str]) -> bool {
247 names.iter().any(|name| self.is_present(name))
248 }
249
250 pub fn all_present(&self, names: &[&str]) -> bool {
252 names.iter().all(|name| self.is_present(name))
253 }
254
255 pub fn value_or<'a>(&'a self, name: &str, default: &'a str) -> &'a str {
257 self.value_of(name).unwrap_or(default)
258 }
259
260 pub fn values_count(&self) -> usize {
262 self.values.len()
263 }
264
265 fn parse_command_args(&mut self, cmd: &Command, args: &[String]) {
266 self.parse_args_list(&cmd.args, args);
267 }
268
269 fn parse_args_list(&mut self, arg_defs: &[Arg], args: &[String]) {
270 let mut i = 0;
271 while i < args.len() {
272 let arg = &args[i];
273
274 if arg.starts_with("--") {
275 let key = &arg[2..];
276 if let Some(arg_def) = arg_defs.iter().find(|a| a.long == key) {
277 if arg_def.takes_value && i + 1 < args.len() {
278 self.args.insert(arg_def.name.clone(), Some(args[i + 1].clone()));
279 i += 2;
280 } else {
281 self.args.insert(arg_def.name.clone(), None);
282 i += 1;
283 }
284 } else {
285 i += 1;
286 }
287 } else if arg.starts_with('-') && arg.len() == 2 {
288 let short = &arg[1..];
289 if let Some(arg_def) = arg_defs.iter().find(|a| a.short.as_deref() == Some(short)) {
290 if arg_def.takes_value && i + 1 < args.len() {
291 self.args.insert(arg_def.name.clone(), Some(args[i + 1].clone()));
292 i += 2;
293 } else {
294 self.args.insert(arg_def.name.clone(), None);
295 i += 1;
296 }
297 } else {
298 i += 1;
299 }
300 } else {
301 self.values.push(arg.clone());
302 i += 1;
303 }
304 }
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311
312 #[test]
313 fn test_arg_creation() {
314 let arg = Arg::new("test")
315 .long("test")
316 .short('t')
317 .help("Test argument")
318 .takes_value(true);
319
320 assert_eq!(arg.name, "test");
321 assert_eq!(arg.long, "test");
322 assert_eq!(arg.short, Some("t".to_string()));
323 }
324
325 #[test]
326 fn test_command_creation() {
327 let cmd = Command::new("test")
328 .about("Test command")
329 .arg(Arg::new("arg1"));
330
331 assert_eq!(cmd.name, "test");
332 assert_eq!(cmd.args.len(), 1);
333 }
334
335 #[test]
336 fn test_value_as_parsing() {
337 let mut matches = Matches {
338 command: None,
339 args: HashMap::new(),
340 values: Vec::new(),
341 };
342 matches.args.insert("port".to_string(), Some("8080".to_string()));
343
344 let port: u16 = matches.value_as("port").unwrap();
345 assert_eq!(port, 8080);
346 }
347
348 #[test]
349 fn test_any_present() {
350 let mut matches = Matches {
351 command: None,
352 args: HashMap::new(),
353 values: Vec::new(),
354 };
355 matches.args.insert("verbose".to_string(), None);
356
357 assert!(matches.any_present(&["verbose", "debug"]));
358 assert!(!matches.any_present(&["quiet", "silent"]));
359 }
360
361 #[test]
362 fn test_all_present() {
363 let mut matches = Matches {
364 command: None,
365 args: HashMap::new(),
366 values: Vec::new(),
367 };
368 matches.args.insert("verbose".to_string(), None);
369 matches.args.insert("debug".to_string(), None);
370
371 assert!(matches.all_present(&["verbose", "debug"]));
372 assert!(!matches.all_present(&["verbose", "debug", "trace"]));
373 }
374
375 #[test]
376 fn test_value_or_default() {
377 let matches = Matches {
378 command: None,
379 args: HashMap::new(),
380 values: Vec::new(),
381 };
382
383 assert_eq!(matches.value_or("port", "8080"), "8080");
384 }
385
386 #[test]
387 fn test_values_count() {
388 let matches = Matches {
389 command: None,
390 args: HashMap::new(),
391 values: vec!["file1".to_string(), "file2".to_string()],
392 };
393
394 assert_eq!(matches.values_count(), 2);
395 }
396}