Skip to main content

usage/
parse.rs

1use crate::miette::{self, bail};
2use indexmap::IndexMap;
3use itertools::Itertools;
4use log::trace;
5use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
6use std::fmt::{Debug, Display, Formatter};
7use std::sync::Arc;
8
9#[cfg(feature = "cli-help")]
10use crate::docs;
11use crate::error::UsageErr;
12use crate::spec::arg::SpecDoubleDashChoices;
13use crate::spec::unknown_flags::UnknownFlags;
14use crate::warn::Warning;
15use crate::{Spec, SpecArg, SpecChoices, SpecCommand, SpecFlag};
16
17/// Merge a subcommand's flags into the currently available flags when descending
18/// into that subcommand.
19///
20/// On descent we drop the parent's non-global flags (they are scoped to the parent)
21/// but keep its global flags so they remain recognized further down. A subcommand may
22/// re-declare a flag that the parent exposed as global (e.g. `-C/--cd`) but mark its own
23/// copy as non-global. In that case we must NOT let the non-global re-declaration shadow
24/// the inherited global flag, otherwise the next descent's `retain(global)` would drop it
25/// entirely and later parsing would treat the already-consumed global token as an
26/// unexpected positional/flag value.
27///
28/// Descending into a *mounted* subcommand (`crossing_mount`) is different: the mounted
29/// command describes another program, which does not accept the mounting CLI's globals.
30/// Those globals stay recognized (they may appear before the mounted command, and Phase 2
31/// re-parses them), but the mounted command's own flags take precedence over them, so its
32/// choices/completions are not replaced by a global's. Which flags a completion may offer
33/// there is a separate question, answered by [`ParseOutput::completion_flags`].
34fn merge_subcommand_flags(
35    available: &mut BTreeMap<String, Arc<SpecFlag>>,
36    new_flags: BTreeMap<String, Arc<SpecFlag>>,
37    crossing_mount: bool,
38) {
39    // Keep only inherited global flags from the parent.
40    available.retain(|_, f| f.global);
41
42    if crossing_mount {
43        // A mounted command owns its flags outright, including names an inherited global also
44        // uses: a word after the mounted command belongs to the mounted program. Words before
45        // it keep resolving to the global they were read as, via `Token::binding`. Aliases the
46        // mounted command does not declare (e.g. a global's short) stay inherited.
47        for (key, flag) in new_flags {
48            available.insert(key, flag);
49        }
50        return;
51    }
52
53    // Cache the merged (global ∪ orphan-alias) flag per re-declared child so every alias key of
54    // that flag ends up sharing one `Arc`. Keyed by the child `Arc`'s identity.
55    let mut merged_cache: HashMap<usize, Arc<SpecFlag>> = HashMap::new();
56    // Maps each merged flag produced below back to the inherited global it was merged from, so
57    // the collision check can compare *origins*: a flag this loop already merged is not a
58    // different global, even though it is a different `Arc`.
59    let mut merged_origin: HashMap<usize, usize> = HashMap::new();
60    // The inherited global a flag stands for: itself, or — for a merged flag — its source global.
61    fn origin_of(merged_origin: &HashMap<usize, usize>, flag: &Arc<SpecFlag>) -> usize {
62        let ptr = Arc::as_ptr(flag) as usize;
63        *merged_origin.get(&ptr).unwrap_or(&ptr)
64    }
65
66    // Iterate the *flattened* child map directly (one entry per alias key). This preserves the
67    // map's existing intra-subcommand collision resolution: when two flags in the same command
68    // share an alias (e.g. `-x --alpha` then `-x --beta`), the BTreeMap already collapsed `-x`
69    // to its last-declared owner, and we must not change which flag owns it.
70    for (key, flag) in new_flags {
71        if flag.global {
72            // A child that re-declares (or adds) a global flag stays recognized everywhere.
73            available.insert(key, flag);
74            continue;
75        }
76
77        // A non-global re-declaration that shares a LONG name with an inherited global flag is
78        // the SAME logical flag (e.g. mise's `-r --raw` re-declaring the long-only `--raw`
79        // global). Keep the global flag (global precedence, so it survives the next descent's
80        // `retain`), but union in any short/long aliases that exist only on the re-declaration,
81        // otherwise those orphan aliases would be silently dropped. Matching on a shared long is
82        // deliberate: a re-declaration sharing only a short letter with an unrelated global
83        // (`-q --quiet` vs `-q --quoting`) is a genuine collision, not an alias addition, and is
84        // handled by the `contains_key` skip below instead.
85        let inherited_global = flag.long.iter().find_map(|l| {
86            available
87                .get(&format!("--{l}"))
88                .filter(|f| f.global)
89                .cloned()
90        });
91        if let Some(global_flag) = inherited_global {
92            // Never clobber a *different* inherited global's alias. If this re-declaration's
93            // orphan alias (e.g. `-r`) is already owned by some other global (e.g. an unrelated
94            // `-r --restrict`), that is a genuine collision: keep the existing global, as global
95            // precedence dictates, instead of stealing the alias for the merged flag.
96            //
97            // Compare origins, not `Arc`s: when the global has several aliases of its own, an
98            // earlier key of this same child already replaced some of them with the merged flag,
99            // which the lookups above may now resolve to. That is the same logical flag, so it
100            // must not read as a collision and leave this key on the pre-merge global.
101            let global_origin = origin_of(&merged_origin, &global_flag);
102            if available.get(&key).is_some_and(|existing| {
103                existing.global && origin_of(&merged_origin, existing) != global_origin
104            }) {
105                continue;
106            }
107            let merged = match merged_cache.get(&(Arc::as_ptr(&flag) as usize)) {
108                Some(merged) => merged.clone(),
109                None => {
110                    let mut merged = (*global_flag).clone();
111                    // `exclusive` is deliberately *not* reconciled here, in either direction.
112                    // One object now answers to two alias sets that may disagree: the child
113                    // owns the spellings it declared, the ancestor keeps the ones only it
114                    // declared. A single bool cannot hold both, so the merged flag carries the
115                    // ancestor's and validation resolves the occurrence by the spelling that
116                    // was typed — the ledger it already consults to decide whether selecting
117                    // the child is company.
118                    for s in &flag.short {
119                        if !merged.short.contains(s) {
120                            merged.short.push(*s);
121                        }
122                    }
123                    for l in &flag.long {
124                        if !merged.long.contains(l) {
125                            merged.long.push(l.clone());
126                        }
127                    }
128                    // A child may deliberately promote one of the ancestor's hidden aliases.
129                    // Hidden lists are subsets of the accepted spellings, so a spelling present
130                    // on the child but absent from its hidden subset is visible at this level.
131                    merged.hidden_short_aliases.retain(|alias| {
132                        !flag.short.contains(alias) || flag.hidden_short_aliases.contains(alias)
133                    });
134                    merged.hidden_aliases.retain(|alias| {
135                        !flag.long.contains(alias) || flag.hidden_aliases.contains(alias)
136                    });
137                    for s in &flag.hidden_short_aliases {
138                        if !merged.hidden_short_aliases.contains(s) {
139                            merged.hidden_short_aliases.push(*s);
140                        }
141                    }
142                    for l in &flag.hidden_aliases {
143                        if !merged.hidden_aliases.contains(l) {
144                            merged.hidden_aliases.push(l.clone());
145                        }
146                    }
147                    let merged = Arc::new(merged);
148                    merged_cache.insert(Arc::as_ptr(&flag) as usize, Arc::clone(&merged));
149                    merged_origin.insert(Arc::as_ptr(&merged) as usize, global_origin);
150                    // Rebind the global's *other* aliases onto the merged flag. The loop only
151                    // visits keys the child declared, so an alias the child left out (the `-y` of
152                    // a `-y --yes` global re-declared as just `--yes`) would otherwise keep
153                    // pointing at the pre-merge flag and miss the aliases just unioned in. One
154                    // logical flag must be one object under every key it answers to.
155                    for existing in available.values_mut() {
156                        if origin_of(&merged_origin, existing) == global_origin {
157                            *existing = Arc::clone(&merged);
158                        }
159                    }
160                    merged
161                }
162            };
163            available.insert(key, merged);
164            continue;
165        }
166
167        // Purely-local flag (shares nothing with an inherited global), or one that collides only
168        // on a short with an unrelated global. Insert this alias but never shadow an inherited
169        // global flag. Such non-global flags are dropped by the next descent's `retain`.
170        if available.contains_key(&key) {
171            continue;
172        }
173        available.insert(key, flag);
174    }
175}
176
177/// Build the lookup keys a flag is registered under in `available_flags`:
178/// `--<long>` for each long name, `-<short>` for each short char, plus the `negate` token.
179fn flag_keys(flag: &SpecFlag) -> Vec<String> {
180    let mut keys: Vec<String> = flag
181        .long
182        .iter()
183        .map(|l| format!("--{l}"))
184        .chain(flag.short.iter().map(|s| format!("-{s}")))
185        .collect();
186    if let Some(negate) = &flag.negate {
187        keys.push(negate.clone());
188    }
189    keys
190}
191
192/// The flags a command declares, keyed by each of their aliases.
193fn gather_flags(cmd: &SpecCommand) -> BTreeMap<String, Arc<SpecFlag>> {
194    cmd.flags
195        .iter()
196        .flat_map(|f| {
197            let f = Arc::new(f.clone()); // One clone per flag, then cheap Arc refs
198            flag_keys(&f)
199                .into_iter()
200                .map(|key| (key, Arc::clone(&f)))
201                .collect::<Vec<_>>()
202        })
203        .collect()
204}
205
206fn unique_flags<'a>(
207    flags: impl IntoIterator<Item = &'a Arc<SpecFlag>>,
208) -> impl Iterator<Item = &'a Arc<SpecFlag>> {
209    let mut seen = HashSet::new();
210    flags
211        .into_iter()
212        .filter(move |flag| seen.insert(Arc::as_ptr(flag) as usize))
213}
214
215/// Every flag a command accepts, resolved the way parsing an invocation of it
216/// resolves them.
217///
218/// `chain` runs from the root command (`spec.cmd`) down to the command in
219/// question; an empty chain yields no flags.
220///
221/// This is not "the command's flags plus its ancestors' globals". A subcommand
222/// that re-declares a global's long name is describing the *same* flag rather
223/// than a new one, so the global's help, argument and effect survive and only
224/// the re-declaration's extra aliases are added — see
225/// [`merge_subcommand_flags`]. Anything that reports a command's flags without
226/// going through this will disagree with what the parser actually accepts.
227pub fn available_flags(chain: &[&SpecCommand]) -> Vec<Arc<SpecFlag>> {
228    let Some((root, rest)) = chain.split_first() else {
229        return vec![];
230    };
231    let mut available = gather_flags(root);
232    for cmd in rest {
233        merge_subcommand_flags(&mut available, gather_flags(cmd), false);
234    }
235
236    // Deduplicating by `Arc` identity is not enough. When a child re-declares a
237    // global that has both a short and a long, the merged flag is written under
238    // the long key while the short key keeps pointing at the pre-merge `Arc` —
239    // two objects for one logical flag. That is harmless for parsing, which
240    // looks flags up by key, but a caller listing flags would see it twice.
241    //
242    // Names break the tie because a long key always sorts before a short one
243    // (`--x` < `-y` at the second byte), so the merged declaration is the one
244    // reached first. Two genuinely distinct flags sharing a name is a spec bug
245    // that `usage lint` reports as a duplicate flag.
246    let mut seen_names = HashSet::new();
247    unique_flags(available.values())
248        .filter(|f| seen_names.insert(f.name.clone()))
249        .cloned()
250        .collect()
251}
252
253/// Extract the flag key from a flag word for lookup in available_flags map
254/// Handles both long flags (--flag, --flag=value) and short flags (-f)
255fn get_flag_key(word: &str) -> &str {
256    if word.starts_with("--") {
257        // Long flag: strip =value if present
258        word.split_once('=').map(|(k, _)| k).unwrap_or(word)
259    } else if let Some((end, _)) = word.char_indices().nth(2) {
260        // Short flag: the dash and one letter, which is one character and not
261        // necessarily one byte.
262        &word[0..end]
263    } else {
264        word
265    }
266}
267
268/// Where a value came from, when it did not come from the command line.
269///
270/// About the *value*, not the flag. `--color` typed bare with `default_missing` has a
271/// token for the flag and none for the value, and that distinction is the whole question
272/// a spec author is asking when they ask why `--color` came out `always`. Values that were
273/// typed are attributed to the token that carried them instead — see [`TokenRole::Value`].
274#[derive(Debug, Clone, PartialEq, Eq)]
275#[non_exhaustive]
276pub enum ValueOrigin {
277    /// A flag that takes a value was given without one, so the declaration supplied it:
278    /// `default_missing`, or the empty tri-state a bare `value_optional` flag records.
279    /// One variant for both, because from argv's side the same thing happened — the flag
280    /// was typed and the value was not.
281    DefaultMissing,
282    /// An environment variable, named.
283    ///
284    /// Named because a flag may list several — `env`, `env_fallback` and `deprecated_env`,
285    /// folded together by [`SpecFlag::env_names`] — and "it came from the environment" does
286    /// not say which declaration fired or which one to delete.
287    Env(String),
288    /// A declared `default`, on the flag or on the flag's argument.
289    ///
290    /// Not two variants: the precedence between them is a spec-authoring oddity rather than
291    /// a fact about the value, and `usage lint` is the place to complain about declaring
292    /// both.
293    Default,
294    /// A `default_if` whose condition matched, with the condition that decided it. The
295    /// selector alone is ambiguous — several conditions may name it with different `when`
296    /// values.
297    DefaultIf {
298        selector: String,
299        when: Option<String>,
300    },
301}
302
303/// What one word of the command line became.
304///
305/// Several because a single token can do more than one thing: `-abc` sets three flags,
306/// `-j8` is a flag and its value.
307#[derive(Debug, Clone)]
308#[non_exhaustive]
309pub enum TokenRole {
310    /// argv[0]. Also a `Command` when a multicall symlink makes the basename a word.
311    Program,
312    /// Selected a subcommand.
313    Command { name: String },
314    /// Named a flag, in this spelling. `negated` for the `negate` form.
315    Flag {
316        flag: Arc<SpecFlag>,
317        spelling: String,
318        negated: bool,
319    },
320    /// Supplied a flag's value. Several values when a `delimiter` split the word.
321    Value {
322        flag: Arc<SpecFlag>,
323        values: Vec<String>,
324        /// Whether the value rode along on the flag's own token (`--env=prod`, `-j8`)
325        /// rather than following it as its own word.
326        attached: bool,
327    },
328    /// Filled a positional argument. Several values when a `delimiter` split the word.
329    Arg {
330        arg: Arc<SpecArg>,
331        values: Vec<String>,
332    },
333    /// An explicit `--`, consumed as a separator.
334    Separator,
335    /// A word the parser answers itself rather than binding: `--help`, `-h`, `--version`,
336    /// `-V`. The parse stops here and the answer travels as an error carrying the text, so
337    /// without a role the word reads as having done nothing while a whole help page arrives
338    /// in the error list.
339    Builtin { spelling: String },
340    /// A declared `value_terminator`, consumed to end a run of values. `ends` names the
341    /// declaration whose run it closed — the word is not one of that run's values, which is
342    /// the whole reason it was declared.
343    ValueTerminator { ends: String },
344    /// A declared `restart_token`: the positional cursor and the values it had filled start
345    /// over here. Recorded because the words before it are still in the report, and without
346    /// this row they look like they filled arguments that then came back empty.
347    Restart,
348    /// A flag-like word no declaration matched. `bound_as` is the positional that took it
349    /// under `unknown_flags="value"`, and `None` when the word was refused.
350    UnknownFlag { bound_as: Option<Arc<SpecArg>> },
351    /// The word reached a declaration that would not take it, and was dropped. Without this
352    /// the token reads as having done nothing, which is the one thing it did not do.
353    Refused { reason: String },
354    /// Forwarded to an external subcommand.
355    External,
356    /// The parser stopped before this word — a help request, a refused value.
357    Unread,
358}
359
360/// One word of the command line, and what it became.
361#[derive(Debug, Clone)]
362#[non_exhaustive]
363pub struct TokenBinding {
364    /// Position in the argv slice the parse was given, argv[0] included.
365    pub index: usize,
366    pub word: String,
367    /// Roles a word the parser made up contributed, folded onto the token it was derived
368    /// from: the tail of a short bundle onto the bundle, a multicall applet name onto
369    /// argv[0]. `word` is what the caller wrote, not what the parser read.
370    pub synthesized: bool,
371    pub roles: Vec<TokenRole>,
372}
373
374#[non_exhaustive]
375pub struct ParseOutput {
376    pub cmd: SpecCommand,
377    pub cmds: Vec<SpecCommand>,
378    pub args: IndexMap<Arc<SpecArg>, ParseValue>,
379    pub flags: IndexMap<Arc<SpecFlag>, ParseValue>,
380    /// What each word of the command line became, in argv order, one entry per word.
381    ///
382    /// The token half of provenance; [`ParseOutput::flag_origins`] and
383    /// [`ParseOutput::arg_origins`] are the other half. A table keyed by token cannot show
384    /// a value that came from nowhere in argv, and a table keyed by declaration cannot show
385    /// a token that bound to nothing, so both exist.
386    pub tokens: Vec<TokenBinding>,
387    /// Where a flag's value came from when it did not come from argv, in the order the
388    /// fallbacks fired. Keyed as [`ParseOutput::flags`] is.
389    ///
390    /// A list rather than one origin: repeated bare occurrences of a `var` flag each take a
391    /// `default_missing` value, so one flag can have several.
392    pub flag_origins: IndexMap<Arc<SpecFlag>, Vec<ValueOrigin>>,
393    /// Where an argument's value came from when it did not come from argv. Keyed as
394    /// [`ParseOutput::args`] is.
395    pub arg_origins: IndexMap<Arc<SpecArg>, Vec<ValueOrigin>>,
396    /// Flags a later occurrence removed, and the flag that removed them.
397    ///
398    /// The overriding name is the half a caller needs: the fallback phase silently declines
399    /// to fill an overridden flag, so "why is `--quiet` unset when its default says
400    /// otherwise" has no answer without it.
401    pub overridden_flags: BTreeMap<String, String>,
402    /// Every flag the parser recognizes at this point, keyed by each of its aliases
403    /// (`--long`, `-s`, negations).
404    ///
405    /// This includes flags that only remain recognized because they may appear *before* a
406    /// mounted command — see [`ParseOutput::completion_flags`] for the set a completion
407    /// should offer.
408    pub available_flags: BTreeMap<String, Arc<SpecFlag>>,
409    pub flag_awaiting_value: Vec<Arc<SpecFlag>>,
410    pub errors: Vec<UsageErr>,
411    /// Deprecated declarations this command line used, for the caller to render when its
412    /// logging is up. Empty from [`parse_partial`]: a half-typed line being completed has
413    /// not used anything yet.
414    pub warnings: Vec<Warning>,
415    /// The positional argument the next word would have filled, i.e. where the parser's
416    /// cursor stopped. `None` once every argument is satisfied.
417    ///
418    /// Completions need exactly this: the parser already accounts for `var_max`, for
419    /// `restart_token` rewinds, and for the jump an explicit `--` performs onto a
420    /// `double_dash="required"` argument, so re-deriving it from `args` would disagree.
421    pub next_arg: Option<Arc<SpecArg>>,
422    /// Whether an explicit `--` was consumed *as a separator*.
423    ///
424    /// A `--` that `double_dash="preserve"` keeps as a value does not count: it is a value
425    /// of the variadic argument collecting it, not a separator, so it does not unlock a
426    /// `double_dash="required"` argument.
427    pub double_dash_seen: bool,
428    /// Remaining argv captured when an unmatched word was forwarded as an external
429    /// subcommand: the command name first, then every token after it.
430    ///
431    /// Absent when no external command was selected. See [`SpecCommand::external_subcommand`].
432    pub external: Option<Vec<String>>,
433}
434
435impl ParseOutput {
436    /// The flags a completion should offer for the parsed command.
437    ///
438    /// Usually every recognized flag, i.e. [`ParseOutput::available_flags`]. Once a mounted
439    /// command has been reached, though, the commands above it belong to the mounting CLI and
440    /// their flags are not accepted there — mise, for example, forwards everything after a task
441    /// name to the task itself — so only the flags declared from the mount boundary down are
442    /// offered. Those globals stay in `available_flags` because they may legitimately appear
443    /// *before* the mounted command.
444    pub fn completion_flags(&self) -> BTreeMap<String, Arc<SpecFlag>> {
445        let Some(boundary) = self.cmds.iter().position(|cmd| cmd.mounted) else {
446            return self.available_flags.clone();
447        };
448        // A mount can also merge flags from its spec's root into the command it is mounted on
449        // (`SpecCommand::flags_from_mount`). Those describe the mounted program too, so the
450        // replay starts one level up to inherit its globals.
451        let start = match boundary.checked_sub(1) {
452            Some(prev) if self.cmds[prev].flags_from_mount => prev,
453            _ => boundary,
454        };
455        // Re-run the descent from there, which starts with no inherited flags. Below the
456        // boundary the mounted program's commands are ordinary commands, so the descents use
457        // the same merge as the real parse.
458        let mut offered = gather_flags(&self.cmds[start]);
459        for cmd in &self.cmds[start + 1..] {
460            merge_subcommand_flags(&mut offered, gather_flags(cmd), false);
461        }
462        offered
463    }
464}
465
466#[derive(Debug, Clone)]
467pub enum ParseValue {
468    Bool(bool),
469    String(String),
470    MultiBool(Vec<bool>),
471    MultiString(Vec<String>),
472}
473
474impl ParseValue {
475    pub fn try_as_bool(self) -> Option<bool> {
476        match self {
477            Self::Bool(value) => Some(value),
478            _ => None,
479        }
480    }
481
482    pub const fn try_as_bool_ref(&self) -> Option<&bool> {
483        match self {
484            Self::Bool(value) => Some(value),
485            _ => None,
486        }
487    }
488
489    pub fn try_as_bool_mut(&mut self) -> Option<&mut bool> {
490        match self {
491            Self::Bool(value) => Some(value),
492            _ => None,
493        }
494    }
495
496    pub fn try_as_string(self) -> Option<String> {
497        match self {
498            Self::String(value) => Some(value),
499            _ => None,
500        }
501    }
502
503    pub const fn try_as_string_ref(&self) -> Option<&String> {
504        match self {
505            Self::String(value) => Some(value),
506            _ => None,
507        }
508    }
509
510    pub fn try_as_string_mut(&mut self) -> Option<&mut String> {
511        match self {
512            Self::String(value) => Some(value),
513            _ => None,
514        }
515    }
516
517    pub fn try_as_multi_bool(self) -> Option<Vec<bool>> {
518        match self {
519            Self::MultiBool(value) => Some(value),
520            _ => None,
521        }
522    }
523
524    pub const fn try_as_multi_bool_ref(&self) -> Option<&Vec<bool>> {
525        match self {
526            Self::MultiBool(value) => Some(value),
527            _ => None,
528        }
529    }
530
531    pub fn try_as_multi_bool_mut(&mut self) -> Option<&mut Vec<bool>> {
532        match self {
533            Self::MultiBool(value) => Some(value),
534            _ => None,
535        }
536    }
537
538    pub fn try_as_multi_string(self) -> Option<Vec<String>> {
539        match self {
540            Self::MultiString(value) => Some(value),
541            _ => None,
542        }
543    }
544
545    pub const fn try_as_multi_string_ref(&self) -> Option<&Vec<String>> {
546        match self {
547            Self::MultiString(value) => Some(value),
548            _ => None,
549        }
550    }
551
552    pub fn try_as_multi_string_mut(&mut self) -> Option<&mut Vec<String>> {
553        match self {
554            Self::MultiString(value) => Some(value),
555            _ => None,
556        }
557    }
558}
559
560/// The deprecated declarations argv itself named: the commands it descended through, and the
561/// flags it bound.
562///
563/// Called before the environment and defaults have filled anything, because afterwards nothing
564/// distinguishes a flag the user typed from one a variable supplied — and the two are reported
565/// differently, at the point where each is applied.
566///
567/// The root is skipped. A `deprecated` root would otherwise warn on every invocation of the CLI,
568/// including `--help`, and the compiled parser reports selected commands rather than the one the
569/// process already is.
570fn collect_deprecations(out: &mut ParseOutput) {
571    for cmd in out.cmds.iter().skip(1) {
572        if cmd.deprecated.is_none()
573            && cmd.deprecated_warn_at.is_none()
574            && cmd.deprecated_remove_at.is_none()
575        {
576            continue;
577        }
578        out.warnings.push(Warning::command(
579            cmd.name.clone(),
580            cmd.deprecated.clone(),
581            cmd.deprecated_warn_at.clone(),
582            cmd.deprecated_remove_at.clone(),
583        ));
584    }
585    for flag in out.flags.keys() {
586        if let Some(warning) = flag_deprecation(flag) {
587            out.warnings.push(warning);
588        }
589    }
590}
591
592/// A warning for a flag that was used, if its declaration is deprecated at all.
593fn flag_deprecation(flag: &SpecFlag) -> Option<Warning> {
594    if flag.deprecated.is_none()
595        && flag.deprecated_warn_at.is_none()
596        && flag.deprecated_remove_at.is_none()
597    {
598        return None;
599    }
600    Some(Warning::flag(
601        flag_spelling(flag),
602        flag.deprecated.clone(),
603        flag.deprecated_warn_at.clone(),
604        flag.deprecated_remove_at.clone(),
605    ))
606}
607
608/// A flag named the way the user names it. The spec's name for it has no dashes, and a warning
609/// about `old-flag` would be about a word nobody typed.
610fn flag_spelling(flag: &SpecFlag) -> String {
611    flag.long
612        .first()
613        .map(|long| format!("--{long}"))
614        .or_else(|| flag.short.first().map(|short| format!("-{short}")))
615        .unwrap_or_else(|| flag.name.clone())
616}
617
618/// The name this flag reads first, which is what to use instead of a deprecated alias.
619fn flag_current_env(flag: &SpecFlag) -> Option<String> {
620    flag.env
621        .clone()
622        .or_else(|| flag.env_fallback.first().cloned())
623}
624
625fn flag_env_is_deprecated(flag: &SpecFlag, name: &str) -> bool {
626    flag.deprecated_env.iter().any(|declared| declared == name)
627}
628
629/// The same two questions for a positional, which has aliases but no `deprecated` of its own.
630fn arg_current_env(arg: &SpecArg) -> Option<String> {
631    arg.env
632        .clone()
633        .or_else(|| arg.env_fallback.first().cloned())
634}
635
636fn arg_env_is_deprecated(arg: &SpecArg, name: &str) -> bool {
637    arg.deprecated_env.iter().any(|declared| declared == name)
638}
639
640/// The first of `names` that is set, and which one it was.
641///
642/// `env_names()` yields the current name, then the declared fallbacks, then the deprecated
643/// aliases, so the winner's identity is what says whether a value arrived through an alias.
644/// Deciding that a second time, from the outside, would be a copy of this precedence rule free to
645/// disagree with it.
646fn first_set_env<'a>(
647    mut names: impl Iterator<Item = &'a str>,
648    get_env: &impl Fn(&str) -> Option<String>,
649) -> Option<(&'a str, String)> {
650    names.find_map(|name| get_env(name).map(|value| (name, value)))
651}
652
653/// Builder for parsing command-line arguments with custom options.
654///
655/// Use this when you need to customize parsing behavior, such as providing
656/// a custom environment variable map instead of using the process environment.
657///
658/// # Example
659/// ```
660/// use std::collections::HashMap;
661/// use usage::Spec;
662/// use usage::parse::Parser;
663///
664/// let spec: Spec = r#"flag "--name <name>" env="NAME""#.parse().unwrap();
665/// let env: HashMap<String, String> = [("NAME".into(), "john".into())].into();
666///
667/// let result = Parser::new(&spec)
668///     .with_env(env)
669///     .parse(&["cmd".into()])
670///     .unwrap();
671/// ```
672#[non_exhaustive]
673pub struct Parser<'a> {
674    spec: &'a Spec,
675    env: Option<HashMap<String, String>>,
676    mount_outputs: Option<HashMap<String, String>>,
677}
678
679impl<'a> Parser<'a> {
680    /// Create a new parser for the given spec.
681    pub fn new(spec: &'a Spec) -> Self {
682        Self {
683            spec,
684            env: None,
685            mount_outputs: None,
686        }
687    }
688
689    /// Use a custom environment variable map instead of the process environment.
690    ///
691    /// This is useful when parsing for tasks in a monorepo where the env vars
692    /// come from a child config file rather than the current process environment.
693    pub fn with_env(mut self, env: HashMap<String, String>) -> Self {
694        self.env = Some(env);
695        self
696    }
697
698    /// Inject deterministic outputs for mount commands instead of executing them.
699    ///
700    /// Keys are the exact `run` strings declared by mount nodes and values are the
701    /// usage specs those commands would print. When this is set, every encountered
702    /// mount must have an entry. Production parsing remains process-backed unless a
703    /// caller explicitly opts into injection.
704    pub fn with_mount_outputs(mut self, outputs: HashMap<String, String>) -> Self {
705        self.mount_outputs = Some(outputs);
706        self
707    }
708
709    /// Parse the input arguments.
710    ///
711    /// Returns the parsed arguments and flags, with defaults and env vars applied.
712    pub fn parse(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
713        let out = self.parse_collecting(input)?;
714        if let Some(err) = out
715            .errors
716            .iter()
717            .find(|e| matches!(e, UsageErr::Help(_) | UsageErr::Version(_)))
718        {
719            bail!("{err}");
720        }
721        if !out.errors.is_empty() {
722            bail!("{}", out.errors.iter().map(|e| e.to_string()).join("\n"));
723        }
724        Ok(out)
725    }
726
727    /// Everything the parse learned, whether or not it succeeded.
728    ///
729    /// [`Parser::parse`] wants the first error and nothing else, which is right for a
730    /// caller about to act on a command line. A caller that wants to *explain* one wants
731    /// the opposite: the bindings that worked and every complaint about the rest, since a
732    /// report saying only "missing required <src>" is the report you already had.
733    ///
734    /// Failures that stop the parse dead — a mount that will not run, a word no
735    /// declaration can take — still come back as `Err`. There is no output to describe in
736    /// those cases; see [`Parser::explain`] for what to do about it.
737    pub fn explain(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
738        self.parse_collecting(input)
739    }
740
741    /// The binding phase's own answer for a line [`Parser::explain`] refused.
742    ///
743    /// `Ok` when the binding phase finished and the failure came after it — a flag left
744    /// waiting for a value, say. Everything argv supplied is there and only the
745    /// environment-and-defaults phase is missing.
746    ///
747    /// `Err` when the binding phase is where it died, leaving the tokens it had attributed
748    /// by then. Those words are most of what a report is for: "no declaration takes `bogus`"
749    /// is more useful next to the three tokens that did bind than on its own. The word that
750    /// caused the failure carries a role saying so, and everything still queued behind it is
751    /// [`TokenRole::Unread`], for the two failures a command line reaches on its own — a
752    /// word nothing declares, and a flag a strict spec refuses. A failure in the spec rather
753    /// than in the line, such as a mount that will not run, stops the trace where it stopped
754    /// and the words past it carry no role.
755    pub fn explain_refused(self, input: &[String]) -> Result<ParseOutput, Vec<TokenBinding>> {
756        let mut trace = Trace::new(input);
757        match parse_partial_traced(
758            self.spec,
759            input,
760            self.env.as_ref(),
761            self.mount_outputs.as_ref(),
762            MountTiming::WhenAWordIsUnknown,
763            &mut trace,
764        ) {
765            // A parse that got as far as stopping normally already moved its tokens onto the
766            // output, which is where a caller should read them from.
767            Ok((out, _)) => Ok(out),
768            Err(_) => Err(trace.tokens),
769        }
770    }
771
772    fn parse_collecting(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
773        let custom_env = self.env.as_ref();
774        let (mut out, overridden_flags) = parse_partial_with_env(
775            self.spec,
776            input,
777            custom_env,
778            self.mount_outputs.as_ref(),
779            MountTiming::WhenAWordIsUnknown,
780        )?;
781        trace!("{out:?}");
782
783        // A flag still waiting for a value never got one, so the command line ended
784        // mid-flag. `parse_partial` leaves this for completions to look at — a
785        // half-typed `--jobs ` is exactly what a completion is asked about — but a
786        // full parse has nothing left to wait for, and dropping the flag silently
787        // made a forgotten value look like a working command.
788        while try_bind_default_missing(
789            &mut out.flags,
790            &mut out.flag_awaiting_value,
791            custom_env,
792            &mut out.flag_origins,
793        )? {}
794        if let Some(flag) = out.flag_awaiting_value.first() {
795            let token = flag
796                .long
797                .first()
798                .map(|l| format!("--{l}"))
799                .or_else(|| flag.short.first().map(|s| format!("-{s}")))
800                .unwrap_or_else(|| flag.name.clone());
801            let rendered = input.join(" ");
802            let span = rendered
803                .rfind(&token)
804                .map(|at| (at, token.len()))
805                .unwrap_or((0, 0));
806            return Err(UsageErr::InvalidFlag {
807                token,
808                reason: "requires an argument".to_string(),
809                span: span.into(),
810                input: rendered,
811            }
812            .into());
813        }
814
815        // Before the environment and defaults have their turn, because both mark a field as
816        // filled and only argv can be reported as something the user typed. Env is reported
817        // where it is applied, below; a default is nobody's request and reports nothing.
818        collect_deprecations(&mut out);
819
820        let get_env = |key: &str| -> Option<String> {
821            if let Some(env_map) = custom_env {
822                env_map.get(key).cloned()
823            } else {
824                std::env::var(key).ok()
825            }
826        };
827
828        // Apply env vars and defaults for args
829        //
830        // Not `skip(out.args.len())`: an explicit `--` can jump the parser's cursor past an arg
831        // that stayed empty, leaving a gap that makes the fill count a wrong starting offset.
832        for arg in out.cmd.args.iter() {
833            if out.args.contains_key(arg) {
834                continue;
835            }
836            if let Some((env_name, env_value)) = first_set_env(arg.env_names(), &get_env) {
837                if arg_env_is_deprecated(arg, env_name) {
838                    out.warnings
839                        .push(Warning::env(env_name, arg_current_env(arg)));
840                }
841                let values = split_fallback_values(std::slice::from_ref(&env_value), arg.delimiter);
842                validate_choice_values(
843                    ChoiceTarget::arg(arg),
844                    &values,
845                    arg.choices.as_ref(),
846                    custom_env,
847                )?;
848                let parsed = if arg.var {
849                    validate_arg_fallback_count(arg, values.len(), &mut out.errors);
850                    ParseValue::MultiString(values)
851                } else {
852                    ParseValue::String(values.into_iter().next().unwrap_or_default())
853                };
854                out.args.insert(Arc::new(arg.clone()), parsed);
855                out.arg_origins
856                    .entry(Arc::new(arg.clone()))
857                    .or_default()
858                    .push(ValueOrigin::Env(env_name.to_string()));
859                continue;
860            }
861            if !arg.default.is_empty() {
862                // Consider var when deciding the type of default return value
863                if arg.var {
864                    let values = split_fallback_values(&arg.default, arg.delimiter);
865                    validate_arg_fallback_count(arg, values.len(), &mut out.errors);
866                    validate_choice_values(
867                        ChoiceTarget::arg(arg),
868                        &values,
869                        arg.choices.as_ref(),
870                        custom_env,
871                    )?;
872                    // For var=true, always return a vec (MultiString)
873                    out.args
874                        .insert(Arc::new(arg.clone()), ParseValue::MultiString(values));
875                    out.arg_origins
876                        .entry(Arc::new(arg.clone()))
877                        .or_default()
878                        .push(ValueOrigin::Default);
879                } else {
880                    validate_choice_value(
881                        ChoiceTarget::arg(arg),
882                        &arg.default[0],
883                        arg.choices.as_ref(),
884                        custom_env,
885                    )?;
886                    // For var=false, return the first default value as String
887                    out.args.insert(
888                        Arc::new(arg.clone()),
889                        ParseValue::String(arg.default[0].clone()),
890                    );
891                    out.arg_origins
892                        .entry(Arc::new(arg.clone()))
893                        .or_default()
894                        .push(ValueOrigin::Default);
895                }
896            }
897        }
898
899        // Environment first, for every flag, so a `default_if` can see a sibling
900        // that was filled from env. Applying both in one pass would make the
901        // answer depend on declaration order: `--bin-names` before `--json`
902        // would miss `EX_JSON=1`.
903        let flags: Vec<Arc<SpecFlag>> = out.available_flags.values().cloned().collect();
904        for flag in &flags {
905            if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
906                continue;
907            }
908            if let Some((env_name, env_value)) = first_set_env(flag.env_names(), &get_env) {
909                // The flag's own deprecation before the alias's, which is the order the
910                // compiled parser reports them in: it walks a command's flags and then its
911                // aliases. Using a deprecated flag through a variable is still using it.
912                if let Some(warning) = flag_deprecation(flag) {
913                    out.warnings.push(warning);
914                }
915                if flag_env_is_deprecated(flag, env_name) {
916                    out.warnings
917                        .push(Warning::env(env_name, flag_current_env(flag)));
918                }
919                if let Some(arg) = flag.arg.as_ref() {
920                    let values =
921                        split_fallback_values(std::slice::from_ref(&env_value), arg.delimiter);
922                    validate_choice_values(
923                        ChoiceTarget::option(flag),
924                        &values,
925                        arg.choices.as_ref(),
926                        custom_env,
927                    )?;
928                    let parsed = if flag.var || arg.var {
929                        if flag.var {
930                            validate_flag_fallback_count(flag, values.len(), &mut out.errors);
931                        }
932                        if arg.var {
933                            validate_flag_arg_fallback_count(
934                                flag,
935                                arg,
936                                values.len(),
937                                &mut out.errors,
938                            );
939                        }
940                        ParseValue::MultiString(values)
941                    } else {
942                        ParseValue::String(values.into_iter().next().unwrap_or_default())
943                    };
944                    out.flags.insert(Arc::clone(flag), parsed);
945                } else {
946                    let is_true = matches!(env_value.as_str(), "1" | "true" | "True" | "TRUE");
947                    out.flags
948                        .insert(Arc::clone(flag), ParseValue::Bool(is_true));
949                }
950                out.flag_origins
951                    .entry(Arc::clone(flag))
952                    .or_default()
953                    .push(ValueOrigin::Env(env_name.to_string()));
954            }
955        }
956        // Decide every `default_if` against argv+env only. Binding as we go would put
957        // a default into `out.flags` and make the next flag's condition treat it as
958        // explicit — Go's `Given()` and the derive's `__given_*` both ignore defaults
959        // here, so an unconditional `default` on `--json` must not fire
960        // `default_if "--json"`.
961        let mut from_default_if: Vec<(Arc<SpecFlag>, crate::SpecDefaultIf)> = Vec::new();
962        for flag in &flags {
963            if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
964                continue;
965            }
966            if let Some(condition) = flag.default_if.iter().find(|condition| {
967                default_if_condition_matches(condition, &out, &overridden_flags, custom_env)
968            }) {
969                from_default_if.push((Arc::clone(flag), condition.clone()));
970            }
971        }
972        for (flag, condition) in &from_default_if {
973            // The whole condition, not just the value: several conditions may name the same
974            // selector with different `when` values, so the selector alone does not say
975            // which one fired.
976            bind_flag_fallback(
977                flag,
978                std::slice::from_ref(&condition.value),
979                &mut out,
980                custom_env,
981                ValueOrigin::DefaultIf {
982                    selector: condition.selector.clone(),
983                    when: condition.when.clone(),
984                },
985            )?;
986        }
987        for flag in &flags {
988            if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
989                continue;
990            }
991            if !flag.default.is_empty() {
992                bind_flag_fallback(
993                    flag,
994                    &flag.default,
995                    &mut out,
996                    custom_env,
997                    ValueOrigin::Default,
998                )?;
999                continue;
1000            }
1001            if let Some(arg) = flag.arg.as_ref() {
1002                if !arg.default.is_empty() {
1003                    bind_flag_fallback(
1004                        flag,
1005                        &arg.default,
1006                        &mut out,
1007                        custom_env,
1008                        ValueOrigin::Default,
1009                    )?;
1010                }
1011            }
1012        }
1013        // Declarative value validation is deliberately post-binding. Defaults and
1014        // environment fallbacks have landed by here, and delimiters were already split
1015        // while binding. Like clap's value parsers, a declaration judges each resulting
1016        // raw value independently.
1017        for (arg, parsed) in &out.args {
1018            validate_expression(
1019                &arg.name,
1020                arg.validate.as_deref(),
1021                arg.validate_error.as_deref(),
1022                parsed,
1023                &mut out.errors,
1024            );
1025        }
1026        for (flag, parsed) in &out.flags {
1027            if let Some(arg) = &flag.arg {
1028                validate_expression(
1029                    &flag.name,
1030                    arg.validate.as_deref(),
1031                    arg.validate_error.as_deref(),
1032                    parsed,
1033                    &mut out.errors,
1034                );
1035            }
1036        }
1037        // Applied once, here, because this is where the CLI's own version is known: a
1038        // `deprecated_warn_at` the spec has not reached yet is an author saying *not yet*.
1039        crate::warn::retain_reached(&mut out.warnings, self.spec.version.as_deref());
1040        Ok(out)
1041    }
1042}
1043
1044/// Parse command-line arguments according to a spec.
1045///
1046/// Returns the parsed arguments and flags, with defaults and env vars applied.
1047/// Uses `std::env::var` for environment variable lookups.
1048///
1049/// For custom environment variable handling, use [`Parser`] instead.
1050#[must_use = "parsing result should be used"]
1051pub fn parse(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
1052    Parser::new(spec).parse(input)
1053}
1054
1055/// Parse command-line arguments without applying defaults.
1056///
1057/// Use this for help text generation or when you need the raw parsed values.
1058#[must_use = "parsing result should be used"]
1059pub fn parse_partial(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
1060    parse_partial_with_env(spec, input, None, None, MountTiming::Eager).map(|(out, _)| out)
1061}
1062
1063/// Basename of argv[0] for a multicall CLI: last path component, with a trailing
1064/// `.exe` stripped so Windows and Unix agree.
1065pub fn multicall_basename(argv0: &str) -> &str {
1066    let name = argv0.rsplit(['/', '\\']).next().unwrap_or(argv0);
1067    match name.get(name.len().saturating_sub(4)..) {
1068        Some(ext) if ext.eq_ignore_ascii_case(".exe") => &name[..name.len() - 4],
1069        _ => name,
1070    }
1071}
1072
1073/// The applet name to parse as the first word, when argv[0] is not the dispatcher.
1074///
1075/// `None` means a dispatcher invocation (`busybox ls`): skip argv[0] and parse the
1076/// rest. `Some` is a symlink invocation (`ls -l`): inject the basename.
1077pub fn multicall_applet<'a>(argv0: &'a str, name: &str, bin: Option<&str>) -> Option<&'a str> {
1078    let base = multicall_basename(argv0);
1079    if !name.is_empty() && base == multicall_basename(name) {
1080        return None;
1081    }
1082    if let Some(bin) = bin {
1083        if !bin.is_empty() && base == multicall_basename(bin) {
1084            return None;
1085        }
1086    }
1087    Some(base)
1088}
1089
1090/// Internal version of parse_partial that accepts an optional custom env map.
1091/// When a command's own `mount` runs, for the root — which nothing descends into.
1092///
1093/// A completion has to know every command before it can offer one, even with
1094/// nothing typed yet, so it resolves up front. An execution knows the word it was
1095/// given, so it only pays for discovery when that word matches nothing declared —
1096/// and a CLI that declares its commands and mounts a few more does not spawn a
1097/// process on every invocation.
1098#[derive(Clone, Copy, PartialEq, Eq)]
1099enum MountTiming {
1100    Eager,
1101    WhenAWordIsUnknown,
1102}
1103
1104/// One word on its way through the parser, with what the parser has learned about it.
1105///
1106/// This holds what a side queue used to: the flag Phase 1 read a word as, previously a
1107/// `VecDeque` popped in step with the words. Two queues staying aligned is an invariant
1108/// nothing checks, and it was delicate enough to need explaining at three call sites; on
1109/// the word itself there is nothing to keep aligned. The argv position is here for the
1110/// same reason: the queue is popped, re-queued, split on `=`, and has subcommand words
1111/// removed from the middle, so position in the queue stops meaning position in argv on the
1112/// first descent.
1113struct Token {
1114    word: String,
1115    /// Where in the caller's argv this word came from.
1116    ///
1117    /// A word the parser made up points at the token it was derived from — the tail of a
1118    /// short bundle at the bundle, a multicall applet name at argv[0] — because that is the
1119    /// token a reader would point at, and there is nothing else to point at.
1120    argv: usize,
1121    /// The flag Phase 1 read this word as, and the command level it read it at.
1122    ///
1123    /// `Some((flag, command_level))` for a flag word, `None` for its value, for anything
1124    /// unresolved, and for every word Phase 1 never reached. The words stay in the queue
1125    /// for Phase 2 to re-parse — that is how they reach `out.flags` and `as_env()` — but by
1126    /// then the recognized flags have changed, because each descent drops the parent's
1127    /// non-global flags and a mounted command may declare the same name as a global seen
1128    /// here. Recording the owner keeps a word bound to the flag it was read as.
1129    ///
1130    /// The level matters to strict parsing: clap permits an inherited global once on each
1131    /// side of a subcommand boundary.
1132    binding: Option<(Arc<SpecFlag>, usize)>,
1133}
1134
1135impl Token {
1136    fn new(word: String, argv: usize) -> Self {
1137        Self {
1138            word,
1139            argv,
1140            binding: None,
1141        }
1142    }
1143}
1144
1145/// The token trace, while it is being built.
1146///
1147/// One row per word of the caller's argv, so a role can be recorded against a position
1148/// without the recorder having to know how many words came before it. Words the parser
1149/// made up have no row of their own and fold onto the row they were derived from.
1150struct Trace {
1151    tokens: Vec<TokenBinding>,
1152}
1153
1154impl Trace {
1155    fn new(input: &[String]) -> Self {
1156        Self {
1157            tokens: input
1158                .iter()
1159                .enumerate()
1160                .map(|(index, word)| TokenBinding {
1161                    index,
1162                    word: word.clone(),
1163                    synthesized: false,
1164                    roles: vec![],
1165                })
1166                .collect(),
1167        }
1168    }
1169
1170    fn record(&mut self, argv: usize, role: TokenRole) {
1171        if let Some(token) = self.tokens.get_mut(argv) {
1172            token.roles.push(role);
1173        }
1174    }
1175
1176    /// Note that what was read at this position is not what the caller wrote there.
1177    fn note_synthesized(&mut self, argv: usize) {
1178        if let Some(token) = self.tokens.get_mut(argv) {
1179            token.synthesized = true;
1180        }
1181    }
1182
1183    /// Every word the parse never reached, once it has stopped.
1184    fn close(&mut self, unread: &VecDeque<Token>) {
1185        for token in unread {
1186            self.record(token.argv, TokenRole::Unread);
1187        }
1188    }
1189}
1190
1191fn parse_partial_with_env(
1192    spec: &Spec,
1193    input: &[String],
1194    custom_env: Option<&HashMap<String, String>>,
1195    mount_outputs: Option<&HashMap<String, String>>,
1196    mount_timing: MountTiming,
1197) -> Result<(ParseOutput, HashSet<String>), miette::Error> {
1198    let mut trace = Trace::new(input);
1199    parse_partial_traced(
1200        spec,
1201        input,
1202        custom_env,
1203        mount_outputs,
1204        mount_timing,
1205        &mut trace,
1206    )
1207}
1208
1209/// The binding phase, with the trace left somewhere the caller can still read it.
1210///
1211/// A failure this phase cannot continue past — a word no declaration can take, a flag a
1212/// strict spec refuses — leaves through `?`, and a trace owned by the loop goes with it. The
1213/// words read before the failure are most of what a report wants, so the caller owns the
1214/// trace instead and keeps them. See [`Parser::explain_refused`].
1215fn parse_partial_traced(
1216    spec: &Spec,
1217    input: &[String],
1218    custom_env: Option<&HashMap<String, String>>,
1219    mount_outputs: Option<&HashMap<String, String>>,
1220    mount_timing: MountTiming,
1221    trace: &mut Trace,
1222) -> Result<(ParseOutput, HashSet<String>), miette::Error> {
1223    if let Some(view) = input.first().and_then(|argv0| spec.view_for_program(argv0)) {
1224        let viewed = spec.for_view(view)?;
1225        return parse_partial_traced(
1226            &viewed,
1227            input,
1228            custom_env,
1229            mount_outputs,
1230            mount_timing,
1231            trace,
1232        );
1233    }
1234    trace!("parse_partial: {input:?}");
1235    let mut input = input
1236        .iter()
1237        .enumerate()
1238        .map(|(argv, word)| Token::new(word.clone(), argv))
1239        .collect::<VecDeque<_>>();
1240    let argv0 = input.pop_front();
1241    if let Some(argv0) = argv0.as_ref() {
1242        trace.record(argv0.argv, TokenRole::Program);
1243    }
1244    if spec.multicall {
1245        if let Some(raw) = argv0 {
1246            if let Some(applet) = multicall_applet(&raw.word, &spec.name, Some(spec.bin.as_str())) {
1247                // A symlink invocation reads a word the caller never typed — the basename of
1248                // the program itself — so argv[0] is both the program and, below, whatever
1249                // that word selects.
1250                trace.note_synthesized(raw.argv);
1251                input.push_front(Token::new(applet.to_string(), raw.argv));
1252            }
1253        }
1254    }
1255    // The policy observes the selected command's own argv, not values eventually filled from
1256    // env/default. Start at the root, then reset on every explicit descent. A default
1257    // subcommand receives the unmatched word that selected it, so it is necessarily non-bare.
1258    let mut command_has_argv = !input.is_empty();
1259
1260    let mut out = ParseOutput {
1261        cmd: spec.cmd.clone(),
1262        cmds: vec![spec.cmd.clone()],
1263        args: IndexMap::new(),
1264        flags: IndexMap::new(),
1265        tokens: vec![],
1266        flag_origins: IndexMap::new(),
1267        arg_origins: IndexMap::new(),
1268        overridden_flags: BTreeMap::new(),
1269        available_flags: gather_flags(&spec.cmd),
1270        flag_awaiting_value: vec![],
1271        errors: vec![],
1272        warnings: vec![],
1273        next_arg: None,
1274        double_dash_seen: false,
1275        external: None,
1276    };
1277    // Keep this internal so adding relationship support remains semver-compatible. The full
1278    // parser uses it to prevent defaults and environment values from restoring overridden flags.
1279    let mut overridden_flags = HashSet::new();
1280    // Which spelling supplied each parsed flag. A child may re-declare one long form of an
1281    // inherited global while the merge keeps the ancestor's other aliases on the same `Arc`.
1282    // The declaration object alone then cannot answer whether `--clean` belonged to the child
1283    // or an inherited `-c` belonged to the ancestor.
1284    let mut parsed_flag_spellings: HashMap<usize, HashSet<String>> = HashMap::new();
1285
1286    // Phase 1: Scan for subcommands and collect global flags
1287    //
1288    // This phase identifies subcommands early because they may have mount points
1289    // that need to be executed with the global flags that appeared before them.
1290    //
1291    // Example: "usage --verbose run task"
1292    //   -> finds "run" subcommand, passes ["--verbose"] to its mount command
1293    //   -> then finds "task" as a subcommand of "run" (if it exists)
1294    //
1295    // We only collect global flags for mounts because:
1296    // - Non-global flags are specific to the current command, not subcommands
1297    // - Global flags affect all commands and should be passed to mount points
1298    let mut prefix_flags: Vec<(Arc<SpecFlag>, Vec<String>)> = vec![];
1299    // Which flag each word skipped here belongs to is recorded on the word — see
1300    // `Token::binding`.
1301    let mut command_arg_found = false;
1302    let mut variadic_flag_active = false;
1303    let mut idx = 0;
1304    // Track whether we've already applied the default_subcommand to prevent
1305    // multiple switches (e.g., if default is "run" and there's a task named "run")
1306    let mut used_default_subcommand = false;
1307    // Whether the command in scope has had its own mounts run. A mount on the root
1308    // is the case that needs this: a subcommand's mounts are run when the parser
1309    // descends into it, but nothing descends into the root.
1310    let mut mounts_resolved = false;
1311    // A completion needs the whole command list before it can offer anything, and
1312    // `mycli <tab>` has no word to trigger discovery with — so waiting for one would
1313    // mean a root mount never contributed to the very thing it exists for.
1314    //
1315    // The default-subcommand gate applies here too, and has to: offering a discovered
1316    // command that a real parse would hand to the default instead would be worse than
1317    // not offering it. A root mount under a `default_subcommand` that does not say
1318    // `overrides_default` therefore contributes nothing anywhere, which is what
1319    // "the default outranks discovery" means.
1320    let default_outranks_mounts =
1321        spec.default_subcommand.is_some() && !out.cmd.mounts.iter().any(|m| m.overrides_default);
1322    if mount_timing == MountTiming::Eager && !default_outranks_mounts && !out.cmd.mounts.is_empty()
1323    {
1324        mounts_resolved = true;
1325        let mut mounted = out.cmd.clone();
1326        mounted.mount(&[], mount_outputs)?;
1327        merge_subcommand_flags(&mut out.available_flags, gather_flags(&mounted), false);
1328        if let Some(last) = out.cmds.last_mut() {
1329            *last = mounted.clone();
1330        }
1331        out.cmd = mounted;
1332    }
1333
1334    while idx < input.len() {
1335        // Only for a word that could name a command, and only when it matches
1336        // nothing already declared. A CLI that declares its commands and mounts more
1337        // does not spawn a process for every invocation, and a flag — `--help`, or
1338        // anything unrecognized — never triggers discovery at all, which it would
1339        // otherwise do simply by not being a subcommand.
1340        // A declared `default_subcommand` already says what an unmatched word means,
1341        // and it costs nothing — so discovery waits behind it unless a mount asks to
1342        // outrank it. Without this, a task runner would spawn its discovery process
1343        // once per task invocation.
1344        let default_catches_it = spec.default_subcommand.as_deref().is_some_and(|name| {
1345            default_accepts_word(&out.cmd, name, &input[idx].word)
1346                && !out.cmd.mounts.iter().any(|m| m.overrides_default)
1347        });
1348        if !mounts_resolved
1349            && !out.cmd.mounts.is_empty()
1350            && !default_catches_it
1351            && is_command_word(&input[idx].word)
1352            && !is_negative_number(&input[idx].word)
1353            && out.cmd.find_subcommand(&input[idx].word).is_none()
1354        {
1355            mounts_resolved = true;
1356            let mut mounted = out.cmd.clone();
1357            mounted.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1358            merge_subcommand_flags(&mut out.available_flags, gather_flags(&mounted), false);
1359            if let Some(last) = out.cmds.last_mut() {
1360                *last = mounted.clone();
1361            }
1362            out.cmd = mounted;
1363        }
1364        if variadic_flag_active
1365            && out.cmd.find_subcommand(&input[idx].word).is_some()
1366            && !out.cmd.subcommand_precedence_over_arg
1367        {
1368            break;
1369        }
1370        if let Some(subcommand) = out.cmd.find_subcommand(&input[idx].word) {
1371            if out.cmd.args_conflicts_with_subcommands && command_arg_found {
1372                bail!(
1373                    "subcommand '{}' cannot be used with arguments on its parent command",
1374                    input[idx].word
1375                );
1376            }
1377            let mut subcommand = subcommand.clone();
1378            // Pass prefix words (global flags before this subcommand) to mount
1379            subcommand.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1380            // Only the *boundary* is a mount crossing: below it, the mounted program's own
1381            // commands are ordinary commands relative to each other.
1382            let crossing_mount = subcommand.mounted && !out.cmd.mounted;
1383            merge_subcommand_flags(
1384                &mut out.available_flags,
1385                gather_flags(&subcommand),
1386                crossing_mount,
1387            );
1388            // Remove subcommand from input
1389            let selected = input.remove(idx);
1390            if let Some(selected) = selected {
1391                trace.record(
1392                    selected.argv,
1393                    TokenRole::Command {
1394                        name: subcommand.name.clone(),
1395                    },
1396                );
1397            }
1398            command_has_argv = idx < input.len();
1399            out.cmds.push(subcommand.clone());
1400            out.cmd = subcommand.clone();
1401            // A descent already ran the new command's mounts, above.
1402            mounts_resolved = true;
1403            prefix_flags.clear();
1404            command_arg_found = false;
1405            variadic_flag_active = false;
1406            // Continue from current position (don't reset to 0)
1407            // After remove(), idx now points to the next element
1408        } else if !is_command_word(&input[idx].word)
1409            || declared_numeric_short(&out.available_flags, &input[idx].word)
1410        {
1411            // Check if this is a known flag
1412            let word = input[idx].word.clone();
1413            let flag_key = get_flag_key(&word);
1414
1415            // A short token keys on its first letter, so `-az` would be recorded as
1416            // `-a` and its tail left over. Check the whole token here, where it is
1417            // first read: a token containing an unrecognized letter is not a bundle,
1418            // and recording it as one is what let `-a` be applied from a token that
1419            // never named it.
1420            let is_bundle = word.starts_with("--")
1421                || short_bundle_is_known(spec, &out.cmds, &out.available_flags, &word);
1422            if let Some(f) = out
1423                .available_flags
1424                .get(flag_key)
1425                .cloned()
1426                .filter(|_| is_bundle)
1427            {
1428                command_arg_found = true;
1429                variadic_flag_active = f.arg.as_ref().is_some_and(|arg| arg.var);
1430                // Skip the flag and keep scanning. Both global and non-global flags may precede
1431                // a subcommand (`mycli --verbose run task`, `mycli run --force task`), and
1432                // stopping at one would hide the subcommand — and any mount on it — from the
1433                // parse, leaving the subcommand name to be mis-read as a positional argument.
1434                //
1435                // Only globals are forwarded to mounts: a non-global flag belongs to the
1436                // command that declared it, not to what is mounted below it.
1437                input[idx].binding = Some((Arc::clone(&f), out.cmds.len() - 1));
1438                let mut forwarded = f.global.then(|| vec![word.clone()]);
1439                idx += 1;
1440
1441                // Only consume next word if flag takes an argument AND value isn't embedded
1442                // Example: "--dir foo" consumes "foo", but "--dir=foo" or "--verbose" do not
1443                if f.arg.is_some()
1444                    && !word.contains('=')
1445                    && idx < input.len()
1446                    && (!is_flag_like(&input[idx].word)
1447                        || (f.arg.as_ref().is_some_and(|arg| arg.allow_negative_numbers)
1448                            && is_negative_number(&input[idx].word)))
1449                {
1450                    if let Some(words) = forwarded.as_mut() {
1451                        words.push(input[idx].word.clone());
1452                    }
1453                    idx += 1;
1454                }
1455                if let Some(words) = forwarded {
1456                    apply_prefix_flag_overrides(&mut prefix_flags, Arc::clone(&f));
1457                    prefix_flags.push((f, words));
1458                }
1459            } else {
1460                // Unknown flag - stop looking for subcommands
1461                // Let the main parsing phase handle the error
1462                break;
1463            }
1464        } else {
1465            if variadic_flag_active && out.cmd.subcommand_precedence_over_arg {
1466                idx += 1;
1467                continue;
1468            }
1469            // Found a word that's not a flag or subcommand
1470            // Check if we should use the default_subcommand (only once, and only at the
1471            // root, which is the only place a spec can declare one — `out.cmds` holds just
1472            // the root until something descends). Without that second condition the one
1473            // declared name is looked up wherever the parser happens to be standing, so an
1474            // unrelated command acquires a default because a name matched one level down:
1475            // with `default_subcommand "ls"` at the top, `ex config zzz` descended into
1476            // `config ls`.
1477            if !used_default_subcommand && out.cmds.len() == 1 {
1478                if let Some(default_name) = &spec.default_subcommand {
1479                    if let Some(subcommand) = out
1480                        .cmd
1481                        .find_subcommand(default_name)
1482                        .filter(|_| default_accepts_word(&out.cmd, default_name, &input[idx].word))
1483                    {
1484                        if out.cmd.args_conflicts_with_subcommands && command_arg_found {
1485                            bail!(
1486                                "subcommand '{}' cannot be used with arguments on its parent command",
1487                                subcommand.name
1488                            );
1489                        }
1490                        let mut subcommand = subcommand.clone();
1491                        // Pass prefix words (global flags before this) to mount
1492                        subcommand.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1493                        let crossing_mount = subcommand.mounted && !out.cmd.mounted;
1494                        merge_subcommand_flags(
1495                            &mut out.available_flags,
1496                            gather_flags(&subcommand),
1497                            crossing_mount,
1498                        );
1499                        out.cmds.push(subcommand.clone());
1500                        out.cmd = subcommand.clone();
1501                        command_has_argv = true;
1502                        prefix_flags.clear();
1503                        command_arg_found = false;
1504                        variadic_flag_active = false;
1505                        // This descent ran the new command's mounts, so lazy
1506                        // discovery must not run them a second time.
1507                        mounts_resolved = true;
1508                        used_default_subcommand = true;
1509                        // Continue the loop to check if this word is a subcommand of the
1510                        // default subcommand (e.g., a task name added via mount).
1511                        // If it's not a subcommand, the next iteration will break and
1512                        // Phase 2 will handle it as a positional arg.
1513                        continue;
1514                    }
1515                }
1516            }
1517            // An unmatched word that names no subcommand is forwarded as an external
1518            // command: this word, then every token after it, including flags. Known
1519            // subcommands already won above, and a default_subcommand already caught.
1520            // clap's `allow_external_subcommands` is this, not `unknown_flags=value`.
1521            if out.cmd.external_subcommand {
1522                let rest: Vec<Token> = input.drain(idx..).collect();
1523                for token in &rest {
1524                    trace.record(token.argv, TokenRole::External);
1525                }
1526                out.external = Some(rest.into_iter().map(|t| t.word).collect());
1527                break;
1528            }
1529            // This could be a positional argument, so stop subcommand search
1530            break;
1531        }
1532    }
1533
1534    // Phase 2: Main argument and flag parsing
1535    //
1536    // Now that we've identified all subcommands and executed their mounts,
1537    // we can parse the remaining arguments, flags, and their values.
1538
1539    // The cursor into `out.cmd.args`, kept as an index rather than a reference because an
1540    // explicit `--` may jump it *past* arguments that stay empty (see the `w == "--"` arm).
1541    // With such a gap `out.args.len()` no longer equals the cursor, so anything asking "is this
1542    // argument filled?" has to consult `out.args` by key instead of counting.
1543    let mut next_arg_idx: usize = 0;
1544    let mut enable_flags = true;
1545    let mut grouped_flag = false;
1546    // Whether an explicit `--` has been consumed *as a separator* (as opposed to being kept as a
1547    // value by `double_dash="preserve"`). Args declared `double_dash="required"` only accept
1548    // words that come after it — see `report_double_dash_violation`.
1549    let mut seen_double_dash = false;
1550    // Args already reported as having been offered a word before the `--` they require, so a
1551    // variadic one does not report the same violation for every word it is offered.
1552    let mut double_dash_violations: HashSet<String> = HashSet::new();
1553    // Scalar occurrences are scoped to the command level where they were written. Inherited
1554    // globals may therefore appear once before and once after a subcommand under clap's strict
1555    // `args_override_self(false)` policy. The bitset also keeps both forms of a negatable flag:
1556    // opposite forms may override one another, while repeating either spelling is an error.
1557    let mut scalar_occurrences: HashMap<(usize, usize), u8> = HashMap::new();
1558
1559    while !input.is_empty() {
1560        let token = input.pop_front().unwrap();
1561        // The flag this word was read as in Phase 1, if it skipped it (see `Token::binding`).
1562        let binding = token.binding;
1563        let argv = token.argv;
1564        let mut w = token.word;
1565        // A short's attached value is re-queued with `grouped_flag` set, and that
1566        // continuation is not a following word. `require_equals` refuses only the
1567        // following word; `-i9229` and `-i=9229` still bind. `default_missing` binds
1568        // only when the value is actually missing, so `-cnever` is still `never`.
1569        let attached_continuation = grouped_flag;
1570
1571        // Check for restart_token - resets argument parsing for multiple command invocations
1572        // e.g., `mise run lint ::: test ::: check` with restart_token=":::"
1573        if let Some(ref restart_token) = out.cmd.restart_token {
1574            if w == *restart_token {
1575                // Reset argument parsing state for a fresh command invocation, keeping the
1576                // flags. `double_dash_violations` is deliberately *not* cleared: `out.errors`
1577                // is not cleared here either, so clearing it would let one arg report the same
1578                // violation once per invocation.
1579                out.args.clear();
1580                // With the values gone, so is where they came from — otherwise the second
1581                // invocation of `run lint ::: test` reports the first one's provenance. The
1582                // token trace is *not* cleared: those words were read, and a report that
1583                // dropped them would show a command line with a hole in it.
1584                out.arg_origins.clear();
1585                trace.record(argv, TokenRole::Restart);
1586                next_arg_idx = 0;
1587                out.flag_awaiting_value.clear(); // Clear any pending flag values
1588                enable_flags = true; // Reset -- separator effect
1589                seen_double_dash = false; // The next invocation needs its own `--`
1590                continue;
1591            }
1592        }
1593
1594        // A flag declared `allow_hyphen_values` takes the next token whatever it looks
1595        // like, and that has to be asked before the separator arm below rather than
1596        // after it. Asked after, a `--` was consumed as a separator while the flag
1597        // stayed hungry, and the flag then ate the word past it: `ex -a -- -x` bound
1598        // `-x` and the separator was simply gone. Asked here, the flag takes the `--`
1599        // itself, which is what clap does with the same declaration — and no flag can
1600        // still be waiting once the separator has done its job, so the starvation rule
1601        // below has no path around it.
1602        if enable_flags
1603            && w.starts_with('-')
1604            && out.flag_awaiting_value.last().is_some_and(|flag| {
1605                !flag.require_equals
1606                    && (flag.allow_hyphen_values()
1607                        || (flag
1608                            .arg
1609                            .as_ref()
1610                            .is_some_and(|arg| arg.allow_negative_numbers)
1611                            && is_negative_number(&w)))
1612            })
1613        {
1614            // A variadic argument collects here too: which token supplied its first
1615            // value says nothing about how many it takes.
1616            let should_return = bind_pending_flag_value(
1617                spec,
1618                &out.cmd,
1619                &mut out.errors,
1620                &mut out.flags,
1621                &mut out.flag_awaiting_value,
1622                &mut w,
1623                &mut input,
1624                custom_env,
1625                trace,
1626                argv,
1627                // The token a hyphen-valued flag takes is the following word, never attached.
1628                false,
1629            )?;
1630            if should_return {
1631                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1632                return Ok((out, overridden_flags));
1633            }
1634            continue;
1635        }
1636
1637        // A flag whose value may be omitted that cannot take this token as a detached
1638        // value finishes bare and leaves the token for whatever comes next:
1639        // `--color --verbose` colours with the missing value and still sets verbose,
1640        // and `--inspect 9229` with `require_equals` binds the missing value rather
1641        // than treating 9229 as the port.
1642        if enable_flags
1643            && !attached_continuation
1644            && !out.flag_awaiting_value.is_empty()
1645            && out.flag_awaiting_value.last().is_some_and(|flag| {
1646                (flag.default_missing.is_some() || flag.value_optional)
1647                    && (flag.require_equals
1648                        || (is_flag_like(&w)
1649                            && !flag.allow_hyphen_values()
1650                            && !(flag
1651                                .arg
1652                                .as_ref()
1653                                .is_some_and(|arg| arg.allow_negative_numbers)
1654                                && is_negative_number(&w))))
1655            })
1656        {
1657            try_bind_default_missing(
1658                &mut out.flags,
1659                &mut out.flag_awaiting_value,
1660                custom_env,
1661                &mut out.flag_origins,
1662            )?;
1663        }
1664
1665        // The first explicit `--` is still a separator after an `automatic` argument has
1666        // stopped flag parsing. Once an explicit separator has done its job, a second one is
1667        // an ordinary value: every parser worth comparing against keeps it (POSIX getopt,
1668        // argparse, clap, commander, yargs), and jdx/usage#229 was a user reporting the old
1669        // behavior as the bug it is.
1670        if w == "--" && !seen_double_dash {
1671            enable_flags = false;
1672
1673            // Only preserve the double dash token if we're collecting values for a variadic arg
1674            // in double_dash == `preserve` mode
1675            let should_preserve = out
1676                .cmd
1677                .args
1678                .get(next_arg_idx)
1679                .map(|arg| arg.var && arg.double_dash == SpecDoubleDashChoices::Preserve)
1680                .unwrap_or(false);
1681
1682            if should_preserve {
1683                // Fall through to arg parsing. This `--` is a *value*, not a separator, so it
1684                // neither counts as one nor unlocks a `double_dash="required"` arg.
1685            } else {
1686                seen_double_dash = true;
1687                trace.record(argv, TokenRole::Separator);
1688
1689                // Everything after an explicit `--` belongs to the arg that requires one, so
1690                // jump the cursor there — past any earlier arg, including a greedy variadic
1691                // that would otherwise swallow the rest. This mirrors clap's `Arg::last(true)`,
1692                // which is what `double_dash="required"` is generated from. Specs without such
1693                // an arg find nothing and keep the cursor where it was.
1694                let target = out.cmd.args.iter().position(|arg| {
1695                    arg.double_dash == SpecDoubleDashChoices::Required
1696                        && !out.args.contains_key(arg)
1697                });
1698                if let Some(target) = target {
1699                    // Forward only. An unfilled required arg declared *before* the cursor is
1700                    // left where it is rather than rewound to — words already assigned to
1701                    // later args would have to be taken back for that to mean anything, and
1702                    // the arg keeps its `MissingArg`. `double_dash="required"` mirrors clap's
1703                    // `Arg::last(true)`, which is the final positional, so a spec that puts
1704                    // one ahead of others is already outside what this models.
1705                    if target > next_arg_idx {
1706                        next_arg_idx = target;
1707                    }
1708                }
1709                continue;
1710            }
1711        }
1712
1713        // long flags
1714        if enable_flags && w.starts_with("--") {
1715            grouped_flag = false;
1716            // `Some` only when an `=` was actually written, so `--jobs=` can supply
1717            // an empty value while `--jobs` supplies none. Collapsing the two lost
1718            // the flag entirely.
1719            let split = w.split_once('=');
1720            let word = split.map(|(word, _)| word).unwrap_or(&w);
1721            let bound_flag = binding.as_ref().map(|(flag, _)| flag);
1722            if let Some(f) = bound_flag.or_else(|| out.available_flags.get(word)) {
1723                let command_level = binding
1724                    .as_ref()
1725                    .map(|(_, level)| *level)
1726                    .unwrap_or(out.cmds.len() - 1);
1727                parsed_flag_spellings
1728                    .entry(Arc::as_ptr(f) as usize)
1729                    .or_default()
1730                    .insert(word.to_string());
1731                // Recorded before the action check below: a token that named a flag named it
1732                // whether or not the parse can carry on afterwards.
1733                trace.record(
1734                    argv,
1735                    TokenRole::Flag {
1736                        flag: Arc::clone(f),
1737                        spelling: word.to_string(),
1738                        negated: f.negate.as_deref() == Some(word),
1739                    },
1740                );
1741                if f.action != crate::SpecFlagAction::Set {
1742                    out.errors.push(render_action_err(spec, &out.cmd, f, word));
1743                    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1744                    return Ok((out, overridden_flags));
1745                }
1746                apply_flag_overrides(
1747                    f,
1748                    &out.available_flags,
1749                    &mut out.flags,
1750                    &mut out.flag_awaiting_value,
1751                    &mut overridden_flags,
1752                    &mut out.overridden_flags,
1753                );
1754                // An attached value only means something to a flag that takes one:
1755                // `--jobs=` is an empty string, while `--force=yes` has nothing to
1756                // give a flag that holds no value. Handing that leftover to the
1757                // positionals would re-split one token into two, so `ex --force=yes`
1758                // would fill an argument the caller never typed a word for.
1759                if f.arg.is_some() {
1760                    record_scalar_flag_occurrence(
1761                        &out.cmds,
1762                        f,
1763                        command_level,
1764                        None,
1765                        &mut scalar_occurrences,
1766                        &mut out.errors,
1767                    );
1768                    let f = Arc::clone(f);
1769                    out.flag_awaiting_value.push(Arc::clone(&f));
1770                    // The `=` has already settled that this text is the value, so it
1771                    // binds here rather than going back on the queue to be read as a
1772                    // token again — where `--jobs=--force` looked like a flag of its
1773                    // own and bound `force`, leaving `jobs` unset.
1774                    if let Some((_, val)) = split {
1775                        // The `=` settles where the *first* value came from and nothing
1776                        // more, so a variadic argument goes on collecting from the words
1777                        // after it exactly as the detached form does.
1778                        let mut val = val.to_string();
1779                        let should_return = bind_pending_flag_value(
1780                            spec,
1781                            &out.cmd,
1782                            &mut out.errors,
1783                            &mut out.flags,
1784                            &mut out.flag_awaiting_value,
1785                            &mut val,
1786                            &mut input,
1787                            custom_env,
1788                            trace,
1789                            argv,
1790                            // The `=` settled that this text is the value, so it rode in on
1791                            // the flag's own token.
1792                            true,
1793                        )?;
1794                        if should_return {
1795                            record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1796                            return Ok((out, overridden_flags));
1797                        }
1798                    }
1799                } else if f.count {
1800                    let arr = out
1801                        .flags
1802                        .entry(Arc::clone(f))
1803                        .or_insert_with(|| ParseValue::MultiBool(vec![]))
1804                        .try_as_multi_bool_mut()
1805                        .unwrap();
1806                    arr.push(true);
1807                } else {
1808                    let negate = f.negate.clone().unwrap_or_default();
1809                    let negated_form = word == negate;
1810                    let value = if f.bool_value {
1811                        match split.map(|(_, value)| value) {
1812                            Some("true") => !negated_form,
1813                            Some("false") => negated_form,
1814                            Some(value) => {
1815                                out.errors.push(UsageErr::InvalidValue {
1816                                    name: f.name.clone(),
1817                                    value: value.to_string(),
1818                                    reason: "expected `true` or `false`".to_string(),
1819                                });
1820                                continue;
1821                            }
1822                            None => !negated_form,
1823                        }
1824                    } else {
1825                        !negated_form
1826                    };
1827                    // Which form was typed is a question about the name, so it is
1828                    // asked of `word` rather than the whole token: the attached value
1829                    // is dropped just above, and comparing `--no-color=yes` against
1830                    // `--no-color` would take the negation down with it.
1831                    record_scalar_flag_occurrence(
1832                        &out.cmds,
1833                        f,
1834                        command_level,
1835                        Some(!negated_form),
1836                        &mut scalar_occurrences,
1837                        &mut out.errors,
1838                    );
1839                    out.flags.insert(Arc::clone(f), ParseValue::Bool(value));
1840                }
1841                continue;
1842            }
1843            if is_help_arg(spec, &out.cmd, &w) {
1844                out.errors
1845                    .push(render_help_err(spec, &out.cmd, w.len() > 2));
1846                trace.record(
1847                    argv,
1848                    TokenRole::Builtin {
1849                        spelling: w.clone(),
1850                    },
1851                );
1852                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1853                return Ok((out, overridden_flags));
1854            }
1855            if is_version_arg(spec, &out.cmds, &w) {
1856                out.errors.push(render_version_err(spec, w.len() > 2));
1857                trace.record(
1858                    argv,
1859                    TokenRole::Builtin {
1860                        spelling: w.clone(),
1861                    },
1862                );
1863                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1864                return Ok((out, overridden_flags));
1865            }
1866            if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
1867                trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
1868                trace.close(&input);
1869                return Err(refused.into());
1870            }
1871        }
1872
1873        // short flags
1874        //
1875        // A fresh token is checked whole before any of it is applied: `-az` with only
1876        // `-a` declared is not a bundle at all, so it must not set `a` on the way to
1877        // discovering that `z` names nothing. A grouped continuation is exempt — its
1878        // token was already checked when it arrived.
1879        let declared_numeric_short = declared_numeric_short(&out.available_flags, &w);
1880        let positional_negative_number = !declared_numeric_short
1881            && is_negative_number(&w)
1882            && out
1883                .cmd
1884                .args
1885                .get(next_arg_idx)
1886                .is_some_and(|arg| arg.allow_negative_numbers);
1887        if enable_flags
1888            && !grouped_flag
1889            // A word phase 1 already resolved to a flag needs no re-checking, and
1890            // the flags in scope have changed since, so re-checking would be wrong.
1891            && binding.is_none()
1892            && w.starts_with('-')
1893            && w.len() > 1
1894            && is_flag_like(&w)
1895            && !positional_negative_number
1896            && !short_bundle_is_known(spec, &out.cmds, &out.available_flags, &w)
1897        {
1898            // Refused if this command asked for that; otherwise it carries on below
1899            // as one word, with none of its letters applied.
1900            if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
1901                trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
1902                trace.close(&input);
1903                return Err(refused.into());
1904            }
1905        } else if enable_flags && !positional_negative_number && w.starts_with('-') && w.len() > 1 {
1906            let short = w.chars().nth(1).unwrap();
1907            if let Some(f) = binding
1908                .as_ref()
1909                .map(|(flag, _)| flag)
1910                .or_else(|| out.available_flags.get(&format!("-{short}")))
1911            {
1912                let command_level = binding
1913                    .as_ref()
1914                    .map(|(_, level)| *level)
1915                    .unwrap_or(out.cmds.len() - 1);
1916                if f.action != crate::SpecFlagAction::Set {
1917                    out.errors
1918                        .push(render_action_err(spec, &out.cmd, f, &format!("-{short}")));
1919                    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1920                    return Ok((out, overridden_flags));
1921                }
1922                parsed_flag_spellings
1923                    .entry(Arc::as_ptr(f) as usize)
1924                    .or_default()
1925                    .insert(format!("-{short}"));
1926                trace.record(
1927                    argv,
1928                    TokenRole::Flag {
1929                        flag: Arc::clone(f),
1930                        spelling: format!("-{short}"),
1931                        // A short spelling is never the negated form: `negate` is a long.
1932                        negated: false,
1933                    },
1934                );
1935                apply_flag_overrides(
1936                    f,
1937                    &out.available_flags,
1938                    &mut out.flags,
1939                    &mut out.flag_awaiting_value,
1940                    &mut overridden_flags,
1941                    &mut out.overridden_flags,
1942                );
1943                let rest = &w[1 + short.len_utf8()..];
1944                if !rest.is_empty() {
1945                    // `-abc` is one token that names three flags, so the tail is read at the
1946                    // bundle's own position rather than at one of its own.
1947                    input.push_front(Token::new(format!("-{rest}"), argv));
1948                }
1949                // A fully consumed short is no longer a grouped continuation.
1950                // Leaving this set after `-ai` made `-i` skip `require_equals`
1951                // and bind the following word.
1952                grouped_flag = !rest.is_empty();
1953                if f.arg.is_some() {
1954                    record_scalar_flag_occurrence(
1955                        &out.cmds,
1956                        f,
1957                        command_level,
1958                        None,
1959                        &mut scalar_occurrences,
1960                        &mut out.errors,
1961                    );
1962                    out.flag_awaiting_value.push(Arc::clone(f));
1963                } else if f.count {
1964                    let arr = out
1965                        .flags
1966                        .entry(Arc::clone(f))
1967                        .or_insert_with(|| ParseValue::MultiBool(vec![]))
1968                        .try_as_multi_bool_mut()
1969                        .unwrap();
1970                    arr.push(true);
1971                } else {
1972                    let negate = f.negate.clone().unwrap_or_default();
1973                    let value = w != negate;
1974                    record_scalar_flag_occurrence(
1975                        &out.cmds,
1976                        f,
1977                        command_level,
1978                        Some(value),
1979                        &mut scalar_occurrences,
1980                        &mut out.errors,
1981                    );
1982                    out.flags.insert(Arc::clone(f), ParseValue::Bool(value));
1983                }
1984                continue;
1985            }
1986            // The letter nothing declared may still be one the parser supplies, and it may
1987            // sit anywhere in the token: `-hv` asks for help as surely as `-vh` does, and
1988            // neither reaches the whole-token spellings below.
1989            if let Some(err) = supplied_short(spec, &out.cmds, short) {
1990                out.errors.push(err);
1991                trace.record(
1992                    argv,
1993                    TokenRole::Builtin {
1994                        spelling: format!("-{short}"),
1995                    },
1996                );
1997                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1998                return Ok((out, overridden_flags));
1999            }
2000            if is_help_arg(spec, &out.cmd, &w) {
2001                out.errors
2002                    .push(render_help_err(spec, &out.cmd, w.len() > 2));
2003                trace.record(
2004                    argv,
2005                    TokenRole::Builtin {
2006                        spelling: w.clone(),
2007                    },
2008                );
2009                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2010                return Ok((out, overridden_flags));
2011            }
2012            if is_version_arg(spec, &out.cmds, &w) {
2013                out.errors.push(render_version_err(spec, w.len() > 2));
2014                trace.record(
2015                    argv,
2016                    TokenRole::Builtin {
2017                        spelling: w.clone(),
2018                    },
2019                );
2020                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2021                return Ok((out, overridden_flags));
2022            }
2023            if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
2024                trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
2025                trace.close(&input);
2026                return Err(refused.into());
2027            }
2028            if grouped_flag {
2029                grouped_flag = false;
2030                w.remove(0);
2031                // What is left is a short flag's attached value, and one `=` between
2032                // the letter and the value is a separator: `-j=8` means 8. Only one,
2033                // so `-j==8` still means `=8`.
2034                if !out.flag_awaiting_value.is_empty() && w.starts_with('=') {
2035                    w.remove(0);
2036                }
2037            }
2038        }
2039
2040        // Only while flags are still being read. A flag still waiting when the separator
2041        // was consumed is starved: its value would have to come from after the `--`,
2042        // where every token is data. Draining there gave `ex --jobs -- x` the word after
2043        // the separator, so the command line quietly meant `ex --jobs=x` and the `--`
2044        // was gone. Left waiting, it is reported as the missing value it is.
2045        // `require_equals` refuses a detached value: `--flag value` is a missing
2046        // value, not a flag of `"value"`. The attached form is still bound above.
2047        // Reported here rather than left waiting until the end of the line: falling
2048        // through would offer `value` to the positionals and call it an unexpected
2049        // word, which is the wrong error and a different one from usage-argv.
2050        if enable_flags
2051            && !attached_continuation
2052            && !out.flag_awaiting_value.is_empty()
2053            && out
2054                .flag_awaiting_value
2055                .last()
2056                .is_some_and(|flag| flag.require_equals)
2057        {
2058            let flag = out.flag_awaiting_value.last().unwrap();
2059            let token = flag
2060                .long
2061                .first()
2062                .map(|l| format!("--{l}"))
2063                .or_else(|| flag.short.first().map(|s| format!("-{s}")))
2064                .unwrap_or_else(|| flag.name.clone());
2065            out.errors.push(UsageErr::InvalidFlag {
2066                token: token.clone(),
2067                reason: "requires an argument".to_string(),
2068                span: (0, 0).into(),
2069                input: format!("{token} {w}"),
2070            });
2071            record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2072            return Ok((out, overridden_flags));
2073        }
2074        if enable_flags && !out.flag_awaiting_value.is_empty() {
2075            // Held before the drain pops it: a flag whose argument is variadic keeps
2076            // taking values after this first one.
2077            let should_return = bind_pending_flag_value(
2078                spec,
2079                &out.cmd,
2080                &mut out.errors,
2081                &mut out.flags,
2082                &mut out.flag_awaiting_value,
2083                &mut w,
2084                &mut input,
2085                custom_env,
2086                trace,
2087                argv,
2088                attached_continuation,
2089            )?;
2090            if should_return {
2091                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2092                return Ok((out, overridden_flags));
2093            }
2094            continue;
2095        }
2096
2097        if out.cmd.allow_missing_positional {
2098            while let Some(current) = out.cmd.args.get(next_arg_idx) {
2099                if current.required || out.args.contains_key(current) {
2100                    break;
2101                }
2102                let required_after = out.cmd.args[next_arg_idx + 1..]
2103                    .iter()
2104                    .filter(|arg| arg.required)
2105                    .count();
2106                if required_after == 0 {
2107                    break;
2108                }
2109                let remaining_values = 1 + input
2110                    .iter()
2111                    .filter(|token| !enable_flags || !is_flag_like(&token.word))
2112                    .count();
2113                if remaining_values > required_after {
2114                    break;
2115                }
2116                next_arg_idx += 1;
2117            }
2118        }
2119
2120        if let Some(arg) = out.cmd.args.get(next_arg_idx) {
2121            if arg.var
2122                && out.args.contains_key(arg)
2123                && arg.value_terminator.as_deref() == Some(w.as_str())
2124            {
2125                trace.record(
2126                    argv,
2127                    TokenRole::ValueTerminator {
2128                        ends: arg.name.clone(),
2129                    },
2130                );
2131                next_arg_idx += 1;
2132                continue;
2133            }
2134            // Before anything else: an arg that requires `--` accepts nothing until one has been
2135            // seen. Checking ahead of `validate_choices` keeps a discarded word from also being
2136            // reported as an invalid choice, and from reaching that function's help escape.
2137            if arg.double_dash == SpecDoubleDashChoices::Required && !seen_double_dash {
2138                report_double_dash_violation(arg, &mut out.errors, &mut double_dash_violations);
2139                trace.record(
2140                    argv,
2141                    TokenRole::Refused {
2142                        reason: format!("{} only accepts words after `--`", arg.name),
2143                    },
2144                );
2145                // Drop the word without filling the arg or advancing the cursor: every later
2146                // word hits the same arg and is rejected the same way, so the parse still ends
2147                // in an error rather than in `unexpected word`.
2148                continue;
2149            }
2150            // Split before judging, as the flag path does: after the split the word is
2151            // no longer one value, and `choices` has to be asked about each. Judging
2152            // first rejects `src:docs` against a list that both halves are on, and
2153            // names the whole word rather than the half that was wrong.
2154            let trailing_value =
2155                seen_double_dash || arg.double_dash == SpecDoubleDashChoices::Automatic;
2156            let suppress_trailing_delimiter =
2157                out.cmds.iter().any(|cmd| cmd.dont_delimit_trailing_values);
2158            let delimiter = if suppress_trailing_delimiter && trailing_value {
2159                None
2160            } else {
2161                arg.delimiter
2162            };
2163            let parts: Vec<String> = match delimiter {
2164                Some(delimiter) => w.split(delimiter).map(str::to_string).collect(),
2165                None => vec![w.clone()],
2166            };
2167            let mut refused = false;
2168            for part in &parts {
2169                if validate_choices(
2170                    spec,
2171                    &out.cmd,
2172                    &mut out.errors,
2173                    ChoiceTarget::arg(arg),
2174                    part,
2175                    arg.choices.as_ref(),
2176                    custom_env,
2177                )? {
2178                    refused = true;
2179                    break;
2180                }
2181            }
2182            if refused {
2183                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2184                return Ok((out, overridden_flags));
2185            }
2186            // `double_dash="automatic"` means the first value this arg takes is the last
2187            // token read as anything but data: a wrapper declaring it can forward flags
2188            // without its caller typing a `--`. Set before the value is stored, so the
2189            // rest of the command line is already past flag parsing.
2190            if arg.double_dash == SpecDoubleDashChoices::Automatic {
2191                enable_flags = false;
2192            }
2193            // A flag-like word reaching a positional while flags are still being read was
2194            // offered to every declaration and matched none: under the default
2195            // `unknown_flags="value"` it becomes data, and saying so is the difference
2196            // between "you have a typo" and "this argument took your typo".
2197            let unknown_flag = enable_flags
2198                && !positional_negative_number
2199                && is_flag_like(&w)
2200                && binding.is_none();
2201            trace.record(
2202                argv,
2203                if unknown_flag {
2204                    TokenRole::UnknownFlag {
2205                        bound_as: Some(Arc::new(arg.clone())),
2206                    }
2207                } else {
2208                    TokenRole::Arg {
2209                        arg: Arc::new(arg.clone()),
2210                        values: parts.clone(),
2211                    }
2212                },
2213            );
2214            if arg.var {
2215                let arr = out
2216                    .args
2217                    .entry(Arc::new(arg.clone()))
2218                    .or_insert_with(|| ParseValue::MultiString(vec![]))
2219                    .try_as_multi_string_mut()
2220                    .unwrap();
2221                // The values this word carried, split above so that everything
2222                // downstream — `choices`, `var_max` stopping the collection, `var_min` —
2223                // counts the values the user meant rather than the words they typed.
2224                arr.extend(parts.iter().cloned());
2225                if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
2226                    next_arg_idx += 1;
2227                }
2228            } else {
2229                out.args
2230                    .insert(Arc::new(arg.clone()), ParseValue::String(w));
2231                next_arg_idx += 1;
2232            }
2233            continue;
2234        }
2235        if is_help_arg(spec, &out.cmd, &w) {
2236            out.errors
2237                .push(render_help_err(spec, &out.cmd, w.len() > 2));
2238            trace.record(
2239                argv,
2240                TokenRole::Builtin {
2241                    spelling: w.clone(),
2242                },
2243            );
2244            record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2245            return Ok((out, overridden_flags));
2246        }
2247        if is_version_arg(spec, &out.cmds, &w) {
2248            out.errors.push(render_version_err(spec, w.len() > 2));
2249            trace.record(
2250                argv,
2251                TokenRole::Builtin {
2252                    spelling: w.clone(),
2253                },
2254            );
2255            record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2256            return Ok((out, overridden_flags));
2257        }
2258        trace.record(
2259            argv,
2260            TokenRole::Refused {
2261                reason: "no declaration takes this word".to_string(),
2262            },
2263        );
2264        trace.close(&input);
2265        bail!("unexpected word: {w}");
2266    }
2267
2268    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2269
2270    // `out.flags` is keyed by `SpecFlag`, whose equality is intentionally name-only. Two
2271    // declarations with the same canonical name therefore share one public value entry even
2272    // when both were typed. The spelling ledger is keyed by declaration identity and retains
2273    // both, which is what exclusivity needs.
2274    let flag_was_parsed =
2275        |flag: &Arc<SpecFlag>| parsed_flag_spellings.contains_key(&(Arc::as_ptr(flag) as usize));
2276
2277    // The spellings the selected command's own declaration speaks for, on this object.
2278    //
2279    // Empty unless that declaration really is this object's: a parent and child may each
2280    // declare `--clean` without merging, leaving two flags that share a name, and the
2281    // ancestor's must not be read as the child's. The test is whether every spelling the child
2282    // declared resolves back here — true of a merged flag, and of a plain local one, but not of
2283    // an ancestor whose long form the child took over.
2284    let child_spellings = |flag: &Arc<SpecFlag>| -> HashSet<String> {
2285        let declared: HashSet<String> = out
2286            .cmd
2287            .flags
2288            .iter()
2289            .filter(|declared| declared.name == flag.name)
2290            .flat_map(flag_keys)
2291            .collect();
2292        // *Any* of them, not all. All was too strong: a child may declare a spelling that
2293        // some other inherited global already owns — `-c --clean` beside an inherited
2294        // `-c --config` — and that collision is resolved in the other global's favor, so the
2295        // child's `-c` resolves elsewhere. Requiring every spelling to land here let one
2296        // unrelated collision disown the child from the `--clean` it plainly does own.
2297        //
2298        // Still enough to tell the two-object case apart, which is what this guards: when a
2299        // child re-declares a global as global, the child's own spellings resolve to the
2300        // child's separate flag, so none of them lands on the ancestor's.
2301        let speaks_for_this_flag = declared.iter().any(|spelling| {
2302            out.available_flags
2303                .get(spelling)
2304                .is_some_and(|available| Arc::ptr_eq(available, flag))
2305        });
2306        if speaks_for_this_flag {
2307            declared
2308        } else {
2309            HashSet::new()
2310        }
2311    };
2312
2313    // Whose `exclusive` an occurrence activates, as `(the child's, an ancestor's)`.
2314    //
2315    // A child that re-declares an inherited global merges into one object answering to two
2316    // alias sets whose declarations may disagree, so there is no single owner to name: the
2317    // child owns the spellings it declared and the ancestor keeps the ones only it declared.
2318    // Both sides can be in play at once — `run -c --clean` is the ancestor's alias and the
2319    // child's in one invocation — and each carries its own declaration's answer.
2320    let exclusivity_in_play = |flag: &Arc<SpecFlag>| -> (bool, bool) {
2321        let child = child_spellings(flag);
2322        let child_exclusive = !child.is_empty()
2323            && out
2324                .cmd
2325                .flags
2326                .iter()
2327                .any(|declared| declared.name == flag.name && declared.exclusive);
2328        match parsed_flag_spellings.get(&(Arc::as_ptr(flag) as usize)) {
2329            Some(spellings) => (
2330                child_exclusive && spellings.iter().any(|s| child.contains(s)),
2331                flag.exclusive && spellings.iter().any(|s| !child.contains(s)),
2332            ),
2333            // An environment value has no spelling to attribute it by. The declaration the
2334            // selected command has in scope is the one that answers — which is the child's
2335            // when it re-declared the flag, and the ancestor's when it did not.
2336            None => (child_exclusive, flag.exclusive && child.is_empty()),
2337        }
2338    };
2339
2340    let exclusive_occurrence = |flag: &Arc<SpecFlag>| {
2341        let (child, ancestor) = exclusivity_in_play(flag);
2342        child || ancestor
2343    };
2344
2345    // clap's `exclusive` is also an escape from requiredness: `--version` has to work on a
2346    // command that otherwise needs an input. Companions are still diagnosed below, but an
2347    // exclusive occurrence suppresses the missing-value checks that would make it unusable
2348    // whether it was alone or not.
2349    let exclusive_present =
2350        unique_flags(out.available_flags.values().chain(out.flags.keys())).any(|flag| {
2351            exclusive_occurrence(flag)
2352                && !overridden_flags.contains(&flag.name)
2353                && (flag_was_parsed(flag) || flag_has_env(flag, custom_env))
2354        });
2355    let requirements_apply = |command_index: usize| {
2356        command_index + 1 == out.cmds.len() || !out.cmds[command_index].subcommand_negates_reqs
2357    };
2358
2359    if out.cmd.arg_required_else_help && !command_has_argv {
2360        out.errors.push(render_help_err(spec, &out.cmd, false));
2361    }
2362
2363    // A command that says it needs a subcommand, given none. Checked on `out.cmd` and nowhere
2364    // else, because `out.cmd` *is* the command the words reached: had a subcommand been taken,
2365    // the child would be here instead. The spec has carried `subcommand_required` since it was
2366    // added for the derive, and this parser never read it — so `mise generate` parsed as a
2367    // complete invocation while usage-argv and clap both refused it.
2368    if out.cmd.subcommand_required && !out.cmd.subcommands.is_empty() && out.external.is_none() {
2369        let mut names: Vec<&str> = out
2370            .cmd
2371            .subcommands
2372            .iter()
2373            // Aliases share a map entry with the name they point at; listing both would offer
2374            // the same command twice under two spellings.
2375            .filter(|(name, sub)| sub.name == **name && !sub.hide)
2376            .map(|(name, _)| name.as_str())
2377            .collect();
2378        names.sort_unstable();
2379        out.errors.push(UsageErr::MissingSubcommand(
2380            out.cmd.name.clone(),
2381            names.join(", "),
2382        ));
2383    }
2384
2385    // Not `skip(out.args.len())`: a `--` may have jumped the cursor past an arg that stayed
2386    // empty, so position and fill count can disagree. Ask `out.args` which args it holds.
2387    if !exclusive_present {
2388        for arg in out
2389            .cmds
2390            .iter()
2391            .enumerate()
2392            .filter(|(index, _)| requirements_apply(*index))
2393            .flat_map(|(_, cmd)| &cmd.args)
2394        {
2395            if out.args.contains_key(arg) {
2396                continue;
2397            }
2398            // Already reported as needing a `--`; one mistake should not yield two messages.
2399            if double_dash_violations.contains(&arg.name) {
2400                continue;
2401            }
2402            let required_if = arg.required_if.iter().any(|selector| {
2403                selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2404            });
2405            let required_if_eq = arg.required_if_eq.iter().any(|condition| {
2406                selector_explicit_has_value(
2407                    &condition.selector,
2408                    &condition.value,
2409                    &out,
2410                    &overridden_flags,
2411                    custom_env,
2412                )
2413            });
2414            let required_if_eq_all = !arg.required_if_eq_all.is_empty()
2415                && arg.required_if_eq_all.iter().all(|condition| {
2416                    selector_explicit_has_value(
2417                        &condition.selector,
2418                        &condition.value,
2419                        &out,
2420                        &overridden_flags,
2421                        custom_env,
2422                    )
2423                });
2424            let unless_any = arg.required_unless.iter().any(|selector| {
2425                selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2426            });
2427            let unless_all = !arg.required_unless_all.is_empty()
2428                && arg.required_unless_all.iter().all(|selector| {
2429                    selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2430                });
2431            let required_unless = (!arg.required_unless.is_empty()
2432                || !arg.required_unless_all.is_empty())
2433                && !(unless_any || unless_all);
2434            if (arg.required
2435                || required_if
2436                || required_if_eq
2437                || required_if_eq_all
2438                || required_unless)
2439                && arg.default.is_empty()
2440            {
2441                // Check if there's an env var available (custom env map takes precedence)
2442                let has_env = arg
2443                    .env
2444                    .as_ref()
2445                    .is_some_and(|env_var| env_contains(custom_env, env_var));
2446                if !has_env {
2447                    out.errors.push(UsageErr::MissingArg(arg.name.clone()));
2448                }
2449            }
2450        }
2451    }
2452
2453    // Conflicts are a question about the invocation as a whole rather than about any one
2454    // token, so they are checked here beside the requirement checks rather than at the
2455    // point a flag is matched — the flag it conflicts with may still be ahead of it.
2456    // Its own loop: the requirement loop below skips the flags that *were* given, which
2457    // is exactly the set this needs.
2458    //
2459    // A value from the environment counts on both sides, matching what
2460    // `selector_is_explicit` says about the other flag: the question is whether a flag
2461    // has a value, not how it got one. That is what clap does, and an asymmetric rule
2462    // would make the same pair of flags a conflict or not depending on which one
2463    // happened to be typed.
2464    for flag in unique_flags(out.available_flags.values()) {
2465        let given = out.flags.contains_key(flag) || flag_has_env(flag, custom_env);
2466        if !given || overridden_flags.contains(&flag.name) {
2467            continue;
2468        }
2469        for other in &flag.conflicts {
2470            if selector_is_explicit(other, &out, &overridden_flags, custom_env) {
2471                out.errors.push(UsageErr::InvalidFlag {
2472                    token: format!("--{}", flag.name),
2473                    reason: format!("conflicts with {other}"),
2474                    span: (0, 0).into(),
2475                    input: format!("--{} {other}", flag.name),
2476                });
2477            }
2478        }
2479        // The positive form, checked in the same pass and under the same rule: a value
2480        // from the environment satisfies a requirement, because the question is whether
2481        // the other flag has a value rather than how it got one. A flag that was
2482        // overridden away has not been given, so it cannot satisfy anything either —
2483        // which is what `selector_is_explicit` already accounts for.
2484        //
2485        // Reported as the missing flag rather than as something wrong with the flag that
2486        // named it, which is what clap says too: an unmet `requires` is a required
2487        // argument that was not provided. Named by its own name, resolved through the
2488        // same matcher, so a `requires="-f"` reports `--force` rather than the selector.
2489        let owner = out
2490            .cmds
2491            .iter()
2492            .rposition(|cmd| cmd.flags.iter().any(|declared| declared.name == flag.name))
2493            .unwrap_or(out.cmds.len() - 1);
2494        if !exclusive_present && requirements_apply(owner) {
2495            for other in &flag.requires {
2496                if !selector_is_satisfied(other, &out, &overridden_flags, custom_env) {
2497                    let name = selector_flag_name(other, &out).unwrap_or_else(|| other.clone());
2498                    if other.starts_with('-') {
2499                        out.errors.push(UsageErr::MissingFlag(name));
2500                    } else {
2501                        out.errors.push(UsageErr::MissingArg(name));
2502                    }
2503                }
2504            }
2505            for condition in &flag.requires_if {
2506                if explicit_flag_has_value(flag, &condition.value, &out, custom_env)
2507                    && !selector_is_satisfied(
2508                        &condition.requires,
2509                        &out,
2510                        &overridden_flags,
2511                        custom_env,
2512                    )
2513                {
2514                    let name = selector_flag_name(&condition.requires, &out)
2515                        .unwrap_or_else(|| condition.requires.clone());
2516                    out.errors.push(UsageErr::MissingFlag(name));
2517                }
2518            }
2519        }
2520    }
2521
2522    // Positionals can declare the same pairwise conflict as flags. Their selector is
2523    // the bare argument name, while a flag keeps its dashed spelling.
2524    for (command_index, arg) in out
2525        .cmds
2526        .iter()
2527        .enumerate()
2528        .flat_map(|(index, cmd)| cmd.args.iter().map(move |arg| (index, arg)))
2529    {
2530        let given = arg_is_explicit(arg, &out, custom_env);
2531        if !given {
2532            continue;
2533        }
2534        for other in &arg.conflicts {
2535            if selector_is_explicit(other, &out, &overridden_flags, custom_env) {
2536                out.errors.push(UsageErr::InvalidFlag {
2537                    token: arg.name.clone(),
2538                    reason: format!("conflicts with {other}"),
2539                    span: (0, 0).into(),
2540                    input: format!("{} {other}", arg.name),
2541                });
2542            }
2543        }
2544        if !exclusive_present && requirements_apply(command_index) {
2545            for other in &arg.requires {
2546                if !selector_is_satisfied(other, &out, &overridden_flags, custom_env) {
2547                    let name = selector_flag_name(other, &out).unwrap_or_else(|| other.clone());
2548                    if other.starts_with('-') {
2549                        out.errors.push(UsageErr::MissingFlag(name));
2550                    } else {
2551                        out.errors.push(UsageErr::MissingArg(name));
2552                    }
2553                }
2554            }
2555        }
2556    }
2557
2558    // An exclusive flag is the whole-command form of a conflict: `--version` means the
2559    // rest of the line has nothing to act on. Everything the invocation supplied counts,
2560    // positionals included, which is what distinguishes it from being in a group with
2561    // every other flag.
2562    //
2563    // Only what was *given*, as `conflicts` reads it: a defaulted flag standing beside an
2564    // exclusive one is nobody saying anything, and counting it would make the exclusive
2565    // flag unusable on any command that has a default. Environment values do count, also as
2566    // `conflicts` reads them, so the spec parser and the derive agree.
2567    for flag in unique_flags(out.available_flags.values().chain(out.flags.keys())) {
2568        // `SpecFlag` equality is intentionally name-only for the public parsed-value map,
2569        // but re-declared aliases can leave distinct declarations with that same name in
2570        // scope. Exclusivity is about the declaration the typed spelling resolved to, so
2571        // compare the parser's `Arc`s by identity here.
2572        let given = flag_was_parsed(flag) || flag_has_env(flag, custom_env);
2573        if !exclusive_occurrence(flag) || !given || overridden_flags.contains(&flag.name) {
2574            continue;
2575        }
2576        let other_flag = unique_flags(out.available_flags.values().chain(out.flags.keys()))
2577            .find(|other| {
2578                !Arc::ptr_eq(other, flag)
2579                    && !overridden_flags.contains(&other.name)
2580                    && (flag_was_parsed(other) || flag_has_env(other, custom_env))
2581            })
2582            .map(|other| format!("--{}", other.name));
2583        let other_arg = out.cmd.args.iter().find(|arg| {
2584            out.args.keys().any(|given| given.name == arg.name)
2585                || arg
2586                    .env
2587                    .as_ref()
2588                    .is_some_and(|env| env_contains(custom_env, env))
2589        });
2590        // Selecting a child is company for an exclusive flag declared by an ancestor. An
2591        // exclusive flag belonging to the child itself does not conflict with the command word
2592        // needed to reach that child — so the question is not who owns the flag but whose
2593        // exclusivity is the one being enforced, which is what `exclusivity_in_play` already
2594        // separated.
2595        let (_, ancestor_exclusivity) = exclusivity_in_play(flag);
2596        let selected_subcommand =
2597            (out.cmds.len() > 1 && ancestor_exclusivity).then(|| out.cmd.name.clone());
2598        let other = other_flag
2599            .or_else(|| other_arg.map(|arg| format!("<{}>", arg.name)))
2600            .or(selected_subcommand);
2601        if let Some(other) = other {
2602            out.errors.push(UsageErr::InvalidFlag {
2603                token: format!("--{}", flag.name),
2604                reason: format!("must be given on its own, and {other} was given too"),
2605                span: (0, 0).into(),
2606                input: format!("--{} {other}", flag.name),
2607            });
2608        }
2609    }
2610
2611    // Groups, checked once per group rather than per flag: both questions a group asks —
2612    // how many members were given, and whether that is enough — are about the set, which
2613    // is the whole reason a group exists rather than a pile of pairwise conflicts.
2614    //
2615    // The same "given" rule as everything else here, so a member filled from the
2616    // environment or a default counts.
2617    // Every command in the chain, not only the selected one: a group may name global
2618    // flags, which belong to an ancestor and are declared there.
2619    let mut group_errors: Vec<UsageErr> = Vec::new();
2620    for (command_index, group) in out
2621        .cmds
2622        .iter()
2623        .enumerate()
2624        .flat_map(|(index, cmd)| cmd.groups.iter().map(move |group| (index, group)))
2625    {
2626        // Counted by the *flag* a selector resolves to, not by the selector. `-f` and
2627        // `--file` are two spellings of one flag, and a group naming both — or naming one
2628        // flag twice — would otherwise report that flag as conflicting with itself the
2629        // moment it was given. Deduplicated rather than refused where the group is
2630        // written, because listing both spellings is redundant, not wrong.
2631        let mut given: Vec<&str> = Vec::new();
2632        let mut seen: Vec<String> = Vec::new();
2633        for selector in &group.members {
2634            if !selector_is_explicit(selector, &out, &overridden_flags, custom_env) {
2635                continue;
2636            }
2637            let name = selector_flag_name(selector, &out).unwrap_or_else(|| selector.clone());
2638            if seen.contains(&name) {
2639                continue;
2640            }
2641            seen.push(name);
2642            given.push(selector.as_str());
2643        }
2644        if !group.multiple && given.len() > 1 {
2645            group_errors.push(UsageErr::InvalidFlag {
2646                token: given[1].to_string(),
2647                reason: format!("cannot be used with {} in group {}", given[0], group.name),
2648                span: (0, 0).into(),
2649                input: format!("{} {}", given[0], given[1]),
2650            });
2651        }
2652        // Requiredness is a *positive* rule, so it reads a default as filling a member —
2653        // the rule `requires` follows. That is also why it cannot reuse `given` above:
2654        // exclusivity must count only what was supplied, or a defaulted member would
2655        // collide with the sibling the user actually typed.
2656        let satisfied = group
2657            .members
2658            .iter()
2659            .any(|selector| selector_is_satisfied(selector, &out, &overridden_flags, custom_env));
2660        if group.required && requirements_apply(command_index) && !satisfied && !exclusive_present {
2661            // The members are what a user has to type, so they are in the message; the
2662            // group's name is there too, since a command with several groups would
2663            // otherwise report the same sentence twice with nothing to tell them apart.
2664            group_errors.push(UsageErr::MissingGroup {
2665                group: group.name.clone(),
2666                members: group.members.join(", "),
2667            });
2668        }
2669    }
2670    out.errors.extend(group_errors);
2671
2672    if !exclusive_present {
2673        for flag in unique_flags(out.available_flags.values()) {
2674            let owner = out
2675                .cmds
2676                .iter()
2677                .rposition(|cmd| cmd.flags.iter().any(|declared| declared.name == flag.name))
2678                .unwrap_or(out.cmds.len() - 1);
2679            if !requirements_apply(owner) {
2680                continue;
2681            }
2682            if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
2683                continue;
2684            }
2685            let has_default =
2686                !flag.default.is_empty() || flag.arg.iter().any(|a| !a.default.is_empty());
2687            let has_env = flag_has_env(flag, custom_env);
2688            let required_if = flag.required_if.iter().any(|selector| {
2689                selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2690            });
2691            let required_if_eq = flag.required_if_eq.iter().any(|condition| {
2692                selector_explicit_has_value(
2693                    &condition.selector,
2694                    &condition.value,
2695                    &out,
2696                    &overridden_flags,
2697                    custom_env,
2698                )
2699            });
2700            let required_if_eq_all = !flag.required_if_eq_all.is_empty()
2701                && flag.required_if_eq_all.iter().all(|condition| {
2702                    selector_explicit_has_value(
2703                        &condition.selector,
2704                        &condition.value,
2705                        &out,
2706                        &overridden_flags,
2707                        custom_env,
2708                    )
2709                });
2710            let unless_any = flag.required_unless.iter().any(|selector| {
2711                selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2712            });
2713            let unless_all = !flag.required_unless_all.is_empty()
2714                && flag.required_unless_all.iter().all(|selector| {
2715                    selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2716                });
2717            let required_unless = (!flag.required_unless.is_empty()
2718                || !flag.required_unless_all.is_empty())
2719                && !(unless_any || unless_all);
2720            if (flag.required
2721                || required_if
2722                || required_if_eq
2723                || required_if_eq_all
2724                || required_unless)
2725                && !has_default
2726                && !has_env
2727            {
2728                out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
2729            }
2730        }
2731    }
2732
2733    // Validate var_min/var_max constraints for variadic args
2734    for (arg, value) in &out.args {
2735        if arg.var {
2736            if let ParseValue::MultiString(values) = value {
2737                if let Some(min) = arg.var_min {
2738                    if values.len() < min {
2739                        out.errors.push(UsageErr::VarArgTooFew {
2740                            name: arg.name.clone(),
2741                            min,
2742                            got: values.len(),
2743                        });
2744                    }
2745                }
2746                if let Some(max) = arg.var_max {
2747                    if values.len() > max {
2748                        out.errors.push(UsageErr::VarArgTooMany {
2749                            name: arg.name.clone(),
2750                            max,
2751                            got: values.len(),
2752                        });
2753                    }
2754                }
2755            }
2756        }
2757    }
2758
2759    // Validate var_min/var_max constraints for variadic flags. These are bounds on
2760    // repeated occurrences of the flag itself. Bounds on its nested argument are enforced
2761    // by binding once per occurrence, where the per-occurrence count is still available.
2762    for flag in unique_flags(out.available_flags.values()) {
2763        if flag.var {
2764            let bound = match out.flags.get(flag) {
2765                Some(ParseValue::MultiString(values)) => values.len(),
2766                Some(ParseValue::MultiBool(values)) => values.len(),
2767                Some(_) => 1,
2768                None => 0,
2769            };
2770            // A partial parse deliberately leaves the final value-optional flag pending so
2771            // completion can still answer for it. It is nevertheless a real occurrence for
2772            // the repeated flag's bounds; the full parser closes it just after this phase.
2773            let pending = out
2774                .flag_awaiting_value
2775                .iter()
2776                .filter(|pending| {
2777                    Arc::ptr_eq(pending, flag)
2778                        && (pending.value_optional || pending.default_missing.is_some())
2779                })
2780                .count();
2781            let count = bound + pending;
2782            if count == 0 {
2783                continue;
2784            }
2785            if let Some(min) = flag.var_min {
2786                if count < min {
2787                    out.errors.push(UsageErr::VarFlagTooFew {
2788                        name: flag.name.clone(),
2789                        min,
2790                        got: count,
2791                    });
2792                }
2793            }
2794            if let Some(max) = flag.var_max {
2795                if count > max {
2796                    out.errors.push(UsageErr::VarFlagTooMany {
2797                        name: flag.name.clone(),
2798                        max,
2799                        got: count,
2800                    });
2801                }
2802            }
2803        }
2804    }
2805
2806    Ok((out, overridden_flags))
2807}
2808
2809fn validate_expression(
2810    name: &str,
2811    expression: Option<&str>,
2812    message: Option<&str>,
2813    parsed: &ParseValue,
2814    errors: &mut Vec<UsageErr>,
2815) {
2816    let Some(expression) = expression else {
2817        return;
2818    };
2819    #[cfg(not(feature = "validation"))]
2820    let _ = expression;
2821    let values: &[String] = match parsed {
2822        ParseValue::String(value) => std::slice::from_ref(value),
2823        ParseValue::MultiString(values) => values,
2824        ParseValue::Bool(_) | ParseValue::MultiBool(_) => return,
2825    };
2826    #[cfg(feature = "validation")]
2827    for value in values {
2828        let reason = match usage_validation::validate(expression, value) {
2829            Ok(true) => continue,
2830            Ok(false) => message
2831                .unwrap_or("does not satisfy the validation expression")
2832                .to_string(),
2833            Err(error) => format!("validation expression failed: {error}"),
2834        };
2835        errors.push(UsageErr::InvalidValue {
2836            name: name.to_string(),
2837            value: value.clone(),
2838            reason,
2839        });
2840        break;
2841    }
2842    #[cfg(not(feature = "validation"))]
2843    if let Some(value) = values.first() {
2844        let _ = message;
2845        errors.push(UsageErr::InvalidValue {
2846            name: name.to_string(),
2847            value: value.clone(),
2848            reason: "expression validation requires the `validation` feature".to_string(),
2849        });
2850    }
2851}
2852
2853#[cfg(all(test, not(feature = "validation")))]
2854mod optional_validation_tests {
2855    use crate::{parse, Spec};
2856
2857    #[test]
2858    fn validation_declarations_require_the_opt_in_runtime_feature() {
2859        let spec: Spec = r#"
2860name "ex"
2861bin "ex"
2862arg "<port>" validate="int(value) > 0"
2863        "#
2864        .parse()
2865        .unwrap();
2866        let error = parse(&spec, &["ex".to_string(), "1".to_string()]).unwrap_err();
2867        assert!(
2868            error
2869                .to_string()
2870                .contains("requires the `validation` feature"),
2871            "{error:?}"
2872        );
2873    }
2874}
2875
2876fn flag_matches_selector(flag: &SpecFlag, selector: &str) -> bool {
2877    flag.name == selector || flag_keys(flag).iter().any(|key| key == selector)
2878}
2879
2880fn flags_override(overrider: &SpecFlag, overridden: &SpecFlag) -> bool {
2881    overrider
2882        .overrides
2883        .iter()
2884        .any(|selector| flag_matches_selector(overridden, selector))
2885}
2886
2887fn apply_prefix_flag_overrides(
2888    prefix_flags: &mut Vec<(Arc<SpecFlag>, Vec<String>)>,
2889    flag: Arc<SpecFlag>,
2890) {
2891    prefix_flags
2892        .retain(|(other, _)| !(flags_override(&flag, other) || flags_override(other, &flag)));
2893}
2894
2895fn mount_prefix_words(prefix_flags: &[(Arc<SpecFlag>, Vec<String>)]) -> Vec<String> {
2896    prefix_flags
2897        .iter()
2898        .flat_map(|(_, words)| words.iter().cloned())
2899        .collect()
2900}
2901
2902fn env_contains(custom_env: Option<&HashMap<String, String>>, env_var: &str) -> bool {
2903    match custom_env {
2904        Some(env) => env.contains_key(env_var),
2905        None => std::env::var(env_var).is_ok(),
2906    }
2907}
2908
2909fn flag_has_env(flag: &SpecFlag, custom_env: Option<&HashMap<String, String>>) -> bool {
2910    flag.env_names()
2911        .any(|env_var| env_contains(custom_env, env_var))
2912}
2913
2914fn fallback_is_true(value: &str) -> bool {
2915    matches!(value, "1" | "true" | "True" | "TRUE")
2916}
2917
2918fn split_fallback_values(values: &[String], delimiter: Option<char>) -> Vec<String> {
2919    match delimiter {
2920        Some(delimiter) => values
2921            .iter()
2922            .flat_map(|value| value.split(delimiter).map(str::to_string))
2923            .collect(),
2924        None => values.to_vec(),
2925    }
2926}
2927
2928fn validate_arg_fallback_count(arg: &SpecArg, count: usize, errors: &mut Vec<UsageErr>) {
2929    if let Some(min) = arg.var_min {
2930        if count < min {
2931            errors.push(UsageErr::VarArgTooFew {
2932                name: arg.name.clone(),
2933                min,
2934                got: count,
2935            });
2936        }
2937    }
2938    if let Some(max) = arg.var_max {
2939        if count > max {
2940            errors.push(UsageErr::VarArgTooMany {
2941                name: arg.name.clone(),
2942                max,
2943                got: count,
2944            });
2945        }
2946    }
2947}
2948
2949fn validate_flag_fallback_count(flag: &SpecFlag, count: usize, errors: &mut Vec<UsageErr>) {
2950    if let Some(min) = flag.var_min {
2951        if count < min {
2952            errors.push(UsageErr::VarFlagTooFew {
2953                name: flag.name.clone(),
2954                min,
2955                got: count,
2956            });
2957        }
2958    }
2959    if let Some(max) = flag.var_max {
2960        if count > max {
2961            errors.push(UsageErr::VarFlagTooMany {
2962                name: flag.name.clone(),
2963                max,
2964                got: count,
2965            });
2966        }
2967    }
2968}
2969
2970fn validate_flag_arg_fallback_count(
2971    flag: &SpecFlag,
2972    arg: &SpecArg,
2973    count: usize,
2974    errors: &mut Vec<UsageErr>,
2975) {
2976    if let Some(min) = arg.var_min {
2977        if count < min {
2978            errors.push(UsageErr::VarFlagTooFew {
2979                name: flag.name.clone(),
2980                min,
2981                got: count,
2982            });
2983        }
2984    }
2985    if let Some(max) = arg.var_max {
2986        if count > max {
2987            errors.push(UsageErr::VarFlagTooMany {
2988                name: flag.name.clone(),
2989                max,
2990                got: count,
2991            });
2992        }
2993    }
2994}
2995
2996/// Bind a fallback the way an unconditional `default` does: one value, or several
2997/// for `var`, and choices checked the same way.
2998fn bind_flag_fallback(
2999    flag: &Arc<SpecFlag>,
3000    values: &[String],
3001    out: &mut ParseOutput,
3002    custom_env: Option<&HashMap<String, String>>,
3003    origin: ValueOrigin,
3004) -> Result<(), miette::Error> {
3005    if values.is_empty() {
3006        return Ok(());
3007    }
3008    if let Some(arg) = flag.arg.as_ref() {
3009        let values = split_fallback_values(values, arg.delimiter);
3010        if flag.var || arg.var {
3011            if flag.var {
3012                validate_flag_fallback_count(flag, values.len(), &mut out.errors);
3013            }
3014            if arg.var {
3015                validate_flag_arg_fallback_count(flag, arg, values.len(), &mut out.errors);
3016            }
3017            validate_choice_values(
3018                ChoiceTarget::option(flag),
3019                &values,
3020                arg.choices.as_ref(),
3021                custom_env,
3022            )?;
3023            out.flags
3024                .insert(Arc::clone(flag), ParseValue::MultiString(values));
3025        } else {
3026            let value = values.into_iter().next().unwrap_or_default();
3027            validate_choice_value(
3028                ChoiceTarget::option(flag),
3029                &value,
3030                arg.choices.as_ref(),
3031                custom_env,
3032            )?;
3033            out.flags
3034                .insert(Arc::clone(flag), ParseValue::String(value));
3035        }
3036    } else if flag.var {
3037        validate_flag_fallback_count(flag, values.len(), &mut out.errors);
3038        let bools: Vec<bool> = values.iter().map(|s| fallback_is_true(s)).collect();
3039        out.flags
3040            .insert(Arc::clone(flag), ParseValue::MultiBool(bools));
3041    } else {
3042        out.flags.insert(
3043            Arc::clone(flag),
3044            ParseValue::Bool(fallback_is_true(&values[0])),
3045        );
3046    }
3047    out.flag_origins
3048        .entry(Arc::clone(flag))
3049        .or_default()
3050        .push(origin);
3051    Ok(())
3052}
3053
3054fn default_if_condition_matches(
3055    condition: &crate::SpecDefaultIf,
3056    out: &ParseOutput,
3057    overridden_flags: &HashSet<String>,
3058    custom_env: Option<&HashMap<String, String>>,
3059) -> bool {
3060    match &condition.when {
3061        None => selector_is_explicit(&condition.selector, out, overridden_flags, custom_env),
3062        Some(when) => {
3063            let Some(flag) = out
3064                .available_flags
3065                .values()
3066                .chain(out.flags.keys())
3067                .find(|flag| flag_matches_selector(flag, &condition.selector))
3068            else {
3069                return false;
3070            };
3071            if overridden_flags.contains(&flag.name) {
3072                return false;
3073            }
3074            explicit_flag_has_value(flag, when, out, custom_env)
3075        }
3076    }
3077}
3078
3079/// Whether an explicitly supplied value of `flag` equals `expected`.
3080///
3081/// clap treats command-line and environment values as explicit for `requires_if`, but
3082/// not defaults. Keep that source distinction here instead of consulting the flag's
3083/// defaults through `selector_is_satisfied`.
3084fn explicit_flag_has_value(
3085    flag: &SpecFlag,
3086    expected: &str,
3087    out: &ParseOutput,
3088    custom_env: Option<&HashMap<String, String>>,
3089) -> bool {
3090    let parsed_matches = out.flags.get(flag).is_some_and(|value| match value {
3091        ParseValue::Bool(value) => value.to_string() == expected,
3092        ParseValue::String(value) => value == expected,
3093        ParseValue::MultiBool(values) => values.iter().any(|value| value.to_string() == expected),
3094        ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3095    });
3096    if out.flags.contains_key(flag) {
3097        return parsed_matches;
3098    }
3099
3100    let value = flag.env_names().find_map(|env| match custom_env {
3101        Some(values) => values.get(env).cloned(),
3102        None => std::env::var(env).ok(),
3103    });
3104    value.is_some_and(
3105        |value| match flag.arg.as_ref().and_then(|arg| arg.delimiter) {
3106            Some(delimiter) => value.split(delimiter).any(|value| value == expected),
3107            None if flag.arg.is_none() => {
3108                matches!(value.as_str(), "1" | "true" | "True" | "TRUE").to_string() == expected
3109            }
3110            None => value == expected,
3111        },
3112    )
3113}
3114
3115fn selector_explicit_has_value(
3116    selector: &str,
3117    expected: &str,
3118    out: &ParseOutput,
3119    overridden_flags: &HashSet<String>,
3120    custom_env: Option<&HashMap<String, String>>,
3121) -> bool {
3122    if let Some(flag) = out
3123        .available_flags
3124        .values()
3125        .chain(out.flags.keys())
3126        .find(|flag| flag_matches_selector(flag, selector))
3127    {
3128        return !overridden_flags.contains(&flag.name)
3129            && explicit_flag_has_value(flag, expected, out, custom_env);
3130    }
3131    let Some(arg) = selector_arg(selector, out) else {
3132        return false;
3133    };
3134    let parsed = out
3135        .args
3136        .iter()
3137        .find(|(given, _)| given.name == arg.name)
3138        .map(|(_, value)| value);
3139    if let Some(value) = parsed {
3140        return match value {
3141            ParseValue::String(value) => value == expected,
3142            ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3143            ParseValue::Bool(value) => value.to_string() == expected,
3144            ParseValue::MultiBool(values) => {
3145                values.iter().any(|value| value.to_string() == expected)
3146            }
3147        };
3148    }
3149    let value = arg.env_names().find_map(|env| match custom_env {
3150        Some(values) => values.get(env).cloned(),
3151        None => std::env::var(env).ok(),
3152    });
3153    value.is_some_and(|value| match arg.delimiter {
3154        Some(delimiter) => value.split(delimiter).any(|value| value == expected),
3155        None => value == expected,
3156    })
3157}
3158
3159fn selector_is_explicit(
3160    selector: &str,
3161    out: &ParseOutput,
3162    overridden_flags: &HashSet<String>,
3163    custom_env: Option<&HashMap<String, String>>,
3164) -> bool {
3165    let flag_is_explicit = out
3166        .available_flags
3167        .values()
3168        .chain(out.flags.keys())
3169        .any(|flag| {
3170            flag_matches_selector(flag, selector)
3171                && !overridden_flags.contains(&flag.name)
3172                && (out.flags.contains_key(flag) || flag_has_env(flag, custom_env))
3173        });
3174    flag_is_explicit
3175        || selector_arg(selector, out).is_some_and(|arg| arg_is_explicit(arg, out, custom_env))
3176}
3177
3178/// The name of the flag a selector points at, for an error that has to name it.
3179///
3180/// `selector_is_explicit` only answers yes or no, which is all a check needs; a message
3181/// about a flag that is *missing* has to say which one, and the selector may be a short
3182/// form or an alias rather than the name.
3183fn selector_flag_name(selector: &str, out: &ParseOutput) -> Option<String> {
3184    out.available_flags
3185        .values()
3186        .chain(out.flags.keys())
3187        .find(|flag| flag_matches_selector(flag, selector))
3188        .map(|flag| flag.name.clone())
3189        .or_else(|| selector_arg(selector, out).map(|arg| arg.name.clone()))
3190}
3191
3192/// Whether a selector's flag ended up with a value, however it got one.
3193///
3194/// The rule for a *positive* relationship, and the difference from
3195/// [`selector_is_explicit`] is deliberate. A negative rule — `conflicts`, or a group's
3196/// exclusivity — has to count only what was given, or a flag with a default would
3197/// conflict with everything and no command line would parse. A positive one asks whether
3198/// the flag it names has a value, and a default is a value: that is already how plain
3199/// `required`, `required_if` and `required_unless` read a default, and `requires` saying
3200/// otherwise would have made the same flag missing here and present ten lines below.
3201fn selector_is_satisfied(
3202    selector: &str,
3203    out: &ParseOutput,
3204    overridden_flags: &HashSet<String>,
3205    custom_env: Option<&HashMap<String, String>>,
3206) -> bool {
3207    if selector_is_explicit(selector, out, overridden_flags, custom_env) {
3208        return true;
3209    }
3210    let flag_is_satisfied = out
3211        .available_flags
3212        .values()
3213        .chain(out.flags.keys())
3214        .filter(|flag| flag_matches_selector(flag, selector))
3215        .any(|flag| {
3216            !overridden_flags.contains(&flag.name)
3217                && (!flag.default.is_empty()
3218                    || flag.arg.iter().any(|a| !a.default.is_empty())
3219                    || flag.default_if.iter().any(|condition| {
3220                        default_if_condition_matches(condition, out, overridden_flags, custom_env)
3221                    }))
3222        });
3223    flag_is_satisfied || selector_arg(selector, out).is_some_and(|arg| !arg.default.is_empty())
3224}
3225
3226fn selector_arg<'a>(selector: &str, out: &'a ParseOutput) -> Option<&'a SpecArg> {
3227    // Bare words are positional selectors. Keep accepting a flag's internal name above
3228    // for existing specs; when both exist, the dashed flag spelling removes ambiguity.
3229    if selector.starts_with('-') {
3230        return None;
3231    }
3232    out.cmds
3233        .iter()
3234        .flat_map(|cmd| &cmd.args)
3235        .find(|arg| arg.name == selector)
3236}
3237
3238fn arg_is_explicit(
3239    arg: &SpecArg,
3240    out: &ParseOutput,
3241    custom_env: Option<&HashMap<String, String>>,
3242) -> bool {
3243    out.args.keys().any(|given| given.name == arg.name)
3244        || arg
3245            .env
3246            .as_ref()
3247            .is_some_and(|env| env_contains(custom_env, env))
3248}
3249
3250fn apply_flag_overrides(
3251    flag: &Arc<SpecFlag>,
3252    available_flags: &BTreeMap<String, Arc<SpecFlag>>,
3253    parsed_flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
3254    pending_flags: &mut Vec<Arc<SpecFlag>>,
3255    overridden_flags: &mut HashSet<String>,
3256    // The reportable half of the same fact: which flag did the overriding. The set above
3257    // only stops a default or an environment value restoring what was overridden, and
3258    // "`--quiet` is unset despite its default" has no answer without the name.
3259    attributed: &mut BTreeMap<String, String>,
3260) {
3261    let overridden_names: HashSet<String> = available_flags
3262        .values()
3263        .chain(parsed_flags.keys())
3264        .filter(|other| flags_override(flag, other) || flags_override(other, flag))
3265        .map(|other| other.name.clone())
3266        .collect();
3267
3268    parsed_flags.retain(|parsed, _| !overridden_names.contains(&parsed.name));
3269    pending_flags.retain(|pending| !overridden_names.contains(&pending.name));
3270    for name in &overridden_names {
3271        attributed.insert(name.clone(), flag.name.clone());
3272    }
3273    overridden_flags.extend(overridden_names);
3274    // An explicit occurrence always restores this flag, including self-overrides.
3275    overridden_flags.remove(&flag.name);
3276    attributed.remove(&flag.name);
3277}
3278
3279#[cfg(feature = "cli-help")]
3280fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool) -> UsageErr {
3281    UsageErr::Help(docs::cli::render_help(spec, cmd, long))
3282}
3283
3284#[cfg(feature = "cli-help")]
3285fn render_help_all_err(spec: &Spec, cmd: &SpecCommand) -> UsageErr {
3286    fn append(out: &mut String, spec: &Spec, cmd: &SpecCommand) {
3287        if !out.is_empty() {
3288            out.push('\n');
3289        }
3290        out.push_str(&docs::cli::render_help(spec, cmd, true));
3291        let mut children: Vec<_> = cmd
3292            .subcommands
3293            .values()
3294            .filter(|child| !child.hide)
3295            .collect();
3296        children.sort_by_key(|child| (child.display_order.unwrap_or(999), child.name.as_str()));
3297        for child in children {
3298            append(out, spec, child);
3299        }
3300    }
3301
3302    let mut out = String::new();
3303    append(&mut out, spec, cmd);
3304    UsageErr::Help(out)
3305}
3306
3307#[cfg(not(feature = "cli-help"))]
3308fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool) -> UsageErr {
3309    UsageErr::Help("help".to_string())
3310}
3311
3312#[cfg(not(feature = "cli-help"))]
3313fn render_help_all_err(_spec: &Spec, _cmd: &SpecCommand) -> UsageErr {
3314    UsageErr::Help("help".to_string())
3315}
3316
3317/// The version to answer with. `--version` prefers the long text and `-V` the concise
3318/// one, each falling back to the other when only one is declared.
3319fn render_version_err(spec: &Spec, long: bool) -> UsageErr {
3320    let value = if long {
3321        spec.long_version.as_ref().or(spec.version.as_ref())
3322    } else {
3323        spec.version.as_ref().or(spec.long_version.as_ref())
3324    };
3325    UsageErr::Version(value.cloned().unwrap_or_default())
3326}
3327
3328fn render_action_err(spec: &Spec, cmd: &SpecCommand, flag: &SpecFlag, spelling: &str) -> UsageErr {
3329    use crate::SpecFlagAction;
3330    match flag.action {
3331        SpecFlagAction::Help => render_help_err(spec, cmd, spelling.starts_with("--")),
3332        SpecFlagAction::HelpShort => render_help_err(spec, cmd, false),
3333        SpecFlagAction::HelpLong => render_help_err(spec, cmd, true),
3334        SpecFlagAction::HelpAll => render_help_all_err(spec, cmd),
3335        SpecFlagAction::Version => render_version_err(spec, spelling.starts_with("--")),
3336        SpecFlagAction::Set => unreachable!("binding actions are handled before this helper"),
3337    }
3338}
3339
3340#[derive(Copy, Clone)]
3341struct ChoiceTarget<'a> {
3342    kind: &'a str,
3343    name: &'a str,
3344}
3345
3346impl<'a> ChoiceTarget<'a> {
3347    fn arg(arg: &'a SpecArg) -> Self {
3348        Self {
3349            kind: "arg",
3350            name: &arg.name,
3351        }
3352    }
3353
3354    fn option(flag: &'a SpecFlag) -> Self {
3355        Self {
3356            kind: "option",
3357            name: &flag.name,
3358        }
3359    }
3360}
3361
3362/// Whether every letter of a short token names a flag in scope.
3363///
3364/// Scanning stops at the first letter whose flag takes a value, because everything
3365/// after it is that value rather than more letters.
3366fn short_bundle_is_known(
3367    spec: &Spec,
3368    cmds: &[SpecCommand],
3369    available: &BTreeMap<String, Arc<SpecFlag>>,
3370    token: &str,
3371) -> bool {
3372    for c in token.chars().skip(1) {
3373        match available.get(&format!("-{c}")) {
3374            // `-h` and `-V` are recognized letters even though no spec declares them, so a
3375            // bundle containing one is a bundle. Without this `-vh` was not read as one at
3376            // all and fell through to `unexpected word`, while usage-argv, usage-go and
3377            // clap all answer it with help.
3378            None if supplied_short(spec, cmds, c).is_some() => {}
3379            None => return false,
3380            Some(f) if f.arg.is_some() => return true,
3381            Some(_) => {}
3382        }
3383    }
3384    true
3385}
3386
3387/// The response `-h` or `-V` produces where nothing declares that letter.
3388///
3389/// The letter form of the flags the parser supplies rather than a spec declaring them,
3390/// under exactly the conditions [`is_help_arg`] and [`is_version_arg`] state — asked
3391/// about here one letter at a time, because a bundle is read one letter at a time.
3392///
3393/// Always the short response: `-h` is short help however many letters share its token,
3394/// and `-V` the concise version. The long forms belong to the long spellings. `-?` is not
3395/// here — it is a whole-token spelling of `-h` rather than a letter anyone bundles.
3396fn supplied_short(spec: &Spec, cmds: &[SpecCommand], letter: char) -> Option<UsageErr> {
3397    let cmd = cmds.last()?;
3398    match letter {
3399        'h' if is_help_arg(spec, cmd, "-h") => Some(render_help_err(spec, cmd, false)),
3400        'V' if is_version_arg(spec, cmds, "-V") => Some(render_version_err(spec, false)),
3401        _ => None,
3402    }
3403}
3404
3405/// Refuse a flag-like token that named nothing, if this command asked for that.
3406///
3407/// Called from the flag branches, where the lookup has just failed and nothing from
3408/// the token has been applied yet — so a bundle like `-az` is refused whole rather
3409/// than after setting `-a`.
3410fn reject_unknown_flag_if_asked(
3411    spec: &Spec,
3412    path: &[SpecCommand],
3413    token: &str,
3414) -> Result<(), UsageErr> {
3415    // A lone `-` is a value by convention. A negative number reaches this only
3416    // when no pending value opted into the narrower exception.
3417    if !is_flag_like(token) {
3418        return Ok(());
3419    }
3420    if effective_unknown_flags(spec, path) != UnknownFlags::Error {
3421        return Ok(());
3422    }
3423    Err(UsageErr::InvalidFlag {
3424        token: token.to_string(),
3425        reason: "no such flag".to_string(),
3426        span: (0, 0).into(),
3427        input: token.to_string(),
3428    })
3429}
3430
3431/// Whether a flag-like token that matches nothing is a value or an error, here.
3432///
3433/// The nearest enclosing command that stated a preference wins, then the spec,
3434/// then the default. Inherited, unlike `effect`: it describes how a command line
3435/// is read, and a CLI that forwards options tends to forward them at every level.
3436fn effective_unknown_flags(spec: &Spec, path: &[SpecCommand]) -> UnknownFlags {
3437    path.iter()
3438        .rev()
3439        .find_map(|cmd| cmd.unknown_flags)
3440        .or(spec.unknown_flags)
3441        .unwrap_or_default()
3442}
3443
3444/// Whether a token would be read as a flag, for the purpose of rejecting unknown
3445/// ones.
3446///
3447/// A lone `-` is a value by convention. Other dash-prefixed tokens are flag-like;
3448/// a field may make the narrower negative-number exception.
3449fn is_flag_like(token: &str) -> bool {
3450    match token.strip_prefix('-') {
3451        None | Some("") => false,
3452        Some(_) => true,
3453    }
3454}
3455
3456fn is_negative_number(token: &str) -> bool {
3457    token.strip_prefix('-').is_some_and(is_number)
3458}
3459
3460fn record_scalar_flag_occurrence(
3461    cmds: &[SpecCommand],
3462    flag: &Arc<SpecFlag>,
3463    command_level: usize,
3464    bool_value: Option<bool>,
3465    occurrences: &mut HashMap<(usize, usize), u8>,
3466    errors: &mut Vec<UsageErr>,
3467) {
3468    let strict = cmds
3469        .get(command_level)
3470        .is_some_and(|cmd| !cmd.args_override_self);
3471    let collects_values = flag.var || flag.arg.as_ref().is_some_and(|arg| arg.var);
3472    if !strict || flag.count || collects_values {
3473        return;
3474    }
3475
3476    let bit = match bool_value {
3477        Some(false) if flag.negate.is_some() => 0b10,
3478        _ => 0b01,
3479    };
3480    let key = (Arc::as_ptr(flag) as usize, command_level);
3481    let seen = occurrences.entry(key).or_default();
3482    if *seen & bit != 0 {
3483        errors.push(UsageErr::DuplicateFlag(flag.name.clone()));
3484    }
3485    *seen |= bit;
3486}
3487
3488/// A token that can select a subcommand, trigger a mount, or be forwarded as an
3489/// external command.
3490///
3491/// Flag-like tokens are not words. A lone `-` is a value — conventionally stdin —
3492/// so it was never a candidate to *select* anything either. usage-argv uses the
3493/// same rule; without it, `-1` skipped the external-subcommand path because Phase 1
3494/// treated every token that `starts_with('-')` as a flag.
3495fn is_command_word(token: &str) -> bool {
3496    (!is_flag_like(token) || is_negative_number(token)) && token != "-"
3497}
3498
3499/// Whether an otherwise numeric-looking token is an exact declared short flag.
3500///
3501/// This check belongs in both parse phases: phase 1 must skip the flag while it
3502/// searches for a later subcommand, and phase 2 must bind it instead of offering
3503/// it to an `allow_negative_numbers` positional.
3504fn declared_numeric_short(available_flags: &BTreeMap<String, Arc<SpecFlag>>, token: &str) -> bool {
3505    token.len() == 2 && token.as_bytes()[1].is_ascii_digit() && available_flags.contains_key(token)
3506}
3507
3508/// Whether an unmatched word belongs to the root's default command.
3509///
3510/// Ordinary words always do. A negative number only does when the default command's
3511/// first positional explicitly accepts one; otherwise it stays at the root, matching
3512/// usage-argv and generated Go.
3513fn default_accepts_word(cmd: &SpecCommand, default_name: &str, token: &str) -> bool {
3514    !is_negative_number(token)
3515        || cmd
3516            .find_subcommand(default_name)
3517            .and_then(|default| default.args.first())
3518            .is_some_and(|arg| arg.allow_negative_numbers)
3519}
3520
3521/// Digits, at most one `.`, and an optional exponent.
3522///
3523/// Spelled out rather than deferred to `f64::from_str`, which also accepts `inf` and
3524/// `NaN`: `-inf` is far likelier to be a misspelled flag than a number somebody meant
3525/// to pass. usage-argv implements the same rule, and the corpus pins the edges — the
3526/// two disagreed about `-1e5` when one used a float parse and the other did not.
3527fn is_number(rest: &str) -> bool {
3528    let (mantissa, exponent) = match rest.find(['e', 'E']) {
3529        Some(at) => (&rest[..at], Some(&rest[at + 1..])),
3530        None => (rest, None),
3531    };
3532
3533    let mut seen_digit = false;
3534    let mut seen_dot = false;
3535    for c in mantissa.chars() {
3536        match c {
3537            '0'..='9' => seen_digit = true,
3538            '.' if !seen_dot => seen_dot = true,
3539            _ => return false,
3540        }
3541    }
3542    if !seen_digit {
3543        return false;
3544    }
3545
3546    match exponent {
3547        None => true,
3548        Some(exp) => {
3549            let digits = exp
3550                .strip_prefix('+')
3551                .or_else(|| exp.strip_prefix('-'))
3552                .unwrap_or(exp);
3553            !digits.is_empty() && digits.chars().all(|c| c.is_ascii_digit())
3554        }
3555    }
3556}
3557
3558/// Bind one value to the flag waiting for it, and let a variadic argument go on
3559/// collecting from the words that follow.
3560///
3561/// Every route to a flag's value comes through here — the following word, the text
3562/// after an `=`, and the token a `allow_hyphen_values` flag takes whatever it looks
3563/// like — so that all three agree on how many values the flag ends up with.
3564#[allow(clippy::too_many_arguments)]
3565fn bind_pending_flag_value(
3566    spec: &Spec,
3567    cmd: &SpecCommand,
3568    errors: &mut Vec<UsageErr>,
3569    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
3570    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
3571    word: &mut String,
3572    input: &mut VecDeque<Token>,
3573    custom_env: Option<&HashMap<String, String>>,
3574    trace: &mut Trace,
3575    // Which token supplied `word`, and whether it rode along on the flag's own token
3576    // (`--jobs=8`, `-j8`) rather than following it. A variadic run's later words carry
3577    // their own positions and are recorded where they are read.
3578    argv: usize,
3579    attached: bool,
3580) -> miette::Result<bool> {
3581    // Held before the drain pops it, along with what the flag is already carrying: a
3582    // `var_max` bounds the values this occurrence takes, not the list they are appended
3583    // to, so a second `--include` starts counting again.
3584    let collecting = flag_awaiting_value
3585        .last()
3586        .filter(|flag| flag.arg.as_ref().is_some_and(|arg| arg.var))
3587        .cloned()
3588        .map(|flag| {
3589            let carried = flags.get(&flag).map(value_count).unwrap_or(0);
3590            (flag, carried)
3591        });
3592    let mut bound = vec![];
3593    let refused = drain_pending_flag_values(
3594        spec,
3595        cmd,
3596        errors,
3597        flags,
3598        flag_awaiting_value,
3599        word,
3600        custom_env,
3601        &mut bound,
3602    )?;
3603    for (flag, values) in bound {
3604        trace.record(
3605            argv,
3606            TokenRole::Value {
3607                flag,
3608                values,
3609                attached,
3610            },
3611        );
3612    }
3613    if refused {
3614        return Ok(true);
3615    }
3616    let Some((flag, carried)) = collecting else {
3617        return Ok(false);
3618    };
3619    collect_variadic_flag_values(
3620        spec,
3621        cmd,
3622        errors,
3623        flags,
3624        flag_awaiting_value,
3625        &flag,
3626        carried,
3627        input,
3628        custom_env,
3629        trace,
3630    )
3631}
3632
3633/// Keep feeding a flag whose argument is variadic from the words that follow it.
3634///
3635/// `--include <pattern>...` collects from a single occurrence, so it takes tokens until
3636/// one is flag-like, a `--` arrives, its `var_max` is reached, or the command line ends.
3637/// This is greedy by design — a command declaring both such a flag and positionals will
3638/// find the flag eating them, and `--` or a `var_max` is how the run is stopped.
3639///
3640/// `carried` is what the flag already held when this occurrence began, so the bound
3641/// counts this run rather than everything the flag has collected across the command
3642/// line. Each value goes through the same drain as the first, so choices are checked
3643/// and the value lands in the same list rather than by a second route that could
3644/// disagree.
3645#[allow(clippy::too_many_arguments)]
3646fn collect_variadic_flag_values(
3647    spec: &Spec,
3648    cmd: &SpecCommand,
3649    errors: &mut Vec<UsageErr>,
3650    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
3651    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
3652    flag: &Arc<SpecFlag>,
3653    carried: usize,
3654    input: &mut VecDeque<Token>,
3655    custom_env: Option<&HashMap<String, String>>,
3656    trace: &mut Trace,
3657) -> miette::Result<bool> {
3658    let max = flag
3659        .arg
3660        .as_ref()
3661        .and_then(|arg| arg.var_max)
3662        .unwrap_or(usize::MAX);
3663    while flags
3664        .get(flag)
3665        .map(value_count)
3666        .unwrap_or(0)
3667        .saturating_sub(carried)
3668        < max
3669    {
3670        let Some(next) = input.front().map(|token| token.word.as_str()) else {
3671            break;
3672        };
3673        if flag
3674            .arg
3675            .as_ref()
3676            .and_then(|arg| arg.value_terminator.as_deref())
3677            == Some(next)
3678        {
3679            let terminator = input.pop_front().unwrap();
3680            trace.record(
3681                terminator.argv,
3682                TokenRole::ValueTerminator {
3683                    ends: flag.name.clone(),
3684                },
3685            );
3686            break;
3687        }
3688        // The separator is left where it is: stopping here hands it to the arm that
3689        // knows what it means, rather than reading it as one more value.
3690        if next == "--"
3691            || (is_flag_like(next)
3692                && !(flag
3693                    .arg
3694                    .as_ref()
3695                    .is_some_and(|arg| arg.allow_negative_numbers)
3696                    && is_negative_number(next)))
3697        {
3698            break;
3699        }
3700        let taken = input.pop_front().unwrap();
3701        let argv = taken.argv;
3702        let mut word = taken.word;
3703        flag_awaiting_value.push(Arc::clone(flag));
3704        let mut bound = vec![];
3705        let refused = drain_pending_flag_values(
3706            spec,
3707            cmd,
3708            errors,
3709            flags,
3710            flag_awaiting_value,
3711            &mut word,
3712            custom_env,
3713            &mut bound,
3714        )?;
3715        for (flag, values) in bound {
3716            // A later word of the same occurrence is its own token, and never attached:
3717            // only the first value can ride along on the flag.
3718            trace.record(
3719                argv,
3720                TokenRole::Value {
3721                    flag,
3722                    values,
3723                    attached: false,
3724                },
3725            );
3726        }
3727        if refused {
3728            return Ok(true);
3729        }
3730    }
3731    // The loop stops once the occurrence has reached its bound, which without a delimiter is
3732    // exactly when it has taken `max` words. A delimiter breaks that: one word can carry
3733    // several values, so the run can end up *past* the bound rather than on it, and stopping
3734    // is no longer the same as staying within it. `--include a,b,c` under `var_max=2` is the
3735    // case — three values out of the one word the loop was entitled to take.
3736    //
3737    // Counted against `carried` like the loop itself, so this stays a statement about the
3738    // occurrence rather than about the list the occurrences build up.
3739    let taken = flags
3740        .get(flag)
3741        .map(value_count)
3742        .unwrap_or(0)
3743        .saturating_sub(carried);
3744    if let Some(min) = flag.arg.as_ref().and_then(|arg| arg.var_min) {
3745        if taken < min {
3746            errors.push(UsageErr::VarFlagTooFew {
3747                name: flag.name.clone(),
3748                min,
3749                got: taken,
3750            });
3751        }
3752    }
3753    if taken > max {
3754        errors.push(UsageErr::VarFlagTooMany {
3755            name: flag.name.clone(),
3756            max,
3757            got: taken,
3758        });
3759    }
3760    Ok(false)
3761}
3762
3763/// How many values a flag is holding, for a bound that counts them.
3764fn value_count(value: &ParseValue) -> usize {
3765    match value {
3766        ParseValue::MultiString(values) => values.len(),
3767        ParseValue::MultiBool(values) => values.len(),
3768        _ => 1,
3769    }
3770}
3771
3772/// Finish a value-optional flag that was given with no value.
3773///
3774/// Returns whether anything was bound. Completions keep the flag waiting — a
3775/// half-typed `--color ` is a question about the value — so this is asked only
3776/// once a full parse has decided the value is not coming, or once the next token
3777/// has made that decision.
3778///
3779/// The missing string is a real value: if the flag names `choices`, it has to
3780/// be one of them, the same way an env var or a `default` is checked. Binding
3781/// first and failing later would leave the flag set to a value the spec forbids.
3782fn try_bind_default_missing(
3783    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
3784    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
3785    custom_env: Option<&HashMap<String, String>>,
3786    origins: &mut IndexMap<Arc<SpecFlag>, Vec<ValueOrigin>>,
3787) -> miette::Result<bool> {
3788    let Some(flag) = flag_awaiting_value.last() else {
3789        return Ok(false);
3790    };
3791    let value = match flag.default_missing.clone() {
3792        Some(value) => value,
3793        None if flag.value_optional => {
3794            let flag = flag_awaiting_value.pop().unwrap();
3795            // Presence in the map distinguishes this from an absent flag; an
3796            // empty collection distinguishes it from an explicitly empty
3797            // `--flag=` string without inventing a sentinel value.
3798            let variadic_value = flag.arg.as_ref().is_some_and(|arg| arg.var);
3799            origins
3800                .entry(Arc::clone(&flag))
3801                .or_default()
3802                .push(ValueOrigin::DefaultMissing);
3803            if flag.var {
3804                // A repeated bare occurrence is still an occurrence. The string collection
3805                // uses an empty value for it, just as the concrete `default_missing` path
3806                // pushes one value per occurrence; otherwise bounds and consumers silently
3807                // lose every bare repeat after the first.
3808                flags
3809                    .entry(flag)
3810                    .or_insert_with(|| ParseValue::MultiString(Vec::new()))
3811                    .try_as_multi_string_mut()
3812                    .unwrap()
3813                    .push(String::new());
3814            } else if variadic_value {
3815                // A variadic occurrence stays pending after each value. Reaching the next
3816                // flag (or EOF) closes that same occurrence; it must not erase what it took.
3817                flags
3818                    .entry(flag)
3819                    .or_insert_with(|| ParseValue::MultiString(Vec::new()));
3820            } else {
3821                // A scalar pending here is a new bare occurrence. The normal permissive
3822                // repeat policy makes the later occurrence a correction, including a
3823                // correction from an explicit value back to the bare tri-state.
3824                flags.insert(flag, ParseValue::MultiString(Vec::new()));
3825            }
3826            return Ok(true);
3827        }
3828        None => return Ok(false),
3829    };
3830    if let Some(arg) = flag.arg.as_ref() {
3831        validate_choice_value(
3832            ChoiceTarget::option(flag),
3833            &value,
3834            arg.choices.as_ref(),
3835            custom_env,
3836        )?;
3837    }
3838    let flag = flag_awaiting_value.pop().unwrap();
3839    origins
3840        .entry(Arc::clone(&flag))
3841        .or_default()
3842        .push(ValueOrigin::DefaultMissing);
3843    let collecting = flag.var || flag.arg.as_ref().is_some_and(|arg| arg.var);
3844    if collecting {
3845        let arr = flags
3846            .entry(flag)
3847            .or_insert_with(|| ParseValue::MultiString(vec![]))
3848            .try_as_multi_string_mut()
3849            .unwrap();
3850        arr.push(value);
3851    } else {
3852        flags.insert(flag, ParseValue::String(value));
3853    }
3854    Ok(true)
3855}
3856
3857/// `bound` collects what each drained flag took, in the order it took it. The values are
3858/// the word after any `delimiter` split, which is the only place that split is known: by the
3859/// time they are in `flags` a scalar and a one-element list are indistinguishable, and a
3860/// second occurrence has appended to the same list.
3861#[allow(clippy::too_many_arguments)]
3862fn drain_pending_flag_values(
3863    spec: &Spec,
3864    cmd: &SpecCommand,
3865    errors: &mut Vec<UsageErr>,
3866    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
3867    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
3868    word: &mut String,
3869    custom_env: Option<&HashMap<String, String>>,
3870    bound: &mut Vec<(Arc<SpecFlag>, Vec<String>)>,
3871) -> miette::Result<bool> {
3872    while let Some(flag) = flag_awaiting_value.pop() {
3873        let arg = flag.arg.as_ref().unwrap();
3874        // Split before anything judges the word, because after the split it is no longer
3875        // one value: `--env dev,prod` is two, and `choices` has to be asked about each.
3876        // Judging first would reject the whole word against a list neither half is on.
3877        let parts: Vec<String> = match arg.delimiter {
3878            Some(delimiter) => word.split(delimiter).map(str::to_string).collect(),
3879            None => vec![std::mem::take(word)],
3880        };
3881        for part in &parts {
3882            if validate_choices(
3883                spec,
3884                cmd,
3885                errors,
3886                ChoiceTarget::option(&flag),
3887                part,
3888                arg.choices.as_ref(),
3889                custom_env,
3890            )? {
3891                return Ok(true);
3892            }
3893        }
3894        word.clear();
3895        bound.push((Arc::clone(&flag), parts.clone()));
3896        // Two ways to hold several values, and both record a list: a `var` flag
3897        // collects one per occurrence, a variadic argument collects several from one.
3898        if flag.var || arg.var {
3899            let arr = flags
3900                .entry(flag)
3901                .or_insert_with(|| ParseValue::MultiString(vec![]))
3902                .try_as_multi_string_mut()
3903                .unwrap();
3904            arr.extend(parts);
3905        } else {
3906            // Nowhere for a second value to go, so the word stands as it was typed. A
3907            // delimiter on a flag that takes one value is refused where it is written.
3908            flags.insert(
3909                flag,
3910                ParseValue::String(parts.into_iter().next().unwrap_or_default()),
3911            );
3912        }
3913    }
3914    Ok(false)
3915}
3916
3917fn choice_error(
3918    target: ChoiceTarget<'_>,
3919    value: &str,
3920    choices: Option<&SpecChoices>,
3921    custom_env: Option<&HashMap<String, String>>,
3922) -> Option<String> {
3923    let choices = choices?;
3924    if !choices.strict {
3925        return None;
3926    }
3927    let values = choices.values_with_env(custom_env);
3928    if choices.matches_with_env(value, custom_env) {
3929        return None;
3930    }
3931    if let Some(env) = choices.env() {
3932        if values.is_empty() {
3933            return Some(format!(
3934                "Invalid choice for {} {}: {value}, no choices resolved from env {env}",
3935                target.kind, target.name,
3936            ));
3937        }
3938    }
3939    Some(format!(
3940        "Invalid choice for {} {}: {value}, expected one of {}",
3941        target.kind,
3942        target.name,
3943        values.join(", ")
3944    ))
3945}
3946
3947fn validate_choices(
3948    spec: &Spec,
3949    cmd: &SpecCommand,
3950    errors: &mut Vec<UsageErr>,
3951    target: ChoiceTarget<'_>,
3952    value: &str,
3953    choices: Option<&SpecChoices>,
3954    custom_env: Option<&HashMap<String, String>>,
3955) -> miette::Result<bool> {
3956    if is_help_arg(spec, cmd, value)
3957        && choices
3958            .is_some_and(|choices| choices.strict && !choices.matches_with_env(value, custom_env))
3959    {
3960        errors.push(render_help_err(spec, cmd, value.len() > 2));
3961        return Ok(true);
3962    }
3963
3964    if let Some(err) = choice_error(target, value, choices, custom_env) {
3965        bail!("{err}");
3966    }
3967    Ok(false)
3968}
3969
3970fn validate_choice_value(
3971    target: ChoiceTarget<'_>,
3972    value: &str,
3973    choices: Option<&SpecChoices>,
3974    custom_env: Option<&HashMap<String, String>>,
3975) -> miette::Result<()> {
3976    if let Some(err) = choice_error(target, value, choices, custom_env) {
3977        bail!("{err}");
3978    }
3979    Ok(())
3980}
3981
3982fn validate_choice_values(
3983    target: ChoiceTarget<'_>,
3984    values: &[String],
3985    choices: Option<&SpecChoices>,
3986    custom_env: Option<&HashMap<String, String>>,
3987) -> miette::Result<()> {
3988    for value in values {
3989        validate_choice_value(target, value, choices, custom_env)?;
3990    }
3991    Ok(())
3992}
3993
3994/// Everything a parse records about where it stopped: the positional cursor, so callers that
3995/// do not re-run the parse — completions, above all — agree with it, and the token trace.
3996///
3997/// Every exit from the binding phase comes through here, which is what makes it the right
3998/// place to close the trace: whatever is still queued was never read, and saying so is more
3999/// useful than leaving those words out of the report entirely.
4000fn record_stop(
4001    out: &mut ParseOutput,
4002    next_arg_idx: usize,
4003    seen_double_dash: bool,
4004    trace: &mut Trace,
4005    unread: &VecDeque<Token>,
4006) {
4007    out.next_arg = out.cmd.args.get(next_arg_idx).cloned().map(Arc::new);
4008    out.double_dash_seen = seen_double_dash;
4009    trace.close(unread);
4010    out.tokens = std::mem::take(&mut trace.tokens);
4011}
4012
4013/// Record that `arg` was handed a word before the `--` it requires.
4014///
4015/// A variadic arg would otherwise report the same mistake once per word it was offered, so the
4016/// message is emitted only the first time each arg is seen. The set is also what suppresses the
4017/// `MissingArg` that a `required` + `double_dash="required"` arg would otherwise collect at the
4018/// end of the parse.
4019fn report_double_dash_violation(
4020    arg: &SpecArg,
4021    errors: &mut Vec<UsageErr>,
4022    violations: &mut HashSet<String>,
4023) {
4024    if violations.insert(arg.name.clone()) {
4025        errors.push(UsageErr::ArgRequiresDoubleDash(arg.name.clone()));
4026    }
4027}
4028
4029/// `--version` and `-V`, which the parser supplies where the spec declares a version.
4030///
4031/// The twin of [`is_help_arg`], and of the `version` bit in usage-argv's and usage-go's
4032/// command tables — both of which accepted these spellings while this parser called them
4033/// unknown words. The help page has always listed `-V, --version` under the same
4034/// condition, and said in as many words that it did so "only where a version is
4035/// declared, which is where a parser accepts one", so a spec with a `version` rendered a
4036/// page advertising a flag the parse refused.
4037///
4038/// The root only, because that is where the page lists it: `version` is a property of
4039/// the program, and a subcommand answering with the program's version is a claim no spec
4040/// made. A declared flag wins by arriving first — every scan consults this only after
4041/// nothing declared matched — so a CLI that spends `-V` on something else keeps it, and
4042/// keeps `--version` supplied beside it.
4043fn is_version_arg(spec: &Spec, cmds: &[SpecCommand], w: &str) -> bool {
4044    (spec.version.is_some() || spec.long_version.is_some())
4045        && cmds.len() == 1
4046        && !spec.cmd.disable_version_flag
4047        && (w == "--version" || w == "-V")
4048}
4049
4050fn is_help_arg(spec: &Spec, cmd: &SpecCommand, w: &str) -> bool {
4051    spec.disable_help != Some(true)
4052        && (((w == "--help" || w == "-h" || w == "-?") && !cmd.disable_help_flag)
4053            || (w == "help" && !cmd.disable_help_subcommand && cmd.subcommands.is_empty()))
4054}
4055
4056impl ParseOutput {
4057    pub fn as_env(&self) -> BTreeMap<String, String> {
4058        let mut env = BTreeMap::new();
4059        for (flag, val) in &self.flags {
4060            let key = format!("usage_{}", crate::case::snake(&flag.name));
4061            let val = match val {
4062                ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
4063                ParseValue::String(s) => s.clone(),
4064                ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
4065                ParseValue::MultiString(s) => crate::shell_words::join(s),
4066            };
4067            env.insert(key, val);
4068        }
4069        for (arg, val) in &self.args {
4070            let key = format!("usage_{}", crate::case::snake(&arg.name));
4071            env.insert(key, val.to_string());
4072        }
4073        env
4074    }
4075}
4076
4077impl Display for ParseValue {
4078    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
4079        match self {
4080            ParseValue::Bool(b) => write!(f, "{b}"),
4081            ParseValue::String(s) => write!(f, "{s}"),
4082            ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
4083            ParseValue::MultiString(s) => write!(f, "{}", crate::shell_words::join(s)),
4084        }
4085    }
4086}
4087
4088/// One `tokens` line for [`Debug`]: the position, the word, and what it became.
4089fn render_token(token: &TokenBinding) -> String {
4090    let roles = token.roles.iter().map(render_role).join(", ");
4091    let synthesized = if token.synthesized { " (read as)" } else { "" };
4092    format!("[{}] {}{synthesized}: {roles}", token.index, token.word)
4093}
4094
4095fn render_role(role: &TokenRole) -> String {
4096    match role {
4097        TokenRole::Program => "program".to_string(),
4098        TokenRole::Command { name } => format!("subcommand {name}"),
4099        TokenRole::Flag {
4100            flag,
4101            spelling,
4102            negated,
4103        } => {
4104            let negated = if *negated { ", negated" } else { "" };
4105            format!("flag {} as {spelling}{negated}", flag.name)
4106        }
4107        TokenRole::Value {
4108            flag,
4109            values,
4110            attached,
4111        } => {
4112            let attached = if *attached { ", attached" } else { "" };
4113            format!("value of {} = {values:?}{attached}", flag.name)
4114        }
4115        TokenRole::Arg { arg, values } => format!("arg {} = {values:?}", arg.name),
4116        TokenRole::Separator => "separator".to_string(),
4117        TokenRole::Builtin { spelling } => format!("built-in {spelling}"),
4118        TokenRole::ValueTerminator { ends } => format!("value terminator, ends {ends}"),
4119        TokenRole::Restart => "restart".to_string(),
4120        TokenRole::UnknownFlag { bound_as } => match bound_as {
4121            Some(arg) => format!("unknown flag, bound as {}", arg.name),
4122            None => "unknown flag".to_string(),
4123        },
4124        TokenRole::Refused { reason } => format!("refused: {reason}"),
4125        TokenRole::External => "external".to_string(),
4126        TokenRole::Unread => "unread".to_string(),
4127    }
4128}
4129
4130impl Debug for ParseOutput {
4131    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
4132        f.debug_struct("ParseOutput")
4133            .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
4134            .field(
4135                "args",
4136                &self
4137                    .args
4138                    .iter()
4139                    .map(|(a, w)| format!("{}: {w}", a.name))
4140                    .collect_vec(),
4141            )
4142            .field(
4143                "available_flags",
4144                &self
4145                    .available_flags
4146                    .iter()
4147                    .map(|(f, w)| format!("{f}: {w}"))
4148                    .collect_vec(),
4149            )
4150            .field(
4151                "flags",
4152                &self
4153                    .flags
4154                    .iter()
4155                    .map(|(f, w)| format!("{}: {w}", f.name))
4156                    .collect_vec(),
4157            )
4158            .field("flag_awaiting_value", &self.flag_awaiting_value)
4159            .field("errors", &self.errors)
4160            .field("external", &self.external)
4161            // Provenance, one line per token and one per fallback. This is the parser's
4162            // debug channel under `USAGE_LOG=trace`, so it is where a spec author looks
4163            // first — `usage explain` renders the same facts for a reader.
4164            .field(
4165                "tokens",
4166                &self.tokens.iter().map(render_token).collect_vec(),
4167            )
4168            .field(
4169                "origins",
4170                &self
4171                    .flag_origins
4172                    .iter()
4173                    .map(|(f, o)| format!("{}: {o:?}", f.name))
4174                    .chain(
4175                        self.arg_origins
4176                            .iter()
4177                            .map(|(a, o)| format!("{}: {o:?}", a.name)),
4178                    )
4179                    .collect_vec(),
4180            )
4181            .field("overridden_flags", &self.overridden_flags)
4182            .finish()
4183    }
4184}
4185
4186#[cfg(test)]
4187mod tests {
4188    use super::*;
4189    use crate::SpecFlagAction;
4190
4191    fn input(words: &[&str]) -> Vec<String> {
4192        words.iter().map(|word| (*word).to_string()).collect()
4193    }
4194
4195    #[test]
4196    fn a_declared_version_supplies_the_flag_the_help_page_lists() {
4197        // The page has always listed `-V, --version` wherever a `version` is declared,
4198        // and usage-argv and usage-go have always accepted both. This parser called them
4199        // unknown words, so the one implementation the corpus measures the others against
4200        // was the one that disagreed.
4201        let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ncmd \"run\"\n"
4202            .parse()
4203            .unwrap();
4204
4205        for spelling in ["--version", "-V"] {
4206            let err = parse(&spec, &input(&["ex", spelling]))
4207                .expect_err("answering with a version ends the parse");
4208            assert_eq!(err.to_string(), "1.2.3", "{spelling}");
4209        }
4210    }
4211
4212    #[test]
4213    fn the_supplied_version_flag_is_the_roots_alone() {
4214        // `version` describes the program, and the page lists the entry on the program's
4215        // own page only. A subcommand answering with it would be a claim no spec made.
4216        let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ncmd \"run\"\n"
4217            .parse()
4218            .unwrap();
4219
4220        let err = parse(&spec, &input(&["ex", "run", "--version"])).unwrap_err();
4221        assert_eq!(err.to_string(), "unexpected word: --version");
4222    }
4223
4224    #[test]
4225    fn no_declared_version_supplies_nothing() {
4226        // A `--version` answering with nothing is worse than one that is not there, which
4227        // is why the entry is conditional on the page and the spelling on the parse.
4228        let spec: Spec = "name \"ex\"\nbin \"ex\"\n".parse().unwrap();
4229
4230        for spelling in ["--version", "-V"] {
4231            let err = parse(&spec, &input(&["ex", spelling])).unwrap_err();
4232            assert_eq!(err.to_string(), format!("unexpected word: {spelling}"));
4233        }
4234    }
4235
4236    #[test]
4237    fn disable_version_flag_removes_the_supplied_spellings() {
4238        let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ndisable_version_flag #true\n"
4239            .parse()
4240            .unwrap();
4241
4242        for spelling in ["--version", "-V"] {
4243            let err = parse(&spec, &input(&["ex", spelling])).unwrap_err();
4244            assert_eq!(err.to_string(), format!("unexpected word: {spelling}"));
4245        }
4246    }
4247
4248    #[test]
4249    fn a_spelling_the_spec_spends_elsewhere_keeps_its_meaning() {
4250        // The page drops each supplied spelling the CLI claimed and keeps the other; the
4251        // parse agrees without being told, because a declared flag is matched first.
4252        let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-V --verbose\"\n"
4253            .parse()
4254            .unwrap();
4255
4256        let out = parse(&spec, &input(&["ex", "-V"])).expect("-V is the CLI's own flag");
4257        assert_eq!(out.flags.len(), 1);
4258
4259        let err = parse(&spec, &input(&["ex", "--version"])).unwrap_err();
4260        assert_eq!(err.to_string(), "1.2.3");
4261    }
4262
4263    #[test]
4264    fn the_supplied_spellings_split_the_two_version_texts() {
4265        // The same split `render_action_err` gives a declared version flag: the long
4266        // spelling prefers `long_version`, the short prefers the concise one.
4267        let spec: Spec =
4268            "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nlong_version \"1.2.3 (abcdef)\"\n"
4269                .parse()
4270                .unwrap();
4271
4272        assert_eq!(
4273            parse(&spec, &input(&["ex", "--version"]))
4274                .unwrap_err()
4275                .to_string(),
4276            "1.2.3 (abcdef)"
4277        );
4278        assert_eq!(
4279            parse(&spec, &input(&["ex", "-V"])).unwrap_err().to_string(),
4280            "1.2.3"
4281        );
4282    }
4283
4284    #[test]
4285    fn a_supplied_short_is_a_letter_a_bundle_may_contain() {
4286        // `-h` and `-V` are recognized letters that no spec declares, so a token holding
4287        // one beside a declared letter is a bundle. usage-lib alone read `-vh` as a word
4288        // naming nothing: usage-argv and usage-go both resolve the letter through the
4289        // same lookup that finds a declared short, and clap prints help for it too.
4290        let spec: Spec =
4291            "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\"\ncmd \"run\"\n"
4292                .parse()
4293                .unwrap();
4294
4295        for token in ["-vh", "-hv"] {
4296            let err = parse(&spec, &input(&["ex", token])).expect_err("help ends the parse");
4297            assert!(err.to_string().starts_with("ex 1.2.3"), "{token}: {err}");
4298        }
4299        for token in ["-vV", "-Vv"] {
4300            let err = parse(&spec, &input(&["ex", token])).expect_err("a version ends it too");
4301            assert_eq!(err.to_string(), "1.2.3", "{token}");
4302        }
4303    }
4304
4305    #[test]
4306    fn a_bundled_help_letter_asks_for_the_short_page() {
4307        // Whatever else shares the token: `-h` is the short spelling, and the letters
4308        // beside it say nothing about which page was asked for.
4309        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-v --verbose\" help=\"Be loud\" {\n    long_help \"Be loud, and say so at length.\"\n}\n"
4310            .parse()
4311            .unwrap();
4312
4313        let short = parse(&spec, &input(&["ex", "-vh"]))
4314            .unwrap_err()
4315            .to_string();
4316        let long = parse(&spec, &input(&["ex", "--help"]))
4317            .unwrap_err()
4318            .to_string();
4319        assert!(short.contains("Be loud"), "{short}");
4320        assert!(!short.contains("at length"), "{short}");
4321        assert!(long.contains("at length"), "{long}");
4322    }
4323
4324    #[test]
4325    fn the_bundled_version_letter_is_the_roots_alone() {
4326        // The same rule the whole-token spelling follows, asked one letter at a time.
4327        let spec: Spec =
4328            "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\" global=#true\ncmd \"run\"\n"
4329                .parse()
4330                .unwrap();
4331
4332        let err = parse(&spec, &input(&["ex", "run", "-vV"])).unwrap_err();
4333        assert_eq!(err.to_string(), "unexpected word: -vV");
4334    }
4335
4336    #[test]
4337    fn a_declared_letter_keeps_its_meaning_inside_a_bundle() {
4338        // Nothing is supplied where the CLI spent the letter itself, so `-vh local` is
4339        // this spec's own `-h`, taking its value from the rest of the token.
4340        let spec: Spec =
4341            "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\"\nflag \"-h --host <host>\"\n"
4342                .parse()
4343                .unwrap();
4344
4345        let out = parse(&spec, &input(&["ex", "-vhlocal"])).expect("a bundle and its value");
4346        assert_eq!(out.flags.len(), 2);
4347        assert!(out
4348            .flags
4349            .iter()
4350            .any(|(flag, value)| flag.name == "host" && value.to_string() == "local"));
4351    }
4352
4353    #[test]
4354    fn disabling_help_takes_the_letter_back_out_of_the_bundle() {
4355        let spec: Spec =
4356            "name \"ex\"\nbin \"ex\"\ndisable_help_flag #true\nflag \"-v --verbose\"\n"
4357                .parse()
4358                .unwrap();
4359
4360        let err = parse(&spec, &input(&["ex", "-vh"])).unwrap_err();
4361        assert_eq!(err.to_string(), "unexpected word: -vh");
4362    }
4363
4364    #[test]
4365    fn a_letter_nothing_supplies_still_refuses_the_whole_bundle() {
4366        // The rule this must not weaken: `-az` is not a bundle at all, so `-a` is not set
4367        // on the way to discovering that `z` names nothing.
4368        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-a --all\"\narg \"[file]\"\n"
4369            .parse()
4370            .unwrap();
4371
4372        let out = parse(&spec, &input(&["ex", "-az"])).expect("it falls through to the argument");
4373        assert!(out.flags.is_empty(), "{:?}", out.flags);
4374        assert_eq!(out.args.len(), 1);
4375    }
4376
4377    fn spec_with_arg(arg: SpecArg) -> Spec {
4378        let cmd = SpecCommand::builder().name("test").arg(arg).build();
4379        Spec {
4380            name: "test".to_string(),
4381            bin: "test".to_string(),
4382            cmd,
4383            ..Default::default()
4384        }
4385    }
4386
4387    fn spec_with_flag(flag: SpecFlag) -> Spec {
4388        let cmd = SpecCommand::builder().name("test").flag(flag).build();
4389        Spec {
4390            name: "test".to_string(),
4391            bin: "test".to_string(),
4392            cmd,
4393            ..Default::default()
4394        }
4395    }
4396
4397    fn parse_with_env(
4398        spec: &Spec,
4399        words: &[&str],
4400        env: &[(&str, &str)],
4401    ) -> Result<ParseOutput, miette::Error> {
4402        let env = env
4403            .iter()
4404            .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
4405            .collect();
4406        Parser::new(spec).with_env(env).parse(&input(words))
4407    }
4408
4409    fn first_string_value(parsed: &ParseOutput) -> &str {
4410        if let Some(ParseValue::String(value)) = parsed.args.values().next() {
4411            return value;
4412        }
4413        if let Some(ParseValue::String(value)) = parsed.flags.values().next() {
4414            return value;
4415        }
4416        panic!("expected first parsed value to be ParseValue::String");
4417    }
4418
4419    #[test]
4420    fn custom_environment_parser_dispatches_executable_views() {
4421        let spec: Spec = r#"
4422bin "ex"
4423view "runner" root="run"
4424cmd "run" {
4425    flag "--token <token>" env="TOKEN"
4426}
4427        "#
4428        .parse()
4429        .unwrap();
4430        let parsed = Parser::new(&spec)
4431            .with_env([("TOKEN".to_string(), "secret".to_string())].into())
4432            .parse(&input(&["runner"]))
4433            .unwrap();
4434
4435        assert_eq!(parsed.cmd.name, "runner");
4436        assert!(parsed.flags.iter().any(|(flag, value)| flag.name == "token"
4437            && matches!(value, ParseValue::String(value) if value == "secret")));
4438    }
4439
4440    #[test]
4441    fn an_executable_view_keeps_the_hosts_version_action() {
4442        let spec: Spec = r#"
4443bin "ex"
4444version "1.2.3"
4445flag "-V --version" action="version"
4446flag "--verbose" global=#true
4447view "runner" root="run" globals=#true
4448cmd "run"
4449        "#
4450        .parse()
4451        .unwrap();
4452
4453        let error = Parser::new(&spec)
4454            .parse(&input(&["runner", "--version"]))
4455            .expect_err("the host version action should answer before view projection");
4456        assert_eq!(error.to_string(), "1.2.3");
4457
4458        let error = Parser::new(&spec)
4459            .parse(&input(&["runner", "--verbose", "--version"]))
4460            .expect_err("the host version action should remain after a carried global");
4461        assert_eq!(error.to_string(), "1.2.3");
4462    }
4463
4464    fn flag_string_value<'a>(parsed: &'a ParseOutput, name: &str) -> &'a str {
4465        let flag = parsed
4466            .flags
4467            .keys()
4468            .find(|flag| flag.name == name)
4469            .unwrap_or_else(|| panic!("expected flag {name}"));
4470        let value = parsed
4471            .flags
4472            .get(flag)
4473            .unwrap_or_else(|| panic!("expected value for flag {name}"));
4474        match value {
4475            ParseValue::String(value) => value,
4476            _ => panic!("expected flag {name} to be ParseValue::String"),
4477        }
4478    }
4479
4480    fn assert_parse_err(result: Result<ParseOutput, miette::Error>, expected: &str) {
4481        let err = result.expect_err("expected parser error");
4482        assert_eq!(format!("{err}"), expected);
4483    }
4484
4485    #[test]
4486    fn a_short_version_action_falls_back_to_the_long_version() {
4487        let flag = SpecFlag::builder()
4488            .short('R')
4489            .action(SpecFlagAction::Version)
4490            .build();
4491        let spec = Spec {
4492            name: "test".to_string(),
4493            bin: "test".to_string(),
4494            long_version: Some("1.2.3\ncommit abc123".to_string()),
4495            ..Default::default()
4496        };
4497        let UsageErr::Version(version) = render_action_err(&spec, &spec.cmd, &flag, "-R") else {
4498            panic!("expected version action")
4499        };
4500        assert_eq!(version, "1.2.3\ncommit abc123");
4501    }
4502
4503    #[cfg(feature = "unstable_choices_env")]
4504    fn spec_arg_choices_env(key: &str) -> Spec {
4505        spec_with_arg(
4506            SpecArg::builder()
4507                .name("env")
4508                .choices_env(key)
4509                .required(false)
4510                .build(),
4511        )
4512    }
4513
4514    #[cfg(feature = "unstable_choices_env")]
4515    fn spec_flag_choices_env(key: &str) -> Spec {
4516        spec_with_flag(
4517            SpecFlag::builder()
4518                .long("env")
4519                .arg(SpecArg::builder().name("env").choices_env(key).build())
4520                .build(),
4521        )
4522    }
4523
4524    #[test]
4525    fn test_parse() {
4526        let cmd = SpecCommand::builder()
4527            .name("test")
4528            .arg(SpecArg::builder().name("arg").build())
4529            .flag(SpecFlag::builder().long("flag").build())
4530            .build();
4531        let spec = Spec {
4532            name: "test".to_string(),
4533            bin: "test".to_string(),
4534            cmd,
4535            ..Default::default()
4536        };
4537        let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
4538        let parsed = parse(&spec, &input).unwrap();
4539        assert_eq!(parsed.cmds.len(), 1);
4540        assert_eq!(parsed.cmds[0].name, "test");
4541        assert_eq!(parsed.args.len(), 1);
4542        assert_eq!(parsed.flags.len(), 1);
4543        assert_eq!(parsed.available_flags.len(), 1);
4544    }
4545
4546    #[test]
4547    fn test_flag_overrides_last_occurrence_wins() {
4548        let spec: Spec = r#"
4549flag "--stdin" default=#true
4550flag "--file <file>" overrides="--stdin"
4551        "#
4552        .parse()
4553        .unwrap();
4554
4555        let file_wins = parse(&spec, &input(&["test", "--stdin", "--file", "input.txt"])).unwrap();
4556        assert_eq!(file_wins.flags.len(), 1);
4557        assert_eq!(flag_string_value(&file_wins, "file"), "input.txt");
4558        assert!(!file_wins.flags.keys().any(|flag| flag.name == "stdin"));
4559
4560        let stdin_wins = parse(&spec, &input(&["test", "--file", "input.txt", "--stdin"])).unwrap();
4561        assert_eq!(stdin_wins.flags.len(), 1);
4562        assert!(stdin_wins.flags.keys().any(|flag| flag.name == "stdin"));
4563        assert!(!stdin_wins.flags.keys().any(|flag| flag.name == "file"));
4564    }
4565
4566    #[test]
4567    fn test_flag_override_clears_pending_value() {
4568        let spec: Spec = r#"
4569flag "--file <file>" overrides="--stdin"
4570flag "--stdin"
4571arg "[input]"
4572        "#
4573        .parse()
4574        .unwrap();
4575
4576        let parsed = parse(&spec, &input(&["test", "--file", "--stdin", "input.txt"])).unwrap();
4577        assert_eq!(parsed.flags.len(), 1);
4578        assert!(parsed.flags.keys().any(|flag| flag.name == "stdin"));
4579        assert_eq!(first_string_value(&parsed), "input.txt");
4580    }
4581
4582    #[cfg(unix)]
4583    #[test]
4584    fn a_mount_on_the_root_discovers_subcommands() {
4585        // The root is a command like any other, so it can find its own subcommands
4586        // by running something. Uses `echo` rather than a fixture because resolving
4587        // a mount is what is being tested.
4588        let spec: Spec = r#"
4589name "ex"
4590bin "ex"
4591cmd "declared"
4592mount run="echo 'cmd \"discovered\"'"
4593"#
4594        .parse()
4595        .unwrap();
4596
4597        let out = parse(&spec, &["ex".to_string(), "discovered".to_string()]).unwrap();
4598        assert_eq!(out.cmd.name, "discovered");
4599    }
4600
4601    #[test]
4602    fn injected_mount_outputs_are_complete_and_never_fall_back_to_processes() {
4603        let spec: Spec = r#"
4604name "ex"
4605bin "ex"
4606mount run="this command must never run"
4607cmd "declared"
4608"#
4609        .parse()
4610        .unwrap();
4611
4612        Parser::new(&spec)
4613            .with_mount_outputs(HashMap::new())
4614            .parse(&input(&["ex", "declared"]))
4615            .expect("a declared command does not resolve the mount");
4616
4617        let error = Parser::new(&spec)
4618            .with_mount_outputs(HashMap::new())
4619            .parse(&input(&["ex", "discovered"]))
4620            .unwrap_err();
4621        assert!(
4622            error
4623                .to_string()
4624                .contains("No injected output was provided for mount command"),
4625            "{error}"
4626        );
4627    }
4628
4629    #[cfg(unix)]
4630    #[test]
4631    fn completion_sees_root_mounted_commands_with_nothing_typed() {
4632        // The case a root mount exists for. `mycli <tab>` has no word to trigger
4633        // discovery with, so a completion has to resolve up front or the mounted
4634        // commands are never offered.
4635        let spec: Spec = r#"
4636name "ex"
4637bin "ex"
4638cmd "declared"
4639mount run="echo 'cmd \"discovered\"'"
4640"#
4641        .parse()
4642        .unwrap();
4643
4644        let out = parse_partial(&spec, &["ex".to_string()]).unwrap();
4645        assert!(
4646            out.cmd.subcommands.contains_key("discovered"),
4647            "a completion should see mounted commands; got {:?}",
4648            out.cmd.subcommands.keys().collect::<Vec<_>>()
4649        );
4650    }
4651
4652    #[cfg(unix)]
4653    #[test]
4654    fn completion_and_execution_agree_about_discovery() {
4655        // Offering a command that a real parse would hand to the default instead is
4656        // worse than not offering it, so the gate applies to both paths. The mount
4657        // fails if it runs, which is how both halves are checked at once.
4658        let spec: Spec = r#"
4659name "ex"
4660bin "ex"
4661default_subcommand "run"
4662cmd "run" {
4663  arg "<task>"
4664}
4665mount run="exit 1"
4666"#
4667        .parse()
4668        .unwrap();
4669
4670        let out = parse_partial(&spec, &["ex".to_string()]).unwrap();
4671        assert!(
4672            !out.cmd.subcommands.contains_key("discovered"),
4673            "a completion must not offer what execution will not route"
4674        );
4675
4676        let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
4677        assert_eq!(out.cmd.name, "run");
4678    }
4679
4680    #[cfg(unix)]
4681    #[test]
4682    fn a_default_subcommand_outranks_discovery() {
4683        // The default already says what an unmatched word means, and says it for
4684        // free. The mount fails if it runs, so parsing proves discovery was skipped.
4685        let spec: Spec = r#"
4686name "ex"
4687bin "ex"
4688default_subcommand "run"
4689cmd "run" {
4690  arg "<task>"
4691}
4692mount run="exit 1"
4693"#
4694        .parse()
4695        .unwrap();
4696
4697        let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
4698        assert_eq!(out.cmd.name, "run");
4699    }
4700
4701    #[cfg(unix)]
4702    #[test]
4703    fn a_mount_may_ask_to_outrank_the_default() {
4704        // Opting in, and paying for it: discovery runs first, so a discovered
4705        // command wins over the fallback.
4706        let spec: Spec = r#"
4707name "ex"
4708bin "ex"
4709default_subcommand "run"
4710cmd "run" {
4711  arg "<task>"
4712}
4713mount run="echo 'cmd \"discovered\"'" overrides_default=#true
4714"#
4715        .parse()
4716        .unwrap();
4717
4718        let out = parse(&spec, &["ex".to_string(), "discovered".to_string()]).unwrap();
4719        assert_eq!(out.cmd.name, "discovered");
4720
4721        // A word it does not know still reaches the default.
4722        let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
4723        assert_eq!(out.cmd.name, "run");
4724    }
4725
4726    #[cfg(unix)]
4727    #[test]
4728    fn a_flag_does_not_run_the_mount() {
4729        // A flag matches no subcommand, which would have been enough to trigger
4730        // discovery — so `ex --help` spawned a process. The mount fails if it runs,
4731        // so parsing at all is the proof that it did not.
4732        let spec: Spec = r#"
4733name "ex"
4734bin "ex"
4735flag "--verbose"
4736cmd "declared"
4737mount run="exit 1"
4738"#
4739        .parse()
4740        .unwrap();
4741
4742        let out = parse(&spec, &["ex".to_string(), "--verbose".to_string()]).unwrap();
4743        assert_eq!(out.cmd.name, "ex");
4744    }
4745
4746    #[cfg(unix)]
4747    #[test]
4748    fn a_declared_subcommand_does_not_run_the_mount() {
4749        // The mount would fail if it ran, so this parsing at all is the proof that
4750        // discovery is skipped when the word is already known. Worth pinning: a root
4751        // mount that resolved eagerly would spawn a process on every invocation.
4752        let spec: Spec = r#"
4753name "ex"
4754bin "ex"
4755cmd "declared"
4756mount run="exit 1"
4757"#
4758        .parse()
4759        .unwrap();
4760
4761        let out = parse(&spec, &["ex".to_string(), "declared".to_string()]).unwrap();
4762        assert_eq!(out.cmd.name, "declared");
4763    }
4764
4765    #[test]
4766    fn a_root_mount_survives_being_written_out() {
4767        let spec: Spec = "name \"ex\"\nbin \"ex\"\nmount run=\"ex plugins --usage\"\n"
4768            .parse()
4769            .unwrap();
4770        assert_eq!(spec.cmd.mounts.len(), 1);
4771
4772        let reparsed: Spec = spec.to_string().parse().unwrap();
4773        assert_eq!(reparsed.cmd.mounts.len(), 1, "written:\n{spec}");
4774        assert_eq!(reparsed.cmd.mounts[0].run, "ex plugins --usage");
4775    }
4776
4777    #[test]
4778    fn test_mount_prefix_applies_flag_overrides() {
4779        let stdin = Arc::new(
4780            SpecFlag::builder()
4781                .name("stdin")
4782                .long("stdin")
4783                .global(true)
4784                .build(),
4785        );
4786        let file = Arc::new(
4787            SpecFlag::builder()
4788                .name("file")
4789                .long("file")
4790                .arg(SpecArg::builder().name("file").build())
4791                .global(true)
4792                .overrides_with(vec!["--stdin".to_string()])
4793                .build(),
4794        );
4795        let mut prefix_flags = vec![(stdin, vec!["--stdin".to_string()])];
4796
4797        apply_prefix_flag_overrides(&mut prefix_flags, Arc::clone(&file));
4798        prefix_flags.push((file, vec!["--file".to_string(), "input.txt".to_string()]));
4799
4800        assert_eq!(mount_prefix_words(&prefix_flags), ["--file", "input.txt"]);
4801    }
4802
4803    #[test]
4804    fn test_flag_override_suppresses_env_value() {
4805        let spec: Spec = r#"
4806flag "--stdin" env="USE_STDIN"
4807flag "--file <file>" overrides="--stdin"
4808        "#
4809        .parse()
4810        .unwrap();
4811
4812        let parsed = parse_with_env(
4813            &spec,
4814            &["test", "--file", "input.txt"],
4815            &[("USE_STDIN", "true")],
4816        )
4817        .unwrap();
4818        assert_eq!(parsed.flags.len(), 1);
4819        assert_eq!(flag_string_value(&parsed, "file"), "input.txt");
4820    }
4821
4822    #[test]
4823    fn test_flag_override_suppresses_required_check() {
4824        let spec: Spec = r#"
4825flag "--stdin" required=#true
4826flag "--file <file>" overrides="--stdin"
4827        "#
4828        .parse()
4829        .unwrap();
4830
4831        let parsed = parse(&spec, &input(&["test", "--file", "input.txt"])).unwrap();
4832        assert_eq!(parsed.flags.len(), 1);
4833        assert_eq!(flag_string_value(&parsed, "file"), "input.txt");
4834    }
4835
4836    #[test]
4837    fn test_flag_required_if() {
4838        let spec: Spec = r#"
4839flag "--dir <dir>"
4840flag "--file <file>" required_if="--dir"
4841        "#
4842        .parse()
4843        .unwrap();
4844
4845        parse(&spec, &input(&["test"])).unwrap();
4846        assert_parse_err(
4847            parse(&spec, &input(&["test", "--dir", "src"])),
4848            "Missing required flag: --file <file>",
4849        );
4850        parse(
4851            &spec,
4852            &input(&["test", "--dir", "src", "--file", "input.txt"]),
4853        )
4854        .unwrap();
4855    }
4856
4857    #[test]
4858    fn test_flag_required_unless() {
4859        let spec: Spec = r#"
4860flag "--stdin"
4861flag "--file <file>" required_unless="--stdin"
4862        "#
4863        .parse()
4864        .unwrap();
4865
4866        assert_parse_err(
4867            parse(&spec, &input(&["test"])),
4868            "Missing required flag: --file <file>",
4869        );
4870        parse(&spec, &input(&["test", "--stdin"])).unwrap();
4871        parse(&spec, &input(&["test", "--file", "input.txt"])).unwrap();
4872    }
4873
4874    #[test]
4875    fn complete_required_relationship_truth_tables() {
4876        let spec: Spec = r#"
4877name "ex"
4878bin "ex"
4879flag "--mode <mode>"
4880flag "--scope <scope>"
4881flag "--token <token>" {
4882    required_if_eq "--mode" "remote"
4883}
4884flag "--approval <approval>" {
4885    required_if_eq_all "--mode" "remote" "--scope" "global"
4886}
4887flag "--input <input>" {
4888    required_unless "--stdin" "--file"
4889}
4890flag "--checksum <checksum>" {
4891    required_unless_all "--stdin" "--file"
4892}
4893flag "--stdin"
4894flag "--file <file>"
4895arg "[request]" {
4896    requires "--mode" "--scope"
4897}
4898"#
4899        .parse()
4900        .unwrap();
4901        let parse_args = |args: &[&str]| {
4902            parse(
4903                &spec,
4904                &args
4905                    .iter()
4906                    .map(|arg| (*arg).to_string())
4907                    .collect::<Vec<_>>(),
4908            )
4909        };
4910
4911        assert!(parse_args(&["ex", "--mode", "remote", "--stdin"]).is_err());
4912        assert!(parse_args(&[
4913            "ex", "--mode", "remote", "--token", "secret", "--scope", "global", "--stdin",
4914        ])
4915        .is_err());
4916        parse_args(&[
4917            "ex",
4918            "--mode",
4919            "remote",
4920            "--token",
4921            "secret",
4922            "--scope",
4923            "global",
4924            "--approval",
4925            "yes",
4926            "--stdin",
4927            "--file",
4928            "in",
4929        ])
4930        .unwrap();
4931        parse_args(&[
4932            "ex",
4933            "--mode",
4934            "local",
4935            "--scope",
4936            "project",
4937            "--stdin",
4938            "--checksum",
4939            "sum",
4940            "request.json",
4941        ])
4942        .unwrap();
4943
4944        let reparsed: Spec = spec.to_string().parse().unwrap();
4945        assert_eq!(reparsed.cmd.flags[2].required_if_eq.len(), 1);
4946        assert_eq!(reparsed.cmd.flags[3].required_if_eq_all.len(), 2);
4947        assert_eq!(reparsed.cmd.flags[5].required_unless_all.len(), 2);
4948        assert_eq!(reparsed.cmd.args[0].requires.len(), 2);
4949    }
4950
4951    #[test]
4952    fn test_conditional_requirements_treat_env_as_explicit() {
4953        let spec: Spec = r#"
4954flag "--dir <dir>" env="INPUT_DIR"
4955flag "--stdin" env="USE_STDIN"
4956flag "--file <file>" required_if="--dir" required_unless="--stdin"
4957        "#
4958        .parse()
4959        .unwrap();
4960
4961        assert_parse_err(
4962            parse_with_env(&spec, &["test"], &[("INPUT_DIR", "src")]),
4963            "Missing required flag: --file <file>",
4964        );
4965        parse_with_env(&spec, &["test"], &[("USE_STDIN", "true")]).unwrap();
4966    }
4967
4968    #[test]
4969    fn test_custom_env_does_not_fall_back_to_process_env() {
4970        assert!(std::env::var("PATH").is_ok());
4971        let spec: Spec = r#"flag "--file <file>" env="PATH" required=#true"#.parse().unwrap();
4972
4973        assert_parse_err(
4974            parse_with_env(&spec, &["test"], &[]),
4975            "Missing required flag: --file <file>",
4976        );
4977    }
4978
4979    #[test]
4980    fn test_conditional_requirements_ignore_defaults_on_condition_flags() {
4981        let spec: Spec = r#"
4982flag "--dir <dir>" default="src"
4983flag "--file <file>" required_if="--dir"
4984        "#
4985        .parse()
4986        .unwrap();
4987
4988        parse(&spec, &input(&["test"])).unwrap();
4989    }
4990
4991    #[test]
4992    fn test_conditional_requirements_see_overridden_flags_as_absent() {
4993        let spec: Spec = r#"
4994flag "--stdin"
4995flag "--dir <dir>" overrides="--stdin"
4996flag "--file <file>" required_unless="--stdin"
4997        "#
4998        .parse()
4999        .unwrap();
5000
5001        assert_parse_err(
5002            parse(&spec, &input(&["test", "--stdin", "--dir", "src"])),
5003            "Missing required flag: --file <file>",
5004        );
5005    }
5006
5007    #[test]
5008    fn short_flag_is_one_character_not_one_byte() {
5009        // A short is declared and read by character. Counting bytes instead either
5010        // refuses the declaration or slices the token inside the character, and clap
5011        // — which many specs are generated from — accepts shorts like this one.
5012        let spec = spec_with_flag(
5013            SpecFlag::builder()
5014                .short('磨')
5015                .long("polish")
5016                .arg(SpecArg::builder().name("opt").build())
5017                .build(),
5018        );
5019        let attached = Parser::new(&spec)
5020            .parse(&input(&["test", "-磨VALUE"]))
5021            .unwrap();
5022        assert_eq!(flag_string_value(&attached, "polish"), "VALUE");
5023        let detached = Parser::new(&spec)
5024            .parse(&input(&["test", "-磨", "V"]))
5025            .unwrap();
5026        assert_eq!(flag_string_value(&detached, "polish"), "V");
5027    }
5028
5029    #[test]
5030    fn test_as_env() {
5031        let cmd = SpecCommand::builder()
5032            .name("test")
5033            .arg(SpecArg::builder().name("arg").build())
5034            .flag(SpecFlag::builder().long("flag").build())
5035            .flag(
5036                SpecFlag::builder()
5037                    .long("force")
5038                    .negate("--no-force")
5039                    .build(),
5040            )
5041            .build();
5042        let spec = Spec {
5043            name: "test".to_string(),
5044            bin: "test".to_string(),
5045            cmd,
5046            ..Default::default()
5047        };
5048        let input = vec![
5049            "test".to_string(),
5050            "--flag".to_string(),
5051            "--no-force".to_string(),
5052        ];
5053        let parsed = parse(&spec, &input).unwrap();
5054        let env = parsed.as_env();
5055        assert_eq!(env.len(), 2);
5056        assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
5057        assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
5058    }
5059
5060    #[test]
5061    fn test_arg_env_var() {
5062        let cmd = SpecCommand::builder()
5063            .name("test")
5064            .arg(
5065                SpecArg::builder()
5066                    .name("input")
5067                    .env("TEST_ARG_INPUT")
5068                    .required(true)
5069                    .build(),
5070            )
5071            .build();
5072        let spec = Spec {
5073            name: "test".to_string(),
5074            bin: "test".to_string(),
5075            cmd,
5076            ..Default::default()
5077        };
5078
5079        // Set env var
5080        std::env::set_var("TEST_ARG_INPUT", "test_file.txt");
5081
5082        let input = vec!["test".to_string()];
5083        let parsed = parse(&spec, &input).unwrap();
5084
5085        assert_eq!(parsed.args.len(), 1);
5086        let arg = parsed.args.keys().next().unwrap();
5087        assert_eq!(arg.name, "input");
5088        let value = parsed.args.values().next().unwrap();
5089        assert_eq!(value.to_string(), "test_file.txt");
5090
5091        // Clean up
5092        std::env::remove_var("TEST_ARG_INPUT");
5093    }
5094
5095    #[test]
5096    fn test_flag_env_var_with_arg() {
5097        let cmd = SpecCommand::builder()
5098            .name("test")
5099            .flag(
5100                SpecFlag::builder()
5101                    .long("output")
5102                    .env("TEST_FLAG_OUTPUT")
5103                    .arg(SpecArg::builder().name("file").build())
5104                    .build(),
5105            )
5106            .build();
5107        let spec = Spec {
5108            name: "test".to_string(),
5109            bin: "test".to_string(),
5110            cmd,
5111            ..Default::default()
5112        };
5113
5114        // Set env var
5115        std::env::set_var("TEST_FLAG_OUTPUT", "output.txt");
5116
5117        let input = vec!["test".to_string()];
5118        let parsed = parse(&spec, &input).unwrap();
5119
5120        assert_eq!(parsed.flags.len(), 1);
5121        let flag = parsed.flags.keys().next().unwrap();
5122        assert_eq!(flag.name, "output");
5123        let value = parsed.flags.values().next().unwrap();
5124        assert_eq!(value.to_string(), "output.txt");
5125
5126        // Clean up
5127        std::env::remove_var("TEST_FLAG_OUTPUT");
5128    }
5129
5130    #[test]
5131    fn test_flag_env_var_boolean() {
5132        let cmd = SpecCommand::builder()
5133            .name("test")
5134            .flag(
5135                SpecFlag::builder()
5136                    .long("verbose")
5137                    .env("TEST_FLAG_VERBOSE")
5138                    .build(),
5139            )
5140            .build();
5141        let spec = Spec {
5142            name: "test".to_string(),
5143            bin: "test".to_string(),
5144            cmd,
5145            ..Default::default()
5146        };
5147
5148        // Set env var to true
5149        std::env::set_var("TEST_FLAG_VERBOSE", "true");
5150
5151        let input = vec!["test".to_string()];
5152        let parsed = parse(&spec, &input).unwrap();
5153
5154        assert_eq!(parsed.flags.len(), 1);
5155        let flag = parsed.flags.keys().next().unwrap();
5156        assert_eq!(flag.name, "verbose");
5157        let value = parsed.flags.values().next().unwrap();
5158        assert_eq!(value.to_string(), "true");
5159
5160        // Clean up
5161        std::env::remove_var("TEST_FLAG_VERBOSE");
5162    }
5163
5164    #[test]
5165    fn test_env_var_precedence() {
5166        // CLI args should take precedence over env vars
5167        let cmd = SpecCommand::builder()
5168            .name("test")
5169            .arg(
5170                SpecArg::builder()
5171                    .name("input")
5172                    .env("TEST_PRECEDENCE_INPUT")
5173                    .required(true)
5174                    .build(),
5175            )
5176            .build();
5177        let spec = Spec {
5178            name: "test".to_string(),
5179            bin: "test".to_string(),
5180            cmd,
5181            ..Default::default()
5182        };
5183
5184        // Set env var
5185        std::env::set_var("TEST_PRECEDENCE_INPUT", "env_file.txt");
5186
5187        let input = vec!["test".to_string(), "cli_file.txt".to_string()];
5188        let parsed = parse(&spec, &input).unwrap();
5189
5190        assert_eq!(parsed.args.len(), 1);
5191        let value = parsed.args.values().next().unwrap();
5192        // CLI arg should take precedence
5193        assert_eq!(value.to_string(), "cli_file.txt");
5194
5195        // Clean up
5196        std::env::remove_var("TEST_PRECEDENCE_INPUT");
5197    }
5198
5199    #[test]
5200    fn test_flag_var_true_with_single_default() {
5201        // When var=true and default="bar", the default should be MultiString(["bar"])
5202        let cmd = SpecCommand::builder()
5203            .name("test")
5204            .flag(
5205                SpecFlag::builder()
5206                    .long("foo")
5207                    .var(true)
5208                    .arg(SpecArg::builder().name("foo").build())
5209                    .default_value("bar")
5210                    .build(),
5211            )
5212            .build();
5213        let spec = Spec {
5214            name: "test".to_string(),
5215            bin: "test".to_string(),
5216            cmd,
5217            ..Default::default()
5218        };
5219
5220        // User doesn't provide the flag
5221        let input = vec!["test".to_string()];
5222        let parsed = parse(&spec, &input).unwrap();
5223
5224        assert_eq!(parsed.flags.len(), 1);
5225        let flag = parsed.flags.keys().next().unwrap();
5226        assert_eq!(flag.name, "foo");
5227        let value = parsed.flags.values().next().unwrap();
5228        // Should be MultiString, not String
5229        match value {
5230            ParseValue::MultiString(v) => {
5231                assert_eq!(v.len(), 1);
5232                assert_eq!(v[0], "bar");
5233            }
5234            _ => panic!("Expected MultiString, got {:?}", value),
5235        }
5236    }
5237
5238    #[test]
5239    fn test_flag_var_true_with_multiple_defaults() {
5240        // When var=true and multiple defaults, should return MultiString(["xyz", "bar"])
5241        let cmd = SpecCommand::builder()
5242            .name("test")
5243            .flag(
5244                SpecFlag::builder()
5245                    .long("foo")
5246                    .var(true)
5247                    .arg(SpecArg::builder().name("foo").build())
5248                    .default_values(["xyz", "bar"])
5249                    .build(),
5250            )
5251            .build();
5252        let spec = Spec {
5253            name: "test".to_string(),
5254            bin: "test".to_string(),
5255            cmd,
5256            ..Default::default()
5257        };
5258
5259        // User doesn't provide the flag
5260        let input = vec!["test".to_string()];
5261        let parsed = parse(&spec, &input).unwrap();
5262
5263        assert_eq!(parsed.flags.len(), 1);
5264        let value = parsed.flags.values().next().unwrap();
5265        // Should be MultiString with both values
5266        match value {
5267            ParseValue::MultiString(v) => {
5268                assert_eq!(v.len(), 2);
5269                assert_eq!(v[0], "xyz");
5270                assert_eq!(v[1], "bar");
5271            }
5272            _ => panic!("Expected MultiString, got {:?}", value),
5273        }
5274    }
5275
5276    #[test]
5277    fn test_flag_var_false_with_default_remains_string() {
5278        // When var=false (default), the default should still be String("bar")
5279        let cmd = SpecCommand::builder()
5280            .name("test")
5281            .flag(
5282                SpecFlag::builder()
5283                    .long("foo")
5284                    .var(false) // Default behavior
5285                    .arg(SpecArg::builder().name("foo").build())
5286                    .default_value("bar")
5287                    .build(),
5288            )
5289            .build();
5290        let spec = Spec {
5291            name: "test".to_string(),
5292            bin: "test".to_string(),
5293            cmd,
5294            ..Default::default()
5295        };
5296
5297        // User doesn't provide the flag
5298        let input = vec!["test".to_string()];
5299        let parsed = parse(&spec, &input).unwrap();
5300
5301        assert_eq!(parsed.flags.len(), 1);
5302        let value = parsed.flags.values().next().unwrap();
5303        // Should be String, not MultiString
5304        match value {
5305            ParseValue::String(s) => {
5306                assert_eq!(s, "bar");
5307            }
5308            _ => panic!("Expected String, got {:?}", value),
5309        }
5310    }
5311
5312    #[test]
5313    fn test_arg_var_true_with_single_default() {
5314        // When arg has var=true and default="bar", the default should be MultiString(["bar"])
5315        let cmd = SpecCommand::builder()
5316            .name("test")
5317            .arg(
5318                SpecArg::builder()
5319                    .name("files")
5320                    .var(true)
5321                    .default_value("default.txt")
5322                    .required(false)
5323                    .build(),
5324            )
5325            .build();
5326        let spec = Spec {
5327            name: "test".to_string(),
5328            bin: "test".to_string(),
5329            cmd,
5330            ..Default::default()
5331        };
5332
5333        // User doesn't provide the arg
5334        let input = vec!["test".to_string()];
5335        let parsed = parse(&spec, &input).unwrap();
5336
5337        assert_eq!(parsed.args.len(), 1);
5338        let value = parsed.args.values().next().unwrap();
5339        // Should be MultiString, not String
5340        match value {
5341            ParseValue::MultiString(v) => {
5342                assert_eq!(v.len(), 1);
5343                assert_eq!(v[0], "default.txt");
5344            }
5345            _ => panic!("Expected MultiString, got {:?}", value),
5346        }
5347    }
5348
5349    #[test]
5350    fn test_arg_var_true_with_multiple_defaults() {
5351        // When arg has var=true and multiple defaults
5352        let cmd = SpecCommand::builder()
5353            .name("test")
5354            .arg(
5355                SpecArg::builder()
5356                    .name("files")
5357                    .var(true)
5358                    .default_values(["file1.txt", "file2.txt"])
5359                    .required(false)
5360                    .build(),
5361            )
5362            .build();
5363        let spec = Spec {
5364            name: "test".to_string(),
5365            bin: "test".to_string(),
5366            cmd,
5367            ..Default::default()
5368        };
5369
5370        // User doesn't provide the arg
5371        let input = vec!["test".to_string()];
5372        let parsed = parse(&spec, &input).unwrap();
5373
5374        assert_eq!(parsed.args.len(), 1);
5375        let value = parsed.args.values().next().unwrap();
5376        // Should be MultiString with both values
5377        match value {
5378            ParseValue::MultiString(v) => {
5379                assert_eq!(v.len(), 2);
5380                assert_eq!(v[0], "file1.txt");
5381                assert_eq!(v[1], "file2.txt");
5382            }
5383            _ => panic!("Expected MultiString, got {:?}", value),
5384        }
5385    }
5386
5387    #[test]
5388    fn test_arg_var_false_with_default_remains_string() {
5389        // When arg has var=false (default), the default should still be String
5390        let cmd = SpecCommand::builder()
5391            .name("test")
5392            .arg(
5393                SpecArg::builder()
5394                    .name("file")
5395                    .var(false)
5396                    .default_value("default.txt")
5397                    .required(false)
5398                    .build(),
5399            )
5400            .build();
5401        let spec = Spec {
5402            name: "test".to_string(),
5403            bin: "test".to_string(),
5404            cmd,
5405            ..Default::default()
5406        };
5407
5408        // User doesn't provide the arg
5409        let input = vec!["test".to_string()];
5410        let parsed = parse(&spec, &input).unwrap();
5411
5412        assert_eq!(parsed.args.len(), 1);
5413        let value = parsed.args.values().next().unwrap();
5414        // Should be String, not MultiString
5415        match value {
5416            ParseValue::String(s) => {
5417                assert_eq!(s, "default.txt");
5418            }
5419            _ => panic!("Expected String, got {:?}", value),
5420        }
5421    }
5422
5423    #[test]
5424    fn test_scalar_defaults_validate_only_first_default_choice() {
5425        let specs = [
5426            spec_with_arg(
5427                SpecArg::builder()
5428                    .name("env")
5429                    .var(false)
5430                    .default_values(["dev", "prod"])
5431                    .choices(["dev"])
5432                    .required(false)
5433                    .build(),
5434            ),
5435            spec_with_flag(
5436                SpecFlag::builder()
5437                    .long("env")
5438                    .arg(
5439                        SpecArg::builder()
5440                            .name("env")
5441                            .default_values(["dev", "prod"])
5442                            .choices(["dev"])
5443                            .build(),
5444                    )
5445                    .build(),
5446            ),
5447        ];
5448
5449        for spec in specs {
5450            let parsed = parse(&spec, &input(&["test"])).unwrap();
5451            assert_eq!(first_string_value(&parsed), "dev");
5452        }
5453    }
5454
5455    #[test]
5456    fn a_delimiter_turns_one_word_into_several_values() {
5457        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--tags <tag>\" var=#true delimiter=\",\"\narg \"[files]...\" var=#true delimiter=\":\"\n"
5458            .parse()
5459            .unwrap();
5460
5461        let parsed = parse(&spec, &input(&["ex", "--tags", "a,b,c", "x:y"])).unwrap();
5462        let multi = |value: &ParseValue| match value {
5463            ParseValue::MultiString(values) => values.clone(),
5464            other => panic!("expected several values, got {other:?}"),
5465        };
5466        let tags = parsed
5467            .flags
5468            .iter()
5469            .find(|(f, _)| f.name == "tags")
5470            .map(|(_, v)| v)
5471            .unwrap();
5472        assert_eq!(multi(tags), vec!["a", "b", "c"]);
5473        assert_eq!(multi(parsed.args.values().next().unwrap()), vec!["x", "y"]);
5474    }
5475
5476    #[test]
5477    fn a_positional_splits_before_its_choices_are_asked() {
5478        // The flag path did this and the positional path did not, so a word whose parts
5479        // were all choices was rejected as one value, and a bad half was reported as the
5480        // whole word.
5481        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[paths]...\" var=#true delimiter=\":\" {\n  choices \"src\" \"docs\"\n}\n"
5482            .parse()
5483            .unwrap();
5484
5485        parse(&spec, &input(&["ex", "src:docs"])).expect("both halves are choices");
5486
5487        let err = parse(&spec, &input(&["ex", "src:nowhere"])).unwrap_err();
5488        let message = err.to_string();
5489        assert!(message.contains("nowhere"), "{message}");
5490        assert!(
5491            !message.contains("src:nowhere"),
5492            "the bad half should be named, not the whole word: {message}"
5493        );
5494    }
5495
5496    #[test]
5497    fn a_split_value_is_counted_and_judged_as_values() {
5498        // Split during the parse rather than after it, so everything downstream sees the
5499        // values the user meant rather than the words they typed: `choices` judges each
5500        // one, and the bounds count them.
5501        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <e>\" var=#true delimiter=\",\" var_max=2 {\n  choices \"dev\" \"prod\"\n}\n"
5502            .parse()
5503            .unwrap();
5504
5505        parse(&spec, &input(&["ex", "--env", "dev,prod"])).expect("two values, both allowed");
5506        let err = parse(&spec, &input(&["ex", "--env", "dev,staging"])).unwrap_err();
5507        assert!(err.to_string().contains("staging"), "{err}");
5508        assert!(
5509            parse(&spec, &input(&["ex", "--env", "dev,prod,dev"])).is_err(),
5510            "three values should breach var_max=2"
5511        );
5512    }
5513
5514    #[test]
5515    fn a_split_bound_counts_one_occurrence_at_a_time() {
5516        // The bound on a variadic flag *argument* is what one occurrence may take. Without a
5517        // delimiter the collection simply stops at it, so it could never be exceeded; a word
5518        // carrying several values can carry an occurrence past it in one step, and that is
5519        // the only way this bound is ever breached.
5520        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--include <pattern>...\" delimiter=\",\" {\n  arg \"<pattern>...\" var=#true var_max=2\n}\n"
5521            .parse()
5522            .unwrap();
5523
5524        parse(&spec, &input(&["ex", "--include", "a,b"])).expect("exactly the bound is fine");
5525        assert!(
5526            parse(&spec, &input(&["ex", "--include", "a,b,c"])).is_err(),
5527            "three values out of one word is still three values"
5528        );
5529        // The rule the corpus documents for plain words, on split ones: a second occurrence
5530        // starts counting again rather than adding to the first.
5531        parse(
5532            &spec,
5533            &input(&["ex", "--include", "a,b", "--include", "c,d"]),
5534        )
5535        .expect("two per occurrence, twice, is within the bound");
5536    }
5537
5538    #[test]
5539    fn a_nested_minimum_is_checked_once_per_flag_occurrence() {
5540        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pair <value>...\" {\n  arg \"<value>...\" var=#true var_min=2 var_max=2\n}\n"
5541            .parse()
5542            .unwrap();
5543
5544        parse(
5545            &spec,
5546            &input(&["ex", "--pair", "a", "b", "--pair", "c", "d"]),
5547        )
5548        .expect("each occurrence satisfies the bound independently");
5549
5550        let error = parse(&spec, &input(&["ex", "--pair", "a", "--pair", "b", "c"])).unwrap_err();
5551        assert!(
5552            error
5553                .to_string()
5554                .contains("requires at least 2 value(s), got 1"),
5555            "{error:?}"
5556        );
5557    }
5558
5559    #[test]
5560    fn an_exclusive_flag_has_to_be_alone() {
5561        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--verbose\"\narg \"[target]\"\n"
5562            .parse()
5563            .unwrap();
5564
5565        parse(&spec, &input(&["ex", "--dump"])).expect("alone is the point");
5566
5567        // Any other flag.
5568        let err = parse(&spec, &input(&["ex", "--dump", "--verbose"])).unwrap_err();
5569        assert!(err.to_string().contains("on its own"), "{err}");
5570
5571        // And a positional, which is what makes this more than a conflict with every
5572        // other flag.
5573        let err = parse(&spec, &input(&["ex", "--dump", "t"])).unwrap_err();
5574        assert!(err.to_string().contains("on its own"), "{err}");
5575
5576        // Not given, so it imposes nothing.
5577        parse(&spec, &input(&["ex", "--verbose", "t"])).expect("without it, nothing changes");
5578    }
5579
5580    #[test]
5581    fn an_exclusive_flag_is_not_disturbed_by_a_default() {
5582        // Only what was supplied counts, as `conflicts` reads it. A default counting as
5583        // company would make an exclusive flag unusable on any command that has one.
5584        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--jobs <n>\" default=\"4\"\n"
5585            .parse()
5586            .unwrap();
5587
5588        parse(&spec, &input(&["ex", "--dump"])).expect("a default is nobody saying anything");
5589        assert!(parse(&spec, &input(&["ex", "--dump", "--jobs", "8"])).is_err());
5590    }
5591
5592    #[test]
5593    fn an_exclusive_flag_bypasses_required_siblings() {
5594        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--out <path>\" required=#true\narg \"<target>\"\n"
5595            .parse()
5596            .unwrap();
5597
5598        parse(&spec, &input(&["ex", "--dump"]))
5599            .expect("exclusive is the command's requiredness escape");
5600        assert!(parse(
5601            &spec,
5602            &input(&["ex", "--dump", "--out", "somewhere", "target"])
5603        )
5604        .is_err());
5605    }
5606
5607    #[test]
5608    fn an_environment_value_counts_for_an_exclusive_flag() {
5609        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--out <path>\" env=\"EX_OUT\"\n"
5610            .parse()
5611            .unwrap();
5612
5613        assert!(parse_with_env(&spec, &["ex", "--dump"], &[("EX_OUT", "somewhere")]).is_err());
5614        parse_with_env(&spec, &["ex", "--dump"], &[]).expect("without the value it is alone");
5615    }
5616
5617    #[test]
5618    fn a_selected_subcommand_counts_for_an_ancestor_exclusive_flag() {
5619        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--version\" global=#true exclusive=#true\ncmd \"run\"\n"
5620            .parse()
5621            .unwrap();
5622
5623        parse(&spec, &input(&["ex", "--version"])).expect("alone is allowed");
5624        assert!(parse(&spec, &input(&["ex", "--version", "run"])).is_err());
5625    }
5626
5627    #[test]
5628    fn a_child_exclusive_flag_is_not_mistaken_for_a_same_named_parent_flag() {
5629        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n}\n"
5630            .parse()
5631            .unwrap();
5632
5633        parse(&spec, &input(&["ex", "run", "--clean"]))
5634            .expect("the child flag is alone within the child command");
5635        assert!(
5636            parse(&spec, &input(&["ex", "--clean", "run"])).is_err(),
5637            "the parent flag still conflicts with selecting the child"
5638        );
5639    }
5640
5641    #[test]
5642    fn a_child_local_exclusive_redeclaration_belongs_to_the_child() {
5643        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n}\n"
5644            .parse()
5645            .unwrap();
5646
5647        parse(&spec, &input(&["ex", "run", "--clean"]))
5648            .expect("the child-local exclusive flag is alone inside the child command");
5649        assert!(
5650            parse(&spec, &input(&["ex", "--clean", "run"])).is_err(),
5651            "the ancestor spelling still conflicts with selecting the child"
5652        );
5653    }
5654
5655    #[test]
5656    fn a_same_named_parent_flag_is_company_for_a_child_exclusive_flag() {
5657        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" global=#true exclusive=#true\n}\n"
5658            .parse()
5659            .unwrap();
5660
5661        parse(&spec, &input(&["ex", "run", "--clean"])).expect("the child exclusive flag is alone");
5662        assert!(
5663            parse(&spec, &input(&["ex", "--clean", "run", "--clean"])).is_err(),
5664            "the distinct parent declaration is still company despite sharing a name"
5665        );
5666    }
5667
5668    #[test]
5669    fn a_local_child_redeclaration_keeps_its_exclusivity_when_merged() {
5670        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n  flag \"--verbose\"\n}\n"
5671            .parse()
5672            .unwrap();
5673
5674        parse(&spec, &input(&["ex", "run", "--clean"]))
5675            .expect("the child exclusive flag is valid alone");
5676        assert!(
5677            parse(&spec, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
5678            "merging with the inherited global must not discard child exclusivity"
5679        );
5680    }
5681
5682    #[test]
5683    fn an_orphan_parent_alias_does_not_disown_a_child_local_exclusive_flag() {
5684        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n}\n"
5685            .parse()
5686            .unwrap();
5687
5688        parse(&spec, &input(&["ex", "run", "--clean"]))
5689            .expect("the typed long form belongs to the child declaration");
5690        assert!(
5691            parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
5692            "the inherited short form still belongs to the ancestor"
5693        );
5694        assert!(
5695            parse(&spec, &input(&["ex", "run", "-c", "--clean"])).is_err(),
5696            "a child spelling cannot mask the ancestor-exclusive occurrence on the same merged flag"
5697        );
5698    }
5699
5700    #[test]
5701    fn an_inherited_alias_keeps_its_ancestor_exclusivity() {
5702        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" global=#true\n}\n"
5703            .parse()
5704            .unwrap();
5705
5706        parse(&spec, &input(&["ex", "run", "--clean"]))
5707            .expect("the child's spelling does not activate the orphan ancestor alias");
5708        assert!(
5709            parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
5710            "the inherited short alias still belongs to the ancestor exclusive flag"
5711        );
5712    }
5713
5714    #[test]
5715    fn an_inherited_negated_alias_keeps_its_ancestor_exclusivity() {
5716        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" negate=\"--no-clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"-c --clean\" global=#true\n}\n"
5717            .parse()
5718            .unwrap();
5719
5720        assert!(
5721            parse(&spec, &input(&["ex", "run", "--no-clean"])).is_err(),
5722            "the inherited negated alias still belongs to the ancestor exclusive flag"
5723        );
5724    }
5725
5726    #[test]
5727    fn a_colliding_alias_does_not_disown_the_child_from_the_rest() {
5728        // The child re-declares the inherited `--clean` as exclusive and gives it a `-c` that
5729        // an unrelated inherited global already owns. That collision is resolved in the other
5730        // global's favor, so the child's `-c` resolves elsewhere — but the child plainly owns
5731        // the `--clean` it declared, and its exclusivity holds.
5732        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\nflag \"-c --config <f>\" global=#true\ncmd \"run\" {\n  flag \"-c --clean\" exclusive=#true\n  flag \"--verbose\"\n}\n"
5733            .parse()
5734            .unwrap();
5735
5736        parse(&spec, &input(&["ex", "run", "--clean"])).expect("alone is allowed");
5737        assert!(
5738            parse(&spec, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
5739            "one unrelated alias collision cannot disown the child from its own flag"
5740        );
5741    }
5742
5743    #[test]
5744    fn a_local_child_declaration_is_not_in_scope_before_the_subcommand() {
5745        // A child's *local* re-declaration describes the flag at the child. Typed ahead of the
5746        // subcommand word the flag can only be the ancestor's, because that is the only one in
5747        // scope there — so the ancestor's exclusivity is the one that answers, whichever way it
5748        // is set. The pair below differ in nothing else, which is what makes this one rule
5749        // rather than two behaviors.
5750        let quiet: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n  flag \"--verbose\"\n}\n"
5751            .parse()
5752            .unwrap();
5753        parse(&quiet, &input(&["ex", "--clean", "run", "--verbose"]))
5754            .expect("the ancestor owns this occurrence, and it is not exclusive");
5755        assert!(
5756            parse(&quiet, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
5757            "after the subcommand word the child's declaration is in scope, and it is exclusive"
5758        );
5759
5760        let loud: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n}\n"
5761            .parse()
5762            .unwrap();
5763        assert!(
5764            parse(&loud, &input(&["ex", "--clean", "run"])).is_err(),
5765            "the same rule, with an exclusive ancestor: selecting the child is company for it"
5766        );
5767    }
5768
5769    #[test]
5770    fn an_orphan_ancestor_alias_keeps_its_exclusivity_past_a_plain_child_redeclaration() {
5771        // The mirror of `a_local_child_redeclaration_keeps_its_exclusivity_when_merged`: the
5772        // child owns `--clean` and says nothing about exclusivity, but `-c` is a spelling only
5773        // the ancestor ever declared, so the ancestor's answer still governs it.
5774        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\"\n  flag \"--verbose\"\n}\n"
5775            .parse()
5776            .unwrap();
5777
5778        assert!(
5779            parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
5780            "the orphan ancestor alias is still the ancestor's exclusive flag"
5781        );
5782        parse(&spec, &input(&["ex", "run", "--clean", "--verbose"]))
5783            .expect("the child's own spelling drops the exclusivity the child did not restate");
5784    }
5785
5786    #[test]
5787    fn a_child_spelling_carries_its_exclusivity_even_beside_an_ancestor_spelling() {
5788        // Both spellings of one merged flag, typed together. The child's `--clean` is exclusive
5789        // whatever else was typed alongside it, so `--verbose` is company; attributing the whole
5790        // occurrence to the ancestor because `-c` appeared in it lost that.
5791        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n  flag \"--verbose\"\n}\n"
5792            .parse()
5793            .unwrap();
5794
5795        assert!(
5796            parse(&spec, &input(&["ex", "run", "-c", "--clean", "--verbose"])).is_err(),
5797            "the child spelling is exclusive whatever it was typed beside"
5798        );
5799        parse(&spec, &input(&["ex", "run", "-c", "--verbose"]))
5800            .expect("the ancestor's own spelling was never exclusive");
5801    }
5802
5803    #[test]
5804    fn an_environment_value_takes_the_exclusivity_of_the_declaration_in_scope() {
5805        // An environment value has no spelling to attribute, so the declaration the selected
5806        // command has in scope answers — in both directions. Comparing whole alias sets asked
5807        // the ancestor instead, because the merged flag also carries its orphan `-c`.
5808        let added: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true env=\"EX_CLEAN\"\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n  flag \"--verbose\"\n}\n"
5809            .parse()
5810            .unwrap();
5811
5812        assert!(
5813            parse_with_env(&added, &["ex", "run", "--verbose"], &[("EX_CLEAN", "1")]).is_err(),
5814            "the child added exclusivity the environment value has to honor"
5815        );
5816
5817        let dropped: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true env=\"EX_CLEAN\"\ncmd \"run\" {\n  flag \"--clean\"\n  flag \"--verbose\"\n}\n"
5818            .parse()
5819            .unwrap();
5820
5821        parse_with_env(&dropped, &["ex", "run", "--verbose"], &[("EX_CLEAN", "1")])
5822            .expect("the child dropped the exclusivity, and the environment value follows it");
5823    }
5824
5825    #[test]
5826    fn a_merged_child_exclusive_flag_still_escapes_requiredness() {
5827        // Exclusivity suppresses missing-value checks, and that has to survive the merge for
5828        // the same reason the companion check does.
5829        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n  flag \"--out <path>\" required=#true\n}\n"
5830            .parse()
5831            .unwrap();
5832
5833        parse(&spec, &input(&["ex", "run", "--clean"]))
5834            .expect("a merged child exclusive flag is still the command's requiredness escape");
5835    }
5836
5837    #[test]
5838    fn a_group_allows_one_member_and_refuses_two() {
5839        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\"\nflag \"--url <u>\"\nflag \"--stdin\"\ngroup \"input\" \"--file\" \"--url\" \"--stdin\"\n"
5840            .parse()
5841            .unwrap();
5842
5843        // One is fine, and so is none: a plain group says "at most one".
5844        parse(&spec, &input(&["ex", "--file", "a.txt"])).expect("one member is fine");
5845        parse(&spec, &input(&["ex"])).expect("a group that is not required asks for nothing");
5846
5847        let err = parse(&spec, &input(&["ex", "--file", "a.txt", "--stdin"])).unwrap_err();
5848        assert!(err.to_string().contains("group input"), "{err}");
5849    }
5850
5851    #[test]
5852    fn positional_selectors_work_in_conflicts_and_groups() {
5853        let conflicts: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--from-file <path>\" conflicts=\"value\"\narg \"[value]\"\n"
5854            .parse()
5855            .unwrap();
5856        parse(&conflicts, &input(&["ex", "--from-file", "vars.env"]))
5857            .expect("the flag alone is valid");
5858        parse(&conflicts, &input(&["ex", "literal"])).expect("the positional alone is valid");
5859        assert!(parse(
5860            &conflicts,
5861            &input(&["ex", "--from-file", "vars.env", "literal"])
5862        )
5863        .is_err());
5864
5865        let positional_source: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--from-file <path>\"\narg \"[value]\" conflicts=\"--from-file\"\n"
5866            .parse()
5867            .unwrap();
5868        assert!(parse(
5869            &positional_source,
5870            &input(&["ex", "--from-file", "vars.env", "literal"])
5871        )
5872        .is_err());
5873
5874        let group: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <path>\"\narg \"[target]\"\ngroup \"input\" \"--file\" \"target\" required=#true\n"
5875            .parse()
5876            .unwrap();
5877        assert!(parse(&group, &input(&["ex"])).is_err());
5878        parse(&group, &input(&["ex", "target-name"]))
5879            .expect("a positional satisfies a required group");
5880        assert!(parse(
5881            &group,
5882            &input(&["ex", "--file", "input.txt", "target-name"])
5883        )
5884        .is_err());
5885    }
5886
5887    #[test]
5888    fn a_required_group_needs_one_of_its_members() {
5889        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
5890            .parse()
5891            .unwrap();
5892
5893        let err = parse(&spec, &input(&["ex"])).unwrap_err();
5894        // The members, because that is what a user has to type; the name, because a
5895        // command with several groups would otherwise report the same sentence twice.
5896        assert!(err.to_string().contains("--file, --url"), "{err}");
5897        assert!(err.to_string().contains("input"), "{err}");
5898
5899        parse(&spec, &input(&["ex", "--url", "u"])).expect("one member satisfies it");
5900    }
5901
5902    #[test]
5903    fn a_multiple_group_only_polices_requiredness() {
5904        // `multiple` with `required` is "at least one of these", so two is fine and
5905        // none is not.
5906        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--a\"\nflag \"--b\"\ngroup \"any\" \"--a\" \"--b\" required=#true multiple=#true\n"
5907            .parse()
5908            .unwrap();
5909
5910        parse(&spec, &input(&["ex", "--a", "--b"])).expect("multiple allows both");
5911        assert!(parse(&spec, &input(&["ex"])).is_err());
5912    }
5913
5914    #[test]
5915    fn a_group_reads_a_default_for_requiredness_and_not_for_exclusivity() {
5916        // The two halves of a group are two kinds of rule, and they read a default
5917        // differently on purpose. Requiredness asks whether a member has a value, and a
5918        // default is a value — the rule `requires` follows. Exclusivity asks what the
5919        // user supplied, because a defaulted member counted as supplied would collide
5920        // with the sibling they actually typed and refuse a correct command line.
5921        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" default=\"a.txt\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
5922            .parse()
5923            .unwrap();
5924
5925        parse(&spec, &input(&["ex"])).expect("the default fills the group");
5926        parse(&spec, &input(&["ex", "--url", "u"]))
5927            .expect("the default must not conflict with the flag the user typed");
5928    }
5929
5930    #[test]
5931    fn a_group_naming_two_spellings_of_one_flag_is_not_a_conflict() {
5932        // `-f` and `--file` are one flag. Counted by selector, giving it once would
5933        // report it as conflicting with itself.
5934        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-f --file <f>\"\nflag \"--url <u>\"\ngroup \"input\" \"-f\" \"--file\" \"--url\"\n"
5935            .parse()
5936            .unwrap();
5937
5938        parse(&spec, &input(&["ex", "--file", "a.txt"])).expect("one flag is one member");
5939        parse(&spec, &input(&["ex", "-f", "a.txt"])).expect("either spelling, still one member");
5940
5941        // A genuine collision is still one.
5942        let err = parse(&spec, &input(&["ex", "--file", "a.txt", "--url", "u"])).unwrap_err();
5943        assert!(err.to_string().contains("group input"), "{err}");
5944    }
5945
5946    #[test]
5947    fn a_group_reads_the_environment_as_given() {
5948        // The environment does count, which is the same asymmetry `conflicts` has: an
5949        // env var is somebody saying something, a default is nobody saying anything.
5950        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" env=\"EX_FILE\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
5951            .parse()
5952            .unwrap();
5953
5954        parse_with_env(&spec, &["ex"], &[("EX_FILE", "a.txt")]).expect("the environment fills it");
5955    }
5956
5957    #[test]
5958    fn a_requirement_names_the_flag_that_is_missing() {
5959        // Reported as the missing flag rather than as something wrong with `--out`,
5960        // which is what clap says for an unmet `requires` and what a user can act on.
5961        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\"\n"
5962            .parse()
5963            .unwrap();
5964
5965        let err = parse(&spec, &input(&["ex", "--out", "a.txt"])).unwrap_err();
5966        assert!(
5967            err.to_string().contains("format"),
5968            "the missing flag should be named: {err}"
5969        );
5970
5971        // Satisfied, in either order.
5972        for words in [
5973            &["ex", "--out", "a.txt", "--format", "json"][..],
5974            &["ex", "--format", "json", "--out", "a.txt"][..],
5975        ] {
5976            parse(&spec, &input(words)).unwrap_or_else(|e| panic!("{words:?}: {e}"));
5977        }
5978
5979        // Nothing happens when the flag that imposes the rule is absent: a requirement
5980        // is a consequence of using the flag, not a rule about the command line.
5981        parse(&spec, &input(&["ex"])).expect("a bare invocation requires nothing");
5982    }
5983
5984    #[test]
5985    fn a_value_activates_only_its_conditional_requirement() {
5986        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--config <file>\" {\n  requires_if \"special.toml\" \"--key\"\n  requires_if \"remote.toml\" \"--token\"\n}\nflag \"--key <key>\"\nflag \"--token <token>\"\n"
5987            .parse()
5988            .unwrap();
5989
5990        parse(&spec, &input(&["ex", "--config", "ordinary.toml"]))
5991            .expect("an unrelated value requires nothing");
5992
5993        let key = parse(&spec, &input(&["ex", "--config", "special.toml"])).unwrap_err();
5994        assert!(key.to_string().contains("key"), "{key}");
5995        parse(
5996            &spec,
5997            &input(&["ex", "--config", "special.toml", "--key", "secret"]),
5998        )
5999        .expect("the matching requirement is satisfied");
6000
6001        let token = parse(&spec, &input(&["ex", "--config", "remote.toml"])).unwrap_err();
6002        assert!(token.to_string().contains("token"), "{token}");
6003    }
6004
6005    #[test]
6006    fn conditional_requirements_read_explicit_env_but_not_defaults() {
6007        let from_env: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--config <file>\" env=\"EX_CONFIG\" {\n  requires_if \"special.toml\" \"--key\"\n}\nflag \"--key <key>\"\n"
6008            .parse()
6009            .unwrap();
6010        let err = parse_with_env(&from_env, &["ex"], &[("EX_CONFIG", "special.toml")]).unwrap_err();
6011        assert!(err.to_string().contains("key"), "{err}");
6012
6013        let from_default: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--config <file>\" default=\"special.toml\" {\n  requires_if \"special.toml\" \"--key\"\n}\nflag \"--key <key>\"\n"
6014            .parse()
6015            .unwrap();
6016        parse(&from_default, &input(&["ex"]))
6017            .expect("a default is not an explicit conditional value");
6018    }
6019
6020    #[test]
6021    fn command_line_values_override_env_for_conditional_requirements() {
6022        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--config <file>\" env=\"EX_CONFIG\" {\n  requires_if \"special.toml\" \"--key\"\n}\nflag \"--key <key>\"\n"
6023            .parse()
6024            .unwrap();
6025
6026        parse_with_env(
6027            &spec,
6028            &["ex", "--config", "ordinary.toml"],
6029            &[("EX_CONFIG", "special.toml")],
6030        )
6031        .expect("the command-line value takes precedence over the environment");
6032    }
6033
6034    #[test]
6035    fn conditional_requirements_normalize_boolean_env_values() {
6036        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--feature\" env=\"EX_FEATURE\" {\n  requires_if \"true\" \"--key\"\n}\nflag \"--key <key>\"\n"
6037            .parse()
6038            .unwrap();
6039
6040        for value in ["1", "true", "True", "TRUE"] {
6041            let err = parse_with_env(&spec, &["ex"], &[("EX_FEATURE", value)]).unwrap_err();
6042            assert!(err.to_string().contains("key"), "{value}: {err}");
6043        }
6044        parse_with_env(&spec, &["ex"], &[("EX_FEATURE", "false")])
6045            .expect("a false environment value does not activate a true condition");
6046    }
6047
6048    #[test]
6049    fn a_default_satisfies_a_requirement() {
6050        // The flag it names has a value, which is the question a requirement asks. Read
6051        // any other way, `--format` would be missing here and present ten lines further
6052        // down, where plain required-ness reads the same default as filling it.
6053        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" default=\"json\"\n"
6054            .parse()
6055            .unwrap();
6056
6057        parse(&spec, &input(&["ex", "--out", "a.txt"]))
6058            .expect("a defaulted flag is not a missing one");
6059    }
6060
6061    #[test]
6062    fn a_present_flag_binds_a_conditional_default() {
6063        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" {\n  default_if \"--json\" \"true\"\n}\nflag \"--json\"\n"
6064            .parse()
6065            .unwrap();
6066
6067        let with = parse(&spec, &input(&["ex", "--json"])).unwrap();
6068        assert_eq!(
6069            with.as_env().get("usage_bin_names").map(String::as_str),
6070            Some("true")
6071        );
6072
6073        let without = parse(&spec, &input(&["ex"])).unwrap();
6074        assert!(
6075            !without.as_env().contains_key("usage_bin_names"),
6076            "IsPresent does nothing when the selector is absent"
6077        );
6078    }
6079
6080    #[test]
6081    fn an_equals_condition_binds_a_conditional_default() {
6082        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--style <s>\" {\n  default_if \"--output\" \"json\" \"pretty\"\n}\nflag \"--output <fmt>\"\n"
6083            .parse()
6084            .unwrap();
6085
6086        let json = parse(&spec, &input(&["ex", "--output", "json"])).unwrap();
6087        assert_eq!(
6088            json.as_env().get("usage_style").map(String::as_str),
6089            Some("pretty")
6090        );
6091        let yaml = parse(&spec, &input(&["ex", "--output", "yaml"])).unwrap();
6092        assert!(!yaml.as_env().contains_key("usage_style"));
6093    }
6094
6095    #[test]
6096    fn an_equals_condition_reads_a_negated_flag() {
6097        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pretty\" {\n  default_if \"--json\" \"false\" \"true\"\n}\nflag \"--json\" negate=\"--no-json\"\n"
6098            .parse()
6099            .unwrap();
6100
6101        let off = parse(&spec, &input(&["ex", "--no-json"])).unwrap();
6102        assert_eq!(
6103            off.as_env().get("usage_pretty").map(String::as_str),
6104            Some("true")
6105        );
6106        let on = parse(&spec, &input(&["ex", "--json"])).unwrap();
6107        assert!(
6108            !on.as_env().contains_key("usage_pretty"),
6109            "--json is true, so when=false should miss"
6110        );
6111    }
6112
6113    #[test]
6114    fn the_first_matching_conditional_default_wins() {
6115        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--style <s>\" {\n  default_if \"--json\" \"compact\"\n  default_if \"--pretty\" \"pretty\"\n}\nflag \"--json\"\nflag \"--pretty\"\n"
6116            .parse()
6117            .unwrap();
6118
6119        let out = parse(&spec, &input(&["ex", "--json", "--pretty"])).unwrap();
6120        assert_eq!(
6121            out.as_env().get("usage_style").map(String::as_str),
6122            Some("compact")
6123        );
6124    }
6125
6126    #[test]
6127    fn argv_and_env_suppress_a_conditional_default() {
6128        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" env=\"EX_BIN\" {\n  default_if \"--json\" \"true\"\n}\nflag \"--json\"\n"
6129            .parse()
6130            .unwrap();
6131
6132        let from_env = parse_with_env(&spec, &["ex", "--json"], &[("EX_BIN", "false")]).unwrap();
6133        assert_eq!(
6134            from_env.as_env().get("usage_bin_names").map(String::as_str),
6135            Some("false"),
6136            "the target's environment wins over default_if"
6137        );
6138    }
6139
6140    #[test]
6141    fn a_sibling_env_activates_a_conditional_default() {
6142        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" {\n  default_if \"--json\" \"true\"\n}\nflag \"--json\" env=\"EX_JSON\"\n"
6143            .parse()
6144            .unwrap();
6145
6146        let out = parse_with_env(&spec, &["ex"], &[("EX_JSON", "1")]).unwrap();
6147        assert_eq!(
6148            out.as_env().get("usage_bin_names").map(String::as_str),
6149            Some("true")
6150        );
6151    }
6152
6153    #[test]
6154    fn a_default_does_not_activate_a_conditional_default() {
6155        // `--json` sorts before `--pretty` in the available-flag map, so a one-pass
6156        // bind would put json's default into `out.flags` and then treat it as
6157        // explicit for pretty's `default_if`. Go and the derive ignore defaults.
6158        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pretty\" {\n  default_if \"--json\" \"true\"\n}\nflag \"--json\" default=#true\n"
6159            .parse()
6160            .unwrap();
6161
6162        let out = parse(&spec, &input(&["ex"])).unwrap();
6163        assert_eq!(
6164            out.as_env().get("usage_json").map(String::as_str),
6165            Some("true")
6166        );
6167        assert!(
6168            !out.as_env().contains_key("usage_pretty"),
6169            "a default is not an explicit value for default_if"
6170        );
6171    }
6172
6173    #[test]
6174    fn a_conditional_default_does_not_activate_requires_if() {
6175        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--format <f>\" {\n  default_if \"--json\" \"json\"\n  requires_if \"json\" \"--schema\"\n}\nflag \"--schema <s>\"\nflag \"--json\"\n"
6176            .parse()
6177            .unwrap();
6178
6179        parse(&spec, &input(&["ex", "--json"]))
6180            .expect("a default_if value is not explicit for requires_if");
6181        assert!(parse(&spec, &input(&["ex", "--format", "json"])).is_err());
6182    }
6183
6184    #[test]
6185    fn a_conditional_default_satisfies_a_requirement() {
6186        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" {\n  default_if \"--json\" \"json\"\n}\nflag \"--json\"\n"
6187            .parse()
6188            .unwrap();
6189
6190        parse(&spec, &input(&["ex", "--out", "a.txt", "--json"]))
6191            .expect("default_if fills the required flag");
6192        assert!(parse(&spec, &input(&["ex", "--out", "a.txt"])).is_err());
6193    }
6194
6195    #[test]
6196    fn an_environment_value_satisfies_a_requirement() {
6197        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" env=\"EX_FORMAT\"\n"
6198            .parse()
6199            .unwrap();
6200
6201        assert!(parse(&spec, &input(&["ex", "--out", "a.txt"])).is_err());
6202        parse_with_env(&spec, &["ex", "--out", "a.txt"], &[("EX_FORMAT", "json")])
6203            .expect("the environment supplies it");
6204    }
6205
6206    #[test]
6207    fn a_requirement_is_satisfied_by_a_short_form() {
6208        // The selector may spell the other flag any way it answers to, so the check
6209        // resolves it the way every other selector is resolved rather than matching
6210        // text. The error names the flag, not the selector.
6211        let spec: Spec =
6212            "name \"ex\"\nbin \"ex\"\nflag \"--sign\" requires=\"-k\"\nflag \"-k --key <k>\"\n"
6213                .parse()
6214                .unwrap();
6215
6216        parse(&spec, &input(&["ex", "--sign", "--key", "x"])).expect("--key satisfies -k");
6217
6218        let err = parse(&spec, &input(&["ex", "--sign"])).unwrap_err();
6219        assert!(err.to_string().contains("key"), "{err}");
6220    }
6221
6222    #[test]
6223    fn conflicting_flags_are_rejected_in_either_order() {
6224        // Declared once, on `--file`, which is all clap exposes — so the check has to
6225        // be order-independent by looking at every flag that was given rather than at
6226        // the one that declared the conflict.
6227        let spec: Spec =
6228            "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" conflicts=\"--stdin\"\nflag \"--stdin\"\n"
6229                .parse()
6230                .unwrap();
6231
6232        for words in [
6233            &["ex", "--file", "a.txt", "--stdin"][..],
6234            &["ex", "--stdin", "--file", "a.txt"][..],
6235        ] {
6236            let err = parse(&spec, &input(words)).unwrap_err();
6237            assert!(
6238                err.to_string().contains("conflicts with --stdin"),
6239                "{words:?} should be refused: {err}"
6240            );
6241        }
6242
6243        // Either one alone is fine.
6244        parse(&spec, &input(&["ex", "--stdin"])).unwrap();
6245        parse(&spec, &input(&["ex", "--file", "a.txt"])).unwrap();
6246    }
6247
6248    #[test]
6249    fn unknown_flags_are_values_by_default() {
6250        // The default, and the reason it is the default: a spec often parses a
6251        // command line whose flags belong to something else.
6252        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--force\"\narg \"[rest]...\"\n"
6253            .parse()
6254            .unwrap();
6255        let out = parse(
6256            &spec,
6257            &["ex".to_string(), "--wat".to_string(), "x".to_string()],
6258        )
6259        .unwrap();
6260        let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
6261        assert_eq!(out.args[rest].to_string(), "--wat x");
6262    }
6263
6264    #[test]
6265    fn repeated_scalar_flags_override_by_default_and_can_be_strict() {
6266        let permissive: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--jobs <n>\"\nflag \"--verbose\"\n"
6267            .parse()
6268            .unwrap();
6269        let out = parse(&permissive, &input(&["ex", "--jobs", "1", "--jobs", "2"]))
6270            .expect("a repeat is a correction by default");
6271        let jobs = out.flags.keys().find(|f| f.name == "jobs").unwrap();
6272        assert_eq!(out.flags[jobs].to_string(), "2");
6273        parse(&permissive, &input(&["ex", "--verbose", "--verbose"]))
6274            .expect("switches use the same default");
6275
6276        let strict: Spec = "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--jobs <n>\"\nflag \"--verbose\"\n"
6277            .parse()
6278            .unwrap();
6279        for words in [
6280            &["ex", "--jobs", "1", "--jobs", "2"][..],
6281            &["ex", "--verbose", "--verbose"][..],
6282        ] {
6283            let err = parse(&strict, &input(words)).unwrap_err();
6284            assert!(
6285                err.to_string().contains("cannot be used multiple times"),
6286                "{err}"
6287            );
6288        }
6289
6290        let reparsed: Spec = strict.to_string().parse().unwrap();
6291        assert!(!reparsed.cmd.args_override_self);
6292    }
6293
6294    #[test]
6295    fn strict_negated_flags_allow_opposite_forms_but_reject_the_same_form() {
6296        let spec: Spec = "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--color\" negate=\"--no-color\"\n"
6297            .parse()
6298            .unwrap();
6299
6300        let out = parse(&spec, &input(&["ex", "--color", "--no-color"]))
6301            .expect("opposite forms override each other");
6302        let color = out.flags.keys().find(|f| f.name == "color").unwrap();
6303        assert!(matches!(out.flags[color], ParseValue::Bool(false)));
6304
6305        for words in [
6306            &["ex", "--color", "--color"][..],
6307            &["ex", "--no-color", "--no-color"][..],
6308        ] {
6309            let err = parse(&spec, &input(words)).unwrap_err();
6310            assert!(err.to_string().contains("cannot be used multiple times"));
6311        }
6312    }
6313
6314    #[test]
6315    fn strict_global_flags_may_repeat_across_command_levels() {
6316        let spec: Spec = "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--color\" negate=\"--no-color\" global=#true\nflag \"--jobs <n>\" global=#true\ncmd \"run\" {\n  args_override_self #false\n}\n"
6317            .parse()
6318            .unwrap();
6319
6320        let out = parse(
6321            &spec,
6322            &input(&[
6323                "ex", "--color", "--jobs", "1", "run", "--color", "--jobs", "2",
6324            ]),
6325        )
6326        .expect("an inherited global is allowed once at each command level");
6327        let jobs = out.flags.keys().find(|f| f.name == "jobs").unwrap();
6328        assert_eq!(out.flags[jobs].to_string(), "2");
6329
6330        for words in [
6331            &["ex", "--color", "--color", "run", "--no-color"][..],
6332            &["ex", "--jobs", "1", "run", "--jobs", "2", "--jobs", "3"][..],
6333        ] {
6334            let err = parse(&spec, &input(words)).unwrap_err();
6335            assert!(err.to_string().contains("cannot be used multiple times"));
6336        }
6337    }
6338
6339    #[test]
6340    fn a_subcommand_can_negate_only_its_parents_requirements() {
6341        let base = r#"name "ex"
6342bin "ex"
6343flag "--config" required=#true
6344flag "--mode" requires="--config"
6345flag "--other"
6346arg "<input>"
6347group "source" "--config" "--other" required=#true
6348cmd "run" { flag "--child" required=#true }
6349"#;
6350        let strict: Spec = base.parse().unwrap();
6351        let err = parse(&strict, &input(&["ex", "run"])).unwrap_err();
6352        let message = err.to_string();
6353        assert!(
6354            message.contains("input") || message.contains("config"),
6355            "{message}"
6356        );
6357
6358        let negated: Spec = base
6359            .replacen("bin \"ex\"", "bin \"ex\"\nsubcommand_negates_reqs #true", 1)
6360            .parse()
6361            .unwrap();
6362        let err = parse(&negated, &input(&["ex", "run"])).unwrap_err();
6363        assert!(
6364            err.to_string().contains("child"),
6365            "the selected command keeps its own requirements: {err}"
6366        );
6367
6368        let mut child_optional = negated.clone();
6369        child_optional.cmd.subcommands["run"].flags[0].required = false;
6370        parse(&child_optional, &input(&["ex", "run"]))
6371            .expect("the child selection satisfies all parent requirements");
6372        parse(&child_optional, &input(&["ex", "--mode", "run"]))
6373            .expect("parent requires relationships are negated too");
6374    }
6375
6376    #[test]
6377    fn a_parent_argument_can_conflict_with_a_later_subcommand() {
6378        let spec: Spec = r#"name "ex"
6379bin "ex"
6380args_conflicts_with_subcommands #true
6381flag "--verbose"
6382cmd "run"
6383"#
6384        .parse()
6385        .unwrap();
6386
6387        parse(&spec, &input(&["ex", "run"]))
6388            .expect("the subcommand is valid without a parent argument");
6389        let err = parse(&spec, &input(&["ex", "--verbose", "run"])).unwrap_err();
6390        assert!(
6391            err.to_string().contains("cannot be used with arguments"),
6392            "{err}"
6393        );
6394    }
6395
6396    #[test]
6397    fn a_subcommand_can_take_precedence_over_a_variadic_flag() {
6398        let base = r#"name "ex"
6399bin "ex"
6400flag "--values <value>..."
6401cmd "run"
6402"#;
6403        let plain: Spec = base.parse().unwrap();
6404        let out = parse(&plain, &input(&["ex", "--values", "a", "run"])).unwrap();
6405        assert_eq!(out.cmd.name, "ex");
6406
6407        let precedence: Spec = base
6408            .replacen(
6409                "bin \"ex\"",
6410                "bin \"ex\"\nsubcommand_precedence_over_arg #true",
6411                1,
6412            )
6413            .parse()
6414            .unwrap();
6415        let out = parse(&precedence, &input(&["ex", "--values", "a", "run"])).unwrap();
6416        assert_eq!(out.cmd.name, "run");
6417    }
6418
6419    #[test]
6420    fn a_required_positional_can_follow_an_unfilled_optional_one() {
6421        let base = r#"name "ex"
6422bin "ex"
6423arg "[optional]"
6424arg "<required>"
6425"#;
6426        let plain: Spec = base.parse().unwrap();
6427        let err = parse(&plain, &input(&["ex", "value"])).unwrap_err();
6428        assert!(err.to_string().contains("required"), "{err}");
6429
6430        let enabled: Spec = base
6431            .replacen(
6432                "bin \"ex\"",
6433                "bin \"ex\"\nallow_missing_positional #true",
6434                1,
6435            )
6436            .parse()
6437            .unwrap();
6438        let out = parse(&enabled, &input(&["ex", "value"])).unwrap();
6439        assert!(!out.args.keys().any(|arg| arg.name == "optional"));
6440        let value = &out
6441            .args
6442            .iter()
6443            .find(|(arg, _)| arg.name == "required")
6444            .unwrap()
6445            .1;
6446        assert!(matches!(value, ParseValue::String(value) if value == "value"));
6447    }
6448
6449    #[test]
6450    fn unknown_flags_can_be_rejected_for_the_whole_cli() {
6451        let spec: Spec =
6452            "name \"ex\"\nbin \"ex\"\nunknown_flags \"error\"\nflag \"--force\"\nflag \"-0 --print0\"\narg \"[rest]...\" allow_negative_numbers=#true\n"
6453                .parse()
6454                .unwrap();
6455        let err = parse(&spec, &["ex".to_string(), "--wat".to_string()]).unwrap_err();
6456        assert!(
6457            err.to_string().contains("--wat"),
6458            "the message should name the token: {err}"
6459        );
6460
6461        // The positional opts into the narrower negative-number carve-out without
6462        // accepting arbitrary unknown flags.
6463        let out = parse(&spec, &["ex".to_string(), "-1".to_string()]).unwrap();
6464        let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
6465        assert_eq!(out.args[rest].to_string(), "-1");
6466
6467        let out = parse(&spec, &["ex".to_string(), "-0".to_string()]).unwrap();
6468        let print0 = out.flags.keys().find(|flag| flag.name == "print0").unwrap();
6469        assert!(matches!(out.flags[print0], ParseValue::Bool(true)));
6470    }
6471
6472    #[test]
6473    fn a_declared_digit_short_does_not_stop_the_subcommand_scan() {
6474        let spec: Spec = r#"
6475name "ex"
6476bin "ex"
6477unknown_flags "error"
6478flag "-0 --print0" global=#true
6479cmd "run" {
6480  flag "--force"
6481}
6482"#
6483        .parse()
6484        .unwrap();
6485        let out = parse(&spec, &input(&["ex", "-0", "run", "--force"])).unwrap();
6486        assert_eq!(out.cmd.name, "run");
6487        let print0 = out.flags.keys().find(|flag| flag.name == "print0").unwrap();
6488        let force = out.flags.keys().find(|flag| flag.name == "force").unwrap();
6489        assert!(matches!(out.flags[print0], ParseValue::Bool(true)));
6490        assert!(matches!(out.flags[force], ParseValue::Bool(true)));
6491    }
6492
6493    #[test]
6494    fn a_command_may_override_the_cli_wide_setting() {
6495        // Strict overall, lenient for the one command that forwards options.
6496        let spec: Spec = r#"
6497name "ex"
6498bin "ex"
6499unknown_flags "error"
6500cmd "exec" unknown_flags="value" {
6501  arg "[rest]..."
6502}
6503cmd "build" {
6504  arg "[rest]..."
6505}
6506"#
6507        .parse()
6508        .unwrap();
6509
6510        let out = parse(
6511            &spec,
6512            &["ex".to_string(), "exec".to_string(), "--wat".to_string()],
6513        )
6514        .unwrap();
6515        let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
6516        assert_eq!(out.args[rest].to_string(), "--wat");
6517
6518        assert!(
6519            parse(
6520                &spec,
6521                &["ex".to_string(), "build".to_string(), "--wat".to_string()]
6522            )
6523            .is_err(),
6524            "a command that says nothing inherits the CLI's choice"
6525        );
6526    }
6527
6528    #[test]
6529    fn the_setting_survives_a_round_trip() {
6530        let spec: Spec =
6531            "name \"ex\"\nbin \"ex\"\nunknown_flags \"error\"\ncmd \"x\" unknown_flags=\"value\"\n"
6532                .parse()
6533                .unwrap();
6534        let reparsed: Spec = spec.to_string().parse().unwrap();
6535        assert_eq!(reparsed.unknown_flags, Some(UnknownFlags::Error));
6536        assert_eq!(
6537            reparsed.cmd.subcommands["x"].unknown_flags,
6538            Some(UnknownFlags::Value)
6539        );
6540    }
6541
6542    #[test]
6543    fn test_default_subcommand() {
6544        // Test that default_subcommand routes to the specified subcommand
6545        let run_cmd = SpecCommand::builder()
6546            .name("run")
6547            .arg(SpecArg::builder().name("task").build())
6548            .build();
6549        let mut cmd = SpecCommand::builder().name("test").build();
6550        cmd.subcommands.insert("run".to_string(), run_cmd);
6551
6552        let spec = Spec {
6553            name: "test".to_string(),
6554            bin: "test".to_string(),
6555            cmd,
6556            default_subcommand: Some("run".to_string()),
6557            ..Default::default()
6558        };
6559
6560        // "test mytask" should be parsed as if it were "test run mytask"
6561        let input = vec!["test".to_string(), "mytask".to_string()];
6562        let parsed = parse(&spec, &input).unwrap();
6563
6564        // Should have two commands: root and "run"
6565        assert_eq!(parsed.cmds.len(), 2);
6566        assert_eq!(parsed.cmds[1].name, "run");
6567
6568        // Should have parsed the task argument
6569        assert_eq!(parsed.args.len(), 1);
6570        let arg = parsed.args.keys().next().unwrap();
6571        assert_eq!(arg.name, "task");
6572        let value = parsed.args.values().next().unwrap();
6573        assert_eq!(value.to_string(), "mytask");
6574    }
6575
6576    #[test]
6577    fn test_default_subcommand_explicit_still_works() {
6578        // Test that explicit subcommand takes precedence
6579        let run_cmd = SpecCommand::builder()
6580            .name("run")
6581            .arg(SpecArg::builder().name("task").build())
6582            .build();
6583        let other_cmd = SpecCommand::builder()
6584            .name("other")
6585            .arg(SpecArg::builder().name("other_arg").build())
6586            .build();
6587        let mut cmd = SpecCommand::builder().name("test").build();
6588        cmd.subcommands.insert("run".to_string(), run_cmd);
6589        cmd.subcommands.insert("other".to_string(), other_cmd);
6590
6591        let spec = Spec {
6592            name: "test".to_string(),
6593            bin: "test".to_string(),
6594            cmd,
6595            default_subcommand: Some("run".to_string()),
6596            ..Default::default()
6597        };
6598
6599        // "test other foo" should use "other" subcommand, not default
6600        let input = vec!["test".to_string(), "other".to_string(), "foo".to_string()];
6601        let parsed = parse(&spec, &input).unwrap();
6602
6603        // Should have used "other" subcommand
6604        assert_eq!(parsed.cmds.len(), 2);
6605        assert_eq!(parsed.cmds[1].name, "other");
6606    }
6607
6608    #[test]
6609    fn test_default_subcommand_applies_only_at_the_root() {
6610        // `default_subcommand` is declared once, for the whole spec, and only at the top. It
6611        // was being looked up wherever the parser happened to be standing, so a command with
6612        // an unrelated subcommand of the same name acquired a default of its own: with
6613        // `default_subcommand "ls"`, `ex config zzz` descended into `config ls` and bound
6614        // `zzz` there. Nothing declared that, and nothing could have.
6615        let mut config_ls = SpecCommand::builder().name("ls").build();
6616        config_ls.args.push(SpecArg::builder().name("what").build());
6617        let mut config_cmd = SpecCommand::builder().name("config").build();
6618        config_cmd.subcommands.insert("ls".to_string(), config_ls);
6619
6620        // The root's own `ls`, which is what its default points at. It takes an argument so
6621        // that a routed word has somewhere to land.
6622        let mut root_ls = SpecCommand::builder().name("ls").build();
6623        root_ls.args.push(SpecArg::builder().name("what").build());
6624        let mut cmd = SpecCommand::builder().name("ex").build();
6625        cmd.subcommands.insert("ls".to_string(), root_ls);
6626        cmd.subcommands.insert("config".to_string(), config_cmd);
6627
6628        let spec = Spec {
6629            name: "ex".to_string(),
6630            bin: "ex".to_string(),
6631            cmd,
6632            default_subcommand: Some("ls".to_string()),
6633            ..Default::default()
6634        };
6635
6636        // `config` has an `ls`, but `config` did not declare a default, so `zzz` is `config`'s
6637        // own business — and `config` takes no argument, so this is an error rather than a
6638        // silent descent.
6639        let input = vec!["ex".to_string(), "config".to_string(), "zzz".to_string()];
6640        assert!(
6641            parse(&spec, &input).is_err(),
6642            "`config` has no default subcommand and no argument, so `zzz` cannot bind"
6643        );
6644
6645        // At the root, where it is declared, it still applies.
6646        let input = vec!["ex".to_string(), "zzz".to_string()];
6647        let parsed = parse(&spec, &input).expect("the root's default applies");
6648        assert_eq!(
6649            parsed
6650                .cmds
6651                .iter()
6652                .map(|c| c.name.as_str())
6653                .collect::<Vec<_>>(),
6654            ["ex", "ls"]
6655        );
6656        assert_eq!(
6657            parsed.args.values().next().map(|v| v.to_string()),
6658            Some("zzz".to_string()),
6659            "and the word binds inside the command it reached"
6660        );
6661    }
6662
6663    #[test]
6664    fn test_default_subcommand_with_nested_subcommands() {
6665        // Test that default_subcommand works when the default subcommand has nested subcommands.
6666        // This is the mise use case: "mise say" should be parsed as "mise run say"
6667        // where "say" is a subcommand of "run" (a task).
6668        let say_cmd = SpecCommand::builder()
6669            .name("say")
6670            .arg(SpecArg::builder().name("name").build())
6671            .build();
6672        let mut run_cmd = SpecCommand::builder().name("run").build();
6673        run_cmd.subcommands.insert("say".to_string(), say_cmd);
6674
6675        let mut cmd = SpecCommand::builder().name("test").build();
6676        cmd.subcommands.insert("run".to_string(), run_cmd);
6677
6678        let spec = Spec {
6679            name: "test".to_string(),
6680            bin: "test".to_string(),
6681            cmd,
6682            default_subcommand: Some("run".to_string()),
6683            ..Default::default()
6684        };
6685
6686        // "test say hello" should be parsed as "test run say hello"
6687        let input = vec!["test".to_string(), "say".to_string(), "hello".to_string()];
6688        let parsed = parse(&spec, &input).unwrap();
6689
6690        // Should have three commands: root, "run", and "say"
6691        assert_eq!(parsed.cmds.len(), 3);
6692        assert_eq!(parsed.cmds[0].name, "test");
6693        assert_eq!(parsed.cmds[1].name, "run");
6694        assert_eq!(parsed.cmds[2].name, "say");
6695
6696        // Should have parsed the "name" argument
6697        assert_eq!(parsed.args.len(), 1);
6698        let arg = parsed.args.keys().next().unwrap();
6699        assert_eq!(arg.name, "name");
6700        let value = parsed.args.values().next().unwrap();
6701        assert_eq!(value.to_string(), "hello");
6702    }
6703
6704    /// Build a spec equivalent to the post-mount structure produced by mise's
6705    /// `mise usage` output: a root with a value-taking global flag (`-C/--cd`), a `run`
6706    /// subcommand that re-declares the same flag as NON-global, and a mounted task
6707    /// (`sample:run`) carrying a positional arg with `choices`.
6708    ///
6709    /// We construct the merged structure directly instead of executing a real mount so the
6710    /// test stays hermetic and cross-platform while still exercising the parser defect.
6711    fn mounted_global_flag_spec() -> Spec {
6712        let task_cmd = SpecCommand::builder()
6713            .name("sample:run")
6714            .arg(
6715                SpecArg::builder()
6716                    .name("profile")
6717                    .choices(["alpha", "beta", "gamma"])
6718                    .build(),
6719            )
6720            .build();
6721        // `run` re-declares `-C/--cd` but as a NON-global flag, mirroring the mise spec.
6722        let mut run_cmd = SpecCommand::builder()
6723            .name("run")
6724            .flag(
6725                SpecFlag::builder()
6726                    .name("cd")
6727                    .short('C')
6728                    .long("cd")
6729                    .arg(SpecArg::builder().name("dir").build())
6730                    .global(false)
6731                    .build(),
6732            )
6733            .build();
6734        run_cmd
6735            .subcommands
6736            .insert("sample:run".to_string(), task_cmd);
6737
6738        let mut cmd = SpecCommand::builder()
6739            .name("test")
6740            .flag(
6741                SpecFlag::builder()
6742                    .name("cd")
6743                    .short('C')
6744                    .long("cd")
6745                    .arg(SpecArg::builder().name("dir").build())
6746                    .global(true)
6747                    .build(),
6748            )
6749            .build();
6750        cmd.subcommands.insert("run".to_string(), run_cmd);
6751
6752        Spec {
6753            name: "test".to_string(),
6754            bin: "test".to_string(),
6755            cmd,
6756            ..Default::default()
6757        }
6758    }
6759
6760    #[test]
6761    fn test_prefix_global_flag_does_not_pollute_choices() {
6762        // Regression for the parser-side root cause referenced by jdx/mise#10069.
6763        //
6764        // When `run` re-declares the global `-C/--cd` as non-global, descending into it (and
6765        // then into the mounted `sample:run`) used to drop the inherited global flag from
6766        // `available_flags`. Phase 2 then no longer recognized the prefix `-C`, so it was
6767        // mis-validated against the task's `choices` positional arg.
6768        let spec = mounted_global_flag_spec();
6769
6770        // The prefix global flag must stay recognized so it is consumed as a flag (not as the
6771        // positional). Before the fix this bailed with "Invalid choice for arg profile: -C".
6772        for words in [
6773            &["test", "-C", "/tmp", "run", "sample:run"][..],
6774            // Embedded-value form must behave identically.
6775            &["test", "--cd=/tmp", "run", "sample:run"][..],
6776        ] {
6777            let parsed = parse_partial(&spec, &input(words)).unwrap();
6778            assert_eq!(
6779                parsed
6780                    .cmds
6781                    .iter()
6782                    .map(|c| c.name.as_str())
6783                    .collect::<Vec<_>>(),
6784                vec!["test", "run", "sample:run"],
6785            );
6786            // No positional arg should have been consumed by the leftover global-flag tokens.
6787            assert!(
6788                parsed.args.is_empty(),
6789                "args should be empty, got {:?}",
6790                parsed.args
6791            );
6792
6793            // Fix (B): the inherited global flag survives the descent even though `run`
6794            // re-declares `-C/--cd` as non-global.
6795            let cd = parsed
6796                .available_flags
6797                .get("--cd")
6798                .expect("--cd should remain available after descending into the subcommand");
6799            assert!(cd.global, "--cd must stay global after descent");
6800            assert!(
6801                parsed.available_flags.get("-C").is_some_and(|f| f.global),
6802                "-C must stay global after descent",
6803            );
6804
6805            // The global flag must still be recorded in `out.flags` so it reaches `as_env()`
6806            // for normal execution and for the env passed to mount scripts. (Removing the
6807            // token in Phase 1 instead of re-parsing it would silently drop `usage_cd`.)
6808            assert_eq!(
6809                parsed.as_env().get("usage_cd").map(String::as_str),
6810                Some("/tmp"),
6811                "global flag value must survive in as_env(), got {:?}",
6812                parsed.as_env(),
6813            );
6814        }
6815
6816        // A real, valid choice still parses through the global flag prefix.
6817        let parsed = parse_partial(
6818            &spec,
6819            &input(&["test", "-C", "/tmp", "run", "sample:run", "alpha"]),
6820        )
6821        .unwrap();
6822        assert_eq!(parsed.args.len(), 1);
6823        assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
6824
6825        // And genuinely invalid choices are still rejected (we didn't disable validation).
6826        assert_parse_err(
6827            parse_partial(&spec, &input(&["test", "run", "sample:run", "wrong"])),
6828            "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
6829        );
6830    }
6831
6832    /// Build a spec mirroring mise's orphan-short re-declarations: a root with a LONG-ONLY
6833    /// global boolean flag (`--raw`, no short), a `run` subcommand that re-declares it as a
6834    /// NON-global flag while ADDING a short (`-r --raw`) plus a purely-local `-f/--force`
6835    /// flag, and a mounted task (`sample:run`) with a `choices` positional arg.
6836    fn mounted_orphan_short_spec() -> Spec {
6837        let task_cmd = SpecCommand::builder()
6838            .name("sample:run")
6839            .arg(
6840                SpecArg::builder()
6841                    .name("profile")
6842                    .choices(["alpha", "beta", "gamma"])
6843                    .build(),
6844            )
6845            .build();
6846        // `run` re-declares `--raw` as NON-global but adds a `-r` short that exists only here,
6847        // and also carries a purely-local `-f/--force` flag (shares nothing with a global).
6848        let mut run_cmd = SpecCommand::builder()
6849            .name("run")
6850            .flag(
6851                SpecFlag::builder()
6852                    .name("raw")
6853                    .short('r')
6854                    .long("raw")
6855                    .global(false)
6856                    .build(),
6857            )
6858            .flag(
6859                SpecFlag::builder()
6860                    .name("force")
6861                    .short('f')
6862                    .long("force")
6863                    .global(false)
6864                    .build(),
6865            )
6866            .build();
6867        run_cmd
6868            .subcommands
6869            .insert("sample:run".to_string(), task_cmd);
6870
6871        // Root global is LONG-ONLY: `--raw` with no short.
6872        let mut cmd = SpecCommand::builder()
6873            .name("test")
6874            .flag(
6875                SpecFlag::builder()
6876                    .name("raw")
6877                    .long("raw")
6878                    .global(true)
6879                    .build(),
6880            )
6881            .build();
6882        cmd.subcommands.insert("run".to_string(), run_cmd);
6883
6884        Spec {
6885            name: "test".to_string(),
6886            bin: "test".to_string(),
6887            cmd,
6888            ..Default::default()
6889        }
6890    }
6891
6892    #[test]
6893    fn test_orphan_short_alias_survives_merge() {
6894        // Follow-up to test_prefix_global_flag_does_not_pollute_choices (jdx/mise#10069):
6895        // when `run` re-declares the long-only global `--raw` as a non-global `-r --raw`, the
6896        // added short `-r` must be unioned onto the surviving inherited global flag instead of
6897        // being discarded with the wholesale re-declaration. Otherwise `mycli run -r <task>`
6898        // would not recognize `-r` and would mis-validate it against the task's `choices` arg.
6899        let spec = mounted_orphan_short_spec();
6900
6901        let parsed = parse_partial(&spec, &input(&["test", "run", "-r", "sample:run"])).unwrap();
6902        assert_eq!(
6903            parsed
6904                .cmds
6905                .iter()
6906                .map(|c| c.name.as_str())
6907                .collect::<Vec<_>>(),
6908            vec!["test", "run", "sample:run"],
6909        );
6910
6911        // (a) The orphan short `-r` survives the descent, merged onto the inherited global flag,
6912        // and the original long `--raw` is still global too.
6913        assert!(
6914            parsed.available_flags.get("-r").is_some_and(|f| f.global),
6915            "-r must be merged onto the inherited global flag and stay global after descent",
6916        );
6917        assert!(
6918            parsed
6919                .available_flags
6920                .get("--raw")
6921                .is_some_and(|f| f.global),
6922            "--raw must stay global after descent",
6923        );
6924
6925        // (b) The token is consumed as a flag, not mistaken for the `choices` positional.
6926        assert!(
6927            parsed.args.is_empty(),
6928            "args should be empty, got {:?}",
6929            parsed.args
6930        );
6931
6932        // (c) The value still reaches as_env() so `usage_raw` is produced for execution/mounts.
6933        assert_eq!(
6934            parsed.as_env().get("usage_raw").map(String::as_str),
6935            Some("true"),
6936            "merged short's value must survive in as_env(), got {:?}",
6937            parsed.as_env(),
6938        );
6939
6940        // (d) Negative case: a purely-local flag that shares nothing with a global is NOT
6941        // promoted/merged — it is correctly dropped when descending into the mount.
6942        assert!(
6943            !parsed.available_flags.contains_key("-f"),
6944            "purely-local -f must not be promoted onto a global",
6945        );
6946        assert!(
6947            !parsed.available_flags.contains_key("--force"),
6948            "purely-local --force must not be promoted onto a global",
6949        );
6950
6951        // A real, valid choice still parses through the merged short prefix.
6952        let parsed =
6953            parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "alpha"])).unwrap();
6954        assert_eq!(parsed.args.len(), 1);
6955        assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
6956
6957        // And genuinely invalid choices are still rejected.
6958        assert_parse_err(
6959            parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "wrong"])),
6960            "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
6961        );
6962    }
6963
6964    #[test]
6965    fn test_orphan_short_does_not_clobber_unrelated_global() {
6966        // When a re-declaration's orphan short collides with a DIFFERENT inherited global's
6967        // short, the merge must not steal it. Here the root has both a long-only `--raw` global
6968        // and a `-r --restrict` global; `run` re-declares `-r --raw` as non-global. `-r` is a
6969        // genuine collision with `--restrict`, so global precedence must keep `-r -> restrict`.
6970        let run_cmd = SpecCommand::builder()
6971            .name("run")
6972            .flag(
6973                SpecFlag::builder()
6974                    .name("raw")
6975                    .short('r')
6976                    .long("raw")
6977                    .global(false)
6978                    .build(),
6979            )
6980            .build();
6981        let mut cmd = SpecCommand::builder()
6982            .name("test")
6983            .flag(
6984                SpecFlag::builder()
6985                    .name("raw")
6986                    .long("raw")
6987                    .global(true)
6988                    .build(),
6989            )
6990            .flag(
6991                SpecFlag::builder()
6992                    .name("restrict")
6993                    .short('r')
6994                    .long("restrict")
6995                    .global(true)
6996                    .build(),
6997            )
6998            .build();
6999        cmd.subcommands.insert("run".to_string(), run_cmd);
7000        let spec = Spec {
7001            name: "test".to_string(),
7002            bin: "test".to_string(),
7003            cmd,
7004            ..Default::default()
7005        };
7006
7007        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7008        // `-r` stays owned by the unrelated `--restrict` global, not stolen by the merged raw.
7009        assert_eq!(
7010            parsed.available_flags.get("-r").map(|f| f.name.as_str()),
7011            Some("restrict"),
7012            "-r must remain owned by the unrelated global it already belonged to",
7013        );
7014        // Both globals are still recognized and global after the descent.
7015        assert!(parsed
7016            .available_flags
7017            .get("--raw")
7018            .is_some_and(|f| f.global));
7019        assert!(parsed
7020            .available_flags
7021            .get("--restrict")
7022            .is_some_and(|f| f.global));
7023    }
7024
7025    #[test]
7026    fn test_redeclared_global_aliases_share_one_flag() {
7027        // A global declared with BOTH a short and a long, re-declared non-globally by a
7028        // subcommand that adds a third alias. Every alias key must resolve to the SAME merged
7029        // flag: the child's keys iterate in BTreeMap order (`--assume-yes`, `--yes`, `-y`), so by
7030        // the time `-y` is reached the long already points at the merged flag. That merged flag is
7031        // not a *different* inherited global, so the collision guard must not skip `-y` and leave
7032        // it pointing at the pre-merge global (which lacks the added `assume-yes` alias).
7033        let spec = r#"
7034flag "-y --yes" global=#true effect="write"
7035cmd "run" {
7036    flag "-y --yes --assume-yes"
7037}
7038"#
7039        .parse::<Spec>()
7040        .unwrap();
7041
7042        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7043
7044        for key in ["-y", "--yes", "--assume-yes"] {
7045            let flag = parsed
7046                .available_flags
7047                .get(key)
7048                .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
7049            assert!(flag.global, "{key} must stay global after the descent");
7050            assert_eq!(
7051                flag.long,
7052                vec!["yes".to_string(), "assume-yes".to_string()],
7053                "{key} must resolve to the flag carrying every alias",
7054            );
7055            assert_eq!(flag.short, vec!['y'], "{key} must keep the global's short");
7056        }
7057
7058        // One logical flag means one object: all three keys share a single `Arc`.
7059        assert_eq!(
7060            unique_flags(parsed.available_flags.values()).count(),
7061            1,
7062            "all aliases must point at one flag object, got {:?}",
7063            parsed.available_flags,
7064        );
7065
7066        // The global's effect survives the merge, so `-y` still marks the command as writing.
7067        assert_eq!(
7068            parsed.available_flags["-y"].effect,
7069            Some(crate::SpecCommandEffect::Write),
7070        );
7071    }
7072
7073    #[test]
7074    fn test_redeclared_global_keeps_hidden_alias_metadata() {
7075        let spec = r#"
7076flag "--yes" global=#true {
7077    alias "-q" "--quietly" hide=#true
7078}
7079cmd "run" {
7080    flag "--yes --assume-yes" {
7081        alias "-s" "--secret" hide=#true
7082    }
7083}
7084"#
7085        .parse::<Spec>()
7086        .unwrap();
7087
7088        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7089        let merged = &parsed.available_flags["--yes"];
7090        assert_eq!(merged.hidden_short_aliases, ['q', 's']);
7091        assert_eq!(merged.hidden_aliases, ["quietly", "secret"]);
7092        for key in ["-q", "-s", "--quietly", "--secret"] {
7093            assert!(Arc::ptr_eq(&parsed.available_flags[key], merged), "{key}");
7094        }
7095    }
7096
7097    #[test]
7098    fn test_redeclared_global_can_promote_hidden_aliases() {
7099        let spec = r#"
7100flag "--yes" global=#true {
7101    alias "-q" "--quietly" hide=#true
7102}
7103cmd "run" {
7104    flag "-q --yes --quietly"
7105}
7106"#
7107        .parse::<Spec>()
7108        .unwrap();
7109
7110        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7111        let merged = &parsed.available_flags["--yes"];
7112        assert!(merged.hidden_short_aliases.is_empty());
7113        assert!(merged.hidden_aliases.is_empty());
7114        for key in ["-q", "--quietly"] {
7115            assert!(Arc::ptr_eq(&parsed.available_flags[key], merged), "{key}");
7116        }
7117    }
7118
7119    #[test]
7120    fn test_partially_redeclared_global_keeps_all_aliases_on_one_flag() {
7121        // Same one-flag-one-object requirement as above, but the child re-declares only ONE of
7122        // the global's three aliases (`--yes`, not `-y`/`--confirm`) while adding a new one. The
7123        // aliases the child omits are never visited by the merge loop, so they must be rebound to
7124        // the merged flag explicitly — otherwise `-y` and `--confirm` keep pointing at the
7125        // pre-merge global and miss the added `assume-yes`.
7126        let spec = r#"
7127flag "-y --yes --confirm" global=#true
7128cmd "run" {
7129    flag "--yes --assume-yes"
7130}
7131"#
7132        .parse::<Spec>()
7133        .unwrap();
7134
7135        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7136
7137        for key in ["-y", "--yes", "--confirm", "--assume-yes"] {
7138            let flag = parsed
7139                .available_flags
7140                .get(key)
7141                .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
7142            assert!(flag.global, "{key} must stay global after the descent");
7143            assert_eq!(
7144                flag.long,
7145                vec![
7146                    "yes".to_string(),
7147                    "confirm".to_string(),
7148                    "assume-yes".to_string()
7149                ],
7150                "{key} must resolve to the flag carrying every alias",
7151            );
7152        }
7153
7154        assert_eq!(
7155            unique_flags(parsed.available_flags.values()).count(),
7156            1,
7157            "all aliases must point at one flag object, got {:?}",
7158            parsed.available_flags,
7159        );
7160    }
7161
7162    /// Build a spec shaped like mise's post-mount structure for jdx/mise#11282: a root with
7163    /// globals (`-E/--env <ENV>`, `--silent`), a `run` subcommand with a non-global flag, and a
7164    /// MOUNTED task command that declares its own `--env` (with choices) plus `--bump`.
7165    ///
7166    /// The task command is marked `mounted` the same way `SpecCommand::mount()` marks the
7167    /// commands it merges in, so the test stays hermetic (no mount subprocess).
7168    fn mounted_task_flag_spec() -> Spec {
7169        let mut task_cmd = SpecCommand::builder()
7170            .name("mytask")
7171            .flag(
7172                SpecFlag::builder()
7173                    .name("env")
7174                    .long("env")
7175                    .arg(
7176                        SpecArg::builder()
7177                            .name("name")
7178                            .choices(["dev", "stage", "prod"])
7179                            .build(),
7180                    )
7181                    .global(false)
7182                    .build(),
7183            )
7184            .flag(
7185                SpecFlag::builder()
7186                    .name("bump")
7187                    .long("bump")
7188                    .arg(
7189                        SpecArg::builder()
7190                            .name("type")
7191                            .choices(["auto", "major"])
7192                            .build(),
7193                    )
7194                    .global(false)
7195                    .build(),
7196            )
7197            .build();
7198        task_cmd.mounted = true;
7199
7200        let mut run_cmd = SpecCommand::builder()
7201            .name("run")
7202            .flag(
7203                SpecFlag::builder()
7204                    .name("force")
7205                    .short('f')
7206                    .long("force")
7207                    .global(false)
7208                    .build(),
7209            )
7210            .build();
7211        run_cmd.subcommands.insert("mytask".to_string(), task_cmd);
7212
7213        let mut cmd = SpecCommand::builder()
7214            .name("test")
7215            .flag(
7216                SpecFlag::builder()
7217                    .name("env")
7218                    .short('E')
7219                    .long("env")
7220                    .arg(SpecArg::builder().name("ENV").build())
7221                    .global(true)
7222                    .build(),
7223            )
7224            .flag(
7225                SpecFlag::builder()
7226                    .name("silent")
7227                    .long("silent")
7228                    .global(true)
7229                    .build(),
7230            )
7231            .build();
7232        cmd.subcommands.insert("run".to_string(), run_cmd);
7233
7234        Spec {
7235            name: "test".to_string(),
7236            bin: "test".to_string(),
7237            cmd,
7238            ..Default::default()
7239        }
7240    }
7241
7242    #[test]
7243    fn test_mount_boundary_does_not_apply_inside_the_mounted_tree() {
7244        // The mounted program's own commands are ordinary commands relative to each other, so
7245        // descending *within* the mounted tree must follow the normal rules — including keeping
7246        // an inherited global that a nested command re-declares as non-global (jdx/usage#649).
7247        // Treating every level of the tree as a mount boundary let the re-declaration shadow the
7248        // global, which the next descent's `retain(global)` then dropped entirely.
7249        let deep = SpecCommand::builder().name("deep").build();
7250        let mut sub = SpecCommand::builder()
7251            .name("sub")
7252            // Re-declares the mounted program's own global as non-global.
7253            .flag(
7254                SpecFlag::builder()
7255                    .name("cd")
7256                    .short('C')
7257                    .long("cd")
7258                    .arg(SpecArg::builder().name("dir").build())
7259                    .global(false)
7260                    .build(),
7261            )
7262            .build();
7263        sub.subcommands.insert("deep".to_string(), deep);
7264        let mut task = SpecCommand::builder()
7265            .name("task")
7266            .flag(
7267                SpecFlag::builder()
7268                    .name("cd")
7269                    .short('C')
7270                    .long("cd")
7271                    .arg(SpecArg::builder().name("dir").build())
7272                    .global(true)
7273                    .build(),
7274            )
7275            .build();
7276        task.subcommands.insert("sub".to_string(), sub);
7277        task.mark_mounted();
7278
7279        let mut run_cmd = SpecCommand::builder().name("run").build();
7280        run_cmd.subcommands.insert("task".to_string(), task);
7281        let mut cmd = SpecCommand::builder().name("test").build();
7282        cmd.subcommands.insert("run".to_string(), run_cmd);
7283        let spec = Spec {
7284            name: "test".to_string(),
7285            bin: "test".to_string(),
7286            cmd,
7287            ..Default::default()
7288        };
7289
7290        let parsed = parse_partial(&spec, &input(&["test", "run", "task", "sub", "deep"])).unwrap();
7291        assert!(
7292            parsed.available_flags.get("--cd").is_some_and(|f| f.global),
7293            "the mounted program's own global must survive descents inside the mounted tree",
7294        );
7295        assert!(
7296            parsed.completion_flags().contains_key("--cd"),
7297            "and must still be offered there: it belongs to the mounted program",
7298        );
7299        assert!(
7300            parsed.completion_flags().contains_key("-C"),
7301            "including the short the nested command re-declared",
7302        );
7303    }
7304
7305    #[test]
7306    fn test_mount_flags_merged_into_the_mounting_cmd_are_offered() {
7307        // A mounted spec may declare flags on its own root, which `SpecCommand::merge` folds
7308        // into the command the mount sits on. They belong to the mounted program, so they must
7309        // be offered inside the mounted commands rather than filtered out with the mounting
7310        // CLI's own flags.
7311        let mut task = SpecCommand::builder()
7312            .name("task")
7313            .flag(
7314                SpecFlag::builder()
7315                    .name("bump")
7316                    .long("bump")
7317                    .global(false)
7318                    .build(),
7319            )
7320            .build();
7321        task.mark_mounted();
7322
7323        let mut run_cmd = SpecCommand::builder().name("run").build();
7324        run_cmd.subcommands.insert("task".to_string(), task);
7325        // What `mount()` leaves behind when the mounted spec's root declares flags.
7326        run_cmd.flags = vec![
7327            SpecFlag::builder()
7328                .name("tglobal")
7329                .long("tglobal")
7330                .global(true)
7331                .build(),
7332            SpecFlag::builder()
7333                .name("tlocal")
7334                .long("tlocal")
7335                .global(false)
7336                .build(),
7337        ];
7338        run_cmd.flags_from_mount = true;
7339
7340        let mut cmd = SpecCommand::builder()
7341            .name("test")
7342            .flag(
7343                SpecFlag::builder()
7344                    .name("silent")
7345                    .long("silent")
7346                    .global(true)
7347                    .build(),
7348            )
7349            .build();
7350        cmd.subcommands.insert("run".to_string(), run_cmd);
7351        let spec = Spec {
7352            name: "test".to_string(),
7353            bin: "test".to_string(),
7354            cmd,
7355            ..Default::default()
7356        };
7357
7358        let parsed = parse_partial(&spec, &input(&["test", "run", "task"])).unwrap();
7359        assert_eq!(
7360            parsed.completion_flags().keys().collect::<Vec<_>>(),
7361            vec!["--bump", "--tglobal"],
7362            "the mounted spec's root global belongs to the mounted program; the mounting CLI's \
7363             `--silent` does not, and the mount's non-global root flag is not inherited",
7364        );
7365    }
7366
7367    #[test]
7368    fn test_mounted_cmd_does_not_offer_mounting_cli_globals() {
7369        // Regression for jdx/mise#11282. A mounted command describes another program, which
7370        // does not accept the mounting CLI's globals (mise forwards everything after a task
7371        // name to the task). They must stay recognized — they may appear before the mounted
7372        // command — but must not be offered in completions there.
7373        let spec = mounted_task_flag_spec();
7374        let parsed = parse_partial(&spec, &input(&["test", "run", "mytask"])).unwrap();
7375
7376        // Still recognized for parsing...
7377        assert!(parsed.available_flags.contains_key("--silent"));
7378        assert!(parsed.available_flags.contains_key("-E"));
7379        // ...but belonging to a command above the mount, so not offered.
7380        assert_eq!(
7381            parsed.completion_flags().keys().collect::<Vec<_>>(),
7382            vec!["--bump", "--env"],
7383            "only the mounted command's own flags may be offered",
7384        );
7385
7386        // `run`'s own non-global flag is dropped on descent, as it always was.
7387        assert!(!parsed.available_flags.contains_key("--force"));
7388    }
7389
7390    #[test]
7391    fn test_mounted_cmd_flag_wins_over_inherited_global() {
7392        // Second half of jdx/mise#11282: the mounted `--env` (with choices) used to be shadowed
7393        // by the root's `--env` global, so completing its value fell back to file completion.
7394        let spec = mounted_task_flag_spec();
7395        let parsed = parse_partial(&spec, &input(&["test", "run", "mytask", "--env"])).unwrap();
7396
7397        let awaiting = parsed
7398            .flag_awaiting_value
7399            .first()
7400            .expect("--env should await a value");
7401        assert_eq!(
7402            awaiting
7403                .arg
7404                .as_ref()
7405                .and_then(|a| a.choices.as_ref())
7406                .map(|c| c.choices.clone()),
7407            Some(vec![
7408                "dev".to_string(),
7409                "stage".to_string(),
7410                "prod".to_string()
7411            ]),
7412            "the mounted command's own --env must win over the inherited global",
7413        );
7414
7415        // The global's short is not declared by the mounted command, so it keeps pointing at
7416        // the global and a value passed before the mounted command still parses.
7417        let parsed =
7418            parse_partial(&spec, &input(&["test", "-E", "anything", "run", "mytask"])).unwrap();
7419        assert!(
7420            parsed.args.is_empty(),
7421            "prefix global tokens must not be consumed as positionals, got {:?}",
7422            parsed.args
7423        );
7424        assert_eq!(
7425            parsed.as_env().get("usage_env").map(String::as_str),
7426            Some("anything"),
7427        );
7428    }
7429
7430    #[test]
7431    fn test_prefix_flag_keeps_the_flag_it_was_read_as() {
7432        // A word before the mounted command is re-parsed by Phase 2, when the mounted command
7433        // already owns the name. It has to stay bound to the flag Phase 1 read it as, or the
7434        // global's value would be validated against the mounted flag's choices and a legitimate
7435        // value would be rejected.
7436        let spec = mounted_task_flag_spec();
7437        let parsed = parse_partial(
7438            &spec,
7439            &input(&["test", "--env", "not-a-task-choice", "run", "mytask"]),
7440        )
7441        .unwrap();
7442        assert!(
7443            parsed.errors.is_empty(),
7444            "prefix global value must not be validated against the mounted flag: {:?}",
7445            parsed
7446                .errors
7447                .iter()
7448                .map(|e| e.to_string())
7449                .collect::<Vec<_>>(),
7450        );
7451        assert_eq!(
7452            parsed.as_env().get("usage_env").map(String::as_str),
7453            Some("not-a-task-choice"),
7454        );
7455
7456        // The embedded-value form binds the same way.
7457        let parsed = parse_partial(
7458            &spec,
7459            &input(&["test", "--env=not-a-task-choice", "run", "mytask"]),
7460        )
7461        .unwrap();
7462        assert!(parsed.errors.is_empty());
7463        assert_eq!(
7464            parsed.as_env().get("usage_env").map(String::as_str),
7465            Some("not-a-task-choice"),
7466        );
7467
7468        // Meanwhile a word *after* the mounted command belongs to the mounted flag, even when
7469        // the same name was already used before it.
7470        let parsed = parse_partial(
7471            &spec,
7472            &input(&["test", "--env", "prod", "run", "mytask", "--env"]),
7473        )
7474        .unwrap();
7475        let awaiting = parsed
7476            .flag_awaiting_value
7477            .first()
7478            .expect("--env should await a value");
7479        assert_eq!(
7480            awaiting
7481                .arg
7482                .as_ref()
7483                .and_then(|a| a.choices.as_ref())
7484                .map(|c| c.choices.clone()),
7485            Some(vec![
7486                "dev".to_string(),
7487                "stage".to_string(),
7488                "prod".to_string()
7489            ]),
7490            "the mounted command's --env must own the name after the mounted command",
7491        );
7492    }
7493
7494    #[test]
7495    fn test_non_global_flag_does_not_hide_subcommand() {
7496        // A non-global flag may precede a subcommand (`mycli run --force task`). Phase 1 used to
7497        // stop scanning at one, so the subcommand — and any mount on it — was never reached and
7498        // its name was left to Phase 2 to mis-read as a positional: `unexpected word: mytask`.
7499        let spec = mounted_task_flag_spec();
7500
7501        for words in [
7502            // `run` declares `-f/--force` as non-global.
7503            &["test", "run", "--force", "mytask"][..],
7504            &["test", "run", "-f", "mytask"][..],
7505            // Mixed with a global before the subcommand.
7506            &["test", "-E", "prod", "run", "--force", "mytask"][..],
7507        ] {
7508            let parsed = parse_partial(&spec, &input(words)).unwrap();
7509            assert_eq!(
7510                parsed
7511                    .cmds
7512                    .iter()
7513                    .map(|c| c.name.as_str())
7514                    .collect::<Vec<_>>(),
7515                vec!["test", "run", "mytask"],
7516                "{words:?} should descend into the mounted command",
7517            );
7518            assert!(
7519                parsed.args.is_empty(),
7520                "{words:?} should not consume a positional, got {:?}",
7521                parsed.args,
7522            );
7523            assert_eq!(
7524                parsed.as_env().get("usage_force").map(String::as_str),
7525                Some("true"),
7526                "the non-global flag must still be recorded for {words:?}",
7527            );
7528        }
7529
7530        // A non-global flag that takes a value consumes it, rather than reading the value as the
7531        // subcommand.
7532        let mut run_cmd = SpecCommand::builder()
7533            .name("run")
7534            .flag(
7535                SpecFlag::builder()
7536                    .name("output")
7537                    .short('o')
7538                    .long("output")
7539                    .arg(SpecArg::builder().name("mode").build())
7540                    .global(false)
7541                    .build(),
7542            )
7543            .build();
7544        run_cmd.subcommands.insert(
7545            "task".to_string(),
7546            SpecCommand::builder().name("task").build(),
7547        );
7548        let mut cmd = SpecCommand::builder().name("test").build();
7549        cmd.subcommands.insert("run".to_string(), run_cmd);
7550        let spec = Spec {
7551            name: "test".to_string(),
7552            bin: "test".to_string(),
7553            cmd,
7554            ..Default::default()
7555        };
7556
7557        let parsed =
7558            parse_partial(&spec, &input(&["test", "run", "--output", "quiet", "task"])).unwrap();
7559        assert_eq!(
7560            parsed
7561                .cmds
7562                .iter()
7563                .map(|c| c.name.as_str())
7564                .collect::<Vec<_>>(),
7565            vec!["test", "run", "task"],
7566        );
7567        assert_eq!(
7568            parsed.as_env().get("usage_output").map(String::as_str),
7569            Some("quiet"),
7570        );
7571
7572        // An unknown flag still stops the scan: it may take a value, so the next word cannot be
7573        // assumed to be a subcommand. `run` takes no positional, so this stays an error.
7574        assert_parse_err(
7575            parse_partial(&spec, &input(&["test", "run", "--nope", "task"])),
7576            "unexpected word: --nope",
7577        );
7578    }
7579
7580    #[test]
7581    fn test_non_mounted_subcommand_offers_inherited_globals() {
7582        // Nothing changes for ordinary (non-mounted) subcommands: a global declared above is
7583        // still both recognized and offered.
7584        let mut run_cmd = SpecCommand::builder().name("run").build();
7585        run_cmd.subcommands.insert(
7586            "nested".to_string(),
7587            SpecCommand::builder().name("nested").build(),
7588        );
7589        let mut cmd = SpecCommand::builder()
7590            .name("test")
7591            .flag(
7592                SpecFlag::builder()
7593                    .name("silent")
7594                    .long("silent")
7595                    .global(true)
7596                    .build(),
7597            )
7598            .build();
7599        cmd.subcommands.insert("run".to_string(), run_cmd);
7600        let spec = Spec {
7601            name: "test".to_string(),
7602            bin: "test".to_string(),
7603            cmd,
7604            ..Default::default()
7605        };
7606
7607        let parsed = parse_partial(&spec, &input(&["test", "run", "nested"])).unwrap();
7608        assert_eq!(
7609            parsed.completion_flags().keys().collect::<Vec<_>>(),
7610            parsed.available_flags.keys().collect::<Vec<_>>(),
7611        );
7612        assert!(parsed.completion_flags().contains_key("--silent"));
7613    }
7614
7615    #[test]
7616    fn test_subcommand_alias_collision_keeps_last_owner() {
7617        // The orphan-alias merge must not disturb how two flags in the SAME subcommand that
7618        // share an alias are resolved. Historically the flattened flag map gave the shared
7619        // alias to the LAST-declared flag (last-writer-wins); that must be preserved.
7620        let run_cmd = SpecCommand::builder()
7621            .name("run")
7622            .flag(
7623                SpecFlag::builder()
7624                    .name("alpha")
7625                    .short('x')
7626                    .long("alpha")
7627                    .global(false)
7628                    .build(),
7629            )
7630            .flag(
7631                SpecFlag::builder()
7632                    .name("beta")
7633                    .short('x')
7634                    .long("beta")
7635                    .global(false)
7636                    .build(),
7637            )
7638            .build();
7639        let mut cmd = SpecCommand::builder().name("test").build();
7640        cmd.subcommands.insert("run".to_string(), run_cmd);
7641        let spec = Spec {
7642            name: "test".to_string(),
7643            bin: "test".to_string(),
7644            cmd,
7645            ..Default::default()
7646        };
7647
7648        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7649        // `-x` is declared by both flags; the last one (`beta`) keeps it, as before the fix.
7650        assert_eq!(
7651            parsed.available_flags.get("-x").map(|f| f.name.as_str()),
7652            Some("beta"),
7653            "the last-declared flag must keep a shared short alias",
7654        );
7655        // Both distinct long aliases remain recognized and point to their own flag.
7656        assert_eq!(
7657            parsed
7658                .available_flags
7659                .get("--alpha")
7660                .map(|f| f.name.as_str()),
7661            Some("alpha"),
7662        );
7663        assert_eq!(
7664            parsed
7665                .available_flags
7666                .get("--beta")
7667                .map(|f| f.name.as_str()),
7668            Some("beta"),
7669        );
7670    }
7671
7672    #[test]
7673    fn test_default_subcommand_same_name_child() {
7674        // Test that default_subcommand doesn't cause issues when the default subcommand
7675        // has a child with the same name (e.g., "run" has a task named "run").
7676        // This verifies we don't switch multiple times or get stuck in a loop.
7677        let run_task = SpecCommand::builder()
7678            .name("run")
7679            .arg(SpecArg::builder().name("args").build())
7680            .build();
7681        let mut run_cmd = SpecCommand::builder().name("run").build();
7682        run_cmd.subcommands.insert("run".to_string(), run_task);
7683
7684        let mut cmd = SpecCommand::builder().name("test").build();
7685        cmd.subcommands.insert("run".to_string(), run_cmd);
7686
7687        let spec = Spec {
7688            name: "test".to_string(),
7689            bin: "test".to_string(),
7690            cmd,
7691            default_subcommand: Some("run".to_string()),
7692            ..Default::default()
7693        };
7694
7695        // "test run" explicitly matches the "run" subcommand (not via default_subcommand)
7696        let input = vec!["test".to_string(), "run".to_string()];
7697        let parsed = parse(&spec, &input).unwrap();
7698
7699        // Should have two commands: root and "run"
7700        assert_eq!(parsed.cmds.len(), 2);
7701        assert_eq!(parsed.cmds[0].name, "test");
7702        assert_eq!(parsed.cmds[1].name, "run");
7703
7704        // "test run run" should descend into the "run" task (child of "run" subcommand)
7705        let input = vec![
7706            "test".to_string(),
7707            "run".to_string(),
7708            "run".to_string(),
7709            "hello".to_string(),
7710        ];
7711        let parsed = parse(&spec, &input).unwrap();
7712
7713        assert_eq!(parsed.cmds.len(), 3);
7714        assert_eq!(parsed.cmds[0].name, "test");
7715        assert_eq!(parsed.cmds[1].name, "run");
7716        assert_eq!(parsed.cmds[2].name, "run");
7717        assert_eq!(parsed.args.len(), 1);
7718        let value = parsed.args.values().next().unwrap();
7719        assert_eq!(value.to_string(), "hello");
7720
7721        // Key test case: "test other" should switch to default subcommand "run"
7722        // and treat "other" as a positional arg (not try to switch again because
7723        // "run" also has a "run" child).
7724        let mut run_cmd = SpecCommand::builder()
7725            .name("run")
7726            .arg(SpecArg::builder().name("task").build())
7727            .build();
7728        let run_task = SpecCommand::builder().name("run").build();
7729        run_cmd.subcommands.insert("run".to_string(), run_task);
7730
7731        let mut cmd = SpecCommand::builder().name("test").build();
7732        cmd.subcommands.insert("run".to_string(), run_cmd);
7733
7734        let spec = Spec {
7735            name: "test".to_string(),
7736            bin: "test".to_string(),
7737            cmd,
7738            default_subcommand: Some("run".to_string()),
7739            ..Default::default()
7740        };
7741
7742        let input = vec!["test".to_string(), "other".to_string()];
7743        let parsed = parse(&spec, &input).unwrap();
7744
7745        // Should have two commands: root and "run" (the default)
7746        // We should NOT have switched again to the "run" task child
7747        assert_eq!(parsed.cmds.len(), 2);
7748        assert_eq!(parsed.cmds[0].name, "test");
7749        assert_eq!(parsed.cmds[1].name, "run");
7750
7751        // "other" should be parsed as a positional arg
7752        assert_eq!(parsed.args.len(), 1);
7753        let value = parsed.args.values().next().unwrap();
7754        assert_eq!(value.to_string(), "other");
7755    }
7756
7757    #[test]
7758    fn test_restart_token() {
7759        // Test that restart_token resets argument parsing
7760        let run_cmd = SpecCommand::builder()
7761            .name("run")
7762            .arg(SpecArg::builder().name("task").build())
7763            .restart_token(":::".to_string())
7764            .build();
7765        let mut cmd = SpecCommand::builder().name("test").build();
7766        cmd.subcommands.insert("run".to_string(), run_cmd);
7767
7768        let spec = Spec {
7769            name: "test".to_string(),
7770            bin: "test".to_string(),
7771            cmd,
7772            ..Default::default()
7773        };
7774
7775        // "test run task1 ::: task2" - should end up with task2 as the arg
7776        let input = vec![
7777            "test".to_string(),
7778            "run".to_string(),
7779            "task1".to_string(),
7780            ":::".to_string(),
7781            "task2".to_string(),
7782        ];
7783        let parsed = parse(&spec, &input).unwrap();
7784
7785        // After restart, args were cleared and task2 was parsed
7786        assert_eq!(parsed.args.len(), 1);
7787        let value = parsed.args.values().next().unwrap();
7788        assert_eq!(value.to_string(), "task2");
7789    }
7790
7791    #[test]
7792    fn test_restart_token_multiple() {
7793        // Test multiple restart tokens
7794        let run_cmd = SpecCommand::builder()
7795            .name("run")
7796            .arg(SpecArg::builder().name("task").build())
7797            .restart_token(":::".to_string())
7798            .build();
7799        let mut cmd = SpecCommand::builder().name("test").build();
7800        cmd.subcommands.insert("run".to_string(), run_cmd);
7801
7802        let spec = Spec {
7803            name: "test".to_string(),
7804            bin: "test".to_string(),
7805            cmd,
7806            ..Default::default()
7807        };
7808
7809        // "test run task1 ::: task2 ::: task3" - should end up with task3 as the arg
7810        let input = vec![
7811            "test".to_string(),
7812            "run".to_string(),
7813            "task1".to_string(),
7814            ":::".to_string(),
7815            "task2".to_string(),
7816            ":::".to_string(),
7817            "task3".to_string(),
7818        ];
7819        let parsed = parse(&spec, &input).unwrap();
7820
7821        // After multiple restarts, args were cleared and task3 was parsed
7822        assert_eq!(parsed.args.len(), 1);
7823        let value = parsed.args.values().next().unwrap();
7824        assert_eq!(value.to_string(), "task3");
7825    }
7826
7827    #[test]
7828    fn test_restart_token_clears_flag_awaiting_value() {
7829        // Test that restart_token clears pending flag values
7830        let run_cmd = SpecCommand::builder()
7831            .name("run")
7832            .arg(SpecArg::builder().name("task").build())
7833            .flag(
7834                SpecFlag::builder()
7835                    .name("jobs")
7836                    .long("jobs")
7837                    .arg(SpecArg::builder().name("count").build())
7838                    .build(),
7839            )
7840            .restart_token(":::".to_string())
7841            .build();
7842        let mut cmd = SpecCommand::builder().name("test").build();
7843        cmd.subcommands.insert("run".to_string(), run_cmd);
7844
7845        let spec = Spec {
7846            name: "test".to_string(),
7847            bin: "test".to_string(),
7848            cmd,
7849            ..Default::default()
7850        };
7851
7852        // "test run task1 --jobs ::: task2" - task2 should be an arg, not a flag value
7853        let input = vec![
7854            "test".to_string(),
7855            "run".to_string(),
7856            "task1".to_string(),
7857            "--jobs".to_string(),
7858            ":::".to_string(),
7859            "task2".to_string(),
7860        ];
7861        let parsed = parse(&spec, &input).unwrap();
7862
7863        // task2 should be parsed as the task arg, not as --jobs value
7864        assert_eq!(parsed.args.len(), 1);
7865        let value = parsed.args.values().next().unwrap();
7866        assert_eq!(value.to_string(), "task2");
7867        // --jobs should not have a value
7868        assert!(parsed.flag_awaiting_value.is_empty());
7869    }
7870
7871    #[test]
7872    fn test_restart_token_resets_double_dash() {
7873        // Test that restart_token resets the -- separator effect
7874        let run_cmd = SpecCommand::builder()
7875            .name("run")
7876            .arg(SpecArg::builder().name("task").build())
7877            .arg(SpecArg::builder().name("extra_args").var(true).build())
7878            .flag(SpecFlag::builder().name("verbose").long("verbose").build())
7879            .restart_token(":::".to_string())
7880            .build();
7881        let mut cmd = SpecCommand::builder().name("test").build();
7882        cmd.subcommands.insert("run".to_string(), run_cmd);
7883
7884        let spec = Spec {
7885            name: "test".to_string(),
7886            bin: "test".to_string(),
7887            cmd,
7888            ..Default::default()
7889        };
7890
7891        // "test run task1 -- extra ::: --verbose task2" - --verbose should be a flag after :::
7892        let input = vec![
7893            "test".to_string(),
7894            "run".to_string(),
7895            "task1".to_string(),
7896            "--".to_string(),
7897            "extra".to_string(),
7898            ":::".to_string(),
7899            "--verbose".to_string(),
7900            "task2".to_string(),
7901        ];
7902        let parsed = parse(&spec, &input).unwrap();
7903
7904        // --verbose should be parsed as a flag (not an arg) after the restart
7905        assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
7906        // task2 should be the arg after restart
7907        let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
7908        let value = parsed.args.get(task_arg).unwrap();
7909        assert_eq!(value.to_string(), "task2");
7910    }
7911
7912    #[test]
7913    fn test_double_dashes_without_preserve() {
7914        // Only the first `--` is a separator; a later one is a value, because flag
7915        // parsing has already stopped and there is nothing left for it to do.
7916        // `preserve` is about the *first* one — see the test below, where none is
7917        // consumed at all.
7918        let run_cmd = SpecCommand::builder()
7919            .name("run")
7920            .arg(SpecArg::builder().name("args").var(true).build())
7921            .build();
7922        let mut cmd = SpecCommand::builder().name("test").build();
7923        cmd.subcommands.insert("run".to_string(), run_cmd);
7924
7925        let spec = Spec {
7926            name: "test".to_string(),
7927            bin: "test".to_string(),
7928            cmd,
7929            ..Default::default()
7930        };
7931
7932        // "test run arg1 -- arg2 -- arg3": the first separates, the second is a value
7933        let input = vec![
7934            "test".to_string(),
7935            "run".to_string(),
7936            "arg1".to_string(),
7937            "--".to_string(),
7938            "arg2".to_string(),
7939            "--".to_string(),
7940            "arg3".to_string(),
7941        ];
7942        let parsed = parse(&spec, &input).unwrap();
7943
7944        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
7945        let value = parsed.args.get(args_arg).unwrap();
7946        assert_eq!(value.to_string(), "arg1 arg2 -- arg3");
7947    }
7948
7949    #[test]
7950    fn test_double_dashes_with_preserve() {
7951        // Test that variadic args WITH `preserve` keep all double dashes
7952        let run_cmd = SpecCommand::builder()
7953            .name("run")
7954            .arg(
7955                SpecArg::builder()
7956                    .name("args")
7957                    .var(true)
7958                    .double_dash(SpecDoubleDashChoices::Preserve)
7959                    .build(),
7960            )
7961            .build();
7962        let mut cmd = SpecCommand::builder().name("test").build();
7963        cmd.subcommands.insert("run".to_string(), run_cmd);
7964
7965        let spec = Spec {
7966            name: "test".to_string(),
7967            bin: "test".to_string(),
7968            cmd,
7969            ..Default::default()
7970        };
7971
7972        // "test run arg1 -- arg2 -- arg3" - all double dashes should be preserved
7973        let input = vec![
7974            "test".to_string(),
7975            "run".to_string(),
7976            "arg1".to_string(),
7977            "--".to_string(),
7978            "arg2".to_string(),
7979            "--".to_string(),
7980            "arg3".to_string(),
7981        ];
7982        let parsed = parse(&spec, &input).unwrap();
7983
7984        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
7985        let value = parsed.args.get(args_arg).unwrap();
7986        assert_eq!(value.to_string(), "arg1 -- arg2 -- arg3");
7987    }
7988
7989    #[test]
7990    fn test_double_dashes_with_preserve_only_dashes() {
7991        // Test that variadic args WITH `preserve` keep all double dashes even
7992        // if the values are just double dashes
7993        let run_cmd = SpecCommand::builder()
7994            .name("run")
7995            .arg(
7996                SpecArg::builder()
7997                    .name("args")
7998                    .var(true)
7999                    .double_dash(SpecDoubleDashChoices::Preserve)
8000                    .build(),
8001            )
8002            .build();
8003        let mut cmd = SpecCommand::builder().name("test").build();
8004        cmd.subcommands.insert("run".to_string(), run_cmd);
8005
8006        let spec = Spec {
8007            name: "test".to_string(),
8008            bin: "test".to_string(),
8009            cmd,
8010            ..Default::default()
8011        };
8012
8013        // "test run -- --" - all double dashes should be preserved
8014        let input = vec![
8015            "test".to_string(),
8016            "run".to_string(),
8017            "--".to_string(),
8018            "--".to_string(),
8019        ];
8020        let parsed = parse(&spec, &input).unwrap();
8021
8022        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
8023        let value = parsed.args.get(args_arg).unwrap();
8024        assert_eq!(value.to_string(), "-- --");
8025    }
8026
8027    #[test]
8028    fn test_double_dashes_with_preserve_multiple_args() {
8029        // Test with multiple args where only the second has has `preserve`
8030        let run_cmd = SpecCommand::builder()
8031            .name("run")
8032            .arg(SpecArg::builder().name("task").build())
8033            .arg(
8034                SpecArg::builder()
8035                    .name("extra_args")
8036                    .var(true)
8037                    .double_dash(SpecDoubleDashChoices::Preserve)
8038                    .build(),
8039            )
8040            .build();
8041        let mut cmd = SpecCommand::builder().name("test").build();
8042        cmd.subcommands.insert("run".to_string(), run_cmd);
8043
8044        let spec = Spec {
8045            name: "test".to_string(),
8046            bin: "test".to_string(),
8047            cmd,
8048            ..Default::default()
8049        };
8050
8051        // The first arg "task1" is captured normally
8052        // Then extra_args with `preserve` captures everything, including the "--" tokens
8053        let input = vec![
8054            "test".to_string(),
8055            "run".to_string(),
8056            "task1".to_string(),
8057            "--".to_string(),
8058            "arg1".to_string(),
8059            "--".to_string(),
8060            "--foo".to_string(),
8061        ];
8062        let parsed = parse(&spec, &input).unwrap();
8063
8064        let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
8065        let task_value = parsed.args.get(task_arg).unwrap();
8066        assert_eq!(task_value.to_string(), "task1");
8067
8068        let extra_arg = parsed.args.keys().find(|a| a.name == "extra_args").unwrap();
8069        let extra_value = parsed.args.get(extra_arg).unwrap();
8070        assert_eq!(extra_value.to_string(), "-- arg1 -- --foo");
8071    }
8072
8073    fn spec_with_args(args: impl IntoIterator<Item = SpecArg>) -> Spec {
8074        let cmd = SpecCommand::builder().name("test").args(args).build();
8075        Spec {
8076            name: "test".to_string(),
8077            bin: "test".to_string(),
8078            cmd,
8079            ..Default::default()
8080        }
8081    }
8082
8083    fn arg_value(parsed: &ParseOutput, name: &str) -> String {
8084        let arg = parsed
8085            .args
8086            .keys()
8087            .find(|a| a.name == name)
8088            .unwrap_or_else(|| panic!("expected arg {name} to be parsed"));
8089        parsed.args.get(arg).unwrap().to_string()
8090    }
8091
8092    fn required_arg(name: &str) -> SpecArg {
8093        SpecArg::builder()
8094            .name(name)
8095            .var(true)
8096            .required(false)
8097            .double_dash(SpecDoubleDashChoices::Required)
8098            .build()
8099    }
8100
8101    #[test]
8102    fn test_double_dash_required_reports_error_once_for_variadic() {
8103        // A variadic arg is offered every remaining word, but the mistake is one mistake.
8104        let spec = spec_with_args([required_arg("files")]);
8105
8106        let parsed = parse_partial(&spec, &input(&["test", "a", "b", "c"])).unwrap();
8107
8108        assert!(parsed.args.is_empty());
8109        assert_eq!(parsed.errors.len(), 1);
8110        assert!(
8111            matches!(&parsed.errors[0], UsageErr::ArgRequiresDoubleDash(name) if name == "files")
8112        );
8113    }
8114
8115    #[test]
8116    fn test_double_dash_required_suppresses_missing_arg() {
8117        // The arg is never filled, so the end-of-parse check would also call it missing.
8118        let spec = spec_with_args([SpecArg::builder()
8119            .name("file")
8120            .required(true)
8121            .double_dash(SpecDoubleDashChoices::Required)
8122            .build()]);
8123
8124        let parsed = parse_partial(&spec, &input(&["test", "x"])).unwrap();
8125
8126        assert_eq!(parsed.errors.len(), 1);
8127        assert!(matches!(
8128            &parsed.errors[0],
8129            UsageErr::ArgRequiresDoubleDash(_)
8130        ));
8131        // The cursor stays put, so a completion keeps offering the same arg.
8132        assert_eq!(
8133            parsed.next_arg.as_ref().map(|a| a.name.as_str()),
8134            Some("file")
8135        );
8136        assert!(!parsed.double_dash_seen);
8137    }
8138
8139    #[test]
8140    fn test_double_dash_routes_to_required_arg() {
8141        // Everything after `--` belongs to the arg that requires it, even though the greedy
8142        // variadic before it would otherwise swallow the rest (clap's `Arg::last(true)`).
8143        let spec = spec_with_args([
8144            SpecArg::builder()
8145                .name("tool")
8146                .var(true)
8147                .required(false)
8148                .build(),
8149            required_arg("command"),
8150        ]);
8151
8152        let parsed = parse(&spec, &input(&["test", "node@20", "--", "node", "app.js"])).unwrap();
8153
8154        assert_eq!(arg_value(&parsed, "tool"), "node@20");
8155        assert_eq!(arg_value(&parsed, "command"), "node app.js");
8156        assert!(parsed.double_dash_seen);
8157    }
8158
8159    #[test]
8160    fn test_double_dash_routes_with_gap_reports_missing_arg() {
8161        // Jumping the cursor leaves `tool` empty even though `command` is filled, so the
8162        // "is it filled?" check cannot be a count of how many args were filled.
8163        let spec = spec_with_args([
8164            SpecArg::builder()
8165                .name("tool")
8166                .var(true)
8167                .required(true)
8168                .build(),
8169            required_arg("command"),
8170        ]);
8171
8172        let parsed = parse_partial(&spec, &input(&["test", "--", "ls"])).unwrap();
8173
8174        assert_eq!(arg_value(&parsed, "command"), "ls");
8175        assert!(parsed.args.keys().all(|a| a.name != "tool"));
8176        assert!(parsed
8177            .errors
8178            .iter()
8179            .any(|e| matches!(e, UsageErr::MissingArg(name) if name == "tool")));
8180    }
8181
8182    #[test]
8183    fn test_double_dash_gap_applies_defaults() {
8184        // Same gap, seen from `Parser::parse`: the skipped arg still gets its default.
8185        let spec = spec_with_args([
8186            SpecArg::builder()
8187                .name("tool")
8188                .var(true)
8189                .required(false)
8190                .default_value("node@20")
8191                .build(),
8192            required_arg("command"),
8193        ]);
8194
8195        let parsed = parse(&spec, &input(&["test", "--", "ls"])).unwrap();
8196
8197        assert_eq!(arg_value(&parsed, "command"), "ls");
8198        assert_eq!(arg_value(&parsed, "tool"), "node@20");
8199    }
8200
8201    fn spec_with_restart_token_and_required_arg() -> Spec {
8202        let run_cmd = SpecCommand::builder()
8203            .name("run")
8204            .arg(SpecArg::builder().name("task").build())
8205            .arg(required_arg("run_args"))
8206            .restart_token(":::".to_string())
8207            .build();
8208        let mut cmd = SpecCommand::builder().name("test").build();
8209        cmd.subcommands.insert("run".to_string(), run_cmd);
8210        Spec {
8211            name: "test".to_string(),
8212            bin: "test".to_string(),
8213            cmd,
8214            ..Default::default()
8215        }
8216    }
8217
8218    #[test]
8219    fn test_double_dash_required_restart_token_resets_separator() {
8220        // The `--` before `:::` belongs to the previous invocation only.
8221        let spec = spec_with_restart_token_and_required_arg();
8222
8223        let parsed = parse_partial(
8224            &spec,
8225            &input(&["test", "run", "task1", "--", "a", ":::", "task2", "b"]),
8226        )
8227        .unwrap();
8228
8229        assert_eq!(arg_value(&parsed, "task"), "task2");
8230        assert!(parsed.args.keys().all(|a| a.name != "run_args"));
8231        // Reported once even though the arg was violated after already succeeding once.
8232        assert_eq!(
8233            parsed
8234                .errors
8235                .iter()
8236                .filter(|e| matches!(e, UsageErr::ArgRequiresDoubleDash(_)))
8237                .count(),
8238            1
8239        );
8240    }
8241
8242    #[test]
8243    fn test_double_dash_required_restart_token_accepts_new_separator() {
8244        let spec = spec_with_restart_token_and_required_arg();
8245
8246        let parsed = parse(
8247            &spec,
8248            &input(&["test", "run", "task1", "--", "a", ":::", "task2", "--", "c"]),
8249        )
8250        .unwrap();
8251
8252        assert_eq!(arg_value(&parsed, "task"), "task2");
8253        assert_eq!(arg_value(&parsed, "run_args"), "c");
8254    }
8255
8256    #[test]
8257    fn test_double_dash_preserve_is_not_a_separator() {
8258        // A `--` that `preserve` keeps is a *value* of that arg, so it must not unlock the
8259        // arg that requires a separator. Deliberate: one token cannot be both.
8260        let spec = spec_with_args([
8261            SpecArg::builder()
8262                .name("kept")
8263                .var(true)
8264                .var_max(1)
8265                .required(false)
8266                .double_dash(SpecDoubleDashChoices::Preserve)
8267                .build(),
8268            required_arg("rest"),
8269        ]);
8270
8271        let parsed = parse_partial(&spec, &input(&["test", "--", "x"])).unwrap();
8272
8273        assert_eq!(arg_value(&parsed, "kept"), "--");
8274        assert!(parsed.args.keys().all(|a| a.name != "rest"));
8275        assert!(!parsed.double_dash_seen);
8276        assert_eq!(parsed.errors.len(), 1);
8277    }
8278
8279    #[test]
8280    fn test_double_dash_required_does_not_bail_in_parse_partial() {
8281        // Completions parse half-typed command lines; they must still get a result.
8282        let spec = spec_with_args([required_arg("file")]);
8283
8284        assert!(parse_partial(&spec, &input(&["test", "x"])).is_ok());
8285        assert!(parse(&spec, &input(&["test", "x"])).is_err());
8286    }
8287
8288    #[test]
8289    fn test_double_dash_without_required_arg_does_not_move_cursor() {
8290        // Specs with no `double_dash="required"` arg are untouched by the jump.
8291        let spec = spec_with_args([
8292            SpecArg::builder().name("first").required(false).build(),
8293            SpecArg::builder().name("second").required(false).build(),
8294        ]);
8295
8296        let parsed = parse(&spec, &input(&["test", "--", "a", "b"])).unwrap();
8297
8298        assert_eq!(arg_value(&parsed, "first"), "a");
8299        assert_eq!(arg_value(&parsed, "second"), "b");
8300        assert!(parsed.next_arg.is_none());
8301    }
8302
8303    #[test]
8304    fn test_parser_with_custom_env_for_required_arg() {
8305        let spec = spec_with_arg(
8306            SpecArg::builder()
8307                .name("name")
8308                .env("NAME")
8309                .required(true)
8310                .build(),
8311        );
8312        std::env::remove_var("NAME");
8313
8314        let parsed = parse_with_env(&spec, &["test"], &[("NAME", "john")])
8315            .expect("parse should succeed with custom env");
8316        assert_eq!(parsed.args.len(), 1);
8317        assert_eq!(first_string_value(&parsed), "john");
8318    }
8319
8320    #[test]
8321    fn test_parser_with_custom_env_for_required_flag() {
8322        let spec = spec_with_flag(
8323            SpecFlag::builder()
8324                .long("name")
8325                .env("NAME")
8326                .required(true)
8327                .arg(SpecArg::builder().name("name").build())
8328                .build(),
8329        );
8330        std::env::remove_var("NAME");
8331
8332        let parsed = parse_with_env(&spec, &["test"], &[("NAME", "jane")])
8333            .expect("parse should succeed with custom env");
8334        assert_eq!(parsed.flags.len(), 1);
8335        assert_eq!(first_string_value(&parsed), "jane");
8336    }
8337
8338    #[test]
8339    fn test_flag_environment_fallbacks_preserve_declaration_order() {
8340        let spec = spec_with_flag(
8341            SpecFlag::builder()
8342                .long("name")
8343                .env("NAME")
8344                .env_fallback("OLD_NAME")
8345                .env_fallback("OLDER_NAME")
8346                .deprecated_env("DEPRECATED_NAME")
8347                .arg(SpecArg::builder().name("name").build())
8348                .build(),
8349        );
8350
8351        let parsed = parse_with_env(
8352            &spec,
8353            &["test"],
8354            &[
8355                ("NAME", "canonical"),
8356                ("OLD_NAME", "fallback"),
8357                ("DEPRECATED_NAME", "deprecated"),
8358            ],
8359        )
8360        .unwrap();
8361        assert_eq!(first_string_value(&parsed), "canonical");
8362
8363        let parsed = parse_with_env(
8364            &spec,
8365            &["test"],
8366            &[("OLDER_NAME", "older"), ("OLD_NAME", "old")],
8367        )
8368        .unwrap();
8369        assert_eq!(first_string_value(&parsed), "old");
8370
8371        let parsed =
8372            parse_with_env(&spec, &["test"], &[("DEPRECATED_NAME", "deprecated")]).unwrap();
8373        assert_eq!(first_string_value(&parsed), "deprecated");
8374    }
8375
8376    #[test]
8377    fn a_value_from_a_deprecated_alias_says_which_name_to_use() {
8378        let spec = spec_with_flag(
8379            SpecFlag::builder()
8380                .long("name")
8381                .env("NAME")
8382                .deprecated_env("DEPRECATED_NAME")
8383                .arg(SpecArg::builder().name("name").build())
8384                .build(),
8385        );
8386
8387        // The current name is not a deprecated one, and says nothing.
8388        let parsed = parse_with_env(&spec, &["test"], &[("NAME", "canonical")]).unwrap();
8389        assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
8390
8391        let parsed =
8392            parse_with_env(&spec, &["test"], &[("DEPRECATED_NAME", "deprecated")]).unwrap();
8393        assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
8394        assert_eq!(
8395            parsed.warnings[0].kind,
8396            crate::warn::WarningKind::DeprecatedEnv
8397        );
8398        assert_eq!(parsed.warnings[0].name, "DEPRECATED_NAME");
8399        assert_eq!(parsed.warnings[0].replacement.as_deref(), Some("NAME"));
8400        // Reported, not printed, and the value still arrives.
8401        assert_eq!(first_string_value(&parsed), "deprecated");
8402    }
8403
8404    #[test]
8405    fn a_deprecated_flag_reports_only_when_it_was_used() {
8406        let spec = spec_with_flag(
8407            SpecFlag::builder()
8408                .long("output")
8409                .deprecated("use --out")
8410                .deprecated_remove_at("3.0.0")
8411                .arg(SpecArg::builder().name("output").build())
8412                .build(),
8413        );
8414
8415        let parsed = parse_with_env(&spec, &["test"], &[]).unwrap();
8416        assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
8417
8418        let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
8419        assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
8420        assert_eq!(
8421            parsed.warnings[0].kind,
8422            crate::warn::WarningKind::DeprecatedFlag
8423        );
8424        // Named the way it was typed, dashes and all.
8425        assert_eq!(parsed.warnings[0].name, "--output");
8426        assert_eq!(parsed.warnings[0].remove_at.as_deref(), Some("3.0.0"));
8427        assert_eq!(
8428            parsed.warnings[0].render(),
8429            "warning: --output is deprecated, removed at 3.0.0: use --out\n",
8430        );
8431    }
8432
8433    #[test]
8434    fn a_milestone_the_spec_has_not_reached_stays_quiet() {
8435        let flag = SpecFlag::builder()
8436            .long("output")
8437            .deprecated("use --out")
8438            .deprecated_warn_at("9.0.0")
8439            .arg(SpecArg::builder().name("output").build())
8440            .build();
8441        let mut spec = spec_with_flag(flag);
8442        spec.version = Some("2.0.0".to_string());
8443
8444        let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
8445        assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
8446
8447        // And once the CLI is the release that was named, it speaks up.
8448        spec.version = Some("9.0.0".to_string());
8449        let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
8450        assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
8451    }
8452
8453    #[test]
8454    fn test_parser_with_custom_env_still_fails_when_missing() {
8455        let spec = spec_with_arg(
8456            SpecArg::builder()
8457                .name("name")
8458                .env("NAME")
8459                .required(true)
8460                .build(),
8461        );
8462        std::env::remove_var("NAME");
8463        assert!(parse_with_env(&spec, &["test"], &[]).is_err());
8464    }
8465
8466    #[test]
8467    fn test_parser_does_not_treat_env_choice_value_as_help() {
8468        let spec = spec_with_arg(
8469            SpecArg::builder()
8470                .name("env")
8471                .env("CURRENT_ENV")
8472                .choices(["dev", "staging"])
8473                .required(false)
8474                .build(),
8475        );
8476
8477        assert_parse_err(
8478            parse_with_env(&spec, &["test"], &[("CURRENT_ENV", "--help")]),
8479            "Invalid choice for arg env: --help, expected one of dev, staging",
8480        );
8481    }
8482
8483    #[test]
8484    fn test_parser_does_not_treat_default_choice_value_as_help() {
8485        let spec = spec_with_flag(
8486            SpecFlag::builder()
8487                .long("env")
8488                .arg(
8489                    SpecArg::builder()
8490                        .name("env")
8491                        .choices(["dev", "staging"])
8492                        .build(),
8493                )
8494                .default_value("--help")
8495                .build(),
8496        );
8497
8498        assert_parse_err(
8499            parse_with_env(&spec, &["test"], &[]),
8500            "Invalid choice for option env: --help, expected one of dev, staging",
8501        );
8502    }
8503
8504    /// argv as `parse` wants it, program name included.
8505    fn words(of: &[&str]) -> Vec<String> {
8506        of.iter().map(|s| s.to_string()).collect()
8507    }
8508
8509    #[test]
8510    fn a_command_that_needs_a_subcommand_says_so() {
8511        // The spec has carried `subcommand_required` since the derive needed it, and this parser
8512        // never read it — so `mise generate`, which declares it, parsed as a complete
8513        // invocation while usage-argv and clap both refused. Found by the differential fuzzer.
8514        let spec: Spec = r#"
8515name "ex"
8516bin "ex"
8517cmd "gen" subcommand_required=#true {
8518    cmd "two" {}
8519    cmd "one" {}
8520    cmd "secret" hide=#true {}
8521    alias "g"
8522}
8523cmd "open" {
8524    cmd "sub" {}
8525}
8526"#
8527        .parse()
8528        .unwrap();
8529
8530        let err = parse(&spec, &words(&["ex", "gen"])).unwrap_err();
8531        // Sorted, so the message does not depend on map order; hidden commands left out,
8532        // because a message telling someone to type a hidden name is worse than a vague one;
8533        // and the alias not listed beside the name it points at.
8534        assert_eq!(err.to_string(), "`gen` needs a subcommand: one of one, two");
8535
8536        // Reached through its alias, and still about the command rather than the spelling.
8537        let err = parse(&spec, &words(&["ex", "g"])).unwrap_err();
8538        assert!(err.to_string().starts_with("`gen` needs a subcommand"));
8539
8540        // Given one: fine.
8541        parse(&spec, &words(&["ex", "gen", "one"])).unwrap();
8542
8543        // And a command that has subcommands without declaring them required is untouched —
8544        // this is the half that keeps the check from being "any command with children".
8545        parse(&spec, &words(&["ex", "open"])).unwrap();
8546        parse(&spec, &words(&["ex", "open", "sub"])).unwrap();
8547    }
8548
8549    #[test]
8550    fn arg_required_else_help_observes_the_selected_commands_argv() {
8551        let spec: Spec = r#"
8552name "ex"
8553bin "ex"
8554flag "--verbose" global=#true
8555cmd "run" arg_required_else_help=#true {
8556    flag "--all"
8557}
8558"#
8559        .parse()
8560        .unwrap();
8561        let words = |items: &[&str]| items.iter().map(|s| (*s).to_string()).collect::<Vec<_>>();
8562
8563        let err = parse(&spec, &words(&["ex", "run"])).unwrap_err();
8564        assert!(err.to_string().contains("Usage: ex run"), "{err}");
8565
8566        // A global before the command belongs to the ancestor. It selected `run`, but did not
8567        // give `run` an argument of its own.
8568        let err = parse(&spec, &words(&["ex", "--verbose", "run"])).unwrap_err();
8569        assert!(err.to_string().contains("Usage: ex run"), "{err}");
8570
8571        parse(&spec, &words(&["ex", "run", "--all"])).expect("run received an argv token");
8572    }
8573
8574    #[test]
8575    fn an_unmatched_word_is_forwarded_when_external_subcommand_is_set() {
8576        let spec: Spec = r#"
8577name "ex"
8578bin "ex"
8579unknown_flags "error"
8580external_subcommand #true
8581cmd "install"
8582flag "-v --verbose" global=#true
8583"#
8584        .parse()
8585        .unwrap();
8586
8587        let parsed = parse(&spec, &input(&["ex", "foo", "--help", "bar"])).unwrap();
8588        assert_eq!(
8589            parsed.external,
8590            Some(vec!["foo".into(), "--help".into(), "bar".into()])
8591        );
8592        assert!(parsed.flags.is_empty());
8593
8594        // Known subcommands still win.
8595        let parsed = parse(&spec, &input(&["ex", "install"])).unwrap();
8596        assert_eq!(parsed.cmd.name, "install");
8597        assert!(parsed.external.is_none());
8598
8599        // A global flag before the unmatched word still binds on the parent.
8600        let parsed = parse(&spec, &input(&["ex", "-v", "foo", "--verbose"])).unwrap();
8601        assert_eq!(
8602            parsed.external,
8603            Some(vec!["foo".into(), "--verbose".into()])
8604        );
8605        assert!(parsed.flags.keys().any(|flag| flag.name == "verbose"));
8606
8607        // An unknown flag on the parent is still an error, which is what clap does.
8608        assert!(parse(&spec, &input(&["ex", "--wat"])).is_err());
8609
8610        // A negative number is a value, not a flag, so it can be the unmatched word.
8611        // usage-argv already forwarded `-1`; Phase 1 used to treat every `starts_with('-')`
8612        // token as a flag and never reach the catch-all.
8613        let parsed = parse(&spec, &input(&["ex", "-1", "rest"])).unwrap();
8614        assert_eq!(parsed.external, Some(vec!["-1".into(), "rest".into()]));
8615    }
8616
8617    #[test]
8618    fn an_external_subcommand_satisfies_subcommand_required() {
8619        let mut spec: Spec = r#"
8620name "ex"
8621bin "ex"
8622external_subcommand #true
8623cmd "install"
8624"#
8625        .parse()
8626        .unwrap();
8627        spec.cmd.subcommand_required = true;
8628
8629        parse(&spec, &input(&["ex", "foo", "--help"])).unwrap();
8630        assert!(parse(&spec, &input(&["ex"])).is_err());
8631    }
8632
8633    #[test]
8634    fn a_default_subcommand_outranks_an_external_one() {
8635        let spec: Spec = r#"
8636name "ex"
8637bin "ex"
8638default_subcommand "run"
8639external_subcommand #true
8640cmd "run" {
8641    arg "[task]"
8642}
8643"#
8644        .parse()
8645        .unwrap();
8646
8647        let parsed = parse(&spec, &input(&["ex", "build"])).unwrap();
8648        assert_eq!(parsed.cmd.name, "run");
8649        assert!(parsed.external.is_none());
8650        assert_eq!(first_string_value(&parsed), "build");
8651    }
8652
8653    #[test]
8654    fn multicall_basename_strips_a_path_and_exe() {
8655        assert_eq!(multicall_basename("/usr/bin/ls"), "ls");
8656        assert_eq!(multicall_basename(r"C:\busybox\ls.exe"), "ls");
8657        assert_eq!(multicall_basename("LS.EXE"), "LS");
8658        assert_eq!(multicall_basename("busybox"), "busybox");
8659    }
8660
8661    #[test]
8662    fn a_multicall_applet_is_the_first_word() {
8663        let spec: Spec = r#"
8664name "busybox"
8665bin "busybox"
8666multicall #true
8667cmd "ls" {
8668    arg "[ARGS]" var=#true
8669}
8670cmd "cat"
8671"#
8672        .parse()
8673        .unwrap();
8674
8675        // A symlink: argv[0] is the applet.
8676        let parsed = parse(&spec, &input(&["/usr/bin/ls", "-l"])).unwrap();
8677        assert_eq!(parsed.cmd.name, "ls");
8678        match parsed.args.values().next() {
8679            Some(ParseValue::MultiString(values)) => assert_eq!(values, &["-l".to_string()]),
8680            other => panic!("expected ARGS to collect -l, got {other:?}"),
8681        }
8682
8683        // A dispatcher invocation still skips argv[0].
8684        let parsed = parse(&spec, &input(&["/usr/bin/busybox", "ls", "-l"])).unwrap();
8685        assert_eq!(parsed.cmd.name, "ls");
8686
8687        // Configured dispatcher values receive the same path and extension normalization.
8688        let mut configured = spec.clone();
8689        configured.name = "BusyBox".to_string();
8690        configured.bin = "/opt/bin/busybox.exe".to_string();
8691        let parsed = parse(&configured, &input(&["/usr/bin/busybox.exe", "ls", "-l"])).unwrap();
8692        assert_eq!(parsed.cmd.name, "ls");
8693
8694        // `.exe` is stripped so Windows and Unix agree.
8695        let parsed = parse(&spec, &input(&["ls.exe"])).unwrap();
8696        assert_eq!(parsed.cmd.name, "ls");
8697
8698        // Without the property, argv[0] is discarded as usual.
8699        let mut plain = spec.clone();
8700        plain.multicall = false;
8701        let parsed = parse(&plain, &input(&["/usr/bin/ls", "ls"])).unwrap();
8702        assert_eq!(parsed.cmd.name, "ls");
8703    }
8704
8705    #[test]
8706    fn a_multicall_unknown_applet_can_be_external() {
8707        let spec: Spec = r#"
8708name "busybox"
8709bin "busybox"
8710multicall #true
8711unknown_flags "error"
8712external_subcommand #true
8713cmd "ls"
8714"#
8715        .parse()
8716        .unwrap();
8717
8718        let parsed = parse(&spec, &input(&["/usr/bin/git", "--help"])).unwrap();
8719        assert_eq!(parsed.external, Some(vec!["git".into(), "--help".into()]));
8720
8721        let mut closed = spec.clone();
8722        closed.cmd.external_subcommand = false;
8723        assert!(parse(&closed, &input(&["wat"])).is_err());
8724    }
8725
8726    #[cfg(feature = "unstable_choices_env")]
8727    #[test]
8728    fn test_parser_arg_choices_from_custom_env() {
8729        let spec = spec_arg_choices_env("DEPLOY_ENVS");
8730
8731        let parsed =
8732            parse_with_env(&spec, &["test", "bar"], &[("DEPLOY_ENVS", "foo,bar baz")]).unwrap();
8733        assert_eq!(first_string_value(&parsed), "bar");
8734
8735        assert_parse_err(
8736            parse_with_env(&spec, &["test", "prod"], &[("DEPLOY_ENVS", "foo,bar baz")]),
8737            "Invalid choice for arg env: prod, expected one of foo, bar, baz",
8738        );
8739        assert_parse_err(
8740            parse_with_env(&spec, &["test", "prod"], &[]),
8741            "Invalid choice for arg env: prod, no choices resolved from env DEPLOY_ENVS",
8742        );
8743    }
8744
8745    #[cfg(feature = "unstable_choices_env")]
8746    #[test]
8747    fn test_parser_validates_flag_choices_from_custom_env() {
8748        let spec = spec_flag_choices_env("DEPLOY_ENVS");
8749        let parsed = parse_with_env(
8750            &spec,
8751            &["test", "--env", "baz"],
8752            &[("DEPLOY_ENVS", "foo,bar baz")],
8753        )
8754        .unwrap();
8755        assert_eq!(first_string_value(&parsed), "baz");
8756    }
8757
8758    #[cfg(feature = "unstable_choices_env")]
8759    #[test]
8760    fn test_parser_revalidates_env_and_default_values_against_choices_env() {
8761        let arg_env_spec = spec_with_arg(
8762            SpecArg::builder()
8763                .name("env")
8764                .env("CURRENT_ENV")
8765                .choices_env("DEPLOY_ENVS")
8766                .build(),
8767        );
8768        assert_parse_err(
8769            parse_with_env(
8770                &arg_env_spec,
8771                &["test"],
8772                &[("CURRENT_ENV", "prod"), ("DEPLOY_ENVS", "dev,staging")],
8773            ),
8774            "Invalid choice for arg env: prod, expected one of dev, staging",
8775        );
8776
8777        let flag_default_spec = spec_with_flag(
8778            SpecFlag::builder()
8779                .long("env")
8780                .arg(
8781                    SpecArg::builder()
8782                        .name("env")
8783                        .choices_env("DEPLOY_ENVS")
8784                        .build(),
8785                )
8786                .default_value("prod")
8787                .build(),
8788        );
8789        assert_parse_err(
8790            parse_with_env(
8791                &flag_default_spec,
8792                &["test"],
8793                &[("DEPLOY_ENVS", "dev,staging")],
8794            ),
8795            "Invalid choice for option env: prod, expected one of dev, staging",
8796        );
8797    }
8798
8799    #[test]
8800    fn test_variadic_arg_captures_unknown_flags_from_spec_string() {
8801        let spec: Spec = r#"
8802            flag "-v --verbose" var=#true
8803            arg "[database]" default="myapp_dev"
8804            arg "[args...]"
8805        "#
8806        .parse()
8807        .unwrap();
8808        let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
8809            .into_iter()
8810            .map(String::from)
8811            .collect();
8812        let parsed = parse(&spec, &input).unwrap();
8813        let env = parsed.as_env();
8814        assert_eq!(env.get("usage_database").unwrap(), "mydb");
8815        assert_eq!(env.get("usage_args").unwrap(), "--host localhost");
8816    }
8817
8818    #[test]
8819    fn test_variadic_arg_captures_unknown_flags() {
8820        let cmd = SpecCommand::builder()
8821            .name("test")
8822            .flag(SpecFlag::builder().short('v').long("verbose").build())
8823            .arg(SpecArg::builder().name("database").required(false).build())
8824            .arg(
8825                SpecArg::builder()
8826                    .name("args")
8827                    .required(false)
8828                    .var(true)
8829                    .build(),
8830            )
8831            .build();
8832        let spec = Spec {
8833            name: "test".to_string(),
8834            bin: "test".to_string(),
8835            cmd,
8836            ..Default::default()
8837        };
8838
8839        // Unknown --host flag and its value should be captured by [args...]
8840        let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
8841            .into_iter()
8842            .map(String::from)
8843            .collect();
8844        let parsed = parse(&spec, &input).unwrap();
8845        assert_eq!(parsed.args.len(), 2);
8846        let args_val = parsed
8847            .args
8848            .iter()
8849            .find(|(a, _)| a.name == "args")
8850            .unwrap()
8851            .1;
8852        match args_val {
8853            ParseValue::MultiString(v) => {
8854                assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
8855            }
8856            _ => panic!("Expected MultiString, got {:?}", args_val),
8857        }
8858    }
8859
8860    #[test]
8861    fn test_variadic_arg_captures_unknown_flags_with_double_dash() {
8862        let cmd = SpecCommand::builder()
8863            .name("test")
8864            .flag(SpecFlag::builder().short('v').long("verbose").build())
8865            .arg(SpecArg::builder().name("database").required(false).build())
8866            .arg(
8867                SpecArg::builder()
8868                    .name("args")
8869                    .required(false)
8870                    .var(true)
8871                    .build(),
8872            )
8873            .build();
8874        let spec = Spec {
8875            name: "test".to_string(),
8876            bin: "test".to_string(),
8877            cmd,
8878            ..Default::default()
8879        };
8880
8881        // With explicit -- separator
8882        let input: Vec<String> = vec!["test", "--", "mydb", "--host", "localhost"]
8883            .into_iter()
8884            .map(String::from)
8885            .collect();
8886        let parsed = parse(&spec, &input).unwrap();
8887        assert_eq!(parsed.args.len(), 2);
8888        let args_val = parsed
8889            .args
8890            .iter()
8891            .find(|(a, _)| a.name == "args")
8892            .unwrap()
8893            .1;
8894        match args_val {
8895            ParseValue::MultiString(v) => {
8896                assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
8897            }
8898            _ => panic!("Expected MultiString, got {:?}", args_val),
8899        }
8900    }
8901
8902    #[test]
8903    fn test_variadic_arg_unknown_flag_equals_value_not_split() {
8904        // Regression: --flag=value should be treated as a single positional token when
8905        // --flag is not a known spec flag, not split into "--flag=value" AND "value".
8906        let spec: Spec = r#"arg "[other_args]" var=#true"#.parse().unwrap();
8907
8908        // Single unknown --flag=value: must not produce a stray "3" positional.
8909        // as_env() shell-joins values, so "=" gets quoted.
8910        let input: Vec<String> = vec!["test", "--option=3"]
8911            .into_iter()
8912            .map(String::from)
8913            .collect();
8914        let parsed = parse(&spec, &input).unwrap();
8915        let env = parsed.as_env();
8916        assert_eq!(
8917            env.get("usage_other_args").map(String::as_str),
8918            Some("'--option=3'"),
8919            "expected a single --option=3 token, got {:?}",
8920            env.get("usage_other_args"),
8921        );
8922
8923        // Multiple unknown --flag=value args should each be kept intact
8924        let input2: Vec<String> = vec!["test", "--foo=bar", "--baz=qux"]
8925            .into_iter()
8926            .map(String::from)
8927            .collect();
8928        let parsed2 = parse(&spec, &input2).unwrap();
8929        let env2 = parsed2.as_env();
8930        assert_eq!(
8931            env2.get("usage_other_args").map(String::as_str),
8932            Some("'--foo=bar' '--baz=qux'"),
8933            "expected two intact tokens, got {:?}",
8934            env2.get("usage_other_args"),
8935        );
8936
8937        // Mix of plain positional args and unknown --flag=value tokens
8938        let input3: Vec<String> = vec!["test", "positional1", "--option=3", "positional2"]
8939            .into_iter()
8940            .map(String::from)
8941            .collect();
8942        let parsed3 = parse(&spec, &input3).unwrap();
8943        let env3 = parsed3.as_env();
8944        assert_eq!(
8945            env3.get("usage_other_args").map(String::as_str),
8946            Some("positional1 '--option=3' positional2"),
8947            "expected positional args and intact flag token, got {:?}",
8948            env3.get("usage_other_args"),
8949        );
8950    }
8951
8952    #[test]
8953    fn test_allow_hyphen_values_consumes_short_flag_collision() {
8954        let spec = r#"
8955flag "-d --working-dir <DIR>"
8956flag "-a --args <ARGS>" allow_hyphen_values=#true
8957"#
8958        .parse::<Spec>()
8959        .unwrap();
8960
8961        let parsed = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap();
8962
8963        assert_eq!(parsed.flags.len(), 1);
8964        assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
8965    }
8966
8967    #[test]
8968    fn test_allow_hyphen_values_consumes_embedded_long_value() {
8969        let spec = r#"
8970flag "-d --working-dir <DIR>"
8971flag "-a --args <ARGS>" allow_hyphen_values=#true
8972"#
8973        .parse::<Spec>()
8974        .unwrap();
8975
8976        let parsed = parse(&spec, &input(&["test", "--args=-destroy"])).unwrap();
8977
8978        assert_eq!(parsed.flags.len(), 1);
8979        assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
8980    }
8981
8982    #[test]
8983    fn test_allow_hyphen_values_takes_the_separator_as_its_value() {
8984        // The flag is declared to accept a token that looks like a flag, and `--` looks
8985        // like one, so it binds — which is what clap does with the same declaration.
8986        // Letting the separator arm run first consumed it and left the flag hungry, and
8987        // the flag then ate the word past it: `-a -- -x` bound `-x` with the `--` gone.
8988        let spec = r#"
8989flag "-a --args <ARGS>" allow_hyphen_values=#true
8990arg "[rest]..."
8991"#
8992        .parse::<Spec>()
8993        .unwrap();
8994
8995        let parsed = parse(&spec, &input(&["test", "-a", "--", "-x"])).unwrap();
8996
8997        assert_eq!(flag_string_value(&parsed, "args"), "--");
8998        let rest = parsed
8999            .args
9000            .values()
9001            .next()
9002            .expect("expected the word after the separator to reach the argument");
9003        assert_eq!(rest.to_string(), "-x");
9004    }
9005
9006    #[test]
9007    fn test_variadic_allow_hyphen_values_collects_after_a_hyphenated_first_value() {
9008        // Which token supplied the first value says nothing about how many the argument
9009        // takes, so collection carries on from a hyphenated one exactly as from a plain
9010        // one. It still stops at the next flag-like token, which is what keeps a second
9011        // occurrence of the flag from being eaten as a value.
9012        let spec = r#"
9013flag "-a --args <ARGS>..." allow_hyphen_values=#true
9014"#
9015        .parse::<Spec>()
9016        .unwrap();
9017
9018        let parsed = parse(&spec, &input(&["test", "-a", "-x", "b", "c"])).unwrap();
9019
9020        let flag = parsed
9021            .flags
9022            .keys()
9023            .find(|flag| flag.name == "args")
9024            .expect("expected args flag");
9025        match parsed.flags.get(flag).expect("expected args value") {
9026            ParseValue::MultiString(values) => assert_eq!(values, &["-x", "b", "c"]),
9027            other => panic!("expected a list of values, got {other:?}"),
9028        }
9029    }
9030
9031    #[test]
9032    fn test_variadic_allow_hyphen_values_consumes_repeated_flag_values() {
9033        let spec = r#"
9034flag "-a --args <ARGS>" var=#true allow_hyphen_values=#true
9035"#
9036        .parse::<Spec>()
9037        .unwrap();
9038
9039        let parsed = parse(&spec, &input(&["test", "-a", "-val1", "-a", "-val2"])).unwrap();
9040
9041        let flag = parsed
9042            .flags
9043            .keys()
9044            .find(|flag| flag.name == "args")
9045            .expect("expected args flag");
9046        let value = parsed.flags.get(flag).expect("expected args value");
9047        match value {
9048            ParseValue::MultiString(values) => {
9049                assert_eq!(values, &vec!["-val1".to_string(), "-val2".to_string()]);
9050            }
9051            _ => panic!("expected MultiString, got {value:?}"),
9052        }
9053    }
9054
9055    #[test]
9056    fn test_require_equals_accepts_attached_and_refuses_detached() {
9057        let spec = r#"
9058flag "--inspect <PORT>" require_equals=#true
9059"#
9060        .parse::<Spec>()
9061        .unwrap();
9062
9063        let parsed = parse(&spec, &input(&["test", "--inspect=9229"])).unwrap();
9064        assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
9065
9066        let err = parse(&spec, &input(&["test", "--inspect", "9229"])).unwrap_err();
9067        let msg = format!("{err}");
9068        assert!(
9069            msg.contains("requires an argument") || msg.contains("inspect"),
9070            "detached value must be refused: {msg}"
9071        );
9072    }
9073
9074    #[test]
9075    fn boolean_flags_can_accept_attached_values_when_enabled() {
9076        let spec: Spec = r#"
9077name "ex"
9078bin "ex"
9079flag "--color" negate="--no-color" bool_value=#true
9080arg "[rest]"
9081"#
9082        .parse()
9083        .unwrap();
9084
9085        for (token, expected) in [
9086            ("--color", true),
9087            ("--color=true", true),
9088            ("--color=false", false),
9089            ("--no-color", false),
9090            ("--no-color=false", true),
9091        ] {
9092            let parsed = parse(&spec, &input(&["ex", token])).unwrap();
9093            assert!(
9094                matches!(
9095                    parsed.flags.get(&spec.cmd.flags[0]),
9096                    Some(ParseValue::Bool(value)) if *value == expected
9097                ),
9098                "{token}"
9099            );
9100        }
9101
9102        let parsed = parse(&spec, &input(&["ex", "--color=false", "word"])).unwrap();
9103        assert!(matches!(
9104            parsed.args.get(&spec.cmd.args[0]),
9105            Some(ParseValue::String(value)) if value == "word"
9106        ));
9107        let err = parse(&spec, &input(&["ex", "--color=maybe"])).unwrap_err();
9108        assert!(err.to_string().contains("expected `true` or `false`"));
9109
9110        let strict: Spec = r#"
9111name "ex"
9112bin "ex"
9113args_override_self #false
9114flag "--color" negate="--no-color" bool_value=#true
9115"#
9116        .parse()
9117        .unwrap();
9118        assert!(parse(&strict, &input(&["ex", "--color=false", "--color=true"])).is_err());
9119        let parsed = parse(
9120            &strict,
9121            &input(&["ex", "--color=false", "--no-color=false"]),
9122        )
9123        .unwrap();
9124        assert!(matches!(
9125            parsed.flags.get(&strict.cmd.flags[0]),
9126            Some(ParseValue::Bool(true))
9127        ));
9128    }
9129
9130    #[test]
9131    fn test_require_equals_refuses_a_detached_value_after_a_short_bundle() {
9132        let spec = r#"
9133flag "-a --all"
9134flag "-i --inspect <PORT>" require_equals=#true
9135"#
9136        .parse::<Spec>()
9137        .unwrap();
9138
9139        let err = parse(&spec, &input(&["test", "-ai", "9229"])).unwrap_err();
9140        let msg = format!("{err}");
9141        assert!(
9142            msg.contains("requires an argument") || msg.contains("inspect"),
9143            "bundled short must refuse the following word: {msg}"
9144        );
9145    }
9146
9147    #[test]
9148    fn test_default_missing_binds_when_the_value_is_left_off() {
9149        let spec = r#"
9150flag "-c --color <WHEN>" default_missing="always"
9151flag "-v --verbose"
9152"#
9153        .parse::<Spec>()
9154        .unwrap();
9155
9156        let parsed = parse(&spec, &input(&["test", "--color"])).unwrap();
9157        assert_eq!(flag_string_value(&parsed, "color"), "always");
9158
9159        let parsed = parse(&spec, &input(&["test", "--color=never"])).unwrap();
9160        assert_eq!(flag_string_value(&parsed, "color"), "never");
9161
9162        let parsed = parse(&spec, &input(&["test", "--color", "never"])).unwrap();
9163        assert_eq!(flag_string_value(&parsed, "color"), "never");
9164
9165        let parsed = parse(&spec, &input(&["test", "--color", "--verbose"])).unwrap();
9166        assert_eq!(flag_string_value(&parsed, "color"), "always");
9167        assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
9168
9169        let parsed = parse(&spec, &input(&["test", "--color="])).unwrap();
9170        assert_eq!(flag_string_value(&parsed, "color"), "");
9171
9172        let parsed = parse(&spec, &input(&["test", "-cnever"])).unwrap();
9173        assert_eq!(flag_string_value(&parsed, "color"), "never");
9174
9175        let parsed = parse(&spec, &input(&["test", "-c", "-v"])).unwrap();
9176        assert_eq!(flag_string_value(&parsed, "color"), "always");
9177        assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
9178    }
9179
9180    #[test]
9181    fn test_optional_flag_value_preserves_bare_and_explicit_empty_forms() {
9182        let spec = r#"
9183flag "--bump [LEVEL]" value_optional=#true
9184flag "--verbose"
9185arg "[FILE]"
9186"#
9187        .parse::<Spec>()
9188        .unwrap();
9189
9190        let absent = parse(&spec, &input(&["test"])).unwrap();
9191        assert!(!absent.flags.keys().any(|flag| flag.name == "bump"));
9192
9193        let bare = parse(&spec, &input(&["test", "--bump", "--verbose", "file.txt"])).unwrap();
9194        let bump = bare
9195            .flags
9196            .iter()
9197            .find(|(flag, _)| flag.name == "bump")
9198            .map(|(_, value)| value)
9199            .unwrap();
9200        assert!(matches!(bump, ParseValue::MultiString(values) if values.is_empty()));
9201        assert!(bare.flags.keys().any(|flag| flag.name == "verbose"));
9202        assert_eq!(arg_value(&bare, "FILE"), "file.txt");
9203
9204        let explicit = parse(&spec, &input(&["test", "--bump=", "file.txt"])).unwrap();
9205        assert_eq!(flag_string_value(&explicit, "bump"), "");
9206
9207        let corrected = parse(
9208            &spec,
9209            &input(&["test", "--bump=2", "--bump", "--verbose", "file.txt"]),
9210        )
9211        .unwrap();
9212        let bump = corrected
9213            .flags
9214            .iter()
9215            .find(|(flag, _)| flag.name == "bump")
9216            .map(|(_, value)| value)
9217            .unwrap();
9218        assert!(matches!(bump, ParseValue::MultiString(values) if values.is_empty()));
9219
9220        let collecting = r#"
9221flag "--tag [TAG]..." value_optional=#true
9222flag "--verbose"
9223"#
9224        .parse::<Spec>()
9225        .unwrap();
9226        let valued = parse(
9227            &collecting,
9228            &input(&["test", "--tag", "one", "two", "--verbose"]),
9229        )
9230        .unwrap();
9231        let tag = valued
9232            .flags
9233            .iter()
9234            .find(|(flag, _)| flag.name == "tag")
9235            .map(|(_, value)| value)
9236            .unwrap();
9237        assert!(matches!(tag, ParseValue::MultiString(values) if values == &["one", "two"]));
9238    }
9239
9240    #[test]
9241    fn test_repeatable_bare_optional_values_count_each_occurrence() {
9242        let spec = r#"
9243flag "--tag [TAG]" var=#true var_min=2 var_max=2 value_optional=#true
9244"#
9245        .parse::<Spec>()
9246        .unwrap();
9247
9248        let parsed = parse(&spec, &input(&["test", "--tag", "--tag"])).unwrap();
9249        let tag = parsed
9250            .flags
9251            .iter()
9252            .find(|(flag, _)| flag.name == "tag")
9253            .map(|(_, value)| value)
9254            .unwrap();
9255        assert!(matches!(tag, ParseValue::MultiString(values) if values == &["", ""]));
9256
9257        assert!(parse(&spec, &input(&["test", "--tag"])).is_err());
9258        assert!(parse(&spec, &input(&["test", "--tag", "--tag", "--tag"])).is_err());
9259    }
9260
9261    #[test]
9262    fn test_repeatable_variadic_optional_values_do_not_gain_bare_occurrences() {
9263        let spec = r#"
9264flag "--tag [TAG]..." var=#true value_optional=#true
9265flag "--verbose"
9266"#
9267        .parse::<Spec>()
9268        .unwrap();
9269
9270        for argv in [
9271            &["test", "--tag", "one", "two"][..],
9272            &["test", "--tag", "one", "two", "--verbose"][..],
9273            &["test", "--tag", "one", "--tag", "two"][..],
9274        ] {
9275            let parsed = parse(&spec, &input(argv)).unwrap();
9276            let tag = parsed
9277                .flags
9278                .iter()
9279                .find(|(flag, _)| flag.name == "tag")
9280                .map(|(_, value)| value)
9281                .unwrap();
9282            assert!(
9283                matches!(tag, ParseValue::MultiString(values) if values == &["one", "two"]),
9284                "argv={argv:?}: {tag:?}"
9285            );
9286        }
9287
9288        let bare = parse(&spec, &input(&["test", "--tag", "--verbose"])).unwrap();
9289        let tag = bare
9290            .flags
9291            .iter()
9292            .find(|(flag, _)| flag.name == "tag")
9293            .map(|(_, value)| value)
9294            .unwrap();
9295        assert!(matches!(tag, ParseValue::MultiString(values) if values == &[""]));
9296    }
9297
9298    #[test]
9299    fn test_default_missing_with_require_equals_refuses_the_following_word() {
9300        let spec = r#"
9301flag "--inspect <PORT>" require_equals=#true default_missing="9229"
9302arg "[rest]"
9303"#
9304        .parse::<Spec>()
9305        .unwrap();
9306
9307        let parsed = parse(&spec, &input(&["test", "--inspect"])).unwrap();
9308        assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
9309
9310        let parsed = parse(&spec, &input(&["test", "--inspect=1234"])).unwrap();
9311        assert_eq!(flag_string_value(&parsed, "inspect"), "1234");
9312
9313        // The following word is not the value; the missing value is, and 80 is a positional.
9314        let parsed = parse(&spec, &input(&["test", "--inspect", "80"])).unwrap();
9315        assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
9316        assert_eq!(
9317            parsed
9318                .args
9319                .values()
9320                .next()
9321                .map(|v| v.to_string())
9322                .as_deref(),
9323            Some("80")
9324        );
9325
9326        let parsed = parse(&spec, &input(&["test", "--inspect="])).unwrap();
9327        assert_eq!(flag_string_value(&parsed, "inspect"), "");
9328    }
9329
9330    #[test]
9331    fn test_default_missing_must_be_a_choice() {
9332        let spec = r#"
9333flag "--color <WHEN>" default_missing="always" {
9334    choices "auto" "always" "never"
9335}
9336"#
9337        .parse::<Spec>()
9338        .unwrap();
9339
9340        let parsed = parse(&spec, &input(&["test", "--color"])).unwrap();
9341        assert_eq!(flag_string_value(&parsed, "color"), "always");
9342
9343        let parsed = parse(&spec, &input(&["test", "--color=never"])).unwrap();
9344        assert_eq!(flag_string_value(&parsed, "color"), "never");
9345
9346        let spec = r#"
9347flag "--color <WHEN>" default_missing="wat" {
9348    choices "auto" "always" "never"
9349}
9350"#
9351        .parse::<Spec>()
9352        .unwrap();
9353
9354        let err = parse(&spec, &input(&["test", "--color"])).unwrap_err();
9355        let msg = format!("{err}");
9356        assert!(
9357            msg.contains("Invalid choice for option color: wat"),
9358            "missing default has to pass choices the same way a typed value does: {msg}"
9359        );
9360
9361        let err = parse(&spec, &input(&["test", "--color=wat"])).unwrap_err();
9362        let msg = format!("{err}");
9363        assert!(
9364            msg.contains("Invalid choice for option color: wat"),
9365            "an attached value that is not a choice is still refused: {msg}"
9366        );
9367
9368        let spec = r#"
9369flag "--inspect <PORT>" require_equals=#true default_missing="wat" {
9370    choices "9229" "80"
9371}
9372arg "[rest]"
9373"#
9374        .parse::<Spec>()
9375        .unwrap();
9376
9377        let err = parse(&spec, &input(&["test", "--inspect", "80"])).unwrap_err();
9378        let msg = format!("{err}");
9379        assert!(
9380            msg.contains("Invalid choice for option inspect: wat"),
9381            "require_equals still binds the missing string, so the error is the choice: {msg}"
9382        );
9383    }
9384
9385    #[test]
9386    fn test_hyphen_values_still_default_to_short_flag_parsing() {
9387        let spec = r#"
9388flag "-d --working-dir <DIR>"
9389flag "-a --args <ARGS>"
9390"#
9391        .parse::<Spec>()
9392        .unwrap();
9393
9394        let parsed = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap();
9395
9396        assert_eq!(flag_string_value(&parsed, "working-dir"), "estroy");
9397    }
9398
9399    /// `available_flags` has to agree with what an actual parse accepts, since
9400    /// its whole reason to exist is answering that question without one.
9401    mod available_flags {
9402        use super::*;
9403
9404        fn spec() -> Spec {
9405            r#"
9406bin "test"
9407flag "-v --verbose" global=#true
9408flag "--raw" global=#true effect="write"
9409flag "--local-only"
9410cmd "run" {
9411    flag "-r --raw"
9412    flag "-w --watch"
9413    cmd "once"
9414}
9415"#
9416            .parse::<Spec>()
9417            .unwrap()
9418        }
9419
9420        fn chain<'a>(spec: &'a Spec, path: &[&str]) -> Vec<&'a SpecCommand> {
9421            let mut chain = vec![&spec.cmd];
9422            for segment in path {
9423                chain.push(chain.last().unwrap().find_subcommand(segment).unwrap());
9424            }
9425            chain
9426        }
9427
9428        fn names(spec: &Spec, path: &[&str]) -> Vec<String> {
9429            let mut names: Vec<_> = available_flags(&chain(spec, path))
9430                .iter()
9431                .map(|f| f.name.clone())
9432                .collect();
9433            names.sort();
9434            names
9435        }
9436
9437        #[test]
9438        fn an_empty_chain_yields_nothing() {
9439            assert!(available_flags(&[]).is_empty());
9440        }
9441
9442        #[test]
9443        fn the_root_gets_its_own_flags() {
9444            let spec = spec();
9445            assert_eq!(names(&spec, &[]), ["local-only", "raw", "verbose"]);
9446        }
9447
9448        #[test]
9449        fn a_subcommand_keeps_globals_and_drops_local_only_ancestors() {
9450            let spec = spec();
9451            assert_eq!(names(&spec, &["run"]), ["raw", "verbose", "watch"]);
9452        }
9453
9454        #[test]
9455        fn a_re_declared_global_is_listed_once() {
9456            // The merge can leave the long key on the merged flag and the short
9457            // key on the pre-merge one. Same flag; it must not be listed twice.
9458            let spec = r#"
9459bin "test"
9460flag "-y --yes" global=#true effect="write"
9461cmd "rm" {
9462    flag "-y --yes"
9463}
9464"#
9465            .parse::<Spec>()
9466            .unwrap();
9467            let flags = available_flags(&chain(&spec, &["rm"]));
9468            assert_eq!(flags.len(), 1, "{flags:?}");
9469            assert_eq!(flags[0].effect.map(|e| e.as_str()), Some("write"));
9470        }
9471
9472        #[test]
9473        fn a_re_declared_global_keeps_the_globals_declaration() {
9474            // `run` re-declares the long-only global `--raw` as `-r --raw`
9475            // without `global`. That is the same flag: the global's `effect`
9476            // survives, the orphan short is unioned in, and it stays global.
9477            let spec = spec();
9478            let flags = available_flags(&chain(&spec, &["run"]));
9479            let raw = flags.iter().find(|f| f.name == "raw").unwrap();
9480            assert!(raw.global);
9481            assert_eq!(raw.effect.map(|e| e.as_str()), Some("write"));
9482            assert_eq!(raw.short, ['r']);
9483        }
9484
9485        #[test]
9486        fn it_matches_what_a_parse_accepts() {
9487            // The invariant. If these ever disagree, one of them is lying to a
9488            // caller about which flags a command takes.
9489            let spec = spec();
9490            for path in [vec![], vec!["run"], vec!["run", "once"]] {
9491                let argv = std::iter::once("test".to_string())
9492                    .chain(path.iter().map(|s| s.to_string()))
9493                    .collect::<Vec<_>>();
9494                let parsed = parse_partial(&spec, &argv).unwrap();
9495
9496                let mut from_parse: Vec<_> = unique_flags(parsed.available_flags.values())
9497                    .map(|f| f.name.clone())
9498                    .collect();
9499                from_parse.sort();
9500                assert_eq!(names(&spec, &path), from_parse, "path {path:?}");
9501            }
9502        }
9503    }
9504
9505    // Provenance: which token bound what, and where a value came from when no token did.
9506
9507    /// Every role a token was given, rendered the way `Debug` renders it, so a test can
9508    /// assert on the whole picture rather than on one field at a time.
9509    fn roles(parsed: &ParseOutput, index: usize) -> Vec<String> {
9510        parsed
9511            .tokens
9512            .iter()
9513            .find(|token| token.index == index)
9514            .unwrap_or_else(|| panic!("no token at {index}"))
9515            .roles
9516            .iter()
9517            .map(render_role)
9518            .collect()
9519    }
9520
9521    fn origins(parsed: &ParseOutput, flag: &str) -> Vec<ValueOrigin> {
9522        parsed
9523            .flag_origins
9524            .iter()
9525            .find(|(f, _)| f.name == flag)
9526            .map(|(_, origins)| origins.clone())
9527            .unwrap_or_default()
9528    }
9529
9530    fn explain_with_env(spec: &Spec, words: &[&str], env: &[(&str, &str)]) -> ParseOutput {
9531        let env = env
9532            .iter()
9533            .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
9534            .collect();
9535        Parser::new(spec)
9536            .with_env(env)
9537            .explain(&input(words))
9538            .unwrap()
9539    }
9540
9541    fn explain(spec: &Spec, words: &[&str]) -> ParseOutput {
9542        explain_with_env(spec, words, &[])
9543    }
9544
9545    #[test]
9546    fn an_attached_long_value_is_recorded_on_the_flag_token() {
9547        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\n"
9548            .parse()
9549            .unwrap();
9550
9551        let parsed = explain(&spec, &["ex", "--env=prod"]);
9552
9553        assert_eq!(roles(&parsed, 0), ["program"]);
9554        assert_eq!(
9555            roles(&parsed, 1),
9556            ["flag env as --env", "value of env = [\"prod\"], attached"]
9557        );
9558        // This is jdx/mise discussion #8883: a hand-written scanner dropped the attached
9559        // form while the detached one worked, and nothing could show the difference.
9560        assert!(origins(&parsed, "env").is_empty(), "typed, so no fallback");
9561    }
9562
9563    #[test]
9564    fn a_detached_long_value_is_recorded_on_its_own_token() {
9565        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\n"
9566            .parse()
9567            .unwrap();
9568
9569        let parsed = explain(&spec, &["ex", "--env", "prod"]);
9570
9571        assert_eq!(roles(&parsed, 1), ["flag env as --env"]);
9572        assert_eq!(roles(&parsed, 2), ["value of env = [\"prod\"]"]);
9573    }
9574
9575    #[test]
9576    fn a_short_bundle_is_attributed_to_the_bundle_token() {
9577        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-a\"\nflag \"-b\"\nflag \"-j <n>\"\n"
9578            .parse()
9579            .unwrap();
9580
9581        let parsed = explain(&spec, &["ex", "-abj8"]);
9582
9583        // One word the caller wrote, four things it did — and the re-queued tails are
9584        // folded back onto it rather than appearing as tokens nobody typed.
9585        assert_eq!(
9586            roles(&parsed, 1),
9587            [
9588                "flag a as -a",
9589                "flag b as -b",
9590                "flag j as -j",
9591                "value of j = [\"8\"], attached",
9592            ]
9593        );
9594        assert_eq!(parsed.tokens.len(), 2);
9595    }
9596
9597    #[test]
9598    fn a_delimiter_splits_one_token_into_several_values() {
9599        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--tags <tags>...\" delimiter=\",\"\n"
9600            .parse()
9601            .unwrap();
9602
9603        let parsed = explain(&spec, &["ex", "--tags", "a,b,c"]);
9604
9605        assert_eq!(
9606            roles(&parsed, 2),
9607            ["value of tags = [\"a\", \"b\", \"c\"]"],
9608            "the values meant, not the word typed"
9609        );
9610    }
9611
9612    #[test]
9613    fn a_separator_and_the_words_after_it_are_distinguished() {
9614        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"<src>\"\narg \"[raw]...\"\n"
9615            .parse()
9616            .unwrap();
9617
9618        let parsed = explain(&spec, &["ex", "a", "--", "-x"]);
9619
9620        assert_eq!(roles(&parsed, 1), ["arg src = [\"a\"]"]);
9621        assert_eq!(roles(&parsed, 2), ["separator"]);
9622        // Past the separator `-x` is data, not an unknown flag.
9623        assert_eq!(roles(&parsed, 3), ["arg raw = [\"-x\"]"]);
9624    }
9625
9626    #[test]
9627    fn a_second_separator_is_data() {
9628        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[raw]...\"\n"
9629            .parse()
9630            .unwrap();
9631
9632        let parsed = explain(&spec, &["ex", "--", "a", "--", "b"]);
9633
9634        assert_eq!(roles(&parsed, 1), ["separator"]);
9635        assert_eq!(roles(&parsed, 3), ["arg raw = [\"--\"]"]);
9636    }
9637
9638    #[test]
9639    fn an_unknown_flag_says_what_took_it() {
9640        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[rest]...\"\n"
9641            .parse()
9642            .unwrap();
9643
9644        let parsed = explain(&spec, &["ex", "--wat"]);
9645
9646        // The default is lax, so the word became data. Which is the useful thing to be
9647        // told: the alternative reading is "you have a typo".
9648        assert_eq!(roles(&parsed, 1), ["unknown flag, bound as rest"]);
9649    }
9650
9651    #[test]
9652    fn a_subcommand_word_is_not_a_positional() {
9653        let spec: Spec = "name \"ex\"\nbin \"ex\"\ncmd \"build\" {\n    arg \"<target>\"\n}\n"
9654            .parse()
9655            .unwrap();
9656
9657        let parsed = explain(&spec, &["ex", "build", "a"]);
9658
9659        assert_eq!(roles(&parsed, 1), ["subcommand build"]);
9660        assert_eq!(roles(&parsed, 2), ["arg target = [\"a\"]"]);
9661    }
9662
9663    #[test]
9664    fn a_multicall_applet_is_read_at_argv0() {
9665        let spec: Spec =
9666            "name \"box\"\nbin \"box\"\nmulticall #true\ncmd \"ls\" {\n    flag \"-l\"\n}\n"
9667                .parse()
9668                .unwrap();
9669
9670        let parsed = explain(&spec, &["/usr/bin/ls", "-l"]);
9671
9672        // argv[0] is both the program and the word that selected the applet, and the word
9673        // read there is not the word the caller wrote.
9674        assert_eq!(roles(&parsed, 0), ["program", "subcommand ls"]);
9675        assert!(parsed.tokens[0].synthesized);
9676        assert_eq!(parsed.tokens[0].word, "/usr/bin/ls");
9677    }
9678
9679    #[test]
9680    fn words_the_parse_never_reached_say_so() {
9681        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[rest]...\"\n"
9682            .parse()
9683            .unwrap();
9684
9685        let parsed = Parser::new(&spec)
9686            .explain(&input(&["ex", "--help", "a"]))
9687            .unwrap();
9688
9689        assert_eq!(roles(&parsed, 2), ["unread"]);
9690    }
9691
9692    #[test]
9693    fn an_env_origin_names_the_variable_that_fired() {
9694        let spec: Spec =
9695            "name \"ex\"\nbin \"ex\"\nflag \"--token <t>\" env=\"EX_TOKEN\" env_fallback=\"EX_TOKEN_OLD\"\n"
9696                .parse()
9697                .unwrap();
9698
9699        let primary = explain_with_env(&spec, &["ex"], &[("EX_TOKEN", "a")]);
9700        assert_eq!(
9701            origins(&primary, "token"),
9702            [ValueOrigin::Env("EX_TOKEN".to_string())]
9703        );
9704
9705        // The fallback firing is a different fact from the primary firing, and which one it
9706        // was is what says which declaration to delete.
9707        let fallback = explain_with_env(&spec, &["ex"], &[("EX_TOKEN_OLD", "b")]);
9708        assert_eq!(
9709            origins(&fallback, "token"),
9710            [ValueOrigin::Env("EX_TOKEN_OLD".to_string())]
9711        );
9712    }
9713
9714    #[test]
9715    fn a_default_origin_is_recorded_for_flags_and_args() {
9716        let spec: Spec =
9717            "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" default=\"auto\"\narg \"[src]\" default=\".\"\n"
9718                .parse()
9719                .unwrap();
9720
9721        let parsed = explain(&spec, &["ex"]);
9722
9723        assert_eq!(origins(&parsed, "color"), [ValueOrigin::Default]);
9724        let (arg, origins) = parsed.arg_origins.iter().next().unwrap();
9725        assert_eq!(arg.name, "src");
9726        assert_eq!(origins, &[ValueOrigin::Default]);
9727    }
9728
9729    #[test]
9730    fn a_default_if_origin_carries_the_condition_that_fired() {
9731        let spec: Spec = r#"
9732name "ex"
9733bin "ex"
9734flag "--profile <p>"
9735flag "--strict" {
9736    default_if "--profile" "prod" "true"
9737}
9738        "#
9739        .parse()
9740        .unwrap();
9741
9742        let parsed = explain(&spec, &["ex", "--profile", "prod"]);
9743
9744        // The selector alone is ambiguous: several conditions may name it with different
9745        // `when` values, so the report has to say which one matched.
9746        assert_eq!(
9747            origins(&parsed, "strict"),
9748            [ValueOrigin::DefaultIf {
9749                selector: "--profile".to_string(),
9750                when: Some("prod".to_string()),
9751            }]
9752        );
9753    }
9754
9755    #[test]
9756    fn a_bare_optional_value_flag_records_default_missing() {
9757        let spec: Spec =
9758            "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" default_missing=\"always\"\nflag \"-v\"\n"
9759                .parse()
9760                .unwrap();
9761
9762        let parsed = explain(&spec, &["ex", "--color", "-v"]);
9763
9764        // The flag was typed and the value was not, which is the distinction a spec author
9765        // is asking about when they ask why `--color` came out `always`.
9766        assert_eq!(roles(&parsed, 1), ["flag color as --color"]);
9767        assert_eq!(origins(&parsed, "color"), [ValueOrigin::DefaultMissing]);
9768        assert_eq!(roles(&parsed, 2), ["flag v as -v"]);
9769    }
9770
9771    #[test]
9772    fn a_var_flag_can_take_one_value_from_argv_and_one_from_default_missing() {
9773        let spec: Spec =
9774            "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" var=#true default_missing=\"always\"\n"
9775                .parse()
9776                .unwrap();
9777
9778        let parsed = explain(&spec, &["ex", "--color=red", "--color"]);
9779
9780        // Why origins are a list: one declaration, two occurrences, two different answers.
9781        assert_eq!(
9782            roles(&parsed, 1),
9783            [
9784                "flag color as --color",
9785                "value of color = [\"red\"], attached"
9786            ]
9787        );
9788        assert_eq!(origins(&parsed, "color"), [ValueOrigin::DefaultMissing]);
9789    }
9790
9791    #[test]
9792    fn an_override_names_the_flag_that_did_it() {
9793        let spec: Spec =
9794            "name \"ex\"\nbin \"ex\"\nflag \"--quiet\" default=\"true\"\nflag \"--loud\" overrides=\"--quiet\"\n"
9795                .parse()
9796                .unwrap();
9797
9798        let parsed = explain(&spec, &["ex", "--loud"]);
9799
9800        // Without the overriding name, "`--quiet` is unset despite its default" has no
9801        // answer: the fallback phase silently declines to fill an overridden flag.
9802        assert_eq!(parsed.overridden_flags.get("quiet").unwrap(), "loud");
9803        assert!(origins(&parsed, "quiet").is_empty());
9804    }
9805
9806    #[test]
9807    fn a_restart_token_leaves_the_tokens_and_clears_the_arg_origins() {
9808        let spec: Spec = r#"
9809name "ex"
9810bin "ex"
9811cmd "run" restart_token=":::" {
9812    arg "<task>" default="build"
9813}
9814        "#
9815        .parse()
9816        .unwrap();
9817
9818        let parsed = explain(&spec, &["ex", "run", "lint", ":::", "test"]);
9819
9820        // The values belong to the last invocation, so provenance must too — but the words
9821        // of the first were still read, and a report that dropped them would show a command
9822        // line with a hole in it.
9823        assert_eq!(roles(&parsed, 2), ["arg task = [\"lint\"]"]);
9824        // And the token that did the resetting says so: without a role of its own it reads
9825        // as a word that did nothing, next to a `lint` that filled an arg now empty.
9826        assert_eq!(roles(&parsed, 3), ["restart"]);
9827        assert_eq!(roles(&parsed, 4), ["arg task = [\"test\"]"]);
9828        assert!(parsed.arg_origins.is_empty());
9829    }
9830
9831    #[test]
9832    fn a_value_terminator_says_which_run_it_ended() {
9833        let spec: Spec = r#"
9834name "ex"
9835bin "ex"
9836flag "--exec <cmd>..." value_terminator=";"
9837arg "<src>"
9838        "#
9839        .parse()
9840        .unwrap();
9841
9842        let parsed = explain(&spec, &["ex", "--exec", "rm", "tmp", ";", "a"]);
9843
9844        assert_eq!(roles(&parsed, 3), ["value of exec = [\"tmp\"]"]);
9845        // The terminator is consumed and is not one of the values, which is the whole reason
9846        // it was declared — so it needs a row saying that rather than an empty one.
9847        assert_eq!(roles(&parsed, 4), ["value terminator, ends exec"]);
9848        assert_eq!(roles(&parsed, 5), ["arg src = [\"a\"]"]);
9849    }
9850
9851    #[test]
9852    fn an_args_value_terminator_says_which_run_it_ended() {
9853        let spec: Spec = r#"
9854name "ex"
9855bin "ex"
9856arg "<files>..." value_terminator=";"
9857arg "[dest]"
9858        "#
9859        .parse()
9860        .unwrap();
9861
9862        let parsed = explain(&spec, &["ex", "a", "b", ";", "out"]);
9863
9864        assert_eq!(roles(&parsed, 2), ["arg files = [\"b\"]"]);
9865        assert_eq!(roles(&parsed, 3), ["value terminator, ends files"]);
9866        assert_eq!(roles(&parsed, 4), ["arg dest = [\"out\"]"]);
9867    }
9868
9869    #[test]
9870    fn explain_keeps_the_bindings_of_a_command_line_that_fails() {
9871        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\narg \"<src>\"\n"
9872            .parse()
9873            .unwrap();
9874
9875        let parsed = Parser::new(&spec)
9876            .explain(&input(&["ex", "--env=prod"]))
9877            .unwrap();
9878
9879        // `parse` reports "missing required <src>" and nothing else, which is the report the
9880        // caller already had. This is the case the whole thing exists for.
9881        assert!(Parser::new(&spec)
9882            .parse(&input(&["ex", "--env=prod"]))
9883            .is_err());
9884        assert_eq!(
9885            roles(&parsed, 1),
9886            ["flag env as --env", "value of env = [\"prod\"], attached"]
9887        );
9888        assert!(
9889            parsed.errors.iter().any(|e| e.to_string().contains("src")),
9890            "{:?}",
9891            parsed.errors
9892        );
9893    }
9894
9895    #[test]
9896    fn an_external_subcommand_forwards_whole_tokens() {
9897        let spec: Spec = "name \"ex\"\nbin \"ex\"\nexternal_subcommand #true\ncmd \"build\"\n"
9898            .parse()
9899            .unwrap();
9900
9901        let parsed = explain(&spec, &["ex", "deploy", "--now"]);
9902
9903        assert_eq!(roles(&parsed, 1), ["external"]);
9904        assert_eq!(roles(&parsed, 2), ["external"]);
9905    }
9906
9907    #[test]
9908    fn a_view_keeps_the_callers_argv_positions() {
9909        let spec: Spec = r#"
9910bin "ex"
9911view "runner" root="run"
9912cmd "run" {
9913    flag "--token <token>"
9914}
9915        "#
9916        .parse()
9917        .unwrap();
9918
9919        let parsed = explain(&spec, &["runner", "--token", "secret"]);
9920
9921        // A view re-enters the parse with the same argv, so the positions still mean what
9922        // the caller wrote.
9923        assert_eq!(roles(&parsed, 0), ["program"]);
9924        assert_eq!(roles(&parsed, 2), ["value of token = [\"secret\"]"]);
9925    }
9926}