Skip to main content

click/
parameter.rs

1//! Base parameter abstraction for click-rs.
2//!
3//! This module provides the `Parameter` trait and common configuration types
4//! for command-line parameters. Options and Arguments implement this trait
5//! separately.
6//!
7//! # Reference
8//!
9//! Based on Python Click's `core.py:Parameter` class (line 2027+).
10
11use std::any::Any;
12use std::fmt;
13use std::sync::Arc;
14
15use crate::context::Context;
16use crate::error::ClickError;
17
18// =============================================================================
19// Nargs Enum
20// =============================================================================
21
22/// Specifies how many arguments a parameter consumes.
23///
24/// This enum corresponds to Python Click's `nargs` parameter:
25/// - `Count(1)` is the default (single value)
26/// - `Variadic` corresponds to `nargs=-1`
27/// - `Optional` corresponds to `nargs=?` (zero or one value)
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29pub enum Nargs {
30    /// Exactly N values (default is 1).
31    Count(usize),
32    /// Zero or more values (Python's `nargs=-1`).
33    /// All remaining arguments are collected.
34    Variadic,
35    /// Optional single value (Python's `nargs=?`).
36    /// Zero or one value; if not provided, uses the default.
37    Optional,
38}
39
40impl Default for Nargs {
41    fn default() -> Self {
42        Nargs::Count(1)
43    }
44}
45
46impl Nargs {
47    /// Returns `true` if this nargs expects exactly one value.
48    pub fn is_single(&self) -> bool {
49        matches!(self, Nargs::Count(1))
50    }
51
52    /// Returns `true` if this nargs can accept multiple values.
53    pub fn is_multi(&self) -> bool {
54        match self {
55            Nargs::Variadic => true,
56            Nargs::Count(n) => *n > 1,
57            Nargs::Optional => false,
58        }
59    }
60
61    /// Returns `true` if this nargs accepts zero or more values.
62    pub fn is_variadic(&self) -> bool {
63        matches!(self, Nargs::Variadic)
64    }
65
66    /// Returns `true` if this nargs is optional (zero or one).
67    pub fn is_optional(&self) -> bool {
68        matches!(self, Nargs::Optional)
69    }
70
71    /// Returns the exact count if this is `Count(n)`, otherwise `None`.
72    pub fn count(&self) -> Option<usize> {
73        match self {
74            Nargs::Count(n) => Some(*n),
75            _ => None,
76        }
77    }
78}
79
80impl fmt::Display for Nargs {
81    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82        match self {
83            Nargs::Count(1) => write!(f, "1"),
84            Nargs::Count(n) => write!(f, "{}", n),
85            Nargs::Variadic => write!(f, "-1"),
86            Nargs::Optional => write!(f, "?"),
87        }
88    }
89}
90
91// =============================================================================
92// Parameter Trait
93// =============================================================================
94
95/// A trait for command-line parameters (options and arguments).
96///
97/// This trait defines the common interface for all parameter types.
98/// Options and Arguments implement this trait with their specific behaviors.
99pub trait Parameter: Send + Sync + fmt::Debug {
100    /// The primary name of the parameter.
101    ///
102    /// For options, this is typically the long option name without dashes.
103    /// For arguments, this is the argument name.
104    fn name(&self) -> &str;
105
106    /// Human-readable name for errors and help text.
107    ///
108    /// For options, this is usually the option flags (e.g., "--name / -n").
109    /// For arguments, this is typically the metavar in uppercase.
110    fn human_readable_name(&self) -> String;
111
112    /// Number of arguments this parameter consumes.
113    fn nargs(&self) -> Nargs;
114
115    /// Whether this parameter can be specified multiple times.
116    ///
117    /// When true, the parameter collects values into a list/tuple.
118    fn multiple(&self) -> bool;
119
120    /// Whether this parameter should be processed before others.
121    ///
122    /// Eager parameters (like `--help` and `--version`) are processed
123    /// first and can short-circuit command execution.
124    fn is_eager(&self) -> bool;
125
126    /// Whether this parameter's value should be exposed in `ctx.params`.
127    ///
128    /// When false, the parameter is still processed but its value
129    /// is not stored in the context parameters.
130    fn expose_value(&self) -> bool;
131
132    /// Whether this parameter is required.
133    ///
134    /// Required parameters must be provided via CLI, environment, or default.
135    fn required(&self) -> bool;
136
137    /// Get environment variable name(s) for this parameter.
138    ///
139    /// Returns `None` if no environment variables are configured.
140    /// Multiple environment variables can be specified; the first
141    /// non-empty value is used.
142    fn envvar(&self) -> Option<&[String]>;
143
144    /// Get help text for this parameter.
145    fn help(&self) -> Option<&str>;
146
147    /// Whether this parameter is hidden from help.
148    fn hidden(&self) -> bool;
149
150    /// Get the metavar for help text (e.g., "FILE", "TEXT").
151    ///
152    /// If not explicitly set, the type's metavar is used.
153    fn get_metavar(&self) -> Option<String>;
154
155    /// Generate help record for formatting.
156    ///
157    /// Returns a tuple of (option_string, help_string) for help display,
158    /// or `None` if this parameter should not appear in help.
159    fn get_help_record(&self) -> Option<(String, String)>;
160
161    /// Get the parameter type name (for error messages).
162    fn param_type_name(&self) -> &str {
163        "parameter"
164    }
165}
166
167// =============================================================================
168// ParameterConfig Struct
169// =============================================================================
170
171/// Callback for parameter value processing.
172///
173/// Receives the current context, the parameter, and the converted value.
174/// Returns the (possibly transformed) value or an error.
175pub type ParameterCallback = Arc<
176    dyn Fn(
177            &Context,
178            &dyn Parameter,
179            Arc<dyn Any + Send + Sync>,
180        ) -> Result<Arc<dyn Any + Send + Sync>, ClickError>
181        + Send
182        + Sync,
183>;
184
185/// Common configuration for all parameter types.
186///
187/// This struct holds the shared settings between Options and Arguments.
188/// It uses a builder pattern for convenient construction.
189#[derive(Clone)]
190pub struct ParameterConfig {
191    /// The parameter name.
192    pub name: String,
193    /// Number of arguments consumed.
194    pub nargs: Nargs,
195    /// Whether the parameter can be specified multiple times.
196    pub multiple: bool,
197    /// Whether this parameter should be processed before others.
198    pub is_eager: bool,
199    /// Whether this parameter's value is exposed in ctx.params.
200    pub expose_value: bool,
201    /// Whether this parameter is required.
202    pub required: bool,
203    /// Environment variable name(s) for this parameter.
204    pub envvar: Option<Vec<String>>,
205    /// Help text for this parameter.
206    pub help: Option<String>,
207    /// Whether this parameter is hidden from help.
208    pub hidden: bool,
209    /// Custom metavar for help text.
210    pub metavar: Option<String>,
211    /// Whether this parameter is deprecated.
212    pub deprecated: Option<DeprecationInfo>,
213    /// Optional callback invoked after conversion.
214    pub callback: Option<ParameterCallback>,
215}
216
217impl fmt::Debug for ParameterConfig {
218    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
219        f.debug_struct("ParameterConfig")
220            .field("name", &self.name)
221            .field("nargs", &self.nargs)
222            .field("multiple", &self.multiple)
223            .field("is_eager", &self.is_eager)
224            .field("expose_value", &self.expose_value)
225            .field("required", &self.required)
226            .field("envvar", &self.envvar)
227            .field("help", &self.help)
228            .field("hidden", &self.hidden)
229            .field("metavar", &self.metavar)
230            .field("deprecated", &self.deprecated)
231            .field("has_callback", &self.callback.is_some())
232            .finish()
233    }
234}
235
236/// Information about a deprecated parameter.
237#[derive(Debug, Clone, Default)]
238pub struct DeprecationInfo {
239    /// Custom deprecation message (if not using the default).
240    pub message: Option<String>,
241}
242
243impl DeprecationInfo {
244    /// Create a new deprecation info with default message.
245    pub fn new() -> Self {
246        Self::default()
247    }
248
249    /// Create a new deprecation info with a custom message.
250    pub fn with_message(message: impl Into<String>) -> Self {
251        Self {
252            message: Some(message.into()),
253        }
254    }
255}
256
257impl Default for ParameterConfig {
258    fn default() -> Self {
259        Self {
260            name: String::new(),
261            nargs: Nargs::default(),
262            multiple: false,
263            is_eager: false,
264            expose_value: true,
265            required: false,
266            envvar: None,
267            help: None,
268            hidden: false,
269            metavar: None,
270            deprecated: None,
271            callback: None,
272        }
273    }
274}
275
276impl ParameterConfig {
277    /// Create a new parameter configuration with the given name.
278    pub fn new(name: impl Into<String>) -> Self {
279        Self {
280            name: name.into(),
281            ..Default::default()
282        }
283    }
284
285    /// Set the number of arguments consumed.
286    pub fn nargs(mut self, nargs: Nargs) -> Self {
287        self.nargs = nargs;
288        self
289    }
290
291    /// Set whether the parameter can be specified multiple times.
292    pub fn multiple(mut self, multiple: bool) -> Self {
293        self.multiple = multiple;
294        self
295    }
296
297    /// Set whether this parameter should be processed before others.
298    pub fn eager(mut self, eager: bool) -> Self {
299        self.is_eager = eager;
300        self
301    }
302
303    /// Set whether this parameter's value is exposed in ctx.params.
304    pub fn expose_value(mut self, expose: bool) -> Self {
305        self.expose_value = expose;
306        self
307    }
308
309    /// Set whether this parameter is required.
310    pub fn required(mut self, required: bool) -> Self {
311        self.required = required;
312        self
313    }
314
315    /// Set a single environment variable for this parameter.
316    pub fn envvar(mut self, var: impl Into<String>) -> Self {
317        self.envvar = Some(vec![var.into()]);
318        self
319    }
320
321    /// Set multiple environment variables for this parameter.
322    pub fn envvars(mut self, vars: impl IntoIterator<Item = impl Into<String>>) -> Self {
323        self.envvar = Some(vars.into_iter().map(|v| v.into()).collect());
324        self
325    }
326
327    /// Set the help text.
328    pub fn help(mut self, help: impl Into<String>) -> Self {
329        self.help = Some(help.into());
330        self
331    }
332
333    /// Set whether this parameter is hidden from help.
334    pub fn hidden(mut self, hidden: bool) -> Self {
335        self.hidden = hidden;
336        self
337    }
338
339    /// Set a custom metavar for help text.
340    pub fn metavar(mut self, metavar: impl Into<String>) -> Self {
341        self.metavar = Some(metavar.into());
342        self
343    }
344
345    /// Set a callback invoked after conversion.
346    pub fn callback(mut self, callback: ParameterCallback) -> Self {
347        self.callback = Some(callback);
348        self
349    }
350
351    /// Mark this parameter as deprecated.
352    pub fn deprecated(mut self, deprecated: bool) -> Self {
353        self.deprecated = if deprecated {
354            Some(DeprecationInfo::default())
355        } else {
356            None
357        };
358        self
359    }
360
361    /// Mark this parameter as deprecated with a custom message.
362    pub fn deprecated_with_message(mut self, message: impl Into<String>) -> Self {
363        self.deprecated = Some(DeprecationInfo::with_message(message));
364        self
365    }
366
367    /// Validate the configuration.
368    ///
369    /// Returns an error if the configuration is invalid.
370    pub fn validate(&self) -> Result<(), ClickError> {
371        // A deprecated parameter cannot be required
372        if self.deprecated.is_some() && self.required {
373            return Err(ClickError::usage(format!(
374                "The parameter '{}' is deprecated and required. \
375                 A deprecated parameter cannot be required.",
376                self.name
377            )));
378        }
379        Ok(())
380    }
381}
382
383// =============================================================================
384// Tests
385// =============================================================================
386
387#[cfg(test)]
388mod tests {
389    use super::*;
390
391    #[test]
392    fn test_nargs_default() {
393        let nargs = Nargs::default();
394        assert_eq!(nargs, Nargs::Count(1));
395        assert!(nargs.is_single());
396        assert!(!nargs.is_multi());
397        assert!(!nargs.is_variadic());
398        assert!(!nargs.is_optional());
399    }
400
401    #[test]
402    fn test_nargs_count() {
403        let nargs = Nargs::Count(3);
404        assert!(!nargs.is_single());
405        assert!(nargs.is_multi());
406        assert!(!nargs.is_variadic());
407        assert!(!nargs.is_optional());
408        assert_eq!(nargs.count(), Some(3));
409    }
410
411    #[test]
412    fn test_nargs_variadic() {
413        let nargs = Nargs::Variadic;
414        assert!(!nargs.is_single());
415        assert!(nargs.is_multi());
416        assert!(nargs.is_variadic());
417        assert!(!nargs.is_optional());
418        assert_eq!(nargs.count(), None);
419    }
420
421    #[test]
422    fn test_nargs_optional() {
423        let nargs = Nargs::Optional;
424        assert!(!nargs.is_single());
425        assert!(!nargs.is_multi());
426        assert!(!nargs.is_variadic());
427        assert!(nargs.is_optional());
428        assert_eq!(nargs.count(), None);
429    }
430
431    #[test]
432    fn test_nargs_display() {
433        assert_eq!(Nargs::Count(1).to_string(), "1");
434        assert_eq!(Nargs::Count(3).to_string(), "3");
435        assert_eq!(Nargs::Variadic.to_string(), "-1");
436        assert_eq!(Nargs::Optional.to_string(), "?");
437    }
438
439    #[test]
440    fn test_parameter_config_builder() {
441        let config = ParameterConfig::new("name")
442            .nargs(Nargs::Count(2))
443            .multiple(true)
444            .eager(true)
445            .expose_value(false)
446            .required(true)
447            .envvar("MY_VAR")
448            .help("Help text")
449            .hidden(false)
450            .metavar("VALUE");
451
452        assert_eq!(config.name, "name");
453        assert_eq!(config.nargs, Nargs::Count(2));
454        assert!(config.multiple);
455        assert!(config.is_eager);
456        assert!(!config.expose_value);
457        assert!(config.required);
458        assert_eq!(config.envvar, Some(vec!["MY_VAR".to_string()]));
459        assert_eq!(config.help, Some("Help text".to_string()));
460        assert!(!config.hidden);
461        assert_eq!(config.metavar, Some("VALUE".to_string()));
462    }
463
464    #[test]
465    fn test_parameter_config_envvars() {
466        let config = ParameterConfig::new("name").envvars(["VAR1", "VAR2", "VAR3"]);
467
468        assert_eq!(
469            config.envvar,
470            Some(vec![
471                "VAR1".to_string(),
472                "VAR2".to_string(),
473                "VAR3".to_string()
474            ])
475        );
476    }
477
478    #[test]
479    fn test_parameter_config_default() {
480        let config = ParameterConfig::default();
481
482        assert_eq!(config.name, "");
483        assert_eq!(config.nargs, Nargs::Count(1));
484        assert!(!config.multiple);
485        assert!(!config.is_eager);
486        assert!(config.expose_value);
487        assert!(!config.required);
488        assert!(config.envvar.is_none());
489        assert!(config.help.is_none());
490        assert!(!config.hidden);
491        assert!(config.metavar.is_none());
492        assert!(config.deprecated.is_none());
493    }
494
495    #[test]
496    fn test_parameter_config_deprecated() {
497        let config = ParameterConfig::new("old_option").deprecated(true);
498        assert!(config.deprecated.is_some());
499        assert!(config.deprecated.as_ref().unwrap().message.is_none());
500
501        let config =
502            ParameterConfig::new("old_option").deprecated_with_message("Use --new-option instead");
503        assert!(config.deprecated.is_some());
504        assert_eq!(
505            config.deprecated.as_ref().unwrap().message,
506            Some("Use --new-option instead".to_string())
507        );
508    }
509
510    #[test]
511    fn test_parameter_config_validate_deprecated_required() {
512        let config = ParameterConfig::new("option")
513            .deprecated(true)
514            .required(true);
515
516        let result = config.validate();
517        assert!(result.is_err());
518        let err = result.unwrap_err();
519        assert!(err.to_string().contains("deprecated"));
520        assert!(err.to_string().contains("required"));
521    }
522
523    #[test]
524    fn test_parameter_config_validate_ok() {
525        let config = ParameterConfig::new("option").required(true);
526
527        assert!(config.validate().is_ok());
528
529        let config = ParameterConfig::new("option").deprecated(true);
530
531        assert!(config.validate().is_ok());
532    }
533
534    #[test]
535    fn test_deprecation_info() {
536        let info = DeprecationInfo::new();
537        assert!(info.message.is_none());
538
539        let info = DeprecationInfo::with_message("Custom message");
540        assert_eq!(info.message, Some("Custom message".to_string()));
541    }
542}