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 println!("{}", cmd.get_help(&help_ctx));
708 return Ok(());
709 }
710 Err(e) => return Err(e),
711 };
712
713 let sub_ctx_arc = Arc::new(sub_ctx);
715 push_context(Arc::clone(&sub_ctx_arc));
716 let result = cmd.invoke(&sub_ctx_arc);
717 pop_context();
718
719 sub_ctx_arc.close();
721
722 if result.is_ok() {
724 process_result(&self.result_callback, ctx, Vec::new())?;
726 }
727
728 result
729 } else {
730 if self.command.callback.is_some() {
735 self.command.invoke(ctx)?;
736 }
737
738 let mut contexts: Vec<(Arc<Context>, &dyn CommandLike)> = Vec::new();
740 let mut remaining_args = args;
741
742 while !remaining_args.is_empty() {
743 let resolved = self.resolve_command(ctx, &remaining_args)?;
744 match resolved {
745 Some((cmd_name, cmd, rest)) => {
746 let mut sub_ctx = ContextBuilder::new()
750 .info_name(cmd_name)
751 .allow_extra_args(true) .allow_interspersed_args(false) .parent(
754 parent_arc
755 .clone()
756 .unwrap_or_else(|| Arc::new(Context::default())),
757 )
758 .build();
759
760 let parse_result = if let Some(command) =
764 cmd.as_any().downcast_ref::<Command>()
765 {
766 command.parse_args(&mut sub_ctx, rest)
767 } else if let Some(group) = cmd.as_any().downcast_ref::<Group>() {
768 group
770 .make_context(cmd_name, rest, parent_arc.clone())
771 .map(|nested_ctx| {
772 sub_ctx = nested_ctx;
773 })
774 } else {
775 cmd.make_context(cmd_name, rest, parent_arc.clone())
777 .map(|fallback_ctx| {
778 sub_ctx = fallback_ctx;
779 })
780 };
781 match parse_result {
782 Ok(()) => {}
783 Err(ClickError::Exit { code: 0 }) => {
784 let help_ctx = ContextBuilder::new()
785 .info_name(format!("{} {}", ctx.command_path(), cmd_name))
786 .build();
787 println!("{}", cmd.get_help(&help_ctx));
788 return Ok(());
789 }
790 Err(e) => return Err(e),
791 }
792
793 remaining_args = sub_ctx.args().to_vec();
795
796 contexts.push((Arc::new(sub_ctx), cmd));
797 }
798 None => {
799 if !remaining_args.is_empty()
804 && remaining_args[0].starts_with('-')
805 && !ctx.resilient_parsing()
806 {
807 return Err(ClickError::usage(format!(
808 "No such option: {}",
809 remaining_args[0]
810 )));
811 }
812 break;
813 }
814 }
815 }
816
817 let mut results: Vec<Box<dyn Any + Send + Sync>> = Vec::new();
819 for (sub_ctx_arc, cmd) in contexts {
820 push_context(Arc::clone(&sub_ctx_arc));
821 let result = cmd.invoke(&sub_ctx_arc);
822 pop_context();
823 sub_ctx_arc.close();
824
825 result?;
827
828 results.push(Box::new(()));
831 }
832
833 process_result(&self.result_callback, ctx, results)?;
835
836 Ok(())
837 }
838 }
839
840 #[allow(clippy::arc_with_non_send_sync)]
841 fn main(&self, args: Vec<String>) -> Result<(), ClickError> {
842 let prog_name = self.command.name.clone().unwrap_or_else(|| {
843 std::env::args()
844 .next()
845 .unwrap_or_else(|| "program".to_string())
846 });
847
848 let args_for_eager = args.clone();
849
850 let ctx_result = self.make_context(&prog_name, args, None);
852
853 match ctx_result {
854 Ok(ctx) => {
855 let ctx = Arc::new(ctx);
856
857 push_context(Arc::clone(&ctx));
859
860 let result = self.invoke(&ctx);
862
863 pop_context();
865
866 ctx.close();
868
869 result
870 }
871 Err(ClickError::Exit { code: 0 }) => {
872 if let Some(version_output) =
877 self.command.get_version_output_from_args(&args_for_eager)
878 {
879 println!("{}", version_output);
880 return Ok(());
881 }
882
883 let ctx = ContextBuilder::new().info_name(&prog_name).build();
886 println!("{}", self.get_help(&ctx));
887 Ok(())
888 }
889 Err(e) => Err(e),
890 }
891 }
892
893 fn get_help(&self, ctx: &Context) -> String {
894 self.get_help_with_commands(ctx)
895 }
896
897 fn get_short_help(&self) -> String {
898 self.command.get_short_help()
899 }
900
901 fn is_hidden(&self) -> bool {
902 self.command.hidden
903 }
904
905 fn get_usage(&self, ctx: &Context) -> String {
906 self.get_usage_with_subcommand(ctx)
907 }
908
909 fn as_any(&self) -> &dyn Any {
910 self
911 }
912}
913
914#[derive(Debug)]
926pub struct CommandCollection {
927 pub base: Group,
929
930 pub sources: Vec<Group>,
932}
933
934impl CommandCollection {
935 #[allow(clippy::new_ret_no_self)]
939 pub fn new(name: &str) -> CommandCollectionBuilder {
940 CommandCollectionBuilder::new(name)
941 }
942
943 pub fn add_source(&mut self, group: Group) {
945 self.sources.push(group);
946 }
947
948 pub fn get_command(&self, name: &str) -> Option<&dyn CommandLike> {
950 if let Some(cmd) = self.base.get_command(name) {
951 return Some(cmd);
952 }
953 for src in &self.sources {
954 if let Some(cmd) = src.get_command(name) {
955 return Some(cmd);
956 }
957 }
958 None
959 }
960
961 pub fn list_commands(&self) -> Vec<String> {
963 let mut names: std::collections::HashSet<String> =
964 self.base.commands.keys().cloned().collect();
965
966 for src in &self.sources {
967 for name in src.commands.keys() {
968 names.insert(name.clone());
969 }
970 }
971
972 let mut out: Vec<String> = names.into_iter().collect();
973 out.sort();
974 out
975 }
976
977 fn resolve_command<'a>(
978 &'a self,
979 ctx: &Context,
980 args: &[String],
981 ) -> Result<Option<(String, &'a dyn CommandLike, Vec<String>)>, ClickError> {
982 if args.is_empty() {
983 return Ok(None);
984 }
985
986 let cmd_name = &args[0];
987 let remaining = args[1..].to_vec();
988
989 if let Some(cmd) = self.base.commands.get(cmd_name) {
990 return Ok(Some((cmd_name.clone(), cmd.as_ref(), remaining)));
991 }
992 for src in &self.sources {
993 if let Some(cmd) = src.commands.get(cmd_name) {
994 return Ok(Some((cmd_name.clone(), cmd.as_ref(), remaining)));
995 }
996 }
997
998 if ctx.resilient_parsing() {
999 return Ok(None);
1000 }
1001 if cmd_name.starts_with('-') {
1002 return Ok(None);
1003 }
1004
1005 Err(ClickError::usage(format!(
1006 "No such command '{}'.",
1007 cmd_name
1008 )))
1009 }
1010
1011 fn format_commands(&self, _ctx: &Context) -> String {
1012 let mut visible_cmds: Vec<(String, &dyn CommandLike)> = self
1013 .list_commands()
1014 .into_iter()
1015 .filter_map(|name| {
1016 self.get_command(&name)
1017 .filter(|cmd| !cmd.is_hidden())
1018 .map(|cmd| (name, cmd))
1019 })
1020 .collect();
1021
1022 if visible_cmds.is_empty() {
1023 return String::new();
1024 }
1025
1026 visible_cmds.sort_by(|a, b| a.0.cmp(&b.0));
1027
1028 let max_width = visible_cmds
1029 .iter()
1030 .map(|(name, _)| name.len())
1031 .max()
1032 .unwrap_or(0);
1033
1034 let mut lines = Vec::new();
1035 lines.push("Commands:".to_string());
1036
1037 for (name, cmd) in visible_cmds {
1038 let help = cmd.get_short_help();
1039 let padding = max_width - name.len() + 2;
1040 lines.push(format!(
1041 " {}{:padding$}{}",
1042 name,
1043 "",
1044 help,
1045 padding = padding
1046 ));
1047 }
1048
1049 lines.join("\n")
1050 }
1051
1052 fn get_usage_with_subcommand(&self, ctx: &Context) -> String {
1053 let base_usage = self.base.command.get_usage(ctx);
1054 format!("{} {}", base_usage, self.base.subcommand_metavar)
1055 }
1056
1057 fn get_help_with_commands(&self, ctx: &Context) -> String {
1058 let mut parts = Vec::new();
1059
1060 parts.push(self.get_usage_with_subcommand(ctx));
1061
1062 if let Some(ref help) = self.base.command.help {
1063 let text = help.lines().next().unwrap_or("");
1064 if !text.is_empty() {
1065 parts.push(String::new());
1066 let help_text = if let Some(ref dep) = self.base.command.deprecated {
1067 if dep.is_empty() {
1068 format!("{} (DEPRECATED)", text)
1069 } else {
1070 format!("{} (DEPRECATED: {})", text, dep)
1071 }
1072 } else {
1073 text.to_string()
1074 };
1075 parts.push(format!(" {}", help_text));
1076 }
1077 }
1078
1079 let opt_records: Vec<(String, String)> = self
1080 .base
1081 .command
1082 .options
1083 .iter()
1084 .filter_map(|opt| opt.get_help_record())
1085 .collect();
1086
1087 let help_opt = self.base.command.get_help_option(ctx);
1088 let help_record = help_opt.as_ref().and_then(|h| h.get_help_record());
1089
1090 if !opt_records.is_empty() || help_record.is_some() {
1091 parts.push(String::new());
1092 parts.push("Options:".to_string());
1093
1094 for (opt_str, help) in &opt_records {
1095 parts.push(format!(" {} {}", opt_str, help));
1096 }
1097 if let Some((opt_str, help)) = help_record {
1098 parts.push(format!(" {} {}", opt_str, help));
1099 }
1100 }
1101
1102 let commands_section = self.format_commands(ctx);
1103 if !commands_section.is_empty() {
1104 parts.push(String::new());
1105 parts.push(commands_section);
1106 }
1107
1108 if let Some(ref epilog) = self.base.command.epilog {
1109 parts.push(String::new());
1110 parts.push(epilog.clone());
1111 }
1112
1113 parts.join("\n")
1114 }
1115}
1116
1117impl CommandLike for CommandCollection {
1118 fn name(&self) -> Option<&str> {
1119 self.base.command.name.as_deref()
1120 }
1121
1122 fn make_context(
1123 &self,
1124 info_name: &str,
1125 args: Vec<String>,
1126 parent: Option<Arc<Context>>,
1127 ) -> Result<Context, ClickError> {
1128 let mut builder = ContextBuilder::new()
1129 .info_name(info_name)
1130 .allow_extra_args(true)
1131 .allow_interspersed_args(false);
1132
1133 if let Some(parent) = parent {
1134 builder = builder.parent(parent);
1135 }
1136
1137 let mut ctx = builder.build();
1138 self.base.command.parse_args(&mut ctx, args)?;
1139 Ok(ctx)
1140 }
1141
1142 fn invoke(&self, ctx: &Context) -> Result<(), ClickError> {
1143 let args = ctx.args().to_vec();
1144
1145 let process_result = |result_callback: &Option<ResultCallback>,
1146 ctx: &Context,
1147 results: Vec<Box<dyn Any + Send + Sync>>|
1148 -> Result<(), ClickError> {
1149 if let Some(ref callback) = result_callback {
1150 callback(ctx, results)?;
1151 }
1152 Ok(())
1153 };
1154
1155 let parent_arc = get_current_context();
1156 let resolved = self.resolve_command(ctx, &args)?;
1157
1158 if resolved.is_none() {
1159 if self.base.invoke_without_command {
1160 let group_result = self.base.command.invoke(ctx);
1161 if group_result.is_ok() {
1162 process_result(&self.base.result_callback, ctx, Vec::new())?;
1163 }
1164 return group_result;
1165 } else if self.base.subcommand_required && !ctx.resilient_parsing() {
1166 return Err(ClickError::usage("Missing command."));
1167 } else {
1168 return Ok(());
1169 }
1170 }
1171
1172 if !self.base.chain {
1173 let (cmd_name, cmd, remaining) = resolved.unwrap();
1174
1175 if self.base.command.callback.is_some() {
1176 self.base.command.invoke(ctx)?;
1177 }
1178
1179 let sub_ctx = cmd.make_context(&cmd_name, remaining, parent_arc)?;
1180
1181 let sub_ctx_arc = Arc::new(sub_ctx);
1182 push_context(Arc::clone(&sub_ctx_arc));
1183 let result = cmd.invoke(&sub_ctx_arc);
1184 pop_context();
1185 sub_ctx_arc.close();
1186
1187 if result.is_ok() {
1188 process_result(&self.base.result_callback, ctx, Vec::new())?;
1189 }
1190
1191 result
1192 } else {
1193 if self.base.command.callback.is_some() {
1194 self.base.command.invoke(ctx)?;
1195 }
1196
1197 let mut contexts: Vec<(Arc<Context>, &dyn CommandLike)> = Vec::new();
1198 let mut remaining_args = args;
1199
1200 while !remaining_args.is_empty() {
1201 let resolved = self.resolve_command(ctx, &remaining_args)?;
1202 match resolved {
1203 Some((cmd_name, cmd, rest)) => {
1204 let mut sub_ctx = ContextBuilder::new()
1205 .info_name(&cmd_name)
1206 .allow_extra_args(true)
1207 .allow_interspersed_args(false)
1208 .parent(
1209 parent_arc
1210 .clone()
1211 .unwrap_or_else(|| Arc::new(Context::default())),
1212 )
1213 .build();
1214
1215 if let Some(command) = cmd.as_any().downcast_ref::<Command>() {
1216 command.parse_args(&mut sub_ctx, rest)?;
1217 } else if let Some(group) = cmd.as_any().downcast_ref::<Group>() {
1218 sub_ctx = group.make_context(&cmd_name, rest, parent_arc.clone())?;
1219 } else if let Some(collection) =
1220 cmd.as_any().downcast_ref::<CommandCollection>()
1221 {
1222 sub_ctx =
1223 collection.make_context(&cmd_name, rest, parent_arc.clone())?;
1224 } else {
1225 sub_ctx = cmd.make_context(&cmd_name, rest, parent_arc.clone())?;
1226 }
1227
1228 remaining_args = sub_ctx.args().to_vec();
1229 contexts.push((Arc::new(sub_ctx), cmd));
1230 }
1231 None => {
1232 if !remaining_args.is_empty()
1233 && remaining_args[0].starts_with('-')
1234 && !ctx.resilient_parsing()
1235 {
1236 return Err(ClickError::usage(format!(
1237 "No such option: {}",
1238 remaining_args[0]
1239 )));
1240 }
1241 break;
1242 }
1243 }
1244 }
1245
1246 let mut results: Vec<Box<dyn Any + Send + Sync>> = Vec::new();
1247 for (sub_ctx_arc, cmd) in contexts {
1248 push_context(Arc::clone(&sub_ctx_arc));
1249 let result = cmd.invoke(&sub_ctx_arc);
1250 pop_context();
1251 sub_ctx_arc.close();
1252 result?;
1253 results.push(Box::new(()));
1254 }
1255
1256 process_result(&self.base.result_callback, ctx, results)?;
1257 Ok(())
1258 }
1259 }
1260
1261 #[allow(clippy::arc_with_non_send_sync)]
1262 fn main(&self, args: Vec<String>) -> Result<(), ClickError> {
1263 let prog_name = self.base.command.name.clone().unwrap_or_else(|| {
1264 std::env::args()
1265 .next()
1266 .unwrap_or_else(|| "program".to_string())
1267 });
1268
1269 let args_for_eager = args.clone();
1270
1271 let ctx_result = self.make_context(&prog_name, args, None);
1273
1274 match ctx_result {
1275 Ok(ctx) => {
1276 let ctx = Arc::new(ctx);
1277
1278 push_context(Arc::clone(&ctx));
1279 let result = self.invoke(&ctx);
1280 pop_context();
1281 ctx.close();
1282 result
1283 }
1284 Err(ClickError::Exit { code: 0 }) => {
1285 if let Some(version_output) = self
1287 .base
1288 .command
1289 .get_version_output_from_args(&args_for_eager)
1290 {
1291 println!("{}", version_output);
1292 return Ok(());
1293 }
1294
1295 let ctx = ContextBuilder::new().info_name(&prog_name).build();
1297 println!("{}", self.get_help(&ctx));
1298 Ok(())
1299 }
1300 Err(e) => Err(e),
1301 }
1302 }
1303
1304 fn get_help(&self, ctx: &Context) -> String {
1305 self.get_help_with_commands(ctx)
1306 }
1307
1308 fn get_short_help(&self) -> String {
1309 self.base.command.get_short_help()
1310 }
1311
1312 fn is_hidden(&self) -> bool {
1313 self.base.command.hidden
1314 }
1315
1316 fn get_usage(&self, ctx: &Context) -> String {
1317 self.get_usage_with_subcommand(ctx)
1318 }
1319
1320 fn as_any(&self) -> &dyn Any {
1321 self
1322 }
1323}
1324
1325pub struct CommandCollectionBuilder {
1327 base: GroupBuilder,
1328 sources: Vec<Group>,
1329}
1330
1331impl CommandCollectionBuilder {
1332 fn new(name: &str) -> Self {
1333 Self {
1334 base: GroupBuilder::new(name),
1335 sources: Vec::new(),
1336 }
1337 }
1338
1339 pub fn source(mut self, group: Group) -> Self {
1341 self.sources.push(group);
1342 self
1343 }
1344
1345 pub fn command(mut self, cmd: impl CommandLike + 'static) -> Self {
1347 self.base = self.base.command(cmd);
1348 self
1349 }
1350
1351 pub fn build(self) -> CommandCollection {
1353 CommandCollection {
1354 base: self.base.build(),
1355 sources: self.sources,
1356 }
1357 }
1358}
1359
1360pub struct GroupBuilder {
1387 name: String,
1388 callback: Option<CommandCallback>,
1389 options: Vec<ClickOption>,
1390 arguments: Vec<Argument>,
1391 help: Option<String>,
1392 epilog: Option<String>,
1393 short_help: Option<String>,
1394 hidden: bool,
1395 deprecated: Option<String>,
1396 commands: HashMap<String, Arc<dyn CommandLike>>,
1397 command_ids_by_name: HashMap<String, usize>,
1398 command_aliases_by_id: HashMap<usize, Vec<String>>,
1399 next_command_id: usize,
1400 chain: bool,
1401 invoke_without_command: bool,
1402 result_callback: Option<ResultCallback>,
1403 subcommand_required: Option<bool>,
1404 subcommand_metavar: Option<String>,
1405 add_help_option: bool,
1406 help_option: Option<ClickOption>,
1407 no_args_is_help: Option<bool>,
1408}
1409
1410impl GroupBuilder {
1411 fn new(name: &str) -> Self {
1413 Self {
1414 name: name.to_string(),
1415 callback: None,
1416 options: Vec::new(),
1417 arguments: Vec::new(),
1418 help: None,
1419 epilog: None,
1420 short_help: None,
1421 hidden: false,
1422 deprecated: None,
1423 commands: HashMap::new(),
1424 command_ids_by_name: HashMap::new(),
1425 command_aliases_by_id: HashMap::new(),
1426 next_command_id: 0,
1427 chain: false,
1428 invoke_without_command: false,
1429 result_callback: None,
1430 subcommand_required: None,
1431 subcommand_metavar: None,
1432 add_help_option: true,
1433 help_option: None,
1434 no_args_is_help: None,
1435 }
1436 }
1437
1438 pub fn callback<F>(mut self, f: F) -> Self
1447 where
1448 F: Fn(&Context) -> Result<(), ClickError> + Send + Sync + 'static,
1449 {
1450 self.callback = Some(Box::new(f));
1451 self
1452 }
1453
1454 pub fn option(mut self, opt: ClickOption) -> Self {
1456 self.options.push(opt);
1457 self
1458 }
1459
1460 pub fn argument(mut self, arg: Argument) -> Self {
1462 self.arguments.push(arg);
1463 self
1464 }
1465
1466 pub fn help(mut self, help: &str) -> Self {
1468 self.help = Some(help.to_string());
1469 self
1470 }
1471
1472 pub fn epilog(mut self, epilog: &str) -> Self {
1474 self.epilog = Some(epilog.to_string());
1475 self
1476 }
1477
1478 pub fn short_help(mut self, short_help: &str) -> Self {
1480 self.short_help = Some(short_help.to_string());
1481 self
1482 }
1483
1484 pub fn hidden(mut self) -> Self {
1486 self.hidden = true;
1487 self
1488 }
1489
1490 pub fn deprecated(mut self, message: &str) -> Self {
1492 self.deprecated = Some(message.to_string());
1493 self
1494 }
1495
1496 pub fn add_help_option(mut self, add: bool) -> Self {
1498 self.add_help_option = add;
1499 self
1500 }
1501
1502 pub fn help_option(mut self, opt: ClickOption) -> Self {
1506 self.add_help_option = true;
1507 self.help_option = Some(opt);
1508 self
1509 }
1510
1511 pub fn no_args_is_help(mut self, value: bool) -> Self {
1515 self.no_args_is_help = Some(value);
1516 self
1517 }
1518
1519 pub fn command(self, cmd: impl CommandLike + 'static) -> Self {
1537 self.command_shared(Arc::new(cmd))
1538 }
1539
1540 pub fn command_with_name(self, name: &str, cmd: impl CommandLike + 'static) -> Self {
1542 self.command_shared_with_name(name, Arc::new(cmd))
1543 }
1544
1545 pub fn command_shared(mut self, cmd: Arc<dyn CommandLike>) -> Self {
1549 let name = cmd.name().map(|s| s.to_string());
1550 if let Some(name) = name {
1551 self = self.command_shared_with_name(&name, cmd);
1552 }
1553 self
1554 }
1555
1556 pub fn command_shared_with_name(mut self, name: &str, cmd: Arc<dyn CommandLike>) -> Self {
1558 if let Some(old_id) = self.command_ids_by_name.get(name).copied() {
1560 if let Some(names) = self.command_aliases_by_id.get_mut(&old_id) {
1561 names.retain(|n| n != name);
1562 }
1563 }
1564
1565 let existing_id = self.commands.iter().find_map(|(n, existing)| {
1567 if Arc::ptr_eq(existing, &cmd) {
1568 self.command_ids_by_name.get(n).copied()
1569 } else {
1570 None
1571 }
1572 });
1573
1574 let id = existing_id.unwrap_or_else(|| {
1575 let id = self.next_command_id;
1576 self.next_command_id += 1;
1577 id
1578 });
1579
1580 self.command_ids_by_name.insert(name.to_string(), id);
1581 self.command_aliases_by_id
1582 .entry(id)
1583 .or_insert_with(Vec::new)
1584 .push(name.to_string());
1585
1586 if let Some(names) = self.command_aliases_by_id.get_mut(&id) {
1587 names.sort();
1588 names.dedup();
1589 }
1590
1591 self.commands.insert(name.to_string(), cmd);
1592 self
1593 }
1594
1595 pub fn chain(mut self, chain: bool) -> Self {
1600 self.chain = chain;
1601 if chain {
1602 self.subcommand_metavar =
1603 Some("COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]...".to_string());
1604 }
1605 self
1606 }
1607
1608 pub fn invoke_without_command(mut self, value: bool) -> Self {
1613 self.invoke_without_command = value;
1614 self
1615 }
1616
1617 pub fn subcommand_required(mut self, required: bool) -> Self {
1621 self.subcommand_required = Some(required);
1622 self
1623 }
1624
1625 pub fn subcommand_metavar(mut self, metavar: &str) -> Self {
1630 self.subcommand_metavar = Some(metavar.to_string());
1631 self
1632 }
1633
1634 pub fn result_callback<F>(mut self, f: F) -> Self
1636 where
1637 F: Fn(&Context, Vec<Box<dyn Any + Send + Sync>>) -> Result<(), ClickError>
1638 + Send
1639 + Sync
1640 + 'static,
1641 {
1642 self.result_callback = Some(Box::new(f));
1643 self
1644 }
1645
1646 pub fn build(self) -> Group {
1648 let no_args_is_help = self.no_args_is_help.unwrap_or(!self.invoke_without_command);
1650
1651 let subcommand_required = self
1653 .subcommand_required
1654 .unwrap_or(!self.invoke_without_command);
1655
1656 let subcommand_metavar = self.subcommand_metavar.unwrap_or_else(|| {
1658 if self.chain {
1659 "COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]...".to_string()
1660 } else {
1661 "COMMAND [ARGS]...".to_string()
1662 }
1663 });
1664
1665 let mut cmd_builder = CommandBuilder::new(&self.name)
1667 .allow_extra_args(true)
1668 .allow_interspersed_args(false)
1669 .add_help_option(self.add_help_option)
1670 .no_args_is_help(no_args_is_help);
1671
1672 if let Some(help_opt) = self.help_option {
1673 cmd_builder = cmd_builder.help_option(help_opt);
1674 }
1675
1676 for opt in self.options {
1678 cmd_builder = cmd_builder.option(opt);
1679 }
1680
1681 for arg in self.arguments {
1683 cmd_builder = cmd_builder.argument(arg);
1684 }
1685
1686 if let Some(help) = self.help {
1688 cmd_builder = cmd_builder.help(&help);
1689 }
1690 if let Some(epilog) = self.epilog {
1691 cmd_builder = cmd_builder.epilog(&epilog);
1692 }
1693 if let Some(short_help) = self.short_help {
1694 cmd_builder = cmd_builder.short_help(&short_help);
1695 }
1696 if self.hidden {
1697 cmd_builder = cmd_builder.hidden();
1698 }
1699 if let Some(deprecated) = self.deprecated {
1700 cmd_builder = cmd_builder.deprecated(&deprecated);
1701 }
1702 if let Some(callback) = self.callback {
1703 let callback_wrapper = move |ctx: &Context| callback(ctx);
1705 cmd_builder = cmd_builder.callback(callback_wrapper);
1706 }
1707
1708 let command = cmd_builder.build();
1709
1710 Group {
1711 command,
1712 commands: self.commands,
1713 command_ids_by_name: self.command_ids_by_name,
1714 command_aliases_by_id: self.command_aliases_by_id,
1715 next_command_id: self.next_command_id,
1716 chain: self.chain,
1717 invoke_without_command: self.invoke_without_command,
1718 result_callback: self.result_callback,
1719 subcommand_required,
1720 subcommand_metavar,
1721 }
1722 }
1723}
1724
1725#[cfg(test)]
1730mod tests {
1731 use super::*;
1732 use std::sync::atomic::{AtomicBool, Ordering};
1733
1734 #[test]
1735 fn test_group_creation_defaults() {
1736 let group = Group::new("test").build();
1737
1738 assert_eq!(group.name(), Some("test"));
1739 assert!(group.commands.is_empty());
1740 assert!(!group.chain);
1741 assert!(!group.invoke_without_command);
1742 assert!(group.subcommand_required);
1743 assert_eq!(group.subcommand_metavar, "COMMAND [ARGS]...");
1744 }
1745
1746 #[test]
1747 fn test_group_with_subcommands() {
1748 let group = Group::new("cli")
1749 .command(Command::new("init").help("Initialize").build())
1750 .command(Command::new("build").help("Build").build())
1751 .build();
1752
1753 assert_eq!(group.commands.len(), 2);
1754 assert!(group.get_command("init").is_some());
1755 assert!(group.get_command("build").is_some());
1756 assert!(group.get_command("unknown").is_none());
1757 }
1758
1759 #[test]
1760 fn test_list_commands_sorted() {
1761 let group = Group::new("cli")
1762 .command(Command::new("zebra").build())
1763 .command(Command::new("alpha").build())
1764 .command(Command::new("middle").build())
1765 .build();
1766
1767 let commands = group.list_commands();
1768 assert_eq!(commands, vec!["alpha", "middle", "zebra"]);
1769 }
1770
1771 #[test]
1772 fn test_add_command_with_name() {
1773 let mut group = Group::new("cli").build();
1774
1775 group.add_command(Command::new("original").build(), Some("renamed"));
1776
1777 assert!(group.get_command("renamed").is_some());
1778 assert!(group.get_command("original").is_none());
1779 }
1780
1781 #[test]
1782 fn test_alias_metadata_for_shared_command() {
1783 let cmd: Arc<dyn CommandLike> = Arc::new(Command::new("original").build());
1784
1785 let group = Group::new("cli")
1786 .command_shared(Arc::clone(&cmd))
1787 .command_shared_with_name("alias", Arc::clone(&cmd))
1788 .build();
1789
1790 assert!(group.get_command("original").is_some());
1791 assert!(group.get_command("alias").is_some());
1792
1793 assert_eq!(
1794 group.list_command_aliases("original"),
1795 vec!["alias".to_string()]
1796 );
1797 assert_eq!(
1798 group.list_command_aliases("alias"),
1799 vec!["original".to_string()]
1800 );
1801
1802 let entries = group.list_command_entries();
1803 let alias_entry = entries
1804 .iter()
1805 .find(|(name, _)| name == "alias")
1806 .expect("alias entry missing");
1807 assert_eq!(alias_entry.1.name(), Some("original"));
1808 }
1809
1810 #[test]
1811 fn test_group_chain_mode() {
1812 let group = Group::new("cli").chain(true).build();
1813
1814 assert!(group.chain);
1815 assert_eq!(
1816 group.subcommand_metavar,
1817 "COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]..."
1818 );
1819 }
1820
1821 #[test]
1822 fn test_invoke_without_command() {
1823 let called = Arc::new(AtomicBool::new(false));
1824 let called_clone = Arc::clone(&called);
1825
1826 let group = Group::new("cli")
1827 .invoke_without_command(true)
1828 .callback(move |_ctx| {
1829 called_clone.store(true, Ordering::SeqCst);
1830 Ok(())
1831 })
1832 .build();
1833
1834 assert!(!group.subcommand_required);
1836
1837 let ctx = ContextBuilder::new().info_name("cli").build();
1839
1840 let result = group.invoke(&ctx);
1842 assert!(result.is_ok());
1843 assert!(called.load(Ordering::SeqCst));
1844 }
1845
1846 #[test]
1847 fn test_group_help_formatting() {
1848 let group = Group::new("cli")
1849 .help("A sample CLI application")
1850 .command(
1851 Command::new("init")
1852 .short_help("Initialize the project")
1853 .build(),
1854 )
1855 .command(
1856 Command::new("build")
1857 .short_help("Build the project")
1858 .build(),
1859 )
1860 .build();
1861
1862 let ctx = ContextBuilder::new().info_name("cli").build();
1863 let help = group.get_help(&ctx);
1864
1865 assert!(help.contains("Usage:"));
1866 assert!(help.contains("cli"));
1867 assert!(help.contains("COMMAND [ARGS]..."));
1868 assert!(help.contains("A sample CLI application"));
1869 assert!(help.contains("Commands:"));
1870 assert!(help.contains("init"));
1871 assert!(help.contains("build"));
1872 }
1873
1874 #[test]
1875 fn test_resolve_command() {
1876 let group = Group::new("cli")
1877 .command(Command::new("hello").build())
1878 .command(Command::new("world").build())
1879 .build();
1880
1881 let ctx = ContextBuilder::new().info_name("cli").build();
1882
1883 let args = vec!["hello".to_string(), "arg1".to_string()];
1885 let resolved = group.resolve_command(&ctx, &args);
1886 assert!(resolved.is_ok());
1887
1888 let (name, _cmd, remaining) = resolved.unwrap().unwrap();
1889 assert_eq!(name, "hello");
1890 assert_eq!(remaining, vec!["arg1".to_string()]);
1891
1892 let args = vec!["unknown".to_string()];
1894 let resolved = group.resolve_command(&ctx, &args);
1895 assert!(resolved.is_err());
1896 }
1897
1898 #[test]
1899 fn test_resolve_command_empty_args() {
1900 let group = Group::new("cli")
1901 .command(Command::new("hello").build())
1902 .build();
1903
1904 let ctx = ContextBuilder::new().info_name("cli").build();
1905
1906 let resolved = group.resolve_command(&ctx, &[]);
1907 assert!(resolved.is_ok());
1908 assert!(resolved.unwrap().is_none());
1909 }
1910
1911 #[test]
1912 fn test_group_with_options() {
1913 let group = Group::new("cli")
1914 .help("A CLI with options")
1915 .option(
1916 ClickOption::new(&["--verbose", "-v"])
1917 .flag("true")
1918 .help("Enable verbose mode")
1919 .build(),
1920 )
1921 .command(Command::new("run").build())
1922 .build();
1923
1924 assert_eq!(group.command.options.len(), 1);
1925
1926 let ctx = ContextBuilder::new().info_name("cli").build();
1927 let help = group.get_help(&ctx);
1928
1929 assert!(help.contains("--verbose"));
1930 assert!(help.contains("Enable verbose mode"));
1931 }
1932
1933 #[test]
1934 fn test_hidden_commands_not_in_help() {
1935 let group = Group::new("cli")
1936 .command(Command::new("visible").build())
1937 .command(Command::new("hidden").hidden().build())
1938 .build();
1939
1940 let ctx = ContextBuilder::new().info_name("cli").build();
1941 let help = group.format_commands(&ctx);
1942
1943 assert!(help.contains("visible"));
1944 assert!(!help.contains("hidden"));
1945 }
1946
1947 #[test]
1948 fn test_subcommand_required_default() {
1949 let group1 = Group::new("cli").build();
1951 assert!(group1.subcommand_required);
1952
1953 let group2 = Group::new("cli").invoke_without_command(true).build();
1955 assert!(!group2.subcommand_required);
1956
1957 let group3 = Group::new("cli")
1959 .invoke_without_command(true)
1960 .subcommand_required(true)
1961 .build();
1962 assert!(group3.subcommand_required);
1963 }
1964
1965 #[test]
1966 fn test_group_short_help() {
1967 let group = Group::new("cli")
1968 .help("This is the long help text. It has multiple sentences.")
1969 .build();
1970
1971 let short = group.get_short_help();
1972 assert_eq!(short, "This is the long help text");
1973
1974 let group_explicit = Group::new("cli")
1975 .help("Long help")
1976 .short_help("Short help")
1977 .build();
1978
1979 let short = group_explicit.get_short_help();
1980 assert_eq!(short, "Short help");
1981 }
1982
1983 #[test]
1984 fn test_group_debug_format() {
1985 let group = Group::new("cli")
1986 .command(Command::new("a").build())
1987 .command(Command::new("b").build())
1988 .build();
1989
1990 let debug_str = format!("{:?}", group);
1991 assert!(debug_str.contains("Group"));
1992 assert!(debug_str.contains("2 subcommands"));
1993 }
1994
1995 #[test]
1996 fn test_nested_groups() {
1997 let sub_group = Group::new("sub")
1998 .help("Subgroup")
1999 .command(Command::new("cmd").build())
2000 .build();
2001
2002 let main_group = Group::new("main")
2003 .help("Main group")
2004 .command(sub_group)
2005 .build();
2006
2007 assert!(main_group.get_command("sub").is_some());
2008
2009 let sub = main_group.get_command("sub").unwrap();
2011 assert_eq!(sub.name(), Some("sub"));
2012 }
2013
2014 #[test]
2015 fn test_command_with_name_builder() {
2016 let group = Group::new("cli")
2017 .command_with_name("alias", Command::new("original").build())
2018 .build();
2019
2020 assert!(group.get_command("alias").is_some());
2021 assert!(group.get_command("original").is_none());
2022 }
2023
2024 #[test]
2025 fn test_missing_command_error() {
2026 let group = Group::new("cli").subcommand_required(true).build();
2027
2028 let ctx = ContextBuilder::new().info_name("cli").build();
2029
2030 let result = group.invoke(&ctx);
2032 assert!(result.is_err());
2033
2034 let err = result.unwrap_err();
2035 assert!(matches!(err, ClickError::UsageError { .. }));
2036 }
2037
2038 #[test]
2039 fn test_commandlike_trait() {
2040 let cmd: Box<dyn CommandLike> = Box::new(Command::new("cmd").build());
2042 let grp: Box<dyn CommandLike> = Box::new(Group::new("grp").build());
2043
2044 assert_eq!(cmd.name(), Some("cmd"));
2045 assert_eq!(grp.name(), Some("grp"));
2046
2047 assert!(!cmd.is_hidden());
2048 assert!(!grp.is_hidden());
2049 }
2050
2051 #[test]
2052 fn test_group_usage() {
2053 let group = Group::new("cli")
2054 .option(ClickOption::new(&["--debug"]).flag("true").build())
2055 .build();
2056
2057 let ctx = ContextBuilder::new().info_name("cli").build();
2058 let usage = group.get_usage(&ctx);
2059
2060 assert!(usage.contains("cli"));
2061 assert!(usage.contains("[OPTIONS]"));
2062 assert!(usage.contains("COMMAND [ARGS]..."));
2063 }
2064
2065 #[test]
2066 fn test_chain_metavar() {
2067 let group = Group::new("cli")
2068 .chain(true)
2069 .subcommand_metavar("CMD1 CMD2...")
2070 .build();
2071
2072 assert_eq!(group.subcommand_metavar, "CMD1 CMD2...");
2074 }
2075
2076 #[test]
2077 fn test_group_deprecated() {
2078 let group = Group::new("old")
2079 .help("Old group")
2080 .deprecated("Use 'new' instead")
2081 .build();
2082
2083 let short = group.get_short_help();
2084 assert!(short.contains("DEPRECATED"));
2085 assert!(short.contains("Use 'new' instead"));
2086 }
2087
2088 #[test]
2093 fn test_subcommand_context_inheritance() {
2094 let parent_info_name = Arc::new(std::sync::Mutex::new(String::new()));
2096 let parent_info_clone = Arc::clone(&parent_info_name);
2097
2098 let group = Group::new("cli")
2099 .command(
2100 Command::new("sub")
2101 .callback(move |ctx| {
2102 if let Some(parent) = ctx.parent() {
2104 let mut lock = parent_info_clone.lock().unwrap();
2105 if let Some(name) = parent.info_name() {
2106 *lock = name.to_string();
2107 }
2108 }
2109 Ok(())
2110 })
2111 .build(),
2112 )
2113 .build();
2114
2115 let result = group.main(vec!["sub".to_string()]);
2117 assert!(result.is_ok());
2118
2119 let captured = parent_info_name.lock().unwrap();
2121 assert_eq!(*captured, "cli");
2122 }
2123
2124 #[test]
2125 fn test_subcommand_inherits_terminal_settings() {
2126 let inherited_width = Arc::new(std::sync::Mutex::new(None::<usize>));
2128 let inherited_color = Arc::new(std::sync::Mutex::new(None::<bool>));
2129 let width_clone = Arc::clone(&inherited_width);
2130 let color_clone = Arc::clone(&inherited_color);
2131
2132 let group = Group::new("cli")
2133 .command(
2134 Command::new("sub")
2135 .callback(move |ctx| {
2136 *width_clone.lock().unwrap() = ctx.terminal_width();
2137 *color_clone.lock().unwrap() = ctx.color();
2138 Ok(())
2139 })
2140 .build(),
2141 )
2142 .build();
2143
2144 let parent_ctx = ContextBuilder::new()
2146 .info_name("cli")
2147 .terminal_width(120)
2148 .color(true)
2149 .allow_extra_args(true)
2150 .build();
2151 let _parent_ctx = Arc::new(parent_ctx);
2152
2153 let ctx = group
2155 .make_context("cli", vec!["sub".to_string()], None)
2156 .unwrap();
2157
2158 push_context(Arc::new(
2161 ContextBuilder::new()
2162 .info_name("cli")
2163 .terminal_width(120)
2164 .color(true)
2165 .allow_extra_args(true)
2166 .build(),
2167 ));
2168
2169 let result = group.invoke(&ctx);
2170 pop_context();
2171
2172 assert!(result.is_ok());
2173 }
2176
2177 #[test]
2178 fn test_chain_mode_multiple_commands() {
2179 let call_order = Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
2181 let order1 = Arc::clone(&call_order);
2182 let order2 = Arc::clone(&call_order);
2183 let order3 = Arc::clone(&call_order);
2184
2185 let group = Group::new("cli")
2186 .chain(true)
2187 .command(
2188 Command::new("cmd1")
2189 .callback(move |_ctx| {
2190 order1.lock().unwrap().push("cmd1".to_string());
2191 Ok(())
2192 })
2193 .build(),
2194 )
2195 .command(
2196 Command::new("cmd2")
2197 .callback(move |_ctx| {
2198 order2.lock().unwrap().push("cmd2".to_string());
2199 Ok(())
2200 })
2201 .build(),
2202 )
2203 .command(
2204 Command::new("cmd3")
2205 .callback(move |_ctx| {
2206 order3.lock().unwrap().push("cmd3".to_string());
2207 Ok(())
2208 })
2209 .build(),
2210 )
2211 .build();
2212
2213 let result = group.main(vec![
2215 "cmd1".to_string(),
2216 "cmd2".to_string(),
2217 "cmd3".to_string(),
2218 ]);
2219 assert!(result.is_ok());
2220
2221 let order = call_order.lock().unwrap();
2223 assert_eq!(*order, vec!["cmd1", "cmd2", "cmd3"]);
2224 }
2225
2226 #[test]
2227 fn test_chain_mode_with_args() {
2228 let captured_args = Arc::new(std::sync::Mutex::new(Vec::<Vec<String>>::new()));
2230 let args1 = Arc::clone(&captured_args);
2231 let args2 = Arc::clone(&captured_args);
2232
2233 let group = Group::new("cli")
2234 .chain(true)
2235 .command(
2236 Command::new("first")
2237 .callback(move |ctx| {
2238 args1.lock().unwrap().push(ctx.args().to_vec());
2239 Ok(())
2240 })
2241 .build(),
2242 )
2243 .command(
2244 Command::new("second")
2245 .callback(move |ctx| {
2246 args2.lock().unwrap().push(ctx.args().to_vec());
2247 Ok(())
2248 })
2249 .build(),
2250 )
2251 .build();
2252
2253 let result = group.main(vec!["first".to_string(), "second".to_string()]);
2255 assert!(result.is_ok());
2256
2257 let args = captured_args.lock().unwrap();
2258 assert_eq!(args.len(), 2);
2259 }
2260
2261 #[test]
2262 fn test_chain_mode_empty_returns_ok() {
2263 let called = Arc::new(AtomicBool::new(false));
2265 let called_clone = Arc::clone(&called);
2266
2267 let group = Group::new("cli")
2268 .chain(true)
2269 .invoke_without_command(true)
2270 .callback(move |_ctx| {
2271 called_clone.store(true, Ordering::SeqCst);
2272 Ok(())
2273 })
2274 .command(Command::new("sub").build())
2275 .build();
2276
2277 let result = group.main(vec![]);
2278 assert!(result.is_ok());
2279 assert!(called.load(Ordering::SeqCst));
2280 }
2281
2282 #[test]
2283 fn test_result_callback_invoked() {
2284 let result_callback_called = Arc::new(AtomicBool::new(false));
2286 let callback_clone = Arc::clone(&result_callback_called);
2287
2288 let group = Group::new("cli")
2289 .command(Command::new("sub").callback(|_ctx| Ok(())).build())
2290 .result_callback(move |_ctx, _results| {
2291 callback_clone.store(true, Ordering::SeqCst);
2292 Ok(())
2293 })
2294 .build();
2295
2296 let result = group.main(vec!["sub".to_string()]);
2297 assert!(result.is_ok());
2298 assert!(result_callback_called.load(Ordering::SeqCst));
2299 }
2300
2301 #[test]
2302 fn test_result_callback_with_chain_mode() {
2303 let result_callback_called = Arc::new(AtomicBool::new(false));
2305 let callback_clone = Arc::clone(&result_callback_called);
2306 let results_count = Arc::new(std::sync::Mutex::new(0usize));
2307 let count_clone = Arc::clone(&results_count);
2308
2309 let group = Group::new("cli")
2310 .chain(true)
2311 .command(Command::new("a").callback(|_| Ok(())).build())
2312 .command(Command::new("b").callback(|_| Ok(())).build())
2313 .result_callback(move |_ctx, results| {
2314 callback_clone.store(true, Ordering::SeqCst);
2315 *count_clone.lock().unwrap() = results.len();
2316 Ok(())
2317 })
2318 .build();
2319
2320 let result = group.main(vec!["a".to_string(), "b".to_string()]);
2321 assert!(result.is_ok());
2322 assert!(result_callback_called.load(Ordering::SeqCst));
2323
2324 let count = *results_count.lock().unwrap();
2326 assert_eq!(count, 2);
2327 }
2328
2329 #[test]
2330 fn test_result_callback_invoke_without_command() {
2331 let result_callback_called = Arc::new(AtomicBool::new(false));
2333 let callback_clone = Arc::clone(&result_callback_called);
2334
2335 let group = Group::new("cli")
2336 .invoke_without_command(true)
2337 .callback(|_ctx| Ok(()))
2338 .result_callback(move |_ctx, _results| {
2339 callback_clone.store(true, Ordering::SeqCst);
2340 Ok(())
2341 })
2342 .build();
2343
2344 let result = group.main(vec![]);
2345 assert!(result.is_ok());
2346 assert!(result_callback_called.load(Ordering::SeqCst));
2347 }
2348
2349 #[test]
2350 fn test_chain_mode_subcommand_failure_stops_chain() {
2351 let second_called = Arc::new(AtomicBool::new(false));
2353 let second_clone = Arc::clone(&second_called);
2354
2355 let group = Group::new("cli")
2356 .chain(true)
2357 .command(
2358 Command::new("fail")
2359 .callback(|_ctx| Err(ClickError::usage("intentional failure")))
2360 .build(),
2361 )
2362 .command(
2363 Command::new("second")
2364 .callback(move |_ctx| {
2365 second_clone.store(true, Ordering::SeqCst);
2366 Ok(())
2367 })
2368 .build(),
2369 )
2370 .build();
2371
2372 let result = group.main(vec!["fail".to_string(), "second".to_string()]);
2373 assert!(result.is_err());
2374 assert!(!second_called.load(Ordering::SeqCst));
2376 }
2377
2378 #[test]
2379 fn test_non_chain_mode_single_command() {
2380 let calls = Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
2382 let calls1 = Arc::clone(&calls);
2383
2384 let group = Group::new("cli")
2385 .chain(false) .command(
2387 Command::new("cmd1")
2388 .callback(move |_ctx| {
2389 calls1.lock().unwrap().push("cmd1".to_string());
2390 Ok(())
2391 })
2392 .build(),
2393 )
2394 .command(Command::new("cmd2").build())
2395 .build();
2396
2397 let result = group.main(vec!["cmd1".to_string()]);
2399 assert!(result.is_ok());
2400
2401 let recorded = calls.lock().unwrap();
2402 assert_eq!(recorded.len(), 1);
2403 assert_eq!(recorded[0], "cmd1");
2404 }
2405
2406 #[test]
2407 fn test_group_callback_called_before_subcommand() {
2408 let call_order = Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
2410 let order_group = Arc::clone(&call_order);
2411 let order_sub = Arc::clone(&call_order);
2412
2413 let group = Group::new("cli")
2414 .callback(move |_ctx| {
2415 order_group.lock().unwrap().push("group".to_string());
2416 Ok(())
2417 })
2418 .command(
2419 Command::new("sub")
2420 .callback(move |_ctx| {
2421 order_sub.lock().unwrap().push("sub".to_string());
2422 Ok(())
2423 })
2424 .build(),
2425 )
2426 .build();
2427
2428 let result = group.main(vec!["sub".to_string()]);
2429 assert!(result.is_ok());
2430
2431 let order = call_order.lock().unwrap();
2432 assert_eq!(*order, vec!["group", "sub"]);
2433 }
2434
2435 #[test]
2436 fn test_group_callback_called_before_chain() {
2437 let call_order = Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
2439 let order_group = Arc::clone(&call_order);
2440 let order_a = Arc::clone(&call_order);
2441 let order_b = Arc::clone(&call_order);
2442
2443 let group = Group::new("cli")
2444 .chain(true)
2445 .callback(move |_ctx| {
2446 order_group.lock().unwrap().push("group".to_string());
2447 Ok(())
2448 })
2449 .command(
2450 Command::new("a")
2451 .callback(move |_ctx| {
2452 order_a.lock().unwrap().push("a".to_string());
2453 Ok(())
2454 })
2455 .build(),
2456 )
2457 .command(
2458 Command::new("b")
2459 .callback(move |_ctx| {
2460 order_b.lock().unwrap().push("b".to_string());
2461 Ok(())
2462 })
2463 .build(),
2464 )
2465 .build();
2466
2467 let result = group.main(vec!["a".to_string(), "b".to_string()]);
2468 assert!(result.is_ok());
2469
2470 let order = call_order.lock().unwrap();
2471 assert_eq!(*order, vec!["group", "a", "b"]);
2472 }
2473
2474 #[test]
2475 fn test_command_collection_list_commands_union_sorted() {
2476 let src = Group::new("src")
2477 .command(Command::new("c").help("C").build())
2478 .command(Command::new("b").help("B").build())
2479 .build();
2480
2481 let collection = CommandCollection::new("coll")
2482 .command(Command::new("a").help("A").build())
2483 .source(src)
2484 .build();
2485
2486 assert_eq!(
2487 collection.list_commands(),
2488 vec!["a".to_string(), "b".to_string(), "c".to_string()]
2489 );
2490 }
2491
2492 #[test]
2493 fn test_command_collection_prefers_base_over_sources() {
2494 let src = Group::new("src")
2495 .command(Command::new("dup").help("Src").build())
2496 .build();
2497
2498 let collection = CommandCollection::new("coll")
2499 .command(Command::new("dup").help("Base").build())
2500 .source(src)
2501 .build();
2502
2503 let ctx = ContextBuilder::new().info_name("coll").build();
2504 let help = collection.get_help(&ctx);
2505 assert!(help.contains("dup"));
2506 assert_eq!(
2507 collection.get_command("dup").unwrap().get_short_help(),
2508 "Base"
2509 );
2510 }
2511
2512 #[test]
2517 fn test_group_help_with_missing_subcommand() {
2518 let group = Group::new("cli")
2520 .subcommand_required(true)
2521 .command(Command::new("sub").build())
2522 .build();
2523
2524 let _ctx = group.make_context("cli", vec![], None);
2526 let ctx = group.make_context("cli", vec!["--help".to_string()], None);
2531 assert!(matches!(ctx, Err(ClickError::Exit { code: 0 })));
2532 }
2533
2534 #[test]
2535 fn test_group_help_with_required_option() {
2536 let group = Group::new("cli")
2538 .option(
2539 ClickOption::new(&["--name", "-n"])
2540 .required()
2541 .build(),
2542 )
2543 .command(Command::new("sub").build())
2544 .build();
2545
2546 let ctx = group.make_context("cli", vec!["sub".to_string()], None);
2548 assert!(ctx.is_err());
2549
2550 let ctx = group.make_context("cli", vec!["--help".to_string()], None);
2552 assert!(matches!(ctx, Err(ClickError::Exit { code: 0 })));
2553 }
2554
2555 #[test]
2556 fn test_group_version_option() {
2557 use crate::option::ClickOption;
2558
2559 let version_opt = ClickOption::new(&["--version", "-V"])
2561 .flag("true")
2562 .eager()
2563 .metavar("__click_version__:myapp 1.0.0")
2564 .help("Show version and exit.")
2565 .build();
2566
2567 let group = Group::new("cli")
2568 .option(version_opt)
2569 .command(Command::new("sub").build())
2570 .build();
2571
2572 let ctx = group.make_context("cli", vec!["--version".to_string()], None);
2574 assert!(matches!(ctx, Err(ClickError::Exit { code: 0 })));
2575 }
2576}