Skip to main content

usage/spec/
flag.rs

1use crate::kdl::{KdlDocument, KdlEntry, KdlNode};
2use crate::miette;
3use itertools::Itertools;
4use serde::Serialize;
5use std::fmt::Display;
6use std::hash::Hash;
7use std::str::FromStr;
8
9use crate::error::UsageErr::InvalidFlag;
10use crate::error::{Result, UsageErr};
11use crate::spec::arg::SpecDoubleDashChoices;
12use crate::spec::builder::SpecFlagBuilder;
13use crate::spec::context::ParsingContext;
14use crate::spec::effect::{EFFECT_VALUES, SpecCommandEffect};
15use crate::spec::helpers::{NodeHelper, fits_selector_property, string_entry};
16use crate::spec::is_false;
17use crate::{
18    SpecAdmonition, SpecAdmonitionKind, SpecArg, SpecChoices, SpecComplete, SpecRequiredIfEq,
19    string,
20};
21
22/// A non-binding action performed when a flag is supplied.
23#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize)]
24#[serde(rename_all = "snake_case")]
25pub enum SpecFlagAction {
26    #[default]
27    Set,
28    Help,
29    HelpShort,
30    HelpLong,
31    HelpAll,
32    Version,
33}
34
35impl SpecFlagAction {
36    fn parse(value: &str) -> Option<Self> {
37        Some(match value {
38            "set" => Self::Set,
39            "help" => Self::Help,
40            "help_short" => Self::HelpShort,
41            "help_long" => Self::HelpLong,
42            "help_all" => Self::HelpAll,
43            "version" => Self::Version,
44            _ => return None,
45        })
46    }
47
48    pub fn as_str(self) -> &'static str {
49        match self {
50            Self::Set => "set",
51            Self::Help => "help",
52            Self::HelpShort => "help_short",
53            Self::HelpLong => "help_long",
54            Self::HelpAll => "help_all",
55            Self::Version => "version",
56        }
57    }
58}
59
60/// A requirement activated by one of a flag's values.
61///
62/// `flag "--config <file>" { requires_if "special.toml" "--key" }`
63/// means `--key` is required only when `--config` was explicitly given the
64/// value `special.toml`.
65#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
66pub struct SpecRequiresIf {
67    /// The declaring flag's value that activates the requirement.
68    pub value: String,
69    /// The flag selector that must then be satisfied.
70    pub requires: String,
71}
72
73/// A default that applies when another flag is given.
74///
75/// Lives on the *target* flag, the inverse of [`SpecRequiresIf`]:
76/// `flag "--bin-names" { default_if "--json" "true" }` binds `true` on
77/// `--bin-names` when `--json` was given. Two arguments are clap's
78/// `ArgPredicate::IsPresent`; three (`default_if "--output" "json" "pretty"`)
79/// are `Equals`. First match wins. Command-line and environment values on
80/// this flag suppress it; a `default_if` value is a default, not an explicit
81/// value, so it does not activate `requires_if`.
82///
83/// clap 4 has `Arg::default_value_if` as a setter with no getter, so a spec
84/// generated from a clap command never carries this — same hole as
85/// [`SpecFlag::requires`].
86#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
87pub struct SpecDefaultIf {
88    /// The other flag that decides whether this default applies (`"--json"`).
89    pub selector: String,
90    /// When set, the selector must have this explicit value (`Equals`).
91    /// When `None`, the selector only has to be present (`IsPresent`).
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub when: Option<String>,
94    /// The value to bind on this flag when the condition matches.
95    pub value: String,
96}
97
98/// A CLI flag/option specification.
99///
100/// Flags are optional arguments that start with `-` (short) or `--` (long).
101/// They can be boolean switches or accept values.
102///
103/// # Example
104///
105/// ```
106/// use usage::SpecFlag;
107///
108/// let flag = SpecFlag::builder()
109///     .short('v')
110///     .long("verbose")
111///     .help("Enable verbose output")
112///     .build();
113/// ```
114#[derive(Debug, Default, Clone, Serialize)]
115#[non_exhaustive]
116pub struct SpecFlag {
117    /// Internal name for the flag (derived from long/short if not set)
118    pub name: String,
119    /// Generated usage string (e.g., "-v, --verbose")
120    pub usage: String,
121    /// Short help text shown in command listings
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub help: Option<String>,
124    /// Extended help text shown with --help
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub help_long: Option<String>,
127    /// Markdown-formatted help text
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub help_md: Option<String>,
130    /// Structured notes and warnings, in presentation order.
131    #[serde(skip_serializing_if = "Vec::is_empty")]
132    pub admonitions: Vec<SpecAdmonition>,
133    /// First line of help text (auto-generated)
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub help_first_line: Option<String>,
136    /// Short flag characters (e.g., 'v' for -v)
137    pub short: Vec<char>,
138    /// Short aliases accepted by parsing but omitted from help and completion.
139    #[serde(skip_serializing_if = "Vec::is_empty")]
140    pub hidden_short_aliases: Vec<char>,
141    /// Long flag names (e.g., "verbose" for --verbose)
142    pub long: Vec<String>,
143    /// Long aliases accepted by parsing but omitted from help and completion.
144    #[serde(skip_serializing_if = "Vec::is_empty")]
145    pub hidden_aliases: Vec<String>,
146    /// Whether this flag must be provided
147    #[serde(skip_serializing_if = "is_false")]
148    pub required: bool,
149    /// Flags whose presence makes this flag required
150    #[serde(skip_serializing_if = "Vec::is_empty")]
151    pub required_if: Vec<String>,
152    /// Value conditions, any one of which makes this flag required.
153    #[serde(skip_serializing_if = "Vec::is_empty")]
154    pub required_if_eq: Vec<SpecRequiredIfEq>,
155    /// Value conditions which must all match to make this flag required.
156    #[serde(skip_serializing_if = "Vec::is_empty")]
157    pub required_if_eq_all: Vec<SpecRequiredIfEq>,
158    /// Flags whose absence makes this flag required
159    #[serde(skip_serializing_if = "Vec::is_empty")]
160    pub required_unless: Vec<String>,
161    /// Only the presence of every selector waives this flag's requirement.
162    #[serde(skip_serializing_if = "Vec::is_empty")]
163    pub required_unless_all: Vec<String>,
164    /// Deprecation message if this flag is deprecated
165    #[serde(skip_serializing_if = "Option::is_none")]
166    pub deprecated: Option<String>,
167    /// Version at which consumers should begin warning about this flag.
168    #[serde(skip_serializing_if = "Option::is_none")]
169    pub deprecated_warn_at: Option<String>,
170    /// Version at which consumers expect this flag to be removed.
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub deprecated_remove_at: Option<String>,
173    /// Whether this flag can be specified multiple times
174    #[serde(skip_serializing_if = "is_false")]
175    pub var: bool,
176    /// Minimum number of times this flag must appear (for var flags)
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub var_min: Option<usize>,
179    /// Maximum number of times this flag can appear (for var flags)
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub var_max: Option<usize>,
182    /// Whether to hide this flag from help output
183    pub hide: bool,
184    /// Hide the default annotation while keeping the default behavior.
185    #[serde(skip_serializing_if = "is_false")]
186    pub hide_default_value: bool,
187    /// Hide the environment annotation entirely.
188    #[serde(skip_serializing_if = "is_false")]
189    pub hide_env: bool,
190    /// Hide an environment value while retaining its variable name.
191    #[serde(skip_serializing_if = "is_false")]
192    pub hide_env_values: bool,
193    /// Hide possible values from help without changing validation.
194    #[serde(skip_serializing_if = "is_false")]
195    pub hide_possible_values: bool,
196    /// Hide this flag only from short help.
197    #[serde(skip_serializing_if = "is_false")]
198    pub hide_short_help: bool,
199    /// Hide this flag only from long help.
200    #[serde(skip_serializing_if = "is_false")]
201    pub hide_long_help: bool,
202    /// Whether this flag is available to all subcommands
203    pub global: bool,
204    /// Whether this is a count flag (e.g., -vvv counts as 3)
205    #[serde(skip_serializing_if = "is_false")]
206    pub count: bool,
207    /// Argument specification if this flag takes a value
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub arg: Option<SpecArg>,
210    /// Default value(s) if the flag is not provided
211    #[serde(skip_serializing_if = "Vec::is_empty")]
212    pub default: Vec<String>,
213    /// Negation prefix (e.g., "no-" for --no-verbose)
214    #[serde(skip_serializing_if = "Option::is_none")]
215    pub negate: Option<String>,
216    /// Flags that this flag mutually overrides; the last one provided wins
217    #[serde(skip_serializing_if = "Vec::is_empty")]
218    pub overrides: Vec<String>,
219    /// Flags that cannot be given alongside this one.
220    ///
221    /// Distinct from [`SpecFlag::overrides`], which is about the *last* one winning:
222    /// conflicting flags are a mistake to report, not an order to resolve. clap has
223    /// had `conflicts_with` for years and mise uses it forty times, so a spec
224    /// generated from a clap command was losing it.
225    #[serde(skip_serializing_if = "Vec::is_empty")]
226    pub conflicts: Vec<String>,
227    /// Flags that must also be given when this one is.
228    ///
229    /// The positive form of [`SpecFlag::conflicts`], and not the same statement as
230    /// [`SpecFlag::required_if`] read backwards: `required_if` lives on the flag that
231    /// becomes required, so declaring `--out` needs `--format` means editing `--format`,
232    /// away from the flag the rule is about. `requires` lives on the flag that imposes
233    /// the rule, which is where clap puts it and where a reader looks for it.
234    ///
235    /// Nothing generated from a clap command can carry this: clap 4.6 has `Arg::requires`
236    /// and its variants as setters with no getter, so a `Command` cannot be asked what it
237    /// requires. A CLI that declares it here gains a constraint its generated spec never
238    /// had.
239    #[serde(skip_serializing_if = "Vec::is_empty")]
240    pub requires: Vec<String>,
241    /// Flags required when this flag is explicitly given a particular value.
242    ///
243    /// Defaults do not activate the condition; command-line and environment
244    /// values do. This matches clap's `requires_if`/`requires_ifs` semantics.
245    #[serde(skip_serializing_if = "Vec::is_empty")]
246    pub requires_if: Vec<SpecRequiresIf>,
247    /// Defaults that apply when another flag is given.
248    ///
249    /// First match wins. Only considered when this flag was not on the command
250    /// line and has no environment value. An applied `default_if` is a default,
251    /// not an explicit value: it satisfies `requires` and does not activate
252    /// `requires_if`.
253    #[serde(skip_serializing_if = "Vec::is_empty")]
254    pub default_if: Vec<SpecDefaultIf>,
255    /// Whether this flag must be given on its own.
256    ///
257    /// The whole-command form of [`SpecFlag::conflicts`]: `--version` and `--help` are
258    /// the shape — asking for one means the rest of the command line has nothing to act
259    /// on. Everything the command declares counts, positionals included, which is what
260    /// makes this different from being in a group with every other flag.
261    #[serde(skip_serializing_if = "is_false")]
262    pub exclusive: bool,
263    /// Whether the value must be attached with `=`: `--flag=value` is accepted
264    /// and `--flag value` is not. clap's `require_equals`. Aube's `--inspect`
265    /// is the fleet case.
266    #[serde(skip_serializing_if = "is_false")]
267    pub require_equals: bool,
268    /// Whether a value-taking flag may be present without a value.
269    ///
270    /// This is executable parser policy, distinct from the nested argument's
271    /// `required` bit, which controls whether help renders `<VALUE>` or `[VALUE]`.
272    #[serde(skip_serializing_if = "is_false")]
273    pub value_optional: bool,
274    /// Whether a boolean switch accepts an explicit attached value.
275    ///
276    /// Only `--flag=true` and `--flag=false` are values; a detached word remains
277    /// a positional and the flag still renders without a value placeholder.
278    #[serde(skip_serializing_if = "is_false")]
279    pub bool_value: bool,
280    /// Value used when the flag is present but no value is given.
281    ///
282    /// clap's `default_missing_value`: `--color` binds this string, `--color=never`
283    /// binds `never`, and an absent flag stays absent (or takes [`Self::default`]).
284    /// Combined with [`Self::require_equals`], a following word is still refused
285    /// (`--inspect 9229`) while a bare `--inspect` binds this.
286    ///
287    /// clap 4 exposes this as a setter with no getter, so a spec generated from a
288    /// clap command never carries it — same hole as [`Self::requires`].
289    #[serde(skip_serializing_if = "Option::is_none")]
290    pub default_missing: Option<String>,
291    /// Raises the effect of the command when this flag is supplied.
292    /// See [`crate::spec::effect::SpecCommandEffect`]; never lowers it.
293    #[serde(skip_serializing_if = "Option::is_none")]
294    pub effect: Option<SpecCommandEffect>,
295    /// Environment variable that can set this flag's value
296    #[serde(skip_serializing_if = "Option::is_none")]
297    pub env: Option<String>,
298    /// Ordered environment variables consulted after [`Self::env`].
299    #[serde(skip_serializing_if = "Vec::is_empty")]
300    pub env_fallback: Vec<String>,
301    /// Ordered compatibility aliases consulted last and advertised as deprecated.
302    #[serde(skip_serializing_if = "Vec::is_empty")]
303    pub deprecated_env: Vec<String>,
304    /// Heading this flag is listed under in help output.
305    ///
306    /// Purely presentational: it groups a long flag list into sections rather
307    /// than changing how anything parses. A CLI with dozens of flags — mise
308    /// groups its `watch` passthrough arguments this way — is unreadable without
309    /// it.
310    #[serde(skip_serializing_if = "Option::is_none")]
311    pub help_heading: Option<String>,
312    /// Named audience or contract surface this flag belongs to. Metadata only.
313    #[serde(skip_serializing_if = "Option::is_none")]
314    pub surface: Option<String>,
315    /// Descriptive conditions under which this flag is available.
316    #[serde(skip_serializing_if = "Vec::is_empty")]
317    pub available_if: Vec<String>,
318    /// Explicit placement within its help section.
319    #[serde(skip_serializing_if = "Option::is_none")]
320    pub display_order: Option<usize>,
321    /// Whether this flag binds a value or requests help/version output.
322    #[serde(skip_serializing_if = "is_set_action")]
323    pub action: SpecFlagAction,
324    /// Whether this is a parser-supplied entry materialized by a generated spec.
325    #[serde(skip_serializing_if = "is_false")]
326    pub builtin: bool,
327}
328
329fn is_set_action(action: &SpecFlagAction) -> bool {
330    *action == SpecFlagAction::Set
331}
332
333impl SpecFlag {
334    /// Create a new builder for SpecFlag
335    pub fn builder() -> SpecFlagBuilder {
336        SpecFlagBuilder::new()
337    }
338
339    /// Environment sources in precedence order: canonical, fallbacks, deprecated aliases.
340    pub fn env_names(&self) -> impl Iterator<Item = &str> {
341        self.env
342            .iter()
343            .map(String::as_str)
344            .chain(self.env_fallback.iter().map(String::as_str))
345            .chain(self.deprecated_env.iter().map(String::as_str))
346    }
347
348    pub(crate) fn parse(ctx: &ParsingContext, node: &NodeHelper) -> Result<Self> {
349        let mut flag: Self = node.arg(0)?.ensure_string()?.parse()?;
350        let mut allow_hyphen_values = false;
351        let mut allow_negative_numbers = false;
352        let mut value_terminator: Option<String> = None;
353        let mut delimiter: Option<String> = None;
354        node.ensure_props_unrepeated(&[
355            "conflicts",
356            "requires",
357            "overrides",
358            "required_if",
359            "required_unless",
360            "required_unless_all",
361        ])?;
362        for (k, v) in node.props() {
363            match k {
364                "help" => flag.help = Some(v.ensure_string()?),
365                "long_help" => flag.help_long = Some(v.ensure_string()?),
366                "help_long" => flag.help_long = Some(v.ensure_string()?),
367                "help_md" => flag.help_md = Some(v.ensure_string()?),
368                "required" => flag.required = v.ensure_bool()?,
369                "required_if" => v.set_selector("required_if", &mut flag.required_if)?,
370                "required_unless" => {
371                    v.set_selector("required_unless", &mut flag.required_unless)?
372                }
373                "required_unless_all" => {
374                    v.set_selector("required_unless_all", &mut flag.required_unless_all)?
375                }
376                "var" => flag.var = v.ensure_bool()?,
377                "var_min" => flag.var_min = v.ensure_usize().map(Some)?,
378                "var_max" => flag.var_max = v.ensure_usize().map(Some)?,
379                "hide" => flag.hide = v.ensure_bool()?,
380                "hide_default_value" => flag.hide_default_value = v.ensure_bool()?,
381                "hide_env" => flag.hide_env = v.ensure_bool()?,
382                "hide_env_values" => flag.hide_env_values = v.ensure_bool()?,
383                "hide_possible_values" => flag.hide_possible_values = v.ensure_bool()?,
384                "hide_short_help" => flag.hide_short_help = v.ensure_bool()?,
385                "hide_long_help" => flag.hide_long_help = v.ensure_bool()?,
386                "deprecated" => {
387                    flag.deprecated = match v.value.as_bool() {
388                        Some(true) => Some("deprecated".into()),
389                        Some(false) => None,
390                        None => Some(v.ensure_string()?),
391                    }
392                }
393                "deprecated_warn_at" => flag.deprecated_warn_at = Some(v.ensure_string()?),
394                "deprecated_remove_at" => flag.deprecated_remove_at = Some(v.ensure_string()?),
395                "global" => flag.global = v.ensure_bool()?,
396                "count" => flag.count = v.ensure_bool()?,
397                "action" => {
398                    let raw = v.ensure_string()?;
399                    let Some(action) = SpecFlagAction::parse(&raw) else {
400                        bail_parse!(ctx, v.entry.span(), "unsupported flag action {raw}");
401                    };
402                    flag.action = action;
403                }
404                "builtin" => flag.builtin = v.ensure_bool()?,
405                "allow_hyphen_values" => allow_hyphen_values = v.ensure_bool()?,
406                "allow_negative_numbers" => allow_negative_numbers = v.ensure_bool()?,
407                "value_terminator" => value_terminator = Some(v.ensure_string()?),
408                "default" => {
409                    // Support both string and boolean defaults
410                    let default_value = match v.value.as_bool() {
411                        Some(b) => b.to_string(),
412                        None => v.ensure_string()?,
413                    };
414                    flag.default = vec![default_value];
415                }
416                "negate" => flag.negate = v.ensure_string().map(Some)?,
417                "overrides" => v.set_selector("overrides", &mut flag.overrides)?,
418                "conflicts" => v.set_selector("conflicts", &mut flag.conflicts)?,
419                "requires" => v.set_selector("requires", &mut flag.requires)?,
420                "exclusive" => flag.exclusive = v.ensure_bool()?,
421                "require_equals" => flag.require_equals = v.ensure_bool()?,
422                "value_optional" => flag.value_optional = v.ensure_bool()?,
423                "bool_value" => flag.bool_value = v.ensure_bool()?,
424                "default_missing" => flag.default_missing = Some(v.ensure_string()?),
425                // Written on the flag and kept on its argument, as `allow_hyphen_values`
426                // is: the value is what gets split, and `flag "--tags <tag>"` is where a
427                // reader writes something about that value.
428                "delimiter" => delimiter = Some(v.ensure_string()?),
429                "effect" => {
430                    let raw = v.ensure_string()?;
431                    match raw.parse() {
432                        Ok(effect) => flag.effect = Some(effect),
433                        Err(_) => bail_parse!(
434                            ctx,
435                            v.entry.span(),
436                            "unsupported effect {raw}, expected one of: {EFFECT_VALUES}"
437                        ),
438                    }
439                }
440                "env" => flag.env = v.ensure_string().map(Some)?,
441                "env_fallback" => flag.env_fallback = vec![v.ensure_string()?],
442                "deprecated_env" => flag.deprecated_env = vec![v.ensure_string()?],
443                "help_heading" => flag.help_heading = v.ensure_string().map(Some)?,
444                "surface" => flag.surface = v.ensure_string().map(Some)?,
445                "available_if" => flag.available_if = vec![v.ensure_string()?],
446                "display_order" => flag.display_order = v.ensure_usize().map(Some)?,
447                k => bail_parse!(ctx, v.entry.span(), "unsupported flag key {k}"),
448            }
449        }
450        if !flag.default.is_empty() {
451            flag.required = false;
452        }
453        for child in node.children() {
454            match child.name() {
455                "arg" => {
456                    let mut arg = SpecArg::parse(ctx, &child)?;
457                    // A `complete` written on the flag before this `arg` belongs to the value
458                    // this `arg` now describes, so it moves across rather than being dropped.
459                    if let Some(mut complete) = flag.arg.take().and_then(|prev| prev.complete) {
460                        if arg.complete.is_some() {
461                            bail_parse!(
462                                ctx,
463                                child.node.name().span(),
464                                "a flag's value may have only one complete"
465                            );
466                        }
467                        complete.name = arg.name.to_lowercase();
468                        arg.complete = Some(complete);
469                    }
470                    flag.arg = Some(arg);
471                }
472                "help" => flag.help = Some(child.arg(0)?.ensure_string()?),
473                "long_help" => flag.help_long = Some(child.arg(0)?.ensure_string()?),
474                "help_long" => flag.help_long = Some(child.arg(0)?.ensure_string()?),
475                "help_md" => flag.help_md = Some(child.arg(0)?.ensure_string()?),
476                "note" => flag
477                    .admonitions
478                    .push(SpecAdmonition::note(child.arg(0)?.ensure_string()?)),
479                "warning" => flag
480                    .admonitions
481                    .push(SpecAdmonition::warning(child.arg(0)?.ensure_string()?)),
482                "required" => flag.required = child.arg(0)?.ensure_bool()?,
483                "required_if" => {
484                    flag.required_if = child
485                        .ensure_arg_len(1..)?
486                        .args()
487                        .map(|arg| arg.ensure_string())
488                        .collect::<Result<Vec<_>>>()?;
489                }
490                "required_if_eq" => {
491                    child.ensure_arg_len(2..=2)?;
492                    flag.required_if_eq.push(SpecRequiredIfEq {
493                        selector: child.arg(0)?.ensure_string()?,
494                        value: child.arg(1)?.ensure_string()?,
495                    });
496                }
497                "required_if_eq_all" => {
498                    let entries = child.args().collect::<Vec<_>>();
499                    if entries.len() < 2 || entries.len() % 2 != 0 {
500                        bail_parse!(
501                            ctx,
502                            child.node.name().span(),
503                            "required_if_eq_all needs selector/value pairs"
504                        );
505                    }
506                    flag.required_if_eq_all = entries
507                        .as_chunks::<2>()
508                        .0
509                        .iter()
510                        .map(|pair| {
511                            Ok(SpecRequiredIfEq {
512                                selector: pair[0].ensure_string()?,
513                                value: pair[1].ensure_string()?,
514                            })
515                        })
516                        .collect::<Result<Vec<_>>>()?;
517                }
518                "required_unless" => {
519                    flag.required_unless = child
520                        .ensure_arg_len(1..)?
521                        .args()
522                        .map(|arg| arg.ensure_string())
523                        .collect::<Result<Vec<_>>>()?;
524                }
525                "required_unless_all" => {
526                    flag.required_unless_all = child
527                        .ensure_arg_len(1..)?
528                        .args()
529                        .map(|arg| arg.ensure_string())
530                        .collect::<Result<Vec<_>>>()?;
531                }
532                "var" => flag.var = child.arg(0)?.ensure_bool()?,
533                "var_min" => flag.var_min = child.arg(0)?.ensure_usize().map(Some)?,
534                "var_max" => flag.var_max = child.arg(0)?.ensure_usize().map(Some)?,
535                "hide" => flag.hide = child.arg(0)?.ensure_bool()?,
536                "hide_default_value" => flag.hide_default_value = child.arg(0)?.ensure_bool()?,
537                "hide_env" => flag.hide_env = child.arg(0)?.ensure_bool()?,
538                "hide_env_values" => flag.hide_env_values = child.arg(0)?.ensure_bool()?,
539                "hide_possible_values" => {
540                    flag.hide_possible_values = child.arg(0)?.ensure_bool()?
541                }
542                "hide_short_help" => flag.hide_short_help = child.arg(0)?.ensure_bool()?,
543                "hide_long_help" => flag.hide_long_help = child.arg(0)?.ensure_bool()?,
544                "deprecated" => {
545                    flag.deprecated = match child.arg(0)?.ensure_bool() {
546                        Ok(true) => Some("deprecated".into()),
547                        Ok(false) => None,
548                        _ => Some(child.arg(0)?.ensure_string()?),
549                    }
550                }
551                "deprecated_warn_at" => {
552                    flag.deprecated_warn_at = Some(child.arg(0)?.ensure_string()?)
553                }
554                "deprecated_remove_at" => {
555                    flag.deprecated_remove_at = Some(child.arg(0)?.ensure_string()?)
556                }
557                "global" => flag.global = child.arg(0)?.ensure_bool()?,
558                "count" => flag.count = child.arg(0)?.ensure_bool()?,
559                "action" => {
560                    let arg = child.arg(0)?;
561                    let raw = arg.ensure_string()?;
562                    let Some(action) = SpecFlagAction::parse(&raw) else {
563                        bail_parse!(ctx, arg.entry.span(), "unsupported flag action {raw}");
564                    };
565                    flag.action = action;
566                }
567                "builtin" => flag.builtin = child.arg(0)?.ensure_bool()?,
568                "allow_hyphen_values" => {
569                    allow_hyphen_values = child.arg(0)?.ensure_bool()?;
570                }
571                "allow_negative_numbers" => {
572                    allow_negative_numbers = child.arg(0)?.ensure_bool()?;
573                }
574                "value_terminator" => {
575                    value_terminator = Some(child.arg(0)?.ensure_string()?);
576                }
577                "default" => {
578                    // Support both single value and multiple values
579                    // default "bar"            -> vec!["bar"]
580                    // default #true            -> vec!["true"]
581                    // default { "xyz"; "bar" } -> vec!["xyz", "bar"]
582                    let children = child.children();
583                    if children.is_empty() {
584                        // Single value: default "bar" or default #true
585                        let arg = child.arg(0)?;
586                        let default_value = match arg.value.as_bool() {
587                            Some(b) => b.to_string(),
588                            None => arg.ensure_string()?,
589                        };
590                        flag.default = vec![default_value];
591                    } else {
592                        // Multiple values from children: default { "xyz"; "bar" }
593                        // In KDL, these are child nodes where the string is the node name
594                        flag.default = children.iter().map(|c| c.name().to_string()).collect();
595                    }
596                }
597                "effect" => {
598                    let arg = child.arg(0)?;
599                    let raw = arg.ensure_string()?;
600                    match raw.parse() {
601                        Ok(effect) => flag.effect = Some(effect),
602                        Err(_) => bail_parse!(
603                            ctx,
604                            arg.entry.span(),
605                            "unsupported effect {raw}, expected one of: {EFFECT_VALUES}"
606                        ),
607                    }
608                }
609                "env" => flag.env = child.arg(0)?.ensure_string().map(Some)?,
610                "env_fallback" => {
611                    flag.env_fallback = child
612                        .ensure_arg_len(1..)?
613                        .args()
614                        .map(|entry| entry.ensure_string())
615                        .collect::<Result<_>>()?;
616                }
617                "deprecated_env" => {
618                    flag.deprecated_env = child
619                        .ensure_arg_len(1..)?
620                        .args()
621                        .map(|entry| entry.ensure_string())
622                        .collect::<Result<_>>()?;
623                }
624                "help_heading" => {
625                    flag.help_heading = child.arg(0)?.ensure_string().map(Some)?;
626                }
627                "surface" => flag.surface = child.arg(0)?.ensure_string().map(Some)?,
628                "available_if" => {
629                    flag.available_if = child
630                        .ensure_arg_len(1..)?
631                        .args()
632                        .map(|entry| entry.ensure_string())
633                        .collect::<Result<_>>()?;
634                }
635                "display_order" => {
636                    flag.display_order = child.arg(0)?.ensure_usize().map(Some)?;
637                }
638                "alias" => {
639                    let hide = child
640                        .get("hide")
641                        .map(|entry| entry.ensure_bool())
642                        .unwrap_or(Ok(false))?;
643                    for entry in child.ensure_arg_len(1..)?.args() {
644                        let spelling = entry.ensure_string()?;
645                        if let Some(long) = spelling.strip_prefix("--") {
646                            if !flag.long.iter().any(|existing| existing == long) {
647                                flag.long.push(long.to_string());
648                            }
649                            if hide && !flag.hidden_aliases.iter().any(|existing| existing == long)
650                            {
651                                flag.hidden_aliases.push(long.to_string());
652                            }
653                        } else if let Some(short) = spelling.strip_prefix('-') {
654                            let mut chars = short.chars();
655                            let Some(short) = chars.next().filter(|_| chars.next().is_none())
656                            else {
657                                bail_parse!(
658                                    ctx,
659                                    entry.entry.span(),
660                                    "a short flag alias must be exactly one character"
661                                );
662                            };
663                            if !flag.short.contains(&short) {
664                                flag.short.push(short);
665                            }
666                            if hide && !flag.hidden_short_aliases.contains(&short) {
667                                flag.hidden_short_aliases.push(short);
668                            }
669                        } else {
670                            bail_parse!(
671                                ctx,
672                                entry.entry.span(),
673                                "flag aliases must begin with - or --"
674                            );
675                        }
676                    }
677                }
678                "conflicts" => {
679                    flag.conflicts = child
680                        .ensure_arg_len(1..)?
681                        .args()
682                        .map(|arg| arg.ensure_string())
683                        .collect::<Result<Vec<_>>>()?;
684                }
685                "overrides" => {
686                    flag.overrides = child
687                        .ensure_arg_len(1..)?
688                        .args()
689                        .map(|arg| arg.ensure_string())
690                        .collect::<Result<Vec<_>>>()?;
691                }
692                "exclusive" => flag.exclusive = child.arg(0)?.ensure_bool()?,
693                "require_equals" => flag.require_equals = child.arg(0)?.ensure_bool()?,
694                "value_optional" => flag.value_optional = child.arg(0)?.ensure_bool()?,
695                "bool_value" => flag.bool_value = child.arg(0)?.ensure_bool()?,
696                "default_missing" => {
697                    flag.default_missing = Some(child.arg(0)?.ensure_string()?);
698                }
699                "requires" => {
700                    flag.requires = child
701                        .ensure_arg_len(1..)?
702                        .args()
703                        .map(|arg| arg.ensure_string())
704                        .collect::<Result<Vec<_>>>()?;
705                }
706                "requires_if" => {
707                    child.ensure_arg_len(2..=2)?;
708                    flag.requires_if.push(SpecRequiresIf {
709                        value: child.arg(0)?.ensure_string()?,
710                        requires: child.arg(1)?.ensure_string()?,
711                    });
712                }
713                "default_if" => {
714                    child.ensure_arg_len(2..=3)?;
715                    let count = child.args().count();
716                    flag.default_if.push(if count == 2 {
717                        SpecDefaultIf {
718                            selector: child.arg(0)?.ensure_string()?,
719                            when: None,
720                            value: child.arg(1)?.ensure_string()?,
721                        }
722                    } else {
723                        SpecDefaultIf {
724                            selector: child.arg(0)?.ensure_string()?,
725                            when: Some(child.arg(1)?.ensure_string()?),
726                            value: child.arg(2)?.ensure_string()?,
727                        }
728                    });
729                }
730                "choices" => {
731                    if let Some(arg) = &mut flag.arg {
732                        arg.choices = Some(SpecChoices::parse(ctx, &child)?);
733                    } else {
734                        bail_parse!(
735                            ctx,
736                            child.node.name().span(),
737                            "flag must have value to have choices"
738                        )
739                    }
740                }
741                // Shorthand for a `complete` inside the flag's `arg`, the same way `choices`
742                // is: a flag has one value, so there is no doubt which one it completes.
743                "complete" => {
744                    let Some(arg) = &mut flag.arg else {
745                        bail_parse!(
746                            ctx,
747                            child.node.name().span(),
748                            "flag must have value to have a complete"
749                        )
750                    };
751                    if arg.complete.is_some() {
752                        bail_parse!(
753                            ctx,
754                            child.node.name().span(),
755                            "a flag's value may have only one complete"
756                        );
757                    }
758                    let mut complete = SpecComplete::parse_inline(ctx, &child)?;
759                    complete.name = arg.name.to_lowercase();
760                    arg.complete = Some(complete);
761                }
762                k => bail_parse!(ctx, child.node.name().span(), "unsupported flag child {k}"),
763            }
764        }
765        if allow_hyphen_values {
766            flag.set_allow_hyphen_values(ctx, node.node.name().span(), true)?;
767        }
768        if allow_negative_numbers {
769            let Some(arg) = flag.arg.as_mut() else {
770                bail_parse!(
771                    ctx,
772                    node.node.name().span(),
773                    "flag must have value to allow negative numbers"
774                );
775            };
776            arg.allow_negative_numbers = true;
777        }
778        if let Some(terminator) = value_terminator {
779            let Some(arg) = flag.arg.as_mut() else {
780                bail_parse!(
781                    ctx,
782                    node.node.name().span(),
783                    "flag must have a variadic value to have a value terminator"
784                );
785            };
786            if !arg.var {
787                bail_parse!(
788                    ctx,
789                    node.node.name().span(),
790                    "value_terminator requires a variadic flag value"
791                );
792            }
793            if terminator.is_empty() {
794                bail_parse!(
795                    ctx,
796                    node.node.name().span(),
797                    "value_terminator cannot be empty"
798                );
799            }
800            arg.value_terminator = Some(terminator);
801        }
802        if flag.require_equals && flag.arg.is_none() {
803            bail_parse!(
804                ctx,
805                node.node.name().span(),
806                "flag must have value to require equals"
807            );
808        }
809        if flag.value_optional && flag.arg.is_none() {
810            bail_parse!(
811                ctx,
812                node.node.name().span(),
813                "flag must have a value to make that value optional"
814            );
815        }
816        if flag.bool_value
817            && (flag.arg.is_some() || flag.count || flag.action != SpecFlagAction::Set)
818        {
819            bail_parse!(
820                ctx,
821                node.node.name().span(),
822                "bool_value is only valid on a boolean switch"
823            );
824        }
825        if flag.default_missing.is_some() && flag.arg.is_none() {
826            bail_parse!(
827                ctx,
828                node.node.name().span(),
829                "flag must have value to have a default when missing"
830            );
831        }
832        // `--color` is a complete invocation, so help shows the value as optional.
833        // The same folding a nested `default` already does for `required`.
834        if flag.default_missing.is_some()
835            && let Some(arg) = flag.arg.as_mut()
836        {
837            arg.required = false;
838        }
839        if let Some(raw) = delimiter {
840            let mut chars = raw.chars();
841            let Some(delimiter) = chars.next().filter(|_| chars.next().is_none()) else {
842                bail_parse!(
843                    ctx,
844                    node.node.name().span(),
845                    "a delimiter is one character, and {raw:?} is not"
846                );
847            };
848            // And one *byte*, for the reason given where an argument reads the same
849            // property: splitting is by byte below this, and a non-ASCII separator would
850            // match the continuation bytes inside unrelated characters.
851            if !delimiter.is_ascii() {
852                bail_parse!(
853                    ctx,
854                    node.node.name().span(),
855                    "a delimiter is one byte, and {delimiter:?} is more than one; use an \
856                     ASCII separator"
857                );
858            }
859            let Some(arg) = flag.arg.as_mut() else {
860                bail_parse!(
861                    ctx,
862                    node.node.name().span(),
863                    "`delimiter` splits a value, and flag --{} takes none",
864                    flag.name
865                );
866            };
867            arg.delimiter = Some(delimiter);
868        }
869        // A delimiter with nowhere to put the extra values would drop everything after
870        // the first separator, silently. Refused where it is written instead — and `var`
871        // on either the flag or its argument is somewhere for them to go, since both are
872        // ways of saying the flag holds a list.
873        if flag.arg.as_ref().is_some_and(|a| a.delimiter.is_some()) && !flag.var {
874            let takes_several = flag.arg.as_ref().is_some_and(|a| a.var);
875            if !takes_several {
876                bail_parse!(
877                    ctx,
878                    node.node.name().span(),
879                    "flag --{} has a delimiter and holds one value; add `var=#true` for \
880                     the values it splits into",
881                    flag.name
882                );
883            }
884        }
885        if flag.action != SpecFlagAction::Set && flag.arg.is_some() {
886            bail_parse!(
887                ctx,
888                node.node.name().span(),
889                "a help or version action does not take a value"
890            );
891        }
892        flag.usage = flag.usage();
893        flag.help_first_line = flag.help.as_ref().map(|s| string::first_line(s));
894        Ok(flag)
895    }
896    pub fn allow_hyphen_values(&self) -> bool {
897        self.arg
898            .as_ref()
899            .is_some_and(|arg| arg.double_dash == SpecDoubleDashChoices::Automatic)
900    }
901
902    pub(crate) fn set_allow_hyphen_values(
903        &mut self,
904        ctx: &ParsingContext,
905        span: miette::SourceSpan,
906        allow: bool,
907    ) -> Result<()> {
908        if let Some(arg) = &mut self.arg {
909            arg.double_dash = if allow {
910                SpecDoubleDashChoices::Automatic
911            } else if arg.double_dash == SpecDoubleDashChoices::Automatic {
912                SpecDoubleDashChoices::Optional
913            } else {
914                arg.double_dash.clone()
915            };
916            Ok(())
917        } else if allow {
918            bail_parse!(ctx, span, "flag must have value to allow hyphen values")
919        } else {
920            Ok(())
921        }
922    }
923
924    pub fn usage(&self) -> String {
925        let mut parts = vec![];
926        let name = get_name_from_short_and_long(&self.short, &self.long).unwrap_or_default();
927        // A flag whose only spelling is its negation — clap's `SetFalse`, tak's
928        // `--no-credit` — is named after that spelling, so the `name:` prefix would repeat
929        // it and the spelling a reader has to type would appear nowhere.
930        let negation_only = self.short.is_empty()
931            && self.long.is_empty()
932            && self
933                .negate
934                .as_deref()
935                .is_some_and(|negate| negate.trim_start_matches('-') == self.name);
936        if negation_only {
937            parts.push(self.negate.clone().unwrap_or_default());
938        } else if name != self.name {
939            parts.push(format!("{}:", self.name));
940        }
941        if let Some(short) = self.short.first() {
942            parts.push(format!("-{short}"));
943        }
944        if let Some(long) = self.long.first() {
945            parts.push(format!("--{long}"));
946        }
947        let mut out = parts.join(" ");
948        if let Some(arg) = &self.arg {
949            let usage = arg.usage();
950            if self.require_equals && (self.value_optional || !arg.required) {
951                out = format!("{out}{}", optional_equals_usage(&usage));
952            } else {
953                let separator = if self.require_equals { "=" } else { " " };
954                out = format!("{out}{separator}{usage}");
955            }
956        }
957        out
958    }
959}
960
961pub(crate) fn optional_equals_usage(usage: &str) -> String {
962    let (value, closing) = if let Some(value) = usage.strip_prefix('[') {
963        (value, ']')
964    } else if let Some(value) = usage.strip_prefix('<') {
965        (value, '>')
966    } else {
967        return format!("={usage}");
968    };
969    let Some(end) = value.find(closing) else {
970        return format!("={usage}");
971    };
972    format!("[={}]{}", &value[..end], &value[end + 1..])
973}
974
975impl From<&SpecFlag> for KdlNode {
976    fn from(flag: &SpecFlag) -> KdlNode {
977        let mut node = KdlNode::new("flag");
978        let visible_shorts = flag
979            .short
980            .iter()
981            .filter(|short| !flag.hidden_short_aliases.contains(short));
982        let visible_longs = flag
983            .long
984            .iter()
985            .filter(|long| !flag.hidden_aliases.contains(long));
986        let inferred_matches = visible_longs
987            .clone()
988            .next()
989            .is_some_and(|long| long == &flag.name)
990            || (visible_longs.clone().next().is_none()
991                && visible_shorts
992                    .clone()
993                    .next()
994                    .is_some_and(|short| short.to_string() == flag.name));
995        let forms = visible_shorts
996            .map(|c| format!("-{c}"))
997            .chain(visible_longs.map(|s| format!("--{s}")))
998            .collect_vec()
999            .join(" ");
1000        let declaration = if inferred_matches {
1001            forms
1002        } else if forms.is_empty() {
1003            format!("{}:", flag.name)
1004        } else {
1005            format!("{}: {forms}", flag.name)
1006        };
1007        node.push(KdlEntry::new(declaration));
1008        if let Some(desc) = &flag.help {
1009            node.push(string_entry(Some("help"), desc));
1010        }
1011        if !flag.hidden_aliases.is_empty() || !flag.hidden_short_aliases.is_empty() {
1012            let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1013            let mut aliases = KdlNode::new("alias");
1014            for alias in &flag.hidden_short_aliases {
1015                aliases.push(string_entry(None, &format!("-{alias}")));
1016            }
1017            for alias in &flag.hidden_aliases {
1018                aliases.push(string_entry(None, &format!("--{alias}")));
1019            }
1020            aliases.push(KdlEntry::new_prop("hide", true));
1021            children.nodes_mut().push(aliases);
1022        }
1023        if let Some(desc) = &flag.help_long {
1024            let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1025            let mut node = KdlNode::new("long_help");
1026            node.push(string_entry(None, desc));
1027            children.nodes_mut().push(node);
1028        }
1029        if let Some(desc) = &flag.help_md {
1030            let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1031            let mut node = KdlNode::new("help_md");
1032            node.push(string_entry(None, desc));
1033            children.nodes_mut().push(node);
1034        }
1035        for admonition in &flag.admonitions {
1036            let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1037            let name = match admonition.kind {
1038                SpecAdmonitionKind::Note => "note",
1039                SpecAdmonitionKind::Warning => "warning",
1040            };
1041            let mut block = KdlNode::new(name);
1042            block.push(string_entry(None, &admonition.text));
1043            children.nodes_mut().push(block);
1044        }
1045        if flag.required {
1046            node.push(KdlEntry::new_prop("required", true));
1047        }
1048        serialize_flag_list(&mut node, "required_if", &flag.required_if);
1049        for condition in &flag.required_if_eq {
1050            let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1051            let mut relation = KdlNode::new("required_if_eq");
1052            relation.push(string_entry(None, &condition.selector));
1053            relation.push(string_entry(None, &condition.value));
1054            children.nodes_mut().push(relation);
1055        }
1056        if !flag.required_if_eq_all.is_empty() {
1057            let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1058            let mut relation = KdlNode::new("required_if_eq_all");
1059            for condition in &flag.required_if_eq_all {
1060                relation.push(string_entry(None, &condition.selector));
1061                relation.push(string_entry(None, &condition.value));
1062            }
1063            children.nodes_mut().push(relation);
1064        }
1065        serialize_flag_list(&mut node, "required_unless", &flag.required_unless);
1066        serialize_flag_list(&mut node, "required_unless_all", &flag.required_unless_all);
1067        if flag.var {
1068            node.push(KdlEntry::new_prop("var", true));
1069        }
1070        if let Some(var_min) = flag.var_min {
1071            node.push(KdlEntry::new_prop("var_min", var_min as i128));
1072        }
1073        if let Some(var_max) = flag.var_max {
1074            node.push(KdlEntry::new_prop("var_max", var_max as i128));
1075        }
1076        if flag.hide {
1077            node.push(KdlEntry::new_prop("hide", true));
1078        }
1079        for (name, hidden) in [
1080            ("hide_default_value", flag.hide_default_value),
1081            ("hide_env", flag.hide_env),
1082            ("hide_env_values", flag.hide_env_values),
1083            ("hide_possible_values", flag.hide_possible_values),
1084            ("hide_short_help", flag.hide_short_help),
1085            ("hide_long_help", flag.hide_long_help),
1086        ] {
1087            if hidden {
1088                node.push(KdlEntry::new_prop(name, true));
1089            }
1090        }
1091        if flag.global {
1092            node.push(KdlEntry::new_prop("global", true));
1093        }
1094        if flag.count {
1095            node.push(KdlEntry::new_prop("count", true));
1096        }
1097        if flag.action != SpecFlagAction::Set {
1098            node.push(string_entry(Some("action"), flag.action.as_str()));
1099        }
1100        if flag.builtin {
1101            node.push(KdlEntry::new_prop("builtin", true));
1102        }
1103        if flag.allow_hyphen_values() {
1104            node.push(KdlEntry::new_prop("allow_hyphen_values", true));
1105        }
1106        if flag
1107            .arg
1108            .as_ref()
1109            .is_some_and(|arg| arg.allow_negative_numbers)
1110        {
1111            node.push(KdlEntry::new_prop("allow_negative_numbers", true));
1112        }
1113        if let Some(terminator) = flag
1114            .arg
1115            .as_ref()
1116            .and_then(|arg| arg.value_terminator.as_deref())
1117        {
1118            node.push(string_entry(Some("value_terminator"), terminator));
1119        }
1120        if let Some(negate) = &flag.negate {
1121            node.push(string_entry(Some("negate"), negate));
1122        }
1123        if flag.overrides.len() == 1 && fits_selector_property(&flag.overrides[0]) {
1124            node.push(string_entry(Some("overrides"), &flag.overrides[0]));
1125        } else if !flag.overrides.is_empty() {
1126            let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1127            let mut overrides = KdlNode::new("overrides");
1128            for target in &flag.overrides {
1129                overrides.push(string_entry(None, target));
1130            }
1131            children.nodes_mut().push(overrides);
1132        }
1133        if flag.conflicts.len() == 1 && fits_selector_property(&flag.conflicts[0]) {
1134            node.push(string_entry(Some("conflicts"), &flag.conflicts[0]));
1135        } else if !flag.conflicts.is_empty() {
1136            let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1137            let mut conflicts = KdlNode::new("conflicts");
1138            for target in &flag.conflicts {
1139                conflicts.push(string_entry(None, target));
1140            }
1141            children.nodes_mut().push(conflicts);
1142        }
1143        if flag.requires.len() == 1 && fits_selector_property(&flag.requires[0]) {
1144            node.push(string_entry(Some("requires"), &flag.requires[0]));
1145        } else if !flag.requires.is_empty() {
1146            let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1147            let mut requires = KdlNode::new("requires");
1148            for target in &flag.requires {
1149                requires.push(string_entry(None, target));
1150            }
1151            children.nodes_mut().push(requires);
1152        }
1153        for condition in &flag.requires_if {
1154            let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1155            let mut requires_if = KdlNode::new("requires_if");
1156            requires_if.push(string_entry(None, &condition.value));
1157            requires_if.push(string_entry(None, &condition.requires));
1158            children.nodes_mut().push(requires_if);
1159        }
1160        for condition in &flag.default_if {
1161            let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1162            let mut default_if = KdlNode::new("default_if");
1163            default_if.push(string_entry(None, &condition.selector));
1164            if let Some(when) = &condition.when {
1165                default_if.push(string_entry(None, when));
1166            }
1167            default_if.push(string_entry(None, &condition.value));
1168            children.nodes_mut().push(default_if);
1169        }
1170        if flag.exclusive {
1171            node.push(KdlEntry::new_prop("exclusive", true));
1172        }
1173        if flag.require_equals {
1174            node.push(KdlEntry::new_prop("require_equals", true));
1175        }
1176        if flag.value_optional {
1177            node.push(KdlEntry::new_prop("value_optional", true));
1178        }
1179        if flag.bool_value {
1180            node.push(KdlEntry::new_prop("bool_value", true));
1181        }
1182        if let Some(missing) = &flag.default_missing {
1183            node.push(string_entry(Some("default_missing"), missing));
1184        }
1185        if let Some(env) = &flag.env {
1186            node.push(string_entry(Some("env"), env));
1187        }
1188        serialize_flag_list(&mut node, "env_fallback", &flag.env_fallback);
1189        serialize_flag_list(&mut node, "deprecated_env", &flag.deprecated_env);
1190        if let Some(help_heading) = &flag.help_heading {
1191            node.push(string_entry(Some("help_heading"), help_heading));
1192        }
1193        if let Some(surface) = &flag.surface {
1194            node.push(string_entry(Some("surface"), surface));
1195        }
1196        serialize_flag_list(&mut node, "available_if", &flag.available_if);
1197        if let Some(order) = flag.display_order {
1198            node.push(KdlEntry::new_prop("display_order", order as i128));
1199        }
1200        if let Some(effect) = &flag.effect {
1201            node.push(string_entry(Some("effect"), effect.as_str()));
1202        }
1203        if let Some(deprecated) = &flag.deprecated {
1204            node.push(string_entry(Some("deprecated"), deprecated));
1205        }
1206        if let Some(at) = &flag.deprecated_warn_at {
1207            node.push(string_entry(Some("deprecated_warn_at"), at));
1208        }
1209        if let Some(at) = &flag.deprecated_remove_at {
1210            node.push(string_entry(Some("deprecated_remove_at"), at));
1211        }
1212        // Serialize default values
1213        if !flag.default.is_empty() {
1214            if flag.default.len() == 1 {
1215                // Single value: use property default="bar"
1216                node.push(KdlEntry::new_prop("default", flag.default[0].clone()));
1217            } else {
1218                // Multiple values: use child node default { "xyz"; "bar" }
1219                let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1220                let mut default_node = KdlNode::new("default");
1221                let default_children = default_node
1222                    .children_mut()
1223                    .get_or_insert_with(KdlDocument::new);
1224                for val in &flag.default {
1225                    default_children
1226                        .nodes_mut()
1227                        .push(KdlNode::new(val.as_str()));
1228                }
1229                children.nodes_mut().push(default_node);
1230            }
1231        }
1232        if let Some(arg) = &flag.arg {
1233            let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1234            if flag.allow_hyphen_values() {
1235                let mut arg = arg.clone();
1236                arg.double_dash = SpecDoubleDashChoices::Optional;
1237                children.nodes_mut().push((&arg).into());
1238            } else {
1239                children.nodes_mut().push(arg.into());
1240            }
1241        }
1242        node
1243    }
1244}
1245
1246fn serialize_flag_list(node: &mut KdlNode, name: &str, flags: &[String]) {
1247    if flags.len() == 1 && fits_selector_property(&flags[0]) {
1248        node.push(string_entry(Some(name), &flags[0]));
1249    } else if !flags.is_empty() {
1250        let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1251        let mut list = KdlNode::new(name);
1252        for flag in flags {
1253            list.push(string_entry(None, flag));
1254        }
1255        children.nodes_mut().push(list);
1256    }
1257}
1258
1259impl FromStr for SpecFlag {
1260    type Err = UsageErr;
1261    fn from_str(input: &str) -> Result<Self> {
1262        let mut flag = Self::default();
1263        // Keep a flag-level repetition marker attached when an equals value follows it.
1264        // Every other ellipsis becomes its own token so its position still distinguishes
1265        // a repeatable flag (`--flag… <ARG>`) from a variadic value (`--flag <ARG>…`).
1266        let input = input
1267            .replace("...", "…")
1268            .replace("…[=", "\u{e000}[=")
1269            .replace("…=", "\u{e000}=")
1270            .replace("…", " … ")
1271            .replace('\u{e000}', "…");
1272        for part in input.split_whitespace() {
1273            if let Some((form, value)) = part
1274                .strip_suffix(']')
1275                .and_then(|part| part.split_once("[="))
1276            {
1277                let (form, repeatable) = form
1278                    .strip_suffix('…')
1279                    .map_or((form, false), |form| (form, true));
1280                let recognized = if let Some(long) = form.strip_prefix("--") {
1281                    if long.is_empty() {
1282                        false
1283                    } else {
1284                        flag.long.push(long.to_string());
1285                        true
1286                    }
1287                } else if let Some(short) = form.strip_prefix('-') {
1288                    if short.chars().count() != 1 {
1289                        return Err(InvalidFlag {
1290                            token: form.to_string(),
1291                            reason:
1292                                "short flags must be a single character (use -- for long flags)"
1293                                    .to_string(),
1294                            span: (0, input.len()).into(),
1295                            input: input.to_string(),
1296                        });
1297                    }
1298                    flag.short.push(short.chars().next().unwrap());
1299                    true
1300                } else {
1301                    false
1302                };
1303                if recognized && !value.is_empty() {
1304                    flag.var |= repeatable;
1305                    flag.require_equals = true;
1306                    flag.arg = Some(match flag.arg.take() {
1307                        Some(existing) => format!("{} [{value}]", existing.usage()).parse()?,
1308                        None => format!("[{value}]").parse()?,
1309                    });
1310                    continue;
1311                }
1312            }
1313            if let Some((form, value)) = part.split_once('=') {
1314                let (form, repeatable) = form
1315                    .strip_suffix('…')
1316                    .map_or((form, false), |form| (form, true));
1317                let recognized = if let Some(long) = form.strip_prefix("--") {
1318                    if long.is_empty() {
1319                        false
1320                    } else {
1321                        flag.long.push(long.to_string());
1322                        true
1323                    }
1324                } else if let Some(short) = form.strip_prefix('-') {
1325                    if short.chars().count() != 1 {
1326                        return Err(InvalidFlag {
1327                            token: form.to_string(),
1328                            reason:
1329                                "short flags must be a single character (use -- for long flags)"
1330                                    .to_string(),
1331                            span: (0, input.len()).into(),
1332                            input: input.to_string(),
1333                        });
1334                    }
1335                    flag.short.push(short.chars().next().unwrap());
1336                    true
1337                } else {
1338                    false
1339                };
1340                if recognized {
1341                    flag.var |= repeatable;
1342                    if !(value.starts_with('<') && value.ends_with('>')
1343                        || value.starts_with('[') && value.ends_with(']'))
1344                    {
1345                        return Err(InvalidFlag {
1346                            token: part.to_string(),
1347                            reason: "an equals sign must attach <arg> or [arg]".to_string(),
1348                            span: (0, input.len()).into(),
1349                            input: input.to_string(),
1350                        });
1351                    }
1352                    flag.require_equals = true;
1353                    flag.arg = Some(match flag.arg.take() {
1354                        Some(existing) => format!("{} {value}", existing.usage()).parse()?,
1355                        None => value.to_string().parse()?,
1356                    });
1357                    continue;
1358                }
1359            }
1360            if let Some(name) = part.strip_suffix(':') {
1361                flag.name = name.to_string();
1362            } else if let Some(long) = part.strip_prefix("--") {
1363                flag.long.push(long.to_string());
1364            } else if let Some(short) = part.strip_prefix('-') {
1365                if short.chars().count() != 1 {
1366                    return Err(InvalidFlag {
1367                        token: format!("-{short}"),
1368                        reason: "short flags must be a single character (use -- for long flags)"
1369                            .to_string(),
1370                        span: (0, input.len()).into(),
1371                        input: input.to_string(),
1372                    });
1373                }
1374                flag.short.push(short.chars().next().unwrap());
1375            } else if part == "…" {
1376                if let Some(arg) = &mut flag.arg {
1377                    arg.var = true;
1378                } else {
1379                    flag.var = true;
1380                }
1381            } else if part.starts_with('<') && part.ends_with('>')
1382                || part.starts_with('[') && part.ends_with(']')
1383            {
1384                flag.arg = Some(match flag.arg.take() {
1385                    Some(existing) => format!("{} {part}", existing.usage()).parse()?,
1386                    None => part.to_string().parse()?,
1387                });
1388            } else {
1389                return Err(InvalidFlag {
1390                    token: part.to_string(),
1391                    reason: "unexpected token (expected -x, --long, <arg>, or [arg])".to_string(),
1392                    span: (0, input.len()).into(),
1393                    input: input.to_string(),
1394                });
1395            }
1396        }
1397        if flag.name.is_empty() {
1398            flag.name = get_name_from_short_and_long(&flag.short, &flag.long).unwrap_or_default();
1399        }
1400        flag.usage = flag.usage();
1401        Ok(flag)
1402    }
1403}
1404
1405#[cfg(feature = "clap")]
1406impl From<&clap::Arg> for SpecFlag {
1407    fn from(c: &clap::Arg) -> Self {
1408        let required = c.is_required_set();
1409        let help = c.get_help().map(|s| s.to_string());
1410        let help_long = c.get_long_help().map(|s| s.to_string());
1411        let help_first_line = help.as_ref().map(|s| string::first_line(s));
1412        let hide = c.is_hide_set();
1413        let var = matches!(
1414            c.get_action(),
1415            clap::ArgAction::Count | clap::ArgAction::Append
1416        );
1417        let default: Vec<String> = crate::spec::arg::default_values(c);
1418        let mut short = c.get_short_and_visible_aliases().unwrap_or_default();
1419        let visible_short = short.clone();
1420        let hidden_short_aliases = c
1421            .get_all_short_aliases()
1422            .unwrap_or_default()
1423            .into_iter()
1424            .filter(|alias| !visible_short.contains(alias))
1425            .collect::<Vec<_>>();
1426        short.extend(hidden_short_aliases.iter().copied());
1427        let mut long = c
1428            .get_long_and_visible_aliases()
1429            .unwrap_or_default()
1430            .into_iter()
1431            .map(|s| s.to_string())
1432            .collect::<Vec<_>>();
1433        let visible_long = long.clone();
1434        let hidden_aliases = c
1435            .get_all_aliases()
1436            .unwrap_or_default()
1437            .into_iter()
1438            .filter(|alias| !visible_long.iter().any(|visible| visible == alias))
1439            .map(str::to_string)
1440            .collect::<Vec<_>>();
1441        long.extend(hidden_aliases.iter().cloned());
1442        let name = get_name_from_short_and_long(&short, &long).unwrap_or_default();
1443        // A false-setting switch is the negative spelling itself. The portable model keeps
1444        // that as `negate`, and a name-only flag form (`color:`) preserves its identity without
1445        // inventing a positive spelling that clap never accepted. One spelling is lossless;
1446        // multiple aliases remain the bridge's documented action lossiness.
1447        let negate = if matches!(c.get_action(), clap::ArgAction::SetFalse)
1448            && short.is_empty()
1449            && long.len() == 1
1450        {
1451            Some(format!("--{}", long.remove(0)))
1452        } else {
1453            None
1454        };
1455        let arg = if let clap::ArgAction::Set | clap::ArgAction::Append = c.get_action() {
1456            let value_names = crate::spec::arg::value_names_from_clap(c);
1457            let mut arg = SpecArg::from(
1458                value_names
1459                    .first()
1460                    .cloned()
1461                    .unwrap_or_else(|| name.clone())
1462                    .as_str(),
1463            );
1464            arg.value_names = value_names;
1465
1466            arg.choices = crate::spec::arg::choices_from_clap(c);
1467
1468            // The flag's argument is built from its value name rather than from the
1469            // clap `Arg`, so what the `Arg` says about the *value* has to be carried
1470            // here — the `From<&clap::Arg> for SpecArg` impl never sees this one.
1471            //
1472            // A delimiter *is* the statement that several values can land, so it brings
1473            // `var` with it rather than waiting for one.
1474            //
1475            // Gating this on the action or on `num_args` was wrong: clap's parser splits
1476            // whenever a delimiter is set — `parser.rs` reaches for
1477            // `arg.get_value_delimiter()` before it looks at anything else — so
1478            // `ArgAction::Set` with `value_delimiter(',')` is one word becoming several,
1479            // and that is the common spelling. Reading it as single-valued dropped the
1480            // delimiter and left a CLI whose defaults split and whose typed values did
1481            // not.
1482            if let Some(delimiter) = c.get_value_delimiter() {
1483                arg.var = true;
1484                // Only if it is one byte. Splitting is by byte everywhere below the spec,
1485                // and a spec carrying a wider separator could not be written back out —
1486                // `to_kdl` would emit what parsing then refuses. clap still splits on it,
1487                // so `var` stays: the values arrive, and only the spec's account of how
1488                // they were separated is lost.
1489                if delimiter.is_ascii() {
1490                    arg.delimiter = Some(delimiter);
1491                }
1492            } else if var || c.get_num_args().is_some_and(|n| n.max_values() > 1) {
1493                arg.var = true;
1494            }
1495            arg.allow_negative_numbers = c.is_allow_negative_numbers_set();
1496            if arg.var
1497                && let Some(terminator) = c.get_value_terminator()
1498            {
1499                arg.value_terminator = Some(terminator.to_string());
1500            }
1501
1502            // These bounds live on the nested value argument and are enforced per occurrence.
1503            // That preserves both a single `Set` and each repetition of `Append`.
1504            crate::spec::arg::value_bounds(c, &mut arg, true);
1505
1506            Some(arg)
1507        } else {
1508            None
1509        };
1510        let mut flag = Self {
1511            name,
1512            usage: "".into(),
1513            short,
1514            hidden_short_aliases,
1515            long,
1516            hidden_aliases,
1517            required,
1518            required_if: vec![],
1519            required_if_eq: vec![],
1520            required_if_eq_all: vec![],
1521            required_unless: vec![],
1522            required_unless_all: vec![],
1523            deprecated_warn_at: None,
1524            deprecated_remove_at: None,
1525            conflicts: vec![],
1526            // clap 4.6 has `Arg::requires` and its variants as setters with no getter, so
1527            // there is nothing to read here however the `Arg` was built. Left empty rather
1528            // than guessed at, and counted by `gen-shadow` as a thing the clap dialect
1529            // cannot carry.
1530            requires: vec![],
1531            // The conditional forms are hidden behind the same clap API boundary.
1532            requires_if: vec![],
1533            // clap 4 has `Arg::default_value_if` as a setter with no getter.
1534            default_if: vec![],
1535            // This one clap does expose, unlike `requires` just above.
1536            exclusive: c.is_exclusive_set(),
1537            require_equals: c.is_require_equals_set(),
1538            value_optional: arg.is_some()
1539                && c.get_num_args()
1540                    .is_some_and(|n| n.min_values() == 0 && n.max_values() > 0),
1541            // clap has no attached-value boolean-switch policy.
1542            bool_value: false,
1543            // clap 4 has `Arg::default_missing_value` as a setter with no getter.
1544            default_missing: None,
1545            help,
1546            help_long,
1547            help_md: None,
1548            admonitions: Vec::new(),
1549            help_first_line,
1550            var,
1551            var_min: None,
1552            var_max: None,
1553            hide,
1554            hide_default_value: c.is_hide_default_value_set(),
1555            hide_env: c.is_hide_env_set(),
1556            hide_env_values: c.is_hide_env_values_set(),
1557            hide_possible_values: c.is_hide_possible_values_set(),
1558            hide_short_help: c.is_hide_short_help_set(),
1559            hide_long_help: c.is_hide_long_help_set(),
1560            global: c.is_global_set(),
1561            arg,
1562            count: matches!(c.get_action(), clap::ArgAction::Count),
1563            action: match c.get_action() {
1564                clap::ArgAction::Help => SpecFlagAction::Help,
1565                clap::ArgAction::HelpShort => SpecFlagAction::HelpShort,
1566                clap::ArgAction::HelpLong => SpecFlagAction::HelpLong,
1567                clap::ArgAction::Version => SpecFlagAction::Version,
1568                _ => SpecFlagAction::Set,
1569            },
1570            builtin: false,
1571            default,
1572            deprecated: None,
1573            negate,
1574            overrides: vec![],
1575            // Filled by the command conversion: clap keeps conflicts on the
1576            // `Command`, not the `Arg`, so an `Arg` alone cannot see them.
1577            // clap has no way to express this; consumers set it on the derived
1578            // spec (see the effect docs).
1579            effect: None,
1580            env: None,
1581            env_fallback: vec![],
1582            deprecated_env: vec![],
1583            help_heading: c.get_help_heading().map(|s| s.to_string()),
1584            surface: None,
1585            available_if: Vec::new(),
1586            display_order: Some(c.get_display_order()),
1587        };
1588        if c.is_allow_hyphen_values_set()
1589            && let Some(arg) = &mut flag.arg
1590        {
1591            arg.double_dash = SpecDoubleDashChoices::Automatic;
1592        }
1593        flag.usage = flag.usage();
1594        flag
1595    }
1596}
1597
1598// #[cfg(feature = "clap")]
1599// impl From<&SpecFlag> for clap::Arg {
1600//     fn from(flag: &SpecFlag) -> Self {
1601//         let mut a = clap::Arg::new(&flag.name);
1602//         if let Some(desc) = &flag.help {
1603//             a = a.help(desc);
1604//         }
1605//         if flag.required {
1606//             a = a.required(true);
1607//         }
1608//         if let Some(arg) = &flag.arg {
1609//             a = a.value_name(&arg.name);
1610//             if arg.var {
1611//                 a = a.action(clap::ArgAction::Append)
1612//             } else {
1613//                 a = a.action(clap::ArgAction::Set)
1614//             }
1615//         } else {
1616//             a = a.action(clap::ArgAction::SetTrue)
1617//         }
1618//         // let mut a = clap::Arg::new(&flag.name)
1619//         //     .required(flag.required)
1620//         //     .action(clap::ArgAction::SetTrue);
1621//         if let Some(short) = flag.short.first() {
1622//             a = a.short(*short);
1623//         }
1624//         if let Some(long) = flag.long.first() {
1625//             a = a.long(long);
1626//         }
1627//         for short in flag.short.iter().skip(1) {
1628//             a = a.visible_short_alias(*short);
1629//         }
1630//         for long in flag.long.iter().skip(1) {
1631//             a = a.visible_alias(long);
1632//         }
1633//         // cmd = cmd.arg(a);
1634//         // if flag.multiple {
1635//         //     a = a.multiple(true);
1636//         // }
1637//         // if flag.hide {
1638//         //     a = a.hide_possible_values(true);
1639//         // }
1640//         a
1641//     }
1642// }
1643
1644impl Display for SpecFlag {
1645    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1646        write!(f, "{}", self.usage())
1647    }
1648}
1649impl PartialEq for SpecFlag {
1650    fn eq(&self, other: &Self) -> bool {
1651        self.name == other.name
1652    }
1653}
1654impl Eq for SpecFlag {}
1655impl Hash for SpecFlag {
1656    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1657        self.name.hash(state);
1658    }
1659}
1660
1661fn get_name_from_short_and_long(short: &[char], long: &[String]) -> Option<String> {
1662    long.first()
1663        .map(|s| s.to_string())
1664        .or_else(|| short.first().map(|c| c.to_string()))
1665}
1666
1667#[cfg(test)]
1668mod tests {
1669    use super::*;
1670    use crate::Spec;
1671    use insta::assert_snapshot;
1672
1673    #[test]
1674    fn from_str() {
1675        assert_snapshot!("-f".parse::<SpecFlag>().unwrap(), @"-f");
1676        assert_snapshot!("--flag".parse::<SpecFlag>().unwrap(), @"--flag");
1677        assert_snapshot!("-f --flag".parse::<SpecFlag>().unwrap(), @"-f --flag");
1678        assert_snapshot!("-f --flag…".parse::<SpecFlag>().unwrap(), @"-f --flag");
1679        assert_snapshot!("-f --flag …".parse::<SpecFlag>().unwrap(), @"-f --flag");
1680        assert_snapshot!("--flag <arg>".parse::<SpecFlag>().unwrap(), @"--flag <arg>");
1681        assert_snapshot!("-f --flag <arg>".parse::<SpecFlag>().unwrap(), @"-f --flag <arg>");
1682        assert_snapshot!("-f --flag… <arg>".parse::<SpecFlag>().unwrap(), @"-f --flag <arg>");
1683        assert_snapshot!("-f --flag <arg>…".parse::<SpecFlag>().unwrap(), @"-f --flag <arg>…");
1684        let range = "--range <start> <end>".parse::<SpecFlag>().unwrap();
1685        let arg = range.arg.as_ref().unwrap();
1686        assert_eq!(arg.value_names, ["start", "end"]);
1687        assert_eq!((arg.var_min, arg.var_max), (Some(2), Some(2)));
1688        assert_snapshot!(range, @"--range <start> <end>");
1689        assert_snapshot!("myflag: -f".parse::<SpecFlag>().unwrap(), @"myflag: -f");
1690        assert_snapshot!("myflag: -f --flag <arg>".parse::<SpecFlag>().unwrap(), @"myflag: -f --flag <arg>");
1691    }
1692
1693    #[test]
1694    fn a_relationship_property_names_one_flag() {
1695        for key in [
1696            "conflicts",
1697            "requires",
1698            "overrides",
1699            "required_if",
1700            "required_unless",
1701            "required_unless_all",
1702        ] {
1703            let several = format!("flag \"--a\" {key}=\"--b --c\"\nflag \"--b\"\nflag \"--c\"\n");
1704            let err = several.parse::<Spec>().unwrap_err();
1705            let crate::error::UsageErr::InvalidInput(err, ..) = err else {
1706                panic!("unexpected error: {err:?}");
1707            };
1708            assert!(
1709                err.contains(&format!("{key} takes one flag or argument per property")),
1710                "{key}: {err}"
1711            );
1712
1713            let repeated = format!("flag \"--a\" {key}=\"--b\" {key}=\"--c\"\n");
1714            let err = repeated.parse::<Spec>().unwrap_err();
1715            let crate::error::UsageErr::InvalidInput(err, ..) = err else {
1716                panic!("unexpected error: {err:?}");
1717            };
1718            assert!(
1719                err.contains(&format!("{key} is given more than once")),
1720                "{key}: {err}"
1721            );
1722        }
1723    }
1724
1725    #[test]
1726    fn several_relationship_targets_are_a_child_node() {
1727        let spec: Spec =
1728            "flag \"--a\" {\n  conflicts \"--b\" \"--c\"\n}\nflag \"--b\"\nflag \"--c\"\n"
1729                .parse()
1730                .unwrap();
1731        assert_eq!(spec.cmd.flags[0].conflicts, ["--b", "--c"]);
1732    }
1733
1734    #[test]
1735    fn clap_token_boundaries_survive_the_bridge() {
1736        let command = clap::Command::new("ex")
1737            .allow_negative_numbers(true)
1738            .arg(
1739                clap::Arg::new("item")
1740                    .long("item")
1741                    .action(clap::ArgAction::Append)
1742                    .value_terminator(";"),
1743            )
1744            .arg(clap::Arg::new("number"));
1745        let spec = Spec::from(&command);
1746        let item = spec.cmd.flags[0].arg.as_ref().unwrap();
1747
1748        assert!(item.allow_negative_numbers);
1749        assert_eq!(item.value_terminator.as_deref(), Some(";"));
1750        assert!(spec.cmd.args[0].allow_negative_numbers);
1751    }
1752
1753    #[test]
1754    fn hidden_aliases_parse_bind_and_round_trip_without_becoming_visible() {
1755        let spec: Spec =
1756            "flag \"-o --output <file>\" {\n  alias \"-q\" \"--quietly\" hide=#true\n}\n"
1757                .parse()
1758                .unwrap();
1759        let flag = &spec.cmd.flags[0];
1760        assert_eq!(flag.short, ['o', 'q']);
1761        assert_eq!(flag.long, ["output", "quietly"]);
1762        assert_eq!(flag.hidden_short_aliases, ['q']);
1763        assert_eq!(flag.hidden_aliases, ["quietly"]);
1764
1765        let emitted = spec.to_string();
1766        assert!(emitted.contains("flag \"-o --output\""), "{emitted}");
1767        assert!(!emitted.contains("flag \"-o -q"), "{emitted}");
1768        assert!(
1769            emitted.contains("alias \"-q\" \"--quietly\" hide=#true"),
1770            "{emitted}"
1771        );
1772        let reparsed: Spec = emitted.parse().unwrap();
1773        assert_eq!(reparsed.cmd.flags[0].short, flag.short);
1774        assert_eq!(reparsed.cmd.flags[0].long, flag.long);
1775        assert_eq!(
1776            reparsed.cmd.flags[0].hidden_short_aliases,
1777            flag.hidden_short_aliases
1778        );
1779        assert_eq!(reparsed.cmd.flags[0].hidden_aliases, flag.hidden_aliases);
1780
1781        let cmd = clap::Command::new("ex").arg(
1782            clap::Arg::new("output")
1783                .short('o')
1784                .short_alias('q')
1785                .long("output")
1786                .visible_alias("out")
1787                .alias("quietly"),
1788        );
1789        let bridged = Spec::from(&cmd);
1790        let flag = &bridged.cmd.flags[0];
1791        assert_eq!(flag.short, ['o', 'q']);
1792        assert_eq!(flag.hidden_short_aliases, ['q']);
1793        assert_eq!(flag.long, ["output", "out", "quietly"]);
1794        assert_eq!(flag.hidden_aliases, ["quietly"]);
1795    }
1796
1797    #[test]
1798    fn conflicts_round_trip_and_come_across_from_clap() {
1799        // Both spellings, as `overrides` has: a property for one, a child node for
1800        // several.
1801        let spec: Spec = "flag \"--file <f>\" conflicts=\"--stdin\"\nflag \"--stdin\" {\n  conflicts \"--file\" \"--url\"\n}\nflag \"--url <u>\"\n"
1802            .parse()
1803            .unwrap();
1804        assert_eq!(spec.cmd.flags[0].conflicts, vec!["--stdin".to_string()]);
1805        assert_eq!(
1806            spec.cmd.flags[1].conflicts,
1807            vec!["--file".to_string(), "--url".to_string()]
1808        );
1809
1810        let reparsed: Spec = spec.to_string().parse().unwrap();
1811        assert_eq!(reparsed.cmd.flags[1].conflicts.len(), 2, "{spec}");
1812    }
1813
1814    #[test]
1815    fn requires_round_trips_in_both_spellings() {
1816        // The same two spellings `conflicts` has, because it is the same shape of
1817        // statement: a property for one selector, a child node for several.
1818        let spec: Spec = "flag \"--out <p>\" requires=\"--format\"\nflag \"--sign\" {\n  requires \"--key\" \"--identity\"\n}\nflag \"--format <f>\"\nflag \"--key <k>\"\nflag \"--identity <i>\"\n"
1819            .parse()
1820            .unwrap();
1821        assert_eq!(spec.cmd.flags[0].requires, vec!["--format".to_string()]);
1822        assert_eq!(
1823            spec.cmd.flags[1].requires,
1824            vec!["--key".to_string(), "--identity".to_string()]
1825        );
1826
1827        let reparsed: Spec = spec.to_string().parse().unwrap();
1828        assert_eq!(reparsed.cmd.flags[0].requires, vec!["--format".to_string()]);
1829        assert_eq!(reparsed.cmd.flags[1].requires.len(), 2, "{spec}");
1830    }
1831
1832    #[test]
1833    fn conditional_requirements_round_trip_in_order() {
1834        let spec: Spec = "flag \"--config <file>\" {\n  requires_if \"special.toml\" \"--key\"\n  requires_if \"remote.toml\" \"--token\"\n}\nflag \"--key <key>\"\nflag \"--token <token>\"\n"
1835            .parse()
1836            .unwrap();
1837        assert_eq!(
1838            spec.cmd.flags[0].requires_if,
1839            [
1840                SpecRequiresIf {
1841                    value: "special.toml".into(),
1842                    requires: "--key".into(),
1843                },
1844                SpecRequiresIf {
1845                    value: "remote.toml".into(),
1846                    requires: "--token".into(),
1847                },
1848            ]
1849        );
1850
1851        let emitted = spec.to_string();
1852        let reparsed: Spec = emitted.parse().unwrap();
1853        assert_eq!(
1854            reparsed.cmd.flags[0].requires_if,
1855            spec.cmd.flags[0].requires_if
1856        );
1857    }
1858
1859    #[test]
1860    fn conditional_defaults_round_trip_in_order_and_cannot_come_across_from_clap() {
1861        let spec: Spec = "flag \"--bin-names\" {\n  default_if \"--json\" \"true\"\n  default_if \"--output\" \"json\" \"pretty\"\n}\nflag \"--json\"\nflag \"--output <fmt>\"\n"
1862            .parse()
1863            .unwrap();
1864        assert_eq!(
1865            spec.cmd.flags[0].default_if,
1866            [
1867                SpecDefaultIf {
1868                    selector: "--json".into(),
1869                    when: None,
1870                    value: "true".into(),
1871                },
1872                SpecDefaultIf {
1873                    selector: "--output".into(),
1874                    when: Some("json".into()),
1875                    value: "pretty".into(),
1876                },
1877            ]
1878        );
1879
1880        let emitted = spec.to_string();
1881        let reparsed: Spec = emitted.parse().unwrap();
1882        assert_eq!(
1883            reparsed.cmd.flags[0].default_if,
1884            spec.cmd.flags[0].default_if
1885        );
1886
1887        // Same hole as `requires`: clap 4 has the setter and keeps the field private.
1888        let cmd = clap::Command::new("ex")
1889            .arg(
1890                clap::Arg::new("bin-names")
1891                    .long("bin-names")
1892                    .action(clap::ArgAction::SetTrue)
1893                    .default_value_if("json", clap::builder::ArgPredicate::IsPresent, "true"),
1894            )
1895            .arg(
1896                clap::Arg::new("json")
1897                    .long("json")
1898                    .action(clap::ArgAction::SetTrue),
1899            );
1900        let spec = Spec::from(&cmd);
1901        let bin_names = spec
1902            .cmd
1903            .flags
1904            .iter()
1905            .find(|f| f.name == "bin-names")
1906            .unwrap();
1907        assert!(
1908            bin_names.default_if.is_empty(),
1909            "clap exposes no getter for `default_value_if`; if this now fails, \
1910             the bridge can carry it and `SpecFlag::default_if` should say so"
1911        );
1912    }
1913
1914    #[test]
1915    fn exclusive_round_trips_and_comes_across_from_clap() {
1916        let spec: Spec = "flag \"--dump\" exclusive=#true\nflag \"--verbose\"\n"
1917            .parse()
1918            .unwrap();
1919        assert!(spec.cmd.flags[0].exclusive);
1920        assert!(!spec.cmd.flags[1].exclusive);
1921
1922        let reparsed: Spec = spec.to_string().parse().unwrap();
1923        assert!(reparsed.cmd.flags[0].exclusive, "{spec}");
1924
1925        // Unlike `requires`, clap answers for this one — `Arg::is_exclusive_set` — so a
1926        // spec generated from a clap command carries it.
1927        let cmd = clap::Command::new("ex")
1928            .arg(clap::Arg::new("dump").long("dump").exclusive(true))
1929            .arg(clap::Arg::new("verbose").long("verbose"));
1930        let spec = Spec::from(&cmd);
1931        let dump = spec.cmd.flags.iter().find(|f| f.name == "dump").unwrap();
1932        assert!(dump.exclusive);
1933        let verbose = spec.cmd.flags.iter().find(|f| f.name == "verbose").unwrap();
1934        assert!(!verbose.exclusive);
1935    }
1936
1937    #[test]
1938    fn a_single_clap_set_false_spelling_becomes_a_negative_only_flag() {
1939        let cmd = clap::Command::new("ex").arg(
1940            clap::Arg::new("color")
1941                .long("color")
1942                .action(clap::ArgAction::SetFalse),
1943        );
1944        let spec = Spec::from(&cmd);
1945        let color = spec.cmd.flags.iter().find(|f| f.name == "color").unwrap();
1946        assert!(color.long.is_empty());
1947        assert!(color.short.is_empty());
1948        assert_eq!(color.negate.as_deref(), Some("--color"));
1949        // Displayed as the spelling a reader has to type. `color:` names the flag's
1950        // identity, which the *spec* keeps below, but as a usage string it showed a reader
1951        // nothing they could enter — a docs heading read `### color:` for a flag whose only
1952        // form is `--color`.
1953        assert_eq!(color.usage(), "--color");
1954        assert_eq!(color.usage, "--color");
1955
1956        let rendered = spec.to_string();
1957        assert!(
1958            rendered.contains("flag color: negate=--color"),
1959            "{rendered}"
1960        );
1961        let reparsed: Spec = rendered.parse().expect("the bridge must emit readable KDL");
1962        assert_eq!(reparsed.cmd.flags[0].negate.as_deref(), Some("--color"));
1963    }
1964
1965    #[test]
1966    fn require_equals_round_trips_and_comes_across_from_clap() {
1967        let spec: Spec = "flag \"--inspect <PORT>\" require_equals=#true\n"
1968            .parse()
1969            .unwrap();
1970        assert!(spec.cmd.flags[0].require_equals);
1971
1972        let reparsed: Spec = spec.to_string().parse().unwrap();
1973        assert!(reparsed.cmd.flags[0].require_equals, "{spec}");
1974
1975        let cmd = clap::Command::new("ex").arg(
1976            clap::Arg::new("inspect")
1977                .long("inspect")
1978                .action(clap::ArgAction::Set)
1979                .require_equals(true),
1980        );
1981        let spec = Spec::from(&cmd);
1982        let inspect = spec.cmd.flags.iter().find(|f| f.name == "inspect").unwrap();
1983        assert!(inspect.require_equals);
1984        assert_eq!(inspect.usage(), "--inspect=<inspect>");
1985        let usage_reparsed: SpecFlag = inspect.usage().parse().unwrap();
1986        assert!(usage_reparsed.require_equals);
1987        assert_eq!(usage_reparsed.long, ["inspect"]);
1988        assert_eq!(usage_reparsed.arg.unwrap().name, "inspect");
1989        assert_eq!(
1990            crate::docs::models::SpecFlag::from(inspect).usage,
1991            "--inspect=<inspect>"
1992        );
1993
1994        let cmd = clap::Command::new("ex").arg(
1995            clap::Arg::new("color")
1996                .long("color")
1997                .action(clap::ArgAction::Set)
1998                .num_args(0..=1)
1999                .require_equals(true),
2000        );
2001        let spec = Spec::from(&cmd);
2002        let color = spec.cmd.flags.iter().find(|f| f.name == "color").unwrap();
2003        assert!(color.require_equals);
2004        assert!(color.value_optional);
2005        assert!(
2006            color.arg.as_ref().unwrap().required,
2007            "clap's optional arity is flag metadata, not positional presentation"
2008        );
2009        assert_eq!(color.usage(), "--color[=color]");
2010        assert_eq!(
2011            crate::docs::models::SpecFlag::from(color).usage,
2012            "--color[=color]"
2013        );
2014
2015        let optional: SpecFlag = "--color [WHEN]".parse().unwrap();
2016        let optional = SpecFlag {
2017            require_equals: true,
2018            ..optional
2019        };
2020        assert_eq!(optional.usage(), "--color[=WHEN]");
2021        let reparsed: SpecFlag = optional.usage().parse().unwrap();
2022        assert!(reparsed.require_equals);
2023        assert!(!reparsed.arg.as_ref().unwrap().required);
2024        assert_eq!(reparsed.arg.as_ref().unwrap().name, "WHEN");
2025        assert_eq!(
2026            crate::docs::models::SpecFlag::from(&optional).usage,
2027            "--color[=WHEN]"
2028        );
2029
2030        let variadic: SpecFlag = "--color [WHEN]…".parse().unwrap();
2031        let variadic = SpecFlag {
2032            require_equals: true,
2033            ..variadic
2034        };
2035        assert_eq!(variadic.usage(), "--color[=WHEN]…");
2036        assert_eq!(
2037            crate::docs::models::SpecFlag::from(&variadic).usage,
2038            "--color[=WHEN]…"
2039        );
2040        assert_eq!(
2041            variadic.usage().parse::<SpecFlag>().unwrap().usage(),
2042            "--color[=WHEN]…"
2043        );
2044
2045        let pair: SpecFlag = "--range [START] [END]".parse().unwrap();
2046        let pair = SpecFlag {
2047            require_equals: true,
2048            ..pair
2049        };
2050        assert_eq!(pair.usage(), "--range[=START] [END]");
2051        assert_eq!(
2052            crate::docs::models::SpecFlag::from(&pair).usage,
2053            "--range[=START] [END]"
2054        );
2055        assert_eq!(
2056            pair.usage().parse::<SpecFlag>().unwrap().usage(),
2057            "--range[=START] [END]"
2058        );
2059
2060        let repeatable: SpecFlag = "--tag <TAG>".parse().unwrap();
2061        let repeatable = SpecFlag {
2062            var: true,
2063            require_equals: true,
2064            ..repeatable
2065        };
2066        assert_eq!(repeatable.usage(), "--tag=<TAG>");
2067        let reparsed: SpecFlag = repeatable.usage().parse().unwrap();
2068        assert!(!reparsed.var);
2069        assert!(reparsed.require_equals);
2070        assert!(!reparsed.arg.as_ref().unwrap().var);
2071
2072        let repeatable_optional: SpecFlag = "--color [WHEN]".parse().unwrap();
2073        let repeatable_optional = SpecFlag {
2074            var: true,
2075            require_equals: true,
2076            ..repeatable_optional
2077        };
2078        assert_eq!(repeatable_optional.usage(), "--color[=WHEN]");
2079        let reparsed: SpecFlag = repeatable_optional.usage().parse().unwrap();
2080        assert!(!reparsed.var);
2081        assert!(reparsed.require_equals);
2082        assert!(!reparsed.arg.as_ref().unwrap().var);
2083    }
2084
2085    #[test]
2086    fn optional_flag_value_policy_round_trips_separately_from_help() {
2087        let spec: Spec = "flag \"--bump [LEVEL]\" value_optional=#true\n"
2088            .parse()
2089            .unwrap();
2090        let bump = &spec.cmd.flags[0];
2091        assert!(bump.value_optional);
2092        assert!(!bump.arg.as_ref().unwrap().required);
2093
2094        let rendered = spec.to_string();
2095        assert!(rendered.contains("value_optional=#true"), "{rendered}");
2096        let reparsed: Spec = rendered.parse().unwrap();
2097        assert!(reparsed.cmd.flags[0].value_optional);
2098
2099        let presentation_only: Spec = "flag \"--bump [LEVEL]\"\n".parse().unwrap();
2100        assert!(!presentation_only.cmd.flags[0].value_optional);
2101
2102        let command = clap::Command::new("ex").arg(
2103            clap::Arg::new("bump")
2104                .long("bump")
2105                .action(clap::ArgAction::Set)
2106                .num_args(0..=1),
2107        );
2108        let bridged = Spec::from(&command);
2109        assert!(bridged.cmd.flags[0].value_optional);
2110
2111        let zero_arity = clap::Command::new("ex").arg(
2112            clap::Arg::new("plain")
2113                .long("plain")
2114                .action(clap::ArgAction::Set)
2115                .num_args(0),
2116        );
2117        assert!(!Spec::from(&zero_arity).cmd.flags[0].value_optional);
2118    }
2119
2120    #[test]
2121    fn explicit_boolean_values_round_trip() {
2122        let spec: Spec = "flag \"--color\" negate=\"--no-color\" bool_value=#true\n"
2123            .parse()
2124            .unwrap();
2125        assert!(spec.cmd.flags[0].bool_value);
2126        let rendered = spec.to_string();
2127        assert!(rendered.contains("bool_value=#true"), "{rendered}");
2128        assert!(rendered.parse::<Spec>().unwrap().cmd.flags[0].bool_value);
2129
2130        for invalid in [
2131            "flag \"--jobs <N>\" bool_value=#true\n",
2132            "flag \"--verbose\" count=#true bool_value=#true\n",
2133        ] {
2134            assert!(invalid.parse::<Spec>().is_err(), "{invalid}");
2135        }
2136    }
2137
2138    #[test]
2139    fn default_missing_round_trips_and_cannot_come_across_from_clap() {
2140        let spec: Spec = "flag \"--color <WHEN>\" default_missing=\"always\"\n"
2141            .parse()
2142            .unwrap();
2143        assert_eq!(spec.cmd.flags[0].default_missing.as_deref(), Some("always"));
2144        assert!(
2145            !spec.cmd.flags[0].arg.as_ref().unwrap().required,
2146            "a missing value is optional, so help should not demand it"
2147        );
2148        assert!(
2149            spec.cmd.flags[0].usage.contains("[WHEN]")
2150                && !spec.cmd.flags[0].usage.contains("<WHEN>"),
2151            "help should show an optional value: {}",
2152            spec.cmd.flags[0].usage
2153        );
2154
2155        let reparsed: Spec = spec.to_string().parse().unwrap();
2156        assert_eq!(
2157            reparsed.cmd.flags[0].default_missing.as_deref(),
2158            Some("always"),
2159            "{spec}"
2160        );
2161
2162        // Same hole as `requires`: clap 4 has the setter and keeps the field private.
2163        let cmd = clap::Command::new("ex").arg(
2164            clap::Arg::new("color")
2165                .long("color")
2166                .action(clap::ArgAction::Set)
2167                .num_args(0..=1)
2168                .default_missing_value("always"),
2169        );
2170        let spec = Spec::from(&cmd);
2171        let color = spec.cmd.flags.iter().find(|f| f.name == "color").unwrap();
2172        assert!(
2173            color.default_missing.is_none(),
2174            "clap exposes no getter for `default_missing_value`; if this now fails, \
2175             the bridge can carry it and `SpecFlag::default_missing` should say so"
2176        );
2177    }
2178
2179    #[test]
2180    fn value_count_bounds_survive_the_clap_bridge() {
2181        let cmd = clap::Command::new("ex")
2182            .arg(
2183                clap::Arg::new("pair")
2184                    .long("pair")
2185                    .action(clap::ArgAction::Set)
2186                    .num_args(2),
2187            )
2188            .arg(
2189                clap::Arg::new("files")
2190                    .value_name("FILES")
2191                    .required(true)
2192                    .num_args(2..=4),
2193            );
2194        let spec = Spec::from(&cmd);
2195
2196        let pair_flag = spec.cmd.flags.iter().find(|f| f.name == "pair").unwrap();
2197        assert!(!pair_flag.var, "the flag itself is not repeatable");
2198        assert_eq!(pair_flag.var_min, None);
2199        assert_eq!(pair_flag.var_max, None);
2200        let pair = pair_flag.arg.as_ref().unwrap();
2201        assert!(pair.var);
2202        assert_eq!(pair.var_min, Some(2));
2203        assert_eq!(pair.var_max, Some(2));
2204
2205        let files = spec.cmd.args.iter().find(|a| a.name == "FILES").unwrap();
2206        assert!(files.var);
2207        assert_eq!(files.var_min, Some(2));
2208        assert_eq!(files.var_max, Some(4));
2209
2210        let words = ["ex", "--pair", "a", "b", "one", "two"].map(str::to_string);
2211        crate::parse(&spec, &words).expect("both clap value-count ranges are satisfied");
2212
2213        let words = ["ex", "--pair", "a", "--", "one", "two"].map(str::to_string);
2214        let err = crate::parse(&spec, &words).unwrap_err();
2215        assert!(
2216            format!("{err:?}").contains("requires at least 2 value(s), got 1"),
2217            "{err:?}"
2218        );
2219
2220        let reparsed: Spec = spec.to_string().parse().unwrap();
2221        let pair = reparsed.cmd.flags[0].arg.as_ref().unwrap();
2222        assert_eq!((pair.var_min, pair.var_max), (Some(2), Some(2)));
2223        assert_eq!(
2224            (reparsed.cmd.args[0].var_min, reparsed.cmd.args[0].var_max),
2225            (Some(2), Some(4))
2226        );
2227    }
2228
2229    #[test]
2230    fn append_value_count_bounds_are_per_occurrence() {
2231        let cmd = clap::Command::new("ex").arg(
2232            clap::Arg::new("pair")
2233                .long("pair")
2234                .action(clap::ArgAction::Append)
2235                .num_args(2),
2236        );
2237        let spec = Spec::from(&cmd);
2238        let flag = &spec.cmd.flags[0];
2239        let values = flag.arg.as_ref().unwrap();
2240        assert!(flag.var);
2241        assert_eq!((values.var_min, values.var_max), (Some(2), Some(2)));
2242
2243        crate::parse(
2244            &spec,
2245            &["ex", "--pair", "a", "b", "--pair", "c", "d"].map(str::to_string),
2246        )
2247        .expect("each occurrence satisfies the fixed cardinality");
2248
2249        let err = crate::parse(
2250            &spec,
2251            &["ex", "--pair", "a", "--pair", "c", "d"].map(str::to_string),
2252        )
2253        .unwrap_err();
2254        assert!(format!("{err:?}").contains("requires at least 2 value(s), got 1"));
2255    }
2256
2257    #[test]
2258    fn ranged_value_names_do_not_emit_invalid_fixed_arity() {
2259        let cmd = clap::Command::new("ex")
2260            .arg(
2261                clap::Arg::new("range")
2262                    .long("range")
2263                    .action(clap::ArgAction::Set)
2264                    .num_args(2..=4)
2265                    .value_names(["START", "END"]),
2266            )
2267            .arg(
2268                clap::Arg::new("files")
2269                    .num_args(1..=3)
2270                    .value_names(["FIRST", "REST"]),
2271            );
2272        let spec = Spec::from(&cmd);
2273        assert_eq!(
2274            spec.cmd.flags[0].arg.as_ref().unwrap().value_names,
2275            ["START"]
2276        );
2277        assert_eq!(spec.cmd.args[0].value_names, ["FIRST"]);
2278        let rendered = spec.to_string();
2279        let _: Spec = rendered.parse().expect("the generated KDL must parse back");
2280    }
2281
2282    #[test]
2283    fn delimiter_value_count_bounds_are_not_mapped() {
2284        let cmd = clap::Command::new("ex")
2285            .arg(
2286                clap::Arg::new("pairs")
2287                    .long("pairs")
2288                    .action(clap::ArgAction::Set)
2289                    .value_delimiter(',')
2290                    .num_args(2),
2291            )
2292            .arg(clap::Arg::new("items").value_delimiter(',').num_args(2..=3));
2293        let spec = Spec::from(&cmd);
2294
2295        let pairs = spec.cmd.flags[0].arg.as_ref().unwrap();
2296        assert!(pairs.var);
2297        assert_eq!(pairs.delimiter, Some(','));
2298        assert_eq!((pairs.var_min, pairs.var_max), (None, None));
2299
2300        let items = &spec.cmd.args[0];
2301        assert!(items.var);
2302        assert_eq!(items.delimiter, Some(','));
2303        assert_eq!((items.var_min, items.var_max), (None, None));
2304    }
2305
2306    #[test]
2307    fn an_optional_flag_value_carries_its_policy_and_bound() {
2308        let cmd = clap::Command::new("ex").arg(
2309            clap::Arg::new("values")
2310                .long("values")
2311                .action(clap::ArgAction::Set)
2312                .num_args(0..=3),
2313        );
2314        let spec = Spec::from(&cmd);
2315        let values = spec.cmd.flags[0].arg.as_ref().unwrap();
2316
2317        assert!(spec.cmd.flags[0].value_optional);
2318        assert_eq!(values.var_min, Some(0));
2319        assert_eq!(values.var_max, Some(3));
2320        assert_eq!(spec.cmd.flags[0].var_min, None);
2321        assert_eq!(spec.cmd.flags[0].var_max, None);
2322    }
2323
2324    #[test]
2325    fn requires_cannot_come_across_from_clap() {
2326        // Not an oversight to be fixed later: clap 4 has `Arg::requires` as a setter
2327        // with no getter and keeps the field private, so there is nothing here to read.
2328        // Asserted rather than left implied, because an empty vector otherwise looks
2329        // like a bug in the bridge — and because a future clap that *does* expose it
2330        // should fail this test rather than pass silently.
2331        let cmd = clap::Command::new("ex")
2332            .arg(clap::Arg::new("out").long("out").requires("format"))
2333            .arg(clap::Arg::new("format").long("format"));
2334        let spec = Spec::from(&cmd);
2335        let out = spec.cmd.flags.iter().find(|f| f.name == "out").unwrap();
2336        assert!(
2337            out.requires.is_empty(),
2338            "clap exposes no getter for `requires`; if this now fails, the bridge can \
2339             carry it and `SpecFlag::requires` should say so"
2340        );
2341    }
2342
2343    #[cfg(feature = "clap")]
2344    #[test]
2345    fn conflicts_survive_the_clap_bridge() {
2346        // clap has had `conflicts_with` for years and mise declares forty of them; the
2347        // bridge was dropping every one, because clap keeps conflicts on the command
2348        // rather than on the argument.
2349        let cmd = clap::Command::new("ex")
2350            .arg(clap::Arg::new("file").long("file").conflicts_with("stdin"))
2351            .arg(clap::Arg::new("stdin").long("stdin"));
2352        let spec: Spec = (&cmd).into();
2353
2354        let file = spec.cmd.flags.iter().find(|f| f.name == "file").unwrap();
2355        assert_eq!(file.conflicts, vec!["--stdin".to_string()]);
2356
2357        // Only the declared direction: clap validates a conflict both ways but reports
2358        // it only from the argument that declared it. Recording it once is enough,
2359        // because the check looks at every flag that was given — see the parser test
2360        // that rejects either order.
2361        let stdin = spec.cmd.flags.iter().find(|f| f.name == "stdin").unwrap();
2362        assert!(stdin.conflicts.is_empty());
2363
2364        let positional = clap::Command::new("ex")
2365            .arg(
2366                clap::Arg::new("from-file")
2367                    .long("from-file")
2368                    .conflicts_with("value"),
2369            )
2370            .arg(clap::Arg::new("value"));
2371        let spec: Spec = (&positional).into();
2372        let from_file = spec
2373            .cmd
2374            .flags
2375            .iter()
2376            .find(|f| f.name == "from-file")
2377            .unwrap();
2378        assert_eq!(from_file.conflicts, vec!["value".to_string()]);
2379
2380        // A short-only target is named `-q`, since that is the only name it has.
2381        // Taking only the long form dropped the conflict and left the spec accepting a
2382        // combination clap rejects.
2383        let shorts = clap::Command::new("ex")
2384            .arg(clap::Arg::new("loud").long("loud").conflicts_with("quiet"))
2385            .arg(clap::Arg::new("quiet").short('q'));
2386        let spec: Spec = (&shorts).into();
2387        let loud = spec.cmd.flags.iter().find(|f| f.name == "loud").unwrap();
2388        assert_eq!(loud.conflicts, vec!["-q".to_string()]);
2389    }
2390
2391    #[test]
2392    fn a_serialized_spec_can_always_be_read_back() {
2393        // Both of these produced KDL that this crate could not reparse: a node
2394        // argument beginning with a dash was rendered bare, and a control character
2395        // was rendered literally. Help text carries the second whenever a CLI
2396        // colors its output.
2397        let spec: Spec = "flag \"--shell <s>\" {\n  required_unless \"--jobs\" \"--color\"\n  overrides \"--keep\" \"--dry-run\"\n  long_help \"Colored.\\u{1b}[0m Text.\"\n}\n"
2398            .parse()
2399            .unwrap();
2400
2401        let serialized = spec.to_string();
2402        let reparsed: Spec = serialized
2403            .parse()
2404            .unwrap_or_else(|e| panic!("a serialized spec should reparse: {e}\n\n{serialized}"));
2405
2406        let flag = &reparsed.cmd.flags[0];
2407        assert_eq!(
2408            flag.required_unless,
2409            vec!["--jobs".to_string(), "--color".to_string()]
2410        );
2411        assert_eq!(
2412            flag.overrides,
2413            vec!["--keep".to_string(), "--dry-run".to_string()]
2414        );
2415        assert_eq!(flag.help_long.as_deref(), Some("Colored.\u{1b}[0m Text."));
2416    }
2417
2418    #[test]
2419    fn help_heading_round_trips() {
2420        // Both spellings: a property, and a child node for when the text is long.
2421        let spec: Spec = r#"
2422flag "--filter <pattern>" help_heading="Filtering"
2423flag "--exclude <pattern>" {
2424  help_heading "Filtering"
2425}
2426arg "<file>" help_heading="Input"
2427"#
2428        .parse()
2429        .unwrap();
2430        assert_eq!(spec.cmd.flags[0].help_heading.as_deref(), Some("Filtering"));
2431        assert_eq!(spec.cmd.flags[1].help_heading.as_deref(), Some("Filtering"));
2432        assert_eq!(spec.cmd.args[0].help_heading.as_deref(), Some("Input"));
2433
2434        // And it survives being written back out.
2435        let reparsed: Spec = spec.to_string().parse().unwrap();
2436        assert_eq!(
2437            reparsed.cmd.flags[0].help_heading.as_deref(),
2438            Some("Filtering")
2439        );
2440        assert_eq!(reparsed.cmd.args[0].help_heading.as_deref(), Some("Input"));
2441    }
2442
2443    #[cfg(feature = "clap")]
2444    #[test]
2445    fn help_heading_comes_across_from_clap() {
2446        // clap has had help_heading for years and the bridge was dropping it, so
2447        // a CLI that grouped its flags lost the grouping on the way into a spec.
2448        let cmd = clap::Command::new("ex")
2449            .arg(
2450                clap::Arg::new("filter")
2451                    .long("filter")
2452                    .help_heading("Filtering"),
2453            )
2454            .arg(clap::Arg::new("plain").long("plain"));
2455        let spec: Spec = (&cmd).into();
2456
2457        let filter = spec
2458            .cmd
2459            .flags
2460            .iter()
2461            .find(|f| f.name == "filter")
2462            .expect("--filter should be in the spec");
2463        assert_eq!(filter.help_heading.as_deref(), Some("Filtering"));
2464
2465        let plain = spec
2466            .cmd
2467            .flags
2468            .iter()
2469            .find(|f| f.name == "plain")
2470            .expect("--plain should be in the spec");
2471        assert_eq!(plain.help_heading, None);
2472    }
2473
2474    #[test]
2475    fn test_flag_with_env() {
2476        let spec = Spec::parse(
2477            &Default::default(),
2478            r#"
2479flag "--color" env="MYCLI_COLOR" help="Enable color output"
2480flag "--verbose" env="MYCLI_VERBOSE"
2481            "#,
2482        )
2483        .unwrap();
2484
2485        assert_snapshot!(spec, @r#"
2486        flag --color help="Enable color output" env=MYCLI_COLOR
2487        flag --verbose env=MYCLI_VERBOSE
2488        "#);
2489
2490        let color_flag = spec.cmd.flags.iter().find(|f| f.name == "color").unwrap();
2491        assert_eq!(color_flag.env, Some("MYCLI_COLOR".to_string()));
2492
2493        let verbose_flag = spec.cmd.flags.iter().find(|f| f.name == "verbose").unwrap();
2494        assert_eq!(verbose_flag.env, Some("MYCLI_VERBOSE".to_string()));
2495    }
2496
2497    #[test]
2498    fn test_flag_with_env_child_node() {
2499        let spec = Spec::parse(
2500            &Default::default(),
2501            r#"
2502flag "--color" help="Enable color output" {
2503    env "MYCLI_COLOR"
2504}
2505flag "--verbose" {
2506    env "MYCLI_VERBOSE"
2507}
2508            "#,
2509        )
2510        .unwrap();
2511
2512        assert_snapshot!(spec, @r#"
2513        flag --color help="Enable color output" env=MYCLI_COLOR
2514        flag --verbose env=MYCLI_VERBOSE
2515        "#);
2516
2517        let color_flag = spec.cmd.flags.iter().find(|f| f.name == "color").unwrap();
2518        assert_eq!(color_flag.env, Some("MYCLI_COLOR".to_string()));
2519
2520        let verbose_flag = spec.cmd.flags.iter().find(|f| f.name == "verbose").unwrap();
2521        assert_eq!(verbose_flag.env, Some("MYCLI_VERBOSE".to_string()));
2522    }
2523
2524    #[test]
2525    fn test_flag_with_overrides() {
2526        let spec = Spec::parse(
2527            &Default::default(),
2528            r#"
2529flag "--file <file>" overrides="--stdin"
2530flag "--format <format>" {
2531    overrides "--json" "--yaml"
2532}
2533            "#,
2534        )
2535        .unwrap();
2536
2537        assert_eq!(spec.cmd.flags[0].overrides, ["--stdin"]);
2538        assert_eq!(spec.cmd.flags[1].overrides, ["--json", "--yaml"]);
2539
2540        let reparsed: Spec = spec.to_string().parse().unwrap();
2541        assert_eq!(reparsed.cmd.flags[0].overrides, ["--stdin"]);
2542        assert_eq!(reparsed.cmd.flags[1].overrides, ["--json", "--yaml"]);
2543    }
2544
2545    #[test]
2546    fn test_flag_with_conditional_requirements() {
2547        let spec = Spec::parse(
2548            &Default::default(),
2549            r#"
2550flag "--file <file>" required_if="--dir"
2551flag "--output <output>" {
2552    required_unless "--stdout" "--check"
2553}
2554            "#,
2555        )
2556        .unwrap();
2557
2558        assert_eq!(spec.cmd.flags[0].required_if, ["--dir"]);
2559        assert_eq!(spec.cmd.flags[1].required_unless, ["--stdout", "--check"]);
2560
2561        let reparsed: Spec = spec.to_string().parse().unwrap();
2562        assert_eq!(reparsed.cmd.flags[0].required_if, ["--dir"]);
2563        assert_eq!(
2564            reparsed.cmd.flags[1].required_unless,
2565            ["--stdout", "--check"]
2566        );
2567    }
2568
2569    #[test]
2570    fn test_flag_with_boolean_defaults() {
2571        let spec = Spec::parse(
2572            &Default::default(),
2573            r#"
2574flag "--color" default=#true
2575flag "--verbose" default=#false
2576flag "--debug" default="true"
2577flag "--quiet" default="false"
2578            "#,
2579        )
2580        .unwrap();
2581
2582        let color_flag = spec.cmd.flags.iter().find(|f| f.name == "color").unwrap();
2583        assert_eq!(color_flag.default, vec!["true".to_string()]);
2584
2585        let verbose_flag = spec.cmd.flags.iter().find(|f| f.name == "verbose").unwrap();
2586        assert_eq!(verbose_flag.default, vec!["false".to_string()]);
2587
2588        let debug_flag = spec.cmd.flags.iter().find(|f| f.name == "debug").unwrap();
2589        assert_eq!(debug_flag.default, vec!["true".to_string()]);
2590
2591        let quiet_flag = spec.cmd.flags.iter().find(|f| f.name == "quiet").unwrap();
2592        assert_eq!(quiet_flag.default, vec!["false".to_string()]);
2593    }
2594
2595    #[test]
2596    fn test_flag_with_boolean_defaults_child_node() {
2597        let spec = Spec::parse(
2598            &Default::default(),
2599            r#"
2600flag "--color" {
2601    default #true
2602}
2603flag "--verbose" {
2604    default #false
2605}
2606            "#,
2607        )
2608        .unwrap();
2609
2610        let color_flag = spec.cmd.flags.iter().find(|f| f.name == "color").unwrap();
2611        assert_eq!(color_flag.default, vec!["true".to_string()]);
2612
2613        let verbose_flag = spec.cmd.flags.iter().find(|f| f.name == "verbose").unwrap();
2614        assert_eq!(verbose_flag.default, vec!["false".to_string()]);
2615    }
2616
2617    #[test]
2618    fn test_flag_with_single_default() {
2619        let spec = Spec::parse(
2620            &Default::default(),
2621            r#"
2622flag "--foo <foo>" var=#true default="bar"
2623            "#,
2624        )
2625        .unwrap();
2626
2627        let flag = spec.cmd.flags.iter().find(|f| f.name == "foo").unwrap();
2628        assert!(flag.var);
2629        assert_eq!(flag.default, vec!["bar".to_string()]);
2630    }
2631
2632    #[test]
2633    fn test_flag_with_multiple_defaults_child_node() {
2634        let spec = Spec::parse(
2635            &Default::default(),
2636            r#"
2637flag "--foo <foo>" var=#true {
2638    default {
2639        "xyz"
2640        "bar"
2641    }
2642}
2643            "#,
2644        )
2645        .unwrap();
2646
2647        let flag = spec.cmd.flags.iter().find(|f| f.name == "foo").unwrap();
2648        assert!(flag.var);
2649        assert_eq!(flag.default, vec!["xyz".to_string(), "bar".to_string()]);
2650    }
2651
2652    #[test]
2653    fn test_flag_with_single_default_child_node() {
2654        let spec = Spec::parse(
2655            &Default::default(),
2656            r#"
2657flag "--foo <foo>" var=#true {
2658    default "bar"
2659}
2660            "#,
2661        )
2662        .unwrap();
2663
2664        let flag = spec.cmd.flags.iter().find(|f| f.name == "foo").unwrap();
2665        assert!(flag.var);
2666        assert_eq!(flag.default, vec!["bar".to_string()]);
2667    }
2668
2669    #[test]
2670    fn test_flag_default_serialization_single() {
2671        let spec = Spec::parse(
2672            &Default::default(),
2673            r#"
2674flag "--foo <foo>" default="bar"
2675            "#,
2676        )
2677        .unwrap();
2678
2679        // When serialized, single default should use property format
2680        let output = spec.to_string();
2681        assert!(output.contains("default=bar") || output.contains(r#"default="bar""#));
2682    }
2683
2684    #[test]
2685    fn test_flag_default_serialization_multiple() {
2686        let spec = Spec::parse(
2687            &Default::default(),
2688            r#"
2689flag "--foo <foo>" var=#true {
2690    default {
2691        "xyz"
2692        "bar"
2693    }
2694}
2695            "#,
2696        )
2697        .unwrap();
2698
2699        // When serialized, multiple defaults should use child node format
2700        let output = spec.to_string();
2701        // The output should contain a default block with children
2702        assert!(output.contains("default {"));
2703    }
2704}