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