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::argument::AnyTypeConverter;
16use crate::context::Context;
17use crate::error::ClickError;
18use crate::parameter::{Nargs, Parameter, ParameterCallback, ParameterConfig};
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(
210                "has_shell_complete",
211                &self.shell_complete_callback.is_some(),
212            )
213            .field("has_type_converter", &true)
214            .finish()
215    }
216}
217
218impl ClickOption {
219    /// Create a new option builder with the given names.
220    ///
221    /// Names should include the dashes (e.g., "--name", "-n").
222    ///
223    /// # Panics
224    ///
225    /// Panics if no valid option names are provided.
226    #[allow(clippy::new_ret_no_self)]
227    pub fn new(names: &[&str]) -> OptionBuilder {
228        OptionBuilder::new(names)
229    }
230
231    /// Get all option names (long and short) joined for display.
232    pub fn opts_string(&self) -> String {
233        let mut parts: Vec<&str> = Vec::new();
234        for s in &self.short {
235            parts.push(s);
236        }
237        for l in &self.long {
238            parts.push(l);
239        }
240        parts.join(", ")
241    }
242
243    /// Get the primary option string (first long or first short).
244    pub fn primary_opt(&self) -> &str {
245        self.long
246            .first()
247            .or(self.short.first())
248            .map(|s| s.as_str())
249            .unwrap_or("")
250    }
251
252    /// Get the type converter for this option.
253    pub fn type_converter(&self) -> &dyn AnyTypeConverter {
254        self.type_converter.as_ref()
255    }
256
257    /// Convert a string value using this option's type converter.
258    /// Returns the converted value as a boxed Any type.
259    pub fn convert_any(&self, value: &str) -> Result<Box<dyn Any + Send + Sync>, String> {
260        self.type_converter.convert_any(value)
261    }
262
263    /// Convert multiple string values using this option's type converter.
264    /// Returns the converted value as a boxed Any type.
265    pub fn convert_multi(&self, values: &[String]) -> Result<Box<dyn Any + Send + Sync>, String> {
266        self.type_converter.convert_multi(values)
267    }
268
269    /// Get shell completions for this option value.
270    ///
271    /// If a custom shell completion callback is set, it takes precedence over
272    /// type-driven completions.
273    pub fn get_completions(&self, ctx: &Context, incomplete: &str) -> Vec<CompletionItem> {
274        if let Some(ref callback) = self.shell_complete_callback {
275            callback(ctx, incomplete)
276        } else {
277            self.type_converter.shell_complete(incomplete)
278        }
279    }
280
281    /// Check if this option has a custom shell completion callback.
282    pub fn has_shell_complete_callback(&self) -> bool {
283        self.shell_complete_callback.is_some()
284    }
285}
286
287impl Parameter for ClickOption {
288    fn name(&self) -> &str {
289        &self.config.name
290    }
291
292    fn human_readable_name(&self) -> String {
293        self.opts_string()
294    }
295
296    fn nargs(&self) -> Nargs {
297        self.config.nargs
298    }
299
300    fn multiple(&self) -> bool {
301        self.config.multiple
302    }
303
304    fn is_eager(&self) -> bool {
305        self.config.is_eager
306    }
307
308    fn expose_value(&self) -> bool {
309        self.config.expose_value
310    }
311
312    fn required(&self) -> bool {
313        self.config.required
314    }
315
316    fn envvar(&self) -> Option<&[String]> {
317        self.config.envvar.as_deref()
318    }
319
320    fn help(&self) -> Option<&str> {
321        self.config.help.as_deref()
322    }
323
324    fn hidden(&self) -> bool {
325        self.config.hidden
326    }
327
328    fn get_metavar(&self) -> Option<String> {
329        // Custom metavar takes precedence
330        if let Some(ref mv) = self.config.metavar {
331            return Some(mv.clone());
332        }
333        // Otherwise use type's metavar
334        if let Some(ref mv) = self.type_metavar {
335            if mv.contains('|') && !(mv.starts_with('[') && mv.ends_with(']')) {
336                return Some(format!("[{}]", mv));
337            }
338            return Some(mv.clone());
339        }
340        if !self.type_name.is_empty() {
341            return Some(self.type_name.clone());
342        }
343        None
344    }
345
346    fn get_help_record(&self) -> Option<(String, String)> {
347        if self.hidden() {
348            return None;
349        }
350
351        // Build the option string
352        let mut opt_parts = Vec::new();
353
354        // Add short options first
355        for s in &self.short {
356            opt_parts.push(s.clone());
357        }
358
359        // Add long options
360        for l in &self.long {
361            opt_parts.push(l.clone());
362        }
363
364        let mut opt_str = opt_parts.join(", ");
365
366        // Add metavar for non-flag options
367        if !self.is_flag && !self.count {
368            if let Some(metavar) = self.get_metavar() {
369                opt_str.push(' ');
370                opt_str.push_str(&metavar);
371            }
372        }
373
374        // Build help string with extras
375        let mut help = self.help().unwrap_or("").to_string();
376        let mut extras = Vec::new();
377
378        // Add envvar info
379        if self.show_envvar {
380            if let Some(envvars) = self.envvar() {
381                extras.push(format!("env var: {}", envvars.join(", ")));
382            }
383        }
384
385        // Add default info
386        if self.show_default {
387            if let Some(ref default) = self.default {
388                if !default.is_empty() {
389                    extras.push(format!("default: {}", default));
390                }
391            }
392        }
393
394        // Add required marker
395        if self.required() {
396            extras.push("required".to_string());
397        }
398
399        // Append extras to help
400        if !extras.is_empty() {
401            let extra_str = extras.join("; ");
402            if help.is_empty() {
403                help = format!("[{}]", extra_str);
404            } else {
405                help = format!("{}  [{}]", help, extra_str);
406            }
407        }
408
409        Some((opt_str, help))
410    }
411
412    fn param_type_name(&self) -> &str {
413        "option"
414    }
415}
416
417// =============================================================================
418// OptionBuilder
419// =============================================================================
420
421/// Builder for constructing ClickOption instances.
422pub struct OptionBuilder {
423    long: Vec<String>,
424    short: Vec<String>,
425    name: String,
426    help: Option<String>,
427    is_flag: bool,
428    is_bool_flag: bool,
429    flag_value: Option<String>,
430    secondary_value: Option<String>,
431    count: bool,
432    required: bool,
433    default: Option<String>,
434    envvar: Option<Vec<String>>,
435    prompt: Option<String>,
436    confirmation_prompt: bool,
437    hide_input: bool,
438    multiple: bool,
439    hidden: bool,
440    eager: bool,
441    show_default: bool,
442    show_envvar: bool,
443    metavar: Option<String>,
444    type_name: String,
445    type_metavar: Option<String>,
446    type_converter: Option<Arc<dyn AnyTypeConverter>>,
447    shell_complete_callback: Option<ShellCompleteCallback>,
448    nargs: Nargs,
449    callback: Option<ParameterCallback>,
450}
451
452impl OptionBuilder {
453    /// Create a new option builder with the given names.
454    ///
455    /// # Panics
456    ///
457    /// Panics if the names cannot be parsed as valid option names.
458    pub fn new(names: &[&str]) -> Self {
459        let (long, short) = split_option_names(names).expect("Invalid option names");
460        let name = derive_param_name(&long, &short);
461
462        Self {
463            long,
464            short,
465            name,
466            help: None,
467            is_flag: false,
468            is_bool_flag: false,
469            flag_value: None,
470            secondary_value: None,
471            count: false,
472            required: false,
473            default: None,
474            envvar: None,
475            prompt: None,
476            confirmation_prompt: false,
477            hide_input: false,
478            multiple: false,
479            hidden: false,
480            eager: false,
481            show_default: false,
482            show_envvar: false,
483            metavar: None,
484            type_name: TypeConverter::name(&STRING).to_string(),
485            type_metavar: TypeConverter::get_metavar(&STRING),
486            type_converter: None,
487            shell_complete_callback: None,
488            nargs: Nargs::Count(1),
489            callback: None,
490        }
491    }
492
493    /// Override the destination parameter name used in context storage.
494    ///
495    /// This allows multiple flags/options to write to the same logical value.
496    pub fn dest(mut self, name: &str) -> Self {
497        self.name = name.to_string();
498        self
499    }
500
501    /// Set the help text for this option.
502    pub fn help(mut self, help: &str) -> Self {
503        self.help = Some(help.to_string());
504        self
505    }
506
507    /// Make this a simple flag with the given value when present.
508    ///
509    /// When the flag is used, its value will be set to the provided value.
510    pub fn flag(mut self, value: &str) -> Self {
511        self.is_flag = true;
512        self.flag_value = Some(value.to_string());
513        self
514    }
515
516    /// Make this a boolean flag (--flag/--no-flag style).
517    ///
518    /// The option will accept both --flag (true) and --no-flag (false).
519    pub fn bool_flag(mut self) -> Self {
520        self.is_flag = true;
521        self.is_bool_flag = true;
522        self.flag_value = Some("true".to_string());
523        self.secondary_value = Some("false".to_string());
524        self
525    }
526
527    /// Enable count mode (-v -v -v = 3).
528    ///
529    /// Each occurrence of the flag increments an integer counter.
530    pub fn count(mut self) -> Self {
531        self.count = true;
532        self.is_flag = true;
533        self.default = Some("0".to_string());
534        self
535    }
536
537    /// Mark this option as required.
538    pub fn required(mut self) -> Self {
539        self.required = true;
540        self
541    }
542
543    /// Set a default value.
544    pub fn default(mut self, value: impl Into<String>) -> Self {
545        self.default = Some(value.into());
546        self
547    }
548
549    /// Set an environment variable for this option.
550    pub fn envvar(mut self, name: &str) -> Self {
551        self.envvar = Some(vec![name.to_string()]);
552        self
553    }
554
555    /// Set multiple environment variables (first found is used).
556    pub fn envvars(mut self, names: impl IntoIterator<Item = impl Into<String>>) -> Self {
557        self.envvar = Some(names.into_iter().map(|n| n.into()).collect());
558        self
559    }
560
561    /// Set a prompt text for interactive input.
562    pub fn prompt(mut self, text: &str) -> Self {
563        self.prompt = Some(text.to_string());
564        self
565    }
566
567    /// Enable confirmation prompt (ask twice).
568    pub fn confirmation_prompt(mut self, confirm: bool) -> Self {
569        self.confirmation_prompt = confirm;
570        self
571    }
572
573    /// Hide input (for passwords).
574    pub fn hide_input(mut self, hide: bool) -> Self {
575        self.hide_input = hide;
576        self
577    }
578
579    /// Allow this option to be specified multiple times.
580    pub fn multiple(mut self) -> Self {
581        self.multiple = true;
582        self
583    }
584
585    /// Set a callback invoked after conversion.
586    pub fn callback<F>(mut self, callback: F) -> Self
587    where
588        F: Fn(
589                &Context,
590                &dyn Parameter,
591                Arc<dyn Any + Send + Sync>,
592            ) -> Result<Arc<dyn Any + Send + Sync>, ClickError>
593            + Send
594            + Sync
595            + 'static,
596    {
597        self.callback = Some(Arc::new(callback));
598        self
599    }
600
601    /// Set a custom shell completion callback for option values.
602    pub fn shell_complete<F>(mut self, callback: F) -> Self
603    where
604        F: Fn(&Context, &str) -> Vec<CompletionItem> + Send + Sync + 'static,
605    {
606        self.shell_complete_callback = Some(Arc::new(callback));
607        self
608    }
609
610    /// Hide this option from help output.
611    pub fn hidden(mut self) -> Self {
612        self.hidden = true;
613        self
614    }
615
616    /// Make this an eager option (processed before others).
617    ///
618    /// Eager options like --help and --version are processed first
619    /// and can short-circuit command execution.
620    pub fn eager(mut self) -> Self {
621        self.eager = true;
622        self
623    }
624
625    /// Show the default value in help text.
626    pub fn show_default(mut self) -> Self {
627        self.show_default = true;
628        self
629    }
630
631    /// Show the environment variable in help text.
632    pub fn show_envvar(mut self) -> Self {
633        self.show_envvar = true;
634        self
635    }
636
637    /// Set a custom metavar for help text.
638    pub fn metavar(mut self, metavar: &str) -> Self {
639        self.metavar = Some(metavar.to_string());
640        self
641    }
642
643    /// Set the type converter for this option.
644    pub fn type_<T: TypeConverter<Value = String> + Send + Sync + 'static>(
645        mut self,
646        type_: T,
647    ) -> Self {
648        self.type_name = type_.name().to_string();
649        self.type_metavar = type_.get_metavar();
650        self.type_converter = Some(Arc::new(type_));
651        self
652    }
653
654    /// Set the type using any TypeConverter (storing name and metavar).
655    pub fn type_any<
656        V: Send + Sync + 'static,
657        T: TypeConverter<Value = V> + Send + Sync + 'static,
658    >(
659        mut self,
660        type_: T,
661    ) -> Self {
662        self.type_name = type_.name().to_string();
663        self.type_metavar = type_.get_metavar();
664        self.type_converter = Some(Arc::new(type_));
665        self
666    }
667
668    /// Set how many arguments this option consumes.
669    pub fn nargs(mut self, nargs: Nargs) -> Self {
670        self.nargs = nargs;
671        self
672    }
673
674    /// Build the ClickOption.
675    pub fn build(self) -> ClickOption {
676        let config = ParameterConfig {
677            name: self.name,
678            nargs: self.nargs,
679            multiple: self.multiple,
680            is_eager: self.eager,
681            expose_value: true,
682            required: self.required,
683            envvar: self.envvar,
684            help: self.help,
685            hidden: self.hidden,
686            metavar: self.metavar,
687            deprecated: None,
688            callback: self.callback,
689        };
690
691        let type_converter: Arc<dyn AnyTypeConverter> =
692            self.type_converter.unwrap_or_else(|| Arc::new(StringType));
693
694        ClickOption {
695            config,
696            long: self.long,
697            short: self.short,
698            is_flag: self.is_flag,
699            is_bool_flag: self.is_bool_flag,
700            flag_value: self.flag_value,
701            secondary_value: self.secondary_value,
702            count: self.count,
703            prompt: self.prompt,
704            confirmation_prompt: self.confirmation_prompt,
705            hide_input: self.hide_input,
706            show_default: self.show_default,
707            show_envvar: self.show_envvar,
708            default: self.default,
709            type_name: self.type_name,
710            type_metavar: self.type_metavar,
711            type_converter,
712            shell_complete_callback: self.shell_complete_callback,
713        }
714    }
715}
716
717// =============================================================================
718// Tests
719// =============================================================================
720
721#[cfg(test)]
722mod tests {
723    use super::*;
724    use crate::types::{Choice, INT};
725
726    #[test]
727    fn test_parse_option_name_long() {
728        let (name, is_long) = parse_option_name("--name").unwrap();
729        assert_eq!(name, "name");
730        assert!(is_long);
731
732        let (name, is_long) = parse_option_name("--full-name").unwrap();
733        assert_eq!(name, "full-name");
734        assert!(is_long);
735    }
736
737    #[test]
738    fn test_parse_option_name_short() {
739        let (name, is_long) = parse_option_name("-n").unwrap();
740        assert_eq!(name, "n");
741        assert!(!is_long);
742
743        let (name, is_long) = parse_option_name("-v").unwrap();
744        assert_eq!(name, "v");
745        assert!(!is_long);
746    }
747
748    #[test]
749    fn test_parse_option_name_errors() {
750        assert!(parse_option_name("--").is_err());
751        assert!(parse_option_name("-").is_err());
752        assert!(parse_option_name("---name").is_err());
753        assert!(parse_option_name("-ab").is_err()); // Multi-char short
754        assert!(parse_option_name("name").is_err()); // No dash
755    }
756
757    #[test]
758    fn test_split_option_names() {
759        let (long, short) = split_option_names(&["--name", "-n", "--full-name"]).unwrap();
760        assert_eq!(long, vec!["--name", "--full-name"]);
761        assert_eq!(short, vec!["-n"]);
762    }
763
764    #[test]
765    fn test_split_option_names_only_short() {
766        let (long, short) = split_option_names(&["-n", "-N"]).unwrap();
767        assert!(long.is_empty());
768        assert_eq!(short, vec!["-n", "-N"]);
769    }
770
771    #[test]
772    fn test_split_option_names_empty() {
773        let result = split_option_names(&[]);
774        assert!(result.is_err());
775    }
776
777    #[test]
778    fn test_derive_param_name() {
779        let long = vec!["--name".to_string(), "--full-name".to_string()];
780        let short = vec!["-n".to_string()];
781        // Should pick longest long option: --full-name -> full_name
782        assert_eq!(derive_param_name(&long, &short), "full_name");
783
784        let long = vec![];
785        let short = vec!["-n".to_string()];
786        assert_eq!(derive_param_name(&long, &short), "n");
787    }
788
789    #[test]
790    fn test_option_builder_basic() {
791        let opt = ClickOption::new(&["--name", "-n"])
792            .help("The name to greet")
793            .build();
794
795        assert_eq!(opt.name(), "name");
796        assert_eq!(opt.long, vec!["--name"]);
797        assert_eq!(opt.short, vec!["-n"]);
798        assert_eq!(opt.help(), Some("The name to greet"));
799        assert!(!opt.is_flag);
800        assert!(!opt.required());
801    }
802
803    #[test]
804    fn test_option_builder_dest_override() {
805        let opt = ClickOption::new(&["--moored"])
806            .dest("ty")
807            .flag("moored")
808            .build();
809        assert_eq!(opt.name(), "ty");
810        assert_eq!(opt.long, vec!["--moored"]);
811        assert_eq!(opt.flag_value, Some("moored".to_string()));
812    }
813
814    #[test]
815    fn test_option_flag() {
816        let opt = ClickOption::new(&["--verbose", "-v"]).flag("true").build();
817
818        assert!(opt.is_flag);
819        assert_eq!(opt.flag_value, Some("true".to_string()));
820    }
821
822    #[test]
823    fn test_option_bool_flag() {
824        let opt = ClickOption::new(&["--debug"]).bool_flag().build();
825
826        assert!(opt.is_flag);
827        assert!(opt.is_bool_flag);
828        assert_eq!(opt.flag_value, Some("true".to_string()));
829        assert_eq!(opt.secondary_value, Some("false".to_string()));
830    }
831
832    #[test]
833    fn test_option_count() {
834        let opt = ClickOption::new(&["--verbose", "-v"]).count().build();
835
836        assert!(opt.count);
837        assert!(opt.is_flag);
838        assert_eq!(opt.default, Some("0".to_string()));
839    }
840
841    #[test]
842    fn test_option_help_record_value() {
843        let opt = ClickOption::new(&["-n", "--name"])
844            .help("Your name")
845            .build();
846
847        let (opts, help) = opt.get_help_record().unwrap();
848        assert_eq!(opts, "-n, --name TEXT");
849        assert_eq!(help, "Your name");
850    }
851
852    #[test]
853    fn test_option_help_record_flag() {
854        let opt = ClickOption::new(&["-v", "--verbose"])
855            .flag("true")
856            .help("Enable verbose mode")
857            .build();
858
859        let (opts, help) = opt.get_help_record().unwrap();
860        assert_eq!(opts, "-v, --verbose");
861        assert_eq!(help, "Enable verbose mode");
862    }
863
864    #[test]
865    fn test_option_help_record_count() {
866        let opt = ClickOption::new(&["--verbose", "-v"])
867            .count()
868            .help("Increase verbosity")
869            .build();
870
871        let (opts, help) = opt.get_help_record().unwrap();
872        // Short options are listed first, then long options
873        assert_eq!(opts, "-v, --verbose");
874        assert_eq!(help, "Increase verbosity");
875    }
876
877    #[test]
878    fn test_option_help_record_with_default() {
879        let opt = ClickOption::new(&["--name"])
880            .default("World")
881            .show_default()
882            .build();
883
884        let (opts, help) = opt.get_help_record().unwrap();
885        assert_eq!(opts, "--name TEXT");
886        assert!(help.contains("default: World"));
887    }
888
889    #[test]
890    fn test_option_help_record_with_envvar() {
891        let opt = ClickOption::new(&["--name"])
892            .envvar("MY_NAME")
893            .show_envvar()
894            .build();
895
896        let (opts, help) = opt.get_help_record().unwrap();
897        assert_eq!(opts, "--name TEXT");
898        assert!(help.contains("env var: MY_NAME"));
899    }
900
901    #[test]
902    fn test_option_help_record_required() {
903        let opt = ClickOption::new(&["--name"]).required().build();
904
905        let (opts, help) = opt.get_help_record().unwrap();
906        assert_eq!(opts, "--name TEXT");
907        assert!(help.contains("required"));
908    }
909
910    #[test]
911    fn test_option_hidden() {
912        let opt = ClickOption::new(&["--secret"]).hidden().build();
913
914        assert!(opt.hidden());
915        assert!(opt.get_help_record().is_none());
916    }
917
918    #[test]
919    fn test_option_with_custom_type() {
920        let opt = ClickOption::new(&["--count"]).type_any(INT).build();
921
922        assert_eq!(opt.get_metavar(), Some("INTEGER".to_string()));
923    }
924
925    #[test]
926    fn test_option_with_choice_type() {
927        let opt = ClickOption::new(&["--format"])
928            .type_(Choice::new(["json", "xml", "csv"]))
929            .build();
930
931        assert_eq!(opt.get_metavar(), Some("[json|xml|csv]".to_string()));
932    }
933
934    #[test]
935    fn test_option_with_metavar_override() {
936        let opt = ClickOption::new(&["--file"]).metavar("PATH").build();
937
938        assert_eq!(opt.get_metavar(), Some("PATH".to_string()));
939    }
940
941    #[test]
942    fn test_option_prompt() {
943        let opt = ClickOption::new(&["--password"])
944            .prompt("Enter password")
945            .hide_input(true)
946            .confirmation_prompt(true)
947            .build();
948
949        assert_eq!(opt.prompt, Some("Enter password".to_string()));
950        assert!(opt.hide_input);
951        assert!(opt.confirmation_prompt);
952    }
953
954    #[test]
955    fn test_option_multiple() {
956        let opt = ClickOption::new(&["--file", "-f"]).multiple().build();
957
958        assert!(opt.multiple());
959    }
960
961    #[test]
962    fn test_option_eager() {
963        let opt = ClickOption::new(&["--help", "-h"]).eager().build();
964
965        assert!(opt.is_eager());
966    }
967
968    #[test]
969    fn test_option_human_readable_name() {
970        let opt = ClickOption::new(&["-n", "--name", "--full-name"]).build();
971
972        assert_eq!(opt.human_readable_name(), "-n, --name, --full-name");
973    }
974
975    #[test]
976    fn test_option_primary_opt() {
977        let opt = ClickOption::new(&["-n", "--name"]).build();
978        assert_eq!(opt.primary_opt(), "--name");
979
980        let opt = ClickOption::new(&["-n", "-N"]).build();
981        assert_eq!(opt.primary_opt(), "-n");
982    }
983
984    #[test]
985    fn test_option_param_type_name() {
986        let opt = ClickOption::new(&["--name"]).build();
987        assert_eq!(opt.param_type_name(), "option");
988    }
989}