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            // NOTE: must `continue`, not `?` — `?` aborts the whole scan at the
941            // first option without a version metavar, missing --version whenever
942            // any other option precedes it in declaration order.
943            let Some(meta) = opt.config.metavar.as_deref() else {
944                continue;
945            };
946            let Some(output) = meta.strip_prefix(Self::VERSION_METAVAR_PREFIX) else {
947                continue;
948            };
949
950            let mut names = opt.long.iter().chain(opt.short.iter());
951            if names.any(|n| args.iter().any(|a| Self::arg_matches_opt(a, n))) {
952                return Some(output.to_string());
953            }
954        }
955        None
956    }
957
958    /// Format the usage line.
959    ///
960    /// Returns a string like: `Usage: COMMAND [OPTIONS] ARGUMENT`
961    pub fn get_usage(&self, ctx: &Context) -> String {
962        let mut pieces = Vec::new();
963
964        // Add options metavar
965        if !self.options_metavar.is_empty() {
966            pieces.push(self.options_metavar.clone());
967        }
968
969        // Add argument metavars
970        for arg in &self.arguments {
971            if !arg.hidden() {
972                pieces.push(arg.make_metavar());
973            }
974        }
975
976        format!("Usage: {} {}", ctx.command_path(), pieces.join(" "))
977    }
978
979    /// Format the help text.
980    ///
981    /// Returns the complete help output including usage, description,
982    /// options, arguments, and epilog.
983    pub fn get_help(&self, ctx: &Context) -> String {
984        let mut parts = Vec::new();
985
986        // Usage line
987        parts.push(self.get_usage(ctx));
988
989        // Help text
990        if let Some(ref help) = self.help {
991            let text = help.lines().next().unwrap_or("");
992            if !text.is_empty() {
993                parts.push(String::new()); // blank line
994                let help_text = if let Some(ref dep) = self.deprecated {
995                    if dep.is_empty() {
996                        format!("{}  (DEPRECATED)", text)
997                    } else {
998                        format!("{}  (DEPRECATED: {})", text, dep)
999                    }
1000                } else {
1001                    text.to_string()
1002                };
1003                parts.push(format!("  {}", help_text));
1004            }
1005        } else if let Some(ref dep) = self.deprecated {
1006            parts.push(String::new());
1007            let dep_msg = if dep.is_empty() {
1008                "(DEPRECATED)".to_string()
1009            } else {
1010                format!("(DEPRECATED: {})", dep)
1011            };
1012            parts.push(format!("  {}", dep_msg));
1013        }
1014
1015        // Options section
1016        let opt_records: Vec<(String, String)> = self
1017            .options
1018            .iter()
1019            .filter_map(|opt| opt.get_help_record())
1020            .collect();
1021
1022        // Add help option record
1023        let help_opt = self.get_help_option(ctx);
1024        let help_record = help_opt.as_ref().and_then(|h| h.get_help_record());
1025
1026        if !opt_records.is_empty() || help_record.is_some() {
1027            parts.push(String::new());
1028            parts.push("Options:".to_string());
1029
1030            for (opt_str, help) in &opt_records {
1031                parts.push(format!("  {}  {}", opt_str, help));
1032            }
1033            if let Some((opt_str, help)) = help_record {
1034                parts.push(format!("  {}  {}", opt_str, help));
1035            }
1036        }
1037
1038        // Arguments section (if any have help text)
1039        let arg_records: Vec<(String, String)> = self
1040            .arguments
1041            .iter()
1042            .filter_map(|arg| arg.get_help_record())
1043            .filter(|(_, help)| !help.is_empty())
1044            .collect();
1045
1046        if !arg_records.is_empty() {
1047            parts.push(String::new());
1048            parts.push("Arguments:".to_string());
1049            for (metavar, help) in &arg_records {
1050                parts.push(format!("  {}  {}", metavar, help));
1051            }
1052        }
1053
1054        // Epilog
1055        if let Some(ref epilog) = self.epilog {
1056            parts.push(String::new());
1057            parts.push(epilog.clone());
1058        }
1059
1060        parts.join("\n")
1061    }
1062
1063    /// Get the short help text for command listings.
1064    ///
1065    /// Returns `short_help` if set, otherwise derives from the first
1066    /// sentence of `help`.
1067    pub fn get_short_help(&self) -> String {
1068        if let Some(ref short_help) = self.short_help {
1069            let text = short_help.clone();
1070            if let Some(ref dep) = self.deprecated {
1071                if dep.is_empty() {
1072                    format!("{} (DEPRECATED)", text)
1073                } else {
1074                    format!("{} (DEPRECATED: {})", text, dep)
1075                }
1076            } else {
1077                text
1078            }
1079        } else if let Some(ref help) = self.help {
1080            // Get first line/sentence
1081            let text = help
1082                .lines()
1083                .next()
1084                .unwrap_or("")
1085                .split('.')
1086                .next()
1087                .unwrap_or("")
1088                .trim();
1089            if let Some(ref dep) = self.deprecated {
1090                if dep.is_empty() {
1091                    format!("{} (DEPRECATED)", text)
1092                } else {
1093                    format!("{} (DEPRECATED: {})", text, dep)
1094                }
1095            } else {
1096                text.to_string()
1097            }
1098        } else if let Some(ref dep) = self.deprecated {
1099            if dep.is_empty() {
1100                "(DEPRECATED)".to_string()
1101            } else {
1102                format!("(DEPRECATED: {})", dep)
1103            }
1104        } else {
1105            String::new()
1106        }
1107    }
1108}
1109
1110// =============================================================================
1111// CommandBuilder
1112// =============================================================================
1113
1114/// Builder for creating [`Command`] instances.
1115///
1116/// Use [`Command::new`] to create a builder, then chain methods to configure
1117/// the command, and finally call [`build`](CommandBuilder::build) to create
1118/// the command.
1119pub struct CommandBuilder {
1120    name: String,
1121    callback: Option<CommandCallback>,
1122    options: Vec<ClickOption>,
1123    arguments: Vec<Argument>,
1124    help: Option<String>,
1125    epilog: Option<String>,
1126    short_help: Option<String>,
1127    options_metavar: String,
1128    add_help_option: bool,
1129    help_option: Option<ClickOption>,
1130    no_args_is_help: bool,
1131    hidden: bool,
1132    deprecated: Option<String>,
1133    allow_extra_args: bool,
1134    allow_interspersed_args: bool,
1135    ignore_unknown_options: bool,
1136}
1137
1138impl CommandBuilder {
1139    /// Create a new command builder with the given name.
1140    pub fn new(name: &str) -> Self {
1141        Self {
1142            name: name.to_string(),
1143            callback: None,
1144            options: Vec::new(),
1145            arguments: Vec::new(),
1146            help: None,
1147            epilog: None,
1148            short_help: None,
1149            options_metavar: "[OPTIONS]".to_string(),
1150            add_help_option: true,
1151            help_option: None,
1152            no_args_is_help: false,
1153            hidden: false,
1154            deprecated: None,
1155            allow_extra_args: false,
1156            allow_interspersed_args: true,
1157            ignore_unknown_options: false,
1158        }
1159    }
1160
1161    /// Set the callback function for this command.
1162    ///
1163    /// The callback receives a reference to the [`Context`] and should return
1164    /// `Ok(())` on success or a [`ClickError`] on failure.
1165    pub fn callback<F>(mut self, f: F) -> Self
1166    where
1167        F: Fn(&Context) -> Result<(), ClickError> + Send + Sync + 'static,
1168    {
1169        self.callback = Some(Box::new(f));
1170        self
1171    }
1172
1173    /// Add an option to this command.
1174    pub fn option(mut self, opt: ClickOption) -> Self {
1175        self.options.push(opt);
1176        self
1177    }
1178
1179    /// Add an argument to this command.
1180    pub fn argument(mut self, arg: Argument) -> Self {
1181        self.arguments.push(arg);
1182        self
1183    }
1184
1185    /// Set the help text for this command.
1186    pub fn help(mut self, help: &str) -> Self {
1187        self.help = Some(help.to_string());
1188        self
1189    }
1190
1191    /// Set the epilog (text shown after help).
1192    pub fn epilog(mut self, epilog: &str) -> Self {
1193        self.epilog = Some(epilog.to_string());
1194        self
1195    }
1196
1197    /// Set the short help text for command listings.
1198    pub fn short_help(mut self, short_help: &str) -> Self {
1199        self.short_help = Some(short_help.to_string());
1200        self
1201    }
1202
1203    /// Set the options metavar (default: "[OPTIONS]").
1204    pub fn options_metavar(mut self, metavar: &str) -> Self {
1205        self.options_metavar = metavar.to_string();
1206        self
1207    }
1208
1209    /// Set whether to add a --help option (default: true).
1210    pub fn add_help_option(mut self, add: bool) -> Self {
1211        self.add_help_option = add;
1212        self
1213    }
1214
1215    /// Override the automatically generated help option.
1216    ///
1217    /// This lets you customize the help option names (e.g. `-h`, `--help`) and
1218    /// its help text while keeping it eager.
1219    ///
1220    /// Setting a custom help option implicitly enables `add_help_option`.
1221    pub fn help_option(mut self, opt: ClickOption) -> Self {
1222        self.add_help_option = true;
1223        self.help_option = Some(opt);
1224        self
1225    }
1226
1227    /// Set whether to show help if no args provided (default: false).
1228    pub fn no_args_is_help(mut self, value: bool) -> Self {
1229        self.no_args_is_help = value;
1230        self
1231    }
1232
1233    /// Hide this command from help output.
1234    pub fn hidden(mut self) -> Self {
1235        self.hidden = true;
1236        self
1237    }
1238
1239    /// Mark this command as deprecated.
1240    ///
1241    /// Pass an empty string for a generic deprecation message,
1242    /// or a custom message for more details.
1243    pub fn deprecated(mut self, message: &str) -> Self {
1244        self.deprecated = Some(message.to_string());
1245        self
1246    }
1247
1248    /// Set whether extra arguments are allowed.
1249    pub fn allow_extra_args(mut self, allow: bool) -> Self {
1250        self.allow_extra_args = allow;
1251        self
1252    }
1253
1254    /// Set whether interspersed arguments are allowed.
1255    pub fn allow_interspersed_args(mut self, allow: bool) -> Self {
1256        self.allow_interspersed_args = allow;
1257        self
1258    }
1259
1260    /// Set whether unknown options should be ignored.
1261    pub fn ignore_unknown_options(mut self, ignore: bool) -> Self {
1262        self.ignore_unknown_options = ignore;
1263        self
1264    }
1265
1266    /// Build the command.
1267    pub fn build(self) -> Command {
1268        Command {
1269            name: Some(self.name),
1270            callback: self.callback,
1271            options: self.options,
1272            arguments: self.arguments,
1273            help: self.help,
1274            epilog: self.epilog,
1275            short_help: self.short_help,
1276            options_metavar: self.options_metavar,
1277            add_help_option: self.add_help_option,
1278            no_args_is_help: self.no_args_is_help,
1279            hidden: self.hidden,
1280            deprecated: self.deprecated,
1281            allow_extra_args: self.allow_extra_args,
1282            allow_interspersed_args: self.allow_interspersed_args,
1283            ignore_unknown_options: self.ignore_unknown_options,
1284            help_option: self.help_option,
1285        }
1286    }
1287}
1288
1289// =============================================================================
1290// Helper Functions
1291// =============================================================================
1292
1293/// Create the standard help option.
1294fn make_help_option(names: &[String]) -> ClickOption {
1295    // Convert to &str for the option builder
1296    let name_refs: Vec<&str> = names.iter().map(|s| s.as_str()).collect();
1297
1298    ClickOption::new(&name_refs)
1299        .flag("true")
1300        .eager()
1301        .help("Show this message and exit.")
1302        .build()
1303}
1304
1305// =============================================================================
1306// Tests
1307// =============================================================================
1308
1309#[cfg(test)]
1310mod tests {
1311    use super::*;
1312    use crate::source::ParameterSource;
1313    use crate::types::INT;
1314    use std::collections::HashMap;
1315    use std::sync::Arc;
1316
1317    #[test]
1318    fn test_command_creation_defaults() {
1319        let cmd = Command::new("test").build();
1320
1321        assert_eq!(cmd.name, Some("test".to_string()));
1322        assert!(cmd.callback.is_none());
1323        assert!(cmd.options.is_empty());
1324        assert!(cmd.arguments.is_empty());
1325        assert!(cmd.help.is_none());
1326        assert!(cmd.epilog.is_none());
1327        assert!(cmd.short_help.is_none());
1328        assert_eq!(cmd.options_metavar, "[OPTIONS]");
1329        assert!(cmd.add_help_option);
1330        assert!(!cmd.no_args_is_help);
1331        assert!(!cmd.hidden);
1332        assert!(cmd.deprecated.is_none());
1333        assert!(!cmd.allow_extra_args);
1334        assert!(cmd.allow_interspersed_args);
1335        assert!(!cmd.ignore_unknown_options);
1336    }
1337
1338    #[test]
1339    fn test_command_builder_chain() {
1340        let cmd = Command::new("hello")
1341            .help("Say hello to someone")
1342            .epilog("Example: hello --name World")
1343            .short_help("Say hello")
1344            .options_metavar("[OPTS]")
1345            .add_help_option(false)
1346            .no_args_is_help(true)
1347            .hidden()
1348            .deprecated("Use 'greet' instead")
1349            .allow_extra_args(true)
1350            .allow_interspersed_args(false)
1351            .ignore_unknown_options(true)
1352            .build();
1353
1354        assert_eq!(cmd.name, Some("hello".to_string()));
1355        assert_eq!(cmd.help, Some("Say hello to someone".to_string()));
1356        assert_eq!(cmd.epilog, Some("Example: hello --name World".to_string()));
1357        assert_eq!(cmd.short_help, Some("Say hello".to_string()));
1358        assert_eq!(cmd.options_metavar, "[OPTS]");
1359        assert!(!cmd.add_help_option);
1360        assert!(cmd.no_args_is_help);
1361        assert!(cmd.hidden);
1362        assert_eq!(cmd.deprecated, Some("Use 'greet' instead".to_string()));
1363        assert!(cmd.allow_extra_args);
1364        assert!(!cmd.allow_interspersed_args);
1365        assert!(cmd.ignore_unknown_options);
1366    }
1367
1368    #[test]
1369    fn test_command_with_callback() {
1370        use std::sync::atomic::{AtomicBool, Ordering};
1371
1372        let called = Arc::new(AtomicBool::new(false));
1373        let called_clone = Arc::clone(&called);
1374
1375        let cmd = Command::new("test")
1376            .callback(move |_ctx| {
1377                called_clone.store(true, Ordering::SeqCst);
1378                Ok(())
1379            })
1380            .build();
1381
1382        assert!(cmd.callback.is_some());
1383
1384        // Create a minimal context and invoke
1385        let ctx = ContextBuilder::new().info_name("test").build();
1386        let result = cmd.invoke(&ctx);
1387        assert!(result.is_ok());
1388        assert!(called.load(Ordering::SeqCst));
1389    }
1390
1391    #[test]
1392    fn test_command_with_option() {
1393        let cmd = Command::new("greet")
1394            .option(
1395                ClickOption::new(&["--name", "-n"])
1396                    .help("Name to greet")
1397                    .default("World")
1398                    .build(),
1399            )
1400            .build();
1401
1402        assert_eq!(cmd.options.len(), 1);
1403        assert_eq!(cmd.options[0].name(), "name");
1404    }
1405
1406    #[test]
1407    fn test_command_with_argument() {
1408        let cmd = Command::new("cat")
1409            .argument(Argument::new("file").help("File to read").build())
1410            .build();
1411
1412        assert_eq!(cmd.arguments.len(), 1);
1413        assert_eq!(cmd.arguments[0].name(), "file");
1414    }
1415
1416    #[test]
1417    fn test_command_with_multiple_params() {
1418        let cmd = Command::new("copy")
1419            .option(
1420                ClickOption::new(&["--recursive", "-r"])
1421                    .flag("true")
1422                    .help("Copy recursively")
1423                    .build(),
1424            )
1425            .argument(Argument::new("src").help("Source path").build())
1426            .argument(Argument::new("dst").help("Destination path").build())
1427            .build();
1428
1429        assert_eq!(cmd.options.len(), 1);
1430        assert_eq!(cmd.arguments.len(), 2);
1431    }
1432
1433    #[test]
1434    fn test_make_context_basic() {
1435        let cmd = Command::new("hello").build();
1436        let ctx = cmd.make_context("hello", vec![], None);
1437
1438        assert!(ctx.is_ok());
1439        let ctx = ctx.unwrap();
1440        assert_eq!(ctx.info_name(), Some("hello"));
1441    }
1442
1443    #[test]
1444    fn test_parse_args_with_option() {
1445        let cmd = Command::new("greet")
1446            .option(
1447                ClickOption::new(&["--name", "-n"])
1448                    .default("World")
1449                    .build(),
1450            )
1451            .build();
1452
1453        let ctx = cmd.make_context("greet", vec!["--name".to_string(), "Alice".to_string()], None);
1454        assert!(ctx.is_ok());
1455
1456        let ctx = ctx.unwrap();
1457        let name = ctx.get_param::<String>("name");
1458        assert_eq!(name, Some(&"Alice".to_string()));
1459    }
1460
1461    #[test]
1462    fn test_parse_args_with_argument() {
1463        let cmd = Command::new("cat")
1464            .argument(Argument::new("file").build())
1465            .build();
1466
1467        let ctx = cmd.make_context("cat", vec!["test.txt".to_string()], None);
1468        assert!(ctx.is_ok());
1469
1470        let ctx = ctx.unwrap();
1471        let file = ctx.get_param::<String>("file");
1472        assert_eq!(file, Some(&"test.txt".to_string()));
1473    }
1474
1475    #[test]
1476    fn test_parse_args_missing_required() {
1477        let cmd = Command::new("cat")
1478            .argument(Argument::new("file").build())
1479            .build();
1480
1481        let ctx = cmd.make_context("cat", vec![], None);
1482        assert!(ctx.is_err());
1483
1484        let err = ctx.unwrap_err();
1485        assert!(matches!(err, ClickError::MissingParameter { .. }));
1486    }
1487
1488    #[test]
1489    fn test_parse_args_extra_args_error() {
1490        let cmd = Command::new("hello").build();
1491
1492        let ctx = cmd.make_context("hello", vec!["extra".to_string()], None);
1493        assert!(ctx.is_err());
1494
1495        let err = ctx.unwrap_err();
1496        assert!(matches!(err, ClickError::UsageError { .. }));
1497    }
1498
1499    #[test]
1500    fn test_parse_args_extra_args_allowed() {
1501        let cmd = Command::new("hello").allow_extra_args(true).build();
1502
1503        let ctx = cmd.make_context("hello", vec!["extra".to_string()], None);
1504        assert!(ctx.is_ok());
1505
1506        let ctx = ctx.unwrap();
1507        assert_eq!(ctx.args(), &["extra".to_string()]);
1508    }
1509
1510    #[test]
1511    fn test_get_usage() {
1512        let cmd = Command::new("copy")
1513            .argument(Argument::new("src").build())
1514            .argument(Argument::new("dst").build())
1515            .build();
1516
1517        let ctx = ContextBuilder::new().info_name("copy").build();
1518        let usage = cmd.get_usage(&ctx);
1519
1520        assert!(usage.contains("Usage:"));
1521        assert!(usage.contains("copy"));
1522        assert!(usage.contains("[OPTIONS]"));
1523        assert!(usage.contains("SRC"));
1524        assert!(usage.contains("DST"));
1525    }
1526
1527    #[test]
1528    fn test_get_help() {
1529        let cmd = Command::new("greet")
1530            .help("Greet someone")
1531            .option(
1532                ClickOption::new(&["--name", "-n"])
1533                    .help("Name to greet")
1534                    .build(),
1535            )
1536            .epilog("Example: greet --name World")
1537            .build();
1538
1539        let ctx = ContextBuilder::new().info_name("greet").build();
1540        let help = cmd.get_help(&ctx);
1541
1542        assert!(help.contains("Usage:"));
1543        assert!(help.contains("Greet someone"));
1544        assert!(help.contains("Options:"));
1545        assert!(help.contains("--name"));
1546        assert!(help.contains("Name to greet"));
1547        assert!(help.contains("Example:"));
1548    }
1549
1550    #[test]
1551    fn test_get_short_help() {
1552        // Explicit short_help
1553        let cmd = Command::new("test").short_help("Test command").build();
1554        assert_eq!(cmd.get_short_help(), "Test command");
1555
1556        // Derived from help
1557        let cmd = Command::new("test")
1558            .help("This is a test. It does things.")
1559            .build();
1560        assert_eq!(cmd.get_short_help(), "This is a test");
1561
1562        // With deprecation
1563        let cmd = Command::new("test")
1564            .short_help("Test command")
1565            .deprecated("Use 'new-test' instead")
1566            .build();
1567        assert!(cmd.get_short_help().contains("DEPRECATED"));
1568        assert!(cmd.get_short_help().contains("Use 'new-test' instead"));
1569    }
1570
1571    #[test]
1572    fn test_make_help_option() {
1573        let help_opt = make_help_option(&["--help".to_string(), "-h".to_string()]);
1574
1575        assert!(help_opt.is_flag);
1576        assert!(help_opt.is_eager());
1577        assert_eq!(help_opt.help(), Some("Show this message and exit."));
1578    }
1579
1580    #[test]
1581    fn test_command_debug() {
1582        let cmd = Command::new("test").build();
1583        let debug_str = format!("{:?}", cmd);
1584
1585        assert!(debug_str.contains("Command"));
1586        assert!(debug_str.contains("test"));
1587    }
1588
1589    #[test]
1590    fn test_invoke_with_deprecation() {
1591        let cmd = Command::new("old")
1592            .deprecated("Use 'new' instead")
1593            .callback(|_| Ok(()))
1594            .build();
1595
1596        let ctx = ContextBuilder::new().info_name("old").build();
1597        // This will print a deprecation warning to stderr
1598        let result = cmd.invoke(&ctx);
1599        assert!(result.is_ok());
1600    }
1601
1602    #[test]
1603    fn test_parse_flag_option() {
1604        let cmd = Command::new("test")
1605            .option(
1606                ClickOption::new(&["--verbose", "-v"])
1607                    .flag("true")
1608                    .build(),
1609            )
1610            .build();
1611
1612        let ctx = cmd.make_context("test", vec!["--verbose".to_string()], None);
1613        assert!(ctx.is_ok());
1614
1615        let ctx = ctx.unwrap();
1616        let verbose = ctx.get_param::<String>("verbose");
1617        assert_eq!(verbose, Some(&"true".to_string()));
1618    }
1619
1620    #[test]
1621    fn test_parse_count_option() {
1622        let cmd = Command::new("test")
1623            .option(ClickOption::new(&["--verbose", "-v"]).count().build())
1624            .build();
1625
1626        let ctx = cmd.make_context(
1627            "test",
1628            vec!["-v".to_string(), "-v".to_string(), "-v".to_string()],
1629            None,
1630        );
1631        assert!(ctx.is_ok());
1632
1633        let ctx = ctx.unwrap();
1634        let verbose = ctx.get_param::<usize>("verbose");
1635        assert_eq!(verbose, Some(&3));
1636    }
1637
1638    #[test]
1639    fn test_option_type_conversion() {
1640        let cmd = Command::new("test")
1641            .option(ClickOption::new(&["--count"]).type_any(INT).build())
1642            .build();
1643
1644        let ctx = cmd.make_context("test", vec!["--count".to_string(), "42".to_string()], None);
1645        assert!(ctx.is_ok());
1646
1647        let ctx = ctx.unwrap();
1648        let count = ctx.get_param::<i64>("count");
1649        assert_eq!(count, Some(&42));
1650    }
1651
1652    #[test]
1653    fn test_option_callback_applied() {
1654        let cmd = Command::new("test")
1655            .option(
1656                ClickOption::new(&["--count"])
1657                    .type_any(INT)
1658                    .callback(|_ctx, _param, value| {
1659                        let count = *value
1660                            .as_ref()
1661                            .downcast_ref::<i64>()
1662                            .ok_or_else(|| ClickError::bad_parameter("bad callback value"))?;
1663                        Ok(Arc::new(count + 1))
1664                    })
1665                    .build(),
1666            )
1667            .build();
1668
1669        let ctx = cmd.make_context("test", vec!["--count".to_string(), "2".to_string()], None);
1670        assert!(ctx.is_ok());
1671
1672        let ctx = ctx.unwrap();
1673        let count = ctx.get_param::<i64>("count");
1674        assert_eq!(count, Some(&3));
1675    }
1676
1677    #[test]
1678    fn test_option_with_default() {
1679        let cmd = Command::new("greet")
1680            .option(
1681                ClickOption::new(&["--name"])
1682                    .default("World")
1683                    .build(),
1684            )
1685            .build();
1686
1687        // Without providing the option
1688        let ctx = cmd.make_context("greet", vec![], None);
1689        assert!(ctx.is_ok());
1690
1691        let ctx = ctx.unwrap();
1692        let name = ctx.get_param::<String>("name");
1693        assert_eq!(name, Some(&"World".to_string()));
1694        assert_eq!(ctx.get_parameter_source("name"), Some(ParameterSource::Default));
1695    }
1696
1697    #[test]
1698    fn test_option_default_map_value() {
1699        let cmd = Command::new("greet")
1700            .option(ClickOption::new(&["--name"]).build())
1701            .build();
1702
1703        let mut defaults: HashMap<String, Arc<dyn std::any::Any + Send + Sync>> = HashMap::new();
1704        defaults.insert("name".to_string(), Arc::new("Bob".to_string()));
1705
1706        let mut ctx = ContextBuilder::new()
1707            .info_name("greet")
1708            .default_map(defaults)
1709            .build();
1710
1711        let result = cmd.parse_args(&mut ctx, vec![]);
1712        assert!(result.is_ok());
1713
1714        let name = ctx.get_param::<String>("name");
1715        assert_eq!(name, Some(&"Bob".to_string()));
1716        assert_eq!(
1717            ctx.get_parameter_source("name"),
1718            Some(ParameterSource::DefaultMap)
1719        );
1720    }
1721
1722    #[test]
1723    fn test_option_envvar_value() {
1724        std::env::set_var("CLICK_TEST_COUNT", "9");
1725        let cmd = Command::new("test")
1726            .option(
1727                ClickOption::new(&["--count"])
1728                    .envvar("CLICK_TEST_COUNT")
1729                    .type_any(INT)
1730                    .build(),
1731            )
1732            .build();
1733
1734        let ctx = cmd.make_context("test", vec![], None);
1735        std::env::remove_var("CLICK_TEST_COUNT");
1736
1737        assert!(ctx.is_ok());
1738        let ctx = ctx.unwrap();
1739        let count = ctx.get_param::<i64>("count");
1740        assert_eq!(count, Some(&9));
1741        assert_eq!(
1742            ctx.get_parameter_source("count"),
1743            Some(ParameterSource::Environment)
1744        );
1745    }
1746
1747    #[test]
1748    fn test_argument_type_conversion() {
1749        let cmd = Command::new("greet")
1750            .argument(Argument::new("count").type_(INT).build())
1751            .build();
1752
1753        let ctx = cmd.make_context("greet", vec!["7".to_string()], None);
1754        assert!(ctx.is_ok());
1755
1756        let ctx = ctx.unwrap();
1757        let count = ctx.get_param::<i64>("count");
1758        assert_eq!(count, Some(&7));
1759        assert_eq!(
1760            ctx.get_parameter_source("count"),
1761            Some(ParameterSource::CommandLine)
1762        );
1763    }
1764
1765    #[test]
1766    fn test_argument_callback_applied() {
1767        let cmd = Command::new("greet")
1768            .argument(
1769                Argument::new("count")
1770                    .type_(INT)
1771                    .callback(|_ctx, _param, value| {
1772                        let count = *value
1773                            .as_ref()
1774                            .downcast_ref::<i64>()
1775                            .ok_or_else(|| ClickError::bad_parameter("bad callback value"))?;
1776                        Ok(Arc::new(count * 2))
1777                    })
1778                    .build(),
1779            )
1780            .build();
1781
1782        let ctx = cmd.make_context("greet", vec!["3".to_string()], None);
1783        assert!(ctx.is_ok());
1784
1785        let ctx = ctx.unwrap();
1786        let count = ctx.get_param::<i64>("count");
1787        assert_eq!(count, Some(&6));
1788    }
1789
1790    #[test]
1791    fn test_argument_type_conversion_error() {
1792        let cmd = Command::new("greet")
1793            .argument(Argument::new("count").type_(INT).build())
1794            .build();
1795
1796        let ctx = cmd.make_context("greet", vec!["nope".to_string()], None);
1797        assert!(matches!(ctx, Err(ClickError::BadParameter { .. })));
1798    }
1799
1800    #[test]
1801    fn test_argument_with_default() {
1802        let cmd = Command::new("greet")
1803            .argument(Argument::new("name").default("World").build())
1804            .build();
1805
1806        // Without providing the argument
1807        let ctx = cmd.make_context("greet", vec![], None);
1808        assert!(ctx.is_ok());
1809
1810        let ctx = ctx.unwrap();
1811        let name = ctx.get_param::<String>("name");
1812        assert_eq!(name, Some(&"World".to_string()));
1813    }
1814
1815    #[test]
1816    fn test_argument_default_map_value() {
1817        let cmd = Command::new("greet")
1818            .argument(Argument::new("name").build())
1819            .build();
1820
1821        let mut defaults: HashMap<String, Arc<dyn std::any::Any + Send + Sync>> = HashMap::new();
1822        defaults.insert("name".to_string(), Arc::new("Alice".to_string()));
1823
1824        let mut ctx = ContextBuilder::new()
1825            .info_name("greet")
1826            .default_map(defaults)
1827            .build();
1828
1829        let result = cmd.parse_args(&mut ctx, vec![]);
1830        assert!(result.is_ok());
1831
1832        let name = ctx.get_param::<String>("name");
1833        assert_eq!(name, Some(&"Alice".to_string()));
1834    }
1835
1836    #[test]
1837    fn test_argument_auto_envvar_prefix() {
1838        std::env::set_var("MYAPP_NAME", "Alice");
1839        let cmd = Command::new("greet")
1840            .argument(Argument::new("name").build())
1841            .build();
1842
1843        let mut ctx = ContextBuilder::new()
1844            .info_name("greet")
1845            .auto_envvar_prefix("MYAPP")
1846            .build();
1847
1848        let result = cmd.parse_args(&mut ctx, vec![]);
1849        std::env::remove_var("MYAPP_NAME");
1850
1851        assert!(result.is_ok());
1852        let name = ctx.get_param::<String>("name");
1853        assert_eq!(name, Some(&"Alice".to_string()));
1854    }
1855
1856    // -------------------------------------------------------------------------
1857    // Optional positional argument tests (fix for Nargs::Optional)
1858    // -------------------------------------------------------------------------
1859
1860    #[test]
1861    fn test_optional_argument_with_value() {
1862        use crate::parameter::Nargs;
1863
1864        let cmd = Command::new("test")
1865            .argument(
1866                Argument::new("file")
1867                    .nargs(Nargs::Optional)
1868                    .default("default.txt")
1869                    .build(),
1870            )
1871            .build();
1872
1873        // With a value provided
1874        let ctx = cmd.make_context("test", vec!["input.txt".to_string()], None);
1875        assert!(ctx.is_ok());
1876
1877        let ctx = ctx.unwrap();
1878        let file = ctx.get_param::<String>("file");
1879        assert_eq!(file, Some(&"input.txt".to_string()));
1880    }
1881
1882    #[test]
1883    fn test_optional_argument_without_value() {
1884        use crate::parameter::Nargs;
1885
1886        let cmd = Command::new("test")
1887            .argument(
1888                Argument::new("file")
1889                    .nargs(Nargs::Optional)
1890                    .default("default.txt")
1891                    .build(),
1892            )
1893            .build();
1894
1895        // Without providing the optional argument - should use default
1896        let ctx = cmd.make_context("test", vec![], None);
1897        assert!(ctx.is_ok());
1898
1899        let ctx = ctx.unwrap();
1900        let file = ctx.get_param::<String>("file");
1901        assert_eq!(file, Some(&"default.txt".to_string()));
1902    }
1903
1904    #[test]
1905    fn test_required_followed_by_optional_argument() {
1906        use crate::parameter::Nargs;
1907
1908        let cmd = Command::new("copy")
1909            .argument(Argument::new("src").build()) // required
1910            .argument(
1911                Argument::new("dst")
1912                    .nargs(Nargs::Optional)
1913                    .default(".")
1914                    .build(),
1915            )
1916            .build();
1917
1918        // Only providing required arg
1919        let ctx = cmd.make_context("copy", vec!["source.txt".to_string()], None);
1920        assert!(ctx.is_ok());
1921
1922        let ctx = ctx.unwrap();
1923        let src = ctx.get_param::<String>("src");
1924        let dst = ctx.get_param::<String>("dst");
1925        assert_eq!(src, Some(&"source.txt".to_string()));
1926        assert_eq!(dst, Some(&".".to_string()));
1927    }
1928
1929    #[test]
1930    fn test_required_followed_by_optional_with_both_values() {
1931        use crate::parameter::Nargs;
1932
1933        let cmd = Command::new("copy")
1934            .argument(Argument::new("src").build()) // required
1935            .argument(
1936                Argument::new("dst")
1937                    .nargs(Nargs::Optional)
1938                    .default(".")
1939                    .build(),
1940            )
1941            .build();
1942
1943        // Providing both args
1944        let ctx = cmd.make_context(
1945            "copy",
1946            vec!["source.txt".to_string(), "dest.txt".to_string()],
1947            None,
1948        );
1949        assert!(ctx.is_ok());
1950
1951        let ctx = ctx.unwrap();
1952        let src = ctx.get_param::<String>("src");
1953        let dst = ctx.get_param::<String>("dst");
1954        assert_eq!(src, Some(&"source.txt".to_string()));
1955        assert_eq!(dst, Some(&"dest.txt".to_string()));
1956    }
1957
1958    // -------------------------------------------------------------------------
1959    // Multi-value argument error test (fix for issue 2)
1960    // -------------------------------------------------------------------------
1961
1962    #[test]
1963    fn test_multi_value_argument_incomplete_error() {
1964        use crate::parameter::Nargs;
1965
1966        let cmd = Command::new("point")
1967            .argument(Argument::new("coords").nargs(Nargs::Count(3)).build())
1968            .build();
1969
1970        // Only providing 2 of 3 required values
1971        let ctx = cmd.make_context(
1972            "point",
1973            vec!["1".to_string(), "2".to_string()],
1974            None,
1975        );
1976
1977        // Should fail with an error about missing values
1978        assert!(ctx.is_err());
1979        let err = ctx.unwrap_err();
1980        assert!(err.to_string().contains("takes 3 values"));
1981    }
1982
1983    // -------------------------------------------------------------------------
1984    // Optional option value test (fix for issue 4)
1985    // -------------------------------------------------------------------------
1986
1987    #[test]
1988    fn test_optional_option_value_with_flag_value() {
1989        use crate::parameter::Nargs;
1990
1991        let cmd = Command::new("test")
1992            .option(
1993                ClickOption::new(&["--opt"])
1994                    .nargs(Nargs::Optional)
1995                    .flag("flagval") // Used when option is used without value
1996                    .default("default")
1997                    .build(),
1998            )
1999            .build();
2000
2001        // Using --opt followed by another option (would trigger FlagNeedsValue)
2002        let ctx = cmd.make_context("test", vec!["--opt".to_string()], None);
2003        assert!(ctx.is_ok());
2004
2005        let ctx = ctx.unwrap();
2006        let opt = ctx.get_param::<String>("opt");
2007        // Should use the flag_value since --opt was used without an argument
2008        assert_eq!(opt, Some(&"flagval".to_string()));
2009    }
2010
2011    #[test]
2012    fn test_flag_value_group_shared_destination() {
2013        let cmd = Command::new("mine")
2014            .option(
2015                ClickOption::new(&["--moored", "-m"])
2016                    .dest("ty")
2017                    .flag("moored")
2018                    .default("moored")
2019                    .build(),
2020            )
2021            .option(
2022                ClickOption::new(&["--drifting", "-d"])
2023                    .dest("ty")
2024                    .flag("drifting")
2025                    .build(),
2026            )
2027            .build();
2028
2029        let ctx_default = cmd.make_context("mine", vec![], None).unwrap();
2030        assert_eq!(ctx_default.get_param::<String>("ty"), Some(&"moored".to_string()));
2031
2032        let ctx_drifting = cmd
2033            .make_context("mine", vec!["--drifting".to_string()], None)
2034            .unwrap();
2035        assert_eq!(
2036            ctx_drifting.get_param::<String>("ty"),
2037            Some(&"drifting".to_string())
2038        );
2039
2040        let ctx_moored = cmd
2041            .make_context("mine", vec!["--moored".to_string()], None)
2042            .unwrap();
2043        assert_eq!(ctx_moored.get_param::<String>("ty"), Some(&"moored".to_string()));
2044    }
2045
2046    #[test]
2047    fn test_flag_value_group_last_option_wins() {
2048        let cmd = Command::new("mine")
2049            .option(
2050                ClickOption::new(&["--moored"])
2051                    .dest("ty")
2052                    .flag("moored")
2053                    .default("moored")
2054                    .build(),
2055            )
2056            .option(
2057                ClickOption::new(&["--drifting"])
2058                    .dest("ty")
2059                    .flag("drifting")
2060                    .build(),
2061            )
2062            .build();
2063
2064        let ctx = cmd
2065            .make_context(
2066                "mine",
2067                vec!["--moored".to_string(), "--drifting".to_string()],
2068                None,
2069            )
2070            .unwrap();
2071        assert_eq!(ctx.get_param::<String>("ty"), Some(&"drifting".to_string()));
2072    }
2073
2074    // -------------------------------------------------------------------------
2075    // Eager option (--help) tests
2076    // -------------------------------------------------------------------------
2077
2078    #[test]
2079    fn test_help_with_missing_required_arg() {
2080        // --help should work even when a required argument is missing
2081        let cmd = Command::new("test")
2082            .argument(Argument::new("required_file").build())
2083            .build();
2084
2085        // Without --help, missing required arg should fail
2086        let ctx = cmd.make_context("test", vec![], None);
2087        assert!(ctx.is_err());
2088
2089        // With --help, should exit cleanly (Exit code 0)
2090        let ctx = cmd.make_context("test", vec!["--help".to_string()], None);
2091        assert!(matches!(ctx, Err(ClickError::Exit { code: 0 })));
2092    }
2093
2094    #[test]
2095    fn test_help_with_missing_required_option() {
2096        // --help should work even when a required option is missing
2097        let cmd = Command::new("test")
2098            .option(
2099                ClickOption::new(&["--name", "-n"])
2100                    .required()
2101                    .build(),
2102            )
2103            .build();
2104
2105        // Without --help, missing required option should fail
2106        let ctx = cmd.make_context("test", vec![], None);
2107        assert!(ctx.is_err());
2108
2109        // With --help, should exit cleanly (Exit code 0)
2110        let ctx = cmd.make_context("test", vec!["--help".to_string()], None);
2111        assert!(matches!(ctx, Err(ClickError::Exit { code: 0 })));
2112    }
2113}