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