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        .chain(cmd.clause.iter().flat_map(|clause| &clause.flags))
197        .flat_map(|f| {
198            let f = Arc::new(f.clone()); // One clone per flag, then cheap Arc refs
199            flag_keys(&f)
200                .into_iter()
201                .map(|key| (key, Arc::clone(&f)))
202                .collect::<Vec<_>>()
203        })
204        .collect()
205}
206
207fn unique_flags<'a>(
208    flags: impl IntoIterator<Item = &'a Arc<SpecFlag>>,
209) -> impl Iterator<Item = &'a Arc<SpecFlag>> {
210    let mut seen = HashSet::new();
211    flags
212        .into_iter()
213        .filter(move |flag| seen.insert(Arc::as_ptr(flag) as usize))
214}
215
216/// Every flag a command accepts, resolved the way parsing an invocation of it
217/// resolves them.
218///
219/// `chain` runs from the root command (`spec.cmd`) down to the command in
220/// question; an empty chain yields no flags.
221///
222/// This is not "the command's flags plus its ancestors' globals". A subcommand
223/// that re-declares a global's long name is describing the *same* flag rather
224/// than a new one, so the global's help, argument and effect survive and only
225/// the re-declaration's extra aliases are added — see
226/// [`merge_subcommand_flags`]. Anything that reports a command's flags without
227/// going through this will disagree with what the parser actually accepts.
228pub fn available_flags(chain: &[&SpecCommand]) -> Vec<Arc<SpecFlag>> {
229    let Some((root, rest)) = chain.split_first() else {
230        return vec![];
231    };
232    let mut available = gather_flags(root);
233    for cmd in rest {
234        merge_subcommand_flags(&mut available, gather_flags(cmd), false);
235    }
236
237    // Deduplicating by `Arc` identity is not enough. When a child re-declares a
238    // global that has both a short and a long, the merged flag is written under
239    // the long key while the short key keeps pointing at the pre-merge `Arc` —
240    // two objects for one logical flag. That is harmless for parsing, which
241    // looks flags up by key, but a caller listing flags would see it twice.
242    //
243    // Names break the tie because a long key always sorts before a short one
244    // (`--x` < `-y` at the second byte), so the merged declaration is the one
245    // reached first. Two genuinely distinct flags sharing a name is a spec bug
246    // that `usage lint` reports as a duplicate flag.
247    let mut seen_names = HashSet::new();
248    unique_flags(available.values())
249        .filter(|f| seen_names.insert(f.name.clone()))
250        .cloned()
251        .collect()
252}
253
254/// Extract the flag key from a flag word for lookup in available_flags map
255/// Handles both long flags (--flag, --flag=value) and short flags (-f)
256fn get_flag_key(word: &str) -> &str {
257    if word.starts_with("--") {
258        // Long flag: strip =value if present
259        word.split_once('=').map(|(k, _)| k).unwrap_or(word)
260    } else if let Some((end, _)) = word.char_indices().nth(2) {
261        // Short flag: the dash and one letter, which is one character and not
262        // necessarily one byte.
263        &word[0..end]
264    } else {
265        word
266    }
267}
268
269/// Where a value came from, when it did not come from the command line.
270///
271/// About the *value*, not the flag. `--color` typed bare with `default_missing` has a
272/// token for the flag and none for the value, and that distinction is the whole question
273/// a spec author is asking when they ask why `--color` came out `always`. Values that were
274/// typed are attributed to the token that carried them instead — see [`TokenRole::Value`].
275#[derive(Debug, Clone, PartialEq, Eq)]
276#[non_exhaustive]
277pub enum ValueOrigin {
278    /// A flag that takes a value was given without one, so the declaration supplied it:
279    /// `default_missing`, or the empty tri-state a bare `value_optional` flag records.
280    /// One variant for both, because from argv's side the same thing happened — the flag
281    /// was typed and the value was not.
282    DefaultMissing,
283    /// An environment variable, named.
284    ///
285    /// Named because a flag may list several — `env`, `env_fallback` and `deprecated_env`,
286    /// folded together by [`SpecFlag::env_names`] — and "it came from the environment" does
287    /// not say which declaration fired or which one to delete.
288    Env(String),
289    /// A declared `default`, on the flag or on the flag's argument.
290    ///
291    /// Not two variants: the precedence between them is a spec-authoring oddity rather than
292    /// a fact about the value, and `usage lint` is the place to complain about declaring
293    /// both.
294    Default,
295    /// A `default_if` whose condition matched, with the condition that decided it. The
296    /// selector alone is ambiguous — several conditions may name it with different `when`
297    /// values.
298    DefaultIf {
299        selector: String,
300        when: Option<String>,
301    },
302}
303
304/// What one word of the command line became.
305///
306/// Several because a single token can do more than one thing: `-abc` sets three flags,
307/// `-j8` is a flag and its value.
308#[derive(Debug, Clone)]
309#[non_exhaustive]
310pub enum TokenRole {
311    /// argv[0]. Also a `Command` when a multicall symlink makes the basename a word.
312    Program,
313    /// Selected a subcommand.
314    Command { name: String },
315    /// Named a flag, in this spelling. `negated` for the `negate` form.
316    Flag {
317        flag: Arc<SpecFlag>,
318        spelling: String,
319        negated: bool,
320    },
321    /// Supplied a flag's value. Several values when a `delimiter` split the word.
322    Value {
323        flag: Arc<SpecFlag>,
324        values: Vec<String>,
325        /// Whether the value rode along on the flag's own token (`--env=prod`, `-j8`)
326        /// rather than following it as its own word.
327        attached: bool,
328    },
329    /// Filled a positional argument. Several values when a `delimiter` split the word.
330    Arg {
331        arg: Arc<SpecArg>,
332        values: Vec<String>,
333    },
334    /// An explicit `--`, consumed as a separator.
335    Separator,
336    /// A word the parser answers itself rather than binding: `--help`, `-h`, `--version`,
337    /// `-V`. The parse stops here and the answer travels as an error carrying the text, so
338    /// without a role the word reads as having done nothing while a whole help page arrives
339    /// in the error list.
340    Builtin { spelling: String },
341    /// A declared `value_terminator`, consumed to end a run of values. `ends` names the
342    /// declaration whose run it closed — the word is not one of that run's values, which is
343    /// the whole reason it was declared.
344    ValueTerminator { ends: String },
345    /// A declared `restart_token`: the positional cursor and the values it had filled start
346    /// over here. Recorded because the words before it are still in the report, and without
347    /// this row they look like they filled arguments that then came back empty.
348    Restart,
349    /// A flag-like word no declaration matched. `bound_as` is the positional that took it
350    /// under `unknown_flags="value"`, and `None` when the word was refused.
351    UnknownFlag { bound_as: Option<Arc<SpecArg>> },
352    /// The word reached a declaration that would not take it, and was dropped. Without this
353    /// the token reads as having done nothing, which is the one thing it did not do.
354    Refused { reason: String },
355    /// Forwarded to an external subcommand.
356    External,
357    /// The parser stopped before this word — a help request, a refused value.
358    Unread,
359    /// Filled a sigil-classified positional after removing its declared prefix.
360    Sigil {
361        arg: Arc<SpecArg>,
362        sigil: String,
363        values: Vec<String>,
364    },
365    /// Ended one instance of a repeatable clause and began the next.
366    ClauseSeparator { name: String },
367}
368
369/// One word of the command line, and what it became.
370#[derive(Debug, Clone)]
371#[non_exhaustive]
372pub struct TokenBinding {
373    /// Position in the argv slice the parse was given, argv[0] included.
374    pub index: usize,
375    pub word: String,
376    /// Roles a word the parser made up contributed, folded onto the token it was derived
377    /// from: the tail of a short bundle onto the bundle, a multicall applet name onto
378    /// argv[0]. `word` is what the caller wrote, not what the parser read.
379    pub synthesized: bool,
380    pub roles: Vec<TokenRole>,
381}
382
383#[non_exhaustive]
384pub struct ParseOutput {
385    pub cmd: SpecCommand,
386    pub cmds: Vec<SpecCommand>,
387    pub args: IndexMap<Arc<SpecArg>, ParseValue>,
388    /// Separator-delimited positional instances, keyed by clause name.
389    pub clauses: IndexMap<String, Vec<IndexMap<Arc<SpecArg>, ParseValue>>>,
390    /// Per-instance flags belonging to a repeatable clause, keyed by clause name.
391    pub clause_flags: IndexMap<String, Vec<IndexMap<Arc<SpecFlag>, ParseValue>>>,
392    pub flags: IndexMap<Arc<SpecFlag>, ParseValue>,
393    /// What each word of the command line became, in argv order, one entry per word.
394    ///
395    /// The token half of provenance; [`ParseOutput::flag_origins`] and
396    /// [`ParseOutput::arg_origins`] are the other half. A table keyed by token cannot show
397    /// a value that came from nowhere in argv, and a table keyed by declaration cannot show
398    /// a token that bound to nothing, so both exist.
399    pub tokens: Vec<TokenBinding>,
400    /// Where a flag's value came from when it did not come from argv, in the order the
401    /// fallbacks fired. Keyed as [`ParseOutput::flags`] is.
402    ///
403    /// A list rather than one origin: repeated bare occurrences of a `var` flag each take a
404    /// `default_missing` value, so one flag can have several.
405    pub flag_origins: IndexMap<Arc<SpecFlag>, Vec<ValueOrigin>>,
406    /// Where an argument's value came from when it did not come from argv. Keyed as
407    /// [`ParseOutput::args`] is.
408    pub arg_origins: IndexMap<Arc<SpecArg>, Vec<ValueOrigin>>,
409    /// Flags a later occurrence removed, and the flag that removed them.
410    ///
411    /// The overriding name is the half a caller needs: the fallback phase silently declines
412    /// to fill an overridden flag, so "why is `--quiet` unset when its default says
413    /// otherwise" has no answer without it.
414    pub overridden_flags: BTreeMap<String, String>,
415    /// Every flag the parser recognizes at this point, keyed by each of its aliases
416    /// (`--long`, `-s`, negations).
417    ///
418    /// This includes flags that only remain recognized because they may appear *before* a
419    /// mounted command — see [`ParseOutput::completion_flags`] for the set a completion
420    /// should offer.
421    pub available_flags: BTreeMap<String, Arc<SpecFlag>>,
422    pub flag_awaiting_value: Vec<Arc<SpecFlag>>,
423    pub errors: Vec<UsageErr>,
424    /// Deprecated declarations this command line used, for the caller to render when its
425    /// logging is up. Empty from [`parse_partial`]: a half-typed line being completed has
426    /// not used anything yet.
427    pub warnings: Vec<Warning>,
428    /// The positional argument the next word would have filled, i.e. where the parser's
429    /// cursor stopped. `None` once every argument is satisfied.
430    ///
431    /// Completions need exactly this: the parser already accounts for `var_max`, for
432    /// `restart_token` rewinds, and for the jump an explicit `--` performs onto a
433    /// `double_dash="required"` argument, so re-deriving it from `args` would disagree.
434    pub next_arg: Option<Arc<SpecArg>>,
435    /// Whether an explicit `--` was consumed *as a separator*.
436    ///
437    /// A `--` that `double_dash="preserve"` keeps as a value does not count: it is a value
438    /// of the variadic argument collecting it, not a separator, so it does not unlock a
439    /// `double_dash="required"` argument.
440    pub double_dash_seen: bool,
441    /// Remaining argv captured when an unmatched word was forwarded as an external
442    /// subcommand: the command name first, then every token after it.
443    ///
444    /// Absent when no external command was selected. See [`SpecCommand::external_subcommand`].
445    pub external: Option<Vec<String>>,
446}
447
448impl ParseOutput {
449    /// The flags a completion should offer for the parsed command.
450    ///
451    /// Usually every recognized flag, i.e. [`ParseOutput::available_flags`]. Once a mounted
452    /// command has been reached, though, the commands above it belong to the mounting CLI and
453    /// their flags are not accepted there — mise, for example, forwards everything after a task
454    /// name to the task itself — so only the flags declared from the mount boundary down are
455    /// offered. Those globals stay in `available_flags` because they may legitimately appear
456    /// *before* the mounted command.
457    pub fn completion_flags(&self) -> BTreeMap<String, Arc<SpecFlag>> {
458        let Some(boundary) = self.cmds.iter().position(|cmd| cmd.mounted) else {
459            return self.available_flags.clone();
460        };
461        // A mount can also merge flags from its spec's root into the command it is mounted on
462        // (`SpecCommand::flags_from_mount`). Those describe the mounted program too, so the
463        // replay starts one level up to inherit its globals.
464        let start = match boundary.checked_sub(1) {
465            Some(prev) if self.cmds[prev].flags_from_mount => prev,
466            _ => boundary,
467        };
468        // Re-run the descent from there, which starts with no inherited flags. Below the
469        // boundary the mounted program's commands are ordinary commands, so the descents use
470        // the same merge as the real parse.
471        let mut offered = gather_flags(&self.cmds[start]);
472        for cmd in &self.cmds[start + 1..] {
473            merge_subcommand_flags(&mut offered, gather_flags(cmd), false);
474        }
475        offered
476    }
477}
478
479#[derive(Debug, Clone)]
480pub enum ParseValue {
481    Bool(bool),
482    String(String),
483    MultiBool(Vec<bool>),
484    MultiString(Vec<String>),
485}
486
487impl ParseValue {
488    pub fn try_as_bool(self) -> Option<bool> {
489        match self {
490            Self::Bool(value) => Some(value),
491            _ => None,
492        }
493    }
494
495    pub const fn try_as_bool_ref(&self) -> Option<&bool> {
496        match self {
497            Self::Bool(value) => Some(value),
498            _ => None,
499        }
500    }
501
502    pub fn try_as_bool_mut(&mut self) -> Option<&mut bool> {
503        match self {
504            Self::Bool(value) => Some(value),
505            _ => None,
506        }
507    }
508
509    pub fn try_as_string(self) -> Option<String> {
510        match self {
511            Self::String(value) => Some(value),
512            _ => None,
513        }
514    }
515
516    pub const fn try_as_string_ref(&self) -> Option<&String> {
517        match self {
518            Self::String(value) => Some(value),
519            _ => None,
520        }
521    }
522
523    pub fn try_as_string_mut(&mut self) -> Option<&mut String> {
524        match self {
525            Self::String(value) => Some(value),
526            _ => None,
527        }
528    }
529
530    pub fn try_as_multi_bool(self) -> Option<Vec<bool>> {
531        match self {
532            Self::MultiBool(value) => Some(value),
533            _ => None,
534        }
535    }
536
537    pub const fn try_as_multi_bool_ref(&self) -> Option<&Vec<bool>> {
538        match self {
539            Self::MultiBool(value) => Some(value),
540            _ => None,
541        }
542    }
543
544    pub fn try_as_multi_bool_mut(&mut self) -> Option<&mut Vec<bool>> {
545        match self {
546            Self::MultiBool(value) => Some(value),
547            _ => None,
548        }
549    }
550
551    pub fn try_as_multi_string(self) -> Option<Vec<String>> {
552        match self {
553            Self::MultiString(value) => Some(value),
554            _ => None,
555        }
556    }
557
558    pub const fn try_as_multi_string_ref(&self) -> Option<&Vec<String>> {
559        match self {
560            Self::MultiString(value) => Some(value),
561            _ => None,
562        }
563    }
564
565    pub fn try_as_multi_string_mut(&mut self) -> Option<&mut Vec<String>> {
566        match self {
567            Self::MultiString(value) => Some(value),
568            _ => None,
569        }
570    }
571}
572
573/// The deprecated declarations argv itself named: the commands it descended through, and the
574/// flags it bound.
575///
576/// Called before the environment and defaults have filled anything, because afterwards nothing
577/// distinguishes a flag the user typed from one a variable supplied — and the two are reported
578/// differently, at the point where each is applied.
579///
580/// The root is skipped. A `deprecated` root would otherwise warn on every invocation of the CLI,
581/// including `--help`, and the compiled parser reports selected commands rather than the one the
582/// process already is.
583fn collect_deprecations(out: &mut ParseOutput) {
584    for cmd in out.cmds.iter().skip(1) {
585        if cmd.deprecated.is_none()
586            && cmd.deprecated_warn_at.is_none()
587            && cmd.deprecated_remove_at.is_none()
588        {
589            continue;
590        }
591        out.warnings.push(Warning::command(
592            cmd.name.clone(),
593            cmd.deprecated.clone(),
594            cmd.deprecated_warn_at.clone(),
595            cmd.deprecated_remove_at.clone(),
596        ));
597    }
598    for flag in out.flags.keys() {
599        if let Some(warning) = flag_deprecation(flag) {
600            out.warnings.push(warning);
601        }
602    }
603}
604
605/// A warning for a flag that was used, if its declaration is deprecated at all.
606fn flag_deprecation(flag: &SpecFlag) -> Option<Warning> {
607    if flag.deprecated.is_none()
608        && flag.deprecated_warn_at.is_none()
609        && flag.deprecated_remove_at.is_none()
610    {
611        return None;
612    }
613    Some(Warning::flag(
614        flag_spelling(flag),
615        flag.deprecated.clone(),
616        flag.deprecated_warn_at.clone(),
617        flag.deprecated_remove_at.clone(),
618    ))
619}
620
621/// A flag named the way the user names it. The spec's name for it has no dashes, and a warning
622/// about `old-flag` would be about a word nobody typed.
623fn flag_spelling(flag: &SpecFlag) -> String {
624    flag.long
625        .first()
626        .map(|long| format!("--{long}"))
627        .or_else(|| flag.short.first().map(|short| format!("-{short}")))
628        .unwrap_or_else(|| flag.name.clone())
629}
630
631/// The name this flag reads first, which is what to use instead of a deprecated alias.
632fn flag_current_env(flag: &SpecFlag) -> Option<String> {
633    flag.env
634        .clone()
635        .or_else(|| flag.env_fallback.first().cloned())
636}
637
638fn flag_env_is_deprecated(flag: &SpecFlag, name: &str) -> bool {
639    flag.deprecated_env.iter().any(|declared| declared == name)
640}
641
642/// The same two questions for a positional, which has aliases but no `deprecated` of its own.
643fn arg_current_env(arg: &SpecArg) -> Option<String> {
644    arg.env
645        .clone()
646        .or_else(|| arg.env_fallback.first().cloned())
647}
648
649fn arg_env_is_deprecated(arg: &SpecArg, name: &str) -> bool {
650    arg.deprecated_env.iter().any(|declared| declared == name)
651}
652
653/// The first of `names` that is set, and which one it was.
654///
655/// `env_names()` yields the current name, then the declared fallbacks, then the deprecated
656/// aliases, so the winner's identity is what says whether a value arrived through an alias.
657/// Deciding that a second time, from the outside, would be a copy of this precedence rule free to
658/// disagree with it.
659fn first_set_env<'a>(
660    mut names: impl Iterator<Item = &'a str>,
661    get_env: &impl Fn(&str) -> Option<String>,
662) -> Option<(&'a str, String)> {
663    names.find_map(|name| get_env(name).map(|value| (name, value)))
664}
665
666/// Builder for parsing command-line arguments with custom options.
667///
668/// Use this when you need to customize parsing behavior, such as providing
669/// a custom environment variable map instead of using the process environment.
670///
671/// # Example
672/// ```
673/// use std::collections::HashMap;
674/// use usage::Spec;
675/// use usage::parse::Parser;
676///
677/// let spec: Spec = r#"flag "--name <name>" env="NAME""#.parse().unwrap();
678/// let env: HashMap<String, String> = [("NAME".into(), "john".into())].into();
679///
680/// let result = Parser::new(&spec)
681///     .with_env(env)
682///     .parse(&["cmd".into()])
683///     .unwrap();
684/// ```
685#[non_exhaustive]
686pub struct Parser<'a> {
687    spec: &'a Spec,
688    env: Option<HashMap<String, String>>,
689    mount_outputs: Option<HashMap<String, String>>,
690}
691
692impl<'a> Parser<'a> {
693    /// Create a new parser for the given spec.
694    pub fn new(spec: &'a Spec) -> Self {
695        Self {
696            spec,
697            env: None,
698            mount_outputs: None,
699        }
700    }
701
702    /// Use a custom environment variable map instead of the process environment.
703    ///
704    /// This is useful when parsing for tasks in a monorepo where the env vars
705    /// come from a child config file rather than the current process environment.
706    pub fn with_env(mut self, env: HashMap<String, String>) -> Self {
707        self.env = Some(env);
708        self
709    }
710
711    /// Inject deterministic outputs for mount commands instead of executing them.
712    ///
713    /// Keys are the exact `run` strings declared by mount nodes and values are the
714    /// usage specs those commands would print. When this is set, every encountered
715    /// mount must have an entry. Production parsing remains process-backed unless a
716    /// caller explicitly opts into injection.
717    pub fn with_mount_outputs(mut self, outputs: HashMap<String, String>) -> Self {
718        self.mount_outputs = Some(outputs);
719        self
720    }
721
722    /// Parse the input arguments.
723    ///
724    /// Returns the parsed arguments and flags, with defaults and env vars applied.
725    pub fn parse(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
726        let out = self.parse_collecting(input)?;
727        if let Some(err) = out
728            .errors
729            .iter()
730            .find(|e| matches!(e, UsageErr::Help(_) | UsageErr::Version(_)))
731        {
732            bail!("{err}");
733        }
734        if !out.errors.is_empty() {
735            bail!("{}", out.errors.iter().map(|e| e.to_string()).join("\n"));
736        }
737        Ok(out)
738    }
739
740    /// Everything the parse learned, whether or not it succeeded.
741    ///
742    /// [`Parser::parse`] wants the first error and nothing else, which is right for a
743    /// caller about to act on a command line. A caller that wants to *explain* one wants
744    /// the opposite: the bindings that worked and every complaint about the rest, since a
745    /// report saying only "missing required <src>" is the report you already had.
746    ///
747    /// Failures that stop the parse dead — a mount that will not run, a word no
748    /// declaration can take — still come back as `Err`. There is no output to describe in
749    /// those cases; see [`Parser::explain`] for what to do about it.
750    pub fn explain(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
751        self.parse_collecting(input)
752    }
753
754    /// The binding phase's own answer for a line [`Parser::explain`] refused.
755    ///
756    /// `Ok` when the binding phase finished and the failure came after it — a flag left
757    /// waiting for a value, say. Everything argv supplied is there and only the
758    /// environment-and-defaults phase is missing.
759    ///
760    /// `Err` when the binding phase is where it died, leaving the tokens it had attributed
761    /// by then. Those words are most of what a report is for: "no declaration takes `bogus`"
762    /// is more useful next to the three tokens that did bind than on its own. The word that
763    /// caused the failure carries a role saying so, and everything still queued behind it is
764    /// [`TokenRole::Unread`], for the two failures a command line reaches on its own — a
765    /// word nothing declares, and a flag a strict spec refuses. A failure in the spec rather
766    /// than in the line, such as a mount that will not run, stops the trace where it stopped
767    /// and the words past it carry no role.
768    pub fn explain_refused(self, input: &[String]) -> Result<ParseOutput, Vec<TokenBinding>> {
769        let mut trace = Trace::new(input);
770        match parse_partial_traced(
771            self.spec,
772            input,
773            self.env.as_ref(),
774            self.mount_outputs.as_ref(),
775            MountTiming::WhenAWordIsUnknown,
776            true,
777            &mut trace,
778        ) {
779            // A parse that got as far as stopping normally already moved its tokens onto the
780            // output, which is where a caller should read them from.
781            Ok((out, _)) => Ok(out),
782            Err(_) => Err(trace.tokens),
783        }
784    }
785
786    fn parse_collecting(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
787        let custom_env = self.env.as_ref();
788        let (mut out, overridden_flags) = parse_partial_with_env(
789            self.spec,
790            input,
791            custom_env,
792            self.mount_outputs.as_ref(),
793            MountTiming::WhenAWordIsUnknown,
794            false,
795        )?;
796        restore_current_clause(&mut out);
797        trace!("{out:?}");
798
799        // A flag still waiting for a value never got one, so the command line ended
800        // mid-flag. `parse_partial` leaves this for completions to look at — a
801        // half-typed `--jobs ` is exactly what a completion is asked about — but a
802        // full parse has nothing left to wait for, and dropping the flag silently
803        // made a forgotten value look like a working command.
804        while try_bind_default_missing(
805            &mut out.flags,
806            &mut out.flag_awaiting_value,
807            custom_env,
808            &mut out.flag_origins,
809        )? {}
810        if let Some(flag) = out.flag_awaiting_value.first() {
811            let token = flag
812                .long
813                .first()
814                .map(|l| format!("--{l}"))
815                .or_else(|| flag.short.first().map(|s| format!("-{s}")))
816                .unwrap_or_else(|| flag.name.clone());
817            let rendered = input.join(" ");
818            let span = rendered
819                .rfind(&token)
820                .map(|at| (at, token.len()))
821                .unwrap_or((0, 0));
822            return Err(UsageErr::InvalidFlag {
823                token,
824                reason: "requires an argument".to_string(),
825                span: span.into(),
826                input: rendered,
827            }
828            .into());
829        }
830
831        // Before the environment and defaults have their turn, because both mark a field as
832        // filled and only argv can be reported as something the user typed. Env is reported
833        // where it is applied, below; a default is nobody's request and reports nothing.
834        collect_deprecations(&mut out);
835
836        let get_env = |key: &str| -> Option<String> {
837            if let Some(env_map) = custom_env {
838                env_map.get(key).cloned()
839            } else {
840                std::env::var(key).ok()
841            }
842        };
843
844        // Apply env vars and defaults for args
845        //
846        // Not `skip(out.args.len())`: an explicit `--` can jump the parser's cursor past an arg
847        // that stayed empty, leaving a gap that makes the fill count a wrong starting offset.
848        for arg in active_args(&out.cmd) {
849            // Clause instances contain argv only: defaults and environment values do not
850            // manufacture fields inside a repeated group.
851            if out.cmd.clause.is_some() {
852                break;
853            }
854            if out.args.contains_key(arg) {
855                continue;
856            }
857            if let Some((env_name, env_value)) = first_set_env(arg.env_names(), &get_env) {
858                if arg_env_is_deprecated(arg, env_name) {
859                    out.warnings
860                        .push(Warning::env(env_name, arg_current_env(arg)));
861                }
862                let values = split_fallback_values(std::slice::from_ref(&env_value), arg.delimiter);
863                validate_choice_values(
864                    ChoiceTarget::arg(arg),
865                    &values,
866                    arg.choices.as_ref(),
867                    custom_env,
868                )?;
869                let parsed = if arg.var {
870                    validate_arg_fallback_count(arg, values.len(), &mut out.errors);
871                    ParseValue::MultiString(values)
872                } else {
873                    ParseValue::String(values.into_iter().next().unwrap_or_default())
874                };
875                out.args.insert(Arc::new(arg.clone()), parsed);
876                out.arg_origins
877                    .entry(Arc::new(arg.clone()))
878                    .or_default()
879                    .push(ValueOrigin::Env(env_name.to_string()));
880                continue;
881            }
882            if !arg.default.is_empty() {
883                // Consider var when deciding the type of default return value
884                if arg.var {
885                    let values = split_fallback_values(&arg.default, arg.delimiter);
886                    validate_arg_fallback_count(arg, values.len(), &mut out.errors);
887                    validate_choice_values(
888                        ChoiceTarget::arg(arg),
889                        &values,
890                        arg.choices.as_ref(),
891                        custom_env,
892                    )?;
893                    // For var=true, always return a vec (MultiString)
894                    out.args
895                        .insert(Arc::new(arg.clone()), ParseValue::MultiString(values));
896                    out.arg_origins
897                        .entry(Arc::new(arg.clone()))
898                        .or_default()
899                        .push(ValueOrigin::Default);
900                } else {
901                    validate_choice_value(
902                        ChoiceTarget::arg(arg),
903                        &arg.default[0],
904                        arg.choices.as_ref(),
905                        custom_env,
906                    )?;
907                    // For var=false, return the first default value as String
908                    out.args.insert(
909                        Arc::new(arg.clone()),
910                        ParseValue::String(arg.default[0].clone()),
911                    );
912                    out.arg_origins
913                        .entry(Arc::new(arg.clone()))
914                        .or_default()
915                        .push(ValueOrigin::Default);
916                }
917            }
918        }
919
920        // Environment first, for every flag, so a `default_if` can see a sibling
921        // that was filled from env. Applying both in one pass would make the
922        // answer depend on declaration order: `--bin-names` before `--json`
923        // would miss `EX_JSON=1`.
924        let flags: Vec<Arc<SpecFlag>> = out
925            .available_flags
926            .values()
927            .filter(|flag| !is_clause_scoped_flag(&out, flag))
928            .cloned()
929            .collect();
930        for flag in &flags {
931            if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
932                continue;
933            }
934            if let Some((env_name, env_value)) = first_set_env(flag.env_names(), &get_env) {
935                // The flag's own deprecation before the alias's, which is the order the
936                // compiled parser reports them in: it walks a command's flags and then its
937                // aliases. Using a deprecated flag through a variable is still using it.
938                if let Some(warning) = flag_deprecation(flag) {
939                    out.warnings.push(warning);
940                }
941                if flag_env_is_deprecated(flag, env_name) {
942                    out.warnings
943                        .push(Warning::env(env_name, flag_current_env(flag)));
944                }
945                if let Some(arg) = flag.arg.as_ref() {
946                    let values =
947                        split_fallback_values(std::slice::from_ref(&env_value), arg.delimiter);
948                    validate_choice_values(
949                        ChoiceTarget::option(flag),
950                        &values,
951                        arg.choices.as_ref(),
952                        custom_env,
953                    )?;
954                    let parsed = if flag.var || arg.var {
955                        if flag.var {
956                            validate_flag_fallback_count(flag, values.len(), &mut out.errors);
957                        }
958                        if arg.var {
959                            validate_flag_arg_fallback_count(
960                                flag,
961                                arg,
962                                values.len(),
963                                &mut out.errors,
964                            );
965                        }
966                        ParseValue::MultiString(values)
967                    } else {
968                        ParseValue::String(values.into_iter().next().unwrap_or_default())
969                    };
970                    out.flags.insert(Arc::clone(flag), parsed);
971                } else {
972                    let is_true = matches!(env_value.as_str(), "1" | "true" | "True" | "TRUE");
973                    out.flags
974                        .insert(Arc::clone(flag), ParseValue::Bool(is_true));
975                }
976                out.flag_origins
977                    .entry(Arc::clone(flag))
978                    .or_default()
979                    .push(ValueOrigin::Env(env_name.to_string()));
980            }
981        }
982        // Decide every `default_if` against argv+env only. Binding as we go would put
983        // a default into `out.flags` and make the next flag's condition treat it as
984        // explicit — Go's `Given()` and the derive's `__given_*` both ignore defaults
985        // here, so an unconditional `default` on `--json` must not fire
986        // `default_if "--json"`.
987        let mut from_default_if: Vec<(Arc<SpecFlag>, crate::SpecDefaultIf)> = Vec::new();
988        for flag in &flags {
989            if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
990                continue;
991            }
992            if let Some(condition) = flag.default_if.iter().find(|condition| {
993                default_if_condition_matches(condition, &out, &overridden_flags, custom_env)
994            }) {
995                from_default_if.push((Arc::clone(flag), condition.clone()));
996            }
997        }
998        for (flag, condition) in &from_default_if {
999            // The whole condition, not just the value: several conditions may name the same
1000            // selector with different `when` values, so the selector alone does not say
1001            // which one fired.
1002            bind_flag_fallback(
1003                flag,
1004                std::slice::from_ref(&condition.value),
1005                &mut out,
1006                custom_env,
1007                ValueOrigin::DefaultIf {
1008                    selector: condition.selector.clone(),
1009                    when: condition.when.clone(),
1010                },
1011            )?;
1012        }
1013        for flag in &flags {
1014            if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
1015                continue;
1016            }
1017            if !flag.default.is_empty() {
1018                bind_flag_fallback(
1019                    flag,
1020                    &flag.default,
1021                    &mut out,
1022                    custom_env,
1023                    ValueOrigin::Default,
1024                )?;
1025                continue;
1026            }
1027            if let Some(arg) = flag.arg.as_ref() {
1028                if !arg.default.is_empty() {
1029                    bind_flag_fallback(
1030                        flag,
1031                        &arg.default,
1032                        &mut out,
1033                        custom_env,
1034                        ValueOrigin::Default,
1035                    )?;
1036                }
1037            }
1038        }
1039        // The binding phase leaves the last clause instance in `args`/`flags` so
1040        // completion can inspect it. A full parse closes it before applying scoped
1041        // fallbacks: defaults and environment values fill instances that argv made,
1042        // but never manufacture a repeated group on their own.
1043        finalize_current_clause(&mut out);
1044        let clause_explicit = apply_clause_flag_fallbacks(&mut out, &overridden_flags, custom_env)?;
1045        validate_clause_relationships(
1046            &mut out,
1047            &overridden_flags,
1048            custom_env,
1049            clause_explicit.as_deref(),
1050        );
1051        // Declarative value validation is deliberately post-binding. Defaults and
1052        // environment fallbacks have landed by here, and delimiters were already split
1053        // while binding. Like clap's value parsers, a declaration judges each resulting
1054        // raw value independently.
1055        for (arg, parsed) in &out.args {
1056            validate_expression(
1057                &arg.name,
1058                arg.validate.as_deref(),
1059                arg.validate_error.as_deref(),
1060                parsed,
1061                &mut out.errors,
1062            );
1063        }
1064        if let Some(clause) = &out.cmd.clause {
1065            let mut clause_errors = Vec::new();
1066            for (index, instance) in out
1067                .clauses
1068                .get(&clause.name)
1069                .into_iter()
1070                .flatten()
1071                .enumerate()
1072            {
1073                for arg in &clause.args {
1074                    let Some(value) = instance.get(arg) else {
1075                        if arg.required {
1076                            clause_errors.push(UsageErr::MissingClauseArg {
1077                                clause: clause.name.clone(),
1078                                instance: index + 1,
1079                                arg: arg.name.clone(),
1080                            });
1081                        }
1082                        continue;
1083                    };
1084                    if let (true, ParseValue::MultiString(values)) = (arg.var, value) {
1085                        if let Some(min) = arg.var_min {
1086                            if values.len() < min {
1087                                clause_errors.push(UsageErr::VarArgTooFew {
1088                                    name: format!(
1089                                        "{} instance {}: {}",
1090                                        clause.name,
1091                                        index + 1,
1092                                        arg.name
1093                                    ),
1094                                    min,
1095                                    got: values.len(),
1096                                });
1097                            }
1098                        }
1099                        if let Some(max) = arg.var_max {
1100                            if values.len() > max {
1101                                clause_errors.push(UsageErr::VarArgTooMany {
1102                                    name: format!(
1103                                        "{} instance {}: {}",
1104                                        clause.name,
1105                                        index + 1,
1106                                        arg.name
1107                                    ),
1108                                    max,
1109                                    got: values.len(),
1110                                });
1111                            }
1112                        }
1113                    }
1114                }
1115            }
1116            out.errors.extend(clause_errors);
1117        }
1118        let clause_flag_names = out
1119            .cmd
1120            .clause
1121            .iter()
1122            .flat_map(|clause| &clause.flags)
1123            .map(|flag| flag.name.as_str())
1124            .collect::<HashSet<_>>();
1125        for (flag, parsed) in out
1126            .flags
1127            .iter()
1128            .filter(|(flag, _)| !clause_flag_names.contains(flag.name.as_str()))
1129        {
1130            if let Some(arg) = &flag.arg {
1131                validate_expression(
1132                    &flag.name,
1133                    arg.validate.as_deref(),
1134                    arg.validate_error.as_deref(),
1135                    parsed,
1136                    &mut out.errors,
1137                );
1138            }
1139        }
1140        // Applied once, here, because this is where the CLI's own version is known: a
1141        // `deprecated_warn_at` the spec has not reached yet is an author saying *not yet*.
1142        crate::warn::retain_reached(&mut out.warnings, self.spec.version.as_deref());
1143        Ok(out)
1144    }
1145}
1146
1147/// Parse command-line arguments according to a spec.
1148///
1149/// Returns the parsed arguments and flags, with defaults and env vars applied.
1150/// Uses `std::env::var` for environment variable lookups.
1151///
1152/// For custom environment variable handling, use [`Parser`] instead.
1153#[must_use = "parsing result should be used"]
1154pub fn parse(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
1155    Parser::new(spec).parse(input)
1156}
1157
1158/// Parse command-line arguments without applying defaults.
1159///
1160/// Use this for help text generation or when you need the raw parsed values.
1161#[must_use = "parsing result should be used"]
1162pub fn parse_partial(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
1163    parse_partial_with_env(spec, input, None, None, MountTiming::Eager, true).map(|(out, _)| out)
1164}
1165
1166/// Basename of argv[0] for a multicall CLI: last path component, with a trailing
1167/// `.exe` stripped so Windows and Unix agree.
1168pub fn multicall_basename(argv0: &str) -> &str {
1169    let name = argv0.rsplit(['/', '\\']).next().unwrap_or(argv0);
1170    match name.get(name.len().saturating_sub(4)..) {
1171        Some(ext) if ext.eq_ignore_ascii_case(".exe") => &name[..name.len() - 4],
1172        _ => name,
1173    }
1174}
1175
1176/// The applet name to parse as the first word, when argv[0] is not the dispatcher.
1177///
1178/// `None` means a dispatcher invocation (`busybox ls`): skip argv[0] and parse the
1179/// rest. `Some` is a symlink invocation (`ls -l`): inject the basename.
1180pub fn multicall_applet<'a>(argv0: &'a str, name: &str, bin: Option<&str>) -> Option<&'a str> {
1181    let base = multicall_basename(argv0);
1182    if !name.is_empty() && base == multicall_basename(name) {
1183        return None;
1184    }
1185    if let Some(bin) = bin {
1186        if !bin.is_empty() && base == multicall_basename(bin) {
1187            return None;
1188        }
1189    }
1190    Some(base)
1191}
1192
1193/// Internal version of parse_partial that accepts an optional custom env map.
1194/// When a command's own `mount` runs, for the root — which nothing descends into.
1195///
1196/// A completion has to know every command before it can offer one, even with
1197/// nothing typed yet, so it resolves up front. An execution knows the word it was
1198/// given, so it only pays for discovery when that word matches nothing declared —
1199/// and a CLI that declares its commands and mounts a few more does not spawn a
1200/// process on every invocation.
1201#[derive(Clone, Copy, PartialEq, Eq)]
1202enum MountTiming {
1203    Eager,
1204    WhenAWordIsUnknown,
1205}
1206
1207/// One word on its way through the parser, with what the parser has learned about it.
1208///
1209/// This holds what a side queue used to: the flag Phase 1 read a word as, previously a
1210/// `VecDeque` popped in step with the words. Two queues staying aligned is an invariant
1211/// nothing checks, and it was delicate enough to need explaining at three call sites; on
1212/// the word itself there is nothing to keep aligned. The argv position is here for the
1213/// same reason: the queue is popped, re-queued, split on `=`, and has subcommand words
1214/// removed from the middle, so position in the queue stops meaning position in argv on the
1215/// first descent.
1216struct Token {
1217    word: String,
1218    /// Where in the caller's argv this word came from.
1219    ///
1220    /// A word the parser made up points at the token it was derived from — the tail of a
1221    /// short bundle at the bundle, a multicall applet name at argv[0] — because that is the
1222    /// token a reader would point at, and there is nothing else to point at.
1223    argv: usize,
1224    /// The flag Phase 1 read this word as, and the command level it read it at.
1225    ///
1226    /// `Some((flag, command_level))` for a flag word, `None` for its value, for anything
1227    /// unresolved, and for every word Phase 1 never reached. The words stay in the queue
1228    /// for Phase 2 to re-parse — that is how they reach `out.flags` and `as_env()` — but by
1229    /// then the recognized flags have changed, because each descent drops the parent's
1230    /// non-global flags and a mounted command may declare the same name as a global seen
1231    /// here. Recording the owner keeps a word bound to the flag it was read as.
1232    ///
1233    /// The level matters to strict parsing: clap permits an inherited global once on each
1234    /// side of a subcommand boundary.
1235    binding: Option<(Arc<SpecFlag>, usize)>,
1236}
1237
1238/// Flag scope for the one token straddling an implicit default-command boundary.
1239struct DefaultBundle {
1240    argv: usize,
1241    flags: BTreeMap<String, Arc<SpecFlag>>,
1242    parent_keys: HashSet<String>,
1243}
1244
1245impl DefaultBundle {
1246    /// Preserve the declaration and command level when a short tail is requeued.
1247    fn binding(&self, key: &str) -> Option<(Arc<SpecFlag>, usize)> {
1248        self.flags.get(key).map(|flag| {
1249            (
1250                Arc::clone(flag),
1251                usize::from(!self.parent_keys.contains(key)),
1252            )
1253        })
1254    }
1255}
1256
1257impl Token {
1258    fn new(word: String, argv: usize) -> Self {
1259        Self {
1260            word,
1261            argv,
1262            binding: None,
1263        }
1264    }
1265}
1266
1267/// The token trace, while it is being built.
1268///
1269/// One row per word of the caller's argv, so a role can be recorded against a position
1270/// without the recorder having to know how many words came before it. Words the parser
1271/// made up have no row of their own and fold onto the row they were derived from.
1272struct Trace {
1273    tokens: Vec<TokenBinding>,
1274}
1275
1276impl Trace {
1277    fn new(input: &[String]) -> Self {
1278        Self {
1279            tokens: input
1280                .iter()
1281                .enumerate()
1282                .map(|(index, word)| TokenBinding {
1283                    index,
1284                    word: word.clone(),
1285                    synthesized: false,
1286                    roles: vec![],
1287                })
1288                .collect(),
1289        }
1290    }
1291
1292    fn record(&mut self, argv: usize, role: TokenRole) {
1293        if let Some(token) = self.tokens.get_mut(argv) {
1294            token.roles.push(role);
1295        }
1296    }
1297
1298    /// Note that what was read at this position is not what the caller wrote there.
1299    fn note_synthesized(&mut self, argv: usize) {
1300        if let Some(token) = self.tokens.get_mut(argv) {
1301            token.synthesized = true;
1302        }
1303    }
1304
1305    /// Every word the parse never reached, once it has stopped.
1306    fn close(&mut self, unread: &VecDeque<Token>) {
1307        for token in unread {
1308            self.record(token.argv, TokenRole::Unread);
1309        }
1310    }
1311}
1312
1313fn parse_partial_with_env(
1314    spec: &Spec,
1315    input: &[String],
1316    custom_env: Option<&HashMap<String, String>>,
1317    mount_outputs: Option<&HashMap<String, String>>,
1318    mount_timing: MountTiming,
1319    validate_clauses: bool,
1320) -> Result<(ParseOutput, HashSet<String>), miette::Error> {
1321    let mut trace = Trace::new(input);
1322    parse_partial_traced(
1323        spec,
1324        input,
1325        custom_env,
1326        mount_outputs,
1327        mount_timing,
1328        validate_clauses,
1329        &mut trace,
1330    )
1331}
1332
1333/// Locate the first default-only flag, while preserving an explicit sibling selector.
1334/// This only reads declarations: it neither binds values nor executes mount commands.
1335fn default_flag_route(spec: &Spec, root: &SpecCommand, input: &VecDeque<Token>) -> Option<usize> {
1336    let default = root.find_subcommand(spec.default_subcommand.as_deref()?)?;
1337    let parent = gather_flags(root);
1338    let child = gather_flags(default);
1339    let path = std::slice::from_ref(root);
1340    let mut at = None;
1341    let mut i = 0;
1342    while let Some(token) = input.get(i).map(|t| t.word.as_str()) {
1343        let scope = if at.is_some() { default } else { root };
1344        if token == "--"
1345            || token == "-"
1346            || scope
1347                .clause
1348                .as_ref()
1349                .is_some_and(|c| c.separator.as_deref() == Some(token))
1350        {
1351            return at;
1352        }
1353        if !is_flag_like(token) {
1354            if at.is_none()
1355                && (root.find_subcommand(token).is_some()
1356                    || (token == "help"
1357                        && spec.disable_help != Some(true)
1358                        && !root.disable_help_subcommand))
1359            {
1360                return None;
1361            }
1362            return at;
1363        }
1364        let mut value_flag = None;
1365        let mut attached = None;
1366        if token.starts_with("--") {
1367            let (name, value) = token
1368                .split_once('=')
1369                .map_or((token, None), |(n, v)| (n, Some(v)));
1370            if !parent.contains_key(name)
1371                && (is_help_arg(spec, root, name) || is_version_arg(spec, path, name))
1372            {
1373                i += 1;
1374                continue;
1375            }
1376            let flag = parent.get(name).or_else(|| child.get(name));
1377            let Some(flag) = flag else {
1378                return at;
1379            };
1380            if !parent.contains_key(name) {
1381                at.get_or_insert(i);
1382            }
1383            if flag.arg.is_some() && flag.negate.as_deref() != Some(name) {
1384                value_flag = Some(flag);
1385                attached = value;
1386            }
1387        } else {
1388            for (offset, letter) in token.char_indices().skip(1) {
1389                let key = format!("-{letter}");
1390                if !parent.contains_key(&key) && supplied_short(spec, path, letter).is_some() {
1391                    continue;
1392                }
1393                let flag = parent.get(&key).or_else(|| child.get(&key));
1394                let Some(flag) = flag else {
1395                    return at;
1396                };
1397                if !parent.contains_key(&key) {
1398                    at.get_or_insert(i);
1399                }
1400                if flag.arg.is_some() {
1401                    value_flag = Some(flag);
1402                    let rest = &token[offset + letter.len_utf8()..];
1403                    attached = (!rest.is_empty()).then_some(rest.strip_prefix('=').unwrap_or(rest));
1404                    break;
1405                }
1406            }
1407        }
1408        i += 1;
1409        if let Some(flag) = value_flag {
1410            let scope = if at.is_some() { default } else { root };
1411            let is_separator = |next: &str| {
1412                scope
1413                    .clause
1414                    .as_ref()
1415                    .is_some_and(|c| c.separator.as_deref() == Some(next))
1416            };
1417            let first = if attached.is_some() {
1418                attached
1419            } else {
1420                input
1421                    .get(i)
1422                    .map(|t| t.word.as_str())
1423                    .filter(|next| !is_separator(next) && accepts_detached_flag_value(flag, next))
1424                    .inspect(|_| i += 1)
1425            };
1426            if first.is_none() && !flag.value_optional && flag.default_missing.is_none() {
1427                return at;
1428            }
1429            let arg = flag.arg.as_ref().expect("value flag");
1430            if arg.var {
1431                let count_values = |v: &str| arg.delimiter.map_or(1, |d| v.split(d).count());
1432                let mut count = first.map_or(0, count_values);
1433                while arg.var_max.is_none_or(|max| count < max) {
1434                    let Some(next) = input.get(i).map(|t| t.word.as_str()) else {
1435                        break;
1436                    };
1437                    if is_separator(next) {
1438                        break;
1439                    }
1440                    if next == "--" || arg.value_terminator.as_deref() == Some(next) {
1441                        if next != "--" {
1442                            i += 1;
1443                        }
1444                        break;
1445                    }
1446                    if is_flag_like(next)
1447                        && !(arg.allow_negative_numbers && is_negative_number(next))
1448                    {
1449                        break;
1450                    }
1451                    if root.subcommand_precedence_over_arg && root.find_subcommand(next).is_some() {
1452                        break;
1453                    }
1454                    count += count_values(next);
1455                    i += 1;
1456                }
1457            }
1458        }
1459    }
1460    at
1461}
1462
1463/// The binding phase, with the trace left somewhere the caller can still read it.
1464///
1465/// A failure this phase cannot continue past — a word no declaration can take, a flag a
1466/// strict spec refuses — leaves through `?`, and a trace owned by the loop goes with it. The
1467/// words read before the failure are most of what a report wants, so the caller owns the
1468/// trace instead and keeps them. See [`Parser::explain_refused`].
1469fn parse_partial_traced(
1470    spec: &Spec,
1471    input: &[String],
1472    custom_env: Option<&HashMap<String, String>>,
1473    mount_outputs: Option<&HashMap<String, String>>,
1474    mount_timing: MountTiming,
1475    validate_clauses: bool,
1476    trace: &mut Trace,
1477) -> Result<(ParseOutput, HashSet<String>), miette::Error> {
1478    if let Some(view) = input.first().and_then(|argv0| spec.view_for_program(argv0)) {
1479        let viewed = spec.for_view(view)?;
1480        return parse_partial_traced(
1481            &viewed,
1482            input,
1483            custom_env,
1484            mount_outputs,
1485            mount_timing,
1486            validate_clauses,
1487            trace,
1488        );
1489    }
1490    trace!("parse_partial: {input:?}");
1491    let mut input = input
1492        .iter()
1493        .enumerate()
1494        .map(|(argv, word)| Token::new(word.clone(), argv))
1495        .collect::<VecDeque<_>>();
1496    let argv0 = input.pop_front();
1497    if let Some(argv0) = argv0.as_ref() {
1498        trace.record(argv0.argv, TokenRole::Program);
1499    }
1500    if spec.multicall {
1501        if let Some(raw) = argv0 {
1502            if let Some(applet) = multicall_applet(&raw.word, &spec.name, Some(spec.bin.as_str())) {
1503                // A symlink invocation reads a word the caller never typed — the basename of
1504                // the program itself — so argv[0] is both the program and, below, whatever
1505                // that word selects.
1506                trace.note_synthesized(raw.argv);
1507                input.push_front(Token::new(applet.to_string(), raw.argv));
1508            }
1509        }
1510    }
1511    // The policy observes the selected command's own argv, not values eventually filled from
1512    // env/default. Start at the root, then reset on every explicit descent. A default
1513    // subcommand receives the unmatched word that selected it, so it is necessarily non-bare.
1514    let mut command_has_argv = !input.is_empty();
1515    // Unlike `out.args`, this survives clause finalization and restart rewinds. It records
1516    // whether the root actually consumed a positional token before empty-default routing.
1517    let mut root_positional_arg_found = false;
1518
1519    let mut out = ParseOutput {
1520        cmd: spec.cmd.clone(),
1521        cmds: vec![spec.cmd.clone()],
1522        args: IndexMap::new(),
1523        clauses: IndexMap::new(),
1524        clause_flags: IndexMap::new(),
1525        flags: IndexMap::new(),
1526        tokens: vec![],
1527        flag_origins: IndexMap::new(),
1528        arg_origins: IndexMap::new(),
1529        overridden_flags: BTreeMap::new(),
1530        available_flags: gather_flags(&spec.cmd),
1531        flag_awaiting_value: vec![],
1532        errors: vec![],
1533        warnings: vec![],
1534        next_arg: None,
1535        double_dash_seen: false,
1536        external: None,
1537    };
1538    // Keep this internal so adding relationship support remains semver-compatible. The full
1539    // parser uses it to prevent defaults and environment values from restoring overridden flags.
1540    let mut overridden_flags = HashSet::new();
1541    // Which spelling supplied each parsed flag. A child may re-declare one long form of an
1542    // inherited global while the merge keeps the ancestor's other aliases on the same `Arc`.
1543    // The declaration object alone then cannot answer whether `--clean` belonged to the child
1544    // or an inherited `-c` belonged to the ancestor.
1545    let mut parsed_flag_spellings: HashMap<usize, HashSet<String>> = HashMap::new();
1546
1547    // Phase 1: Scan for subcommands and collect global flags
1548    //
1549    // This phase identifies subcommands early because they may have mount points
1550    // that need to be executed with the global flags that appeared before them.
1551    //
1552    // Example: "usage --verbose run task"
1553    //   -> finds "run" subcommand, passes ["--verbose"] to its mount command
1554    //   -> then finds "task" as a subcommand of "run" (if it exists)
1555    //
1556    // We only collect global flags for mounts because:
1557    // - Non-global flags are specific to the current command, not subcommands
1558    // - Global flags affect all commands and should be passed to mount points
1559    let mut prefix_flags: Vec<(Arc<SpecFlag>, Vec<String>)> = vec![];
1560    // Which flag each word skipped here belongs to is recorded on the word — see
1561    // `Token::binding`.
1562    let mut command_arg_found = false;
1563    let mut variadic_flag_active = false;
1564    let mut idx = 0;
1565    // Track whether we've already applied the default_subcommand to prevent
1566    // multiple switches (e.g., if default is "run" and there's a task named "run")
1567    let mut used_default_subcommand = false;
1568    let mut default_bundle: Option<DefaultBundle> = None;
1569    // Whether the command in scope has had its own mounts run. A mount on the root
1570    // is the case that needs this: a subcommand's mounts are run when the parser
1571    // descends into it, but nothing descends into the root.
1572    let mut mounts_resolved = false;
1573    // A completion needs the whole command list before it can offer anything, and
1574    // `mycli <tab>` has no word to trigger discovery with — so waiting for one would
1575    // mean a root mount never contributed to the very thing it exists for.
1576    //
1577    // The default-subcommand gate applies here too, and has to: offering a discovered
1578    // command that a real parse would hand to the default instead would be worse than
1579    // not offering it. A root mount under a `default_subcommand` that does not say
1580    // `overrides_default` therefore contributes nothing anywhere, which is what
1581    // "the default outranks discovery" means.
1582    let default_outranks_mounts =
1583        spec.default_subcommand.is_some() && !out.cmd.mounts.iter().any(|m| m.overrides_default);
1584    if mount_timing == MountTiming::Eager && !default_outranks_mounts && !out.cmd.mounts.is_empty()
1585    {
1586        mounts_resolved = true;
1587        let mut mounted = out.cmd.clone();
1588        mounted.mount(&[], mount_outputs)?;
1589        merge_subcommand_flags(&mut out.available_flags, gather_flags(&mounted), false);
1590        if let Some(last) = out.cmds.last_mut() {
1591            *last = mounted.clone();
1592        }
1593        out.cmd = mounted;
1594    }
1595
1596    let default_flag_at = spec
1597        .default_subcommand_flags
1598        .then(|| default_flag_route(spec, &out.cmd, &input))
1599        .flatten();
1600
1601    while idx < input.len() {
1602        // Only for a word that could name a command, and only when it matches
1603        // nothing already declared. A CLI that declares its commands and mounts more
1604        // does not spawn a process for every invocation, and a flag — `--help`, or
1605        // anything unrecognized — never triggers discovery at all, which it would
1606        // otherwise do simply by not being a subcommand.
1607        // A declared `default_subcommand` already says what an unmatched word means,
1608        // and it costs nothing — so discovery waits behind it unless a mount asks to
1609        // outrank it. Without this, a task runner would spawn its discovery process
1610        // once per task invocation.
1611        let default_catches_it = spec.default_subcommand.as_deref().is_some_and(|name| {
1612            default_accepts_word(&out.cmd, name, &input[idx].word)
1613                && !out.cmd.mounts.iter().any(|m| m.overrides_default)
1614        });
1615        if !mounts_resolved
1616            && !out.cmd.mounts.is_empty()
1617            && !default_catches_it
1618            && is_command_word(&input[idx].word)
1619            && !is_negative_number(&input[idx].word)
1620            && out.cmd.find_subcommand(&input[idx].word).is_none()
1621        {
1622            mounts_resolved = true;
1623            let mut mounted = out.cmd.clone();
1624            mounted.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1625            merge_subcommand_flags(&mut out.available_flags, gather_flags(&mounted), false);
1626            if let Some(last) = out.cmds.last_mut() {
1627                *last = mounted.clone();
1628            }
1629            out.cmd = mounted;
1630        }
1631        if variadic_flag_active
1632            && out.cmd.find_subcommand(&input[idx].word).is_some()
1633            && !out.cmd.subcommand_precedence_over_arg
1634        {
1635            break;
1636        }
1637        let implicit_default = !used_default_subcommand && default_flag_at == Some(idx);
1638        let selected_command = if implicit_default {
1639            out.cmd
1640                .find_subcommand(spec.default_subcommand.as_deref().expect("default route"))
1641        } else {
1642            out.cmd.find_subcommand(&input[idx].word)
1643        };
1644        if let Some(subcommand) = selected_command {
1645            let boundary_has_parent = out.cmd.args_conflicts_with_subcommands
1646                && implicit_default
1647                && input[idx].word.starts_with('-')
1648                && !input[idx].word.starts_with("--")
1649                && {
1650                    let child_flags = gather_flags(subcommand);
1651                    let mut found = false;
1652                    for short in input[idx].word[1..].chars() {
1653                        let key = format!("-{short}");
1654                        if out.available_flags.contains_key(&key)
1655                            || supplied_short(spec, &out.cmds, short).is_some()
1656                        {
1657                            found = true;
1658                            break;
1659                        }
1660                        match child_flags.get(&key) {
1661                            Some(flag) if flag.arg.is_none() => {}
1662                            _ => break,
1663                        }
1664                    }
1665                    found
1666                };
1667            if out.cmd.args_conflicts_with_subcommands && (command_arg_found || boundary_has_parent)
1668            {
1669                bail!(
1670                    "subcommand '{}' cannot be used with arguments on its parent command",
1671                    subcommand.name
1672                );
1673            }
1674            let boundary_parent = (implicit_default
1675                && input[idx].word.starts_with('-')
1676                && !input[idx].word.starts_with("--"))
1677            .then(|| out.available_flags.clone());
1678            let mut subcommand = subcommand.clone();
1679            // Pass prefix words (global flags before this subcommand) to mount
1680            subcommand.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1681            // Only the *boundary* is a mount crossing: below it, the mounted program's own
1682            // commands are ordinary commands relative to each other.
1683            let crossing_mount = subcommand.mounted && !out.cmd.mounted;
1684            merge_subcommand_flags(
1685                &mut out.available_flags,
1686                gather_flags(&subcommand),
1687                crossing_mount,
1688            );
1689            if let Some(parent) = boundary_parent {
1690                let parent_keys = parent.keys().cloned().collect();
1691                let mut flags = out.available_flags.clone();
1692                flags.extend(parent);
1693                default_bundle = Some(DefaultBundle {
1694                    argv: input[idx].argv,
1695                    flags,
1696                    parent_keys,
1697                });
1698            }
1699            // Remove subcommand from input
1700            let selected = if implicit_default {
1701                used_default_subcommand = true;
1702                None
1703            } else {
1704                input.remove(idx)
1705            };
1706            if let Some(selected) = selected {
1707                trace.record(
1708                    selected.argv,
1709                    TokenRole::Command {
1710                        name: subcommand.name.clone(),
1711                    },
1712                );
1713            }
1714            command_has_argv = idx < input.len();
1715            out.cmds.push(subcommand.clone());
1716            out.cmd = subcommand.clone();
1717            // A descent already ran the new command's mounts, above.
1718            mounts_resolved = true;
1719            prefix_flags.clear();
1720            command_arg_found = false;
1721            variadic_flag_active = false;
1722            // Continue from current position (don't reset to 0)
1723            // After remove(), idx now points to the next element
1724        } else if !is_command_word(&input[idx].word)
1725            || declared_numeric_short(&out.available_flags, &input[idx].word)
1726        {
1727            // Check if this is a known flag
1728            let word = input[idx].word.clone();
1729            let flag_key = get_flag_key(&word);
1730
1731            // A short token keys on its first letter, so `-az` would be recorded as
1732            // `-a` and its tail left over. Check the whole token here, where it is
1733            // first read: a token containing an unrecognized letter is not a bundle,
1734            // and recording it as one is what let `-a` be applied from a token that
1735            // never named it.
1736            let boundary = default_bundle
1737                .as_ref()
1738                .filter(|bundle| bundle.argv == input[idx].argv);
1739            let flags = boundary.map_or(&out.available_flags, |bundle| &bundle.flags);
1740            let is_bundle =
1741                word.starts_with("--") || short_bundle_is_known(spec, &out.cmds, flags, &word);
1742            if let Some(f) = flags.get(flag_key).cloned().filter(|_| is_bundle) {
1743                command_arg_found = true;
1744                variadic_flag_active = f.arg.as_ref().is_some_and(|arg| arg.var);
1745                // Skip the flag and keep scanning. Both global and non-global flags may precede
1746                // a subcommand (`mycli --verbose run task`, `mycli run --force task`), and
1747                // stopping at one would hide the subcommand — and any mount on it — from the
1748                // parse, leaving the subcommand name to be mis-read as a positional argument.
1749                //
1750                // Only globals are forwarded to mounts: a non-global flag belongs to the
1751                // command that declared it, not to what is mounted below it.
1752                input[idx].binding = boundary
1753                    .and_then(|bundle| bundle.binding(flag_key))
1754                    .or_else(|| Some((Arc::clone(&f), out.cmds.len() - 1)));
1755                let mut forwarded = f.global.then(|| vec![word.clone()]);
1756                idx += 1;
1757
1758                // Only consume next word if flag takes an argument AND value isn't embedded
1759                // Example: "--dir foo" consumes "foo", but "--dir=foo" or "--verbose" do not
1760                if f.arg.is_some()
1761                    && !word.contains('=')
1762                    && idx < input.len()
1763                    && accepts_detached_flag_value(&f, &input[idx].word)
1764                {
1765                    if let Some(words) = forwarded.as_mut() {
1766                        words.push(input[idx].word.clone());
1767                    }
1768                    idx += 1;
1769                }
1770                if let Some(words) = forwarded {
1771                    apply_prefix_flag_overrides(&mut prefix_flags, Arc::clone(&f));
1772                    prefix_flags.push((f, words));
1773                }
1774            } else {
1775                // Unknown flag - stop looking for subcommands
1776                // Let the main parsing phase handle the error
1777                break;
1778            }
1779        } else {
1780            if variadic_flag_active && out.cmd.subcommand_precedence_over_arg {
1781                idx += 1;
1782                continue;
1783            }
1784            // Found a word that's not a flag or subcommand
1785            // Check if we should use the default_subcommand (only once, and only at the
1786            // root, which is the only place a spec can declare one — `out.cmds` holds just
1787            // the root until something descends). Without that second condition the one
1788            // declared name is looked up wherever the parser happens to be standing, so an
1789            // unrelated command acquires a default because a name matched one level down:
1790            // with `default_subcommand "ls"` at the top, `ex config zzz` descended into
1791            // `config ls`.
1792            if !used_default_subcommand && out.cmds.len() == 1 {
1793                if let Some(default_name) = &spec.default_subcommand {
1794                    if let Some(subcommand) = out
1795                        .cmd
1796                        .find_subcommand(default_name)
1797                        .filter(|_| default_accepts_word(&out.cmd, default_name, &input[idx].word))
1798                    {
1799                        if out.cmd.args_conflicts_with_subcommands && command_arg_found {
1800                            bail!(
1801                                "subcommand '{}' cannot be used with arguments on its parent command",
1802                                subcommand.name
1803                            );
1804                        }
1805                        let mut subcommand = subcommand.clone();
1806                        // Pass prefix words (global flags before this) to mount
1807                        subcommand.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1808                        let crossing_mount = subcommand.mounted && !out.cmd.mounted;
1809                        merge_subcommand_flags(
1810                            &mut out.available_flags,
1811                            gather_flags(&subcommand),
1812                            crossing_mount,
1813                        );
1814                        out.cmds.push(subcommand.clone());
1815                        out.cmd = subcommand.clone();
1816                        command_has_argv = true;
1817                        prefix_flags.clear();
1818                        command_arg_found = false;
1819                        variadic_flag_active = false;
1820                        // This descent ran the new command's mounts, so lazy
1821                        // discovery must not run them a second time.
1822                        mounts_resolved = true;
1823                        used_default_subcommand = true;
1824                        // Continue the loop to check if this word is a subcommand of the
1825                        // default subcommand (e.g., a task name added via mount).
1826                        // If it's not a subcommand, the next iteration will break and
1827                        // Phase 2 will handle it as a positional arg.
1828                        continue;
1829                    }
1830                }
1831            }
1832            // Sigil-classified positionals do not occupy the ordinary positional cursor and
1833            // therefore do not close subcommand routing. Phase 2 binds and strips them. A
1834            // default subcommand gets first refusal so interpreted and compiled routing agree
1835            // when the root sigil and the default command can both accept this word.
1836            if match_sigil_arg_chain(&out.cmds, &input[idx].word).is_some() {
1837                idx += 1;
1838                continue;
1839            }
1840            // An unmatched word that names no subcommand is forwarded as an external
1841            // command: this word, then every token after it, including flags. Known
1842            // subcommands already won above, and a default_subcommand already caught.
1843            // clap's `allow_external_subcommands` is this, not `unknown_flags=value`.
1844            if out.cmd.external_subcommand {
1845                let rest: Vec<Token> = input.drain(idx..).collect();
1846                for token in &rest {
1847                    trace.record(token.argv, TokenRole::External);
1848                }
1849                out.external = Some(rest.into_iter().map(|t| t.word).collect());
1850                break;
1851            }
1852            // This could be a positional argument, so stop subcommand search
1853            break;
1854        }
1855    }
1856
1857    // Phase 2: Main argument and flag parsing
1858    //
1859    // Now that we've identified all subcommands and executed their mounts,
1860    // we can parse the remaining arguments, flags, and their values.
1861
1862    // The cursor into the active positional arguments, kept as an index rather than a reference
1863    // because an explicit `--` may jump it *past* arguments that stay empty (see the `w == "--"`
1864    // arm).
1865    // With such a gap `out.args.len()` no longer equals the cursor, so anything asking "is this
1866    // argument filled?" has to consult `out.args` by key instead of counting.
1867    let mut next_arg_idx = cursor_skip_sigils(&out.cmd, 0);
1868    let mut enable_flags = true;
1869    let mut grouped_flag = false;
1870    // Whether an explicit `--` has been consumed *as a separator* (as opposed to being kept as a
1871    // value by `double_dash="preserve"`). Args declared `double_dash="required"` only accept
1872    // words that come after it — see `report_double_dash_violation`.
1873    let mut seen_double_dash = false;
1874    // Sigils are a leading-segment grammar. A restart begins a later segment but does not
1875    // reopen sigil classification for this invocation.
1876    let mut restart_seen = false;
1877    // Args already reported as having been offered a word before the `--` they require, so a
1878    // variadic one does not report the same violation for every word it is offered.
1879    let mut double_dash_violations: HashSet<String> = HashSet::new();
1880    // A root clause separator is user-supplied syntax, so it must suppress the
1881    // empty-input default just like an explicit `--`. This is separate from
1882    // `seen_double_dash`, which resets for each clause instance.
1883    let mut root_clause_separator_seen = false;
1884    // Scalar occurrences are scoped to the command level where they were written. Inherited
1885    // globals may therefore appear once before and once after a subcommand under clap's strict
1886    // `args_override_self(false)` policy. The bitset also keeps both forms of a negatable flag:
1887    // opposite forms may override one another, while repeating either spelling is an error.
1888    let mut scalar_occurrences: HashMap<(usize, usize), u8> = HashMap::new();
1889
1890    while !input.is_empty() {
1891        let token = input.pop_front().unwrap();
1892        // The flag this word was read as in Phase 1, if it skipped it (see `Token::binding`).
1893        let binding = token.binding;
1894        let argv = token.argv;
1895        let mut w = token.word;
1896        // A short's attached value is re-queued with `grouped_flag` set, and that
1897        // continuation is not a following word. `require_equals` refuses only the
1898        // following word; `-i9229` and `-i=9229` still bind. `default_missing` binds
1899        // only when the value is actually missing, so `-cnever` is still `never`.
1900        let attached_continuation = grouped_flag;
1901        // The opt-in lookahead uses declared value arity, so binding must consume
1902        // the same attached value even after the default changed flag scope.
1903        // Other specs retain the reference parser's legacy short-bundle rules.
1904        if spec.default_subcommand_flags
1905            && attached_continuation
1906            && !out.flag_awaiting_value.is_empty()
1907        {
1908            grouped_flag = false;
1909            w.remove(0);
1910            if w.starts_with('=') {
1911                w.remove(0);
1912            }
1913            if bind_pending_flag_value(
1914                spec,
1915                &out.cmd,
1916                &mut out.errors,
1917                &mut out.flags,
1918                &mut out.flag_awaiting_value,
1919                &mut w,
1920                &mut input,
1921                custom_env,
1922                trace,
1923                argv,
1924                true,
1925            )? {
1926                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1927                return Ok((out, overridden_flags));
1928            }
1929            continue;
1930        }
1931
1932        // A clause boundary is syntax even after an automatic trailing argument disabled
1933        // flags. Only an explicit `--` protects a literal separator.
1934        if !seen_double_dash {
1935            if let Some(clause) = out.cmd.clause.as_ref() {
1936                if clause.separator.as_deref() == Some(w.as_str()) {
1937                    while try_bind_default_missing(
1938                        &mut out.flags,
1939                        &mut out.flag_awaiting_value,
1940                        custom_env,
1941                        &mut out.flag_origins,
1942                    )? {}
1943                    if let Some(flag) = out.flag_awaiting_value.first() {
1944                        let spelling = flag
1945                            .long
1946                            .first()
1947                            .map(|long| format!("--{long}"))
1948                            .or_else(|| flag.short.first().map(|short| format!("-{short}")))
1949                            .unwrap_or_else(|| flag.name.clone());
1950                        return Err(UsageErr::InvalidFlag {
1951                            token: spelling.clone(),
1952                            reason: "requires an argument".to_string(),
1953                            span: (0, spelling.len()).into(),
1954                            input: spelling,
1955                        }
1956                        .into());
1957                    }
1958                    let name = clause.name.clone();
1959                    if out.cmds.len() == 1 {
1960                        root_clause_separator_seen = true;
1961                    }
1962                    finalize_current_clause(&mut out);
1963                    out.arg_origins.clear();
1964                    trace.record(argv, TokenRole::ClauseSeparator { name });
1965                    next_arg_idx = 0;
1966                    out.flag_awaiting_value.clear();
1967                    reset_clause_scalar_occurrences(&out, &mut scalar_occurrences);
1968                    enable_flags = true;
1969                    seen_double_dash = false;
1970                    continue;
1971                }
1972            }
1973        }
1974
1975        // Check for restart_token - resets argument parsing for multiple command invocations
1976        // e.g., `mise run lint ::: test ::: check` with restart_token=":::"
1977        if let Some(ref restart_token) = out.cmd.restart_token {
1978            if w == *restart_token {
1979                // Reset argument parsing state for a fresh command invocation, keeping the
1980                // flags. `double_dash_violations` is deliberately *not* cleared: `out.errors`
1981                // is not cleared here either, so clearing it would let one arg report the same
1982                // violation once per invocation.
1983                out.args.clear();
1984                // With the values gone, so is where they came from — otherwise the second
1985                // invocation of `run lint ::: test` reports the first one's provenance. The
1986                // token trace is *not* cleared: those words were read, and a report that
1987                // dropped them would show a command line with a hole in it.
1988                out.arg_origins.clear();
1989                trace.record(argv, TokenRole::Restart);
1990                next_arg_idx = cursor_skip_sigils(&out.cmd, 0);
1991                restart_seen = true;
1992                out.flag_awaiting_value.clear(); // Clear any pending flag values
1993                enable_flags = true; // Reset -- separator effect
1994                seen_double_dash = false; // The next invocation needs its own `--`
1995                continue;
1996            }
1997        }
1998
1999        // A flag declared `allow_hyphen_values` takes the next token whatever it looks
2000        // like, and that has to be asked before the separator arm below rather than
2001        // after it. Asked after, a `--` was consumed as a separator while the flag
2002        // stayed hungry, and the flag then ate the word past it: `ex -a -- -x` bound
2003        // `-x` and the separator was simply gone. Asked here, the flag takes the `--`
2004        // itself, which is what clap does with the same declaration — and no flag can
2005        // still be waiting once the separator has done its job, so the starvation rule
2006        // below has no path around it.
2007        if enable_flags
2008            && !attached_continuation
2009            && w.starts_with('-')
2010            && out
2011                .flag_awaiting_value
2012                .last()
2013                .is_some_and(|flag| accepts_detached_flag_value(flag, &w))
2014        {
2015            // A variadic argument collects here too: which token supplied its first
2016            // value says nothing about how many it takes.
2017            let should_return = bind_pending_flag_value(
2018                spec,
2019                &out.cmd,
2020                &mut out.errors,
2021                &mut out.flags,
2022                &mut out.flag_awaiting_value,
2023                &mut w,
2024                &mut input,
2025                custom_env,
2026                trace,
2027                argv,
2028                // The token a hyphen-valued flag takes is the following word, never attached.
2029                false,
2030            )?;
2031            if should_return {
2032                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2033                return Ok((out, overridden_flags));
2034            }
2035            continue;
2036        }
2037
2038        // A flag whose value may be omitted that cannot take this token as a detached
2039        // value finishes bare and leaves the token for whatever comes next:
2040        // `--color --verbose` colours with the missing value and still sets verbose,
2041        // and `--inspect 9229` with `require_equals` binds the missing value rather
2042        // than treating 9229 as the port.
2043        if enable_flags
2044            && !attached_continuation
2045            && !out.flag_awaiting_value.is_empty()
2046            && out.flag_awaiting_value.last().is_some_and(|flag| {
2047                (flag.default_missing.is_some() || flag.value_optional)
2048                    && !accepts_detached_flag_value(flag, &w)
2049            })
2050        {
2051            try_bind_default_missing(
2052                &mut out.flags,
2053                &mut out.flag_awaiting_value,
2054                custom_env,
2055                &mut out.flag_origins,
2056            )?;
2057        }
2058
2059        // The first explicit `--` is still a separator after an `automatic` argument has
2060        // stopped flag parsing. Once an explicit separator has done its job, a second one is
2061        // an ordinary value: every parser worth comparing against keeps it (POSIX getopt,
2062        // argparse, clap, commander, yargs), and jdx/usage#229 was a user reporting the old
2063        // behavior as the bug it is.
2064        if w == "--" && !seen_double_dash {
2065            enable_flags = false;
2066
2067            // Only preserve the double dash token if we're collecting values for a variadic arg
2068            // in double_dash == `preserve` mode
2069            let should_preserve = active_args(&out.cmd)
2070                .get(next_arg_idx)
2071                .map(|arg| arg.var && arg.double_dash == SpecDoubleDashChoices::Preserve)
2072                .unwrap_or(false);
2073
2074            if should_preserve {
2075                // Fall through to arg parsing. This `--` is a *value*, not a separator, so it
2076                // neither counts as one nor unlocks a `double_dash="required"` arg.
2077            } else {
2078                seen_double_dash = true;
2079                trace.record(argv, TokenRole::Separator);
2080
2081                // Everything after an explicit `--` belongs to the arg that requires one, so
2082                // jump the cursor there — past any earlier arg, including a greedy variadic
2083                // that would otherwise swallow the rest. This mirrors clap's `Arg::last(true)`,
2084                // which is what `double_dash="required"` is generated from. Specs without such
2085                // an arg find nothing and keep the cursor where it was.
2086                let target = active_args(&out.cmd).iter().position(|arg| {
2087                    arg.double_dash == SpecDoubleDashChoices::Required
2088                        && !out.args.contains_key(arg)
2089                });
2090                if let Some(target) = target {
2091                    // Forward only. An unfilled required arg declared *before* the cursor is
2092                    // left where it is rather than rewound to — words already assigned to
2093                    // later args would have to be taken back for that to mean anything, and
2094                    // the arg keeps its `MissingArg`. `double_dash="required"` mirrors clap's
2095                    // `Arg::last(true)`, which is the final positional, so a spec that puts
2096                    // one ahead of others is already outside what this models.
2097                    if target > next_arg_idx {
2098                        next_arg_idx = target;
2099                    }
2100                }
2101                continue;
2102            }
2103        }
2104
2105        // long flags
2106        if enable_flags && w.starts_with("--") {
2107            grouped_flag = false;
2108            // `Some` only when an `=` was actually written, so `--jobs=` can supply
2109            // an empty value while `--jobs` supplies none. Collapsing the two lost
2110            // the flag entirely.
2111            let split = w.split_once('=');
2112            let word = split.map(|(word, _)| word).unwrap_or(&w);
2113            let bound_flag = binding.as_ref().map(|(flag, _)| flag);
2114            if let Some(f) = bound_flag.or_else(|| out.available_flags.get(word)) {
2115                let command_level = binding
2116                    .as_ref()
2117                    .map(|(_, level)| *level)
2118                    .unwrap_or(out.cmds.len() - 1);
2119                parsed_flag_spellings
2120                    .entry(Arc::as_ptr(f) as usize)
2121                    .or_default()
2122                    .insert(word.to_string());
2123                // Recorded before the action check below: a token that named a flag named it
2124                // whether or not the parse can carry on afterwards.
2125                trace.record(
2126                    argv,
2127                    TokenRole::Flag {
2128                        flag: Arc::clone(f),
2129                        spelling: word.to_string(),
2130                        negated: f.negate.as_deref() == Some(word),
2131                    },
2132                );
2133                if f.action != crate::SpecFlagAction::Set {
2134                    out.errors.push(render_action_err(spec, &out.cmd, f, word));
2135                    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2136                    return Ok((out, overridden_flags));
2137                }
2138                apply_flag_overrides(
2139                    f,
2140                    &out.available_flags,
2141                    &mut out.flags,
2142                    &mut out.flag_awaiting_value,
2143                    &mut overridden_flags,
2144                    &mut out.overridden_flags,
2145                );
2146                if let Some(pending) = out.flag_awaiting_value.first() {
2147                    out.errors.push(render_missing_flag_value(pending, &w));
2148                    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2149                    return Ok((out, overridden_flags));
2150                }
2151                // An attached value only means something to a flag that takes one:
2152                // `--jobs=` is an empty string, while `--force=yes` has nothing to
2153                // give a flag that holds no value. Handing that leftover to the
2154                // positionals would re-split one token into two, so `ex --force=yes`
2155                // would fill an argument the caller never typed a word for.
2156                if f.arg.is_some() {
2157                    record_scalar_flag_occurrence(
2158                        &out.cmds,
2159                        f,
2160                        command_level,
2161                        None,
2162                        &mut scalar_occurrences,
2163                        &mut out.errors,
2164                    );
2165                    let f = Arc::clone(f);
2166                    out.flag_awaiting_value.push(Arc::clone(&f));
2167                    // The `=` has already settled that this text is the value, so it
2168                    // binds here rather than going back on the queue to be read as a
2169                    // token again — where `--jobs=--force` looked like a flag of its
2170                    // own and bound `force`, leaving `jobs` unset.
2171                    if let Some((_, val)) = split {
2172                        // The `=` settles where the *first* value came from and nothing
2173                        // more, so a variadic argument goes on collecting from the words
2174                        // after it exactly as the detached form does.
2175                        let mut val = val.to_string();
2176                        let should_return = bind_pending_flag_value(
2177                            spec,
2178                            &out.cmd,
2179                            &mut out.errors,
2180                            &mut out.flags,
2181                            &mut out.flag_awaiting_value,
2182                            &mut val,
2183                            &mut input,
2184                            custom_env,
2185                            trace,
2186                            argv,
2187                            // The `=` settled that this text is the value, so it rode in on
2188                            // the flag's own token.
2189                            true,
2190                        )?;
2191                        if should_return {
2192                            record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2193                            return Ok((out, overridden_flags));
2194                        }
2195                    }
2196                } else if f.count {
2197                    let arr = out
2198                        .flags
2199                        .entry(Arc::clone(f))
2200                        .or_insert_with(|| ParseValue::MultiBool(vec![]))
2201                        .try_as_multi_bool_mut()
2202                        .unwrap();
2203                    arr.push(true);
2204                } else {
2205                    let negate = f.negate.clone().unwrap_or_default();
2206                    let negated_form = word == negate;
2207                    let value = if f.bool_value {
2208                        match split.map(|(_, value)| value) {
2209                            Some("true") => !negated_form,
2210                            Some("false") => negated_form,
2211                            Some(value) => {
2212                                out.errors.push(UsageErr::InvalidValue {
2213                                    name: f.name.clone(),
2214                                    value: value.to_string(),
2215                                    reason: "expected `true` or `false`".to_string(),
2216                                });
2217                                continue;
2218                            }
2219                            None => !negated_form,
2220                        }
2221                    } else {
2222                        !negated_form
2223                    };
2224                    // Which form was typed is a question about the name, so it is
2225                    // asked of `word` rather than the whole token: the attached value
2226                    // is dropped just above, and comparing `--no-color=yes` against
2227                    // `--no-color` would take the negation down with it.
2228                    record_scalar_flag_occurrence(
2229                        &out.cmds,
2230                        f,
2231                        command_level,
2232                        Some(!negated_form),
2233                        &mut scalar_occurrences,
2234                        &mut out.errors,
2235                    );
2236                    out.flags.insert(Arc::clone(f), ParseValue::Bool(value));
2237                }
2238                continue;
2239            }
2240            if is_help_arg(spec, &out.cmd, &w) {
2241                out.errors
2242                    .push(render_help_err(spec, &out.cmd, w.len() > 2));
2243                trace.record(
2244                    argv,
2245                    TokenRole::Builtin {
2246                        spelling: w.clone(),
2247                    },
2248                );
2249                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2250                return Ok((out, overridden_flags));
2251            }
2252            if is_version_arg(spec, &out.cmds, &w) {
2253                out.errors.push(render_version_err(spec, w.len() > 2));
2254                trace.record(
2255                    argv,
2256                    TokenRole::Builtin {
2257                        spelling: w.clone(),
2258                    },
2259                );
2260                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2261                return Ok((out, overridden_flags));
2262            }
2263            if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
2264                trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
2265                trace.close(&input);
2266                return Err(refused.into());
2267            }
2268        }
2269
2270        // short flags
2271        //
2272        // A fresh token is checked whole before any of it is applied: `-az` with only
2273        // `-a` declared is not a bundle at all, so it must not set `a` on the way to
2274        // discovering that `z` names nothing. A grouped continuation is exempt — its
2275        // token was already checked when it arrived.
2276        let declared_numeric_short = declared_numeric_short(&out.available_flags, &w);
2277        let positional_negative_number = !declared_numeric_short
2278            && is_negative_number(&w)
2279            && active_args(&out.cmd)
2280                .get(next_arg_idx)
2281                .is_some_and(|arg| arg.allow_negative_numbers);
2282        if enable_flags
2283            && !grouped_flag
2284            // A word phase 1 already resolved to a flag needs no re-checking, and
2285            // the flags in scope have changed since, so re-checking would be wrong.
2286            && binding.is_none()
2287            && w.starts_with('-')
2288            && w.len() > 1
2289            && is_flag_like(&w)
2290            && !positional_negative_number
2291            && !short_bundle_is_known(spec, &out.cmds, &out.available_flags, &w)
2292        {
2293            // Refused if this command asked for that; otherwise it carries on below
2294            // as one word, with none of its letters applied.
2295            if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
2296                trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
2297                trace.close(&input);
2298                return Err(refused.into());
2299            }
2300        } else if enable_flags && !positional_negative_number && w.starts_with('-') && w.len() > 1 {
2301            let short = w.chars().nth(1).unwrap();
2302            if let Some(f) = binding
2303                .as_ref()
2304                .map(|(flag, _)| flag)
2305                .or_else(|| out.available_flags.get(&format!("-{short}")))
2306            {
2307                let command_level = binding
2308                    .as_ref()
2309                    .map(|(_, level)| *level)
2310                    .unwrap_or(out.cmds.len() - 1);
2311                if f.action != crate::SpecFlagAction::Set {
2312                    out.errors
2313                        .push(render_action_err(spec, &out.cmd, f, &format!("-{short}")));
2314                    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2315                    return Ok((out, overridden_flags));
2316                }
2317                parsed_flag_spellings
2318                    .entry(Arc::as_ptr(f) as usize)
2319                    .or_default()
2320                    .insert(format!("-{short}"));
2321                trace.record(
2322                    argv,
2323                    TokenRole::Flag {
2324                        flag: Arc::clone(f),
2325                        spelling: format!("-{short}"),
2326                        // A short spelling is never the negated form: `negate` is a long.
2327                        negated: false,
2328                    },
2329                );
2330                apply_flag_overrides(
2331                    f,
2332                    &out.available_flags,
2333                    &mut out.flags,
2334                    &mut out.flag_awaiting_value,
2335                    &mut overridden_flags,
2336                    &mut out.overridden_flags,
2337                );
2338                if !attached_continuation {
2339                    if let Some(pending) = out.flag_awaiting_value.first() {
2340                        out.errors.push(render_missing_flag_value(pending, &w));
2341                        record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2342                        return Ok((out, overridden_flags));
2343                    }
2344                }
2345                let rest = &w[1 + short.len_utf8()..];
2346                if !rest.is_empty() {
2347                    // `-abc` is one token that names three flags, so the tail is read at the
2348                    // bundle's own position rather than at one of its own.
2349                    let mut tail = Token::new(format!("-{rest}"), argv);
2350                    if f.arg.is_none() {
2351                        tail.binding = default_bundle
2352                            .as_ref()
2353                            .filter(|bundle| bundle.argv == argv)
2354                            .and_then(|bundle| bundle.binding(get_flag_key(&tail.word)));
2355                    }
2356                    input.push_front(tail);
2357                }
2358                // A fully consumed short is no longer a grouped continuation.
2359                // Leaving this set after `-ai` made `-i` skip `require_equals`
2360                // and bind the following word.
2361                grouped_flag = !rest.is_empty();
2362                if f.arg.is_some() {
2363                    record_scalar_flag_occurrence(
2364                        &out.cmds,
2365                        f,
2366                        command_level,
2367                        None,
2368                        &mut scalar_occurrences,
2369                        &mut out.errors,
2370                    );
2371                    out.flag_awaiting_value.push(Arc::clone(f));
2372                } else if f.count {
2373                    let arr = out
2374                        .flags
2375                        .entry(Arc::clone(f))
2376                        .or_insert_with(|| ParseValue::MultiBool(vec![]))
2377                        .try_as_multi_bool_mut()
2378                        .unwrap();
2379                    arr.push(true);
2380                } else {
2381                    let negate = f.negate.clone().unwrap_or_default();
2382                    let value = w != negate;
2383                    record_scalar_flag_occurrence(
2384                        &out.cmds,
2385                        f,
2386                        command_level,
2387                        Some(value),
2388                        &mut scalar_occurrences,
2389                        &mut out.errors,
2390                    );
2391                    out.flags.insert(Arc::clone(f), ParseValue::Bool(value));
2392                }
2393                continue;
2394            }
2395            // The letter nothing declared may still be one the parser supplies, and it may
2396            // sit anywhere in the token: `-hv` asks for help as surely as `-vh` does, and
2397            // neither reaches the whole-token spellings below.
2398            if let Some(err) = supplied_short(spec, &out.cmds, short) {
2399                out.errors.push(err);
2400                trace.record(
2401                    argv,
2402                    TokenRole::Builtin {
2403                        spelling: format!("-{short}"),
2404                    },
2405                );
2406                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2407                return Ok((out, overridden_flags));
2408            }
2409            if is_help_arg(spec, &out.cmd, &w) {
2410                out.errors
2411                    .push(render_help_err(spec, &out.cmd, w.len() > 2));
2412                trace.record(
2413                    argv,
2414                    TokenRole::Builtin {
2415                        spelling: w.clone(),
2416                    },
2417                );
2418                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2419                return Ok((out, overridden_flags));
2420            }
2421            if is_version_arg(spec, &out.cmds, &w) {
2422                out.errors.push(render_version_err(spec, w.len() > 2));
2423                trace.record(
2424                    argv,
2425                    TokenRole::Builtin {
2426                        spelling: w.clone(),
2427                    },
2428                );
2429                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2430                return Ok((out, overridden_flags));
2431            }
2432            if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
2433                trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
2434                trace.close(&input);
2435                return Err(refused.into());
2436            }
2437            if grouped_flag {
2438                grouped_flag = false;
2439                w.remove(0);
2440                // What is left is a short flag's attached value, and one `=` between
2441                // the letter and the value is a separator: `-j=8` means 8. Only one,
2442                // so `-j==8` still means `=8`.
2443                if !out.flag_awaiting_value.is_empty() && w.starts_with('=') {
2444                    w.remove(0);
2445                }
2446            }
2447        }
2448
2449        // Only while flags are still being read. A flag still waiting when the separator
2450        // was consumed is starved: its value would have to come from after the `--`,
2451        // where every token is data. Draining there gave `ex --jobs -- x` the word after
2452        // the separator, so the command line quietly meant `ex --jobs=x` and the `--`
2453        // was gone. Left waiting, it is reported as the missing value it is.
2454        // `require_equals` refuses a detached value: `--flag value` is a missing
2455        // value, not a flag of `"value"`. The attached form is still bound above.
2456        // Reported here rather than left waiting until the end of the line: falling
2457        // through would offer `value` to the positionals and call it an unexpected
2458        // word, which is the wrong error and a different one from usage-argv.
2459        if enable_flags
2460            && !attached_continuation
2461            && !out.flag_awaiting_value.is_empty()
2462            && out
2463                .flag_awaiting_value
2464                .last()
2465                .is_some_and(|flag| flag.require_equals)
2466        {
2467            let flag = out.flag_awaiting_value.last().unwrap();
2468            let token = flag
2469                .long
2470                .first()
2471                .map(|l| format!("--{l}"))
2472                .or_else(|| flag.short.first().map(|s| format!("-{s}")))
2473                .unwrap_or_else(|| flag.name.clone());
2474            out.errors.push(UsageErr::InvalidFlag {
2475                token: token.clone(),
2476                reason: "requires an argument".to_string(),
2477                span: (0, 0).into(),
2478                input: format!("{token} {w}"),
2479            });
2480            record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2481            return Ok((out, overridden_flags));
2482        }
2483        if enable_flags
2484            && !out.flag_awaiting_value.is_empty()
2485            && (attached_continuation
2486                || out
2487                    .flag_awaiting_value
2488                    .last()
2489                    .is_some_and(|flag| accepts_detached_flag_value(flag, &w)))
2490        {
2491            // Held before the drain pops it: a flag whose argument is variadic keeps
2492            // taking values after this first one.
2493            let should_return = bind_pending_flag_value(
2494                spec,
2495                &out.cmd,
2496                &mut out.errors,
2497                &mut out.flags,
2498                &mut out.flag_awaiting_value,
2499                &mut w,
2500                &mut input,
2501                custom_env,
2502                trace,
2503                argv,
2504                attached_continuation,
2505            )?;
2506            if should_return {
2507                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2508                return Ok((out, overridden_flags));
2509            }
2510            continue;
2511        }
2512
2513        if let Some((arg, sigil, value)) = (enable_flags && !restart_seen)
2514            .then(|| match_sigil_arg_chain(&out.cmds, &w))
2515            .flatten()
2516        {
2517            if value.is_empty() {
2518                out.errors.push(UsageErr::InvalidValue {
2519                    name: arg.name.clone(),
2520                    value: w.clone(),
2521                    reason: format!("expected a value after sigil {sigil:?}"),
2522                });
2523                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2524                return Ok((out, overridden_flags));
2525            }
2526            let trailing_value = arg.double_dash == SpecDoubleDashChoices::Automatic;
2527            let suppress_trailing_delimiter =
2528                out.cmds.iter().any(|cmd| cmd.dont_delimit_trailing_values);
2529            let delimiter = if suppress_trailing_delimiter && trailing_value {
2530                None
2531            } else {
2532                arg.delimiter
2533            };
2534            let parts = match delimiter {
2535                Some(delimiter) => value
2536                    .split(delimiter)
2537                    .map(str::to_string)
2538                    .collect::<Vec<_>>(),
2539                None => vec![value.to_string()],
2540            };
2541            let mut refused = false;
2542            for part in &parts {
2543                if validate_choices(
2544                    spec,
2545                    &out.cmd,
2546                    &mut out.errors,
2547                    ChoiceTarget::arg(arg),
2548                    part,
2549                    arg.choices.as_ref(),
2550                    custom_env,
2551                )? {
2552                    refused = true;
2553                    break;
2554                }
2555            }
2556            if refused {
2557                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2558                return Ok((out, overridden_flags));
2559            }
2560            trace.record(
2561                argv,
2562                TokenRole::Sigil {
2563                    arg: Arc::new(arg.clone()),
2564                    sigil: sigil.to_string(),
2565                    values: parts.clone(),
2566                },
2567            );
2568            let key = Arc::new(arg.clone());
2569            if arg.var {
2570                let arr = out
2571                    .args
2572                    .entry(key)
2573                    .or_insert_with(|| ParseValue::MultiString(vec![]))
2574                    .try_as_multi_string_mut()
2575                    .unwrap();
2576                arr.extend(parts);
2577            } else {
2578                out.args.insert(key, ParseValue::String(value.to_string()));
2579            }
2580            if out.cmds.len() == 1 {
2581                root_positional_arg_found = true;
2582            }
2583            continue;
2584        }
2585
2586        if out.cmd.allow_missing_positional {
2587            next_arg_idx = cursor_skip_sigils(&out.cmd, next_arg_idx);
2588            while let Some(current) = active_args(&out.cmd).get(next_arg_idx) {
2589                if current.required || out.args.contains_key(current) {
2590                    break;
2591                }
2592                let required_after = active_args(&out.cmd)[next_arg_idx + 1..]
2593                    .iter()
2594                    .filter(|arg| arg.required && arg.sigil.is_none())
2595                    .count();
2596                if required_after == 0 {
2597                    break;
2598                }
2599                let remaining_values = 1 + input
2600                    .iter()
2601                    .filter(|token| {
2602                        (!enable_flags || !is_flag_like(&token.word))
2603                            && (!enable_flags
2604                                || restart_seen
2605                                || match_sigil_arg_chain(&out.cmds, &token.word).is_none())
2606                    })
2607                    .count();
2608                if remaining_values > required_after {
2609                    break;
2610                }
2611                next_arg_idx = cursor_skip_sigils(&out.cmd, next_arg_idx + 1);
2612            }
2613        }
2614
2615        if let Some(arg) = active_args(&out.cmd).get(next_arg_idx) {
2616            if arg.var
2617                && out.args.contains_key(arg)
2618                && arg.value_terminator.as_deref() == Some(w.as_str())
2619            {
2620                trace.record(
2621                    argv,
2622                    TokenRole::ValueTerminator {
2623                        ends: arg.name.clone(),
2624                    },
2625                );
2626                next_arg_idx += 1;
2627                continue;
2628            }
2629            // Before anything else: an arg that requires `--` accepts nothing until one has been
2630            // seen. Checking ahead of `validate_choices` keeps a discarded word from also being
2631            // reported as an invalid choice, and from reaching that function's help escape.
2632            if arg.double_dash == SpecDoubleDashChoices::Required && !seen_double_dash {
2633                report_double_dash_violation(arg, &mut out.errors, &mut double_dash_violations);
2634                trace.record(
2635                    argv,
2636                    TokenRole::Refused {
2637                        reason: format!("{} only accepts words after `--`", arg.name),
2638                    },
2639                );
2640                // Drop the word without filling the arg or advancing the cursor: every later
2641                // word hits the same arg and is rejected the same way, so the parse still ends
2642                // in an error rather than in `unexpected word`.
2643                continue;
2644            }
2645            // Split before judging, as the flag path does: after the split the word is
2646            // no longer one value, and `choices` has to be asked about each. Judging
2647            // first rejects `src:docs` against a list that both halves are on, and
2648            // names the whole word rather than the half that was wrong.
2649            let trailing_value =
2650                seen_double_dash || arg.double_dash == SpecDoubleDashChoices::Automatic;
2651            let suppress_trailing_delimiter =
2652                out.cmds.iter().any(|cmd| cmd.dont_delimit_trailing_values);
2653            let delimiter = if suppress_trailing_delimiter && trailing_value {
2654                None
2655            } else {
2656                arg.delimiter
2657            };
2658            let parts: Vec<String> = match delimiter {
2659                Some(delimiter) => w.split(delimiter).map(str::to_string).collect(),
2660                None => vec![w.clone()],
2661            };
2662            let mut refused = false;
2663            for part in &parts {
2664                if validate_choices(
2665                    spec,
2666                    &out.cmd,
2667                    &mut out.errors,
2668                    ChoiceTarget::arg(arg),
2669                    part,
2670                    arg.choices.as_ref(),
2671                    custom_env,
2672                )? {
2673                    refused = true;
2674                    break;
2675                }
2676            }
2677            if refused {
2678                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2679                return Ok((out, overridden_flags));
2680            }
2681            // `double_dash="automatic"` means the first value this arg takes is the last
2682            // token read as anything but data: a wrapper declaring it can forward flags
2683            // without its caller typing a `--`. Set before the value is stored, so the
2684            // rest of the command line is already past flag parsing.
2685            if arg.double_dash == SpecDoubleDashChoices::Automatic {
2686                enable_flags = false;
2687            }
2688            // A flag-like word reaching a positional while flags are still being read was
2689            // offered to every declaration and matched none: under the default
2690            // `unknown_flags="value"` it becomes data, and saying so is the difference
2691            // between "you have a typo" and "this argument took your typo".
2692            let unknown_flag = enable_flags
2693                && !positional_negative_number
2694                && is_flag_like(&w)
2695                && binding.is_none();
2696            trace.record(
2697                argv,
2698                if unknown_flag {
2699                    TokenRole::UnknownFlag {
2700                        bound_as: Some(Arc::new(arg.clone())),
2701                    }
2702                } else {
2703                    TokenRole::Arg {
2704                        arg: Arc::new(arg.clone()),
2705                        values: parts.clone(),
2706                    }
2707                },
2708            );
2709            if out.cmds.len() == 1 {
2710                root_positional_arg_found = true;
2711            }
2712            if arg.var {
2713                let arr = out
2714                    .args
2715                    .entry(Arc::new(arg.clone()))
2716                    .or_insert_with(|| ParseValue::MultiString(vec![]))
2717                    .try_as_multi_string_mut()
2718                    .unwrap();
2719                // The values this word carried, split above so that everything
2720                // downstream — `choices`, `var_max` stopping the collection, `var_min` —
2721                // counts the values the user meant rather than the words they typed.
2722                arr.extend(parts.iter().cloned());
2723                if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
2724                    next_arg_idx += 1;
2725                }
2726            } else {
2727                out.args
2728                    .insert(Arc::new(arg.clone()), ParseValue::String(w));
2729                next_arg_idx += 1;
2730            }
2731            if out
2732                .cmd
2733                .clause
2734                .as_ref()
2735                .is_some_and(|clause| clause.separator.is_none())
2736                && next_arg_idx >= active_args(&out.cmd).len()
2737            {
2738                while try_bind_default_missing(
2739                    &mut out.flags,
2740                    &mut out.flag_awaiting_value,
2741                    custom_env,
2742                    &mut out.flag_origins,
2743                )? {}
2744                if let Some(flag) = out.flag_awaiting_value.first() {
2745                    let spelling = flag
2746                        .long
2747                        .first()
2748                        .map(|long| format!("--{long}"))
2749                        .or_else(|| flag.short.first().map(|short| format!("-{short}")))
2750                        .unwrap_or_else(|| flag.name.clone());
2751                    return Err(UsageErr::InvalidFlag {
2752                        token: spelling.clone(),
2753                        reason: "requires an argument".to_string(),
2754                        span: (0, spelling.len()).into(),
2755                        input: spelling,
2756                    }
2757                    .into());
2758                }
2759                let name = out.cmd.clause.as_ref().unwrap().name.clone();
2760                finalize_current_clause(&mut out);
2761                out.arg_origins.clear();
2762                trace.record(argv, TokenRole::ClauseSeparator { name });
2763                next_arg_idx = 0;
2764                out.flag_awaiting_value.clear();
2765                reset_clause_scalar_occurrences(&out, &mut scalar_occurrences);
2766                enable_flags = true;
2767                seen_double_dash = false;
2768            }
2769            continue;
2770        }
2771        if is_help_arg(spec, &out.cmd, &w) {
2772            out.errors
2773                .push(render_help_err(spec, &out.cmd, w.len() > 2));
2774            trace.record(
2775                argv,
2776                TokenRole::Builtin {
2777                    spelling: w.clone(),
2778                },
2779            );
2780            record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2781            return Ok((out, overridden_flags));
2782        }
2783        if is_version_arg(spec, &out.cmds, &w) {
2784            out.errors.push(render_version_err(spec, w.len() > 2));
2785            trace.record(
2786                argv,
2787                TokenRole::Builtin {
2788                    spelling: w.clone(),
2789                },
2790            );
2791            record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2792            return Ok((out, overridden_flags));
2793        }
2794        trace.record(
2795            argv,
2796            TokenRole::Refused {
2797                reason: "no declaration takes this word".to_string(),
2798            },
2799        );
2800        trace.close(&input);
2801        bail!("unexpected word: {w}");
2802    }
2803
2804    // Resolve optional defaults before deciding whether argv is empty. This is deliberately
2805    // inside the opt-in gate: completion's partial parser must retain its pending value when
2806    // the feature is disabled.
2807    if spec.default_subcommand_on_empty
2808        && spec.default_subcommand.is_some()
2809        && out.cmds.len() == 1
2810        && !root_positional_arg_found
2811        && !seen_double_dash
2812        && !root_clause_separator_seen
2813        && out.errors.is_empty()
2814    {
2815        while try_bind_default_missing(
2816            &mut out.flags,
2817            &mut out.flag_awaiting_value,
2818            custom_env,
2819            &mut out.flag_origins,
2820        )? {}
2821        if !out.flag_awaiting_value.is_empty() {
2822            // A required flag value is still pending; leave it for full-parse diagnostics or
2823            // completion rather than routing to a synthetic child.
2824        } else if let Some(subcommand) = out
2825            .cmd
2826            .find_subcommand(spec.default_subcommand.as_deref().unwrap())
2827        {
2828            if out.cmd.args_conflicts_with_subcommands && command_arg_found {
2829                bail!(
2830                    "subcommand '{}' cannot be used with arguments on its parent command",
2831                    subcommand.name
2832                );
2833            }
2834            let mut subcommand = subcommand.clone();
2835            subcommand.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
2836            let crossing_mount = subcommand.mounted && !out.cmd.mounted;
2837            merge_subcommand_flags(
2838                &mut out.available_flags,
2839                gather_flags(&subcommand),
2840                crossing_mount,
2841            );
2842            out.cmds.push(subcommand.clone());
2843            out.cmd = subcommand;
2844            command_has_argv = false;
2845            prefix_flags.clear();
2846            next_arg_idx = cursor_skip_sigils(&out.cmd, 0);
2847        }
2848    }
2849
2850    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2851    if validate_clauses {
2852        validate_clause_relationships(&mut out, &overridden_flags, custom_env, None);
2853    }
2854    let clause_flag_names = out
2855        .cmd
2856        .clause
2857        .iter()
2858        .flat_map(|clause| &clause.flags)
2859        .map(|flag| flag.name.clone())
2860        .collect::<HashSet<_>>();
2861
2862    // `out.flags` is keyed by `SpecFlag`, whose equality is intentionally name-only. Two
2863    // declarations with the same canonical name therefore share one public value entry even
2864    // when both were typed. The spelling ledger is keyed by declaration identity and retains
2865    // both, which is what exclusivity needs.
2866    let flag_was_parsed =
2867        |flag: &Arc<SpecFlag>| parsed_flag_spellings.contains_key(&(Arc::as_ptr(flag) as usize));
2868
2869    // The spellings the selected command's own declaration speaks for, on this object.
2870    //
2871    // Empty unless that declaration really is this object's: a parent and child may each
2872    // declare `--clean` without merging, leaving two flags that share a name, and the
2873    // ancestor's must not be read as the child's. The test is whether every spelling the child
2874    // declared resolves back here — true of a merged flag, and of a plain local one, but not of
2875    // an ancestor whose long form the child took over.
2876    let child_spellings = |flag: &Arc<SpecFlag>| -> HashSet<String> {
2877        let declared: HashSet<String> = out
2878            .cmd
2879            .flags
2880            .iter()
2881            .filter(|declared| declared.name == flag.name)
2882            .flat_map(flag_keys)
2883            .collect();
2884        // *Any* of them, not all. All was too strong: a child may declare a spelling that
2885        // some other inherited global already owns — `-c --clean` beside an inherited
2886        // `-c --config` — and that collision is resolved in the other global's favor, so the
2887        // child's `-c` resolves elsewhere. Requiring every spelling to land here let one
2888        // unrelated collision disown the child from the `--clean` it plainly does own.
2889        //
2890        // Still enough to tell the two-object case apart, which is what this guards: when a
2891        // child re-declares a global as global, the child's own spellings resolve to the
2892        // child's separate flag, so none of them lands on the ancestor's.
2893        let speaks_for_this_flag = declared.iter().any(|spelling| {
2894            out.available_flags
2895                .get(spelling)
2896                .is_some_and(|available| Arc::ptr_eq(available, flag))
2897        });
2898        if speaks_for_this_flag {
2899            declared
2900        } else {
2901            HashSet::new()
2902        }
2903    };
2904
2905    // Whose `exclusive` an occurrence activates, as `(the child's, an ancestor's)`.
2906    //
2907    // A child that re-declares an inherited global merges into one object answering to two
2908    // alias sets whose declarations may disagree, so there is no single owner to name: the
2909    // child owns the spellings it declared and the ancestor keeps the ones only it declared.
2910    // Both sides can be in play at once — `run -c --clean` is the ancestor's alias and the
2911    // child's in one invocation — and each carries its own declaration's answer.
2912    let exclusivity_in_play = |flag: &Arc<SpecFlag>| -> (bool, bool) {
2913        let child = child_spellings(flag);
2914        let child_exclusive = !child.is_empty()
2915            && out
2916                .cmd
2917                .flags
2918                .iter()
2919                .any(|declared| declared.name == flag.name && declared.exclusive);
2920        match parsed_flag_spellings.get(&(Arc::as_ptr(flag) as usize)) {
2921            Some(spellings) => (
2922                child_exclusive && spellings.iter().any(|s| child.contains(s)),
2923                flag.exclusive && spellings.iter().any(|s| !child.contains(s)),
2924            ),
2925            // An environment value has no spelling to attribute it by. The declaration the
2926            // selected command has in scope is the one that answers — which is the child's
2927            // when it re-declared the flag, and the ancestor's when it did not.
2928            None => (child_exclusive, flag.exclusive && child.is_empty()),
2929        }
2930    };
2931
2932    let exclusive_occurrence = |flag: &Arc<SpecFlag>| {
2933        let (child, ancestor) = exclusivity_in_play(flag);
2934        child || ancestor
2935    };
2936
2937    // clap's `exclusive` is also an escape from requiredness: `--version` has to work on a
2938    // command that otherwise needs an input. Companions are still diagnosed below, but an
2939    // exclusive occurrence suppresses the missing-value checks that would make it unusable
2940    // whether it was alone or not.
2941    let exclusive_present = unique_flags(out.available_flags.values().chain(out.flags.keys()))
2942        .filter(|flag| !clause_flag_names.contains(&flag.name))
2943        .any(|flag| {
2944            exclusive_occurrence(flag)
2945                && !overridden_flags.contains(&flag.name)
2946                && (flag_was_parsed(flag) || flag_has_env(flag, custom_env))
2947        });
2948    let requirements_apply = |command_index: usize| {
2949        command_index + 1 == out.cmds.len() || !out.cmds[command_index].subcommand_negates_reqs
2950    };
2951
2952    if out.cmd.arg_required_else_help && !command_has_argv {
2953        out.errors.push(render_help_err(spec, &out.cmd, false));
2954    }
2955
2956    // A command that says it needs a subcommand, given none. Checked on `out.cmd` and nowhere
2957    // else, because `out.cmd` *is* the command the words reached: had a subcommand been taken,
2958    // the child would be here instead. The spec has carried `subcommand_required` since it was
2959    // added for the derive, and this parser never read it — so `mise generate` parsed as a
2960    // complete invocation while usage-argv and clap both refused it.
2961    if out.cmd.subcommand_required && !out.cmd.subcommands.is_empty() && out.external.is_none() {
2962        let mut names: Vec<&str> = out
2963            .cmd
2964            .subcommands
2965            .iter()
2966            // Aliases share a map entry with the name they point at; listing both would offer
2967            // the same command twice under two spellings.
2968            .filter(|(name, sub)| sub.name == **name && !sub.hide)
2969            .map(|(name, _)| name.as_str())
2970            .collect();
2971        names.sort_unstable();
2972        out.errors.push(UsageErr::MissingSubcommand(
2973            out.cmd.name.clone(),
2974            names.join(", "),
2975        ));
2976    }
2977
2978    // Not `skip(out.args.len())`: a `--` may have jumped the cursor past an arg that stayed
2979    // empty, so position and fill count can disagree. Ask `out.args` which args it holds.
2980    if !exclusive_present {
2981        for arg in out
2982            .cmds
2983            .iter()
2984            .enumerate()
2985            .filter(|(index, _)| requirements_apply(*index))
2986            .flat_map(|(_, cmd)| &cmd.args)
2987        {
2988            if out.args.contains_key(arg) {
2989                continue;
2990            }
2991            // Already reported as needing a `--`; one mistake should not yield two messages.
2992            if double_dash_violations.contains(&arg.name) {
2993                continue;
2994            }
2995            let required_if = arg.required_if.iter().any(|selector| {
2996                selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2997            });
2998            let required_if_eq = arg.required_if_eq.iter().any(|condition| {
2999                selector_explicit_has_value(
3000                    &condition.selector,
3001                    &condition.value,
3002                    &out,
3003                    &overridden_flags,
3004                    custom_env,
3005                )
3006            });
3007            let required_if_eq_all = !arg.required_if_eq_all.is_empty()
3008                && arg.required_if_eq_all.iter().all(|condition| {
3009                    selector_explicit_has_value(
3010                        &condition.selector,
3011                        &condition.value,
3012                        &out,
3013                        &overridden_flags,
3014                        custom_env,
3015                    )
3016                });
3017            let unless_any = arg.required_unless.iter().any(|selector| {
3018                selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3019            });
3020            let unless_all = !arg.required_unless_all.is_empty()
3021                && arg.required_unless_all.iter().all(|selector| {
3022                    selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3023                });
3024            let required_unless = !(unless_any
3025                || unless_all
3026                || (arg.required_unless.is_empty() && arg.required_unless_all.is_empty()));
3027            if (arg.required
3028                || required_if
3029                || required_if_eq
3030                || required_if_eq_all
3031                || required_unless)
3032                && arg.default.is_empty()
3033            {
3034                // Check if there's an env var available (custom env map takes precedence)
3035                let has_env = arg
3036                    .env
3037                    .as_ref()
3038                    .is_some_and(|env_var| env_contains(custom_env, env_var));
3039                if !has_env {
3040                    out.errors.push(UsageErr::MissingArg(arg.name.clone()));
3041                }
3042            }
3043        }
3044    }
3045
3046    // Conflicts are a question about the invocation as a whole rather than about any one
3047    // token, so they are checked here beside the requirement checks rather than at the
3048    // point a flag is matched — the flag it conflicts with may still be ahead of it.
3049    // Its own loop: the requirement loop below skips the flags that *were* given, which
3050    // is exactly the set this needs.
3051    //
3052    // A value from the environment counts on both sides, matching what
3053    // `selector_is_explicit` says about the other flag: the question is whether a flag
3054    // has a value, not how it got one. That is what clap does, and an asymmetric rule
3055    // would make the same pair of flags a conflict or not depending on which one
3056    // happened to be typed.
3057    for flag in unique_flags(out.available_flags.values())
3058        .filter(|flag| !clause_flag_names.contains(&flag.name))
3059    {
3060        let given = out.flags.contains_key(flag) || flag_has_env(flag, custom_env);
3061        if !given || overridden_flags.contains(&flag.name) {
3062            continue;
3063        }
3064        for other in &flag.conflicts {
3065            if selector_is_explicit(other, &out, &overridden_flags, custom_env) {
3066                out.errors.push(UsageErr::InvalidFlag {
3067                    token: format!("--{}", flag.name),
3068                    reason: format!("conflicts with {other}"),
3069                    span: (0, 0).into(),
3070                    input: format!("--{} {other}", flag.name),
3071                });
3072            }
3073        }
3074        // The positive form, checked in the same pass and under the same rule: a value
3075        // from the environment satisfies a requirement, because the question is whether
3076        // the other flag has a value rather than how it got one. A flag that was
3077        // overridden away has not been given, so it cannot satisfy anything either —
3078        // which is what `selector_is_explicit` already accounts for.
3079        //
3080        // Reported as the missing flag rather than as something wrong with the flag that
3081        // named it, which is what clap says too: an unmet `requires` is a required
3082        // argument that was not provided. Named by its own name, resolved through the
3083        // same matcher, so a `requires="-f"` reports `--force` rather than the selector.
3084        let owner = out
3085            .cmds
3086            .iter()
3087            .rposition(|cmd| cmd.flags.iter().any(|declared| declared.name == flag.name))
3088            .unwrap_or(out.cmds.len() - 1);
3089        if !exclusive_present && requirements_apply(owner) {
3090            for other in &flag.requires {
3091                if !selector_is_satisfied(other, &out, &overridden_flags, custom_env) {
3092                    let name = selector_flag_name(other, &out).unwrap_or_else(|| other.clone());
3093                    if other.starts_with('-') {
3094                        out.errors.push(UsageErr::MissingFlag(name));
3095                    } else {
3096                        out.errors.push(UsageErr::MissingArg(name));
3097                    }
3098                }
3099            }
3100            for condition in &flag.requires_if {
3101                if explicit_flag_has_value(flag, &condition.value, &out, custom_env)
3102                    && !selector_is_satisfied(
3103                        &condition.requires,
3104                        &out,
3105                        &overridden_flags,
3106                        custom_env,
3107                    )
3108                {
3109                    let name = selector_flag_name(&condition.requires, &out)
3110                        .unwrap_or_else(|| condition.requires.clone());
3111                    out.errors.push(UsageErr::MissingFlag(name));
3112                }
3113            }
3114        }
3115    }
3116
3117    // Positionals can declare the same pairwise conflict as flags. Their selector is
3118    // the bare argument name, while a flag keeps its dashed spelling.
3119    for (command_index, arg) in out
3120        .cmds
3121        .iter()
3122        .enumerate()
3123        .flat_map(|(index, cmd)| cmd.args.iter().map(move |arg| (index, arg)))
3124    {
3125        let given = arg_is_explicit(arg, &out, custom_env);
3126        if !given {
3127            continue;
3128        }
3129        for other in &arg.conflicts {
3130            if selector_is_explicit(other, &out, &overridden_flags, custom_env) {
3131                out.errors.push(UsageErr::InvalidFlag {
3132                    token: arg.name.clone(),
3133                    reason: format!("conflicts with {other}"),
3134                    span: (0, 0).into(),
3135                    input: format!("{} {other}", arg.name),
3136                });
3137            }
3138        }
3139        if !exclusive_present && requirements_apply(command_index) {
3140            for other in &arg.requires {
3141                if !selector_is_satisfied(other, &out, &overridden_flags, custom_env) {
3142                    let name = selector_flag_name(other, &out).unwrap_or_else(|| other.clone());
3143                    if other.starts_with('-') {
3144                        out.errors.push(UsageErr::MissingFlag(name));
3145                    } else {
3146                        out.errors.push(UsageErr::MissingArg(name));
3147                    }
3148                }
3149            }
3150        }
3151    }
3152
3153    // An exclusive flag is the whole-command form of a conflict: `--version` means the
3154    // rest of the line has nothing to act on. Everything the invocation supplied counts,
3155    // positionals included, which is what distinguishes it from being in a group with
3156    // every other flag.
3157    //
3158    // Only what was *given*, as `conflicts` reads it: a defaulted flag standing beside an
3159    // exclusive one is nobody saying anything, and counting it would make the exclusive
3160    // flag unusable on any command that has a default. Environment values do count, also as
3161    // `conflicts` reads them, so the spec parser and the derive agree.
3162    for flag in unique_flags(out.available_flags.values().chain(out.flags.keys()))
3163        .filter(|flag| !clause_flag_names.contains(&flag.name))
3164    {
3165        // `SpecFlag` equality is intentionally name-only for the public parsed-value map,
3166        // but re-declared aliases can leave distinct declarations with that same name in
3167        // scope. Exclusivity is about the declaration the typed spelling resolved to, so
3168        // compare the parser's `Arc`s by identity here.
3169        let given = flag_was_parsed(flag) || flag_has_env(flag, custom_env);
3170        if !exclusive_occurrence(flag) || !given || overridden_flags.contains(&flag.name) {
3171            continue;
3172        }
3173        let other_flag = unique_flags(out.available_flags.values().chain(out.flags.keys()))
3174            .filter(|other| !clause_flag_names.contains(&other.name))
3175            .find(|other| {
3176                !Arc::ptr_eq(other, flag)
3177                    && !overridden_flags.contains(&other.name)
3178                    && (flag_was_parsed(other) || flag_has_env(other, custom_env))
3179            })
3180            .map(|other| format!("--{}", other.name));
3181        let other_arg = active_args(&out.cmd).iter().find(|arg| {
3182            out.args.keys().any(|given| given.name == arg.name)
3183                || out
3184                    .clauses
3185                    .values()
3186                    .flatten()
3187                    .any(|instance| instance.keys().any(|given| given.name == arg.name))
3188                || arg
3189                    .env
3190                    .as_ref()
3191                    .is_some_and(|env| env_contains(custom_env, env))
3192        });
3193        // Selecting a child is company for an exclusive flag declared by an ancestor. An
3194        // exclusive flag belonging to the child itself does not conflict with the command word
3195        // needed to reach that child — so the question is not who owns the flag but whose
3196        // exclusivity is the one being enforced, which is what `exclusivity_in_play` already
3197        // separated.
3198        let (_, ancestor_exclusivity) = exclusivity_in_play(flag);
3199        let selected_subcommand =
3200            (out.cmds.len() > 1 && ancestor_exclusivity).then(|| out.cmd.name.clone());
3201        let other = other_flag
3202            .or_else(|| other_arg.map(|arg| format!("<{}>", arg.name)))
3203            .or(selected_subcommand);
3204        if let Some(other) = other {
3205            out.errors.push(UsageErr::InvalidFlag {
3206                token: format!("--{}", flag.name),
3207                reason: format!("must be given on its own, and {other} was given too"),
3208                span: (0, 0).into(),
3209                input: format!("--{} {other}", flag.name),
3210            });
3211        }
3212    }
3213
3214    // Groups, checked once per group rather than per flag: both questions a group asks —
3215    // how many members were given, and whether that is enough — are about the set, which
3216    // is the whole reason a group exists rather than a pile of pairwise conflicts.
3217    //
3218    // The same "given" rule as everything else here, so a member filled from the
3219    // environment or a default counts.
3220    // Every command in the chain, not only the selected one: a group may name global
3221    // flags, which belong to an ancestor and are declared there.
3222    let mut group_errors: Vec<UsageErr> = Vec::new();
3223    for (command_index, group) in out
3224        .cmds
3225        .iter()
3226        .enumerate()
3227        .flat_map(|(index, cmd)| cmd.groups.iter().map(move |group| (index, group)))
3228    {
3229        // Counted by the *flag* a selector resolves to, not by the selector. `-f` and
3230        // `--file` are two spellings of one flag, and a group naming both — or naming one
3231        // flag twice — would otherwise report that flag as conflicting with itself the
3232        // moment it was given. Deduplicated rather than refused where the group is
3233        // written, because listing both spellings is redundant, not wrong.
3234        let mut given: Vec<&str> = Vec::new();
3235        let mut seen: Vec<String> = Vec::new();
3236        for selector in &group.members {
3237            if !selector_is_explicit(selector, &out, &overridden_flags, custom_env) {
3238                continue;
3239            }
3240            let name = selector_flag_name(selector, &out).unwrap_or_else(|| selector.clone());
3241            if seen.contains(&name) {
3242                continue;
3243            }
3244            seen.push(name);
3245            given.push(selector.as_str());
3246        }
3247        if !group.multiple && given.len() > 1 {
3248            group_errors.push(UsageErr::InvalidFlag {
3249                token: given[1].to_string(),
3250                reason: format!("cannot be used with {} in group {}", given[0], group.name),
3251                span: (0, 0).into(),
3252                input: format!("{} {}", given[0], given[1]),
3253            });
3254        }
3255        // Requiredness is a *positive* rule, so it reads a default as filling a member —
3256        // the rule `requires` follows. That is also why it cannot reuse `given` above:
3257        // exclusivity must count only what was supplied, or a defaulted member would
3258        // collide with the sibling the user actually typed.
3259        let satisfied = group
3260            .members
3261            .iter()
3262            .any(|selector| selector_is_satisfied(selector, &out, &overridden_flags, custom_env));
3263        if group.required && requirements_apply(command_index) && !satisfied && !exclusive_present {
3264            // The members are what a user has to type, so they are in the message; the
3265            // group's name is there too, since a command with several groups would
3266            // otherwise report the same sentence twice with nothing to tell them apart.
3267            group_errors.push(UsageErr::MissingGroup {
3268                group: group.name.clone(),
3269                members: group.members.join(", "),
3270            });
3271        }
3272    }
3273    out.errors.extend(group_errors);
3274
3275    if !exclusive_present {
3276        for flag in unique_flags(out.available_flags.values())
3277            .filter(|flag| !clause_flag_names.contains(&flag.name))
3278        {
3279            let owner = out
3280                .cmds
3281                .iter()
3282                .rposition(|cmd| cmd.flags.iter().any(|declared| declared.name == flag.name))
3283                .unwrap_or(out.cmds.len() - 1);
3284            if !requirements_apply(owner) {
3285                continue;
3286            }
3287            if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
3288                continue;
3289            }
3290            let has_default =
3291                !flag.default.is_empty() || flag.arg.iter().any(|a| !a.default.is_empty());
3292            let has_env = flag_has_env(flag, custom_env);
3293            let required_if = flag.required_if.iter().any(|selector| {
3294                selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3295            });
3296            let required_if_eq = flag.required_if_eq.iter().any(|condition| {
3297                selector_explicit_has_value(
3298                    &condition.selector,
3299                    &condition.value,
3300                    &out,
3301                    &overridden_flags,
3302                    custom_env,
3303                )
3304            });
3305            let required_if_eq_all = !flag.required_if_eq_all.is_empty()
3306                && flag.required_if_eq_all.iter().all(|condition| {
3307                    selector_explicit_has_value(
3308                        &condition.selector,
3309                        &condition.value,
3310                        &out,
3311                        &overridden_flags,
3312                        custom_env,
3313                    )
3314                });
3315            let unless_any = flag.required_unless.iter().any(|selector| {
3316                selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3317            });
3318            let unless_all = !flag.required_unless_all.is_empty()
3319                && flag.required_unless_all.iter().all(|selector| {
3320                    selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3321                });
3322            let required_unless = !(unless_any
3323                || unless_all
3324                || (flag.required_unless.is_empty() && flag.required_unless_all.is_empty()));
3325            if (flag.required
3326                || required_if
3327                || required_if_eq
3328                || required_if_eq_all
3329                || required_unless)
3330                && !has_default
3331                && !has_env
3332            {
3333                out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
3334            }
3335        }
3336    }
3337
3338    // Validate var_min/var_max constraints for variadic args
3339    for (arg, value) in &out.args {
3340        if arg.var {
3341            if let ParseValue::MultiString(values) = value {
3342                if let Some(min) = arg.var_min {
3343                    if values.len() < min {
3344                        out.errors.push(UsageErr::VarArgTooFew {
3345                            name: arg.name.clone(),
3346                            min,
3347                            got: values.len(),
3348                        });
3349                    }
3350                }
3351                if let Some(max) = arg.var_max {
3352                    if values.len() > max {
3353                        out.errors.push(UsageErr::VarArgTooMany {
3354                            name: arg.name.clone(),
3355                            max,
3356                            got: values.len(),
3357                        });
3358                    }
3359                }
3360            }
3361        }
3362    }
3363
3364    // Validate var_min/var_max constraints for variadic flags. These are bounds on
3365    // repeated occurrences of the flag itself. Bounds on its nested argument are enforced
3366    // by binding once per occurrence, where the per-occurrence count is still available.
3367    for flag in unique_flags(out.available_flags.values())
3368        .filter(|flag| !clause_flag_names.contains(&flag.name))
3369    {
3370        if flag.var {
3371            let bound = match out.flags.get(flag) {
3372                Some(ParseValue::MultiString(values)) => values.len(),
3373                Some(ParseValue::MultiBool(values)) => values.len(),
3374                Some(_) => 1,
3375                None => 0,
3376            };
3377            // A partial parse deliberately leaves the final value-optional flag pending so
3378            // completion can still answer for it. It is nevertheless a real occurrence for
3379            // the repeated flag's bounds; the full parser closes it just after this phase.
3380            let pending = out
3381                .flag_awaiting_value
3382                .iter()
3383                .filter(|pending| {
3384                    Arc::ptr_eq(pending, flag)
3385                        && (pending.value_optional || pending.default_missing.is_some())
3386                })
3387                .count();
3388            let count = bound + pending;
3389            if count == 0 {
3390                continue;
3391            }
3392            if let Some(min) = flag.var_min {
3393                if count < min {
3394                    out.errors.push(UsageErr::VarFlagTooFew {
3395                        name: flag.name.clone(),
3396                        min,
3397                        got: count,
3398                    });
3399                }
3400            }
3401            if let Some(max) = flag.var_max {
3402                if count > max {
3403                    out.errors.push(UsageErr::VarFlagTooMany {
3404                        name: flag.name.clone(),
3405                        max,
3406                        got: count,
3407                    });
3408                }
3409            }
3410        }
3411    }
3412
3413    Ok((out, overridden_flags))
3414}
3415
3416fn validate_expression(
3417    name: &str,
3418    expression: Option<&str>,
3419    message: Option<&str>,
3420    parsed: &ParseValue,
3421    errors: &mut Vec<UsageErr>,
3422) {
3423    let Some(expression) = expression else {
3424        return;
3425    };
3426    #[cfg(not(feature = "validation"))]
3427    let _ = expression;
3428    let values: &[String] = match parsed {
3429        ParseValue::String(value) => std::slice::from_ref(value),
3430        ParseValue::MultiString(values) => values,
3431        ParseValue::Bool(_) | ParseValue::MultiBool(_) => return,
3432    };
3433    #[cfg(feature = "validation")]
3434    for value in values {
3435        let reason = match usage_validation::validate(expression, value) {
3436            Ok(true) => continue,
3437            Ok(false) => message
3438                .unwrap_or("does not satisfy the validation expression")
3439                .to_string(),
3440            Err(error) => format!("validation expression failed: {error}"),
3441        };
3442        errors.push(UsageErr::InvalidValue {
3443            name: name.to_string(),
3444            value: value.clone(),
3445            reason,
3446        });
3447        break;
3448    }
3449    #[cfg(not(feature = "validation"))]
3450    if let Some(value) = values.first() {
3451        let _ = message;
3452        errors.push(UsageErr::InvalidValue {
3453            name: name.to_string(),
3454            value: value.clone(),
3455            reason: "expression validation requires the `validation` feature".to_string(),
3456        });
3457    }
3458}
3459
3460#[cfg(all(test, not(feature = "validation")))]
3461mod optional_validation_tests {
3462    use crate::{parse, Spec};
3463
3464    #[test]
3465    fn validation_declarations_require_the_opt_in_runtime_feature() {
3466        let spec: Spec = r#"
3467name "ex"
3468bin "ex"
3469arg "<port>" validate="int(value) > 0"
3470        "#
3471        .parse()
3472        .unwrap();
3473        let error = parse(&spec, &["ex".to_string(), "1".to_string()]).unwrap_err();
3474        assert!(
3475            error
3476                .to_string()
3477                .contains("requires the `validation` feature"),
3478            "{error:?}"
3479        );
3480    }
3481}
3482
3483fn flag_matches_selector(flag: &SpecFlag, selector: &str) -> bool {
3484    flag.name == selector || flag_keys(flag).iter().any(|key| key == selector)
3485}
3486
3487fn flags_override(overrider: &SpecFlag, overridden: &SpecFlag) -> bool {
3488    overrider
3489        .overrides
3490        .iter()
3491        .any(|selector| flag_matches_selector(overridden, selector))
3492}
3493
3494fn apply_prefix_flag_overrides(
3495    prefix_flags: &mut Vec<(Arc<SpecFlag>, Vec<String>)>,
3496    flag: Arc<SpecFlag>,
3497) {
3498    prefix_flags
3499        .retain(|(other, _)| !(flags_override(&flag, other) || flags_override(other, &flag)));
3500}
3501
3502fn mount_prefix_words(prefix_flags: &[(Arc<SpecFlag>, Vec<String>)]) -> Vec<String> {
3503    prefix_flags
3504        .iter()
3505        .flat_map(|(_, words)| words.iter().cloned())
3506        .collect()
3507}
3508
3509fn env_contains(custom_env: Option<&HashMap<String, String>>, env_var: &str) -> bool {
3510    match custom_env {
3511        Some(env) => env.contains_key(env_var),
3512        None => std::env::var(env_var).is_ok(),
3513    }
3514}
3515
3516fn flag_has_env(flag: &SpecFlag, custom_env: Option<&HashMap<String, String>>) -> bool {
3517    flag.env_names()
3518        .any(|env_var| env_contains(custom_env, env_var))
3519}
3520
3521fn fallback_is_true(value: &str) -> bool {
3522    matches!(value, "1" | "true" | "True" | "TRUE")
3523}
3524
3525fn split_fallback_values(values: &[String], delimiter: Option<char>) -> Vec<String> {
3526    match delimiter {
3527        Some(delimiter) => values
3528            .iter()
3529            .flat_map(|value| value.split(delimiter).map(str::to_string))
3530            .collect(),
3531        None => values.to_vec(),
3532    }
3533}
3534
3535fn validate_arg_fallback_count(arg: &SpecArg, count: usize, errors: &mut Vec<UsageErr>) {
3536    if let Some(min) = arg.var_min {
3537        if count < min {
3538            errors.push(UsageErr::VarArgTooFew {
3539                name: arg.name.clone(),
3540                min,
3541                got: count,
3542            });
3543        }
3544    }
3545    if let Some(max) = arg.var_max {
3546        if count > max {
3547            errors.push(UsageErr::VarArgTooMany {
3548                name: arg.name.clone(),
3549                max,
3550                got: count,
3551            });
3552        }
3553    }
3554}
3555
3556fn validate_flag_fallback_count(flag: &SpecFlag, count: usize, errors: &mut Vec<UsageErr>) {
3557    if let Some(min) = flag.var_min {
3558        if count < min {
3559            errors.push(UsageErr::VarFlagTooFew {
3560                name: flag.name.clone(),
3561                min,
3562                got: count,
3563            });
3564        }
3565    }
3566    if let Some(max) = flag.var_max {
3567        if count > max {
3568            errors.push(UsageErr::VarFlagTooMany {
3569                name: flag.name.clone(),
3570                max,
3571                got: count,
3572            });
3573        }
3574    }
3575}
3576
3577fn validate_flag_arg_fallback_count(
3578    flag: &SpecFlag,
3579    arg: &SpecArg,
3580    count: usize,
3581    errors: &mut Vec<UsageErr>,
3582) {
3583    if let Some(min) = arg.var_min {
3584        if count < min {
3585            errors.push(UsageErr::VarFlagTooFew {
3586                name: flag.name.clone(),
3587                min,
3588                got: count,
3589            });
3590        }
3591    }
3592    if let Some(max) = arg.var_max {
3593        if count > max {
3594            errors.push(UsageErr::VarFlagTooMany {
3595                name: flag.name.clone(),
3596                max,
3597                got: count,
3598            });
3599        }
3600    }
3601}
3602
3603/// Bind a fallback the way an unconditional `default` does: one value, or several
3604/// for `var`, and choices checked the same way.
3605fn bind_flag_fallback(
3606    flag: &Arc<SpecFlag>,
3607    values: &[String],
3608    out: &mut ParseOutput,
3609    custom_env: Option<&HashMap<String, String>>,
3610    origin: ValueOrigin,
3611) -> Result<(), miette::Error> {
3612    let Some(value) = flag_fallback_value(flag, values, &mut out.errors, custom_env)? else {
3613        return Ok(());
3614    };
3615    out.flags.insert(Arc::clone(flag), value);
3616    out.flag_origins
3617        .entry(Arc::clone(flag))
3618        .or_default()
3619        .push(origin);
3620    Ok(())
3621}
3622
3623fn flag_fallback_value(
3624    flag: &SpecFlag,
3625    values: &[String],
3626    errors: &mut Vec<UsageErr>,
3627    custom_env: Option<&HashMap<String, String>>,
3628) -> Result<Option<ParseValue>, miette::Error> {
3629    if values.is_empty() {
3630        return Ok(None);
3631    }
3632    if let Some(arg) = flag.arg.as_ref() {
3633        let values = split_fallback_values(values, arg.delimiter);
3634        if flag.var || arg.var {
3635            if flag.var {
3636                validate_flag_fallback_count(flag, values.len(), errors);
3637            }
3638            if arg.var {
3639                validate_flag_arg_fallback_count(flag, arg, values.len(), errors);
3640            }
3641            validate_choice_values(
3642                ChoiceTarget::option(flag),
3643                &values,
3644                arg.choices.as_ref(),
3645                custom_env,
3646            )?;
3647            Ok(Some(ParseValue::MultiString(values)))
3648        } else {
3649            let value = values.into_iter().next().unwrap_or_default();
3650            validate_choice_value(
3651                ChoiceTarget::option(flag),
3652                &value,
3653                arg.choices.as_ref(),
3654                custom_env,
3655            )?;
3656            Ok(Some(ParseValue::String(value)))
3657        }
3658    } else if flag.var {
3659        validate_flag_fallback_count(flag, values.len(), errors);
3660        let bools: Vec<bool> = values.iter().map(|s| fallback_is_true(s)).collect();
3661        Ok(Some(ParseValue::MultiBool(bools)))
3662    } else {
3663        Ok(Some(ParseValue::Bool(fallback_is_true(&values[0]))))
3664    }
3665}
3666
3667/// Fill scoped flags inside the clause instances argv created.
3668///
3669/// The returned sets contain argv and environment sources only. Relationship
3670/// validation uses them to keep defaults from becoming explicit conflicts while
3671/// still allowing a fallback value to satisfy a required scoped flag.
3672fn apply_clause_flag_fallbacks(
3673    out: &mut ParseOutput,
3674    overridden_flags: &HashSet<String>,
3675    custom_env: Option<&HashMap<String, String>>,
3676) -> Result<Option<Vec<HashSet<String>>>, miette::Error> {
3677    let Some(clause) = out.cmd.clause.clone() else {
3678        return Ok(None);
3679    };
3680    let Some(positional_instances) = out.clauses.get(&clause.name) else {
3681        return Ok(None);
3682    };
3683    let positional_instances = positional_instances.clone();
3684    let mut flag_instances = out
3685        .clause_flags
3686        .shift_remove(&clause.name)
3687        .unwrap_or_default();
3688    flag_instances.resize_with(positional_instances.len(), IndexMap::new);
3689    let get_env = |key: &str| -> Option<String> {
3690        custom_env
3691            .and_then(|values| values.get(key).cloned())
3692            .or_else(|| {
3693                custom_env
3694                    .is_none()
3695                    .then(|| std::env::var(key).ok())
3696                    .flatten()
3697            })
3698    };
3699    let mut explicit_instances = Vec::with_capacity(flag_instances.len());
3700
3701    for (instance_index, scoped) in flag_instances.iter_mut().enumerate() {
3702        let positional = &positional_instances[instance_index];
3703        let mut explicit = scoped
3704            .keys()
3705            .map(|flag| flag.name.clone())
3706            .collect::<HashSet<_>>();
3707
3708        // Environment first so every default_if sees all explicit sources,
3709        // independent of declaration order.
3710        for declared in &clause.flags {
3711            let flag = Arc::new(declared.clone());
3712            if scoped.contains_key(&flag) {
3713                continue;
3714            }
3715            let Some((env_name, env_value)) = first_set_env(declared.env_names(), &get_env) else {
3716                continue;
3717            };
3718            if let Some(warning) = flag_deprecation(declared) {
3719                out.warnings.push(warning);
3720            }
3721            if flag_env_is_deprecated(declared, env_name) {
3722                out.warnings
3723                    .push(Warning::env(env_name, flag_current_env(declared)));
3724            }
3725            if let Some(value) = flag_fallback_value(
3726                declared,
3727                std::slice::from_ref(&env_value),
3728                &mut out.errors,
3729                custom_env,
3730            )? {
3731                scoped.insert(flag, value);
3732                explicit.insert(declared.name.clone());
3733            }
3734        }
3735
3736        let condition_matches =
3737            |condition: &crate::SpecDefaultIf| {
3738                if let Some(flag) = clause
3739                    .flags
3740                    .iter()
3741                    .find(|flag| flag_matches_selector(flag, &condition.selector))
3742                {
3743                    if !explicit.contains(&flag.name) {
3744                        return false;
3745                    }
3746                    return condition.when.as_ref().is_none_or(|expected| {
3747                        scoped
3748                            .iter()
3749                            .find(|(present, _)| present.name == flag.name)
3750                            .is_some_and(|(_, value)| parse_value_has(value, expected))
3751                    });
3752                }
3753                if let Some(arg) = clause.args.iter().find(|arg| {
3754                    !condition.selector.starts_with('-') && arg.name == condition.selector
3755                }) {
3756                    return positional.get(arg).is_some_and(|value| {
3757                        condition
3758                            .when
3759                            .as_ref()
3760                            .is_none_or(|expected| parse_value_has(value, expected))
3761                    });
3762                }
3763                let command_flag = out
3764                    .available_flags
3765                    .values()
3766                    .chain(out.flags.keys())
3767                    .filter(|flag| !is_clause_scoped_flag(out, flag))
3768                    .find(|flag| flag_matches_selector(flag, &condition.selector));
3769                command_flag.is_some_and(|flag| {
3770                    !overridden_flags.contains(&flag.name)
3771                        && command_flag_has_explicit_source(flag, out)
3772                        && condition.when.as_ref().is_none_or(|expected| {
3773                            out.flags
3774                                .get(flag)
3775                                .is_some_and(|value| parse_value_has(value, expected))
3776                        })
3777                })
3778            };
3779
3780        let conditional = clause
3781            .flags
3782            .iter()
3783            .filter(|flag| !scoped.keys().any(|present| present.name == flag.name))
3784            .filter_map(|flag| {
3785                flag.default_if
3786                    .iter()
3787                    .find(|condition| condition_matches(condition))
3788                    .map(|condition| (flag.clone(), condition.value.clone()))
3789            })
3790            .collect::<Vec<_>>();
3791        for (declared, value) in conditional {
3792            if let Some(value) = flag_fallback_value(
3793                &declared,
3794                std::slice::from_ref(&value),
3795                &mut out.errors,
3796                custom_env,
3797            )? {
3798                scoped.insert(Arc::new(declared), value);
3799            }
3800        }
3801
3802        for declared in &clause.flags {
3803            let flag = Arc::new(declared.clone());
3804            if scoped.contains_key(&flag) {
3805                continue;
3806            }
3807            let values = if !declared.default.is_empty() {
3808                &declared.default
3809            } else if let Some(arg) = declared.arg.as_ref().filter(|arg| !arg.default.is_empty()) {
3810                &arg.default
3811            } else {
3812                continue;
3813            };
3814            if let Some(value) = flag_fallback_value(declared, values, &mut out.errors, custom_env)?
3815            {
3816                scoped.insert(flag, value);
3817            }
3818        }
3819        explicit_instances.push(explicit);
3820    }
3821
3822    out.clause_flags.insert(clause.name, flag_instances);
3823    Ok(Some(explicit_instances))
3824}
3825
3826fn command_flag_has_explicit_source(flag: &SpecFlag, out: &ParseOutput) -> bool {
3827    out.flags.contains_key(flag)
3828        && out.flag_origins.get(flag).is_none_or(|origins| {
3829            origins
3830                .iter()
3831                .any(|origin| matches!(origin, ValueOrigin::DefaultMissing | ValueOrigin::Env(_)))
3832        })
3833}
3834
3835fn default_if_condition_matches(
3836    condition: &crate::SpecDefaultIf,
3837    out: &ParseOutput,
3838    overridden_flags: &HashSet<String>,
3839    custom_env: Option<&HashMap<String, String>>,
3840) -> bool {
3841    match &condition.when {
3842        None => selector_is_explicit(&condition.selector, out, overridden_flags, custom_env),
3843        Some(when) => {
3844            let Some(flag) = out
3845                .available_flags
3846                .values()
3847                .chain(out.flags.keys())
3848                .find(|flag| flag_matches_selector(flag, &condition.selector))
3849            else {
3850                return false;
3851            };
3852            if overridden_flags.contains(&flag.name) {
3853                return false;
3854            }
3855            explicit_flag_has_value(flag, when, out, custom_env)
3856        }
3857    }
3858}
3859
3860/// Whether an explicitly supplied value of `flag` equals `expected`.
3861///
3862/// clap treats command-line and environment values as explicit for `requires_if`, but
3863/// not defaults. Keep that source distinction here instead of consulting the flag's
3864/// defaults through `selector_is_satisfied`.
3865fn explicit_flag_has_value(
3866    flag: &SpecFlag,
3867    expected: &str,
3868    out: &ParseOutput,
3869    custom_env: Option<&HashMap<String, String>>,
3870) -> bool {
3871    let parsed_matches = out.flags.get(flag).is_some_and(|value| match value {
3872        ParseValue::Bool(value) => value.to_string() == expected,
3873        ParseValue::String(value) => value == expected,
3874        ParseValue::MultiBool(values) => values.iter().any(|value| value.to_string() == expected),
3875        ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3876    });
3877    if out.flags.contains_key(flag) {
3878        return parsed_matches;
3879    }
3880
3881    let value = flag.env_names().find_map(|env| match custom_env {
3882        Some(values) => values.get(env).cloned(),
3883        None => std::env::var(env).ok(),
3884    });
3885    value.is_some_and(
3886        |value| match flag.arg.as_ref().and_then(|arg| arg.delimiter) {
3887            Some(delimiter) => value.split(delimiter).any(|value| value == expected),
3888            None if flag.arg.is_none() => {
3889                matches!(value.as_str(), "1" | "true" | "True" | "TRUE").to_string() == expected
3890            }
3891            None => value == expected,
3892        },
3893    )
3894}
3895
3896fn parse_value_has(value: &ParseValue, expected: &str) -> bool {
3897    match value {
3898        ParseValue::Bool(value) => value.to_string() == expected,
3899        ParseValue::String(value) => value == expected,
3900        ParseValue::MultiBool(values) => values.iter().any(|value| value.to_string() == expected),
3901        ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3902    }
3903}
3904
3905/// Clause flags share the command's spelling table so they can be recognized while parsing,
3906/// but their values and validation belong to one clause instance rather than the command.
3907fn is_clause_scoped_flag(out: &ParseOutput, flag: &SpecFlag) -> bool {
3908    out.cmd
3909        .clause
3910        .as_ref()
3911        .is_some_and(|clause| clause.flags.iter().any(|scoped| scoped.name == flag.name))
3912}
3913
3914fn selected_clause_flag<'a>(out: &'a ParseOutput, selector: &str) -> Option<&'a SpecFlag> {
3915    out.cmd
3916        .clause
3917        .as_ref()?
3918        .flags
3919        .iter()
3920        .find(|flag| flag_matches_selector(flag, selector))
3921}
3922
3923fn clause_flag_is_explicit(out: &ParseOutput, flag: &SpecFlag) -> bool {
3924    out.clause_flags
3925        .values()
3926        .flatten()
3927        .any(|instance| instance.keys().any(|present| present.name == flag.name))
3928        || out.flags.keys().any(|present| present.name == flag.name)
3929}
3930
3931fn clause_flag_has_value(out: &ParseOutput, flag: &SpecFlag, expected: &str) -> bool {
3932    out.clause_flags.values().flatten().any(|instance| {
3933        instance
3934            .iter()
3935            .any(|(present, value)| present.name == flag.name && parse_value_has(value, expected))
3936    }) || out
3937        .flags
3938        .iter()
3939        .any(|(present, value)| present.name == flag.name && parse_value_has(value, expected))
3940}
3941
3942fn validate_clause_relationships(
3943    out: &mut ParseOutput,
3944    overridden_flags: &HashSet<String>,
3945    custom_env: Option<&HashMap<String, String>>,
3946    explicit_instances: Option<&[HashSet<String>]>,
3947) {
3948    let Some(clause) = out.cmd.clause.as_ref() else {
3949        return;
3950    };
3951    let Some(instances) = out.clauses.get(&clause.name) else {
3952        return;
3953    };
3954    let command_flag_is_explicit = |selector: &str| {
3955        out.available_flags
3956            .values()
3957            .chain(out.flags.keys())
3958            .filter(|flag| !is_clause_scoped_flag(out, flag))
3959            .any(|flag| {
3960                flag_matches_selector(flag, selector)
3961                    && !overridden_flags.contains(&flag.name)
3962                    && (out.flags.contains_key(flag) || flag_has_env(flag, custom_env))
3963            })
3964    };
3965    let command_flag_matches_value = |selector: &str, expected: &str| {
3966        out.available_flags
3967            .values()
3968            .chain(out.flags.keys())
3969            .filter(|flag| !is_clause_scoped_flag(out, flag))
3970            .find(|flag| flag_matches_selector(flag, selector))
3971            .is_some_and(|flag| {
3972                !overridden_flags.contains(&flag.name)
3973                    && explicit_flag_has_value(flag, expected, out, custom_env)
3974            })
3975    };
3976    let command_flag_is_satisfied = |selector: &str| {
3977        out.available_flags
3978            .values()
3979            .chain(out.flags.keys())
3980            .filter(|flag| !is_clause_scoped_flag(out, flag))
3981            .any(|flag| flag_matches_selector(flag, selector))
3982            && selector_is_satisfied(selector, out, overridden_flags, custom_env)
3983    };
3984    let mut errors = Vec::new();
3985    for (instance_index, instance) in instances.iter().enumerate() {
3986        let explicit = explicit_instances.and_then(|instances| instances.get(instance_index));
3987        let scoped = out
3988            .clause_flags
3989            .get(&clause.name)
3990            .and_then(|instances| instances.get(instance_index));
3991        let scoped_flag = |selector: &str| {
3992            clause
3993                .flags
3994                .iter()
3995                .find(|flag| flag_matches_selector(flag, selector))
3996        };
3997        let scoped_value = |flag: &SpecFlag| {
3998            scoped.and_then(|values| {
3999                values
4000                    .iter()
4001                    .find(|(present, _)| present.name == flag.name)
4002                    .map(|(_, value)| value)
4003            })
4004        };
4005        let arg_is_explicit = |selector: &str| {
4006            instance
4007                .keys()
4008                .any(|arg| !selector.starts_with('-') && arg.name == selector)
4009        };
4010        let selector_is_explicit = |selector: &str| {
4011            scoped_flag(selector).is_some_and(|flag| {
4012                explicit
4013                    .map(|given| given.contains(&flag.name))
4014                    .unwrap_or_else(|| scoped_value(flag).is_some())
4015            }) || command_flag_is_explicit(selector)
4016                || arg_is_explicit(selector)
4017        };
4018        let selector_has_value = |selector: &str, expected: &str| {
4019            scoped_flag(selector)
4020                .filter(|flag| {
4021                    explicit
4022                        .map(|given| given.contains(&flag.name))
4023                        .unwrap_or_else(|| scoped_value(flag).is_some())
4024                })
4025                .and_then(scoped_value)
4026                .is_some_and(|value| parse_value_has(value, expected))
4027                || command_flag_matches_value(selector, expected)
4028                || instance.iter().any(|(arg, value)| {
4029                    !selector.starts_with('-')
4030                        && arg.name == selector
4031                        && parse_value_has(value, expected)
4032                })
4033        };
4034        let selector_is_satisfied = |selector: &str| {
4035            scoped_flag(selector).is_some_and(|flag| scoped_value(flag).is_some())
4036                || command_flag_is_satisfied(selector)
4037                || arg_is_explicit(selector)
4038        };
4039        let selector_name = |selector: &str| {
4040            scoped_flag(selector)
4041                .map(|flag| flag.name.clone())
4042                .or_else(|| selector_flag_name(selector, out))
4043                .unwrap_or_else(|| selector.to_string())
4044        };
4045
4046        for flag in &clause.flags {
4047            let value = scoped_value(flag);
4048            if value.is_none() {
4049                let required_if = flag
4050                    .required_if
4051                    .iter()
4052                    .any(|selector| selector_is_explicit(selector));
4053                let required_if_eq = flag
4054                    .required_if_eq
4055                    .iter()
4056                    .any(|condition| selector_has_value(&condition.selector, &condition.value));
4057                let required_if_eq_all = !flag.required_if_eq_all.is_empty()
4058                    && flag
4059                        .required_if_eq_all
4060                        .iter()
4061                        .all(|condition| selector_has_value(&condition.selector, &condition.value));
4062                let unless_any = flag
4063                    .required_unless
4064                    .iter()
4065                    .any(|selector| selector_is_explicit(selector));
4066                let unless_all = !flag.required_unless_all.is_empty()
4067                    && flag
4068                        .required_unless_all
4069                        .iter()
4070                        .all(|selector| selector_is_explicit(selector));
4071                let required_unless = !(unless_any
4072                    || unless_all
4073                    || (flag.required_unless.is_empty() && flag.required_unless_all.is_empty()));
4074                if flag.required
4075                    || required_if
4076                    || required_if_eq
4077                    || required_if_eq_all
4078                    || required_unless
4079                {
4080                    errors.push(UsageErr::MissingFlag(flag.name.clone()));
4081                }
4082                continue;
4083            }
4084
4085            let explicitly_provided = explicit
4086                .map(|given| given.contains(&flag.name))
4087                .unwrap_or(true);
4088
4089            if explicitly_provided {
4090                for other in &flag.conflicts {
4091                    if selector_is_explicit(other) {
4092                        errors.push(UsageErr::InvalidFlag {
4093                            token: format!("--{}", flag.name),
4094                            reason: format!("conflicts with {other}"),
4095                            span: (0, 0).into(),
4096                            input: format!("--{} {other}", flag.name),
4097                        });
4098                    }
4099                }
4100                for other in &flag.requires {
4101                    if !selector_is_satisfied(other) {
4102                        errors.push(UsageErr::MissingFlag(selector_name(other)));
4103                    }
4104                }
4105                for condition in &flag.requires_if {
4106                    if value.is_some_and(|value| parse_value_has(value, &condition.value))
4107                        && !selector_is_satisfied(&condition.requires)
4108                    {
4109                        errors.push(UsageErr::MissingFlag(selector_name(&condition.requires)));
4110                    }
4111                }
4112            }
4113            if let (true, Some(value)) = (flag.var, value) {
4114                let count = match value {
4115                    ParseValue::MultiString(values) => values.len(),
4116                    ParseValue::MultiBool(values) => values.len(),
4117                    _ => 1,
4118                };
4119                if let Some(min) = flag.var_min.filter(|min| count < *min) {
4120                    errors.push(UsageErr::VarFlagTooFew {
4121                        name: flag.name.clone(),
4122                        min,
4123                        got: count,
4124                    });
4125                }
4126                if let Some(max) = flag.var_max.filter(|max| count > *max) {
4127                    errors.push(UsageErr::VarFlagTooMany {
4128                        name: flag.name.clone(),
4129                        max,
4130                        got: count,
4131                    });
4132                }
4133            }
4134            if let (Some(arg), Some(value)) = (&flag.arg, value) {
4135                validate_expression(
4136                    &flag.name,
4137                    arg.validate.as_deref(),
4138                    arg.validate_error.as_deref(),
4139                    value,
4140                    &mut errors,
4141                );
4142            }
4143        }
4144        for arg in &clause.args {
4145            let given = instance.keys().any(|present| present.name == arg.name);
4146            if !given {
4147                let required_if = arg
4148                    .required_if
4149                    .iter()
4150                    .any(|selector| selector_is_explicit(selector));
4151                let required_if_eq = arg
4152                    .required_if_eq
4153                    .iter()
4154                    .any(|condition| selector_has_value(&condition.selector, &condition.value));
4155                let required_if_eq_all = !arg.required_if_eq_all.is_empty()
4156                    && arg
4157                        .required_if_eq_all
4158                        .iter()
4159                        .all(|condition| selector_has_value(&condition.selector, &condition.value));
4160                let unless_any = arg
4161                    .required_unless
4162                    .iter()
4163                    .any(|selector| selector_is_explicit(selector));
4164                let unless_all = !arg.required_unless_all.is_empty()
4165                    && arg
4166                        .required_unless_all
4167                        .iter()
4168                        .all(|selector| selector_is_explicit(selector));
4169                let required_unless = !(unless_any
4170                    || unless_all
4171                    || (arg.required_unless.is_empty() && arg.required_unless_all.is_empty()));
4172                if required_if || required_if_eq || required_if_eq_all || required_unless {
4173                    errors.push(UsageErr::MissingClauseArg {
4174                        clause: clause.name.clone(),
4175                        instance: instance_index + 1,
4176                        arg: arg.name.clone(),
4177                    });
4178                }
4179                continue;
4180            }
4181            for other in &arg.conflicts {
4182                if selector_is_explicit(other) {
4183                    errors.push(UsageErr::InvalidFlag {
4184                        token: arg.name.clone(),
4185                        reason: format!("conflicts with {other}"),
4186                        span: (0, 0).into(),
4187                        input: format!("{} {other}", arg.name),
4188                    });
4189                }
4190            }
4191            for other in &arg.requires {
4192                if selector_is_satisfied(other) {
4193                    continue;
4194                }
4195                if other.starts_with('-') {
4196                    errors.push(UsageErr::MissingFlag(selector_name(other)));
4197                } else {
4198                    errors.push(UsageErr::MissingClauseArg {
4199                        clause: clause.name.clone(),
4200                        instance: instance_index + 1,
4201                        arg: other.clone(),
4202                    });
4203                }
4204            }
4205        }
4206    }
4207    out.errors.extend(errors);
4208}
4209
4210fn selector_explicit_has_value(
4211    selector: &str,
4212    expected: &str,
4213    out: &ParseOutput,
4214    overridden_flags: &HashSet<String>,
4215    custom_env: Option<&HashMap<String, String>>,
4216) -> bool {
4217    if let Some(flag) = selected_clause_flag(out, selector) {
4218        return clause_flag_has_value(out, flag, expected);
4219    }
4220    if let Some(flag) = out
4221        .available_flags
4222        .values()
4223        .chain(out.flags.keys())
4224        .filter(|flag| !is_clause_scoped_flag(out, flag))
4225        .find(|flag| flag_matches_selector(flag, selector))
4226    {
4227        return !overridden_flags.contains(&flag.name)
4228            && explicit_flag_has_value(flag, expected, out, custom_env);
4229    }
4230    let Some(arg) = selector_arg(selector, out) else {
4231        return false;
4232    };
4233    let parsed = out
4234        .args
4235        .iter()
4236        .find(|(given, _)| given.name == arg.name)
4237        .map(|(_, value)| value)
4238        .or_else(|| {
4239            out.clauses.values().flatten().find_map(|instance| {
4240                instance
4241                    .iter()
4242                    .find(|(given, _)| given.name == arg.name)
4243                    .map(|(_, value)| value)
4244            })
4245        });
4246    if let Some(value) = parsed {
4247        return match value {
4248            ParseValue::String(value) => value == expected,
4249            ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
4250            ParseValue::Bool(value) => value.to_string() == expected,
4251            ParseValue::MultiBool(values) => {
4252                values.iter().any(|value| value.to_string() == expected)
4253            }
4254        };
4255    }
4256    let value = arg.env_names().find_map(|env| match custom_env {
4257        Some(values) => values.get(env).cloned(),
4258        None => std::env::var(env).ok(),
4259    });
4260    value.is_some_and(|value| match arg.delimiter {
4261        Some(delimiter) => value.split(delimiter).any(|value| value == expected),
4262        None => value == expected,
4263    })
4264}
4265
4266fn selector_is_explicit(
4267    selector: &str,
4268    out: &ParseOutput,
4269    overridden_flags: &HashSet<String>,
4270    custom_env: Option<&HashMap<String, String>>,
4271) -> bool {
4272    let scoped_flag_is_explicit =
4273        selected_clause_flag(out, selector).is_some_and(|flag| clause_flag_is_explicit(out, flag));
4274    let flag_is_explicit = out
4275        .available_flags
4276        .values()
4277        .chain(out.flags.keys())
4278        .filter(|flag| !is_clause_scoped_flag(out, flag))
4279        .any(|flag| {
4280            flag_matches_selector(flag, selector)
4281                && !overridden_flags.contains(&flag.name)
4282                && (out.flags.contains_key(flag) || flag_has_env(flag, custom_env))
4283        });
4284    scoped_flag_is_explicit
4285        || flag_is_explicit
4286        || selector_arg(selector, out).is_some_and(|arg| arg_is_explicit(arg, out, custom_env))
4287}
4288
4289/// The name of the flag a selector points at, for an error that has to name it.
4290///
4291/// `selector_is_explicit` only answers yes or no, which is all a check needs; a message
4292/// about a flag that is *missing* has to say which one, and the selector may be a short
4293/// form or an alias rather than the name.
4294fn selector_flag_name(selector: &str, out: &ParseOutput) -> Option<String> {
4295    if let Some(flag) = selected_clause_flag(out, selector) {
4296        return Some(flag.name.clone());
4297    }
4298    out.available_flags
4299        .values()
4300        .chain(out.flags.keys())
4301        .filter(|flag| !is_clause_scoped_flag(out, flag))
4302        .find(|flag| flag_matches_selector(flag, selector))
4303        .map(|flag| flag.name.clone())
4304        .or_else(|| selector_arg(selector, out).map(|arg| arg.name.clone()))
4305}
4306
4307/// Whether a selector's flag ended up with a value, however it got one.
4308///
4309/// The rule for a *positive* relationship, and the difference from
4310/// [`selector_is_explicit`] is deliberate. A negative rule — `conflicts`, or a group's
4311/// exclusivity — has to count only what was given, or a flag with a default would
4312/// conflict with everything and no command line would parse. A positive one asks whether
4313/// the flag it names has a value, and a default is a value: that is already how plain
4314/// `required`, `required_if` and `required_unless` read a default, and `requires` saying
4315/// otherwise would have made the same flag missing here and present ten lines below.
4316fn selector_is_satisfied(
4317    selector: &str,
4318    out: &ParseOutput,
4319    overridden_flags: &HashSet<String>,
4320    custom_env: Option<&HashMap<String, String>>,
4321) -> bool {
4322    if selector_is_explicit(selector, out, overridden_flags, custom_env) {
4323        return true;
4324    }
4325    let flag_is_satisfied = out
4326        .available_flags
4327        .values()
4328        .chain(out.flags.keys())
4329        .filter(|flag| !is_clause_scoped_flag(out, flag))
4330        .filter(|flag| flag_matches_selector(flag, selector))
4331        .any(|flag| {
4332            !overridden_flags.contains(&flag.name)
4333                && (!flag.default.is_empty()
4334                    || flag.arg.iter().any(|a| !a.default.is_empty())
4335                    || flag.default_if.iter().any(|condition| {
4336                        default_if_condition_matches(condition, out, overridden_flags, custom_env)
4337                    }))
4338        });
4339    flag_is_satisfied || selector_arg(selector, out).is_some_and(|arg| !arg.default.is_empty())
4340}
4341
4342fn selector_arg<'a>(selector: &str, out: &'a ParseOutput) -> Option<&'a SpecArg> {
4343    // Bare words are positional selectors. Keep accepting a flag's internal name above
4344    // for existing specs; when both exist, the dashed flag spelling removes ambiguity.
4345    if selector.starts_with('-') {
4346        return None;
4347    }
4348    out.cmds
4349        .iter()
4350        .flat_map(active_args)
4351        .find(|arg| arg.name == selector)
4352}
4353
4354fn arg_is_explicit(
4355    arg: &SpecArg,
4356    out: &ParseOutput,
4357    custom_env: Option<&HashMap<String, String>>,
4358) -> bool {
4359    out.args.keys().any(|given| given.name == arg.name)
4360        || out
4361            .clauses
4362            .values()
4363            .flatten()
4364            .any(|instance| instance.keys().any(|given| given.name == arg.name))
4365        || arg
4366            .env
4367            .as_ref()
4368            .is_some_and(|env| env_contains(custom_env, env))
4369}
4370
4371fn apply_flag_overrides(
4372    flag: &Arc<SpecFlag>,
4373    available_flags: &BTreeMap<String, Arc<SpecFlag>>,
4374    parsed_flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4375    pending_flags: &mut Vec<Arc<SpecFlag>>,
4376    overridden_flags: &mut HashSet<String>,
4377    // The reportable half of the same fact: which flag did the overriding. The set above
4378    // only stops a default or an environment value restoring what was overridden, and
4379    // "`--quiet` is unset despite its default" has no answer without the name.
4380    attributed: &mut BTreeMap<String, String>,
4381) {
4382    let overridden_names: HashSet<String> = available_flags
4383        .values()
4384        .chain(parsed_flags.keys())
4385        .filter(|other| flags_override(flag, other) || flags_override(other, flag))
4386        .map(|other| other.name.clone())
4387        .collect();
4388
4389    parsed_flags.retain(|parsed, _| !overridden_names.contains(&parsed.name));
4390    pending_flags.retain(|pending| !overridden_names.contains(&pending.name));
4391    for name in &overridden_names {
4392        attributed.insert(name.clone(), flag.name.clone());
4393    }
4394    overridden_flags.extend(overridden_names);
4395    // An explicit occurrence always restores this flag, including self-overrides.
4396    overridden_flags.remove(&flag.name);
4397    attributed.remove(&flag.name);
4398}
4399
4400#[cfg(feature = "cli-help")]
4401fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool) -> UsageErr {
4402    UsageErr::Help(docs::cli::render_help(spec, cmd, long))
4403}
4404
4405#[cfg(feature = "cli-help")]
4406fn render_help_all_err(spec: &Spec, cmd: &SpecCommand) -> UsageErr {
4407    fn append(out: &mut String, spec: &Spec, cmd: &SpecCommand) {
4408        if !out.is_empty() {
4409            out.push('\n');
4410        }
4411        out.push_str(&docs::cli::render_help(spec, cmd, true));
4412        let mut children: Vec<_> = cmd
4413            .subcommands
4414            .values()
4415            .filter(|child| !child.hide)
4416            .collect();
4417        children.sort_by_key(|child| (child.display_order.unwrap_or(999), child.name.as_str()));
4418        for child in children {
4419            append(out, spec, child);
4420        }
4421    }
4422
4423    let mut out = String::new();
4424    append(&mut out, spec, cmd);
4425    UsageErr::Help(out)
4426}
4427
4428#[cfg(not(feature = "cli-help"))]
4429fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool) -> UsageErr {
4430    UsageErr::Help("help".to_string())
4431}
4432
4433#[cfg(not(feature = "cli-help"))]
4434fn render_help_all_err(_spec: &Spec, _cmd: &SpecCommand) -> UsageErr {
4435    UsageErr::Help("help".to_string())
4436}
4437
4438/// The version to answer with. `--version` prefers the long text and `-V` the concise
4439/// one, each falling back to the other when only one is declared.
4440fn render_version_err(spec: &Spec, long: bool) -> UsageErr {
4441    let value = if long {
4442        spec.long_version.as_ref().or(spec.version.as_ref())
4443    } else {
4444        spec.version.as_ref().or(spec.long_version.as_ref())
4445    };
4446    UsageErr::Version(value.cloned().unwrap_or_default())
4447}
4448
4449fn render_action_err(spec: &Spec, cmd: &SpecCommand, flag: &SpecFlag, spelling: &str) -> UsageErr {
4450    use crate::SpecFlagAction;
4451    match flag.action {
4452        SpecFlagAction::Help => render_help_err(spec, cmd, spelling.starts_with("--")),
4453        SpecFlagAction::HelpShort => render_help_err(spec, cmd, false),
4454        SpecFlagAction::HelpLong => render_help_err(spec, cmd, true),
4455        SpecFlagAction::HelpAll => render_help_all_err(spec, cmd),
4456        SpecFlagAction::Version => render_version_err(spec, spelling.starts_with("--")),
4457        SpecFlagAction::Set => unreachable!("binding actions are handled before this helper"),
4458    }
4459}
4460
4461/// Report a required flag value that was displaced by a later option.
4462fn render_missing_flag_value(flag: &SpecFlag, following: &str) -> UsageErr {
4463    let token = flag
4464        .long
4465        .first()
4466        .map(|long| format!("--{long}"))
4467        .or_else(|| flag.short.first().map(|short| format!("-{short}")))
4468        .unwrap_or_else(|| flag.name.clone());
4469    UsageErr::InvalidFlag {
4470        token: token.clone(),
4471        reason: "requires an argument".to_string(),
4472        span: (0, 0).into(),
4473        input: format!("{token} {following}"),
4474    }
4475}
4476
4477#[derive(Copy, Clone)]
4478struct ChoiceTarget<'a> {
4479    kind: &'a str,
4480    name: &'a str,
4481}
4482
4483impl<'a> ChoiceTarget<'a> {
4484    fn arg(arg: &'a SpecArg) -> Self {
4485        Self {
4486            kind: "arg",
4487            name: &arg.name,
4488        }
4489    }
4490
4491    fn option(flag: &'a SpecFlag) -> Self {
4492        Self {
4493            kind: "option",
4494            name: &flag.name,
4495        }
4496    }
4497}
4498
4499/// Whether every letter of a short token names a flag in scope.
4500///
4501/// Scanning stops at the first letter whose flag takes a value, because everything
4502/// after it is that value rather than more letters.
4503fn short_bundle_is_known(
4504    spec: &Spec,
4505    cmds: &[SpecCommand],
4506    available: &BTreeMap<String, Arc<SpecFlag>>,
4507    token: &str,
4508) -> bool {
4509    for c in token.chars().skip(1) {
4510        match available.get(&format!("-{c}")) {
4511            // `-h` and `-V` are recognized letters even though no spec declares them, so a
4512            // bundle containing one is a bundle. Without this `-vh` was not read as one at
4513            // all and fell through to `unexpected word`, while usage-argv, usage-go and
4514            // clap all answer it with help.
4515            None if supplied_short(spec, cmds, c).is_some() => {}
4516            None => return false,
4517            Some(f) if f.arg.is_some() => return true,
4518            Some(_) => {}
4519        }
4520    }
4521    true
4522}
4523
4524/// The response `-h` or `-V` produces where nothing declares that letter.
4525///
4526/// The letter form of the flags the parser supplies rather than a spec declaring them,
4527/// under exactly the conditions [`is_help_arg`] and [`is_version_arg`] state — asked
4528/// about here one letter at a time, because a bundle is read one letter at a time.
4529///
4530/// Always the short response: `-h` is short help however many letters share its token,
4531/// and `-V` the concise version. The long forms belong to the long spellings. `-?` is not
4532/// here — it is a whole-token spelling of `-h` rather than a letter anyone bundles.
4533fn supplied_short(spec: &Spec, cmds: &[SpecCommand], letter: char) -> Option<UsageErr> {
4534    let cmd = cmds.last()?;
4535    match letter {
4536        'h' if is_help_arg(spec, cmd, "-h") => Some(render_help_err(spec, cmd, false)),
4537        'V' if is_version_arg(spec, cmds, "-V") => Some(render_version_err(spec, false)),
4538        _ => None,
4539    }
4540}
4541
4542/// Refuse a flag-like token that named nothing, if this command asked for that.
4543///
4544/// Called from the flag branches, where the lookup has just failed and nothing from
4545/// the token has been applied yet — so a bundle like `-az` is refused whole rather
4546/// than after setting `-a`.
4547fn reject_unknown_flag_if_asked(
4548    spec: &Spec,
4549    path: &[SpecCommand],
4550    token: &str,
4551) -> Result<(), UsageErr> {
4552    // A lone `-` is a value by convention. A negative number reaches this only
4553    // when no pending value opted into the narrower exception.
4554    if !is_flag_like(token) {
4555        return Ok(());
4556    }
4557    if effective_unknown_flags(spec, path) != UnknownFlags::Error {
4558        return Ok(());
4559    }
4560    Err(UsageErr::InvalidFlag {
4561        token: token.to_string(),
4562        reason: "no such flag".to_string(),
4563        span: (0, 0).into(),
4564        input: token.to_string(),
4565    })
4566}
4567
4568/// Whether a flag-like token that matches nothing is a value or an error, here.
4569///
4570/// The nearest enclosing command that stated a preference wins, then the spec,
4571/// then the default. Inherited, unlike `effect`: it describes how a command line
4572/// is read, and a CLI that forwards options tends to forward them at every level.
4573fn effective_unknown_flags(spec: &Spec, path: &[SpecCommand]) -> UnknownFlags {
4574    path.iter()
4575        .rev()
4576        .find_map(|cmd| cmd.unknown_flags)
4577        .or(spec.unknown_flags)
4578        .unwrap_or_default()
4579}
4580
4581/// Whether a token would be read as a flag, for the purpose of rejecting unknown
4582/// ones.
4583///
4584/// A lone `-` is a value by convention. Other dash-prefixed tokens are flag-like;
4585/// a field may make the narrower negative-number exception.
4586fn is_flag_like(token: &str) -> bool {
4587    match token.strip_prefix('-') {
4588        None | Some("") => false,
4589        Some(_) => true,
4590    }
4591}
4592
4593fn is_negative_number(token: &str) -> bool {
4594    token.strip_prefix('-').is_some_and(is_number)
4595}
4596
4597/// Whether a flag may claim its following token as a detached value.
4598///
4599/// A required value keeps the historical negative-number exception. A value
4600/// that may be omitted needs an explicit opt-in to distinguish a negative value
4601/// from the flag's bare form.
4602fn accepts_detached_flag_value(flag: &SpecFlag, token: &str) -> bool {
4603    !flag.require_equals
4604        && (!is_flag_like(token)
4605            || flag.allow_hyphen_values()
4606            || (is_negative_number(token)
4607                && (flag
4608                    .arg
4609                    .as_ref()
4610                    .is_some_and(|arg| arg.allow_negative_numbers)
4611                    || (flag.default_missing.is_none() && !flag.value_optional))))
4612}
4613
4614fn record_scalar_flag_occurrence(
4615    cmds: &[SpecCommand],
4616    flag: &Arc<SpecFlag>,
4617    command_level: usize,
4618    bool_value: Option<bool>,
4619    occurrences: &mut HashMap<(usize, usize), u8>,
4620    errors: &mut Vec<UsageErr>,
4621) {
4622    let strict = cmds.get(command_level).is_some_and(|cmd| {
4623        !cmd.args_override_self
4624            || cmd
4625                .clause
4626                .as_ref()
4627                .is_some_and(|clause| clause.flags.iter().any(|candidate| candidate == &**flag))
4628    });
4629    let collects_values = flag.var || flag.arg.as_ref().is_some_and(|arg| arg.var);
4630    if !strict || flag.count || collects_values {
4631        return;
4632    }
4633
4634    let bit = match bool_value {
4635        Some(false) if flag.negate.is_some() => 0b10,
4636        _ => 0b01,
4637    };
4638    let key = (Arc::as_ptr(flag) as usize, command_level);
4639    let seen = occurrences.entry(key).or_default();
4640    if *seen & bit != 0 {
4641        errors.push(UsageErr::DuplicateFlag(flag.name.clone()));
4642    }
4643    *seen |= bit;
4644}
4645
4646/// A token that can select a subcommand, trigger a mount, or be forwarded as an
4647/// external command.
4648///
4649/// Flag-like tokens are not words. A lone `-` is a value — conventionally stdin —
4650/// so it was never a candidate to *select* anything either. usage-argv uses the
4651/// same rule; without it, `-1` skipped the external-subcommand path because Phase 1
4652/// treated every token that `starts_with('-')` as a flag.
4653fn is_command_word(token: &str) -> bool {
4654    (!is_flag_like(token) || is_negative_number(token)) && token != "-"
4655}
4656
4657/// Whether an otherwise numeric-looking token is an exact declared short flag.
4658///
4659/// This check belongs in both parse phases: phase 1 must skip the flag while it
4660/// searches for a later subcommand, and phase 2 must bind it instead of offering
4661/// it to an `allow_negative_numbers` positional.
4662fn declared_numeric_short(available_flags: &BTreeMap<String, Arc<SpecFlag>>, token: &str) -> bool {
4663    token.len() == 2 && token.as_bytes()[1].is_ascii_digit() && available_flags.contains_key(token)
4664}
4665
4666/// Whether an unmatched word belongs to the root's default command.
4667///
4668/// Ordinary words always do. A negative number only does when the default command's
4669/// first positional explicitly accepts one; otherwise it stays at the root, matching
4670/// usage-argv and generated Go.
4671fn default_accepts_word(cmd: &SpecCommand, default_name: &str, token: &str) -> bool {
4672    !is_negative_number(token)
4673        || cmd
4674            .find_subcommand(default_name)
4675            .and_then(|default| default.args.first())
4676            .is_some_and(|arg| arg.allow_negative_numbers)
4677}
4678
4679/// Digits, at most one `.`, and an optional exponent.
4680///
4681/// Spelled out rather than deferred to `f64::from_str`, which also accepts `inf` and
4682/// `NaN`: `-inf` is far likelier to be a misspelled flag than a number somebody meant
4683/// to pass. usage-argv implements the same rule, and the corpus pins the edges — the
4684/// two disagreed about `-1e5` when one used a float parse and the other did not.
4685fn is_number(rest: &str) -> bool {
4686    let (mantissa, exponent) = match rest.find(['e', 'E']) {
4687        Some(at) => (&rest[..at], Some(&rest[at + 1..])),
4688        None => (rest, None),
4689    };
4690
4691    let mut seen_digit = false;
4692    let mut seen_dot = false;
4693    for c in mantissa.chars() {
4694        match c {
4695            '0'..='9' => seen_digit = true,
4696            '.' if !seen_dot => seen_dot = true,
4697            _ => return false,
4698        }
4699    }
4700    if !seen_digit {
4701        return false;
4702    }
4703
4704    match exponent {
4705        None => true,
4706        Some(exp) => {
4707            let digits = exp
4708                .strip_prefix('+')
4709                .or_else(|| exp.strip_prefix('-'))
4710                .unwrap_or(exp);
4711            !digits.is_empty() && digits.chars().all(|c| c.is_ascii_digit())
4712        }
4713    }
4714}
4715
4716/// Bind one value to the flag waiting for it, and let a variadic argument go on
4717/// collecting from the words that follow.
4718///
4719/// Every route to a flag's value comes through here — the following word, the text
4720/// after an `=`, and the token a `allow_hyphen_values` flag takes whatever it looks
4721/// like — so that all three agree on how many values the flag ends up with.
4722#[allow(clippy::too_many_arguments)]
4723fn bind_pending_flag_value(
4724    spec: &Spec,
4725    cmd: &SpecCommand,
4726    errors: &mut Vec<UsageErr>,
4727    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4728    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
4729    word: &mut String,
4730    input: &mut VecDeque<Token>,
4731    custom_env: Option<&HashMap<String, String>>,
4732    trace: &mut Trace,
4733    // Which token supplied `word`, and whether it rode along on the flag's own token
4734    // (`--jobs=8`, `-j8`) rather than following it. A variadic run's later words carry
4735    // their own positions and are recorded where they are read.
4736    argv: usize,
4737    attached: bool,
4738) -> miette::Result<bool> {
4739    // Held before the drain pops it, along with what the flag is already carrying: a
4740    // `var_max` bounds the values this occurrence takes, not the list they are appended
4741    // to, so a second `--include` starts counting again.
4742    let collecting = flag_awaiting_value
4743        .last()
4744        .filter(|flag| flag.arg.as_ref().is_some_and(|arg| arg.var))
4745        .cloned()
4746        .map(|flag| {
4747            let carried = flags.get(&flag).map(value_count).unwrap_or(0);
4748            (flag, carried)
4749        });
4750    let mut bound = vec![];
4751    let refused = drain_pending_flag_values(
4752        spec,
4753        cmd,
4754        errors,
4755        flags,
4756        flag_awaiting_value,
4757        word,
4758        custom_env,
4759        &mut bound,
4760    )?;
4761    for (flag, values) in bound {
4762        trace.record(
4763            argv,
4764            TokenRole::Value {
4765                flag,
4766                values,
4767                attached,
4768            },
4769        );
4770    }
4771    if refused {
4772        return Ok(true);
4773    }
4774    let Some((flag, carried)) = collecting else {
4775        return Ok(false);
4776    };
4777    collect_variadic_flag_values(
4778        spec,
4779        cmd,
4780        errors,
4781        flags,
4782        flag_awaiting_value,
4783        &flag,
4784        carried,
4785        input,
4786        custom_env,
4787        trace,
4788    )
4789}
4790
4791/// Keep feeding a flag whose argument is variadic from the words that follow it.
4792///
4793/// `--include <pattern>...` collects from a single occurrence, so it takes tokens until
4794/// one is flag-like, a `--` arrives, its `var_max` is reached, or the command line ends.
4795/// This is greedy by design — a command declaring both such a flag and positionals will
4796/// find the flag eating them, and `--` or a `var_max` is how the run is stopped.
4797///
4798/// `carried` is what the flag already held when this occurrence began, so the bound
4799/// counts this run rather than everything the flag has collected across the command
4800/// line. Each value goes through the same drain as the first, so choices are checked
4801/// and the value lands in the same list rather than by a second route that could
4802/// disagree.
4803#[allow(clippy::too_many_arguments)]
4804fn collect_variadic_flag_values(
4805    spec: &Spec,
4806    cmd: &SpecCommand,
4807    errors: &mut Vec<UsageErr>,
4808    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4809    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
4810    flag: &Arc<SpecFlag>,
4811    carried: usize,
4812    input: &mut VecDeque<Token>,
4813    custom_env: Option<&HashMap<String, String>>,
4814    trace: &mut Trace,
4815) -> miette::Result<bool> {
4816    let max = flag
4817        .arg
4818        .as_ref()
4819        .and_then(|arg| arg.var_max)
4820        .unwrap_or(usize::MAX);
4821    while flags
4822        .get(flag)
4823        .map(value_count)
4824        .unwrap_or(0)
4825        .saturating_sub(carried)
4826        < max
4827    {
4828        let Some(next) = input.front().map(|token| token.word.as_str()) else {
4829            break;
4830        };
4831        if flag
4832            .arg
4833            .as_ref()
4834            .and_then(|arg| arg.value_terminator.as_deref())
4835            == Some(next)
4836        {
4837            let terminator = input.pop_front().unwrap();
4838            trace.record(
4839                terminator.argv,
4840                TokenRole::ValueTerminator {
4841                    ends: flag.name.clone(),
4842                },
4843            );
4844            break;
4845        }
4846        // The separator is left where it is: stopping here hands it to the arm that
4847        // knows what it means, rather than reading it as one more value.
4848        if next == "--"
4849            || (is_flag_like(next)
4850                && !(flag
4851                    .arg
4852                    .as_ref()
4853                    .is_some_and(|arg| arg.allow_negative_numbers)
4854                    && is_negative_number(next)))
4855        {
4856            break;
4857        }
4858        let taken = input.pop_front().unwrap();
4859        let argv = taken.argv;
4860        let mut word = taken.word;
4861        flag_awaiting_value.push(Arc::clone(flag));
4862        let mut bound = vec![];
4863        let refused = drain_pending_flag_values(
4864            spec,
4865            cmd,
4866            errors,
4867            flags,
4868            flag_awaiting_value,
4869            &mut word,
4870            custom_env,
4871            &mut bound,
4872        )?;
4873        for (flag, values) in bound {
4874            // A later word of the same occurrence is its own token, and never attached:
4875            // only the first value can ride along on the flag.
4876            trace.record(
4877                argv,
4878                TokenRole::Value {
4879                    flag,
4880                    values,
4881                    attached: false,
4882                },
4883            );
4884        }
4885        if refused {
4886            return Ok(true);
4887        }
4888    }
4889    // The loop stops once the occurrence has reached its bound, which without a delimiter is
4890    // exactly when it has taken `max` words. A delimiter breaks that: one word can carry
4891    // several values, so the run can end up *past* the bound rather than on it, and stopping
4892    // is no longer the same as staying within it. `--include a,b,c` under `var_max=2` is the
4893    // case — three values out of the one word the loop was entitled to take.
4894    //
4895    // Counted against `carried` like the loop itself, so this stays a statement about the
4896    // occurrence rather than about the list the occurrences build up.
4897    let taken = flags
4898        .get(flag)
4899        .map(value_count)
4900        .unwrap_or(0)
4901        .saturating_sub(carried);
4902    if let Some(min) = flag.arg.as_ref().and_then(|arg| arg.var_min) {
4903        if taken < min {
4904            errors.push(UsageErr::VarFlagTooFew {
4905                name: flag.name.clone(),
4906                min,
4907                got: taken,
4908            });
4909        }
4910    }
4911    if taken > max {
4912        errors.push(UsageErr::VarFlagTooMany {
4913            name: flag.name.clone(),
4914            max,
4915            got: taken,
4916        });
4917    }
4918    Ok(false)
4919}
4920
4921/// How many values a flag is holding, for a bound that counts them.
4922fn value_count(value: &ParseValue) -> usize {
4923    match value {
4924        ParseValue::MultiString(values) => values.len(),
4925        ParseValue::MultiBool(values) => values.len(),
4926        _ => 1,
4927    }
4928}
4929
4930/// Finish a value-optional flag that was given with no value.
4931///
4932/// Returns whether anything was bound. Completions keep the flag waiting — a
4933/// half-typed `--color ` is a question about the value — so this is asked only
4934/// once a full parse has decided the value is not coming, or once the next token
4935/// has made that decision.
4936///
4937/// The missing string is a real value: if the flag names `choices`, it has to
4938/// be one of them, the same way an env var or a `default` is checked. Binding
4939/// first and failing later would leave the flag set to a value the spec forbids.
4940fn try_bind_default_missing(
4941    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4942    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
4943    custom_env: Option<&HashMap<String, String>>,
4944    origins: &mut IndexMap<Arc<SpecFlag>, Vec<ValueOrigin>>,
4945) -> miette::Result<bool> {
4946    let Some(flag) = flag_awaiting_value.last() else {
4947        return Ok(false);
4948    };
4949    let value = match flag.default_missing.clone() {
4950        Some(value) => value,
4951        None if flag.value_optional => {
4952            let flag = flag_awaiting_value.pop().unwrap();
4953            // Presence in the map distinguishes this from an absent flag; an
4954            // empty collection distinguishes it from an explicitly empty
4955            // `--flag=` string without inventing a sentinel value.
4956            let variadic_value = flag.arg.as_ref().is_some_and(|arg| arg.var);
4957            origins
4958                .entry(Arc::clone(&flag))
4959                .or_default()
4960                .push(ValueOrigin::DefaultMissing);
4961            if flag.var {
4962                // A repeated bare occurrence is still an occurrence. The string collection
4963                // uses an empty value for it, just as the concrete `default_missing` path
4964                // pushes one value per occurrence; otherwise bounds and consumers silently
4965                // lose every bare repeat after the first.
4966                flags
4967                    .entry(flag)
4968                    .or_insert_with(|| ParseValue::MultiString(Vec::new()))
4969                    .try_as_multi_string_mut()
4970                    .unwrap()
4971                    .push(String::new());
4972            } else if variadic_value {
4973                // A variadic occurrence stays pending after each value. Reaching the next
4974                // flag (or EOF) closes that same occurrence; it must not erase what it took.
4975                flags
4976                    .entry(flag)
4977                    .or_insert_with(|| ParseValue::MultiString(Vec::new()));
4978            } else {
4979                // A scalar pending here is a new bare occurrence. The normal permissive
4980                // repeat policy makes the later occurrence a correction, including a
4981                // correction from an explicit value back to the bare tri-state.
4982                flags.insert(flag, ParseValue::MultiString(Vec::new()));
4983            }
4984            return Ok(true);
4985        }
4986        None => return Ok(false),
4987    };
4988    if let Some(arg) = flag.arg.as_ref() {
4989        validate_choice_value(
4990            ChoiceTarget::option(flag),
4991            &value,
4992            arg.choices.as_ref(),
4993            custom_env,
4994        )?;
4995    }
4996    let flag = flag_awaiting_value.pop().unwrap();
4997    origins
4998        .entry(Arc::clone(&flag))
4999        .or_default()
5000        .push(ValueOrigin::DefaultMissing);
5001    let collecting = flag.var || flag.arg.as_ref().is_some_and(|arg| arg.var);
5002    if collecting {
5003        let arr = flags
5004            .entry(flag)
5005            .or_insert_with(|| ParseValue::MultiString(vec![]))
5006            .try_as_multi_string_mut()
5007            .unwrap();
5008        arr.push(value);
5009    } else {
5010        flags.insert(flag, ParseValue::String(value));
5011    }
5012    Ok(true)
5013}
5014
5015/// `bound` collects what each drained flag took, in the order it took it. The values are
5016/// the word after any `delimiter` split, which is the only place that split is known: by the
5017/// time they are in `flags` a scalar and a one-element list are indistinguishable, and a
5018/// second occurrence has appended to the same list.
5019#[allow(clippy::too_many_arguments)]
5020fn drain_pending_flag_values(
5021    spec: &Spec,
5022    cmd: &SpecCommand,
5023    errors: &mut Vec<UsageErr>,
5024    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
5025    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
5026    word: &mut String,
5027    custom_env: Option<&HashMap<String, String>>,
5028    bound: &mut Vec<(Arc<SpecFlag>, Vec<String>)>,
5029) -> miette::Result<bool> {
5030    while let Some(flag) = flag_awaiting_value.pop() {
5031        let arg = flag.arg.as_ref().unwrap();
5032        // Split before anything judges the word, because after the split it is no longer
5033        // one value: `--env dev,prod` is two, and `choices` has to be asked about each.
5034        // Judging first would reject the whole word against a list neither half is on.
5035        let parts: Vec<String> = match arg.delimiter {
5036            Some(delimiter) => word.split(delimiter).map(str::to_string).collect(),
5037            None => vec![std::mem::take(word)],
5038        };
5039        for part in &parts {
5040            if validate_choices(
5041                spec,
5042                cmd,
5043                errors,
5044                ChoiceTarget::option(&flag),
5045                part,
5046                arg.choices.as_ref(),
5047                custom_env,
5048            )? {
5049                return Ok(true);
5050            }
5051        }
5052        word.clear();
5053        bound.push((Arc::clone(&flag), parts.clone()));
5054        // Two ways to hold several values, and both record a list: a `var` flag
5055        // collects one per occurrence, a variadic argument collects several from one.
5056        if flag.var || arg.var {
5057            let arr = flags
5058                .entry(flag)
5059                .or_insert_with(|| ParseValue::MultiString(vec![]))
5060                .try_as_multi_string_mut()
5061                .unwrap();
5062            arr.extend(parts);
5063        } else {
5064            // Nowhere for a second value to go, so the word stands as it was typed. A
5065            // delimiter on a flag that takes one value is refused where it is written.
5066            flags.insert(
5067                flag,
5068                ParseValue::String(parts.into_iter().next().unwrap_or_default()),
5069            );
5070        }
5071    }
5072    Ok(false)
5073}
5074
5075fn choice_error(
5076    target: ChoiceTarget<'_>,
5077    value: &str,
5078    choices: Option<&SpecChoices>,
5079    custom_env: Option<&HashMap<String, String>>,
5080) -> Option<String> {
5081    let choices = choices?;
5082    if !choices.strict {
5083        return None;
5084    }
5085    let values = choices.values_with_env(custom_env);
5086    if choices.matches_with_env(value, custom_env) {
5087        return None;
5088    }
5089    if let Some(env) = choices.env() {
5090        if values.is_empty() {
5091            return Some(format!(
5092                "Invalid choice for {} {}: {value}, no choices resolved from env {env}",
5093                target.kind, target.name,
5094            ));
5095        }
5096    }
5097    Some(format!(
5098        "Invalid choice for {} {}: {value}, expected one of {}",
5099        target.kind,
5100        target.name,
5101        values.join(", ")
5102    ))
5103}
5104
5105fn validate_choices(
5106    spec: &Spec,
5107    cmd: &SpecCommand,
5108    errors: &mut Vec<UsageErr>,
5109    target: ChoiceTarget<'_>,
5110    value: &str,
5111    choices: Option<&SpecChoices>,
5112    custom_env: Option<&HashMap<String, String>>,
5113) -> miette::Result<bool> {
5114    if is_help_arg(spec, cmd, value)
5115        && choices
5116            .is_some_and(|choices| choices.strict && !choices.matches_with_env(value, custom_env))
5117    {
5118        errors.push(render_help_err(spec, cmd, value.len() > 2));
5119        return Ok(true);
5120    }
5121
5122    if let Some(err) = choice_error(target, value, choices, custom_env) {
5123        bail!("{err}");
5124    }
5125    Ok(false)
5126}
5127
5128fn validate_choice_value(
5129    target: ChoiceTarget<'_>,
5130    value: &str,
5131    choices: Option<&SpecChoices>,
5132    custom_env: Option<&HashMap<String, String>>,
5133) -> miette::Result<()> {
5134    if let Some(err) = choice_error(target, value, choices, custom_env) {
5135        bail!("{err}");
5136    }
5137    Ok(())
5138}
5139
5140fn validate_choice_values(
5141    target: ChoiceTarget<'_>,
5142    values: &[String],
5143    choices: Option<&SpecChoices>,
5144    custom_env: Option<&HashMap<String, String>>,
5145) -> miette::Result<()> {
5146    for value in values {
5147        validate_choice_value(target, value, choices, custom_env)?;
5148    }
5149    Ok(())
5150}
5151
5152/// Everything a parse records about where it stopped: the positional cursor, so callers that
5153/// do not re-run the parse — completions, above all — agree with it, and the token trace.
5154///
5155/// Every exit from the binding phase comes through here, which is what makes it the right
5156/// place to close the trace: whatever is still queued was never read, and saying so is more
5157/// useful than leaving those words out of the report entirely.
5158fn record_stop(
5159    out: &mut ParseOutput,
5160    next_arg_idx: usize,
5161    seen_double_dash: bool,
5162    trace: &mut Trace,
5163    unread: &VecDeque<Token>,
5164) {
5165    out.next_arg = active_args(&out.cmd)
5166        .get(cursor_skip_sigils(&out.cmd, next_arg_idx))
5167        .cloned()
5168        .map(Arc::new);
5169    out.double_dash_seen = seen_double_dash;
5170    finalize_current_clause(out);
5171    trace.close(unread);
5172    out.tokens = std::mem::take(&mut trace.tokens);
5173}
5174
5175fn finalize_current_clause(out: &mut ParseOutput) {
5176    let Some(clause) = out.cmd.clause.as_ref() else {
5177        return;
5178    };
5179    let has_scoped_flags = clause
5180        .flags
5181        .iter()
5182        .any(|flag| out.flags.contains_key(&Arc::new(flag.clone())));
5183    if clause.separator.is_none() && out.args.is_empty() && !has_scoped_flags {
5184        return;
5185    }
5186    out.clauses
5187        .entry(clause.name.clone())
5188        .or_default()
5189        .push(std::mem::take(&mut out.args));
5190    let mut flags = IndexMap::new();
5191    for clause_flag in &clause.flags {
5192        let key = Arc::new(clause_flag.clone());
5193        if let Some((flag, value)) = out.flags.shift_remove_entry(&key) {
5194            flags.insert(flag, value);
5195        }
5196    }
5197    out.clause_flags
5198        .entry(clause.name.clone())
5199        .or_default()
5200        .push(flags);
5201}
5202
5203fn restore_current_clause(out: &mut ParseOutput) {
5204    let Some(clause) = out.cmd.clause.as_ref() else {
5205        return;
5206    };
5207    if let Some(current) = out.clauses.get_mut(&clause.name).and_then(Vec::pop) {
5208        out.args = current;
5209    }
5210    if let Some(current) = out.clause_flags.get_mut(&clause.name).and_then(Vec::pop) {
5211        out.flags.extend(current);
5212    }
5213}
5214
5215fn reset_clause_scalar_occurrences(
5216    out: &ParseOutput,
5217    occurrences: &mut HashMap<(usize, usize), u8>,
5218) {
5219    let Some(clause) = out.cmd.clause.as_ref() else {
5220        return;
5221    };
5222    let names = clause
5223        .flags
5224        .iter()
5225        .map(|flag| flag.name.as_str())
5226        .collect::<HashSet<_>>();
5227    let pointers = unique_flags(out.available_flags.values())
5228        .filter(|flag| names.contains(flag.name.as_str()))
5229        .map(|flag| Arc::as_ptr(flag) as usize)
5230        .collect::<HashSet<_>>();
5231    occurrences.retain(|(flag, _), _| !pointers.contains(flag));
5232}
5233
5234fn cursor_skip_sigils(cmd: &SpecCommand, mut idx: usize) -> usize {
5235    while active_args(cmd)
5236        .get(idx)
5237        .is_some_and(|arg| arg.sigil.is_some())
5238    {
5239        idx += 1;
5240    }
5241    idx
5242}
5243
5244fn active_args(cmd: &SpecCommand) -> &[SpecArg] {
5245    cmd.clause
5246        .as_ref()
5247        .map(|clause| clause.args.as_slice())
5248        .unwrap_or(cmd.args.as_slice())
5249}
5250
5251fn match_sigil_arg<'a>(
5252    cmd: &'a SpecCommand,
5253    word: &'a str,
5254) -> Option<(&'a SpecArg, &'a str, &'a str)> {
5255    cmd.args
5256        .iter()
5257        .filter_map(|arg| {
5258            let sigil = arg.sigil.as_deref()?;
5259            let value = word.strip_prefix(sigil)?;
5260            Some((arg, sigil, value))
5261        })
5262        .max_by_key(|(_, sigil, _)| sigil.len())
5263}
5264
5265fn match_sigil_arg_chain<'a>(
5266    cmds: &'a [SpecCommand],
5267    word: &'a str,
5268) -> Option<(&'a SpecArg, &'a str, &'a str)> {
5269    cmds.iter()
5270        .filter_map(|cmd| match_sigil_arg(cmd, word))
5271        .max_by_key(|(_, sigil, _)| sigil.len())
5272}
5273
5274/// Record that `arg` was handed a word before the `--` it requires.
5275///
5276/// A variadic arg would otherwise report the same mistake once per word it was offered, so the
5277/// message is emitted only the first time each arg is seen. The set is also what suppresses the
5278/// `MissingArg` that a `required` + `double_dash="required"` arg would otherwise collect at the
5279/// end of the parse.
5280fn report_double_dash_violation(
5281    arg: &SpecArg,
5282    errors: &mut Vec<UsageErr>,
5283    violations: &mut HashSet<String>,
5284) {
5285    if violations.insert(arg.name.clone()) {
5286        errors.push(UsageErr::ArgRequiresDoubleDash(arg.name.clone()));
5287    }
5288}
5289
5290/// `--version` and `-V`, which the parser supplies where the spec declares a version.
5291///
5292/// The twin of [`is_help_arg`], and of the `version` bit in usage-argv's and usage-go's
5293/// command tables — both of which accepted these spellings while this parser called them
5294/// unknown words. The help page has always listed `-V, --version` under the same
5295/// condition, and said in as many words that it did so "only where a version is
5296/// declared, which is where a parser accepts one", so a spec with a `version` rendered a
5297/// page advertising a flag the parse refused.
5298///
5299/// The root only, because that is where the page lists it: `version` is a property of
5300/// the program, and a subcommand answering with the program's version is a claim no spec
5301/// made. A declared flag wins by arriving first — every scan consults this only after
5302/// nothing declared matched — so a CLI that spends `-V` on something else keeps it, and
5303/// keeps `--version` supplied beside it.
5304fn is_version_arg(spec: &Spec, cmds: &[SpecCommand], w: &str) -> bool {
5305    (spec.version.is_some() || spec.long_version.is_some())
5306        && cmds.len() == 1
5307        && !spec.cmd.disable_version_flag
5308        && (w == "--version" || w == "-V")
5309}
5310
5311fn is_help_arg(spec: &Spec, cmd: &SpecCommand, w: &str) -> bool {
5312    spec.disable_help != Some(true)
5313        && (((w == "--help" || w == "-h" || w == "-?") && !cmd.disable_help_flag)
5314            || (w == "help" && !cmd.disable_help_subcommand && cmd.subcommands.is_empty()))
5315}
5316
5317impl ParseOutput {
5318    pub fn as_env(&self) -> BTreeMap<String, String> {
5319        let mut env = BTreeMap::new();
5320        for (flag, val) in &self.flags {
5321            let key = format!("usage_{}", crate::case::snake(&flag.name));
5322            let val = match val {
5323                ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
5324                ParseValue::String(s) => s.clone(),
5325                ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
5326                ParseValue::MultiString(s) => crate::shell_words::join(s),
5327            };
5328            env.insert(key, val);
5329        }
5330        for (arg, val) in &self.args {
5331            let key = format!("usage_{}", crate::case::snake(&arg.name));
5332            env.insert(key, val.to_string());
5333        }
5334        env
5335    }
5336}
5337
5338impl Display for ParseValue {
5339    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
5340        match self {
5341            ParseValue::Bool(b) => write!(f, "{b}"),
5342            ParseValue::String(s) => write!(f, "{s}"),
5343            ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
5344            ParseValue::MultiString(s) => write!(f, "{}", crate::shell_words::join(s)),
5345        }
5346    }
5347}
5348
5349/// One `tokens` line for [`Debug`]: the position, the word, and what it became.
5350fn render_token(token: &TokenBinding) -> String {
5351    let roles = token.roles.iter().map(render_role).join(", ");
5352    let synthesized = if token.synthesized { " (read as)" } else { "" };
5353    format!("[{}] {}{synthesized}: {roles}", token.index, token.word)
5354}
5355
5356fn render_role(role: &TokenRole) -> String {
5357    match role {
5358        TokenRole::Program => "program".to_string(),
5359        TokenRole::Command { name } => format!("subcommand {name}"),
5360        TokenRole::Flag {
5361            flag,
5362            spelling,
5363            negated,
5364        } => {
5365            let negated = if *negated { ", negated" } else { "" };
5366            format!("flag {} as {spelling}{negated}", flag.name)
5367        }
5368        TokenRole::Value {
5369            flag,
5370            values,
5371            attached,
5372        } => {
5373            let attached = if *attached { ", attached" } else { "" };
5374            format!("value of {} = {values:?}{attached}", flag.name)
5375        }
5376        TokenRole::Arg { arg, values } => format!("arg {} = {values:?}", arg.name),
5377        TokenRole::Sigil { arg, sigil, values } => {
5378            format!("sigil arg {} ({sigil}) = {values:?}", arg.name)
5379        }
5380        TokenRole::Separator => "separator".to_string(),
5381        TokenRole::Builtin { spelling } => format!("built-in {spelling}"),
5382        TokenRole::ValueTerminator { ends } => format!("value terminator, ends {ends}"),
5383        TokenRole::Restart => "restart".to_string(),
5384        TokenRole::ClauseSeparator { name } => format!("clause separator for {name}"),
5385        TokenRole::UnknownFlag { bound_as } => match bound_as {
5386            Some(arg) => format!("unknown flag, bound as {}", arg.name),
5387            None => "unknown flag".to_string(),
5388        },
5389        TokenRole::Refused { reason } => format!("refused: {reason}"),
5390        TokenRole::External => "external".to_string(),
5391        TokenRole::Unread => "unread".to_string(),
5392    }
5393}
5394
5395impl Debug for ParseOutput {
5396    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
5397        f.debug_struct("ParseOutput")
5398            .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
5399            .field(
5400                "args",
5401                &self
5402                    .args
5403                    .iter()
5404                    .map(|(a, w)| format!("{}: {w}", a.name))
5405                    .collect_vec(),
5406            )
5407            .field("clauses", &self.clauses)
5408            .field(
5409                "available_flags",
5410                &self
5411                    .available_flags
5412                    .iter()
5413                    .map(|(f, w)| format!("{f}: {w}"))
5414                    .collect_vec(),
5415            )
5416            .field(
5417                "flags",
5418                &self
5419                    .flags
5420                    .iter()
5421                    .map(|(f, w)| format!("{}: {w}", f.name))
5422                    .collect_vec(),
5423            )
5424            .field("flag_awaiting_value", &self.flag_awaiting_value)
5425            .field("errors", &self.errors)
5426            .field("external", &self.external)
5427            // Provenance, one line per token and one per fallback. This is the parser's
5428            // debug channel under `USAGE_LOG=trace`, so it is where a spec author looks
5429            // first — `usage explain` renders the same facts for a reader.
5430            .field(
5431                "tokens",
5432                &self.tokens.iter().map(render_token).collect_vec(),
5433            )
5434            .field(
5435                "origins",
5436                &self
5437                    .flag_origins
5438                    .iter()
5439                    .map(|(f, o)| format!("{}: {o:?}", f.name))
5440                    .chain(
5441                        self.arg_origins
5442                            .iter()
5443                            .map(|(a, o)| format!("{}: {o:?}", a.name)),
5444                    )
5445                    .collect_vec(),
5446            )
5447            .field("overridden_flags", &self.overridden_flags)
5448            .finish()
5449    }
5450}
5451
5452#[cfg(test)]
5453mod tests {
5454    use super::*;
5455    use crate::SpecFlagAction;
5456
5457    fn input(words: &[&str]) -> Vec<String> {
5458        words.iter().map(|word| (*word).to_string()).collect()
5459    }
5460
5461    #[test]
5462    fn a_declared_version_supplies_the_flag_the_help_page_lists() {
5463        // The page has always listed `-V, --version` wherever a `version` is declared,
5464        // and usage-argv and usage-go have always accepted both. This parser called them
5465        // unknown words, so the one implementation the corpus measures the others against
5466        // was the one that disagreed.
5467        let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ncmd \"run\"\n"
5468            .parse()
5469            .unwrap();
5470
5471        for spelling in ["--version", "-V"] {
5472            let err = parse(&spec, &input(&["ex", spelling]))
5473                .expect_err("answering with a version ends the parse");
5474            assert_eq!(err.to_string(), "1.2.3", "{spelling}");
5475        }
5476    }
5477
5478    #[test]
5479    fn the_supplied_version_flag_is_the_roots_alone() {
5480        // `version` describes the program, and the page lists the entry on the program's
5481        // own page only. A subcommand answering with it would be a claim no spec made.
5482        let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ncmd \"run\"\n"
5483            .parse()
5484            .unwrap();
5485
5486        let err = parse(&spec, &input(&["ex", "run", "--version"])).unwrap_err();
5487        assert_eq!(err.to_string(), "unexpected word: --version");
5488    }
5489
5490    #[test]
5491    fn no_declared_version_supplies_nothing() {
5492        // A `--version` answering with nothing is worse than one that is not there, which
5493        // is why the entry is conditional on the page and the spelling on the parse.
5494        let spec: Spec = "name \"ex\"\nbin \"ex\"\n".parse().unwrap();
5495
5496        for spelling in ["--version", "-V"] {
5497            let err = parse(&spec, &input(&["ex", spelling])).unwrap_err();
5498            assert_eq!(err.to_string(), format!("unexpected word: {spelling}"));
5499        }
5500    }
5501
5502    #[test]
5503    fn disable_version_flag_removes_the_supplied_spellings() {
5504        let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ndisable_version_flag #true\n"
5505            .parse()
5506            .unwrap();
5507
5508        for spelling in ["--version", "-V"] {
5509            let err = parse(&spec, &input(&["ex", spelling])).unwrap_err();
5510            assert_eq!(err.to_string(), format!("unexpected word: {spelling}"));
5511        }
5512    }
5513
5514    #[test]
5515    fn a_spelling_the_spec_spends_elsewhere_keeps_its_meaning() {
5516        // The page drops each supplied spelling the CLI claimed and keeps the other; the
5517        // parse agrees without being told, because a declared flag is matched first.
5518        let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-V --verbose\"\n"
5519            .parse()
5520            .unwrap();
5521
5522        let out = parse(&spec, &input(&["ex", "-V"])).expect("-V is the CLI's own flag");
5523        assert_eq!(out.flags.len(), 1);
5524
5525        let err = parse(&spec, &input(&["ex", "--version"])).unwrap_err();
5526        assert_eq!(err.to_string(), "1.2.3");
5527    }
5528
5529    #[test]
5530    fn the_supplied_spellings_split_the_two_version_texts() {
5531        // The same split `render_action_err` gives a declared version flag: the long
5532        // spelling prefers `long_version`, the short prefers the concise one.
5533        let spec: Spec =
5534            "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nlong_version \"1.2.3 (abcdef)\"\n"
5535                .parse()
5536                .unwrap();
5537
5538        assert_eq!(
5539            parse(&spec, &input(&["ex", "--version"]))
5540                .unwrap_err()
5541                .to_string(),
5542            "1.2.3 (abcdef)"
5543        );
5544        assert_eq!(
5545            parse(&spec, &input(&["ex", "-V"])).unwrap_err().to_string(),
5546            "1.2.3"
5547        );
5548    }
5549
5550    #[test]
5551    fn a_supplied_short_is_a_letter_a_bundle_may_contain() {
5552        // `-h` and `-V` are recognized letters that no spec declares, so a token holding
5553        // one beside a declared letter is a bundle. usage-lib alone read `-vh` as a word
5554        // naming nothing: usage-argv and usage-go both resolve the letter through the
5555        // same lookup that finds a declared short, and clap prints help for it too.
5556        let spec: Spec =
5557            "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\"\ncmd \"run\"\n"
5558                .parse()
5559                .unwrap();
5560
5561        for token in ["-vh", "-hv"] {
5562            let err = parse(&spec, &input(&["ex", token])).expect_err("help ends the parse");
5563            assert!(err.to_string().starts_with("ex 1.2.3"), "{token}: {err}");
5564        }
5565        for token in ["-vV", "-Vv"] {
5566            let err = parse(&spec, &input(&["ex", token])).expect_err("a version ends it too");
5567            assert_eq!(err.to_string(), "1.2.3", "{token}");
5568        }
5569    }
5570
5571    #[test]
5572    fn a_bundled_help_letter_asks_for_the_short_page() {
5573        // Whatever else shares the token: `-h` is the short spelling, and the letters
5574        // beside it say nothing about which page was asked for.
5575        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"
5576            .parse()
5577            .unwrap();
5578
5579        let short = parse(&spec, &input(&["ex", "-vh"]))
5580            .unwrap_err()
5581            .to_string();
5582        let long = parse(&spec, &input(&["ex", "--help"]))
5583            .unwrap_err()
5584            .to_string();
5585        assert!(short.contains("Be loud"), "{short}");
5586        assert!(!short.contains("at length"), "{short}");
5587        assert!(long.contains("at length"), "{long}");
5588    }
5589
5590    #[test]
5591    fn the_bundled_version_letter_is_the_roots_alone() {
5592        // The same rule the whole-token spelling follows, asked one letter at a time.
5593        let spec: Spec =
5594            "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\" global=#true\ncmd \"run\"\n"
5595                .parse()
5596                .unwrap();
5597
5598        let err = parse(&spec, &input(&["ex", "run", "-vV"])).unwrap_err();
5599        assert_eq!(err.to_string(), "unexpected word: -vV");
5600    }
5601
5602    #[test]
5603    fn a_declared_letter_keeps_its_meaning_inside_a_bundle() {
5604        // Nothing is supplied where the CLI spent the letter itself, so `-vh local` is
5605        // this spec's own `-h`, taking its value from the rest of the token.
5606        let spec: Spec =
5607            "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\"\nflag \"-h --host <host>\"\n"
5608                .parse()
5609                .unwrap();
5610
5611        let out = parse(&spec, &input(&["ex", "-vhlocal"])).expect("a bundle and its value");
5612        assert_eq!(out.flags.len(), 2);
5613        assert!(out
5614            .flags
5615            .iter()
5616            .any(|(flag, value)| flag.name == "host" && value.to_string() == "local"));
5617    }
5618
5619    #[test]
5620    fn disabling_help_takes_the_letter_back_out_of_the_bundle() {
5621        let spec: Spec =
5622            "name \"ex\"\nbin \"ex\"\ndisable_help_flag #true\nflag \"-v --verbose\"\n"
5623                .parse()
5624                .unwrap();
5625
5626        let err = parse(&spec, &input(&["ex", "-vh"])).unwrap_err();
5627        assert_eq!(err.to_string(), "unexpected word: -vh");
5628    }
5629
5630    #[test]
5631    fn a_letter_nothing_supplies_still_refuses_the_whole_bundle() {
5632        // The rule this must not weaken: `-az` is not a bundle at all, so `-a` is not set
5633        // on the way to discovering that `z` names nothing.
5634        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-a --all\"\narg \"[file]\"\n"
5635            .parse()
5636            .unwrap();
5637
5638        let out = parse(&spec, &input(&["ex", "-az"])).expect("it falls through to the argument");
5639        assert!(out.flags.is_empty(), "{:?}", out.flags);
5640        assert_eq!(out.args.len(), 1);
5641    }
5642
5643    fn spec_with_arg(arg: SpecArg) -> Spec {
5644        let cmd = SpecCommand::builder().name("test").arg(arg).build();
5645        Spec {
5646            name: "test".to_string(),
5647            bin: "test".to_string(),
5648            cmd,
5649            ..Default::default()
5650        }
5651    }
5652
5653    fn spec_with_flag(flag: SpecFlag) -> Spec {
5654        let cmd = SpecCommand::builder().name("test").flag(flag).build();
5655        Spec {
5656            name: "test".to_string(),
5657            bin: "test".to_string(),
5658            cmd,
5659            ..Default::default()
5660        }
5661    }
5662
5663    fn parse_with_env(
5664        spec: &Spec,
5665        words: &[&str],
5666        env: &[(&str, &str)],
5667    ) -> Result<ParseOutput, miette::Error> {
5668        let env = env
5669            .iter()
5670            .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
5671            .collect();
5672        Parser::new(spec).with_env(env).parse(&input(words))
5673    }
5674
5675    fn first_string_value(parsed: &ParseOutput) -> &str {
5676        if let Some(ParseValue::String(value)) = parsed.args.values().next() {
5677            return value;
5678        }
5679        if let Some(ParseValue::String(value)) = parsed.flags.values().next() {
5680            return value;
5681        }
5682        panic!("expected first parsed value to be ParseValue::String");
5683    }
5684
5685    #[test]
5686    fn custom_environment_parser_dispatches_executable_views() {
5687        let spec: Spec = r#"
5688bin "ex"
5689view "runner" root="run"
5690cmd "run" {
5691    flag "--token <token>" env="TOKEN"
5692}
5693        "#
5694        .parse()
5695        .unwrap();
5696        let parsed = Parser::new(&spec)
5697            .with_env([("TOKEN".to_string(), "secret".to_string())].into())
5698            .parse(&input(&["runner"]))
5699            .unwrap();
5700
5701        assert_eq!(parsed.cmd.name, "runner");
5702        assert!(parsed.flags.iter().any(|(flag, value)| flag.name == "token"
5703            && matches!(value, ParseValue::String(value) if value == "secret")));
5704    }
5705
5706    #[test]
5707    fn an_executable_view_keeps_the_hosts_version_action() {
5708        let spec: Spec = r#"
5709bin "ex"
5710version "1.2.3"
5711flag "-V --version" action="version"
5712flag "--verbose" global=#true
5713view "runner" root="run" globals=#true
5714cmd "run"
5715        "#
5716        .parse()
5717        .unwrap();
5718
5719        let error = Parser::new(&spec)
5720            .parse(&input(&["runner", "--version"]))
5721            .expect_err("the host version action should answer before view projection");
5722        assert_eq!(error.to_string(), "1.2.3");
5723
5724        let error = Parser::new(&spec)
5725            .parse(&input(&["runner", "--verbose", "--version"]))
5726            .expect_err("the host version action should remain after a carried global");
5727        assert_eq!(error.to_string(), "1.2.3");
5728    }
5729
5730    fn flag_string_value<'a>(parsed: &'a ParseOutput, name: &str) -> &'a str {
5731        let flag = parsed
5732            .flags
5733            .keys()
5734            .find(|flag| flag.name == name)
5735            .unwrap_or_else(|| panic!("expected flag {name}"));
5736        let value = parsed
5737            .flags
5738            .get(flag)
5739            .unwrap_or_else(|| panic!("expected value for flag {name}"));
5740        match value {
5741            ParseValue::String(value) => value,
5742            _ => panic!("expected flag {name} to be ParseValue::String"),
5743        }
5744    }
5745
5746    fn assert_parse_err(result: Result<ParseOutput, miette::Error>, expected: &str) {
5747        let err = result.expect_err("expected parser error");
5748        assert_eq!(format!("{err}"), expected);
5749    }
5750
5751    #[test]
5752    fn a_short_version_action_falls_back_to_the_long_version() {
5753        let flag = SpecFlag::builder()
5754            .short('R')
5755            .action(SpecFlagAction::Version)
5756            .build();
5757        let spec = Spec {
5758            name: "test".to_string(),
5759            bin: "test".to_string(),
5760            long_version: Some("1.2.3\ncommit abc123".to_string()),
5761            ..Default::default()
5762        };
5763        let UsageErr::Version(version) = render_action_err(&spec, &spec.cmd, &flag, "-R") else {
5764            panic!("expected version action")
5765        };
5766        assert_eq!(version, "1.2.3\ncommit abc123");
5767    }
5768
5769    #[cfg(feature = "unstable_choices_env")]
5770    fn spec_arg_choices_env(key: &str) -> Spec {
5771        spec_with_arg(
5772            SpecArg::builder()
5773                .name("env")
5774                .choices_env(key)
5775                .required(false)
5776                .build(),
5777        )
5778    }
5779
5780    #[cfg(feature = "unstable_choices_env")]
5781    fn spec_flag_choices_env(key: &str) -> Spec {
5782        spec_with_flag(
5783            SpecFlag::builder()
5784                .long("env")
5785                .arg(SpecArg::builder().name("env").choices_env(key).build())
5786                .build(),
5787        )
5788    }
5789
5790    #[test]
5791    fn test_parse() {
5792        let cmd = SpecCommand::builder()
5793            .name("test")
5794            .arg(SpecArg::builder().name("arg").build())
5795            .flag(SpecFlag::builder().long("flag").build())
5796            .build();
5797        let spec = Spec {
5798            name: "test".to_string(),
5799            bin: "test".to_string(),
5800            cmd,
5801            ..Default::default()
5802        };
5803        let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
5804        let parsed = parse(&spec, &input).unwrap();
5805        assert_eq!(parsed.cmds.len(), 1);
5806        assert_eq!(parsed.cmds[0].name, "test");
5807        assert_eq!(parsed.args.len(), 1);
5808        assert_eq!(parsed.flags.len(), 1);
5809        assert_eq!(parsed.available_flags.len(), 1);
5810    }
5811
5812    #[test]
5813    fn test_flag_overrides_last_occurrence_wins() {
5814        let spec: Spec = r#"
5815flag "--stdin" default=#true
5816flag "--file <file>" overrides="--stdin"
5817        "#
5818        .parse()
5819        .unwrap();
5820
5821        let file_wins = parse(&spec, &input(&["test", "--stdin", "--file", "input.txt"])).unwrap();
5822        assert_eq!(file_wins.flags.len(), 1);
5823        assert_eq!(flag_string_value(&file_wins, "file"), "input.txt");
5824        assert!(!file_wins.flags.keys().any(|flag| flag.name == "stdin"));
5825
5826        let stdin_wins = parse(&spec, &input(&["test", "--file", "input.txt", "--stdin"])).unwrap();
5827        assert_eq!(stdin_wins.flags.len(), 1);
5828        assert!(stdin_wins.flags.keys().any(|flag| flag.name == "stdin"));
5829        assert!(!stdin_wins.flags.keys().any(|flag| flag.name == "file"));
5830    }
5831
5832    #[test]
5833    fn test_flag_override_clears_pending_value() {
5834        let spec: Spec = r#"
5835flag "--file <file>" overrides="--stdin"
5836flag "--stdin"
5837arg "[input]"
5838        "#
5839        .parse()
5840        .unwrap();
5841
5842        let parsed = parse(&spec, &input(&["test", "--file", "--stdin", "input.txt"])).unwrap();
5843        assert_eq!(parsed.flags.len(), 1);
5844        assert!(parsed.flags.keys().any(|flag| flag.name == "stdin"));
5845        assert_eq!(first_string_value(&parsed), "input.txt");
5846    }
5847
5848    #[cfg(unix)]
5849    #[test]
5850    fn a_mount_on_the_root_discovers_subcommands() {
5851        // The root is a command like any other, so it can find its own subcommands
5852        // by running something. Uses `echo` rather than a fixture because resolving
5853        // a mount is what is being tested.
5854        let spec: Spec = r#"
5855name "ex"
5856bin "ex"
5857cmd "declared"
5858mount run="echo 'cmd \"discovered\"'"
5859"#
5860        .parse()
5861        .unwrap();
5862
5863        let out = parse(&spec, &["ex".to_string(), "discovered".to_string()]).unwrap();
5864        assert_eq!(out.cmd.name, "discovered");
5865    }
5866
5867    #[test]
5868    fn injected_mount_outputs_are_complete_and_never_fall_back_to_processes() {
5869        let spec: Spec = r#"
5870name "ex"
5871bin "ex"
5872mount run="this command must never run"
5873cmd "declared"
5874"#
5875        .parse()
5876        .unwrap();
5877
5878        Parser::new(&spec)
5879            .with_mount_outputs(HashMap::new())
5880            .parse(&input(&["ex", "declared"]))
5881            .expect("a declared command does not resolve the mount");
5882
5883        let error = Parser::new(&spec)
5884            .with_mount_outputs(HashMap::new())
5885            .parse(&input(&["ex", "discovered"]))
5886            .unwrap_err();
5887        assert!(
5888            error
5889                .to_string()
5890                .contains("No injected output was provided for mount command"),
5891            "{error}"
5892        );
5893    }
5894
5895    #[cfg(unix)]
5896    #[test]
5897    fn completion_sees_root_mounted_commands_with_nothing_typed() {
5898        // The case a root mount exists for. `mycli <tab>` has no word to trigger
5899        // discovery with, so a completion has to resolve up front or the mounted
5900        // commands are never offered.
5901        let spec: Spec = r#"
5902name "ex"
5903bin "ex"
5904cmd "declared"
5905mount run="echo 'cmd \"discovered\"'"
5906"#
5907        .parse()
5908        .unwrap();
5909
5910        let out = parse_partial(&spec, &["ex".to_string()]).unwrap();
5911        assert!(
5912            out.cmd.subcommands.contains_key("discovered"),
5913            "a completion should see mounted commands; got {:?}",
5914            out.cmd.subcommands.keys().collect::<Vec<_>>()
5915        );
5916    }
5917
5918    #[cfg(unix)]
5919    #[test]
5920    fn completion_and_execution_agree_about_discovery() {
5921        // Offering a command that a real parse would hand to the default instead is
5922        // worse than not offering it, so the gate applies to both paths. The mount
5923        // fails if it runs, which is how both halves are checked at once.
5924        let spec: Spec = r#"
5925name "ex"
5926bin "ex"
5927default_subcommand "run"
5928cmd "run" {
5929  arg "<task>"
5930}
5931mount run="exit 1"
5932"#
5933        .parse()
5934        .unwrap();
5935
5936        let out = parse_partial(&spec, &["ex".to_string()]).unwrap();
5937        assert!(
5938            !out.cmd.subcommands.contains_key("discovered"),
5939            "a completion must not offer what execution will not route"
5940        );
5941
5942        let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
5943        assert_eq!(out.cmd.name, "run");
5944    }
5945
5946    #[cfg(unix)]
5947    #[test]
5948    fn a_default_subcommand_outranks_discovery() {
5949        // The default already says what an unmatched word means, and says it for
5950        // free. The mount fails if it runs, so parsing proves discovery was skipped.
5951        let spec: Spec = r#"
5952name "ex"
5953bin "ex"
5954default_subcommand "run"
5955cmd "run" {
5956  arg "<task>"
5957}
5958mount run="exit 1"
5959"#
5960        .parse()
5961        .unwrap();
5962
5963        let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
5964        assert_eq!(out.cmd.name, "run");
5965    }
5966
5967    #[cfg(unix)]
5968    #[test]
5969    fn a_mount_may_ask_to_outrank_the_default() {
5970        // Opting in, and paying for it: discovery runs first, so a discovered
5971        // command wins over the fallback.
5972        let spec: Spec = r#"
5973name "ex"
5974bin "ex"
5975default_subcommand "run"
5976cmd "run" {
5977  arg "<task>"
5978}
5979mount run="echo 'cmd \"discovered\"'" overrides_default=#true
5980"#
5981        .parse()
5982        .unwrap();
5983
5984        let out = parse(&spec, &["ex".to_string(), "discovered".to_string()]).unwrap();
5985        assert_eq!(out.cmd.name, "discovered");
5986
5987        // A word it does not know still reaches the default.
5988        let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
5989        assert_eq!(out.cmd.name, "run");
5990    }
5991
5992    #[cfg(unix)]
5993    #[test]
5994    fn a_flag_does_not_run_the_mount() {
5995        // A flag matches no subcommand, which would have been enough to trigger
5996        // discovery — so `ex --help` spawned a process. The mount fails if it runs,
5997        // so parsing at all is the proof that it did not.
5998        let spec: Spec = r#"
5999name "ex"
6000bin "ex"
6001flag "--verbose"
6002cmd "declared"
6003mount run="exit 1"
6004"#
6005        .parse()
6006        .unwrap();
6007
6008        let out = parse(&spec, &["ex".to_string(), "--verbose".to_string()]).unwrap();
6009        assert_eq!(out.cmd.name, "ex");
6010    }
6011
6012    #[cfg(unix)]
6013    #[test]
6014    fn a_declared_subcommand_does_not_run_the_mount() {
6015        // The mount would fail if it ran, so this parsing at all is the proof that
6016        // discovery is skipped when the word is already known. Worth pinning: a root
6017        // mount that resolved eagerly would spawn a process on every invocation.
6018        let spec: Spec = r#"
6019name "ex"
6020bin "ex"
6021cmd "declared"
6022mount run="exit 1"
6023"#
6024        .parse()
6025        .unwrap();
6026
6027        let out = parse(&spec, &["ex".to_string(), "declared".to_string()]).unwrap();
6028        assert_eq!(out.cmd.name, "declared");
6029    }
6030
6031    #[test]
6032    fn a_root_mount_survives_being_written_out() {
6033        let spec: Spec = "name \"ex\"\nbin \"ex\"\nmount run=\"ex plugins --usage\"\n"
6034            .parse()
6035            .unwrap();
6036        assert_eq!(spec.cmd.mounts.len(), 1);
6037
6038        let reparsed: Spec = spec.to_string().parse().unwrap();
6039        assert_eq!(reparsed.cmd.mounts.len(), 1, "written:\n{spec}");
6040        assert_eq!(reparsed.cmd.mounts[0].run, "ex plugins --usage");
6041    }
6042
6043    #[test]
6044    fn test_mount_prefix_applies_flag_overrides() {
6045        let stdin = Arc::new(
6046            SpecFlag::builder()
6047                .name("stdin")
6048                .long("stdin")
6049                .global(true)
6050                .build(),
6051        );
6052        let file = Arc::new(
6053            SpecFlag::builder()
6054                .name("file")
6055                .long("file")
6056                .arg(SpecArg::builder().name("file").build())
6057                .global(true)
6058                .overrides_with(vec!["--stdin".to_string()])
6059                .build(),
6060        );
6061        let mut prefix_flags = vec![(stdin, vec!["--stdin".to_string()])];
6062
6063        apply_prefix_flag_overrides(&mut prefix_flags, Arc::clone(&file));
6064        prefix_flags.push((file, vec!["--file".to_string(), "input.txt".to_string()]));
6065
6066        assert_eq!(mount_prefix_words(&prefix_flags), ["--file", "input.txt"]);
6067    }
6068
6069    #[test]
6070    fn test_flag_override_suppresses_env_value() {
6071        let spec: Spec = r#"
6072flag "--stdin" env="USE_STDIN"
6073flag "--file <file>" overrides="--stdin"
6074        "#
6075        .parse()
6076        .unwrap();
6077
6078        let parsed = parse_with_env(
6079            &spec,
6080            &["test", "--file", "input.txt"],
6081            &[("USE_STDIN", "true")],
6082        )
6083        .unwrap();
6084        assert_eq!(parsed.flags.len(), 1);
6085        assert_eq!(flag_string_value(&parsed, "file"), "input.txt");
6086    }
6087
6088    #[test]
6089    fn test_flag_override_suppresses_required_check() {
6090        let spec: Spec = r#"
6091flag "--stdin" required=#true
6092flag "--file <file>" overrides="--stdin"
6093        "#
6094        .parse()
6095        .unwrap();
6096
6097        let parsed = parse(&spec, &input(&["test", "--file", "input.txt"])).unwrap();
6098        assert_eq!(parsed.flags.len(), 1);
6099        assert_eq!(flag_string_value(&parsed, "file"), "input.txt");
6100    }
6101
6102    #[test]
6103    fn test_flag_required_if() {
6104        let spec: Spec = r#"
6105flag "--dir <dir>"
6106flag "--file <file>" required_if="--dir"
6107        "#
6108        .parse()
6109        .unwrap();
6110
6111        parse(&spec, &input(&["test"])).unwrap();
6112        assert_parse_err(
6113            parse(&spec, &input(&["test", "--dir", "src"])),
6114            "Missing required flag: --file <file>",
6115        );
6116        parse(
6117            &spec,
6118            &input(&["test", "--dir", "src", "--file", "input.txt"]),
6119        )
6120        .unwrap();
6121    }
6122
6123    #[test]
6124    fn test_flag_required_unless() {
6125        let spec: Spec = r#"
6126flag "--stdin"
6127flag "--file <file>" required_unless="--stdin"
6128        "#
6129        .parse()
6130        .unwrap();
6131
6132        assert_parse_err(
6133            parse(&spec, &input(&["test"])),
6134            "Missing required flag: --file <file>",
6135        );
6136        parse(&spec, &input(&["test", "--stdin"])).unwrap();
6137        parse(&spec, &input(&["test", "--file", "input.txt"])).unwrap();
6138    }
6139
6140    #[test]
6141    fn complete_required_relationship_truth_tables() {
6142        let spec: Spec = r#"
6143name "ex"
6144bin "ex"
6145flag "--mode <mode>"
6146flag "--scope <scope>"
6147flag "--token <token>" {
6148    required_if_eq "--mode" "remote"
6149}
6150flag "--approval <approval>" {
6151    required_if_eq_all "--mode" "remote" "--scope" "global"
6152}
6153flag "--input <input>" {
6154    required_unless "--stdin" "--file"
6155}
6156flag "--checksum <checksum>" {
6157    required_unless_all "--stdin" "--file"
6158}
6159flag "--stdin"
6160flag "--file <file>"
6161arg "[request]" {
6162    requires "--mode" "--scope"
6163}
6164"#
6165        .parse()
6166        .unwrap();
6167        let parse_args = |args: &[&str]| {
6168            parse(
6169                &spec,
6170                &args
6171                    .iter()
6172                    .map(|arg| (*arg).to_string())
6173                    .collect::<Vec<_>>(),
6174            )
6175        };
6176
6177        assert!(parse_args(&["ex", "--mode", "remote", "--stdin"]).is_err());
6178        assert!(parse_args(&[
6179            "ex", "--mode", "remote", "--token", "secret", "--scope", "global", "--stdin",
6180        ])
6181        .is_err());
6182        parse_args(&[
6183            "ex",
6184            "--mode",
6185            "remote",
6186            "--token",
6187            "secret",
6188            "--scope",
6189            "global",
6190            "--approval",
6191            "yes",
6192            "--stdin",
6193            "--file",
6194            "in",
6195        ])
6196        .unwrap();
6197        parse_args(&[
6198            "ex",
6199            "--mode",
6200            "local",
6201            "--scope",
6202            "project",
6203            "--stdin",
6204            "--checksum",
6205            "sum",
6206            "request.json",
6207        ])
6208        .unwrap();
6209
6210        let reparsed: Spec = spec.to_string().parse().unwrap();
6211        assert_eq!(reparsed.cmd.flags[2].required_if_eq.len(), 1);
6212        assert_eq!(reparsed.cmd.flags[3].required_if_eq_all.len(), 2);
6213        assert_eq!(reparsed.cmd.flags[5].required_unless_all.len(), 2);
6214        assert_eq!(reparsed.cmd.args[0].requires.len(), 2);
6215    }
6216
6217    #[test]
6218    fn test_conditional_requirements_treat_env_as_explicit() {
6219        let spec: Spec = r#"
6220flag "--dir <dir>" env="INPUT_DIR"
6221flag "--stdin" env="USE_STDIN"
6222flag "--file <file>" required_if="--dir" required_unless="--stdin"
6223        "#
6224        .parse()
6225        .unwrap();
6226
6227        assert_parse_err(
6228            parse_with_env(&spec, &["test"], &[("INPUT_DIR", "src")]),
6229            "Missing required flag: --file <file>",
6230        );
6231        parse_with_env(&spec, &["test"], &[("USE_STDIN", "true")]).unwrap();
6232    }
6233
6234    #[test]
6235    fn test_custom_env_does_not_fall_back_to_process_env() {
6236        assert!(std::env::var("PATH").is_ok());
6237        let spec: Spec = r#"flag "--file <file>" env="PATH" required=#true"#.parse().unwrap();
6238
6239        assert_parse_err(
6240            parse_with_env(&spec, &["test"], &[]),
6241            "Missing required flag: --file <file>",
6242        );
6243    }
6244
6245    #[test]
6246    fn test_conditional_requirements_ignore_defaults_on_condition_flags() {
6247        let spec: Spec = r#"
6248flag "--dir <dir>" default="src"
6249flag "--file <file>" required_if="--dir"
6250        "#
6251        .parse()
6252        .unwrap();
6253
6254        parse(&spec, &input(&["test"])).unwrap();
6255    }
6256
6257    #[test]
6258    fn test_conditional_requirements_see_overridden_flags_as_absent() {
6259        let spec: Spec = r#"
6260flag "--stdin"
6261flag "--dir <dir>" overrides="--stdin"
6262flag "--file <file>" required_unless="--stdin"
6263        "#
6264        .parse()
6265        .unwrap();
6266
6267        assert_parse_err(
6268            parse(&spec, &input(&["test", "--stdin", "--dir", "src"])),
6269            "Missing required flag: --file <file>",
6270        );
6271    }
6272
6273    #[test]
6274    fn short_flag_is_one_character_not_one_byte() {
6275        // A short is declared and read by character. Counting bytes instead either
6276        // refuses the declaration or slices the token inside the character, and clap
6277        // — which many specs are generated from — accepts shorts like this one.
6278        let spec = spec_with_flag(
6279            SpecFlag::builder()
6280                .short('磨')
6281                .long("polish")
6282                .arg(SpecArg::builder().name("opt").build())
6283                .build(),
6284        );
6285        let attached = Parser::new(&spec)
6286            .parse(&input(&["test", "-磨VALUE"]))
6287            .unwrap();
6288        assert_eq!(flag_string_value(&attached, "polish"), "VALUE");
6289        let detached = Parser::new(&spec)
6290            .parse(&input(&["test", "-磨", "V"]))
6291            .unwrap();
6292        assert_eq!(flag_string_value(&detached, "polish"), "V");
6293    }
6294
6295    #[test]
6296    fn test_as_env() {
6297        let cmd = SpecCommand::builder()
6298            .name("test")
6299            .arg(SpecArg::builder().name("arg").build())
6300            .flag(SpecFlag::builder().long("flag").build())
6301            .flag(
6302                SpecFlag::builder()
6303                    .long("force")
6304                    .negate("--no-force")
6305                    .build(),
6306            )
6307            .build();
6308        let spec = Spec {
6309            name: "test".to_string(),
6310            bin: "test".to_string(),
6311            cmd,
6312            ..Default::default()
6313        };
6314        let input = vec![
6315            "test".to_string(),
6316            "--flag".to_string(),
6317            "--no-force".to_string(),
6318        ];
6319        let parsed = parse(&spec, &input).unwrap();
6320        let env = parsed.as_env();
6321        assert_eq!(env.len(), 2);
6322        assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
6323        assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
6324    }
6325
6326    #[test]
6327    fn test_arg_env_var() {
6328        let cmd = SpecCommand::builder()
6329            .name("test")
6330            .arg(
6331                SpecArg::builder()
6332                    .name("input")
6333                    .env("TEST_ARG_INPUT")
6334                    .required(true)
6335                    .build(),
6336            )
6337            .build();
6338        let spec = Spec {
6339            name: "test".to_string(),
6340            bin: "test".to_string(),
6341            cmd,
6342            ..Default::default()
6343        };
6344
6345        // Set env var
6346        std::env::set_var("TEST_ARG_INPUT", "test_file.txt");
6347
6348        let input = vec!["test".to_string()];
6349        let parsed = parse(&spec, &input).unwrap();
6350
6351        assert_eq!(parsed.args.len(), 1);
6352        let arg = parsed.args.keys().next().unwrap();
6353        assert_eq!(arg.name, "input");
6354        let value = parsed.args.values().next().unwrap();
6355        assert_eq!(value.to_string(), "test_file.txt");
6356
6357        // Clean up
6358        std::env::remove_var("TEST_ARG_INPUT");
6359    }
6360
6361    #[test]
6362    fn test_flag_env_var_with_arg() {
6363        let cmd = SpecCommand::builder()
6364            .name("test")
6365            .flag(
6366                SpecFlag::builder()
6367                    .long("output")
6368                    .env("TEST_FLAG_OUTPUT")
6369                    .arg(SpecArg::builder().name("file").build())
6370                    .build(),
6371            )
6372            .build();
6373        let spec = Spec {
6374            name: "test".to_string(),
6375            bin: "test".to_string(),
6376            cmd,
6377            ..Default::default()
6378        };
6379
6380        // Set env var
6381        std::env::set_var("TEST_FLAG_OUTPUT", "output.txt");
6382
6383        let input = vec!["test".to_string()];
6384        let parsed = parse(&spec, &input).unwrap();
6385
6386        assert_eq!(parsed.flags.len(), 1);
6387        let flag = parsed.flags.keys().next().unwrap();
6388        assert_eq!(flag.name, "output");
6389        let value = parsed.flags.values().next().unwrap();
6390        assert_eq!(value.to_string(), "output.txt");
6391
6392        // Clean up
6393        std::env::remove_var("TEST_FLAG_OUTPUT");
6394    }
6395
6396    #[test]
6397    fn test_flag_env_var_boolean() {
6398        let cmd = SpecCommand::builder()
6399            .name("test")
6400            .flag(
6401                SpecFlag::builder()
6402                    .long("verbose")
6403                    .env("TEST_FLAG_VERBOSE")
6404                    .build(),
6405            )
6406            .build();
6407        let spec = Spec {
6408            name: "test".to_string(),
6409            bin: "test".to_string(),
6410            cmd,
6411            ..Default::default()
6412        };
6413
6414        // Set env var to true
6415        std::env::set_var("TEST_FLAG_VERBOSE", "true");
6416
6417        let input = vec!["test".to_string()];
6418        let parsed = parse(&spec, &input).unwrap();
6419
6420        assert_eq!(parsed.flags.len(), 1);
6421        let flag = parsed.flags.keys().next().unwrap();
6422        assert_eq!(flag.name, "verbose");
6423        let value = parsed.flags.values().next().unwrap();
6424        assert_eq!(value.to_string(), "true");
6425
6426        // Clean up
6427        std::env::remove_var("TEST_FLAG_VERBOSE");
6428    }
6429
6430    #[test]
6431    fn test_env_var_precedence() {
6432        // CLI args should take precedence over env vars
6433        let cmd = SpecCommand::builder()
6434            .name("test")
6435            .arg(
6436                SpecArg::builder()
6437                    .name("input")
6438                    .env("TEST_PRECEDENCE_INPUT")
6439                    .required(true)
6440                    .build(),
6441            )
6442            .build();
6443        let spec = Spec {
6444            name: "test".to_string(),
6445            bin: "test".to_string(),
6446            cmd,
6447            ..Default::default()
6448        };
6449
6450        // Set env var
6451        std::env::set_var("TEST_PRECEDENCE_INPUT", "env_file.txt");
6452
6453        let input = vec!["test".to_string(), "cli_file.txt".to_string()];
6454        let parsed = parse(&spec, &input).unwrap();
6455
6456        assert_eq!(parsed.args.len(), 1);
6457        let value = parsed.args.values().next().unwrap();
6458        // CLI arg should take precedence
6459        assert_eq!(value.to_string(), "cli_file.txt");
6460
6461        // Clean up
6462        std::env::remove_var("TEST_PRECEDENCE_INPUT");
6463    }
6464
6465    #[test]
6466    fn test_flag_var_true_with_single_default() {
6467        // When var=true and default="bar", the default should be MultiString(["bar"])
6468        let cmd = SpecCommand::builder()
6469            .name("test")
6470            .flag(
6471                SpecFlag::builder()
6472                    .long("foo")
6473                    .var(true)
6474                    .arg(SpecArg::builder().name("foo").build())
6475                    .default_value("bar")
6476                    .build(),
6477            )
6478            .build();
6479        let spec = Spec {
6480            name: "test".to_string(),
6481            bin: "test".to_string(),
6482            cmd,
6483            ..Default::default()
6484        };
6485
6486        // User doesn't provide the flag
6487        let input = vec!["test".to_string()];
6488        let parsed = parse(&spec, &input).unwrap();
6489
6490        assert_eq!(parsed.flags.len(), 1);
6491        let flag = parsed.flags.keys().next().unwrap();
6492        assert_eq!(flag.name, "foo");
6493        let value = parsed.flags.values().next().unwrap();
6494        // Should be MultiString, not String
6495        match value {
6496            ParseValue::MultiString(v) => {
6497                assert_eq!(v.len(), 1);
6498                assert_eq!(v[0], "bar");
6499            }
6500            _ => panic!("Expected MultiString, got {:?}", value),
6501        }
6502    }
6503
6504    #[test]
6505    fn test_flag_var_true_with_multiple_defaults() {
6506        // When var=true and multiple defaults, should return MultiString(["xyz", "bar"])
6507        let cmd = SpecCommand::builder()
6508            .name("test")
6509            .flag(
6510                SpecFlag::builder()
6511                    .long("foo")
6512                    .var(true)
6513                    .arg(SpecArg::builder().name("foo").build())
6514                    .default_values(["xyz", "bar"])
6515                    .build(),
6516            )
6517            .build();
6518        let spec = Spec {
6519            name: "test".to_string(),
6520            bin: "test".to_string(),
6521            cmd,
6522            ..Default::default()
6523        };
6524
6525        // User doesn't provide the flag
6526        let input = vec!["test".to_string()];
6527        let parsed = parse(&spec, &input).unwrap();
6528
6529        assert_eq!(parsed.flags.len(), 1);
6530        let value = parsed.flags.values().next().unwrap();
6531        // Should be MultiString with both values
6532        match value {
6533            ParseValue::MultiString(v) => {
6534                assert_eq!(v.len(), 2);
6535                assert_eq!(v[0], "xyz");
6536                assert_eq!(v[1], "bar");
6537            }
6538            _ => panic!("Expected MultiString, got {:?}", value),
6539        }
6540    }
6541
6542    #[test]
6543    fn test_flag_var_false_with_default_remains_string() {
6544        // When var=false (default), the default should still be String("bar")
6545        let cmd = SpecCommand::builder()
6546            .name("test")
6547            .flag(
6548                SpecFlag::builder()
6549                    .long("foo")
6550                    .var(false) // Default behavior
6551                    .arg(SpecArg::builder().name("foo").build())
6552                    .default_value("bar")
6553                    .build(),
6554            )
6555            .build();
6556        let spec = Spec {
6557            name: "test".to_string(),
6558            bin: "test".to_string(),
6559            cmd,
6560            ..Default::default()
6561        };
6562
6563        // User doesn't provide the flag
6564        let input = vec!["test".to_string()];
6565        let parsed = parse(&spec, &input).unwrap();
6566
6567        assert_eq!(parsed.flags.len(), 1);
6568        let value = parsed.flags.values().next().unwrap();
6569        // Should be String, not MultiString
6570        match value {
6571            ParseValue::String(s) => {
6572                assert_eq!(s, "bar");
6573            }
6574            _ => panic!("Expected String, got {:?}", value),
6575        }
6576    }
6577
6578    #[test]
6579    fn test_arg_var_true_with_single_default() {
6580        // When arg has var=true and default="bar", the default should be MultiString(["bar"])
6581        let cmd = SpecCommand::builder()
6582            .name("test")
6583            .arg(
6584                SpecArg::builder()
6585                    .name("files")
6586                    .var(true)
6587                    .default_value("default.txt")
6588                    .required(false)
6589                    .build(),
6590            )
6591            .build();
6592        let spec = Spec {
6593            name: "test".to_string(),
6594            bin: "test".to_string(),
6595            cmd,
6596            ..Default::default()
6597        };
6598
6599        // User doesn't provide the arg
6600        let input = vec!["test".to_string()];
6601        let parsed = parse(&spec, &input).unwrap();
6602
6603        assert_eq!(parsed.args.len(), 1);
6604        let value = parsed.args.values().next().unwrap();
6605        // Should be MultiString, not String
6606        match value {
6607            ParseValue::MultiString(v) => {
6608                assert_eq!(v.len(), 1);
6609                assert_eq!(v[0], "default.txt");
6610            }
6611            _ => panic!("Expected MultiString, got {:?}", value),
6612        }
6613    }
6614
6615    #[test]
6616    fn test_arg_var_true_with_multiple_defaults() {
6617        // When arg has var=true and multiple defaults
6618        let cmd = SpecCommand::builder()
6619            .name("test")
6620            .arg(
6621                SpecArg::builder()
6622                    .name("files")
6623                    .var(true)
6624                    .default_values(["file1.txt", "file2.txt"])
6625                    .required(false)
6626                    .build(),
6627            )
6628            .build();
6629        let spec = Spec {
6630            name: "test".to_string(),
6631            bin: "test".to_string(),
6632            cmd,
6633            ..Default::default()
6634        };
6635
6636        // User doesn't provide the arg
6637        let input = vec!["test".to_string()];
6638        let parsed = parse(&spec, &input).unwrap();
6639
6640        assert_eq!(parsed.args.len(), 1);
6641        let value = parsed.args.values().next().unwrap();
6642        // Should be MultiString with both values
6643        match value {
6644            ParseValue::MultiString(v) => {
6645                assert_eq!(v.len(), 2);
6646                assert_eq!(v[0], "file1.txt");
6647                assert_eq!(v[1], "file2.txt");
6648            }
6649            _ => panic!("Expected MultiString, got {:?}", value),
6650        }
6651    }
6652
6653    #[test]
6654    fn test_arg_var_false_with_default_remains_string() {
6655        // When arg has var=false (default), the default should still be String
6656        let cmd = SpecCommand::builder()
6657            .name("test")
6658            .arg(
6659                SpecArg::builder()
6660                    .name("file")
6661                    .var(false)
6662                    .default_value("default.txt")
6663                    .required(false)
6664                    .build(),
6665            )
6666            .build();
6667        let spec = Spec {
6668            name: "test".to_string(),
6669            bin: "test".to_string(),
6670            cmd,
6671            ..Default::default()
6672        };
6673
6674        // User doesn't provide the arg
6675        let input = vec!["test".to_string()];
6676        let parsed = parse(&spec, &input).unwrap();
6677
6678        assert_eq!(parsed.args.len(), 1);
6679        let value = parsed.args.values().next().unwrap();
6680        // Should be String, not MultiString
6681        match value {
6682            ParseValue::String(s) => {
6683                assert_eq!(s, "default.txt");
6684            }
6685            _ => panic!("Expected String, got {:?}", value),
6686        }
6687    }
6688
6689    #[test]
6690    fn test_scalar_defaults_validate_only_first_default_choice() {
6691        let specs = [
6692            spec_with_arg(
6693                SpecArg::builder()
6694                    .name("env")
6695                    .var(false)
6696                    .default_values(["dev", "prod"])
6697                    .choices(["dev"])
6698                    .required(false)
6699                    .build(),
6700            ),
6701            spec_with_flag(
6702                SpecFlag::builder()
6703                    .long("env")
6704                    .arg(
6705                        SpecArg::builder()
6706                            .name("env")
6707                            .default_values(["dev", "prod"])
6708                            .choices(["dev"])
6709                            .build(),
6710                    )
6711                    .build(),
6712            ),
6713        ];
6714
6715        for spec in specs {
6716            let parsed = parse(&spec, &input(&["test"])).unwrap();
6717            assert_eq!(first_string_value(&parsed), "dev");
6718        }
6719    }
6720
6721    #[test]
6722    fn a_delimiter_turns_one_word_into_several_values() {
6723        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--tags <tag>\" var=#true delimiter=\",\"\narg \"[files]...\" var=#true delimiter=\":\"\n"
6724            .parse()
6725            .unwrap();
6726
6727        let parsed = parse(&spec, &input(&["ex", "--tags", "a,b,c", "x:y"])).unwrap();
6728        let multi = |value: &ParseValue| match value {
6729            ParseValue::MultiString(values) => values.clone(),
6730            other => panic!("expected several values, got {other:?}"),
6731        };
6732        let tags = parsed
6733            .flags
6734            .iter()
6735            .find(|(f, _)| f.name == "tags")
6736            .map(|(_, v)| v)
6737            .unwrap();
6738        assert_eq!(multi(tags), vec!["a", "b", "c"]);
6739        assert_eq!(multi(parsed.args.values().next().unwrap()), vec!["x", "y"]);
6740    }
6741
6742    #[test]
6743    fn a_positional_splits_before_its_choices_are_asked() {
6744        // The flag path did this and the positional path did not, so a word whose parts
6745        // were all choices was rejected as one value, and a bad half was reported as the
6746        // whole word.
6747        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[paths]...\" var=#true delimiter=\":\" {\n  choices \"src\" \"docs\"\n}\n"
6748            .parse()
6749            .unwrap();
6750
6751        parse(&spec, &input(&["ex", "src:docs"])).expect("both halves are choices");
6752
6753        let err = parse(&spec, &input(&["ex", "src:nowhere"])).unwrap_err();
6754        let message = err.to_string();
6755        assert!(message.contains("nowhere"), "{message}");
6756        assert!(
6757            !message.contains("src:nowhere"),
6758            "the bad half should be named, not the whole word: {message}"
6759        );
6760    }
6761
6762    #[test]
6763    fn a_split_value_is_counted_and_judged_as_values() {
6764        // Split during the parse rather than after it, so everything downstream sees the
6765        // values the user meant rather than the words they typed: `choices` judges each
6766        // one, and the bounds count them.
6767        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <e>\" var=#true delimiter=\",\" var_max=2 {\n  choices \"dev\" \"prod\"\n}\n"
6768            .parse()
6769            .unwrap();
6770
6771        parse(&spec, &input(&["ex", "--env", "dev,prod"])).expect("two values, both allowed");
6772        let err = parse(&spec, &input(&["ex", "--env", "dev,staging"])).unwrap_err();
6773        assert!(err.to_string().contains("staging"), "{err}");
6774        assert!(
6775            parse(&spec, &input(&["ex", "--env", "dev,prod,dev"])).is_err(),
6776            "three values should breach var_max=2"
6777        );
6778    }
6779
6780    #[test]
6781    fn a_split_bound_counts_one_occurrence_at_a_time() {
6782        // The bound on a variadic flag *argument* is what one occurrence may take. Without a
6783        // delimiter the collection simply stops at it, so it could never be exceeded; a word
6784        // carrying several values can carry an occurrence past it in one step, and that is
6785        // the only way this bound is ever breached.
6786        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--include <pattern>...\" delimiter=\",\" {\n  arg \"<pattern>...\" var=#true var_max=2\n}\n"
6787            .parse()
6788            .unwrap();
6789
6790        parse(&spec, &input(&["ex", "--include", "a,b"])).expect("exactly the bound is fine");
6791        assert!(
6792            parse(&spec, &input(&["ex", "--include", "a,b,c"])).is_err(),
6793            "three values out of one word is still three values"
6794        );
6795        // The rule the corpus documents for plain words, on split ones: a second occurrence
6796        // starts counting again rather than adding to the first.
6797        parse(
6798            &spec,
6799            &input(&["ex", "--include", "a,b", "--include", "c,d"]),
6800        )
6801        .expect("two per occurrence, twice, is within the bound");
6802    }
6803
6804    #[test]
6805    fn a_nested_minimum_is_checked_once_per_flag_occurrence() {
6806        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pair <value>...\" {\n  arg \"<value>...\" var=#true var_min=2 var_max=2\n}\n"
6807            .parse()
6808            .unwrap();
6809
6810        parse(
6811            &spec,
6812            &input(&["ex", "--pair", "a", "b", "--pair", "c", "d"]),
6813        )
6814        .expect("each occurrence satisfies the bound independently");
6815
6816        let error = parse(&spec, &input(&["ex", "--pair", "a", "--pair", "b", "c"])).unwrap_err();
6817        assert!(
6818            error
6819                .to_string()
6820                .contains("requires at least 2 value(s), got 1"),
6821            "{error:?}"
6822        );
6823    }
6824
6825    #[test]
6826    fn an_exclusive_flag_has_to_be_alone() {
6827        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--verbose\"\narg \"[target]\"\n"
6828            .parse()
6829            .unwrap();
6830
6831        parse(&spec, &input(&["ex", "--dump"])).expect("alone is the point");
6832
6833        // Any other flag.
6834        let err = parse(&spec, &input(&["ex", "--dump", "--verbose"])).unwrap_err();
6835        assert!(err.to_string().contains("on its own"), "{err}");
6836
6837        // And a positional, which is what makes this more than a conflict with every
6838        // other flag.
6839        let err = parse(&spec, &input(&["ex", "--dump", "t"])).unwrap_err();
6840        assert!(err.to_string().contains("on its own"), "{err}");
6841
6842        // Not given, so it imposes nothing.
6843        parse(&spec, &input(&["ex", "--verbose", "t"])).expect("without it, nothing changes");
6844    }
6845
6846    #[test]
6847    fn an_exclusive_flag_conflicts_with_clause_arguments() {
6848        let spec: Spec = r#"name "ex"
6849bin "ex"
6850flag "--dump" exclusive=#true
6851clause "tasks" separator=":::" {
6852  arg "<task>"
6853}
6854"#
6855        .parse()
6856        .unwrap();
6857
6858        let err = parse(&spec, &input(&["ex", "--dump", "lint"])).unwrap_err();
6859        assert!(err.to_string().contains("on its own"), "{err}");
6860    }
6861
6862    #[test]
6863    fn a_partial_parse_reports_the_next_implicit_clause_argument() {
6864        let spec: Spec = r#"name "ex"
6865bin "ex"
6866clause "tools" {
6867  arg "<tool@version>"
6868}
6869"#
6870        .parse()
6871        .unwrap();
6872
6873        let parsed = parse_partial(&spec, &input(&["ex"])).unwrap();
6874
6875        assert_eq!(
6876            parsed.next_arg.as_ref().map(|arg| arg.name.as_str()),
6877            Some("tool@version")
6878        );
6879    }
6880
6881    #[test]
6882    fn an_exclusive_flag_is_not_disturbed_by_a_default() {
6883        // Only what was supplied counts, as `conflicts` reads it. A default counting as
6884        // company would make an exclusive flag unusable on any command that has one.
6885        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--jobs <n>\" default=\"4\"\n"
6886            .parse()
6887            .unwrap();
6888
6889        parse(&spec, &input(&["ex", "--dump"])).expect("a default is nobody saying anything");
6890        assert!(parse(&spec, &input(&["ex", "--dump", "--jobs", "8"])).is_err());
6891    }
6892
6893    #[test]
6894    fn an_exclusive_flag_bypasses_required_siblings() {
6895        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--out <path>\" required=#true\narg \"<target>\"\n"
6896            .parse()
6897            .unwrap();
6898
6899        parse(&spec, &input(&["ex", "--dump"]))
6900            .expect("exclusive is the command's requiredness escape");
6901        assert!(parse(
6902            &spec,
6903            &input(&["ex", "--dump", "--out", "somewhere", "target"])
6904        )
6905        .is_err());
6906    }
6907
6908    #[test]
6909    fn an_environment_value_counts_for_an_exclusive_flag() {
6910        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--out <path>\" env=\"EX_OUT\"\n"
6911            .parse()
6912            .unwrap();
6913
6914        assert!(parse_with_env(&spec, &["ex", "--dump"], &[("EX_OUT", "somewhere")]).is_err());
6915        parse_with_env(&spec, &["ex", "--dump"], &[]).expect("without the value it is alone");
6916    }
6917
6918    #[test]
6919    fn a_selected_subcommand_counts_for_an_ancestor_exclusive_flag() {
6920        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--version\" global=#true exclusive=#true\ncmd \"run\"\n"
6921            .parse()
6922            .unwrap();
6923
6924        parse(&spec, &input(&["ex", "--version"])).expect("alone is allowed");
6925        assert!(parse(&spec, &input(&["ex", "--version", "run"])).is_err());
6926    }
6927
6928    #[test]
6929    fn a_child_exclusive_flag_is_not_mistaken_for_a_same_named_parent_flag() {
6930        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n}\n"
6931            .parse()
6932            .unwrap();
6933
6934        parse(&spec, &input(&["ex", "run", "--clean"]))
6935            .expect("the child flag is alone within the child command");
6936        assert!(
6937            parse(&spec, &input(&["ex", "--clean", "run"])).is_err(),
6938            "the parent flag still conflicts with selecting the child"
6939        );
6940    }
6941
6942    #[test]
6943    fn a_child_local_exclusive_redeclaration_belongs_to_the_child() {
6944        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n}\n"
6945            .parse()
6946            .unwrap();
6947
6948        parse(&spec, &input(&["ex", "run", "--clean"]))
6949            .expect("the child-local exclusive flag is alone inside the child command");
6950        assert!(
6951            parse(&spec, &input(&["ex", "--clean", "run"])).is_err(),
6952            "the ancestor spelling still conflicts with selecting the child"
6953        );
6954    }
6955
6956    #[test]
6957    fn a_same_named_parent_flag_is_company_for_a_child_exclusive_flag() {
6958        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" global=#true exclusive=#true\n}\n"
6959            .parse()
6960            .unwrap();
6961
6962        parse(&spec, &input(&["ex", "run", "--clean"])).expect("the child exclusive flag is alone");
6963        assert!(
6964            parse(&spec, &input(&["ex", "--clean", "run", "--clean"])).is_err(),
6965            "the distinct parent declaration is still company despite sharing a name"
6966        );
6967    }
6968
6969    #[test]
6970    fn a_local_child_redeclaration_keeps_its_exclusivity_when_merged() {
6971        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n  flag \"--verbose\"\n}\n"
6972            .parse()
6973            .unwrap();
6974
6975        parse(&spec, &input(&["ex", "run", "--clean"]))
6976            .expect("the child exclusive flag is valid alone");
6977        assert!(
6978            parse(&spec, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
6979            "merging with the inherited global must not discard child exclusivity"
6980        );
6981    }
6982
6983    #[test]
6984    fn an_orphan_parent_alias_does_not_disown_a_child_local_exclusive_flag() {
6985        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n}\n"
6986            .parse()
6987            .unwrap();
6988
6989        parse(&spec, &input(&["ex", "run", "--clean"]))
6990            .expect("the typed long form belongs to the child declaration");
6991        assert!(
6992            parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
6993            "the inherited short form still belongs to the ancestor"
6994        );
6995        assert!(
6996            parse(&spec, &input(&["ex", "run", "-c", "--clean"])).is_err(),
6997            "a child spelling cannot mask the ancestor-exclusive occurrence on the same merged flag"
6998        );
6999    }
7000
7001    #[test]
7002    fn an_inherited_alias_keeps_its_ancestor_exclusivity() {
7003        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" global=#true\n}\n"
7004            .parse()
7005            .unwrap();
7006
7007        parse(&spec, &input(&["ex", "run", "--clean"]))
7008            .expect("the child's spelling does not activate the orphan ancestor alias");
7009        assert!(
7010            parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
7011            "the inherited short alias still belongs to the ancestor exclusive flag"
7012        );
7013    }
7014
7015    #[test]
7016    fn an_inherited_negated_alias_keeps_its_ancestor_exclusivity() {
7017        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"
7018            .parse()
7019            .unwrap();
7020
7021        assert!(
7022            parse(&spec, &input(&["ex", "run", "--no-clean"])).is_err(),
7023            "the inherited negated alias still belongs to the ancestor exclusive flag"
7024        );
7025    }
7026
7027    #[test]
7028    fn a_colliding_alias_does_not_disown_the_child_from_the_rest() {
7029        // The child re-declares the inherited `--clean` as exclusive and gives it a `-c` that
7030        // an unrelated inherited global already owns. That collision is resolved in the other
7031        // global's favor, so the child's `-c` resolves elsewhere — but the child plainly owns
7032        // the `--clean` it declared, and its exclusivity holds.
7033        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"
7034            .parse()
7035            .unwrap();
7036
7037        parse(&spec, &input(&["ex", "run", "--clean"])).expect("alone is allowed");
7038        assert!(
7039            parse(&spec, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
7040            "one unrelated alias collision cannot disown the child from its own flag"
7041        );
7042    }
7043
7044    #[test]
7045    fn a_local_child_declaration_is_not_in_scope_before_the_subcommand() {
7046        // A child's *local* re-declaration describes the flag at the child. Typed ahead of the
7047        // subcommand word the flag can only be the ancestor's, because that is the only one in
7048        // scope there — so the ancestor's exclusivity is the one that answers, whichever way it
7049        // is set. The pair below differ in nothing else, which is what makes this one rule
7050        // rather than two behaviors.
7051        let quiet: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n  flag \"--verbose\"\n}\n"
7052            .parse()
7053            .unwrap();
7054        parse(&quiet, &input(&["ex", "--clean", "run", "--verbose"]))
7055            .expect("the ancestor owns this occurrence, and it is not exclusive");
7056        assert!(
7057            parse(&quiet, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
7058            "after the subcommand word the child's declaration is in scope, and it is exclusive"
7059        );
7060
7061        let loud: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n}\n"
7062            .parse()
7063            .unwrap();
7064        assert!(
7065            parse(&loud, &input(&["ex", "--clean", "run"])).is_err(),
7066            "the same rule, with an exclusive ancestor: selecting the child is company for it"
7067        );
7068    }
7069
7070    #[test]
7071    fn an_orphan_ancestor_alias_keeps_its_exclusivity_past_a_plain_child_redeclaration() {
7072        // The mirror of `a_local_child_redeclaration_keeps_its_exclusivity_when_merged`: the
7073        // child owns `--clean` and says nothing about exclusivity, but `-c` is a spelling only
7074        // the ancestor ever declared, so the ancestor's answer still governs it.
7075        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\"\n  flag \"--verbose\"\n}\n"
7076            .parse()
7077            .unwrap();
7078
7079        assert!(
7080            parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
7081            "the orphan ancestor alias is still the ancestor's exclusive flag"
7082        );
7083        parse(&spec, &input(&["ex", "run", "--clean", "--verbose"]))
7084            .expect("the child's own spelling drops the exclusivity the child did not restate");
7085    }
7086
7087    #[test]
7088    fn a_child_spelling_carries_its_exclusivity_even_beside_an_ancestor_spelling() {
7089        // Both spellings of one merged flag, typed together. The child's `--clean` is exclusive
7090        // whatever else was typed alongside it, so `--verbose` is company; attributing the whole
7091        // occurrence to the ancestor because `-c` appeared in it lost that.
7092        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n  flag \"--verbose\"\n}\n"
7093            .parse()
7094            .unwrap();
7095
7096        assert!(
7097            parse(&spec, &input(&["ex", "run", "-c", "--clean", "--verbose"])).is_err(),
7098            "the child spelling is exclusive whatever it was typed beside"
7099        );
7100        parse(&spec, &input(&["ex", "run", "-c", "--verbose"]))
7101            .expect("the ancestor's own spelling was never exclusive");
7102    }
7103
7104    #[test]
7105    fn an_environment_value_takes_the_exclusivity_of_the_declaration_in_scope() {
7106        // An environment value has no spelling to attribute, so the declaration the selected
7107        // command has in scope answers — in both directions. Comparing whole alias sets asked
7108        // the ancestor instead, because the merged flag also carries its orphan `-c`.
7109        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"
7110            .parse()
7111            .unwrap();
7112
7113        assert!(
7114            parse_with_env(&added, &["ex", "run", "--verbose"], &[("EX_CLEAN", "1")]).is_err(),
7115            "the child added exclusivity the environment value has to honor"
7116        );
7117
7118        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"
7119            .parse()
7120            .unwrap();
7121
7122        parse_with_env(&dropped, &["ex", "run", "--verbose"], &[("EX_CLEAN", "1")])
7123            .expect("the child dropped the exclusivity, and the environment value follows it");
7124    }
7125
7126    #[test]
7127    fn a_merged_child_exclusive_flag_still_escapes_requiredness() {
7128        // Exclusivity suppresses missing-value checks, and that has to survive the merge for
7129        // the same reason the companion check does.
7130        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"
7131            .parse()
7132            .unwrap();
7133
7134        parse(&spec, &input(&["ex", "run", "--clean"]))
7135            .expect("a merged child exclusive flag is still the command's requiredness escape");
7136    }
7137
7138    #[test]
7139    fn a_group_allows_one_member_and_refuses_two() {
7140        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\"\nflag \"--url <u>\"\nflag \"--stdin\"\ngroup \"input\" \"--file\" \"--url\" \"--stdin\"\n"
7141            .parse()
7142            .unwrap();
7143
7144        // One is fine, and so is none: a plain group says "at most one".
7145        parse(&spec, &input(&["ex", "--file", "a.txt"])).expect("one member is fine");
7146        parse(&spec, &input(&["ex"])).expect("a group that is not required asks for nothing");
7147
7148        let err = parse(&spec, &input(&["ex", "--file", "a.txt", "--stdin"])).unwrap_err();
7149        assert!(err.to_string().contains("group input"), "{err}");
7150    }
7151
7152    #[test]
7153    fn positional_selectors_work_in_conflicts_and_groups() {
7154        let conflicts: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--from-file <path>\" conflicts=\"value\"\narg \"[value]\"\n"
7155            .parse()
7156            .unwrap();
7157        parse(&conflicts, &input(&["ex", "--from-file", "vars.env"]))
7158            .expect("the flag alone is valid");
7159        parse(&conflicts, &input(&["ex", "literal"])).expect("the positional alone is valid");
7160        assert!(parse(
7161            &conflicts,
7162            &input(&["ex", "--from-file", "vars.env", "literal"])
7163        )
7164        .is_err());
7165
7166        let positional_source: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--from-file <path>\"\narg \"[value]\" conflicts=\"--from-file\"\n"
7167            .parse()
7168            .unwrap();
7169        assert!(parse(
7170            &positional_source,
7171            &input(&["ex", "--from-file", "vars.env", "literal"])
7172        )
7173        .is_err());
7174
7175        let group: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <path>\"\narg \"[target]\"\ngroup \"input\" \"--file\" \"target\" required=#true\n"
7176            .parse()
7177            .unwrap();
7178        assert!(parse(&group, &input(&["ex"])).is_err());
7179        parse(&group, &input(&["ex", "target-name"]))
7180            .expect("a positional satisfies a required group");
7181        assert!(parse(
7182            &group,
7183            &input(&["ex", "--file", "input.txt", "target-name"])
7184        )
7185        .is_err());
7186    }
7187
7188    #[test]
7189    fn a_required_group_needs_one_of_its_members() {
7190        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
7191            .parse()
7192            .unwrap();
7193
7194        let err = parse(&spec, &input(&["ex"])).unwrap_err();
7195        // The members, because that is what a user has to type; the name, because a
7196        // command with several groups would otherwise report the same sentence twice.
7197        assert!(err.to_string().contains("--file, --url"), "{err}");
7198        assert!(err.to_string().contains("input"), "{err}");
7199
7200        parse(&spec, &input(&["ex", "--url", "u"])).expect("one member satisfies it");
7201    }
7202
7203    #[test]
7204    fn a_multiple_group_only_polices_requiredness() {
7205        // `multiple` with `required` is "at least one of these", so two is fine and
7206        // none is not.
7207        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--a\"\nflag \"--b\"\ngroup \"any\" \"--a\" \"--b\" required=#true multiple=#true\n"
7208            .parse()
7209            .unwrap();
7210
7211        parse(&spec, &input(&["ex", "--a", "--b"])).expect("multiple allows both");
7212        assert!(parse(&spec, &input(&["ex"])).is_err());
7213    }
7214
7215    #[test]
7216    fn a_group_reads_a_default_for_requiredness_and_not_for_exclusivity() {
7217        // The two halves of a group are two kinds of rule, and they read a default
7218        // differently on purpose. Requiredness asks whether a member has a value, and a
7219        // default is a value — the rule `requires` follows. Exclusivity asks what the
7220        // user supplied, because a defaulted member counted as supplied would collide
7221        // with the sibling they actually typed and refuse a correct command line.
7222        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" default=\"a.txt\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
7223            .parse()
7224            .unwrap();
7225
7226        parse(&spec, &input(&["ex"])).expect("the default fills the group");
7227        parse(&spec, &input(&["ex", "--url", "u"]))
7228            .expect("the default must not conflict with the flag the user typed");
7229    }
7230
7231    #[test]
7232    fn a_group_naming_two_spellings_of_one_flag_is_not_a_conflict() {
7233        // `-f` and `--file` are one flag. Counted by selector, giving it once would
7234        // report it as conflicting with itself.
7235        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-f --file <f>\"\nflag \"--url <u>\"\ngroup \"input\" \"-f\" \"--file\" \"--url\"\n"
7236            .parse()
7237            .unwrap();
7238
7239        parse(&spec, &input(&["ex", "--file", "a.txt"])).expect("one flag is one member");
7240        parse(&spec, &input(&["ex", "-f", "a.txt"])).expect("either spelling, still one member");
7241
7242        // A genuine collision is still one.
7243        let err = parse(&spec, &input(&["ex", "--file", "a.txt", "--url", "u"])).unwrap_err();
7244        assert!(err.to_string().contains("group input"), "{err}");
7245    }
7246
7247    #[test]
7248    fn a_group_reads_the_environment_as_given() {
7249        // The environment does count, which is the same asymmetry `conflicts` has: an
7250        // env var is somebody saying something, a default is nobody saying anything.
7251        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" env=\"EX_FILE\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
7252            .parse()
7253            .unwrap();
7254
7255        parse_with_env(&spec, &["ex"], &[("EX_FILE", "a.txt")]).expect("the environment fills it");
7256    }
7257
7258    #[test]
7259    fn a_requirement_names_the_flag_that_is_missing() {
7260        // Reported as the missing flag rather than as something wrong with `--out`,
7261        // which is what clap says for an unmet `requires` and what a user can act on.
7262        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\"\n"
7263            .parse()
7264            .unwrap();
7265
7266        let err = parse(&spec, &input(&["ex", "--out", "a.txt"])).unwrap_err();
7267        assert!(
7268            err.to_string().contains("format"),
7269            "the missing flag should be named: {err}"
7270        );
7271
7272        // Satisfied, in either order.
7273        for words in [
7274            &["ex", "--out", "a.txt", "--format", "json"][..],
7275            &["ex", "--format", "json", "--out", "a.txt"][..],
7276        ] {
7277            parse(&spec, &input(words)).unwrap_or_else(|e| panic!("{words:?}: {e}"));
7278        }
7279
7280        // Nothing happens when the flag that imposes the rule is absent: a requirement
7281        // is a consequence of using the flag, not a rule about the command line.
7282        parse(&spec, &input(&["ex"])).expect("a bare invocation requires nothing");
7283    }
7284
7285    #[test]
7286    fn a_value_activates_only_its_conditional_requirement() {
7287        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"
7288            .parse()
7289            .unwrap();
7290
7291        parse(&spec, &input(&["ex", "--config", "ordinary.toml"]))
7292            .expect("an unrelated value requires nothing");
7293
7294        let key = parse(&spec, &input(&["ex", "--config", "special.toml"])).unwrap_err();
7295        assert!(key.to_string().contains("key"), "{key}");
7296        parse(
7297            &spec,
7298            &input(&["ex", "--config", "special.toml", "--key", "secret"]),
7299        )
7300        .expect("the matching requirement is satisfied");
7301
7302        let token = parse(&spec, &input(&["ex", "--config", "remote.toml"])).unwrap_err();
7303        assert!(token.to_string().contains("token"), "{token}");
7304    }
7305
7306    #[test]
7307    fn conditional_requirements_read_explicit_env_but_not_defaults() {
7308        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"
7309            .parse()
7310            .unwrap();
7311        let err = parse_with_env(&from_env, &["ex"], &[("EX_CONFIG", "special.toml")]).unwrap_err();
7312        assert!(err.to_string().contains("key"), "{err}");
7313
7314        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"
7315            .parse()
7316            .unwrap();
7317        parse(&from_default, &input(&["ex"]))
7318            .expect("a default is not an explicit conditional value");
7319    }
7320
7321    #[test]
7322    fn command_line_values_override_env_for_conditional_requirements() {
7323        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--config <file>\" env=\"EX_CONFIG\" {\n  requires_if \"special.toml\" \"--key\"\n}\nflag \"--key <key>\"\n"
7324            .parse()
7325            .unwrap();
7326
7327        parse_with_env(
7328            &spec,
7329            &["ex", "--config", "ordinary.toml"],
7330            &[("EX_CONFIG", "special.toml")],
7331        )
7332        .expect("the command-line value takes precedence over the environment");
7333    }
7334
7335    #[test]
7336    fn conditional_requirements_normalize_boolean_env_values() {
7337        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--feature\" env=\"EX_FEATURE\" {\n  requires_if \"true\" \"--key\"\n}\nflag \"--key <key>\"\n"
7338            .parse()
7339            .unwrap();
7340
7341        for value in ["1", "true", "True", "TRUE"] {
7342            let err = parse_with_env(&spec, &["ex"], &[("EX_FEATURE", value)]).unwrap_err();
7343            assert!(err.to_string().contains("key"), "{value}: {err}");
7344        }
7345        parse_with_env(&spec, &["ex"], &[("EX_FEATURE", "false")])
7346            .expect("a false environment value does not activate a true condition");
7347    }
7348
7349    #[test]
7350    fn a_default_satisfies_a_requirement() {
7351        // The flag it names has a value, which is the question a requirement asks. Read
7352        // any other way, `--format` would be missing here and present ten lines further
7353        // down, where plain required-ness reads the same default as filling it.
7354        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" default=\"json\"\n"
7355            .parse()
7356            .unwrap();
7357
7358        parse(&spec, &input(&["ex", "--out", "a.txt"]))
7359            .expect("a defaulted flag is not a missing one");
7360    }
7361
7362    #[test]
7363    fn a_present_flag_binds_a_conditional_default() {
7364        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" {\n  default_if \"--json\" \"true\"\n}\nflag \"--json\"\n"
7365            .parse()
7366            .unwrap();
7367
7368        let with = parse(&spec, &input(&["ex", "--json"])).unwrap();
7369        assert_eq!(
7370            with.as_env().get("usage_bin_names").map(String::as_str),
7371            Some("true")
7372        );
7373
7374        let without = parse(&spec, &input(&["ex"])).unwrap();
7375        assert!(
7376            !without.as_env().contains_key("usage_bin_names"),
7377            "IsPresent does nothing when the selector is absent"
7378        );
7379    }
7380
7381    #[test]
7382    fn an_equals_condition_binds_a_conditional_default() {
7383        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--style <s>\" {\n  default_if \"--output\" \"json\" \"pretty\"\n}\nflag \"--output <fmt>\"\n"
7384            .parse()
7385            .unwrap();
7386
7387        let json = parse(&spec, &input(&["ex", "--output", "json"])).unwrap();
7388        assert_eq!(
7389            json.as_env().get("usage_style").map(String::as_str),
7390            Some("pretty")
7391        );
7392        let yaml = parse(&spec, &input(&["ex", "--output", "yaml"])).unwrap();
7393        assert!(!yaml.as_env().contains_key("usage_style"));
7394    }
7395
7396    #[test]
7397    fn an_equals_condition_reads_a_negated_flag() {
7398        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pretty\" {\n  default_if \"--json\" \"false\" \"true\"\n}\nflag \"--json\" negate=\"--no-json\"\n"
7399            .parse()
7400            .unwrap();
7401
7402        let off = parse(&spec, &input(&["ex", "--no-json"])).unwrap();
7403        assert_eq!(
7404            off.as_env().get("usage_pretty").map(String::as_str),
7405            Some("true")
7406        );
7407        let on = parse(&spec, &input(&["ex", "--json"])).unwrap();
7408        assert!(
7409            !on.as_env().contains_key("usage_pretty"),
7410            "--json is true, so when=false should miss"
7411        );
7412    }
7413
7414    #[test]
7415    fn the_first_matching_conditional_default_wins() {
7416        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"
7417            .parse()
7418            .unwrap();
7419
7420        let out = parse(&spec, &input(&["ex", "--json", "--pretty"])).unwrap();
7421        assert_eq!(
7422            out.as_env().get("usage_style").map(String::as_str),
7423            Some("compact")
7424        );
7425    }
7426
7427    #[test]
7428    fn argv_and_env_suppress_a_conditional_default() {
7429        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" env=\"EX_BIN\" {\n  default_if \"--json\" \"true\"\n}\nflag \"--json\"\n"
7430            .parse()
7431            .unwrap();
7432
7433        let from_env = parse_with_env(&spec, &["ex", "--json"], &[("EX_BIN", "false")]).unwrap();
7434        assert_eq!(
7435            from_env.as_env().get("usage_bin_names").map(String::as_str),
7436            Some("false"),
7437            "the target's environment wins over default_if"
7438        );
7439    }
7440
7441    #[test]
7442    fn a_sibling_env_activates_a_conditional_default() {
7443        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" {\n  default_if \"--json\" \"true\"\n}\nflag \"--json\" env=\"EX_JSON\"\n"
7444            .parse()
7445            .unwrap();
7446
7447        let out = parse_with_env(&spec, &["ex"], &[("EX_JSON", "1")]).unwrap();
7448        assert_eq!(
7449            out.as_env().get("usage_bin_names").map(String::as_str),
7450            Some("true")
7451        );
7452    }
7453
7454    #[test]
7455    fn a_default_does_not_activate_a_conditional_default() {
7456        // `--json` sorts before `--pretty` in the available-flag map, so a one-pass
7457        // bind would put json's default into `out.flags` and then treat it as
7458        // explicit for pretty's `default_if`. Go and the derive ignore defaults.
7459        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pretty\" {\n  default_if \"--json\" \"true\"\n}\nflag \"--json\" default=#true\n"
7460            .parse()
7461            .unwrap();
7462
7463        let out = parse(&spec, &input(&["ex"])).unwrap();
7464        assert_eq!(
7465            out.as_env().get("usage_json").map(String::as_str),
7466            Some("true")
7467        );
7468        assert!(
7469            !out.as_env().contains_key("usage_pretty"),
7470            "a default is not an explicit value for default_if"
7471        );
7472    }
7473
7474    #[test]
7475    fn a_conditional_default_does_not_activate_requires_if() {
7476        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"
7477            .parse()
7478            .unwrap();
7479
7480        parse(&spec, &input(&["ex", "--json"]))
7481            .expect("a default_if value is not explicit for requires_if");
7482        assert!(parse(&spec, &input(&["ex", "--format", "json"])).is_err());
7483    }
7484
7485    #[test]
7486    fn a_conditional_default_satisfies_a_requirement() {
7487        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" {\n  default_if \"--json\" \"json\"\n}\nflag \"--json\"\n"
7488            .parse()
7489            .unwrap();
7490
7491        parse(&spec, &input(&["ex", "--out", "a.txt", "--json"]))
7492            .expect("default_if fills the required flag");
7493        assert!(parse(&spec, &input(&["ex", "--out", "a.txt"])).is_err());
7494    }
7495
7496    #[test]
7497    fn an_environment_value_satisfies_a_requirement() {
7498        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" env=\"EX_FORMAT\"\n"
7499            .parse()
7500            .unwrap();
7501
7502        assert!(parse(&spec, &input(&["ex", "--out", "a.txt"])).is_err());
7503        parse_with_env(&spec, &["ex", "--out", "a.txt"], &[("EX_FORMAT", "json")])
7504            .expect("the environment supplies it");
7505    }
7506
7507    #[test]
7508    fn a_requirement_is_satisfied_by_a_short_form() {
7509        // The selector may spell the other flag any way it answers to, so the check
7510        // resolves it the way every other selector is resolved rather than matching
7511        // text. The error names the flag, not the selector.
7512        let spec: Spec =
7513            "name \"ex\"\nbin \"ex\"\nflag \"--sign\" requires=\"-k\"\nflag \"-k --key <k>\"\n"
7514                .parse()
7515                .unwrap();
7516
7517        parse(&spec, &input(&["ex", "--sign", "--key", "x"])).expect("--key satisfies -k");
7518
7519        let err = parse(&spec, &input(&["ex", "--sign"])).unwrap_err();
7520        assert!(err.to_string().contains("key"), "{err}");
7521    }
7522
7523    #[test]
7524    fn conflicting_flags_are_rejected_in_either_order() {
7525        // Declared once, on `--file`, which is all clap exposes — so the check has to
7526        // be order-independent by looking at every flag that was given rather than at
7527        // the one that declared the conflict.
7528        let spec: Spec =
7529            "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" conflicts=\"--stdin\"\nflag \"--stdin\"\n"
7530                .parse()
7531                .unwrap();
7532
7533        for words in [
7534            &["ex", "--file", "a.txt", "--stdin"][..],
7535            &["ex", "--stdin", "--file", "a.txt"][..],
7536        ] {
7537            let err = parse(&spec, &input(words)).unwrap_err();
7538            assert!(
7539                err.to_string().contains("conflicts with --stdin"),
7540                "{words:?} should be refused: {err}"
7541            );
7542        }
7543
7544        // Either one alone is fine.
7545        parse(&spec, &input(&["ex", "--stdin"])).unwrap();
7546        parse(&spec, &input(&["ex", "--file", "a.txt"])).unwrap();
7547    }
7548
7549    #[test]
7550    fn unknown_flags_are_values_by_default() {
7551        // The default, and the reason it is the default: a spec often parses a
7552        // command line whose flags belong to something else.
7553        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--force\"\narg \"[rest]...\"\n"
7554            .parse()
7555            .unwrap();
7556        let out = parse(
7557            &spec,
7558            &["ex".to_string(), "--wat".to_string(), "x".to_string()],
7559        )
7560        .unwrap();
7561        let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
7562        assert_eq!(out.args[rest].to_string(), "--wat x");
7563    }
7564
7565    #[test]
7566    fn repeated_scalar_flags_override_by_default_and_can_be_strict() {
7567        let permissive: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--jobs <n>\"\nflag \"--verbose\"\n"
7568            .parse()
7569            .unwrap();
7570        let out = parse(&permissive, &input(&["ex", "--jobs", "1", "--jobs", "2"]))
7571            .expect("a repeat is a correction by default");
7572        let jobs = out.flags.keys().find(|f| f.name == "jobs").unwrap();
7573        assert_eq!(out.flags[jobs].to_string(), "2");
7574        parse(&permissive, &input(&["ex", "--verbose", "--verbose"]))
7575            .expect("switches use the same default");
7576
7577        let strict: Spec = "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--jobs <n>\"\nflag \"--verbose\"\n"
7578            .parse()
7579            .unwrap();
7580        for words in [
7581            &["ex", "--jobs", "1", "--jobs", "2"][..],
7582            &["ex", "--verbose", "--verbose"][..],
7583        ] {
7584            let err = parse(&strict, &input(words)).unwrap_err();
7585            assert!(
7586                err.to_string().contains("cannot be used multiple times"),
7587                "{err}"
7588            );
7589        }
7590
7591        let reparsed: Spec = strict.to_string().parse().unwrap();
7592        assert!(!reparsed.cmd.args_override_self);
7593    }
7594
7595    #[test]
7596    fn strict_negated_flags_allow_opposite_forms_but_reject_the_same_form() {
7597        let spec: Spec = "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--color\" negate=\"--no-color\"\n"
7598            .parse()
7599            .unwrap();
7600
7601        let out = parse(&spec, &input(&["ex", "--color", "--no-color"]))
7602            .expect("opposite forms override each other");
7603        let color = out.flags.keys().find(|f| f.name == "color").unwrap();
7604        assert!(matches!(out.flags[color], ParseValue::Bool(false)));
7605
7606        for words in [
7607            &["ex", "--color", "--color"][..],
7608            &["ex", "--no-color", "--no-color"][..],
7609        ] {
7610            let err = parse(&spec, &input(words)).unwrap_err();
7611            assert!(err.to_string().contains("cannot be used multiple times"));
7612        }
7613    }
7614
7615    #[test]
7616    fn strict_global_flags_may_repeat_across_command_levels() {
7617        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"
7618            .parse()
7619            .unwrap();
7620
7621        let out = parse(
7622            &spec,
7623            &input(&[
7624                "ex", "--color", "--jobs", "1", "run", "--color", "--jobs", "2",
7625            ]),
7626        )
7627        .expect("an inherited global is allowed once at each command level");
7628        let jobs = out.flags.keys().find(|f| f.name == "jobs").unwrap();
7629        assert_eq!(out.flags[jobs].to_string(), "2");
7630
7631        for words in [
7632            &["ex", "--color", "--color", "run", "--no-color"][..],
7633            &["ex", "--jobs", "1", "run", "--jobs", "2", "--jobs", "3"][..],
7634        ] {
7635            let err = parse(&spec, &input(words)).unwrap_err();
7636            assert!(err.to_string().contains("cannot be used multiple times"));
7637        }
7638    }
7639
7640    #[test]
7641    fn a_subcommand_can_negate_only_its_parents_requirements() {
7642        let base = r#"name "ex"
7643bin "ex"
7644flag "--config" required=#true
7645flag "--mode" requires="--config"
7646flag "--other"
7647arg "<input>"
7648group "source" "--config" "--other" required=#true
7649cmd "run" { flag "--child" required=#true }
7650"#;
7651        let strict: Spec = base.parse().unwrap();
7652        let err = parse(&strict, &input(&["ex", "run"])).unwrap_err();
7653        let message = err.to_string();
7654        assert!(
7655            message.contains("input") || message.contains("config"),
7656            "{message}"
7657        );
7658
7659        let negated: Spec = base
7660            .replacen("bin \"ex\"", "bin \"ex\"\nsubcommand_negates_reqs #true", 1)
7661            .parse()
7662            .unwrap();
7663        let err = parse(&negated, &input(&["ex", "run"])).unwrap_err();
7664        assert!(
7665            err.to_string().contains("child"),
7666            "the selected command keeps its own requirements: {err}"
7667        );
7668
7669        let mut child_optional = negated.clone();
7670        child_optional.cmd.subcommands["run"].flags[0].required = false;
7671        parse(&child_optional, &input(&["ex", "run"]))
7672            .expect("the child selection satisfies all parent requirements");
7673        parse(&child_optional, &input(&["ex", "--mode", "run"]))
7674            .expect("parent requires relationships are negated too");
7675    }
7676
7677    #[test]
7678    fn a_parent_argument_can_conflict_with_a_later_subcommand() {
7679        let spec: Spec = r#"name "ex"
7680bin "ex"
7681args_conflicts_with_subcommands #true
7682flag "--verbose"
7683cmd "run"
7684"#
7685        .parse()
7686        .unwrap();
7687
7688        parse(&spec, &input(&["ex", "run"]))
7689            .expect("the subcommand is valid without a parent argument");
7690        let err = parse(&spec, &input(&["ex", "--verbose", "run"])).unwrap_err();
7691        assert!(
7692            err.to_string().contains("cannot be used with arguments"),
7693            "{err}"
7694        );
7695    }
7696
7697    #[test]
7698    fn a_subcommand_can_take_precedence_over_a_variadic_flag() {
7699        let base = r#"name "ex"
7700bin "ex"
7701flag "--values <value>..."
7702cmd "run"
7703"#;
7704        let plain: Spec = base.parse().unwrap();
7705        let out = parse(&plain, &input(&["ex", "--values", "a", "run"])).unwrap();
7706        assert_eq!(out.cmd.name, "ex");
7707
7708        let precedence: Spec = base
7709            .replacen(
7710                "bin \"ex\"",
7711                "bin \"ex\"\nsubcommand_precedence_over_arg #true",
7712                1,
7713            )
7714            .parse()
7715            .unwrap();
7716        let out = parse(&precedence, &input(&["ex", "--values", "a", "run"])).unwrap();
7717        assert_eq!(out.cmd.name, "run");
7718    }
7719
7720    #[test]
7721    fn a_required_positional_can_follow_an_unfilled_optional_one() {
7722        let base = r#"name "ex"
7723bin "ex"
7724arg "[optional]"
7725arg "<required>"
7726"#;
7727        let plain: Spec = base.parse().unwrap();
7728        let err = parse(&plain, &input(&["ex", "value"])).unwrap_err();
7729        assert!(err.to_string().contains("required"), "{err}");
7730
7731        let enabled: Spec = base
7732            .replacen(
7733                "bin \"ex\"",
7734                "bin \"ex\"\nallow_missing_positional #true",
7735                1,
7736            )
7737            .parse()
7738            .unwrap();
7739        let out = parse(&enabled, &input(&["ex", "value"])).unwrap();
7740        assert!(!out.args.keys().any(|arg| arg.name == "optional"));
7741        let value = &out
7742            .args
7743            .iter()
7744            .find(|(arg, _)| arg.name == "required")
7745            .unwrap()
7746            .1;
7747        assert!(matches!(value, ParseValue::String(value) if value == "value"));
7748    }
7749
7750    #[test]
7751    fn sigil_args_do_not_block_optional_positional_skipping() {
7752        let spec: Spec = r#"name "ex"
7753bin "ex"
7754allow_missing_positional #true
7755arg "[optional]"
7756arg "[tool]" sigil="@"
7757arg "<required>"
7758"#
7759        .parse()
7760        .unwrap();
7761
7762        let out = parse(&spec, &input(&["ex", "@node", "value"])).unwrap();
7763        assert!(!out.args.keys().any(|arg| arg.name == "optional"));
7764        let tool = out.args.keys().find(|arg| arg.name == "tool").unwrap();
7765        let required = out.args.keys().find(|arg| arg.name == "required").unwrap();
7766        assert_eq!(out.args[tool].to_string(), "node");
7767        assert_eq!(out.args[required].to_string(), "value");
7768    }
7769
7770    #[test]
7771    fn unknown_flags_can_be_rejected_for_the_whole_cli() {
7772        let spec: Spec =
7773            "name \"ex\"\nbin \"ex\"\nunknown_flags \"error\"\nflag \"--force\"\nflag \"-0 --print0\"\narg \"[rest]...\" allow_negative_numbers=#true\n"
7774                .parse()
7775                .unwrap();
7776        let err = parse(&spec, &["ex".to_string(), "--wat".to_string()]).unwrap_err();
7777        assert!(
7778            err.to_string().contains("--wat"),
7779            "the message should name the token: {err}"
7780        );
7781
7782        // The positional opts into the narrower negative-number carve-out without
7783        // accepting arbitrary unknown flags.
7784        let out = parse(&spec, &["ex".to_string(), "-1".to_string()]).unwrap();
7785        let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
7786        assert_eq!(out.args[rest].to_string(), "-1");
7787
7788        let out = parse(&spec, &["ex".to_string(), "-0".to_string()]).unwrap();
7789        let print0 = out.flags.keys().find(|flag| flag.name == "print0").unwrap();
7790        assert!(matches!(out.flags[print0], ParseValue::Bool(true)));
7791    }
7792
7793    #[test]
7794    fn a_declared_digit_short_does_not_stop_the_subcommand_scan() {
7795        let spec: Spec = r#"
7796name "ex"
7797bin "ex"
7798unknown_flags "error"
7799flag "-0 --print0" global=#true
7800cmd "run" {
7801  flag "--force"
7802}
7803"#
7804        .parse()
7805        .unwrap();
7806        let out = parse(&spec, &input(&["ex", "-0", "run", "--force"])).unwrap();
7807        assert_eq!(out.cmd.name, "run");
7808        let print0 = out.flags.keys().find(|flag| flag.name == "print0").unwrap();
7809        let force = out.flags.keys().find(|flag| flag.name == "force").unwrap();
7810        assert!(matches!(out.flags[print0], ParseValue::Bool(true)));
7811        assert!(matches!(out.flags[force], ParseValue::Bool(true)));
7812    }
7813
7814    #[test]
7815    fn a_command_may_override_the_cli_wide_setting() {
7816        // Strict overall, lenient for the one command that forwards options.
7817        let spec: Spec = r#"
7818name "ex"
7819bin "ex"
7820unknown_flags "error"
7821cmd "exec" unknown_flags="value" {
7822  arg "[rest]..."
7823}
7824cmd "build" {
7825  arg "[rest]..."
7826}
7827"#
7828        .parse()
7829        .unwrap();
7830
7831        let out = parse(
7832            &spec,
7833            &["ex".to_string(), "exec".to_string(), "--wat".to_string()],
7834        )
7835        .unwrap();
7836        let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
7837        assert_eq!(out.args[rest].to_string(), "--wat");
7838
7839        assert!(
7840            parse(
7841                &spec,
7842                &["ex".to_string(), "build".to_string(), "--wat".to_string()]
7843            )
7844            .is_err(),
7845            "a command that says nothing inherits the CLI's choice"
7846        );
7847    }
7848
7849    #[test]
7850    fn the_setting_survives_a_round_trip() {
7851        let spec: Spec =
7852            "name \"ex\"\nbin \"ex\"\nunknown_flags \"error\"\ncmd \"x\" unknown_flags=\"value\"\n"
7853                .parse()
7854                .unwrap();
7855        let reparsed: Spec = spec.to_string().parse().unwrap();
7856        assert_eq!(reparsed.unknown_flags, Some(UnknownFlags::Error));
7857        assert_eq!(
7858            reparsed.cmd.subcommands["x"].unknown_flags,
7859            Some(UnknownFlags::Value)
7860        );
7861    }
7862
7863    #[test]
7864    fn test_default_subcommand() {
7865        // Test that default_subcommand routes to the specified subcommand
7866        let run_cmd = SpecCommand::builder()
7867            .name("run")
7868            .arg(SpecArg::builder().name("task").build())
7869            .build();
7870        let mut cmd = SpecCommand::builder().name("test").build();
7871        cmd.subcommands.insert("run".to_string(), run_cmd);
7872
7873        let spec = Spec {
7874            name: "test".to_string(),
7875            bin: "test".to_string(),
7876            cmd,
7877            default_subcommand: Some("run".to_string()),
7878            ..Default::default()
7879        };
7880
7881        // "test mytask" should be parsed as if it were "test run mytask"
7882        let input = vec!["test".to_string(), "mytask".to_string()];
7883        let parsed = parse(&spec, &input).unwrap();
7884
7885        // Should have two commands: root and "run"
7886        assert_eq!(parsed.cmds.len(), 2);
7887        assert_eq!(parsed.cmds[1].name, "run");
7888
7889        // Should have parsed the task argument
7890        assert_eq!(parsed.args.len(), 1);
7891        let arg = parsed.args.keys().next().unwrap();
7892        assert_eq!(arg.name, "task");
7893        let value = parsed.args.values().next().unwrap();
7894        assert_eq!(value.to_string(), "mytask");
7895    }
7896
7897    #[test]
7898    fn default_subcommand_outranks_root_sigil_arg() {
7899        let spec: Spec = r#"
7900name "test"
7901bin "test"
7902default_subcommand "run"
7903arg "[tools]..." sigil="+"
7904cmd "run" { arg "<task>" }
7905"#
7906        .parse()
7907        .unwrap();
7908
7909        let parsed = parse(&spec, &input(&["test", "+node", "node"])).unwrap();
7910        assert_eq!(parsed.cmd.name, "run");
7911        let value = |name| {
7912            parsed
7913                .args
7914                .iter()
7915                .find(|(arg, _)| arg.name == name)
7916                .map(|(_, value)| value.to_string())
7917                .unwrap()
7918        };
7919        assert_eq!(value("tools"), "node");
7920        assert_eq!(value("task"), "node");
7921    }
7922
7923    #[test]
7924    fn test_default_subcommand_explicit_still_works() {
7925        // Test that explicit subcommand takes precedence
7926        let run_cmd = SpecCommand::builder()
7927            .name("run")
7928            .arg(SpecArg::builder().name("task").build())
7929            .build();
7930        let other_cmd = SpecCommand::builder()
7931            .name("other")
7932            .arg(SpecArg::builder().name("other_arg").build())
7933            .build();
7934        let mut cmd = SpecCommand::builder().name("test").build();
7935        cmd.subcommands.insert("run".to_string(), run_cmd);
7936        cmd.subcommands.insert("other".to_string(), other_cmd);
7937
7938        let spec = Spec {
7939            name: "test".to_string(),
7940            bin: "test".to_string(),
7941            cmd,
7942            default_subcommand: Some("run".to_string()),
7943            ..Default::default()
7944        };
7945
7946        // "test other foo" should use "other" subcommand, not default
7947        let input = vec!["test".to_string(), "other".to_string(), "foo".to_string()];
7948        let parsed = parse(&spec, &input).unwrap();
7949
7950        // Should have used "other" subcommand
7951        assert_eq!(parsed.cmds.len(), 2);
7952        assert_eq!(parsed.cmds[1].name, "other");
7953    }
7954
7955    #[test]
7956    fn test_default_subcommand_applies_only_at_the_root() {
7957        // `default_subcommand` is declared once, for the whole spec, and only at the top. It
7958        // was being looked up wherever the parser happened to be standing, so a command with
7959        // an unrelated subcommand of the same name acquired a default of its own: with
7960        // `default_subcommand "ls"`, `ex config zzz` descended into `config ls` and bound
7961        // `zzz` there. Nothing declared that, and nothing could have.
7962        let mut config_ls = SpecCommand::builder().name("ls").build();
7963        config_ls.args.push(SpecArg::builder().name("what").build());
7964        let mut config_cmd = SpecCommand::builder().name("config").build();
7965        config_cmd.subcommands.insert("ls".to_string(), config_ls);
7966
7967        // The root's own `ls`, which is what its default points at. It takes an argument so
7968        // that a routed word has somewhere to land.
7969        let mut root_ls = SpecCommand::builder().name("ls").build();
7970        root_ls.args.push(SpecArg::builder().name("what").build());
7971        let mut cmd = SpecCommand::builder().name("ex").build();
7972        cmd.subcommands.insert("ls".to_string(), root_ls);
7973        cmd.subcommands.insert("config".to_string(), config_cmd);
7974
7975        let spec = Spec {
7976            name: "ex".to_string(),
7977            bin: "ex".to_string(),
7978            cmd,
7979            default_subcommand: Some("ls".to_string()),
7980            ..Default::default()
7981        };
7982
7983        // `config` has an `ls`, but `config` did not declare a default, so `zzz` is `config`'s
7984        // own business — and `config` takes no argument, so this is an error rather than a
7985        // silent descent.
7986        let input = vec!["ex".to_string(), "config".to_string(), "zzz".to_string()];
7987        assert!(
7988            parse(&spec, &input).is_err(),
7989            "`config` has no default subcommand and no argument, so `zzz` cannot bind"
7990        );
7991
7992        // At the root, where it is declared, it still applies.
7993        let input = vec!["ex".to_string(), "zzz".to_string()];
7994        let parsed = parse(&spec, &input).expect("the root's default applies");
7995        assert_eq!(
7996            parsed
7997                .cmds
7998                .iter()
7999                .map(|c| c.name.as_str())
8000                .collect::<Vec<_>>(),
8001            ["ex", "ls"]
8002        );
8003        assert_eq!(
8004            parsed.args.values().next().map(|v| v.to_string()),
8005            Some("zzz".to_string()),
8006            "and the word binds inside the command it reached"
8007        );
8008    }
8009
8010    #[test]
8011    fn test_default_subcommand_with_nested_subcommands() {
8012        // Test that default_subcommand works when the default subcommand has nested subcommands.
8013        // This is the mise use case: "mise say" should be parsed as "mise run say"
8014        // where "say" is a subcommand of "run" (a task).
8015        let say_cmd = SpecCommand::builder()
8016            .name("say")
8017            .arg(SpecArg::builder().name("name").build())
8018            .build();
8019        let mut run_cmd = SpecCommand::builder().name("run").build();
8020        run_cmd.subcommands.insert("say".to_string(), say_cmd);
8021
8022        let mut cmd = SpecCommand::builder().name("test").build();
8023        cmd.subcommands.insert("run".to_string(), run_cmd);
8024
8025        let spec = Spec {
8026            name: "test".to_string(),
8027            bin: "test".to_string(),
8028            cmd,
8029            default_subcommand: Some("run".to_string()),
8030            ..Default::default()
8031        };
8032
8033        // "test say hello" should be parsed as "test run say hello"
8034        let input = vec!["test".to_string(), "say".to_string(), "hello".to_string()];
8035        let parsed = parse(&spec, &input).unwrap();
8036
8037        // Should have three commands: root, "run", and "say"
8038        assert_eq!(parsed.cmds.len(), 3);
8039        assert_eq!(parsed.cmds[0].name, "test");
8040        assert_eq!(parsed.cmds[1].name, "run");
8041        assert_eq!(parsed.cmds[2].name, "say");
8042
8043        // Should have parsed the "name" argument
8044        assert_eq!(parsed.args.len(), 1);
8045        let arg = parsed.args.keys().next().unwrap();
8046        assert_eq!(arg.name, "name");
8047        let value = parsed.args.values().next().unwrap();
8048        assert_eq!(value.to_string(), "hello");
8049    }
8050
8051    /// Build a spec equivalent to the post-mount structure produced by mise's
8052    /// `mise usage` output: a root with a value-taking global flag (`-C/--cd`), a `run`
8053    /// subcommand that re-declares the same flag as NON-global, and a mounted task
8054    /// (`sample:run`) carrying a positional arg with `choices`.
8055    ///
8056    /// We construct the merged structure directly instead of executing a real mount so the
8057    /// test stays hermetic and cross-platform while still exercising the parser defect.
8058    fn mounted_global_flag_spec() -> Spec {
8059        let task_cmd = SpecCommand::builder()
8060            .name("sample:run")
8061            .arg(
8062                SpecArg::builder()
8063                    .name("profile")
8064                    .choices(["alpha", "beta", "gamma"])
8065                    .build(),
8066            )
8067            .build();
8068        // `run` re-declares `-C/--cd` but as a NON-global flag, mirroring the mise spec.
8069        let mut run_cmd = SpecCommand::builder()
8070            .name("run")
8071            .flag(
8072                SpecFlag::builder()
8073                    .name("cd")
8074                    .short('C')
8075                    .long("cd")
8076                    .arg(SpecArg::builder().name("dir").build())
8077                    .global(false)
8078                    .build(),
8079            )
8080            .build();
8081        run_cmd
8082            .subcommands
8083            .insert("sample:run".to_string(), task_cmd);
8084
8085        let mut cmd = SpecCommand::builder()
8086            .name("test")
8087            .flag(
8088                SpecFlag::builder()
8089                    .name("cd")
8090                    .short('C')
8091                    .long("cd")
8092                    .arg(SpecArg::builder().name("dir").build())
8093                    .global(true)
8094                    .build(),
8095            )
8096            .build();
8097        cmd.subcommands.insert("run".to_string(), run_cmd);
8098
8099        Spec {
8100            name: "test".to_string(),
8101            bin: "test".to_string(),
8102            cmd,
8103            ..Default::default()
8104        }
8105    }
8106
8107    #[test]
8108    fn test_prefix_global_flag_does_not_pollute_choices() {
8109        // Regression for the parser-side root cause referenced by jdx/mise#10069.
8110        //
8111        // When `run` re-declares the global `-C/--cd` as non-global, descending into it (and
8112        // then into the mounted `sample:run`) used to drop the inherited global flag from
8113        // `available_flags`. Phase 2 then no longer recognized the prefix `-C`, so it was
8114        // mis-validated against the task's `choices` positional arg.
8115        let spec = mounted_global_flag_spec();
8116
8117        // The prefix global flag must stay recognized so it is consumed as a flag (not as the
8118        // positional). Before the fix this bailed with "Invalid choice for arg profile: -C".
8119        for words in [
8120            &["test", "-C", "/tmp", "run", "sample:run"][..],
8121            // Embedded-value form must behave identically.
8122            &["test", "--cd=/tmp", "run", "sample:run"][..],
8123        ] {
8124            let parsed = parse_partial(&spec, &input(words)).unwrap();
8125            assert_eq!(
8126                parsed
8127                    .cmds
8128                    .iter()
8129                    .map(|c| c.name.as_str())
8130                    .collect::<Vec<_>>(),
8131                vec!["test", "run", "sample:run"],
8132            );
8133            // No positional arg should have been consumed by the leftover global-flag tokens.
8134            assert!(
8135                parsed.args.is_empty(),
8136                "args should be empty, got {:?}",
8137                parsed.args
8138            );
8139
8140            // Fix (B): the inherited global flag survives the descent even though `run`
8141            // re-declares `-C/--cd` as non-global.
8142            let cd = parsed
8143                .available_flags
8144                .get("--cd")
8145                .expect("--cd should remain available after descending into the subcommand");
8146            assert!(cd.global, "--cd must stay global after descent");
8147            assert!(
8148                parsed.available_flags.get("-C").is_some_and(|f| f.global),
8149                "-C must stay global after descent",
8150            );
8151
8152            // The global flag must still be recorded in `out.flags` so it reaches `as_env()`
8153            // for normal execution and for the env passed to mount scripts. (Removing the
8154            // token in Phase 1 instead of re-parsing it would silently drop `usage_cd`.)
8155            assert_eq!(
8156                parsed.as_env().get("usage_cd").map(String::as_str),
8157                Some("/tmp"),
8158                "global flag value must survive in as_env(), got {:?}",
8159                parsed.as_env(),
8160            );
8161        }
8162
8163        // A real, valid choice still parses through the global flag prefix.
8164        let parsed = parse_partial(
8165            &spec,
8166            &input(&["test", "-C", "/tmp", "run", "sample:run", "alpha"]),
8167        )
8168        .unwrap();
8169        assert_eq!(parsed.args.len(), 1);
8170        assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
8171
8172        // And genuinely invalid choices are still rejected (we didn't disable validation).
8173        assert_parse_err(
8174            parse_partial(&spec, &input(&["test", "run", "sample:run", "wrong"])),
8175            "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
8176        );
8177    }
8178
8179    /// Build a spec mirroring mise's orphan-short re-declarations: a root with a LONG-ONLY
8180    /// global boolean flag (`--raw`, no short), a `run` subcommand that re-declares it as a
8181    /// NON-global flag while ADDING a short (`-r --raw`) plus a purely-local `-f/--force`
8182    /// flag, and a mounted task (`sample:run`) with a `choices` positional arg.
8183    fn mounted_orphan_short_spec() -> Spec {
8184        let task_cmd = SpecCommand::builder()
8185            .name("sample:run")
8186            .arg(
8187                SpecArg::builder()
8188                    .name("profile")
8189                    .choices(["alpha", "beta", "gamma"])
8190                    .build(),
8191            )
8192            .build();
8193        // `run` re-declares `--raw` as NON-global but adds a `-r` short that exists only here,
8194        // and also carries a purely-local `-f/--force` flag (shares nothing with a global).
8195        let mut run_cmd = SpecCommand::builder()
8196            .name("run")
8197            .flag(
8198                SpecFlag::builder()
8199                    .name("raw")
8200                    .short('r')
8201                    .long("raw")
8202                    .global(false)
8203                    .build(),
8204            )
8205            .flag(
8206                SpecFlag::builder()
8207                    .name("force")
8208                    .short('f')
8209                    .long("force")
8210                    .global(false)
8211                    .build(),
8212            )
8213            .build();
8214        run_cmd
8215            .subcommands
8216            .insert("sample:run".to_string(), task_cmd);
8217
8218        // Root global is LONG-ONLY: `--raw` with no short.
8219        let mut cmd = SpecCommand::builder()
8220            .name("test")
8221            .flag(
8222                SpecFlag::builder()
8223                    .name("raw")
8224                    .long("raw")
8225                    .global(true)
8226                    .build(),
8227            )
8228            .build();
8229        cmd.subcommands.insert("run".to_string(), run_cmd);
8230
8231        Spec {
8232            name: "test".to_string(),
8233            bin: "test".to_string(),
8234            cmd,
8235            ..Default::default()
8236        }
8237    }
8238
8239    #[test]
8240    fn test_orphan_short_alias_survives_merge() {
8241        // Follow-up to test_prefix_global_flag_does_not_pollute_choices (jdx/mise#10069):
8242        // when `run` re-declares the long-only global `--raw` as a non-global `-r --raw`, the
8243        // added short `-r` must be unioned onto the surviving inherited global flag instead of
8244        // being discarded with the wholesale re-declaration. Otherwise `mycli run -r <task>`
8245        // would not recognize `-r` and would mis-validate it against the task's `choices` arg.
8246        let spec = mounted_orphan_short_spec();
8247
8248        let parsed = parse_partial(&spec, &input(&["test", "run", "-r", "sample:run"])).unwrap();
8249        assert_eq!(
8250            parsed
8251                .cmds
8252                .iter()
8253                .map(|c| c.name.as_str())
8254                .collect::<Vec<_>>(),
8255            vec!["test", "run", "sample:run"],
8256        );
8257
8258        // (a) The orphan short `-r` survives the descent, merged onto the inherited global flag,
8259        // and the original long `--raw` is still global too.
8260        assert!(
8261            parsed.available_flags.get("-r").is_some_and(|f| f.global),
8262            "-r must be merged onto the inherited global flag and stay global after descent",
8263        );
8264        assert!(
8265            parsed
8266                .available_flags
8267                .get("--raw")
8268                .is_some_and(|f| f.global),
8269            "--raw must stay global after descent",
8270        );
8271
8272        // (b) The token is consumed as a flag, not mistaken for the `choices` positional.
8273        assert!(
8274            parsed.args.is_empty(),
8275            "args should be empty, got {:?}",
8276            parsed.args
8277        );
8278
8279        // (c) The value still reaches as_env() so `usage_raw` is produced for execution/mounts.
8280        assert_eq!(
8281            parsed.as_env().get("usage_raw").map(String::as_str),
8282            Some("true"),
8283            "merged short's value must survive in as_env(), got {:?}",
8284            parsed.as_env(),
8285        );
8286
8287        // (d) Negative case: a purely-local flag that shares nothing with a global is NOT
8288        // promoted/merged — it is correctly dropped when descending into the mount.
8289        assert!(
8290            !parsed.available_flags.contains_key("-f"),
8291            "purely-local -f must not be promoted onto a global",
8292        );
8293        assert!(
8294            !parsed.available_flags.contains_key("--force"),
8295            "purely-local --force must not be promoted onto a global",
8296        );
8297
8298        // A real, valid choice still parses through the merged short prefix.
8299        let parsed =
8300            parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "alpha"])).unwrap();
8301        assert_eq!(parsed.args.len(), 1);
8302        assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
8303
8304        // And genuinely invalid choices are still rejected.
8305        assert_parse_err(
8306            parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "wrong"])),
8307            "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
8308        );
8309    }
8310
8311    #[test]
8312    fn test_orphan_short_does_not_clobber_unrelated_global() {
8313        // When a re-declaration's orphan short collides with a DIFFERENT inherited global's
8314        // short, the merge must not steal it. Here the root has both a long-only `--raw` global
8315        // and a `-r --restrict` global; `run` re-declares `-r --raw` as non-global. `-r` is a
8316        // genuine collision with `--restrict`, so global precedence must keep `-r -> restrict`.
8317        let run_cmd = SpecCommand::builder()
8318            .name("run")
8319            .flag(
8320                SpecFlag::builder()
8321                    .name("raw")
8322                    .short('r')
8323                    .long("raw")
8324                    .global(false)
8325                    .build(),
8326            )
8327            .build();
8328        let mut cmd = SpecCommand::builder()
8329            .name("test")
8330            .flag(
8331                SpecFlag::builder()
8332                    .name("raw")
8333                    .long("raw")
8334                    .global(true)
8335                    .build(),
8336            )
8337            .flag(
8338                SpecFlag::builder()
8339                    .name("restrict")
8340                    .short('r')
8341                    .long("restrict")
8342                    .global(true)
8343                    .build(),
8344            )
8345            .build();
8346        cmd.subcommands.insert("run".to_string(), run_cmd);
8347        let spec = Spec {
8348            name: "test".to_string(),
8349            bin: "test".to_string(),
8350            cmd,
8351            ..Default::default()
8352        };
8353
8354        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8355        // `-r` stays owned by the unrelated `--restrict` global, not stolen by the merged raw.
8356        assert_eq!(
8357            parsed.available_flags.get("-r").map(|f| f.name.as_str()),
8358            Some("restrict"),
8359            "-r must remain owned by the unrelated global it already belonged to",
8360        );
8361        // Both globals are still recognized and global after the descent.
8362        assert!(parsed
8363            .available_flags
8364            .get("--raw")
8365            .is_some_and(|f| f.global));
8366        assert!(parsed
8367            .available_flags
8368            .get("--restrict")
8369            .is_some_and(|f| f.global));
8370    }
8371
8372    #[test]
8373    fn test_redeclared_global_aliases_share_one_flag() {
8374        // A global declared with BOTH a short and a long, re-declared non-globally by a
8375        // subcommand that adds a third alias. Every alias key must resolve to the SAME merged
8376        // flag: the child's keys iterate in BTreeMap order (`--assume-yes`, `--yes`, `-y`), so by
8377        // the time `-y` is reached the long already points at the merged flag. That merged flag is
8378        // not a *different* inherited global, so the collision guard must not skip `-y` and leave
8379        // it pointing at the pre-merge global (which lacks the added `assume-yes` alias).
8380        let spec = r#"
8381flag "-y --yes" global=#true effect="write"
8382cmd "run" {
8383    flag "-y --yes --assume-yes"
8384}
8385"#
8386        .parse::<Spec>()
8387        .unwrap();
8388
8389        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8390
8391        for key in ["-y", "--yes", "--assume-yes"] {
8392            let flag = parsed
8393                .available_flags
8394                .get(key)
8395                .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
8396            assert!(flag.global, "{key} must stay global after the descent");
8397            assert_eq!(
8398                flag.long,
8399                vec!["yes".to_string(), "assume-yes".to_string()],
8400                "{key} must resolve to the flag carrying every alias",
8401            );
8402            assert_eq!(flag.short, vec!['y'], "{key} must keep the global's short");
8403        }
8404
8405        // One logical flag means one object: all three keys share a single `Arc`.
8406        assert_eq!(
8407            unique_flags(parsed.available_flags.values()).count(),
8408            1,
8409            "all aliases must point at one flag object, got {:?}",
8410            parsed.available_flags,
8411        );
8412
8413        // The global's effect survives the merge, so `-y` still marks the command as writing.
8414        assert_eq!(
8415            parsed.available_flags["-y"].effect,
8416            Some(crate::SpecCommandEffect::Write),
8417        );
8418    }
8419
8420    #[test]
8421    fn test_redeclared_global_keeps_hidden_alias_metadata() {
8422        let spec = r#"
8423flag "--yes" global=#true {
8424    alias "-q" "--quietly" hide=#true
8425}
8426cmd "run" {
8427    flag "--yes --assume-yes" {
8428        alias "-s" "--secret" hide=#true
8429    }
8430}
8431"#
8432        .parse::<Spec>()
8433        .unwrap();
8434
8435        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8436        let merged = &parsed.available_flags["--yes"];
8437        assert_eq!(merged.hidden_short_aliases, ['q', 's']);
8438        assert_eq!(merged.hidden_aliases, ["quietly", "secret"]);
8439        for key in ["-q", "-s", "--quietly", "--secret"] {
8440            assert!(Arc::ptr_eq(&parsed.available_flags[key], merged), "{key}");
8441        }
8442    }
8443
8444    #[test]
8445    fn test_redeclared_global_can_promote_hidden_aliases() {
8446        let spec = r#"
8447flag "--yes" global=#true {
8448    alias "-q" "--quietly" hide=#true
8449}
8450cmd "run" {
8451    flag "-q --yes --quietly"
8452}
8453"#
8454        .parse::<Spec>()
8455        .unwrap();
8456
8457        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8458        let merged = &parsed.available_flags["--yes"];
8459        assert!(merged.hidden_short_aliases.is_empty());
8460        assert!(merged.hidden_aliases.is_empty());
8461        for key in ["-q", "--quietly"] {
8462            assert!(Arc::ptr_eq(&parsed.available_flags[key], merged), "{key}");
8463        }
8464    }
8465
8466    #[test]
8467    fn test_partially_redeclared_global_keeps_all_aliases_on_one_flag() {
8468        // Same one-flag-one-object requirement as above, but the child re-declares only ONE of
8469        // the global's three aliases (`--yes`, not `-y`/`--confirm`) while adding a new one. The
8470        // aliases the child omits are never visited by the merge loop, so they must be rebound to
8471        // the merged flag explicitly — otherwise `-y` and `--confirm` keep pointing at the
8472        // pre-merge global and miss the added `assume-yes`.
8473        let spec = r#"
8474flag "-y --yes --confirm" global=#true
8475cmd "run" {
8476    flag "--yes --assume-yes"
8477}
8478"#
8479        .parse::<Spec>()
8480        .unwrap();
8481
8482        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8483
8484        for key in ["-y", "--yes", "--confirm", "--assume-yes"] {
8485            let flag = parsed
8486                .available_flags
8487                .get(key)
8488                .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
8489            assert!(flag.global, "{key} must stay global after the descent");
8490            assert_eq!(
8491                flag.long,
8492                vec![
8493                    "yes".to_string(),
8494                    "confirm".to_string(),
8495                    "assume-yes".to_string()
8496                ],
8497                "{key} must resolve to the flag carrying every alias",
8498            );
8499        }
8500
8501        assert_eq!(
8502            unique_flags(parsed.available_flags.values()).count(),
8503            1,
8504            "all aliases must point at one flag object, got {:?}",
8505            parsed.available_flags,
8506        );
8507    }
8508
8509    /// Build a spec shaped like mise's post-mount structure for jdx/mise#11282: a root with
8510    /// globals (`-E/--env <ENV>`, `--silent`), a `run` subcommand with a non-global flag, and a
8511    /// MOUNTED task command that declares its own `--env` (with choices) plus `--bump`.
8512    ///
8513    /// The task command is marked `mounted` the same way `SpecCommand::mount()` marks the
8514    /// commands it merges in, so the test stays hermetic (no mount subprocess).
8515    fn mounted_task_flag_spec() -> Spec {
8516        let mut task_cmd = SpecCommand::builder()
8517            .name("mytask")
8518            .flag(
8519                SpecFlag::builder()
8520                    .name("env")
8521                    .long("env")
8522                    .arg(
8523                        SpecArg::builder()
8524                            .name("name")
8525                            .choices(["dev", "stage", "prod"])
8526                            .build(),
8527                    )
8528                    .global(false)
8529                    .build(),
8530            )
8531            .flag(
8532                SpecFlag::builder()
8533                    .name("bump")
8534                    .long("bump")
8535                    .arg(
8536                        SpecArg::builder()
8537                            .name("type")
8538                            .choices(["auto", "major"])
8539                            .build(),
8540                    )
8541                    .global(false)
8542                    .build(),
8543            )
8544            .build();
8545        task_cmd.mounted = true;
8546
8547        let mut run_cmd = SpecCommand::builder()
8548            .name("run")
8549            .flag(
8550                SpecFlag::builder()
8551                    .name("force")
8552                    .short('f')
8553                    .long("force")
8554                    .global(false)
8555                    .build(),
8556            )
8557            .build();
8558        run_cmd.subcommands.insert("mytask".to_string(), task_cmd);
8559
8560        let mut cmd = SpecCommand::builder()
8561            .name("test")
8562            .flag(
8563                SpecFlag::builder()
8564                    .name("env")
8565                    .short('E')
8566                    .long("env")
8567                    .arg(SpecArg::builder().name("ENV").build())
8568                    .global(true)
8569                    .build(),
8570            )
8571            .flag(
8572                SpecFlag::builder()
8573                    .name("silent")
8574                    .long("silent")
8575                    .global(true)
8576                    .build(),
8577            )
8578            .build();
8579        cmd.subcommands.insert("run".to_string(), run_cmd);
8580
8581        Spec {
8582            name: "test".to_string(),
8583            bin: "test".to_string(),
8584            cmd,
8585            ..Default::default()
8586        }
8587    }
8588
8589    #[test]
8590    fn test_mount_boundary_does_not_apply_inside_the_mounted_tree() {
8591        // The mounted program's own commands are ordinary commands relative to each other, so
8592        // descending *within* the mounted tree must follow the normal rules — including keeping
8593        // an inherited global that a nested command re-declares as non-global (jdx/usage#649).
8594        // Treating every level of the tree as a mount boundary let the re-declaration shadow the
8595        // global, which the next descent's `retain(global)` then dropped entirely.
8596        let deep = SpecCommand::builder().name("deep").build();
8597        let mut sub = SpecCommand::builder()
8598            .name("sub")
8599            // Re-declares the mounted program's own global as non-global.
8600            .flag(
8601                SpecFlag::builder()
8602                    .name("cd")
8603                    .short('C')
8604                    .long("cd")
8605                    .arg(SpecArg::builder().name("dir").build())
8606                    .global(false)
8607                    .build(),
8608            )
8609            .build();
8610        sub.subcommands.insert("deep".to_string(), deep);
8611        let mut task = SpecCommand::builder()
8612            .name("task")
8613            .flag(
8614                SpecFlag::builder()
8615                    .name("cd")
8616                    .short('C')
8617                    .long("cd")
8618                    .arg(SpecArg::builder().name("dir").build())
8619                    .global(true)
8620                    .build(),
8621            )
8622            .build();
8623        task.subcommands.insert("sub".to_string(), sub);
8624        task.mark_mounted();
8625
8626        let mut run_cmd = SpecCommand::builder().name("run").build();
8627        run_cmd.subcommands.insert("task".to_string(), task);
8628        let mut cmd = SpecCommand::builder().name("test").build();
8629        cmd.subcommands.insert("run".to_string(), run_cmd);
8630        let spec = Spec {
8631            name: "test".to_string(),
8632            bin: "test".to_string(),
8633            cmd,
8634            ..Default::default()
8635        };
8636
8637        let parsed = parse_partial(&spec, &input(&["test", "run", "task", "sub", "deep"])).unwrap();
8638        assert!(
8639            parsed.available_flags.get("--cd").is_some_and(|f| f.global),
8640            "the mounted program's own global must survive descents inside the mounted tree",
8641        );
8642        assert!(
8643            parsed.completion_flags().contains_key("--cd"),
8644            "and must still be offered there: it belongs to the mounted program",
8645        );
8646        assert!(
8647            parsed.completion_flags().contains_key("-C"),
8648            "including the short the nested command re-declared",
8649        );
8650    }
8651
8652    #[test]
8653    fn test_mount_flags_merged_into_the_mounting_cmd_are_offered() {
8654        // A mounted spec may declare flags on its own root, which `SpecCommand::merge` folds
8655        // into the command the mount sits on. They belong to the mounted program, so they must
8656        // be offered inside the mounted commands rather than filtered out with the mounting
8657        // CLI's own flags.
8658        let mut task = SpecCommand::builder()
8659            .name("task")
8660            .flag(
8661                SpecFlag::builder()
8662                    .name("bump")
8663                    .long("bump")
8664                    .global(false)
8665                    .build(),
8666            )
8667            .build();
8668        task.mark_mounted();
8669
8670        let mut run_cmd = SpecCommand::builder().name("run").build();
8671        run_cmd.subcommands.insert("task".to_string(), task);
8672        // What `mount()` leaves behind when the mounted spec's root declares flags.
8673        run_cmd.flags = vec![
8674            SpecFlag::builder()
8675                .name("tglobal")
8676                .long("tglobal")
8677                .global(true)
8678                .build(),
8679            SpecFlag::builder()
8680                .name("tlocal")
8681                .long("tlocal")
8682                .global(false)
8683                .build(),
8684        ];
8685        run_cmd.flags_from_mount = true;
8686
8687        let mut cmd = SpecCommand::builder()
8688            .name("test")
8689            .flag(
8690                SpecFlag::builder()
8691                    .name("silent")
8692                    .long("silent")
8693                    .global(true)
8694                    .build(),
8695            )
8696            .build();
8697        cmd.subcommands.insert("run".to_string(), run_cmd);
8698        let spec = Spec {
8699            name: "test".to_string(),
8700            bin: "test".to_string(),
8701            cmd,
8702            ..Default::default()
8703        };
8704
8705        let parsed = parse_partial(&spec, &input(&["test", "run", "task"])).unwrap();
8706        assert_eq!(
8707            parsed.completion_flags().keys().collect::<Vec<_>>(),
8708            vec!["--bump", "--tglobal"],
8709            "the mounted spec's root global belongs to the mounted program; the mounting CLI's \
8710             `--silent` does not, and the mount's non-global root flag is not inherited",
8711        );
8712    }
8713
8714    #[test]
8715    fn test_mounted_cmd_does_not_offer_mounting_cli_globals() {
8716        // Regression for jdx/mise#11282. A mounted command describes another program, which
8717        // does not accept the mounting CLI's globals (mise forwards everything after a task
8718        // name to the task). They must stay recognized — they may appear before the mounted
8719        // command — but must not be offered in completions there.
8720        let spec = mounted_task_flag_spec();
8721        let parsed = parse_partial(&spec, &input(&["test", "run", "mytask"])).unwrap();
8722
8723        // Still recognized for parsing...
8724        assert!(parsed.available_flags.contains_key("--silent"));
8725        assert!(parsed.available_flags.contains_key("-E"));
8726        // ...but belonging to a command above the mount, so not offered.
8727        assert_eq!(
8728            parsed.completion_flags().keys().collect::<Vec<_>>(),
8729            vec!["--bump", "--env"],
8730            "only the mounted command's own flags may be offered",
8731        );
8732
8733        // `run`'s own non-global flag is dropped on descent, as it always was.
8734        assert!(!parsed.available_flags.contains_key("--force"));
8735    }
8736
8737    #[test]
8738    fn test_mounted_cmd_flag_wins_over_inherited_global() {
8739        // Second half of jdx/mise#11282: the mounted `--env` (with choices) used to be shadowed
8740        // by the root's `--env` global, so completing its value fell back to file completion.
8741        let spec = mounted_task_flag_spec();
8742        let parsed = parse_partial(&spec, &input(&["test", "run", "mytask", "--env"])).unwrap();
8743
8744        let awaiting = parsed
8745            .flag_awaiting_value
8746            .first()
8747            .expect("--env should await a value");
8748        assert_eq!(
8749            awaiting
8750                .arg
8751                .as_ref()
8752                .and_then(|a| a.choices.as_ref())
8753                .map(|c| c.choices.clone()),
8754            Some(vec![
8755                "dev".to_string(),
8756                "stage".to_string(),
8757                "prod".to_string()
8758            ]),
8759            "the mounted command's own --env must win over the inherited global",
8760        );
8761
8762        // The global's short is not declared by the mounted command, so it keeps pointing at
8763        // the global and a value passed before the mounted command still parses.
8764        let parsed =
8765            parse_partial(&spec, &input(&["test", "-E", "anything", "run", "mytask"])).unwrap();
8766        assert!(
8767            parsed.args.is_empty(),
8768            "prefix global tokens must not be consumed as positionals, got {:?}",
8769            parsed.args
8770        );
8771        assert_eq!(
8772            parsed.as_env().get("usage_env").map(String::as_str),
8773            Some("anything"),
8774        );
8775    }
8776
8777    #[test]
8778    fn test_prefix_flag_keeps_the_flag_it_was_read_as() {
8779        // A word before the mounted command is re-parsed by Phase 2, when the mounted command
8780        // already owns the name. It has to stay bound to the flag Phase 1 read it as, or the
8781        // global's value would be validated against the mounted flag's choices and a legitimate
8782        // value would be rejected.
8783        let spec = mounted_task_flag_spec();
8784        let parsed = parse_partial(
8785            &spec,
8786            &input(&["test", "--env", "not-a-task-choice", "run", "mytask"]),
8787        )
8788        .unwrap();
8789        assert!(
8790            parsed.errors.is_empty(),
8791            "prefix global value must not be validated against the mounted flag: {:?}",
8792            parsed
8793                .errors
8794                .iter()
8795                .map(|e| e.to_string())
8796                .collect::<Vec<_>>(),
8797        );
8798        assert_eq!(
8799            parsed.as_env().get("usage_env").map(String::as_str),
8800            Some("not-a-task-choice"),
8801        );
8802
8803        // The embedded-value form binds the same way.
8804        let parsed = parse_partial(
8805            &spec,
8806            &input(&["test", "--env=not-a-task-choice", "run", "mytask"]),
8807        )
8808        .unwrap();
8809        assert!(parsed.errors.is_empty());
8810        assert_eq!(
8811            parsed.as_env().get("usage_env").map(String::as_str),
8812            Some("not-a-task-choice"),
8813        );
8814
8815        // Meanwhile a word *after* the mounted command belongs to the mounted flag, even when
8816        // the same name was already used before it.
8817        let parsed = parse_partial(
8818            &spec,
8819            &input(&["test", "--env", "prod", "run", "mytask", "--env"]),
8820        )
8821        .unwrap();
8822        let awaiting = parsed
8823            .flag_awaiting_value
8824            .first()
8825            .expect("--env should await a value");
8826        assert_eq!(
8827            awaiting
8828                .arg
8829                .as_ref()
8830                .and_then(|a| a.choices.as_ref())
8831                .map(|c| c.choices.clone()),
8832            Some(vec![
8833                "dev".to_string(),
8834                "stage".to_string(),
8835                "prod".to_string()
8836            ]),
8837            "the mounted command's --env must own the name after the mounted command",
8838        );
8839    }
8840
8841    #[test]
8842    fn test_non_global_flag_does_not_hide_subcommand() {
8843        // A non-global flag may precede a subcommand (`mycli run --force task`). Phase 1 used to
8844        // stop scanning at one, so the subcommand — and any mount on it — was never reached and
8845        // its name was left to Phase 2 to mis-read as a positional: `unexpected word: mytask`.
8846        let spec = mounted_task_flag_spec();
8847
8848        for words in [
8849            // `run` declares `-f/--force` as non-global.
8850            &["test", "run", "--force", "mytask"][..],
8851            &["test", "run", "-f", "mytask"][..],
8852            // Mixed with a global before the subcommand.
8853            &["test", "-E", "prod", "run", "--force", "mytask"][..],
8854        ] {
8855            let parsed = parse_partial(&spec, &input(words)).unwrap();
8856            assert_eq!(
8857                parsed
8858                    .cmds
8859                    .iter()
8860                    .map(|c| c.name.as_str())
8861                    .collect::<Vec<_>>(),
8862                vec!["test", "run", "mytask"],
8863                "{words:?} should descend into the mounted command",
8864            );
8865            assert!(
8866                parsed.args.is_empty(),
8867                "{words:?} should not consume a positional, got {:?}",
8868                parsed.args,
8869            );
8870            assert_eq!(
8871                parsed.as_env().get("usage_force").map(String::as_str),
8872                Some("true"),
8873                "the non-global flag must still be recorded for {words:?}",
8874            );
8875        }
8876
8877        // A non-global flag that takes a value consumes it, rather than reading the value as the
8878        // subcommand.
8879        let mut run_cmd = SpecCommand::builder()
8880            .name("run")
8881            .flag(
8882                SpecFlag::builder()
8883                    .name("output")
8884                    .short('o')
8885                    .long("output")
8886                    .arg(SpecArg::builder().name("mode").build())
8887                    .global(false)
8888                    .build(),
8889            )
8890            .build();
8891        run_cmd.subcommands.insert(
8892            "task".to_string(),
8893            SpecCommand::builder().name("task").build(),
8894        );
8895        let mut cmd = SpecCommand::builder().name("test").build();
8896        cmd.subcommands.insert("run".to_string(), run_cmd);
8897        let spec = Spec {
8898            name: "test".to_string(),
8899            bin: "test".to_string(),
8900            cmd,
8901            ..Default::default()
8902        };
8903
8904        let parsed =
8905            parse_partial(&spec, &input(&["test", "run", "--output", "quiet", "task"])).unwrap();
8906        assert_eq!(
8907            parsed
8908                .cmds
8909                .iter()
8910                .map(|c| c.name.as_str())
8911                .collect::<Vec<_>>(),
8912            vec!["test", "run", "task"],
8913        );
8914        assert_eq!(
8915            parsed.as_env().get("usage_output").map(String::as_str),
8916            Some("quiet"),
8917        );
8918
8919        // An unknown flag still stops the scan: it may take a value, so the next word cannot be
8920        // assumed to be a subcommand. `run` takes no positional, so this stays an error.
8921        assert_parse_err(
8922            parse_partial(&spec, &input(&["test", "run", "--nope", "task"])),
8923            "unexpected word: --nope",
8924        );
8925    }
8926
8927    #[test]
8928    fn test_non_mounted_subcommand_offers_inherited_globals() {
8929        // Nothing changes for ordinary (non-mounted) subcommands: a global declared above is
8930        // still both recognized and offered.
8931        let mut run_cmd = SpecCommand::builder().name("run").build();
8932        run_cmd.subcommands.insert(
8933            "nested".to_string(),
8934            SpecCommand::builder().name("nested").build(),
8935        );
8936        let mut cmd = SpecCommand::builder()
8937            .name("test")
8938            .flag(
8939                SpecFlag::builder()
8940                    .name("silent")
8941                    .long("silent")
8942                    .global(true)
8943                    .build(),
8944            )
8945            .build();
8946        cmd.subcommands.insert("run".to_string(), run_cmd);
8947        let spec = Spec {
8948            name: "test".to_string(),
8949            bin: "test".to_string(),
8950            cmd,
8951            ..Default::default()
8952        };
8953
8954        let parsed = parse_partial(&spec, &input(&["test", "run", "nested"])).unwrap();
8955        assert_eq!(
8956            parsed.completion_flags().keys().collect::<Vec<_>>(),
8957            parsed.available_flags.keys().collect::<Vec<_>>(),
8958        );
8959        assert!(parsed.completion_flags().contains_key("--silent"));
8960    }
8961
8962    #[test]
8963    fn test_subcommand_alias_collision_keeps_last_owner() {
8964        // The orphan-alias merge must not disturb how two flags in the SAME subcommand that
8965        // share an alias are resolved. Historically the flattened flag map gave the shared
8966        // alias to the LAST-declared flag (last-writer-wins); that must be preserved.
8967        let run_cmd = SpecCommand::builder()
8968            .name("run")
8969            .flag(
8970                SpecFlag::builder()
8971                    .name("alpha")
8972                    .short('x')
8973                    .long("alpha")
8974                    .global(false)
8975                    .build(),
8976            )
8977            .flag(
8978                SpecFlag::builder()
8979                    .name("beta")
8980                    .short('x')
8981                    .long("beta")
8982                    .global(false)
8983                    .build(),
8984            )
8985            .build();
8986        let mut cmd = SpecCommand::builder().name("test").build();
8987        cmd.subcommands.insert("run".to_string(), run_cmd);
8988        let spec = Spec {
8989            name: "test".to_string(),
8990            bin: "test".to_string(),
8991            cmd,
8992            ..Default::default()
8993        };
8994
8995        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8996        // `-x` is declared by both flags; the last one (`beta`) keeps it, as before the fix.
8997        assert_eq!(
8998            parsed.available_flags.get("-x").map(|f| f.name.as_str()),
8999            Some("beta"),
9000            "the last-declared flag must keep a shared short alias",
9001        );
9002        // Both distinct long aliases remain recognized and point to their own flag.
9003        assert_eq!(
9004            parsed
9005                .available_flags
9006                .get("--alpha")
9007                .map(|f| f.name.as_str()),
9008            Some("alpha"),
9009        );
9010        assert_eq!(
9011            parsed
9012                .available_flags
9013                .get("--beta")
9014                .map(|f| f.name.as_str()),
9015            Some("beta"),
9016        );
9017    }
9018
9019    #[test]
9020    fn test_default_subcommand_same_name_child() {
9021        // Test that default_subcommand doesn't cause issues when the default subcommand
9022        // has a child with the same name (e.g., "run" has a task named "run").
9023        // This verifies we don't switch multiple times or get stuck in a loop.
9024        let run_task = SpecCommand::builder()
9025            .name("run")
9026            .arg(SpecArg::builder().name("args").build())
9027            .build();
9028        let mut run_cmd = SpecCommand::builder().name("run").build();
9029        run_cmd.subcommands.insert("run".to_string(), run_task);
9030
9031        let mut cmd = SpecCommand::builder().name("test").build();
9032        cmd.subcommands.insert("run".to_string(), run_cmd);
9033
9034        let spec = Spec {
9035            name: "test".to_string(),
9036            bin: "test".to_string(),
9037            cmd,
9038            default_subcommand: Some("run".to_string()),
9039            ..Default::default()
9040        };
9041
9042        // "test run" explicitly matches the "run" subcommand (not via default_subcommand)
9043        let input = vec!["test".to_string(), "run".to_string()];
9044        let parsed = parse(&spec, &input).unwrap();
9045
9046        // Should have two commands: root and "run"
9047        assert_eq!(parsed.cmds.len(), 2);
9048        assert_eq!(parsed.cmds[0].name, "test");
9049        assert_eq!(parsed.cmds[1].name, "run");
9050
9051        // "test run run" should descend into the "run" task (child of "run" subcommand)
9052        let input = vec![
9053            "test".to_string(),
9054            "run".to_string(),
9055            "run".to_string(),
9056            "hello".to_string(),
9057        ];
9058        let parsed = parse(&spec, &input).unwrap();
9059
9060        assert_eq!(parsed.cmds.len(), 3);
9061        assert_eq!(parsed.cmds[0].name, "test");
9062        assert_eq!(parsed.cmds[1].name, "run");
9063        assert_eq!(parsed.cmds[2].name, "run");
9064        assert_eq!(parsed.args.len(), 1);
9065        let value = parsed.args.values().next().unwrap();
9066        assert_eq!(value.to_string(), "hello");
9067
9068        // Key test case: "test other" should switch to default subcommand "run"
9069        // and treat "other" as a positional arg (not try to switch again because
9070        // "run" also has a "run" child).
9071        let mut run_cmd = SpecCommand::builder()
9072            .name("run")
9073            .arg(SpecArg::builder().name("task").build())
9074            .build();
9075        let run_task = SpecCommand::builder().name("run").build();
9076        run_cmd.subcommands.insert("run".to_string(), run_task);
9077
9078        let mut cmd = SpecCommand::builder().name("test").build();
9079        cmd.subcommands.insert("run".to_string(), run_cmd);
9080
9081        let spec = Spec {
9082            name: "test".to_string(),
9083            bin: "test".to_string(),
9084            cmd,
9085            default_subcommand: Some("run".to_string()),
9086            ..Default::default()
9087        };
9088
9089        let input = vec!["test".to_string(), "other".to_string()];
9090        let parsed = parse(&spec, &input).unwrap();
9091
9092        // Should have two commands: root and "run" (the default)
9093        // We should NOT have switched again to the "run" task child
9094        assert_eq!(parsed.cmds.len(), 2);
9095        assert_eq!(parsed.cmds[0].name, "test");
9096        assert_eq!(parsed.cmds[1].name, "run");
9097
9098        // "other" should be parsed as a positional arg
9099        assert_eq!(parsed.args.len(), 1);
9100        let value = parsed.args.values().next().unwrap();
9101        assert_eq!(value.to_string(), "other");
9102    }
9103
9104    #[test]
9105    fn test_restart_token() {
9106        // Test that restart_token resets argument parsing
9107        let run_cmd = SpecCommand::builder()
9108            .name("run")
9109            .arg(SpecArg::builder().name("task").build())
9110            .restart_token(":::".to_string())
9111            .build();
9112        let mut cmd = SpecCommand::builder().name("test").build();
9113        cmd.subcommands.insert("run".to_string(), run_cmd);
9114
9115        let spec = Spec {
9116            name: "test".to_string(),
9117            bin: "test".to_string(),
9118            cmd,
9119            ..Default::default()
9120        };
9121
9122        // "test run task1 ::: task2" - should end up with task2 as the arg
9123        let input = vec![
9124            "test".to_string(),
9125            "run".to_string(),
9126            "task1".to_string(),
9127            ":::".to_string(),
9128            "task2".to_string(),
9129        ];
9130        let parsed = parse(&spec, &input).unwrap();
9131
9132        // After restart, args were cleared and task2 was parsed
9133        assert_eq!(parsed.args.len(), 1);
9134        let value = parsed.args.values().next().unwrap();
9135        assert_eq!(value.to_string(), "task2");
9136    }
9137
9138    #[test]
9139    fn test_restart_token_multiple() {
9140        // Test multiple restart tokens
9141        let run_cmd = SpecCommand::builder()
9142            .name("run")
9143            .arg(SpecArg::builder().name("task").build())
9144            .restart_token(":::".to_string())
9145            .build();
9146        let mut cmd = SpecCommand::builder().name("test").build();
9147        cmd.subcommands.insert("run".to_string(), run_cmd);
9148
9149        let spec = Spec {
9150            name: "test".to_string(),
9151            bin: "test".to_string(),
9152            cmd,
9153            ..Default::default()
9154        };
9155
9156        // "test run task1 ::: task2 ::: task3" - should end up with task3 as the arg
9157        let input = vec![
9158            "test".to_string(),
9159            "run".to_string(),
9160            "task1".to_string(),
9161            ":::".to_string(),
9162            "task2".to_string(),
9163            ":::".to_string(),
9164            "task3".to_string(),
9165        ];
9166        let parsed = parse(&spec, &input).unwrap();
9167
9168        // After multiple restarts, args were cleared and task3 was parsed
9169        assert_eq!(parsed.args.len(), 1);
9170        let value = parsed.args.values().next().unwrap();
9171        assert_eq!(value.to_string(), "task3");
9172    }
9173
9174    #[test]
9175    fn test_restart_token_clears_flag_awaiting_value() {
9176        // Test that restart_token clears pending flag values
9177        let run_cmd = SpecCommand::builder()
9178            .name("run")
9179            .arg(SpecArg::builder().name("task").build())
9180            .flag(
9181                SpecFlag::builder()
9182                    .name("jobs")
9183                    .long("jobs")
9184                    .arg(SpecArg::builder().name("count").build())
9185                    .build(),
9186            )
9187            .restart_token(":::".to_string())
9188            .build();
9189        let mut cmd = SpecCommand::builder().name("test").build();
9190        cmd.subcommands.insert("run".to_string(), run_cmd);
9191
9192        let spec = Spec {
9193            name: "test".to_string(),
9194            bin: "test".to_string(),
9195            cmd,
9196            ..Default::default()
9197        };
9198
9199        // "test run task1 --jobs ::: task2" - task2 should be an arg, not a flag value
9200        let input = vec![
9201            "test".to_string(),
9202            "run".to_string(),
9203            "task1".to_string(),
9204            "--jobs".to_string(),
9205            ":::".to_string(),
9206            "task2".to_string(),
9207        ];
9208        let parsed = parse(&spec, &input).unwrap();
9209
9210        // task2 should be parsed as the task arg, not as --jobs value
9211        assert_eq!(parsed.args.len(), 1);
9212        let value = parsed.args.values().next().unwrap();
9213        assert_eq!(value.to_string(), "task2");
9214        // --jobs should not have a value
9215        assert!(parsed.flag_awaiting_value.is_empty());
9216    }
9217
9218    #[test]
9219    fn test_restart_token_resets_double_dash() {
9220        // Test that restart_token resets the -- separator effect
9221        let run_cmd = SpecCommand::builder()
9222            .name("run")
9223            .arg(SpecArg::builder().name("task").build())
9224            .arg(SpecArg::builder().name("extra_args").var(true).build())
9225            .flag(SpecFlag::builder().name("verbose").long("verbose").build())
9226            .restart_token(":::".to_string())
9227            .build();
9228        let mut cmd = SpecCommand::builder().name("test").build();
9229        cmd.subcommands.insert("run".to_string(), run_cmd);
9230
9231        let spec = Spec {
9232            name: "test".to_string(),
9233            bin: "test".to_string(),
9234            cmd,
9235            ..Default::default()
9236        };
9237
9238        // "test run task1 -- extra ::: --verbose task2" - --verbose should be a flag after :::
9239        let input = vec![
9240            "test".to_string(),
9241            "run".to_string(),
9242            "task1".to_string(),
9243            "--".to_string(),
9244            "extra".to_string(),
9245            ":::".to_string(),
9246            "--verbose".to_string(),
9247            "task2".to_string(),
9248        ];
9249        let parsed = parse(&spec, &input).unwrap();
9250
9251        // --verbose should be parsed as a flag (not an arg) after the restart
9252        assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
9253        // task2 should be the arg after restart
9254        let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
9255        let value = parsed.args.get(task_arg).unwrap();
9256        assert_eq!(value.to_string(), "task2");
9257    }
9258
9259    #[test]
9260    fn test_double_dashes_without_preserve() {
9261        // Only the first `--` is a separator; a later one is a value, because flag
9262        // parsing has already stopped and there is nothing left for it to do.
9263        // `preserve` is about the *first* one — see the test below, where none is
9264        // consumed at all.
9265        let run_cmd = SpecCommand::builder()
9266            .name("run")
9267            .arg(SpecArg::builder().name("args").var(true).build())
9268            .build();
9269        let mut cmd = SpecCommand::builder().name("test").build();
9270        cmd.subcommands.insert("run".to_string(), run_cmd);
9271
9272        let spec = Spec {
9273            name: "test".to_string(),
9274            bin: "test".to_string(),
9275            cmd,
9276            ..Default::default()
9277        };
9278
9279        // "test run arg1 -- arg2 -- arg3": the first separates, the second is a value
9280        let input = vec![
9281            "test".to_string(),
9282            "run".to_string(),
9283            "arg1".to_string(),
9284            "--".to_string(),
9285            "arg2".to_string(),
9286            "--".to_string(),
9287            "arg3".to_string(),
9288        ];
9289        let parsed = parse(&spec, &input).unwrap();
9290
9291        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
9292        let value = parsed.args.get(args_arg).unwrap();
9293        assert_eq!(value.to_string(), "arg1 arg2 -- arg3");
9294    }
9295
9296    #[test]
9297    fn test_double_dashes_with_preserve() {
9298        // Test that variadic args WITH `preserve` keep all double dashes
9299        let run_cmd = SpecCommand::builder()
9300            .name("run")
9301            .arg(
9302                SpecArg::builder()
9303                    .name("args")
9304                    .var(true)
9305                    .double_dash(SpecDoubleDashChoices::Preserve)
9306                    .build(),
9307            )
9308            .build();
9309        let mut cmd = SpecCommand::builder().name("test").build();
9310        cmd.subcommands.insert("run".to_string(), run_cmd);
9311
9312        let spec = Spec {
9313            name: "test".to_string(),
9314            bin: "test".to_string(),
9315            cmd,
9316            ..Default::default()
9317        };
9318
9319        // "test run arg1 -- arg2 -- arg3" - all double dashes should be preserved
9320        let input = vec![
9321            "test".to_string(),
9322            "run".to_string(),
9323            "arg1".to_string(),
9324            "--".to_string(),
9325            "arg2".to_string(),
9326            "--".to_string(),
9327            "arg3".to_string(),
9328        ];
9329        let parsed = parse(&spec, &input).unwrap();
9330
9331        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
9332        let value = parsed.args.get(args_arg).unwrap();
9333        assert_eq!(value.to_string(), "arg1 -- arg2 -- arg3");
9334    }
9335
9336    #[test]
9337    fn test_double_dashes_with_preserve_only_dashes() {
9338        // Test that variadic args WITH `preserve` keep all double dashes even
9339        // if the values are just double dashes
9340        let run_cmd = SpecCommand::builder()
9341            .name("run")
9342            .arg(
9343                SpecArg::builder()
9344                    .name("args")
9345                    .var(true)
9346                    .double_dash(SpecDoubleDashChoices::Preserve)
9347                    .build(),
9348            )
9349            .build();
9350        let mut cmd = SpecCommand::builder().name("test").build();
9351        cmd.subcommands.insert("run".to_string(), run_cmd);
9352
9353        let spec = Spec {
9354            name: "test".to_string(),
9355            bin: "test".to_string(),
9356            cmd,
9357            ..Default::default()
9358        };
9359
9360        // "test run -- --" - all double dashes should be preserved
9361        let input = vec![
9362            "test".to_string(),
9363            "run".to_string(),
9364            "--".to_string(),
9365            "--".to_string(),
9366        ];
9367        let parsed = parse(&spec, &input).unwrap();
9368
9369        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
9370        let value = parsed.args.get(args_arg).unwrap();
9371        assert_eq!(value.to_string(), "-- --");
9372    }
9373
9374    #[test]
9375    fn test_double_dashes_with_preserve_multiple_args() {
9376        // Test with multiple args where only the second has has `preserve`
9377        let run_cmd = SpecCommand::builder()
9378            .name("run")
9379            .arg(SpecArg::builder().name("task").build())
9380            .arg(
9381                SpecArg::builder()
9382                    .name("extra_args")
9383                    .var(true)
9384                    .double_dash(SpecDoubleDashChoices::Preserve)
9385                    .build(),
9386            )
9387            .build();
9388        let mut cmd = SpecCommand::builder().name("test").build();
9389        cmd.subcommands.insert("run".to_string(), run_cmd);
9390
9391        let spec = Spec {
9392            name: "test".to_string(),
9393            bin: "test".to_string(),
9394            cmd,
9395            ..Default::default()
9396        };
9397
9398        // The first arg "task1" is captured normally
9399        // Then extra_args with `preserve` captures everything, including the "--" tokens
9400        let input = vec![
9401            "test".to_string(),
9402            "run".to_string(),
9403            "task1".to_string(),
9404            "--".to_string(),
9405            "arg1".to_string(),
9406            "--".to_string(),
9407            "--foo".to_string(),
9408        ];
9409        let parsed = parse(&spec, &input).unwrap();
9410
9411        let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
9412        let task_value = parsed.args.get(task_arg).unwrap();
9413        assert_eq!(task_value.to_string(), "task1");
9414
9415        let extra_arg = parsed.args.keys().find(|a| a.name == "extra_args").unwrap();
9416        let extra_value = parsed.args.get(extra_arg).unwrap();
9417        assert_eq!(extra_value.to_string(), "-- arg1 -- --foo");
9418    }
9419
9420    fn spec_with_args(args: impl IntoIterator<Item = SpecArg>) -> Spec {
9421        let cmd = SpecCommand::builder().name("test").args(args).build();
9422        Spec {
9423            name: "test".to_string(),
9424            bin: "test".to_string(),
9425            cmd,
9426            ..Default::default()
9427        }
9428    }
9429
9430    fn arg_value(parsed: &ParseOutput, name: &str) -> String {
9431        let arg = parsed
9432            .args
9433            .keys()
9434            .find(|a| a.name == name)
9435            .unwrap_or_else(|| panic!("expected arg {name} to be parsed"));
9436        parsed.args.get(arg).unwrap().to_string()
9437    }
9438
9439    fn required_arg(name: &str) -> SpecArg {
9440        SpecArg::builder()
9441            .name(name)
9442            .var(true)
9443            .required(false)
9444            .double_dash(SpecDoubleDashChoices::Required)
9445            .build()
9446    }
9447
9448    #[test]
9449    fn test_double_dash_required_reports_error_once_for_variadic() {
9450        // A variadic arg is offered every remaining word, but the mistake is one mistake.
9451        let spec = spec_with_args([required_arg("files")]);
9452
9453        let parsed = parse_partial(&spec, &input(&["test", "a", "b", "c"])).unwrap();
9454
9455        assert!(parsed.args.is_empty());
9456        assert_eq!(parsed.errors.len(), 1);
9457        assert!(
9458            matches!(&parsed.errors[0], UsageErr::ArgRequiresDoubleDash(name) if name == "files")
9459        );
9460    }
9461
9462    #[test]
9463    fn test_double_dash_required_suppresses_missing_arg() {
9464        // The arg is never filled, so the end-of-parse check would also call it missing.
9465        let spec = spec_with_args([SpecArg::builder()
9466            .name("file")
9467            .required(true)
9468            .double_dash(SpecDoubleDashChoices::Required)
9469            .build()]);
9470
9471        let parsed = parse_partial(&spec, &input(&["test", "x"])).unwrap();
9472
9473        assert_eq!(parsed.errors.len(), 1);
9474        assert!(matches!(
9475            &parsed.errors[0],
9476            UsageErr::ArgRequiresDoubleDash(_)
9477        ));
9478        // The cursor stays put, so a completion keeps offering the same arg.
9479        assert_eq!(
9480            parsed.next_arg.as_ref().map(|a| a.name.as_str()),
9481            Some("file")
9482        );
9483        assert!(!parsed.double_dash_seen);
9484    }
9485
9486    #[test]
9487    fn test_double_dash_routes_to_required_arg() {
9488        // Everything after `--` belongs to the arg that requires it, even though the greedy
9489        // variadic before it would otherwise swallow the rest (clap's `Arg::last(true)`).
9490        let spec = spec_with_args([
9491            SpecArg::builder()
9492                .name("tool")
9493                .var(true)
9494                .required(false)
9495                .build(),
9496            required_arg("command"),
9497        ]);
9498
9499        let parsed = parse(&spec, &input(&["test", "node@20", "--", "node", "app.js"])).unwrap();
9500
9501        assert_eq!(arg_value(&parsed, "tool"), "node@20");
9502        assert_eq!(arg_value(&parsed, "command"), "node app.js");
9503        assert!(parsed.double_dash_seen);
9504    }
9505
9506    #[test]
9507    fn test_double_dash_routes_with_gap_reports_missing_arg() {
9508        // Jumping the cursor leaves `tool` empty even though `command` is filled, so the
9509        // "is it filled?" check cannot be a count of how many args were filled.
9510        let spec = spec_with_args([
9511            SpecArg::builder()
9512                .name("tool")
9513                .var(true)
9514                .required(true)
9515                .build(),
9516            required_arg("command"),
9517        ]);
9518
9519        let parsed = parse_partial(&spec, &input(&["test", "--", "ls"])).unwrap();
9520
9521        assert_eq!(arg_value(&parsed, "command"), "ls");
9522        assert!(parsed.args.keys().all(|a| a.name != "tool"));
9523        assert!(parsed
9524            .errors
9525            .iter()
9526            .any(|e| matches!(e, UsageErr::MissingArg(name) if name == "tool")));
9527    }
9528
9529    #[test]
9530    fn test_double_dash_gap_applies_defaults() {
9531        // Same gap, seen from `Parser::parse`: the skipped arg still gets its default.
9532        let spec = spec_with_args([
9533            SpecArg::builder()
9534                .name("tool")
9535                .var(true)
9536                .required(false)
9537                .default_value("node@20")
9538                .build(),
9539            required_arg("command"),
9540        ]);
9541
9542        let parsed = parse(&spec, &input(&["test", "--", "ls"])).unwrap();
9543
9544        assert_eq!(arg_value(&parsed, "command"), "ls");
9545        assert_eq!(arg_value(&parsed, "tool"), "node@20");
9546    }
9547
9548    fn spec_with_restart_token_and_required_arg() -> Spec {
9549        let run_cmd = SpecCommand::builder()
9550            .name("run")
9551            .arg(SpecArg::builder().name("task").build())
9552            .arg(required_arg("run_args"))
9553            .restart_token(":::".to_string())
9554            .build();
9555        let mut cmd = SpecCommand::builder().name("test").build();
9556        cmd.subcommands.insert("run".to_string(), run_cmd);
9557        Spec {
9558            name: "test".to_string(),
9559            bin: "test".to_string(),
9560            cmd,
9561            ..Default::default()
9562        }
9563    }
9564
9565    #[test]
9566    fn test_double_dash_required_restart_token_resets_separator() {
9567        // The `--` before `:::` belongs to the previous invocation only.
9568        let spec = spec_with_restart_token_and_required_arg();
9569
9570        let parsed = parse_partial(
9571            &spec,
9572            &input(&["test", "run", "task1", "--", "a", ":::", "task2", "b"]),
9573        )
9574        .unwrap();
9575
9576        assert_eq!(arg_value(&parsed, "task"), "task2");
9577        assert!(parsed.args.keys().all(|a| a.name != "run_args"));
9578        // Reported once even though the arg was violated after already succeeding once.
9579        assert_eq!(
9580            parsed
9581                .errors
9582                .iter()
9583                .filter(|e| matches!(e, UsageErr::ArgRequiresDoubleDash(_)))
9584                .count(),
9585            1
9586        );
9587    }
9588
9589    #[test]
9590    fn test_double_dash_required_restart_token_accepts_new_separator() {
9591        let spec = spec_with_restart_token_and_required_arg();
9592
9593        let parsed = parse(
9594            &spec,
9595            &input(&["test", "run", "task1", "--", "a", ":::", "task2", "--", "c"]),
9596        )
9597        .unwrap();
9598
9599        assert_eq!(arg_value(&parsed, "task"), "task2");
9600        assert_eq!(arg_value(&parsed, "run_args"), "c");
9601    }
9602
9603    #[test]
9604    fn test_double_dash_preserve_is_not_a_separator() {
9605        // A `--` that `preserve` keeps is a *value* of that arg, so it must not unlock the
9606        // arg that requires a separator. Deliberate: one token cannot be both.
9607        let spec = spec_with_args([
9608            SpecArg::builder()
9609                .name("kept")
9610                .var(true)
9611                .var_max(1)
9612                .required(false)
9613                .double_dash(SpecDoubleDashChoices::Preserve)
9614                .build(),
9615            required_arg("rest"),
9616        ]);
9617
9618        let parsed = parse_partial(&spec, &input(&["test", "--", "x"])).unwrap();
9619
9620        assert_eq!(arg_value(&parsed, "kept"), "--");
9621        assert!(parsed.args.keys().all(|a| a.name != "rest"));
9622        assert!(!parsed.double_dash_seen);
9623        assert_eq!(parsed.errors.len(), 1);
9624    }
9625
9626    #[test]
9627    fn test_double_dash_required_does_not_bail_in_parse_partial() {
9628        // Completions parse half-typed command lines; they must still get a result.
9629        let spec = spec_with_args([required_arg("file")]);
9630
9631        assert!(parse_partial(&spec, &input(&["test", "x"])).is_ok());
9632        assert!(parse(&spec, &input(&["test", "x"])).is_err());
9633    }
9634
9635    #[test]
9636    fn test_double_dash_without_required_arg_does_not_move_cursor() {
9637        // Specs with no `double_dash="required"` arg are untouched by the jump.
9638        let spec = spec_with_args([
9639            SpecArg::builder().name("first").required(false).build(),
9640            SpecArg::builder().name("second").required(false).build(),
9641        ]);
9642
9643        let parsed = parse(&spec, &input(&["test", "--", "a", "b"])).unwrap();
9644
9645        assert_eq!(arg_value(&parsed, "first"), "a");
9646        assert_eq!(arg_value(&parsed, "second"), "b");
9647        assert!(parsed.next_arg.is_none());
9648    }
9649
9650    #[test]
9651    fn test_parser_with_custom_env_for_required_arg() {
9652        let spec = spec_with_arg(
9653            SpecArg::builder()
9654                .name("name")
9655                .env("NAME")
9656                .required(true)
9657                .build(),
9658        );
9659        std::env::remove_var("NAME");
9660
9661        let parsed = parse_with_env(&spec, &["test"], &[("NAME", "john")])
9662            .expect("parse should succeed with custom env");
9663        assert_eq!(parsed.args.len(), 1);
9664        assert_eq!(first_string_value(&parsed), "john");
9665    }
9666
9667    #[test]
9668    fn test_parser_with_custom_env_for_required_flag() {
9669        let spec = spec_with_flag(
9670            SpecFlag::builder()
9671                .long("name")
9672                .env("NAME")
9673                .required(true)
9674                .arg(SpecArg::builder().name("name").build())
9675                .build(),
9676        );
9677        std::env::remove_var("NAME");
9678
9679        let parsed = parse_with_env(&spec, &["test"], &[("NAME", "jane")])
9680            .expect("parse should succeed with custom env");
9681        assert_eq!(parsed.flags.len(), 1);
9682        assert_eq!(first_string_value(&parsed), "jane");
9683    }
9684
9685    #[test]
9686    fn test_flag_environment_fallbacks_preserve_declaration_order() {
9687        let spec = spec_with_flag(
9688            SpecFlag::builder()
9689                .long("name")
9690                .env("NAME")
9691                .env_fallback("OLD_NAME")
9692                .env_fallback("OLDER_NAME")
9693                .deprecated_env("DEPRECATED_NAME")
9694                .arg(SpecArg::builder().name("name").build())
9695                .build(),
9696        );
9697
9698        let parsed = parse_with_env(
9699            &spec,
9700            &["test"],
9701            &[
9702                ("NAME", "canonical"),
9703                ("OLD_NAME", "fallback"),
9704                ("DEPRECATED_NAME", "deprecated"),
9705            ],
9706        )
9707        .unwrap();
9708        assert_eq!(first_string_value(&parsed), "canonical");
9709
9710        let parsed = parse_with_env(
9711            &spec,
9712            &["test"],
9713            &[("OLDER_NAME", "older"), ("OLD_NAME", "old")],
9714        )
9715        .unwrap();
9716        assert_eq!(first_string_value(&parsed), "old");
9717
9718        let parsed =
9719            parse_with_env(&spec, &["test"], &[("DEPRECATED_NAME", "deprecated")]).unwrap();
9720        assert_eq!(first_string_value(&parsed), "deprecated");
9721    }
9722
9723    #[test]
9724    fn a_value_from_a_deprecated_alias_says_which_name_to_use() {
9725        let spec = spec_with_flag(
9726            SpecFlag::builder()
9727                .long("name")
9728                .env("NAME")
9729                .deprecated_env("DEPRECATED_NAME")
9730                .arg(SpecArg::builder().name("name").build())
9731                .build(),
9732        );
9733
9734        // The current name is not a deprecated one, and says nothing.
9735        let parsed = parse_with_env(&spec, &["test"], &[("NAME", "canonical")]).unwrap();
9736        assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
9737
9738        let parsed =
9739            parse_with_env(&spec, &["test"], &[("DEPRECATED_NAME", "deprecated")]).unwrap();
9740        assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
9741        assert_eq!(
9742            parsed.warnings[0].kind,
9743            crate::warn::WarningKind::DeprecatedEnv
9744        );
9745        assert_eq!(parsed.warnings[0].name, "DEPRECATED_NAME");
9746        assert_eq!(parsed.warnings[0].replacement.as_deref(), Some("NAME"));
9747        // Reported, not printed, and the value still arrives.
9748        assert_eq!(first_string_value(&parsed), "deprecated");
9749    }
9750
9751    #[test]
9752    fn a_deprecated_flag_reports_only_when_it_was_used() {
9753        let spec = spec_with_flag(
9754            SpecFlag::builder()
9755                .long("output")
9756                .deprecated("use --out")
9757                .deprecated_remove_at("3.0.0")
9758                .arg(SpecArg::builder().name("output").build())
9759                .build(),
9760        );
9761
9762        let parsed = parse_with_env(&spec, &["test"], &[]).unwrap();
9763        assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
9764
9765        let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
9766        assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
9767        assert_eq!(
9768            parsed.warnings[0].kind,
9769            crate::warn::WarningKind::DeprecatedFlag
9770        );
9771        // Named the way it was typed, dashes and all.
9772        assert_eq!(parsed.warnings[0].name, "--output");
9773        assert_eq!(parsed.warnings[0].remove_at.as_deref(), Some("3.0.0"));
9774        assert_eq!(
9775            parsed.warnings[0].render(),
9776            "warning: --output is deprecated, removed at 3.0.0: use --out\n",
9777        );
9778    }
9779
9780    #[test]
9781    fn a_milestone_the_spec_has_not_reached_stays_quiet() {
9782        let flag = SpecFlag::builder()
9783            .long("output")
9784            .deprecated("use --out")
9785            .deprecated_warn_at("9.0.0")
9786            .arg(SpecArg::builder().name("output").build())
9787            .build();
9788        let mut spec = spec_with_flag(flag);
9789        spec.version = Some("2.0.0".to_string());
9790
9791        let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
9792        assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
9793
9794        // And once the CLI is the release that was named, it speaks up.
9795        spec.version = Some("9.0.0".to_string());
9796        let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
9797        assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
9798    }
9799
9800    #[test]
9801    fn test_parser_with_custom_env_still_fails_when_missing() {
9802        let spec = spec_with_arg(
9803            SpecArg::builder()
9804                .name("name")
9805                .env("NAME")
9806                .required(true)
9807                .build(),
9808        );
9809        std::env::remove_var("NAME");
9810        assert!(parse_with_env(&spec, &["test"], &[]).is_err());
9811    }
9812
9813    #[test]
9814    fn test_parser_does_not_treat_env_choice_value_as_help() {
9815        let spec = spec_with_arg(
9816            SpecArg::builder()
9817                .name("env")
9818                .env("CURRENT_ENV")
9819                .choices(["dev", "staging"])
9820                .required(false)
9821                .build(),
9822        );
9823
9824        assert_parse_err(
9825            parse_with_env(&spec, &["test"], &[("CURRENT_ENV", "--help")]),
9826            "Invalid choice for arg env: --help, expected one of dev, staging",
9827        );
9828    }
9829
9830    #[test]
9831    fn test_parser_does_not_treat_default_choice_value_as_help() {
9832        let spec = spec_with_flag(
9833            SpecFlag::builder()
9834                .long("env")
9835                .arg(
9836                    SpecArg::builder()
9837                        .name("env")
9838                        .choices(["dev", "staging"])
9839                        .build(),
9840                )
9841                .default_value("--help")
9842                .build(),
9843        );
9844
9845        assert_parse_err(
9846            parse_with_env(&spec, &["test"], &[]),
9847            "Invalid choice for option env: --help, expected one of dev, staging",
9848        );
9849    }
9850
9851    /// argv as `parse` wants it, program name included.
9852    fn words(of: &[&str]) -> Vec<String> {
9853        of.iter().map(|s| s.to_string()).collect()
9854    }
9855
9856    #[test]
9857    fn a_command_that_needs_a_subcommand_says_so() {
9858        // The spec has carried `subcommand_required` since the derive needed it, and this parser
9859        // never read it — so `mise generate`, which declares it, parsed as a complete
9860        // invocation while usage-argv and clap both refused. Found by the differential fuzzer.
9861        let spec: Spec = r#"
9862name "ex"
9863bin "ex"
9864cmd "gen" subcommand_required=#true {
9865    cmd "two" {}
9866    cmd "one" {}
9867    cmd "secret" hide=#true {}
9868    alias "g"
9869}
9870cmd "open" {
9871    cmd "sub" {}
9872}
9873"#
9874        .parse()
9875        .unwrap();
9876
9877        let err = parse(&spec, &words(&["ex", "gen"])).unwrap_err();
9878        // Sorted, so the message does not depend on map order; hidden commands left out,
9879        // because a message telling someone to type a hidden name is worse than a vague one;
9880        // and the alias not listed beside the name it points at.
9881        assert_eq!(err.to_string(), "`gen` needs a subcommand: one of one, two");
9882
9883        // Reached through its alias, and still about the command rather than the spelling.
9884        let err = parse(&spec, &words(&["ex", "g"])).unwrap_err();
9885        assert!(err.to_string().starts_with("`gen` needs a subcommand"));
9886
9887        // Given one: fine.
9888        parse(&spec, &words(&["ex", "gen", "one"])).unwrap();
9889
9890        // And a command that has subcommands without declaring them required is untouched —
9891        // this is the half that keeps the check from being "any command with children".
9892        parse(&spec, &words(&["ex", "open"])).unwrap();
9893        parse(&spec, &words(&["ex", "open", "sub"])).unwrap();
9894    }
9895
9896    #[test]
9897    fn arg_required_else_help_observes_the_selected_commands_argv() {
9898        let spec: Spec = r#"
9899name "ex"
9900bin "ex"
9901flag "--verbose" global=#true
9902cmd "run" arg_required_else_help=#true {
9903    flag "--all"
9904}
9905"#
9906        .parse()
9907        .unwrap();
9908        let words = |items: &[&str]| items.iter().map(|s| (*s).to_string()).collect::<Vec<_>>();
9909
9910        let err = parse(&spec, &words(&["ex", "run"])).unwrap_err();
9911        assert!(err.to_string().contains("Usage: ex run"), "{err}");
9912
9913        // A global before the command belongs to the ancestor. It selected `run`, but did not
9914        // give `run` an argument of its own.
9915        let err = parse(&spec, &words(&["ex", "--verbose", "run"])).unwrap_err();
9916        assert!(err.to_string().contains("Usage: ex run"), "{err}");
9917
9918        parse(&spec, &words(&["ex", "run", "--all"])).expect("run received an argv token");
9919    }
9920
9921    #[test]
9922    fn empty_default_with_parent_flags_observes_child_argv() {
9923        let spec: Spec = r#"
9924name "ex"
9925bin "ex"
9926default_subcommand "run"
9927default_subcommand_on_empty #true
9928flag "--verbose" global=#true
9929cmd "run" arg_required_else_help=#true {}
9930"#
9931        .parse()
9932        .unwrap();
9933        let words = |items: &[&str]| items.iter().map(|s| (*s).to_string()).collect::<Vec<_>>();
9934        let err = parse(&spec, &words(&["ex", "--verbose"])).unwrap_err();
9935        assert!(err.to_string().contains("Usage: ex run"), "{err}");
9936    }
9937
9938    #[test]
9939    fn an_unmatched_word_is_forwarded_when_external_subcommand_is_set() {
9940        let spec: Spec = r#"
9941name "ex"
9942bin "ex"
9943unknown_flags "error"
9944external_subcommand #true
9945cmd "install"
9946flag "-v --verbose" global=#true
9947"#
9948        .parse()
9949        .unwrap();
9950
9951        let parsed = parse(&spec, &input(&["ex", "foo", "--help", "bar"])).unwrap();
9952        assert_eq!(
9953            parsed.external,
9954            Some(vec!["foo".into(), "--help".into(), "bar".into()])
9955        );
9956        assert!(parsed.flags.is_empty());
9957
9958        // Known subcommands still win.
9959        let parsed = parse(&spec, &input(&["ex", "install"])).unwrap();
9960        assert_eq!(parsed.cmd.name, "install");
9961        assert!(parsed.external.is_none());
9962
9963        // A global flag before the unmatched word still binds on the parent.
9964        let parsed = parse(&spec, &input(&["ex", "-v", "foo", "--verbose"])).unwrap();
9965        assert_eq!(
9966            parsed.external,
9967            Some(vec!["foo".into(), "--verbose".into()])
9968        );
9969        assert!(parsed.flags.keys().any(|flag| flag.name == "verbose"));
9970
9971        // An unknown flag on the parent is still an error, which is what clap does.
9972        assert!(parse(&spec, &input(&["ex", "--wat"])).is_err());
9973
9974        // A negative number is a value, not a flag, so it can be the unmatched word.
9975        // usage-argv already forwarded `-1`; Phase 1 used to treat every `starts_with('-')`
9976        // token as a flag and never reach the catch-all.
9977        let parsed = parse(&spec, &input(&["ex", "-1", "rest"])).unwrap();
9978        assert_eq!(parsed.external, Some(vec!["-1".into(), "rest".into()]));
9979    }
9980
9981    #[test]
9982    fn an_external_subcommand_satisfies_subcommand_required() {
9983        let mut spec: Spec = r#"
9984name "ex"
9985bin "ex"
9986external_subcommand #true
9987cmd "install"
9988"#
9989        .parse()
9990        .unwrap();
9991        spec.cmd.subcommand_required = true;
9992
9993        parse(&spec, &input(&["ex", "foo", "--help"])).unwrap();
9994        assert!(parse(&spec, &input(&["ex"])).is_err());
9995    }
9996
9997    #[test]
9998    fn a_default_subcommand_outranks_an_external_one() {
9999        let spec: Spec = r#"
10000name "ex"
10001bin "ex"
10002default_subcommand "run"
10003external_subcommand #true
10004cmd "run" {
10005    arg "[task]"
10006}
10007"#
10008        .parse()
10009        .unwrap();
10010
10011        let parsed = parse(&spec, &input(&["ex", "build"])).unwrap();
10012        assert_eq!(parsed.cmd.name, "run");
10013        assert!(parsed.external.is_none());
10014        assert_eq!(first_string_value(&parsed), "build");
10015    }
10016
10017    #[test]
10018    fn multicall_basename_strips_a_path_and_exe() {
10019        assert_eq!(multicall_basename("/usr/bin/ls"), "ls");
10020        assert_eq!(multicall_basename(r"C:\busybox\ls.exe"), "ls");
10021        assert_eq!(multicall_basename("LS.EXE"), "LS");
10022        assert_eq!(multicall_basename("busybox"), "busybox");
10023    }
10024
10025    #[test]
10026    fn a_multicall_applet_is_the_first_word() {
10027        let spec: Spec = r#"
10028name "busybox"
10029bin "busybox"
10030multicall #true
10031cmd "ls" {
10032    arg "[ARGS]" var=#true
10033}
10034cmd "cat"
10035"#
10036        .parse()
10037        .unwrap();
10038
10039        // A symlink: argv[0] is the applet.
10040        let parsed = parse(&spec, &input(&["/usr/bin/ls", "-l"])).unwrap();
10041        assert_eq!(parsed.cmd.name, "ls");
10042        match parsed.args.values().next() {
10043            Some(ParseValue::MultiString(values)) => assert_eq!(values, &["-l".to_string()]),
10044            other => panic!("expected ARGS to collect -l, got {other:?}"),
10045        }
10046
10047        // A dispatcher invocation still skips argv[0].
10048        let parsed = parse(&spec, &input(&["/usr/bin/busybox", "ls", "-l"])).unwrap();
10049        assert_eq!(parsed.cmd.name, "ls");
10050
10051        // Configured dispatcher values receive the same path and extension normalization.
10052        let mut configured = spec.clone();
10053        configured.name = "BusyBox".to_string();
10054        configured.bin = "/opt/bin/busybox.exe".to_string();
10055        let parsed = parse(&configured, &input(&["/usr/bin/busybox.exe", "ls", "-l"])).unwrap();
10056        assert_eq!(parsed.cmd.name, "ls");
10057
10058        // `.exe` is stripped so Windows and Unix agree.
10059        let parsed = parse(&spec, &input(&["ls.exe"])).unwrap();
10060        assert_eq!(parsed.cmd.name, "ls");
10061
10062        // Without the property, argv[0] is discarded as usual.
10063        let mut plain = spec.clone();
10064        plain.multicall = false;
10065        let parsed = parse(&plain, &input(&["/usr/bin/ls", "ls"])).unwrap();
10066        assert_eq!(parsed.cmd.name, "ls");
10067    }
10068
10069    #[test]
10070    fn a_multicall_unknown_applet_can_be_external() {
10071        let spec: Spec = r#"
10072name "busybox"
10073bin "busybox"
10074multicall #true
10075unknown_flags "error"
10076external_subcommand #true
10077cmd "ls"
10078"#
10079        .parse()
10080        .unwrap();
10081
10082        let parsed = parse(&spec, &input(&["/usr/bin/git", "--help"])).unwrap();
10083        assert_eq!(parsed.external, Some(vec!["git".into(), "--help".into()]));
10084
10085        let mut closed = spec.clone();
10086        closed.cmd.external_subcommand = false;
10087        assert!(parse(&closed, &input(&["wat"])).is_err());
10088    }
10089
10090    #[cfg(feature = "unstable_choices_env")]
10091    #[test]
10092    fn test_parser_arg_choices_from_custom_env() {
10093        let spec = spec_arg_choices_env("DEPLOY_ENVS");
10094
10095        let parsed =
10096            parse_with_env(&spec, &["test", "bar"], &[("DEPLOY_ENVS", "foo,bar baz")]).unwrap();
10097        assert_eq!(first_string_value(&parsed), "bar");
10098
10099        assert_parse_err(
10100            parse_with_env(&spec, &["test", "prod"], &[("DEPLOY_ENVS", "foo,bar baz")]),
10101            "Invalid choice for arg env: prod, expected one of foo, bar, baz",
10102        );
10103        assert_parse_err(
10104            parse_with_env(&spec, &["test", "prod"], &[]),
10105            "Invalid choice for arg env: prod, no choices resolved from env DEPLOY_ENVS",
10106        );
10107    }
10108
10109    #[cfg(feature = "unstable_choices_env")]
10110    #[test]
10111    fn test_parser_validates_flag_choices_from_custom_env() {
10112        let spec = spec_flag_choices_env("DEPLOY_ENVS");
10113        let parsed = parse_with_env(
10114            &spec,
10115            &["test", "--env", "baz"],
10116            &[("DEPLOY_ENVS", "foo,bar baz")],
10117        )
10118        .unwrap();
10119        assert_eq!(first_string_value(&parsed), "baz");
10120    }
10121
10122    #[cfg(feature = "unstable_choices_env")]
10123    #[test]
10124    fn test_parser_revalidates_env_and_default_values_against_choices_env() {
10125        let arg_env_spec = spec_with_arg(
10126            SpecArg::builder()
10127                .name("env")
10128                .env("CURRENT_ENV")
10129                .choices_env("DEPLOY_ENVS")
10130                .build(),
10131        );
10132        assert_parse_err(
10133            parse_with_env(
10134                &arg_env_spec,
10135                &["test"],
10136                &[("CURRENT_ENV", "prod"), ("DEPLOY_ENVS", "dev,staging")],
10137            ),
10138            "Invalid choice for arg env: prod, expected one of dev, staging",
10139        );
10140
10141        let flag_default_spec = spec_with_flag(
10142            SpecFlag::builder()
10143                .long("env")
10144                .arg(
10145                    SpecArg::builder()
10146                        .name("env")
10147                        .choices_env("DEPLOY_ENVS")
10148                        .build(),
10149                )
10150                .default_value("prod")
10151                .build(),
10152        );
10153        assert_parse_err(
10154            parse_with_env(
10155                &flag_default_spec,
10156                &["test"],
10157                &[("DEPLOY_ENVS", "dev,staging")],
10158            ),
10159            "Invalid choice for option env: prod, expected one of dev, staging",
10160        );
10161    }
10162
10163    #[test]
10164    fn test_variadic_arg_captures_unknown_flags_from_spec_string() {
10165        let spec: Spec = r#"
10166            flag "-v --verbose" var=#true
10167            arg "[database]" default="myapp_dev"
10168            arg "[args...]"
10169        "#
10170        .parse()
10171        .unwrap();
10172        let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
10173            .into_iter()
10174            .map(String::from)
10175            .collect();
10176        let parsed = parse(&spec, &input).unwrap();
10177        let env = parsed.as_env();
10178        assert_eq!(env.get("usage_database").unwrap(), "mydb");
10179        assert_eq!(env.get("usage_args").unwrap(), "--host localhost");
10180    }
10181
10182    #[test]
10183    fn test_variadic_arg_captures_unknown_flags() {
10184        let cmd = SpecCommand::builder()
10185            .name("test")
10186            .flag(SpecFlag::builder().short('v').long("verbose").build())
10187            .arg(SpecArg::builder().name("database").required(false).build())
10188            .arg(
10189                SpecArg::builder()
10190                    .name("args")
10191                    .required(false)
10192                    .var(true)
10193                    .build(),
10194            )
10195            .build();
10196        let spec = Spec {
10197            name: "test".to_string(),
10198            bin: "test".to_string(),
10199            cmd,
10200            ..Default::default()
10201        };
10202
10203        // Unknown --host flag and its value should be captured by [args...]
10204        let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
10205            .into_iter()
10206            .map(String::from)
10207            .collect();
10208        let parsed = parse(&spec, &input).unwrap();
10209        assert_eq!(parsed.args.len(), 2);
10210        let args_val = parsed
10211            .args
10212            .iter()
10213            .find(|(a, _)| a.name == "args")
10214            .unwrap()
10215            .1;
10216        match args_val {
10217            ParseValue::MultiString(v) => {
10218                assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
10219            }
10220            _ => panic!("Expected MultiString, got {:?}", args_val),
10221        }
10222    }
10223
10224    #[test]
10225    fn test_variadic_arg_captures_unknown_flags_with_double_dash() {
10226        let cmd = SpecCommand::builder()
10227            .name("test")
10228            .flag(SpecFlag::builder().short('v').long("verbose").build())
10229            .arg(SpecArg::builder().name("database").required(false).build())
10230            .arg(
10231                SpecArg::builder()
10232                    .name("args")
10233                    .required(false)
10234                    .var(true)
10235                    .build(),
10236            )
10237            .build();
10238        let spec = Spec {
10239            name: "test".to_string(),
10240            bin: "test".to_string(),
10241            cmd,
10242            ..Default::default()
10243        };
10244
10245        // With explicit -- separator
10246        let input: Vec<String> = vec!["test", "--", "mydb", "--host", "localhost"]
10247            .into_iter()
10248            .map(String::from)
10249            .collect();
10250        let parsed = parse(&spec, &input).unwrap();
10251        assert_eq!(parsed.args.len(), 2);
10252        let args_val = parsed
10253            .args
10254            .iter()
10255            .find(|(a, _)| a.name == "args")
10256            .unwrap()
10257            .1;
10258        match args_val {
10259            ParseValue::MultiString(v) => {
10260                assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
10261            }
10262            _ => panic!("Expected MultiString, got {:?}", args_val),
10263        }
10264    }
10265
10266    #[test]
10267    fn test_variadic_arg_unknown_flag_equals_value_not_split() {
10268        // Regression: --flag=value should be treated as a single positional token when
10269        // --flag is not a known spec flag, not split into "--flag=value" AND "value".
10270        let spec: Spec = r#"arg "[other_args]" var=#true"#.parse().unwrap();
10271
10272        // Single unknown --flag=value: must not produce a stray "3" positional.
10273        // as_env() shell-joins values, so "=" gets quoted.
10274        let input: Vec<String> = vec!["test", "--option=3"]
10275            .into_iter()
10276            .map(String::from)
10277            .collect();
10278        let parsed = parse(&spec, &input).unwrap();
10279        let env = parsed.as_env();
10280        assert_eq!(
10281            env.get("usage_other_args").map(String::as_str),
10282            Some("'--option=3'"),
10283            "expected a single --option=3 token, got {:?}",
10284            env.get("usage_other_args"),
10285        );
10286
10287        // Multiple unknown --flag=value args should each be kept intact
10288        let input2: Vec<String> = vec!["test", "--foo=bar", "--baz=qux"]
10289            .into_iter()
10290            .map(String::from)
10291            .collect();
10292        let parsed2 = parse(&spec, &input2).unwrap();
10293        let env2 = parsed2.as_env();
10294        assert_eq!(
10295            env2.get("usage_other_args").map(String::as_str),
10296            Some("'--foo=bar' '--baz=qux'"),
10297            "expected two intact tokens, got {:?}",
10298            env2.get("usage_other_args"),
10299        );
10300
10301        // Mix of plain positional args and unknown --flag=value tokens
10302        let input3: Vec<String> = vec!["test", "positional1", "--option=3", "positional2"]
10303            .into_iter()
10304            .map(String::from)
10305            .collect();
10306        let parsed3 = parse(&spec, &input3).unwrap();
10307        let env3 = parsed3.as_env();
10308        assert_eq!(
10309            env3.get("usage_other_args").map(String::as_str),
10310            Some("positional1 '--option=3' positional2"),
10311            "expected positional args and intact flag token, got {:?}",
10312            env3.get("usage_other_args"),
10313        );
10314    }
10315
10316    #[test]
10317    fn test_allow_hyphen_values_consumes_short_flag_collision() {
10318        let spec = r#"
10319flag "-d --working-dir <DIR>"
10320flag "-a --args <ARGS>" allow_hyphen_values=#true
10321"#
10322        .parse::<Spec>()
10323        .unwrap();
10324
10325        let parsed = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap();
10326
10327        assert_eq!(parsed.flags.len(), 1);
10328        assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
10329    }
10330
10331    #[test]
10332    fn test_allow_hyphen_values_consumes_embedded_long_value() {
10333        let spec = r#"
10334flag "-d --working-dir <DIR>"
10335flag "-a --args <ARGS>" allow_hyphen_values=#true
10336"#
10337        .parse::<Spec>()
10338        .unwrap();
10339
10340        let parsed = parse(&spec, &input(&["test", "--args=-destroy"])).unwrap();
10341
10342        assert_eq!(parsed.flags.len(), 1);
10343        assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
10344    }
10345
10346    #[test]
10347    fn test_allow_hyphen_values_takes_the_separator_as_its_value() {
10348        // The flag is declared to accept a token that looks like a flag, and `--` looks
10349        // like one, so it binds — which is what clap does with the same declaration.
10350        // Letting the separator arm run first consumed it and left the flag hungry, and
10351        // the flag then ate the word past it: `-a -- -x` bound `-x` with the `--` gone.
10352        let spec = r#"
10353flag "-a --args <ARGS>" allow_hyphen_values=#true
10354arg "[rest]..."
10355"#
10356        .parse::<Spec>()
10357        .unwrap();
10358
10359        let parsed = parse(&spec, &input(&["test", "-a", "--", "-x"])).unwrap();
10360
10361        assert_eq!(flag_string_value(&parsed, "args"), "--");
10362        let rest = parsed
10363            .args
10364            .values()
10365            .next()
10366            .expect("expected the word after the separator to reach the argument");
10367        assert_eq!(rest.to_string(), "-x");
10368    }
10369
10370    #[test]
10371    fn test_variadic_allow_hyphen_values_collects_after_a_hyphenated_first_value() {
10372        // Which token supplied the first value says nothing about how many the argument
10373        // takes, so collection carries on from a hyphenated one exactly as from a plain
10374        // one. It still stops at the next flag-like token, which is what keeps a second
10375        // occurrence of the flag from being eaten as a value.
10376        let spec = r#"
10377flag "-a --args <ARGS>..." allow_hyphen_values=#true
10378"#
10379        .parse::<Spec>()
10380        .unwrap();
10381
10382        let parsed = parse(&spec, &input(&["test", "-a", "-x", "b", "c"])).unwrap();
10383
10384        let flag = parsed
10385            .flags
10386            .keys()
10387            .find(|flag| flag.name == "args")
10388            .expect("expected args flag");
10389        match parsed.flags.get(flag).expect("expected args value") {
10390            ParseValue::MultiString(values) => assert_eq!(values, &["-x", "b", "c"]),
10391            other => panic!("expected a list of values, got {other:?}"),
10392        }
10393    }
10394
10395    #[test]
10396    fn test_variadic_allow_hyphen_values_consumes_repeated_flag_values() {
10397        let spec = r#"
10398flag "-a --args <ARGS>" var=#true allow_hyphen_values=#true
10399"#
10400        .parse::<Spec>()
10401        .unwrap();
10402
10403        let parsed = parse(&spec, &input(&["test", "-a", "-val1", "-a", "-val2"])).unwrap();
10404
10405        let flag = parsed
10406            .flags
10407            .keys()
10408            .find(|flag| flag.name == "args")
10409            .expect("expected args flag");
10410        let value = parsed.flags.get(flag).expect("expected args value");
10411        match value {
10412            ParseValue::MultiString(values) => {
10413                assert_eq!(values, &vec!["-val1".to_string(), "-val2".to_string()]);
10414            }
10415            _ => panic!("expected MultiString, got {value:?}"),
10416        }
10417    }
10418
10419    #[test]
10420    fn test_require_equals_accepts_attached_and_refuses_detached() {
10421        let spec = r#"
10422flag "--inspect <PORT>" require_equals=#true
10423"#
10424        .parse::<Spec>()
10425        .unwrap();
10426
10427        let parsed = parse(&spec, &input(&["test", "--inspect=9229"])).unwrap();
10428        assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
10429
10430        let err = parse(&spec, &input(&["test", "--inspect", "9229"])).unwrap_err();
10431        let msg = format!("{err}");
10432        assert!(
10433            msg.contains("requires an argument") || msg.contains("inspect"),
10434            "detached value must be refused: {msg}"
10435        );
10436    }
10437
10438    #[test]
10439    fn boolean_flags_can_accept_attached_values_when_enabled() {
10440        let spec: Spec = r#"
10441name "ex"
10442bin "ex"
10443flag "--color" negate="--no-color" bool_value=#true
10444arg "[rest]"
10445"#
10446        .parse()
10447        .unwrap();
10448
10449        for (token, expected) in [
10450            ("--color", true),
10451            ("--color=true", true),
10452            ("--color=false", false),
10453            ("--no-color", false),
10454            ("--no-color=false", true),
10455        ] {
10456            let parsed = parse(&spec, &input(&["ex", token])).unwrap();
10457            assert!(
10458                matches!(
10459                    parsed.flags.get(&spec.cmd.flags[0]),
10460                    Some(ParseValue::Bool(value)) if *value == expected
10461                ),
10462                "{token}"
10463            );
10464        }
10465
10466        let parsed = parse(&spec, &input(&["ex", "--color=false", "word"])).unwrap();
10467        assert!(matches!(
10468            parsed.args.get(&spec.cmd.args[0]),
10469            Some(ParseValue::String(value)) if value == "word"
10470        ));
10471        let err = parse(&spec, &input(&["ex", "--color=maybe"])).unwrap_err();
10472        assert!(err.to_string().contains("expected `true` or `false`"));
10473
10474        let strict: Spec = r#"
10475name "ex"
10476bin "ex"
10477args_override_self #false
10478flag "--color" negate="--no-color" bool_value=#true
10479"#
10480        .parse()
10481        .unwrap();
10482        assert!(parse(&strict, &input(&["ex", "--color=false", "--color=true"])).is_err());
10483        let parsed = parse(
10484            &strict,
10485            &input(&["ex", "--color=false", "--no-color=false"]),
10486        )
10487        .unwrap();
10488        assert!(matches!(
10489            parsed.flags.get(&strict.cmd.flags[0]),
10490            Some(ParseValue::Bool(true))
10491        ));
10492    }
10493
10494    #[test]
10495    fn test_require_equals_refuses_a_detached_value_after_a_short_bundle() {
10496        let spec = r#"
10497flag "-a --all"
10498flag "-i --inspect <PORT>" require_equals=#true
10499"#
10500        .parse::<Spec>()
10501        .unwrap();
10502
10503        let err = parse(&spec, &input(&["test", "-ai", "9229"])).unwrap_err();
10504        let msg = format!("{err}");
10505        assert!(
10506            msg.contains("requires an argument") || msg.contains("inspect"),
10507            "bundled short must refuse the following word: {msg}"
10508        );
10509    }
10510
10511    #[test]
10512    fn test_default_missing_binds_when_the_value_is_left_off() {
10513        let spec = r#"
10514flag "-c --color <WHEN>" default_missing="always"
10515flag "-v --verbose"
10516"#
10517        .parse::<Spec>()
10518        .unwrap();
10519
10520        let parsed = parse(&spec, &input(&["test", "--color"])).unwrap();
10521        assert_eq!(flag_string_value(&parsed, "color"), "always");
10522
10523        let parsed = parse(&spec, &input(&["test", "--color=never"])).unwrap();
10524        assert_eq!(flag_string_value(&parsed, "color"), "never");
10525
10526        let parsed = parse(&spec, &input(&["test", "--color", "never"])).unwrap();
10527        assert_eq!(flag_string_value(&parsed, "color"), "never");
10528
10529        let parsed = parse(&spec, &input(&["test", "--color", "--verbose"])).unwrap();
10530        assert_eq!(flag_string_value(&parsed, "color"), "always");
10531        assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
10532
10533        let parsed = parse(&spec, &input(&["test", "--color="])).unwrap();
10534        assert_eq!(flag_string_value(&parsed, "color"), "");
10535
10536        let parsed = parse(&spec, &input(&["test", "-cnever"])).unwrap();
10537        assert_eq!(flag_string_value(&parsed, "color"), "never");
10538
10539        let parsed = parse(&spec, &input(&["test", "-c", "-v"])).unwrap();
10540        assert_eq!(flag_string_value(&parsed, "color"), "always");
10541        assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
10542    }
10543
10544    #[test]
10545    fn test_default_missing_requires_opt_in_for_detached_negative_flag_values() {
10546        let spec = r#"
10547flag "--apps <N>"
10548flag "--jobs <N>" default_missing="default missing"
10549flag "--kids <N>" default_missing="default missing" allow_negative_numbers=#true
10550"#
10551        .parse::<Spec>()
10552        .unwrap();
10553
10554        let parsed = parse(&spec, &input(&["test", "--apps", "-1"])).unwrap();
10555        assert_eq!(flag_string_value(&parsed, "apps"), "-1");
10556
10557        let err = parse(&spec, &input(&["test", "--jobs", "-1"])).unwrap_err();
10558        assert!(
10559            err.to_string().contains("unexpected word: -1"),
10560            "default_missing must keep an unopted negative value separate: {err}"
10561        );
10562
10563        let parsed = parse(&spec, &input(&["test", "--kids", "-1"])).unwrap();
10564        assert_eq!(flag_string_value(&parsed, "kids"), "-1");
10565
10566        let external_spec = r#"
10567external_subcommand #true
10568flag "--apps <N>"
10569"#
10570        .parse::<Spec>()
10571        .unwrap();
10572        let parsed = parse(&external_spec, &input(&["test", "--apps", "-1"])).unwrap();
10573        assert_eq!(flag_string_value(&parsed, "apps"), "-1");
10574        assert!(parsed.external.is_none());
10575
10576        for words in [
10577            &["test", "--apps", "--jobs", "1"][..],
10578            &["test", "--apps", "--kids", "-1"][..],
10579        ] {
10580            let err = parse(&spec, &input(words)).unwrap_err();
10581            let message = err.to_string();
10582            assert!(
10583                message.contains("--apps") && message.contains("requires an argument"),
10584                "the earlier flag must report its missing value for {words:?}: {message}"
10585            );
10586        }
10587    }
10588
10589    #[test]
10590    fn test_optional_flag_value_preserves_bare_and_explicit_empty_forms() {
10591        let spec = r#"
10592flag "--bump [LEVEL]" value_optional=#true
10593flag "--verbose"
10594arg "[FILE]"
10595"#
10596        .parse::<Spec>()
10597        .unwrap();
10598
10599        let absent = parse(&spec, &input(&["test"])).unwrap();
10600        assert!(!absent.flags.keys().any(|flag| flag.name == "bump"));
10601
10602        let bare = parse(&spec, &input(&["test", "--bump", "--verbose", "file.txt"])).unwrap();
10603        let bump = bare
10604            .flags
10605            .iter()
10606            .find(|(flag, _)| flag.name == "bump")
10607            .map(|(_, value)| value)
10608            .unwrap();
10609        assert!(matches!(bump, ParseValue::MultiString(values) if values.is_empty()));
10610        assert!(bare.flags.keys().any(|flag| flag.name == "verbose"));
10611        assert_eq!(arg_value(&bare, "FILE"), "file.txt");
10612
10613        let explicit = parse(&spec, &input(&["test", "--bump=", "file.txt"])).unwrap();
10614        assert_eq!(flag_string_value(&explicit, "bump"), "");
10615
10616        let corrected = parse(
10617            &spec,
10618            &input(&["test", "--bump=2", "--bump", "--verbose", "file.txt"]),
10619        )
10620        .unwrap();
10621        let bump = corrected
10622            .flags
10623            .iter()
10624            .find(|(flag, _)| flag.name == "bump")
10625            .map(|(_, value)| value)
10626            .unwrap();
10627        assert!(matches!(bump, ParseValue::MultiString(values) if values.is_empty()));
10628
10629        let collecting = r#"
10630flag "--tag [TAG]..." value_optional=#true
10631flag "--verbose"
10632"#
10633        .parse::<Spec>()
10634        .unwrap();
10635        let valued = parse(
10636            &collecting,
10637            &input(&["test", "--tag", "one", "two", "--verbose"]),
10638        )
10639        .unwrap();
10640        let tag = valued
10641            .flags
10642            .iter()
10643            .find(|(flag, _)| flag.name == "tag")
10644            .map(|(_, value)| value)
10645            .unwrap();
10646        assert!(matches!(tag, ParseValue::MultiString(values) if values == &["one", "two"]));
10647    }
10648
10649    #[test]
10650    fn test_repeatable_bare_optional_values_count_each_occurrence() {
10651        let spec = r#"
10652flag "--tag [TAG]" var=#true var_min=2 var_max=2 value_optional=#true
10653"#
10654        .parse::<Spec>()
10655        .unwrap();
10656
10657        let parsed = parse(&spec, &input(&["test", "--tag", "--tag"])).unwrap();
10658        let tag = parsed
10659            .flags
10660            .iter()
10661            .find(|(flag, _)| flag.name == "tag")
10662            .map(|(_, value)| value)
10663            .unwrap();
10664        assert!(matches!(tag, ParseValue::MultiString(values) if values == &["", ""]));
10665
10666        assert!(parse(&spec, &input(&["test", "--tag"])).is_err());
10667        assert!(parse(&spec, &input(&["test", "--tag", "--tag", "--tag"])).is_err());
10668    }
10669
10670    #[test]
10671    fn test_repeatable_variadic_optional_values_do_not_gain_bare_occurrences() {
10672        let spec = r#"
10673flag "--tag [TAG]..." var=#true value_optional=#true
10674flag "--verbose"
10675"#
10676        .parse::<Spec>()
10677        .unwrap();
10678
10679        for argv in [
10680            &["test", "--tag", "one", "two"][..],
10681            &["test", "--tag", "one", "two", "--verbose"][..],
10682            &["test", "--tag", "one", "--tag", "two"][..],
10683        ] {
10684            let parsed = parse(&spec, &input(argv)).unwrap();
10685            let tag = parsed
10686                .flags
10687                .iter()
10688                .find(|(flag, _)| flag.name == "tag")
10689                .map(|(_, value)| value)
10690                .unwrap();
10691            assert!(
10692                matches!(tag, ParseValue::MultiString(values) if values == &["one", "two"]),
10693                "argv={argv:?}: {tag:?}"
10694            );
10695        }
10696
10697        let bare = parse(&spec, &input(&["test", "--tag", "--verbose"])).unwrap();
10698        let tag = bare
10699            .flags
10700            .iter()
10701            .find(|(flag, _)| flag.name == "tag")
10702            .map(|(_, value)| value)
10703            .unwrap();
10704        assert!(matches!(tag, ParseValue::MultiString(values) if values == &[""]));
10705    }
10706
10707    #[test]
10708    fn test_default_missing_with_require_equals_refuses_the_following_word() {
10709        let spec = r#"
10710flag "--inspect <PORT>" require_equals=#true default_missing="9229"
10711arg "[rest]"
10712"#
10713        .parse::<Spec>()
10714        .unwrap();
10715
10716        let parsed = parse(&spec, &input(&["test", "--inspect"])).unwrap();
10717        assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
10718
10719        let parsed = parse(&spec, &input(&["test", "--inspect=1234"])).unwrap();
10720        assert_eq!(flag_string_value(&parsed, "inspect"), "1234");
10721
10722        // The following word is not the value; the missing value is, and 80 is a positional.
10723        let parsed = parse(&spec, &input(&["test", "--inspect", "80"])).unwrap();
10724        assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
10725        assert_eq!(
10726            parsed
10727                .args
10728                .values()
10729                .next()
10730                .map(|v| v.to_string())
10731                .as_deref(),
10732            Some("80")
10733        );
10734
10735        let parsed = parse(&spec, &input(&["test", "--inspect="])).unwrap();
10736        assert_eq!(flag_string_value(&parsed, "inspect"), "");
10737    }
10738
10739    #[test]
10740    fn test_default_missing_must_be_a_choice() {
10741        let spec = r#"
10742flag "--color <WHEN>" default_missing="always" {
10743    choices "auto" "always" "never"
10744}
10745"#
10746        .parse::<Spec>()
10747        .unwrap();
10748
10749        let parsed = parse(&spec, &input(&["test", "--color"])).unwrap();
10750        assert_eq!(flag_string_value(&parsed, "color"), "always");
10751
10752        let parsed = parse(&spec, &input(&["test", "--color=never"])).unwrap();
10753        assert_eq!(flag_string_value(&parsed, "color"), "never");
10754
10755        let spec = r#"
10756flag "--color <WHEN>" default_missing="wat" {
10757    choices "auto" "always" "never"
10758}
10759"#
10760        .parse::<Spec>()
10761        .unwrap();
10762
10763        let err = parse(&spec, &input(&["test", "--color"])).unwrap_err();
10764        let msg = format!("{err}");
10765        assert!(
10766            msg.contains("Invalid choice for option color: wat"),
10767            "missing default has to pass choices the same way a typed value does: {msg}"
10768        );
10769
10770        let err = parse(&spec, &input(&["test", "--color=wat"])).unwrap_err();
10771        let msg = format!("{err}");
10772        assert!(
10773            msg.contains("Invalid choice for option color: wat"),
10774            "an attached value that is not a choice is still refused: {msg}"
10775        );
10776
10777        let spec = r#"
10778flag "--inspect <PORT>" require_equals=#true default_missing="wat" {
10779    choices "9229" "80"
10780}
10781arg "[rest]"
10782"#
10783        .parse::<Spec>()
10784        .unwrap();
10785
10786        let err = parse(&spec, &input(&["test", "--inspect", "80"])).unwrap_err();
10787        let msg = format!("{err}");
10788        assert!(
10789            msg.contains("Invalid choice for option inspect: wat"),
10790            "require_equals still binds the missing string, so the error is the choice: {msg}"
10791        );
10792    }
10793
10794    #[test]
10795    fn test_hyphen_values_still_start_short_flag_parsing() {
10796        let spec = r#"
10797flag "-d --working-dir <DIR>"
10798flag "-a --args <ARGS>"
10799"#
10800        .parse::<Spec>()
10801        .unwrap();
10802
10803        let err = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap_err();
10804        let message = err.to_string();
10805        assert!(
10806            message.contains("--args") && message.contains("requires an argument"),
10807            "the recognized -d must leave the earlier -a missing: {message}"
10808        );
10809    }
10810
10811    /// `available_flags` has to agree with what an actual parse accepts, since
10812    /// its whole reason to exist is answering that question without one.
10813    mod available_flags {
10814        use super::*;
10815
10816        fn spec() -> Spec {
10817            r#"
10818bin "test"
10819flag "-v --verbose" global=#true
10820flag "--raw" global=#true effect="write"
10821flag "--local-only"
10822cmd "run" {
10823    flag "-r --raw"
10824    flag "-w --watch"
10825    cmd "once"
10826}
10827"#
10828            .parse::<Spec>()
10829            .unwrap()
10830        }
10831
10832        fn chain<'a>(spec: &'a Spec, path: &[&str]) -> Vec<&'a SpecCommand> {
10833            let mut chain = vec![&spec.cmd];
10834            for segment in path {
10835                chain.push(chain.last().unwrap().find_subcommand(segment).unwrap());
10836            }
10837            chain
10838        }
10839
10840        fn names(spec: &Spec, path: &[&str]) -> Vec<String> {
10841            let mut names: Vec<_> = available_flags(&chain(spec, path))
10842                .iter()
10843                .map(|f| f.name.clone())
10844                .collect();
10845            names.sort();
10846            names
10847        }
10848
10849        #[test]
10850        fn an_empty_chain_yields_nothing() {
10851            assert!(available_flags(&[]).is_empty());
10852        }
10853
10854        #[test]
10855        fn the_root_gets_its_own_flags() {
10856            let spec = spec();
10857            assert_eq!(names(&spec, &[]), ["local-only", "raw", "verbose"]);
10858        }
10859
10860        #[test]
10861        fn a_subcommand_keeps_globals_and_drops_local_only_ancestors() {
10862            let spec = spec();
10863            assert_eq!(names(&spec, &["run"]), ["raw", "verbose", "watch"]);
10864        }
10865
10866        #[test]
10867        fn a_re_declared_global_is_listed_once() {
10868            // The merge can leave the long key on the merged flag and the short
10869            // key on the pre-merge one. Same flag; it must not be listed twice.
10870            let spec = r#"
10871bin "test"
10872flag "-y --yes" global=#true effect="write"
10873cmd "rm" {
10874    flag "-y --yes"
10875}
10876"#
10877            .parse::<Spec>()
10878            .unwrap();
10879            let flags = available_flags(&chain(&spec, &["rm"]));
10880            assert_eq!(flags.len(), 1, "{flags:?}");
10881            assert_eq!(flags[0].effect.map(|e| e.as_str()), Some("write"));
10882        }
10883
10884        #[test]
10885        fn a_re_declared_global_keeps_the_globals_declaration() {
10886            // `run` re-declares the long-only global `--raw` as `-r --raw`
10887            // without `global`. That is the same flag: the global's `effect`
10888            // survives, the orphan short is unioned in, and it stays global.
10889            let spec = spec();
10890            let flags = available_flags(&chain(&spec, &["run"]));
10891            let raw = flags.iter().find(|f| f.name == "raw").unwrap();
10892            assert!(raw.global);
10893            assert_eq!(raw.effect.map(|e| e.as_str()), Some("write"));
10894            assert_eq!(raw.short, ['r']);
10895        }
10896
10897        #[test]
10898        fn it_matches_what_a_parse_accepts() {
10899            // The invariant. If these ever disagree, one of them is lying to a
10900            // caller about which flags a command takes.
10901            let spec = spec();
10902            for path in [vec![], vec!["run"], vec!["run", "once"]] {
10903                let argv = std::iter::once("test".to_string())
10904                    .chain(path.iter().map(|s| s.to_string()))
10905                    .collect::<Vec<_>>();
10906                let parsed = parse_partial(&spec, &argv).unwrap();
10907
10908                let mut from_parse: Vec<_> = unique_flags(parsed.available_flags.values())
10909                    .map(|f| f.name.clone())
10910                    .collect();
10911                from_parse.sort();
10912                assert_eq!(names(&spec, &path), from_parse, "path {path:?}");
10913            }
10914        }
10915    }
10916
10917    // Provenance: which token bound what, and where a value came from when no token did.
10918
10919    /// Every role a token was given, rendered the way `Debug` renders it, so a test can
10920    /// assert on the whole picture rather than on one field at a time.
10921    fn roles(parsed: &ParseOutput, index: usize) -> Vec<String> {
10922        parsed
10923            .tokens
10924            .iter()
10925            .find(|token| token.index == index)
10926            .unwrap_or_else(|| panic!("no token at {index}"))
10927            .roles
10928            .iter()
10929            .map(render_role)
10930            .collect()
10931    }
10932
10933    fn origins(parsed: &ParseOutput, flag: &str) -> Vec<ValueOrigin> {
10934        parsed
10935            .flag_origins
10936            .iter()
10937            .find(|(f, _)| f.name == flag)
10938            .map(|(_, origins)| origins.clone())
10939            .unwrap_or_default()
10940    }
10941
10942    fn explain_with_env(spec: &Spec, words: &[&str], env: &[(&str, &str)]) -> ParseOutput {
10943        let env = env
10944            .iter()
10945            .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
10946            .collect();
10947        Parser::new(spec)
10948            .with_env(env)
10949            .explain(&input(words))
10950            .unwrap()
10951    }
10952
10953    fn explain(spec: &Spec, words: &[&str]) -> ParseOutput {
10954        explain_with_env(spec, words, &[])
10955    }
10956
10957    #[test]
10958    fn an_attached_long_value_is_recorded_on_the_flag_token() {
10959        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\n"
10960            .parse()
10961            .unwrap();
10962
10963        let parsed = explain(&spec, &["ex", "--env=prod"]);
10964
10965        assert_eq!(roles(&parsed, 0), ["program"]);
10966        assert_eq!(
10967            roles(&parsed, 1),
10968            ["flag env as --env", "value of env = [\"prod\"], attached"]
10969        );
10970        // This is jdx/mise discussion #8883: a hand-written scanner dropped the attached
10971        // form while the detached one worked, and nothing could show the difference.
10972        assert!(origins(&parsed, "env").is_empty(), "typed, so no fallback");
10973    }
10974
10975    #[test]
10976    fn a_detached_long_value_is_recorded_on_its_own_token() {
10977        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\n"
10978            .parse()
10979            .unwrap();
10980
10981        let parsed = explain(&spec, &["ex", "--env", "prod"]);
10982
10983        assert_eq!(roles(&parsed, 1), ["flag env as --env"]);
10984        assert_eq!(roles(&parsed, 2), ["value of env = [\"prod\"]"]);
10985    }
10986
10987    #[test]
10988    fn a_short_bundle_is_attributed_to_the_bundle_token() {
10989        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-a\"\nflag \"-b\"\nflag \"-j <n>\"\n"
10990            .parse()
10991            .unwrap();
10992
10993        let parsed = explain(&spec, &["ex", "-abj8"]);
10994
10995        // One word the caller wrote, four things it did — and the re-queued tails are
10996        // folded back onto it rather than appearing as tokens nobody typed.
10997        assert_eq!(
10998            roles(&parsed, 1),
10999            [
11000                "flag a as -a",
11001                "flag b as -b",
11002                "flag j as -j",
11003                "value of j = [\"8\"], attached",
11004            ]
11005        );
11006        assert_eq!(parsed.tokens.len(), 2);
11007    }
11008
11009    #[test]
11010    fn a_delimiter_splits_one_token_into_several_values() {
11011        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--tags <tags>...\" delimiter=\",\"\n"
11012            .parse()
11013            .unwrap();
11014
11015        let parsed = explain(&spec, &["ex", "--tags", "a,b,c"]);
11016
11017        assert_eq!(
11018            roles(&parsed, 2),
11019            ["value of tags = [\"a\", \"b\", \"c\"]"],
11020            "the values meant, not the word typed"
11021        );
11022    }
11023
11024    #[test]
11025    fn a_separator_and_the_words_after_it_are_distinguished() {
11026        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"<src>\"\narg \"[raw]...\"\n"
11027            .parse()
11028            .unwrap();
11029
11030        let parsed = explain(&spec, &["ex", "a", "--", "-x"]);
11031
11032        assert_eq!(roles(&parsed, 1), ["arg src = [\"a\"]"]);
11033        assert_eq!(roles(&parsed, 2), ["separator"]);
11034        // Past the separator `-x` is data, not an unknown flag.
11035        assert_eq!(roles(&parsed, 3), ["arg raw = [\"-x\"]"]);
11036    }
11037
11038    #[test]
11039    fn a_second_separator_is_data() {
11040        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[raw]...\"\n"
11041            .parse()
11042            .unwrap();
11043
11044        let parsed = explain(&spec, &["ex", "--", "a", "--", "b"]);
11045
11046        assert_eq!(roles(&parsed, 1), ["separator"]);
11047        assert_eq!(roles(&parsed, 3), ["arg raw = [\"--\"]"]);
11048    }
11049
11050    #[test]
11051    fn an_unknown_flag_says_what_took_it() {
11052        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[rest]...\"\n"
11053            .parse()
11054            .unwrap();
11055
11056        let parsed = explain(&spec, &["ex", "--wat"]);
11057
11058        // The default is lax, so the word became data. Which is the useful thing to be
11059        // told: the alternative reading is "you have a typo".
11060        assert_eq!(roles(&parsed, 1), ["unknown flag, bound as rest"]);
11061    }
11062
11063    #[test]
11064    fn a_subcommand_word_is_not_a_positional() {
11065        let spec: Spec = "name \"ex\"\nbin \"ex\"\ncmd \"build\" {\n    arg \"<target>\"\n}\n"
11066            .parse()
11067            .unwrap();
11068
11069        let parsed = explain(&spec, &["ex", "build", "a"]);
11070
11071        assert_eq!(roles(&parsed, 1), ["subcommand build"]);
11072        assert_eq!(roles(&parsed, 2), ["arg target = [\"a\"]"]);
11073    }
11074
11075    #[test]
11076    fn a_multicall_applet_is_read_at_argv0() {
11077        let spec: Spec =
11078            "name \"box\"\nbin \"box\"\nmulticall #true\ncmd \"ls\" {\n    flag \"-l\"\n}\n"
11079                .parse()
11080                .unwrap();
11081
11082        let parsed = explain(&spec, &["/usr/bin/ls", "-l"]);
11083
11084        // argv[0] is both the program and the word that selected the applet, and the word
11085        // read there is not the word the caller wrote.
11086        assert_eq!(roles(&parsed, 0), ["program", "subcommand ls"]);
11087        assert!(parsed.tokens[0].synthesized);
11088        assert_eq!(parsed.tokens[0].word, "/usr/bin/ls");
11089    }
11090
11091    #[test]
11092    fn words_the_parse_never_reached_say_so() {
11093        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[rest]...\"\n"
11094            .parse()
11095            .unwrap();
11096
11097        let parsed = Parser::new(&spec)
11098            .explain(&input(&["ex", "--help", "a"]))
11099            .unwrap();
11100
11101        assert_eq!(roles(&parsed, 2), ["unread"]);
11102    }
11103
11104    #[test]
11105    fn an_env_origin_names_the_variable_that_fired() {
11106        let spec: Spec =
11107            "name \"ex\"\nbin \"ex\"\nflag \"--token <t>\" env=\"EX_TOKEN\" env_fallback=\"EX_TOKEN_OLD\"\n"
11108                .parse()
11109                .unwrap();
11110
11111        let primary = explain_with_env(&spec, &["ex"], &[("EX_TOKEN", "a")]);
11112        assert_eq!(
11113            origins(&primary, "token"),
11114            [ValueOrigin::Env("EX_TOKEN".to_string())]
11115        );
11116
11117        // The fallback firing is a different fact from the primary firing, and which one it
11118        // was is what says which declaration to delete.
11119        let fallback = explain_with_env(&spec, &["ex"], &[("EX_TOKEN_OLD", "b")]);
11120        assert_eq!(
11121            origins(&fallback, "token"),
11122            [ValueOrigin::Env("EX_TOKEN_OLD".to_string())]
11123        );
11124    }
11125
11126    #[test]
11127    fn a_default_origin_is_recorded_for_flags_and_args() {
11128        let spec: Spec =
11129            "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" default=\"auto\"\narg \"[src]\" default=\".\"\n"
11130                .parse()
11131                .unwrap();
11132
11133        let parsed = explain(&spec, &["ex"]);
11134
11135        assert_eq!(origins(&parsed, "color"), [ValueOrigin::Default]);
11136        let (arg, origins) = parsed.arg_origins.iter().next().unwrap();
11137        assert_eq!(arg.name, "src");
11138        assert_eq!(origins, &[ValueOrigin::Default]);
11139    }
11140
11141    #[test]
11142    fn a_default_if_origin_carries_the_condition_that_fired() {
11143        let spec: Spec = r#"
11144name "ex"
11145bin "ex"
11146flag "--profile <p>"
11147flag "--strict" {
11148    default_if "--profile" "prod" "true"
11149}
11150        "#
11151        .parse()
11152        .unwrap();
11153
11154        let parsed = explain(&spec, &["ex", "--profile", "prod"]);
11155
11156        // The selector alone is ambiguous: several conditions may name it with different
11157        // `when` values, so the report has to say which one matched.
11158        assert_eq!(
11159            origins(&parsed, "strict"),
11160            [ValueOrigin::DefaultIf {
11161                selector: "--profile".to_string(),
11162                when: Some("prod".to_string()),
11163            }]
11164        );
11165    }
11166
11167    #[test]
11168    fn a_bare_optional_value_flag_records_default_missing() {
11169        let spec: Spec =
11170            "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" default_missing=\"always\"\nflag \"-v\"\n"
11171                .parse()
11172                .unwrap();
11173
11174        let parsed = explain(&spec, &["ex", "--color", "-v"]);
11175
11176        // The flag was typed and the value was not, which is the distinction a spec author
11177        // is asking about when they ask why `--color` came out `always`.
11178        assert_eq!(roles(&parsed, 1), ["flag color as --color"]);
11179        assert_eq!(origins(&parsed, "color"), [ValueOrigin::DefaultMissing]);
11180        assert_eq!(roles(&parsed, 2), ["flag v as -v"]);
11181    }
11182
11183    #[test]
11184    fn a_var_flag_can_take_one_value_from_argv_and_one_from_default_missing() {
11185        let spec: Spec =
11186            "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" var=#true default_missing=\"always\"\n"
11187                .parse()
11188                .unwrap();
11189
11190        let parsed = explain(&spec, &["ex", "--color=red", "--color"]);
11191
11192        // Why origins are a list: one declaration, two occurrences, two different answers.
11193        assert_eq!(
11194            roles(&parsed, 1),
11195            [
11196                "flag color as --color",
11197                "value of color = [\"red\"], attached"
11198            ]
11199        );
11200        assert_eq!(origins(&parsed, "color"), [ValueOrigin::DefaultMissing]);
11201    }
11202
11203    #[test]
11204    fn an_override_names_the_flag_that_did_it() {
11205        let spec: Spec =
11206            "name \"ex\"\nbin \"ex\"\nflag \"--quiet\" default=\"true\"\nflag \"--loud\" overrides=\"--quiet\"\n"
11207                .parse()
11208                .unwrap();
11209
11210        let parsed = explain(&spec, &["ex", "--loud"]);
11211
11212        // Without the overriding name, "`--quiet` is unset despite its default" has no
11213        // answer: the fallback phase silently declines to fill an overridden flag.
11214        assert_eq!(parsed.overridden_flags.get("quiet").unwrap(), "loud");
11215        assert!(origins(&parsed, "quiet").is_empty());
11216    }
11217
11218    #[test]
11219    fn a_restart_token_leaves_the_tokens_and_clears_the_arg_origins() {
11220        let spec: Spec = r#"
11221name "ex"
11222bin "ex"
11223cmd "run" restart_token=":::" {
11224    arg "<task>" default="build"
11225}
11226        "#
11227        .parse()
11228        .unwrap();
11229
11230        let parsed = explain(&spec, &["ex", "run", "lint", ":::", "test"]);
11231
11232        // The values belong to the last invocation, so provenance must too — but the words
11233        // of the first were still read, and a report that dropped them would show a command
11234        // line with a hole in it.
11235        assert_eq!(roles(&parsed, 2), ["arg task = [\"lint\"]"]);
11236        // And the token that did the resetting says so: without a role of its own it reads
11237        // as a word that did nothing, next to a `lint` that filled an arg now empty.
11238        assert_eq!(roles(&parsed, 3), ["restart"]);
11239        assert_eq!(roles(&parsed, 4), ["arg task = [\"test\"]"]);
11240        assert!(parsed.arg_origins.is_empty());
11241    }
11242
11243    #[test]
11244    fn a_value_terminator_says_which_run_it_ended() {
11245        let spec: Spec = r#"
11246name "ex"
11247bin "ex"
11248flag "--exec <cmd>..." value_terminator=";"
11249arg "<src>"
11250        "#
11251        .parse()
11252        .unwrap();
11253
11254        let parsed = explain(&spec, &["ex", "--exec", "rm", "tmp", ";", "a"]);
11255
11256        assert_eq!(roles(&parsed, 3), ["value of exec = [\"tmp\"]"]);
11257        // The terminator is consumed and is not one of the values, which is the whole reason
11258        // it was declared — so it needs a row saying that rather than an empty one.
11259        assert_eq!(roles(&parsed, 4), ["value terminator, ends exec"]);
11260        assert_eq!(roles(&parsed, 5), ["arg src = [\"a\"]"]);
11261    }
11262
11263    #[test]
11264    fn an_args_value_terminator_says_which_run_it_ended() {
11265        let spec: Spec = r#"
11266name "ex"
11267bin "ex"
11268arg "<files>..." value_terminator=";"
11269arg "[dest]"
11270        "#
11271        .parse()
11272        .unwrap();
11273
11274        let parsed = explain(&spec, &["ex", "a", "b", ";", "out"]);
11275
11276        assert_eq!(roles(&parsed, 2), ["arg files = [\"b\"]"]);
11277        assert_eq!(roles(&parsed, 3), ["value terminator, ends files"]);
11278        assert_eq!(roles(&parsed, 4), ["arg dest = [\"out\"]"]);
11279    }
11280
11281    #[test]
11282    fn explain_keeps_the_bindings_of_a_command_line_that_fails() {
11283        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\narg \"<src>\"\n"
11284            .parse()
11285            .unwrap();
11286
11287        let parsed = Parser::new(&spec)
11288            .explain(&input(&["ex", "--env=prod"]))
11289            .unwrap();
11290
11291        // `parse` reports "missing required <src>" and nothing else, which is the report the
11292        // caller already had. This is the case the whole thing exists for.
11293        assert!(Parser::new(&spec)
11294            .parse(&input(&["ex", "--env=prod"]))
11295            .is_err());
11296        assert_eq!(
11297            roles(&parsed, 1),
11298            ["flag env as --env", "value of env = [\"prod\"], attached"]
11299        );
11300        assert!(
11301            parsed.errors.iter().any(|e| e.to_string().contains("src")),
11302            "{:?}",
11303            parsed.errors
11304        );
11305    }
11306
11307    #[test]
11308    fn an_external_subcommand_forwards_whole_tokens() {
11309        let spec: Spec = "name \"ex\"\nbin \"ex\"\nexternal_subcommand #true\ncmd \"build\"\n"
11310            .parse()
11311            .unwrap();
11312
11313        let parsed = explain(&spec, &["ex", "deploy", "--now"]);
11314
11315        assert_eq!(roles(&parsed, 1), ["external"]);
11316        assert_eq!(roles(&parsed, 2), ["external"]);
11317    }
11318
11319    #[test]
11320    fn a_view_keeps_the_callers_argv_positions() {
11321        let spec: Spec = r#"
11322bin "ex"
11323view "runner" root="run"
11324cmd "run" {
11325    flag "--token <token>"
11326}
11327        "#
11328        .parse()
11329        .unwrap();
11330
11331        let parsed = explain(&spec, &["runner", "--token", "secret"]);
11332
11333        // A view re-enters the parse with the same argv, so the positions still mean what
11334        // the caller wrote.
11335        assert_eq!(roles(&parsed, 0), ["program"]);
11336        assert_eq!(roles(&parsed, 2), ["value of token = [\"secret\"]"]);
11337    }
11338
11339    #[test]
11340    fn empty_default_does_not_hide_a_missing_parent_flag_value() {
11341        let spec: Spec = r#"
11342name "ex"
11343bin "ex"
11344default_subcommand "run"
11345default_subcommand_on_empty #true
11346flag "--value <value>"
11347cmd "run" arg_required_else_help=#true
11348"#
11349        .parse()
11350        .unwrap();
11351        let err = Parser::new(&spec)
11352            .parse(&input(&["ex", "--value"]))
11353            .unwrap_err();
11354        assert!(err.to_string().contains("requires an argument"), "{err}");
11355        assert!(!err.to_string().contains("Usage: ex run"), "{err}");
11356    }
11357
11358    #[test]
11359    fn empty_default_honors_parent_args_conflict_policy() {
11360        let spec: Spec = r#"
11361name "ex"
11362bin "ex"
11363default_subcommand "run"
11364default_subcommand_on_empty #true
11365args_conflicts_with_subcommands #true
11366flag "--verbose"
11367cmd "run"
11368"#
11369        .parse()
11370        .unwrap();
11371        let err = Parser::new(&spec)
11372            .parse(&input(&["ex", "--verbose"]))
11373            .unwrap_err();
11374        assert!(
11375            err.to_string().contains("cannot be used with arguments"),
11376            "{err}"
11377        );
11378    }
11379
11380    #[test]
11381    fn empty_default_mount_matches_explicit_selection() {
11382        let spec: Spec = r#"
11383name "ex"
11384bin "ex"
11385default_subcommand "run"
11386default_subcommand_on_empty #true
11387cmd "run" { mount run="discover" }
11388"#
11389        .parse()
11390        .unwrap();
11391        let mut mounts = HashMap::new();
11392        mounts.insert(
11393            "discover".to_string(),
11394            "name \"run\"\nbin \"run\"\nflag \"--mounted <mounted>\" default=\"yes\"\n".to_string(),
11395        );
11396        for argv in [
11397            vec!["ex".to_string()],
11398            vec!["ex".to_string(), "run".to_string()],
11399        ] {
11400            let out = Parser::new(&spec)
11401                .with_mount_outputs(mounts.clone())
11402                .parse(&argv)
11403                .unwrap();
11404            assert_eq!(out.cmds.last().unwrap().name, "run");
11405            assert_eq!(out.flags.values().next().unwrap().to_string(), "yes");
11406        }
11407    }
11408
11409    #[test]
11410    fn pending_parent_value_stays_pending_in_partial_parse() {
11411        let spec: Spec = r#"
11412name "ex"
11413bin "ex"
11414default_subcommand "run"
11415default_subcommand_on_empty #true
11416flag "--value <value>"
11417cmd "run" {}
11418"#
11419        .parse()
11420        .unwrap();
11421        let out = parse_partial(&spec, &input(&["ex", "--value"])).unwrap();
11422        assert_eq!(out.cmd.name, "ex");
11423        assert_eq!(out.flag_awaiting_value[0].name, "value");
11424    }
11425
11426    #[test]
11427    fn optional_default_missing_still_selects_empty_child() {
11428        let spec: Spec = r#"
11429name "ex"
11430bin "ex"
11431default_subcommand "run"
11432default_subcommand_on_empty #true
11433flag "--value <value>" default_missing="fallback"
11434cmd "run" {}
11435"#
11436        .parse()
11437        .unwrap();
11438        let out = Parser::new(&spec)
11439            .parse(&input(&["ex", "--value"]))
11440            .unwrap();
11441        assert_eq!(out.cmd.name, "run");
11442        assert_eq!(out.flags.values().next().unwrap().to_string(), "fallback");
11443    }
11444
11445    #[test]
11446    fn empty_default_resets_cursor_after_root_sigil() {
11447        let spec: Spec = r#"
11448name "ex"
11449bin "ex"
11450default_subcommand "run"
11451default_subcommand_on_empty #true
11452arg "[tool]..." sigil="@"
11453cmd "run" { arg "[path]" }
11454"#
11455        .parse()
11456        .unwrap();
11457        let out = parse_partial(&spec, &input(&["ex"])).unwrap();
11458        assert_eq!(out.cmd.name, "run");
11459        assert_eq!(out.next_arg.as_ref().unwrap().name, "path");
11460    }
11461
11462    #[test]
11463    fn root_clause_positional_prevents_empty_default() {
11464        let spec: Spec = r#"
11465name "ex"
11466bin "ex"
11467default_subcommand "run"
11468default_subcommand_on_empty #true
11469clause "tasks" {
11470  arg "<number>" allow_negative_numbers=#true
11471}
11472cmd "run" { arg "[task]" }
11473"#
11474        .parse()
11475        .unwrap();
11476        let out = Parser::new(&spec).parse(&input(&["ex", "-7"])).unwrap();
11477        assert!(
11478            out.cmds.iter().all(|cmd| cmd.name != "run"),
11479            "cmds={:?} args={:?}",
11480            out.cmds.iter().map(|c| &c.name).collect::<Vec<_>>(),
11481            out.args
11482        );
11483        let number = spec.cmd.clause.as_ref().unwrap().args.first().unwrap();
11484        assert_eq!(
11485            out.clauses["tasks"][0].get(number).unwrap().to_string(),
11486            "-7"
11487        );
11488    }
11489}