Skip to main content

usage/spec/
flag.rs

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