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