Skip to main content

usage/spec/
arg.rs

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