Skip to main content

click/
group.rs

1//! Command groups for click-rs.
2//!
3//! This module provides the [`Group`] struct, which is a command that contains
4//! and dispatches to subcommands. Groups can be nested to create hierarchical
5//! command-line interfaces.
6//!
7//! # Reference
8//!
9//! Based on Python Click's `core.py:Group` class (line 1503+).
10//!
11//! # Example
12//!
13//! ```
14//! use click::group::{Group, CommandLike};
15//! use click::command::Command;
16//! use click::context::Context;
17//!
18//! let cli = Group::new("cli")
19//!     .help("A sample CLI application")
20//!     .command(
21//!         Command::new("init")
22//!             .help("Initialize the project")
23//!             .callback(|_ctx| {
24//!                 println!("Initializing...");
25//!                 Ok(())
26//!             })
27//!             .build()
28//!     )
29//!     .command(
30//!         Command::new("build")
31//!             .help("Build the project")
32//!             .callback(|_ctx| {
33//!                 println!("Building...");
34//!                 Ok(())
35//!             })
36//!             .build()
37//!     )
38//!     .build();
39//!
40//! assert_eq!(cli.list_commands().len(), 2);
41//! ```
42
43use 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
54// =============================================================================
55// CommandLike Trait
56// =============================================================================
57
58/// Shared interface for Command and Group.
59///
60/// This trait provides a common interface that both [`Command`] and [`Group`]
61/// implement, allowing them to be used interchangeably in many contexts.
62pub trait CommandLike: Send + Sync {
63    /// Get the name of this command.
64    fn name(&self) -> Option<&str>;
65
66    /// Create a context for executing this command.
67    ///
68    /// # Arguments
69    ///
70    /// * `info_name` - The name to display in help/usage
71    /// * `args` - The arguments to parse
72    /// * `parent` - Optional parent context for nested commands
73    fn make_context(
74        &self,
75        info_name: &str,
76        args: Vec<String>,
77        parent: Option<Arc<Context>>,
78    ) -> Result<Context, ClickError>;
79
80    /// Invoke the command with the given context.
81    fn invoke(&self, ctx: &Context) -> Result<(), ClickError>;
82
83    /// Main entry point - make context, parse args, and invoke.
84    fn main(&self, args: Vec<String>) -> Result<(), ClickError>;
85
86    /// Get the full help text for this command.
87    fn get_help(&self, ctx: &Context) -> String;
88
89    /// Get the short help text for command listings.
90    fn get_short_help(&self) -> String;
91
92    /// Check if this command is hidden from help output.
93    fn is_hidden(&self) -> bool;
94
95    /// Get the usage line for this command.
96    fn get_usage(&self, ctx: &Context) -> String;
97
98    /// Convert to Any for downcasting.
99    fn as_any(&self) -> &dyn Any;
100}
101
102// =============================================================================
103// CommandLike impl for Command
104// =============================================================================
105
106impl 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
149// =============================================================================
150// ResultCallback Type
151// =============================================================================
152
153/// Type for result callbacks that process subcommand return values.
154///
155/// The callback receives the context and a vector of return values from
156/// subcommand invocations. In chain mode, this will contain values from
157/// all chained commands; otherwise, it will contain a single value.
158pub type ResultCallback =
159    Box<dyn Fn(&Context, Vec<Box<dyn Any + Send + Sync>>) -> Result<(), ClickError> + Send + Sync>;
160
161// =============================================================================
162// Group Struct
163// =============================================================================
164
165/// A command group that contains and dispatches to subcommands.
166///
167/// Groups are the primary way to organize CLI applications with multiple
168/// commands. They can be nested to create complex command hierarchies.
169///
170/// # Features
171///
172/// - Subcommand registration and dispatch
173/// - Optional group callback (runs before subcommand)
174/// - Chain mode for executing multiple subcommands
175/// - Automatic help generation with command listing
176///
177/// # Example
178///
179/// ```
180/// use click::group::Group;
181/// use click::command::Command;
182///
183/// let cli = Group::new("myapp")
184///     .help("My application")
185///     .invoke_without_command(true)
186///     .callback(|_ctx| {
187///         println!("No subcommand provided");
188///         Ok(())
189///     })
190///     .command(
191///         Command::new("hello")
192///             .help("Say hello")
193///             .build()
194///     )
195///     .build();
196/// ```
197pub struct Group {
198    /// The underlying command (for the group's own options/arguments).
199    pub command: Command,
200
201    /// Registered subcommands (name -> Command or Group).
202    pub commands: HashMap<String, Arc<dyn CommandLike>>,
203
204    /// Alias metadata for commands registered through the Group/GroupBuilder APIs.
205    ///
206    /// This tracks which registered names (keys in `commands`) point at the same underlying
207    /// command object, so callers can distinguish:
208    /// - canonical command name (`command.name()`)
209    /// - registered name (key in the parent group)
210    /// - other aliases (other keys mapping to the same command)
211    command_ids_by_name: HashMap<String, usize>,
212    command_aliases_by_id: HashMap<usize, Vec<String>>,
213    next_command_id: usize,
214
215    /// Whether to execute multiple subcommands in sequence.
216    pub chain: bool,
217
218    /// Whether to invoke the group callback even if no subcommand is provided.
219    pub invoke_without_command: bool,
220
221    /// Optional callback to process subcommand results.
222    pub result_callback: Option<ResultCallback>,
223
224    /// Whether a subcommand is required (error if not provided).
225    ///
226    /// Defaults to `true` unless `invoke_without_command` is `true`.
227    pub subcommand_required: bool,
228
229    /// The metavar to show for subcommands in usage.
230    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    /// Create a new group builder with the given name.
268    ///
269    /// # Example
270    ///
271    /// ```
272    /// use click::group::Group;
273    ///
274    /// let group = Group::new("mygroup")
275    ///     .help("My command group")
276    ///     .build();
277    /// ```
278    #[allow(clippy::new_ret_no_self)]
279    pub fn new(name: &str) -> GroupBuilder {
280        GroupBuilder::new(name)
281    }
282
283    /// Add a subcommand to this group.
284    ///
285    /// If `name` is provided, it overrides the command's own name.
286    ///
287    /// # Example
288    ///
289    /// ```
290    /// use click::group::Group;
291    /// use click::command::Command;
292    ///
293    /// let mut group = Group::new("cli").build();
294    /// group.add_command(Command::new("hello").build(), None);
295    /// group.add_command(Command::new("greet").build(), Some("hi")); // registered as "hi"
296    ///
297    /// assert!(group.get_command("hello").is_some());
298    /// assert!(group.get_command("hi").is_some());
299    /// assert!(group.get_command("greet").is_none()); // not found by original name
300    /// ```
301    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    /// Add a subcommand to this group using a shared command object.
312    ///
313    /// This makes it possible to register the same command under multiple names (aliases)
314    /// while still being able to query alias metadata.
315    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 we're replacing an existing name, unlink it from prior alias metadata.
323        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        // Find an existing id for this command (if it's already registered under another name).
330        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        // Keep alias lists deterministic.
351        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    /// Get a subcommand by name.
360    ///
361    /// # Example
362    ///
363    /// ```
364    /// use click::group::Group;
365    /// use click::command::Command;
366    ///
367    /// let group = Group::new("cli")
368    ///     .command(Command::new("hello").build())
369    ///     .build();
370    ///
371    /// assert!(group.get_command("hello").is_some());
372    /// assert!(group.get_command("unknown").is_none());
373    /// ```
374    pub fn get_command(&self, name: &str) -> Option<&dyn CommandLike> {
375        self.commands.get(name).map(|c| c.as_ref())
376    }
377
378    /// List all command registrations as `(registered_name, command)` pairs.
379    ///
380    /// This is useful when you need both the registered key and the canonical name
381    /// stored inside the command itself (`command.name()`).
382    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    /// List other registered names (aliases) that map to the same command.
396    ///
397    /// Returns an empty list if the command is not found or if it has no aliases.
398    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    /// List all subcommand names (sorted alphabetically).
417    ///
418    /// # Example
419    ///
420    /// ```
421    /// use click::group::Group;
422    /// use click::command::Command;
423    ///
424    /// let group = Group::new("cli")
425    ///     .command(Command::new("build").build())
426    ///     .command(Command::new("init").build())
427    ///     .command(Command::new("deploy").build())
428    ///     .build();
429    ///
430    /// let commands = group.list_commands();
431    /// assert_eq!(commands, vec!["build", "deploy", "init"]);
432    /// ```
433    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    /// Resolve a command from arguments.
440    ///
441    /// Returns the command name, the command, and the remaining arguments.
442    /// Returns an error if the command is not found (unless in resilient parsing mode).
443    ///
444    /// # Arguments
445    ///
446    /// * `ctx` - The current context
447    /// * `args` - The arguments to parse
448    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        // Try to find the command
461        if let Some(cmd) = self.commands.get(cmd_name) {
462            // Find the key that matches (for returning &str with correct lifetime)
463            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        // Command not found
471        if ctx.resilient_parsing() {
472            return Ok(None);
473        }
474
475        // Check if the first arg looks like an option
476        if cmd_name.starts_with('-') {
477            // It's an option, not a command name - let the parser handle it
478            return Ok(None);
479        }
480
481        Err(ClickError::usage(format!(
482            "No such command '{}'.",
483            cmd_name
484        )))
485    }
486
487    /// Format the commands section for help output.
488    ///
489    /// Returns a formatted string listing all visible subcommands.
490    pub fn format_commands(&self, _ctx: &Context) -> String {
491        let mut lines = Vec::new();
492
493        // Get visible commands (not hidden)
494        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        // Calculate the max command name width
509        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    /// Get the usage line including the subcommand metavar.
533    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    /// Get help text including the subcommand listing.
539    fn get_help_with_commands(&self, ctx: &Context) -> String {
540        let mut parts = Vec::new();
541
542        // Usage line with subcommand metavar
543        parts.push(self.get_usage_with_subcommand(ctx));
544
545        // Help text
546        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        // Options section
564        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        // Commands section
587        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        // Epilog
594        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
603// =============================================================================
604// CommandLike impl for Group
605// =============================================================================
606
607impl 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        // Groups need to allow extra args for subcommand dispatch
619        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        // Parse the group's own arguments
631        self.command.parse_args(&mut ctx, args)?;
632
633        Ok(ctx)
634    }
635
636    fn invoke(&self, ctx: &Context) -> Result<(), ClickError> {
637        // Get the remaining args after parsing group options
638        let args = ctx.args().to_vec();
639
640        // Helper function to process results through result_callback
641        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        // Get the parent context Arc from the thread-local stack for proper inheritance
652        let parent_arc = get_current_context();
653
654        // Resolve first subcommand to check if there's any
655        let resolved = self.resolve_command(ctx, &args)?;
656
657        if resolved.is_none() {
658            // No subcommand provided
659            if self.invoke_without_command {
660                // Invoke group callback and process result
661                let group_result = self.command.invoke(ctx);
662                if group_result.is_ok() {
663                    // For invoke_without_command, result is empty list in chain mode
664                    // or the group's return value (which we don't capture) otherwise
665                    let results: Vec<Box<dyn Any + Send + Sync>> = if self.chain {
666                        Vec::new()
667                    } else {
668                        // In non-chain mode, we pass an empty result since Rust callbacks
669                        // return Result<(), ClickError> not arbitrary values
670                        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        // We have at least one subcommand
683        if !self.chain {
684            // Non-chain mode: invoke single subcommand
685            let (cmd_name, cmd, remaining) = resolved.unwrap();
686
687            // Note: Setting invoked_subcommand requires interior mutability in Context.
688            // The Context struct uses RefCell for close_callbacks but not for invoked_subcommand.
689            // For now, we skip setting it; a future refactor could add RefCell<Option<String>>.
690
691            // Invoke group callback first (if present)
692            if self.command.callback.is_some() {
693                self.command.invoke(ctx)?;
694            }
695
696            // Create subcommand context with proper parent inheritance.
697            // Exit{0} here means an eager option (--help) fired during the
698            // subcommand's own parsing: mirror Command::main and render THAT
699            // subcommand's help with its full command path, instead of letting
700            // the exit bubble up and terminate silently.
701            let sub_ctx = match cmd.make_context(cmd_name, remaining, parent_arc) {
702                Ok(sub_ctx) => sub_ctx,
703                Err(ClickError::Exit { code: 0 }) => {
704                    let help_ctx = ContextBuilder::new()
705                        .info_name(format!("{} {}", ctx.command_path(), cmd_name))
706                        .build();
707                    let help_text = if let Some(renderer) = ctx.help_renderer() {
708                        renderer(cmd, &help_ctx)
709                    } else {
710                        cmd.get_help(&help_ctx)
711                    };
712                    println!("{}", help_text);
713                    return Ok(());
714                }
715                Err(e) => return Err(e),
716            };
717
718            // Push and invoke subcommand
719            let sub_ctx_arc = Arc::new(sub_ctx);
720            push_context(Arc::clone(&sub_ctx_arc));
721            let result = cmd.invoke(&sub_ctx_arc);
722            pop_context();
723
724            // Close subcommand context
725            sub_ctx_arc.close();
726
727            // Process result callback if set
728            if result.is_ok() {
729                // In non-chain mode, result is a single value (empty since we don't capture return)
730                process_result(&self.result_callback, ctx, Vec::new())?;
731            }
732
733            result
734        } else {
735            // Chain mode: invoke multiple subcommands in sequence
736            // Note: In Python Click, invoked_subcommand is set to "*" in chain mode
737
738            // Invoke group callback first (if present)
739            if self.command.callback.is_some() {
740                self.command.invoke(ctx)?;
741            }
742
743            // Collect all subcommand contexts first (like Python Click does)
744            let mut contexts: Vec<(Arc<Context>, &dyn CommandLike)> = Vec::new();
745            let mut remaining_args = args;
746
747            while !remaining_args.is_empty() {
748                let resolved = self.resolve_command(ctx, &remaining_args)?;
749                match resolved {
750                    Some((cmd_name, cmd, rest)) => {
751                        // In chain mode, subcommands allow extra args and no interspersed args
752                        // so remaining tokens are passed to the next command resolution.
753                        // We create the context with chain mode settings, overriding the command's defaults.
754                        let mut sub_ctx = ContextBuilder::new()
755                            .info_name(cmd_name)
756                            .allow_extra_args(true) // Chain mode: allow extra args for next cmd
757                            .allow_interspersed_args(false) // Chain mode: no interspersed
758                            .parent(
759                                parent_arc
760                                    .clone()
761                                    .unwrap_or_else(|| Arc::new(Context::default())),
762                            )
763                            .build();
764
765                        // Parse args using the command (this populates the context).
766                        // Exit{0} = an eager --help fired for this chain member:
767                        // render its help (full path) instead of a silent exit.
768                        let parse_result =
769                            if let Some(command) = cmd.as_any().downcast_ref::<Command>() {
770                                command.parse_args(&mut sub_ctx, rest)
771                            } else if let Some(group) = cmd.as_any().downcast_ref::<Group>() {
772                                // For nested groups, use make_context which handles group-specific parsing
773                                group.make_context(cmd_name, rest, parent_arc.clone()).map(
774                                    |nested_ctx| {
775                                        sub_ctx = nested_ctx;
776                                    },
777                                )
778                            } else {
779                                // Fallback: use make_context (may error on extra args)
780                                cmd.make_context(cmd_name, rest, parent_arc.clone()).map(
781                                    |fallback_ctx| {
782                                        sub_ctx = fallback_ctx;
783                                    },
784                                )
785                            };
786                        match parse_result {
787                            Ok(()) => {}
788                            Err(ClickError::Exit { code: 0 }) => {
789                                let help_ctx = ContextBuilder::new()
790                                    .info_name(format!("{} {}", ctx.command_path(), cmd_name))
791                                    .build();
792                                let help_text = if let Some(renderer) = ctx.help_renderer() {
793                                    renderer(cmd, &help_ctx)
794                                } else {
795                                    cmd.get_help(&help_ctx)
796                                };
797                                println!("{}", help_text);
798                                return Ok(());
799                            }
800                            Err(e) => return Err(e),
801                        }
802
803                        // The subcommand's unparsed args become input for next command
804                        remaining_args = sub_ctx.args().to_vec();
805
806                        contexts.push((Arc::new(sub_ctx), cmd));
807                    }
808                    None => {
809                        // No more commands found - remaining args are truly extra
810                        // In chain mode, we don't error on extra args that look like commands
811                        // but couldn't be resolved. However, if they look like options,
812                        // that's an error (unless resilient_parsing)
813                        if !remaining_args.is_empty()
814                            && remaining_args[0].starts_with('-')
815                            && !ctx.resilient_parsing()
816                        {
817                            return Err(ClickError::usage(format!(
818                                "No such option: {}",
819                                remaining_args[0]
820                            )));
821                        }
822                        break;
823                    }
824                }
825            }
826
827            // Invoke all subcommands and collect results
828            let mut results: Vec<Box<dyn Any + Send + Sync>> = Vec::new();
829            for (sub_ctx_arc, cmd) in contexts {
830                push_context(Arc::clone(&sub_ctx_arc));
831                let result = cmd.invoke(&sub_ctx_arc);
832                pop_context();
833                sub_ctx_arc.close();
834
835                // If any subcommand fails, propagate the error
836                result?;
837
838                // We don't capture return values since Rust callbacks return Result<(), ClickError>
839                // In a more advanced implementation, callbacks could return arbitrary values
840                results.push(Box::new(()));
841            }
842
843            // Process result callback with collected results
844            process_result(&self.result_callback, ctx, results)?;
845
846            Ok(())
847        }
848    }
849
850    #[allow(clippy::arc_with_non_send_sync)]
851    fn main(&self, args: Vec<String>) -> Result<(), ClickError> {
852        let prog_name = self.command.name.clone().unwrap_or_else(|| {
853            std::env::args()
854                .next()
855                .unwrap_or_else(|| "program".to_string())
856        });
857
858        let args_for_eager = args.clone();
859
860        // Try to make context - this may fail early for --help or --version
861        let ctx_result = self.make_context(&prog_name, args, None);
862
863        match ctx_result {
864            Ok(ctx) => {
865                let ctx = Arc::new(ctx);
866
867                // Push context onto thread-local stack
868                push_context(Arc::clone(&ctx));
869
870                // Invoke the group
871                let result = self.invoke(&ctx);
872
873                // Pop context
874                pop_context();
875
876                // Run close callbacks
877                ctx.close();
878
879                result
880            }
881            Err(ClickError::Exit { code: 0 }) => {
882                // Help or version (or other eager exits) was requested.
883                //
884                // Version is implemented as an eager option that signals Exit(0) and stores the
885                // output string in the option metavar with a reserved prefix.
886                if let Some(version_output) =
887                    self.command.get_version_output_from_args(&args_for_eager)
888                {
889                    println!("{}", version_output);
890                    return Ok(());
891                }
892
893                // Default: print help.
894                // Create a minimal context for help formatting.
895                let ctx = ContextBuilder::new().info_name(&prog_name).build();
896                println!("{}", self.get_help(&ctx));
897                Ok(())
898            }
899            Err(e) => Err(e),
900        }
901    }
902
903    fn get_help(&self, ctx: &Context) -> String {
904        self.get_help_with_commands(ctx)
905    }
906
907    fn get_short_help(&self) -> String {
908        self.command.get_short_help()
909    }
910
911    fn is_hidden(&self) -> bool {
912        self.command.hidden
913    }
914
915    fn get_usage(&self, ctx: &Context) -> String {
916        self.get_usage_with_subcommand(ctx)
917    }
918
919    fn as_any(&self) -> &dyn Any {
920        self
921    }
922}
923
924// =============================================================================
925// CommandCollection
926// =============================================================================
927
928/// A group-like command that merges commands from multiple groups.
929///
930/// This is the click-rs equivalent of Python Click's `CommandCollection`.
931/// Commands are resolved by searching the base group first, then each source
932/// group in insertion order.
933///
934/// Only the base group's parameters (options/arguments/callback/help) are used.
935#[derive(Debug)]
936pub struct CommandCollection {
937    /// The base group providing parameters and help formatting.
938    pub base: Group,
939
940    /// Additional groups to source subcommands from.
941    pub sources: Vec<Group>,
942}
943
944impl CommandCollection {
945    /// Create a new `CommandCollection` builder with the given base name.
946    ///
947    /// The base group can register its own subcommands via `.command(...)`.
948    #[allow(clippy::new_ret_no_self)]
949    pub fn new(name: &str) -> CommandCollectionBuilder {
950        CommandCollectionBuilder::new(name)
951    }
952
953    /// Add a source group.
954    pub fn add_source(&mut self, group: Group) {
955        self.sources.push(group);
956    }
957
958    /// Get a subcommand by name, searching base first then sources.
959    pub fn get_command(&self, name: &str) -> Option<&dyn CommandLike> {
960        if let Some(cmd) = self.base.get_command(name) {
961            return Some(cmd);
962        }
963        for src in &self.sources {
964            if let Some(cmd) = src.get_command(name) {
965                return Some(cmd);
966            }
967        }
968        None
969    }
970
971    /// List all unique subcommand names from base + sources, sorted.
972    pub fn list_commands(&self) -> Vec<String> {
973        let mut names: std::collections::HashSet<String> =
974            self.base.commands.keys().cloned().collect();
975
976        for src in &self.sources {
977            for name in src.commands.keys() {
978                names.insert(name.clone());
979            }
980        }
981
982        let mut out: Vec<String> = names.into_iter().collect();
983        out.sort();
984        out
985    }
986
987    fn resolve_command<'a>(
988        &'a self,
989        ctx: &Context,
990        args: &[String],
991    ) -> Result<Option<(String, &'a dyn CommandLike, Vec<String>)>, ClickError> {
992        if args.is_empty() {
993            return Ok(None);
994        }
995
996        let cmd_name = &args[0];
997        let remaining = args[1..].to_vec();
998
999        if let Some(cmd) = self.base.commands.get(cmd_name) {
1000            return Ok(Some((cmd_name.clone(), cmd.as_ref(), remaining)));
1001        }
1002        for src in &self.sources {
1003            if let Some(cmd) = src.commands.get(cmd_name) {
1004                return Ok(Some((cmd_name.clone(), cmd.as_ref(), remaining)));
1005            }
1006        }
1007
1008        if ctx.resilient_parsing() {
1009            return Ok(None);
1010        }
1011        if cmd_name.starts_with('-') {
1012            return Ok(None);
1013        }
1014
1015        Err(ClickError::usage(format!(
1016            "No such command '{}'.",
1017            cmd_name
1018        )))
1019    }
1020
1021    fn format_commands(&self, _ctx: &Context) -> String {
1022        let mut visible_cmds: Vec<(String, &dyn CommandLike)> = self
1023            .list_commands()
1024            .into_iter()
1025            .filter_map(|name| {
1026                self.get_command(&name)
1027                    .filter(|cmd| !cmd.is_hidden())
1028                    .map(|cmd| (name, cmd))
1029            })
1030            .collect();
1031
1032        if visible_cmds.is_empty() {
1033            return String::new();
1034        }
1035
1036        visible_cmds.sort_by(|a, b| a.0.cmp(&b.0));
1037
1038        let max_width = visible_cmds
1039            .iter()
1040            .map(|(name, _)| name.len())
1041            .max()
1042            .unwrap_or(0);
1043
1044        let mut lines = Vec::new();
1045        lines.push("Commands:".to_string());
1046
1047        for (name, cmd) in visible_cmds {
1048            let help = cmd.get_short_help();
1049            let padding = max_width - name.len() + 2;
1050            lines.push(format!(
1051                "  {}{:padding$}{}",
1052                name,
1053                "",
1054                help,
1055                padding = padding
1056            ));
1057        }
1058
1059        lines.join("\n")
1060    }
1061
1062    fn get_usage_with_subcommand(&self, ctx: &Context) -> String {
1063        let base_usage = self.base.command.get_usage(ctx);
1064        format!("{} {}", base_usage, self.base.subcommand_metavar)
1065    }
1066
1067    fn get_help_with_commands(&self, ctx: &Context) -> String {
1068        let mut parts = Vec::new();
1069
1070        parts.push(self.get_usage_with_subcommand(ctx));
1071
1072        if let Some(ref help) = self.base.command.help {
1073            let text = help.lines().next().unwrap_or("");
1074            if !text.is_empty() {
1075                parts.push(String::new());
1076                let help_text = if let Some(ref dep) = self.base.command.deprecated {
1077                    if dep.is_empty() {
1078                        format!("{}  (DEPRECATED)", text)
1079                    } else {
1080                        format!("{}  (DEPRECATED: {})", text, dep)
1081                    }
1082                } else {
1083                    text.to_string()
1084                };
1085                parts.push(format!("  {}", help_text));
1086            }
1087        }
1088
1089        let opt_records: Vec<(String, String)> = self
1090            .base
1091            .command
1092            .options
1093            .iter()
1094            .filter_map(|opt| opt.get_help_record())
1095            .collect();
1096
1097        let help_opt = self.base.command.get_help_option(ctx);
1098        let help_record = help_opt.as_ref().and_then(|h| h.get_help_record());
1099
1100        if !opt_records.is_empty() || help_record.is_some() {
1101            parts.push(String::new());
1102            parts.push("Options:".to_string());
1103
1104            for (opt_str, help) in &opt_records {
1105                parts.push(format!("  {}  {}", opt_str, help));
1106            }
1107            if let Some((opt_str, help)) = help_record {
1108                parts.push(format!("  {}  {}", opt_str, help));
1109            }
1110        }
1111
1112        let commands_section = self.format_commands(ctx);
1113        if !commands_section.is_empty() {
1114            parts.push(String::new());
1115            parts.push(commands_section);
1116        }
1117
1118        if let Some(ref epilog) = self.base.command.epilog {
1119            parts.push(String::new());
1120            parts.push(epilog.clone());
1121        }
1122
1123        parts.join("\n")
1124    }
1125}
1126
1127impl CommandLike for CommandCollection {
1128    fn name(&self) -> Option<&str> {
1129        self.base.command.name.as_deref()
1130    }
1131
1132    fn make_context(
1133        &self,
1134        info_name: &str,
1135        args: Vec<String>,
1136        parent: Option<Arc<Context>>,
1137    ) -> Result<Context, ClickError> {
1138        let mut builder = ContextBuilder::new()
1139            .info_name(info_name)
1140            .allow_extra_args(true)
1141            .allow_interspersed_args(false);
1142
1143        if let Some(parent) = parent {
1144            builder = builder.parent(parent);
1145        }
1146
1147        let mut ctx = builder.build();
1148        self.base.command.parse_args(&mut ctx, args)?;
1149        Ok(ctx)
1150    }
1151
1152    fn invoke(&self, ctx: &Context) -> Result<(), ClickError> {
1153        let args = ctx.args().to_vec();
1154
1155        let process_result = |result_callback: &Option<ResultCallback>,
1156                              ctx: &Context,
1157                              results: Vec<Box<dyn Any + Send + Sync>>|
1158         -> Result<(), ClickError> {
1159            if let Some(ref callback) = result_callback {
1160                callback(ctx, results)?;
1161            }
1162            Ok(())
1163        };
1164
1165        let parent_arc = get_current_context();
1166        let resolved = self.resolve_command(ctx, &args)?;
1167
1168        if resolved.is_none() {
1169            if self.base.invoke_without_command {
1170                let group_result = self.base.command.invoke(ctx);
1171                if group_result.is_ok() {
1172                    process_result(&self.base.result_callback, ctx, Vec::new())?;
1173                }
1174                return group_result;
1175            } else if self.base.subcommand_required && !ctx.resilient_parsing() {
1176                return Err(ClickError::usage("Missing command."));
1177            } else {
1178                return Ok(());
1179            }
1180        }
1181
1182        if !self.base.chain {
1183            let (cmd_name, cmd, remaining) = resolved.unwrap();
1184
1185            if self.base.command.callback.is_some() {
1186                self.base.command.invoke(ctx)?;
1187            }
1188
1189            let sub_ctx = cmd.make_context(&cmd_name, remaining, parent_arc)?;
1190
1191            let sub_ctx_arc = Arc::new(sub_ctx);
1192            push_context(Arc::clone(&sub_ctx_arc));
1193            let result = cmd.invoke(&sub_ctx_arc);
1194            pop_context();
1195            sub_ctx_arc.close();
1196
1197            if result.is_ok() {
1198                process_result(&self.base.result_callback, ctx, Vec::new())?;
1199            }
1200
1201            result
1202        } else {
1203            if self.base.command.callback.is_some() {
1204                self.base.command.invoke(ctx)?;
1205            }
1206
1207            let mut contexts: Vec<(Arc<Context>, &dyn CommandLike)> = Vec::new();
1208            let mut remaining_args = args;
1209
1210            while !remaining_args.is_empty() {
1211                let resolved = self.resolve_command(ctx, &remaining_args)?;
1212                match resolved {
1213                    Some((cmd_name, cmd, rest)) => {
1214                        let mut sub_ctx = ContextBuilder::new()
1215                            .info_name(&cmd_name)
1216                            .allow_extra_args(true)
1217                            .allow_interspersed_args(false)
1218                            .parent(
1219                                parent_arc
1220                                    .clone()
1221                                    .unwrap_or_else(|| Arc::new(Context::default())),
1222                            )
1223                            .build();
1224
1225                        if let Some(command) = cmd.as_any().downcast_ref::<Command>() {
1226                            command.parse_args(&mut sub_ctx, rest)?;
1227                        } else if let Some(group) = cmd.as_any().downcast_ref::<Group>() {
1228                            sub_ctx = group.make_context(&cmd_name, rest, parent_arc.clone())?;
1229                        } else if let Some(collection) =
1230                            cmd.as_any().downcast_ref::<CommandCollection>()
1231                        {
1232                            sub_ctx =
1233                                collection.make_context(&cmd_name, rest, parent_arc.clone())?;
1234                        } else {
1235                            sub_ctx = cmd.make_context(&cmd_name, rest, parent_arc.clone())?;
1236                        }
1237
1238                        remaining_args = sub_ctx.args().to_vec();
1239                        contexts.push((Arc::new(sub_ctx), cmd));
1240                    }
1241                    None => {
1242                        if !remaining_args.is_empty()
1243                            && remaining_args[0].starts_with('-')
1244                            && !ctx.resilient_parsing()
1245                        {
1246                            return Err(ClickError::usage(format!(
1247                                "No such option: {}",
1248                                remaining_args[0]
1249                            )));
1250                        }
1251                        break;
1252                    }
1253                }
1254            }
1255
1256            let mut results: Vec<Box<dyn Any + Send + Sync>> = Vec::new();
1257            for (sub_ctx_arc, cmd) in contexts {
1258                push_context(Arc::clone(&sub_ctx_arc));
1259                let result = cmd.invoke(&sub_ctx_arc);
1260                pop_context();
1261                sub_ctx_arc.close();
1262                result?;
1263                results.push(Box::new(()));
1264            }
1265
1266            process_result(&self.base.result_callback, ctx, results)?;
1267            Ok(())
1268        }
1269    }
1270
1271    #[allow(clippy::arc_with_non_send_sync)]
1272    fn main(&self, args: Vec<String>) -> Result<(), ClickError> {
1273        let prog_name = self.base.command.name.clone().unwrap_or_else(|| {
1274            std::env::args()
1275                .next()
1276                .unwrap_or_else(|| "program".to_string())
1277        });
1278
1279        let args_for_eager = args.clone();
1280
1281        // Try to make context - this may fail early for --help or --version
1282        let ctx_result = self.make_context(&prog_name, args, None);
1283
1284        match ctx_result {
1285            Ok(ctx) => {
1286                let ctx = Arc::new(ctx);
1287
1288                push_context(Arc::clone(&ctx));
1289                let result = self.invoke(&ctx);
1290                pop_context();
1291                ctx.close();
1292                result
1293            }
1294            Err(ClickError::Exit { code: 0 }) => {
1295                // Help or version (or other eager exits) was requested.
1296                if let Some(version_output) = self
1297                    .base
1298                    .command
1299                    .get_version_output_from_args(&args_for_eager)
1300                {
1301                    println!("{}", version_output);
1302                    return Ok(());
1303                }
1304
1305                // Default: print help.
1306                let ctx = ContextBuilder::new().info_name(&prog_name).build();
1307                println!("{}", self.get_help(&ctx));
1308                Ok(())
1309            }
1310            Err(e) => Err(e),
1311        }
1312    }
1313
1314    fn get_help(&self, ctx: &Context) -> String {
1315        self.get_help_with_commands(ctx)
1316    }
1317
1318    fn get_short_help(&self) -> String {
1319        self.base.command.get_short_help()
1320    }
1321
1322    fn is_hidden(&self) -> bool {
1323        self.base.command.hidden
1324    }
1325
1326    fn get_usage(&self, ctx: &Context) -> String {
1327        self.get_usage_with_subcommand(ctx)
1328    }
1329
1330    fn as_any(&self) -> &dyn Any {
1331        self
1332    }
1333}
1334
1335/// Builder for [`CommandCollection`].
1336pub struct CommandCollectionBuilder {
1337    base: GroupBuilder,
1338    sources: Vec<Group>,
1339}
1340
1341impl CommandCollectionBuilder {
1342    fn new(name: &str) -> Self {
1343        Self {
1344            base: GroupBuilder::new(name),
1345            sources: Vec::new(),
1346        }
1347    }
1348
1349    /// Add a source group.
1350    pub fn source(mut self, group: Group) -> Self {
1351        self.sources.push(group);
1352        self
1353    }
1354
1355    /// Add a subcommand to the base group.
1356    pub fn command(mut self, cmd: impl CommandLike + 'static) -> Self {
1357        self.base = self.base.command(cmd);
1358        self
1359    }
1360
1361    /// Build the `CommandCollection`.
1362    pub fn build(self) -> CommandCollection {
1363        CommandCollection {
1364            base: self.base.build(),
1365            sources: self.sources,
1366        }
1367    }
1368}
1369
1370// =============================================================================
1371// GroupBuilder
1372// =============================================================================
1373
1374/// Builder for creating [`Group`] instances.
1375///
1376/// Use [`Group::new`] to create a builder, then chain methods to configure
1377/// the group, and finally call [`build`](GroupBuilder::build) to create
1378/// the group.
1379///
1380/// # Example
1381///
1382/// ```
1383/// use click::group::Group;
1384/// use click::command::Command;
1385///
1386/// let group = Group::new("cli")
1387///     .help("My CLI application")
1388///     .callback(|_ctx| {
1389///         println!("Group callback");
1390///         Ok(())
1391///     })
1392///     .invoke_without_command(true)
1393///     .command(Command::new("hello").build())
1394///     .build();
1395/// ```
1396pub struct GroupBuilder {
1397    name: String,
1398    callback: Option<CommandCallback>,
1399    options: Vec<ClickOption>,
1400    arguments: Vec<Argument>,
1401    help: Option<String>,
1402    epilog: Option<String>,
1403    short_help: Option<String>,
1404    hidden: bool,
1405    deprecated: Option<String>,
1406    commands: HashMap<String, Arc<dyn CommandLike>>,
1407    command_ids_by_name: HashMap<String, usize>,
1408    command_aliases_by_id: HashMap<usize, Vec<String>>,
1409    next_command_id: usize,
1410    chain: bool,
1411    invoke_without_command: bool,
1412    result_callback: Option<ResultCallback>,
1413    subcommand_required: Option<bool>,
1414    subcommand_metavar: Option<String>,
1415    add_help_option: bool,
1416    help_option: Option<ClickOption>,
1417    no_args_is_help: Option<bool>,
1418}
1419
1420impl GroupBuilder {
1421    /// Create a new group builder with the given name.
1422    fn new(name: &str) -> Self {
1423        Self {
1424            name: name.to_string(),
1425            callback: None,
1426            options: Vec::new(),
1427            arguments: Vec::new(),
1428            help: None,
1429            epilog: None,
1430            short_help: None,
1431            hidden: false,
1432            deprecated: None,
1433            commands: HashMap::new(),
1434            command_ids_by_name: HashMap::new(),
1435            command_aliases_by_id: HashMap::new(),
1436            next_command_id: 0,
1437            chain: false,
1438            invoke_without_command: false,
1439            result_callback: None,
1440            subcommand_required: None,
1441            subcommand_metavar: None,
1442            add_help_option: true,
1443            help_option: None,
1444            no_args_is_help: None,
1445        }
1446    }
1447
1448    // -------------------------------------------------------------------------
1449    // Inherited from Command
1450    // -------------------------------------------------------------------------
1451
1452    /// Set the callback function for this group.
1453    ///
1454    /// The callback is invoked before subcommand dispatch (or alone if
1455    /// `invoke_without_command` is true and no subcommand is given).
1456    pub fn callback<F>(mut self, f: F) -> Self
1457    where
1458        F: Fn(&Context) -> Result<(), ClickError> + Send + Sync + 'static,
1459    {
1460        self.callback = Some(Box::new(f));
1461        self
1462    }
1463
1464    /// Add an option to this group.
1465    pub fn option(mut self, opt: ClickOption) -> Self {
1466        self.options.push(opt);
1467        self
1468    }
1469
1470    /// Add an argument to this group.
1471    pub fn argument(mut self, arg: Argument) -> Self {
1472        self.arguments.push(arg);
1473        self
1474    }
1475
1476    /// Set the help text for this group.
1477    pub fn help(mut self, help: &str) -> Self {
1478        self.help = Some(help.to_string());
1479        self
1480    }
1481
1482    /// Set the epilog (text shown after help).
1483    pub fn epilog(mut self, epilog: &str) -> Self {
1484        self.epilog = Some(epilog.to_string());
1485        self
1486    }
1487
1488    /// Set the short help text for command listings.
1489    pub fn short_help(mut self, short_help: &str) -> Self {
1490        self.short_help = Some(short_help.to_string());
1491        self
1492    }
1493
1494    /// Hide this group from help output.
1495    pub fn hidden(mut self) -> Self {
1496        self.hidden = true;
1497        self
1498    }
1499
1500    /// Mark this group as deprecated.
1501    pub fn deprecated(mut self, message: &str) -> Self {
1502        self.deprecated = Some(message.to_string());
1503        self
1504    }
1505
1506    /// Set whether to add a --help option (default: true).
1507    pub fn add_help_option(mut self, add: bool) -> Self {
1508        self.add_help_option = add;
1509        self
1510    }
1511
1512    /// Override the automatically generated help option.
1513    ///
1514    /// Setting a custom help option implicitly enables `add_help_option`.
1515    pub fn help_option(mut self, opt: ClickOption) -> Self {
1516        self.add_help_option = true;
1517        self.help_option = Some(opt);
1518        self
1519    }
1520
1521    /// Set whether to show help if no args provided.
1522    ///
1523    /// Defaults to the opposite of `invoke_without_command`.
1524    pub fn no_args_is_help(mut self, value: bool) -> Self {
1525        self.no_args_is_help = Some(value);
1526        self
1527    }
1528
1529    // -------------------------------------------------------------------------
1530    // Group-specific
1531    // -------------------------------------------------------------------------
1532
1533    /// Add a subcommand to this group.
1534    ///
1535    /// # Example
1536    ///
1537    /// ```
1538    /// use click::group::Group;
1539    /// use click::command::Command;
1540    ///
1541    /// let group = Group::new("cli")
1542    ///     .command(Command::new("hello").build())
1543    ///     .command(Command::new("goodbye").build())
1544    ///     .build();
1545    /// ```
1546    pub fn command(self, cmd: impl CommandLike + 'static) -> Self {
1547        self.command_shared(Arc::new(cmd))
1548    }
1549
1550    /// Add a subcommand with a specific name (overriding the command's name).
1551    pub fn command_with_name(self, name: &str, cmd: impl CommandLike + 'static) -> Self {
1552        self.command_shared_with_name(name, Arc::new(cmd))
1553    }
1554
1555    /// Add a shared subcommand to this group.
1556    ///
1557    /// This makes it possible to register a single command under multiple names (aliases).
1558    pub fn command_shared(mut self, cmd: Arc<dyn CommandLike>) -> Self {
1559        let name = cmd.name().map(|s| s.to_string());
1560        if let Some(name) = name {
1561            self = self.command_shared_with_name(&name, cmd);
1562        }
1563        self
1564    }
1565
1566    /// Add a shared subcommand with a specific registered name.
1567    pub fn command_shared_with_name(mut self, name: &str, cmd: Arc<dyn CommandLike>) -> Self {
1568        // If we're replacing an existing name, unlink it from prior alias metadata.
1569        if let Some(old_id) = self.command_ids_by_name.get(name).copied() {
1570            if let Some(names) = self.command_aliases_by_id.get_mut(&old_id) {
1571                names.retain(|n| n != name);
1572            }
1573        }
1574
1575        // Find an existing id for this command (if it's already registered under another name).
1576        let existing_id = self.commands.iter().find_map(|(n, existing)| {
1577            if Arc::ptr_eq(existing, &cmd) {
1578                self.command_ids_by_name.get(n).copied()
1579            } else {
1580                None
1581            }
1582        });
1583
1584        let id = existing_id.unwrap_or_else(|| {
1585            let id = self.next_command_id;
1586            self.next_command_id += 1;
1587            id
1588        });
1589
1590        self.command_ids_by_name.insert(name.to_string(), id);
1591        self.command_aliases_by_id
1592            .entry(id)
1593            .or_insert_with(Vec::new)
1594            .push(name.to_string());
1595
1596        if let Some(names) = self.command_aliases_by_id.get_mut(&id) {
1597            names.sort();
1598            names.dedup();
1599        }
1600
1601        self.commands.insert(name.to_string(), cmd);
1602        self
1603    }
1604
1605    /// Enable or disable chain mode.
1606    ///
1607    /// In chain mode, multiple subcommands can be invoked in sequence:
1608    /// `cli cmd1 arg1 cmd2 arg2`
1609    pub fn chain(mut self, chain: bool) -> Self {
1610        self.chain = chain;
1611        if chain {
1612            self.subcommand_metavar =
1613                Some("COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]...".to_string());
1614        }
1615        self
1616    }
1617
1618    /// Set whether to invoke the group callback without a subcommand.
1619    ///
1620    /// If true, the group's callback is invoked even when no subcommand
1621    /// is provided.
1622    pub fn invoke_without_command(mut self, value: bool) -> Self {
1623        self.invoke_without_command = value;
1624        self
1625    }
1626
1627    /// Set whether a subcommand is required.
1628    ///
1629    /// If not explicitly set, defaults to the opposite of `invoke_without_command`.
1630    pub fn subcommand_required(mut self, required: bool) -> Self {
1631        self.subcommand_required = Some(required);
1632        self
1633    }
1634
1635    /// Set the metavar for subcommands in usage output.
1636    ///
1637    /// Default is "COMMAND [ARGS]..." (or "COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]..."
1638    /// in chain mode).
1639    pub fn subcommand_metavar(mut self, metavar: &str) -> Self {
1640        self.subcommand_metavar = Some(metavar.to_string());
1641        self
1642    }
1643
1644    /// Set the result callback for processing subcommand results.
1645    pub fn result_callback<F>(mut self, f: F) -> Self
1646    where
1647        F: Fn(&Context, Vec<Box<dyn Any + Send + Sync>>) -> Result<(), ClickError>
1648            + Send
1649            + Sync
1650            + 'static,
1651    {
1652        self.result_callback = Some(Box::new(f));
1653        self
1654    }
1655
1656    /// Build the group.
1657    pub fn build(self) -> Group {
1658        // Determine no_args_is_help default
1659        let no_args_is_help = self.no_args_is_help.unwrap_or(!self.invoke_without_command);
1660
1661        // Determine subcommand_required default
1662        let subcommand_required = self
1663            .subcommand_required
1664            .unwrap_or(!self.invoke_without_command);
1665
1666        // Determine subcommand_metavar
1667        let subcommand_metavar = self.subcommand_metavar.unwrap_or_else(|| {
1668            if self.chain {
1669                "COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]...".to_string()
1670            } else {
1671                "COMMAND [ARGS]...".to_string()
1672            }
1673        });
1674
1675        // Build the underlying command
1676        let mut cmd_builder = CommandBuilder::new(&self.name)
1677            .allow_extra_args(true)
1678            .allow_interspersed_args(false)
1679            .add_help_option(self.add_help_option)
1680            .no_args_is_help(no_args_is_help);
1681
1682        if let Some(help_opt) = self.help_option {
1683            cmd_builder = cmd_builder.help_option(help_opt);
1684        }
1685
1686        // Add options
1687        for opt in self.options {
1688            cmd_builder = cmd_builder.option(opt);
1689        }
1690
1691        // Add arguments
1692        for arg in self.arguments {
1693            cmd_builder = cmd_builder.argument(arg);
1694        }
1695
1696        // Set other properties
1697        if let Some(help) = self.help {
1698            cmd_builder = cmd_builder.help(&help);
1699        }
1700        if let Some(epilog) = self.epilog {
1701            cmd_builder = cmd_builder.epilog(&epilog);
1702        }
1703        if let Some(short_help) = self.short_help {
1704            cmd_builder = cmd_builder.short_help(&short_help);
1705        }
1706        if self.hidden {
1707            cmd_builder = cmd_builder.hidden();
1708        }
1709        if let Some(deprecated) = self.deprecated {
1710            cmd_builder = cmd_builder.deprecated(&deprecated);
1711        }
1712        if let Some(callback) = self.callback {
1713            // We need to wrap the callback
1714            let callback_wrapper = move |ctx: &Context| callback(ctx);
1715            cmd_builder = cmd_builder.callback(callback_wrapper);
1716        }
1717
1718        let command = cmd_builder.build();
1719
1720        Group {
1721            command,
1722            commands: self.commands,
1723            command_ids_by_name: self.command_ids_by_name,
1724            command_aliases_by_id: self.command_aliases_by_id,
1725            next_command_id: self.next_command_id,
1726            chain: self.chain,
1727            invoke_without_command: self.invoke_without_command,
1728            result_callback: self.result_callback,
1729            subcommand_required,
1730            subcommand_metavar,
1731        }
1732    }
1733}
1734
1735// =============================================================================
1736// Tests
1737// =============================================================================
1738
1739#[cfg(test)]
1740mod tests {
1741    use super::*;
1742    use std::sync::atomic::{AtomicBool, Ordering};
1743
1744    #[test]
1745    fn test_group_creation_defaults() {
1746        let group = Group::new("test").build();
1747
1748        assert_eq!(group.name(), Some("test"));
1749        assert!(group.commands.is_empty());
1750        assert!(!group.chain);
1751        assert!(!group.invoke_without_command);
1752        assert!(group.subcommand_required);
1753        assert_eq!(group.subcommand_metavar, "COMMAND [ARGS]...");
1754    }
1755
1756    #[test]
1757    fn test_group_with_subcommands() {
1758        let group = Group::new("cli")
1759            .command(Command::new("init").help("Initialize").build())
1760            .command(Command::new("build").help("Build").build())
1761            .build();
1762
1763        assert_eq!(group.commands.len(), 2);
1764        assert!(group.get_command("init").is_some());
1765        assert!(group.get_command("build").is_some());
1766        assert!(group.get_command("unknown").is_none());
1767    }
1768
1769    #[test]
1770    fn test_list_commands_sorted() {
1771        let group = Group::new("cli")
1772            .command(Command::new("zebra").build())
1773            .command(Command::new("alpha").build())
1774            .command(Command::new("middle").build())
1775            .build();
1776
1777        let commands = group.list_commands();
1778        assert_eq!(commands, vec!["alpha", "middle", "zebra"]);
1779    }
1780
1781    #[test]
1782    fn test_add_command_with_name() {
1783        let mut group = Group::new("cli").build();
1784
1785        group.add_command(Command::new("original").build(), Some("renamed"));
1786
1787        assert!(group.get_command("renamed").is_some());
1788        assert!(group.get_command("original").is_none());
1789    }
1790
1791    #[test]
1792    fn test_alias_metadata_for_shared_command() {
1793        let cmd: Arc<dyn CommandLike> = Arc::new(Command::new("original").build());
1794
1795        let group = Group::new("cli")
1796            .command_shared(Arc::clone(&cmd))
1797            .command_shared_with_name("alias", Arc::clone(&cmd))
1798            .build();
1799
1800        assert!(group.get_command("original").is_some());
1801        assert!(group.get_command("alias").is_some());
1802
1803        assert_eq!(
1804            group.list_command_aliases("original"),
1805            vec!["alias".to_string()]
1806        );
1807        assert_eq!(
1808            group.list_command_aliases("alias"),
1809            vec!["original".to_string()]
1810        );
1811
1812        let entries = group.list_command_entries();
1813        let alias_entry = entries
1814            .iter()
1815            .find(|(name, _)| name == "alias")
1816            .expect("alias entry missing");
1817        assert_eq!(alias_entry.1.name(), Some("original"));
1818    }
1819
1820    #[test]
1821    fn test_group_chain_mode() {
1822        let group = Group::new("cli").chain(true).build();
1823
1824        assert!(group.chain);
1825        assert_eq!(
1826            group.subcommand_metavar,
1827            "COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]..."
1828        );
1829    }
1830
1831    #[test]
1832    fn test_invoke_without_command() {
1833        let called = Arc::new(AtomicBool::new(false));
1834        let called_clone = Arc::clone(&called);
1835
1836        let group = Group::new("cli")
1837            .invoke_without_command(true)
1838            .callback(move |_ctx| {
1839                called_clone.store(true, Ordering::SeqCst);
1840                Ok(())
1841            })
1842            .build();
1843
1844        // invoke_without_command implies subcommand_required = false
1845        assert!(!group.subcommand_required);
1846
1847        // Create context with no subcommand
1848        let ctx = ContextBuilder::new().info_name("cli").build();
1849
1850        // Invoke should call the group callback
1851        let result = group.invoke(&ctx);
1852        assert!(result.is_ok());
1853        assert!(called.load(Ordering::SeqCst));
1854    }
1855
1856    #[test]
1857    fn test_group_help_formatting() {
1858        let group = Group::new("cli")
1859            .help("A sample CLI application")
1860            .command(
1861                Command::new("init")
1862                    .short_help("Initialize the project")
1863                    .build(),
1864            )
1865            .command(
1866                Command::new("build")
1867                    .short_help("Build the project")
1868                    .build(),
1869            )
1870            .build();
1871
1872        let ctx = ContextBuilder::new().info_name("cli").build();
1873        let help = group.get_help(&ctx);
1874
1875        assert!(help.contains("Usage:"));
1876        assert!(help.contains("cli"));
1877        assert!(help.contains("COMMAND [ARGS]..."));
1878        assert!(help.contains("A sample CLI application"));
1879        assert!(help.contains("Commands:"));
1880        assert!(help.contains("init"));
1881        assert!(help.contains("build"));
1882    }
1883
1884    #[test]
1885    fn test_resolve_command() {
1886        let group = Group::new("cli")
1887            .command(Command::new("hello").build())
1888            .command(Command::new("world").build())
1889            .build();
1890
1891        let ctx = ContextBuilder::new().info_name("cli").build();
1892
1893        // Resolve existing command
1894        let args = vec!["hello".to_string(), "arg1".to_string()];
1895        let resolved = group.resolve_command(&ctx, &args);
1896        assert!(resolved.is_ok());
1897
1898        let (name, _cmd, remaining) = resolved.unwrap().unwrap();
1899        assert_eq!(name, "hello");
1900        assert_eq!(remaining, vec!["arg1".to_string()]);
1901
1902        // Resolve non-existent command
1903        let args = vec!["unknown".to_string()];
1904        let resolved = group.resolve_command(&ctx, &args);
1905        assert!(resolved.is_err());
1906    }
1907
1908    #[test]
1909    fn test_resolve_command_empty_args() {
1910        let group = Group::new("cli")
1911            .command(Command::new("hello").build())
1912            .build();
1913
1914        let ctx = ContextBuilder::new().info_name("cli").build();
1915
1916        let resolved = group.resolve_command(&ctx, &[]);
1917        assert!(resolved.is_ok());
1918        assert!(resolved.unwrap().is_none());
1919    }
1920
1921    #[test]
1922    fn test_group_with_options() {
1923        let group = Group::new("cli")
1924            .help("A CLI with options")
1925            .option(
1926                ClickOption::new(&["--verbose", "-v"])
1927                    .flag("true")
1928                    .help("Enable verbose mode")
1929                    .build(),
1930            )
1931            .command(Command::new("run").build())
1932            .build();
1933
1934        assert_eq!(group.command.options.len(), 1);
1935
1936        let ctx = ContextBuilder::new().info_name("cli").build();
1937        let help = group.get_help(&ctx);
1938
1939        assert!(help.contains("--verbose"));
1940        assert!(help.contains("Enable verbose mode"));
1941    }
1942
1943    #[test]
1944    fn test_hidden_commands_not_in_help() {
1945        let group = Group::new("cli")
1946            .command(Command::new("visible").build())
1947            .command(Command::new("hidden").hidden().build())
1948            .build();
1949
1950        let ctx = ContextBuilder::new().info_name("cli").build();
1951        let help = group.format_commands(&ctx);
1952
1953        assert!(help.contains("visible"));
1954        assert!(!help.contains("hidden"));
1955    }
1956
1957    #[test]
1958    fn test_subcommand_required_default() {
1959        // Without invoke_without_command: subcommand_required = true
1960        let group1 = Group::new("cli").build();
1961        assert!(group1.subcommand_required);
1962
1963        // With invoke_without_command: subcommand_required = false
1964        let group2 = Group::new("cli").invoke_without_command(true).build();
1965        assert!(!group2.subcommand_required);
1966
1967        // Explicit override
1968        let group3 = Group::new("cli")
1969            .invoke_without_command(true)
1970            .subcommand_required(true)
1971            .build();
1972        assert!(group3.subcommand_required);
1973    }
1974
1975    #[test]
1976    fn test_group_short_help() {
1977        let group = Group::new("cli")
1978            .help("This is the long help text. It has multiple sentences.")
1979            .build();
1980
1981        let short = group.get_short_help();
1982        assert_eq!(short, "This is the long help text");
1983
1984        let group_explicit = Group::new("cli")
1985            .help("Long help")
1986            .short_help("Short help")
1987            .build();
1988
1989        let short = group_explicit.get_short_help();
1990        assert_eq!(short, "Short help");
1991    }
1992
1993    #[test]
1994    fn test_group_debug_format() {
1995        let group = Group::new("cli")
1996            .command(Command::new("a").build())
1997            .command(Command::new("b").build())
1998            .build();
1999
2000        let debug_str = format!("{:?}", group);
2001        assert!(debug_str.contains("Group"));
2002        assert!(debug_str.contains("2 subcommands"));
2003    }
2004
2005    #[test]
2006    fn test_nested_groups() {
2007        let sub_group = Group::new("sub")
2008            .help("Subgroup")
2009            .command(Command::new("cmd").build())
2010            .build();
2011
2012        let main_group = Group::new("main")
2013            .help("Main group")
2014            .command(sub_group)
2015            .build();
2016
2017        assert!(main_group.get_command("sub").is_some());
2018
2019        // Can get the nested command through the subgroup
2020        let sub = main_group.get_command("sub").unwrap();
2021        assert_eq!(sub.name(), Some("sub"));
2022    }
2023
2024    #[test]
2025    fn test_command_with_name_builder() {
2026        let group = Group::new("cli")
2027            .command_with_name("alias", Command::new("original").build())
2028            .build();
2029
2030        assert!(group.get_command("alias").is_some());
2031        assert!(group.get_command("original").is_none());
2032    }
2033
2034    #[test]
2035    fn test_missing_command_error() {
2036        let group = Group::new("cli").subcommand_required(true).build();
2037
2038        let ctx = ContextBuilder::new().info_name("cli").build();
2039
2040        // No args and subcommand required should error
2041        let result = group.invoke(&ctx);
2042        assert!(result.is_err());
2043
2044        let err = result.unwrap_err();
2045        assert!(matches!(err, ClickError::UsageError { .. }));
2046    }
2047
2048    #[test]
2049    fn test_commandlike_trait() {
2050        // Test that both Command and Group implement CommandLike
2051        let cmd: Box<dyn CommandLike> = Box::new(Command::new("cmd").build());
2052        let grp: Box<dyn CommandLike> = Box::new(Group::new("grp").build());
2053
2054        assert_eq!(cmd.name(), Some("cmd"));
2055        assert_eq!(grp.name(), Some("grp"));
2056
2057        assert!(!cmd.is_hidden());
2058        assert!(!grp.is_hidden());
2059    }
2060
2061    #[test]
2062    fn test_group_usage() {
2063        let group = Group::new("cli")
2064            .option(ClickOption::new(&["--debug"]).flag("true").build())
2065            .build();
2066
2067        let ctx = ContextBuilder::new().info_name("cli").build();
2068        let usage = group.get_usage(&ctx);
2069
2070        assert!(usage.contains("cli"));
2071        assert!(usage.contains("[OPTIONS]"));
2072        assert!(usage.contains("COMMAND [ARGS]..."));
2073    }
2074
2075    #[test]
2076    fn test_chain_metavar() {
2077        let group = Group::new("cli")
2078            .chain(true)
2079            .subcommand_metavar("CMD1 CMD2...")
2080            .build();
2081
2082        // Custom metavar should override chain default
2083        assert_eq!(group.subcommand_metavar, "CMD1 CMD2...");
2084    }
2085
2086    #[test]
2087    fn test_group_deprecated() {
2088        let group = Group::new("old")
2089            .help("Old group")
2090            .deprecated("Use 'new' instead")
2091            .build();
2092
2093        let short = group.get_short_help();
2094        assert!(short.contains("DEPRECATED"));
2095        assert!(short.contains("Use 'new' instead"));
2096    }
2097
2098    // =========================================================================
2099    // Tests for context inheritance, chain mode, and result_callback
2100    // =========================================================================
2101
2102    #[test]
2103    fn test_subcommand_context_inheritance() {
2104        // Test that subcommand context properly inherits from parent context
2105        let parent_info_name = Arc::new(std::sync::Mutex::new(String::new()));
2106        let parent_info_clone = Arc::clone(&parent_info_name);
2107
2108        let group = Group::new("cli")
2109            .command(
2110                Command::new("sub")
2111                    .callback(move |ctx| {
2112                        // Check that the parent context is accessible
2113                        if let Some(parent) = ctx.parent() {
2114                            let mut lock = parent_info_clone.lock().unwrap();
2115                            if let Some(name) = parent.info_name() {
2116                                *lock = name.to_string();
2117                            }
2118                        }
2119                        Ok(())
2120                    })
2121                    .build(),
2122            )
2123            .build();
2124
2125        // Use main() which sets up proper context stack
2126        let result = group.main(vec!["sub".to_string()]);
2127        assert!(result.is_ok());
2128
2129        // The subcommand should have seen "cli" as parent info_name
2130        let captured = parent_info_name.lock().unwrap();
2131        assert_eq!(*captured, "cli");
2132    }
2133
2134    #[test]
2135    fn test_subcommand_inherits_terminal_settings() {
2136        // Test that subcommand context inherits terminal_width and color settings
2137        let inherited_width = Arc::new(std::sync::Mutex::new(None::<usize>));
2138        let inherited_color = Arc::new(std::sync::Mutex::new(None::<bool>));
2139        let width_clone = Arc::clone(&inherited_width);
2140        let color_clone = Arc::clone(&inherited_color);
2141
2142        let group = Group::new("cli")
2143            .command(
2144                Command::new("sub")
2145                    .callback(move |ctx| {
2146                        *width_clone.lock().unwrap() = ctx.terminal_width();
2147                        *color_clone.lock().unwrap() = ctx.color();
2148                        Ok(())
2149                    })
2150                    .build(),
2151            )
2152            .build();
2153
2154        // Create parent context with specific settings
2155        let parent_ctx = ContextBuilder::new()
2156            .info_name("cli")
2157            .terminal_width(120)
2158            .color(true)
2159            .allow_extra_args(true)
2160            .build();
2161        let _parent_ctx = Arc::new(parent_ctx);
2162
2163        // Parse args through the group
2164        let ctx = group
2165            .make_context("cli", vec!["sub".to_string()], None)
2166            .unwrap();
2167
2168        // Manually set the inherited values (simulating what ContextBuilder does with parent)
2169        // In real usage, main() would set these up properly
2170        push_context(Arc::new(
2171            ContextBuilder::new()
2172                .info_name("cli")
2173                .terminal_width(120)
2174                .color(true)
2175                .allow_extra_args(true)
2176                .build(),
2177        ));
2178
2179        let result = group.invoke(&ctx);
2180        pop_context();
2181
2182        assert!(result.is_ok());
2183        // Note: The actual inheritance depends on Context implementation
2184        // This test verifies the invoke path doesn't break
2185    }
2186
2187    #[test]
2188    fn test_chain_mode_multiple_commands() {
2189        // Test that chain mode invokes multiple subcommands
2190        let call_order = Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
2191        let order1 = Arc::clone(&call_order);
2192        let order2 = Arc::clone(&call_order);
2193        let order3 = Arc::clone(&call_order);
2194
2195        let group = Group::new("cli")
2196            .chain(true)
2197            .command(
2198                Command::new("cmd1")
2199                    .callback(move |_ctx| {
2200                        order1.lock().unwrap().push("cmd1".to_string());
2201                        Ok(())
2202                    })
2203                    .build(),
2204            )
2205            .command(
2206                Command::new("cmd2")
2207                    .callback(move |_ctx| {
2208                        order2.lock().unwrap().push("cmd2".to_string());
2209                        Ok(())
2210                    })
2211                    .build(),
2212            )
2213            .command(
2214                Command::new("cmd3")
2215                    .callback(move |_ctx| {
2216                        order3.lock().unwrap().push("cmd3".to_string());
2217                        Ok(())
2218                    })
2219                    .build(),
2220            )
2221            .build();
2222
2223        // Invoke with multiple commands
2224        let result = group.main(vec![
2225            "cmd1".to_string(),
2226            "cmd2".to_string(),
2227            "cmd3".to_string(),
2228        ]);
2229        assert!(result.is_ok());
2230
2231        // All commands should have been called in order
2232        let order = call_order.lock().unwrap();
2233        assert_eq!(*order, vec!["cmd1", "cmd2", "cmd3"]);
2234    }
2235
2236    #[test]
2237    fn test_chain_mode_with_args() {
2238        // Test chain mode where commands have arguments
2239        let captured_args = Arc::new(std::sync::Mutex::new(Vec::<Vec<String>>::new()));
2240        let args1 = Arc::clone(&captured_args);
2241        let args2 = Arc::clone(&captured_args);
2242
2243        let group = Group::new("cli")
2244            .chain(true)
2245            .command(
2246                Command::new("first")
2247                    .callback(move |ctx| {
2248                        args1.lock().unwrap().push(ctx.args().to_vec());
2249                        Ok(())
2250                    })
2251                    .build(),
2252            )
2253            .command(
2254                Command::new("second")
2255                    .callback(move |ctx| {
2256                        args2.lock().unwrap().push(ctx.args().to_vec());
2257                        Ok(())
2258                    })
2259                    .build(),
2260            )
2261            .build();
2262
2263        // Both commands called without arguments to each
2264        let result = group.main(vec!["first".to_string(), "second".to_string()]);
2265        assert!(result.is_ok());
2266
2267        let args = captured_args.lock().unwrap();
2268        assert_eq!(args.len(), 2);
2269    }
2270
2271    #[test]
2272    fn test_chain_mode_empty_returns_ok() {
2273        // Test that chain mode with invoke_without_command returns ok with no commands
2274        let called = Arc::new(AtomicBool::new(false));
2275        let called_clone = Arc::clone(&called);
2276
2277        let group = Group::new("cli")
2278            .chain(true)
2279            .invoke_without_command(true)
2280            .callback(move |_ctx| {
2281                called_clone.store(true, Ordering::SeqCst);
2282                Ok(())
2283            })
2284            .command(Command::new("sub").build())
2285            .build();
2286
2287        let result = group.main(vec![]);
2288        assert!(result.is_ok());
2289        assert!(called.load(Ordering::SeqCst));
2290    }
2291
2292    #[test]
2293    fn test_result_callback_invoked() {
2294        // Test that result_callback is called after subcommand execution
2295        let result_callback_called = Arc::new(AtomicBool::new(false));
2296        let callback_clone = Arc::clone(&result_callback_called);
2297
2298        let group = Group::new("cli")
2299            .command(Command::new("sub").callback(|_ctx| Ok(())).build())
2300            .result_callback(move |_ctx, _results| {
2301                callback_clone.store(true, Ordering::SeqCst);
2302                Ok(())
2303            })
2304            .build();
2305
2306        let result = group.main(vec!["sub".to_string()]);
2307        assert!(result.is_ok());
2308        assert!(result_callback_called.load(Ordering::SeqCst));
2309    }
2310
2311    #[test]
2312    fn test_result_callback_with_chain_mode() {
2313        // Test that result_callback receives results from all chained commands
2314        let result_callback_called = Arc::new(AtomicBool::new(false));
2315        let callback_clone = Arc::clone(&result_callback_called);
2316        let results_count = Arc::new(std::sync::Mutex::new(0usize));
2317        let count_clone = Arc::clone(&results_count);
2318
2319        let group = Group::new("cli")
2320            .chain(true)
2321            .command(Command::new("a").callback(|_| Ok(())).build())
2322            .command(Command::new("b").callback(|_| Ok(())).build())
2323            .result_callback(move |_ctx, results| {
2324                callback_clone.store(true, Ordering::SeqCst);
2325                *count_clone.lock().unwrap() = results.len();
2326                Ok(())
2327            })
2328            .build();
2329
2330        let result = group.main(vec!["a".to_string(), "b".to_string()]);
2331        assert!(result.is_ok());
2332        assert!(result_callback_called.load(Ordering::SeqCst));
2333
2334        // Should have 2 results (one for each command)
2335        let count = *results_count.lock().unwrap();
2336        assert_eq!(count, 2);
2337    }
2338
2339    #[test]
2340    fn test_result_callback_invoke_without_command() {
2341        // Test that result_callback is called even when invoke_without_command is used
2342        let result_callback_called = Arc::new(AtomicBool::new(false));
2343        let callback_clone = Arc::clone(&result_callback_called);
2344
2345        let group = Group::new("cli")
2346            .invoke_without_command(true)
2347            .callback(|_ctx| Ok(()))
2348            .result_callback(move |_ctx, _results| {
2349                callback_clone.store(true, Ordering::SeqCst);
2350                Ok(())
2351            })
2352            .build();
2353
2354        let result = group.main(vec![]);
2355        assert!(result.is_ok());
2356        assert!(result_callback_called.load(Ordering::SeqCst));
2357    }
2358
2359    #[test]
2360    fn test_chain_mode_subcommand_failure_stops_chain() {
2361        // Test that if a subcommand fails, the chain stops
2362        let second_called = Arc::new(AtomicBool::new(false));
2363        let second_clone = Arc::clone(&second_called);
2364
2365        let group = Group::new("cli")
2366            .chain(true)
2367            .command(
2368                Command::new("fail")
2369                    .callback(|_ctx| Err(ClickError::usage("intentional failure")))
2370                    .build(),
2371            )
2372            .command(
2373                Command::new("second")
2374                    .callback(move |_ctx| {
2375                        second_clone.store(true, Ordering::SeqCst);
2376                        Ok(())
2377                    })
2378                    .build(),
2379            )
2380            .build();
2381
2382        let result = group.main(vec!["fail".to_string(), "second".to_string()]);
2383        assert!(result.is_err());
2384        // Second command should not have been called
2385        assert!(!second_called.load(Ordering::SeqCst));
2386    }
2387
2388    #[test]
2389    fn test_non_chain_mode_single_command() {
2390        // Test that non-chain mode only invokes one command
2391        let calls = Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
2392        let calls1 = Arc::clone(&calls);
2393
2394        let group = Group::new("cli")
2395            .chain(false) // explicitly not chain mode
2396            .command(
2397                Command::new("cmd1")
2398                    .callback(move |_ctx| {
2399                        calls1.lock().unwrap().push("cmd1".to_string());
2400                        Ok(())
2401                    })
2402                    .build(),
2403            )
2404            .command(Command::new("cmd2").build())
2405            .build();
2406
2407        // In non-chain mode, "cmd2" would be passed as arg to cmd1, not as separate command
2408        let result = group.main(vec!["cmd1".to_string()]);
2409        assert!(result.is_ok());
2410
2411        let recorded = calls.lock().unwrap();
2412        assert_eq!(recorded.len(), 1);
2413        assert_eq!(recorded[0], "cmd1");
2414    }
2415
2416    #[test]
2417    fn test_group_callback_called_before_subcommand() {
2418        // Test that group callback is called before subcommand in non-chain mode
2419        let call_order = Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
2420        let order_group = Arc::clone(&call_order);
2421        let order_sub = Arc::clone(&call_order);
2422
2423        let group = Group::new("cli")
2424            .callback(move |_ctx| {
2425                order_group.lock().unwrap().push("group".to_string());
2426                Ok(())
2427            })
2428            .command(
2429                Command::new("sub")
2430                    .callback(move |_ctx| {
2431                        order_sub.lock().unwrap().push("sub".to_string());
2432                        Ok(())
2433                    })
2434                    .build(),
2435            )
2436            .build();
2437
2438        let result = group.main(vec!["sub".to_string()]);
2439        assert!(result.is_ok());
2440
2441        let order = call_order.lock().unwrap();
2442        assert_eq!(*order, vec!["group", "sub"]);
2443    }
2444
2445    #[test]
2446    fn test_group_callback_called_before_chain() {
2447        // Test that group callback is called before chained subcommands
2448        let call_order = Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
2449        let order_group = Arc::clone(&call_order);
2450        let order_a = Arc::clone(&call_order);
2451        let order_b = Arc::clone(&call_order);
2452
2453        let group = Group::new("cli")
2454            .chain(true)
2455            .callback(move |_ctx| {
2456                order_group.lock().unwrap().push("group".to_string());
2457                Ok(())
2458            })
2459            .command(
2460                Command::new("a")
2461                    .callback(move |_ctx| {
2462                        order_a.lock().unwrap().push("a".to_string());
2463                        Ok(())
2464                    })
2465                    .build(),
2466            )
2467            .command(
2468                Command::new("b")
2469                    .callback(move |_ctx| {
2470                        order_b.lock().unwrap().push("b".to_string());
2471                        Ok(())
2472                    })
2473                    .build(),
2474            )
2475            .build();
2476
2477        let result = group.main(vec!["a".to_string(), "b".to_string()]);
2478        assert!(result.is_ok());
2479
2480        let order = call_order.lock().unwrap();
2481        assert_eq!(*order, vec!["group", "a", "b"]);
2482    }
2483
2484    #[test]
2485    fn test_command_collection_list_commands_union_sorted() {
2486        let src = Group::new("src")
2487            .command(Command::new("c").help("C").build())
2488            .command(Command::new("b").help("B").build())
2489            .build();
2490
2491        let collection = CommandCollection::new("coll")
2492            .command(Command::new("a").help("A").build())
2493            .source(src)
2494            .build();
2495
2496        assert_eq!(
2497            collection.list_commands(),
2498            vec!["a".to_string(), "b".to_string(), "c".to_string()]
2499        );
2500    }
2501
2502    #[test]
2503    fn test_command_collection_prefers_base_over_sources() {
2504        let src = Group::new("src")
2505            .command(Command::new("dup").help("Src").build())
2506            .build();
2507
2508        let collection = CommandCollection::new("coll")
2509            .command(Command::new("dup").help("Base").build())
2510            .source(src)
2511            .build();
2512
2513        let ctx = ContextBuilder::new().info_name("coll").build();
2514        let help = collection.get_help(&ctx);
2515        assert!(help.contains("dup"));
2516        assert_eq!(
2517            collection.get_command("dup").unwrap().get_short_help(),
2518            "Base"
2519        );
2520    }
2521
2522    // =========================================================================
2523    // Tests for eager option handling in Groups (--help, --version)
2524    // =========================================================================
2525
2526    #[test]
2527    fn test_group_help_with_missing_subcommand() {
2528        // --help should work even when subcommand is missing and required
2529        let group = Group::new("cli")
2530            .subcommand_required(true)
2531            .command(Command::new("sub").build())
2532            .build();
2533
2534        // Without --help, missing subcommand should fail
2535        let _ctx = group.make_context("cli", vec![], None);
2536        // Note: Group doesn't fail in make_context for missing subcommand,
2537        // it fails in invoke(). So this test verifies --help triggers early.
2538
2539        // With --help, should exit cleanly (Exit code 0)
2540        let ctx = group.make_context("cli", vec!["--help".to_string()], None);
2541        assert!(matches!(ctx, Err(ClickError::Exit { code: 0 })));
2542    }
2543
2544    #[test]
2545    fn test_group_help_with_required_option() {
2546        // --help should work even when a required option is missing
2547        let group = Group::new("cli")
2548            .option(ClickOption::new(&["--name", "-n"]).required().build())
2549            .command(Command::new("sub").build())
2550            .build();
2551
2552        // Without --help, missing required option should fail
2553        let ctx = group.make_context("cli", vec!["sub".to_string()], None);
2554        assert!(ctx.is_err());
2555
2556        // With --help, should exit cleanly (Exit code 0)
2557        let ctx = group.make_context("cli", vec!["--help".to_string()], None);
2558        assert!(matches!(ctx, Err(ClickError::Exit { code: 0 })));
2559    }
2560
2561    #[test]
2562    fn test_group_version_option() {
2563        use crate::option::ClickOption;
2564
2565        // Create a version option that uses the special metavar prefix
2566        let version_opt = ClickOption::new(&["--version", "-V"])
2567            .flag("true")
2568            .eager()
2569            .metavar("__click_version__:myapp 1.0.0")
2570            .help("Show version and exit.")
2571            .build();
2572
2573        let group = Group::new("cli")
2574            .option(version_opt)
2575            .command(Command::new("sub").build())
2576            .build();
2577
2578        // --version should trigger Exit(0)
2579        let ctx = group.make_context("cli", vec!["--version".to_string()], None);
2580        assert!(matches!(ctx, Err(ClickError::Exit { code: 0 })));
2581    }
2582
2583    // =========================================================================
2584    // Tests for pluggable help renderer invoked on subcommand --help
2585    // =========================================================================
2586
2587    #[test]
2588    fn test_custom_renderer_invoked_for_subcommand_help() {
2589        use crate::context::{ContextBuilder, HelpRenderer};
2590        use std::sync::Mutex;
2591
2592        // Record what the renderer was called with
2593        let captured_name: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
2594        let captured_clone = Arc::clone(&captured_name);
2595
2596        let renderer: HelpRenderer = Arc::new(move |_cmd, ctx| {
2597            let mut lock = captured_clone.lock().unwrap();
2598            *lock = Some(ctx.info_name().unwrap_or("").to_string());
2599            format!("RICH:{}", ctx.info_name().unwrap_or(""))
2600        });
2601
2602        let group = Group::new("cli")
2603            .command(Command::new("sub").help("Does something.").build())
2604            .build();
2605
2606        // Install renderer on a context, then use main() path through testing::CliRunner
2607        // Instead, simulate directly: make the root context with renderer, push it, invoke.
2608        let root_ctx = Arc::new(
2609            ContextBuilder::new()
2610                .info_name("cli")
2611                .allow_extra_args(true)
2612                .allow_interspersed_args(false)
2613                .help_renderer(renderer)
2614                .build(),
2615        );
2616
2617        // Parse args for the group (produces root_ctx with args = ["sub", "--help"])
2618        // We simulate the Group::invoke flow: build a ctx with remaining args.
2619        let mut ctx = ContextBuilder::new()
2620            .info_name("cli")
2621            .allow_extra_args(true)
2622            .allow_interspersed_args(false)
2623            .help_renderer(Arc::new(move |_cmd, ctx2| {
2624                format!("RICH2:{}", ctx2.info_name().unwrap_or(""))
2625            }))
2626            .build();
2627        // Inject the remaining args that would trigger subcommand --help
2628        ctx.args_mut().push("sub".to_string());
2629        ctx.args_mut().push("--help".to_string());
2630
2631        push_context(Arc::clone(&root_ctx));
2632        let result = group.invoke(&ctx);
2633        pop_context();
2634
2635        // Result is Ok because we handled Exit{0} in the renderer path
2636        assert!(result.is_ok());
2637    }
2638
2639    #[test]
2640    fn test_fallback_renderer_used_when_no_custom_renderer() {
2641        // Without a renderer, the plain get_help() fallback is used; result is Ok.
2642        let group = Group::new("cli")
2643            .command(Command::new("sub").help("Sub help text.").build())
2644            .build();
2645
2646        let mut ctx = ContextBuilder::new()
2647            .info_name("cli")
2648            .allow_extra_args(true)
2649            .allow_interspersed_args(false)
2650            .build();
2651        ctx.args_mut().push("sub".to_string());
2652        ctx.args_mut().push("--help".to_string());
2653
2654        let root_ctx = Arc::new(ContextBuilder::new().info_name("cli").build());
2655        push_context(Arc::clone(&root_ctx));
2656        let result = group.invoke(&ctx);
2657        pop_context();
2658
2659        assert!(result.is_ok());
2660    }
2661
2662    #[test]
2663    fn test_custom_renderer_invoked_for_chain_subcommand_help() {
2664        // Chain mode: verify renderer is called for --help on a chain member.
2665        use crate::context::{ContextBuilder, HelpRenderer};
2666        use std::sync::atomic::{AtomicBool, Ordering};
2667
2668        let renderer_called = Arc::new(AtomicBool::new(false));
2669        let called_clone = Arc::clone(&renderer_called);
2670
2671        let renderer: HelpRenderer = Arc::new(move |_cmd, _ctx| {
2672            called_clone.store(true, Ordering::SeqCst);
2673            "CHAIN_RICH".to_string()
2674        });
2675
2676        let group = Group::new("cli")
2677            .chain(true)
2678            .command(Command::new("step1").help("Step one.").build())
2679            .build();
2680
2681        let mut ctx = ContextBuilder::new()
2682            .info_name("cli")
2683            .allow_extra_args(true)
2684            .allow_interspersed_args(false)
2685            .help_renderer(renderer)
2686            .build();
2687        ctx.args_mut().push("step1".to_string());
2688        ctx.args_mut().push("--help".to_string());
2689
2690        let root_ctx = Arc::new(ContextBuilder::new().info_name("cli").build());
2691        push_context(Arc::clone(&root_ctx));
2692        let result = group.invoke(&ctx);
2693        pop_context();
2694
2695        assert!(result.is_ok());
2696        assert!(renderer_called.load(Ordering::SeqCst));
2697    }
2698}