Skip to main content

usage/spec/
flag.rs

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