Skip to main content

usage/spec/
flag.rs

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