Skip to main content

click/
command.rs

1//! Command building block for click-rs.
2//!
3//! This module provides the [`Command`] struct, which is the basic building block
4//! of command-line interfaces in click-rs. A command handles argument parsing
5//! and invokes a callback function with the parsed values.
6//!
7//! # Reference
8//!
9//! Based on Python Click's `core.py:Command` class (line 873+).
10//!
11//! # Example
12//!
13//! ```
14//! use click::command::Command;
15//! use click::context::Context;
16//! use click::option::ClickOption;
17//! use click::argument::Argument;
18//!
19//! let cmd = Command::new("greet")
20//!     .help("Greet someone by name")
21//!     .option(
22//!         ClickOption::new(&["--name", "-n"])
23//!             .help("Name to greet")
24//!             .default("World")
25//!             .build()
26//!     )
27//!     .argument(
28//!         Argument::new("count")
29//!             .help("Number of times to greet")
30//!             .build()
31//!     )
32//!     .callback(|ctx| {
33//!         // Access parsed values from ctx.params()
34//!         Ok(())
35//!     })
36//!     .build();
37//!
38//! assert_eq!(cmd.name, Some("greet".to_string()));
39//! assert!(!cmd.hidden);
40//! ```
41
42use std::collections::HashMap;
43use std::sync::Arc;
44
45use crate::argument::Argument;
46use crate::context::{push_context, pop_context, Context, ContextBuilder};
47use crate::error::{ClickError, ErrorContext};
48use crate::option::ClickOption;
49use crate::parameter::{Nargs, Parameter};
50use crate::parser::{OptionAction, OptionParser, ParsedValue, NARGS_OPTIONAL};
51use crate::source::ParameterSource;
52use crate::termui;
53
54// =============================================================================
55// CommandCallback Type
56// =============================================================================
57
58/// Type for command callbacks.
59///
60/// The callback receives a reference to the [`Context`] containing parsed
61/// parameter values and returns a `Result` indicating success or failure.
62pub type CommandCallback = Box<dyn Fn(&Context) -> Result<(), ClickError> + Send + Sync>;
63
64// =============================================================================
65// Command Struct
66// =============================================================================
67
68/// A command is the basic building block of click-rs CLIs.
69///
70/// Commands handle argument parsing and invoke a callback with the parsed values.
71/// They can be nested within [`Group`]s to create subcommand hierarchies.
72///
73/// # Construction
74///
75/// Use [`Command::new`] to create a [`CommandBuilder`] for fluent construction:
76///
77/// ```
78/// use click::command::Command;
79///
80/// let cmd = Command::new("mycommand")
81///     .help("Description of the command")
82///     .build();
83/// ```
84pub struct Command {
85    /// The name of the command.
86    ///
87    /// When registered on a [`Group`], this becomes the subcommand name.
88    /// Use [`Context::info_name`] to get the actual invocation name.
89    pub name: Option<String>,
90
91    /// The callback to execute when the command is invoked.
92    ///
93    /// May be `None` if the command is just a container for subcommands.
94    pub callback: Option<CommandCallback>,
95
96    /// The options for this command.
97    pub options: Vec<ClickOption>,
98
99    /// The arguments for this command.
100    pub arguments: Vec<Argument>,
101
102    /// The help text for this command.
103    ///
104    /// Displayed in the help output after the usage line.
105    pub help: Option<String>,
106
107    /// Text printed at the end of the help page.
108    pub epilog: Option<String>,
109
110    /// Short help shown in parent command listings.
111    ///
112    /// If not set, derived from the first line of `help`.
113    pub short_help: Option<String>,
114
115    /// The metavar for options (default: "[OPTIONS]").
116    pub options_metavar: String,
117
118    /// Whether to add a --help option automatically.
119    pub add_help_option: bool,
120
121    /// Show help if no arguments are provided.
122    pub no_args_is_help: bool,
123
124    /// Hide this command from help output.
125    pub hidden: bool,
126
127    /// Mark this command as deprecated.
128    ///
129    /// If `Some`, the command shows a deprecation warning when invoked.
130    /// The string can be empty for a generic message, or contain custom text.
131    pub deprecated: Option<String>,
132
133    /// Whether to allow extra arguments (default: false).
134    pub allow_extra_args: bool,
135
136    /// Whether to allow interspersed arguments (default: true).
137    pub allow_interspersed_args: bool,
138
139    /// Whether to ignore unknown options (default: false).
140    pub ignore_unknown_options: bool,
141
142    /// Cached help option (created lazily).
143    help_option: Option<ClickOption>,
144}
145
146impl std::fmt::Debug for Command {
147    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148        f.debug_struct("Command")
149            .field("name", &self.name)
150            .field("options", &format!("<{} options>", self.options.len()))
151            .field("arguments", &format!("<{} arguments>", self.arguments.len()))
152            .field("help", &self.help)
153            .field("epilog", &self.epilog)
154            .field("short_help", &self.short_help)
155            .field("options_metavar", &self.options_metavar)
156            .field("add_help_option", &self.add_help_option)
157            .field("no_args_is_help", &self.no_args_is_help)
158            .field("hidden", &self.hidden)
159            .field("deprecated", &self.deprecated)
160            .field("allow_extra_args", &self.allow_extra_args)
161            .field("allow_interspersed_args", &self.allow_interspersed_args)
162            .field("ignore_unknown_options", &self.ignore_unknown_options)
163            .finish()
164    }
165}
166
167impl Default for Command {
168    fn default() -> Self {
169        Self {
170            name: None,
171            callback: None,
172            options: Vec::new(),
173            arguments: Vec::new(),
174            help: None,
175            epilog: None,
176            short_help: None,
177            options_metavar: "[OPTIONS]".to_string(),
178            add_help_option: true,
179            no_args_is_help: false,
180            hidden: false,
181            deprecated: None,
182            allow_extra_args: false,
183            allow_interspersed_args: true,
184            ignore_unknown_options: false,
185            help_option: None,
186        }
187    }
188}
189
190impl Command {
191    const VERSION_METAVAR_PREFIX: &'static str = "__click_version__:";
192
193    /// Create a new command builder with the given name.
194    ///
195    /// # Example
196    ///
197    /// ```
198    /// use click::command::Command;
199    ///
200    /// let cmd = Command::new("hello")
201    ///     .help("Say hello")
202    ///     .build();
203    /// ```
204    #[allow(clippy::new_ret_no_self)]
205    pub fn new(name: &str) -> CommandBuilder {
206        CommandBuilder::new(name)
207    }
208
209    /// Get the help option if enabled.
210    ///
211    /// Returns `None` if `add_help_option` is false or if the help option
212    /// names conflict with existing parameters.
213    pub fn get_help_option(&self, ctx: &Context) -> Option<ClickOption> {
214        if !self.add_help_option {
215            return None;
216        }
217
218        let help_names = self.get_help_option_names(ctx);
219        if help_names.is_empty() {
220            return None;
221        }
222
223        // If we have a cached help option, return a clone
224        if let Some(ref help_opt) = self.help_option {
225            return Some(help_opt.clone());
226        }
227
228        // Create the help option
229        Some(make_help_option(&help_names))
230    }
231
232    /// Get the names available for the help option.
233    ///
234    /// Filters out any names that are already used by existing parameters.
235    fn get_help_option_names(&self, ctx: &Context) -> Vec<String> {
236        let mut all_names: std::collections::HashSet<String> =
237            ctx.help_option_names().iter().cloned().collect();
238
239        // Remove names used by existing options
240        for opt in &self.options {
241            for name in &opt.long {
242                all_names.remove(name);
243            }
244            for name in &opt.short {
245                all_names.remove(name);
246            }
247        }
248
249        all_names.into_iter().collect()
250    }
251
252    /// Get the total number of parameters (options + arguments).
253    pub fn param_count(&self) -> usize {
254        self.options.len() + self.arguments.len()
255    }
256
257    /// Get all parameters (options and arguments) for this command.
258    ///
259    /// Returns a vector of trait objects. Options are returned first,
260    /// followed by arguments, in the order they were added.
261    pub fn get_params(&self, _ctx: &Context) -> Vec<&dyn Parameter> {
262        let mut params: Vec<&dyn Parameter> = Vec::with_capacity(self.param_count());
263        for opt in &self.options {
264            params.push(opt);
265        }
266        for arg in &self.arguments {
267            params.push(arg);
268        }
269        params
270    }
271
272    /// Create a context for this command.
273    ///
274    /// This sets up parsing state and applies command defaults.
275    ///
276    /// # Arguments
277    ///
278    /// * `info_name` - The name to display in help/usage (usually the command name)
279    /// * `args` - The arguments to parse
280    /// * `parent` - Optional parent context for nested commands
281    ///
282    /// # Example
283    ///
284    /// ```
285    /// use click::command::Command;
286    ///
287    /// let cmd = Command::new("hello").build();
288    /// let ctx = cmd.make_context("hello", vec![], None).unwrap();
289    /// assert_eq!(ctx.info_name(), Some("hello"));
290    /// ```
291    pub fn make_context(
292        &self,
293        info_name: &str,
294        args: Vec<String>,
295        parent: Option<Arc<Context>>,
296    ) -> Result<Context, ClickError> {
297        let mut builder = ContextBuilder::new()
298            .info_name(info_name)
299            .allow_extra_args(self.allow_extra_args)
300            .allow_interspersed_args(self.allow_interspersed_args)
301            .ignore_unknown_options(self.ignore_unknown_options);
302
303        if let Some(parent) = parent {
304            builder = builder.parent(parent);
305        }
306
307        let mut ctx = builder.build();
308
309        // Parse arguments and populate context
310        self.parse_args(&mut ctx, args)?;
311
312        Ok(ctx)
313    }
314
315    /// Parse arguments and populate the context.
316    ///
317    /// This method creates a parser, adds all parameters to it, parses the
318    /// arguments, and stores the results in the context.
319    pub fn parse_args(&self, ctx: &mut Context, args: Vec<String>) -> Result<(), ClickError> {
320        // Check for no_args_is_help
321        if args.is_empty() && self.no_args_is_help && !ctx.resilient_parsing() {
322            // Return help instead of an error
323            return Err(ClickError::Exit { code: 0 });
324        }
325
326        // Create the parser
327        let mut parser = OptionParser::new()
328            .allow_interspersed_args(ctx.allow_interspersed_args())
329            .ignore_unknown_options(ctx.ignore_unknown_options());
330
331        // Add help option if enabled
332        let help_opt = self.get_help_option(ctx);
333
334        // Add options to parser
335        for opt in &self.options {
336            self.add_option_to_parser(&mut parser, opt);
337        }
338        if let Some(ref help) = help_opt {
339            self.add_option_to_parser(&mut parser, help);
340        }
341
342        // Add arguments to parser
343        for arg in &self.arguments {
344            self.add_argument_to_parser(&mut parser, arg);
345        }
346
347        // Parse arguments
348        let (opts, remaining, _order) = parser.parse_args(args)?;
349
350        // Process eager options first (like --help)
351        // This must happen before validation so --help works with missing required params
352        if let Some(ref help) = help_opt {
353            if let Some(ParsedValue::Single(val)) = opts.get(help.name()) {
354                if val == "true" {
355                    // Help was requested - print help and exit
356                    // Store in context for later use
357                    ctx.params_mut().insert(
358                        help.name().to_string(),
359                        Arc::new(true) as Arc<dyn std::any::Any + Send + Sync>,
360                    );
361                    // In Click, help triggers an eager exit. We signal this with Exit.
362                    // The caller (main) should catch this and print help.
363                    return Err(ClickError::Exit { code: 0 });
364                }
365            }
366        }
367
368        // Process other eager options
369        for opt in &self.options {
370            if opt.is_eager() {
371                if let Some(ParsedValue::Single(val)) = opts.get(opt.name()) {
372                    if val == "true" || !val.is_empty() {
373                        // Eager option was triggered - process and potentially exit
374                        // For now, just mark it in context
375                        ctx.params_mut().insert(
376                            opt.name().to_string(),
377                            Arc::new(val.clone()) as Arc<dyn std::any::Any + Send + Sync>,
378                        );
379
380                        // Special-case version option: eager print + exit, before validation.
381                        if opt
382                            .config
383                            .metavar
384                            .as_deref()
385                            .is_some_and(|m| m.starts_with(Self::VERSION_METAVAR_PREFIX))
386                        {
387                            return Err(ClickError::Exit { code: 0 });
388                        }
389                    }
390                }
391            }
392        }
393
394        // Process options (validates required options)
395        for opt in &self.options {
396            self.process_option_value(ctx, opt, &opts)?;
397        }
398        if let Some(ref help) = help_opt {
399            self.process_option_value(ctx, help, &opts)?;
400        }
401
402        // Process arguments (validates required arguments)
403        for arg in &self.arguments {
404            self.process_argument_value(ctx, arg, &opts)?;
405        }
406
407        // Handle extra arguments
408        if !remaining.is_empty() && !ctx.allow_extra_args() && !ctx.resilient_parsing() {
409            let extra_args = remaining.join(" ");
410            return Err(ClickError::usage(if remaining.len() == 1 {
411                format!("Got unexpected extra argument ({})", extra_args)
412            } else {
413                format!("Got unexpected extra arguments ({})", extra_args)
414            }));
415        }
416
417        // Store remaining args in context
418        ctx.args_mut().extend(remaining);
419
420        Ok(())
421    }
422
423    /// Add a ClickOption to the parser.
424    fn add_option_to_parser(&self, parser: &mut OptionParser, opt: &ClickOption) {
425        let mut opts: Vec<&str> = Vec::new();
426        for s in &opt.short {
427            opts.push(s.as_str());
428        }
429        for l in &opt.long {
430            opts.push(l.as_str());
431        }
432
433        let (action, nargs, const_value, flag_needs_value) = if opt.count {
434            (OptionAction::Count, 0, None, false)
435        } else if opt.is_flag {
436            let const_val = opt.flag_value.as_deref().unwrap_or("true");
437            (OptionAction::StoreConst, 0, Some(const_val), false)
438        } else if opt.multiple() {
439            let (nargs_val, fnv) = match opt.nargs() {
440                Nargs::Count(n) => (n as i32, false),
441                Nargs::Variadic => (-1, false),
442                Nargs::Optional => (1, true), // Optional option values use flag_needs_value
443            };
444            (OptionAction::Append, nargs_val, None, fnv)
445        } else {
446            let (nargs_val, fnv) = match opt.nargs() {
447                Nargs::Count(n) => (n as i32, false),
448                Nargs::Variadic => (-1, false),
449                Nargs::Optional => (1, true), // Optional option values use flag_needs_value
450            };
451            (OptionAction::Store, nargs_val, None, fnv)
452        };
453
454        parser.add_option_ex(&opts, opt.name(), action, nargs, const_value, flag_needs_value);
455    }
456
457    /// Add an Argument to the parser.
458    fn add_argument_to_parser(&self, parser: &mut OptionParser, arg: &Argument) {
459        let nargs_val = match arg.nargs() {
460            Nargs::Count(n) => n as i32,
461            Nargs::Variadic => -1,
462            Nargs::Optional => NARGS_OPTIONAL, // Use special marker for optional args
463        };
464
465        parser.add_argument(arg.name(), nargs_val);
466    }
467
468    /// Resolve an environment variable value for a parameter, if configured.
469    fn resolve_envvar_value(&self, ctx: &Context, param: &dyn Parameter) -> Option<String> {
470        let mut candidates: Vec<String> = Vec::new();
471
472        if let Some(envvars) = param.envvar() {
473            candidates.extend(envvars.iter().cloned());
474        } else if let Some(prefix) = ctx.auto_envvar_prefix() {
475            let normalized = param.name().to_uppercase().replace('-', "_");
476            candidates.push(format!("{}_{}", prefix, normalized));
477        }
478
479        for name in candidates {
480            if let Ok(value) = std::env::var(&name) {
481                if !value.is_empty() {
482                    return Some(value);
483                }
484            }
485        }
486
487        None
488    }
489
490    /// Process a parsed option value and store it in the context.
491    fn process_option_value(
492        &self,
493        ctx: &mut Context,
494        opt: &ClickOption,
495        opts: &HashMap<String, ParsedValue>,
496    ) -> Result<(), ClickError> {
497        let name = opt.name();
498        let parsed_value = opts.get(name);
499
500        // Check for FlagNeedsValue marker from Append action
501        // Uses internal namespace prefix to avoid collision with user options
502        let flag_needs_value_key = format!("__click_internal_flag_needs_value_{}", name);
503        let had_flag_needs_value = opts.get(&flag_needs_value_key).is_some();
504
505        let make_error_ctx = || {
506            ErrorContext::new()
507                .with_command_path(ctx.command_path())
508                .with_usage(self.get_usage(ctx))
509                .with_help_options(ctx.help_option_names().to_vec())
510        };
511
512        let convert_single = |value: &str| -> Result<Arc<dyn std::any::Any + Send + Sync>, ClickError> {
513            opt.convert_any(value)
514                .map(Arc::from)
515                .map_err(|msg| {
516                    ClickError::bad_parameter_named(msg, opt.human_readable_name())
517                        .with_context(make_error_ctx())
518                })
519        };
520
521        let convert_multi = |values: &[String]| -> Result<Arc<dyn std::any::Any + Send + Sync>, ClickError> {
522            opt.convert_multi(values)
523                .map(Arc::from)
524                .map_err(|msg| {
525                    ClickError::bad_parameter_named(msg, opt.human_readable_name())
526                        .with_context(make_error_ctx())
527                })
528        };
529
530        // Convert ParsedValue to a boxed value for storage
531        let envvar_value = if matches!(parsed_value, Some(ParsedValue::Unset) | None) {
532            self.resolve_envvar_value(ctx, opt)
533        } else {
534            None
535        };
536
537        let default_map_value = if matches!(parsed_value, Some(ParsedValue::Unset) | None) {
538            ctx.lookup_default_value(name)
539        } else {
540            None
541        };
542
543        let mut source: Option<ParameterSource> = None;
544        let mut value: Option<Arc<dyn std::any::Any + Send + Sync>> = match parsed_value {
545            Some(ParsedValue::Count(n)) => {
546                source = Some(ParameterSource::CommandLine);
547                Some(Arc::new(*n))
548            }
549            Some(ParsedValue::Flag(b)) => {
550                source = Some(ParameterSource::CommandLine);
551                Some(Arc::new(*b))
552            }
553            Some(ParsedValue::Single(s)) => {
554                source = Some(ParameterSource::CommandLine);
555                Some(convert_single(s)?)
556            }
557            Some(ParsedValue::Multiple(v)) => {
558                let mut values = v.clone();
559                if had_flag_needs_value {
560                    if values.is_empty() {
561                        if let Some(ref flag_val) = opt.flag_value {
562                            values.push(flag_val.clone());
563                        } else if let Some(ref default) = opt.default {
564                            values.push(default.clone());
565                        } else {
566                            values.push(String::new());
567                        }
568                    } else if let Some(ref flag_val) = opt.flag_value {
569                        values.push(flag_val.clone());
570                    } else {
571                        values.push(String::new());
572                    }
573                }
574                source = Some(ParameterSource::CommandLine);
575                Some(convert_multi(&values)?)
576            }
577            Some(ParsedValue::FlagNeedsValue) => {
578                let fallback = opt
579                    .flag_value
580                    .as_ref()
581                    .or(opt.default.as_ref())
582                    .cloned()
583                    .unwrap_or_else(String::new);
584                source = Some(ParameterSource::CommandLine);
585                Some(convert_single(&fallback)?)
586            }
587            Some(ParsedValue::Unset) | None => {
588                if let Some(envval) = envvar_value {
589                    source = Some(ParameterSource::Environment);
590                    if opt.count {
591                        let parsed = envval.parse::<usize>().map_err(|_| {
592                            ClickError::bad_parameter_named(
593                                format!("'{}' is not a valid integer.", envval),
594                                opt.human_readable_name(),
595                            )
596                            .with_context(make_error_ctx())
597                        })?;
598                        Some(Arc::new(parsed))
599                    } else if opt.nargs().is_multi() || opt.multiple() {
600                        let values = opt.type_converter().split_envvar_value(&envval);
601                        if values.is_empty() {
602                            None
603                        } else {
604                            Some(convert_multi(&values)?)
605                        }
606                    } else {
607                        Some(convert_single(&envval)?)
608                    }
609                } else if let Some(default_map) = default_map_value {
610                    source = Some(ParameterSource::DefaultMap);
611                    Some(default_map)
612                } else if let Some(ref prompt_text) = opt.prompt {
613                    if ctx.resilient_parsing() || opt.is_flag || opt.count {
614                        None
615                    } else {
616                        let default_value = opt.default.clone();
617                        let prompted = termui::prompt(
618                            prompt_text,
619                            default_value,
620                            opt.hide_input,
621                            opt.confirmation_prompt,
622                            |input| {
623                                match opt.convert_any(input) {
624                                    Ok(any_val) => {
625                                        if let Ok(val) = any_val.downcast::<String>() {
626                                            Ok(*val)
627                                        } else {
628                                            Ok(input.to_string())
629                                        }
630                                    }
631                                    Err(msg) => Err(msg),
632                                }
633                            },
634                        )?;
635
636                        source = Some(ParameterSource::Prompt);
637                        if opt.nargs().is_multi() || opt.multiple() {
638                            Some(convert_multi(&vec![prompted])?)
639                        } else {
640                            Some(convert_single(&prompted)?)
641                        }
642                    }
643                } else if opt.count {
644                    source = Some(ParameterSource::Default);
645                    Some(Arc::new(0usize))
646                } else if let Some(ref default) = opt.default {
647                    source = Some(ParameterSource::Default);
648                    if opt.nargs().is_multi() || opt.multiple() {
649                        Some(convert_multi(&vec![default.clone()])?)
650                    } else {
651                        Some(convert_single(default)?)
652                    }
653                } else {
654                    None
655                }
656            }
657        };
658
659        // Check if required option is missing
660        if value.is_none() && opt.required() && !ctx.resilient_parsing() {
661            let error_ctx = ErrorContext::new()
662                .with_command_path(ctx.command_path())
663                .with_usage(self.get_usage(ctx))
664                .with_help_options(ctx.help_option_names().to_vec());
665            return Err(ClickError::missing_option(opt.human_readable_name()).with_context(error_ctx));
666        }
667
668        if let Some(ref callback) = opt.config.callback {
669            if let Some(current) = value.take() {
670                value = Some(callback(ctx, opt, current)?);
671            }
672        }
673
674        // Store in context if expose_value is true
675        if let Some(v) = value {
676            if let Some(source) = source {
677                ctx.set_parameter_source(name, source);
678            }
679            if opt.expose_value() {
680                ctx.params_mut().insert(name.to_string(), v);
681            }
682        }
683
684        Ok(())
685    }
686
687    /// Process a parsed argument value and store it in the context.
688    fn process_argument_value(
689        &self,
690        ctx: &mut Context,
691        arg: &Argument,
692        opts: &HashMap<String, ParsedValue>,
693    ) -> Result<(), ClickError> {
694        let name = arg.name();
695        let parsed_value = opts.get(name);
696
697        let envvar_value = if matches!(parsed_value, Some(ParsedValue::Unset) | None) {
698            self.resolve_envvar_value(ctx, arg)
699        } else {
700            None
701        };
702
703        let make_error_ctx = || {
704            ErrorContext::new()
705                .with_command_path(ctx.command_path())
706                .with_usage(self.get_usage(ctx))
707                .with_help_options(ctx.help_option_names().to_vec())
708        };
709
710        let convert_single = |value: &str| -> Result<Arc<dyn std::any::Any + Send + Sync>, ClickError> {
711            arg.convert_any(value)
712                .map(Arc::from)
713                .map_err(|msg| {
714                    ClickError::bad_parameter_named(msg, arg.human_readable_name())
715                        .with_context(make_error_ctx())
716                })
717        };
718
719        let convert_multi = |values: &[String]| -> Result<Arc<dyn std::any::Any + Send + Sync>, ClickError> {
720            arg.type_converter()
721                .convert_multi(values)
722                .map(Arc::from)
723                .map_err(|msg| {
724                    ClickError::bad_parameter_named(msg, arg.human_readable_name())
725                        .with_context(make_error_ctx())
726                })
727        };
728
729        // Convert ParsedValue to a boxed value for storage
730        let default_map_value = if matches!(parsed_value, Some(ParsedValue::Unset) | None) {
731            ctx.lookup_default_value(name)
732        } else {
733            None
734        };
735
736        let mut source: Option<ParameterSource> = None;
737        let mut value: Option<Arc<dyn std::any::Any + Send + Sync>> = match parsed_value {
738            Some(ParsedValue::Single(s)) => {
739                source = Some(ParameterSource::CommandLine);
740                Some(convert_single(s)?)
741            }
742            Some(ParsedValue::Multiple(v)) => {
743                source = Some(ParameterSource::CommandLine);
744                Some(convert_multi(v)?)
745            }
746            Some(ParsedValue::Count(n)) => {
747                source = Some(ParameterSource::CommandLine);
748                Some(Arc::new(*n))
749            }
750            Some(ParsedValue::Flag(b)) => {
751                source = Some(ParameterSource::CommandLine);
752                Some(Arc::new(*b))
753            }
754            Some(ParsedValue::FlagNeedsValue) | Some(ParsedValue::Unset) | None => {
755                if let Some(envval) = envvar_value {
756                    source = Some(ParameterSource::Environment);
757                    if arg.nargs().is_multi() || arg.multiple() {
758                        let values = arg.type_converter().split_envvar_value(&envval);
759                        if values.is_empty() {
760                            None
761                        } else {
762                            Some(convert_multi(&values)?)
763                        }
764                    } else {
765                        Some(convert_single(&envval)?)
766                    }
767                } else if let Some(default_map) = default_map_value {
768                    source = Some(ParameterSource::DefaultMap);
769                    Some(default_map)
770                } else {
771                    arg.default_value()
772                        .map(|d| {
773                            source = Some(ParameterSource::Default);
774                            if arg.nargs().is_multi() || arg.multiple() {
775                                convert_multi(&vec![d.to_string()]).map_err(|e| e)
776                            } else {
777                                convert_single(d).map_err(|e| e)
778                            }
779                        })
780                        .transpose()?
781                }
782            }
783        };
784
785        // Check if required argument is missing
786        if value.is_none() && arg.required() && !ctx.resilient_parsing() {
787            let error_ctx = ErrorContext::new()
788                .with_command_path(ctx.command_path())
789                .with_usage(self.get_usage(ctx))
790                .with_help_options(ctx.help_option_names().to_vec());
791            return Err(ClickError::missing_argument(arg.human_readable_name()).with_context(error_ctx));
792        }
793
794        if let Some(ref callback) = arg.config.callback {
795            if let Some(current) = value.take() {
796                value = Some(callback(ctx, arg, current)?);
797            }
798        }
799
800        // Store in context if expose_value is true
801        if let Some(v) = value {
802            if let Some(source) = source {
803                ctx.set_parameter_source(name, source);
804            }
805            if arg.expose_value() {
806                ctx.params_mut().insert(name.to_string(), v);
807            }
808        }
809
810        Ok(())
811    }
812
813    /// Invoke the command callback with the context.
814    ///
815    /// Returns `Ok(())` if no callback is set.
816    pub fn invoke(&self, ctx: &Context) -> Result<(), ClickError> {
817        // Show deprecation warning
818        if let Some(ref deprecated) = self.deprecated {
819            let extra = if deprecated.is_empty() {
820                String::new()
821            } else {
822                format!(" {}", deprecated)
823            };
824            eprintln!(
825                "DeprecationWarning: The command '{}' is deprecated.{}",
826                self.name.as_deref().unwrap_or(""),
827                extra
828            );
829        }
830
831        // Invoke callback
832        if let Some(ref callback) = self.callback {
833            callback(ctx)
834        } else {
835            Ok(())
836        }
837    }
838
839    /// Main entry point - make context, parse args, and invoke.
840    ///
841    /// This is the primary way to run a command as a CLI application.
842    ///
843    /// # Arguments
844    ///
845    /// * `args` - Command-line arguments (typically `std::env::args().skip(1)`)
846    ///
847    /// # Example
848    ///
849    /// ```no_run
850    /// use click::command::Command;
851    ///
852    /// let cmd = Command::new("hello")
853    ///     .callback(|_ctx| {
854    ///         println!("Hello, world!");
855    ///         Ok(())
856    ///     })
857    ///     .build();
858    ///
859    /// let args: Vec<String> = std::env::args().skip(1).collect();
860    /// if let Err(e) = cmd.main(args) {
861    ///     eprintln!("{}", e.format_full());
862    ///     std::process::exit(e.exit_code());
863    /// }
864    /// ```
865    #[allow(clippy::arc_with_non_send_sync)]
866    pub fn main(&self, args: Vec<String>) -> Result<(), ClickError> {
867        let prog_name = self.name.clone().unwrap_or_else(|| {
868            std::env::args()
869                .next()
870                .unwrap_or_else(|| "program".to_string())
871        });
872
873        let args_for_eager = args.clone();
874
875        // Try to make context - this may fail early for --help
876        let ctx_result = self.make_context(&prog_name, args, None);
877
878        match ctx_result {
879            Ok(ctx) => {
880                // Note: Context contains RefCell for close_callbacks which makes it !Send+!Sync.
881                // This is fine for single-threaded CLI usage.
882                let ctx = Arc::new(ctx);
883
884                // Push context onto thread-local stack
885                push_context(Arc::clone(&ctx));
886
887                // Invoke the command
888                let result = self.invoke(&ctx);
889
890                // Pop context
891                pop_context();
892
893                // Run close callbacks
894                ctx.close();
895
896                result
897            }
898            Err(ClickError::Exit { code: 0 }) => {
899                // Help (or other eager exits) was requested.
900                //
901                // Version is implemented as an eager option that signals Exit(0) and stores the
902                // output string in the option metavar with a reserved prefix.
903                if let Some(version_output) = self.get_version_output_from_args(&args_for_eager) {
904                    println!("{}", version_output);
905                    return Ok(());
906                }
907
908                // Default: print help.
909                // Create a minimal context for help formatting.
910                let ctx = ContextBuilder::new().info_name(&prog_name).build();
911                println!("{}", self.get_help(&ctx));
912                Ok(())
913            }
914            Err(e) => Err(e),
915        }
916    }
917
918    fn arg_matches_opt(arg: &str, opt: &str) -> bool {
919        if arg == opt {
920            return true;
921        }
922        if opt.starts_with("--") && arg.starts_with(opt) && arg.get(opt.len()..opt.len() + 1) == Some("=") {
923            return true;
924        }
925        if opt.starts_with('-') && opt.len() == 2 && !opt.starts_with("--") {
926            let needle = opt.chars().nth(1).unwrap_or('\0');
927            if arg.starts_with('-') && !arg.starts_with("--") {
928                return arg.chars().skip(1).any(|c| c == needle);
929            }
930        }
931        false
932    }
933
934    /// Check if a version option was triggered and return the version output.
935    ///
936    /// This is used internally by `main()` and `Group::main()` to handle
937    /// version options that trigger `Exit { code: 0 }`.
938    pub fn get_version_output_from_args(&self, args: &[String]) -> Option<String> {
939        for opt in &self.options {
940            let meta = opt.config.metavar.as_deref()?;
941            let output = meta.strip_prefix(Self::VERSION_METAVAR_PREFIX)?;
942
943            let mut names = opt.long.iter().chain(opt.short.iter());
944            if names.any(|n| args.iter().any(|a| Self::arg_matches_opt(a, n))) {
945                return Some(output.to_string());
946            }
947        }
948        None
949    }
950
951    /// Format the usage line.
952    ///
953    /// Returns a string like: `Usage: COMMAND [OPTIONS] ARGUMENT`
954    pub fn get_usage(&self, ctx: &Context) -> String {
955        let mut pieces = Vec::new();
956
957        // Add options metavar
958        if !self.options_metavar.is_empty() {
959            pieces.push(self.options_metavar.clone());
960        }
961
962        // Add argument metavars
963        for arg in &self.arguments {
964            if !arg.hidden() {
965                pieces.push(arg.make_metavar());
966            }
967        }
968
969        format!("Usage: {} {}", ctx.command_path(), pieces.join(" "))
970    }
971
972    /// Format the help text.
973    ///
974    /// Returns the complete help output including usage, description,
975    /// options, arguments, and epilog.
976    pub fn get_help(&self, ctx: &Context) -> String {
977        let mut parts = Vec::new();
978
979        // Usage line
980        parts.push(self.get_usage(ctx));
981
982        // Help text
983        if let Some(ref help) = self.help {
984            let text = help.lines().next().unwrap_or("");
985            if !text.is_empty() {
986                parts.push(String::new()); // blank line
987                let help_text = if let Some(ref dep) = self.deprecated {
988                    if dep.is_empty() {
989                        format!("{}  (DEPRECATED)", text)
990                    } else {
991                        format!("{}  (DEPRECATED: {})", text, dep)
992                    }
993                } else {
994                    text.to_string()
995                };
996                parts.push(format!("  {}", help_text));
997            }
998        } else if let Some(ref dep) = self.deprecated {
999            parts.push(String::new());
1000            let dep_msg = if dep.is_empty() {
1001                "(DEPRECATED)".to_string()
1002            } else {
1003                format!("(DEPRECATED: {})", dep)
1004            };
1005            parts.push(format!("  {}", dep_msg));
1006        }
1007
1008        // Options section
1009        let opt_records: Vec<(String, String)> = self
1010            .options
1011            .iter()
1012            .filter_map(|opt| opt.get_help_record())
1013            .collect();
1014
1015        // Add help option record
1016        let help_opt = self.get_help_option(ctx);
1017        let help_record = help_opt.as_ref().and_then(|h| h.get_help_record());
1018
1019        if !opt_records.is_empty() || help_record.is_some() {
1020            parts.push(String::new());
1021            parts.push("Options:".to_string());
1022
1023            for (opt_str, help) in &opt_records {
1024                parts.push(format!("  {}  {}", opt_str, help));
1025            }
1026            if let Some((opt_str, help)) = help_record {
1027                parts.push(format!("  {}  {}", opt_str, help));
1028            }
1029        }
1030
1031        // Arguments section (if any have help text)
1032        let arg_records: Vec<(String, String)> = self
1033            .arguments
1034            .iter()
1035            .filter_map(|arg| arg.get_help_record())
1036            .filter(|(_, help)| !help.is_empty())
1037            .collect();
1038
1039        if !arg_records.is_empty() {
1040            parts.push(String::new());
1041            parts.push("Arguments:".to_string());
1042            for (metavar, help) in &arg_records {
1043                parts.push(format!("  {}  {}", metavar, help));
1044            }
1045        }
1046
1047        // Epilog
1048        if let Some(ref epilog) = self.epilog {
1049            parts.push(String::new());
1050            parts.push(epilog.clone());
1051        }
1052
1053        parts.join("\n")
1054    }
1055
1056    /// Get the short help text for command listings.
1057    ///
1058    /// Returns `short_help` if set, otherwise derives from the first
1059    /// sentence of `help`.
1060    pub fn get_short_help(&self) -> String {
1061        if let Some(ref short_help) = self.short_help {
1062            let text = short_help.clone();
1063            if let Some(ref dep) = self.deprecated {
1064                if dep.is_empty() {
1065                    format!("{} (DEPRECATED)", text)
1066                } else {
1067                    format!("{} (DEPRECATED: {})", text, dep)
1068                }
1069            } else {
1070                text
1071            }
1072        } else if let Some(ref help) = self.help {
1073            // Get first line/sentence
1074            let text = help
1075                .lines()
1076                .next()
1077                .unwrap_or("")
1078                .split('.')
1079                .next()
1080                .unwrap_or("")
1081                .trim();
1082            if let Some(ref dep) = self.deprecated {
1083                if dep.is_empty() {
1084                    format!("{} (DEPRECATED)", text)
1085                } else {
1086                    format!("{} (DEPRECATED: {})", text, dep)
1087                }
1088            } else {
1089                text.to_string()
1090            }
1091        } else if let Some(ref dep) = self.deprecated {
1092            if dep.is_empty() {
1093                "(DEPRECATED)".to_string()
1094            } else {
1095                format!("(DEPRECATED: {})", dep)
1096            }
1097        } else {
1098            String::new()
1099        }
1100    }
1101}
1102
1103// =============================================================================
1104// CommandBuilder
1105// =============================================================================
1106
1107/// Builder for creating [`Command`] instances.
1108///
1109/// Use [`Command::new`] to create a builder, then chain methods to configure
1110/// the command, and finally call [`build`](CommandBuilder::build) to create
1111/// the command.
1112pub struct CommandBuilder {
1113    name: String,
1114    callback: Option<CommandCallback>,
1115    options: Vec<ClickOption>,
1116    arguments: Vec<Argument>,
1117    help: Option<String>,
1118    epilog: Option<String>,
1119    short_help: Option<String>,
1120    options_metavar: String,
1121    add_help_option: bool,
1122    help_option: Option<ClickOption>,
1123    no_args_is_help: bool,
1124    hidden: bool,
1125    deprecated: Option<String>,
1126    allow_extra_args: bool,
1127    allow_interspersed_args: bool,
1128    ignore_unknown_options: bool,
1129}
1130
1131impl CommandBuilder {
1132    /// Create a new command builder with the given name.
1133    pub fn new(name: &str) -> Self {
1134        Self {
1135            name: name.to_string(),
1136            callback: None,
1137            options: Vec::new(),
1138            arguments: Vec::new(),
1139            help: None,
1140            epilog: None,
1141            short_help: None,
1142            options_metavar: "[OPTIONS]".to_string(),
1143            add_help_option: true,
1144            help_option: None,
1145            no_args_is_help: false,
1146            hidden: false,
1147            deprecated: None,
1148            allow_extra_args: false,
1149            allow_interspersed_args: true,
1150            ignore_unknown_options: false,
1151        }
1152    }
1153
1154    /// Set the callback function for this command.
1155    ///
1156    /// The callback receives a reference to the [`Context`] and should return
1157    /// `Ok(())` on success or a [`ClickError`] on failure.
1158    pub fn callback<F>(mut self, f: F) -> Self
1159    where
1160        F: Fn(&Context) -> Result<(), ClickError> + Send + Sync + 'static,
1161    {
1162        self.callback = Some(Box::new(f));
1163        self
1164    }
1165
1166    /// Add an option to this command.
1167    pub fn option(mut self, opt: ClickOption) -> Self {
1168        self.options.push(opt);
1169        self
1170    }
1171
1172    /// Add an argument to this command.
1173    pub fn argument(mut self, arg: Argument) -> Self {
1174        self.arguments.push(arg);
1175        self
1176    }
1177
1178    /// Set the help text for this command.
1179    pub fn help(mut self, help: &str) -> Self {
1180        self.help = Some(help.to_string());
1181        self
1182    }
1183
1184    /// Set the epilog (text shown after help).
1185    pub fn epilog(mut self, epilog: &str) -> Self {
1186        self.epilog = Some(epilog.to_string());
1187        self
1188    }
1189
1190    /// Set the short help text for command listings.
1191    pub fn short_help(mut self, short_help: &str) -> Self {
1192        self.short_help = Some(short_help.to_string());
1193        self
1194    }
1195
1196    /// Set the options metavar (default: "[OPTIONS]").
1197    pub fn options_metavar(mut self, metavar: &str) -> Self {
1198        self.options_metavar = metavar.to_string();
1199        self
1200    }
1201
1202    /// Set whether to add a --help option (default: true).
1203    pub fn add_help_option(mut self, add: bool) -> Self {
1204        self.add_help_option = add;
1205        self
1206    }
1207
1208    /// Override the automatically generated help option.
1209    ///
1210    /// This lets you customize the help option names (e.g. `-h`, `--help`) and
1211    /// its help text while keeping it eager.
1212    ///
1213    /// Setting a custom help option implicitly enables `add_help_option`.
1214    pub fn help_option(mut self, opt: ClickOption) -> Self {
1215        self.add_help_option = true;
1216        self.help_option = Some(opt);
1217        self
1218    }
1219
1220    /// Set whether to show help if no args provided (default: false).
1221    pub fn no_args_is_help(mut self, value: bool) -> Self {
1222        self.no_args_is_help = value;
1223        self
1224    }
1225
1226    /// Hide this command from help output.
1227    pub fn hidden(mut self) -> Self {
1228        self.hidden = true;
1229        self
1230    }
1231
1232    /// Mark this command as deprecated.
1233    ///
1234    /// Pass an empty string for a generic deprecation message,
1235    /// or a custom message for more details.
1236    pub fn deprecated(mut self, message: &str) -> Self {
1237        self.deprecated = Some(message.to_string());
1238        self
1239    }
1240
1241    /// Set whether extra arguments are allowed.
1242    pub fn allow_extra_args(mut self, allow: bool) -> Self {
1243        self.allow_extra_args = allow;
1244        self
1245    }
1246
1247    /// Set whether interspersed arguments are allowed.
1248    pub fn allow_interspersed_args(mut self, allow: bool) -> Self {
1249        self.allow_interspersed_args = allow;
1250        self
1251    }
1252
1253    /// Set whether unknown options should be ignored.
1254    pub fn ignore_unknown_options(mut self, ignore: bool) -> Self {
1255        self.ignore_unknown_options = ignore;
1256        self
1257    }
1258
1259    /// Build the command.
1260    pub fn build(self) -> Command {
1261        Command {
1262            name: Some(self.name),
1263            callback: self.callback,
1264            options: self.options,
1265            arguments: self.arguments,
1266            help: self.help,
1267            epilog: self.epilog,
1268            short_help: self.short_help,
1269            options_metavar: self.options_metavar,
1270            add_help_option: self.add_help_option,
1271            no_args_is_help: self.no_args_is_help,
1272            hidden: self.hidden,
1273            deprecated: self.deprecated,
1274            allow_extra_args: self.allow_extra_args,
1275            allow_interspersed_args: self.allow_interspersed_args,
1276            ignore_unknown_options: self.ignore_unknown_options,
1277            help_option: self.help_option,
1278        }
1279    }
1280}
1281
1282// =============================================================================
1283// Helper Functions
1284// =============================================================================
1285
1286/// Create the standard help option.
1287fn make_help_option(names: &[String]) -> ClickOption {
1288    // Convert to &str for the option builder
1289    let name_refs: Vec<&str> = names.iter().map(|s| s.as_str()).collect();
1290
1291    ClickOption::new(&name_refs)
1292        .flag("true")
1293        .eager()
1294        .help("Show this message and exit.")
1295        .build()
1296}
1297
1298// =============================================================================
1299// Tests
1300// =============================================================================
1301
1302#[cfg(test)]
1303mod tests {
1304    use super::*;
1305    use crate::source::ParameterSource;
1306    use crate::types::INT;
1307    use std::collections::HashMap;
1308    use std::sync::Arc;
1309
1310    #[test]
1311    fn test_command_creation_defaults() {
1312        let cmd = Command::new("test").build();
1313
1314        assert_eq!(cmd.name, Some("test".to_string()));
1315        assert!(cmd.callback.is_none());
1316        assert!(cmd.options.is_empty());
1317        assert!(cmd.arguments.is_empty());
1318        assert!(cmd.help.is_none());
1319        assert!(cmd.epilog.is_none());
1320        assert!(cmd.short_help.is_none());
1321        assert_eq!(cmd.options_metavar, "[OPTIONS]");
1322        assert!(cmd.add_help_option);
1323        assert!(!cmd.no_args_is_help);
1324        assert!(!cmd.hidden);
1325        assert!(cmd.deprecated.is_none());
1326        assert!(!cmd.allow_extra_args);
1327        assert!(cmd.allow_interspersed_args);
1328        assert!(!cmd.ignore_unknown_options);
1329    }
1330
1331    #[test]
1332    fn test_command_builder_chain() {
1333        let cmd = Command::new("hello")
1334            .help("Say hello to someone")
1335            .epilog("Example: hello --name World")
1336            .short_help("Say hello")
1337            .options_metavar("[OPTS]")
1338            .add_help_option(false)
1339            .no_args_is_help(true)
1340            .hidden()
1341            .deprecated("Use 'greet' instead")
1342            .allow_extra_args(true)
1343            .allow_interspersed_args(false)
1344            .ignore_unknown_options(true)
1345            .build();
1346
1347        assert_eq!(cmd.name, Some("hello".to_string()));
1348        assert_eq!(cmd.help, Some("Say hello to someone".to_string()));
1349        assert_eq!(cmd.epilog, Some("Example: hello --name World".to_string()));
1350        assert_eq!(cmd.short_help, Some("Say hello".to_string()));
1351        assert_eq!(cmd.options_metavar, "[OPTS]");
1352        assert!(!cmd.add_help_option);
1353        assert!(cmd.no_args_is_help);
1354        assert!(cmd.hidden);
1355        assert_eq!(cmd.deprecated, Some("Use 'greet' instead".to_string()));
1356        assert!(cmd.allow_extra_args);
1357        assert!(!cmd.allow_interspersed_args);
1358        assert!(cmd.ignore_unknown_options);
1359    }
1360
1361    #[test]
1362    fn test_command_with_callback() {
1363        use std::sync::atomic::{AtomicBool, Ordering};
1364
1365        let called = Arc::new(AtomicBool::new(false));
1366        let called_clone = Arc::clone(&called);
1367
1368        let cmd = Command::new("test")
1369            .callback(move |_ctx| {
1370                called_clone.store(true, Ordering::SeqCst);
1371                Ok(())
1372            })
1373            .build();
1374
1375        assert!(cmd.callback.is_some());
1376
1377        // Create a minimal context and invoke
1378        let ctx = ContextBuilder::new().info_name("test").build();
1379        let result = cmd.invoke(&ctx);
1380        assert!(result.is_ok());
1381        assert!(called.load(Ordering::SeqCst));
1382    }
1383
1384    #[test]
1385    fn test_command_with_option() {
1386        let cmd = Command::new("greet")
1387            .option(
1388                ClickOption::new(&["--name", "-n"])
1389                    .help("Name to greet")
1390                    .default("World")
1391                    .build(),
1392            )
1393            .build();
1394
1395        assert_eq!(cmd.options.len(), 1);
1396        assert_eq!(cmd.options[0].name(), "name");
1397    }
1398
1399    #[test]
1400    fn test_command_with_argument() {
1401        let cmd = Command::new("cat")
1402            .argument(Argument::new("file").help("File to read").build())
1403            .build();
1404
1405        assert_eq!(cmd.arguments.len(), 1);
1406        assert_eq!(cmd.arguments[0].name(), "file");
1407    }
1408
1409    #[test]
1410    fn test_command_with_multiple_params() {
1411        let cmd = Command::new("copy")
1412            .option(
1413                ClickOption::new(&["--recursive", "-r"])
1414                    .flag("true")
1415                    .help("Copy recursively")
1416                    .build(),
1417            )
1418            .argument(Argument::new("src").help("Source path").build())
1419            .argument(Argument::new("dst").help("Destination path").build())
1420            .build();
1421
1422        assert_eq!(cmd.options.len(), 1);
1423        assert_eq!(cmd.arguments.len(), 2);
1424    }
1425
1426    #[test]
1427    fn test_make_context_basic() {
1428        let cmd = Command::new("hello").build();
1429        let ctx = cmd.make_context("hello", vec![], None);
1430
1431        assert!(ctx.is_ok());
1432        let ctx = ctx.unwrap();
1433        assert_eq!(ctx.info_name(), Some("hello"));
1434    }
1435
1436    #[test]
1437    fn test_parse_args_with_option() {
1438        let cmd = Command::new("greet")
1439            .option(
1440                ClickOption::new(&["--name", "-n"])
1441                    .default("World")
1442                    .build(),
1443            )
1444            .build();
1445
1446        let ctx = cmd.make_context("greet", vec!["--name".to_string(), "Alice".to_string()], None);
1447        assert!(ctx.is_ok());
1448
1449        let ctx = ctx.unwrap();
1450        let name = ctx.get_param::<String>("name");
1451        assert_eq!(name, Some(&"Alice".to_string()));
1452    }
1453
1454    #[test]
1455    fn test_parse_args_with_argument() {
1456        let cmd = Command::new("cat")
1457            .argument(Argument::new("file").build())
1458            .build();
1459
1460        let ctx = cmd.make_context("cat", vec!["test.txt".to_string()], None);
1461        assert!(ctx.is_ok());
1462
1463        let ctx = ctx.unwrap();
1464        let file = ctx.get_param::<String>("file");
1465        assert_eq!(file, Some(&"test.txt".to_string()));
1466    }
1467
1468    #[test]
1469    fn test_parse_args_missing_required() {
1470        let cmd = Command::new("cat")
1471            .argument(Argument::new("file").build())
1472            .build();
1473
1474        let ctx = cmd.make_context("cat", vec![], None);
1475        assert!(ctx.is_err());
1476
1477        let err = ctx.unwrap_err();
1478        assert!(matches!(err, ClickError::MissingParameter { .. }));
1479    }
1480
1481    #[test]
1482    fn test_parse_args_extra_args_error() {
1483        let cmd = Command::new("hello").build();
1484
1485        let ctx = cmd.make_context("hello", vec!["extra".to_string()], None);
1486        assert!(ctx.is_err());
1487
1488        let err = ctx.unwrap_err();
1489        assert!(matches!(err, ClickError::UsageError { .. }));
1490    }
1491
1492    #[test]
1493    fn test_parse_args_extra_args_allowed() {
1494        let cmd = Command::new("hello").allow_extra_args(true).build();
1495
1496        let ctx = cmd.make_context("hello", vec!["extra".to_string()], None);
1497        assert!(ctx.is_ok());
1498
1499        let ctx = ctx.unwrap();
1500        assert_eq!(ctx.args(), &["extra".to_string()]);
1501    }
1502
1503    #[test]
1504    fn test_get_usage() {
1505        let cmd = Command::new("copy")
1506            .argument(Argument::new("src").build())
1507            .argument(Argument::new("dst").build())
1508            .build();
1509
1510        let ctx = ContextBuilder::new().info_name("copy").build();
1511        let usage = cmd.get_usage(&ctx);
1512
1513        assert!(usage.contains("Usage:"));
1514        assert!(usage.contains("copy"));
1515        assert!(usage.contains("[OPTIONS]"));
1516        assert!(usage.contains("SRC"));
1517        assert!(usage.contains("DST"));
1518    }
1519
1520    #[test]
1521    fn test_get_help() {
1522        let cmd = Command::new("greet")
1523            .help("Greet someone")
1524            .option(
1525                ClickOption::new(&["--name", "-n"])
1526                    .help("Name to greet")
1527                    .build(),
1528            )
1529            .epilog("Example: greet --name World")
1530            .build();
1531
1532        let ctx = ContextBuilder::new().info_name("greet").build();
1533        let help = cmd.get_help(&ctx);
1534
1535        assert!(help.contains("Usage:"));
1536        assert!(help.contains("Greet someone"));
1537        assert!(help.contains("Options:"));
1538        assert!(help.contains("--name"));
1539        assert!(help.contains("Name to greet"));
1540        assert!(help.contains("Example:"));
1541    }
1542
1543    #[test]
1544    fn test_get_short_help() {
1545        // Explicit short_help
1546        let cmd = Command::new("test").short_help("Test command").build();
1547        assert_eq!(cmd.get_short_help(), "Test command");
1548
1549        // Derived from help
1550        let cmd = Command::new("test")
1551            .help("This is a test. It does things.")
1552            .build();
1553        assert_eq!(cmd.get_short_help(), "This is a test");
1554
1555        // With deprecation
1556        let cmd = Command::new("test")
1557            .short_help("Test command")
1558            .deprecated("Use 'new-test' instead")
1559            .build();
1560        assert!(cmd.get_short_help().contains("DEPRECATED"));
1561        assert!(cmd.get_short_help().contains("Use 'new-test' instead"));
1562    }
1563
1564    #[test]
1565    fn test_make_help_option() {
1566        let help_opt = make_help_option(&["--help".to_string(), "-h".to_string()]);
1567
1568        assert!(help_opt.is_flag);
1569        assert!(help_opt.is_eager());
1570        assert_eq!(help_opt.help(), Some("Show this message and exit."));
1571    }
1572
1573    #[test]
1574    fn test_command_debug() {
1575        let cmd = Command::new("test").build();
1576        let debug_str = format!("{:?}", cmd);
1577
1578        assert!(debug_str.contains("Command"));
1579        assert!(debug_str.contains("test"));
1580    }
1581
1582    #[test]
1583    fn test_invoke_with_deprecation() {
1584        let cmd = Command::new("old")
1585            .deprecated("Use 'new' instead")
1586            .callback(|_| Ok(()))
1587            .build();
1588
1589        let ctx = ContextBuilder::new().info_name("old").build();
1590        // This will print a deprecation warning to stderr
1591        let result = cmd.invoke(&ctx);
1592        assert!(result.is_ok());
1593    }
1594
1595    #[test]
1596    fn test_parse_flag_option() {
1597        let cmd = Command::new("test")
1598            .option(
1599                ClickOption::new(&["--verbose", "-v"])
1600                    .flag("true")
1601                    .build(),
1602            )
1603            .build();
1604
1605        let ctx = cmd.make_context("test", vec!["--verbose".to_string()], None);
1606        assert!(ctx.is_ok());
1607
1608        let ctx = ctx.unwrap();
1609        let verbose = ctx.get_param::<String>("verbose");
1610        assert_eq!(verbose, Some(&"true".to_string()));
1611    }
1612
1613    #[test]
1614    fn test_parse_count_option() {
1615        let cmd = Command::new("test")
1616            .option(ClickOption::new(&["--verbose", "-v"]).count().build())
1617            .build();
1618
1619        let ctx = cmd.make_context(
1620            "test",
1621            vec!["-v".to_string(), "-v".to_string(), "-v".to_string()],
1622            None,
1623        );
1624        assert!(ctx.is_ok());
1625
1626        let ctx = ctx.unwrap();
1627        let verbose = ctx.get_param::<usize>("verbose");
1628        assert_eq!(verbose, Some(&3));
1629    }
1630
1631    #[test]
1632    fn test_option_type_conversion() {
1633        let cmd = Command::new("test")
1634            .option(ClickOption::new(&["--count"]).type_any(INT).build())
1635            .build();
1636
1637        let ctx = cmd.make_context("test", vec!["--count".to_string(), "42".to_string()], None);
1638        assert!(ctx.is_ok());
1639
1640        let ctx = ctx.unwrap();
1641        let count = ctx.get_param::<i64>("count");
1642        assert_eq!(count, Some(&42));
1643    }
1644
1645    #[test]
1646    fn test_option_callback_applied() {
1647        let cmd = Command::new("test")
1648            .option(
1649                ClickOption::new(&["--count"])
1650                    .type_any(INT)
1651                    .callback(|_ctx, _param, value| {
1652                        let count = *value
1653                            .as_ref()
1654                            .downcast_ref::<i64>()
1655                            .ok_or_else(|| ClickError::bad_parameter("bad callback value"))?;
1656                        Ok(Arc::new(count + 1))
1657                    })
1658                    .build(),
1659            )
1660            .build();
1661
1662        let ctx = cmd.make_context("test", vec!["--count".to_string(), "2".to_string()], None);
1663        assert!(ctx.is_ok());
1664
1665        let ctx = ctx.unwrap();
1666        let count = ctx.get_param::<i64>("count");
1667        assert_eq!(count, Some(&3));
1668    }
1669
1670    #[test]
1671    fn test_option_with_default() {
1672        let cmd = Command::new("greet")
1673            .option(
1674                ClickOption::new(&["--name"])
1675                    .default("World")
1676                    .build(),
1677            )
1678            .build();
1679
1680        // Without providing the option
1681        let ctx = cmd.make_context("greet", vec![], None);
1682        assert!(ctx.is_ok());
1683
1684        let ctx = ctx.unwrap();
1685        let name = ctx.get_param::<String>("name");
1686        assert_eq!(name, Some(&"World".to_string()));
1687        assert_eq!(ctx.get_parameter_source("name"), Some(ParameterSource::Default));
1688    }
1689
1690    #[test]
1691    fn test_option_default_map_value() {
1692        let cmd = Command::new("greet")
1693            .option(ClickOption::new(&["--name"]).build())
1694            .build();
1695
1696        let mut defaults: HashMap<String, Arc<dyn std::any::Any + Send + Sync>> = HashMap::new();
1697        defaults.insert("name".to_string(), Arc::new("Bob".to_string()));
1698
1699        let mut ctx = ContextBuilder::new()
1700            .info_name("greet")
1701            .default_map(defaults)
1702            .build();
1703
1704        let result = cmd.parse_args(&mut ctx, vec![]);
1705        assert!(result.is_ok());
1706
1707        let name = ctx.get_param::<String>("name");
1708        assert_eq!(name, Some(&"Bob".to_string()));
1709        assert_eq!(
1710            ctx.get_parameter_source("name"),
1711            Some(ParameterSource::DefaultMap)
1712        );
1713    }
1714
1715    #[test]
1716    fn test_option_envvar_value() {
1717        std::env::set_var("CLICK_TEST_COUNT", "9");
1718        let cmd = Command::new("test")
1719            .option(
1720                ClickOption::new(&["--count"])
1721                    .envvar("CLICK_TEST_COUNT")
1722                    .type_any(INT)
1723                    .build(),
1724            )
1725            .build();
1726
1727        let ctx = cmd.make_context("test", vec![], None);
1728        std::env::remove_var("CLICK_TEST_COUNT");
1729
1730        assert!(ctx.is_ok());
1731        let ctx = ctx.unwrap();
1732        let count = ctx.get_param::<i64>("count");
1733        assert_eq!(count, Some(&9));
1734        assert_eq!(
1735            ctx.get_parameter_source("count"),
1736            Some(ParameterSource::Environment)
1737        );
1738    }
1739
1740    #[test]
1741    fn test_argument_type_conversion() {
1742        let cmd = Command::new("greet")
1743            .argument(Argument::new("count").type_(INT).build())
1744            .build();
1745
1746        let ctx = cmd.make_context("greet", vec!["7".to_string()], None);
1747        assert!(ctx.is_ok());
1748
1749        let ctx = ctx.unwrap();
1750        let count = ctx.get_param::<i64>("count");
1751        assert_eq!(count, Some(&7));
1752        assert_eq!(
1753            ctx.get_parameter_source("count"),
1754            Some(ParameterSource::CommandLine)
1755        );
1756    }
1757
1758    #[test]
1759    fn test_argument_callback_applied() {
1760        let cmd = Command::new("greet")
1761            .argument(
1762                Argument::new("count")
1763                    .type_(INT)
1764                    .callback(|_ctx, _param, value| {
1765                        let count = *value
1766                            .as_ref()
1767                            .downcast_ref::<i64>()
1768                            .ok_or_else(|| ClickError::bad_parameter("bad callback value"))?;
1769                        Ok(Arc::new(count * 2))
1770                    })
1771                    .build(),
1772            )
1773            .build();
1774
1775        let ctx = cmd.make_context("greet", vec!["3".to_string()], None);
1776        assert!(ctx.is_ok());
1777
1778        let ctx = ctx.unwrap();
1779        let count = ctx.get_param::<i64>("count");
1780        assert_eq!(count, Some(&6));
1781    }
1782
1783    #[test]
1784    fn test_argument_type_conversion_error() {
1785        let cmd = Command::new("greet")
1786            .argument(Argument::new("count").type_(INT).build())
1787            .build();
1788
1789        let ctx = cmd.make_context("greet", vec!["nope".to_string()], None);
1790        assert!(matches!(ctx, Err(ClickError::BadParameter { .. })));
1791    }
1792
1793    #[test]
1794    fn test_argument_with_default() {
1795        let cmd = Command::new("greet")
1796            .argument(Argument::new("name").default("World").build())
1797            .build();
1798
1799        // Without providing the argument
1800        let ctx = cmd.make_context("greet", vec![], None);
1801        assert!(ctx.is_ok());
1802
1803        let ctx = ctx.unwrap();
1804        let name = ctx.get_param::<String>("name");
1805        assert_eq!(name, Some(&"World".to_string()));
1806    }
1807
1808    #[test]
1809    fn test_argument_default_map_value() {
1810        let cmd = Command::new("greet")
1811            .argument(Argument::new("name").build())
1812            .build();
1813
1814        let mut defaults: HashMap<String, Arc<dyn std::any::Any + Send + Sync>> = HashMap::new();
1815        defaults.insert("name".to_string(), Arc::new("Alice".to_string()));
1816
1817        let mut ctx = ContextBuilder::new()
1818            .info_name("greet")
1819            .default_map(defaults)
1820            .build();
1821
1822        let result = cmd.parse_args(&mut ctx, vec![]);
1823        assert!(result.is_ok());
1824
1825        let name = ctx.get_param::<String>("name");
1826        assert_eq!(name, Some(&"Alice".to_string()));
1827    }
1828
1829    #[test]
1830    fn test_argument_auto_envvar_prefix() {
1831        std::env::set_var("MYAPP_NAME", "Alice");
1832        let cmd = Command::new("greet")
1833            .argument(Argument::new("name").build())
1834            .build();
1835
1836        let mut ctx = ContextBuilder::new()
1837            .info_name("greet")
1838            .auto_envvar_prefix("MYAPP")
1839            .build();
1840
1841        let result = cmd.parse_args(&mut ctx, vec![]);
1842        std::env::remove_var("MYAPP_NAME");
1843
1844        assert!(result.is_ok());
1845        let name = ctx.get_param::<String>("name");
1846        assert_eq!(name, Some(&"Alice".to_string()));
1847    }
1848
1849    // -------------------------------------------------------------------------
1850    // Optional positional argument tests (fix for Nargs::Optional)
1851    // -------------------------------------------------------------------------
1852
1853    #[test]
1854    fn test_optional_argument_with_value() {
1855        use crate::parameter::Nargs;
1856
1857        let cmd = Command::new("test")
1858            .argument(
1859                Argument::new("file")
1860                    .nargs(Nargs::Optional)
1861                    .default("default.txt")
1862                    .build(),
1863            )
1864            .build();
1865
1866        // With a value provided
1867        let ctx = cmd.make_context("test", vec!["input.txt".to_string()], None);
1868        assert!(ctx.is_ok());
1869
1870        let ctx = ctx.unwrap();
1871        let file = ctx.get_param::<String>("file");
1872        assert_eq!(file, Some(&"input.txt".to_string()));
1873    }
1874
1875    #[test]
1876    fn test_optional_argument_without_value() {
1877        use crate::parameter::Nargs;
1878
1879        let cmd = Command::new("test")
1880            .argument(
1881                Argument::new("file")
1882                    .nargs(Nargs::Optional)
1883                    .default("default.txt")
1884                    .build(),
1885            )
1886            .build();
1887
1888        // Without providing the optional argument - should use default
1889        let ctx = cmd.make_context("test", vec![], None);
1890        assert!(ctx.is_ok());
1891
1892        let ctx = ctx.unwrap();
1893        let file = ctx.get_param::<String>("file");
1894        assert_eq!(file, Some(&"default.txt".to_string()));
1895    }
1896
1897    #[test]
1898    fn test_required_followed_by_optional_argument() {
1899        use crate::parameter::Nargs;
1900
1901        let cmd = Command::new("copy")
1902            .argument(Argument::new("src").build()) // required
1903            .argument(
1904                Argument::new("dst")
1905                    .nargs(Nargs::Optional)
1906                    .default(".")
1907                    .build(),
1908            )
1909            .build();
1910
1911        // Only providing required arg
1912        let ctx = cmd.make_context("copy", vec!["source.txt".to_string()], None);
1913        assert!(ctx.is_ok());
1914
1915        let ctx = ctx.unwrap();
1916        let src = ctx.get_param::<String>("src");
1917        let dst = ctx.get_param::<String>("dst");
1918        assert_eq!(src, Some(&"source.txt".to_string()));
1919        assert_eq!(dst, Some(&".".to_string()));
1920    }
1921
1922    #[test]
1923    fn test_required_followed_by_optional_with_both_values() {
1924        use crate::parameter::Nargs;
1925
1926        let cmd = Command::new("copy")
1927            .argument(Argument::new("src").build()) // required
1928            .argument(
1929                Argument::new("dst")
1930                    .nargs(Nargs::Optional)
1931                    .default(".")
1932                    .build(),
1933            )
1934            .build();
1935
1936        // Providing both args
1937        let ctx = cmd.make_context(
1938            "copy",
1939            vec!["source.txt".to_string(), "dest.txt".to_string()],
1940            None,
1941        );
1942        assert!(ctx.is_ok());
1943
1944        let ctx = ctx.unwrap();
1945        let src = ctx.get_param::<String>("src");
1946        let dst = ctx.get_param::<String>("dst");
1947        assert_eq!(src, Some(&"source.txt".to_string()));
1948        assert_eq!(dst, Some(&"dest.txt".to_string()));
1949    }
1950
1951    // -------------------------------------------------------------------------
1952    // Multi-value argument error test (fix for issue 2)
1953    // -------------------------------------------------------------------------
1954
1955    #[test]
1956    fn test_multi_value_argument_incomplete_error() {
1957        use crate::parameter::Nargs;
1958
1959        let cmd = Command::new("point")
1960            .argument(Argument::new("coords").nargs(Nargs::Count(3)).build())
1961            .build();
1962
1963        // Only providing 2 of 3 required values
1964        let ctx = cmd.make_context(
1965            "point",
1966            vec!["1".to_string(), "2".to_string()],
1967            None,
1968        );
1969
1970        // Should fail with an error about missing values
1971        assert!(ctx.is_err());
1972        let err = ctx.unwrap_err();
1973        assert!(err.to_string().contains("takes 3 values"));
1974    }
1975
1976    // -------------------------------------------------------------------------
1977    // Optional option value test (fix for issue 4)
1978    // -------------------------------------------------------------------------
1979
1980    #[test]
1981    fn test_optional_option_value_with_flag_value() {
1982        use crate::parameter::Nargs;
1983
1984        let cmd = Command::new("test")
1985            .option(
1986                ClickOption::new(&["--opt"])
1987                    .nargs(Nargs::Optional)
1988                    .flag("flagval") // Used when option is used without value
1989                    .default("default")
1990                    .build(),
1991            )
1992            .build();
1993
1994        // Using --opt followed by another option (would trigger FlagNeedsValue)
1995        let ctx = cmd.make_context("test", vec!["--opt".to_string()], None);
1996        assert!(ctx.is_ok());
1997
1998        let ctx = ctx.unwrap();
1999        let opt = ctx.get_param::<String>("opt");
2000        // Should use the flag_value since --opt was used without an argument
2001        assert_eq!(opt, Some(&"flagval".to_string()));
2002    }
2003
2004    #[test]
2005    fn test_flag_value_group_shared_destination() {
2006        let cmd = Command::new("mine")
2007            .option(
2008                ClickOption::new(&["--moored", "-m"])
2009                    .dest("ty")
2010                    .flag("moored")
2011                    .default("moored")
2012                    .build(),
2013            )
2014            .option(
2015                ClickOption::new(&["--drifting", "-d"])
2016                    .dest("ty")
2017                    .flag("drifting")
2018                    .build(),
2019            )
2020            .build();
2021
2022        let ctx_default = cmd.make_context("mine", vec![], None).unwrap();
2023        assert_eq!(ctx_default.get_param::<String>("ty"), Some(&"moored".to_string()));
2024
2025        let ctx_drifting = cmd
2026            .make_context("mine", vec!["--drifting".to_string()], None)
2027            .unwrap();
2028        assert_eq!(
2029            ctx_drifting.get_param::<String>("ty"),
2030            Some(&"drifting".to_string())
2031        );
2032
2033        let ctx_moored = cmd
2034            .make_context("mine", vec!["--moored".to_string()], None)
2035            .unwrap();
2036        assert_eq!(ctx_moored.get_param::<String>("ty"), Some(&"moored".to_string()));
2037    }
2038
2039    #[test]
2040    fn test_flag_value_group_last_option_wins() {
2041        let cmd = Command::new("mine")
2042            .option(
2043                ClickOption::new(&["--moored"])
2044                    .dest("ty")
2045                    .flag("moored")
2046                    .default("moored")
2047                    .build(),
2048            )
2049            .option(
2050                ClickOption::new(&["--drifting"])
2051                    .dest("ty")
2052                    .flag("drifting")
2053                    .build(),
2054            )
2055            .build();
2056
2057        let ctx = cmd
2058            .make_context(
2059                "mine",
2060                vec!["--moored".to_string(), "--drifting".to_string()],
2061                None,
2062            )
2063            .unwrap();
2064        assert_eq!(ctx.get_param::<String>("ty"), Some(&"drifting".to_string()));
2065    }
2066
2067    // -------------------------------------------------------------------------
2068    // Eager option (--help) tests
2069    // -------------------------------------------------------------------------
2070
2071    #[test]
2072    fn test_help_with_missing_required_arg() {
2073        // --help should work even when a required argument is missing
2074        let cmd = Command::new("test")
2075            .argument(Argument::new("required_file").build())
2076            .build();
2077
2078        // Without --help, missing required arg should fail
2079        let ctx = cmd.make_context("test", vec![], None);
2080        assert!(ctx.is_err());
2081
2082        // With --help, should exit cleanly (Exit code 0)
2083        let ctx = cmd.make_context("test", vec!["--help".to_string()], None);
2084        assert!(matches!(ctx, Err(ClickError::Exit { code: 0 })));
2085    }
2086
2087    #[test]
2088    fn test_help_with_missing_required_option() {
2089        // --help should work even when a required option is missing
2090        let cmd = Command::new("test")
2091            .option(
2092                ClickOption::new(&["--name", "-n"])
2093                    .required()
2094                    .build(),
2095            )
2096            .build();
2097
2098        // Without --help, missing required option should fail
2099        let ctx = cmd.make_context("test", vec![], None);
2100        assert!(ctx.is_err());
2101
2102        // With --help, should exit cleanly (Exit code 0)
2103        let ctx = cmd.make_context("test", vec!["--help".to_string()], None);
2104        assert!(matches!(ctx, Err(ClickError::Exit { code: 0 })));
2105    }
2106}