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