Skip to main content

click/
option.rs

1//! Option parameter for click-rs.
2//!
3//! This module provides the `ClickOption` struct for named command-line parameters
4//! (e.g., `--name`, `-n`). Options differ from arguments in that they are
5//! typically optional and can have flags, boolean modes, and prompts.
6//!
7//! # Reference
8//!
9//! Based on Python Click's `core.py:Option` class (line 2641+).
10
11use std::any::Any;
12use std::fmt;
13use std::sync::Arc;
14
15use crate::context::Context;
16use crate::error::ClickError;
17use crate::parameter::{Nargs, Parameter, ParameterCallback, ParameterConfig};
18use crate::argument::AnyTypeConverter;
19use crate::types::{CompletionItem, StringType, TypeConverter, STRING};
20
21/// Custom shell completion callback type for option values.
22pub type ShellCompleteCallback = Arc<dyn Fn(&Context, &str) -> Vec<CompletionItem> + Send + Sync>;
23
24// =============================================================================
25// Option Name Parsing
26// =============================================================================
27
28/// Parse a single option name, returning (stripped_name, is_long).
29///
30/// - Long options start with "--" (e.g., "--name" -> ("name", true))
31/// - Short options start with "-" and are single char (e.g., "-n" -> ("n", false))
32///
33/// # Errors
34///
35/// Returns an error if the name is not a valid option format.
36pub fn parse_option_name(name: &str) -> Result<(String, bool), String> {
37    if let Some(stripped) = name.strip_prefix("--") {
38        if stripped.is_empty() {
39            return Err("Long option name cannot be empty: '--'".to_string());
40        }
41        if stripped.starts_with('-') {
42            return Err(format!(
43                "Long option name cannot start with dash: '{}'",
44                name
45            ));
46        }
47        Ok((stripped.to_string(), true))
48    } else if let Some(stripped) = name.strip_prefix('-') {
49        if stripped.is_empty() {
50            return Err("Short option name cannot be empty: '-'".to_string());
51        }
52        if stripped.len() != 1 {
53            return Err(format!(
54                "Short option must be a single character: '{}' (use '--{}' for long options)",
55                name, stripped
56            ));
57        }
58        if stripped.starts_with('-') {
59            return Err(format!("Short option cannot be a dash: '{}'", name));
60        }
61        Ok((stripped.to_string(), false))
62    } else {
63        Err(format!(
64            "Option name must start with '-' or '--': '{}'",
65            name
66        ))
67    }
68}
69
70/// Split option names into (long_options, short_options).
71///
72/// Each name is validated and categorized. The first valid long option
73/// (or first short option if no long) determines the parameter name.
74///
75/// # Returns
76///
77/// A tuple of (long_options, short_options) where each is a Vec of the
78/// original names (with dashes).
79pub fn split_option_names(names: &[&str]) -> Result<(Vec<String>, Vec<String>), String> {
80    let mut long = Vec::new();
81    let mut short = Vec::new();
82
83    for name in names {
84        let (_, is_long) = parse_option_name(name)?;
85        if is_long {
86            long.push(name.to_string());
87        } else {
88            short.push(name.to_string());
89        }
90    }
91
92    if long.is_empty() && short.is_empty() {
93        return Err("At least one option name is required".to_string());
94    }
95
96    Ok((long, short))
97}
98
99/// Derive the parameter name from option names.
100///
101/// Prefers long options over short. Strips dashes and replaces '-' with '_'.
102fn derive_param_name(long: &[String], short: &[String]) -> String {
103    let source = if !long.is_empty() {
104        // Use the longest long option (most descriptive)
105        long.iter()
106            .max_by_key(|s| s.len())
107            .map(|s| s.trim_start_matches('-'))
108            .unwrap_or("")
109    } else if !short.is_empty() {
110        short
111            .first()
112            .map(|s| s.trim_start_matches('-'))
113            .unwrap_or("")
114    } else {
115        ""
116    };
117
118    source.replace('-', "_")
119}
120
121// =============================================================================
122// ClickOption Struct
123// =============================================================================
124
125/// A named command-line parameter (e.g., --name, -n).
126///
127/// Options are typically optional values that can be specified with flags.
128/// They support various modes including:
129/// - Regular value options (`--name VALUE`)
130/// - Flags (`--verbose`)
131/// - Boolean flags (`--flag/--no-flag`)
132/// - Count mode (`-v -v -v` = 3)
133/// - Prompts for interactive input
134///
135/// Note: Named `ClickOption` to avoid shadowing `std::option::Option`.
136#[derive(Clone)]
137pub struct ClickOption {
138    /// Base parameter configuration.
139    pub config: ParameterConfig,
140
141    /// Long option names (e.g., ["--name", "--full-name"]).
142    pub long: Vec<String>,
143
144    /// Short option names (e.g., ["-n", "-N"]).
145    pub short: Vec<String>,
146
147    /// Whether this is a flag (no value required).
148    pub is_flag: bool,
149
150    /// Whether this is a boolean flag (--flag/--no-flag).
151    pub is_bool_flag: bool,
152
153    /// Value when flag is present (None means true for bool flags).
154    pub flag_value: Option<String>,
155
156    /// Secondary value for --no-flag (None means false for bool flags).
157    pub secondary_value: Option<String>,
158
159    /// Count mode (-v -v -v = 3).
160    pub count: bool,
161
162    /// Prompt text for interactive input (None = no prompt).
163    pub prompt: Option<String>,
164
165    /// Whether to ask twice (confirmation).
166    pub confirmation_prompt: bool,
167
168    /// Hide input (for passwords).
169    pub hide_input: bool,
170
171    /// Show default value in help text.
172    pub show_default: bool,
173
174    /// Show environment variable in help text.
175    pub show_envvar: bool,
176
177    /// Default value as string.
178    pub default: Option<String>,
179
180    /// Type name (for display/debugging).
181    type_name: String,
182    /// Type metavar (for help text).
183    type_metavar: Option<String>,
184    /// Type converter (type-erased).
185    type_converter: Arc<dyn AnyTypeConverter>,
186
187    /// Custom shell completion callback for option values.
188    shell_complete_callback: Option<ShellCompleteCallback>,
189}
190
191impl fmt::Debug for ClickOption {
192    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193        f.debug_struct("ClickOption")
194            .field("config", &self.config)
195            .field("long", &self.long)
196            .field("short", &self.short)
197            .field("is_flag", &self.is_flag)
198            .field("is_bool_flag", &self.is_bool_flag)
199            .field("flag_value", &self.flag_value)
200            .field("secondary_value", &self.secondary_value)
201            .field("count", &self.count)
202            .field("prompt", &self.prompt)
203            .field("confirmation_prompt", &self.confirmation_prompt)
204            .field("hide_input", &self.hide_input)
205            .field("show_default", &self.show_default)
206            .field("show_envvar", &self.show_envvar)
207            .field("default", &self.default)
208            .field("type_name", &self.type_name)
209            .field("has_shell_complete", &self.shell_complete_callback.is_some())
210            .field("has_type_converter", &true)
211            .finish()
212    }
213}
214
215impl ClickOption {
216    /// Create a new option builder with the given names.
217    ///
218    /// Names should include the dashes (e.g., "--name", "-n").
219    ///
220    /// # Panics
221    ///
222    /// Panics if no valid option names are provided.
223    #[allow(clippy::new_ret_no_self)]
224    pub fn new(names: &[&str]) -> OptionBuilder {
225        OptionBuilder::new(names)
226    }
227
228    /// Get all option names (long and short) joined for display.
229    pub fn opts_string(&self) -> String {
230        let mut parts: Vec<&str> = Vec::new();
231        for s in &self.short {
232            parts.push(s);
233        }
234        for l in &self.long {
235            parts.push(l);
236        }
237        parts.join(", ")
238    }
239
240    /// Get the primary option string (first long or first short).
241    pub fn primary_opt(&self) -> &str {
242        self.long
243            .first()
244            .or(self.short.first())
245            .map(|s| s.as_str())
246            .unwrap_or("")
247    }
248
249    /// Get the type converter for this option.
250    pub fn type_converter(&self) -> &dyn AnyTypeConverter {
251        self.type_converter.as_ref()
252    }
253
254    /// Convert a string value using this option's type converter.
255    /// Returns the converted value as a boxed Any type.
256    pub fn convert_any(&self, value: &str) -> Result<Box<dyn Any + Send + Sync>, String> {
257        self.type_converter.convert_any(value)
258    }
259
260    /// Convert multiple string values using this option's type converter.
261    /// Returns the converted value as a boxed Any type.
262    pub fn convert_multi(&self, values: &[String]) -> Result<Box<dyn Any + Send + Sync>, String> {
263        self.type_converter.convert_multi(values)
264    }
265
266    /// Get shell completions for this option value.
267    ///
268    /// If a custom shell completion callback is set, it takes precedence over
269    /// type-driven completions.
270    pub fn get_completions(&self, ctx: &Context, incomplete: &str) -> Vec<CompletionItem> {
271        if let Some(ref callback) = self.shell_complete_callback {
272            callback(ctx, incomplete)
273        } else {
274            self.type_converter.shell_complete(incomplete)
275        }
276    }
277
278    /// Check if this option has a custom shell completion callback.
279    pub fn has_shell_complete_callback(&self) -> bool {
280        self.shell_complete_callback.is_some()
281    }
282}
283
284impl Parameter for ClickOption {
285    fn name(&self) -> &str {
286        &self.config.name
287    }
288
289    fn human_readable_name(&self) -> String {
290        self.opts_string()
291    }
292
293    fn nargs(&self) -> Nargs {
294        self.config.nargs
295    }
296
297    fn multiple(&self) -> bool {
298        self.config.multiple
299    }
300
301    fn is_eager(&self) -> bool {
302        self.config.is_eager
303    }
304
305    fn expose_value(&self) -> bool {
306        self.config.expose_value
307    }
308
309    fn required(&self) -> bool {
310        self.config.required
311    }
312
313    fn envvar(&self) -> Option<&[String]> {
314        self.config.envvar.as_deref()
315    }
316
317    fn help(&self) -> Option<&str> {
318        self.config.help.as_deref()
319    }
320
321    fn hidden(&self) -> bool {
322        self.config.hidden
323    }
324
325    fn get_metavar(&self) -> Option<String> {
326        // Custom metavar takes precedence
327        if let Some(ref mv) = self.config.metavar {
328            return Some(mv.clone());
329        }
330        // Otherwise use type's metavar
331        if let Some(ref mv) = self.type_metavar {
332            if mv.contains('|') && !(mv.starts_with('[') && mv.ends_with(']')) {
333                return Some(format!("[{}]", mv));
334            }
335            return Some(mv.clone());
336        }
337        if !self.type_name.is_empty() {
338            return Some(self.type_name.clone());
339        }
340        None
341    }
342
343    fn get_help_record(&self) -> Option<(String, String)> {
344        if self.hidden() {
345            return None;
346        }
347
348        // Build the option string
349        let mut opt_parts = Vec::new();
350
351        // Add short options first
352        for s in &self.short {
353            opt_parts.push(s.clone());
354        }
355
356        // Add long options
357        for l in &self.long {
358            opt_parts.push(l.clone());
359        }
360
361        let mut opt_str = opt_parts.join(", ");
362
363        // Add metavar for non-flag options
364        if !self.is_flag && !self.count {
365            if let Some(metavar) = self.get_metavar() {
366                opt_str.push(' ');
367                opt_str.push_str(&metavar);
368            }
369        }
370
371        // Build help string with extras
372        let mut help = self.help().unwrap_or("").to_string();
373        let mut extras = Vec::new();
374
375        // Add envvar info
376        if self.show_envvar {
377            if let Some(envvars) = self.envvar() {
378                extras.push(format!("env var: {}", envvars.join(", ")));
379            }
380        }
381
382        // Add default info
383        if self.show_default {
384            if let Some(ref default) = self.default {
385                if !default.is_empty() {
386                    extras.push(format!("default: {}", default));
387                }
388            }
389        }
390
391        // Add required marker
392        if self.required() {
393            extras.push("required".to_string());
394        }
395
396        // Append extras to help
397        if !extras.is_empty() {
398            let extra_str = extras.join("; ");
399            if help.is_empty() {
400                help = format!("[{}]", extra_str);
401            } else {
402                help = format!("{}  [{}]", help, extra_str);
403            }
404        }
405
406        Some((opt_str, help))
407    }
408
409    fn param_type_name(&self) -> &str {
410        "option"
411    }
412}
413
414// =============================================================================
415// OptionBuilder
416// =============================================================================
417
418/// Builder for constructing ClickOption instances.
419pub struct OptionBuilder {
420    long: Vec<String>,
421    short: Vec<String>,
422    name: String,
423    help: Option<String>,
424    is_flag: bool,
425    is_bool_flag: bool,
426    flag_value: Option<String>,
427    secondary_value: Option<String>,
428    count: bool,
429    required: bool,
430    default: Option<String>,
431    envvar: Option<Vec<String>>,
432    prompt: Option<String>,
433    confirmation_prompt: bool,
434    hide_input: bool,
435    multiple: bool,
436    hidden: bool,
437    eager: bool,
438    show_default: bool,
439    show_envvar: bool,
440    metavar: Option<String>,
441    type_name: String,
442    type_metavar: Option<String>,
443    type_converter: Option<Arc<dyn AnyTypeConverter>>,
444    shell_complete_callback: Option<ShellCompleteCallback>,
445    nargs: Nargs,
446    callback: Option<ParameterCallback>,
447}
448
449impl OptionBuilder {
450    /// Create a new option builder with the given names.
451    ///
452    /// # Panics
453    ///
454    /// Panics if the names cannot be parsed as valid option names.
455    pub fn new(names: &[&str]) -> Self {
456        let (long, short) = split_option_names(names).expect("Invalid option names");
457        let name = derive_param_name(&long, &short);
458
459        Self {
460            long,
461            short,
462            name,
463            help: None,
464            is_flag: false,
465            is_bool_flag: false,
466            flag_value: None,
467            secondary_value: None,
468            count: false,
469            required: false,
470            default: None,
471            envvar: None,
472            prompt: None,
473            confirmation_prompt: false,
474            hide_input: false,
475            multiple: false,
476            hidden: false,
477            eager: false,
478            show_default: false,
479            show_envvar: false,
480            metavar: None,
481            type_name: TypeConverter::name(&STRING).to_string(),
482            type_metavar: TypeConverter::get_metavar(&STRING),
483            type_converter: None,
484            shell_complete_callback: None,
485            nargs: Nargs::Count(1),
486            callback: None,
487        }
488    }
489
490    /// Override the destination parameter name used in context storage.
491    ///
492    /// This allows multiple flags/options to write to the same logical value.
493    pub fn dest(mut self, name: &str) -> Self {
494        self.name = name.to_string();
495        self
496    }
497
498    /// Set the help text for this option.
499    pub fn help(mut self, help: &str) -> Self {
500        self.help = Some(help.to_string());
501        self
502    }
503
504    /// Make this a simple flag with the given value when present.
505    ///
506    /// When the flag is used, its value will be set to the provided value.
507    pub fn flag(mut self, value: &str) -> Self {
508        self.is_flag = true;
509        self.flag_value = Some(value.to_string());
510        self
511    }
512
513    /// Make this a boolean flag (--flag/--no-flag style).
514    ///
515    /// The option will accept both --flag (true) and --no-flag (false).
516    pub fn bool_flag(mut self) -> Self {
517        self.is_flag = true;
518        self.is_bool_flag = true;
519        self.flag_value = Some("true".to_string());
520        self.secondary_value = Some("false".to_string());
521        self
522    }
523
524    /// Enable count mode (-v -v -v = 3).
525    ///
526    /// Each occurrence of the flag increments an integer counter.
527    pub fn count(mut self) -> Self {
528        self.count = true;
529        self.is_flag = true;
530        self.default = Some("0".to_string());
531        self
532    }
533
534    /// Mark this option as required.
535    pub fn required(mut self) -> Self {
536        self.required = true;
537        self
538    }
539
540    /// Set a default value.
541    pub fn default(mut self, value: impl Into<String>) -> Self {
542        self.default = Some(value.into());
543        self
544    }
545
546    /// Set an environment variable for this option.
547    pub fn envvar(mut self, name: &str) -> Self {
548        self.envvar = Some(vec![name.to_string()]);
549        self
550    }
551
552    /// Set multiple environment variables (first found is used).
553    pub fn envvars(mut self, names: impl IntoIterator<Item = impl Into<String>>) -> Self {
554        self.envvar = Some(names.into_iter().map(|n| n.into()).collect());
555        self
556    }
557
558    /// Set a prompt text for interactive input.
559    pub fn prompt(mut self, text: &str) -> Self {
560        self.prompt = Some(text.to_string());
561        self
562    }
563
564    /// Enable confirmation prompt (ask twice).
565    pub fn confirmation_prompt(mut self, confirm: bool) -> Self {
566        self.confirmation_prompt = confirm;
567        self
568    }
569
570    /// Hide input (for passwords).
571    pub fn hide_input(mut self, hide: bool) -> Self {
572        self.hide_input = hide;
573        self
574    }
575
576    /// Allow this option to be specified multiple times.
577    pub fn multiple(mut self) -> Self {
578        self.multiple = true;
579        self
580    }
581
582    /// Set a callback invoked after conversion.
583    pub fn callback<F>(mut self, callback: F) -> Self
584    where
585        F: Fn(&Context, &dyn Parameter, Arc<dyn Any + Send + Sync>)
586                -> Result<Arc<dyn Any + Send + Sync>, ClickError>
587            + Send
588            + Sync
589            + 'static,
590    {
591        self.callback = Some(Arc::new(callback));
592        self
593    }
594
595    /// Set a custom shell completion callback for option values.
596    pub fn shell_complete<F>(mut self, callback: F) -> Self
597    where
598        F: Fn(&Context, &str) -> Vec<CompletionItem> + Send + Sync + 'static,
599    {
600        self.shell_complete_callback = Some(Arc::new(callback));
601        self
602    }
603
604    /// Hide this option from help output.
605    pub fn hidden(mut self) -> Self {
606        self.hidden = true;
607        self
608    }
609
610    /// Make this an eager option (processed before others).
611    ///
612    /// Eager options like --help and --version are processed first
613    /// and can short-circuit command execution.
614    pub fn eager(mut self) -> Self {
615        self.eager = true;
616        self
617    }
618
619    /// Show the default value in help text.
620    pub fn show_default(mut self) -> Self {
621        self.show_default = true;
622        self
623    }
624
625    /// Show the environment variable in help text.
626    pub fn show_envvar(mut self) -> Self {
627        self.show_envvar = true;
628        self
629    }
630
631    /// Set a custom metavar for help text.
632    pub fn metavar(mut self, metavar: &str) -> Self {
633        self.metavar = Some(metavar.to_string());
634        self
635    }
636
637    /// Set the type converter for this option.
638    pub fn type_<T: TypeConverter<Value = String> + Send + Sync + 'static>(
639        mut self,
640        type_: T,
641    ) -> Self {
642        self.type_name = type_.name().to_string();
643        self.type_metavar = type_.get_metavar();
644        self.type_converter = Some(Arc::new(type_));
645        self
646    }
647
648    /// Set the type using any TypeConverter (storing name and metavar).
649    pub fn type_any<V: Send + Sync + 'static, T: TypeConverter<Value = V> + Send + Sync + 'static>(
650        mut self,
651        type_: T,
652    ) -> Self {
653        self.type_name = type_.name().to_string();
654        self.type_metavar = type_.get_metavar();
655        self.type_converter = Some(Arc::new(type_));
656        self
657    }
658
659    /// Set how many arguments this option consumes.
660    pub fn nargs(mut self, nargs: Nargs) -> Self {
661        self.nargs = nargs;
662        self
663    }
664
665    /// Build the ClickOption.
666    pub fn build(self) -> ClickOption {
667        let config = ParameterConfig {
668            name: self.name,
669            nargs: self.nargs,
670            multiple: self.multiple,
671            is_eager: self.eager,
672            expose_value: true,
673            required: self.required,
674            envvar: self.envvar,
675            help: self.help,
676            hidden: self.hidden,
677            metavar: self.metavar,
678            deprecated: None,
679            callback: self.callback,
680        };
681
682        let type_converter: Arc<dyn AnyTypeConverter> =
683            self.type_converter.unwrap_or_else(|| Arc::new(StringType));
684
685        ClickOption {
686            config,
687            long: self.long,
688            short: self.short,
689            is_flag: self.is_flag,
690            is_bool_flag: self.is_bool_flag,
691            flag_value: self.flag_value,
692            secondary_value: self.secondary_value,
693            count: self.count,
694            prompt: self.prompt,
695            confirmation_prompt: self.confirmation_prompt,
696            hide_input: self.hide_input,
697            show_default: self.show_default,
698            show_envvar: self.show_envvar,
699            default: self.default,
700            type_name: self.type_name,
701            type_metavar: self.type_metavar,
702            type_converter,
703            shell_complete_callback: self.shell_complete_callback,
704        }
705    }
706}
707
708// =============================================================================
709// Tests
710// =============================================================================
711
712#[cfg(test)]
713mod tests {
714    use super::*;
715    use crate::types::{Choice, INT};
716
717    #[test]
718    fn test_parse_option_name_long() {
719        let (name, is_long) = parse_option_name("--name").unwrap();
720        assert_eq!(name, "name");
721        assert!(is_long);
722
723        let (name, is_long) = parse_option_name("--full-name").unwrap();
724        assert_eq!(name, "full-name");
725        assert!(is_long);
726    }
727
728    #[test]
729    fn test_parse_option_name_short() {
730        let (name, is_long) = parse_option_name("-n").unwrap();
731        assert_eq!(name, "n");
732        assert!(!is_long);
733
734        let (name, is_long) = parse_option_name("-v").unwrap();
735        assert_eq!(name, "v");
736        assert!(!is_long);
737    }
738
739    #[test]
740    fn test_parse_option_name_errors() {
741        assert!(parse_option_name("--").is_err());
742        assert!(parse_option_name("-").is_err());
743        assert!(parse_option_name("---name").is_err());
744        assert!(parse_option_name("-ab").is_err()); // Multi-char short
745        assert!(parse_option_name("name").is_err()); // No dash
746    }
747
748    #[test]
749    fn test_split_option_names() {
750        let (long, short) = split_option_names(&["--name", "-n", "--full-name"]).unwrap();
751        assert_eq!(long, vec!["--name", "--full-name"]);
752        assert_eq!(short, vec!["-n"]);
753    }
754
755    #[test]
756    fn test_split_option_names_only_short() {
757        let (long, short) = split_option_names(&["-n", "-N"]).unwrap();
758        assert!(long.is_empty());
759        assert_eq!(short, vec!["-n", "-N"]);
760    }
761
762    #[test]
763    fn test_split_option_names_empty() {
764        let result = split_option_names(&[]);
765        assert!(result.is_err());
766    }
767
768    #[test]
769    fn test_derive_param_name() {
770        let long = vec!["--name".to_string(), "--full-name".to_string()];
771        let short = vec!["-n".to_string()];
772        // Should pick longest long option: --full-name -> full_name
773        assert_eq!(derive_param_name(&long, &short), "full_name");
774
775        let long = vec![];
776        let short = vec!["-n".to_string()];
777        assert_eq!(derive_param_name(&long, &short), "n");
778    }
779
780    #[test]
781    fn test_option_builder_basic() {
782        let opt = ClickOption::new(&["--name", "-n"])
783            .help("The name to greet")
784            .build();
785
786        assert_eq!(opt.name(), "name");
787        assert_eq!(opt.long, vec!["--name"]);
788        assert_eq!(opt.short, vec!["-n"]);
789        assert_eq!(opt.help(), Some("The name to greet"));
790        assert!(!opt.is_flag);
791        assert!(!opt.required());
792    }
793
794    #[test]
795    fn test_option_builder_dest_override() {
796        let opt = ClickOption::new(&["--moored"]).dest("ty").flag("moored").build();
797        assert_eq!(opt.name(), "ty");
798        assert_eq!(opt.long, vec!["--moored"]);
799        assert_eq!(opt.flag_value, Some("moored".to_string()));
800    }
801
802    #[test]
803    fn test_option_flag() {
804        let opt = ClickOption::new(&["--verbose", "-v"]).flag("true").build();
805
806        assert!(opt.is_flag);
807        assert_eq!(opt.flag_value, Some("true".to_string()));
808    }
809
810    #[test]
811    fn test_option_bool_flag() {
812        let opt = ClickOption::new(&["--debug"]).bool_flag().build();
813
814        assert!(opt.is_flag);
815        assert!(opt.is_bool_flag);
816        assert_eq!(opt.flag_value, Some("true".to_string()));
817        assert_eq!(opt.secondary_value, Some("false".to_string()));
818    }
819
820    #[test]
821    fn test_option_count() {
822        let opt = ClickOption::new(&["--verbose", "-v"]).count().build();
823
824        assert!(opt.count);
825        assert!(opt.is_flag);
826        assert_eq!(opt.default, Some("0".to_string()));
827    }
828
829    #[test]
830    fn test_option_help_record_value() {
831        let opt = ClickOption::new(&["-n", "--name"])
832            .help("Your name")
833            .build();
834
835        let (opts, help) = opt.get_help_record().unwrap();
836        assert_eq!(opts, "-n, --name TEXT");
837        assert_eq!(help, "Your name");
838    }
839
840    #[test]
841    fn test_option_help_record_flag() {
842        let opt = ClickOption::new(&["-v", "--verbose"])
843            .flag("true")
844            .help("Enable verbose mode")
845            .build();
846
847        let (opts, help) = opt.get_help_record().unwrap();
848        assert_eq!(opts, "-v, --verbose");
849        assert_eq!(help, "Enable verbose mode");
850    }
851
852    #[test]
853    fn test_option_help_record_count() {
854        let opt = ClickOption::new(&["--verbose", "-v"])
855            .count()
856            .help("Increase verbosity")
857            .build();
858
859        let (opts, help) = opt.get_help_record().unwrap();
860        // Short options are listed first, then long options
861        assert_eq!(opts, "-v, --verbose");
862        assert_eq!(help, "Increase verbosity");
863    }
864
865    #[test]
866    fn test_option_help_record_with_default() {
867        let opt = ClickOption::new(&["--name"])
868            .default("World")
869            .show_default()
870            .build();
871
872        let (opts, help) = opt.get_help_record().unwrap();
873        assert_eq!(opts, "--name TEXT");
874        assert!(help.contains("default: World"));
875    }
876
877    #[test]
878    fn test_option_help_record_with_envvar() {
879        let opt = ClickOption::new(&["--name"])
880            .envvar("MY_NAME")
881            .show_envvar()
882            .build();
883
884        let (opts, help) = opt.get_help_record().unwrap();
885        assert_eq!(opts, "--name TEXT");
886        assert!(help.contains("env var: MY_NAME"));
887    }
888
889    #[test]
890    fn test_option_help_record_required() {
891        let opt = ClickOption::new(&["--name"]).required().build();
892
893        let (opts, help) = opt.get_help_record().unwrap();
894        assert_eq!(opts, "--name TEXT");
895        assert!(help.contains("required"));
896    }
897
898    #[test]
899    fn test_option_hidden() {
900        let opt = ClickOption::new(&["--secret"]).hidden().build();
901
902        assert!(opt.hidden());
903        assert!(opt.get_help_record().is_none());
904    }
905
906    #[test]
907    fn test_option_with_custom_type() {
908        let opt = ClickOption::new(&["--count"]).type_any(INT).build();
909
910        assert_eq!(opt.get_metavar(), Some("INTEGER".to_string()));
911    }
912
913    #[test]
914    fn test_option_with_choice_type() {
915        let opt = ClickOption::new(&["--format"])
916            .type_(Choice::new(["json", "xml", "csv"]))
917            .build();
918
919        assert_eq!(opt.get_metavar(), Some("[json|xml|csv]".to_string()));
920    }
921
922    #[test]
923    fn test_option_with_metavar_override() {
924        let opt = ClickOption::new(&["--file"]).metavar("PATH").build();
925
926        assert_eq!(opt.get_metavar(), Some("PATH".to_string()));
927    }
928
929    #[test]
930    fn test_option_prompt() {
931        let opt = ClickOption::new(&["--password"])
932            .prompt("Enter password")
933            .hide_input(true)
934            .confirmation_prompt(true)
935            .build();
936
937        assert_eq!(opt.prompt, Some("Enter password".to_string()));
938        assert!(opt.hide_input);
939        assert!(opt.confirmation_prompt);
940    }
941
942    #[test]
943    fn test_option_multiple() {
944        let opt = ClickOption::new(&["--file", "-f"]).multiple().build();
945
946        assert!(opt.multiple());
947    }
948
949    #[test]
950    fn test_option_eager() {
951        let opt = ClickOption::new(&["--help", "-h"]).eager().build();
952
953        assert!(opt.is_eager());
954    }
955
956    #[test]
957    fn test_option_human_readable_name() {
958        let opt = ClickOption::new(&["-n", "--name", "--full-name"]).build();
959
960        assert_eq!(opt.human_readable_name(), "-n, --name, --full-name");
961    }
962
963    #[test]
964    fn test_option_primary_opt() {
965        let opt = ClickOption::new(&["-n", "--name"]).build();
966        assert_eq!(opt.primary_opt(), "--name");
967
968        let opt = ClickOption::new(&["-n", "-N"]).build();
969        assert_eq!(opt.primary_opt(), "-n");
970    }
971
972    #[test]
973    fn test_option_param_type_name() {
974        let opt = ClickOption::new(&["--name"]).build();
975        assert_eq!(opt.param_type_name(), "option");
976    }
977}