1use std::any::Any;
44use std::collections::HashMap;
45use std::sync::Arc;
46
47use crate::argument::Argument;
48use crate::command::{Command, CommandBuilder, CommandCallback};
49use crate::context::{get_current_context, pop_context, push_context, Context, ContextBuilder};
50use crate::error::ClickError;
51use crate::option::ClickOption;
52use crate::parameter::Parameter;
53
54pub trait CommandLike: Send + Sync {
63 fn name(&self) -> Option<&str>;
65
66 fn make_context(
74 &self,
75 info_name: &str,
76 args: Vec<String>,
77 parent: Option<Arc<Context>>,
78 ) -> Result<Context, ClickError>;
79
80 fn invoke(&self, ctx: &Context) -> Result<(), ClickError>;
82
83 fn main(&self, args: Vec<String>) -> Result<(), ClickError>;
85
86 fn get_help(&self, ctx: &Context) -> String;
88
89 fn get_short_help(&self) -> String;
91
92 fn is_hidden(&self) -> bool;
94
95 fn get_usage(&self, ctx: &Context) -> String;
97
98 fn as_any(&self) -> &dyn Any;
100}
101
102impl CommandLike for Command {
107 fn name(&self) -> Option<&str> {
108 self.name.as_deref()
109 }
110
111 fn make_context(
112 &self,
113 info_name: &str,
114 args: Vec<String>,
115 parent: Option<Arc<Context>>,
116 ) -> Result<Context, ClickError> {
117 Command::make_context(self, info_name, args, parent)
118 }
119
120 fn invoke(&self, ctx: &Context) -> Result<(), ClickError> {
121 Command::invoke(self, ctx)
122 }
123
124 fn main(&self, args: Vec<String>) -> Result<(), ClickError> {
125 Command::main(self, args)
126 }
127
128 fn get_help(&self, ctx: &Context) -> String {
129 Command::get_help(self, ctx)
130 }
131
132 fn get_short_help(&self) -> String {
133 Command::get_short_help(self)
134 }
135
136 fn is_hidden(&self) -> bool {
137 self.hidden
138 }
139
140 fn get_usage(&self, ctx: &Context) -> String {
141 Command::get_usage(self, ctx)
142 }
143
144 fn as_any(&self) -> &dyn Any {
145 self
146 }
147}
148
149pub type ResultCallback =
159 Box<dyn Fn(&Context, Vec<Box<dyn Any + Send + Sync>>) -> Result<(), ClickError> + Send + Sync>;
160
161pub struct Group {
198 pub command: Command,
200
201 pub commands: HashMap<String, Arc<dyn CommandLike>>,
203
204 command_ids_by_name: HashMap<String, usize>,
212 command_aliases_by_id: HashMap<usize, Vec<String>>,
213 next_command_id: usize,
214
215 pub chain: bool,
217
218 pub invoke_without_command: bool,
220
221 pub result_callback: Option<ResultCallback>,
223
224 pub subcommand_required: bool,
228
229 pub subcommand_metavar: String,
231}
232
233impl std::fmt::Debug for Group {
234 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
235 f.debug_struct("Group")
236 .field("command", &self.command)
237 .field(
238 "commands",
239 &format!("<{} subcommands>", self.commands.len()),
240 )
241 .field("chain", &self.chain)
242 .field("invoke_without_command", &self.invoke_without_command)
243 .field("subcommand_required", &self.subcommand_required)
244 .field("subcommand_metavar", &self.subcommand_metavar)
245 .finish()
246 }
247}
248
249impl Default for Group {
250 fn default() -> Self {
251 Self {
252 command: Command::default(),
253 commands: HashMap::new(),
254 command_ids_by_name: HashMap::new(),
255 command_aliases_by_id: HashMap::new(),
256 next_command_id: 0,
257 chain: false,
258 invoke_without_command: false,
259 result_callback: None,
260 subcommand_required: true,
261 subcommand_metavar: "COMMAND [ARGS]...".to_string(),
262 }
263 }
264}
265
266impl Group {
267 #[allow(clippy::new_ret_no_self)]
279 pub fn new(name: &str) -> GroupBuilder {
280 GroupBuilder::new(name)
281 }
282
283 pub fn add_command(&mut self, cmd: impl CommandLike + 'static, name: Option<&str>) {
302 let cmd_name = name
303 .map(|s| s.to_string())
304 .or_else(|| cmd.name().map(|s| s.to_string()));
305
306 if let Some(n) = cmd_name {
307 self.add_command_shared(Arc::new(cmd), Some(&n));
308 }
309 }
310
311 pub fn add_command_shared(&mut self, cmd: Arc<dyn CommandLike>, name: Option<&str>) {
316 let cmd_name = name
317 .map(|s| s.to_string())
318 .or_else(|| cmd.name().map(|s| s.to_string()));
319
320 let Some(name) = cmd_name else { return };
321
322 if let Some(old_id) = self.command_ids_by_name.get(&name).copied() {
324 if let Some(names) = self.command_aliases_by_id.get_mut(&old_id) {
325 names.retain(|n| n != &name);
326 }
327 }
328
329 let existing_id = self.commands.iter().find_map(|(n, existing)| {
331 if Arc::ptr_eq(existing, &cmd) {
332 self.command_ids_by_name.get(n).copied()
333 } else {
334 None
335 }
336 });
337
338 let id = existing_id.unwrap_or_else(|| {
339 let id = self.next_command_id;
340 self.next_command_id += 1;
341 id
342 });
343
344 self.command_ids_by_name.insert(name.clone(), id);
345 self.command_aliases_by_id
346 .entry(id)
347 .or_insert_with(Vec::new)
348 .push(name.clone());
349
350 if let Some(names) = self.command_aliases_by_id.get_mut(&id) {
352 names.sort();
353 names.dedup();
354 }
355
356 self.commands.insert(name, cmd);
357 }
358
359 pub fn get_command(&self, name: &str) -> Option<&dyn CommandLike> {
375 self.commands.get(name).map(|c| c.as_ref())
376 }
377
378 pub fn list_command_entries(&self) -> Vec<(String, &dyn CommandLike)> {
383 let mut names: Vec<&String> = self.commands.keys().collect();
384 names.sort();
385 names
386 .into_iter()
387 .filter_map(|name| {
388 self.commands
389 .get(name)
390 .map(|cmd| (name.clone(), cmd.as_ref()))
391 })
392 .collect()
393 }
394
395 pub fn list_command_aliases(&self, name: &str) -> Vec<String> {
399 let Some(id) = self.command_ids_by_name.get(name).copied() else {
400 return Vec::new();
401 };
402 let Some(names) = self.command_aliases_by_id.get(&id) else {
403 return Vec::new();
404 };
405
406 let mut out: Vec<String> = names
407 .iter()
408 .filter(|n| n.as_str() != name)
409 .cloned()
410 .collect();
411 out.sort();
412 out.dedup();
413 out
414 }
415
416 pub fn list_commands(&self) -> Vec<&str> {
434 let mut names: Vec<&str> = self.commands.keys().map(|s| s.as_str()).collect();
435 names.sort();
436 names
437 }
438
439 pub fn resolve_command<'a>(
449 &'a self,
450 ctx: &Context,
451 args: &[String],
452 ) -> Result<Option<(&'a str, &'a dyn CommandLike, Vec<String>)>, ClickError> {
453 if args.is_empty() {
454 return Ok(None);
455 }
456
457 let cmd_name = &args[0];
458 let remaining = args[1..].to_vec();
459
460 if let Some(cmd) = self.commands.get(cmd_name) {
462 for (key, _) in &self.commands {
464 if key == cmd_name {
465 return Ok(Some((key.as_str(), cmd.as_ref(), remaining)));
466 }
467 }
468 }
469
470 if ctx.resilient_parsing() {
472 return Ok(None);
473 }
474
475 if cmd_name.starts_with('-') {
477 return Ok(None);
479 }
480
481 Err(ClickError::usage(format!(
482 "No such command '{}'.",
483 cmd_name
484 )))
485 }
486
487 pub fn format_commands(&self, _ctx: &Context) -> String {
491 let mut lines = Vec::new();
492
493 let visible_cmds: Vec<(&str, &dyn CommandLike)> = self
495 .list_commands()
496 .into_iter()
497 .filter_map(|name| {
498 self.get_command(name)
499 .filter(|cmd| !cmd.is_hidden())
500 .map(|cmd| (name, cmd))
501 })
502 .collect();
503
504 if visible_cmds.is_empty() {
505 return String::new();
506 }
507
508 let max_width = visible_cmds
510 .iter()
511 .map(|(name, _)| name.len())
512 .max()
513 .unwrap_or(0);
514
515 lines.push("Commands:".to_string());
516
517 for (name, cmd) in visible_cmds {
518 let help = cmd.get_short_help();
519 let padding = max_width - name.len() + 2;
520 lines.push(format!(
521 " {}{:padding$}{}",
522 name,
523 "",
524 help,
525 padding = padding
526 ));
527 }
528
529 lines.join("\n")
530 }
531
532 fn get_usage_with_subcommand(&self, ctx: &Context) -> String {
534 let base_usage = self.command.get_usage(ctx);
535 format!("{} {}", base_usage, self.subcommand_metavar)
536 }
537
538 fn get_help_with_commands(&self, ctx: &Context) -> String {
540 let mut parts = Vec::new();
541
542 parts.push(self.get_usage_with_subcommand(ctx));
544
545 if let Some(ref help) = self.command.help {
547 let text = help.lines().next().unwrap_or("");
548 if !text.is_empty() {
549 parts.push(String::new());
550 let help_text = if let Some(ref dep) = self.command.deprecated {
551 if dep.is_empty() {
552 format!("{} (DEPRECATED)", text)
553 } else {
554 format!("{} (DEPRECATED: {})", text, dep)
555 }
556 } else {
557 text.to_string()
558 };
559 parts.push(format!(" {}", help_text));
560 }
561 }
562
563 let opt_records: Vec<(String, String)> = self
565 .command
566 .options
567 .iter()
568 .filter_map(|opt| opt.get_help_record())
569 .collect();
570
571 let help_opt = self.command.get_help_option(ctx);
572 let help_record = help_opt.as_ref().and_then(|h| h.get_help_record());
573
574 if !opt_records.is_empty() || help_record.is_some() {
575 parts.push(String::new());
576 parts.push("Options:".to_string());
577
578 for (opt_str, help) in &opt_records {
579 parts.push(format!(" {} {}", opt_str, help));
580 }
581 if let Some((opt_str, help)) = help_record {
582 parts.push(format!(" {} {}", opt_str, help));
583 }
584 }
585
586 let commands_section = self.format_commands(ctx);
588 if !commands_section.is_empty() {
589 parts.push(String::new());
590 parts.push(commands_section);
591 }
592
593 if let Some(ref epilog) = self.command.epilog {
595 parts.push(String::new());
596 parts.push(epilog.clone());
597 }
598
599 parts.join("\n")
600 }
601}
602
603impl CommandLike for Group {
608 fn name(&self) -> Option<&str> {
609 self.command.name.as_deref()
610 }
611
612 fn make_context(
613 &self,
614 info_name: &str,
615 args: Vec<String>,
616 parent: Option<Arc<Context>>,
617 ) -> Result<Context, ClickError> {
618 let mut builder = ContextBuilder::new()
620 .info_name(info_name)
621 .allow_extra_args(true)
622 .allow_interspersed_args(false);
623
624 if let Some(parent) = parent {
625 builder = builder.parent(parent);
626 }
627
628 let mut ctx = builder.build();
629
630 self.command.parse_args(&mut ctx, args)?;
632
633 Ok(ctx)
634 }
635
636 fn invoke(&self, ctx: &Context) -> Result<(), ClickError> {
637 let args = ctx.args().to_vec();
639
640 let process_result = |result_callback: &Option<ResultCallback>,
642 ctx: &Context,
643 results: Vec<Box<dyn Any + Send + Sync>>|
644 -> Result<(), ClickError> {
645 if let Some(ref callback) = result_callback {
646 callback(ctx, results)?;
647 }
648 Ok(())
649 };
650
651 let parent_arc = get_current_context();
653
654 let resolved = self.resolve_command(ctx, &args)?;
656
657 if resolved.is_none() {
658 if self.invoke_without_command {
660 let group_result = self.command.invoke(ctx);
662 if group_result.is_ok() {
663 let results: Vec<Box<dyn Any + Send + Sync>> = if self.chain {
666 Vec::new()
667 } else {
668 Vec::new()
671 };
672 process_result(&self.result_callback, ctx, results)?;
673 }
674 return group_result;
675 } else if self.subcommand_required && !ctx.resilient_parsing() {
676 return Err(ClickError::usage("Missing command."));
677 } else {
678 return Ok(());
679 }
680 }
681
682 if !self.chain {
684 let (cmd_name, cmd, remaining) = resolved.unwrap();
686
687 if self.command.callback.is_some() {
693 self.command.invoke(ctx)?;
694 }
695
696 let sub_ctx = match cmd.make_context(cmd_name, remaining, parent_arc) {
702 Ok(sub_ctx) => sub_ctx,
703 Err(ClickError::Exit { code: 0 }) => {
704 let help_ctx = ContextBuilder::new()
705 .info_name(format!("{} {}", ctx.command_path(), cmd_name))
706 .build();
707 let help_text = if let Some(renderer) = ctx.help_renderer() {
708 renderer(cmd, &help_ctx)
709 } else {
710 cmd.get_help(&help_ctx)
711 };
712 println!("{}", help_text);
713 return Ok(());
714 }
715 Err(e) => return Err(e),
716 };
717
718 let sub_ctx_arc = Arc::new(sub_ctx);
720 push_context(Arc::clone(&sub_ctx_arc));
721 let result = cmd.invoke(&sub_ctx_arc);
722 pop_context();
723
724 sub_ctx_arc.close();
726
727 if result.is_ok() {
729 process_result(&self.result_callback, ctx, Vec::new())?;
731 }
732
733 result
734 } else {
735 if self.command.callback.is_some() {
740 self.command.invoke(ctx)?;
741 }
742
743 let mut contexts: Vec<(Arc<Context>, &dyn CommandLike)> = Vec::new();
745 let mut remaining_args = args;
746
747 while !remaining_args.is_empty() {
748 let resolved = self.resolve_command(ctx, &remaining_args)?;
749 match resolved {
750 Some((cmd_name, cmd, rest)) => {
751 let mut sub_ctx = ContextBuilder::new()
755 .info_name(cmd_name)
756 .allow_extra_args(true) .allow_interspersed_args(false) .parent(
759 parent_arc
760 .clone()
761 .unwrap_or_else(|| Arc::new(Context::default())),
762 )
763 .build();
764
765 let parse_result =
769 if let Some(command) = cmd.as_any().downcast_ref::<Command>() {
770 command.parse_args(&mut sub_ctx, rest)
771 } else if let Some(group) = cmd.as_any().downcast_ref::<Group>() {
772 group.make_context(cmd_name, rest, parent_arc.clone()).map(
774 |nested_ctx| {
775 sub_ctx = nested_ctx;
776 },
777 )
778 } else {
779 cmd.make_context(cmd_name, rest, parent_arc.clone()).map(
781 |fallback_ctx| {
782 sub_ctx = fallback_ctx;
783 },
784 )
785 };
786 match parse_result {
787 Ok(()) => {}
788 Err(ClickError::Exit { code: 0 }) => {
789 let help_ctx = ContextBuilder::new()
790 .info_name(format!("{} {}", ctx.command_path(), cmd_name))
791 .build();
792 let help_text = if let Some(renderer) = ctx.help_renderer() {
793 renderer(cmd, &help_ctx)
794 } else {
795 cmd.get_help(&help_ctx)
796 };
797 println!("{}", help_text);
798 return Ok(());
799 }
800 Err(e) => return Err(e),
801 }
802
803 remaining_args = sub_ctx.args().to_vec();
805
806 contexts.push((Arc::new(sub_ctx), cmd));
807 }
808 None => {
809 if !remaining_args.is_empty()
814 && remaining_args[0].starts_with('-')
815 && !ctx.resilient_parsing()
816 {
817 return Err(ClickError::usage(format!(
818 "No such option: {}",
819 remaining_args[0]
820 )));
821 }
822 break;
823 }
824 }
825 }
826
827 let mut results: Vec<Box<dyn Any + Send + Sync>> = Vec::new();
829 for (sub_ctx_arc, cmd) in contexts {
830 push_context(Arc::clone(&sub_ctx_arc));
831 let result = cmd.invoke(&sub_ctx_arc);
832 pop_context();
833 sub_ctx_arc.close();
834
835 result?;
837
838 results.push(Box::new(()));
841 }
842
843 process_result(&self.result_callback, ctx, results)?;
845
846 Ok(())
847 }
848 }
849
850 #[allow(clippy::arc_with_non_send_sync)]
851 fn main(&self, args: Vec<String>) -> Result<(), ClickError> {
852 let prog_name = self.command.name.clone().unwrap_or_else(|| {
853 std::env::args()
854 .next()
855 .unwrap_or_else(|| "program".to_string())
856 });
857
858 let args_for_eager = args.clone();
859
860 let ctx_result = self.make_context(&prog_name, args, None);
862
863 match ctx_result {
864 Ok(ctx) => {
865 let ctx = Arc::new(ctx);
866
867 push_context(Arc::clone(&ctx));
869
870 let result = self.invoke(&ctx);
872
873 pop_context();
875
876 ctx.close();
878
879 result
880 }
881 Err(ClickError::Exit { code: 0 }) => {
882 if let Some(version_output) =
887 self.command.get_version_output_from_args(&args_for_eager)
888 {
889 println!("{}", version_output);
890 return Ok(());
891 }
892
893 let ctx = ContextBuilder::new().info_name(&prog_name).build();
896 println!("{}", self.get_help(&ctx));
897 Ok(())
898 }
899 Err(e) => Err(e),
900 }
901 }
902
903 fn get_help(&self, ctx: &Context) -> String {
904 self.get_help_with_commands(ctx)
905 }
906
907 fn get_short_help(&self) -> String {
908 self.command.get_short_help()
909 }
910
911 fn is_hidden(&self) -> bool {
912 self.command.hidden
913 }
914
915 fn get_usage(&self, ctx: &Context) -> String {
916 self.get_usage_with_subcommand(ctx)
917 }
918
919 fn as_any(&self) -> &dyn Any {
920 self
921 }
922}
923
924#[derive(Debug)]
936pub struct CommandCollection {
937 pub base: Group,
939
940 pub sources: Vec<Group>,
942}
943
944impl CommandCollection {
945 #[allow(clippy::new_ret_no_self)]
949 pub fn new(name: &str) -> CommandCollectionBuilder {
950 CommandCollectionBuilder::new(name)
951 }
952
953 pub fn add_source(&mut self, group: Group) {
955 self.sources.push(group);
956 }
957
958 pub fn get_command(&self, name: &str) -> Option<&dyn CommandLike> {
960 if let Some(cmd) = self.base.get_command(name) {
961 return Some(cmd);
962 }
963 for src in &self.sources {
964 if let Some(cmd) = src.get_command(name) {
965 return Some(cmd);
966 }
967 }
968 None
969 }
970
971 pub fn list_commands(&self) -> Vec<String> {
973 let mut names: std::collections::HashSet<String> =
974 self.base.commands.keys().cloned().collect();
975
976 for src in &self.sources {
977 for name in src.commands.keys() {
978 names.insert(name.clone());
979 }
980 }
981
982 let mut out: Vec<String> = names.into_iter().collect();
983 out.sort();
984 out
985 }
986
987 fn resolve_command<'a>(
988 &'a self,
989 ctx: &Context,
990 args: &[String],
991 ) -> Result<Option<(String, &'a dyn CommandLike, Vec<String>)>, ClickError> {
992 if args.is_empty() {
993 return Ok(None);
994 }
995
996 let cmd_name = &args[0];
997 let remaining = args[1..].to_vec();
998
999 if let Some(cmd) = self.base.commands.get(cmd_name) {
1000 return Ok(Some((cmd_name.clone(), cmd.as_ref(), remaining)));
1001 }
1002 for src in &self.sources {
1003 if let Some(cmd) = src.commands.get(cmd_name) {
1004 return Ok(Some((cmd_name.clone(), cmd.as_ref(), remaining)));
1005 }
1006 }
1007
1008 if ctx.resilient_parsing() {
1009 return Ok(None);
1010 }
1011 if cmd_name.starts_with('-') {
1012 return Ok(None);
1013 }
1014
1015 Err(ClickError::usage(format!(
1016 "No such command '{}'.",
1017 cmd_name
1018 )))
1019 }
1020
1021 fn format_commands(&self, _ctx: &Context) -> String {
1022 let mut visible_cmds: Vec<(String, &dyn CommandLike)> = self
1023 .list_commands()
1024 .into_iter()
1025 .filter_map(|name| {
1026 self.get_command(&name)
1027 .filter(|cmd| !cmd.is_hidden())
1028 .map(|cmd| (name, cmd))
1029 })
1030 .collect();
1031
1032 if visible_cmds.is_empty() {
1033 return String::new();
1034 }
1035
1036 visible_cmds.sort_by(|a, b| a.0.cmp(&b.0));
1037
1038 let max_width = visible_cmds
1039 .iter()
1040 .map(|(name, _)| name.len())
1041 .max()
1042 .unwrap_or(0);
1043
1044 let mut lines = Vec::new();
1045 lines.push("Commands:".to_string());
1046
1047 for (name, cmd) in visible_cmds {
1048 let help = cmd.get_short_help();
1049 let padding = max_width - name.len() + 2;
1050 lines.push(format!(
1051 " {}{:padding$}{}",
1052 name,
1053 "",
1054 help,
1055 padding = padding
1056 ));
1057 }
1058
1059 lines.join("\n")
1060 }
1061
1062 fn get_usage_with_subcommand(&self, ctx: &Context) -> String {
1063 let base_usage = self.base.command.get_usage(ctx);
1064 format!("{} {}", base_usage, self.base.subcommand_metavar)
1065 }
1066
1067 fn get_help_with_commands(&self, ctx: &Context) -> String {
1068 let mut parts = Vec::new();
1069
1070 parts.push(self.get_usage_with_subcommand(ctx));
1071
1072 if let Some(ref help) = self.base.command.help {
1073 let text = help.lines().next().unwrap_or("");
1074 if !text.is_empty() {
1075 parts.push(String::new());
1076 let help_text = if let Some(ref dep) = self.base.command.deprecated {
1077 if dep.is_empty() {
1078 format!("{} (DEPRECATED)", text)
1079 } else {
1080 format!("{} (DEPRECATED: {})", text, dep)
1081 }
1082 } else {
1083 text.to_string()
1084 };
1085 parts.push(format!(" {}", help_text));
1086 }
1087 }
1088
1089 let opt_records: Vec<(String, String)> = self
1090 .base
1091 .command
1092 .options
1093 .iter()
1094 .filter_map(|opt| opt.get_help_record())
1095 .collect();
1096
1097 let help_opt = self.base.command.get_help_option(ctx);
1098 let help_record = help_opt.as_ref().and_then(|h| h.get_help_record());
1099
1100 if !opt_records.is_empty() || help_record.is_some() {
1101 parts.push(String::new());
1102 parts.push("Options:".to_string());
1103
1104 for (opt_str, help) in &opt_records {
1105 parts.push(format!(" {} {}", opt_str, help));
1106 }
1107 if let Some((opt_str, help)) = help_record {
1108 parts.push(format!(" {} {}", opt_str, help));
1109 }
1110 }
1111
1112 let commands_section = self.format_commands(ctx);
1113 if !commands_section.is_empty() {
1114 parts.push(String::new());
1115 parts.push(commands_section);
1116 }
1117
1118 if let Some(ref epilog) = self.base.command.epilog {
1119 parts.push(String::new());
1120 parts.push(epilog.clone());
1121 }
1122
1123 parts.join("\n")
1124 }
1125}
1126
1127impl CommandLike for CommandCollection {
1128 fn name(&self) -> Option<&str> {
1129 self.base.command.name.as_deref()
1130 }
1131
1132 fn make_context(
1133 &self,
1134 info_name: &str,
1135 args: Vec<String>,
1136 parent: Option<Arc<Context>>,
1137 ) -> Result<Context, ClickError> {
1138 let mut builder = ContextBuilder::new()
1139 .info_name(info_name)
1140 .allow_extra_args(true)
1141 .allow_interspersed_args(false);
1142
1143 if let Some(parent) = parent {
1144 builder = builder.parent(parent);
1145 }
1146
1147 let mut ctx = builder.build();
1148 self.base.command.parse_args(&mut ctx, args)?;
1149 Ok(ctx)
1150 }
1151
1152 fn invoke(&self, ctx: &Context) -> Result<(), ClickError> {
1153 let args = ctx.args().to_vec();
1154
1155 let process_result = |result_callback: &Option<ResultCallback>,
1156 ctx: &Context,
1157 results: Vec<Box<dyn Any + Send + Sync>>|
1158 -> Result<(), ClickError> {
1159 if let Some(ref callback) = result_callback {
1160 callback(ctx, results)?;
1161 }
1162 Ok(())
1163 };
1164
1165 let parent_arc = get_current_context();
1166 let resolved = self.resolve_command(ctx, &args)?;
1167
1168 if resolved.is_none() {
1169 if self.base.invoke_without_command {
1170 let group_result = self.base.command.invoke(ctx);
1171 if group_result.is_ok() {
1172 process_result(&self.base.result_callback, ctx, Vec::new())?;
1173 }
1174 return group_result;
1175 } else if self.base.subcommand_required && !ctx.resilient_parsing() {
1176 return Err(ClickError::usage("Missing command."));
1177 } else {
1178 return Ok(());
1179 }
1180 }
1181
1182 if !self.base.chain {
1183 let (cmd_name, cmd, remaining) = resolved.unwrap();
1184
1185 if self.base.command.callback.is_some() {
1186 self.base.command.invoke(ctx)?;
1187 }
1188
1189 let sub_ctx = cmd.make_context(&cmd_name, remaining, parent_arc)?;
1190
1191 let sub_ctx_arc = Arc::new(sub_ctx);
1192 push_context(Arc::clone(&sub_ctx_arc));
1193 let result = cmd.invoke(&sub_ctx_arc);
1194 pop_context();
1195 sub_ctx_arc.close();
1196
1197 if result.is_ok() {
1198 process_result(&self.base.result_callback, ctx, Vec::new())?;
1199 }
1200
1201 result
1202 } else {
1203 if self.base.command.callback.is_some() {
1204 self.base.command.invoke(ctx)?;
1205 }
1206
1207 let mut contexts: Vec<(Arc<Context>, &dyn CommandLike)> = Vec::new();
1208 let mut remaining_args = args;
1209
1210 while !remaining_args.is_empty() {
1211 let resolved = self.resolve_command(ctx, &remaining_args)?;
1212 match resolved {
1213 Some((cmd_name, cmd, rest)) => {
1214 let mut sub_ctx = ContextBuilder::new()
1215 .info_name(&cmd_name)
1216 .allow_extra_args(true)
1217 .allow_interspersed_args(false)
1218 .parent(
1219 parent_arc
1220 .clone()
1221 .unwrap_or_else(|| Arc::new(Context::default())),
1222 )
1223 .build();
1224
1225 if let Some(command) = cmd.as_any().downcast_ref::<Command>() {
1226 command.parse_args(&mut sub_ctx, rest)?;
1227 } else if let Some(group) = cmd.as_any().downcast_ref::<Group>() {
1228 sub_ctx = group.make_context(&cmd_name, rest, parent_arc.clone())?;
1229 } else if let Some(collection) =
1230 cmd.as_any().downcast_ref::<CommandCollection>()
1231 {
1232 sub_ctx =
1233 collection.make_context(&cmd_name, rest, parent_arc.clone())?;
1234 } else {
1235 sub_ctx = cmd.make_context(&cmd_name, rest, parent_arc.clone())?;
1236 }
1237
1238 remaining_args = sub_ctx.args().to_vec();
1239 contexts.push((Arc::new(sub_ctx), cmd));
1240 }
1241 None => {
1242 if !remaining_args.is_empty()
1243 && remaining_args[0].starts_with('-')
1244 && !ctx.resilient_parsing()
1245 {
1246 return Err(ClickError::usage(format!(
1247 "No such option: {}",
1248 remaining_args[0]
1249 )));
1250 }
1251 break;
1252 }
1253 }
1254 }
1255
1256 let mut results: Vec<Box<dyn Any + Send + Sync>> = Vec::new();
1257 for (sub_ctx_arc, cmd) in contexts {
1258 push_context(Arc::clone(&sub_ctx_arc));
1259 let result = cmd.invoke(&sub_ctx_arc);
1260 pop_context();
1261 sub_ctx_arc.close();
1262 result?;
1263 results.push(Box::new(()));
1264 }
1265
1266 process_result(&self.base.result_callback, ctx, results)?;
1267 Ok(())
1268 }
1269 }
1270
1271 #[allow(clippy::arc_with_non_send_sync)]
1272 fn main(&self, args: Vec<String>) -> Result<(), ClickError> {
1273 let prog_name = self.base.command.name.clone().unwrap_or_else(|| {
1274 std::env::args()
1275 .next()
1276 .unwrap_or_else(|| "program".to_string())
1277 });
1278
1279 let args_for_eager = args.clone();
1280
1281 let ctx_result = self.make_context(&prog_name, args, None);
1283
1284 match ctx_result {
1285 Ok(ctx) => {
1286 let ctx = Arc::new(ctx);
1287
1288 push_context(Arc::clone(&ctx));
1289 let result = self.invoke(&ctx);
1290 pop_context();
1291 ctx.close();
1292 result
1293 }
1294 Err(ClickError::Exit { code: 0 }) => {
1295 if let Some(version_output) = self
1297 .base
1298 .command
1299 .get_version_output_from_args(&args_for_eager)
1300 {
1301 println!("{}", version_output);
1302 return Ok(());
1303 }
1304
1305 let ctx = ContextBuilder::new().info_name(&prog_name).build();
1307 println!("{}", self.get_help(&ctx));
1308 Ok(())
1309 }
1310 Err(e) => Err(e),
1311 }
1312 }
1313
1314 fn get_help(&self, ctx: &Context) -> String {
1315 self.get_help_with_commands(ctx)
1316 }
1317
1318 fn get_short_help(&self) -> String {
1319 self.base.command.get_short_help()
1320 }
1321
1322 fn is_hidden(&self) -> bool {
1323 self.base.command.hidden
1324 }
1325
1326 fn get_usage(&self, ctx: &Context) -> String {
1327 self.get_usage_with_subcommand(ctx)
1328 }
1329
1330 fn as_any(&self) -> &dyn Any {
1331 self
1332 }
1333}
1334
1335pub struct CommandCollectionBuilder {
1337 base: GroupBuilder,
1338 sources: Vec<Group>,
1339}
1340
1341impl CommandCollectionBuilder {
1342 fn new(name: &str) -> Self {
1343 Self {
1344 base: GroupBuilder::new(name),
1345 sources: Vec::new(),
1346 }
1347 }
1348
1349 pub fn source(mut self, group: Group) -> Self {
1351 self.sources.push(group);
1352 self
1353 }
1354
1355 pub fn command(mut self, cmd: impl CommandLike + 'static) -> Self {
1357 self.base = self.base.command(cmd);
1358 self
1359 }
1360
1361 pub fn build(self) -> CommandCollection {
1363 CommandCollection {
1364 base: self.base.build(),
1365 sources: self.sources,
1366 }
1367 }
1368}
1369
1370pub struct GroupBuilder {
1397 name: String,
1398 callback: Option<CommandCallback>,
1399 options: Vec<ClickOption>,
1400 arguments: Vec<Argument>,
1401 help: Option<String>,
1402 epilog: Option<String>,
1403 short_help: Option<String>,
1404 hidden: bool,
1405 deprecated: Option<String>,
1406 commands: HashMap<String, Arc<dyn CommandLike>>,
1407 command_ids_by_name: HashMap<String, usize>,
1408 command_aliases_by_id: HashMap<usize, Vec<String>>,
1409 next_command_id: usize,
1410 chain: bool,
1411 invoke_without_command: bool,
1412 result_callback: Option<ResultCallback>,
1413 subcommand_required: Option<bool>,
1414 subcommand_metavar: Option<String>,
1415 add_help_option: bool,
1416 help_option: Option<ClickOption>,
1417 no_args_is_help: Option<bool>,
1418}
1419
1420impl GroupBuilder {
1421 fn new(name: &str) -> Self {
1423 Self {
1424 name: name.to_string(),
1425 callback: None,
1426 options: Vec::new(),
1427 arguments: Vec::new(),
1428 help: None,
1429 epilog: None,
1430 short_help: None,
1431 hidden: false,
1432 deprecated: None,
1433 commands: HashMap::new(),
1434 command_ids_by_name: HashMap::new(),
1435 command_aliases_by_id: HashMap::new(),
1436 next_command_id: 0,
1437 chain: false,
1438 invoke_without_command: false,
1439 result_callback: None,
1440 subcommand_required: None,
1441 subcommand_metavar: None,
1442 add_help_option: true,
1443 help_option: None,
1444 no_args_is_help: None,
1445 }
1446 }
1447
1448 pub fn callback<F>(mut self, f: F) -> Self
1457 where
1458 F: Fn(&Context) -> Result<(), ClickError> + Send + Sync + 'static,
1459 {
1460 self.callback = Some(Box::new(f));
1461 self
1462 }
1463
1464 pub fn option(mut self, opt: ClickOption) -> Self {
1466 self.options.push(opt);
1467 self
1468 }
1469
1470 pub fn argument(mut self, arg: Argument) -> Self {
1472 self.arguments.push(arg);
1473 self
1474 }
1475
1476 pub fn help(mut self, help: &str) -> Self {
1478 self.help = Some(help.to_string());
1479 self
1480 }
1481
1482 pub fn epilog(mut self, epilog: &str) -> Self {
1484 self.epilog = Some(epilog.to_string());
1485 self
1486 }
1487
1488 pub fn short_help(mut self, short_help: &str) -> Self {
1490 self.short_help = Some(short_help.to_string());
1491 self
1492 }
1493
1494 pub fn hidden(mut self) -> Self {
1496 self.hidden = true;
1497 self
1498 }
1499
1500 pub fn deprecated(mut self, message: &str) -> Self {
1502 self.deprecated = Some(message.to_string());
1503 self
1504 }
1505
1506 pub fn add_help_option(mut self, add: bool) -> Self {
1508 self.add_help_option = add;
1509 self
1510 }
1511
1512 pub fn help_option(mut self, opt: ClickOption) -> Self {
1516 self.add_help_option = true;
1517 self.help_option = Some(opt);
1518 self
1519 }
1520
1521 pub fn no_args_is_help(mut self, value: bool) -> Self {
1525 self.no_args_is_help = Some(value);
1526 self
1527 }
1528
1529 pub fn command(self, cmd: impl CommandLike + 'static) -> Self {
1547 self.command_shared(Arc::new(cmd))
1548 }
1549
1550 pub fn command_with_name(self, name: &str, cmd: impl CommandLike + 'static) -> Self {
1552 self.command_shared_with_name(name, Arc::new(cmd))
1553 }
1554
1555 pub fn command_shared(mut self, cmd: Arc<dyn CommandLike>) -> Self {
1559 let name = cmd.name().map(|s| s.to_string());
1560 if let Some(name) = name {
1561 self = self.command_shared_with_name(&name, cmd);
1562 }
1563 self
1564 }
1565
1566 pub fn command_shared_with_name(mut self, name: &str, cmd: Arc<dyn CommandLike>) -> Self {
1568 if let Some(old_id) = self.command_ids_by_name.get(name).copied() {
1570 if let Some(names) = self.command_aliases_by_id.get_mut(&old_id) {
1571 names.retain(|n| n != name);
1572 }
1573 }
1574
1575 let existing_id = self.commands.iter().find_map(|(n, existing)| {
1577 if Arc::ptr_eq(existing, &cmd) {
1578 self.command_ids_by_name.get(n).copied()
1579 } else {
1580 None
1581 }
1582 });
1583
1584 let id = existing_id.unwrap_or_else(|| {
1585 let id = self.next_command_id;
1586 self.next_command_id += 1;
1587 id
1588 });
1589
1590 self.command_ids_by_name.insert(name.to_string(), id);
1591 self.command_aliases_by_id
1592 .entry(id)
1593 .or_insert_with(Vec::new)
1594 .push(name.to_string());
1595
1596 if let Some(names) = self.command_aliases_by_id.get_mut(&id) {
1597 names.sort();
1598 names.dedup();
1599 }
1600
1601 self.commands.insert(name.to_string(), cmd);
1602 self
1603 }
1604
1605 pub fn chain(mut self, chain: bool) -> Self {
1610 self.chain = chain;
1611 if chain {
1612 self.subcommand_metavar =
1613 Some("COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]...".to_string());
1614 }
1615 self
1616 }
1617
1618 pub fn invoke_without_command(mut self, value: bool) -> Self {
1623 self.invoke_without_command = value;
1624 self
1625 }
1626
1627 pub fn subcommand_required(mut self, required: bool) -> Self {
1631 self.subcommand_required = Some(required);
1632 self
1633 }
1634
1635 pub fn subcommand_metavar(mut self, metavar: &str) -> Self {
1640 self.subcommand_metavar = Some(metavar.to_string());
1641 self
1642 }
1643
1644 pub fn result_callback<F>(mut self, f: F) -> Self
1646 where
1647 F: Fn(&Context, Vec<Box<dyn Any + Send + Sync>>) -> Result<(), ClickError>
1648 + Send
1649 + Sync
1650 + 'static,
1651 {
1652 self.result_callback = Some(Box::new(f));
1653 self
1654 }
1655
1656 pub fn build(self) -> Group {
1658 let no_args_is_help = self.no_args_is_help.unwrap_or(!self.invoke_without_command);
1660
1661 let subcommand_required = self
1663 .subcommand_required
1664 .unwrap_or(!self.invoke_without_command);
1665
1666 let subcommand_metavar = self.subcommand_metavar.unwrap_or_else(|| {
1668 if self.chain {
1669 "COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]...".to_string()
1670 } else {
1671 "COMMAND [ARGS]...".to_string()
1672 }
1673 });
1674
1675 let mut cmd_builder = CommandBuilder::new(&self.name)
1677 .allow_extra_args(true)
1678 .allow_interspersed_args(false)
1679 .add_help_option(self.add_help_option)
1680 .no_args_is_help(no_args_is_help);
1681
1682 if let Some(help_opt) = self.help_option {
1683 cmd_builder = cmd_builder.help_option(help_opt);
1684 }
1685
1686 for opt in self.options {
1688 cmd_builder = cmd_builder.option(opt);
1689 }
1690
1691 for arg in self.arguments {
1693 cmd_builder = cmd_builder.argument(arg);
1694 }
1695
1696 if let Some(help) = self.help {
1698 cmd_builder = cmd_builder.help(&help);
1699 }
1700 if let Some(epilog) = self.epilog {
1701 cmd_builder = cmd_builder.epilog(&epilog);
1702 }
1703 if let Some(short_help) = self.short_help {
1704 cmd_builder = cmd_builder.short_help(&short_help);
1705 }
1706 if self.hidden {
1707 cmd_builder = cmd_builder.hidden();
1708 }
1709 if let Some(deprecated) = self.deprecated {
1710 cmd_builder = cmd_builder.deprecated(&deprecated);
1711 }
1712 if let Some(callback) = self.callback {
1713 let callback_wrapper = move |ctx: &Context| callback(ctx);
1715 cmd_builder = cmd_builder.callback(callback_wrapper);
1716 }
1717
1718 let command = cmd_builder.build();
1719
1720 Group {
1721 command,
1722 commands: self.commands,
1723 command_ids_by_name: self.command_ids_by_name,
1724 command_aliases_by_id: self.command_aliases_by_id,
1725 next_command_id: self.next_command_id,
1726 chain: self.chain,
1727 invoke_without_command: self.invoke_without_command,
1728 result_callback: self.result_callback,
1729 subcommand_required,
1730 subcommand_metavar,
1731 }
1732 }
1733}
1734
1735#[cfg(test)]
1740mod tests {
1741 use super::*;
1742 use std::sync::atomic::{AtomicBool, Ordering};
1743
1744 #[test]
1745 fn test_group_creation_defaults() {
1746 let group = Group::new("test").build();
1747
1748 assert_eq!(group.name(), Some("test"));
1749 assert!(group.commands.is_empty());
1750 assert!(!group.chain);
1751 assert!(!group.invoke_without_command);
1752 assert!(group.subcommand_required);
1753 assert_eq!(group.subcommand_metavar, "COMMAND [ARGS]...");
1754 }
1755
1756 #[test]
1757 fn test_group_with_subcommands() {
1758 let group = Group::new("cli")
1759 .command(Command::new("init").help("Initialize").build())
1760 .command(Command::new("build").help("Build").build())
1761 .build();
1762
1763 assert_eq!(group.commands.len(), 2);
1764 assert!(group.get_command("init").is_some());
1765 assert!(group.get_command("build").is_some());
1766 assert!(group.get_command("unknown").is_none());
1767 }
1768
1769 #[test]
1770 fn test_list_commands_sorted() {
1771 let group = Group::new("cli")
1772 .command(Command::new("zebra").build())
1773 .command(Command::new("alpha").build())
1774 .command(Command::new("middle").build())
1775 .build();
1776
1777 let commands = group.list_commands();
1778 assert_eq!(commands, vec!["alpha", "middle", "zebra"]);
1779 }
1780
1781 #[test]
1782 fn test_add_command_with_name() {
1783 let mut group = Group::new("cli").build();
1784
1785 group.add_command(Command::new("original").build(), Some("renamed"));
1786
1787 assert!(group.get_command("renamed").is_some());
1788 assert!(group.get_command("original").is_none());
1789 }
1790
1791 #[test]
1792 fn test_alias_metadata_for_shared_command() {
1793 let cmd: Arc<dyn CommandLike> = Arc::new(Command::new("original").build());
1794
1795 let group = Group::new("cli")
1796 .command_shared(Arc::clone(&cmd))
1797 .command_shared_with_name("alias", Arc::clone(&cmd))
1798 .build();
1799
1800 assert!(group.get_command("original").is_some());
1801 assert!(group.get_command("alias").is_some());
1802
1803 assert_eq!(
1804 group.list_command_aliases("original"),
1805 vec!["alias".to_string()]
1806 );
1807 assert_eq!(
1808 group.list_command_aliases("alias"),
1809 vec!["original".to_string()]
1810 );
1811
1812 let entries = group.list_command_entries();
1813 let alias_entry = entries
1814 .iter()
1815 .find(|(name, _)| name == "alias")
1816 .expect("alias entry missing");
1817 assert_eq!(alias_entry.1.name(), Some("original"));
1818 }
1819
1820 #[test]
1821 fn test_group_chain_mode() {
1822 let group = Group::new("cli").chain(true).build();
1823
1824 assert!(group.chain);
1825 assert_eq!(
1826 group.subcommand_metavar,
1827 "COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]..."
1828 );
1829 }
1830
1831 #[test]
1832 fn test_invoke_without_command() {
1833 let called = Arc::new(AtomicBool::new(false));
1834 let called_clone = Arc::clone(&called);
1835
1836 let group = Group::new("cli")
1837 .invoke_without_command(true)
1838 .callback(move |_ctx| {
1839 called_clone.store(true, Ordering::SeqCst);
1840 Ok(())
1841 })
1842 .build();
1843
1844 assert!(!group.subcommand_required);
1846
1847 let ctx = ContextBuilder::new().info_name("cli").build();
1849
1850 let result = group.invoke(&ctx);
1852 assert!(result.is_ok());
1853 assert!(called.load(Ordering::SeqCst));
1854 }
1855
1856 #[test]
1857 fn test_group_help_formatting() {
1858 let group = Group::new("cli")
1859 .help("A sample CLI application")
1860 .command(
1861 Command::new("init")
1862 .short_help("Initialize the project")
1863 .build(),
1864 )
1865 .command(
1866 Command::new("build")
1867 .short_help("Build the project")
1868 .build(),
1869 )
1870 .build();
1871
1872 let ctx = ContextBuilder::new().info_name("cli").build();
1873 let help = group.get_help(&ctx);
1874
1875 assert!(help.contains("Usage:"));
1876 assert!(help.contains("cli"));
1877 assert!(help.contains("COMMAND [ARGS]..."));
1878 assert!(help.contains("A sample CLI application"));
1879 assert!(help.contains("Commands:"));
1880 assert!(help.contains("init"));
1881 assert!(help.contains("build"));
1882 }
1883
1884 #[test]
1885 fn test_resolve_command() {
1886 let group = Group::new("cli")
1887 .command(Command::new("hello").build())
1888 .command(Command::new("world").build())
1889 .build();
1890
1891 let ctx = ContextBuilder::new().info_name("cli").build();
1892
1893 let args = vec!["hello".to_string(), "arg1".to_string()];
1895 let resolved = group.resolve_command(&ctx, &args);
1896 assert!(resolved.is_ok());
1897
1898 let (name, _cmd, remaining) = resolved.unwrap().unwrap();
1899 assert_eq!(name, "hello");
1900 assert_eq!(remaining, vec!["arg1".to_string()]);
1901
1902 let args = vec!["unknown".to_string()];
1904 let resolved = group.resolve_command(&ctx, &args);
1905 assert!(resolved.is_err());
1906 }
1907
1908 #[test]
1909 fn test_resolve_command_empty_args() {
1910 let group = Group::new("cli")
1911 .command(Command::new("hello").build())
1912 .build();
1913
1914 let ctx = ContextBuilder::new().info_name("cli").build();
1915
1916 let resolved = group.resolve_command(&ctx, &[]);
1917 assert!(resolved.is_ok());
1918 assert!(resolved.unwrap().is_none());
1919 }
1920
1921 #[test]
1922 fn test_group_with_options() {
1923 let group = Group::new("cli")
1924 .help("A CLI with options")
1925 .option(
1926 ClickOption::new(&["--verbose", "-v"])
1927 .flag("true")
1928 .help("Enable verbose mode")
1929 .build(),
1930 )
1931 .command(Command::new("run").build())
1932 .build();
1933
1934 assert_eq!(group.command.options.len(), 1);
1935
1936 let ctx = ContextBuilder::new().info_name("cli").build();
1937 let help = group.get_help(&ctx);
1938
1939 assert!(help.contains("--verbose"));
1940 assert!(help.contains("Enable verbose mode"));
1941 }
1942
1943 #[test]
1944 fn test_hidden_commands_not_in_help() {
1945 let group = Group::new("cli")
1946 .command(Command::new("visible").build())
1947 .command(Command::new("hidden").hidden().build())
1948 .build();
1949
1950 let ctx = ContextBuilder::new().info_name("cli").build();
1951 let help = group.format_commands(&ctx);
1952
1953 assert!(help.contains("visible"));
1954 assert!(!help.contains("hidden"));
1955 }
1956
1957 #[test]
1958 fn test_subcommand_required_default() {
1959 let group1 = Group::new("cli").build();
1961 assert!(group1.subcommand_required);
1962
1963 let group2 = Group::new("cli").invoke_without_command(true).build();
1965 assert!(!group2.subcommand_required);
1966
1967 let group3 = Group::new("cli")
1969 .invoke_without_command(true)
1970 .subcommand_required(true)
1971 .build();
1972 assert!(group3.subcommand_required);
1973 }
1974
1975 #[test]
1976 fn test_group_short_help() {
1977 let group = Group::new("cli")
1978 .help("This is the long help text. It has multiple sentences.")
1979 .build();
1980
1981 let short = group.get_short_help();
1982 assert_eq!(short, "This is the long help text");
1983
1984 let group_explicit = Group::new("cli")
1985 .help("Long help")
1986 .short_help("Short help")
1987 .build();
1988
1989 let short = group_explicit.get_short_help();
1990 assert_eq!(short, "Short help");
1991 }
1992
1993 #[test]
1994 fn test_group_debug_format() {
1995 let group = Group::new("cli")
1996 .command(Command::new("a").build())
1997 .command(Command::new("b").build())
1998 .build();
1999
2000 let debug_str = format!("{:?}", group);
2001 assert!(debug_str.contains("Group"));
2002 assert!(debug_str.contains("2 subcommands"));
2003 }
2004
2005 #[test]
2006 fn test_nested_groups() {
2007 let sub_group = Group::new("sub")
2008 .help("Subgroup")
2009 .command(Command::new("cmd").build())
2010 .build();
2011
2012 let main_group = Group::new("main")
2013 .help("Main group")
2014 .command(sub_group)
2015 .build();
2016
2017 assert!(main_group.get_command("sub").is_some());
2018
2019 let sub = main_group.get_command("sub").unwrap();
2021 assert_eq!(sub.name(), Some("sub"));
2022 }
2023
2024 #[test]
2025 fn test_command_with_name_builder() {
2026 let group = Group::new("cli")
2027 .command_with_name("alias", Command::new("original").build())
2028 .build();
2029
2030 assert!(group.get_command("alias").is_some());
2031 assert!(group.get_command("original").is_none());
2032 }
2033
2034 #[test]
2035 fn test_missing_command_error() {
2036 let group = Group::new("cli").subcommand_required(true).build();
2037
2038 let ctx = ContextBuilder::new().info_name("cli").build();
2039
2040 let result = group.invoke(&ctx);
2042 assert!(result.is_err());
2043
2044 let err = result.unwrap_err();
2045 assert!(matches!(err, ClickError::UsageError { .. }));
2046 }
2047
2048 #[test]
2049 fn test_commandlike_trait() {
2050 let cmd: Box<dyn CommandLike> = Box::new(Command::new("cmd").build());
2052 let grp: Box<dyn CommandLike> = Box::new(Group::new("grp").build());
2053
2054 assert_eq!(cmd.name(), Some("cmd"));
2055 assert_eq!(grp.name(), Some("grp"));
2056
2057 assert!(!cmd.is_hidden());
2058 assert!(!grp.is_hidden());
2059 }
2060
2061 #[test]
2062 fn test_group_usage() {
2063 let group = Group::new("cli")
2064 .option(ClickOption::new(&["--debug"]).flag("true").build())
2065 .build();
2066
2067 let ctx = ContextBuilder::new().info_name("cli").build();
2068 let usage = group.get_usage(&ctx);
2069
2070 assert!(usage.contains("cli"));
2071 assert!(usage.contains("[OPTIONS]"));
2072 assert!(usage.contains("COMMAND [ARGS]..."));
2073 }
2074
2075 #[test]
2076 fn test_chain_metavar() {
2077 let group = Group::new("cli")
2078 .chain(true)
2079 .subcommand_metavar("CMD1 CMD2...")
2080 .build();
2081
2082 assert_eq!(group.subcommand_metavar, "CMD1 CMD2...");
2084 }
2085
2086 #[test]
2087 fn test_group_deprecated() {
2088 let group = Group::new("old")
2089 .help("Old group")
2090 .deprecated("Use 'new' instead")
2091 .build();
2092
2093 let short = group.get_short_help();
2094 assert!(short.contains("DEPRECATED"));
2095 assert!(short.contains("Use 'new' instead"));
2096 }
2097
2098 #[test]
2103 fn test_subcommand_context_inheritance() {
2104 let parent_info_name = Arc::new(std::sync::Mutex::new(String::new()));
2106 let parent_info_clone = Arc::clone(&parent_info_name);
2107
2108 let group = Group::new("cli")
2109 .command(
2110 Command::new("sub")
2111 .callback(move |ctx| {
2112 if let Some(parent) = ctx.parent() {
2114 let mut lock = parent_info_clone.lock().unwrap();
2115 if let Some(name) = parent.info_name() {
2116 *lock = name.to_string();
2117 }
2118 }
2119 Ok(())
2120 })
2121 .build(),
2122 )
2123 .build();
2124
2125 let result = group.main(vec!["sub".to_string()]);
2127 assert!(result.is_ok());
2128
2129 let captured = parent_info_name.lock().unwrap();
2131 assert_eq!(*captured, "cli");
2132 }
2133
2134 #[test]
2135 fn test_subcommand_inherits_terminal_settings() {
2136 let inherited_width = Arc::new(std::sync::Mutex::new(None::<usize>));
2138 let inherited_color = Arc::new(std::sync::Mutex::new(None::<bool>));
2139 let width_clone = Arc::clone(&inherited_width);
2140 let color_clone = Arc::clone(&inherited_color);
2141
2142 let group = Group::new("cli")
2143 .command(
2144 Command::new("sub")
2145 .callback(move |ctx| {
2146 *width_clone.lock().unwrap() = ctx.terminal_width();
2147 *color_clone.lock().unwrap() = ctx.color();
2148 Ok(())
2149 })
2150 .build(),
2151 )
2152 .build();
2153
2154 let parent_ctx = ContextBuilder::new()
2156 .info_name("cli")
2157 .terminal_width(120)
2158 .color(true)
2159 .allow_extra_args(true)
2160 .build();
2161 let _parent_ctx = Arc::new(parent_ctx);
2162
2163 let ctx = group
2165 .make_context("cli", vec!["sub".to_string()], None)
2166 .unwrap();
2167
2168 push_context(Arc::new(
2171 ContextBuilder::new()
2172 .info_name("cli")
2173 .terminal_width(120)
2174 .color(true)
2175 .allow_extra_args(true)
2176 .build(),
2177 ));
2178
2179 let result = group.invoke(&ctx);
2180 pop_context();
2181
2182 assert!(result.is_ok());
2183 }
2186
2187 #[test]
2188 fn test_chain_mode_multiple_commands() {
2189 let call_order = Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
2191 let order1 = Arc::clone(&call_order);
2192 let order2 = Arc::clone(&call_order);
2193 let order3 = Arc::clone(&call_order);
2194
2195 let group = Group::new("cli")
2196 .chain(true)
2197 .command(
2198 Command::new("cmd1")
2199 .callback(move |_ctx| {
2200 order1.lock().unwrap().push("cmd1".to_string());
2201 Ok(())
2202 })
2203 .build(),
2204 )
2205 .command(
2206 Command::new("cmd2")
2207 .callback(move |_ctx| {
2208 order2.lock().unwrap().push("cmd2".to_string());
2209 Ok(())
2210 })
2211 .build(),
2212 )
2213 .command(
2214 Command::new("cmd3")
2215 .callback(move |_ctx| {
2216 order3.lock().unwrap().push("cmd3".to_string());
2217 Ok(())
2218 })
2219 .build(),
2220 )
2221 .build();
2222
2223 let result = group.main(vec![
2225 "cmd1".to_string(),
2226 "cmd2".to_string(),
2227 "cmd3".to_string(),
2228 ]);
2229 assert!(result.is_ok());
2230
2231 let order = call_order.lock().unwrap();
2233 assert_eq!(*order, vec!["cmd1", "cmd2", "cmd3"]);
2234 }
2235
2236 #[test]
2237 fn test_chain_mode_with_args() {
2238 let captured_args = Arc::new(std::sync::Mutex::new(Vec::<Vec<String>>::new()));
2240 let args1 = Arc::clone(&captured_args);
2241 let args2 = Arc::clone(&captured_args);
2242
2243 let group = Group::new("cli")
2244 .chain(true)
2245 .command(
2246 Command::new("first")
2247 .callback(move |ctx| {
2248 args1.lock().unwrap().push(ctx.args().to_vec());
2249 Ok(())
2250 })
2251 .build(),
2252 )
2253 .command(
2254 Command::new("second")
2255 .callback(move |ctx| {
2256 args2.lock().unwrap().push(ctx.args().to_vec());
2257 Ok(())
2258 })
2259 .build(),
2260 )
2261 .build();
2262
2263 let result = group.main(vec!["first".to_string(), "second".to_string()]);
2265 assert!(result.is_ok());
2266
2267 let args = captured_args.lock().unwrap();
2268 assert_eq!(args.len(), 2);
2269 }
2270
2271 #[test]
2272 fn test_chain_mode_empty_returns_ok() {
2273 let called = Arc::new(AtomicBool::new(false));
2275 let called_clone = Arc::clone(&called);
2276
2277 let group = Group::new("cli")
2278 .chain(true)
2279 .invoke_without_command(true)
2280 .callback(move |_ctx| {
2281 called_clone.store(true, Ordering::SeqCst);
2282 Ok(())
2283 })
2284 .command(Command::new("sub").build())
2285 .build();
2286
2287 let result = group.main(vec![]);
2288 assert!(result.is_ok());
2289 assert!(called.load(Ordering::SeqCst));
2290 }
2291
2292 #[test]
2293 fn test_result_callback_invoked() {
2294 let result_callback_called = Arc::new(AtomicBool::new(false));
2296 let callback_clone = Arc::clone(&result_callback_called);
2297
2298 let group = Group::new("cli")
2299 .command(Command::new("sub").callback(|_ctx| Ok(())).build())
2300 .result_callback(move |_ctx, _results| {
2301 callback_clone.store(true, Ordering::SeqCst);
2302 Ok(())
2303 })
2304 .build();
2305
2306 let result = group.main(vec!["sub".to_string()]);
2307 assert!(result.is_ok());
2308 assert!(result_callback_called.load(Ordering::SeqCst));
2309 }
2310
2311 #[test]
2312 fn test_result_callback_with_chain_mode() {
2313 let result_callback_called = Arc::new(AtomicBool::new(false));
2315 let callback_clone = Arc::clone(&result_callback_called);
2316 let results_count = Arc::new(std::sync::Mutex::new(0usize));
2317 let count_clone = Arc::clone(&results_count);
2318
2319 let group = Group::new("cli")
2320 .chain(true)
2321 .command(Command::new("a").callback(|_| Ok(())).build())
2322 .command(Command::new("b").callback(|_| Ok(())).build())
2323 .result_callback(move |_ctx, results| {
2324 callback_clone.store(true, Ordering::SeqCst);
2325 *count_clone.lock().unwrap() = results.len();
2326 Ok(())
2327 })
2328 .build();
2329
2330 let result = group.main(vec!["a".to_string(), "b".to_string()]);
2331 assert!(result.is_ok());
2332 assert!(result_callback_called.load(Ordering::SeqCst));
2333
2334 let count = *results_count.lock().unwrap();
2336 assert_eq!(count, 2);
2337 }
2338
2339 #[test]
2340 fn test_result_callback_invoke_without_command() {
2341 let result_callback_called = Arc::new(AtomicBool::new(false));
2343 let callback_clone = Arc::clone(&result_callback_called);
2344
2345 let group = Group::new("cli")
2346 .invoke_without_command(true)
2347 .callback(|_ctx| Ok(()))
2348 .result_callback(move |_ctx, _results| {
2349 callback_clone.store(true, Ordering::SeqCst);
2350 Ok(())
2351 })
2352 .build();
2353
2354 let result = group.main(vec![]);
2355 assert!(result.is_ok());
2356 assert!(result_callback_called.load(Ordering::SeqCst));
2357 }
2358
2359 #[test]
2360 fn test_chain_mode_subcommand_failure_stops_chain() {
2361 let second_called = Arc::new(AtomicBool::new(false));
2363 let second_clone = Arc::clone(&second_called);
2364
2365 let group = Group::new("cli")
2366 .chain(true)
2367 .command(
2368 Command::new("fail")
2369 .callback(|_ctx| Err(ClickError::usage("intentional failure")))
2370 .build(),
2371 )
2372 .command(
2373 Command::new("second")
2374 .callback(move |_ctx| {
2375 second_clone.store(true, Ordering::SeqCst);
2376 Ok(())
2377 })
2378 .build(),
2379 )
2380 .build();
2381
2382 let result = group.main(vec!["fail".to_string(), "second".to_string()]);
2383 assert!(result.is_err());
2384 assert!(!second_called.load(Ordering::SeqCst));
2386 }
2387
2388 #[test]
2389 fn test_non_chain_mode_single_command() {
2390 let calls = Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
2392 let calls1 = Arc::clone(&calls);
2393
2394 let group = Group::new("cli")
2395 .chain(false) .command(
2397 Command::new("cmd1")
2398 .callback(move |_ctx| {
2399 calls1.lock().unwrap().push("cmd1".to_string());
2400 Ok(())
2401 })
2402 .build(),
2403 )
2404 .command(Command::new("cmd2").build())
2405 .build();
2406
2407 let result = group.main(vec!["cmd1".to_string()]);
2409 assert!(result.is_ok());
2410
2411 let recorded = calls.lock().unwrap();
2412 assert_eq!(recorded.len(), 1);
2413 assert_eq!(recorded[0], "cmd1");
2414 }
2415
2416 #[test]
2417 fn test_group_callback_called_before_subcommand() {
2418 let call_order = Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
2420 let order_group = Arc::clone(&call_order);
2421 let order_sub = Arc::clone(&call_order);
2422
2423 let group = Group::new("cli")
2424 .callback(move |_ctx| {
2425 order_group.lock().unwrap().push("group".to_string());
2426 Ok(())
2427 })
2428 .command(
2429 Command::new("sub")
2430 .callback(move |_ctx| {
2431 order_sub.lock().unwrap().push("sub".to_string());
2432 Ok(())
2433 })
2434 .build(),
2435 )
2436 .build();
2437
2438 let result = group.main(vec!["sub".to_string()]);
2439 assert!(result.is_ok());
2440
2441 let order = call_order.lock().unwrap();
2442 assert_eq!(*order, vec!["group", "sub"]);
2443 }
2444
2445 #[test]
2446 fn test_group_callback_called_before_chain() {
2447 let call_order = Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
2449 let order_group = Arc::clone(&call_order);
2450 let order_a = Arc::clone(&call_order);
2451 let order_b = Arc::clone(&call_order);
2452
2453 let group = Group::new("cli")
2454 .chain(true)
2455 .callback(move |_ctx| {
2456 order_group.lock().unwrap().push("group".to_string());
2457 Ok(())
2458 })
2459 .command(
2460 Command::new("a")
2461 .callback(move |_ctx| {
2462 order_a.lock().unwrap().push("a".to_string());
2463 Ok(())
2464 })
2465 .build(),
2466 )
2467 .command(
2468 Command::new("b")
2469 .callback(move |_ctx| {
2470 order_b.lock().unwrap().push("b".to_string());
2471 Ok(())
2472 })
2473 .build(),
2474 )
2475 .build();
2476
2477 let result = group.main(vec!["a".to_string(), "b".to_string()]);
2478 assert!(result.is_ok());
2479
2480 let order = call_order.lock().unwrap();
2481 assert_eq!(*order, vec!["group", "a", "b"]);
2482 }
2483
2484 #[test]
2485 fn test_command_collection_list_commands_union_sorted() {
2486 let src = Group::new("src")
2487 .command(Command::new("c").help("C").build())
2488 .command(Command::new("b").help("B").build())
2489 .build();
2490
2491 let collection = CommandCollection::new("coll")
2492 .command(Command::new("a").help("A").build())
2493 .source(src)
2494 .build();
2495
2496 assert_eq!(
2497 collection.list_commands(),
2498 vec!["a".to_string(), "b".to_string(), "c".to_string()]
2499 );
2500 }
2501
2502 #[test]
2503 fn test_command_collection_prefers_base_over_sources() {
2504 let src = Group::new("src")
2505 .command(Command::new("dup").help("Src").build())
2506 .build();
2507
2508 let collection = CommandCollection::new("coll")
2509 .command(Command::new("dup").help("Base").build())
2510 .source(src)
2511 .build();
2512
2513 let ctx = ContextBuilder::new().info_name("coll").build();
2514 let help = collection.get_help(&ctx);
2515 assert!(help.contains("dup"));
2516 assert_eq!(
2517 collection.get_command("dup").unwrap().get_short_help(),
2518 "Base"
2519 );
2520 }
2521
2522 #[test]
2527 fn test_group_help_with_missing_subcommand() {
2528 let group = Group::new("cli")
2530 .subcommand_required(true)
2531 .command(Command::new("sub").build())
2532 .build();
2533
2534 let _ctx = group.make_context("cli", vec![], None);
2536 let ctx = group.make_context("cli", vec!["--help".to_string()], None);
2541 assert!(matches!(ctx, Err(ClickError::Exit { code: 0 })));
2542 }
2543
2544 #[test]
2545 fn test_group_help_with_required_option() {
2546 let group = Group::new("cli")
2548 .option(ClickOption::new(&["--name", "-n"]).required().build())
2549 .command(Command::new("sub").build())
2550 .build();
2551
2552 let ctx = group.make_context("cli", vec!["sub".to_string()], None);
2554 assert!(ctx.is_err());
2555
2556 let ctx = group.make_context("cli", vec!["--help".to_string()], None);
2558 assert!(matches!(ctx, Err(ClickError::Exit { code: 0 })));
2559 }
2560
2561 #[test]
2562 fn test_group_version_option() {
2563 use crate::option::ClickOption;
2564
2565 let version_opt = ClickOption::new(&["--version", "-V"])
2567 .flag("true")
2568 .eager()
2569 .metavar("__click_version__:myapp 1.0.0")
2570 .help("Show version and exit.")
2571 .build();
2572
2573 let group = Group::new("cli")
2574 .option(version_opt)
2575 .command(Command::new("sub").build())
2576 .build();
2577
2578 let ctx = group.make_context("cli", vec!["--version".to_string()], None);
2580 assert!(matches!(ctx, Err(ClickError::Exit { code: 0 })));
2581 }
2582
2583 #[test]
2588 fn test_custom_renderer_invoked_for_subcommand_help() {
2589 use crate::context::{ContextBuilder, HelpRenderer};
2590 use std::sync::Mutex;
2591
2592 let captured_name: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
2594 let captured_clone = Arc::clone(&captured_name);
2595
2596 let renderer: HelpRenderer = Arc::new(move |_cmd, ctx| {
2597 let mut lock = captured_clone.lock().unwrap();
2598 *lock = Some(ctx.info_name().unwrap_or("").to_string());
2599 format!("RICH:{}", ctx.info_name().unwrap_or(""))
2600 });
2601
2602 let group = Group::new("cli")
2603 .command(Command::new("sub").help("Does something.").build())
2604 .build();
2605
2606 let root_ctx = Arc::new(
2609 ContextBuilder::new()
2610 .info_name("cli")
2611 .allow_extra_args(true)
2612 .allow_interspersed_args(false)
2613 .help_renderer(renderer)
2614 .build(),
2615 );
2616
2617 let mut ctx = ContextBuilder::new()
2620 .info_name("cli")
2621 .allow_extra_args(true)
2622 .allow_interspersed_args(false)
2623 .help_renderer(Arc::new(move |_cmd, ctx2| {
2624 format!("RICH2:{}", ctx2.info_name().unwrap_or(""))
2625 }))
2626 .build();
2627 ctx.args_mut().push("sub".to_string());
2629 ctx.args_mut().push("--help".to_string());
2630
2631 push_context(Arc::clone(&root_ctx));
2632 let result = group.invoke(&ctx);
2633 pop_context();
2634
2635 assert!(result.is_ok());
2637 }
2638
2639 #[test]
2640 fn test_fallback_renderer_used_when_no_custom_renderer() {
2641 let group = Group::new("cli")
2643 .command(Command::new("sub").help("Sub help text.").build())
2644 .build();
2645
2646 let mut ctx = ContextBuilder::new()
2647 .info_name("cli")
2648 .allow_extra_args(true)
2649 .allow_interspersed_args(false)
2650 .build();
2651 ctx.args_mut().push("sub".to_string());
2652 ctx.args_mut().push("--help".to_string());
2653
2654 let root_ctx = Arc::new(ContextBuilder::new().info_name("cli").build());
2655 push_context(Arc::clone(&root_ctx));
2656 let result = group.invoke(&ctx);
2657 pop_context();
2658
2659 assert!(result.is_ok());
2660 }
2661
2662 #[test]
2663 fn test_custom_renderer_invoked_for_chain_subcommand_help() {
2664 use crate::context::{ContextBuilder, HelpRenderer};
2666 use std::sync::atomic::{AtomicBool, Ordering};
2667
2668 let renderer_called = Arc::new(AtomicBool::new(false));
2669 let called_clone = Arc::clone(&renderer_called);
2670
2671 let renderer: HelpRenderer = Arc::new(move |_cmd, _ctx| {
2672 called_clone.store(true, Ordering::SeqCst);
2673 "CHAIN_RICH".to_string()
2674 });
2675
2676 let group = Group::new("cli")
2677 .chain(true)
2678 .command(Command::new("step1").help("Step one.").build())
2679 .build();
2680
2681 let mut ctx = ContextBuilder::new()
2682 .info_name("cli")
2683 .allow_extra_args(true)
2684 .allow_interspersed_args(false)
2685 .help_renderer(renderer)
2686 .build();
2687 ctx.args_mut().push("step1".to_string());
2688 ctx.args_mut().push("--help".to_string());
2689
2690 let root_ctx = Arc::new(ContextBuilder::new().info_name("cli").build());
2691 push_context(Arc::clone(&root_ctx));
2692 let result = group.invoke(&ctx);
2693 pop_context();
2694
2695 assert!(result.is_ok());
2696 assert!(renderer_called.load(Ordering::SeqCst));
2697 }
2698}