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