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