1use std::collections::HashMap;
19use std::env;
20use std::fs;
21
22mod colors {
24 pub const RESET: &str = "\x1b[0m";
25 pub const BOLD: &str = "\x1b[1m";
26 pub const RED: &str = "\x1b[31m";
27 pub const GREEN: &str = "\x1b[32m";
28 pub const YELLOW: &str = "\x1b[33m";
29 pub const BLUE: &str = "\x1b[34m";
30 pub const CYAN: &str = "\x1b[36m";
31 pub const GRAY: &str = "\x1b[90m";
32
33 pub fn colorize(text: &str, color: &str) -> String {
34 if is_color_supported() {
35 format!("{}{}{}", color, text, RESET)
36 } else {
37 text.to_string()
38 }
39 }
40
41 fn is_color_supported() -> bool {
42 std::env::var("NO_COLOR").is_err()
43 && (std::env::var("TERM").map(|t| t != "dumb").unwrap_or(false)
44 || std::env::var("COLORTERM").is_ok())
45 }
46}
47
48#[derive(Clone)]
50pub struct ArgGroup {
51 name: String,
52 args: Vec<String>,
53 required: bool,
54 multiple: bool,
55}
56
57impl ArgGroup {
58 pub fn new(name: impl Into<String>) -> Self {
59 Self {
60 name: name.into(),
61 args: Vec::new(),
62 required: false,
63 multiple: false,
64 }
65 }
66
67 pub fn args(mut self, args: &[&str]) -> Self {
68 self.args = args.iter().map(|s| s.to_string()).collect();
69 self
70 }
71
72 pub fn required(mut self, req: bool) -> Self {
73 self.required = req;
74 self
75 }
76
77 pub fn multiple(mut self, mult: bool) -> Self {
78 self.multiple = mult;
79 self
80 }
81}
82
83pub type Validator = fn(&str) -> Result<(), String>;
85
86#[derive(Debug, Clone, Copy, PartialEq)]
88pub enum Shell {
89 Bash,
90 Zsh,
91 Fish,
92 PowerShell,
93}
94
95#[derive(Debug, Clone, PartialEq)]
97pub enum ValueSource {
98 CommandLine,
99 Environment,
100 ConfigFile,
101 Default,
102}
103
104#[macro_export]
106macro_rules! cli {
107 ($name:expr => {
108 version: $version:expr,
109 about: $about:expr,
110 args: [$($arg:expr),* $(,)?]
111 }) => {
112 {
113 let mut app = $crate::App::new($name)
114 .version($version)
115 .about($about);
116 $(
117 app = app.arg($arg);
118 )*
119 app
120 }
121 };
122}
123
124#[macro_export]
126macro_rules! arg {
127 ($name:expr) => {
128 $crate::Arg::new($name)
129 };
130 ($name:expr, short: $short:expr) => {
131 $crate::Arg::new($name).short($short)
132 };
133 ($name:expr, required) => {
134 $crate::Arg::new($name).required(true).takes_value(true)
135 };
136 ($name:expr, $($key:ident: $value:expr),* $(,)?) => {
137 {
138 let mut a = $crate::Arg::new($name);
139 $(
140 a = arg!(@set a, $key: $value);
141 )*
142 a
143 }
144 };
145 (@set $arg:expr, short: $short:expr) => { $arg.short($short) };
146 (@set $arg:expr, help: $help:expr) => { $arg.help($help) };
147 (@set $arg:expr, required: $req:expr) => { $arg.required($req) };
148 (@set $arg:expr, takes_value: $tv:expr) => { $arg.takes_value($tv) };
149 (@set $arg:expr, default: $def:expr) => { $arg.default_value($def) };
150}
151
152pub struct App {
158 name: String,
159 version: String,
160 about: String,
161 author: Option<String>,
162 commands: Vec<Command>,
163 global_args: Vec<Arg>,
164 groups: Vec<ArgGroup>,
165 colored_help: bool,
166 config_file: Option<String>,
167 env_prefix: Option<String>,
168}
169
170impl App {
171 pub fn new(name: impl Into<String>) -> Self {
172 Self {
173 name: name.into(),
174 version: "1.0.0".to_string(),
175 about: String::new(),
176 author: None,
177 commands: Vec::new(),
178 global_args: Vec::new(),
179 groups: Vec::new(),
180 colored_help: true,
181 config_file: None,
182 env_prefix: None,
183 }
184 }
185
186 pub fn version(mut self, version: impl Into<String>) -> Self {
187 self.version = version.into();
188 self
189 }
190
191 pub fn about(mut self, about: impl Into<String>) -> Self {
192 self.about = about.into();
193 self
194 }
195
196 pub fn author(mut self, author: impl Into<String>) -> Self {
197 self.author = Some(author.into());
198 self
199 }
200
201 pub fn command(mut self, cmd: Command) -> Self {
202 self.commands.push(cmd);
203 self
204 }
205
206 pub fn arg(mut self, arg: Arg) -> Self {
207 self.global_args.push(arg);
208 self
209 }
210
211 pub fn group(mut self, group: ArgGroup) -> Self {
212 self.groups.push(group);
213 self
214 }
215
216 pub fn colored_help(mut self, colored: bool) -> Self {
217 self.colored_help = colored;
218 self
219 }
220
221 pub fn config_file(mut self, path: impl Into<String>) -> Self {
223 self.config_file = Some(path.into());
224 self
225 }
226
227 pub fn env_prefix(mut self, prefix: impl Into<String>) -> Self {
230 self.env_prefix = Some(prefix.into());
231 self
232 }
233
234 pub fn generate_completion(&self, shell: Shell) -> String {
236 match shell {
237 Shell::Bash => self.generate_bash_completion(),
238 Shell::Zsh => self.generate_zsh_completion(),
239 Shell::Fish => self.generate_fish_completion(),
240 Shell::PowerShell => self.generate_powershell_completion(),
241 }
242 }
243
244 fn generate_bash_completion(&self) -> String {
245 let mut script = format!("_{}_completion() {{\n", self.name);
246 script.push_str(" local cur prev opts\n");
247 script.push_str(" COMPREPLY=()\n");
248 script.push_str(" cur=\"${COMP_WORDS[COMP_CWORD]}\"\n");
249 script.push_str(" prev=\"${COMP_WORDS[COMP_CWORD-1]}\"\n\n");
250
251 script.push_str(" opts=\"");
253 for arg in &self.global_args {
254 script.push_str(&format!("--{} ", arg.long));
255 if let Some(short) = &arg.short {
256 script.push_str(&format!("-{} ", short));
257 }
258 }
259 script.push_str("\"\n\n");
260
261 if !self.commands.is_empty() {
263 script.push_str(" local commands=\"");
264 for cmd in &self.commands {
265 script.push_str(&format!("{} ", cmd.name));
266 }
267 script.push_str("\"\n\n");
268 }
269
270 script.push_str(" COMPREPLY=( $(compgen -W \"${opts} ${commands}\" -- ${cur}) )\n");
271 script.push_str(" return 0\n");
272 script.push_str("}\n\n");
273 script.push_str(&format!("complete -F _{}_completion {}\n", self.name, self.name));
274
275 script
276 }
277
278 fn generate_zsh_completion(&self) -> String {
279 let mut script = format!("#compdef {}\n\n", self.name);
280 script.push_str(&format!("_{}_completion() {{\n", self.name));
281 script.push_str(" local -a opts\n");
282 script.push_str(" opts=(\n");
283
284 for arg in &self.global_args {
285 let help = arg.help.replace('\"', "'");
286 if let Some(short) = &arg.short {
287 script.push_str(&format!(" '(-{})--{}[{}]'\n", short, arg.long, help));
288 } else {
289 script.push_str(&format!(" '--{}[{}]'\n", arg.long, help));
290 }
291 }
292
293 script.push_str(" )\n");
294 script.push_str(" _arguments $opts\n");
295 script.push_str("}\n\n");
296 script.push_str(&format!("_{}_completion\n", self.name));
297
298 script
299 }
300
301 fn generate_fish_completion(&self) -> String {
302 let mut script = String::new();
303
304 for arg in &self.global_args {
305 script.push_str(&format!("complete -c {} -l {} -d '{}'\n",
306 self.name, arg.long, arg.help.replace('\'', "\\'")));
307
308 if let Some(short) = &arg.short {
309 script.push_str(&format!("complete -c {} -s {} -d '{}'\n",
310 self.name, short, arg.help.replace('\'', "\\'")));
311 }
312 }
313
314 for cmd in &self.commands {
315 script.push_str(&format!("complete -c {} -f -a '{}' -d '{}'\n",
316 self.name, cmd.name, cmd.about.replace('\'', "\\'")));
317 }
318
319 script
320 }
321
322 fn generate_powershell_completion(&self) -> String {
323 let mut script = format!("Register-ArgumentCompleter -CommandName {} -ScriptBlock {{\n", self.name);
324 script.push_str(" param($commandName, $wordToComplete, $commandAst, $fakeBoundParameter)\n\n");
325 script.push_str(" $completions = @(\n");
326
327 for arg in &self.global_args {
328 script.push_str(&format!(" @{{ CompletionText = '--{}'; ListItemText = '--{}'; ToolTip = '{}' }},\n",
329 arg.long, arg.long, arg.help.replace('\"', "'")));
330 }
331
332 for cmd in &self.commands {
333 script.push_str(&format!(" @{{ CompletionText = '{}'; ListItemText = '{}'; ToolTip = '{}' }},\n",
334 cmd.name, cmd.name, cmd.about.replace('\"', "'")));
335 }
336
337 script.push_str(" )\n\n");
338 script.push_str(" $completions | Where-Object { $_.CompletionText -like \"$wordToComplete*\" } | \n");
339 script.push_str(" ForEach-Object { [System.Management.Automation.CompletionResult]::new($_.CompletionText, $_.ListItemText, 'ParameterValue', $_.ToolTip) }\n");
340 script.push_str("}\n");
341
342 script
343 }
344
345 pub fn parse(self) -> Matches {
346 let args: Vec<String> = env::args().skip(1).collect();
347 self.parse_args(&args)
348 }
349
350 fn parse_args(self, args: &[String]) -> Matches {
351 let mut matches = Matches {
352 command: None,
353 args: HashMap::new(),
354 values: Vec::new(),
355 sources: HashMap::new(),
356 };
357
358 if args.is_empty() {
359 return matches;
360 }
361
362 if args[0] == "--help" || args[0] == "-h" {
364 self.print_help();
365 std::process::exit(0);
366 }
367 if args[0] == "--version" || args[0] == "-V" {
368 println!("{} {}", self.name, self.version);
369 std::process::exit(0);
370 }
371
372 if let Some(cmd) = self.commands.iter().find(|c| c.name == args[0]) {
374 matches.command = Some(args[0].clone());
375 matches.parse_command_args(cmd, &args[1..]);
376 } else {
377 matches.parse_args_list(&self.global_args, args);
378 }
379
380 self.apply_defaults_and_validate(&mut matches);
382
383 matches
384 }
385
386 fn apply_defaults_and_validate(&self, matches: &mut Matches) {
387 let config_values = if let Some(ref path) = self.config_file {
389 self.load_config_file(path)
390 } else {
391 HashMap::new()
392 };
393
394 for arg in &self.global_args {
395 let arg_name = &arg.name;
396
397 if !matches.is_present(arg_name) {
399 if let Some(ref env_var) = arg.env_var {
401 if let Ok(env_val) = env::var(env_var) {
402 matches.args.insert(arg_name.clone(), Some(env_val));
403 matches.sources.insert(arg_name.clone(), ValueSource::Environment);
404 continue;
405 }
406 }
407
408 if let Some(ref prefix) = self.env_prefix {
410 let env_key = format!("{}_{}", prefix.to_uppercase(), arg.long.to_uppercase());
411 if let Ok(env_val) = env::var(&env_key) {
412 matches.args.insert(arg_name.clone(), Some(env_val));
413 matches.sources.insert(arg_name.clone(), ValueSource::Environment);
414 continue;
415 }
416 }
417
418 if let Some(config_val) = config_values.get(arg_name) {
420 matches.args.insert(arg_name.clone(), Some(config_val.clone()));
421 matches.sources.insert(arg_name.clone(), ValueSource::ConfigFile);
422 continue;
423 }
424
425 if arg.default_value.is_some() {
427 matches.args.insert(arg_name.clone(), arg.default_value.clone());
428 matches.sources.insert(arg_name.clone(), ValueSource::Default);
429 }
430 } else {
431 matches.sources.insert(arg_name.clone(), ValueSource::CommandLine);
433 }
434
435 if arg.required && !matches.is_present(arg_name) {
437 let msg = if self.colored_help {
438 format!("Error: {} is required", colors::colorize(&format!("--{}", arg.long), colors::RED))
439 } else {
440 format!("Error: --{} is required", arg.long)
441 };
442 eprintln!("{}", msg);
443 std::process::exit(1);
444 }
445
446 if !arg.possible_values.is_empty() {
448 if let Some(value) = matches.value_of(&arg.name) {
449 if !arg.possible_values.iter().any(|v| v == value) {
450 let msg = if self.colored_help {
451 format!(
452 "Error: invalid value {} for {}",
453 colors::colorize(&format!("'{}'", value), colors::RED),
454 colors::colorize(&format!("--{}", arg.long), colors::CYAN)
455 )
456 } else {
457 format!("Error: invalid value '{}' for --{}", value, arg.long)
458 };
459 eprintln!("{}", msg);
460 eprintln!("Possible values: {}", arg.possible_values.join(", "));
461 std::process::exit(1);
462 }
463 }
464 }
465
466 if let Some(validator) = &arg.validator {
468 if let Some(value) = matches.value_of(&arg.name) {
469 if let Err(err) = validator(value) {
470 let msg = if self.colored_help {
471 format!(
472 "Error: validation failed for {}: {}",
473 colors::colorize(&format!("--{}", arg.long), colors::CYAN),
474 colors::colorize(&err, colors::RED)
475 )
476 } else {
477 format!("Error: validation failed for --{}: {}", arg.long, err)
478 };
479 eprintln!("{}", msg);
480 std::process::exit(1);
481 }
482 }
483 }
484
485 for conflict in &arg.conflicts_with {
487 if matches.is_present(arg_name) && matches.is_present(conflict) {
488 let msg = if self.colored_help {
489 format!(
490 "Error: {} conflicts with {}",
491 colors::colorize(&format!("--{}", arg.long), colors::RED),
492 colors::colorize(&format!("--{}", conflict), colors::RED)
493 )
494 } else {
495 format!("Error: --{} conflicts with --{}", arg.long, conflict)
496 };
497 eprintln!("{}", msg);
498 std::process::exit(1);
499 }
500 }
501
502 for required in &arg.requires {
504 if matches.is_present(arg_name) && !matches.is_present(required) {
505 let msg = if self.colored_help {
506 format!(
507 "Error: {} requires {}",
508 colors::colorize(&format!("--{}", arg.long), colors::CYAN),
509 colors::colorize(&format!("--{}", required), colors::YELLOW)
510 )
511 } else {
512 format!("Error: --{} requires --{}", arg.long, required)
513 };
514 eprintln!("{}", msg);
515 std::process::exit(1);
516 }
517 }
518 }
519
520 for group in &self.groups {
522 let present_args: Vec<String> = group.args.iter()
523 .filter(|arg_name| matches.is_present(arg_name))
524 .map(|s| s.clone())
525 .collect();
526
527 if group.required && present_args.is_empty() {
529 let msg = if self.colored_help {
530 format!(
531 "Error: at least one of {} is required",
532 colors::colorize(&format!("[{}]", group.args.join(", ")), colors::YELLOW)
533 )
534 } else {
535 format!("Error: at least one of [{}] is required", group.args.join(", "))
536 };
537 eprintln!("{}", msg);
538 std::process::exit(1);
539 }
540
541 if !group.multiple && present_args.len() > 1 {
543 let msg = if self.colored_help {
544 format!(
545 "Error: arguments {} are mutually exclusive",
546 colors::colorize(&present_args.join(", "), colors::RED)
547 )
548 } else {
549 format!("Error: arguments {} are mutually exclusive", present_args.join(", "))
550 };
551 eprintln!("{}", msg);
552 std::process::exit(1);
553 }
554 }
555 }
556
557 fn print_help(&self) {
558 let name = if self.colored_help {
559 colors::colorize(&self.name, colors::BOLD)
560 } else {
561 self.name.clone()
562 };
563 println!("{}", name);
564
565 if !self.about.is_empty() {
566 println!("{}\n", self.about);
567 }
568
569 let usage = if self.colored_help {
570 format!("{}: {} [OPTIONS] [COMMAND]",
571 colors::colorize("Usage", colors::BOLD),
572 self.name.to_lowercase()
573 )
574 } else {
575 format!("Usage: {} [OPTIONS] [COMMAND]", self.name.to_lowercase())
576 };
577 println!("{}\n", usage);
578
579 if !self.commands.is_empty() {
580 let header = if self.colored_help {
581 colors::colorize("Commands:", colors::BOLD)
582 } else {
583 "Commands:".to_string()
584 };
585 println!("{}", header);
586
587 for cmd in &self.commands {
588 let cmd_name = if self.colored_help {
589 colors::colorize(&cmd.name, colors::CYAN)
590 } else {
591 cmd.name.clone()
592 };
593 println!(" {:<12} {}", cmd_name, cmd.about);
594 }
595 println!();
596 }
597
598 let options_header = if self.colored_help {
599 colors::colorize("Options:", colors::BOLD)
600 } else {
601 "Options:".to_string()
602 };
603 println!("{}", options_header);
604
605 let help_text = if self.colored_help {
606 format!(" {}, {} Print help",
607 colors::colorize("-h", colors::GREEN),
608 colors::colorize("--help", colors::GREEN)
609 )
610 } else {
611 " -h, --help Print help".to_string()
612 };
613 println!("{}", help_text);
614
615 let version_text = if self.colored_help {
616 format!(" {}, {} Print version",
617 colors::colorize("-V", colors::GREEN),
618 colors::colorize("--version", colors::GREEN)
619 )
620 } else {
621 " -V, --version Print version".to_string()
622 };
623 println!("{}", version_text);
624
625 for arg in &self.global_args {
626 let short = arg.short.as_ref().map(|s| format!("-{}, ", s)).unwrap_or_default();
627 let long_with_color = if self.colored_help {
628 colors::colorize(&format!("--{}", arg.long), colors::GREEN)
629 } else {
630 format!("--{}", arg.long)
631 };
632
633 let required_marker = if arg.required && self.colored_help {
634 format!(" {}", colors::colorize("[required]", colors::RED))
635 } else if arg.required {
636 " [required]".to_string()
637 } else {
638 String::new()
639 };
640
641 println!(" {}{:<12} {}{}", short, long_with_color, arg.help, required_marker);
642 }
643 }
644
645 fn load_config_file(&self, path: &str) -> HashMap<String, String> {
647 let mut config = HashMap::new();
648
649 if let Ok(contents) = fs::read_to_string(path) {
650 for line in contents.lines() {
651 let line = line.trim();
652
653 if line.is_empty() || line.starts_with('#') || line.starts_with("//") {
655 continue;
656 }
657
658 let parts: Vec<&str> = if line.contains('=') {
660 line.splitn(2, '=').collect()
661 } else if line.contains(':') {
662 line.splitn(2, ':').collect()
663 } else {
664 continue;
665 };
666
667 if parts.len() == 2 {
668 let key = parts[0].trim().to_lowercase();
669 let value = parts[1].trim().trim_matches('"').to_string();
670 config.insert(key, value);
671 }
672 }
673 }
674
675 config
676 }
677}
678
679pub struct Command {
684 name: String,
685 about: String,
686 args: Vec<Arg>,
687}
688
689impl Command {
690 pub fn new(name: impl Into<String>) -> Self {
691 Self {
692 name: name.into(),
693 about: String::new(),
694 args: Vec::new(),
695 }
696 }
697
698 pub fn about(mut self, about: impl Into<String>) -> Self {
699 self.about = about.into();
700 self
701 }
702
703 pub fn arg(mut self, arg: Arg) -> Self {
704 self.args.push(arg);
705 self
706 }
707}
708
709pub struct Arg {
714 name: String,
715 long: String,
716 short: Option<String>,
717 help: String,
718 takes_value: bool,
719 required: bool,
720 default_value: Option<String>,
721 possible_values: Vec<String>,
722 validator: Option<Validator>,
723 env_var: Option<String>,
724 hidden: bool,
725 conflicts_with: Vec<String>,
726 requires: Vec<String>,
727}
728
729impl Arg {
730 pub fn new(name: impl Into<String>) -> Self {
731 let name = name.into();
732 Self {
733 long: name.clone(),
734 name,
735 short: None,
736 help: String::new(),
737 takes_value: false,
738 required: false,
739 default_value: None,
740 possible_values: Vec::new(),
741 validator: None,
742 env_var: None,
743 hidden: false,
744 conflicts_with: Vec::new(),
745 requires: Vec::new(),
746 }
747 }
748
749 pub fn long(mut self, long: impl Into<String>) -> Self {
750 self.long = long.into();
751 self
752 }
753
754 pub fn short(mut self, short: char) -> Self {
755 self.short = Some(short.to_string());
756 self
757 }
758
759 pub fn help(mut self, help: impl Into<String>) -> Self {
760 self.help = help.into();
761 self
762 }
763
764 pub fn takes_value(mut self, takes: bool) -> Self {
765 self.takes_value = takes;
766 self
767 }
768
769 pub fn required(mut self, req: bool) -> Self {
770 self.required = req;
771 self
772 }
773
774 pub fn default_value(mut self, value: impl Into<String>) -> Self {
784 self.default_value = Some(value.into());
785 self
786 }
787
788 pub fn possible_values(mut self, values: &[&str]) -> Self {
798 self.possible_values = values.iter().map(|s| s.to_string()).collect();
799 self
800 }
801
802 pub fn validator(mut self, f: Validator) -> Self {
816 self.validator = Some(f);
817 self
818 }
819
820 pub fn env(mut self, var: impl Into<String>) -> Self {
822 self.env_var = Some(var.into());
823 self
824 }
825
826 pub fn hidden(mut self, hide: bool) -> Self {
828 self.hidden = hide;
829 self
830 }
831
832 pub fn conflicts_with(mut self, arg: impl Into<String>) -> Self {
834 self.conflicts_with.push(arg.into());
835 self
836 }
837
838 pub fn requires(mut self, arg: impl Into<String>) -> Self {
840 self.requires.push(arg.into());
841 self
842 }
843}
844
845pub struct Matches {
850 command: Option<String>,
851 args: HashMap<String, Option<String>>,
852 values: Vec<String>,
853 sources: HashMap<String, ValueSource>,
854}
855
856impl Matches {
857 pub fn subcommand(&self) -> Option<&str> {
858 self.command.as_deref()
859 }
860
861 pub fn is_present(&self, name: &str) -> bool {
862 self.args.contains_key(name)
863 }
864
865 pub fn value_of(&self, name: &str) -> Option<&str> {
866 self.args.get(name)?.as_deref()
867 }
868
869 pub fn values(&self) -> &[String] {
870 &self.values
871 }
872
873 pub fn value_source(&self, name: &str) -> Option<&ValueSource> {
875 self.sources.get(name)
876 }
877
878 pub fn value_as<T>(&self, name: &str) -> Option<T>
888 where
889 T: std::str::FromStr,
890 {
891 self.value_of(name)?.parse().ok()
892 }
893
894 pub fn any_present(&self, names: &[&str]) -> bool {
904 names.iter().any(|name| self.is_present(name))
905 }
906
907 pub fn all_present(&self, names: &[&str]) -> bool {
909 names.iter().all(|name| self.is_present(name))
910 }
911
912 pub fn value_or<'a>(&'a self, name: &str, default: &'a str) -> &'a str {
914 self.value_of(name).unwrap_or(default)
915 }
916
917 pub fn values_count(&self) -> usize {
919 self.values.len()
920 }
921
922 fn parse_command_args(&mut self, cmd: &Command, args: &[String]) {
923 self.parse_args_list(&cmd.args, args);
924 }
925
926 fn parse_args_list(&mut self, arg_defs: &[Arg], args: &[String]) {
927 let mut i = 0;
928 while i < args.len() {
929 let arg = &args[i];
930
931 if arg.starts_with("--") {
932 let key = &arg[2..];
933 if let Some(arg_def) = arg_defs.iter().find(|a| a.long == key) {
934 if arg_def.takes_value && i + 1 < args.len() {
935 self.args.insert(arg_def.name.clone(), Some(args[i + 1].clone()));
936 i += 2;
937 } else {
938 self.args.insert(arg_def.name.clone(), None);
939 i += 1;
940 }
941 } else {
942 i += 1;
943 }
944 } else if arg.starts_with('-') && arg.len() == 2 {
945 let short = &arg[1..];
946 if let Some(arg_def) = arg_defs.iter().find(|a| a.short.as_deref() == Some(short)) {
947 if arg_def.takes_value && i + 1 < args.len() {
948 self.args.insert(arg_def.name.clone(), Some(args[i + 1].clone()));
949 i += 2;
950 } else {
951 self.args.insert(arg_def.name.clone(), None);
952 i += 1;
953 }
954 } else {
955 i += 1;
956 }
957 } else {
958 self.values.push(arg.clone());
959 i += 1;
960 }
961 }
962 }
963}
964
965#[cfg(test)]
966mod tests {
967 use super::*;
968
969 #[test]
970 fn test_arg_creation() {
971 let arg = Arg::new("test")
972 .long("test")
973 .short('t')
974 .help("Test argument")
975 .takes_value(true);
976
977 assert_eq!(arg.name, "test");
978 assert_eq!(arg.long, "test");
979 assert_eq!(arg.short, Some("t".to_string()));
980 }
981
982 #[test]
983 fn test_command_creation() {
984 let cmd = Command::new("test")
985 .about("Test command")
986 .arg(Arg::new("arg1"));
987
988 assert_eq!(cmd.name, "test");
989 assert_eq!(cmd.args.len(), 1);
990 }
991
992 #[test]
993 fn test_value_as_parsing() {
994 let mut matches = Matches {
995 command: None,
996 args: HashMap::new(),
997 values: Vec::new(),
998 sources: HashMap::new(),
999 };
1000 matches.args.insert("port".to_string(), Some("8080".to_string()));
1001
1002 let port: u16 = matches.value_as("port").unwrap();
1003 assert_eq!(port, 8080);
1004 }
1005
1006 #[test]
1007 fn test_any_present() {
1008 let mut matches = Matches {
1009 command: None,
1010 args: HashMap::new(),
1011 values: Vec::new(),
1012 sources: HashMap::new(),
1013 };
1014 matches.args.insert("verbose".to_string(), None);
1015
1016 assert!(matches.any_present(&["verbose", "debug"]));
1017 assert!(!matches.any_present(&["quiet", "silent"]));
1018 }
1019
1020 #[test]
1021 fn test_all_present() {
1022 let mut matches = Matches {
1023 command: None,
1024 args: HashMap::new(),
1025 values: Vec::new(),
1026 sources: HashMap::new(),
1027 };
1028 matches.args.insert("verbose".to_string(), None);
1029 matches.args.insert("debug".to_string(), None);
1030
1031 assert!(matches.all_present(&["verbose", "debug"]));
1032 assert!(!matches.all_present(&["verbose", "debug", "trace"]));
1033 }
1034
1035 #[test]
1036 fn test_value_or_default() {
1037 let matches = Matches {
1038 command: None,
1039 args: HashMap::new(),
1040 values: Vec::new(),
1041 sources: HashMap::new(),
1042 };
1043
1044 assert_eq!(matches.value_or("port", "8080"), "8080");
1045 }
1046
1047 #[test]
1048 fn test_values_count() {
1049 let matches = Matches {
1050 command: None,
1051 args: HashMap::new(),
1052 values: vec!["file1".to_string(), "file2".to_string()],
1053 sources: HashMap::new(),
1054 };
1055
1056 assert_eq!(matches.values_count(), 2);
1057 }
1058}