1use std::collections::HashMap;
15use std::env;
16
17mod colors {
19 pub const RESET: &str = "\x1b[0m";
20 pub const BOLD: &str = "\x1b[1m";
21 pub const RED: &str = "\x1b[31m";
22 pub const GREEN: &str = "\x1b[32m";
23 pub const YELLOW: &str = "\x1b[33m";
24 pub const BLUE: &str = "\x1b[34m";
25 pub const CYAN: &str = "\x1b[36m";
26 pub const GRAY: &str = "\x1b[90m";
27
28 pub fn colorize(text: &str, color: &str) -> String {
29 if is_color_supported() {
30 format!("{}{}{}", color, text, RESET)
31 } else {
32 text.to_string()
33 }
34 }
35
36 fn is_color_supported() -> bool {
37 std::env::var("NO_COLOR").is_err()
38 && (std::env::var("TERM").map(|t| t != "dumb").unwrap_or(false)
39 || std::env::var("COLORTERM").is_ok())
40 }
41}
42
43#[derive(Clone)]
45pub struct ArgGroup {
46 name: String,
47 args: Vec<String>,
48 required: bool,
49 multiple: bool,
50}
51
52impl ArgGroup {
53 pub fn new(name: impl Into<String>) -> Self {
54 Self {
55 name: name.into(),
56 args: Vec::new(),
57 required: false,
58 multiple: false,
59 }
60 }
61
62 pub fn args(mut self, args: &[&str]) -> Self {
63 self.args = args.iter().map(|s| s.to_string()).collect();
64 self
65 }
66
67 pub fn required(mut self, req: bool) -> Self {
68 self.required = req;
69 self
70 }
71
72 pub fn multiple(mut self, mult: bool) -> Self {
73 self.multiple = mult;
74 self
75 }
76}
77
78pub type Validator = fn(&str) -> Result<(), String>;
80
81#[derive(Debug, Clone, Copy, PartialEq)]
83pub enum Shell {
84 Bash,
85 Zsh,
86 Fish,
87 PowerShell,
88}
89
90pub struct App {
96 name: String,
97 version: String,
98 about: String,
99 author: Option<String>,
100 commands: Vec<Command>,
101 global_args: Vec<Arg>,
102 groups: Vec<ArgGroup>,
103 colored_help: bool,
104}
105
106impl App {
107 pub fn new(name: impl Into<String>) -> Self {
108 Self {
109 name: name.into(),
110 version: "1.0.0".to_string(),
111 about: String::new(),
112 author: None,
113 commands: Vec::new(),
114 global_args: Vec::new(),
115 groups: Vec::new(),
116 colored_help: true,
117 }
118 }
119
120 pub fn version(mut self, version: impl Into<String>) -> Self {
121 self.version = version.into();
122 self
123 }
124
125 pub fn about(mut self, about: impl Into<String>) -> Self {
126 self.about = about.into();
127 self
128 }
129
130 pub fn author(mut self, author: impl Into<String>) -> Self {
131 self.author = Some(author.into());
132 self
133 }
134
135 pub fn command(mut self, cmd: Command) -> Self {
136 self.commands.push(cmd);
137 self
138 }
139
140 pub fn arg(mut self, arg: Arg) -> Self {
141 self.global_args.push(arg);
142 self
143 }
144
145 pub fn group(mut self, group: ArgGroup) -> Self {
146 self.groups.push(group);
147 self
148 }
149
150 pub fn colored_help(mut self, colored: bool) -> Self {
151 self.colored_help = colored;
152 self
153 }
154
155 pub fn generate_completion(&self, shell: Shell) -> String {
157 match shell {
158 Shell::Bash => self.generate_bash_completion(),
159 Shell::Zsh => self.generate_zsh_completion(),
160 Shell::Fish => self.generate_fish_completion(),
161 Shell::PowerShell => self.generate_powershell_completion(),
162 }
163 }
164
165 fn generate_bash_completion(&self) -> String {
166 let mut script = format!("_{}_completion() {{\n", self.name);
167 script.push_str(" local cur prev opts\n");
168 script.push_str(" COMPREPLY=()\n");
169 script.push_str(" cur=\"${COMP_WORDS[COMP_CWORD]}\"\n");
170 script.push_str(" prev=\"${COMP_WORDS[COMP_CWORD-1]}\"\n\n");
171
172 script.push_str(" opts=\"");
174 for arg in &self.global_args {
175 script.push_str(&format!("--{} ", arg.long));
176 if let Some(short) = &arg.short {
177 script.push_str(&format!("-{} ", short));
178 }
179 }
180 script.push_str("\"\n\n");
181
182 if !self.commands.is_empty() {
184 script.push_str(" local commands=\"");
185 for cmd in &self.commands {
186 script.push_str(&format!("{} ", cmd.name));
187 }
188 script.push_str("\"\n\n");
189 }
190
191 script.push_str(" COMPREPLY=( $(compgen -W \"${opts} ${commands}\" -- ${cur}) )\n");
192 script.push_str(" return 0\n");
193 script.push_str("}\n\n");
194 script.push_str(&format!("complete -F _{}_completion {}\n", self.name, self.name));
195
196 script
197 }
198
199 fn generate_zsh_completion(&self) -> String {
200 let mut script = format!("#compdef {}\n\n", self.name);
201 script.push_str(&format!("_{}_completion() {{\n", self.name));
202 script.push_str(" local -a opts\n");
203 script.push_str(" opts=(\n");
204
205 for arg in &self.global_args {
206 let help = arg.help.replace('\"', "'");
207 if let Some(short) = &arg.short {
208 script.push_str(&format!(" '(-{})--{}[{}]'\n", short, arg.long, help));
209 } else {
210 script.push_str(&format!(" '--{}[{}]'\n", arg.long, help));
211 }
212 }
213
214 script.push_str(" )\n");
215 script.push_str(" _arguments $opts\n");
216 script.push_str("}\n\n");
217 script.push_str(&format!("_{}_completion\n", self.name));
218
219 script
220 }
221
222 fn generate_fish_completion(&self) -> String {
223 let mut script = String::new();
224
225 for arg in &self.global_args {
226 script.push_str(&format!("complete -c {} -l {} -d '{}'\n",
227 self.name, arg.long, arg.help.replace('\'', "\\'")));
228
229 if let Some(short) = &arg.short {
230 script.push_str(&format!("complete -c {} -s {} -d '{}'\n",
231 self.name, short, arg.help.replace('\'', "\\'")));
232 }
233 }
234
235 for cmd in &self.commands {
236 script.push_str(&format!("complete -c {} -f -a '{}' -d '{}'\n",
237 self.name, cmd.name, cmd.about.replace('\'', "\\'")));
238 }
239
240 script
241 }
242
243 fn generate_powershell_completion(&self) -> String {
244 let mut script = format!("Register-ArgumentCompleter -CommandName {} -ScriptBlock {{\n", self.name);
245 script.push_str(" param($commandName, $wordToComplete, $commandAst, $fakeBoundParameter)\n\n");
246 script.push_str(" $completions = @(\n");
247
248 for arg in &self.global_args {
249 script.push_str(&format!(" @{{ CompletionText = '--{}'; ListItemText = '--{}'; ToolTip = '{}' }},\n",
250 arg.long, arg.long, arg.help.replace('\"', "'")));
251 }
252
253 for cmd in &self.commands {
254 script.push_str(&format!(" @{{ CompletionText = '{}'; ListItemText = '{}'; ToolTip = '{}' }},\n",
255 cmd.name, cmd.name, cmd.about.replace('\"', "'")));
256 }
257
258 script.push_str(" )\n\n");
259 script.push_str(" $completions | Where-Object { $_.CompletionText -like \"$wordToComplete*\" } | \n");
260 script.push_str(" ForEach-Object { [System.Management.Automation.CompletionResult]::new($_.CompletionText, $_.ListItemText, 'ParameterValue', $_.ToolTip) }\n");
261 script.push_str("}\n");
262
263 script
264 }
265
266 pub fn parse(self) -> Matches {
267 let args: Vec<String> = env::args().skip(1).collect();
268 self.parse_args(&args)
269 }
270
271 fn parse_args(self, args: &[String]) -> Matches {
272 let mut matches = Matches {
273 command: None,
274 args: HashMap::new(),
275 values: Vec::new(),
276 };
277
278 if args.is_empty() {
279 return matches;
280 }
281
282 if args[0] == "--help" || args[0] == "-h" {
284 self.print_help();
285 std::process::exit(0);
286 }
287 if args[0] == "--version" || args[0] == "-V" {
288 println!("{} {}", self.name, self.version);
289 std::process::exit(0);
290 }
291
292 if let Some(cmd) = self.commands.iter().find(|c| c.name == args[0]) {
294 matches.command = Some(args[0].clone());
295 matches.parse_command_args(cmd, &args[1..]);
296 } else {
297 matches.parse_args_list(&self.global_args, args);
298 }
299
300 self.apply_defaults_and_validate(&mut matches);
302
303 matches
304 }
305
306 fn apply_defaults_and_validate(&self, matches: &mut Matches) {
307 for arg in &self.global_args {
308 if !matches.is_present(&arg.name) && arg.default_value.is_some() {
310 matches.args.insert(
311 arg.name.clone(),
312 arg.default_value.clone(),
313 );
314 }
315
316 if arg.required && !matches.is_present(&arg.name) {
318 let msg = if self.colored_help {
319 format!("Error: {} is required", colors::colorize(&format!("--{}", arg.long), colors::RED))
320 } else {
321 format!("Error: --{} is required", arg.long)
322 };
323 eprintln!("{}", msg);
324 std::process::exit(1);
325 }
326
327 if !arg.possible_values.is_empty() {
329 if let Some(value) = matches.value_of(&arg.name) {
330 if !arg.possible_values.iter().any(|v| v == value) {
331 let msg = if self.colored_help {
332 format!(
333 "Error: invalid value {} for {}",
334 colors::colorize(&format!("'{}'", value), colors::RED),
335 colors::colorize(&format!("--{}", arg.long), colors::CYAN)
336 )
337 } else {
338 format!("Error: invalid value '{}' for --{}", value, arg.long)
339 };
340 eprintln!("{}", msg);
341 eprintln!("Possible values: {}", arg.possible_values.join(", "));
342 std::process::exit(1);
343 }
344 }
345 }
346
347 if let Some(validator) = &arg.validator {
349 if let Some(value) = matches.value_of(&arg.name) {
350 if let Err(err) = validator(value) {
351 let msg = if self.colored_help {
352 format!(
353 "Error: validation failed for {}: {}",
354 colors::colorize(&format!("--{}", arg.long), colors::CYAN),
355 colors::colorize(&err, colors::RED)
356 )
357 } else {
358 format!("Error: validation failed for --{}: {}", arg.long, err)
359 };
360 eprintln!("{}", msg);
361 std::process::exit(1);
362 }
363 }
364 }
365 }
366
367 for group in &self.groups {
369 let present_args: Vec<String> = group.args.iter()
370 .filter(|arg_name| matches.is_present(arg_name))
371 .map(|s| s.clone())
372 .collect();
373
374 if group.required && present_args.is_empty() {
376 let msg = if self.colored_help {
377 format!(
378 "Error: at least one of {} is required",
379 colors::colorize(&format!("[{}]", group.args.join(", ")), colors::YELLOW)
380 )
381 } else {
382 format!("Error: at least one of [{}] is required", group.args.join(", "))
383 };
384 eprintln!("{}", msg);
385 std::process::exit(1);
386 }
387
388 if !group.multiple && present_args.len() > 1 {
390 let msg = if self.colored_help {
391 format!(
392 "Error: arguments {} are mutually exclusive",
393 colors::colorize(&present_args.join(", "), colors::RED)
394 )
395 } else {
396 format!("Error: arguments {} are mutually exclusive", present_args.join(", "))
397 };
398 eprintln!("{}", msg);
399 std::process::exit(1);
400 }
401 }
402 }
403
404 fn print_help(&self) {
405 let name = if self.colored_help {
406 colors::colorize(&self.name, colors::BOLD)
407 } else {
408 self.name.clone()
409 };
410 println!("{}", name);
411
412 if !self.about.is_empty() {
413 println!("{}\n", self.about);
414 }
415
416 let usage = if self.colored_help {
417 format!("{}: {} [OPTIONS] [COMMAND]",
418 colors::colorize("Usage", colors::BOLD),
419 self.name.to_lowercase()
420 )
421 } else {
422 format!("Usage: {} [OPTIONS] [COMMAND]", self.name.to_lowercase())
423 };
424 println!("{}\n", usage);
425
426 if !self.commands.is_empty() {
427 let header = if self.colored_help {
428 colors::colorize("Commands:", colors::BOLD)
429 } else {
430 "Commands:".to_string()
431 };
432 println!("{}", header);
433
434 for cmd in &self.commands {
435 let cmd_name = if self.colored_help {
436 colors::colorize(&cmd.name, colors::CYAN)
437 } else {
438 cmd.name.clone()
439 };
440 println!(" {:<12} {}", cmd_name, cmd.about);
441 }
442 println!();
443 }
444
445 let options_header = if self.colored_help {
446 colors::colorize("Options:", colors::BOLD)
447 } else {
448 "Options:".to_string()
449 };
450 println!("{}", options_header);
451
452 let help_text = if self.colored_help {
453 format!(" {}, {} Print help",
454 colors::colorize("-h", colors::GREEN),
455 colors::colorize("--help", colors::GREEN)
456 )
457 } else {
458 " -h, --help Print help".to_string()
459 };
460 println!("{}", help_text);
461
462 let version_text = if self.colored_help {
463 format!(" {}, {} Print version",
464 colors::colorize("-V", colors::GREEN),
465 colors::colorize("--version", colors::GREEN)
466 )
467 } else {
468 " -V, --version Print version".to_string()
469 };
470 println!("{}", version_text);
471
472 for arg in &self.global_args {
473 let short = arg.short.as_ref().map(|s| format!("-{}, ", s)).unwrap_or_default();
474 let long_with_color = if self.colored_help {
475 colors::colorize(&format!("--{}", arg.long), colors::GREEN)
476 } else {
477 format!("--{}", arg.long)
478 };
479
480 let required_marker = if arg.required && self.colored_help {
481 format!(" {}", colors::colorize("[required]", colors::RED))
482 } else if arg.required {
483 " [required]".to_string()
484 } else {
485 String::new()
486 };
487
488 println!(" {}{:<12} {}{}", short, long_with_color, arg.help, required_marker);
489 }
490 }
491}
492
493pub struct Command {
498 name: String,
499 about: String,
500 args: Vec<Arg>,
501}
502
503impl Command {
504 pub fn new(name: impl Into<String>) -> Self {
505 Self {
506 name: name.into(),
507 about: String::new(),
508 args: Vec::new(),
509 }
510 }
511
512 pub fn about(mut self, about: impl Into<String>) -> Self {
513 self.about = about.into();
514 self
515 }
516
517 pub fn arg(mut self, arg: Arg) -> Self {
518 self.args.push(arg);
519 self
520 }
521}
522
523pub struct Arg {
528 name: String,
529 long: String,
530 short: Option<String>,
531 help: String,
532 takes_value: bool,
533 required: bool,
534 default_value: Option<String>,
535 possible_values: Vec<String>,
536 validator: Option<Validator>,
537}
538
539impl Arg {
540 pub fn new(name: impl Into<String>) -> Self {
541 let name = name.into();
542 Self {
543 long: name.clone(),
544 name,
545 short: None,
546 help: String::new(),
547 takes_value: false,
548 required: false,
549 default_value: None,
550 possible_values: Vec::new(),
551 validator: None,
552 }
553 }
554
555 pub fn long(mut self, long: impl Into<String>) -> Self {
556 self.long = long.into();
557 self
558 }
559
560 pub fn short(mut self, short: char) -> Self {
561 self.short = Some(short.to_string());
562 self
563 }
564
565 pub fn help(mut self, help: impl Into<String>) -> Self {
566 self.help = help.into();
567 self
568 }
569
570 pub fn takes_value(mut self, takes: bool) -> Self {
571 self.takes_value = takes;
572 self
573 }
574
575 pub fn required(mut self, req: bool) -> Self {
576 self.required = req;
577 self
578 }
579
580 pub fn default_value(mut self, value: impl Into<String>) -> Self {
590 self.default_value = Some(value.into());
591 self
592 }
593
594 pub fn possible_values(mut self, values: &[&str]) -> Self {
604 self.possible_values = values.iter().map(|s| s.to_string()).collect();
605 self
606 }
607
608 pub fn validator(mut self, f: Validator) -> Self {
622 self.validator = Some(f);
623 self
624 }
625}
626
627pub struct Matches {
632 command: Option<String>,
633 args: HashMap<String, Option<String>>,
634 values: Vec<String>,
635}
636
637impl Matches {
638 pub fn subcommand(&self) -> Option<&str> {
639 self.command.as_deref()
640 }
641
642 pub fn is_present(&self, name: &str) -> bool {
643 self.args.contains_key(name)
644 }
645
646 pub fn value_of(&self, name: &str) -> Option<&str> {
647 self.args.get(name)?.as_deref()
648 }
649
650 pub fn values(&self) -> &[String] {
651 &self.values
652 }
653
654 pub fn value_as<T>(&self, name: &str) -> Option<T>
664 where
665 T: std::str::FromStr,
666 {
667 self.value_of(name)?.parse().ok()
668 }
669
670 pub fn any_present(&self, names: &[&str]) -> bool {
680 names.iter().any(|name| self.is_present(name))
681 }
682
683 pub fn all_present(&self, names: &[&str]) -> bool {
685 names.iter().all(|name| self.is_present(name))
686 }
687
688 pub fn value_or<'a>(&'a self, name: &str, default: &'a str) -> &'a str {
690 self.value_of(name).unwrap_or(default)
691 }
692
693 pub fn values_count(&self) -> usize {
695 self.values.len()
696 }
697
698 fn parse_command_args(&mut self, cmd: &Command, args: &[String]) {
699 self.parse_args_list(&cmd.args, args);
700 }
701
702 fn parse_args_list(&mut self, arg_defs: &[Arg], args: &[String]) {
703 let mut i = 0;
704 while i < args.len() {
705 let arg = &args[i];
706
707 if arg.starts_with("--") {
708 let key = &arg[2..];
709 if let Some(arg_def) = arg_defs.iter().find(|a| a.long == key) {
710 if arg_def.takes_value && i + 1 < args.len() {
711 self.args.insert(arg_def.name.clone(), Some(args[i + 1].clone()));
712 i += 2;
713 } else {
714 self.args.insert(arg_def.name.clone(), None);
715 i += 1;
716 }
717 } else {
718 i += 1;
719 }
720 } else if arg.starts_with('-') && arg.len() == 2 {
721 let short = &arg[1..];
722 if let Some(arg_def) = arg_defs.iter().find(|a| a.short.as_deref() == Some(short)) {
723 if arg_def.takes_value && i + 1 < args.len() {
724 self.args.insert(arg_def.name.clone(), Some(args[i + 1].clone()));
725 i += 2;
726 } else {
727 self.args.insert(arg_def.name.clone(), None);
728 i += 1;
729 }
730 } else {
731 i += 1;
732 }
733 } else {
734 self.values.push(arg.clone());
735 i += 1;
736 }
737 }
738 }
739}
740
741#[cfg(test)]
742mod tests {
743 use super::*;
744
745 #[test]
746 fn test_arg_creation() {
747 let arg = Arg::new("test")
748 .long("test")
749 .short('t')
750 .help("Test argument")
751 .takes_value(true);
752
753 assert_eq!(arg.name, "test");
754 assert_eq!(arg.long, "test");
755 assert_eq!(arg.short, Some("t".to_string()));
756 }
757
758 #[test]
759 fn test_command_creation() {
760 let cmd = Command::new("test")
761 .about("Test command")
762 .arg(Arg::new("arg1"));
763
764 assert_eq!(cmd.name, "test");
765 assert_eq!(cmd.args.len(), 1);
766 }
767
768 #[test]
769 fn test_value_as_parsing() {
770 let mut matches = Matches {
771 command: None,
772 args: HashMap::new(),
773 values: Vec::new(),
774 };
775 matches.args.insert("port".to_string(), Some("8080".to_string()));
776
777 let port: u16 = matches.value_as("port").unwrap();
778 assert_eq!(port, 8080);
779 }
780
781 #[test]
782 fn test_any_present() {
783 let mut matches = Matches {
784 command: None,
785 args: HashMap::new(),
786 values: Vec::new(),
787 };
788 matches.args.insert("verbose".to_string(), None);
789
790 assert!(matches.any_present(&["verbose", "debug"]));
791 assert!(!matches.any_present(&["quiet", "silent"]));
792 }
793
794 #[test]
795 fn test_all_present() {
796 let mut matches = Matches {
797 command: None,
798 args: HashMap::new(),
799 values: Vec::new(),
800 };
801 matches.args.insert("verbose".to_string(), None);
802 matches.args.insert("debug".to_string(), None);
803
804 assert!(matches.all_present(&["verbose", "debug"]));
805 assert!(!matches.all_present(&["verbose", "debug", "trace"]));
806 }
807
808 #[test]
809 fn test_value_or_default() {
810 let matches = Matches {
811 command: None,
812 args: HashMap::new(),
813 values: Vec::new(),
814 };
815
816 assert_eq!(matches.value_or("port", "8080"), "8080");
817 }
818
819 #[test]
820 fn test_values_count() {
821 let matches = Matches {
822 command: None,
823 args: HashMap::new(),
824 values: vec!["file1".to_string(), "file2".to_string()],
825 };
826
827 assert_eq!(matches.values_count(), 2);
828 }
829}