Skip to main content

usage/spec/
arg.rs

1use kdl::{KdlDocument, KdlEntry, KdlNode};
2use serde::Serialize;
3use std::fmt::Display;
4use std::hash::Hash;
5use std::str::FromStr;
6
7use crate::error::UsageErr;
8use crate::spec::builder::SpecArgBuilder;
9use crate::spec::context::ParsingContext;
10use crate::spec::effect::{SpecCommandEffect, EFFECT_VALUES};
11use crate::spec::helpers::{string_entry, NodeHelper};
12use crate::spec::is_false;
13use crate::{string, SpecAdmonition, SpecAdmonitionKind, SpecChoices};
14#[cfg(feature = "clap")]
15use crate::{SpecChoice, SpecChoiceAlias};
16
17/// A value comparison that can make another argument required.
18#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
19pub struct SpecRequiredIfEq {
20    pub selector: String,
21    pub value: String,
22}
23
24#[derive(Debug, Default, Clone, Serialize, PartialEq, Eq, strum::EnumString, strum::Display)]
25#[strum(serialize_all = "snake_case")]
26pub enum SpecDoubleDashChoices {
27    /// Once an arg is entered, behave as if "--" was passed
28    Automatic,
29    /// Allow "--" to be passed
30    #[default]
31    Optional,
32    /// Require "--" to be passed
33    Required,
34    /// Preserve "--" tokens as values (only for variadic args)
35    Preserve,
36}
37
38/// A positional argument specification.
39///
40/// Arguments are positional values passed to a command without a flag prefix.
41/// They can be required or optional, and can accept multiple values (variadic).
42///
43/// # Example
44///
45/// ```
46/// use usage::SpecArg;
47///
48/// let arg = SpecArg::builder()
49///     .name("file")
50///     .required(true)
51///     .help("Input file to process")
52///     .build();
53/// ```
54#[derive(Debug, Default, Clone, Serialize)]
55#[non_exhaustive]
56pub struct SpecArg {
57    /// Name of the argument (used in help text)
58    pub name: String,
59    /// Ordered placeholders for a fixed-arity value, such as `START` and `END`.
60    /// Empty means the argument's `name` is the sole placeholder.
61    #[serde(skip_serializing_if = "Vec::is_empty")]
62    pub value_names: Vec<String>,
63    /// Generated usage string (e.g., "<file>" or "[file]")
64    pub usage: String,
65    /// Short help text shown in command listings
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub help: Option<String>,
68    /// Extended help text shown with --help
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub help_long: Option<String>,
71    /// Markdown-formatted help text
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub help_md: Option<String>,
74    /// Structured notes and warnings, in presentation order.
75    #[serde(skip_serializing_if = "Vec::is_empty")]
76    pub admonitions: Vec<SpecAdmonition>,
77    /// First line of help text (auto-generated)
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub help_first_line: Option<String>,
80    /// Whether this argument must be provided
81    pub required: bool,
82    /// How to handle the "--" separator
83    pub double_dash: SpecDoubleDashChoices,
84    /// Whether this argument accepts multiple values
85    #[serde(skip_serializing_if = "is_false")]
86    pub var: bool,
87    /// Minimum number of values for variadic arguments
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub var_min: Option<usize>,
90    /// Maximum number of values for variadic arguments
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub var_max: Option<usize>,
93    /// The character a single word is split on to produce several values.
94    ///
95    /// `--tags a,b,c` as three values rather than one, which is clap's
96    /// `value_delimiter`. Only meaningful where several values can land, so it goes with
97    /// [`SpecArg::var`]; declaring it anywhere else is refused rather than silently
98    /// dropping everything after the first separator.
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub delimiter: Option<char>,
101    /// Accept negative numeric tokens without accepting arbitrary dash-prefixed words.
102    #[serde(skip_serializing_if = "is_false")]
103    pub allow_negative_numbers: bool,
104    /// End this variadic argument when this token is seen, without binding it.
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub value_terminator: Option<String>,
107    /// Whether to hide this argument from help output
108    pub hide: bool,
109    /// Hide the default annotation while keeping the default behavior.
110    #[serde(skip_serializing_if = "is_false")]
111    pub hide_default_value: bool,
112    /// Hide the environment annotation entirely.
113    #[serde(skip_serializing_if = "is_false")]
114    pub hide_env: bool,
115    /// Hide an environment value while retaining its variable name.
116    #[serde(skip_serializing_if = "is_false")]
117    pub hide_env_values: bool,
118    /// Hide possible values from help without changing validation.
119    #[serde(skip_serializing_if = "is_false")]
120    pub hide_possible_values: bool,
121    /// Hide this argument only from short help.
122    #[serde(skip_serializing_if = "is_false")]
123    pub hide_short_help: bool,
124    /// Hide this argument only from long help.
125    #[serde(skip_serializing_if = "is_false")]
126    pub hide_long_help: bool,
127    /// Arguments and flags that cannot be given alongside this positional.
128    ///
129    /// A bare selector names another positional by name; flag selectors keep their
130    /// `--long` or `-s` spelling.
131    #[serde(skip_serializing_if = "Vec::is_empty")]
132    pub conflicts: Vec<String>,
133    /// Arguments that must also be present when this positional is present.
134    #[serde(skip_serializing_if = "Vec::is_empty")]
135    pub requires: Vec<String>,
136    /// Presence conditions, any one of which makes this positional required.
137    #[serde(skip_serializing_if = "Vec::is_empty")]
138    pub required_if: Vec<String>,
139    /// Value conditions, any one of which makes this positional required.
140    #[serde(skip_serializing_if = "Vec::is_empty")]
141    pub required_if_eq: Vec<SpecRequiredIfEq>,
142    /// Value conditions which must all match to make this positional required.
143    #[serde(skip_serializing_if = "Vec::is_empty")]
144    pub required_if_eq_all: Vec<SpecRequiredIfEq>,
145    /// Any present selector waives this positional's requirement.
146    #[serde(skip_serializing_if = "Vec::is_empty")]
147    pub required_unless: Vec<String>,
148    /// Only the presence of every selector waives this positional's requirement.
149    #[serde(skip_serializing_if = "Vec::is_empty")]
150    pub required_unless_all: Vec<String>,
151    /// Default value(s) if the argument is not provided
152    #[serde(skip_serializing_if = "Vec::is_empty")]
153    pub default: Vec<String>,
154    /// Valid choices for this argument
155    #[serde(skip_serializing_if = "Option::is_none")]
156    pub choices: Option<SpecChoices>,
157    /// A portable expr expression that must return true for each raw value.
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub validate: Option<String>,
160    /// Message reported when [`SpecArg::validate`] returns false.
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub validate_error: Option<String>,
163    /// Raises the effect of the command when this argument is supplied.
164    /// See [`crate::spec::effect::SpecCommandEffect`]; never lowers it.
165    #[serde(skip_serializing_if = "Option::is_none")]
166    pub effect: Option<SpecCommandEffect>,
167    /// Environment variable that can provide this argument's value
168    #[serde(skip_serializing_if = "Option::is_none")]
169    pub env: Option<String>,
170    /// Additional environment variables, consulted in declaration order.
171    #[serde(skip_serializing_if = "Vec::is_empty")]
172    pub env_fallback: Vec<String>,
173    /// Deprecated environment aliases, consulted after ordinary fallbacks.
174    #[serde(skip_serializing_if = "Vec::is_empty")]
175    pub deprecated_env: Vec<String>,
176    /// Heading this argument is listed under in help output. Presentational only,
177    /// like the flag field of the same name.
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub help_heading: Option<String>,
180    /// Named audience or contract surface this argument belongs to.
181    ///
182    /// Metadata only: parsers and help renderers do not filter on this value.
183    #[serde(skip_serializing_if = "Option::is_none")]
184    pub surface: Option<String>,
185    /// Conditions under which this argument is available, in declaration order.
186    ///
187    /// These are descriptive labels for docs, schema consumers, and compatibility tools.
188    #[serde(skip_serializing_if = "Vec::is_empty")]
189    pub available_if: Vec<String>,
190    /// Explicit placement within its help section.
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub display_order: Option<usize>,
193}
194
195impl SpecArg {
196    /// Create a new builder for SpecArg
197    pub fn builder() -> SpecArgBuilder {
198        SpecArgBuilder::new()
199    }
200
201    /// Environment variable names in the order used to fill this argument.
202    pub fn env_names(&self) -> impl Iterator<Item = &str> {
203        self.env
204            .iter()
205            .map(String::as_str)
206            .chain(self.env_fallback.iter().map(String::as_str))
207            .chain(self.deprecated_env.iter().map(String::as_str))
208    }
209
210    pub(crate) fn parse(ctx: &ParsingContext, node: &NodeHelper) -> Result<Self, UsageErr> {
211        let mut arg: SpecArg = node.arg(0)?.ensure_string()?.parse()?;
212        for (k, v) in node.props() {
213            match k {
214                "help" => arg.help = Some(v.ensure_string()?),
215                "long_help" => arg.help_long = Some(v.ensure_string()?),
216                "help_long" => arg.help_long = Some(v.ensure_string()?),
217                "help_md" => arg.help_md = Some(v.ensure_string()?),
218                "required" => arg.required = v.ensure_bool()?,
219                "double_dash" => arg.double_dash = v.ensure_string()?.parse()?,
220                "var" => arg.var = v.ensure_bool()?,
221                "delimiter" => {
222                    let raw = v.ensure_string()?;
223                    let mut chars = raw.chars();
224                    match (chars.next(), chars.next()) {
225                        // ASCII, not merely one character. Splitting is by byte everywhere
226                        // below this — the derive says so where it reads the same property —
227                        // and a non-ASCII separator has no single byte to be. Worse than
228                        // having none: its bytes are continuation bytes, which appear inside
229                        // unrelated characters, so it would split words nobody separated.
230                        (Some(c), None) if c.is_ascii() => arg.delimiter = Some(c),
231                        (Some(c), None) => bail_parse!(
232                            ctx,
233                            v.entry.span(),
234                            "a delimiter is one byte, and {c:?} is more than one; use an \
235                             ASCII separator"
236                        ),
237                        _ => bail_parse!(
238                            ctx,
239                            v.entry.span(),
240                            "a delimiter is one character, and {raw:?} is not"
241                        ),
242                    }
243                }
244                "allow_negative_numbers" => arg.allow_negative_numbers = v.ensure_bool()?,
245                "value_terminator" => arg.value_terminator = v.ensure_string().map(Some)?,
246                "hide" => arg.hide = v.ensure_bool()?,
247                "hide_default_value" => arg.hide_default_value = v.ensure_bool()?,
248                "hide_env" => arg.hide_env = v.ensure_bool()?,
249                "hide_env_values" => arg.hide_env_values = v.ensure_bool()?,
250                "hide_possible_values" => arg.hide_possible_values = v.ensure_bool()?,
251                "hide_short_help" => arg.hide_short_help = v.ensure_bool()?,
252                "hide_long_help" => arg.hide_long_help = v.ensure_bool()?,
253                "conflicts" => arg.conflicts = vec![v.ensure_string()?],
254                "requires" => arg.requires = vec![v.ensure_string()?],
255                "required_if" => arg.required_if = vec![v.ensure_string()?],
256                "required_unless" => arg.required_unless = vec![v.ensure_string()?],
257                "required_unless_all" => arg.required_unless_all = vec![v.ensure_string()?],
258                "var_min" => arg.var_min = v.ensure_usize().map(Some)?,
259                "var_max" => arg.var_max = v.ensure_usize().map(Some)?,
260                "default" => arg.default = vec![v.ensure_string()?],
261                "effect" => {
262                    let raw = v.ensure_string()?;
263                    match raw.parse() {
264                        Ok(effect) => arg.effect = Some(effect),
265                        Err(_) => bail_parse!(
266                            ctx,
267                            v.entry.span(),
268                            "unsupported effect {raw}, expected one of: {EFFECT_VALUES}"
269                        ),
270                    }
271                }
272                "env" => arg.env = v.ensure_string().map(Some)?,
273                "env_fallback" => arg.env_fallback = vec![v.ensure_string()?],
274                "deprecated_env" => arg.deprecated_env = vec![v.ensure_string()?],
275                "validate" => arg.validate = v.ensure_string().map(Some)?,
276                "validate_error" => arg.validate_error = v.ensure_string().map(Some)?,
277                "help_heading" => arg.help_heading = v.ensure_string().map(Some)?,
278                "surface" => arg.surface = v.ensure_string().map(Some)?,
279                "available_if" => arg.available_if = vec![v.ensure_string()?],
280                "display_order" => arg.display_order = v.ensure_usize().map(Some)?,
281                k => bail_parse!(ctx, v.entry.span(), "unsupported arg key {k}"),
282            }
283        }
284        if !arg.default.is_empty() {
285            arg.required = false;
286        }
287        for child in node.children() {
288            match child.name() {
289                "choices" => arg.choices = Some(SpecChoices::parse(ctx, &child)?),
290                "effect" => {
291                    let a = child.arg(0)?;
292                    let raw = a.ensure_string()?;
293                    match raw.parse() {
294                        Ok(effect) => arg.effect = Some(effect),
295                        Err(_) => bail_parse!(
296                            ctx,
297                            a.entry.span(),
298                            "unsupported effect {raw}, expected one of: {EFFECT_VALUES}"
299                        ),
300                    }
301                }
302                "env" => arg.env = child.arg(0)?.ensure_string().map(Some)?,
303                "env_fallback" => arg.env_fallback = string_args(&child)?,
304                "deprecated_env" => arg.deprecated_env = string_args(&child)?,
305                "validate" => arg.validate = child.arg(0)?.ensure_string().map(Some)?,
306                "validate_error" => {
307                    arg.validate_error = child.arg(0)?.ensure_string().map(Some)?;
308                }
309                "help_heading" => {
310                    arg.help_heading = child.arg(0)?.ensure_string().map(Some)?;
311                }
312                "surface" => arg.surface = child.arg(0)?.ensure_string().map(Some)?,
313                "available_if" => arg.available_if = string_args(&child)?,
314                "display_order" => {
315                    arg.display_order = child.arg(0)?.ensure_usize().map(Some)?;
316                }
317                "default" => {
318                    // Support both single value and multiple values
319                    // default "bar"            -> vec!["bar"]
320                    // default { "xyz"; "bar" } -> vec!["xyz", "bar"]
321                    let children = child.children();
322                    if children.is_empty() {
323                        // Single value: default "bar"
324                        arg.default = vec![child.arg(0)?.ensure_string()?];
325                    } else {
326                        // Multiple values from children: default { "xyz"; "bar" }
327                        // In KDL, these are child nodes where the string is the node name
328                        arg.default = children.iter().map(|c| c.name().to_string()).collect();
329                    }
330                }
331                "help" => arg.help = Some(child.arg(0)?.ensure_string()?),
332                "long_help" => arg.help_long = Some(child.arg(0)?.ensure_string()?),
333                "help_long" => arg.help_long = Some(child.arg(0)?.ensure_string()?),
334                "help_md" => arg.help_md = Some(child.arg(0)?.ensure_string()?),
335                "note" => arg
336                    .admonitions
337                    .push(SpecAdmonition::note(child.arg(0)?.ensure_string()?)),
338                "warning" => arg
339                    .admonitions
340                    .push(SpecAdmonition::warning(child.arg(0)?.ensure_string()?)),
341                "required" => arg.required = child.arg(0)?.ensure_bool()?,
342                "var" => arg.var = child.arg(0)?.ensure_bool()?,
343                "var_min" => arg.var_min = child.arg(0)?.ensure_usize().map(Some)?,
344                "var_max" => arg.var_max = child.arg(0)?.ensure_usize().map(Some)?,
345                "value_names" => {
346                    arg.value_names = child
347                        .ensure_arg_len(1..)?
348                        .args()
349                        .map(|entry| entry.ensure_string())
350                        .collect::<Result<Vec<_>, _>>()?;
351                }
352                "allow_negative_numbers" => {
353                    arg.allow_negative_numbers = child.arg(0)?.ensure_bool()?;
354                }
355                "value_terminator" => {
356                    arg.value_terminator = child.arg(0)?.ensure_string().map(Some)?;
357                }
358                "hide" => arg.hide = child.arg(0)?.ensure_bool()?,
359                "hide_default_value" => arg.hide_default_value = child.arg(0)?.ensure_bool()?,
360                "hide_env" => arg.hide_env = child.arg(0)?.ensure_bool()?,
361                "hide_env_values" => arg.hide_env_values = child.arg(0)?.ensure_bool()?,
362                "hide_possible_values" => arg.hide_possible_values = child.arg(0)?.ensure_bool()?,
363                "hide_short_help" => arg.hide_short_help = child.arg(0)?.ensure_bool()?,
364                "hide_long_help" => arg.hide_long_help = child.arg(0)?.ensure_bool()?,
365                "conflicts" => {
366                    arg.conflicts = child
367                        .ensure_arg_len(1..)?
368                        .args()
369                        .map(|entry| entry.ensure_string())
370                        .collect::<Result<Vec<_>, _>>()?;
371                }
372                "requires" => arg.requires = string_args(&child)?,
373                "required_if" => arg.required_if = string_args(&child)?,
374                "required_if_eq" => arg.required_if_eq.push(required_if_eq(&child)?),
375                "required_if_eq_all" => {
376                    let len = child.args().count();
377                    if len < 2 || len % 2 != 0 {
378                        bail_parse!(
379                            ctx,
380                            child.node.name().span(),
381                            "required_if_eq_all needs selector/value pairs"
382                        );
383                    }
384                    arg.required_if_eq_all = required_if_eq_pairs(&child)?;
385                }
386                "required_unless" => arg.required_unless = string_args(&child)?,
387                "required_unless_all" => arg.required_unless_all = string_args(&child)?,
388                "double_dash" => arg.double_dash = child.arg(0)?.ensure_string()?.parse()?,
389                k => bail_parse!(ctx, child.node.name().span(), "unsupported arg child {k}"),
390            }
391        }
392        if let Some(first) = arg.value_names.first() {
393            arg.name.clone_from(first);
394        }
395        if arg.value_names.len() > 1 {
396            let arity = arg.value_names.len();
397            match (arg.var_min, arg.var_max) {
398                (None, None) => {
399                    arg.var_min = Some(arity);
400                    arg.var_max = Some(arity);
401                }
402                (Some(min), Some(max)) if min == arity && max == arity => {}
403                _ => bail_parse!(
404                    ctx,
405                    node.node.name().span(),
406                    "{arity} value names require var_min={arity} and var_max={arity}"
407                ),
408            }
409            arg.var = true;
410        }
411        if arg.validate_error.is_some() && arg.validate.is_none() {
412            bail_parse!(
413                ctx,
414                node.node.name().span(),
415                "validate_error requires a validate expression"
416            );
417        }
418        if arg.value_terminator.as_deref() == Some("") {
419            bail_parse!(
420                ctx,
421                node.node.name().span(),
422                "value_terminator cannot be empty"
423            );
424        }
425        if arg.value_terminator.is_some() && !arg.var {
426            bail_parse!(
427                ctx,
428                node.node.name().span(),
429                "value_terminator requires a variadic argument"
430            );
431        }
432        #[cfg(feature = "validation")]
433        if let Some(expression) = &arg.validate {
434            if let Err(error) = usage_validation::check(expression) {
435                bail_parse!(
436                    ctx,
437                    node.node.name().span(),
438                    "invalid validation expression: {error}"
439                );
440            }
441        }
442        arg.usage = arg.usage();
443        if let Some(help) = &arg.help {
444            arg.help_first_line = Some(string::first_line(help));
445        }
446        Ok(arg)
447    }
448}
449
450impl SpecArg {
451    pub fn usage(&self) -> String {
452        let exact_arity = self.var.then_some(()).and_then(|()| {
453            self.var_min
454                .zip(self.var_max)
455                .filter(|(min, max)| min == max && *min > 1)
456                .map(|(arity, _)| arity)
457        });
458        if self.value_names.len() > 1 || exact_arity.is_some() {
459            let labels = if self.value_names.len() > 1 {
460                self.value_names.clone()
461            } else {
462                vec![
463                    self.value_names
464                        .first()
465                        .cloned()
466                        .unwrap_or_else(|| self.name.clone());
467                    exact_arity.expect("branch checked")
468                ]
469            };
470            let placeholders = labels
471                .iter()
472                .map(|name| {
473                    if self.required {
474                        format!("<{name}>")
475                    } else {
476                        format!("[{name}]")
477                    }
478                })
479                .collect::<Vec<_>>()
480                .join(" ");
481            return if self.double_dash == SpecDoubleDashChoices::Required {
482                format!("-- {placeholders}")
483            } else {
484                placeholders
485            };
486        }
487        let name = if self.double_dash == SpecDoubleDashChoices::Required {
488            format!("-- {}", self.name)
489        } else {
490            self.name.clone()
491        };
492        let mut name = if self.required {
493            format!("<{name}>")
494        } else {
495            format!("[{name}]")
496        };
497        if self.var {
498            name = format!("{name}…");
499        }
500        name
501    }
502}
503
504impl From<&SpecArg> for KdlNode {
505    fn from(arg: &SpecArg) -> Self {
506        let mut node = KdlNode::new("arg");
507        node.push(KdlEntry::new(arg.usage()));
508        if let Some(desc) = &arg.help {
509            node.push(string_entry(Some("help"), desc));
510        }
511        if let Some(desc) = &arg.help_long {
512            node.push(string_entry(Some("help_long"), desc));
513        }
514        if let Some(desc) = &arg.help_md {
515            node.push(string_entry(Some("help_md"), desc));
516        }
517        for admonition in &arg.admonitions {
518            let children = node.children_mut().get_or_insert_with(KdlDocument::new);
519            let name = match admonition.kind {
520                SpecAdmonitionKind::Note => "note",
521                SpecAdmonitionKind::Warning => "warning",
522            };
523            let mut block = KdlNode::new(name);
524            block.push(string_entry(None, &admonition.text));
525            children.nodes_mut().push(block);
526        }
527        if !arg.required {
528            node.push(KdlEntry::new_prop("required", false));
529        }
530        if arg.double_dash == SpecDoubleDashChoices::Automatic
531            || arg.double_dash == SpecDoubleDashChoices::Preserve
532        {
533            node.push(KdlEntry::new_prop(
534                "double_dash",
535                arg.double_dash.to_string(),
536            ));
537        }
538        if arg.var {
539            node.push(KdlEntry::new_prop("var", true));
540        }
541        if let Some(min) = arg.var_min {
542            node.push(KdlEntry::new_prop("var_min", min as i128));
543        }
544        if let Some(max) = arg.var_max {
545            node.push(KdlEntry::new_prop("var_max", max as i128));
546        }
547        if let Some(delimiter) = arg.delimiter {
548            node.push(string_entry(Some("delimiter"), &delimiter.to_string()));
549        }
550        if arg.allow_negative_numbers {
551            node.push(KdlEntry::new_prop("allow_negative_numbers", true));
552        }
553        if let Some(terminator) = &arg.value_terminator {
554            node.push(string_entry(Some("value_terminator"), terminator));
555        }
556        if arg.hide {
557            node.push(KdlEntry::new_prop("hide", true));
558        }
559        for (name, hidden) in [
560            ("hide_default_value", arg.hide_default_value),
561            ("hide_env", arg.hide_env),
562            ("hide_env_values", arg.hide_env_values),
563            ("hide_possible_values", arg.hide_possible_values),
564            ("hide_short_help", arg.hide_short_help),
565            ("hide_long_help", arg.hide_long_help),
566        ] {
567            if hidden {
568                node.push(KdlEntry::new_prop(name, true));
569            }
570        }
571        if arg.conflicts.len() == 1 {
572            node.push(string_entry(Some("conflicts"), &arg.conflicts[0]));
573        } else if !arg.conflicts.is_empty() {
574            let children = node.children_mut().get_or_insert_with(KdlDocument::new);
575            let mut conflicts = KdlNode::new("conflicts");
576            for target in &arg.conflicts {
577                conflicts.push(string_entry(None, target));
578            }
579            children.nodes_mut().push(conflicts);
580        }
581        serialize_selector_list(&mut node, "requires", &arg.requires);
582        serialize_selector_list(&mut node, "required_if", &arg.required_if);
583        serialize_required_if_eq(&mut node, "required_if_eq", &arg.required_if_eq);
584        if !arg.required_if_eq_all.is_empty() {
585            serialize_required_if_eq(&mut node, "required_if_eq_all", &arg.required_if_eq_all);
586        }
587        serialize_selector_list(&mut node, "required_unless", &arg.required_unless);
588        serialize_selector_list(&mut node, "required_unless_all", &arg.required_unless_all);
589        // Serialize default values
590        if !arg.default.is_empty() {
591            if arg.default.len() == 1 {
592                // Single value: use property default="bar"
593                node.push(string_entry(Some("default"), &arg.default[0]));
594            } else {
595                // Multiple values: use child node default { "xyz"; "bar" }
596                let children = node.children_mut().get_or_insert_with(KdlDocument::new);
597                let mut default_node = KdlNode::new("default");
598                let default_children = default_node
599                    .children_mut()
600                    .get_or_insert_with(KdlDocument::new);
601                for val in &arg.default {
602                    default_children
603                        .nodes_mut()
604                        .push(KdlNode::new(val.as_str()));
605                }
606                children.nodes_mut().push(default_node);
607            }
608        }
609        if let Some(env) = &arg.env {
610            node.push(string_entry(Some("env"), env));
611        }
612        serialize_selector_list(&mut node, "env_fallback", &arg.env_fallback);
613        serialize_selector_list(&mut node, "deprecated_env", &arg.deprecated_env);
614        if let Some(validate) = &arg.validate {
615            node.push(string_entry(Some("validate"), validate));
616        }
617        if arg.validate.is_some() {
618            if let Some(error) = &arg.validate_error {
619                node.push(string_entry(Some("validate_error"), error));
620            }
621        }
622        if let Some(help_heading) = &arg.help_heading {
623            node.push(string_entry(Some("help_heading"), help_heading));
624        }
625        if let Some(surface) = &arg.surface {
626            node.push(string_entry(Some("surface"), surface));
627        }
628        serialize_selector_list(&mut node, "available_if", &arg.available_if);
629        if let Some(order) = arg.display_order {
630            node.push(KdlEntry::new_prop("display_order", order as i128));
631        }
632        if let Some(effect) = &arg.effect {
633            node.push(string_entry(Some("effect"), effect.as_str()));
634        }
635        if let Some(choices) = &arg.choices {
636            let children = node.children_mut().get_or_insert_with(KdlDocument::new);
637            children.nodes_mut().push(choices.into());
638        }
639        node
640    }
641}
642
643fn string_args(node: &NodeHelper<'_>) -> Result<Vec<String>, UsageErr> {
644    node.ensure_arg_len(1..)?
645        .args()
646        .map(|entry| entry.ensure_string())
647        .collect()
648}
649
650fn required_if_eq(node: &NodeHelper<'_>) -> Result<SpecRequiredIfEq, UsageErr> {
651    node.ensure_arg_len(2..=2)?;
652    Ok(SpecRequiredIfEq {
653        selector: node.arg(0)?.ensure_string()?,
654        value: node.arg(1)?.ensure_string()?,
655    })
656}
657
658fn required_if_eq_pairs(node: &NodeHelper<'_>) -> Result<Vec<SpecRequiredIfEq>, UsageErr> {
659    let entries = node.args().collect::<Vec<_>>();
660    entries
661        .chunks_exact(2)
662        .map(|pair| {
663            Ok(SpecRequiredIfEq {
664                selector: pair[0].ensure_string()?,
665                value: pair[1].ensure_string()?,
666            })
667        })
668        .collect()
669}
670
671fn serialize_selector_list(node: &mut KdlNode, name: &str, selectors: &[String]) {
672    if selectors.len() == 1 {
673        node.push(string_entry(Some(name), &selectors[0]));
674    } else if !selectors.is_empty() {
675        let children = node.children_mut().get_or_insert_with(KdlDocument::new);
676        let mut relation = KdlNode::new(name);
677        for selector in selectors {
678            relation.push(string_entry(None, selector));
679        }
680        children.nodes_mut().push(relation);
681    }
682}
683
684fn serialize_required_if_eq(node: &mut KdlNode, name: &str, conditions: &[SpecRequiredIfEq]) {
685    if conditions.is_empty() {
686        return;
687    }
688    let children = node.children_mut().get_or_insert_with(KdlDocument::new);
689    if name == "required_if_eq_all" {
690        let mut relation = KdlNode::new(name);
691        for condition in conditions {
692            relation.push(string_entry(None, &condition.selector));
693            relation.push(string_entry(None, &condition.value));
694        }
695        children.nodes_mut().push(relation);
696    } else {
697        for condition in conditions {
698            let mut relation = KdlNode::new(name);
699            relation.push(string_entry(None, &condition.selector));
700            relation.push(string_entry(None, &condition.value));
701            children.nodes_mut().push(relation);
702        }
703    }
704}
705
706impl From<&str> for SpecArg {
707    fn from(input: &str) -> Self {
708        let (input, after_double_dash) = input
709            .strip_prefix("-- ")
710            .map_or((input, false), |rest| (rest, true));
711        if let Some(placeholders) = fixed_placeholders(input) {
712            let required = placeholders
713                .iter()
714                .all(|placeholder| placeholder.starts_with('<'));
715            let value_names = placeholders
716                .iter()
717                .map(|placeholder| placeholder[1..placeholder.len() - 1].to_string())
718                .collect::<Vec<_>>();
719            let mut arg = SpecArg {
720                name: value_names[0].clone(),
721                value_names,
722                required,
723                var: true,
724                var_min: Some(placeholders.len()),
725                var_max: Some(placeholders.len()),
726                double_dash: if after_double_dash {
727                    SpecDoubleDashChoices::Required
728                } else {
729                    SpecDoubleDashChoices::Optional
730                },
731                ..Default::default()
732            };
733            arg.usage = arg.usage();
734            return arg;
735        }
736        let mut arg = SpecArg {
737            name: input.to_string(),
738            required: true,
739            double_dash: if after_double_dash {
740                SpecDoubleDashChoices::Required
741            } else {
742                SpecDoubleDashChoices::Optional
743            },
744            ..Default::default()
745        };
746        // Handle trailing ellipsis: "foo..." or "foo…" or "<foo>..." or "[foo]..."
747        if let Some(name) = arg
748            .name
749            .strip_suffix("...")
750            .or_else(|| arg.name.strip_suffix("…"))
751        {
752            arg.var = true;
753            arg.name = name.to_string();
754        }
755        let first = arg.name.chars().next().unwrap_or_default();
756        let last = arg.name.chars().last().unwrap_or_default();
757        match (first, last) {
758            ('[', ']') => {
759                arg.name = arg.name[1..arg.name.len() - 1].to_string();
760                arg.required = false;
761            }
762            ('<', '>') => {
763                arg.name = arg.name[1..arg.name.len() - 1].to_string();
764            }
765            _ => {}
766        }
767        // The single-placeholder shorthand encloses the separator with the value:
768        // `[-- target]`. Multi-placeholder canonical output puts it before the
769        // placeholders (`-- [START] [END]`) and was handled above.
770        if let Some(name) = arg.name.strip_prefix("-- ") {
771            arg.double_dash = SpecDoubleDashChoices::Required;
772            arg.name = name.to_string();
773        }
774        // Also handle ellipsis inside brackets: "[args...]" or "<args...>"
775        if !arg.var {
776            if let Some(name) = arg
777                .name
778                .strip_suffix("...")
779                .or_else(|| arg.name.strip_suffix("…"))
780            {
781                arg.var = true;
782                arg.name = name.to_string();
783            }
784        }
785        // As `SpecArg::parse` does for the KDL child-node spelling. Without it, an arg
786        // written inline on a flag (`flag "--format <FMT>"`) carried an empty `usage`
787        // until the spec had been through one round trip, at which point it came back as
788        // a child node and got one — so a spec was not equal to itself re-read.
789        arg.usage = arg.usage();
790        arg
791    }
792}
793impl FromStr for SpecArg {
794    type Err = UsageErr;
795    fn from_str(input: &str) -> std::result::Result<Self, UsageErr> {
796        if fixed_placeholders(input.strip_prefix("-- ").unwrap_or(input)).is_some_and(
797            |placeholders| {
798                placeholders
799                    .windows(2)
800                    .any(|pair| pair[0].starts_with('<') != pair[1].starts_with('<'))
801            },
802        ) {
803            let message =
804                "fixed-arity placeholders must be either all required or all optional".to_string();
805            return Err(UsageErr::InvalidInput(
806                message,
807                (0, input.len()).into(),
808                miette::NamedSource::new("argument", input.to_string()),
809            ));
810        }
811        Ok(input.into())
812    }
813}
814
815/// Return a multi-placeholder declaration without allocating for the overwhelmingly common
816/// single-placeholder case.
817fn fixed_placeholders(input: &str) -> Option<Vec<&str>> {
818    if !input.bytes().any(|byte| byte.is_ascii_whitespace()) {
819        return None;
820    }
821    let placeholders: Vec<_> = input.split_whitespace().collect();
822    (placeholders.len() > 1
823        && placeholders.iter().all(|placeholder| {
824            matches!(
825                (placeholder.chars().next(), placeholder.chars().last()),
826                (Some('<'), Some('>')) | (Some('['), Some(']'))
827            )
828        }))
829    .then_some(placeholders)
830}
831
832/// A clap argument's defaults, as the spec has to record them.
833///
834/// clap splits a value by the argument's `value_delimiter` before anyone sees it, defaults
835/// included — so `default_value = "a,b,c"` with `value_delimiter = ','` is three values, not one.
836/// The spec has no delimiter of its own; it has a list, which is the same statement. Recording the
837/// joined string instead described a CLI whose default is a single value that its own `choices`
838/// forbid, which is how mise's `--fs-events` reached the spec.
839#[cfg(feature = "clap")]
840pub(crate) fn default_values(arg: &clap::Arg) -> Vec<String> {
841    let raw = arg
842        .get_default_values()
843        .iter()
844        .map(|v| v.to_string_lossy().to_string());
845    match arg.get_value_delimiter() {
846        Some(delimiter) => raw
847            .flat_map(|v| {
848                v.split(delimiter)
849                    .map(|part| part.to_string())
850                    .collect::<Vec<_>>()
851            })
852            .collect(),
853        None => raw.collect(),
854    }
855}
856
857/// Carry clap's value-count range into the spec where the two parsers mean the same thing.
858///
859/// A positional may accept zero values through its ordinary optionality. A flag
860/// with `num_args(0..)` additionally permits a bare occurrence; callers pass
861/// `zero_values_supported` only when they also carry that executable policy on
862/// the containing flag.
863#[cfg(feature = "clap")]
864pub(crate) fn value_bounds(source: &clap::Arg, target: &mut SpecArg, zero_values_supported: bool) {
865    // clap verifies num_args against raw command-line tokens and only splits each token on the
866    // delimiter afterward. Usage splits first and its bounds count the resulting values. Carrying
867    // the range would therefore change the contract (for example, two comma-separated tokens can
868    // become four Usage values), so leave it unmapped until the spec can distinguish both counts.
869    if source.get_value_delimiter().is_some() {
870        return;
871    }
872
873    let Some(range) = source.get_num_args() else {
874        if target.value_names.len() > 1 {
875            let arity = target.value_names.len();
876            target.var = true;
877            target.var_min = Some(arity);
878            target.var_max = Some(arity);
879        }
880        return;
881    };
882    let min = range.min_values();
883    let max = range.max_values();
884    if max <= 1 || min == 0 && !zero_values_supported {
885        return;
886    }
887
888    target.var = true;
889    target.var_min = Some(min);
890    target.var_max = (max != usize::MAX).then_some(max);
891}
892
893/// Value labels that can survive the spec's fixed-arity representation.
894///
895/// Clap permits several labels beside a ranged `num_args`; usage gives distinct labels only to
896/// an exact number of slots. Keep the first display label for a range and let the fidelity report
897/// name the loss instead of emitting KDL that cannot be parsed back.
898#[cfg(feature = "clap")]
899pub(crate) fn value_names_from_clap(source: &clap::Arg) -> Vec<String> {
900    let names: Vec<String> = source
901        .get_value_names()
902        .unwrap_or_default()
903        .iter()
904        .map(ToString::to_string)
905        .collect();
906    if names.len() <= 1 {
907        return names;
908    }
909    let mismatched_range = source.get_num_args().is_some_and(|range| {
910        range.min_values() != names.len() || range.max_values() != names.len()
911    });
912    if source.get_value_delimiter().is_some() || mismatched_range {
913        names.into_iter().take(1).collect()
914    } else {
915        names
916    }
917}
918
919/// The portable completion type corresponding to clap's complete `ValueHint` vocabulary.
920#[cfg(feature = "clap")]
921pub(crate) fn value_hint_type(hint: clap::ValueHint) -> Option<&'static str> {
922    use clap::ValueHint;
923
924    match hint {
925        ValueHint::Unknown => None,
926        ValueHint::Other => Some("none"),
927        ValueHint::AnyPath | ValueHint::FilePath => Some("path"),
928        ValueHint::DirPath => Some("dir"),
929        ValueHint::ExecutablePath => Some("executable"),
930        ValueHint::CommandName | ValueHint::CommandString => Some("command"),
931        ValueHint::CommandWithArguments => Some("command_args"),
932        ValueHint::Username => Some("username"),
933        ValueHint::Hostname => Some("hostname"),
934        ValueHint::Url => Some("url"),
935        ValueHint::EmailAddress => Some("email"),
936        _ => None,
937    }
938}
939
940#[cfg(feature = "clap")]
941pub(crate) fn choices_from_clap(arg: &clap::Arg) -> Option<SpecChoices> {
942    let possible = arg.get_possible_values();
943    if possible.is_empty() {
944        return None;
945    }
946    let choices = possible
947        .iter()
948        .map(|value| value.get_name().to_string())
949        .collect();
950    let details = possible
951        .iter()
952        .filter_map(|value| {
953            let aliases: Vec<_> = value
954                .get_name_and_aliases()
955                .skip(1)
956                .map(|alias| SpecChoiceAlias {
957                    value: alias.to_string(),
958                    // clap PossibleValue aliases are always hidden.
959                    hide: true,
960                })
961                .collect();
962            let detail = SpecChoice {
963                value: value.get_name().to_string(),
964                help: value.get_help().map(ToString::to_string),
965                hide: value.is_hide_set(),
966                aliases,
967            };
968            (detail.help.is_some() || detail.hide || !detail.aliases.is_empty()).then_some(detail)
969        })
970        .collect();
971    Some(SpecChoices {
972        choices,
973        details,
974        ignore_case: arg.is_ignore_case_set(),
975        ..Default::default()
976    })
977}
978
979#[cfg(feature = "clap")]
980impl From<&clap::Arg> for SpecArg {
981    fn from(arg: &clap::Arg) -> Self {
982        let source = arg;
983        let required = arg.is_required_set();
984        let help = arg.get_help().map(|s| s.to_string());
985        let help_long = arg.get_long_help().map(|s| s.to_string());
986        let help_first_line = help.as_ref().map(|s| string::first_line(s));
987        let hide = arg.is_hide_set();
988        // One byte only, for the reason given on the flag: a wider separator cannot be
989        // written back out. `var` below still reads the original, since clap splits on it
990        // either way and the field does collect several values.
991        let delimiter = arg.get_value_delimiter();
992        let recorded_delimiter = delimiter.filter(char::is_ascii);
993        let value_terminator = arg.get_value_terminator().map(ToString::to_string);
994        let var = matches!(
995            arg.get_action(),
996            clap::ArgAction::Count | clap::ArgAction::Append
997        ) || delimiter.is_some();
998        let choices = choices_from_clap(arg);
999        let value_names = value_names_from_clap(arg);
1000        let mut arg = Self {
1001            name: value_names
1002                .first()
1003                .cloned()
1004                .unwrap_or_else(|| source.get_id().to_string()),
1005            value_names,
1006            usage: "".into(),
1007            required,
1008            double_dash: if arg.is_last_set() {
1009                SpecDoubleDashChoices::Required
1010            } else if arg.is_trailing_var_arg_set() {
1011                SpecDoubleDashChoices::Automatic
1012            } else {
1013                SpecDoubleDashChoices::Optional
1014            },
1015            help,
1016            help_long,
1017            help_md: None,
1018            admonitions: Vec::new(),
1019            help_first_line,
1020            var,
1021            var_max: None,
1022            var_min: None,
1023            // clap answers for this one, and the same getter `default_values` already
1024            // uses just above: a default is split by it, and so is a typed value.
1025            delimiter: recorded_delimiter,
1026            allow_negative_numbers: arg.is_allow_negative_numbers_set(),
1027            value_terminator: None,
1028            hide,
1029            hide_default_value: arg.is_hide_default_value_set(),
1030            hide_env: arg.is_hide_env_set(),
1031            hide_env_values: arg.is_hide_env_values_set(),
1032            hide_possible_values: arg.is_hide_possible_values_set(),
1033            hide_short_help: arg.is_hide_short_help_set(),
1034            hide_long_help: arg.is_hide_long_help_set(),
1035            conflicts: Vec::new(),
1036            requires: Vec::new(),
1037            required_if: Vec::new(),
1038            required_if_eq: Vec::new(),
1039            required_if_eq_all: Vec::new(),
1040            required_unless: Vec::new(),
1041            required_unless_all: Vec::new(),
1042            default: default_values(arg),
1043            choices: None,
1044            validate: None,
1045            validate_error: None,
1046            effect: None,
1047            env: None,
1048            env_fallback: Vec::new(),
1049            deprecated_env: Vec::new(),
1050            help_heading: arg.get_help_heading().map(|s| s.to_string()),
1051            surface: None,
1052            available_if: Vec::new(),
1053            display_order: Some(arg.get_display_order()),
1054        };
1055        arg.choices = choices;
1056
1057        value_bounds(source, &mut arg, true);
1058        if arg.var {
1059            arg.value_terminator = value_terminator;
1060        }
1061
1062        arg
1063    }
1064}
1065
1066impl Display for SpecArg {
1067    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1068        write!(f, "{}", self.usage())
1069    }
1070}
1071impl PartialEq for SpecArg {
1072    fn eq(&self, other: &Self) -> bool {
1073        self.name == other.name
1074    }
1075}
1076impl Eq for SpecArg {}
1077impl Hash for SpecArg {
1078    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1079        self.name.hash(state);
1080    }
1081}
1082
1083#[cfg(all(test, feature = "validation"))]
1084mod validation_tests {
1085    use std::collections::HashMap;
1086
1087    use crate::{parse, parse::Parser, Spec};
1088
1089    fn spec() -> Spec {
1090        r#"
1091name "ex"
1092bin "ex"
1093arg "<port>" validate="int(value) >= 1 && int(value) <= 65535" validate_error="must be a valid port"
1094        "#
1095        .parse()
1096        .unwrap()
1097    }
1098
1099    #[test]
1100    fn validation_round_trips_through_kdl() {
1101        let spec = spec();
1102        let kdl = spec.to_string();
1103        let reparsed: Spec = kdl.parse().unwrap();
1104        let arg = &reparsed.cmd.args[0];
1105        assert_eq!(
1106            arg.validate.as_deref(),
1107            Some("int(value) >= 1 && int(value) <= 65535")
1108        );
1109        assert_eq!(arg.validate_error.as_deref(), Some("must be a valid port"));
1110    }
1111
1112    #[test]
1113    fn invalid_validation_declarations_are_rejected_with_the_spec() {
1114        let missing_expression = r#"name "demo"
1115bin "demo"
1116arg "<port>" validate_error="must be a port"
1117"#;
1118        assert!(missing_expression.parse::<Spec>().is_err());
1119
1120        let invalid_expression = r#"name "demo"
1121bin "demo"
1122arg "<port>" validate="int(value) >"
1123"#;
1124        assert!(invalid_expression.parse::<Spec>().is_err());
1125    }
1126
1127    #[test]
1128    fn reference_parser_validates_each_raw_value() {
1129        parse(&spec(), &["ex".to_string(), "9229".to_string()]).unwrap();
1130
1131        let error = parse(&spec(), &["ex".to_string(), "0".to_string()]).unwrap_err();
1132        assert!(
1133            error.to_string().contains("must be a valid port"),
1134            "{error:?}"
1135        );
1136
1137        let variadic: Spec = r#"
1138name "ex"
1139bin "ex"
1140arg "<port>" var=#true validate="int(value) > 0" validate_error="port must be positive"
1141        "#
1142        .parse()
1143        .unwrap();
1144        let error = parse(
1145            &variadic,
1146            &["ex".to_string(), "0".to_string(), "-1".to_string()],
1147        )
1148        .unwrap_err()
1149        .to_string();
1150        assert_eq!(error.matches("port must be positive").count(), 1, "{error}");
1151    }
1152
1153    #[test]
1154    fn reference_parser_validates_environment_and_default_fallbacks() {
1155        let spec: Spec = r#"
1156name "ex"
1157bin "ex"
1158arg "[port]" env="PORT" validate="int(value) > 0" validate_error="port must be positive"
1159flag "--mode" default="bad" {
1160    arg "<mode>" validate="value == 'good'" validate_error="mode must be good"
1161}
1162arg "[ports]..." env="PORTS" var=#true var_max=1 delimiter="," validate="int(value) > 0" validate_error="all ports must be positive"
1163flag "--levels" env="LEVELS" {
1164    arg "<level>..." var=#true var_max=1 delimiter="," validate="value == 'good'" validate_error="all levels must be good"
1165}
1166flag "--modes" default="good,bad" {
1167    arg "<mode>..." var=#true var_max=1 delimiter="," validate="value == 'good'" validate_error="all modes must be good"
1168}
1169flag "--conditional" {
1170    default_if "--trigger" "good,bad"
1171    arg "<conditional>..." var=#true var_max=1 delimiter="," validate="value == 'good'" validate_error="all conditional values must be good"
1172}
1173flag "--repeats <repeat>" env="REPEATS" var=#true var_max=1 delimiter=","
1174flag "--trigger"
1175        "#
1176        .parse()
1177        .unwrap();
1178        let env = HashMap::from([
1179            ("PORT".to_string(), "0".to_string()),
1180            ("PORTS".to_string(), "1,0".to_string()),
1181            ("LEVELS".to_string(), "good,bad".to_string()),
1182            ("REPEATS".to_string(), "one,two".to_string()),
1183        ]);
1184        let error = Parser::new(&spec)
1185            .with_env(env)
1186            .parse(&["ex".to_string(), "--trigger".to_string()])
1187            .unwrap_err();
1188        let error = error.to_string();
1189        assert!(error.contains("port must be positive"), "{error}");
1190        assert!(error.contains("mode must be good"), "{error}");
1191        assert!(error.contains("all ports must be positive"), "{error}");
1192        assert!(error.contains("all levels must be good"), "{error}");
1193        assert!(error.contains("all modes must be good"), "{error}");
1194        assert!(
1195            error.contains("all conditional values must be good"),
1196            "{error}"
1197        );
1198        assert!(
1199            error.contains("Variadic argument <ports> accepts at most 1 value(s), got 2"),
1200            "{error}"
1201        );
1202        for flag in ["levels", "modes", "conditional", "repeats"] {
1203            assert!(
1204                error.contains(&format!(
1205                    "Variadic flag --{flag} accepts at most 1 value(s), got 2"
1206                )),
1207                "{error}"
1208            );
1209        }
1210    }
1211}
1212
1213#[cfg(test)]
1214mod delimiter_tests {
1215    use crate::Spec;
1216
1217    #[test]
1218    fn a_delimiter_has_to_be_one_byte() {
1219        // Splitting is by byte below the spec. A separator that is one *character* but
1220        // several bytes has no byte to be, and picking its low one would match the
1221        // continuation bytes inside unrelated characters — `§` would split `aЧb`. Refused
1222        // where it is written, which is the derive's rule too.
1223        for spec in [
1224            "flag \"--tags <tag>\" var=#true delimiter=\"§\"\n",
1225            "arg \"[tags]...\" var=#true delimiter=\"、\"\n",
1226        ] {
1227            let err = spec.parse::<Spec>().unwrap_err();
1228            assert!(format!("{err:?}").contains("one byte"), "{err:?}");
1229        }
1230
1231        // A clap command may still declare one; clap splits on it by character. The spec
1232        // cannot say so, and drops it rather than recording a separator it could not write
1233        // back out — the values still arrive, since `var` is set either way.
1234        let cmd = clap::Command::new("ex").arg(
1235            clap::Arg::new("tags")
1236                .long("tags")
1237                .value_delimiter('、')
1238                .action(clap::ArgAction::Set),
1239        );
1240        let spec = Spec::from(&cmd);
1241        let arg = spec.cmd.flags[0].arg.as_ref().unwrap();
1242        assert_eq!(
1243            arg.delimiter, None,
1244            "a separator it cannot write is not recorded"
1245        );
1246        assert!(arg.var, "clap still splits, so the values still arrive");
1247        spec.to_string()
1248            .parse::<Spec>()
1249            .expect("what the bridge produces has to parse back");
1250    }
1251
1252    #[test]
1253    fn a_delimiter_round_trips_and_comes_across_from_clap() {
1254        let spec: Spec = "flag \"--tags <tag>\" var=#true delimiter=\",\"\n"
1255            .parse()
1256            .unwrap();
1257        let arg = spec.cmd.flags[0].arg.as_ref().unwrap();
1258        assert_eq!(arg.delimiter, Some(','));
1259
1260        let reparsed: Spec = spec.to_string().parse().unwrap();
1261        let arg = reparsed.cmd.flags[0].arg.as_ref().unwrap();
1262        assert_eq!(arg.delimiter, Some(','), "{spec}");
1263
1264        // clap answers for this one, through the same getter the default splitting
1265        // already used.
1266        let cmd = clap::Command::new("ex").arg(
1267            clap::Arg::new("tags")
1268                .long("tags")
1269                .value_delimiter(',')
1270                .num_args(1..)
1271                .default_value("a,b"),
1272        );
1273        let spec = Spec::from(&cmd);
1274        let flag = &spec.cmd.flags[0];
1275        assert_eq!(flag.arg.as_ref().unwrap().delimiter, Some(','));
1276        // And the default is still recorded split, which is the same statement. On the
1277        // flag rather than on its argument, which is where the bridge puts a flag's.
1278        assert_eq!(flag.default, vec!["a", "b"]);
1279    }
1280
1281    #[test]
1282    fn a_single_valued_clap_arg_keeps_its_delimiter() {
1283        // clap's parser splits whenever a delimiter is set, whatever `num_args` says, so
1284        // `ArgAction::Set` with `value_delimiter(',')` is one word becoming several — the
1285        // common spelling. Reading it as single-valued dropped the delimiter and left a
1286        // CLI whose defaults split and whose typed values did not.
1287        let cmd = clap::Command::new("ex").arg(
1288            clap::Arg::new("tags")
1289                .long("tags")
1290                .action(clap::ArgAction::Set)
1291                .value_delimiter(','),
1292        );
1293        let spec = Spec::from(&cmd);
1294        let arg = spec.cmd.flags[0].arg.as_ref().unwrap();
1295        assert_eq!(arg.delimiter, Some(','));
1296        // And it says so: a delimiter is the statement that several values can land, so
1297        // the emitted spec has somewhere to put them and parses back.
1298        assert!(arg.var, "a delimiter brings `var` with it");
1299        let _: Spec = spec.to_string().parse().expect("{spec}");
1300    }
1301
1302    #[test]
1303    fn a_single_valued_clap_positional_splits_into_stored_values() {
1304        // The positional bridge uses `SpecArg::from(&clap::Arg)` directly, unlike a
1305        // flag. A delimiter therefore has to make that argument variadic here too or
1306        // parsing validates the split parts and then stores the original unsplit word.
1307        let cmd = clap::Command::new("ex").arg(
1308            clap::Arg::new("tags")
1309                .action(clap::ArgAction::Set)
1310                .value_delimiter(',')
1311                .value_parser(["a", "b"]),
1312        );
1313        let spec = Spec::from(&cmd);
1314        let arg = &spec.cmd.args[0];
1315        assert!(arg.var, "a positional delimiter brings `var` with it");
1316        assert_eq!(arg.delimiter, Some(','));
1317
1318        let input = ["ex", "a,b"].map(str::to_string);
1319        let parsed = crate::parse(&spec, &input).expect("both split values are choices");
1320        let value = parsed
1321            .args
1322            .values()
1323            .next()
1324            .expect("the positional was stored");
1325        assert!(matches!(
1326            value,
1327            crate::parse::ParseValue::MultiString(values)
1328                if values == &["a".to_string(), "b".to_string()]
1329        ));
1330    }
1331
1332    #[test]
1333    fn a_delimiter_needs_somewhere_to_put_what_it_splits() {
1334        // Without `var` everything after the first separator would be dropped, silently.
1335        let err = "flag \"--tags <tag>\" delimiter=\",\"\n"
1336            .parse::<Spec>()
1337            .unwrap_err();
1338        assert!(format!("{err:?}").contains("one value"), "{err:?}");
1339
1340        let err = "arg \"[tags]\" delimiter=\",\"\n"
1341            .parse::<Spec>()
1342            .unwrap_err();
1343        assert!(format!("{err:?}").contains("one value"), "{err:?}");
1344
1345        // A flag that takes no value has nothing to split at all.
1346        let err = "flag \"--quiet\" delimiter=\",\"\n"
1347            .parse::<Spec>()
1348            .unwrap_err();
1349        assert!(format!("{err:?}").contains("takes none"), "{err:?}");
1350
1351        // One character, or it is not a delimiter.
1352        let err = "flag \"--tags <tag>\" var=#true delimiter=\"::\"\n"
1353            .parse::<Spec>()
1354            .unwrap_err();
1355        assert!(format!("{err:?}").contains("one character"), "{err:?}");
1356    }
1357}
1358
1359#[cfg(test)]
1360mod possible_value_tests {
1361    use clap::builder::PossibleValue;
1362
1363    #[test]
1364    fn clap_possible_value_metadata_survives_the_bridge() {
1365        let command = clap::Command::new("ex").arg(
1366            clap::Arg::new("color").ignore_case(true).value_parser([
1367                PossibleValue::new("always")
1368                    .help("Always use color")
1369                    .alias("yes"),
1370                PossibleValue::new("never").hide(true),
1371            ]),
1372        );
1373        let spec = crate::Spec::from(&command);
1374        let choices = spec.cmd.args[0].choices.as_ref().unwrap();
1375        assert_eq!(choices.choices, ["always", "never"]);
1376        assert!(choices.ignore_case);
1377        assert!(choices.matches("YES"));
1378        assert_eq!(choices.values(), ["always"]);
1379        assert_eq!(choices.details[0].help.as_deref(), Some("Always use color"));
1380        assert!(choices.details[0].aliases[0].hide);
1381        assert!(choices.details[1].hide);
1382    }
1383}
1384
1385#[cfg(test)]
1386mod tests {
1387    use crate::{Spec, SpecArg};
1388    use insta::assert_snapshot;
1389
1390    #[test]
1391    fn test_arg_with_env() {
1392        let spec = Spec::parse(
1393            &Default::default(),
1394            r#"
1395arg "<input>" env="MY_INPUT" help="Input file"
1396arg "<output>" env="MY_OUTPUT"
1397            "#,
1398        )
1399        .unwrap();
1400
1401        assert_snapshot!(spec, @r#"
1402        arg <input> help="Input file" env=MY_INPUT
1403        arg <output> env=MY_OUTPUT
1404        "#);
1405
1406        let input_arg = spec.cmd.args.iter().find(|a| a.name == "input").unwrap();
1407        assert_eq!(input_arg.env, Some("MY_INPUT".to_string()));
1408
1409        let output_arg = spec.cmd.args.iter().find(|a| a.name == "output").unwrap();
1410        assert_eq!(output_arg.env, Some("MY_OUTPUT".to_string()));
1411    }
1412
1413    #[test]
1414    fn test_arg_with_env_child_node() {
1415        let spec = Spec::parse(
1416            &Default::default(),
1417            r#"
1418arg "<input>" help="Input file" {
1419    env "MY_INPUT"
1420}
1421arg "<output>" {
1422    env "MY_OUTPUT"
1423}
1424            "#,
1425        )
1426        .unwrap();
1427
1428        assert_snapshot!(spec, @r#"
1429        arg <input> help="Input file" env=MY_INPUT
1430        arg <output> env=MY_OUTPUT
1431        "#);
1432
1433        let input_arg = spec.cmd.args.iter().find(|a| a.name == "input").unwrap();
1434        assert_eq!(input_arg.env, Some("MY_INPUT".to_string()));
1435
1436        let output_arg = spec.cmd.args.iter().find(|a| a.name == "output").unwrap();
1437        assert_eq!(output_arg.env, Some("MY_OUTPUT".to_string()));
1438    }
1439
1440    #[test]
1441    fn test_arg_variadic_syntax() {
1442        use crate::SpecArg;
1443
1444        // Trailing ellipsis with required brackets
1445        let arg: SpecArg = "<files>...".into();
1446        assert_eq!(arg.name, "files");
1447        assert!(arg.var);
1448        assert!(arg.required);
1449
1450        // Trailing ellipsis with optional brackets
1451        let arg: SpecArg = "[files]...".into();
1452        assert_eq!(arg.name, "files");
1453        assert!(arg.var);
1454        assert!(!arg.required);
1455
1456        // Unicode ellipsis
1457        let arg: SpecArg = "<files>…".into();
1458        assert_eq!(arg.name, "files");
1459        assert!(arg.var);
1460
1461        let arg: SpecArg = "[files]…".into();
1462        assert_eq!(arg.name, "files");
1463        assert!(arg.var);
1464        assert!(!arg.required);
1465
1466        // Ellipsis inside brackets: [args...] and <args...>
1467        let arg: SpecArg = "[args...]".into();
1468        assert_eq!(arg.name, "args");
1469        assert!(arg.var);
1470        assert!(!arg.required);
1471
1472        let arg: SpecArg = "<args...>".into();
1473        assert_eq!(arg.name, "args");
1474        assert!(arg.var);
1475        assert!(arg.required);
1476
1477        // Unicode ellipsis inside brackets
1478        let arg: SpecArg = "[args…]".into();
1479        assert_eq!(arg.name, "args");
1480        assert!(arg.var);
1481        assert!(!arg.required);
1482    }
1483
1484    #[test]
1485    fn fixed_arity_placeholders_round_trip() {
1486        let spec: Spec = "arg \"<START> <END>\"\n".parse().unwrap();
1487        let arg = &spec.cmd.args[0];
1488        assert_eq!(arg.value_names, ["START", "END"]);
1489        assert_eq!((arg.var_min, arg.var_max), (Some(2), Some(2)));
1490        assert_eq!(arg.usage, "<START> <END>");
1491
1492        let reparsed: Spec = spec.to_string().parse().unwrap();
1493        assert_eq!(reparsed.cmd.args[0].value_names, ["START", "END"]);
1494    }
1495
1496    #[test]
1497    fn fixed_arity_placeholders_reject_mismatched_bounds() {
1498        let error = "arg \"<START> <END>\" var_min=1 var_max=2\n"
1499            .parse::<Spec>()
1500            .unwrap_err();
1501        assert!(
1502            format!("{error:?}").contains("require var_min=2 and var_max=2"),
1503            "{error:?}"
1504        );
1505    }
1506
1507    #[test]
1508    fn a_single_value_name_replaces_the_display_name() {
1509        let spec: Spec = "arg \"<input>\" { value_names \"INPUT\" }\n"
1510            .parse()
1511            .unwrap();
1512        let arg = &spec.cmd.args[0];
1513        assert_eq!(arg.name, "INPUT");
1514        assert_eq!(arg.usage, "<INPUT>");
1515
1516        let built = SpecArg::builder()
1517            .name("input")
1518            .required(true)
1519            .value_names(["INPUT"])
1520            .build();
1521        assert_eq!(built.name, "INPUT");
1522        assert_eq!(built.usage, "<INPUT>");
1523    }
1524
1525    #[test]
1526    fn builder_fixed_arity_survives_later_bound_setters() {
1527        let after = SpecArg::builder()
1528            .value_names(["START", "END"])
1529            .var(false)
1530            .var_min(1)
1531            .var_max(4)
1532            .build();
1533        let before = SpecArg::builder()
1534            .var(false)
1535            .var_min(1)
1536            .var_max(4)
1537            .value_names(["START", "END"])
1538            .build();
1539        for arg in [after, before] {
1540            assert!(arg.var);
1541            assert_eq!((arg.var_min, arg.var_max), (Some(2), Some(2)));
1542            assert_eq!(arg.usage, "[START] [END]");
1543        }
1544    }
1545
1546    #[test]
1547    fn one_label_with_exact_bounds_renders_each_value_slot() {
1548        let spec: Spec = "arg \"<item>…\" var_min=2 var_max=2 { value_names \"ITEM\" }\n"
1549            .parse()
1550            .unwrap();
1551        assert_eq!(spec.cmd.args[0].usage, "<ITEM> <ITEM>");
1552        let reparsed: Spec = spec.to_string().parse().unwrap();
1553        assert_eq!(reparsed.cmd.args[0].value_names, ["ITEM", "ITEM"]);
1554        assert_eq!(
1555            (reparsed.cmd.args[0].var_min, reparsed.cmd.args[0].var_max),
1556            (Some(2), Some(2))
1557        );
1558
1559        let built = SpecArg::builder()
1560            .value_names(["ITEM"])
1561            .required(true)
1562            .var(true)
1563            .var_min(2)
1564            .var_max(2)
1565            .build();
1566        assert_eq!(built.usage, "<ITEM> <ITEM>");
1567    }
1568
1569    #[test]
1570    fn fixed_arity_placeholders_reject_mixed_requiredness() {
1571        let error = "arg \"<START> [END]\"\n".parse::<Spec>().unwrap_err();
1572        assert!(
1573            format!("{error:?}")
1574                .contains("fixed-arity placeholders must be either all required or all optional"),
1575            "{error:?}"
1576        );
1577    }
1578
1579    #[test]
1580    fn test_arg_child_nodes() {
1581        let spec = Spec::parse(
1582            &Default::default(),
1583            r#"
1584arg "<environment>" {
1585    help "Deployment environment"
1586    choices "dev" "staging" "prod"
1587}
1588arg "[services]" {
1589    help "Services to deploy"
1590    var #true
1591    var_min 0
1592}
1593            "#,
1594        )
1595        .unwrap();
1596
1597        let env_arg = spec
1598            .cmd
1599            .args
1600            .iter()
1601            .find(|a| a.name == "environment")
1602            .unwrap();
1603        assert_eq!(env_arg.help, Some("Deployment environment".to_string()));
1604        assert!(env_arg.choices.is_some());
1605
1606        let svc_arg = spec.cmd.args.iter().find(|a| a.name == "services").unwrap();
1607        assert_eq!(svc_arg.help, Some("Services to deploy".to_string()));
1608        assert!(svc_arg.var);
1609        assert_eq!(svc_arg.var_min, Some(0));
1610    }
1611
1612    #[test]
1613    fn test_arg_long_help_child_node() {
1614        let spec = Spec::parse(
1615            &Default::default(),
1616            r#"
1617arg "<input>" {
1618    help "Input file"
1619    long_help "Extended help text for input"
1620}
1621            "#,
1622        )
1623        .unwrap();
1624
1625        let input_arg = spec.cmd.args.iter().find(|a| a.name == "input").unwrap();
1626        assert_eq!(input_arg.help, Some("Input file".to_string()));
1627        assert_eq!(
1628            input_arg.help_long,
1629            Some("Extended help text for input".to_string())
1630        );
1631    }
1632
1633    #[test]
1634    fn positional_conflicts_round_trip_without_dropping_members() {
1635        let spec: Spec = "arg \"[VALUE]\" { conflicts \"--from-file\" \"--stdin\" }\n"
1636            .parse()
1637            .unwrap();
1638        assert_eq!(
1639            spec.cmd.args[0].conflicts,
1640            vec!["--from-file".to_string(), "--stdin".to_string()]
1641        );
1642
1643        let rendered = spec.to_string();
1644        let reparsed: Spec = rendered.parse().unwrap();
1645        assert_eq!(reparsed.cmd.args[0].conflicts, spec.cmd.args[0].conflicts);
1646    }
1647}