Skip to main content

usage/spec/
arg.rs

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