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