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