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    run_commands: bool,
691}
692
693impl<'a> Parser<'a> {
694    /// Create a new parser for the given spec.
695    pub fn new(spec: &'a Spec) -> Self {
696        Self {
697            spec,
698            env: None,
699            mount_outputs: None,
700            run_commands: true,
701        }
702    }
703
704    /// Use a custom environment variable map instead of the process environment.
705    ///
706    /// This is useful when parsing for tasks in a monorepo where the env vars
707    /// come from a child config file rather than the current process environment.
708    pub fn with_env(mut self, env: HashMap<String, String>) -> Self {
709        self.env = Some(env);
710        self
711    }
712
713    /// Inject deterministic outputs for mount commands instead of executing them.
714    ///
715    /// Keys are the exact `run` strings declared by mount nodes and values are the
716    /// usage specs those commands would print. When this is set, every encountered
717    /// mount must have an entry. Production parsing remains process-backed unless a
718    /// caller explicitly opts into injection.
719    pub fn with_mount_outputs(mut self, outputs: HashMap<String, String>) -> Self {
720        self.mount_outputs = Some(outputs);
721        self
722    }
723
724    /// Never run a `choices run=` command while parsing.
725    ///
726    /// For a caller that inspects a command line rather than acting on it. A value that the
727    /// declared choices do not accept is let through unchecked instead of being checked
728    /// against the command's output, and a help page describes the command rather than
729    /// listing what it prints. Mounts are unaffected; see [`Parser::with_mount_outputs`].
730    pub fn without_running_commands(mut self) -> Self {
731        self.run_commands = false;
732        self
733    }
734
735    /// Parse the input arguments.
736    ///
737    /// Returns the parsed arguments and flags, with defaults and env vars applied.
738    pub fn parse(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
739        let out = self.parse_collecting(input)?;
740        if let Some(err) = out
741            .errors
742            .iter()
743            .find(|e| matches!(e, UsageErr::Help(_) | UsageErr::Version(_)))
744        {
745            bail!("{err}");
746        }
747        if !out.errors.is_empty() {
748            bail!("{}", out.errors.iter().map(|e| e.to_string()).join("\n"));
749        }
750        Ok(out)
751    }
752
753    /// Everything the parse learned, whether or not it succeeded.
754    ///
755    /// [`Parser::parse`] wants the first error and nothing else, which is right for a
756    /// caller about to act on a command line. A caller that wants to *explain* one wants
757    /// the opposite: the bindings that worked and every complaint about the rest, since a
758    /// report saying only "missing required <src>" is the report you already had.
759    ///
760    /// Failures that stop the parse dead — a mount that will not run, a word no
761    /// declaration can take — still come back as `Err`. There is no output to describe in
762    /// those cases; see [`Parser::explain`] for what to do about it.
763    pub fn explain(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
764        self.parse_collecting(input)
765    }
766
767    /// The binding phase's own answer for a line [`Parser::explain`] refused.
768    ///
769    /// `Ok` when the binding phase finished and the failure came after it — a flag left
770    /// waiting for a value, say. Everything argv supplied is there and only the
771    /// environment-and-defaults phase is missing.
772    ///
773    /// `Err` when the binding phase is where it died, leaving the tokens it had attributed
774    /// by then. Those words are most of what a report is for: "no declaration takes `bogus`"
775    /// is more useful next to the three tokens that did bind than on its own. The word that
776    /// caused the failure carries a role saying so, and everything still queued behind it is
777    /// [`TokenRole::Unread`], for the two failures a command line reaches on its own — a
778    /// word nothing declares, and a flag a strict spec refuses. A failure in the spec rather
779    /// than in the line, such as a mount that will not run, stops the trace where it stopped
780    /// and the words past it carry no role.
781    pub fn explain_refused(self, input: &[String]) -> Result<ParseOutput, Vec<TokenBinding>> {
782        let _no_runs = (!self.run_commands).then(crate::spec::choices::NoRunScope::enter);
783        let mut trace = Trace::new(input);
784        match parse_partial_traced(
785            self.spec,
786            input,
787            self.env.as_ref(),
788            self.mount_outputs.as_ref(),
789            MountTiming::WhenAWordIsUnknown,
790            true,
791            &mut trace,
792        ) {
793            // A parse that got as far as stopping normally already moved its tokens onto the
794            // output, which is where a caller should read them from.
795            Ok((out, _)) => Ok(out),
796            Err(_) => Err(trace.tokens),
797        }
798    }
799
800    fn parse_collecting(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
801        // Held across both phases: defaults are checked against the same `choices run=`
802        // output the words were.
803        let _runs = crate::spec::choices::RunScope::enter();
804        let _no_runs = (!self.run_commands).then(crate::spec::choices::NoRunScope::enter);
805        let custom_env = self.env.as_ref();
806        let (mut out, overridden_flags) = parse_partial_with_env(
807            self.spec,
808            input,
809            custom_env,
810            self.mount_outputs.as_ref(),
811            MountTiming::WhenAWordIsUnknown,
812            false,
813        )?;
814        restore_current_clause(&mut out);
815        trace!("{out:?}");
816
817        // A flag still waiting for a value never got one, so the command line ended
818        // mid-flag. `parse_partial` leaves this for completions to look at — a
819        // half-typed `--jobs ` is exactly what a completion is asked about — but a
820        // full parse has nothing left to wait for, and dropping the flag silently
821        // made a forgotten value look like a working command.
822        while try_bind_default_missing(
823            &mut out.flags,
824            &mut out.flag_awaiting_value,
825            custom_env,
826            &mut out.flag_origins,
827        )? {}
828        if let Some(flag) = out.flag_awaiting_value.first() {
829            let token = flag
830                .long
831                .first()
832                .map(|l| format!("--{l}"))
833                .or_else(|| flag.short.first().map(|s| format!("-{s}")))
834                .unwrap_or_else(|| flag.name.clone());
835            let rendered = input.join(" ");
836            let span = rendered
837                .rfind(&token)
838                .map(|at| (at, token.len()))
839                .unwrap_or((0, 0));
840            return Err(UsageErr::InvalidFlag {
841                token,
842                reason: "requires an argument".to_string(),
843                span: span.into(),
844                input: rendered,
845            }
846            .into());
847        }
848
849        // Before the environment and defaults have their turn, because both mark a field as
850        // filled and only argv can be reported as something the user typed. Env is reported
851        // where it is applied, below; a default is nobody's request and reports nothing.
852        collect_deprecations(&mut out);
853
854        let get_env = |key: &str| -> Option<String> {
855            if let Some(env_map) = custom_env {
856                env_map.get(key).cloned()
857            } else {
858                std::env::var(key).ok()
859            }
860        };
861
862        // Apply env vars and defaults for args
863        //
864        // Not `skip(out.args.len())`: an explicit `--` can jump the parser's cursor past an arg
865        // that stayed empty, leaving a gap that makes the fill count a wrong starting offset.
866        for arg in active_args(&out.cmd) {
867            // Clause instances contain argv only: defaults and environment values do not
868            // manufacture fields inside a repeated group.
869            if out.cmd.clause.is_some() {
870                break;
871            }
872            if out.args.contains_key(arg) {
873                continue;
874            }
875            if let Some((env_name, env_value)) = first_set_env(arg.env_names(), &get_env) {
876                if arg_env_is_deprecated(arg, env_name) {
877                    out.warnings
878                        .push(Warning::env(env_name, arg_current_env(arg)));
879                }
880                let values = split_fallback_values(std::slice::from_ref(&env_value), arg.delimiter);
881                validate_choice_values(
882                    ChoiceTarget::arg(arg),
883                    &values,
884                    arg.choices.as_ref(),
885                    custom_env,
886                )?;
887                let parsed = if arg.var {
888                    validate_arg_fallback_count(arg, values.len(), &mut out.errors);
889                    ParseValue::MultiString(values)
890                } else {
891                    ParseValue::String(values.into_iter().next().unwrap_or_default())
892                };
893                out.args.insert(Arc::new(arg.clone()), parsed);
894                out.arg_origins
895                    .entry(Arc::new(arg.clone()))
896                    .or_default()
897                    .push(ValueOrigin::Env(env_name.to_string()));
898                continue;
899            }
900            if !arg.default.is_empty() {
901                // Consider var when deciding the type of default return value
902                if arg.var {
903                    let values = split_fallback_values(&arg.default, arg.delimiter);
904                    validate_arg_fallback_count(arg, values.len(), &mut out.errors);
905                    validate_choice_values(
906                        ChoiceTarget::arg(arg),
907                        &values,
908                        arg.choices.as_ref(),
909                        custom_env,
910                    )?;
911                    // For var=true, always return a vec (MultiString)
912                    out.args
913                        .insert(Arc::new(arg.clone()), ParseValue::MultiString(values));
914                    out.arg_origins
915                        .entry(Arc::new(arg.clone()))
916                        .or_default()
917                        .push(ValueOrigin::Default);
918                } else {
919                    validate_choice_value(
920                        ChoiceTarget::arg(arg),
921                        &arg.default[0],
922                        arg.choices.as_ref(),
923                        custom_env,
924                    )?;
925                    // For var=false, return the first default value as String
926                    out.args.insert(
927                        Arc::new(arg.clone()),
928                        ParseValue::String(arg.default[0].clone()),
929                    );
930                    out.arg_origins
931                        .entry(Arc::new(arg.clone()))
932                        .or_default()
933                        .push(ValueOrigin::Default);
934                }
935            }
936        }
937
938        // Environment first, for every flag, so a `default_if` can see a sibling
939        // that was filled from env. Applying both in one pass would make the
940        // answer depend on declaration order: `--bin-names` before `--json`
941        // would miss `EX_JSON=1`.
942        let flags: Vec<Arc<SpecFlag>> = out
943            .available_flags
944            .values()
945            .filter(|flag| !is_clause_scoped_flag(&out, flag))
946            .cloned()
947            .collect();
948        for flag in &flags {
949            if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
950                continue;
951            }
952            if let Some((env_name, env_value)) = first_set_env(flag.env_names(), &get_env) {
953                // The flag's own deprecation before the alias's, which is the order the
954                // compiled parser reports them in: it walks a command's flags and then its
955                // aliases. Using a deprecated flag through a variable is still using it.
956                if let Some(warning) = flag_deprecation(flag) {
957                    out.warnings.push(warning);
958                }
959                if flag_env_is_deprecated(flag, env_name) {
960                    out.warnings
961                        .push(Warning::env(env_name, flag_current_env(flag)));
962                }
963                if let Some(arg) = flag.arg.as_ref() {
964                    let values =
965                        split_fallback_values(std::slice::from_ref(&env_value), arg.delimiter);
966                    validate_choice_values(
967                        ChoiceTarget::option(flag),
968                        &values,
969                        arg.choices.as_ref(),
970                        custom_env,
971                    )?;
972                    let parsed = if flag.var || arg.var {
973                        if flag.var {
974                            validate_flag_fallback_count(flag, values.len(), &mut out.errors);
975                        }
976                        if arg.var {
977                            validate_flag_arg_fallback_count(
978                                flag,
979                                arg,
980                                values.len(),
981                                &mut out.errors,
982                            );
983                        }
984                        ParseValue::MultiString(values)
985                    } else {
986                        ParseValue::String(values.into_iter().next().unwrap_or_default())
987                    };
988                    out.flags.insert(Arc::clone(flag), parsed);
989                } else {
990                    let is_true = matches!(env_value.as_str(), "1" | "true" | "True" | "TRUE");
991                    out.flags
992                        .insert(Arc::clone(flag), ParseValue::Bool(is_true));
993                }
994                out.flag_origins
995                    .entry(Arc::clone(flag))
996                    .or_default()
997                    .push(ValueOrigin::Env(env_name.to_string()));
998            }
999        }
1000        // Decide every `default_if` against argv+env only. Binding as we go would put
1001        // a default into `out.flags` and make the next flag's condition treat it as
1002        // explicit — Go's `Given()` and the derive's `__given_*` both ignore defaults
1003        // here, so an unconditional `default` on `--json` must not fire
1004        // `default_if "--json"`.
1005        let mut from_default_if: Vec<(Arc<SpecFlag>, crate::SpecDefaultIf)> = Vec::new();
1006        for flag in &flags {
1007            if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
1008                continue;
1009            }
1010            if let Some(condition) = flag.default_if.iter().find(|condition| {
1011                default_if_condition_matches(condition, &out, &overridden_flags, custom_env)
1012            }) {
1013                from_default_if.push((Arc::clone(flag), condition.clone()));
1014            }
1015        }
1016        for (flag, condition) in &from_default_if {
1017            // The whole condition, not just the value: several conditions may name the same
1018            // selector with different `when` values, so the selector alone does not say
1019            // which one fired.
1020            bind_flag_fallback(
1021                flag,
1022                std::slice::from_ref(&condition.value),
1023                &mut out,
1024                custom_env,
1025                ValueOrigin::DefaultIf {
1026                    selector: condition.selector.clone(),
1027                    when: condition.when.clone(),
1028                },
1029            )?;
1030        }
1031        for flag in &flags {
1032            if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
1033                continue;
1034            }
1035            if !flag.default.is_empty() {
1036                bind_flag_fallback(
1037                    flag,
1038                    &flag.default,
1039                    &mut out,
1040                    custom_env,
1041                    ValueOrigin::Default,
1042                )?;
1043                continue;
1044            }
1045            if let Some(arg) = flag.arg.as_ref()
1046                && !arg.default.is_empty()
1047            {
1048                bind_flag_fallback(
1049                    flag,
1050                    &arg.default,
1051                    &mut out,
1052                    custom_env,
1053                    ValueOrigin::Default,
1054                )?;
1055            }
1056        }
1057        // The binding phase leaves the last clause instance in `args`/`flags` so
1058        // completion can inspect it. A full parse closes it before applying scoped
1059        // fallbacks: defaults and environment values fill instances that argv made,
1060        // but never manufacture a repeated group on their own.
1061        finalize_current_clause(&mut out);
1062        let clause_explicit = apply_clause_flag_fallbacks(&mut out, &overridden_flags, custom_env)?;
1063        validate_clause_relationships(
1064            &mut out,
1065            &overridden_flags,
1066            custom_env,
1067            clause_explicit.as_deref(),
1068        );
1069        // Declarative value validation is deliberately post-binding. Defaults and
1070        // environment fallbacks have landed by here, and delimiters were already split
1071        // while binding. Like clap's value parsers, a declaration judges each resulting
1072        // raw value independently.
1073        for (arg, parsed) in &out.args {
1074            validate_expression(
1075                &arg.name,
1076                arg.validate.as_deref(),
1077                arg.validate_error.as_deref(),
1078                parsed,
1079                &mut out.errors,
1080            );
1081        }
1082        if let Some(clause) = &out.cmd.clause {
1083            let mut clause_errors = Vec::new();
1084            for (index, instance) in out
1085                .clauses
1086                .get(&clause.name)
1087                .into_iter()
1088                .flatten()
1089                .enumerate()
1090            {
1091                for arg in &clause.args {
1092                    let Some(value) = instance.get(arg) else {
1093                        if arg.required {
1094                            clause_errors.push(UsageErr::MissingClauseArg {
1095                                clause: clause.name.clone(),
1096                                instance: index + 1,
1097                                arg: arg.name.clone(),
1098                            });
1099                        }
1100                        continue;
1101                    };
1102                    if let (true, ParseValue::MultiString(values)) = (arg.var, value) {
1103                        if let Some(min) = arg.var_min
1104                            && values.len() < min
1105                        {
1106                            clause_errors.push(UsageErr::VarArgTooFew {
1107                                name: format!(
1108                                    "{} instance {}: {}",
1109                                    clause.name,
1110                                    index + 1,
1111                                    arg.name
1112                                ),
1113                                min,
1114                                got: values.len(),
1115                            });
1116                        }
1117                        if let Some(max) = arg.var_max
1118                            && values.len() > max
1119                        {
1120                            clause_errors.push(UsageErr::VarArgTooMany {
1121                                name: format!(
1122                                    "{} instance {}: {}",
1123                                    clause.name,
1124                                    index + 1,
1125                                    arg.name
1126                                ),
1127                                max,
1128                                got: values.len(),
1129                            });
1130                        }
1131                    }
1132                }
1133            }
1134            out.errors.extend(clause_errors);
1135        }
1136        let clause_flag_names = out
1137            .cmd
1138            .clause
1139            .iter()
1140            .flat_map(|clause| &clause.flags)
1141            .map(|flag| flag.name.as_str())
1142            .collect::<HashSet<_>>();
1143        for (flag, parsed) in out
1144            .flags
1145            .iter()
1146            .filter(|(flag, _)| !clause_flag_names.contains(flag.name.as_str()))
1147        {
1148            if let Some(arg) = &flag.arg {
1149                validate_expression(
1150                    &flag.name,
1151                    arg.validate.as_deref(),
1152                    arg.validate_error.as_deref(),
1153                    parsed,
1154                    &mut out.errors,
1155                );
1156            }
1157        }
1158        // Applied once, here, because this is where the CLI's own version is known: a
1159        // `deprecated_warn_at` the spec has not reached yet is an author saying *not yet*.
1160        crate::warn::retain_reached(&mut out.warnings, self.spec.version.as_deref());
1161        Ok(out)
1162    }
1163}
1164
1165/// Parse command-line arguments according to a spec.
1166///
1167/// Returns the parsed arguments and flags, with defaults and env vars applied.
1168/// Uses `std::env::var` for environment variable lookups.
1169///
1170/// For custom environment variable handling, use [`Parser`] instead.
1171#[must_use = "parsing result should be used"]
1172pub fn parse(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
1173    Parser::new(spec).parse(input)
1174}
1175
1176/// Parse command-line arguments without applying defaults.
1177///
1178/// Use this for help text generation or when you need the raw parsed values.
1179#[must_use = "parsing result should be used"]
1180pub fn parse_partial(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
1181    parse_partial_with_env(spec, input, None, None, MountTiming::Eager, true).map(|(out, _)| out)
1182}
1183
1184/// Basename of argv[0] for a multicall CLI: last path component, with a trailing
1185/// `.exe` stripped so Windows and Unix agree.
1186pub fn multicall_basename(argv0: &str) -> &str {
1187    let name = argv0.rsplit(['/', '\\']).next().unwrap_or(argv0);
1188    match name.get(name.len().saturating_sub(4)..) {
1189        Some(ext) if ext.eq_ignore_ascii_case(".exe") => &name[..name.len() - 4],
1190        _ => name,
1191    }
1192}
1193
1194/// The applet name to parse as the first word, when argv[0] is not the dispatcher.
1195///
1196/// `None` means a dispatcher invocation (`busybox ls`): skip argv[0] and parse the
1197/// rest. `Some` is a symlink invocation (`ls -l`): inject the basename.
1198pub fn multicall_applet<'a>(argv0: &'a str, name: &str, bin: Option<&str>) -> Option<&'a str> {
1199    let base = multicall_basename(argv0);
1200    if !name.is_empty() && base == multicall_basename(name) {
1201        return None;
1202    }
1203    if let Some(bin) = bin
1204        && !bin.is_empty()
1205        && base == multicall_basename(bin)
1206    {
1207        return None;
1208    }
1209    Some(base)
1210}
1211
1212/// Internal version of parse_partial that accepts an optional custom env map.
1213/// When a command's own `mount` runs, for the root — which nothing descends into.
1214///
1215/// A completion has to know every command before it can offer one, even with
1216/// nothing typed yet, so it resolves up front. An execution knows the word it was
1217/// given, so it only pays for discovery when that word matches nothing declared —
1218/// and a CLI that declares its commands and mounts a few more does not spawn a
1219/// process on every invocation.
1220#[derive(Clone, Copy, PartialEq, Eq)]
1221enum MountTiming {
1222    Eager,
1223    WhenAWordIsUnknown,
1224}
1225
1226/// One word on its way through the parser, with what the parser has learned about it.
1227///
1228/// This holds what a side queue used to: the flag Phase 1 read a word as, previously a
1229/// `VecDeque` popped in step with the words. Two queues staying aligned is an invariant
1230/// nothing checks, and it was delicate enough to need explaining at three call sites; on
1231/// the word itself there is nothing to keep aligned. The argv position is here for the
1232/// same reason: the queue is popped, re-queued, split on `=`, and has subcommand words
1233/// removed from the middle, so position in the queue stops meaning position in argv on the
1234/// first descent.
1235struct Token {
1236    word: String,
1237    /// Where in the caller's argv this word came from.
1238    ///
1239    /// A word the parser made up points at the token it was derived from — the tail of a
1240    /// short bundle at the bundle, a multicall applet name at argv[0] — because that is the
1241    /// token a reader would point at, and there is nothing else to point at.
1242    argv: usize,
1243    /// The flag Phase 1 read this word as, and the command level it read it at.
1244    ///
1245    /// `Some((flag, command_level))` for a flag word, `None` for its value, for anything
1246    /// unresolved, and for every word Phase 1 never reached. The words stay in the queue
1247    /// for Phase 2 to re-parse — that is how they reach `out.flags` and `as_env()` — but by
1248    /// then the recognized flags have changed, because each descent drops the parent's
1249    /// non-global flags and a mounted command may declare the same name as a global seen
1250    /// here. Recording the owner keeps a word bound to the flag it was read as.
1251    ///
1252    /// The level matters to strict parsing: clap permits an inherited global once on each
1253    /// side of a subcommand boundary.
1254    binding: Option<(Arc<SpecFlag>, usize)>,
1255}
1256
1257/// Flag scope for the one token straddling an implicit default-command boundary.
1258struct DefaultBundle {
1259    argv: usize,
1260    flags: BTreeMap<String, Arc<SpecFlag>>,
1261    parent_keys: HashSet<String>,
1262}
1263
1264impl DefaultBundle {
1265    /// Preserve the declaration and command level when a short tail is requeued.
1266    fn binding(&self, key: &str) -> Option<(Arc<SpecFlag>, usize)> {
1267        self.flags.get(key).map(|flag| {
1268            (
1269                Arc::clone(flag),
1270                usize::from(!self.parent_keys.contains(key)),
1271            )
1272        })
1273    }
1274}
1275
1276impl Token {
1277    fn new(word: String, argv: usize) -> Self {
1278        Self {
1279            word,
1280            argv,
1281            binding: None,
1282        }
1283    }
1284}
1285
1286/// The token trace, while it is being built.
1287///
1288/// One row per word of the caller's argv, so a role can be recorded against a position
1289/// without the recorder having to know how many words came before it. Words the parser
1290/// made up have no row of their own and fold onto the row they were derived from.
1291struct Trace {
1292    tokens: Vec<TokenBinding>,
1293}
1294
1295impl Trace {
1296    fn new(input: &[String]) -> Self {
1297        Self {
1298            tokens: input
1299                .iter()
1300                .enumerate()
1301                .map(|(index, word)| TokenBinding {
1302                    index,
1303                    word: word.clone(),
1304                    synthesized: false,
1305                    roles: vec![],
1306                })
1307                .collect(),
1308        }
1309    }
1310
1311    fn record(&mut self, argv: usize, role: TokenRole) {
1312        if let Some(token) = self.tokens.get_mut(argv) {
1313            token.roles.push(role);
1314        }
1315    }
1316
1317    /// Note that what was read at this position is not what the caller wrote there.
1318    fn note_synthesized(&mut self, argv: usize) {
1319        if let Some(token) = self.tokens.get_mut(argv) {
1320            token.synthesized = true;
1321        }
1322    }
1323
1324    /// Every word the parse never reached, once it has stopped.
1325    fn close(&mut self, unread: &VecDeque<Token>) {
1326        for token in unread {
1327            self.record(token.argv, TokenRole::Unread);
1328        }
1329    }
1330}
1331
1332fn parse_partial_with_env(
1333    spec: &Spec,
1334    input: &[String],
1335    custom_env: Option<&HashMap<String, String>>,
1336    mount_outputs: Option<&HashMap<String, String>>,
1337    mount_timing: MountTiming,
1338    validate_clauses: bool,
1339) -> Result<(ParseOutput, HashSet<String>), miette::Error> {
1340    let mut trace = Trace::new(input);
1341    parse_partial_traced(
1342        spec,
1343        input,
1344        custom_env,
1345        mount_outputs,
1346        mount_timing,
1347        validate_clauses,
1348        &mut trace,
1349    )
1350}
1351
1352/// Locate the first default-only flag, while preserving an explicit sibling selector.
1353/// This only reads declarations: it neither binds values nor executes mount commands.
1354fn default_flag_route(spec: &Spec, root: &SpecCommand, input: &VecDeque<Token>) -> Option<usize> {
1355    let default = root.find_subcommand(spec.default_subcommand.as_deref()?)?;
1356    let parent = gather_flags(root);
1357    let child = gather_flags(default);
1358    let path = std::slice::from_ref(root);
1359    let mut at = None;
1360    let mut i = 0;
1361    while let Some(token) = input.get(i).map(|t| t.word.as_str()) {
1362        let scope = if at.is_some() { default } else { root };
1363        if token == "--"
1364            || token == "-"
1365            || scope
1366                .clause
1367                .as_ref()
1368                .is_some_and(|c| c.separator.as_deref() == Some(token))
1369        {
1370            return at;
1371        }
1372        if !is_flag_like(token) {
1373            if at.is_none()
1374                && (root.find_subcommand(token).is_some()
1375                    || (token == "help"
1376                        && spec.disable_help != Some(true)
1377                        && !root.disable_help_subcommand))
1378            {
1379                return None;
1380            }
1381            return at;
1382        }
1383        let mut value_flag = None;
1384        let mut attached = None;
1385        if token.starts_with("--") {
1386            let (name, value) = token
1387                .split_once('=')
1388                .map_or((token, None), |(n, v)| (n, Some(v)));
1389            if !parent.contains_key(name)
1390                && (is_help_arg(spec, root, name) || is_version_arg(spec, path, name))
1391            {
1392                i += 1;
1393                continue;
1394            }
1395            let flag = parent.get(name).or_else(|| child.get(name));
1396            let Some(flag) = flag else {
1397                return at;
1398            };
1399            if !parent.contains_key(name) {
1400                at.get_or_insert(i);
1401            }
1402            if flag.arg.is_some() && flag.negate.as_deref() != Some(name) {
1403                value_flag = Some(flag);
1404                attached = value;
1405            }
1406        } else {
1407            for (offset, letter) in token.char_indices().skip(1) {
1408                let key = format!("-{letter}");
1409                if !parent.contains_key(&key) && supplies_short(spec, path, letter) {
1410                    continue;
1411                }
1412                let flag = parent.get(&key).or_else(|| child.get(&key));
1413                let Some(flag) = flag else {
1414                    return at;
1415                };
1416                if !parent.contains_key(&key) {
1417                    at.get_or_insert(i);
1418                }
1419                if flag.arg.is_some() {
1420                    value_flag = Some(flag);
1421                    let rest = &token[offset + letter.len_utf8()..];
1422                    attached = (!rest.is_empty()).then_some(rest.strip_prefix('=').unwrap_or(rest));
1423                    break;
1424                }
1425            }
1426        }
1427        i += 1;
1428        if let Some(flag) = value_flag {
1429            let scope = if at.is_some() { default } else { root };
1430            let is_separator = |next: &str| {
1431                scope
1432                    .clause
1433                    .as_ref()
1434                    .is_some_and(|c| c.separator.as_deref() == Some(next))
1435            };
1436            let first = if attached.is_some() {
1437                attached
1438            } else {
1439                input
1440                    .get(i)
1441                    .map(|t| t.word.as_str())
1442                    .filter(|next| !is_separator(next) && accepts_detached_flag_value(flag, next))
1443                    .inspect(|_| i += 1)
1444            };
1445            if first.is_none() && !flag.value_optional && flag.default_missing.is_none() {
1446                return at;
1447            }
1448            let arg = flag.arg.as_ref().expect("value flag");
1449            if arg.var {
1450                let count_values = |v: &str| arg.delimiter.map_or(1, |d| v.split(d).count());
1451                let mut count = first.map_or(0, count_values);
1452                while arg.var_max.is_none_or(|max| count < max) {
1453                    let Some(next) = input.get(i).map(|t| t.word.as_str()) else {
1454                        break;
1455                    };
1456                    if is_separator(next) {
1457                        break;
1458                    }
1459                    if next == "--" || arg.value_terminator.as_deref() == Some(next) {
1460                        if next != "--" {
1461                            i += 1;
1462                        }
1463                        break;
1464                    }
1465                    if is_flag_like(next)
1466                        && !(arg.allow_negative_numbers && is_negative_number(next))
1467                    {
1468                        break;
1469                    }
1470                    if root.subcommand_precedence_over_arg && root.find_subcommand(next).is_some() {
1471                        break;
1472                    }
1473                    count += count_values(next);
1474                    i += 1;
1475                }
1476            }
1477        }
1478    }
1479    at
1480}
1481
1482/// The binding phase, with the trace left somewhere the caller can still read it.
1483///
1484/// A failure this phase cannot continue past — a word no declaration can take, a flag a
1485/// strict spec refuses — leaves through `?`, and a trace owned by the loop goes with it. The
1486/// words read before the failure are most of what a report wants, so the caller owns the
1487/// trace instead and keeps them. See [`Parser::explain_refused`].
1488fn parse_partial_traced(
1489    spec: &Spec,
1490    input: &[String],
1491    custom_env: Option<&HashMap<String, String>>,
1492    mount_outputs: Option<&HashMap<String, String>>,
1493    mount_timing: MountTiming,
1494    validate_clauses: bool,
1495    trace: &mut Trace,
1496) -> Result<(ParseOutput, HashSet<String>), miette::Error> {
1497    // Each `choices run=` runs once per parse, however many values are checked against it.
1498    let _runs = crate::spec::choices::RunScope::enter();
1499    if let Some(view) = input.first().and_then(|argv0| spec.view_for_program(argv0)) {
1500        let viewed = spec.for_view(view)?;
1501        return parse_partial_traced(
1502            &viewed,
1503            input,
1504            custom_env,
1505            mount_outputs,
1506            mount_timing,
1507            validate_clauses,
1508            trace,
1509        );
1510    }
1511    trace!("parse_partial: {input:?}");
1512    let mut input = input
1513        .iter()
1514        .enumerate()
1515        .map(|(argv, word)| Token::new(word.clone(), argv))
1516        .collect::<VecDeque<_>>();
1517    let argv0 = input.pop_front();
1518    if let Some(argv0) = argv0.as_ref() {
1519        trace.record(argv0.argv, TokenRole::Program);
1520    }
1521    if spec.multicall
1522        && let Some(raw) = argv0
1523        && let Some(applet) = multicall_applet(&raw.word, &spec.name, Some(spec.bin.as_str()))
1524    {
1525        // A symlink invocation reads a word the caller never typed — the basename of
1526        // the program itself — so argv[0] is both the program and, below, whatever
1527        // that word selects.
1528        trace.note_synthesized(raw.argv);
1529        input.push_front(Token::new(applet.to_string(), raw.argv));
1530    }
1531    // The policy observes the selected command's own argv, not values eventually filled from
1532    // env/default. Start at the root, then reset on every explicit descent. A default
1533    // subcommand receives the unmatched word that selected it, so it is necessarily non-bare.
1534    let mut command_has_argv = !input.is_empty();
1535    // Unlike `out.args`, this survives clause finalization and restart rewinds. It records
1536    // whether the root actually consumed a positional token before empty-default routing.
1537    let mut root_positional_arg_found = false;
1538
1539    let mut out = ParseOutput {
1540        cmd: spec.cmd.clone(),
1541        cmds: vec![spec.cmd.clone()],
1542        args: IndexMap::new(),
1543        clauses: IndexMap::new(),
1544        clause_flags: IndexMap::new(),
1545        flags: IndexMap::new(),
1546        tokens: vec![],
1547        flag_origins: IndexMap::new(),
1548        arg_origins: IndexMap::new(),
1549        overridden_flags: BTreeMap::new(),
1550        available_flags: gather_flags(&spec.cmd),
1551        flag_awaiting_value: vec![],
1552        errors: vec![],
1553        warnings: vec![],
1554        next_arg: None,
1555        double_dash_seen: false,
1556        external: None,
1557    };
1558    // Keep this internal so adding relationship support remains semver-compatible. The full
1559    // parser uses it to prevent defaults and environment values from restoring overridden flags.
1560    let mut overridden_flags = HashSet::new();
1561    // Which spelling supplied each parsed flag. A child may re-declare one long form of an
1562    // inherited global while the merge keeps the ancestor's other aliases on the same `Arc`.
1563    // The declaration object alone then cannot answer whether `--clean` belonged to the child
1564    // or an inherited `-c` belonged to the ancestor.
1565    let mut parsed_flag_spellings: HashMap<usize, HashSet<String>> = HashMap::new();
1566
1567    // Phase 1: Scan for subcommands and collect global flags
1568    //
1569    // This phase identifies subcommands early because they may have mount points
1570    // that need to be executed with the global flags that appeared before them.
1571    //
1572    // Example: "usage --verbose run task"
1573    //   -> finds "run" subcommand, passes ["--verbose"] to its mount command
1574    //   -> then finds "task" as a subcommand of "run" (if it exists)
1575    //
1576    // We only collect global flags for mounts because:
1577    // - Non-global flags are specific to the current command, not subcommands
1578    // - Global flags affect all commands and should be passed to mount points
1579    let mut prefix_flags: Vec<(Arc<SpecFlag>, Vec<String>)> = vec![];
1580    // Which flag each word skipped here belongs to is recorded on the word — see
1581    // `Token::binding`.
1582    let mut command_arg_found = false;
1583    let mut variadic_flag_active = false;
1584    let mut idx = 0;
1585    // Track whether we've already applied the default_subcommand to prevent
1586    // multiple switches (e.g., if default is "run" and there's a task named "run")
1587    let mut used_default_subcommand = false;
1588    let mut default_bundle: Option<DefaultBundle> = None;
1589    // Whether the command in scope has had its own mounts run. A mount on the root
1590    // is the case that needs this: a subcommand's mounts are run when the parser
1591    // descends into it, but nothing descends into the root.
1592    let mut mounts_resolved = false;
1593    // A completion needs the whole command list before it can offer anything, and
1594    // `mycli <tab>` has no word to trigger discovery with — so waiting for one would
1595    // mean a root mount never contributed to the very thing it exists for.
1596    //
1597    // The default-subcommand gate applies here too, and has to: offering a discovered
1598    // command that a real parse would hand to the default instead would be worse than
1599    // not offering it. A root mount under a `default_subcommand` that does not say
1600    // `overrides_default` therefore contributes nothing anywhere, which is what
1601    // "the default outranks discovery" means.
1602    let default_outranks_mounts =
1603        spec.default_subcommand.is_some() && !out.cmd.mounts.iter().any(|m| m.overrides_default);
1604    if mount_timing == MountTiming::Eager && !default_outranks_mounts && !out.cmd.mounts.is_empty()
1605    {
1606        mounts_resolved = true;
1607        let mut mounted = out.cmd.clone();
1608        mounted.mount(&[], mount_outputs)?;
1609        merge_subcommand_flags(&mut out.available_flags, gather_flags(&mounted), false);
1610        if let Some(last) = out.cmds.last_mut() {
1611            *last = mounted.clone();
1612        }
1613        out.cmd = mounted;
1614    }
1615
1616    let default_flag_at = spec
1617        .default_subcommand_flags
1618        .then(|| default_flag_route(spec, &out.cmd, &input))
1619        .flatten();
1620
1621    while idx < input.len() {
1622        // Only for a word that could name a command, and only when it matches
1623        // nothing already declared. A CLI that declares its commands and mounts more
1624        // does not spawn a process for every invocation, and a flag — `--help`, or
1625        // anything unrecognized — never triggers discovery at all, which it would
1626        // otherwise do simply by not being a subcommand.
1627        // A declared `default_subcommand` already says what an unmatched word means,
1628        // and it costs nothing — so discovery waits behind it unless a mount asks to
1629        // outrank it. Without this, a task runner would spawn its discovery process
1630        // once per task invocation.
1631        let default_catches_it = spec.default_subcommand.as_deref().is_some_and(|name| {
1632            default_accepts_word(&out.cmd, name, &input[idx].word)
1633                && !out.cmd.mounts.iter().any(|m| m.overrides_default)
1634        });
1635        if !mounts_resolved
1636            && !out.cmd.mounts.is_empty()
1637            && !default_catches_it
1638            && is_command_word(&input[idx].word)
1639            && !is_negative_number(&input[idx].word)
1640            && out.cmd.find_subcommand(&input[idx].word).is_none()
1641        {
1642            mounts_resolved = true;
1643            let mut mounted = out.cmd.clone();
1644            mounted.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1645            merge_subcommand_flags(&mut out.available_flags, gather_flags(&mounted), false);
1646            if let Some(last) = out.cmds.last_mut() {
1647                *last = mounted.clone();
1648            }
1649            out.cmd = mounted;
1650        }
1651        if variadic_flag_active
1652            && out.cmd.find_subcommand(&input[idx].word).is_some()
1653            && !out.cmd.subcommand_precedence_over_arg
1654        {
1655            break;
1656        }
1657        let implicit_default = !used_default_subcommand && default_flag_at == Some(idx);
1658        let selected_command = if implicit_default {
1659            out.cmd
1660                .find_subcommand(spec.default_subcommand.as_deref().expect("default route"))
1661        } else {
1662            out.cmd.find_subcommand(&input[idx].word)
1663        };
1664        if let Some(subcommand) = selected_command {
1665            let boundary_has_parent = out.cmd.args_conflicts_with_subcommands
1666                && implicit_default
1667                && input[idx].word.starts_with('-')
1668                && !input[idx].word.starts_with("--")
1669                && {
1670                    let child_flags = gather_flags(subcommand);
1671                    let mut found = false;
1672                    for short in input[idx].word[1..].chars() {
1673                        let key = format!("-{short}");
1674                        if out.available_flags.contains_key(&key)
1675                            || supplies_short(spec, &out.cmds, short)
1676                        {
1677                            found = true;
1678                            break;
1679                        }
1680                        match child_flags.get(&key) {
1681                            Some(flag) if flag.arg.is_none() => {}
1682                            _ => break,
1683                        }
1684                    }
1685                    found
1686                };
1687            if out.cmd.args_conflicts_with_subcommands && (command_arg_found || boundary_has_parent)
1688            {
1689                bail!(
1690                    "subcommand '{}' cannot be used with arguments on its parent command",
1691                    subcommand.name
1692                );
1693            }
1694            let boundary_parent = (implicit_default
1695                && input[idx].word.starts_with('-')
1696                && !input[idx].word.starts_with("--"))
1697            .then(|| out.available_flags.clone());
1698            let mut subcommand = subcommand.clone();
1699            // Pass prefix words (global flags before this subcommand) to mount
1700            subcommand.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1701            // Only the *boundary* is a mount crossing: below it, the mounted program's own
1702            // commands are ordinary commands relative to each other.
1703            let crossing_mount = subcommand.mounted && !out.cmd.mounted;
1704            merge_subcommand_flags(
1705                &mut out.available_flags,
1706                gather_flags(&subcommand),
1707                crossing_mount,
1708            );
1709            if let Some(parent) = boundary_parent {
1710                let parent_keys = parent.keys().cloned().collect();
1711                let mut flags = out.available_flags.clone();
1712                flags.extend(parent);
1713                default_bundle = Some(DefaultBundle {
1714                    argv: input[idx].argv,
1715                    flags,
1716                    parent_keys,
1717                });
1718            }
1719            // Remove subcommand from input
1720            let selected = if implicit_default {
1721                used_default_subcommand = true;
1722                None
1723            } else {
1724                input.remove(idx)
1725            };
1726            if let Some(selected) = selected {
1727                trace.record(
1728                    selected.argv,
1729                    TokenRole::Command {
1730                        name: subcommand.name.clone(),
1731                    },
1732                );
1733            }
1734            command_has_argv = idx < input.len();
1735            out.cmds.push(subcommand.clone());
1736            out.cmd = subcommand.clone();
1737            // A descent already ran the new command's mounts, above.
1738            mounts_resolved = true;
1739            prefix_flags.clear();
1740            command_arg_found = false;
1741            variadic_flag_active = false;
1742            // Continue from current position (don't reset to 0)
1743            // After remove(), idx now points to the next element
1744        } else if !is_command_word(&input[idx].word)
1745            || declared_numeric_short(&out.available_flags, &input[idx].word)
1746        {
1747            // Check if this is a known flag
1748            let word = input[idx].word.clone();
1749            let flag_key = get_flag_key(&word);
1750
1751            // A short token keys on its first letter, so `-az` would be recorded as
1752            // `-a` and its tail left over. Check the whole token here, where it is
1753            // first read: a token containing an unrecognized letter is not a bundle,
1754            // and recording it as one is what let `-a` be applied from a token that
1755            // never named it.
1756            let boundary = default_bundle
1757                .as_ref()
1758                .filter(|bundle| bundle.argv == input[idx].argv);
1759            let flags = boundary.map_or(&out.available_flags, |bundle| &bundle.flags);
1760            let is_bundle =
1761                word.starts_with("--") || short_bundle_is_known(spec, &out.cmds, flags, &word);
1762            if let Some(f) = flags.get(flag_key).cloned().filter(|_| is_bundle) {
1763                command_arg_found = true;
1764                variadic_flag_active = f.arg.as_ref().is_some_and(|arg| arg.var);
1765                // Skip the flag and keep scanning. Both global and non-global flags may precede
1766                // a subcommand (`mycli --verbose run task`, `mycli run --force task`), and
1767                // stopping at one would hide the subcommand — and any mount on it — from the
1768                // parse, leaving the subcommand name to be mis-read as a positional argument.
1769                //
1770                // Only globals are forwarded to mounts: a non-global flag belongs to the
1771                // command that declared it, not to what is mounted below it.
1772                input[idx].binding = boundary
1773                    .and_then(|bundle| bundle.binding(flag_key))
1774                    .or_else(|| Some((Arc::clone(&f), out.cmds.len() - 1)));
1775                let mut forwarded = f.global.then(|| vec![word.clone()]);
1776                idx += 1;
1777
1778                // Only consume next word if flag takes an argument AND value isn't embedded
1779                // Example: "--dir foo" consumes "foo", but "--dir=foo" or "--verbose" do not
1780                if f.arg.is_some()
1781                    && !word.contains('=')
1782                    && idx < input.len()
1783                    && accepts_detached_flag_value(&f, &input[idx].word)
1784                {
1785                    if let Some(words) = forwarded.as_mut() {
1786                        words.push(input[idx].word.clone());
1787                    }
1788                    idx += 1;
1789                }
1790                if let Some(words) = forwarded {
1791                    apply_prefix_flag_overrides(&mut prefix_flags, Arc::clone(&f));
1792                    prefix_flags.push((f, words));
1793                }
1794            } else {
1795                // Unknown flag - stop looking for subcommands
1796                // Let the main parsing phase handle the error
1797                break;
1798            }
1799        } else {
1800            if variadic_flag_active && out.cmd.subcommand_precedence_over_arg {
1801                idx += 1;
1802                continue;
1803            }
1804            // Found a word that's not a flag or subcommand
1805            // Check if we should use the default_subcommand (only once, and only at the
1806            // root, which is the only place a spec can declare one — `out.cmds` holds just
1807            // the root until something descends). Without that second condition the one
1808            // declared name is looked up wherever the parser happens to be standing, so an
1809            // unrelated command acquires a default because a name matched one level down:
1810            // with `default_subcommand "ls"` at the top, `ex config zzz` descended into
1811            // `config ls`.
1812            if !used_default_subcommand
1813                && out.cmds.len() == 1
1814                && let Some(default_name) = &spec.default_subcommand
1815                && let Some(subcommand) = out
1816                    .cmd
1817                    .find_subcommand(default_name)
1818                    .filter(|_| default_accepts_word(&out.cmd, default_name, &input[idx].word))
1819            {
1820                if out.cmd.args_conflicts_with_subcommands && command_arg_found {
1821                    bail!(
1822                        "subcommand '{}' cannot be used with arguments on its parent command",
1823                        subcommand.name
1824                    );
1825                }
1826                let mut subcommand = subcommand.clone();
1827                // Pass prefix words (global flags before this) to mount
1828                subcommand.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1829                let crossing_mount = subcommand.mounted && !out.cmd.mounted;
1830                merge_subcommand_flags(
1831                    &mut out.available_flags,
1832                    gather_flags(&subcommand),
1833                    crossing_mount,
1834                );
1835                out.cmds.push(subcommand.clone());
1836                out.cmd = subcommand.clone();
1837                command_has_argv = true;
1838                prefix_flags.clear();
1839                command_arg_found = false;
1840                variadic_flag_active = false;
1841                // This descent ran the new command's mounts, so lazy
1842                // discovery must not run them a second time.
1843                mounts_resolved = true;
1844                used_default_subcommand = true;
1845                // Continue the loop to check if this word is a subcommand of the
1846                // default subcommand (e.g., a task name added via mount).
1847                // If it's not a subcommand, the next iteration will break and
1848                // Phase 2 will handle it as a positional arg.
1849                continue;
1850            }
1851            // Sigil-classified positionals do not occupy the ordinary positional cursor and
1852            // therefore do not close subcommand routing. Phase 2 binds and strips them. A
1853            // default subcommand gets first refusal so interpreted and compiled routing agree
1854            // when the root sigil and the default command can both accept this word.
1855            if match_sigil_arg_chain(&out.cmds, &input[idx].word).is_some() {
1856                idx += 1;
1857                continue;
1858            }
1859            // An unmatched word that names no subcommand is forwarded as an external
1860            // command: this word, then every token after it, including flags. Known
1861            // subcommands already won above, and a default_subcommand already caught.
1862            // clap's `allow_external_subcommands` is this, not `unknown_flags=value`.
1863            if out.cmd.external_subcommand {
1864                let rest: Vec<Token> = input.drain(idx..).collect();
1865                for token in &rest {
1866                    trace.record(token.argv, TokenRole::External);
1867                }
1868                out.external = Some(rest.into_iter().map(|t| t.word).collect());
1869                break;
1870            }
1871            // This could be a positional argument, so stop subcommand search
1872            break;
1873        }
1874    }
1875
1876    // Phase 2: Main argument and flag parsing
1877    //
1878    // Now that we've identified all subcommands and executed their mounts,
1879    // we can parse the remaining arguments, flags, and their values.
1880
1881    // The cursor into the active positional arguments, kept as an index rather than a reference
1882    // because an explicit `--` may jump it *past* arguments that stay empty (see the `w == "--"`
1883    // arm).
1884    // With such a gap `out.args.len()` no longer equals the cursor, so anything asking "is this
1885    // argument filled?" has to consult `out.args` by key instead of counting.
1886    let mut next_arg_idx = cursor_skip_sigils(&out.cmd, 0);
1887    let mut enable_flags = true;
1888    let mut grouped_flag = false;
1889    // Whether an explicit `--` has been consumed *as a separator* (as opposed to being kept as a
1890    // value by `double_dash="preserve"`). Args declared `double_dash="required"` only accept
1891    // words that come after it — see `report_double_dash_violation`.
1892    let mut seen_double_dash = false;
1893    // Sigils are a leading-segment grammar. A restart begins a later segment but does not
1894    // reopen sigil classification for this invocation.
1895    let mut restart_seen = false;
1896    // Args already reported as having been offered a word before the `--` they require, so a
1897    // variadic one does not report the same violation for every word it is offered.
1898    let mut double_dash_violations: HashSet<String> = HashSet::new();
1899    // A root clause separator is user-supplied syntax, so it must suppress the
1900    // empty-input default just like an explicit `--`. This is separate from
1901    // `seen_double_dash`, which resets for each clause instance.
1902    let mut root_clause_separator_seen = false;
1903    // Scalar occurrences are scoped to the command level where they were written. Inherited
1904    // globals may therefore appear once before and once after a subcommand under clap's strict
1905    // `args_override_self(false)` policy. The bitset also keeps both forms of a negatable flag:
1906    // opposite forms may override one another, while repeating either spelling is an error.
1907    let mut scalar_occurrences: HashMap<(usize, usize), u8> = HashMap::new();
1908
1909    while !input.is_empty() {
1910        let token = input.pop_front().unwrap();
1911        // The flag this word was read as in Phase 1, if it skipped it (see `Token::binding`).
1912        let binding = token.binding;
1913        let argv = token.argv;
1914        let mut w = token.word;
1915        // A short's attached value is re-queued with `grouped_flag` set, and that
1916        // continuation is not a following word. `require_equals` refuses only the
1917        // following word; `-i9229` and `-i=9229` still bind. `default_missing` binds
1918        // only when the value is actually missing, so `-cnever` is still `never`.
1919        let attached_continuation = grouped_flag;
1920        // The opt-in lookahead uses declared value arity, so binding must consume
1921        // the same attached value even after the default changed flag scope.
1922        // Other specs retain the reference parser's legacy short-bundle rules.
1923        if spec.default_subcommand_flags
1924            && attached_continuation
1925            && !out.flag_awaiting_value.is_empty()
1926        {
1927            grouped_flag = false;
1928            w.remove(0);
1929            if w.starts_with('=') {
1930                w.remove(0);
1931            }
1932            if bind_pending_flag_value(
1933                spec,
1934                &out.cmd,
1935                &mut out.errors,
1936                &mut out.flags,
1937                &mut out.flag_awaiting_value,
1938                &mut w,
1939                &mut input,
1940                custom_env,
1941                trace,
1942                argv,
1943                true,
1944            )? {
1945                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1946                return Ok((out, overridden_flags));
1947            }
1948            continue;
1949        }
1950
1951        // A clause boundary is syntax even after an automatic trailing argument disabled
1952        // flags. Only an explicit `--` protects a literal separator.
1953        if !seen_double_dash
1954            && let Some(clause) = out.cmd.clause.as_ref()
1955            && clause.separator.as_deref() == Some(w.as_str())
1956        {
1957            while try_bind_default_missing(
1958                &mut out.flags,
1959                &mut out.flag_awaiting_value,
1960                custom_env,
1961                &mut out.flag_origins,
1962            )? {}
1963            if let Some(flag) = out.flag_awaiting_value.first() {
1964                let spelling = flag
1965                    .long
1966                    .first()
1967                    .map(|long| format!("--{long}"))
1968                    .or_else(|| flag.short.first().map(|short| format!("-{short}")))
1969                    .unwrap_or_else(|| flag.name.clone());
1970                return Err(UsageErr::InvalidFlag {
1971                    token: spelling.clone(),
1972                    reason: "requires an argument".to_string(),
1973                    span: (0, spelling.len()).into(),
1974                    input: spelling,
1975                }
1976                .into());
1977            }
1978            let name = clause.name.clone();
1979            if out.cmds.len() == 1 {
1980                root_clause_separator_seen = true;
1981            }
1982            finalize_current_clause(&mut out);
1983            out.arg_origins.clear();
1984            trace.record(argv, TokenRole::ClauseSeparator { name });
1985            next_arg_idx = 0;
1986            out.flag_awaiting_value.clear();
1987            reset_clause_scalar_occurrences(&out, &mut scalar_occurrences);
1988            enable_flags = true;
1989            seen_double_dash = false;
1990            continue;
1991        }
1992
1993        // Check for restart_token - resets argument parsing for multiple command invocations
1994        // e.g., `mise run lint ::: test ::: check` with restart_token=":::"
1995        if let Some(ref restart_token) = out.cmd.restart_token
1996            && w == *restart_token
1997        {
1998            // Reset argument parsing state for a fresh command invocation, keeping the
1999            // flags. `double_dash_violations` is deliberately *not* cleared: `out.errors`
2000            // is not cleared here either, so clearing it would let one arg report the same
2001            // violation once per invocation.
2002            out.args.clear();
2003            // With the values gone, so is where they came from — otherwise the second
2004            // invocation of `run lint ::: test` reports the first one's provenance. The
2005            // token trace is *not* cleared: those words were read, and a report that
2006            // dropped them would show a command line with a hole in it.
2007            out.arg_origins.clear();
2008            trace.record(argv, TokenRole::Restart);
2009            next_arg_idx = cursor_skip_sigils(&out.cmd, 0);
2010            restart_seen = true;
2011            out.flag_awaiting_value.clear(); // Clear any pending flag values
2012            enable_flags = true; // Reset -- separator effect
2013            seen_double_dash = false; // The next invocation needs its own `--`
2014            continue;
2015        }
2016
2017        // A flag declared `allow_hyphen_values` takes the next token whatever it looks
2018        // like, and that has to be asked before the separator arm below rather than
2019        // after it. Asked after, a `--` was consumed as a separator while the flag
2020        // stayed hungry, and the flag then ate the word past it: `ex -a -- -x` bound
2021        // `-x` and the separator was simply gone. Asked here, the flag takes the `--`
2022        // itself, which is what clap does with the same declaration — and no flag can
2023        // still be waiting once the separator has done its job, so the starvation rule
2024        // below has no path around it.
2025        if enable_flags
2026            && !attached_continuation
2027            && w.starts_with('-')
2028            && out
2029                .flag_awaiting_value
2030                .last()
2031                .is_some_and(|flag| accepts_detached_flag_value(flag, &w))
2032        {
2033            // A variadic argument collects here too: which token supplied its first
2034            // value says nothing about how many it takes.
2035            let should_return = bind_pending_flag_value(
2036                spec,
2037                &out.cmd,
2038                &mut out.errors,
2039                &mut out.flags,
2040                &mut out.flag_awaiting_value,
2041                &mut w,
2042                &mut input,
2043                custom_env,
2044                trace,
2045                argv,
2046                // The token a hyphen-valued flag takes is the following word, never attached.
2047                false,
2048            )?;
2049            if should_return {
2050                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2051                return Ok((out, overridden_flags));
2052            }
2053            continue;
2054        }
2055
2056        // A flag whose value may be omitted that cannot take this token as a detached
2057        // value finishes bare and leaves the token for whatever comes next:
2058        // `--color --verbose` colours with the missing value and still sets verbose,
2059        // and `--inspect 9229` with `require_equals` binds the missing value rather
2060        // than treating 9229 as the port.
2061        if enable_flags
2062            && !attached_continuation
2063            && !out.flag_awaiting_value.is_empty()
2064            && out.flag_awaiting_value.last().is_some_and(|flag| {
2065                (flag.default_missing.is_some() || flag.value_optional)
2066                    && !accepts_detached_flag_value(flag, &w)
2067            })
2068        {
2069            try_bind_default_missing(
2070                &mut out.flags,
2071                &mut out.flag_awaiting_value,
2072                custom_env,
2073                &mut out.flag_origins,
2074            )?;
2075        }
2076
2077        // The first explicit `--` is still a separator after an `automatic` argument has
2078        // stopped flag parsing. Once an explicit separator has done its job, a second one is
2079        // an ordinary value: every parser worth comparing against keeps it (POSIX getopt,
2080        // argparse, clap, commander, yargs), and jdx/usage#229 was a user reporting the old
2081        // behavior as the bug it is.
2082        if w == "--" && !seen_double_dash {
2083            enable_flags = false;
2084
2085            // Only preserve the double dash token if we're collecting values for a variadic arg
2086            // in double_dash == `preserve` mode
2087            let should_preserve = active_args(&out.cmd)
2088                .get(next_arg_idx)
2089                .map(|arg| arg.var && arg.double_dash == SpecDoubleDashChoices::Preserve)
2090                .unwrap_or(false);
2091
2092            if should_preserve {
2093                // Fall through to arg parsing. This `--` is a *value*, not a separator, so it
2094                // neither counts as one nor unlocks a `double_dash="required"` arg.
2095            } else {
2096                seen_double_dash = true;
2097                trace.record(argv, TokenRole::Separator);
2098
2099                // Everything after an explicit `--` belongs to the arg that requires one, so
2100                // jump the cursor there — past any earlier arg, including a greedy variadic
2101                // that would otherwise swallow the rest. This mirrors clap's `Arg::last(true)`,
2102                // which is what `double_dash="required"` is generated from. Specs without such
2103                // an arg find nothing and keep the cursor where it was.
2104                let target = active_args(&out.cmd).iter().position(|arg| {
2105                    arg.double_dash == SpecDoubleDashChoices::Required
2106                        && !out.args.contains_key(arg)
2107                });
2108                if let Some(target) = target {
2109                    // Forward only. An unfilled required arg declared *before* the cursor is
2110                    // left where it is rather than rewound to — words already assigned to
2111                    // later args would have to be taken back for that to mean anything, and
2112                    // the arg keeps its `MissingArg`. `double_dash="required"` mirrors clap's
2113                    // `Arg::last(true)`, which is the final positional, so a spec that puts
2114                    // one ahead of others is already outside what this models.
2115                    if target > next_arg_idx {
2116                        next_arg_idx = target;
2117                    }
2118                }
2119                continue;
2120            }
2121        }
2122
2123        // long flags
2124        if enable_flags && w.starts_with("--") {
2125            grouped_flag = false;
2126            // `Some` only when an `=` was actually written, so `--jobs=` can supply
2127            // an empty value while `--jobs` supplies none. Collapsing the two lost
2128            // the flag entirely.
2129            let split = w.split_once('=');
2130            let word = split.map(|(word, _)| word).unwrap_or(&w);
2131            let bound_flag = binding.as_ref().map(|(flag, _)| flag);
2132            if let Some(f) = bound_flag.or_else(|| out.available_flags.get(word)) {
2133                let command_level = binding
2134                    .as_ref()
2135                    .map(|(_, level)| *level)
2136                    .unwrap_or(out.cmds.len() - 1);
2137                parsed_flag_spellings
2138                    .entry(Arc::as_ptr(f) as usize)
2139                    .or_default()
2140                    .insert(word.to_string());
2141                // Recorded before the action check below: a token that named a flag named it
2142                // whether or not the parse can carry on afterwards.
2143                trace.record(
2144                    argv,
2145                    TokenRole::Flag {
2146                        flag: Arc::clone(f),
2147                        spelling: word.to_string(),
2148                        negated: f.negate.as_deref() == Some(word),
2149                    },
2150                );
2151                if f.action != crate::SpecFlagAction::Set {
2152                    out.errors
2153                        .push(render_action_err(spec, &out.cmd, f, word, custom_env));
2154                    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2155                    return Ok((out, overridden_flags));
2156                }
2157                apply_flag_overrides(
2158                    f,
2159                    &out.available_flags,
2160                    &mut out.flags,
2161                    &mut out.flag_awaiting_value,
2162                    &mut overridden_flags,
2163                    &mut out.overridden_flags,
2164                );
2165                if let Some(pending) = out.flag_awaiting_value.first() {
2166                    out.errors.push(render_missing_flag_value(pending, &w));
2167                    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2168                    return Ok((out, overridden_flags));
2169                }
2170                // An attached value only means something to a flag that takes one:
2171                // `--jobs=` is an empty string, while `--force=yes` has nothing to
2172                // give a flag that holds no value. Handing that leftover to the
2173                // positionals would re-split one token into two, so `ex --force=yes`
2174                // would fill an argument the caller never typed a word for.
2175                if f.arg.is_some() {
2176                    record_scalar_flag_occurrence(
2177                        &out.cmds,
2178                        f,
2179                        command_level,
2180                        None,
2181                        &mut scalar_occurrences,
2182                        &mut out.errors,
2183                    );
2184                    let f = Arc::clone(f);
2185                    out.flag_awaiting_value.push(Arc::clone(&f));
2186                    // The `=` has already settled that this text is the value, so it
2187                    // binds here rather than going back on the queue to be read as a
2188                    // token again — where `--jobs=--force` looked like a flag of its
2189                    // own and bound `force`, leaving `jobs` unset.
2190                    if let Some((_, val)) = split {
2191                        // The `=` settles where the *first* value came from and nothing
2192                        // more, so a variadic argument goes on collecting from the words
2193                        // after it exactly as the detached form does.
2194                        let mut val = val.to_string();
2195                        let should_return = bind_pending_flag_value(
2196                            spec,
2197                            &out.cmd,
2198                            &mut out.errors,
2199                            &mut out.flags,
2200                            &mut out.flag_awaiting_value,
2201                            &mut val,
2202                            &mut input,
2203                            custom_env,
2204                            trace,
2205                            argv,
2206                            // The `=` settled that this text is the value, so it rode in on
2207                            // the flag's own token.
2208                            true,
2209                        )?;
2210                        if should_return {
2211                            record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2212                            return Ok((out, overridden_flags));
2213                        }
2214                    }
2215                } else if f.count {
2216                    let arr = out
2217                        .flags
2218                        .entry(Arc::clone(f))
2219                        .or_insert_with(|| ParseValue::MultiBool(vec![]))
2220                        .try_as_multi_bool_mut()
2221                        .unwrap();
2222                    arr.push(true);
2223                } else {
2224                    let negate = f.negate.clone().unwrap_or_default();
2225                    let negated_form = word == negate;
2226                    let value = if f.bool_value {
2227                        match split.map(|(_, value)| value) {
2228                            Some("true") => !negated_form,
2229                            Some("false") => negated_form,
2230                            Some(value) => {
2231                                out.errors.push(UsageErr::InvalidValue {
2232                                    name: f.name.clone(),
2233                                    value: value.to_string(),
2234                                    reason: "expected `true` or `false`".to_string(),
2235                                });
2236                                continue;
2237                            }
2238                            None => !negated_form,
2239                        }
2240                    } else {
2241                        !negated_form
2242                    };
2243                    // Which form was typed is a question about the name, so it is
2244                    // asked of `word` rather than the whole token: the attached value
2245                    // is dropped just above, and comparing `--no-color=yes` against
2246                    // `--no-color` would take the negation down with it.
2247                    record_scalar_flag_occurrence(
2248                        &out.cmds,
2249                        f,
2250                        command_level,
2251                        Some(!negated_form),
2252                        &mut scalar_occurrences,
2253                        &mut out.errors,
2254                    );
2255                    out.flags.insert(Arc::clone(f), ParseValue::Bool(value));
2256                }
2257                continue;
2258            }
2259            if is_help_arg(spec, &out.cmd, &w) {
2260                out.errors
2261                    .push(render_help_err(spec, &out.cmd, w.len() > 2, custom_env));
2262                trace.record(
2263                    argv,
2264                    TokenRole::Builtin {
2265                        spelling: w.clone(),
2266                    },
2267                );
2268                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2269                return Ok((out, overridden_flags));
2270            }
2271            if is_version_arg(spec, &out.cmds, &w) {
2272                out.errors.push(render_version_err(spec, w.len() > 2));
2273                trace.record(
2274                    argv,
2275                    TokenRole::Builtin {
2276                        spelling: w.clone(),
2277                    },
2278                );
2279                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2280                return Ok((out, overridden_flags));
2281            }
2282            if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
2283                trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
2284                trace.close(&input);
2285                return Err(refused.into());
2286            }
2287        }
2288
2289        // short flags
2290        //
2291        // A fresh token is checked whole before any of it is applied: `-az` with only
2292        // `-a` declared is not a bundle at all, so it must not set `a` on the way to
2293        // discovering that `z` names nothing. A grouped continuation is exempt — its
2294        // token was already checked when it arrived.
2295        let declared_numeric_short = declared_numeric_short(&out.available_flags, &w);
2296        let positional_negative_number = !declared_numeric_short
2297            && is_negative_number(&w)
2298            && active_args(&out.cmd)
2299                .get(next_arg_idx)
2300                .is_some_and(|arg| arg.allow_negative_numbers);
2301        if enable_flags
2302            && !grouped_flag
2303            // A word phase 1 already resolved to a flag needs no re-checking, and
2304            // the flags in scope have changed since, so re-checking would be wrong.
2305            && binding.is_none()
2306            && w.starts_with('-')
2307            && w.len() > 1
2308            && is_flag_like(&w)
2309            && !positional_negative_number
2310            && !short_bundle_is_known(spec, &out.cmds, &out.available_flags, &w)
2311        {
2312            // Refused if this command asked for that; otherwise it carries on below
2313            // as one word, with none of its letters applied.
2314            if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
2315                trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
2316                trace.close(&input);
2317                return Err(refused.into());
2318            }
2319        } else if enable_flags && !positional_negative_number && w.starts_with('-') && w.len() > 1 {
2320            let short = w.chars().nth(1).unwrap();
2321            if let Some(f) = binding
2322                .as_ref()
2323                .map(|(flag, _)| flag)
2324                .or_else(|| out.available_flags.get(&format!("-{short}")))
2325            {
2326                let command_level = binding
2327                    .as_ref()
2328                    .map(|(_, level)| *level)
2329                    .unwrap_or(out.cmds.len() - 1);
2330                if f.action != crate::SpecFlagAction::Set {
2331                    out.errors.push(render_action_err(
2332                        spec,
2333                        &out.cmd,
2334                        f,
2335                        &format!("-{short}"),
2336                        custom_env,
2337                    ));
2338                    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2339                    return Ok((out, overridden_flags));
2340                }
2341                parsed_flag_spellings
2342                    .entry(Arc::as_ptr(f) as usize)
2343                    .or_default()
2344                    .insert(format!("-{short}"));
2345                trace.record(
2346                    argv,
2347                    TokenRole::Flag {
2348                        flag: Arc::clone(f),
2349                        spelling: format!("-{short}"),
2350                        // A short spelling is never the negated form: `negate` is a long.
2351                        negated: false,
2352                    },
2353                );
2354                apply_flag_overrides(
2355                    f,
2356                    &out.available_flags,
2357                    &mut out.flags,
2358                    &mut out.flag_awaiting_value,
2359                    &mut overridden_flags,
2360                    &mut out.overridden_flags,
2361                );
2362                if !attached_continuation && let Some(pending) = out.flag_awaiting_value.first() {
2363                    out.errors.push(render_missing_flag_value(pending, &w));
2364                    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2365                    return Ok((out, overridden_flags));
2366                }
2367                let rest = &w[1 + short.len_utf8()..];
2368                if !rest.is_empty() {
2369                    // `-abc` is one token that names three flags, so the tail is read at the
2370                    // bundle's own position rather than at one of its own.
2371                    let mut tail = Token::new(format!("-{rest}"), argv);
2372                    if f.arg.is_none() {
2373                        tail.binding = default_bundle
2374                            .as_ref()
2375                            .filter(|bundle| bundle.argv == argv)
2376                            .and_then(|bundle| bundle.binding(get_flag_key(&tail.word)));
2377                    }
2378                    input.push_front(tail);
2379                }
2380                // A fully consumed short is no longer a grouped continuation.
2381                // Leaving this set after `-ai` made `-i` skip `require_equals`
2382                // and bind the following word.
2383                grouped_flag = !rest.is_empty();
2384                if f.arg.is_some() {
2385                    record_scalar_flag_occurrence(
2386                        &out.cmds,
2387                        f,
2388                        command_level,
2389                        None,
2390                        &mut scalar_occurrences,
2391                        &mut out.errors,
2392                    );
2393                    out.flag_awaiting_value.push(Arc::clone(f));
2394                } else if f.count {
2395                    let arr = out
2396                        .flags
2397                        .entry(Arc::clone(f))
2398                        .or_insert_with(|| ParseValue::MultiBool(vec![]))
2399                        .try_as_multi_bool_mut()
2400                        .unwrap();
2401                    arr.push(true);
2402                } else {
2403                    let negate = f.negate.clone().unwrap_or_default();
2404                    let value = w != negate;
2405                    record_scalar_flag_occurrence(
2406                        &out.cmds,
2407                        f,
2408                        command_level,
2409                        Some(value),
2410                        &mut scalar_occurrences,
2411                        &mut out.errors,
2412                    );
2413                    out.flags.insert(Arc::clone(f), ParseValue::Bool(value));
2414                }
2415                continue;
2416            }
2417            // The letter nothing declared may still be one the parser supplies, and it may
2418            // sit anywhere in the token: `-hv` asks for help as surely as `-vh` does, and
2419            // neither reaches the whole-token spellings below.
2420            if let Some(err) = supplied_short(spec, &out.cmds, short, custom_env) {
2421                out.errors.push(err);
2422                trace.record(
2423                    argv,
2424                    TokenRole::Builtin {
2425                        spelling: format!("-{short}"),
2426                    },
2427                );
2428                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2429                return Ok((out, overridden_flags));
2430            }
2431            if is_help_arg(spec, &out.cmd, &w) {
2432                out.errors
2433                    .push(render_help_err(spec, &out.cmd, w.len() > 2, custom_env));
2434                trace.record(
2435                    argv,
2436                    TokenRole::Builtin {
2437                        spelling: w.clone(),
2438                    },
2439                );
2440                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2441                return Ok((out, overridden_flags));
2442            }
2443            if is_version_arg(spec, &out.cmds, &w) {
2444                out.errors.push(render_version_err(spec, w.len() > 2));
2445                trace.record(
2446                    argv,
2447                    TokenRole::Builtin {
2448                        spelling: w.clone(),
2449                    },
2450                );
2451                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2452                return Ok((out, overridden_flags));
2453            }
2454            if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
2455                trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
2456                trace.close(&input);
2457                return Err(refused.into());
2458            }
2459            if grouped_flag {
2460                grouped_flag = false;
2461                w.remove(0);
2462                // What is left is a short flag's attached value, and one `=` between
2463                // the letter and the value is a separator: `-j=8` means 8. Only one,
2464                // so `-j==8` still means `=8`.
2465                if !out.flag_awaiting_value.is_empty() && w.starts_with('=') {
2466                    w.remove(0);
2467                }
2468            }
2469        }
2470
2471        // Only while flags are still being read. A flag still waiting when the separator
2472        // was consumed is starved: its value would have to come from after the `--`,
2473        // where every token is data. Draining there gave `ex --jobs -- x` the word after
2474        // the separator, so the command line quietly meant `ex --jobs=x` and the `--`
2475        // was gone. Left waiting, it is reported as the missing value it is.
2476        // `require_equals` refuses a detached value: `--flag value` is a missing
2477        // value, not a flag of `"value"`. The attached form is still bound above.
2478        // Reported here rather than left waiting until the end of the line: falling
2479        // through would offer `value` to the positionals and call it an unexpected
2480        // word, which is the wrong error and a different one from usage-argv.
2481        if enable_flags
2482            && !attached_continuation
2483            && !out.flag_awaiting_value.is_empty()
2484            && out
2485                .flag_awaiting_value
2486                .last()
2487                .is_some_and(|flag| flag.require_equals)
2488        {
2489            let flag = out.flag_awaiting_value.last().unwrap();
2490            let token = flag
2491                .long
2492                .first()
2493                .map(|l| format!("--{l}"))
2494                .or_else(|| flag.short.first().map(|s| format!("-{s}")))
2495                .unwrap_or_else(|| flag.name.clone());
2496            out.errors.push(UsageErr::InvalidFlag {
2497                token: token.clone(),
2498                reason: "requires an argument".to_string(),
2499                span: (0, 0).into(),
2500                input: format!("{token} {w}"),
2501            });
2502            record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2503            return Ok((out, overridden_flags));
2504        }
2505        if enable_flags
2506            && !out.flag_awaiting_value.is_empty()
2507            && (attached_continuation
2508                || out
2509                    .flag_awaiting_value
2510                    .last()
2511                    .is_some_and(|flag| accepts_detached_flag_value(flag, &w)))
2512        {
2513            // Held before the drain pops it: a flag whose argument is variadic keeps
2514            // taking values after this first one.
2515            let should_return = bind_pending_flag_value(
2516                spec,
2517                &out.cmd,
2518                &mut out.errors,
2519                &mut out.flags,
2520                &mut out.flag_awaiting_value,
2521                &mut w,
2522                &mut input,
2523                custom_env,
2524                trace,
2525                argv,
2526                attached_continuation,
2527            )?;
2528            if should_return {
2529                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2530                return Ok((out, overridden_flags));
2531            }
2532            continue;
2533        }
2534
2535        if let Some((arg, sigil, value)) = (enable_flags && !restart_seen)
2536            .then(|| match_sigil_arg_chain(&out.cmds, &w))
2537            .flatten()
2538        {
2539            if value.is_empty() {
2540                out.errors.push(UsageErr::InvalidValue {
2541                    name: arg.name.clone(),
2542                    value: w.clone(),
2543                    reason: format!("expected a value after sigil {sigil:?}"),
2544                });
2545                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2546                return Ok((out, overridden_flags));
2547            }
2548            let trailing_value = arg.double_dash == SpecDoubleDashChoices::Automatic;
2549            let suppress_trailing_delimiter =
2550                out.cmds.iter().any(|cmd| cmd.dont_delimit_trailing_values);
2551            let delimiter = if suppress_trailing_delimiter && trailing_value {
2552                None
2553            } else {
2554                arg.delimiter
2555            };
2556            let parts = match delimiter {
2557                Some(delimiter) => value
2558                    .split(delimiter)
2559                    .map(str::to_string)
2560                    .collect::<Vec<_>>(),
2561                None => vec![value.to_string()],
2562            };
2563            let mut refused = false;
2564            for part in &parts {
2565                if validate_choices(
2566                    spec,
2567                    &out.cmd,
2568                    &mut out.errors,
2569                    ChoiceTarget::arg(arg),
2570                    part,
2571                    arg.choices.as_ref(),
2572                    custom_env,
2573                )? {
2574                    refused = true;
2575                    break;
2576                }
2577            }
2578            if refused {
2579                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2580                return Ok((out, overridden_flags));
2581            }
2582            trace.record(
2583                argv,
2584                TokenRole::Sigil {
2585                    arg: Arc::new(arg.clone()),
2586                    sigil: sigil.to_string(),
2587                    values: parts.clone(),
2588                },
2589            );
2590            let key = Arc::new(arg.clone());
2591            if arg.var {
2592                let arr = out
2593                    .args
2594                    .entry(key)
2595                    .or_insert_with(|| ParseValue::MultiString(vec![]))
2596                    .try_as_multi_string_mut()
2597                    .unwrap();
2598                arr.extend(parts);
2599            } else {
2600                out.args.insert(key, ParseValue::String(value.to_string()));
2601            }
2602            if out.cmds.len() == 1 {
2603                root_positional_arg_found = true;
2604            }
2605            continue;
2606        }
2607
2608        if out.cmd.allow_missing_positional {
2609            next_arg_idx = cursor_skip_sigils(&out.cmd, next_arg_idx);
2610            while let Some(current) = active_args(&out.cmd).get(next_arg_idx) {
2611                if current.required || out.args.contains_key(current) {
2612                    break;
2613                }
2614                let required_after = active_args(&out.cmd)[next_arg_idx + 1..]
2615                    .iter()
2616                    .filter(|arg| arg.required && arg.sigil.is_none())
2617                    .count();
2618                if required_after == 0 {
2619                    break;
2620                }
2621                let remaining_values = 1 + input
2622                    .iter()
2623                    .filter(|token| {
2624                        (!enable_flags || !is_flag_like(&token.word))
2625                            && (!enable_flags
2626                                || restart_seen
2627                                || match_sigil_arg_chain(&out.cmds, &token.word).is_none())
2628                    })
2629                    .count();
2630                if remaining_values > required_after {
2631                    break;
2632                }
2633                next_arg_idx = cursor_skip_sigils(&out.cmd, next_arg_idx + 1);
2634            }
2635        }
2636
2637        if let Some(arg) = active_args(&out.cmd).get(next_arg_idx) {
2638            if arg.var
2639                && out.args.contains_key(arg)
2640                && arg.value_terminator.as_deref() == Some(w.as_str())
2641            {
2642                trace.record(
2643                    argv,
2644                    TokenRole::ValueTerminator {
2645                        ends: arg.name.clone(),
2646                    },
2647                );
2648                next_arg_idx += 1;
2649                continue;
2650            }
2651            // Before anything else: an arg that requires `--` accepts nothing until one has been
2652            // seen. Checking ahead of `validate_choices` keeps a discarded word from also being
2653            // reported as an invalid choice, and from reaching that function's help escape.
2654            if arg.double_dash == SpecDoubleDashChoices::Required && !seen_double_dash {
2655                report_double_dash_violation(arg, &mut out.errors, &mut double_dash_violations);
2656                trace.record(
2657                    argv,
2658                    TokenRole::Refused {
2659                        reason: format!("{} only accepts words after `--`", arg.name),
2660                    },
2661                );
2662                // Drop the word without filling the arg or advancing the cursor: every later
2663                // word hits the same arg and is rejected the same way, so the parse still ends
2664                // in an error rather than in `unexpected word`.
2665                continue;
2666            }
2667            // Split before judging, as the flag path does: after the split the word is
2668            // no longer one value, and `choices` has to be asked about each. Judging
2669            // first rejects `src:docs` against a list that both halves are on, and
2670            // names the whole word rather than the half that was wrong.
2671            let trailing_value =
2672                seen_double_dash || arg.double_dash == SpecDoubleDashChoices::Automatic;
2673            let suppress_trailing_delimiter =
2674                out.cmds.iter().any(|cmd| cmd.dont_delimit_trailing_values);
2675            let delimiter = if suppress_trailing_delimiter && trailing_value {
2676                None
2677            } else {
2678                arg.delimiter
2679            };
2680            let parts: Vec<String> = match delimiter {
2681                Some(delimiter) => w.split(delimiter).map(str::to_string).collect(),
2682                None => vec![w.clone()],
2683            };
2684            let mut refused = false;
2685            for part in &parts {
2686                if validate_choices(
2687                    spec,
2688                    &out.cmd,
2689                    &mut out.errors,
2690                    ChoiceTarget::arg(arg),
2691                    part,
2692                    arg.choices.as_ref(),
2693                    custom_env,
2694                )? {
2695                    refused = true;
2696                    break;
2697                }
2698            }
2699            if refused {
2700                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2701                return Ok((out, overridden_flags));
2702            }
2703            // `double_dash="automatic"` means the first value this arg takes is the last
2704            // token read as anything but data: a wrapper declaring it can forward flags
2705            // without its caller typing a `--`. Set before the value is stored, so the
2706            // rest of the command line is already past flag parsing.
2707            if arg.double_dash == SpecDoubleDashChoices::Automatic {
2708                enable_flags = false;
2709            }
2710            // A flag-like word reaching a positional while flags are still being read was
2711            // offered to every declaration and matched none: under the default
2712            // `unknown_flags="value"` it becomes data, and saying so is the difference
2713            // between "you have a typo" and "this argument took your typo".
2714            let unknown_flag = enable_flags
2715                && !positional_negative_number
2716                && is_flag_like(&w)
2717                && binding.is_none();
2718            trace.record(
2719                argv,
2720                if unknown_flag {
2721                    TokenRole::UnknownFlag {
2722                        bound_as: Some(Arc::new(arg.clone())),
2723                    }
2724                } else {
2725                    TokenRole::Arg {
2726                        arg: Arc::new(arg.clone()),
2727                        values: parts.clone(),
2728                    }
2729                },
2730            );
2731            if out.cmds.len() == 1 {
2732                root_positional_arg_found = true;
2733            }
2734            if arg.var {
2735                let arr = out
2736                    .args
2737                    .entry(Arc::new(arg.clone()))
2738                    .or_insert_with(|| ParseValue::MultiString(vec![]))
2739                    .try_as_multi_string_mut()
2740                    .unwrap();
2741                // The values this word carried, split above so that everything
2742                // downstream — `choices`, `var_max` stopping the collection, `var_min` —
2743                // counts the values the user meant rather than the words they typed.
2744                arr.extend(parts.iter().cloned());
2745                if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
2746                    next_arg_idx += 1;
2747                }
2748            } else {
2749                out.args
2750                    .insert(Arc::new(arg.clone()), ParseValue::String(w));
2751                next_arg_idx += 1;
2752            }
2753            if out
2754                .cmd
2755                .clause
2756                .as_ref()
2757                .is_some_and(|clause| clause.separator.is_none())
2758                && next_arg_idx >= active_args(&out.cmd).len()
2759            {
2760                while try_bind_default_missing(
2761                    &mut out.flags,
2762                    &mut out.flag_awaiting_value,
2763                    custom_env,
2764                    &mut out.flag_origins,
2765                )? {}
2766                if let Some(flag) = out.flag_awaiting_value.first() {
2767                    let spelling = flag
2768                        .long
2769                        .first()
2770                        .map(|long| format!("--{long}"))
2771                        .or_else(|| flag.short.first().map(|short| format!("-{short}")))
2772                        .unwrap_or_else(|| flag.name.clone());
2773                    return Err(UsageErr::InvalidFlag {
2774                        token: spelling.clone(),
2775                        reason: "requires an argument".to_string(),
2776                        span: (0, spelling.len()).into(),
2777                        input: spelling,
2778                    }
2779                    .into());
2780                }
2781                let name = out.cmd.clause.as_ref().unwrap().name.clone();
2782                finalize_current_clause(&mut out);
2783                out.arg_origins.clear();
2784                trace.record(argv, TokenRole::ClauseSeparator { name });
2785                next_arg_idx = 0;
2786                out.flag_awaiting_value.clear();
2787                reset_clause_scalar_occurrences(&out, &mut scalar_occurrences);
2788                enable_flags = true;
2789                seen_double_dash = false;
2790            }
2791            continue;
2792        }
2793        if is_help_arg(spec, &out.cmd, &w) {
2794            out.errors
2795                .push(render_help_err(spec, &out.cmd, w.len() > 2, custom_env));
2796            trace.record(
2797                argv,
2798                TokenRole::Builtin {
2799                    spelling: w.clone(),
2800                },
2801            );
2802            record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2803            return Ok((out, overridden_flags));
2804        }
2805        if is_version_arg(spec, &out.cmds, &w) {
2806            out.errors.push(render_version_err(spec, w.len() > 2));
2807            trace.record(
2808                argv,
2809                TokenRole::Builtin {
2810                    spelling: w.clone(),
2811                },
2812            );
2813            record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2814            return Ok((out, overridden_flags));
2815        }
2816        trace.record(
2817            argv,
2818            TokenRole::Refused {
2819                reason: "no declaration takes this word".to_string(),
2820            },
2821        );
2822        trace.close(&input);
2823        bail!("unexpected word: {w}");
2824    }
2825
2826    // Resolve optional defaults before deciding whether argv is empty. This is deliberately
2827    // inside the opt-in gate: completion's partial parser must retain its pending value when
2828    // the feature is disabled.
2829    if spec.default_subcommand_on_empty
2830        && spec.default_subcommand.is_some()
2831        && out.cmds.len() == 1
2832        && !root_positional_arg_found
2833        && !seen_double_dash
2834        && !root_clause_separator_seen
2835        && out.errors.is_empty()
2836    {
2837        while try_bind_default_missing(
2838            &mut out.flags,
2839            &mut out.flag_awaiting_value,
2840            custom_env,
2841            &mut out.flag_origins,
2842        )? {}
2843        if !out.flag_awaiting_value.is_empty() {
2844            // A required flag value is still pending; leave it for full-parse diagnostics or
2845            // completion rather than routing to a synthetic child.
2846        } else if let Some(subcommand) = out
2847            .cmd
2848            .find_subcommand(spec.default_subcommand.as_deref().unwrap())
2849        {
2850            if out.cmd.args_conflicts_with_subcommands && command_arg_found {
2851                bail!(
2852                    "subcommand '{}' cannot be used with arguments on its parent command",
2853                    subcommand.name
2854                );
2855            }
2856            let mut subcommand = subcommand.clone();
2857            subcommand.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
2858            let crossing_mount = subcommand.mounted && !out.cmd.mounted;
2859            merge_subcommand_flags(
2860                &mut out.available_flags,
2861                gather_flags(&subcommand),
2862                crossing_mount,
2863            );
2864            out.cmds.push(subcommand.clone());
2865            out.cmd = subcommand;
2866            command_has_argv = false;
2867            prefix_flags.clear();
2868            next_arg_idx = cursor_skip_sigils(&out.cmd, 0);
2869        }
2870    }
2871
2872    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2873    if validate_clauses {
2874        validate_clause_relationships(&mut out, &overridden_flags, custom_env, None);
2875    }
2876    let clause_flag_names = out
2877        .cmd
2878        .clause
2879        .iter()
2880        .flat_map(|clause| &clause.flags)
2881        .map(|flag| flag.name.clone())
2882        .collect::<HashSet<_>>();
2883
2884    // `out.flags` is keyed by `SpecFlag`, whose equality is intentionally name-only. Two
2885    // declarations with the same canonical name therefore share one public value entry even
2886    // when both were typed. The spelling ledger is keyed by declaration identity and retains
2887    // both, which is what exclusivity needs.
2888    let flag_was_parsed =
2889        |flag: &Arc<SpecFlag>| parsed_flag_spellings.contains_key(&(Arc::as_ptr(flag) as usize));
2890
2891    // The spellings the selected command's own declaration speaks for, on this object.
2892    //
2893    // Empty unless that declaration really is this object's: a parent and child may each
2894    // declare `--clean` without merging, leaving two flags that share a name, and the
2895    // ancestor's must not be read as the child's. The test is whether every spelling the child
2896    // declared resolves back here — true of a merged flag, and of a plain local one, but not of
2897    // an ancestor whose long form the child took over.
2898    let child_spellings = |flag: &Arc<SpecFlag>| -> HashSet<String> {
2899        let declared: HashSet<String> = out
2900            .cmd
2901            .flags
2902            .iter()
2903            .filter(|declared| declared.name == flag.name)
2904            .flat_map(flag_keys)
2905            .collect();
2906        // *Any* of them, not all. All was too strong: a child may declare a spelling that
2907        // some other inherited global already owns — `-c --clean` beside an inherited
2908        // `-c --config` — and that collision is resolved in the other global's favor, so the
2909        // child's `-c` resolves elsewhere. Requiring every spelling to land here let one
2910        // unrelated collision disown the child from the `--clean` it plainly does own.
2911        //
2912        // Still enough to tell the two-object case apart, which is what this guards: when a
2913        // child re-declares a global as global, the child's own spellings resolve to the
2914        // child's separate flag, so none of them lands on the ancestor's.
2915        let speaks_for_this_flag = declared.iter().any(|spelling| {
2916            out.available_flags
2917                .get(spelling)
2918                .is_some_and(|available| Arc::ptr_eq(available, flag))
2919        });
2920        if speaks_for_this_flag {
2921            declared
2922        } else {
2923            HashSet::new()
2924        }
2925    };
2926
2927    // Whose `exclusive` an occurrence activates, as `(the child's, an ancestor's)`.
2928    //
2929    // A child that re-declares an inherited global merges into one object answering to two
2930    // alias sets whose declarations may disagree, so there is no single owner to name: the
2931    // child owns the spellings it declared and the ancestor keeps the ones only it declared.
2932    // Both sides can be in play at once — `run -c --clean` is the ancestor's alias and the
2933    // child's in one invocation — and each carries its own declaration's answer.
2934    let exclusivity_in_play = |flag: &Arc<SpecFlag>| -> (bool, bool) {
2935        let child = child_spellings(flag);
2936        let child_exclusive = !child.is_empty()
2937            && out
2938                .cmd
2939                .flags
2940                .iter()
2941                .any(|declared| declared.name == flag.name && declared.exclusive);
2942        match parsed_flag_spellings.get(&(Arc::as_ptr(flag) as usize)) {
2943            Some(spellings) => (
2944                child_exclusive && spellings.iter().any(|s| child.contains(s)),
2945                flag.exclusive && spellings.iter().any(|s| !child.contains(s)),
2946            ),
2947            // An environment value has no spelling to attribute it by. The declaration the
2948            // selected command has in scope is the one that answers — which is the child's
2949            // when it re-declared the flag, and the ancestor's when it did not.
2950            None => (child_exclusive, flag.exclusive && child.is_empty()),
2951        }
2952    };
2953
2954    let exclusive_occurrence = |flag: &Arc<SpecFlag>| {
2955        let (child, ancestor) = exclusivity_in_play(flag);
2956        child || ancestor
2957    };
2958
2959    // clap's `exclusive` is also an escape from requiredness: `--version` has to work on a
2960    // command that otherwise needs an input. Companions are still diagnosed below, but an
2961    // exclusive occurrence suppresses the missing-value checks that would make it unusable
2962    // whether it was alone or not.
2963    let exclusive_present = unique_flags(out.available_flags.values().chain(out.flags.keys()))
2964        .filter(|flag| !clause_flag_names.contains(&flag.name))
2965        .any(|flag| {
2966            exclusive_occurrence(flag)
2967                && !overridden_flags.contains(&flag.name)
2968                && (flag_was_parsed(flag) || flag_has_env(flag, custom_env))
2969        });
2970    let requirements_apply = |command_index: usize| {
2971        command_index + 1 == out.cmds.len() || !out.cmds[command_index].subcommand_negates_reqs
2972    };
2973
2974    if out.cmd.arg_required_else_help && !command_has_argv {
2975        out.errors
2976            .push(render_help_err(spec, &out.cmd, false, custom_env));
2977    }
2978
2979    // A command that says it needs a subcommand, given none. Checked on `out.cmd` and nowhere
2980    // else, because `out.cmd` *is* the command the words reached: had a subcommand been taken,
2981    // the child would be here instead. The spec has carried `subcommand_required` since it was
2982    // added for the derive, and this parser never read it — so `mise generate` parsed as a
2983    // complete invocation while usage-argv and clap both refused it.
2984    if out.cmd.subcommand_required && !out.cmd.subcommands.is_empty() && out.external.is_none() {
2985        let mut names: Vec<&str> = out
2986            .cmd
2987            .subcommands
2988            .iter()
2989            // Aliases share a map entry with the name they point at; listing both would offer
2990            // the same command twice under two spellings.
2991            .filter(|(name, sub)| sub.name == **name && !sub.hide)
2992            .map(|(name, _)| name.as_str())
2993            .collect();
2994        names.sort_unstable();
2995        out.errors.push(UsageErr::MissingSubcommand(
2996            out.cmd.name.clone(),
2997            names.join(", "),
2998        ));
2999    }
3000
3001    // Not `skip(out.args.len())`: a `--` may have jumped the cursor past an arg that stayed
3002    // empty, so position and fill count can disagree. Ask `out.args` which args it holds.
3003    if !exclusive_present {
3004        for arg in out
3005            .cmds
3006            .iter()
3007            .enumerate()
3008            .filter(|(index, _)| requirements_apply(*index))
3009            .flat_map(|(_, cmd)| &cmd.args)
3010        {
3011            if out.args.contains_key(arg) {
3012                continue;
3013            }
3014            // Already reported as needing a `--`; one mistake should not yield two messages.
3015            if double_dash_violations.contains(&arg.name) {
3016                continue;
3017            }
3018            let required_if = arg.required_if.iter().any(|selector| {
3019                selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3020            });
3021            let required_if_eq = arg.required_if_eq.iter().any(|condition| {
3022                selector_explicit_has_value(
3023                    &condition.selector,
3024                    &condition.value,
3025                    &out,
3026                    &overridden_flags,
3027                    custom_env,
3028                )
3029            });
3030            let required_if_eq_all = !arg.required_if_eq_all.is_empty()
3031                && arg.required_if_eq_all.iter().all(|condition| {
3032                    selector_explicit_has_value(
3033                        &condition.selector,
3034                        &condition.value,
3035                        &out,
3036                        &overridden_flags,
3037                        custom_env,
3038                    )
3039                });
3040            let unless_any = arg.required_unless.iter().any(|selector| {
3041                selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3042            });
3043            let unless_all = !arg.required_unless_all.is_empty()
3044                && arg.required_unless_all.iter().all(|selector| {
3045                    selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3046                });
3047            let required_unless = !(unless_any
3048                || unless_all
3049                || (arg.required_unless.is_empty() && arg.required_unless_all.is_empty()));
3050            if (arg.required
3051                || required_if
3052                || required_if_eq
3053                || required_if_eq_all
3054                || required_unless)
3055                && arg.default.is_empty()
3056            {
3057                // Check if there's an env var available (custom env map takes precedence)
3058                let has_env = arg
3059                    .env
3060                    .as_ref()
3061                    .is_some_and(|env_var| env_contains(custom_env, env_var));
3062                if !has_env {
3063                    out.errors.push(UsageErr::MissingArg(arg.name.clone()));
3064                }
3065            }
3066        }
3067    }
3068
3069    // Conflicts are a question about the invocation as a whole rather than about any one
3070    // token, so they are checked here beside the requirement checks rather than at the
3071    // point a flag is matched — the flag it conflicts with may still be ahead of it.
3072    // Its own loop: the requirement loop below skips the flags that *were* given, which
3073    // is exactly the set this needs.
3074    //
3075    // A value from the environment counts on both sides, matching what
3076    // `selector_is_explicit` says about the other flag: the question is whether a flag
3077    // has a value, not how it got one. That is what clap does, and an asymmetric rule
3078    // would make the same pair of flags a conflict or not depending on which one
3079    // happened to be typed.
3080    for flag in unique_flags(out.available_flags.values())
3081        .filter(|flag| !clause_flag_names.contains(&flag.name))
3082    {
3083        let given = out.flags.contains_key(flag) || flag_has_env(flag, custom_env);
3084        if !given || overridden_flags.contains(&flag.name) {
3085            continue;
3086        }
3087        for other in &flag.conflicts {
3088            if selector_is_explicit(other, &out, &overridden_flags, custom_env) {
3089                out.errors.push(UsageErr::InvalidFlag {
3090                    token: format!("--{}", flag.name),
3091                    reason: format!("conflicts with {other}"),
3092                    span: (0, 0).into(),
3093                    input: format!("--{} {other}", flag.name),
3094                });
3095            }
3096        }
3097        // The positive form, checked in the same pass and under the same rule: a value
3098        // from the environment satisfies a requirement, because the question is whether
3099        // the other flag has a value rather than how it got one. A flag that was
3100        // overridden away has not been given, so it cannot satisfy anything either —
3101        // which is what `selector_is_explicit` already accounts for.
3102        //
3103        // Reported as the missing flag rather than as something wrong with the flag that
3104        // named it, which is what clap says too: an unmet `requires` is a required
3105        // argument that was not provided. Named by its own name, resolved through the
3106        // same matcher, so a `requires="-f"` reports `--force` rather than the selector.
3107        let owner = out
3108            .cmds
3109            .iter()
3110            .rposition(|cmd| cmd.flags.iter().any(|declared| declared.name == flag.name))
3111            .unwrap_or(out.cmds.len() - 1);
3112        if !exclusive_present && requirements_apply(owner) {
3113            for other in &flag.requires {
3114                if !selector_is_satisfied(other, &out, &overridden_flags, custom_env) {
3115                    let name = selector_flag_name(other, &out).unwrap_or_else(|| other.clone());
3116                    if other.starts_with('-') {
3117                        out.errors.push(UsageErr::MissingFlag(name));
3118                    } else {
3119                        out.errors.push(UsageErr::MissingArg(name));
3120                    }
3121                }
3122            }
3123            for condition in &flag.requires_if {
3124                if explicit_flag_has_value(flag, &condition.value, &out, custom_env)
3125                    && !selector_is_satisfied(
3126                        &condition.requires,
3127                        &out,
3128                        &overridden_flags,
3129                        custom_env,
3130                    )
3131                {
3132                    let name = selector_flag_name(&condition.requires, &out)
3133                        .unwrap_or_else(|| condition.requires.clone());
3134                    out.errors.push(UsageErr::MissingFlag(name));
3135                }
3136            }
3137        }
3138    }
3139
3140    // Positionals can declare the same pairwise conflict as flags. Their selector is
3141    // the bare argument name, while a flag keeps its dashed spelling.
3142    for (command_index, arg) in out
3143        .cmds
3144        .iter()
3145        .enumerate()
3146        .flat_map(|(index, cmd)| cmd.args.iter().map(move |arg| (index, arg)))
3147    {
3148        let given = arg_is_explicit(arg, &out, custom_env);
3149        if !given {
3150            continue;
3151        }
3152        for other in &arg.conflicts {
3153            if selector_is_explicit(other, &out, &overridden_flags, custom_env) {
3154                out.errors.push(UsageErr::InvalidFlag {
3155                    token: arg.name.clone(),
3156                    reason: format!("conflicts with {other}"),
3157                    span: (0, 0).into(),
3158                    input: format!("{} {other}", arg.name),
3159                });
3160            }
3161        }
3162        if !exclusive_present && requirements_apply(command_index) {
3163            for other in &arg.requires {
3164                if !selector_is_satisfied(other, &out, &overridden_flags, custom_env) {
3165                    let name = selector_flag_name(other, &out).unwrap_or_else(|| other.clone());
3166                    if other.starts_with('-') {
3167                        out.errors.push(UsageErr::MissingFlag(name));
3168                    } else {
3169                        out.errors.push(UsageErr::MissingArg(name));
3170                    }
3171                }
3172            }
3173        }
3174    }
3175
3176    // An exclusive flag is the whole-command form of a conflict: `--version` means the
3177    // rest of the line has nothing to act on. Everything the invocation supplied counts,
3178    // positionals included, which is what distinguishes it from being in a group with
3179    // every other flag.
3180    //
3181    // Only what was *given*, as `conflicts` reads it: a defaulted flag standing beside an
3182    // exclusive one is nobody saying anything, and counting it would make the exclusive
3183    // flag unusable on any command that has a default. Environment values do count, also as
3184    // `conflicts` reads them, so the spec parser and the derive agree.
3185    for flag in unique_flags(out.available_flags.values().chain(out.flags.keys()))
3186        .filter(|flag| !clause_flag_names.contains(&flag.name))
3187    {
3188        // `SpecFlag` equality is intentionally name-only for the public parsed-value map,
3189        // but re-declared aliases can leave distinct declarations with that same name in
3190        // scope. Exclusivity is about the declaration the typed spelling resolved to, so
3191        // compare the parser's `Arc`s by identity here.
3192        let given = flag_was_parsed(flag) || flag_has_env(flag, custom_env);
3193        if !exclusive_occurrence(flag) || !given || overridden_flags.contains(&flag.name) {
3194            continue;
3195        }
3196        let other_flag = unique_flags(out.available_flags.values().chain(out.flags.keys()))
3197            .filter(|other| !clause_flag_names.contains(&other.name))
3198            .find(|other| {
3199                !Arc::ptr_eq(other, flag)
3200                    && !overridden_flags.contains(&other.name)
3201                    && (flag_was_parsed(other) || flag_has_env(other, custom_env))
3202            })
3203            .map(|other| format!("--{}", other.name));
3204        let other_arg = active_args(&out.cmd).iter().find(|arg| {
3205            out.args.keys().any(|given| given.name == arg.name)
3206                || out
3207                    .clauses
3208                    .values()
3209                    .flatten()
3210                    .any(|instance| instance.keys().any(|given| given.name == arg.name))
3211                || arg
3212                    .env
3213                    .as_ref()
3214                    .is_some_and(|env| env_contains(custom_env, env))
3215        });
3216        // Selecting a child is company for an exclusive flag declared by an ancestor. An
3217        // exclusive flag belonging to the child itself does not conflict with the command word
3218        // needed to reach that child — so the question is not who owns the flag but whose
3219        // exclusivity is the one being enforced, which is what `exclusivity_in_play` already
3220        // separated.
3221        let (_, ancestor_exclusivity) = exclusivity_in_play(flag);
3222        let selected_subcommand =
3223            (out.cmds.len() > 1 && ancestor_exclusivity).then(|| out.cmd.name.clone());
3224        let other = other_flag
3225            .or_else(|| other_arg.map(|arg| format!("<{}>", arg.name)))
3226            .or(selected_subcommand);
3227        if let Some(other) = other {
3228            out.errors.push(UsageErr::InvalidFlag {
3229                token: format!("--{}", flag.name),
3230                reason: format!("must be given on its own, and {other} was given too"),
3231                span: (0, 0).into(),
3232                input: format!("--{} {other}", flag.name),
3233            });
3234        }
3235    }
3236
3237    // Groups, checked once per group rather than per flag: both questions a group asks —
3238    // how many members were given, and whether that is enough — are about the set, which
3239    // is the whole reason a group exists rather than a pile of pairwise conflicts.
3240    //
3241    // The same "given" rule as everything else here, so a member filled from the
3242    // environment or a default counts.
3243    // Every command in the chain, not only the selected one: a group may name global
3244    // flags, which belong to an ancestor and are declared there.
3245    let mut group_errors: Vec<UsageErr> = Vec::new();
3246    for (command_index, group) in out
3247        .cmds
3248        .iter()
3249        .enumerate()
3250        .flat_map(|(index, cmd)| cmd.groups.iter().map(move |group| (index, group)))
3251    {
3252        // Counted by the *flag* a selector resolves to, not by the selector. `-f` and
3253        // `--file` are two spellings of one flag, and a group naming both — or naming one
3254        // flag twice — would otherwise report that flag as conflicting with itself the
3255        // moment it was given. Deduplicated rather than refused where the group is
3256        // written, because listing both spellings is redundant, not wrong.
3257        let mut given: Vec<&str> = Vec::new();
3258        let mut seen: Vec<String> = Vec::new();
3259        for selector in &group.members {
3260            if !selector_is_explicit(selector, &out, &overridden_flags, custom_env) {
3261                continue;
3262            }
3263            let name = selector_flag_name(selector, &out).unwrap_or_else(|| selector.clone());
3264            if seen.contains(&name) {
3265                continue;
3266            }
3267            seen.push(name);
3268            given.push(selector.as_str());
3269        }
3270        if !group.multiple && given.len() > 1 {
3271            group_errors.push(UsageErr::InvalidFlag {
3272                token: given[1].to_string(),
3273                reason: format!("cannot be used with {} in group {}", given[0], group.name),
3274                span: (0, 0).into(),
3275                input: format!("{} {}", given[0], given[1]),
3276            });
3277        }
3278        // Requiredness is a *positive* rule, so it reads a default as filling a member —
3279        // the rule `requires` follows. That is also why it cannot reuse `given` above:
3280        // exclusivity must count only what was supplied, or a defaulted member would
3281        // collide with the sibling the user actually typed.
3282        let satisfied = group
3283            .members
3284            .iter()
3285            .any(|selector| selector_is_satisfied(selector, &out, &overridden_flags, custom_env));
3286        if group.required && requirements_apply(command_index) && !satisfied && !exclusive_present {
3287            // The members are what a user has to type, so they are in the message; the
3288            // group's name is there too, since a command with several groups would
3289            // otherwise report the same sentence twice with nothing to tell them apart.
3290            group_errors.push(UsageErr::MissingGroup {
3291                group: group.name.clone(),
3292                members: group.members.join(", "),
3293            });
3294        }
3295    }
3296    out.errors.extend(group_errors);
3297
3298    if !exclusive_present {
3299        for flag in unique_flags(out.available_flags.values())
3300            .filter(|flag| !clause_flag_names.contains(&flag.name))
3301        {
3302            let owner = out
3303                .cmds
3304                .iter()
3305                .rposition(|cmd| cmd.flags.iter().any(|declared| declared.name == flag.name))
3306                .unwrap_or(out.cmds.len() - 1);
3307            if !requirements_apply(owner) {
3308                continue;
3309            }
3310            if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
3311                continue;
3312            }
3313            let has_default =
3314                !flag.default.is_empty() || flag.arg.iter().any(|a| !a.default.is_empty());
3315            let has_env = flag_has_env(flag, custom_env);
3316            let required_if = flag.required_if.iter().any(|selector| {
3317                selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3318            });
3319            let required_if_eq = flag.required_if_eq.iter().any(|condition| {
3320                selector_explicit_has_value(
3321                    &condition.selector,
3322                    &condition.value,
3323                    &out,
3324                    &overridden_flags,
3325                    custom_env,
3326                )
3327            });
3328            let required_if_eq_all = !flag.required_if_eq_all.is_empty()
3329                && flag.required_if_eq_all.iter().all(|condition| {
3330                    selector_explicit_has_value(
3331                        &condition.selector,
3332                        &condition.value,
3333                        &out,
3334                        &overridden_flags,
3335                        custom_env,
3336                    )
3337                });
3338            let unless_any = flag.required_unless.iter().any(|selector| {
3339                selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3340            });
3341            let unless_all = !flag.required_unless_all.is_empty()
3342                && flag.required_unless_all.iter().all(|selector| {
3343                    selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3344                });
3345            let required_unless = !(unless_any
3346                || unless_all
3347                || (flag.required_unless.is_empty() && flag.required_unless_all.is_empty()));
3348            if (flag.required
3349                || required_if
3350                || required_if_eq
3351                || required_if_eq_all
3352                || required_unless)
3353                && !has_default
3354                && !has_env
3355            {
3356                out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
3357            }
3358        }
3359    }
3360
3361    // Validate var_min/var_max constraints for variadic args
3362    for (arg, value) in &out.args {
3363        if arg.var
3364            && let ParseValue::MultiString(values) = value
3365        {
3366            if let Some(min) = arg.var_min
3367                && values.len() < min
3368            {
3369                out.errors.push(UsageErr::VarArgTooFew {
3370                    name: arg.name.clone(),
3371                    min,
3372                    got: values.len(),
3373                });
3374            }
3375            if let Some(max) = arg.var_max
3376                && values.len() > max
3377            {
3378                out.errors.push(UsageErr::VarArgTooMany {
3379                    name: arg.name.clone(),
3380                    max,
3381                    got: values.len(),
3382                });
3383            }
3384        }
3385    }
3386
3387    // Validate var_min/var_max constraints for variadic flags. These are bounds on
3388    // repeated occurrences of the flag itself. Bounds on its nested argument are enforced
3389    // by binding once per occurrence, where the per-occurrence count is still available.
3390    for flag in unique_flags(out.available_flags.values())
3391        .filter(|flag| !clause_flag_names.contains(&flag.name))
3392    {
3393        if flag.var {
3394            let bound = match out.flags.get(flag) {
3395                Some(ParseValue::MultiString(values)) => values.len(),
3396                Some(ParseValue::MultiBool(values)) => values.len(),
3397                Some(_) => 1,
3398                None => 0,
3399            };
3400            // A partial parse deliberately leaves the final value-optional flag pending so
3401            // completion can still answer for it. It is nevertheless a real occurrence for
3402            // the repeated flag's bounds; the full parser closes it just after this phase.
3403            let pending = out
3404                .flag_awaiting_value
3405                .iter()
3406                .filter(|pending| {
3407                    Arc::ptr_eq(pending, flag)
3408                        && (pending.value_optional || pending.default_missing.is_some())
3409                })
3410                .count();
3411            let count = bound + pending;
3412            if count == 0 {
3413                continue;
3414            }
3415            if let Some(min) = flag.var_min
3416                && count < min
3417            {
3418                out.errors.push(UsageErr::VarFlagTooFew {
3419                    name: flag.name.clone(),
3420                    min,
3421                    got: count,
3422                });
3423            }
3424            if let Some(max) = flag.var_max
3425                && count > max
3426            {
3427                out.errors.push(UsageErr::VarFlagTooMany {
3428                    name: flag.name.clone(),
3429                    max,
3430                    got: count,
3431                });
3432            }
3433        }
3434    }
3435
3436    Ok((out, overridden_flags))
3437}
3438
3439fn validate_expression(
3440    name: &str,
3441    expression: Option<&str>,
3442    message: Option<&str>,
3443    parsed: &ParseValue,
3444    errors: &mut Vec<UsageErr>,
3445) {
3446    let Some(expression) = expression else {
3447        return;
3448    };
3449    #[cfg(not(feature = "validation"))]
3450    let _ = expression;
3451    let values: &[String] = match parsed {
3452        ParseValue::String(value) => std::slice::from_ref(value),
3453        ParseValue::MultiString(values) => values,
3454        ParseValue::Bool(_) | ParseValue::MultiBool(_) => return,
3455    };
3456    #[cfg(feature = "validation")]
3457    for value in values {
3458        let reason = match usage_validation::validate(expression, value) {
3459            Ok(true) => continue,
3460            Ok(false) => message
3461                .unwrap_or("does not satisfy the validation expression")
3462                .to_string(),
3463            Err(error) => format!("validation expression failed: {error}"),
3464        };
3465        errors.push(UsageErr::InvalidValue {
3466            name: name.to_string(),
3467            value: value.clone(),
3468            reason,
3469        });
3470        break;
3471    }
3472    #[cfg(not(feature = "validation"))]
3473    if let Some(value) = values.first() {
3474        let _ = message;
3475        errors.push(UsageErr::InvalidValue {
3476            name: name.to_string(),
3477            value: value.clone(),
3478            reason: "expression validation requires the `validation` feature".to_string(),
3479        });
3480    }
3481}
3482
3483#[cfg(all(test, not(feature = "validation")))]
3484mod optional_validation_tests {
3485    use crate::{Spec, parse};
3486
3487    #[test]
3488    fn validation_declarations_require_the_opt_in_runtime_feature() {
3489        let spec: Spec = r#"
3490name "ex"
3491bin "ex"
3492arg "<port>" validate="int(value) > 0"
3493        "#
3494        .parse()
3495        .unwrap();
3496        let error = parse(&spec, &["ex".to_string(), "1".to_string()]).unwrap_err();
3497        assert!(
3498            error
3499                .to_string()
3500                .contains("requires the `validation` feature"),
3501            "{error:?}"
3502        );
3503    }
3504}
3505
3506fn flag_matches_selector(flag: &SpecFlag, selector: &str) -> bool {
3507    flag.name == selector || flag_keys(flag).iter().any(|key| key == selector)
3508}
3509
3510fn flags_override(overrider: &SpecFlag, overridden: &SpecFlag) -> bool {
3511    overrider
3512        .overrides
3513        .iter()
3514        .any(|selector| flag_matches_selector(overridden, selector))
3515}
3516
3517fn apply_prefix_flag_overrides(
3518    prefix_flags: &mut Vec<(Arc<SpecFlag>, Vec<String>)>,
3519    flag: Arc<SpecFlag>,
3520) {
3521    prefix_flags
3522        .retain(|(other, _)| !(flags_override(&flag, other) || flags_override(other, &flag)));
3523}
3524
3525fn mount_prefix_words(prefix_flags: &[(Arc<SpecFlag>, Vec<String>)]) -> Vec<String> {
3526    prefix_flags
3527        .iter()
3528        .flat_map(|(_, words)| words.iter().cloned())
3529        .collect()
3530}
3531
3532fn env_contains(custom_env: Option<&HashMap<String, String>>, env_var: &str) -> bool {
3533    match custom_env {
3534        Some(env) => env.contains_key(env_var),
3535        None => std::env::var(env_var).is_ok(),
3536    }
3537}
3538
3539fn flag_has_env(flag: &SpecFlag, custom_env: Option<&HashMap<String, String>>) -> bool {
3540    flag.env_names()
3541        .any(|env_var| env_contains(custom_env, env_var))
3542}
3543
3544fn fallback_is_true(value: &str) -> bool {
3545    matches!(value, "1" | "true" | "True" | "TRUE")
3546}
3547
3548fn split_fallback_values(values: &[String], delimiter: Option<char>) -> Vec<String> {
3549    match delimiter {
3550        Some(delimiter) => values
3551            .iter()
3552            .flat_map(|value| value.split(delimiter).map(str::to_string))
3553            .collect(),
3554        None => values.to_vec(),
3555    }
3556}
3557
3558fn validate_arg_fallback_count(arg: &SpecArg, count: usize, errors: &mut Vec<UsageErr>) {
3559    if let Some(min) = arg.var_min
3560        && count < min
3561    {
3562        errors.push(UsageErr::VarArgTooFew {
3563            name: arg.name.clone(),
3564            min,
3565            got: count,
3566        });
3567    }
3568    if let Some(max) = arg.var_max
3569        && count > max
3570    {
3571        errors.push(UsageErr::VarArgTooMany {
3572            name: arg.name.clone(),
3573            max,
3574            got: count,
3575        });
3576    }
3577}
3578
3579fn validate_flag_fallback_count(flag: &SpecFlag, count: usize, errors: &mut Vec<UsageErr>) {
3580    if let Some(min) = flag.var_min
3581        && count < min
3582    {
3583        errors.push(UsageErr::VarFlagTooFew {
3584            name: flag.name.clone(),
3585            min,
3586            got: count,
3587        });
3588    }
3589    if let Some(max) = flag.var_max
3590        && count > max
3591    {
3592        errors.push(UsageErr::VarFlagTooMany {
3593            name: flag.name.clone(),
3594            max,
3595            got: count,
3596        });
3597    }
3598}
3599
3600fn validate_flag_arg_fallback_count(
3601    flag: &SpecFlag,
3602    arg: &SpecArg,
3603    count: usize,
3604    errors: &mut Vec<UsageErr>,
3605) {
3606    if let Some(min) = arg.var_min
3607        && count < min
3608    {
3609        errors.push(UsageErr::VarFlagTooFew {
3610            name: flag.name.clone(),
3611            min,
3612            got: count,
3613        });
3614    }
3615    if let Some(max) = arg.var_max
3616        && count > max
3617    {
3618        errors.push(UsageErr::VarFlagTooMany {
3619            name: flag.name.clone(),
3620            max,
3621            got: count,
3622        });
3623    }
3624}
3625
3626/// Bind a fallback the way an unconditional `default` does: one value, or several
3627/// for `var`, and choices checked the same way.
3628fn bind_flag_fallback(
3629    flag: &Arc<SpecFlag>,
3630    values: &[String],
3631    out: &mut ParseOutput,
3632    custom_env: Option<&HashMap<String, String>>,
3633    origin: ValueOrigin,
3634) -> Result<(), miette::Error> {
3635    let Some(value) = flag_fallback_value(flag, values, &mut out.errors, custom_env)? else {
3636        return Ok(());
3637    };
3638    out.flags.insert(Arc::clone(flag), value);
3639    out.flag_origins
3640        .entry(Arc::clone(flag))
3641        .or_default()
3642        .push(origin);
3643    Ok(())
3644}
3645
3646fn flag_fallback_value(
3647    flag: &SpecFlag,
3648    values: &[String],
3649    errors: &mut Vec<UsageErr>,
3650    custom_env: Option<&HashMap<String, String>>,
3651) -> Result<Option<ParseValue>, miette::Error> {
3652    if values.is_empty() {
3653        return Ok(None);
3654    }
3655    if let Some(arg) = flag.arg.as_ref() {
3656        let values = split_fallback_values(values, arg.delimiter);
3657        if flag.var || arg.var {
3658            if flag.var {
3659                validate_flag_fallback_count(flag, values.len(), errors);
3660            }
3661            if arg.var {
3662                validate_flag_arg_fallback_count(flag, arg, values.len(), errors);
3663            }
3664            validate_choice_values(
3665                ChoiceTarget::option(flag),
3666                &values,
3667                arg.choices.as_ref(),
3668                custom_env,
3669            )?;
3670            Ok(Some(ParseValue::MultiString(values)))
3671        } else {
3672            let value = values.into_iter().next().unwrap_or_default();
3673            validate_choice_value(
3674                ChoiceTarget::option(flag),
3675                &value,
3676                arg.choices.as_ref(),
3677                custom_env,
3678            )?;
3679            Ok(Some(ParseValue::String(value)))
3680        }
3681    } else if flag.var {
3682        validate_flag_fallback_count(flag, values.len(), errors);
3683        let bools: Vec<bool> = values.iter().map(|s| fallback_is_true(s)).collect();
3684        Ok(Some(ParseValue::MultiBool(bools)))
3685    } else {
3686        Ok(Some(ParseValue::Bool(fallback_is_true(&values[0]))))
3687    }
3688}
3689
3690/// Fill scoped flags inside the clause instances argv created.
3691///
3692/// The returned sets contain argv and environment sources only. Relationship
3693/// validation uses them to keep defaults from becoming explicit conflicts while
3694/// still allowing a fallback value to satisfy a required scoped flag.
3695fn apply_clause_flag_fallbacks(
3696    out: &mut ParseOutput,
3697    overridden_flags: &HashSet<String>,
3698    custom_env: Option<&HashMap<String, String>>,
3699) -> Result<Option<Vec<HashSet<String>>>, miette::Error> {
3700    let Some(clause) = out.cmd.clause.clone() else {
3701        return Ok(None);
3702    };
3703    let Some(positional_instances) = out.clauses.get(&clause.name) else {
3704        return Ok(None);
3705    };
3706    let positional_instances = positional_instances.clone();
3707    let mut flag_instances = out
3708        .clause_flags
3709        .shift_remove(&clause.name)
3710        .unwrap_or_default();
3711    flag_instances.resize_with(positional_instances.len(), IndexMap::new);
3712    let get_env = |key: &str| -> Option<String> {
3713        custom_env
3714            .and_then(|values| values.get(key).cloned())
3715            .or_else(|| {
3716                custom_env
3717                    .is_none()
3718                    .then(|| std::env::var(key).ok())
3719                    .flatten()
3720            })
3721    };
3722    let mut explicit_instances = Vec::with_capacity(flag_instances.len());
3723
3724    for (instance_index, scoped) in flag_instances.iter_mut().enumerate() {
3725        let positional = &positional_instances[instance_index];
3726        let mut explicit = scoped
3727            .keys()
3728            .map(|flag| flag.name.clone())
3729            .collect::<HashSet<_>>();
3730
3731        // Environment first so every default_if sees all explicit sources,
3732        // independent of declaration order.
3733        for declared in &clause.flags {
3734            let flag = Arc::new(declared.clone());
3735            if scoped.contains_key(&flag) {
3736                continue;
3737            }
3738            let Some((env_name, env_value)) = first_set_env(declared.env_names(), &get_env) else {
3739                continue;
3740            };
3741            if let Some(warning) = flag_deprecation(declared) {
3742                out.warnings.push(warning);
3743            }
3744            if flag_env_is_deprecated(declared, env_name) {
3745                out.warnings
3746                    .push(Warning::env(env_name, flag_current_env(declared)));
3747            }
3748            if let Some(value) = flag_fallback_value(
3749                declared,
3750                std::slice::from_ref(&env_value),
3751                &mut out.errors,
3752                custom_env,
3753            )? {
3754                scoped.insert(flag, value);
3755                explicit.insert(declared.name.clone());
3756            }
3757        }
3758
3759        let condition_matches =
3760            |condition: &crate::SpecDefaultIf| {
3761                if let Some(flag) = clause
3762                    .flags
3763                    .iter()
3764                    .find(|flag| flag_matches_selector(flag, &condition.selector))
3765                {
3766                    if !explicit.contains(&flag.name) {
3767                        return false;
3768                    }
3769                    return condition.when.as_ref().is_none_or(|expected| {
3770                        scoped
3771                            .iter()
3772                            .find(|(present, _)| present.name == flag.name)
3773                            .is_some_and(|(_, value)| parse_value_has(value, expected))
3774                    });
3775                }
3776                if let Some(arg) = clause.args.iter().find(|arg| {
3777                    !condition.selector.starts_with('-') && arg.name == condition.selector
3778                }) {
3779                    return positional.get(arg).is_some_and(|value| {
3780                        condition
3781                            .when
3782                            .as_ref()
3783                            .is_none_or(|expected| parse_value_has(value, expected))
3784                    });
3785                }
3786                let command_flag = out
3787                    .available_flags
3788                    .values()
3789                    .chain(out.flags.keys())
3790                    .filter(|flag| !is_clause_scoped_flag(out, flag))
3791                    .find(|flag| flag_matches_selector(flag, &condition.selector));
3792                command_flag.is_some_and(|flag| {
3793                    !overridden_flags.contains(&flag.name)
3794                        && command_flag_has_explicit_source(flag, out)
3795                        && condition.when.as_ref().is_none_or(|expected| {
3796                            out.flags
3797                                .get(flag)
3798                                .is_some_and(|value| parse_value_has(value, expected))
3799                        })
3800                })
3801            };
3802
3803        let conditional = clause
3804            .flags
3805            .iter()
3806            .filter(|flag| !scoped.keys().any(|present| present.name == flag.name))
3807            .filter_map(|flag| {
3808                flag.default_if
3809                    .iter()
3810                    .find(|condition| condition_matches(condition))
3811                    .map(|condition| (flag.clone(), condition.value.clone()))
3812            })
3813            .collect::<Vec<_>>();
3814        for (declared, value) in conditional {
3815            if let Some(value) = flag_fallback_value(
3816                &declared,
3817                std::slice::from_ref(&value),
3818                &mut out.errors,
3819                custom_env,
3820            )? {
3821                scoped.insert(Arc::new(declared), value);
3822            }
3823        }
3824
3825        for declared in &clause.flags {
3826            let flag = Arc::new(declared.clone());
3827            if scoped.contains_key(&flag) {
3828                continue;
3829            }
3830            let values = if !declared.default.is_empty() {
3831                &declared.default
3832            } else if let Some(arg) = declared.arg.as_ref().filter(|arg| !arg.default.is_empty()) {
3833                &arg.default
3834            } else {
3835                continue;
3836            };
3837            if let Some(value) = flag_fallback_value(declared, values, &mut out.errors, custom_env)?
3838            {
3839                scoped.insert(flag, value);
3840            }
3841        }
3842        explicit_instances.push(explicit);
3843    }
3844
3845    out.clause_flags.insert(clause.name, flag_instances);
3846    Ok(Some(explicit_instances))
3847}
3848
3849fn command_flag_has_explicit_source(flag: &SpecFlag, out: &ParseOutput) -> bool {
3850    out.flags.contains_key(flag)
3851        && out.flag_origins.get(flag).is_none_or(|origins| {
3852            origins
3853                .iter()
3854                .any(|origin| matches!(origin, ValueOrigin::DefaultMissing | ValueOrigin::Env(_)))
3855        })
3856}
3857
3858fn default_if_condition_matches(
3859    condition: &crate::SpecDefaultIf,
3860    out: &ParseOutput,
3861    overridden_flags: &HashSet<String>,
3862    custom_env: Option<&HashMap<String, String>>,
3863) -> bool {
3864    match &condition.when {
3865        None => selector_is_explicit(&condition.selector, out, overridden_flags, custom_env),
3866        Some(when) => {
3867            let Some(flag) = out
3868                .available_flags
3869                .values()
3870                .chain(out.flags.keys())
3871                .find(|flag| flag_matches_selector(flag, &condition.selector))
3872            else {
3873                return false;
3874            };
3875            if overridden_flags.contains(&flag.name) {
3876                return false;
3877            }
3878            explicit_flag_has_value(flag, when, out, custom_env)
3879        }
3880    }
3881}
3882
3883/// Whether an explicitly supplied value of `flag` equals `expected`.
3884///
3885/// clap treats command-line and environment values as explicit for `requires_if`, but
3886/// not defaults. Keep that source distinction here instead of consulting the flag's
3887/// defaults through `selector_is_satisfied`.
3888fn explicit_flag_has_value(
3889    flag: &SpecFlag,
3890    expected: &str,
3891    out: &ParseOutput,
3892    custom_env: Option<&HashMap<String, String>>,
3893) -> bool {
3894    let parsed_matches = out.flags.get(flag).is_some_and(|value| match value {
3895        ParseValue::Bool(value) => value.to_string() == expected,
3896        ParseValue::String(value) => value == expected,
3897        ParseValue::MultiBool(values) => values.iter().any(|value| value.to_string() == expected),
3898        ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3899    });
3900    if out.flags.contains_key(flag) {
3901        return parsed_matches;
3902    }
3903
3904    let value = flag.env_names().find_map(|env| match custom_env {
3905        Some(values) => values.get(env).cloned(),
3906        None => std::env::var(env).ok(),
3907    });
3908    value.is_some_and(
3909        |value| match flag.arg.as_ref().and_then(|arg| arg.delimiter) {
3910            Some(delimiter) => value.split(delimiter).any(|value| value == expected),
3911            None if flag.arg.is_none() => {
3912                matches!(value.as_str(), "1" | "true" | "True" | "TRUE").to_string() == expected
3913            }
3914            None => value == expected,
3915        },
3916    )
3917}
3918
3919fn parse_value_has(value: &ParseValue, expected: &str) -> bool {
3920    match value {
3921        ParseValue::Bool(value) => value.to_string() == expected,
3922        ParseValue::String(value) => value == expected,
3923        ParseValue::MultiBool(values) => values.iter().any(|value| value.to_string() == expected),
3924        ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3925    }
3926}
3927
3928/// Clause flags share the command's spelling table so they can be recognized while parsing,
3929/// but their values and validation belong to one clause instance rather than the command.
3930fn is_clause_scoped_flag(out: &ParseOutput, flag: &SpecFlag) -> bool {
3931    out.cmd
3932        .clause
3933        .as_ref()
3934        .is_some_and(|clause| clause.flags.iter().any(|scoped| scoped.name == flag.name))
3935}
3936
3937fn selected_clause_flag<'a>(out: &'a ParseOutput, selector: &str) -> Option<&'a SpecFlag> {
3938    out.cmd
3939        .clause
3940        .as_ref()?
3941        .flags
3942        .iter()
3943        .find(|flag| flag_matches_selector(flag, selector))
3944}
3945
3946fn clause_flag_is_explicit(out: &ParseOutput, flag: &SpecFlag) -> bool {
3947    out.clause_flags
3948        .values()
3949        .flatten()
3950        .any(|instance| instance.keys().any(|present| present.name == flag.name))
3951        || out.flags.keys().any(|present| present.name == flag.name)
3952}
3953
3954fn clause_flag_has_value(out: &ParseOutput, flag: &SpecFlag, expected: &str) -> bool {
3955    out.clause_flags.values().flatten().any(|instance| {
3956        instance
3957            .iter()
3958            .any(|(present, value)| present.name == flag.name && parse_value_has(value, expected))
3959    }) || out
3960        .flags
3961        .iter()
3962        .any(|(present, value)| present.name == flag.name && parse_value_has(value, expected))
3963}
3964
3965fn validate_clause_relationships(
3966    out: &mut ParseOutput,
3967    overridden_flags: &HashSet<String>,
3968    custom_env: Option<&HashMap<String, String>>,
3969    explicit_instances: Option<&[HashSet<String>]>,
3970) {
3971    let Some(clause) = out.cmd.clause.as_ref() else {
3972        return;
3973    };
3974    let Some(instances) = out.clauses.get(&clause.name) else {
3975        return;
3976    };
3977    let command_flag_is_explicit = |selector: &str| {
3978        out.available_flags
3979            .values()
3980            .chain(out.flags.keys())
3981            .filter(|flag| !is_clause_scoped_flag(out, flag))
3982            .any(|flag| {
3983                flag_matches_selector(flag, selector)
3984                    && !overridden_flags.contains(&flag.name)
3985                    && (out.flags.contains_key(flag) || flag_has_env(flag, custom_env))
3986            })
3987    };
3988    let command_flag_matches_value = |selector: &str, expected: &str| {
3989        out.available_flags
3990            .values()
3991            .chain(out.flags.keys())
3992            .filter(|flag| !is_clause_scoped_flag(out, flag))
3993            .find(|flag| flag_matches_selector(flag, selector))
3994            .is_some_and(|flag| {
3995                !overridden_flags.contains(&flag.name)
3996                    && explicit_flag_has_value(flag, expected, out, custom_env)
3997            })
3998    };
3999    let command_flag_is_satisfied = |selector: &str| {
4000        out.available_flags
4001            .values()
4002            .chain(out.flags.keys())
4003            .filter(|flag| !is_clause_scoped_flag(out, flag))
4004            .any(|flag| flag_matches_selector(flag, selector))
4005            && selector_is_satisfied(selector, out, overridden_flags, custom_env)
4006    };
4007    let mut errors = Vec::new();
4008    for (instance_index, instance) in instances.iter().enumerate() {
4009        let explicit = explicit_instances.and_then(|instances| instances.get(instance_index));
4010        let scoped = out
4011            .clause_flags
4012            .get(&clause.name)
4013            .and_then(|instances| instances.get(instance_index));
4014        let scoped_flag = |selector: &str| {
4015            clause
4016                .flags
4017                .iter()
4018                .find(|flag| flag_matches_selector(flag, selector))
4019        };
4020        let scoped_value = |flag: &SpecFlag| {
4021            scoped.and_then(|values| {
4022                values
4023                    .iter()
4024                    .find(|(present, _)| present.name == flag.name)
4025                    .map(|(_, value)| value)
4026            })
4027        };
4028        let arg_is_explicit = |selector: &str| {
4029            instance
4030                .keys()
4031                .any(|arg| !selector.starts_with('-') && arg.name == selector)
4032        };
4033        let selector_is_explicit = |selector: &str| {
4034            scoped_flag(selector).is_some_and(|flag| {
4035                explicit
4036                    .map(|given| given.contains(&flag.name))
4037                    .unwrap_or_else(|| scoped_value(flag).is_some())
4038            }) || command_flag_is_explicit(selector)
4039                || arg_is_explicit(selector)
4040        };
4041        let selector_has_value = |selector: &str, expected: &str| {
4042            scoped_flag(selector)
4043                .filter(|flag| {
4044                    explicit
4045                        .map(|given| given.contains(&flag.name))
4046                        .unwrap_or_else(|| scoped_value(flag).is_some())
4047                })
4048                .and_then(scoped_value)
4049                .is_some_and(|value| parse_value_has(value, expected))
4050                || command_flag_matches_value(selector, expected)
4051                || instance.iter().any(|(arg, value)| {
4052                    !selector.starts_with('-')
4053                        && arg.name == selector
4054                        && parse_value_has(value, expected)
4055                })
4056        };
4057        let selector_is_satisfied = |selector: &str| {
4058            scoped_flag(selector).is_some_and(|flag| scoped_value(flag).is_some())
4059                || command_flag_is_satisfied(selector)
4060                || arg_is_explicit(selector)
4061        };
4062        let selector_name = |selector: &str| {
4063            scoped_flag(selector)
4064                .map(|flag| flag.name.clone())
4065                .or_else(|| selector_flag_name(selector, out))
4066                .unwrap_or_else(|| selector.to_string())
4067        };
4068
4069        for flag in &clause.flags {
4070            let value = scoped_value(flag);
4071            if value.is_none() {
4072                let required_if = flag
4073                    .required_if
4074                    .iter()
4075                    .any(|selector| selector_is_explicit(selector));
4076                let required_if_eq = flag
4077                    .required_if_eq
4078                    .iter()
4079                    .any(|condition| selector_has_value(&condition.selector, &condition.value));
4080                let required_if_eq_all = !flag.required_if_eq_all.is_empty()
4081                    && flag
4082                        .required_if_eq_all
4083                        .iter()
4084                        .all(|condition| selector_has_value(&condition.selector, &condition.value));
4085                let unless_any = flag
4086                    .required_unless
4087                    .iter()
4088                    .any(|selector| selector_is_explicit(selector));
4089                let unless_all = !flag.required_unless_all.is_empty()
4090                    && flag
4091                        .required_unless_all
4092                        .iter()
4093                        .all(|selector| selector_is_explicit(selector));
4094                let required_unless = !(unless_any
4095                    || unless_all
4096                    || (flag.required_unless.is_empty() && flag.required_unless_all.is_empty()));
4097                if flag.required
4098                    || required_if
4099                    || required_if_eq
4100                    || required_if_eq_all
4101                    || required_unless
4102                {
4103                    errors.push(UsageErr::MissingFlag(flag.name.clone()));
4104                }
4105                continue;
4106            }
4107
4108            let explicitly_provided = explicit
4109                .map(|given| given.contains(&flag.name))
4110                .unwrap_or(true);
4111
4112            if explicitly_provided {
4113                for other in &flag.conflicts {
4114                    if selector_is_explicit(other) {
4115                        errors.push(UsageErr::InvalidFlag {
4116                            token: format!("--{}", flag.name),
4117                            reason: format!("conflicts with {other}"),
4118                            span: (0, 0).into(),
4119                            input: format!("--{} {other}", flag.name),
4120                        });
4121                    }
4122                }
4123                for other in &flag.requires {
4124                    if !selector_is_satisfied(other) {
4125                        errors.push(UsageErr::MissingFlag(selector_name(other)));
4126                    }
4127                }
4128                for condition in &flag.requires_if {
4129                    if value.is_some_and(|value| parse_value_has(value, &condition.value))
4130                        && !selector_is_satisfied(&condition.requires)
4131                    {
4132                        errors.push(UsageErr::MissingFlag(selector_name(&condition.requires)));
4133                    }
4134                }
4135            }
4136            if let (true, Some(value)) = (flag.var, value) {
4137                let count = match value {
4138                    ParseValue::MultiString(values) => values.len(),
4139                    ParseValue::MultiBool(values) => values.len(),
4140                    _ => 1,
4141                };
4142                if let Some(min) = flag.var_min.filter(|min| count < *min) {
4143                    errors.push(UsageErr::VarFlagTooFew {
4144                        name: flag.name.clone(),
4145                        min,
4146                        got: count,
4147                    });
4148                }
4149                if let Some(max) = flag.var_max.filter(|max| count > *max) {
4150                    errors.push(UsageErr::VarFlagTooMany {
4151                        name: flag.name.clone(),
4152                        max,
4153                        got: count,
4154                    });
4155                }
4156            }
4157            if let (Some(arg), Some(value)) = (&flag.arg, value) {
4158                validate_expression(
4159                    &flag.name,
4160                    arg.validate.as_deref(),
4161                    arg.validate_error.as_deref(),
4162                    value,
4163                    &mut errors,
4164                );
4165            }
4166        }
4167        for arg in &clause.args {
4168            let given = instance.keys().any(|present| present.name == arg.name);
4169            if !given {
4170                let required_if = arg
4171                    .required_if
4172                    .iter()
4173                    .any(|selector| selector_is_explicit(selector));
4174                let required_if_eq = arg
4175                    .required_if_eq
4176                    .iter()
4177                    .any(|condition| selector_has_value(&condition.selector, &condition.value));
4178                let required_if_eq_all = !arg.required_if_eq_all.is_empty()
4179                    && arg
4180                        .required_if_eq_all
4181                        .iter()
4182                        .all(|condition| selector_has_value(&condition.selector, &condition.value));
4183                let unless_any = arg
4184                    .required_unless
4185                    .iter()
4186                    .any(|selector| selector_is_explicit(selector));
4187                let unless_all = !arg.required_unless_all.is_empty()
4188                    && arg
4189                        .required_unless_all
4190                        .iter()
4191                        .all(|selector| selector_is_explicit(selector));
4192                let required_unless = !(unless_any
4193                    || unless_all
4194                    || (arg.required_unless.is_empty() && arg.required_unless_all.is_empty()));
4195                if required_if || required_if_eq || required_if_eq_all || required_unless {
4196                    errors.push(UsageErr::MissingClauseArg {
4197                        clause: clause.name.clone(),
4198                        instance: instance_index + 1,
4199                        arg: arg.name.clone(),
4200                    });
4201                }
4202                continue;
4203            }
4204            for other in &arg.conflicts {
4205                if selector_is_explicit(other) {
4206                    errors.push(UsageErr::InvalidFlag {
4207                        token: arg.name.clone(),
4208                        reason: format!("conflicts with {other}"),
4209                        span: (0, 0).into(),
4210                        input: format!("{} {other}", arg.name),
4211                    });
4212                }
4213            }
4214            for other in &arg.requires {
4215                if selector_is_satisfied(other) {
4216                    continue;
4217                }
4218                if other.starts_with('-') {
4219                    errors.push(UsageErr::MissingFlag(selector_name(other)));
4220                } else {
4221                    errors.push(UsageErr::MissingClauseArg {
4222                        clause: clause.name.clone(),
4223                        instance: instance_index + 1,
4224                        arg: other.clone(),
4225                    });
4226                }
4227            }
4228        }
4229    }
4230    out.errors.extend(errors);
4231}
4232
4233fn selector_explicit_has_value(
4234    selector: &str,
4235    expected: &str,
4236    out: &ParseOutput,
4237    overridden_flags: &HashSet<String>,
4238    custom_env: Option<&HashMap<String, String>>,
4239) -> bool {
4240    if let Some(flag) = selected_clause_flag(out, selector) {
4241        return clause_flag_has_value(out, flag, expected);
4242    }
4243    if let Some(flag) = out
4244        .available_flags
4245        .values()
4246        .chain(out.flags.keys())
4247        .filter(|flag| !is_clause_scoped_flag(out, flag))
4248        .find(|flag| flag_matches_selector(flag, selector))
4249    {
4250        return !overridden_flags.contains(&flag.name)
4251            && explicit_flag_has_value(flag, expected, out, custom_env);
4252    }
4253    let Some(arg) = selector_arg(selector, out) else {
4254        return false;
4255    };
4256    let parsed = out
4257        .args
4258        .iter()
4259        .find(|(given, _)| given.name == arg.name)
4260        .map(|(_, value)| value)
4261        .or_else(|| {
4262            out.clauses.values().flatten().find_map(|instance| {
4263                instance
4264                    .iter()
4265                    .find(|(given, _)| given.name == arg.name)
4266                    .map(|(_, value)| value)
4267            })
4268        });
4269    if let Some(value) = parsed {
4270        return match value {
4271            ParseValue::String(value) => value == expected,
4272            ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
4273            ParseValue::Bool(value) => value.to_string() == expected,
4274            ParseValue::MultiBool(values) => {
4275                values.iter().any(|value| value.to_string() == expected)
4276            }
4277        };
4278    }
4279    let value = arg.env_names().find_map(|env| match custom_env {
4280        Some(values) => values.get(env).cloned(),
4281        None => std::env::var(env).ok(),
4282    });
4283    value.is_some_and(|value| match arg.delimiter {
4284        Some(delimiter) => value.split(delimiter).any(|value| value == expected),
4285        None => value == expected,
4286    })
4287}
4288
4289fn selector_is_explicit(
4290    selector: &str,
4291    out: &ParseOutput,
4292    overridden_flags: &HashSet<String>,
4293    custom_env: Option<&HashMap<String, String>>,
4294) -> bool {
4295    let scoped_flag_is_explicit =
4296        selected_clause_flag(out, selector).is_some_and(|flag| clause_flag_is_explicit(out, flag));
4297    let flag_is_explicit = out
4298        .available_flags
4299        .values()
4300        .chain(out.flags.keys())
4301        .filter(|flag| !is_clause_scoped_flag(out, flag))
4302        .any(|flag| {
4303            flag_matches_selector(flag, selector)
4304                && !overridden_flags.contains(&flag.name)
4305                && (out.flags.contains_key(flag) || flag_has_env(flag, custom_env))
4306        });
4307    scoped_flag_is_explicit
4308        || flag_is_explicit
4309        || selector_arg(selector, out).is_some_and(|arg| arg_is_explicit(arg, out, custom_env))
4310}
4311
4312/// The name of the flag a selector points at, for an error that has to name it.
4313///
4314/// `selector_is_explicit` only answers yes or no, which is all a check needs; a message
4315/// about a flag that is *missing* has to say which one, and the selector may be a short
4316/// form or an alias rather than the name.
4317fn selector_flag_name(selector: &str, out: &ParseOutput) -> Option<String> {
4318    if let Some(flag) = selected_clause_flag(out, selector) {
4319        return Some(flag.name.clone());
4320    }
4321    out.available_flags
4322        .values()
4323        .chain(out.flags.keys())
4324        .filter(|flag| !is_clause_scoped_flag(out, flag))
4325        .find(|flag| flag_matches_selector(flag, selector))
4326        .map(|flag| flag.name.clone())
4327        .or_else(|| selector_arg(selector, out).map(|arg| arg.name.clone()))
4328}
4329
4330/// Whether a selector's flag ended up with a value, however it got one.
4331///
4332/// The rule for a *positive* relationship, and the difference from
4333/// [`selector_is_explicit`] is deliberate. A negative rule — `conflicts`, or a group's
4334/// exclusivity — has to count only what was given, or a flag with a default would
4335/// conflict with everything and no command line would parse. A positive one asks whether
4336/// the flag it names has a value, and a default is a value: that is already how plain
4337/// `required`, `required_if` and `required_unless` read a default, and `requires` saying
4338/// otherwise would have made the same flag missing here and present ten lines below.
4339fn selector_is_satisfied(
4340    selector: &str,
4341    out: &ParseOutput,
4342    overridden_flags: &HashSet<String>,
4343    custom_env: Option<&HashMap<String, String>>,
4344) -> bool {
4345    if selector_is_explicit(selector, out, overridden_flags, custom_env) {
4346        return true;
4347    }
4348    let flag_is_satisfied = out
4349        .available_flags
4350        .values()
4351        .chain(out.flags.keys())
4352        .filter(|flag| !is_clause_scoped_flag(out, flag))
4353        .filter(|flag| flag_matches_selector(flag, selector))
4354        .any(|flag| {
4355            !overridden_flags.contains(&flag.name)
4356                && (!flag.default.is_empty()
4357                    || flag.arg.iter().any(|a| !a.default.is_empty())
4358                    || flag.default_if.iter().any(|condition| {
4359                        default_if_condition_matches(condition, out, overridden_flags, custom_env)
4360                    }))
4361        });
4362    flag_is_satisfied || selector_arg(selector, out).is_some_and(|arg| !arg.default.is_empty())
4363}
4364
4365fn selector_arg<'a>(selector: &str, out: &'a ParseOutput) -> Option<&'a SpecArg> {
4366    // Bare words are positional selectors. Keep accepting a flag's internal name above
4367    // for existing specs; when both exist, the dashed flag spelling removes ambiguity.
4368    if selector.starts_with('-') {
4369        return None;
4370    }
4371    out.cmds
4372        .iter()
4373        .flat_map(active_args)
4374        .find(|arg| arg.name == selector)
4375}
4376
4377fn arg_is_explicit(
4378    arg: &SpecArg,
4379    out: &ParseOutput,
4380    custom_env: Option<&HashMap<String, String>>,
4381) -> bool {
4382    out.args.keys().any(|given| given.name == arg.name)
4383        || out
4384            .clauses
4385            .values()
4386            .flatten()
4387            .any(|instance| instance.keys().any(|given| given.name == arg.name))
4388        || arg
4389            .env
4390            .as_ref()
4391            .is_some_and(|env| env_contains(custom_env, env))
4392}
4393
4394fn apply_flag_overrides(
4395    flag: &Arc<SpecFlag>,
4396    available_flags: &BTreeMap<String, Arc<SpecFlag>>,
4397    parsed_flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4398    pending_flags: &mut Vec<Arc<SpecFlag>>,
4399    overridden_flags: &mut HashSet<String>,
4400    // The reportable half of the same fact: which flag did the overriding. The set above
4401    // only stops a default or an environment value restoring what was overridden, and
4402    // "`--quiet` is unset despite its default" has no answer without the name.
4403    attributed: &mut BTreeMap<String, String>,
4404) {
4405    let overridden_names: HashSet<String> = available_flags
4406        .values()
4407        .chain(parsed_flags.keys())
4408        .filter(|other| flags_override(flag, other) || flags_override(other, flag))
4409        .map(|other| other.name.clone())
4410        .collect();
4411
4412    parsed_flags.retain(|parsed, _| !overridden_names.contains(&parsed.name));
4413    pending_flags.retain(|pending| !overridden_names.contains(&pending.name));
4414    for name in &overridden_names {
4415        attributed.insert(name.clone(), flag.name.clone());
4416    }
4417    overridden_flags.extend(overridden_names);
4418    // An explicit occurrence always restores this flag, including self-overrides.
4419    overridden_flags.remove(&flag.name);
4420    attributed.remove(&flag.name);
4421}
4422
4423/// The help a parse answers `--help` with. A parse is a program running, so its page lists
4424/// what `choices run=` prints, run against the environment the parse was given.
4425#[cfg(feature = "cli-help")]
4426fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool, env: Env<'_>) -> UsageErr {
4427    UsageErr::Help(docs::cli::render_runtime_help(
4428        spec,
4429        cmd,
4430        long,
4431        docs::cli::Style::PLAIN,
4432        env,
4433    ))
4434}
4435
4436#[cfg(feature = "cli-help")]
4437fn render_help_all_err(spec: &Spec, cmd: &SpecCommand, env: Env<'_>) -> UsageErr {
4438    fn append(out: &mut String, spec: &Spec, cmd: &SpecCommand, env: Env<'_>) {
4439        if !out.is_empty() {
4440            out.push('\n');
4441        }
4442        out.push_str(&docs::cli::render_runtime_help(
4443            spec,
4444            cmd,
4445            true,
4446            docs::cli::Style::PLAIN,
4447            env,
4448        ));
4449        let mut children: Vec<_> = cmd
4450            .subcommands
4451            .values()
4452            .filter(|child| !child.hide)
4453            .collect();
4454        children.sort_by_key(|child| (child.display_order.unwrap_or(999), child.name.as_str()));
4455        for child in children {
4456            append(out, spec, child, env);
4457        }
4458    }
4459
4460    let mut out = String::new();
4461    append(&mut out, spec, cmd, env);
4462    UsageErr::Help(out)
4463}
4464
4465#[cfg(not(feature = "cli-help"))]
4466fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool, _env: Env<'_>) -> UsageErr {
4467    UsageErr::Help("help".to_string())
4468}
4469
4470#[cfg(not(feature = "cli-help"))]
4471fn render_help_all_err(_spec: &Spec, _cmd: &SpecCommand, _env: Env<'_>) -> UsageErr {
4472    UsageErr::Help("help".to_string())
4473}
4474
4475/// The version to answer with. `--version` prefers the long text and `-V` the concise
4476/// one, each falling back to the other when only one is declared.
4477fn render_version_err(spec: &Spec, long: bool) -> UsageErr {
4478    let value = if long {
4479        spec.long_version.as_ref().or(spec.version.as_ref())
4480    } else {
4481        spec.version.as_ref().or(spec.long_version.as_ref())
4482    };
4483    UsageErr::Version(value.cloned().unwrap_or_default())
4484}
4485
4486/// The environment a parse was given, which is also what a `choices run=` command runs with.
4487type Env<'a> = Option<&'a HashMap<String, String>>;
4488
4489fn render_action_err(
4490    spec: &Spec,
4491    cmd: &SpecCommand,
4492    flag: &SpecFlag,
4493    spelling: &str,
4494    env: Env<'_>,
4495) -> UsageErr {
4496    use crate::SpecFlagAction;
4497    match flag.action {
4498        SpecFlagAction::Help => render_help_err(spec, cmd, spelling.starts_with("--"), env),
4499        SpecFlagAction::HelpShort => render_help_err(spec, cmd, false, env),
4500        SpecFlagAction::HelpLong => render_help_err(spec, cmd, true, env),
4501        SpecFlagAction::HelpAll => render_help_all_err(spec, cmd, env),
4502        SpecFlagAction::Version => render_version_err(spec, spelling.starts_with("--")),
4503        SpecFlagAction::Set => unreachable!("binding actions are handled before this helper"),
4504    }
4505}
4506
4507/// Report a required flag value that was displaced by a later option.
4508fn render_missing_flag_value(flag: &SpecFlag, following: &str) -> UsageErr {
4509    let token = flag
4510        .long
4511        .first()
4512        .map(|long| format!("--{long}"))
4513        .or_else(|| flag.short.first().map(|short| format!("-{short}")))
4514        .unwrap_or_else(|| flag.name.clone());
4515    UsageErr::InvalidFlag {
4516        token: token.clone(),
4517        reason: "requires an argument".to_string(),
4518        span: (0, 0).into(),
4519        input: format!("{token} {following}"),
4520    }
4521}
4522
4523#[derive(Copy, Clone)]
4524struct ChoiceTarget<'a> {
4525    kind: &'a str,
4526    name: &'a str,
4527}
4528
4529impl<'a> ChoiceTarget<'a> {
4530    fn arg(arg: &'a SpecArg) -> Self {
4531        Self {
4532            kind: "arg",
4533            name: &arg.name,
4534        }
4535    }
4536
4537    fn option(flag: &'a SpecFlag) -> Self {
4538        Self {
4539            kind: "option",
4540            name: &flag.name,
4541        }
4542    }
4543}
4544
4545/// Whether every letter of a short token names a flag in scope.
4546///
4547/// Scanning stops at the first letter whose flag takes a value, because everything
4548/// after it is that value rather than more letters.
4549fn short_bundle_is_known(
4550    spec: &Spec,
4551    cmds: &[SpecCommand],
4552    available: &BTreeMap<String, Arc<SpecFlag>>,
4553    token: &str,
4554) -> bool {
4555    for c in token.chars().skip(1) {
4556        match available.get(&format!("-{c}")) {
4557            // `-h` and `-V` are recognized letters even though no spec declares them, so a
4558            // bundle containing one is a bundle. Without this `-vh` was not read as one at
4559            // all and fell through to `unexpected word`, while usage-argv, usage-go and
4560            // clap all answer it with help.
4561            None if supplies_short(spec, cmds, c) => {}
4562            None => return false,
4563            Some(f) if f.arg.is_some() => return true,
4564            Some(_) => {}
4565        }
4566    }
4567    true
4568}
4569
4570/// The response `-h` or `-V` produces where nothing declares that letter.
4571///
4572/// The letter form of the flags the parser supplies rather than a spec declaring them,
4573/// under exactly the conditions [`is_help_arg`] and [`is_version_arg`] state — asked
4574/// about here one letter at a time, because a bundle is read one letter at a time.
4575///
4576/// Always the short response: `-h` is short help however many letters share its token,
4577/// and `-V` the concise version. The long forms belong to the long spellings. `-?` is not
4578/// here — it is a whole-token spelling of `-h` rather than a letter anyone bundles.
4579fn supplied_short(
4580    spec: &Spec,
4581    cmds: &[SpecCommand],
4582    letter: char,
4583    env: Env<'_>,
4584) -> Option<UsageErr> {
4585    let cmd = cmds.last()?;
4586    match letter {
4587        'h' if is_help_arg(spec, cmd, "-h") => Some(render_help_err(spec, cmd, false, env)),
4588        'V' if is_version_arg(spec, cmds, "-V") => Some(render_version_err(spec, false)),
4589        _ => None,
4590    }
4591}
4592
4593/// Whether [`supplied_short`] would answer for `letter`, without drawing the page it would
4594/// answer with — which can mean running `choices run=` commands.
4595fn supplies_short(spec: &Spec, cmds: &[SpecCommand], letter: char) -> bool {
4596    cmds.last().is_some_and(|cmd| match letter {
4597        'h' => is_help_arg(spec, cmd, "-h"),
4598        'V' => is_version_arg(spec, cmds, "-V"),
4599        _ => false,
4600    })
4601}
4602
4603/// Refuse a flag-like token that named nothing, if this command asked for that.
4604///
4605/// Called from the flag branches, where the lookup has just failed and nothing from
4606/// the token has been applied yet — so a bundle like `-az` is refused whole rather
4607/// than after setting `-a`.
4608fn reject_unknown_flag_if_asked(
4609    spec: &Spec,
4610    path: &[SpecCommand],
4611    token: &str,
4612) -> Result<(), UsageErr> {
4613    // A lone `-` is a value by convention. A negative number reaches this only
4614    // when no pending value opted into the narrower exception.
4615    if !is_flag_like(token) {
4616        return Ok(());
4617    }
4618    if effective_unknown_flags(spec, path) != UnknownFlags::Error {
4619        return Ok(());
4620    }
4621    Err(UsageErr::InvalidFlag {
4622        token: token.to_string(),
4623        reason: "no such flag".to_string(),
4624        span: (0, 0).into(),
4625        input: token.to_string(),
4626    })
4627}
4628
4629/// Whether a flag-like token that matches nothing is a value or an error, here.
4630///
4631/// The nearest enclosing command that stated a preference wins, then the spec,
4632/// then the default. Inherited, unlike `effect`: it describes how a command line
4633/// is read, and a CLI that forwards options tends to forward them at every level.
4634fn effective_unknown_flags(spec: &Spec, path: &[SpecCommand]) -> UnknownFlags {
4635    path.iter()
4636        .rev()
4637        .find_map(|cmd| cmd.unknown_flags)
4638        .or(spec.unknown_flags)
4639        .unwrap_or_default()
4640}
4641
4642/// Whether a token would be read as a flag, for the purpose of rejecting unknown
4643/// ones.
4644///
4645/// A lone `-` is a value by convention. Other dash-prefixed tokens are flag-like;
4646/// a field may make the narrower negative-number exception.
4647fn is_flag_like(token: &str) -> bool {
4648    match token.strip_prefix('-') {
4649        None | Some("") => false,
4650        Some(_) => true,
4651    }
4652}
4653
4654fn is_negative_number(token: &str) -> bool {
4655    token.strip_prefix('-').is_some_and(is_number)
4656}
4657
4658/// Whether a flag may claim its following token as a detached value.
4659///
4660/// A required value keeps the historical negative-number exception. A value
4661/// that may be omitted needs an explicit opt-in to distinguish a negative value
4662/// from the flag's bare form.
4663fn accepts_detached_flag_value(flag: &SpecFlag, token: &str) -> bool {
4664    !flag.require_equals
4665        && (!is_flag_like(token)
4666            || flag.allow_hyphen_values()
4667            || (is_negative_number(token)
4668                && (flag
4669                    .arg
4670                    .as_ref()
4671                    .is_some_and(|arg| arg.allow_negative_numbers)
4672                    || (flag.default_missing.is_none() && !flag.value_optional))))
4673}
4674
4675fn record_scalar_flag_occurrence(
4676    cmds: &[SpecCommand],
4677    flag: &Arc<SpecFlag>,
4678    command_level: usize,
4679    bool_value: Option<bool>,
4680    occurrences: &mut HashMap<(usize, usize), u8>,
4681    errors: &mut Vec<UsageErr>,
4682) {
4683    let strict = cmds.get(command_level).is_some_and(|cmd| {
4684        !cmd.args_override_self
4685            || cmd
4686                .clause
4687                .as_ref()
4688                .is_some_and(|clause| clause.flags.iter().any(|candidate| candidate == &**flag))
4689    });
4690    let collects_values = flag.var || flag.arg.as_ref().is_some_and(|arg| arg.var);
4691    if !strict || flag.count || collects_values {
4692        return;
4693    }
4694
4695    let bit = match bool_value {
4696        Some(false) if flag.negate.is_some() => 0b10,
4697        _ => 0b01,
4698    };
4699    let key = (Arc::as_ptr(flag) as usize, command_level);
4700    let seen = occurrences.entry(key).or_default();
4701    if *seen & bit != 0 {
4702        errors.push(UsageErr::DuplicateFlag(flag.name.clone()));
4703    }
4704    *seen |= bit;
4705}
4706
4707/// A token that can select a subcommand, trigger a mount, or be forwarded as an
4708/// external command.
4709///
4710/// Flag-like tokens are not words. A lone `-` is a value — conventionally stdin —
4711/// so it was never a candidate to *select* anything either. usage-argv uses the
4712/// same rule; without it, `-1` skipped the external-subcommand path because Phase 1
4713/// treated every token that `starts_with('-')` as a flag.
4714fn is_command_word(token: &str) -> bool {
4715    (!is_flag_like(token) || is_negative_number(token)) && token != "-"
4716}
4717
4718/// Whether an otherwise numeric-looking token is an exact declared short flag.
4719///
4720/// This check belongs in both parse phases: phase 1 must skip the flag while it
4721/// searches for a later subcommand, and phase 2 must bind it instead of offering
4722/// it to an `allow_negative_numbers` positional.
4723fn declared_numeric_short(available_flags: &BTreeMap<String, Arc<SpecFlag>>, token: &str) -> bool {
4724    token.len() == 2 && token.as_bytes()[1].is_ascii_digit() && available_flags.contains_key(token)
4725}
4726
4727/// Whether an unmatched word belongs to the root's default command.
4728///
4729/// Ordinary words always do. A negative number only does when the default command's
4730/// first positional explicitly accepts one; otherwise it stays at the root, matching
4731/// usage-argv and generated Go.
4732fn default_accepts_word(cmd: &SpecCommand, default_name: &str, token: &str) -> bool {
4733    !is_negative_number(token)
4734        || cmd
4735            .find_subcommand(default_name)
4736            .and_then(|default| default.args.first())
4737            .is_some_and(|arg| arg.allow_negative_numbers)
4738}
4739
4740/// Digits, at most one `.`, and an optional exponent.
4741///
4742/// Spelled out rather than deferred to `f64::from_str`, which also accepts `inf` and
4743/// `NaN`: `-inf` is far likelier to be a misspelled flag than a number somebody meant
4744/// to pass. usage-argv implements the same rule, and the corpus pins the edges — the
4745/// two disagreed about `-1e5` when one used a float parse and the other did not.
4746fn is_number(rest: &str) -> bool {
4747    let (mantissa, exponent) = match rest.find(['e', 'E']) {
4748        Some(at) => (&rest[..at], Some(&rest[at + 1..])),
4749        None => (rest, None),
4750    };
4751
4752    let mut seen_digit = false;
4753    let mut seen_dot = false;
4754    for c in mantissa.chars() {
4755        match c {
4756            '0'..='9' => seen_digit = true,
4757            '.' if !seen_dot => seen_dot = true,
4758            _ => return false,
4759        }
4760    }
4761    if !seen_digit {
4762        return false;
4763    }
4764
4765    match exponent {
4766        None => true,
4767        Some(exp) => {
4768            let digits = exp
4769                .strip_prefix('+')
4770                .or_else(|| exp.strip_prefix('-'))
4771                .unwrap_or(exp);
4772            !digits.is_empty() && digits.chars().all(|c| c.is_ascii_digit())
4773        }
4774    }
4775}
4776
4777/// Bind one value to the flag waiting for it, and let a variadic argument go on
4778/// collecting from the words that follow.
4779///
4780/// Every route to a flag's value comes through here — the following word, the text
4781/// after an `=`, and the token a `allow_hyphen_values` flag takes whatever it looks
4782/// like — so that all three agree on how many values the flag ends up with.
4783#[allow(clippy::too_many_arguments)]
4784fn bind_pending_flag_value(
4785    spec: &Spec,
4786    cmd: &SpecCommand,
4787    errors: &mut Vec<UsageErr>,
4788    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4789    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
4790    word: &mut String,
4791    input: &mut VecDeque<Token>,
4792    custom_env: Option<&HashMap<String, String>>,
4793    trace: &mut Trace,
4794    // Which token supplied `word`, and whether it rode along on the flag's own token
4795    // (`--jobs=8`, `-j8`) rather than following it. A variadic run's later words carry
4796    // their own positions and are recorded where they are read.
4797    argv: usize,
4798    attached: bool,
4799) -> miette::Result<bool> {
4800    // Held before the drain pops it, along with what the flag is already carrying: a
4801    // `var_max` bounds the values this occurrence takes, not the list they are appended
4802    // to, so a second `--include` starts counting again.
4803    let collecting = flag_awaiting_value
4804        .last()
4805        .filter(|flag| flag.arg.as_ref().is_some_and(|arg| arg.var))
4806        .cloned()
4807        .map(|flag| {
4808            let carried = flags.get(&flag).map(value_count).unwrap_or(0);
4809            (flag, carried)
4810        });
4811    let mut bound = vec![];
4812    let refused = drain_pending_flag_values(
4813        spec,
4814        cmd,
4815        errors,
4816        flags,
4817        flag_awaiting_value,
4818        word,
4819        custom_env,
4820        &mut bound,
4821    )?;
4822    for (flag, values) in bound {
4823        trace.record(
4824            argv,
4825            TokenRole::Value {
4826                flag,
4827                values,
4828                attached,
4829            },
4830        );
4831    }
4832    if refused {
4833        return Ok(true);
4834    }
4835    let Some((flag, carried)) = collecting else {
4836        return Ok(false);
4837    };
4838    collect_variadic_flag_values(
4839        spec,
4840        cmd,
4841        errors,
4842        flags,
4843        flag_awaiting_value,
4844        &flag,
4845        carried,
4846        input,
4847        custom_env,
4848        trace,
4849    )
4850}
4851
4852/// Keep feeding a flag whose argument is variadic from the words that follow it.
4853///
4854/// `--include <pattern>...` collects from a single occurrence, so it takes tokens until
4855/// one is flag-like, a `--` arrives, its `var_max` is reached, or the command line ends.
4856/// This is greedy by design — a command declaring both such a flag and positionals will
4857/// find the flag eating them, and `--` or a `var_max` is how the run is stopped.
4858///
4859/// `carried` is what the flag already held when this occurrence began, so the bound
4860/// counts this run rather than everything the flag has collected across the command
4861/// line. Each value goes through the same drain as the first, so choices are checked
4862/// and the value lands in the same list rather than by a second route that could
4863/// disagree.
4864#[allow(clippy::too_many_arguments)]
4865fn collect_variadic_flag_values(
4866    spec: &Spec,
4867    cmd: &SpecCommand,
4868    errors: &mut Vec<UsageErr>,
4869    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4870    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
4871    flag: &Arc<SpecFlag>,
4872    carried: usize,
4873    input: &mut VecDeque<Token>,
4874    custom_env: Option<&HashMap<String, String>>,
4875    trace: &mut Trace,
4876) -> miette::Result<bool> {
4877    let max = flag
4878        .arg
4879        .as_ref()
4880        .and_then(|arg| arg.var_max)
4881        .unwrap_or(usize::MAX);
4882    while flags
4883        .get(flag)
4884        .map(value_count)
4885        .unwrap_or(0)
4886        .saturating_sub(carried)
4887        < max
4888    {
4889        let Some(next) = input.front().map(|token| token.word.as_str()) else {
4890            break;
4891        };
4892        if flag
4893            .arg
4894            .as_ref()
4895            .and_then(|arg| arg.value_terminator.as_deref())
4896            == Some(next)
4897        {
4898            let terminator = input.pop_front().unwrap();
4899            trace.record(
4900                terminator.argv,
4901                TokenRole::ValueTerminator {
4902                    ends: flag.name.clone(),
4903                },
4904            );
4905            break;
4906        }
4907        // The separator is left where it is: stopping here hands it to the arm that
4908        // knows what it means, rather than reading it as one more value.
4909        if next == "--"
4910            || (is_flag_like(next)
4911                && !(flag
4912                    .arg
4913                    .as_ref()
4914                    .is_some_and(|arg| arg.allow_negative_numbers)
4915                    && is_negative_number(next)))
4916        {
4917            break;
4918        }
4919        let taken = input.pop_front().unwrap();
4920        let argv = taken.argv;
4921        let mut word = taken.word;
4922        flag_awaiting_value.push(Arc::clone(flag));
4923        let mut bound = vec![];
4924        let refused = drain_pending_flag_values(
4925            spec,
4926            cmd,
4927            errors,
4928            flags,
4929            flag_awaiting_value,
4930            &mut word,
4931            custom_env,
4932            &mut bound,
4933        )?;
4934        for (flag, values) in bound {
4935            // A later word of the same occurrence is its own token, and never attached:
4936            // only the first value can ride along on the flag.
4937            trace.record(
4938                argv,
4939                TokenRole::Value {
4940                    flag,
4941                    values,
4942                    attached: false,
4943                },
4944            );
4945        }
4946        if refused {
4947            return Ok(true);
4948        }
4949    }
4950    // The loop stops once the occurrence has reached its bound, which without a delimiter is
4951    // exactly when it has taken `max` words. A delimiter breaks that: one word can carry
4952    // several values, so the run can end up *past* the bound rather than on it, and stopping
4953    // is no longer the same as staying within it. `--include a,b,c` under `var_max=2` is the
4954    // case — three values out of the one word the loop was entitled to take.
4955    //
4956    // Counted against `carried` like the loop itself, so this stays a statement about the
4957    // occurrence rather than about the list the occurrences build up.
4958    let taken = flags
4959        .get(flag)
4960        .map(value_count)
4961        .unwrap_or(0)
4962        .saturating_sub(carried);
4963    if let Some(min) = flag.arg.as_ref().and_then(|arg| arg.var_min)
4964        && taken < min
4965    {
4966        errors.push(UsageErr::VarFlagTooFew {
4967            name: flag.name.clone(),
4968            min,
4969            got: taken,
4970        });
4971    }
4972    if taken > max {
4973        errors.push(UsageErr::VarFlagTooMany {
4974            name: flag.name.clone(),
4975            max,
4976            got: taken,
4977        });
4978    }
4979    Ok(false)
4980}
4981
4982/// How many values a flag is holding, for a bound that counts them.
4983fn value_count(value: &ParseValue) -> usize {
4984    match value {
4985        ParseValue::MultiString(values) => values.len(),
4986        ParseValue::MultiBool(values) => values.len(),
4987        _ => 1,
4988    }
4989}
4990
4991/// Finish a value-optional flag that was given with no value.
4992///
4993/// Returns whether anything was bound. Completions keep the flag waiting — a
4994/// half-typed `--color ` is a question about the value — so this is asked only
4995/// once a full parse has decided the value is not coming, or once the next token
4996/// has made that decision.
4997///
4998/// The missing string is a real value: if the flag names `choices`, it has to
4999/// be one of them, the same way an env var or a `default` is checked. Binding
5000/// first and failing later would leave the flag set to a value the spec forbids.
5001fn try_bind_default_missing(
5002    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
5003    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
5004    custom_env: Option<&HashMap<String, String>>,
5005    origins: &mut IndexMap<Arc<SpecFlag>, Vec<ValueOrigin>>,
5006) -> miette::Result<bool> {
5007    let Some(flag) = flag_awaiting_value.last() else {
5008        return Ok(false);
5009    };
5010    let value = match flag.default_missing.clone() {
5011        Some(value) => value,
5012        None if flag.value_optional => {
5013            let flag = flag_awaiting_value.pop().unwrap();
5014            // Presence in the map distinguishes this from an absent flag; an
5015            // empty collection distinguishes it from an explicitly empty
5016            // `--flag=` string without inventing a sentinel value.
5017            let variadic_value = flag.arg.as_ref().is_some_and(|arg| arg.var);
5018            origins
5019                .entry(Arc::clone(&flag))
5020                .or_default()
5021                .push(ValueOrigin::DefaultMissing);
5022            if flag.var {
5023                // A repeated bare occurrence is still an occurrence. The string collection
5024                // uses an empty value for it, just as the concrete `default_missing` path
5025                // pushes one value per occurrence; otherwise bounds and consumers silently
5026                // lose every bare repeat after the first.
5027                flags
5028                    .entry(flag)
5029                    .or_insert_with(|| ParseValue::MultiString(Vec::new()))
5030                    .try_as_multi_string_mut()
5031                    .unwrap()
5032                    .push(String::new());
5033            } else if variadic_value {
5034                // A variadic occurrence stays pending after each value. Reaching the next
5035                // flag (or EOF) closes that same occurrence; it must not erase what it took.
5036                flags
5037                    .entry(flag)
5038                    .or_insert_with(|| ParseValue::MultiString(Vec::new()));
5039            } else {
5040                // A scalar pending here is a new bare occurrence. The normal permissive
5041                // repeat policy makes the later occurrence a correction, including a
5042                // correction from an explicit value back to the bare tri-state.
5043                flags.insert(flag, ParseValue::MultiString(Vec::new()));
5044            }
5045            return Ok(true);
5046        }
5047        None => return Ok(false),
5048    };
5049    if let Some(arg) = flag.arg.as_ref() {
5050        validate_choice_value(
5051            ChoiceTarget::option(flag),
5052            &value,
5053            arg.choices.as_ref(),
5054            custom_env,
5055        )?;
5056    }
5057    let flag = flag_awaiting_value.pop().unwrap();
5058    origins
5059        .entry(Arc::clone(&flag))
5060        .or_default()
5061        .push(ValueOrigin::DefaultMissing);
5062    let collecting = flag.var || flag.arg.as_ref().is_some_and(|arg| arg.var);
5063    if collecting {
5064        let arr = flags
5065            .entry(flag)
5066            .or_insert_with(|| ParseValue::MultiString(vec![]))
5067            .try_as_multi_string_mut()
5068            .unwrap();
5069        arr.push(value);
5070    } else {
5071        flags.insert(flag, ParseValue::String(value));
5072    }
5073    Ok(true)
5074}
5075
5076/// `bound` collects what each drained flag took, in the order it took it. The values are
5077/// the word after any `delimiter` split, which is the only place that split is known: by the
5078/// time they are in `flags` a scalar and a one-element list are indistinguishable, and a
5079/// second occurrence has appended to the same list.
5080#[allow(clippy::too_many_arguments)]
5081fn drain_pending_flag_values(
5082    spec: &Spec,
5083    cmd: &SpecCommand,
5084    errors: &mut Vec<UsageErr>,
5085    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
5086    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
5087    word: &mut String,
5088    custom_env: Option<&HashMap<String, String>>,
5089    bound: &mut Vec<(Arc<SpecFlag>, Vec<String>)>,
5090) -> miette::Result<bool> {
5091    while let Some(flag) = flag_awaiting_value.pop() {
5092        let arg = flag.arg.as_ref().unwrap();
5093        // Split before anything judges the word, because after the split it is no longer
5094        // one value: `--env dev,prod` is two, and `choices` has to be asked about each.
5095        // Judging first would reject the whole word against a list neither half is on.
5096        let parts: Vec<String> = match arg.delimiter {
5097            Some(delimiter) => word.split(delimiter).map(str::to_string).collect(),
5098            None => vec![std::mem::take(word)],
5099        };
5100        for part in &parts {
5101            if validate_choices(
5102                spec,
5103                cmd,
5104                errors,
5105                ChoiceTarget::option(&flag),
5106                part,
5107                arg.choices.as_ref(),
5108                custom_env,
5109            )? {
5110                return Ok(true);
5111            }
5112        }
5113        word.clear();
5114        bound.push((Arc::clone(&flag), parts.clone()));
5115        // Two ways to hold several values, and both record a list: a `var` flag
5116        // collects one per occurrence, a variadic argument collects several from one.
5117        if flag.var || arg.var {
5118            let arr = flags
5119                .entry(flag)
5120                .or_insert_with(|| ParseValue::MultiString(vec![]))
5121                .try_as_multi_string_mut()
5122                .unwrap();
5123            arr.extend(parts);
5124        } else {
5125            // Nowhere for a second value to go, so the word stands as it was typed. A
5126            // delimiter on a flag that takes one value is refused where it is written.
5127            flags.insert(
5128                flag,
5129                ParseValue::String(parts.into_iter().next().unwrap_or_default()),
5130            );
5131        }
5132    }
5133    Ok(false)
5134}
5135
5136fn choice_error(
5137    target: ChoiceTarget<'_>,
5138    value: &str,
5139    choices: Option<&SpecChoices>,
5140    custom_env: Option<&HashMap<String, String>>,
5141) -> Option<String> {
5142    let choices = choices?;
5143    if !choices.strict {
5144        return None;
5145    }
5146    // The declared values and `env=` first, so a value they accept never costs a command.
5147    match choices.matches_resolved(value, custom_env) {
5148        Ok(true) => return None,
5149        Ok(false) => {}
5150        // Only a command could say, and this parse may not run one: let the value through
5151        // rather than report an error the real run might not have.
5152        Err(_) if choices.run().is_some() && crate::spec::choices::runs_disabled() => return None,
5153        Err(err) => {
5154            return Some(format!(
5155                "Could not check {} {}: {value} against its choices: {err}",
5156                target.kind, target.name,
5157            ));
5158        }
5159    }
5160    // Already run by the check above, so this reads what it printed rather than running it
5161    // again.
5162    let values = choices
5163        .resolved_values(custom_env)
5164        .unwrap_or_else(|_| choices.values_with_env(custom_env));
5165    if let Some(run) = choices.run()
5166        && values.is_empty()
5167    {
5168        return Some(format!(
5169            "Invalid choice for {} {}: {value}, `{run}` printed no choices",
5170            target.kind, target.name,
5171        ));
5172    }
5173    if let Some(env) = choices.env()
5174        && values.is_empty()
5175    {
5176        return Some(format!(
5177            "Invalid choice for {} {}: {value}, no choices resolved from env {env}",
5178            target.kind, target.name,
5179        ));
5180    }
5181    Some(format!(
5182        "Invalid choice for {} {}: {value}, expected one of {}",
5183        target.kind,
5184        target.name,
5185        values.join(", ")
5186    ))
5187}
5188
5189fn validate_choices(
5190    spec: &Spec,
5191    cmd: &SpecCommand,
5192    errors: &mut Vec<UsageErr>,
5193    target: ChoiceTarget<'_>,
5194    value: &str,
5195    choices: Option<&SpecChoices>,
5196    custom_env: Option<&HashMap<String, String>>,
5197) -> miette::Result<bool> {
5198    // A choice named like a help flag is a value, whether it was declared or printed by
5199    // `run=`. A command that cannot be run leaves the word to mean help, which is what a
5200    // user typing it most likely wanted.
5201    if is_help_arg(spec, cmd, value)
5202        && choices.is_some_and(|choices| {
5203            choices.strict && !choices.matches_resolved(value, custom_env).unwrap_or(false)
5204        })
5205    {
5206        errors.push(render_help_err(spec, cmd, value.len() > 2, custom_env));
5207        return Ok(true);
5208    }
5209
5210    if let Some(err) = choice_error(target, value, choices, custom_env) {
5211        bail!("{err}");
5212    }
5213    Ok(false)
5214}
5215
5216fn validate_choice_value(
5217    target: ChoiceTarget<'_>,
5218    value: &str,
5219    choices: Option<&SpecChoices>,
5220    custom_env: Option<&HashMap<String, String>>,
5221) -> miette::Result<()> {
5222    if let Some(err) = choice_error(target, value, choices, custom_env) {
5223        bail!("{err}");
5224    }
5225    Ok(())
5226}
5227
5228fn validate_choice_values(
5229    target: ChoiceTarget<'_>,
5230    values: &[String],
5231    choices: Option<&SpecChoices>,
5232    custom_env: Option<&HashMap<String, String>>,
5233) -> miette::Result<()> {
5234    for value in values {
5235        validate_choice_value(target, value, choices, custom_env)?;
5236    }
5237    Ok(())
5238}
5239
5240/// Everything a parse records about where it stopped: the positional cursor, so callers that
5241/// do not re-run the parse — completions, above all — agree with it, and the token trace.
5242///
5243/// Every exit from the binding phase comes through here, which is what makes it the right
5244/// place to close the trace: whatever is still queued was never read, and saying so is more
5245/// useful than leaving those words out of the report entirely.
5246fn record_stop(
5247    out: &mut ParseOutput,
5248    next_arg_idx: usize,
5249    seen_double_dash: bool,
5250    trace: &mut Trace,
5251    unread: &VecDeque<Token>,
5252) {
5253    out.next_arg = active_args(&out.cmd)
5254        .get(cursor_skip_sigils(&out.cmd, next_arg_idx))
5255        .cloned()
5256        .map(Arc::new);
5257    out.double_dash_seen = seen_double_dash;
5258    finalize_current_clause(out);
5259    trace.close(unread);
5260    out.tokens = std::mem::take(&mut trace.tokens);
5261}
5262
5263fn finalize_current_clause(out: &mut ParseOutput) {
5264    let Some(clause) = out.cmd.clause.as_ref() else {
5265        return;
5266    };
5267    let has_scoped_flags = clause
5268        .flags
5269        .iter()
5270        .any(|flag| out.flags.contains_key(&Arc::new(flag.clone())));
5271    if clause.separator.is_none() && out.args.is_empty() && !has_scoped_flags {
5272        return;
5273    }
5274    out.clauses
5275        .entry(clause.name.clone())
5276        .or_default()
5277        .push(std::mem::take(&mut out.args));
5278    let mut flags = IndexMap::new();
5279    for clause_flag in &clause.flags {
5280        let key = Arc::new(clause_flag.clone());
5281        if let Some((flag, value)) = out.flags.shift_remove_entry(&key) {
5282            flags.insert(flag, value);
5283        }
5284    }
5285    out.clause_flags
5286        .entry(clause.name.clone())
5287        .or_default()
5288        .push(flags);
5289}
5290
5291fn restore_current_clause(out: &mut ParseOutput) {
5292    let Some(clause) = out.cmd.clause.as_ref() else {
5293        return;
5294    };
5295    if let Some(current) = out.clauses.get_mut(&clause.name).and_then(Vec::pop) {
5296        out.args = current;
5297    }
5298    if let Some(current) = out.clause_flags.get_mut(&clause.name).and_then(Vec::pop) {
5299        out.flags.extend(current);
5300    }
5301}
5302
5303fn reset_clause_scalar_occurrences(
5304    out: &ParseOutput,
5305    occurrences: &mut HashMap<(usize, usize), u8>,
5306) {
5307    let Some(clause) = out.cmd.clause.as_ref() else {
5308        return;
5309    };
5310    let names = clause
5311        .flags
5312        .iter()
5313        .map(|flag| flag.name.as_str())
5314        .collect::<HashSet<_>>();
5315    let pointers = unique_flags(out.available_flags.values())
5316        .filter(|flag| names.contains(flag.name.as_str()))
5317        .map(|flag| Arc::as_ptr(flag) as usize)
5318        .collect::<HashSet<_>>();
5319    occurrences.retain(|(flag, _), _| !pointers.contains(flag));
5320}
5321
5322fn cursor_skip_sigils(cmd: &SpecCommand, mut idx: usize) -> usize {
5323    while active_args(cmd)
5324        .get(idx)
5325        .is_some_and(|arg| arg.sigil.is_some())
5326    {
5327        idx += 1;
5328    }
5329    idx
5330}
5331
5332fn active_args(cmd: &SpecCommand) -> &[SpecArg] {
5333    cmd.clause
5334        .as_ref()
5335        .map(|clause| clause.args.as_slice())
5336        .unwrap_or(cmd.args.as_slice())
5337}
5338
5339fn match_sigil_arg<'a>(
5340    cmd: &'a SpecCommand,
5341    word: &'a str,
5342) -> Option<(&'a SpecArg, &'a str, &'a str)> {
5343    cmd.args
5344        .iter()
5345        .filter_map(|arg| {
5346            let sigil = arg.sigil.as_deref()?;
5347            let value = word.strip_prefix(sigil)?;
5348            Some((arg, sigil, value))
5349        })
5350        .max_by_key(|(_, sigil, _)| sigil.len())
5351}
5352
5353fn match_sigil_arg_chain<'a>(
5354    cmds: &'a [SpecCommand],
5355    word: &'a str,
5356) -> Option<(&'a SpecArg, &'a str, &'a str)> {
5357    cmds.iter()
5358        .filter_map(|cmd| match_sigil_arg(cmd, word))
5359        .max_by_key(|(_, sigil, _)| sigil.len())
5360}
5361
5362/// Record that `arg` was handed a word before the `--` it requires.
5363///
5364/// A variadic arg would otherwise report the same mistake once per word it was offered, so the
5365/// message is emitted only the first time each arg is seen. The set is also what suppresses the
5366/// `MissingArg` that a `required` + `double_dash="required"` arg would otherwise collect at the
5367/// end of the parse.
5368fn report_double_dash_violation(
5369    arg: &SpecArg,
5370    errors: &mut Vec<UsageErr>,
5371    violations: &mut HashSet<String>,
5372) {
5373    if violations.insert(arg.name.clone()) {
5374        errors.push(UsageErr::ArgRequiresDoubleDash(arg.name.clone()));
5375    }
5376}
5377
5378/// `--version` and `-V`, which the parser supplies where the spec declares a version.
5379///
5380/// The twin of [`is_help_arg`], and of the `version` bit in usage-argv's and usage-go's
5381/// command tables — both of which accepted these spellings while this parser called them
5382/// unknown words. The help page has always listed `-V, --version` under the same
5383/// condition, and said in as many words that it did so "only where a version is
5384/// declared, which is where a parser accepts one", so a spec with a `version` rendered a
5385/// page advertising a flag the parse refused.
5386///
5387/// The root only, because that is where the page lists it: `version` is a property of
5388/// the program, and a subcommand answering with the program's version is a claim no spec
5389/// made. A declared flag wins by arriving first — every scan consults this only after
5390/// nothing declared matched — so a CLI that spends `-V` on something else keeps it, and
5391/// keeps `--version` supplied beside it.
5392fn is_version_arg(spec: &Spec, cmds: &[SpecCommand], w: &str) -> bool {
5393    (spec.version.is_some() || spec.long_version.is_some())
5394        && cmds.len() == 1
5395        && !spec.cmd.disable_version_flag
5396        && (w == "--version" || w == "-V")
5397}
5398
5399fn is_help_arg(spec: &Spec, cmd: &SpecCommand, w: &str) -> bool {
5400    spec.disable_help != Some(true)
5401        && (((w == "--help" || w == "-h" || w == "-?") && !cmd.disable_help_flag)
5402            || (w == "help" && !cmd.disable_help_subcommand && cmd.subcommands.is_empty()))
5403}
5404
5405impl ParseOutput {
5406    pub fn as_env(&self) -> BTreeMap<String, String> {
5407        let mut env = BTreeMap::new();
5408        for (flag, val) in &self.flags {
5409            let key = format!("usage_{}", crate::case::snake(&flag.name));
5410            let val = match val {
5411                ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
5412                ParseValue::String(s) => s.clone(),
5413                ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
5414                ParseValue::MultiString(s) => crate::shell_words::join(s),
5415            };
5416            env.insert(key, val);
5417        }
5418        for (arg, val) in &self.args {
5419            let key = format!("usage_{}", crate::case::snake(&arg.name));
5420            env.insert(key, val.to_string());
5421        }
5422        // The subcommand path, canonical names joined by spaces (`db migrate`), so a script can
5423        // dispatch on it without re-reading argv. Unset at the root, where there is nothing to
5424        // dispatch on. `cmd` is also an ordinary argument name, so a flag or arg the spec
5425        // declares under it anywhere on the path, clauses included, keeps the variable: a
5426        // script reading `$usage_cmd` for its own `<cmd>` must never get the path instead,
5427        // even when the arg was left out.
5428        let path = self.cmds.iter().skip(1).map(|c| c.name.as_str()).join(" ");
5429        let declared = |c: &SpecCommand| {
5430            let clause = c.clause.as_ref();
5431            c.args
5432                .iter()
5433                .chain(clause.into_iter().flat_map(|cl| &cl.args))
5434                .any(|a| crate::case::snake(&a.name) == "cmd")
5435                || c.flags
5436                    .iter()
5437                    .chain(clause.into_iter().flat_map(|cl| &cl.flags))
5438                    .any(|f| crate::case::snake(&f.name) == "cmd")
5439        };
5440        if !path.is_empty() && !self.cmds.iter().any(declared) {
5441            env.entry("usage_cmd".to_string()).or_insert(path);
5442        }
5443        env
5444    }
5445}
5446
5447impl Display for ParseValue {
5448    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
5449        match self {
5450            ParseValue::Bool(b) => write!(f, "{b}"),
5451            ParseValue::String(s) => write!(f, "{s}"),
5452            ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
5453            ParseValue::MultiString(s) => write!(f, "{}", crate::shell_words::join(s)),
5454        }
5455    }
5456}
5457
5458/// One `tokens` line for [`Debug`]: the position, the word, and what it became.
5459fn render_token(token: &TokenBinding) -> String {
5460    let roles = token.roles.iter().map(render_role).join(", ");
5461    let synthesized = if token.synthesized { " (read as)" } else { "" };
5462    format!("[{}] {}{synthesized}: {roles}", token.index, token.word)
5463}
5464
5465fn render_role(role: &TokenRole) -> String {
5466    match role {
5467        TokenRole::Program => "program".to_string(),
5468        TokenRole::Command { name } => format!("subcommand {name}"),
5469        TokenRole::Flag {
5470            flag,
5471            spelling,
5472            negated,
5473        } => {
5474            let negated = if *negated { ", negated" } else { "" };
5475            format!("flag {} as {spelling}{negated}", flag.name)
5476        }
5477        TokenRole::Value {
5478            flag,
5479            values,
5480            attached,
5481        } => {
5482            let attached = if *attached { ", attached" } else { "" };
5483            format!("value of {} = {values:?}{attached}", flag.name)
5484        }
5485        TokenRole::Arg { arg, values } => format!("arg {} = {values:?}", arg.name),
5486        TokenRole::Sigil { arg, sigil, values } => {
5487            format!("sigil arg {} ({sigil}) = {values:?}", arg.name)
5488        }
5489        TokenRole::Separator => "separator".to_string(),
5490        TokenRole::Builtin { spelling } => format!("built-in {spelling}"),
5491        TokenRole::ValueTerminator { ends } => format!("value terminator, ends {ends}"),
5492        TokenRole::Restart => "restart".to_string(),
5493        TokenRole::ClauseSeparator { name } => format!("clause separator for {name}"),
5494        TokenRole::UnknownFlag { bound_as } => match bound_as {
5495            Some(arg) => format!("unknown flag, bound as {}", arg.name),
5496            None => "unknown flag".to_string(),
5497        },
5498        TokenRole::Refused { reason } => format!("refused: {reason}"),
5499        TokenRole::External => "external".to_string(),
5500        TokenRole::Unread => "unread".to_string(),
5501    }
5502}
5503
5504impl Debug for ParseOutput {
5505    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
5506        f.debug_struct("ParseOutput")
5507            .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
5508            .field(
5509                "args",
5510                &self
5511                    .args
5512                    .iter()
5513                    .map(|(a, w)| format!("{}: {w}", a.name))
5514                    .collect_vec(),
5515            )
5516            .field("clauses", &self.clauses)
5517            .field(
5518                "available_flags",
5519                &self
5520                    .available_flags
5521                    .iter()
5522                    .map(|(f, w)| format!("{f}: {w}"))
5523                    .collect_vec(),
5524            )
5525            .field(
5526                "flags",
5527                &self
5528                    .flags
5529                    .iter()
5530                    .map(|(f, w)| format!("{}: {w}", f.name))
5531                    .collect_vec(),
5532            )
5533            .field("flag_awaiting_value", &self.flag_awaiting_value)
5534            .field("errors", &self.errors)
5535            .field("external", &self.external)
5536            // Provenance, one line per token and one per fallback. This is the parser's
5537            // debug channel under `USAGE_LOG=trace`, so it is where a spec author looks
5538            // first — `usage explain` renders the same facts for a reader.
5539            .field(
5540                "tokens",
5541                &self.tokens.iter().map(render_token).collect_vec(),
5542            )
5543            .field(
5544                "origins",
5545                &self
5546                    .flag_origins
5547                    .iter()
5548                    .map(|(f, o)| format!("{}: {o:?}", f.name))
5549                    .chain(
5550                        self.arg_origins
5551                            .iter()
5552                            .map(|(a, o)| format!("{}: {o:?}", a.name)),
5553                    )
5554                    .collect_vec(),
5555            )
5556            .field("overridden_flags", &self.overridden_flags)
5557            .finish()
5558    }
5559}
5560
5561#[cfg(test)]
5562mod tests {
5563    use super::*;
5564    use crate::SpecFlagAction;
5565
5566    fn input(words: &[&str]) -> Vec<String> {
5567        words.iter().map(|word| (*word).to_string()).collect()
5568    }
5569
5570    #[test]
5571    fn a_declared_version_supplies_the_flag_the_help_page_lists() {
5572        // The page has always listed `-V, --version` wherever a `version` is declared,
5573        // and usage-argv and usage-go have always accepted both. This parser called them
5574        // unknown words, so the one implementation the corpus measures the others against
5575        // was the one that disagreed.
5576        let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ncmd \"run\"\n"
5577            .parse()
5578            .unwrap();
5579
5580        for spelling in ["--version", "-V"] {
5581            let err = parse(&spec, &input(&["ex", spelling]))
5582                .expect_err("answering with a version ends the parse");
5583            assert_eq!(err.to_string(), "1.2.3", "{spelling}");
5584        }
5585    }
5586
5587    #[test]
5588    fn the_supplied_version_flag_is_the_roots_alone() {
5589        // `version` describes the program, and the page lists the entry on the program's
5590        // own page only. A subcommand answering with it would be a claim no spec made.
5591        let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ncmd \"run\"\n"
5592            .parse()
5593            .unwrap();
5594
5595        let err = parse(&spec, &input(&["ex", "run", "--version"])).unwrap_err();
5596        assert_eq!(err.to_string(), "unexpected word: --version");
5597    }
5598
5599    #[test]
5600    fn no_declared_version_supplies_nothing() {
5601        // A `--version` answering with nothing is worse than one that is not there, which
5602        // is why the entry is conditional on the page and the spelling on the parse.
5603        let spec: Spec = "name \"ex\"\nbin \"ex\"\n".parse().unwrap();
5604
5605        for spelling in ["--version", "-V"] {
5606            let err = parse(&spec, &input(&["ex", spelling])).unwrap_err();
5607            assert_eq!(err.to_string(), format!("unexpected word: {spelling}"));
5608        }
5609    }
5610
5611    #[test]
5612    fn disable_version_flag_removes_the_supplied_spellings() {
5613        let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ndisable_version_flag #true\n"
5614            .parse()
5615            .unwrap();
5616
5617        for spelling in ["--version", "-V"] {
5618            let err = parse(&spec, &input(&["ex", spelling])).unwrap_err();
5619            assert_eq!(err.to_string(), format!("unexpected word: {spelling}"));
5620        }
5621    }
5622
5623    #[test]
5624    fn a_spelling_the_spec_spends_elsewhere_keeps_its_meaning() {
5625        // The page drops each supplied spelling the CLI claimed and keeps the other; the
5626        // parse agrees without being told, because a declared flag is matched first.
5627        let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-V --verbose\"\n"
5628            .parse()
5629            .unwrap();
5630
5631        let out = parse(&spec, &input(&["ex", "-V"])).expect("-V is the CLI's own flag");
5632        assert_eq!(out.flags.len(), 1);
5633
5634        let err = parse(&spec, &input(&["ex", "--version"])).unwrap_err();
5635        assert_eq!(err.to_string(), "1.2.3");
5636    }
5637
5638    #[test]
5639    fn the_supplied_spellings_split_the_two_version_texts() {
5640        // The same split `render_action_err` gives a declared version flag: the long
5641        // spelling prefers `long_version`, the short prefers the concise one.
5642        let spec: Spec =
5643            "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nlong_version \"1.2.3 (abcdef)\"\n"
5644                .parse()
5645                .unwrap();
5646
5647        assert_eq!(
5648            parse(&spec, &input(&["ex", "--version"]))
5649                .unwrap_err()
5650                .to_string(),
5651            "1.2.3 (abcdef)"
5652        );
5653        assert_eq!(
5654            parse(&spec, &input(&["ex", "-V"])).unwrap_err().to_string(),
5655            "1.2.3"
5656        );
5657    }
5658
5659    #[test]
5660    fn a_supplied_short_is_a_letter_a_bundle_may_contain() {
5661        // `-h` and `-V` are recognized letters that no spec declares, so a token holding
5662        // one beside a declared letter is a bundle. usage-lib alone read `-vh` as a word
5663        // naming nothing: usage-argv and usage-go both resolve the letter through the
5664        // same lookup that finds a declared short, and clap prints help for it too.
5665        let spec: Spec =
5666            "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\"\ncmd \"run\"\n"
5667                .parse()
5668                .unwrap();
5669
5670        for token in ["-vh", "-hv"] {
5671            let err = parse(&spec, &input(&["ex", token])).expect_err("help ends the parse");
5672            assert!(err.to_string().starts_with("ex 1.2.3"), "{token}: {err}");
5673        }
5674        for token in ["-vV", "-Vv"] {
5675            let err = parse(&spec, &input(&["ex", token])).expect_err("a version ends it too");
5676            assert_eq!(err.to_string(), "1.2.3", "{token}");
5677        }
5678    }
5679
5680    #[test]
5681    fn a_bundled_help_letter_asks_for_the_short_page() {
5682        // Whatever else shares the token: `-h` is the short spelling, and the letters
5683        // beside it say nothing about which page was asked for.
5684        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"
5685            .parse()
5686            .unwrap();
5687
5688        let short = parse(&spec, &input(&["ex", "-vh"]))
5689            .unwrap_err()
5690            .to_string();
5691        let long = parse(&spec, &input(&["ex", "--help"]))
5692            .unwrap_err()
5693            .to_string();
5694        assert!(short.contains("Be loud"), "{short}");
5695        assert!(!short.contains("at length"), "{short}");
5696        assert!(long.contains("at length"), "{long}");
5697    }
5698
5699    #[test]
5700    fn the_bundled_version_letter_is_the_roots_alone() {
5701        // The same rule the whole-token spelling follows, asked one letter at a time.
5702        let spec: Spec =
5703            "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\" global=#true\ncmd \"run\"\n"
5704                .parse()
5705                .unwrap();
5706
5707        let err = parse(&spec, &input(&["ex", "run", "-vV"])).unwrap_err();
5708        assert_eq!(err.to_string(), "unexpected word: -vV");
5709    }
5710
5711    #[test]
5712    fn a_declared_letter_keeps_its_meaning_inside_a_bundle() {
5713        // Nothing is supplied where the CLI spent the letter itself, so `-vh local` is
5714        // this spec's own `-h`, taking its value from the rest of the token.
5715        let spec: Spec =
5716            "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\"\nflag \"-h --host <host>\"\n"
5717                .parse()
5718                .unwrap();
5719
5720        let out = parse(&spec, &input(&["ex", "-vhlocal"])).expect("a bundle and its value");
5721        assert_eq!(out.flags.len(), 2);
5722        assert!(
5723            out.flags
5724                .iter()
5725                .any(|(flag, value)| flag.name == "host" && value.to_string() == "local")
5726        );
5727    }
5728
5729    #[test]
5730    fn disabling_help_takes_the_letter_back_out_of_the_bundle() {
5731        let spec: Spec =
5732            "name \"ex\"\nbin \"ex\"\ndisable_help_flag #true\nflag \"-v --verbose\"\n"
5733                .parse()
5734                .unwrap();
5735
5736        let err = parse(&spec, &input(&["ex", "-vh"])).unwrap_err();
5737        assert_eq!(err.to_string(), "unexpected word: -vh");
5738    }
5739
5740    #[test]
5741    fn a_letter_nothing_supplies_still_refuses_the_whole_bundle() {
5742        // The rule this must not weaken: `-az` is not a bundle at all, so `-a` is not set
5743        // on the way to discovering that `z` names nothing.
5744        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-a --all\"\narg \"[file]\"\n"
5745            .parse()
5746            .unwrap();
5747
5748        let out = parse(&spec, &input(&["ex", "-az"])).expect("it falls through to the argument");
5749        assert!(out.flags.is_empty(), "{:?}", out.flags);
5750        assert_eq!(out.args.len(), 1);
5751    }
5752
5753    fn spec_with_arg(arg: SpecArg) -> Spec {
5754        let cmd = SpecCommand::builder().name("test").arg(arg).build();
5755        Spec {
5756            name: "test".to_string(),
5757            bin: "test".to_string(),
5758            cmd,
5759            ..Default::default()
5760        }
5761    }
5762
5763    fn spec_with_flag(flag: SpecFlag) -> Spec {
5764        let cmd = SpecCommand::builder().name("test").flag(flag).build();
5765        Spec {
5766            name: "test".to_string(),
5767            bin: "test".to_string(),
5768            cmd,
5769            ..Default::default()
5770        }
5771    }
5772
5773    fn parse_with_env(
5774        spec: &Spec,
5775        words: &[&str],
5776        env: &[(&str, &str)],
5777    ) -> Result<ParseOutput, miette::Error> {
5778        let env = env
5779            .iter()
5780            .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
5781            .collect();
5782        Parser::new(spec).with_env(env).parse(&input(words))
5783    }
5784
5785    fn first_string_value(parsed: &ParseOutput) -> &str {
5786        if let Some(ParseValue::String(value)) = parsed.args.values().next() {
5787            return value;
5788        }
5789        if let Some(ParseValue::String(value)) = parsed.flags.values().next() {
5790            return value;
5791        }
5792        panic!("expected first parsed value to be ParseValue::String");
5793    }
5794
5795    #[test]
5796    fn custom_environment_parser_dispatches_executable_views() {
5797        let spec: Spec = r#"
5798bin "ex"
5799view "runner" root="run"
5800cmd "run" {
5801    flag "--token <token>" env="TOKEN"
5802}
5803        "#
5804        .parse()
5805        .unwrap();
5806        let parsed = Parser::new(&spec)
5807            .with_env([("TOKEN".to_string(), "secret".to_string())].into())
5808            .parse(&input(&["runner"]))
5809            .unwrap();
5810
5811        assert_eq!(parsed.cmd.name, "runner");
5812        assert!(parsed.flags.iter().any(|(flag, value)| flag.name == "token"
5813            && matches!(value, ParseValue::String(value) if value == "secret")));
5814    }
5815
5816    #[test]
5817    fn an_executable_view_keeps_the_hosts_version_action() {
5818        let spec: Spec = r#"
5819bin "ex"
5820version "1.2.3"
5821flag "-V --version" action="version"
5822flag "--verbose" global=#true
5823view "runner" root="run" globals=#true
5824cmd "run"
5825        "#
5826        .parse()
5827        .unwrap();
5828
5829        let error = Parser::new(&spec)
5830            .parse(&input(&["runner", "--version"]))
5831            .expect_err("the host version action should answer before view projection");
5832        assert_eq!(error.to_string(), "1.2.3");
5833
5834        let error = Parser::new(&spec)
5835            .parse(&input(&["runner", "--verbose", "--version"]))
5836            .expect_err("the host version action should remain after a carried global");
5837        assert_eq!(error.to_string(), "1.2.3");
5838    }
5839
5840    fn flag_string_value<'a>(parsed: &'a ParseOutput, name: &str) -> &'a str {
5841        let flag = parsed
5842            .flags
5843            .keys()
5844            .find(|flag| flag.name == name)
5845            .unwrap_or_else(|| panic!("expected flag {name}"));
5846        let value = parsed
5847            .flags
5848            .get(flag)
5849            .unwrap_or_else(|| panic!("expected value for flag {name}"));
5850        match value {
5851            ParseValue::String(value) => value,
5852            _ => panic!("expected flag {name} to be ParseValue::String"),
5853        }
5854    }
5855
5856    fn assert_parse_err(result: Result<ParseOutput, miette::Error>, expected: &str) {
5857        let err = result.expect_err("expected parser error");
5858        assert_eq!(format!("{err}"), expected);
5859    }
5860
5861    #[test]
5862    fn a_short_version_action_falls_back_to_the_long_version() {
5863        let flag = SpecFlag::builder()
5864            .short('R')
5865            .action(SpecFlagAction::Version)
5866            .build();
5867        let spec = Spec {
5868            name: "test".to_string(),
5869            bin: "test".to_string(),
5870            long_version: Some("1.2.3\ncommit abc123".to_string()),
5871            ..Default::default()
5872        };
5873        let UsageErr::Version(version) = render_action_err(&spec, &spec.cmd, &flag, "-R", None)
5874        else {
5875            panic!("expected version action")
5876        };
5877        assert_eq!(version, "1.2.3\ncommit abc123");
5878    }
5879
5880    #[cfg(feature = "unstable_choices_env")]
5881    fn spec_arg_choices_env(key: &str) -> Spec {
5882        spec_with_arg(
5883            SpecArg::builder()
5884                .name("env")
5885                .choices_env(key)
5886                .required(false)
5887                .build(),
5888        )
5889    }
5890
5891    #[cfg(feature = "unstable_choices_env")]
5892    fn spec_flag_choices_env(key: &str) -> Spec {
5893        spec_with_flag(
5894            SpecFlag::builder()
5895                .long("env")
5896                .arg(SpecArg::builder().name("env").choices_env(key).build())
5897                .build(),
5898        )
5899    }
5900
5901    #[test]
5902    fn test_parse() {
5903        let cmd = SpecCommand::builder()
5904            .name("test")
5905            .arg(SpecArg::builder().name("arg").build())
5906            .flag(SpecFlag::builder().long("flag").build())
5907            .build();
5908        let spec = Spec {
5909            name: "test".to_string(),
5910            bin: "test".to_string(),
5911            cmd,
5912            ..Default::default()
5913        };
5914        let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
5915        let parsed = parse(&spec, &input).unwrap();
5916        assert_eq!(parsed.cmds.len(), 1);
5917        assert_eq!(parsed.cmds[0].name, "test");
5918        assert_eq!(parsed.args.len(), 1);
5919        assert_eq!(parsed.flags.len(), 1);
5920        assert_eq!(parsed.available_flags.len(), 1);
5921    }
5922
5923    #[test]
5924    fn test_flag_overrides_last_occurrence_wins() {
5925        let spec: Spec = r#"
5926flag "--stdin" default=#true
5927flag "--file <file>" overrides="--stdin"
5928        "#
5929        .parse()
5930        .unwrap();
5931
5932        let file_wins = parse(&spec, &input(&["test", "--stdin", "--file", "input.txt"])).unwrap();
5933        assert_eq!(file_wins.flags.len(), 1);
5934        assert_eq!(flag_string_value(&file_wins, "file"), "input.txt");
5935        assert!(!file_wins.flags.keys().any(|flag| flag.name == "stdin"));
5936
5937        let stdin_wins = parse(&spec, &input(&["test", "--file", "input.txt", "--stdin"])).unwrap();
5938        assert_eq!(stdin_wins.flags.len(), 1);
5939        assert!(stdin_wins.flags.keys().any(|flag| flag.name == "stdin"));
5940        assert!(!stdin_wins.flags.keys().any(|flag| flag.name == "file"));
5941    }
5942
5943    #[test]
5944    fn test_flag_override_clears_pending_value() {
5945        let spec: Spec = r#"
5946flag "--file <file>" overrides="--stdin"
5947flag "--stdin"
5948arg "[input]"
5949        "#
5950        .parse()
5951        .unwrap();
5952
5953        let parsed = parse(&spec, &input(&["test", "--file", "--stdin", "input.txt"])).unwrap();
5954        assert_eq!(parsed.flags.len(), 1);
5955        assert!(parsed.flags.keys().any(|flag| flag.name == "stdin"));
5956        assert_eq!(first_string_value(&parsed), "input.txt");
5957    }
5958
5959    #[cfg(unix)]
5960    #[test]
5961    fn a_mount_on_the_root_discovers_subcommands() {
5962        // The root is a command like any other, so it can find its own subcommands
5963        // by running something. Uses `echo` rather than a fixture because resolving
5964        // a mount is what is being tested.
5965        let spec: Spec = r#"
5966name "ex"
5967bin "ex"
5968cmd "declared"
5969mount run="echo 'cmd \"discovered\"'"
5970"#
5971        .parse()
5972        .unwrap();
5973
5974        let out = parse(&spec, &["ex".to_string(), "discovered".to_string()]).unwrap();
5975        assert_eq!(out.cmd.name, "discovered");
5976    }
5977
5978    #[test]
5979    fn injected_mount_outputs_are_complete_and_never_fall_back_to_processes() {
5980        let spec: Spec = r#"
5981name "ex"
5982bin "ex"
5983mount run="this command must never run"
5984cmd "declared"
5985"#
5986        .parse()
5987        .unwrap();
5988
5989        Parser::new(&spec)
5990            .with_mount_outputs(HashMap::new())
5991            .parse(&input(&["ex", "declared"]))
5992            .expect("a declared command does not resolve the mount");
5993
5994        let error = Parser::new(&spec)
5995            .with_mount_outputs(HashMap::new())
5996            .parse(&input(&["ex", "discovered"]))
5997            .unwrap_err();
5998        assert!(
5999            error
6000                .to_string()
6001                .contains("No injected output was provided for mount command"),
6002            "{error}"
6003        );
6004    }
6005
6006    #[cfg(unix)]
6007    #[test]
6008    fn completion_sees_root_mounted_commands_with_nothing_typed() {
6009        // The case a root mount exists for. `mycli <tab>` has no word to trigger
6010        // discovery with, so a completion has to resolve up front or the mounted
6011        // commands are never offered.
6012        let spec: Spec = r#"
6013name "ex"
6014bin "ex"
6015cmd "declared"
6016mount run="echo 'cmd \"discovered\"'"
6017"#
6018        .parse()
6019        .unwrap();
6020
6021        let out = parse_partial(&spec, &["ex".to_string()]).unwrap();
6022        assert!(
6023            out.cmd.subcommands.contains_key("discovered"),
6024            "a completion should see mounted commands; got {:?}",
6025            out.cmd.subcommands.keys().collect::<Vec<_>>()
6026        );
6027    }
6028
6029    #[cfg(unix)]
6030    #[test]
6031    fn completion_and_execution_agree_about_discovery() {
6032        // Offering a command that a real parse would hand to the default instead is
6033        // worse than not offering it, so the gate applies to both paths. The mount
6034        // fails if it runs, which is how both halves are checked at once.
6035        let spec: Spec = r#"
6036name "ex"
6037bin "ex"
6038default_subcommand "run"
6039cmd "run" {
6040  arg "<task>"
6041}
6042mount run="exit 1"
6043"#
6044        .parse()
6045        .unwrap();
6046
6047        let out = parse_partial(&spec, &["ex".to_string()]).unwrap();
6048        assert!(
6049            !out.cmd.subcommands.contains_key("discovered"),
6050            "a completion must not offer what execution will not route"
6051        );
6052
6053        let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
6054        assert_eq!(out.cmd.name, "run");
6055    }
6056
6057    #[cfg(unix)]
6058    #[test]
6059    fn a_default_subcommand_outranks_discovery() {
6060        // The default already says what an unmatched word means, and says it for
6061        // free. The mount fails if it runs, so parsing proves discovery was skipped.
6062        let spec: Spec = r#"
6063name "ex"
6064bin "ex"
6065default_subcommand "run"
6066cmd "run" {
6067  arg "<task>"
6068}
6069mount run="exit 1"
6070"#
6071        .parse()
6072        .unwrap();
6073
6074        let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
6075        assert_eq!(out.cmd.name, "run");
6076    }
6077
6078    #[cfg(unix)]
6079    #[test]
6080    fn a_mount_may_ask_to_outrank_the_default() {
6081        // Opting in, and paying for it: discovery runs first, so a discovered
6082        // command wins over the fallback.
6083        let spec: Spec = r#"
6084name "ex"
6085bin "ex"
6086default_subcommand "run"
6087cmd "run" {
6088  arg "<task>"
6089}
6090mount run="echo 'cmd \"discovered\"'" overrides_default=#true
6091"#
6092        .parse()
6093        .unwrap();
6094
6095        let out = parse(&spec, &["ex".to_string(), "discovered".to_string()]).unwrap();
6096        assert_eq!(out.cmd.name, "discovered");
6097
6098        // A word it does not know still reaches the default.
6099        let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
6100        assert_eq!(out.cmd.name, "run");
6101    }
6102
6103    #[cfg(unix)]
6104    #[test]
6105    fn a_flag_does_not_run_the_mount() {
6106        // A flag matches no subcommand, which would have been enough to trigger
6107        // discovery — so `ex --help` spawned a process. The mount fails if it runs,
6108        // so parsing at all is the proof that it did not.
6109        let spec: Spec = r#"
6110name "ex"
6111bin "ex"
6112flag "--verbose"
6113cmd "declared"
6114mount run="exit 1"
6115"#
6116        .parse()
6117        .unwrap();
6118
6119        let out = parse(&spec, &["ex".to_string(), "--verbose".to_string()]).unwrap();
6120        assert_eq!(out.cmd.name, "ex");
6121    }
6122
6123    #[cfg(unix)]
6124    #[test]
6125    fn a_declared_subcommand_does_not_run_the_mount() {
6126        // The mount would fail if it ran, so this parsing at all is the proof that
6127        // discovery is skipped when the word is already known. Worth pinning: a root
6128        // mount that resolved eagerly would spawn a process on every invocation.
6129        let spec: Spec = r#"
6130name "ex"
6131bin "ex"
6132cmd "declared"
6133mount run="exit 1"
6134"#
6135        .parse()
6136        .unwrap();
6137
6138        let out = parse(&spec, &["ex".to_string(), "declared".to_string()]).unwrap();
6139        assert_eq!(out.cmd.name, "declared");
6140    }
6141
6142    #[test]
6143    fn a_root_mount_survives_being_written_out() {
6144        let spec: Spec = "name \"ex\"\nbin \"ex\"\nmount run=\"ex plugins --usage\"\n"
6145            .parse()
6146            .unwrap();
6147        assert_eq!(spec.cmd.mounts.len(), 1);
6148
6149        let reparsed: Spec = spec.to_string().parse().unwrap();
6150        assert_eq!(reparsed.cmd.mounts.len(), 1, "written:\n{spec}");
6151        assert_eq!(reparsed.cmd.mounts[0].run, "ex plugins --usage");
6152    }
6153
6154    #[test]
6155    fn test_mount_prefix_applies_flag_overrides() {
6156        let stdin = Arc::new(
6157            SpecFlag::builder()
6158                .name("stdin")
6159                .long("stdin")
6160                .global(true)
6161                .build(),
6162        );
6163        let file = Arc::new(
6164            SpecFlag::builder()
6165                .name("file")
6166                .long("file")
6167                .arg(SpecArg::builder().name("file").build())
6168                .global(true)
6169                .overrides_with(vec!["--stdin".to_string()])
6170                .build(),
6171        );
6172        let mut prefix_flags = vec![(stdin, vec!["--stdin".to_string()])];
6173
6174        apply_prefix_flag_overrides(&mut prefix_flags, Arc::clone(&file));
6175        prefix_flags.push((file, vec!["--file".to_string(), "input.txt".to_string()]));
6176
6177        assert_eq!(mount_prefix_words(&prefix_flags), ["--file", "input.txt"]);
6178    }
6179
6180    #[test]
6181    fn test_flag_override_suppresses_env_value() {
6182        let spec: Spec = r#"
6183flag "--stdin" env="USE_STDIN"
6184flag "--file <file>" overrides="--stdin"
6185        "#
6186        .parse()
6187        .unwrap();
6188
6189        let parsed = parse_with_env(
6190            &spec,
6191            &["test", "--file", "input.txt"],
6192            &[("USE_STDIN", "true")],
6193        )
6194        .unwrap();
6195        assert_eq!(parsed.flags.len(), 1);
6196        assert_eq!(flag_string_value(&parsed, "file"), "input.txt");
6197    }
6198
6199    #[test]
6200    fn test_flag_override_suppresses_required_check() {
6201        let spec: Spec = r#"
6202flag "--stdin" required=#true
6203flag "--file <file>" overrides="--stdin"
6204        "#
6205        .parse()
6206        .unwrap();
6207
6208        let parsed = parse(&spec, &input(&["test", "--file", "input.txt"])).unwrap();
6209        assert_eq!(parsed.flags.len(), 1);
6210        assert_eq!(flag_string_value(&parsed, "file"), "input.txt");
6211    }
6212
6213    #[test]
6214    fn test_flag_required_if() {
6215        let spec: Spec = r#"
6216flag "--dir <dir>"
6217flag "--file <file>" required_if="--dir"
6218        "#
6219        .parse()
6220        .unwrap();
6221
6222        parse(&spec, &input(&["test"])).unwrap();
6223        assert_parse_err(
6224            parse(&spec, &input(&["test", "--dir", "src"])),
6225            "Missing required flag: --file <file>",
6226        );
6227        parse(
6228            &spec,
6229            &input(&["test", "--dir", "src", "--file", "input.txt"]),
6230        )
6231        .unwrap();
6232    }
6233
6234    #[test]
6235    fn test_flag_required_unless() {
6236        let spec: Spec = r#"
6237flag "--stdin"
6238flag "--file <file>" required_unless="--stdin"
6239        "#
6240        .parse()
6241        .unwrap();
6242
6243        assert_parse_err(
6244            parse(&spec, &input(&["test"])),
6245            "Missing required flag: --file <file>",
6246        );
6247        parse(&spec, &input(&["test", "--stdin"])).unwrap();
6248        parse(&spec, &input(&["test", "--file", "input.txt"])).unwrap();
6249    }
6250
6251    #[test]
6252    fn complete_required_relationship_truth_tables() {
6253        let spec: Spec = r#"
6254name "ex"
6255bin "ex"
6256flag "--mode <mode>"
6257flag "--scope <scope>"
6258flag "--token <token>" {
6259    required_if_eq "--mode" "remote"
6260}
6261flag "--approval <approval>" {
6262    required_if_eq_all "--mode" "remote" "--scope" "global"
6263}
6264flag "--input <input>" {
6265    required_unless "--stdin" "--file"
6266}
6267flag "--checksum <checksum>" {
6268    required_unless_all "--stdin" "--file"
6269}
6270flag "--stdin"
6271flag "--file <file>"
6272arg "[request]" {
6273    requires "--mode" "--scope"
6274}
6275"#
6276        .parse()
6277        .unwrap();
6278        let parse_args = |args: &[&str]| {
6279            parse(
6280                &spec,
6281                &args
6282                    .iter()
6283                    .map(|arg| (*arg).to_string())
6284                    .collect::<Vec<_>>(),
6285            )
6286        };
6287
6288        assert!(parse_args(&["ex", "--mode", "remote", "--stdin"]).is_err());
6289        assert!(
6290            parse_args(&[
6291                "ex", "--mode", "remote", "--token", "secret", "--scope", "global", "--stdin",
6292            ])
6293            .is_err()
6294        );
6295        parse_args(&[
6296            "ex",
6297            "--mode",
6298            "remote",
6299            "--token",
6300            "secret",
6301            "--scope",
6302            "global",
6303            "--approval",
6304            "yes",
6305            "--stdin",
6306            "--file",
6307            "in",
6308        ])
6309        .unwrap();
6310        parse_args(&[
6311            "ex",
6312            "--mode",
6313            "local",
6314            "--scope",
6315            "project",
6316            "--stdin",
6317            "--checksum",
6318            "sum",
6319            "request.json",
6320        ])
6321        .unwrap();
6322
6323        let reparsed: Spec = spec.to_string().parse().unwrap();
6324        assert_eq!(reparsed.cmd.flags[2].required_if_eq.len(), 1);
6325        assert_eq!(reparsed.cmd.flags[3].required_if_eq_all.len(), 2);
6326        assert_eq!(reparsed.cmd.flags[5].required_unless_all.len(), 2);
6327        assert_eq!(reparsed.cmd.args[0].requires.len(), 2);
6328    }
6329
6330    #[test]
6331    fn test_conditional_requirements_treat_env_as_explicit() {
6332        let spec: Spec = r#"
6333flag "--dir <dir>" env="INPUT_DIR"
6334flag "--stdin" env="USE_STDIN"
6335flag "--file <file>" required_if="--dir" required_unless="--stdin"
6336        "#
6337        .parse()
6338        .unwrap();
6339
6340        assert_parse_err(
6341            parse_with_env(&spec, &["test"], &[("INPUT_DIR", "src")]),
6342            "Missing required flag: --file <file>",
6343        );
6344        parse_with_env(&spec, &["test"], &[("USE_STDIN", "true")]).unwrap();
6345    }
6346
6347    #[test]
6348    fn test_custom_env_does_not_fall_back_to_process_env() {
6349        assert!(std::env::var("PATH").is_ok());
6350        let spec: Spec = r#"flag "--file <file>" env="PATH" required=#true"#.parse().unwrap();
6351
6352        assert_parse_err(
6353            parse_with_env(&spec, &["test"], &[]),
6354            "Missing required flag: --file <file>",
6355        );
6356    }
6357
6358    #[test]
6359    fn test_conditional_requirements_ignore_defaults_on_condition_flags() {
6360        let spec: Spec = r#"
6361flag "--dir <dir>" default="src"
6362flag "--file <file>" required_if="--dir"
6363        "#
6364        .parse()
6365        .unwrap();
6366
6367        parse(&spec, &input(&["test"])).unwrap();
6368    }
6369
6370    #[test]
6371    fn test_conditional_requirements_see_overridden_flags_as_absent() {
6372        let spec: Spec = r#"
6373flag "--stdin"
6374flag "--dir <dir>" overrides="--stdin"
6375flag "--file <file>" required_unless="--stdin"
6376        "#
6377        .parse()
6378        .unwrap();
6379
6380        assert_parse_err(
6381            parse(&spec, &input(&["test", "--stdin", "--dir", "src"])),
6382            "Missing required flag: --file <file>",
6383        );
6384    }
6385
6386    #[test]
6387    fn short_flag_is_one_character_not_one_byte() {
6388        // A short is declared and read by character. Counting bytes instead either
6389        // refuses the declaration or slices the token inside the character, and clap
6390        // — which many specs are generated from — accepts shorts like this one.
6391        let spec = spec_with_flag(
6392            SpecFlag::builder()
6393                .short('磨')
6394                .long("polish")
6395                .arg(SpecArg::builder().name("opt").build())
6396                .build(),
6397        );
6398        let attached = Parser::new(&spec)
6399            .parse(&input(&["test", "-磨VALUE"]))
6400            .unwrap();
6401        assert_eq!(flag_string_value(&attached, "polish"), "VALUE");
6402        let detached = Parser::new(&spec)
6403            .parse(&input(&["test", "-磨", "V"]))
6404            .unwrap();
6405        assert_eq!(flag_string_value(&detached, "polish"), "V");
6406    }
6407
6408    #[test]
6409    fn test_as_env() {
6410        let cmd = SpecCommand::builder()
6411            .name("test")
6412            .arg(SpecArg::builder().name("arg").build())
6413            .flag(SpecFlag::builder().long("flag").build())
6414            .flag(
6415                SpecFlag::builder()
6416                    .long("force")
6417                    .negate("--no-force")
6418                    .build(),
6419            )
6420            .build();
6421        let spec = Spec {
6422            name: "test".to_string(),
6423            bin: "test".to_string(),
6424            cmd,
6425            ..Default::default()
6426        };
6427        let input = vec![
6428            "test".to_string(),
6429            "--flag".to_string(),
6430            "--no-force".to_string(),
6431        ];
6432        let parsed = parse(&spec, &input).unwrap();
6433        let env = parsed.as_env();
6434        assert_eq!(env.len(), 2);
6435        assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
6436        assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
6437    }
6438
6439    #[test]
6440    fn as_env_names_the_subcommand_path() {
6441        let spec: Spec = r#"
6442            flag "-v --verbose"
6443            cmd "db" {
6444                cmd "migrate" {
6445                    alias "m"
6446                    arg "[target]"
6447                }
6448            }
6449            "#
6450        .parse()
6451        .unwrap();
6452        let env = |args: &[&str]| {
6453            let input: Vec<String> = std::iter::once("test")
6454                .chain(args.iter().copied())
6455                .map(String::from)
6456                .collect();
6457            parse(&spec, &input).unwrap().as_env()
6458        };
6459
6460        // The canonical name, not the alias that was typed.
6461        let nested = env(&["db", "m", "v2"]);
6462        assert_eq!(
6463            nested.get("usage_cmd").map(String::as_str),
6464            Some("db migrate")
6465        );
6466        assert_eq!(nested.get("usage_target").map(String::as_str), Some("v2"));
6467        assert_eq!(
6468            env(&["db"]).get("usage_cmd").map(String::as_str),
6469            Some("db")
6470        );
6471        assert!(!env(&["-v"]).contains_key("usage_cmd"));
6472    }
6473
6474    #[test]
6475    fn as_env_leaves_usage_cmd_to_a_declared_cmd() {
6476        // Declared on the chosen command, and supplied: the value is the arg's.
6477        let spec: Spec = r#"
6478            cmd "run" {
6479                arg "[cmd]"
6480            }
6481            "#
6482        .parse()
6483        .unwrap();
6484        let input = ["test", "run", "build"].map(String::from);
6485        let env = parse(&spec, &input).unwrap().as_env();
6486        assert_eq!(env.get("usage_cmd").map(String::as_str), Some("build"));
6487        // Declared but left out: still the arg's, so unset rather than the path.
6488        let input = ["test", "run"].map(String::from);
6489        assert!(
6490            !parse(&spec, &input)
6491                .unwrap()
6492                .as_env()
6493                .contains_key("usage_cmd")
6494        );
6495
6496        // Declared as a global flag on an ancestor.
6497        let spec: Spec = r#"
6498            flag "--cmd <cmd>" global=#true
6499            cmd "run"
6500            "#
6501        .parse()
6502        .unwrap();
6503        let input = ["test", "run"].map(String::from);
6504        assert!(
6505            !parse(&spec, &input)
6506                .unwrap()
6507                .as_env()
6508                .contains_key("usage_cmd")
6509        );
6510
6511        // Declared inside a clause, and the clause left out.
6512        let spec: Spec = r#"
6513            cmd "run" {
6514                clause "steps" separator=":::" {
6515                    arg "[cmd]"
6516                }
6517            }
6518            "#
6519        .parse()
6520        .unwrap();
6521        let input = ["test", "run"].map(String::from);
6522        assert!(
6523            !parse(&spec, &input)
6524                .unwrap()
6525                .as_env()
6526                .contains_key("usage_cmd")
6527        );
6528    }
6529
6530    #[test]
6531    fn test_arg_env_var() {
6532        let cmd = SpecCommand::builder()
6533            .name("test")
6534            .arg(
6535                SpecArg::builder()
6536                    .name("input")
6537                    .env("TEST_ARG_INPUT")
6538                    .required(true)
6539                    .build(),
6540            )
6541            .build();
6542        let spec = Spec {
6543            name: "test".to_string(),
6544            bin: "test".to_string(),
6545            cmd,
6546            ..Default::default()
6547        };
6548
6549        // Set env var
6550        // FIXME: Audit that the environment access only happens in single-threaded code.
6551        unsafe { std::env::set_var("TEST_ARG_INPUT", "test_file.txt") };
6552
6553        let input = vec!["test".to_string()];
6554        let parsed = parse(&spec, &input).unwrap();
6555
6556        assert_eq!(parsed.args.len(), 1);
6557        let arg = parsed.args.keys().next().unwrap();
6558        assert_eq!(arg.name, "input");
6559        let value = parsed.args.values().next().unwrap();
6560        assert_eq!(value.to_string(), "test_file.txt");
6561
6562        // Clean up
6563        // FIXME: Audit that the environment access only happens in single-threaded code.
6564        unsafe { std::env::remove_var("TEST_ARG_INPUT") };
6565    }
6566
6567    #[test]
6568    fn test_flag_env_var_with_arg() {
6569        let cmd = SpecCommand::builder()
6570            .name("test")
6571            .flag(
6572                SpecFlag::builder()
6573                    .long("output")
6574                    .env("TEST_FLAG_OUTPUT")
6575                    .arg(SpecArg::builder().name("file").build())
6576                    .build(),
6577            )
6578            .build();
6579        let spec = Spec {
6580            name: "test".to_string(),
6581            bin: "test".to_string(),
6582            cmd,
6583            ..Default::default()
6584        };
6585
6586        // Set env var
6587        // FIXME: Audit that the environment access only happens in single-threaded code.
6588        unsafe { std::env::set_var("TEST_FLAG_OUTPUT", "output.txt") };
6589
6590        let input = vec!["test".to_string()];
6591        let parsed = parse(&spec, &input).unwrap();
6592
6593        assert_eq!(parsed.flags.len(), 1);
6594        let flag = parsed.flags.keys().next().unwrap();
6595        assert_eq!(flag.name, "output");
6596        let value = parsed.flags.values().next().unwrap();
6597        assert_eq!(value.to_string(), "output.txt");
6598
6599        // Clean up
6600        // FIXME: Audit that the environment access only happens in single-threaded code.
6601        unsafe { std::env::remove_var("TEST_FLAG_OUTPUT") };
6602    }
6603
6604    #[test]
6605    fn test_flag_env_var_boolean() {
6606        let cmd = SpecCommand::builder()
6607            .name("test")
6608            .flag(
6609                SpecFlag::builder()
6610                    .long("verbose")
6611                    .env("TEST_FLAG_VERBOSE")
6612                    .build(),
6613            )
6614            .build();
6615        let spec = Spec {
6616            name: "test".to_string(),
6617            bin: "test".to_string(),
6618            cmd,
6619            ..Default::default()
6620        };
6621
6622        // Set env var to true
6623        // FIXME: Audit that the environment access only happens in single-threaded code.
6624        unsafe { std::env::set_var("TEST_FLAG_VERBOSE", "true") };
6625
6626        let input = vec!["test".to_string()];
6627        let parsed = parse(&spec, &input).unwrap();
6628
6629        assert_eq!(parsed.flags.len(), 1);
6630        let flag = parsed.flags.keys().next().unwrap();
6631        assert_eq!(flag.name, "verbose");
6632        let value = parsed.flags.values().next().unwrap();
6633        assert_eq!(value.to_string(), "true");
6634
6635        // Clean up
6636        // FIXME: Audit that the environment access only happens in single-threaded code.
6637        unsafe { std::env::remove_var("TEST_FLAG_VERBOSE") };
6638    }
6639
6640    #[test]
6641    fn test_env_var_precedence() {
6642        // CLI args should take precedence over env vars
6643        let cmd = SpecCommand::builder()
6644            .name("test")
6645            .arg(
6646                SpecArg::builder()
6647                    .name("input")
6648                    .env("TEST_PRECEDENCE_INPUT")
6649                    .required(true)
6650                    .build(),
6651            )
6652            .build();
6653        let spec = Spec {
6654            name: "test".to_string(),
6655            bin: "test".to_string(),
6656            cmd,
6657            ..Default::default()
6658        };
6659
6660        // Set env var
6661        // FIXME: Audit that the environment access only happens in single-threaded code.
6662        unsafe { std::env::set_var("TEST_PRECEDENCE_INPUT", "env_file.txt") };
6663
6664        let input = vec!["test".to_string(), "cli_file.txt".to_string()];
6665        let parsed = parse(&spec, &input).unwrap();
6666
6667        assert_eq!(parsed.args.len(), 1);
6668        let value = parsed.args.values().next().unwrap();
6669        // CLI arg should take precedence
6670        assert_eq!(value.to_string(), "cli_file.txt");
6671
6672        // Clean up
6673        // FIXME: Audit that the environment access only happens in single-threaded code.
6674        unsafe { std::env::remove_var("TEST_PRECEDENCE_INPUT") };
6675    }
6676
6677    #[test]
6678    fn test_flag_var_true_with_single_default() {
6679        // When var=true and default="bar", the default should be MultiString(["bar"])
6680        let cmd = SpecCommand::builder()
6681            .name("test")
6682            .flag(
6683                SpecFlag::builder()
6684                    .long("foo")
6685                    .var(true)
6686                    .arg(SpecArg::builder().name("foo").build())
6687                    .default_value("bar")
6688                    .build(),
6689            )
6690            .build();
6691        let spec = Spec {
6692            name: "test".to_string(),
6693            bin: "test".to_string(),
6694            cmd,
6695            ..Default::default()
6696        };
6697
6698        // User doesn't provide the flag
6699        let input = vec!["test".to_string()];
6700        let parsed = parse(&spec, &input).unwrap();
6701
6702        assert_eq!(parsed.flags.len(), 1);
6703        let flag = parsed.flags.keys().next().unwrap();
6704        assert_eq!(flag.name, "foo");
6705        let value = parsed.flags.values().next().unwrap();
6706        // Should be MultiString, not String
6707        match value {
6708            ParseValue::MultiString(v) => {
6709                assert_eq!(v.len(), 1);
6710                assert_eq!(v[0], "bar");
6711            }
6712            _ => panic!("Expected MultiString, got {:?}", value),
6713        }
6714    }
6715
6716    #[test]
6717    fn test_flag_var_true_with_multiple_defaults() {
6718        // When var=true and multiple defaults, should return MultiString(["xyz", "bar"])
6719        let cmd = SpecCommand::builder()
6720            .name("test")
6721            .flag(
6722                SpecFlag::builder()
6723                    .long("foo")
6724                    .var(true)
6725                    .arg(SpecArg::builder().name("foo").build())
6726                    .default_values(["xyz", "bar"])
6727                    .build(),
6728            )
6729            .build();
6730        let spec = Spec {
6731            name: "test".to_string(),
6732            bin: "test".to_string(),
6733            cmd,
6734            ..Default::default()
6735        };
6736
6737        // User doesn't provide the flag
6738        let input = vec!["test".to_string()];
6739        let parsed = parse(&spec, &input).unwrap();
6740
6741        assert_eq!(parsed.flags.len(), 1);
6742        let value = parsed.flags.values().next().unwrap();
6743        // Should be MultiString with both values
6744        match value {
6745            ParseValue::MultiString(v) => {
6746                assert_eq!(v.len(), 2);
6747                assert_eq!(v[0], "xyz");
6748                assert_eq!(v[1], "bar");
6749            }
6750            _ => panic!("Expected MultiString, got {:?}", value),
6751        }
6752    }
6753
6754    #[test]
6755    fn test_flag_var_false_with_default_remains_string() {
6756        // When var=false (default), the default should still be String("bar")
6757        let cmd = SpecCommand::builder()
6758            .name("test")
6759            .flag(
6760                SpecFlag::builder()
6761                    .long("foo")
6762                    .var(false) // Default behavior
6763                    .arg(SpecArg::builder().name("foo").build())
6764                    .default_value("bar")
6765                    .build(),
6766            )
6767            .build();
6768        let spec = Spec {
6769            name: "test".to_string(),
6770            bin: "test".to_string(),
6771            cmd,
6772            ..Default::default()
6773        };
6774
6775        // User doesn't provide the flag
6776        let input = vec!["test".to_string()];
6777        let parsed = parse(&spec, &input).unwrap();
6778
6779        assert_eq!(parsed.flags.len(), 1);
6780        let value = parsed.flags.values().next().unwrap();
6781        // Should be String, not MultiString
6782        match value {
6783            ParseValue::String(s) => {
6784                assert_eq!(s, "bar");
6785            }
6786            _ => panic!("Expected String, got {:?}", value),
6787        }
6788    }
6789
6790    #[test]
6791    fn test_arg_var_true_with_single_default() {
6792        // When arg has var=true and default="bar", the default should be MultiString(["bar"])
6793        let cmd = SpecCommand::builder()
6794            .name("test")
6795            .arg(
6796                SpecArg::builder()
6797                    .name("files")
6798                    .var(true)
6799                    .default_value("default.txt")
6800                    .required(false)
6801                    .build(),
6802            )
6803            .build();
6804        let spec = Spec {
6805            name: "test".to_string(),
6806            bin: "test".to_string(),
6807            cmd,
6808            ..Default::default()
6809        };
6810
6811        // User doesn't provide the arg
6812        let input = vec!["test".to_string()];
6813        let parsed = parse(&spec, &input).unwrap();
6814
6815        assert_eq!(parsed.args.len(), 1);
6816        let value = parsed.args.values().next().unwrap();
6817        // Should be MultiString, not String
6818        match value {
6819            ParseValue::MultiString(v) => {
6820                assert_eq!(v.len(), 1);
6821                assert_eq!(v[0], "default.txt");
6822            }
6823            _ => panic!("Expected MultiString, got {:?}", value),
6824        }
6825    }
6826
6827    #[test]
6828    fn test_arg_var_true_with_multiple_defaults() {
6829        // When arg has var=true and multiple defaults
6830        let cmd = SpecCommand::builder()
6831            .name("test")
6832            .arg(
6833                SpecArg::builder()
6834                    .name("files")
6835                    .var(true)
6836                    .default_values(["file1.txt", "file2.txt"])
6837                    .required(false)
6838                    .build(),
6839            )
6840            .build();
6841        let spec = Spec {
6842            name: "test".to_string(),
6843            bin: "test".to_string(),
6844            cmd,
6845            ..Default::default()
6846        };
6847
6848        // User doesn't provide the arg
6849        let input = vec!["test".to_string()];
6850        let parsed = parse(&spec, &input).unwrap();
6851
6852        assert_eq!(parsed.args.len(), 1);
6853        let value = parsed.args.values().next().unwrap();
6854        // Should be MultiString with both values
6855        match value {
6856            ParseValue::MultiString(v) => {
6857                assert_eq!(v.len(), 2);
6858                assert_eq!(v[0], "file1.txt");
6859                assert_eq!(v[1], "file2.txt");
6860            }
6861            _ => panic!("Expected MultiString, got {:?}", value),
6862        }
6863    }
6864
6865    #[test]
6866    fn test_arg_var_false_with_default_remains_string() {
6867        // When arg has var=false (default), the default should still be String
6868        let cmd = SpecCommand::builder()
6869            .name("test")
6870            .arg(
6871                SpecArg::builder()
6872                    .name("file")
6873                    .var(false)
6874                    .default_value("default.txt")
6875                    .required(false)
6876                    .build(),
6877            )
6878            .build();
6879        let spec = Spec {
6880            name: "test".to_string(),
6881            bin: "test".to_string(),
6882            cmd,
6883            ..Default::default()
6884        };
6885
6886        // User doesn't provide the arg
6887        let input = vec!["test".to_string()];
6888        let parsed = parse(&spec, &input).unwrap();
6889
6890        assert_eq!(parsed.args.len(), 1);
6891        let value = parsed.args.values().next().unwrap();
6892        // Should be String, not MultiString
6893        match value {
6894            ParseValue::String(s) => {
6895                assert_eq!(s, "default.txt");
6896            }
6897            _ => panic!("Expected String, got {:?}", value),
6898        }
6899    }
6900
6901    #[test]
6902    fn test_scalar_defaults_validate_only_first_default_choice() {
6903        let specs = [
6904            spec_with_arg(
6905                SpecArg::builder()
6906                    .name("env")
6907                    .var(false)
6908                    .default_values(["dev", "prod"])
6909                    .choices(["dev"])
6910                    .required(false)
6911                    .build(),
6912            ),
6913            spec_with_flag(
6914                SpecFlag::builder()
6915                    .long("env")
6916                    .arg(
6917                        SpecArg::builder()
6918                            .name("env")
6919                            .default_values(["dev", "prod"])
6920                            .choices(["dev"])
6921                            .build(),
6922                    )
6923                    .build(),
6924            ),
6925        ];
6926
6927        for spec in specs {
6928            let parsed = parse(&spec, &input(&["test"])).unwrap();
6929            assert_eq!(first_string_value(&parsed), "dev");
6930        }
6931    }
6932
6933    #[test]
6934    fn a_delimiter_turns_one_word_into_several_values() {
6935        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--tags <tag>\" var=#true delimiter=\",\"\narg \"[files]...\" var=#true delimiter=\":\"\n"
6936            .parse()
6937            .unwrap();
6938
6939        let parsed = parse(&spec, &input(&["ex", "--tags", "a,b,c", "x:y"])).unwrap();
6940        let multi = |value: &ParseValue| match value {
6941            ParseValue::MultiString(values) => values.clone(),
6942            other => panic!("expected several values, got {other:?}"),
6943        };
6944        let tags = parsed
6945            .flags
6946            .iter()
6947            .find(|(f, _)| f.name == "tags")
6948            .map(|(_, v)| v)
6949            .unwrap();
6950        assert_eq!(multi(tags), vec!["a", "b", "c"]);
6951        assert_eq!(multi(parsed.args.values().next().unwrap()), vec!["x", "y"]);
6952    }
6953
6954    #[test]
6955    fn a_positional_splits_before_its_choices_are_asked() {
6956        // The flag path did this and the positional path did not, so a word whose parts
6957        // were all choices was rejected as one value, and a bad half was reported as the
6958        // whole word.
6959        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[paths]...\" var=#true delimiter=\":\" {\n  choices \"src\" \"docs\"\n}\n"
6960            .parse()
6961            .unwrap();
6962
6963        parse(&spec, &input(&["ex", "src:docs"])).expect("both halves are choices");
6964
6965        let err = parse(&spec, &input(&["ex", "src:nowhere"])).unwrap_err();
6966        let message = err.to_string();
6967        assert!(message.contains("nowhere"), "{message}");
6968        assert!(
6969            !message.contains("src:nowhere"),
6970            "the bad half should be named, not the whole word: {message}"
6971        );
6972    }
6973
6974    #[test]
6975    fn a_split_value_is_counted_and_judged_as_values() {
6976        // Split during the parse rather than after it, so everything downstream sees the
6977        // values the user meant rather than the words they typed: `choices` judges each
6978        // one, and the bounds count them.
6979        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <e>\" var=#true delimiter=\",\" var_max=2 {\n  choices \"dev\" \"prod\"\n}\n"
6980            .parse()
6981            .unwrap();
6982
6983        parse(&spec, &input(&["ex", "--env", "dev,prod"])).expect("two values, both allowed");
6984        let err = parse(&spec, &input(&["ex", "--env", "dev,staging"])).unwrap_err();
6985        assert!(err.to_string().contains("staging"), "{err}");
6986        assert!(
6987            parse(&spec, &input(&["ex", "--env", "dev,prod,dev"])).is_err(),
6988            "three values should breach var_max=2"
6989        );
6990    }
6991
6992    #[test]
6993    fn a_split_bound_counts_one_occurrence_at_a_time() {
6994        // The bound on a variadic flag *argument* is what one occurrence may take. Without a
6995        // delimiter the collection simply stops at it, so it could never be exceeded; a word
6996        // carrying several values can carry an occurrence past it in one step, and that is
6997        // the only way this bound is ever breached.
6998        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--include <pattern>...\" delimiter=\",\" {\n  arg \"<pattern>...\" var=#true var_max=2\n}\n"
6999            .parse()
7000            .unwrap();
7001
7002        parse(&spec, &input(&["ex", "--include", "a,b"])).expect("exactly the bound is fine");
7003        assert!(
7004            parse(&spec, &input(&["ex", "--include", "a,b,c"])).is_err(),
7005            "three values out of one word is still three values"
7006        );
7007        // The rule the corpus documents for plain words, on split ones: a second occurrence
7008        // starts counting again rather than adding to the first.
7009        parse(
7010            &spec,
7011            &input(&["ex", "--include", "a,b", "--include", "c,d"]),
7012        )
7013        .expect("two per occurrence, twice, is within the bound");
7014    }
7015
7016    #[test]
7017    fn a_nested_minimum_is_checked_once_per_flag_occurrence() {
7018        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pair <value>...\" {\n  arg \"<value>...\" var=#true var_min=2 var_max=2\n}\n"
7019            .parse()
7020            .unwrap();
7021
7022        parse(
7023            &spec,
7024            &input(&["ex", "--pair", "a", "b", "--pair", "c", "d"]),
7025        )
7026        .expect("each occurrence satisfies the bound independently");
7027
7028        let error = parse(&spec, &input(&["ex", "--pair", "a", "--pair", "b", "c"])).unwrap_err();
7029        assert!(
7030            error
7031                .to_string()
7032                .contains("requires at least 2 value(s), got 1"),
7033            "{error:?}"
7034        );
7035    }
7036
7037    #[test]
7038    fn an_exclusive_flag_has_to_be_alone() {
7039        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--verbose\"\narg \"[target]\"\n"
7040            .parse()
7041            .unwrap();
7042
7043        parse(&spec, &input(&["ex", "--dump"])).expect("alone is the point");
7044
7045        // Any other flag.
7046        let err = parse(&spec, &input(&["ex", "--dump", "--verbose"])).unwrap_err();
7047        assert!(err.to_string().contains("on its own"), "{err}");
7048
7049        // And a positional, which is what makes this more than a conflict with every
7050        // other flag.
7051        let err = parse(&spec, &input(&["ex", "--dump", "t"])).unwrap_err();
7052        assert!(err.to_string().contains("on its own"), "{err}");
7053
7054        // Not given, so it imposes nothing.
7055        parse(&spec, &input(&["ex", "--verbose", "t"])).expect("without it, nothing changes");
7056    }
7057
7058    #[test]
7059    fn an_exclusive_flag_conflicts_with_clause_arguments() {
7060        let spec: Spec = r#"name "ex"
7061bin "ex"
7062flag "--dump" exclusive=#true
7063clause "tasks" separator=":::" {
7064  arg "<task>"
7065}
7066"#
7067        .parse()
7068        .unwrap();
7069
7070        let err = parse(&spec, &input(&["ex", "--dump", "lint"])).unwrap_err();
7071        assert!(err.to_string().contains("on its own"), "{err}");
7072    }
7073
7074    #[test]
7075    fn a_partial_parse_reports_the_next_implicit_clause_argument() {
7076        let spec: Spec = r#"name "ex"
7077bin "ex"
7078clause "tools" {
7079  arg "<tool@version>"
7080}
7081"#
7082        .parse()
7083        .unwrap();
7084
7085        let parsed = parse_partial(&spec, &input(&["ex"])).unwrap();
7086
7087        assert_eq!(
7088            parsed.next_arg.as_ref().map(|arg| arg.name.as_str()),
7089            Some("tool@version")
7090        );
7091    }
7092
7093    #[test]
7094    fn an_exclusive_flag_is_not_disturbed_by_a_default() {
7095        // Only what was supplied counts, as `conflicts` reads it. A default counting as
7096        // company would make an exclusive flag unusable on any command that has one.
7097        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--jobs <n>\" default=\"4\"\n"
7098            .parse()
7099            .unwrap();
7100
7101        parse(&spec, &input(&["ex", "--dump"])).expect("a default is nobody saying anything");
7102        assert!(parse(&spec, &input(&["ex", "--dump", "--jobs", "8"])).is_err());
7103    }
7104
7105    #[test]
7106    fn an_exclusive_flag_bypasses_required_siblings() {
7107        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--out <path>\" required=#true\narg \"<target>\"\n"
7108            .parse()
7109            .unwrap();
7110
7111        parse(&spec, &input(&["ex", "--dump"]))
7112            .expect("exclusive is the command's requiredness escape");
7113        assert!(
7114            parse(
7115                &spec,
7116                &input(&["ex", "--dump", "--out", "somewhere", "target"])
7117            )
7118            .is_err()
7119        );
7120    }
7121
7122    #[test]
7123    fn an_environment_value_counts_for_an_exclusive_flag() {
7124        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--out <path>\" env=\"EX_OUT\"\n"
7125            .parse()
7126            .unwrap();
7127
7128        assert!(parse_with_env(&spec, &["ex", "--dump"], &[("EX_OUT", "somewhere")]).is_err());
7129        parse_with_env(&spec, &["ex", "--dump"], &[]).expect("without the value it is alone");
7130    }
7131
7132    #[test]
7133    fn a_selected_subcommand_counts_for_an_ancestor_exclusive_flag() {
7134        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--version\" global=#true exclusive=#true\ncmd \"run\"\n"
7135            .parse()
7136            .unwrap();
7137
7138        parse(&spec, &input(&["ex", "--version"])).expect("alone is allowed");
7139        assert!(parse(&spec, &input(&["ex", "--version", "run"])).is_err());
7140    }
7141
7142    #[test]
7143    fn a_child_exclusive_flag_is_not_mistaken_for_a_same_named_parent_flag() {
7144        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n}\n"
7145            .parse()
7146            .unwrap();
7147
7148        parse(&spec, &input(&["ex", "run", "--clean"]))
7149            .expect("the child flag is alone within the child command");
7150        assert!(
7151            parse(&spec, &input(&["ex", "--clean", "run"])).is_err(),
7152            "the parent flag still conflicts with selecting the child"
7153        );
7154    }
7155
7156    #[test]
7157    fn a_child_local_exclusive_redeclaration_belongs_to_the_child() {
7158        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n}\n"
7159            .parse()
7160            .unwrap();
7161
7162        parse(&spec, &input(&["ex", "run", "--clean"]))
7163            .expect("the child-local exclusive flag is alone inside the child command");
7164        assert!(
7165            parse(&spec, &input(&["ex", "--clean", "run"])).is_err(),
7166            "the ancestor spelling still conflicts with selecting the child"
7167        );
7168    }
7169
7170    #[test]
7171    fn a_same_named_parent_flag_is_company_for_a_child_exclusive_flag() {
7172        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" global=#true exclusive=#true\n}\n"
7173            .parse()
7174            .unwrap();
7175
7176        parse(&spec, &input(&["ex", "run", "--clean"])).expect("the child exclusive flag is alone");
7177        assert!(
7178            parse(&spec, &input(&["ex", "--clean", "run", "--clean"])).is_err(),
7179            "the distinct parent declaration is still company despite sharing a name"
7180        );
7181    }
7182
7183    #[test]
7184    fn a_local_child_redeclaration_keeps_its_exclusivity_when_merged() {
7185        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n  flag \"--verbose\"\n}\n"
7186            .parse()
7187            .unwrap();
7188
7189        parse(&spec, &input(&["ex", "run", "--clean"]))
7190            .expect("the child exclusive flag is valid alone");
7191        assert!(
7192            parse(&spec, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
7193            "merging with the inherited global must not discard child exclusivity"
7194        );
7195    }
7196
7197    #[test]
7198    fn an_orphan_parent_alias_does_not_disown_a_child_local_exclusive_flag() {
7199        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n}\n"
7200            .parse()
7201            .unwrap();
7202
7203        parse(&spec, &input(&["ex", "run", "--clean"]))
7204            .expect("the typed long form belongs to the child declaration");
7205        assert!(
7206            parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
7207            "the inherited short form still belongs to the ancestor"
7208        );
7209        assert!(
7210            parse(&spec, &input(&["ex", "run", "-c", "--clean"])).is_err(),
7211            "a child spelling cannot mask the ancestor-exclusive occurrence on the same merged flag"
7212        );
7213    }
7214
7215    #[test]
7216    fn an_inherited_alias_keeps_its_ancestor_exclusivity() {
7217        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" global=#true\n}\n"
7218            .parse()
7219            .unwrap();
7220
7221        parse(&spec, &input(&["ex", "run", "--clean"]))
7222            .expect("the child's spelling does not activate the orphan ancestor alias");
7223        assert!(
7224            parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
7225            "the inherited short alias still belongs to the ancestor exclusive flag"
7226        );
7227    }
7228
7229    #[test]
7230    fn an_inherited_negated_alias_keeps_its_ancestor_exclusivity() {
7231        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"
7232            .parse()
7233            .unwrap();
7234
7235        assert!(
7236            parse(&spec, &input(&["ex", "run", "--no-clean"])).is_err(),
7237            "the inherited negated alias still belongs to the ancestor exclusive flag"
7238        );
7239    }
7240
7241    #[test]
7242    fn a_colliding_alias_does_not_disown_the_child_from_the_rest() {
7243        // The child re-declares the inherited `--clean` as exclusive and gives it a `-c` that
7244        // an unrelated inherited global already owns. That collision is resolved in the other
7245        // global's favor, so the child's `-c` resolves elsewhere — but the child plainly owns
7246        // the `--clean` it declared, and its exclusivity holds.
7247        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"
7248            .parse()
7249            .unwrap();
7250
7251        parse(&spec, &input(&["ex", "run", "--clean"])).expect("alone is allowed");
7252        assert!(
7253            parse(&spec, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
7254            "one unrelated alias collision cannot disown the child from its own flag"
7255        );
7256    }
7257
7258    #[test]
7259    fn a_local_child_declaration_is_not_in_scope_before_the_subcommand() {
7260        // A child's *local* re-declaration describes the flag at the child. Typed ahead of the
7261        // subcommand word the flag can only be the ancestor's, because that is the only one in
7262        // scope there — so the ancestor's exclusivity is the one that answers, whichever way it
7263        // is set. The pair below differ in nothing else, which is what makes this one rule
7264        // rather than two behaviors.
7265        let quiet: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n  flag \"--verbose\"\n}\n"
7266            .parse()
7267            .unwrap();
7268        parse(&quiet, &input(&["ex", "--clean", "run", "--verbose"]))
7269            .expect("the ancestor owns this occurrence, and it is not exclusive");
7270        assert!(
7271            parse(&quiet, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
7272            "after the subcommand word the child's declaration is in scope, and it is exclusive"
7273        );
7274
7275        let loud: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n}\n"
7276            .parse()
7277            .unwrap();
7278        assert!(
7279            parse(&loud, &input(&["ex", "--clean", "run"])).is_err(),
7280            "the same rule, with an exclusive ancestor: selecting the child is company for it"
7281        );
7282    }
7283
7284    #[test]
7285    fn an_orphan_ancestor_alias_keeps_its_exclusivity_past_a_plain_child_redeclaration() {
7286        // The mirror of `a_local_child_redeclaration_keeps_its_exclusivity_when_merged`: the
7287        // child owns `--clean` and says nothing about exclusivity, but `-c` is a spelling only
7288        // the ancestor ever declared, so the ancestor's answer still governs it.
7289        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\"\n  flag \"--verbose\"\n}\n"
7290            .parse()
7291            .unwrap();
7292
7293        assert!(
7294            parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
7295            "the orphan ancestor alias is still the ancestor's exclusive flag"
7296        );
7297        parse(&spec, &input(&["ex", "run", "--clean", "--verbose"]))
7298            .expect("the child's own spelling drops the exclusivity the child did not restate");
7299    }
7300
7301    #[test]
7302    fn a_child_spelling_carries_its_exclusivity_even_beside_an_ancestor_spelling() {
7303        // Both spellings of one merged flag, typed together. The child's `--clean` is exclusive
7304        // whatever else was typed alongside it, so `--verbose` is company; attributing the whole
7305        // occurrence to the ancestor because `-c` appeared in it lost that.
7306        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n  flag \"--verbose\"\n}\n"
7307            .parse()
7308            .unwrap();
7309
7310        assert!(
7311            parse(&spec, &input(&["ex", "run", "-c", "--clean", "--verbose"])).is_err(),
7312            "the child spelling is exclusive whatever it was typed beside"
7313        );
7314        parse(&spec, &input(&["ex", "run", "-c", "--verbose"]))
7315            .expect("the ancestor's own spelling was never exclusive");
7316    }
7317
7318    #[test]
7319    fn an_environment_value_takes_the_exclusivity_of_the_declaration_in_scope() {
7320        // An environment value has no spelling to attribute, so the declaration the selected
7321        // command has in scope answers — in both directions. Comparing whole alias sets asked
7322        // the ancestor instead, because the merged flag also carries its orphan `-c`.
7323        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"
7324            .parse()
7325            .unwrap();
7326
7327        assert!(
7328            parse_with_env(&added, &["ex", "run", "--verbose"], &[("EX_CLEAN", "1")]).is_err(),
7329            "the child added exclusivity the environment value has to honor"
7330        );
7331
7332        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"
7333            .parse()
7334            .unwrap();
7335
7336        parse_with_env(&dropped, &["ex", "run", "--verbose"], &[("EX_CLEAN", "1")])
7337            .expect("the child dropped the exclusivity, and the environment value follows it");
7338    }
7339
7340    #[test]
7341    fn a_merged_child_exclusive_flag_still_escapes_requiredness() {
7342        // Exclusivity suppresses missing-value checks, and that has to survive the merge for
7343        // the same reason the companion check does.
7344        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"
7345            .parse()
7346            .unwrap();
7347
7348        parse(&spec, &input(&["ex", "run", "--clean"]))
7349            .expect("a merged child exclusive flag is still the command's requiredness escape");
7350    }
7351
7352    #[test]
7353    fn a_group_allows_one_member_and_refuses_two() {
7354        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\"\nflag \"--url <u>\"\nflag \"--stdin\"\ngroup \"input\" \"--file\" \"--url\" \"--stdin\"\n"
7355            .parse()
7356            .unwrap();
7357
7358        // One is fine, and so is none: a plain group says "at most one".
7359        parse(&spec, &input(&["ex", "--file", "a.txt"])).expect("one member is fine");
7360        parse(&spec, &input(&["ex"])).expect("a group that is not required asks for nothing");
7361
7362        let err = parse(&spec, &input(&["ex", "--file", "a.txt", "--stdin"])).unwrap_err();
7363        assert!(err.to_string().contains("group input"), "{err}");
7364    }
7365
7366    #[test]
7367    fn positional_selectors_work_in_conflicts_and_groups() {
7368        let conflicts: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--from-file <path>\" conflicts=\"value\"\narg \"[value]\"\n"
7369            .parse()
7370            .unwrap();
7371        parse(&conflicts, &input(&["ex", "--from-file", "vars.env"]))
7372            .expect("the flag alone is valid");
7373        parse(&conflicts, &input(&["ex", "literal"])).expect("the positional alone is valid");
7374        assert!(
7375            parse(
7376                &conflicts,
7377                &input(&["ex", "--from-file", "vars.env", "literal"])
7378            )
7379            .is_err()
7380        );
7381
7382        let positional_source: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--from-file <path>\"\narg \"[value]\" conflicts=\"--from-file\"\n"
7383            .parse()
7384            .unwrap();
7385        assert!(
7386            parse(
7387                &positional_source,
7388                &input(&["ex", "--from-file", "vars.env", "literal"])
7389            )
7390            .is_err()
7391        );
7392
7393        let group: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <path>\"\narg \"[target]\"\ngroup \"input\" \"--file\" \"target\" required=#true\n"
7394            .parse()
7395            .unwrap();
7396        assert!(parse(&group, &input(&["ex"])).is_err());
7397        parse(&group, &input(&["ex", "target-name"]))
7398            .expect("a positional satisfies a required group");
7399        assert!(
7400            parse(
7401                &group,
7402                &input(&["ex", "--file", "input.txt", "target-name"])
7403            )
7404            .is_err()
7405        );
7406    }
7407
7408    #[test]
7409    fn a_required_group_needs_one_of_its_members() {
7410        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
7411            .parse()
7412            .unwrap();
7413
7414        let err = parse(&spec, &input(&["ex"])).unwrap_err();
7415        // The members, because that is what a user has to type; the name, because a
7416        // command with several groups would otherwise report the same sentence twice.
7417        assert!(err.to_string().contains("--file, --url"), "{err}");
7418        assert!(err.to_string().contains("input"), "{err}");
7419
7420        parse(&spec, &input(&["ex", "--url", "u"])).expect("one member satisfies it");
7421    }
7422
7423    #[test]
7424    fn a_multiple_group_only_polices_requiredness() {
7425        // `multiple` with `required` is "at least one of these", so two is fine and
7426        // none is not.
7427        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--a\"\nflag \"--b\"\ngroup \"any\" \"--a\" \"--b\" required=#true multiple=#true\n"
7428            .parse()
7429            .unwrap();
7430
7431        parse(&spec, &input(&["ex", "--a", "--b"])).expect("multiple allows both");
7432        assert!(parse(&spec, &input(&["ex"])).is_err());
7433    }
7434
7435    #[test]
7436    fn a_group_reads_a_default_for_requiredness_and_not_for_exclusivity() {
7437        // The two halves of a group are two kinds of rule, and they read a default
7438        // differently on purpose. Requiredness asks whether a member has a value, and a
7439        // default is a value — the rule `requires` follows. Exclusivity asks what the
7440        // user supplied, because a defaulted member counted as supplied would collide
7441        // with the sibling they actually typed and refuse a correct command line.
7442        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" default=\"a.txt\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
7443            .parse()
7444            .unwrap();
7445
7446        parse(&spec, &input(&["ex"])).expect("the default fills the group");
7447        parse(&spec, &input(&["ex", "--url", "u"]))
7448            .expect("the default must not conflict with the flag the user typed");
7449    }
7450
7451    #[test]
7452    fn a_group_naming_two_spellings_of_one_flag_is_not_a_conflict() {
7453        // `-f` and `--file` are one flag. Counted by selector, giving it once would
7454        // report it as conflicting with itself.
7455        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-f --file <f>\"\nflag \"--url <u>\"\ngroup \"input\" \"-f\" \"--file\" \"--url\"\n"
7456            .parse()
7457            .unwrap();
7458
7459        parse(&spec, &input(&["ex", "--file", "a.txt"])).expect("one flag is one member");
7460        parse(&spec, &input(&["ex", "-f", "a.txt"])).expect("either spelling, still one member");
7461
7462        // A genuine collision is still one.
7463        let err = parse(&spec, &input(&["ex", "--file", "a.txt", "--url", "u"])).unwrap_err();
7464        assert!(err.to_string().contains("group input"), "{err}");
7465    }
7466
7467    #[test]
7468    fn a_group_reads_the_environment_as_given() {
7469        // The environment does count, which is the same asymmetry `conflicts` has: an
7470        // env var is somebody saying something, a default is nobody saying anything.
7471        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" env=\"EX_FILE\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
7472            .parse()
7473            .unwrap();
7474
7475        parse_with_env(&spec, &["ex"], &[("EX_FILE", "a.txt")]).expect("the environment fills it");
7476    }
7477
7478    #[test]
7479    fn a_requirement_names_the_flag_that_is_missing() {
7480        // Reported as the missing flag rather than as something wrong with `--out`,
7481        // which is what clap says for an unmet `requires` and what a user can act on.
7482        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\"\n"
7483            .parse()
7484            .unwrap();
7485
7486        let err = parse(&spec, &input(&["ex", "--out", "a.txt"])).unwrap_err();
7487        assert!(
7488            err.to_string().contains("format"),
7489            "the missing flag should be named: {err}"
7490        );
7491
7492        // Satisfied, in either order.
7493        for words in [
7494            &["ex", "--out", "a.txt", "--format", "json"][..],
7495            &["ex", "--format", "json", "--out", "a.txt"][..],
7496        ] {
7497            parse(&spec, &input(words)).unwrap_or_else(|e| panic!("{words:?}: {e}"));
7498        }
7499
7500        // Nothing happens when the flag that imposes the rule is absent: a requirement
7501        // is a consequence of using the flag, not a rule about the command line.
7502        parse(&spec, &input(&["ex"])).expect("a bare invocation requires nothing");
7503    }
7504
7505    #[test]
7506    fn a_value_activates_only_its_conditional_requirement() {
7507        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"
7508            .parse()
7509            .unwrap();
7510
7511        parse(&spec, &input(&["ex", "--config", "ordinary.toml"]))
7512            .expect("an unrelated value requires nothing");
7513
7514        let key = parse(&spec, &input(&["ex", "--config", "special.toml"])).unwrap_err();
7515        assert!(key.to_string().contains("key"), "{key}");
7516        parse(
7517            &spec,
7518            &input(&["ex", "--config", "special.toml", "--key", "secret"]),
7519        )
7520        .expect("the matching requirement is satisfied");
7521
7522        let token = parse(&spec, &input(&["ex", "--config", "remote.toml"])).unwrap_err();
7523        assert!(token.to_string().contains("token"), "{token}");
7524    }
7525
7526    #[test]
7527    fn conditional_requirements_read_explicit_env_but_not_defaults() {
7528        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"
7529            .parse()
7530            .unwrap();
7531        let err = parse_with_env(&from_env, &["ex"], &[("EX_CONFIG", "special.toml")]).unwrap_err();
7532        assert!(err.to_string().contains("key"), "{err}");
7533
7534        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"
7535            .parse()
7536            .unwrap();
7537        parse(&from_default, &input(&["ex"]))
7538            .expect("a default is not an explicit conditional value");
7539    }
7540
7541    #[test]
7542    fn command_line_values_override_env_for_conditional_requirements() {
7543        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--config <file>\" env=\"EX_CONFIG\" {\n  requires_if \"special.toml\" \"--key\"\n}\nflag \"--key <key>\"\n"
7544            .parse()
7545            .unwrap();
7546
7547        parse_with_env(
7548            &spec,
7549            &["ex", "--config", "ordinary.toml"],
7550            &[("EX_CONFIG", "special.toml")],
7551        )
7552        .expect("the command-line value takes precedence over the environment");
7553    }
7554
7555    #[test]
7556    fn conditional_requirements_normalize_boolean_env_values() {
7557        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--feature\" env=\"EX_FEATURE\" {\n  requires_if \"true\" \"--key\"\n}\nflag \"--key <key>\"\n"
7558            .parse()
7559            .unwrap();
7560
7561        for value in ["1", "true", "True", "TRUE"] {
7562            let err = parse_with_env(&spec, &["ex"], &[("EX_FEATURE", value)]).unwrap_err();
7563            assert!(err.to_string().contains("key"), "{value}: {err}");
7564        }
7565        parse_with_env(&spec, &["ex"], &[("EX_FEATURE", "false")])
7566            .expect("a false environment value does not activate a true condition");
7567    }
7568
7569    #[test]
7570    fn a_default_satisfies_a_requirement() {
7571        // The flag it names has a value, which is the question a requirement asks. Read
7572        // any other way, `--format` would be missing here and present ten lines further
7573        // down, where plain required-ness reads the same default as filling it.
7574        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" default=\"json\"\n"
7575            .parse()
7576            .unwrap();
7577
7578        parse(&spec, &input(&["ex", "--out", "a.txt"]))
7579            .expect("a defaulted flag is not a missing one");
7580    }
7581
7582    #[test]
7583    fn a_present_flag_binds_a_conditional_default() {
7584        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" {\n  default_if \"--json\" \"true\"\n}\nflag \"--json\"\n"
7585            .parse()
7586            .unwrap();
7587
7588        let with = parse(&spec, &input(&["ex", "--json"])).unwrap();
7589        assert_eq!(
7590            with.as_env().get("usage_bin_names").map(String::as_str),
7591            Some("true")
7592        );
7593
7594        let without = parse(&spec, &input(&["ex"])).unwrap();
7595        assert!(
7596            !without.as_env().contains_key("usage_bin_names"),
7597            "IsPresent does nothing when the selector is absent"
7598        );
7599    }
7600
7601    #[test]
7602    fn an_equals_condition_binds_a_conditional_default() {
7603        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--style <s>\" {\n  default_if \"--output\" \"json\" \"pretty\"\n}\nflag \"--output <fmt>\"\n"
7604            .parse()
7605            .unwrap();
7606
7607        let json = parse(&spec, &input(&["ex", "--output", "json"])).unwrap();
7608        assert_eq!(
7609            json.as_env().get("usage_style").map(String::as_str),
7610            Some("pretty")
7611        );
7612        let yaml = parse(&spec, &input(&["ex", "--output", "yaml"])).unwrap();
7613        assert!(!yaml.as_env().contains_key("usage_style"));
7614    }
7615
7616    #[test]
7617    fn an_equals_condition_reads_a_negated_flag() {
7618        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pretty\" {\n  default_if \"--json\" \"false\" \"true\"\n}\nflag \"--json\" negate=\"--no-json\"\n"
7619            .parse()
7620            .unwrap();
7621
7622        let off = parse(&spec, &input(&["ex", "--no-json"])).unwrap();
7623        assert_eq!(
7624            off.as_env().get("usage_pretty").map(String::as_str),
7625            Some("true")
7626        );
7627        let on = parse(&spec, &input(&["ex", "--json"])).unwrap();
7628        assert!(
7629            !on.as_env().contains_key("usage_pretty"),
7630            "--json is true, so when=false should miss"
7631        );
7632    }
7633
7634    #[test]
7635    fn the_first_matching_conditional_default_wins() {
7636        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"
7637            .parse()
7638            .unwrap();
7639
7640        let out = parse(&spec, &input(&["ex", "--json", "--pretty"])).unwrap();
7641        assert_eq!(
7642            out.as_env().get("usage_style").map(String::as_str),
7643            Some("compact")
7644        );
7645    }
7646
7647    #[test]
7648    fn argv_and_env_suppress_a_conditional_default() {
7649        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" env=\"EX_BIN\" {\n  default_if \"--json\" \"true\"\n}\nflag \"--json\"\n"
7650            .parse()
7651            .unwrap();
7652
7653        let from_env = parse_with_env(&spec, &["ex", "--json"], &[("EX_BIN", "false")]).unwrap();
7654        assert_eq!(
7655            from_env.as_env().get("usage_bin_names").map(String::as_str),
7656            Some("false"),
7657            "the target's environment wins over default_if"
7658        );
7659    }
7660
7661    #[test]
7662    fn a_sibling_env_activates_a_conditional_default() {
7663        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" {\n  default_if \"--json\" \"true\"\n}\nflag \"--json\" env=\"EX_JSON\"\n"
7664            .parse()
7665            .unwrap();
7666
7667        let out = parse_with_env(&spec, &["ex"], &[("EX_JSON", "1")]).unwrap();
7668        assert_eq!(
7669            out.as_env().get("usage_bin_names").map(String::as_str),
7670            Some("true")
7671        );
7672    }
7673
7674    #[test]
7675    fn a_default_does_not_activate_a_conditional_default() {
7676        // `--json` sorts before `--pretty` in the available-flag map, so a one-pass
7677        // bind would put json's default into `out.flags` and then treat it as
7678        // explicit for pretty's `default_if`. Go and the derive ignore defaults.
7679        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pretty\" {\n  default_if \"--json\" \"true\"\n}\nflag \"--json\" default=#true\n"
7680            .parse()
7681            .unwrap();
7682
7683        let out = parse(&spec, &input(&["ex"])).unwrap();
7684        assert_eq!(
7685            out.as_env().get("usage_json").map(String::as_str),
7686            Some("true")
7687        );
7688        assert!(
7689            !out.as_env().contains_key("usage_pretty"),
7690            "a default is not an explicit value for default_if"
7691        );
7692    }
7693
7694    #[test]
7695    fn a_conditional_default_does_not_activate_requires_if() {
7696        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"
7697            .parse()
7698            .unwrap();
7699
7700        parse(&spec, &input(&["ex", "--json"]))
7701            .expect("a default_if value is not explicit for requires_if");
7702        assert!(parse(&spec, &input(&["ex", "--format", "json"])).is_err());
7703    }
7704
7705    #[test]
7706    fn a_conditional_default_satisfies_a_requirement() {
7707        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" {\n  default_if \"--json\" \"json\"\n}\nflag \"--json\"\n"
7708            .parse()
7709            .unwrap();
7710
7711        parse(&spec, &input(&["ex", "--out", "a.txt", "--json"]))
7712            .expect("default_if fills the required flag");
7713        assert!(parse(&spec, &input(&["ex", "--out", "a.txt"])).is_err());
7714    }
7715
7716    #[test]
7717    fn an_environment_value_satisfies_a_requirement() {
7718        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" env=\"EX_FORMAT\"\n"
7719            .parse()
7720            .unwrap();
7721
7722        assert!(parse(&spec, &input(&["ex", "--out", "a.txt"])).is_err());
7723        parse_with_env(&spec, &["ex", "--out", "a.txt"], &[("EX_FORMAT", "json")])
7724            .expect("the environment supplies it");
7725    }
7726
7727    #[test]
7728    fn a_requirement_is_satisfied_by_a_short_form() {
7729        // The selector may spell the other flag any way it answers to, so the check
7730        // resolves it the way every other selector is resolved rather than matching
7731        // text. The error names the flag, not the selector.
7732        let spec: Spec =
7733            "name \"ex\"\nbin \"ex\"\nflag \"--sign\" requires=\"-k\"\nflag \"-k --key <k>\"\n"
7734                .parse()
7735                .unwrap();
7736
7737        parse(&spec, &input(&["ex", "--sign", "--key", "x"])).expect("--key satisfies -k");
7738
7739        let err = parse(&spec, &input(&["ex", "--sign"])).unwrap_err();
7740        assert!(err.to_string().contains("key"), "{err}");
7741    }
7742
7743    #[test]
7744    fn conflicting_flags_are_rejected_in_either_order() {
7745        // Declared once, on `--file`, which is all clap exposes — so the check has to
7746        // be order-independent by looking at every flag that was given rather than at
7747        // the one that declared the conflict.
7748        let spec: Spec =
7749            "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" conflicts=\"--stdin\"\nflag \"--stdin\"\n"
7750                .parse()
7751                .unwrap();
7752
7753        for words in [
7754            &["ex", "--file", "a.txt", "--stdin"][..],
7755            &["ex", "--stdin", "--file", "a.txt"][..],
7756        ] {
7757            let err = parse(&spec, &input(words)).unwrap_err();
7758            assert!(
7759                err.to_string().contains("conflicts with --stdin"),
7760                "{words:?} should be refused: {err}"
7761            );
7762        }
7763
7764        // Either one alone is fine.
7765        parse(&spec, &input(&["ex", "--stdin"])).unwrap();
7766        parse(&spec, &input(&["ex", "--file", "a.txt"])).unwrap();
7767    }
7768
7769    #[test]
7770    fn unknown_flags_are_values_by_default() {
7771        // The default, and the reason it is the default: a spec often parses a
7772        // command line whose flags belong to something else.
7773        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--force\"\narg \"[rest]...\"\n"
7774            .parse()
7775            .unwrap();
7776        let out = parse(
7777            &spec,
7778            &["ex".to_string(), "--wat".to_string(), "x".to_string()],
7779        )
7780        .unwrap();
7781        let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
7782        assert_eq!(out.args[rest].to_string(), "--wat x");
7783    }
7784
7785    #[test]
7786    fn repeated_scalar_flags_override_by_default_and_can_be_strict() {
7787        let permissive: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--jobs <n>\"\nflag \"--verbose\"\n"
7788            .parse()
7789            .unwrap();
7790        let out = parse(&permissive, &input(&["ex", "--jobs", "1", "--jobs", "2"]))
7791            .expect("a repeat is a correction by default");
7792        let jobs = out.flags.keys().find(|f| f.name == "jobs").unwrap();
7793        assert_eq!(out.flags[jobs].to_string(), "2");
7794        parse(&permissive, &input(&["ex", "--verbose", "--verbose"]))
7795            .expect("switches use the same default");
7796
7797        let strict: Spec = "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--jobs <n>\"\nflag \"--verbose\"\n"
7798            .parse()
7799            .unwrap();
7800        for words in [
7801            &["ex", "--jobs", "1", "--jobs", "2"][..],
7802            &["ex", "--verbose", "--verbose"][..],
7803        ] {
7804            let err = parse(&strict, &input(words)).unwrap_err();
7805            assert!(
7806                err.to_string().contains("cannot be used multiple times"),
7807                "{err}"
7808            );
7809        }
7810
7811        let reparsed: Spec = strict.to_string().parse().unwrap();
7812        assert!(!reparsed.cmd.args_override_self);
7813    }
7814
7815    #[test]
7816    fn strict_negated_flags_allow_opposite_forms_but_reject_the_same_form() {
7817        let spec: Spec = "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--color\" negate=\"--no-color\"\n"
7818            .parse()
7819            .unwrap();
7820
7821        let out = parse(&spec, &input(&["ex", "--color", "--no-color"]))
7822            .expect("opposite forms override each other");
7823        let color = out.flags.keys().find(|f| f.name == "color").unwrap();
7824        assert!(matches!(out.flags[color], ParseValue::Bool(false)));
7825
7826        for words in [
7827            &["ex", "--color", "--color"][..],
7828            &["ex", "--no-color", "--no-color"][..],
7829        ] {
7830            let err = parse(&spec, &input(words)).unwrap_err();
7831            assert!(err.to_string().contains("cannot be used multiple times"));
7832        }
7833    }
7834
7835    #[test]
7836    fn strict_global_flags_may_repeat_across_command_levels() {
7837        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"
7838            .parse()
7839            .unwrap();
7840
7841        let out = parse(
7842            &spec,
7843            &input(&[
7844                "ex", "--color", "--jobs", "1", "run", "--color", "--jobs", "2",
7845            ]),
7846        )
7847        .expect("an inherited global is allowed once at each command level");
7848        let jobs = out.flags.keys().find(|f| f.name == "jobs").unwrap();
7849        assert_eq!(out.flags[jobs].to_string(), "2");
7850
7851        for words in [
7852            &["ex", "--color", "--color", "run", "--no-color"][..],
7853            &["ex", "--jobs", "1", "run", "--jobs", "2", "--jobs", "3"][..],
7854        ] {
7855            let err = parse(&spec, &input(words)).unwrap_err();
7856            assert!(err.to_string().contains("cannot be used multiple times"));
7857        }
7858    }
7859
7860    #[test]
7861    fn a_subcommand_can_negate_only_its_parents_requirements() {
7862        let base = r#"name "ex"
7863bin "ex"
7864flag "--config" required=#true
7865flag "--mode" requires="--config"
7866flag "--other"
7867arg "<input>"
7868group "source" "--config" "--other" required=#true
7869cmd "run" { flag "--child" required=#true }
7870"#;
7871        let strict: Spec = base.parse().unwrap();
7872        let err = parse(&strict, &input(&["ex", "run"])).unwrap_err();
7873        let message = err.to_string();
7874        assert!(
7875            message.contains("input") || message.contains("config"),
7876            "{message}"
7877        );
7878
7879        let negated: Spec = base
7880            .replacen("bin \"ex\"", "bin \"ex\"\nsubcommand_negates_reqs #true", 1)
7881            .parse()
7882            .unwrap();
7883        let err = parse(&negated, &input(&["ex", "run"])).unwrap_err();
7884        assert!(
7885            err.to_string().contains("child"),
7886            "the selected command keeps its own requirements: {err}"
7887        );
7888
7889        let mut child_optional = negated.clone();
7890        child_optional.cmd.subcommands["run"].flags[0].required = false;
7891        parse(&child_optional, &input(&["ex", "run"]))
7892            .expect("the child selection satisfies all parent requirements");
7893        parse(&child_optional, &input(&["ex", "--mode", "run"]))
7894            .expect("parent requires relationships are negated too");
7895    }
7896
7897    #[test]
7898    fn a_parent_argument_can_conflict_with_a_later_subcommand() {
7899        let spec: Spec = r#"name "ex"
7900bin "ex"
7901args_conflicts_with_subcommands #true
7902flag "--verbose"
7903cmd "run"
7904"#
7905        .parse()
7906        .unwrap();
7907
7908        parse(&spec, &input(&["ex", "run"]))
7909            .expect("the subcommand is valid without a parent argument");
7910        let err = parse(&spec, &input(&["ex", "--verbose", "run"])).unwrap_err();
7911        assert!(
7912            err.to_string().contains("cannot be used with arguments"),
7913            "{err}"
7914        );
7915    }
7916
7917    #[test]
7918    fn a_subcommand_can_take_precedence_over_a_variadic_flag() {
7919        let base = r#"name "ex"
7920bin "ex"
7921flag "--values <value>..."
7922cmd "run"
7923"#;
7924        let plain: Spec = base.parse().unwrap();
7925        let out = parse(&plain, &input(&["ex", "--values", "a", "run"])).unwrap();
7926        assert_eq!(out.cmd.name, "ex");
7927
7928        let precedence: Spec = base
7929            .replacen(
7930                "bin \"ex\"",
7931                "bin \"ex\"\nsubcommand_precedence_over_arg #true",
7932                1,
7933            )
7934            .parse()
7935            .unwrap();
7936        let out = parse(&precedence, &input(&["ex", "--values", "a", "run"])).unwrap();
7937        assert_eq!(out.cmd.name, "run");
7938    }
7939
7940    #[test]
7941    fn a_required_positional_can_follow_an_unfilled_optional_one() {
7942        let base = r#"name "ex"
7943bin "ex"
7944arg "[optional]"
7945arg "<required>"
7946"#;
7947        let plain: Spec = base.parse().unwrap();
7948        let err = parse(&plain, &input(&["ex", "value"])).unwrap_err();
7949        assert!(err.to_string().contains("required"), "{err}");
7950
7951        let enabled: Spec = base
7952            .replacen(
7953                "bin \"ex\"",
7954                "bin \"ex\"\nallow_missing_positional #true",
7955                1,
7956            )
7957            .parse()
7958            .unwrap();
7959        let out = parse(&enabled, &input(&["ex", "value"])).unwrap();
7960        assert!(!out.args.keys().any(|arg| arg.name == "optional"));
7961        let value = &out
7962            .args
7963            .iter()
7964            .find(|(arg, _)| arg.name == "required")
7965            .unwrap()
7966            .1;
7967        assert!(matches!(value, ParseValue::String(value) if value == "value"));
7968    }
7969
7970    #[test]
7971    fn sigil_args_do_not_block_optional_positional_skipping() {
7972        let spec: Spec = r#"name "ex"
7973bin "ex"
7974allow_missing_positional #true
7975arg "[optional]"
7976arg "[tool]" sigil="@"
7977arg "<required>"
7978"#
7979        .parse()
7980        .unwrap();
7981
7982        let out = parse(&spec, &input(&["ex", "@node", "value"])).unwrap();
7983        assert!(!out.args.keys().any(|arg| arg.name == "optional"));
7984        let tool = out.args.keys().find(|arg| arg.name == "tool").unwrap();
7985        let required = out.args.keys().find(|arg| arg.name == "required").unwrap();
7986        assert_eq!(out.args[tool].to_string(), "node");
7987        assert_eq!(out.args[required].to_string(), "value");
7988    }
7989
7990    #[test]
7991    fn unknown_flags_can_be_rejected_for_the_whole_cli() {
7992        let spec: Spec =
7993            "name \"ex\"\nbin \"ex\"\nunknown_flags \"error\"\nflag \"--force\"\nflag \"-0 --print0\"\narg \"[rest]...\" allow_negative_numbers=#true\n"
7994                .parse()
7995                .unwrap();
7996        let err = parse(&spec, &["ex".to_string(), "--wat".to_string()]).unwrap_err();
7997        assert!(
7998            err.to_string().contains("--wat"),
7999            "the message should name the token: {err}"
8000        );
8001
8002        // The positional opts into the narrower negative-number carve-out without
8003        // accepting arbitrary unknown flags.
8004        let out = parse(&spec, &["ex".to_string(), "-1".to_string()]).unwrap();
8005        let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
8006        assert_eq!(out.args[rest].to_string(), "-1");
8007
8008        let out = parse(&spec, &["ex".to_string(), "-0".to_string()]).unwrap();
8009        let print0 = out.flags.keys().find(|flag| flag.name == "print0").unwrap();
8010        assert!(matches!(out.flags[print0], ParseValue::Bool(true)));
8011    }
8012
8013    #[test]
8014    fn a_declared_digit_short_does_not_stop_the_subcommand_scan() {
8015        let spec: Spec = r#"
8016name "ex"
8017bin "ex"
8018unknown_flags "error"
8019flag "-0 --print0" global=#true
8020cmd "run" {
8021  flag "--force"
8022}
8023"#
8024        .parse()
8025        .unwrap();
8026        let out = parse(&spec, &input(&["ex", "-0", "run", "--force"])).unwrap();
8027        assert_eq!(out.cmd.name, "run");
8028        let print0 = out.flags.keys().find(|flag| flag.name == "print0").unwrap();
8029        let force = out.flags.keys().find(|flag| flag.name == "force").unwrap();
8030        assert!(matches!(out.flags[print0], ParseValue::Bool(true)));
8031        assert!(matches!(out.flags[force], ParseValue::Bool(true)));
8032    }
8033
8034    #[test]
8035    fn a_command_may_override_the_cli_wide_setting() {
8036        // Strict overall, lenient for the one command that forwards options.
8037        let spec: Spec = r#"
8038name "ex"
8039bin "ex"
8040unknown_flags "error"
8041cmd "exec" unknown_flags="value" {
8042  arg "[rest]..."
8043}
8044cmd "build" {
8045  arg "[rest]..."
8046}
8047"#
8048        .parse()
8049        .unwrap();
8050
8051        let out = parse(
8052            &spec,
8053            &["ex".to_string(), "exec".to_string(), "--wat".to_string()],
8054        )
8055        .unwrap();
8056        let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
8057        assert_eq!(out.args[rest].to_string(), "--wat");
8058
8059        assert!(
8060            parse(
8061                &spec,
8062                &["ex".to_string(), "build".to_string(), "--wat".to_string()]
8063            )
8064            .is_err(),
8065            "a command that says nothing inherits the CLI's choice"
8066        );
8067    }
8068
8069    #[test]
8070    fn the_setting_survives_a_round_trip() {
8071        let spec: Spec =
8072            "name \"ex\"\nbin \"ex\"\nunknown_flags \"error\"\ncmd \"x\" unknown_flags=\"value\"\n"
8073                .parse()
8074                .unwrap();
8075        let reparsed: Spec = spec.to_string().parse().unwrap();
8076        assert_eq!(reparsed.unknown_flags, Some(UnknownFlags::Error));
8077        assert_eq!(
8078            reparsed.cmd.subcommands["x"].unknown_flags,
8079            Some(UnknownFlags::Value)
8080        );
8081    }
8082
8083    #[test]
8084    fn test_default_subcommand() {
8085        // Test that default_subcommand routes to the specified subcommand
8086        let run_cmd = SpecCommand::builder()
8087            .name("run")
8088            .arg(SpecArg::builder().name("task").build())
8089            .build();
8090        let mut cmd = SpecCommand::builder().name("test").build();
8091        cmd.subcommands.insert("run".to_string(), run_cmd);
8092
8093        let spec = Spec {
8094            name: "test".to_string(),
8095            bin: "test".to_string(),
8096            cmd,
8097            default_subcommand: Some("run".to_string()),
8098            ..Default::default()
8099        };
8100
8101        // "test mytask" should be parsed as if it were "test run mytask"
8102        let input = vec!["test".to_string(), "mytask".to_string()];
8103        let parsed = parse(&spec, &input).unwrap();
8104
8105        // Should have two commands: root and "run"
8106        assert_eq!(parsed.cmds.len(), 2);
8107        assert_eq!(parsed.cmds[1].name, "run");
8108
8109        // Should have parsed the task argument
8110        assert_eq!(parsed.args.len(), 1);
8111        let arg = parsed.args.keys().next().unwrap();
8112        assert_eq!(arg.name, "task");
8113        let value = parsed.args.values().next().unwrap();
8114        assert_eq!(value.to_string(), "mytask");
8115    }
8116
8117    #[test]
8118    fn default_subcommand_outranks_root_sigil_arg() {
8119        let spec: Spec = r#"
8120name "test"
8121bin "test"
8122default_subcommand "run"
8123arg "[tools]..." sigil="+"
8124cmd "run" { arg "<task>" }
8125"#
8126        .parse()
8127        .unwrap();
8128
8129        let parsed = parse(&spec, &input(&["test", "+node", "node"])).unwrap();
8130        assert_eq!(parsed.cmd.name, "run");
8131        let value = |name| {
8132            parsed
8133                .args
8134                .iter()
8135                .find(|(arg, _)| arg.name == name)
8136                .map(|(_, value)| value.to_string())
8137                .unwrap()
8138        };
8139        assert_eq!(value("tools"), "node");
8140        assert_eq!(value("task"), "node");
8141    }
8142
8143    #[test]
8144    fn test_default_subcommand_explicit_still_works() {
8145        // Test that explicit subcommand takes precedence
8146        let run_cmd = SpecCommand::builder()
8147            .name("run")
8148            .arg(SpecArg::builder().name("task").build())
8149            .build();
8150        let other_cmd = SpecCommand::builder()
8151            .name("other")
8152            .arg(SpecArg::builder().name("other_arg").build())
8153            .build();
8154        let mut cmd = SpecCommand::builder().name("test").build();
8155        cmd.subcommands.insert("run".to_string(), run_cmd);
8156        cmd.subcommands.insert("other".to_string(), other_cmd);
8157
8158        let spec = Spec {
8159            name: "test".to_string(),
8160            bin: "test".to_string(),
8161            cmd,
8162            default_subcommand: Some("run".to_string()),
8163            ..Default::default()
8164        };
8165
8166        // "test other foo" should use "other" subcommand, not default
8167        let input = vec!["test".to_string(), "other".to_string(), "foo".to_string()];
8168        let parsed = parse(&spec, &input).unwrap();
8169
8170        // Should have used "other" subcommand
8171        assert_eq!(parsed.cmds.len(), 2);
8172        assert_eq!(parsed.cmds[1].name, "other");
8173    }
8174
8175    #[test]
8176    fn test_default_subcommand_applies_only_at_the_root() {
8177        // `default_subcommand` is declared once, for the whole spec, and only at the top. It
8178        // was being looked up wherever the parser happened to be standing, so a command with
8179        // an unrelated subcommand of the same name acquired a default of its own: with
8180        // `default_subcommand "ls"`, `ex config zzz` descended into `config ls` and bound
8181        // `zzz` there. Nothing declared that, and nothing could have.
8182        let mut config_ls = SpecCommand::builder().name("ls").build();
8183        config_ls.args.push(SpecArg::builder().name("what").build());
8184        let mut config_cmd = SpecCommand::builder().name("config").build();
8185        config_cmd.subcommands.insert("ls".to_string(), config_ls);
8186
8187        // The root's own `ls`, which is what its default points at. It takes an argument so
8188        // that a routed word has somewhere to land.
8189        let mut root_ls = SpecCommand::builder().name("ls").build();
8190        root_ls.args.push(SpecArg::builder().name("what").build());
8191        let mut cmd = SpecCommand::builder().name("ex").build();
8192        cmd.subcommands.insert("ls".to_string(), root_ls);
8193        cmd.subcommands.insert("config".to_string(), config_cmd);
8194
8195        let spec = Spec {
8196            name: "ex".to_string(),
8197            bin: "ex".to_string(),
8198            cmd,
8199            default_subcommand: Some("ls".to_string()),
8200            ..Default::default()
8201        };
8202
8203        // `config` has an `ls`, but `config` did not declare a default, so `zzz` is `config`'s
8204        // own business — and `config` takes no argument, so this is an error rather than a
8205        // silent descent.
8206        let input = vec!["ex".to_string(), "config".to_string(), "zzz".to_string()];
8207        assert!(
8208            parse(&spec, &input).is_err(),
8209            "`config` has no default subcommand and no argument, so `zzz` cannot bind"
8210        );
8211
8212        // At the root, where it is declared, it still applies.
8213        let input = vec!["ex".to_string(), "zzz".to_string()];
8214        let parsed = parse(&spec, &input).expect("the root's default applies");
8215        assert_eq!(
8216            parsed
8217                .cmds
8218                .iter()
8219                .map(|c| c.name.as_str())
8220                .collect::<Vec<_>>(),
8221            ["ex", "ls"]
8222        );
8223        assert_eq!(
8224            parsed.args.values().next().map(|v| v.to_string()),
8225            Some("zzz".to_string()),
8226            "and the word binds inside the command it reached"
8227        );
8228    }
8229
8230    #[test]
8231    fn test_default_subcommand_with_nested_subcommands() {
8232        // Test that default_subcommand works when the default subcommand has nested subcommands.
8233        // This is the mise use case: "mise say" should be parsed as "mise run say"
8234        // where "say" is a subcommand of "run" (a task).
8235        let say_cmd = SpecCommand::builder()
8236            .name("say")
8237            .arg(SpecArg::builder().name("name").build())
8238            .build();
8239        let mut run_cmd = SpecCommand::builder().name("run").build();
8240        run_cmd.subcommands.insert("say".to_string(), say_cmd);
8241
8242        let mut cmd = SpecCommand::builder().name("test").build();
8243        cmd.subcommands.insert("run".to_string(), run_cmd);
8244
8245        let spec = Spec {
8246            name: "test".to_string(),
8247            bin: "test".to_string(),
8248            cmd,
8249            default_subcommand: Some("run".to_string()),
8250            ..Default::default()
8251        };
8252
8253        // "test say hello" should be parsed as "test run say hello"
8254        let input = vec!["test".to_string(), "say".to_string(), "hello".to_string()];
8255        let parsed = parse(&spec, &input).unwrap();
8256
8257        // Should have three commands: root, "run", and "say"
8258        assert_eq!(parsed.cmds.len(), 3);
8259        assert_eq!(parsed.cmds[0].name, "test");
8260        assert_eq!(parsed.cmds[1].name, "run");
8261        assert_eq!(parsed.cmds[2].name, "say");
8262
8263        // Should have parsed the "name" argument
8264        assert_eq!(parsed.args.len(), 1);
8265        let arg = parsed.args.keys().next().unwrap();
8266        assert_eq!(arg.name, "name");
8267        let value = parsed.args.values().next().unwrap();
8268        assert_eq!(value.to_string(), "hello");
8269    }
8270
8271    /// Build a spec equivalent to the post-mount structure produced by mise's
8272    /// `mise usage` output: a root with a value-taking global flag (`-C/--cd`), a `run`
8273    /// subcommand that re-declares the same flag as NON-global, and a mounted task
8274    /// (`sample:run`) carrying a positional arg with `choices`.
8275    ///
8276    /// We construct the merged structure directly instead of executing a real mount so the
8277    /// test stays hermetic and cross-platform while still exercising the parser defect.
8278    fn mounted_global_flag_spec() -> Spec {
8279        let task_cmd = SpecCommand::builder()
8280            .name("sample:run")
8281            .arg(
8282                SpecArg::builder()
8283                    .name("profile")
8284                    .choices(["alpha", "beta", "gamma"])
8285                    .build(),
8286            )
8287            .build();
8288        // `run` re-declares `-C/--cd` but as a NON-global flag, mirroring the mise spec.
8289        let mut run_cmd = SpecCommand::builder()
8290            .name("run")
8291            .flag(
8292                SpecFlag::builder()
8293                    .name("cd")
8294                    .short('C')
8295                    .long("cd")
8296                    .arg(SpecArg::builder().name("dir").build())
8297                    .global(false)
8298                    .build(),
8299            )
8300            .build();
8301        run_cmd
8302            .subcommands
8303            .insert("sample:run".to_string(), task_cmd);
8304
8305        let mut cmd = SpecCommand::builder()
8306            .name("test")
8307            .flag(
8308                SpecFlag::builder()
8309                    .name("cd")
8310                    .short('C')
8311                    .long("cd")
8312                    .arg(SpecArg::builder().name("dir").build())
8313                    .global(true)
8314                    .build(),
8315            )
8316            .build();
8317        cmd.subcommands.insert("run".to_string(), run_cmd);
8318
8319        Spec {
8320            name: "test".to_string(),
8321            bin: "test".to_string(),
8322            cmd,
8323            ..Default::default()
8324        }
8325    }
8326
8327    #[test]
8328    fn test_prefix_global_flag_does_not_pollute_choices() {
8329        // Regression for the parser-side root cause referenced by jdx/mise#10069.
8330        //
8331        // When `run` re-declares the global `-C/--cd` as non-global, descending into it (and
8332        // then into the mounted `sample:run`) used to drop the inherited global flag from
8333        // `available_flags`. Phase 2 then no longer recognized the prefix `-C`, so it was
8334        // mis-validated against the task's `choices` positional arg.
8335        let spec = mounted_global_flag_spec();
8336
8337        // The prefix global flag must stay recognized so it is consumed as a flag (not as the
8338        // positional). Before the fix this bailed with "Invalid choice for arg profile: -C".
8339        for words in [
8340            &["test", "-C", "/tmp", "run", "sample:run"][..],
8341            // Embedded-value form must behave identically.
8342            &["test", "--cd=/tmp", "run", "sample:run"][..],
8343        ] {
8344            let parsed = parse_partial(&spec, &input(words)).unwrap();
8345            assert_eq!(
8346                parsed
8347                    .cmds
8348                    .iter()
8349                    .map(|c| c.name.as_str())
8350                    .collect::<Vec<_>>(),
8351                vec!["test", "run", "sample:run"],
8352            );
8353            // No positional arg should have been consumed by the leftover global-flag tokens.
8354            assert!(
8355                parsed.args.is_empty(),
8356                "args should be empty, got {:?}",
8357                parsed.args
8358            );
8359
8360            // Fix (B): the inherited global flag survives the descent even though `run`
8361            // re-declares `-C/--cd` as non-global.
8362            let cd = parsed
8363                .available_flags
8364                .get("--cd")
8365                .expect("--cd should remain available after descending into the subcommand");
8366            assert!(cd.global, "--cd must stay global after descent");
8367            assert!(
8368                parsed.available_flags.get("-C").is_some_and(|f| f.global),
8369                "-C must stay global after descent",
8370            );
8371
8372            // The global flag must still be recorded in `out.flags` so it reaches `as_env()`
8373            // for normal execution and for the env passed to mount scripts. (Removing the
8374            // token in Phase 1 instead of re-parsing it would silently drop `usage_cd`.)
8375            assert_eq!(
8376                parsed.as_env().get("usage_cd").map(String::as_str),
8377                Some("/tmp"),
8378                "global flag value must survive in as_env(), got {:?}",
8379                parsed.as_env(),
8380            );
8381        }
8382
8383        // A real, valid choice still parses through the global flag prefix.
8384        let parsed = parse_partial(
8385            &spec,
8386            &input(&["test", "-C", "/tmp", "run", "sample:run", "alpha"]),
8387        )
8388        .unwrap();
8389        assert_eq!(parsed.args.len(), 1);
8390        assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
8391
8392        // And genuinely invalid choices are still rejected (we didn't disable validation).
8393        assert_parse_err(
8394            parse_partial(&spec, &input(&["test", "run", "sample:run", "wrong"])),
8395            "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
8396        );
8397    }
8398
8399    /// Build a spec mirroring mise's orphan-short re-declarations: a root with a LONG-ONLY
8400    /// global boolean flag (`--raw`, no short), a `run` subcommand that re-declares it as a
8401    /// NON-global flag while ADDING a short (`-r --raw`) plus a purely-local `-f/--force`
8402    /// flag, and a mounted task (`sample:run`) with a `choices` positional arg.
8403    fn mounted_orphan_short_spec() -> Spec {
8404        let task_cmd = SpecCommand::builder()
8405            .name("sample:run")
8406            .arg(
8407                SpecArg::builder()
8408                    .name("profile")
8409                    .choices(["alpha", "beta", "gamma"])
8410                    .build(),
8411            )
8412            .build();
8413        // `run` re-declares `--raw` as NON-global but adds a `-r` short that exists only here,
8414        // and also carries a purely-local `-f/--force` flag (shares nothing with a global).
8415        let mut run_cmd = SpecCommand::builder()
8416            .name("run")
8417            .flag(
8418                SpecFlag::builder()
8419                    .name("raw")
8420                    .short('r')
8421                    .long("raw")
8422                    .global(false)
8423                    .build(),
8424            )
8425            .flag(
8426                SpecFlag::builder()
8427                    .name("force")
8428                    .short('f')
8429                    .long("force")
8430                    .global(false)
8431                    .build(),
8432            )
8433            .build();
8434        run_cmd
8435            .subcommands
8436            .insert("sample:run".to_string(), task_cmd);
8437
8438        // Root global is LONG-ONLY: `--raw` with no short.
8439        let mut cmd = SpecCommand::builder()
8440            .name("test")
8441            .flag(
8442                SpecFlag::builder()
8443                    .name("raw")
8444                    .long("raw")
8445                    .global(true)
8446                    .build(),
8447            )
8448            .build();
8449        cmd.subcommands.insert("run".to_string(), run_cmd);
8450
8451        Spec {
8452            name: "test".to_string(),
8453            bin: "test".to_string(),
8454            cmd,
8455            ..Default::default()
8456        }
8457    }
8458
8459    #[test]
8460    fn test_orphan_short_alias_survives_merge() {
8461        // Follow-up to test_prefix_global_flag_does_not_pollute_choices (jdx/mise#10069):
8462        // when `run` re-declares the long-only global `--raw` as a non-global `-r --raw`, the
8463        // added short `-r` must be unioned onto the surviving inherited global flag instead of
8464        // being discarded with the wholesale re-declaration. Otherwise `mycli run -r <task>`
8465        // would not recognize `-r` and would mis-validate it against the task's `choices` arg.
8466        let spec = mounted_orphan_short_spec();
8467
8468        let parsed = parse_partial(&spec, &input(&["test", "run", "-r", "sample:run"])).unwrap();
8469        assert_eq!(
8470            parsed
8471                .cmds
8472                .iter()
8473                .map(|c| c.name.as_str())
8474                .collect::<Vec<_>>(),
8475            vec!["test", "run", "sample:run"],
8476        );
8477
8478        // (a) The orphan short `-r` survives the descent, merged onto the inherited global flag,
8479        // and the original long `--raw` is still global too.
8480        assert!(
8481            parsed.available_flags.get("-r").is_some_and(|f| f.global),
8482            "-r must be merged onto the inherited global flag and stay global after descent",
8483        );
8484        assert!(
8485            parsed
8486                .available_flags
8487                .get("--raw")
8488                .is_some_and(|f| f.global),
8489            "--raw must stay global after descent",
8490        );
8491
8492        // (b) The token is consumed as a flag, not mistaken for the `choices` positional.
8493        assert!(
8494            parsed.args.is_empty(),
8495            "args should be empty, got {:?}",
8496            parsed.args
8497        );
8498
8499        // (c) The value still reaches as_env() so `usage_raw` is produced for execution/mounts.
8500        assert_eq!(
8501            parsed.as_env().get("usage_raw").map(String::as_str),
8502            Some("true"),
8503            "merged short's value must survive in as_env(), got {:?}",
8504            parsed.as_env(),
8505        );
8506
8507        // (d) Negative case: a purely-local flag that shares nothing with a global is NOT
8508        // promoted/merged — it is correctly dropped when descending into the mount.
8509        assert!(
8510            !parsed.available_flags.contains_key("-f"),
8511            "purely-local -f must not be promoted onto a global",
8512        );
8513        assert!(
8514            !parsed.available_flags.contains_key("--force"),
8515            "purely-local --force must not be promoted onto a global",
8516        );
8517
8518        // A real, valid choice still parses through the merged short prefix.
8519        let parsed =
8520            parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "alpha"])).unwrap();
8521        assert_eq!(parsed.args.len(), 1);
8522        assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
8523
8524        // And genuinely invalid choices are still rejected.
8525        assert_parse_err(
8526            parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "wrong"])),
8527            "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
8528        );
8529    }
8530
8531    #[test]
8532    fn test_orphan_short_does_not_clobber_unrelated_global() {
8533        // When a re-declaration's orphan short collides with a DIFFERENT inherited global's
8534        // short, the merge must not steal it. Here the root has both a long-only `--raw` global
8535        // and a `-r --restrict` global; `run` re-declares `-r --raw` as non-global. `-r` is a
8536        // genuine collision with `--restrict`, so global precedence must keep `-r -> restrict`.
8537        let run_cmd = SpecCommand::builder()
8538            .name("run")
8539            .flag(
8540                SpecFlag::builder()
8541                    .name("raw")
8542                    .short('r')
8543                    .long("raw")
8544                    .global(false)
8545                    .build(),
8546            )
8547            .build();
8548        let mut cmd = SpecCommand::builder()
8549            .name("test")
8550            .flag(
8551                SpecFlag::builder()
8552                    .name("raw")
8553                    .long("raw")
8554                    .global(true)
8555                    .build(),
8556            )
8557            .flag(
8558                SpecFlag::builder()
8559                    .name("restrict")
8560                    .short('r')
8561                    .long("restrict")
8562                    .global(true)
8563                    .build(),
8564            )
8565            .build();
8566        cmd.subcommands.insert("run".to_string(), run_cmd);
8567        let spec = Spec {
8568            name: "test".to_string(),
8569            bin: "test".to_string(),
8570            cmd,
8571            ..Default::default()
8572        };
8573
8574        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8575        // `-r` stays owned by the unrelated `--restrict` global, not stolen by the merged raw.
8576        assert_eq!(
8577            parsed.available_flags.get("-r").map(|f| f.name.as_str()),
8578            Some("restrict"),
8579            "-r must remain owned by the unrelated global it already belonged to",
8580        );
8581        // Both globals are still recognized and global after the descent.
8582        assert!(
8583            parsed
8584                .available_flags
8585                .get("--raw")
8586                .is_some_and(|f| f.global)
8587        );
8588        assert!(
8589            parsed
8590                .available_flags
8591                .get("--restrict")
8592                .is_some_and(|f| f.global)
8593        );
8594    }
8595
8596    #[test]
8597    fn test_redeclared_global_aliases_share_one_flag() {
8598        // A global declared with BOTH a short and a long, re-declared non-globally by a
8599        // subcommand that adds a third alias. Every alias key must resolve to the SAME merged
8600        // flag: the child's keys iterate in BTreeMap order (`--assume-yes`, `--yes`, `-y`), so by
8601        // the time `-y` is reached the long already points at the merged flag. That merged flag is
8602        // not a *different* inherited global, so the collision guard must not skip `-y` and leave
8603        // it pointing at the pre-merge global (which lacks the added `assume-yes` alias).
8604        let spec = r#"
8605flag "-y --yes" global=#true effect="write"
8606cmd "run" {
8607    flag "-y --yes --assume-yes"
8608}
8609"#
8610        .parse::<Spec>()
8611        .unwrap();
8612
8613        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8614
8615        for key in ["-y", "--yes", "--assume-yes"] {
8616            let flag = parsed
8617                .available_flags
8618                .get(key)
8619                .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
8620            assert!(flag.global, "{key} must stay global after the descent");
8621            assert_eq!(
8622                flag.long,
8623                vec!["yes".to_string(), "assume-yes".to_string()],
8624                "{key} must resolve to the flag carrying every alias",
8625            );
8626            assert_eq!(flag.short, vec!['y'], "{key} must keep the global's short");
8627        }
8628
8629        // One logical flag means one object: all three keys share a single `Arc`.
8630        assert_eq!(
8631            unique_flags(parsed.available_flags.values()).count(),
8632            1,
8633            "all aliases must point at one flag object, got {:?}",
8634            parsed.available_flags,
8635        );
8636
8637        // The global's effect survives the merge, so `-y` still marks the command as writing.
8638        assert_eq!(
8639            parsed.available_flags["-y"].effect,
8640            Some(crate::SpecCommandEffect::Write),
8641        );
8642    }
8643
8644    #[test]
8645    fn test_redeclared_global_keeps_hidden_alias_metadata() {
8646        let spec = r#"
8647flag "--yes" global=#true {
8648    alias "-q" "--quietly" hide=#true
8649}
8650cmd "run" {
8651    flag "--yes --assume-yes" {
8652        alias "-s" "--secret" hide=#true
8653    }
8654}
8655"#
8656        .parse::<Spec>()
8657        .unwrap();
8658
8659        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8660        let merged = &parsed.available_flags["--yes"];
8661        assert_eq!(merged.hidden_short_aliases, ['q', 's']);
8662        assert_eq!(merged.hidden_aliases, ["quietly", "secret"]);
8663        for key in ["-q", "-s", "--quietly", "--secret"] {
8664            assert!(Arc::ptr_eq(&parsed.available_flags[key], merged), "{key}");
8665        }
8666    }
8667
8668    #[test]
8669    fn test_redeclared_global_can_promote_hidden_aliases() {
8670        let spec = r#"
8671flag "--yes" global=#true {
8672    alias "-q" "--quietly" hide=#true
8673}
8674cmd "run" {
8675    flag "-q --yes --quietly"
8676}
8677"#
8678        .parse::<Spec>()
8679        .unwrap();
8680
8681        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8682        let merged = &parsed.available_flags["--yes"];
8683        assert!(merged.hidden_short_aliases.is_empty());
8684        assert!(merged.hidden_aliases.is_empty());
8685        for key in ["-q", "--quietly"] {
8686            assert!(Arc::ptr_eq(&parsed.available_flags[key], merged), "{key}");
8687        }
8688    }
8689
8690    #[test]
8691    fn test_partially_redeclared_global_keeps_all_aliases_on_one_flag() {
8692        // Same one-flag-one-object requirement as above, but the child re-declares only ONE of
8693        // the global's three aliases (`--yes`, not `-y`/`--confirm`) while adding a new one. The
8694        // aliases the child omits are never visited by the merge loop, so they must be rebound to
8695        // the merged flag explicitly — otherwise `-y` and `--confirm` keep pointing at the
8696        // pre-merge global and miss the added `assume-yes`.
8697        let spec = r#"
8698flag "-y --yes --confirm" global=#true
8699cmd "run" {
8700    flag "--yes --assume-yes"
8701}
8702"#
8703        .parse::<Spec>()
8704        .unwrap();
8705
8706        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8707
8708        for key in ["-y", "--yes", "--confirm", "--assume-yes"] {
8709            let flag = parsed
8710                .available_flags
8711                .get(key)
8712                .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
8713            assert!(flag.global, "{key} must stay global after the descent");
8714            assert_eq!(
8715                flag.long,
8716                vec![
8717                    "yes".to_string(),
8718                    "confirm".to_string(),
8719                    "assume-yes".to_string()
8720                ],
8721                "{key} must resolve to the flag carrying every alias",
8722            );
8723        }
8724
8725        assert_eq!(
8726            unique_flags(parsed.available_flags.values()).count(),
8727            1,
8728            "all aliases must point at one flag object, got {:?}",
8729            parsed.available_flags,
8730        );
8731    }
8732
8733    /// Build a spec shaped like mise's post-mount structure for jdx/mise#11282: a root with
8734    /// globals (`-E/--env <ENV>`, `--silent`), a `run` subcommand with a non-global flag, and a
8735    /// MOUNTED task command that declares its own `--env` (with choices) plus `--bump`.
8736    ///
8737    /// The task command is marked `mounted` the same way `SpecCommand::mount()` marks the
8738    /// commands it merges in, so the test stays hermetic (no mount subprocess).
8739    fn mounted_task_flag_spec() -> Spec {
8740        let mut task_cmd = SpecCommand::builder()
8741            .name("mytask")
8742            .flag(
8743                SpecFlag::builder()
8744                    .name("env")
8745                    .long("env")
8746                    .arg(
8747                        SpecArg::builder()
8748                            .name("name")
8749                            .choices(["dev", "stage", "prod"])
8750                            .build(),
8751                    )
8752                    .global(false)
8753                    .build(),
8754            )
8755            .flag(
8756                SpecFlag::builder()
8757                    .name("bump")
8758                    .long("bump")
8759                    .arg(
8760                        SpecArg::builder()
8761                            .name("type")
8762                            .choices(["auto", "major"])
8763                            .build(),
8764                    )
8765                    .global(false)
8766                    .build(),
8767            )
8768            .build();
8769        task_cmd.mounted = true;
8770
8771        let mut run_cmd = SpecCommand::builder()
8772            .name("run")
8773            .flag(
8774                SpecFlag::builder()
8775                    .name("force")
8776                    .short('f')
8777                    .long("force")
8778                    .global(false)
8779                    .build(),
8780            )
8781            .build();
8782        run_cmd.subcommands.insert("mytask".to_string(), task_cmd);
8783
8784        let mut cmd = SpecCommand::builder()
8785            .name("test")
8786            .flag(
8787                SpecFlag::builder()
8788                    .name("env")
8789                    .short('E')
8790                    .long("env")
8791                    .arg(SpecArg::builder().name("ENV").build())
8792                    .global(true)
8793                    .build(),
8794            )
8795            .flag(
8796                SpecFlag::builder()
8797                    .name("silent")
8798                    .long("silent")
8799                    .global(true)
8800                    .build(),
8801            )
8802            .build();
8803        cmd.subcommands.insert("run".to_string(), run_cmd);
8804
8805        Spec {
8806            name: "test".to_string(),
8807            bin: "test".to_string(),
8808            cmd,
8809            ..Default::default()
8810        }
8811    }
8812
8813    #[test]
8814    fn test_mount_boundary_does_not_apply_inside_the_mounted_tree() {
8815        // The mounted program's own commands are ordinary commands relative to each other, so
8816        // descending *within* the mounted tree must follow the normal rules — including keeping
8817        // an inherited global that a nested command re-declares as non-global (jdx/usage#649).
8818        // Treating every level of the tree as a mount boundary let the re-declaration shadow the
8819        // global, which the next descent's `retain(global)` then dropped entirely.
8820        let deep = SpecCommand::builder().name("deep").build();
8821        let mut sub = SpecCommand::builder()
8822            .name("sub")
8823            // Re-declares the mounted program's own global as non-global.
8824            .flag(
8825                SpecFlag::builder()
8826                    .name("cd")
8827                    .short('C')
8828                    .long("cd")
8829                    .arg(SpecArg::builder().name("dir").build())
8830                    .global(false)
8831                    .build(),
8832            )
8833            .build();
8834        sub.subcommands.insert("deep".to_string(), deep);
8835        let mut task = SpecCommand::builder()
8836            .name("task")
8837            .flag(
8838                SpecFlag::builder()
8839                    .name("cd")
8840                    .short('C')
8841                    .long("cd")
8842                    .arg(SpecArg::builder().name("dir").build())
8843                    .global(true)
8844                    .build(),
8845            )
8846            .build();
8847        task.subcommands.insert("sub".to_string(), sub);
8848        task.mark_mounted();
8849
8850        let mut run_cmd = SpecCommand::builder().name("run").build();
8851        run_cmd.subcommands.insert("task".to_string(), task);
8852        let mut cmd = SpecCommand::builder().name("test").build();
8853        cmd.subcommands.insert("run".to_string(), run_cmd);
8854        let spec = Spec {
8855            name: "test".to_string(),
8856            bin: "test".to_string(),
8857            cmd,
8858            ..Default::default()
8859        };
8860
8861        let parsed = parse_partial(&spec, &input(&["test", "run", "task", "sub", "deep"])).unwrap();
8862        assert!(
8863            parsed.available_flags.get("--cd").is_some_and(|f| f.global),
8864            "the mounted program's own global must survive descents inside the mounted tree",
8865        );
8866        assert!(
8867            parsed.completion_flags().contains_key("--cd"),
8868            "and must still be offered there: it belongs to the mounted program",
8869        );
8870        assert!(
8871            parsed.completion_flags().contains_key("-C"),
8872            "including the short the nested command re-declared",
8873        );
8874    }
8875
8876    #[test]
8877    fn test_mount_flags_merged_into_the_mounting_cmd_are_offered() {
8878        // A mounted spec may declare flags on its own root, which `SpecCommand::merge` folds
8879        // into the command the mount sits on. They belong to the mounted program, so they must
8880        // be offered inside the mounted commands rather than filtered out with the mounting
8881        // CLI's own flags.
8882        let mut task = SpecCommand::builder()
8883            .name("task")
8884            .flag(
8885                SpecFlag::builder()
8886                    .name("bump")
8887                    .long("bump")
8888                    .global(false)
8889                    .build(),
8890            )
8891            .build();
8892        task.mark_mounted();
8893
8894        let mut run_cmd = SpecCommand::builder().name("run").build();
8895        run_cmd.subcommands.insert("task".to_string(), task);
8896        // What `mount()` leaves behind when the mounted spec's root declares flags.
8897        run_cmd.flags = vec![
8898            SpecFlag::builder()
8899                .name("tglobal")
8900                .long("tglobal")
8901                .global(true)
8902                .build(),
8903            SpecFlag::builder()
8904                .name("tlocal")
8905                .long("tlocal")
8906                .global(false)
8907                .build(),
8908        ];
8909        run_cmd.flags_from_mount = true;
8910
8911        let mut cmd = SpecCommand::builder()
8912            .name("test")
8913            .flag(
8914                SpecFlag::builder()
8915                    .name("silent")
8916                    .long("silent")
8917                    .global(true)
8918                    .build(),
8919            )
8920            .build();
8921        cmd.subcommands.insert("run".to_string(), run_cmd);
8922        let spec = Spec {
8923            name: "test".to_string(),
8924            bin: "test".to_string(),
8925            cmd,
8926            ..Default::default()
8927        };
8928
8929        let parsed = parse_partial(&spec, &input(&["test", "run", "task"])).unwrap();
8930        assert_eq!(
8931            parsed.completion_flags().keys().collect::<Vec<_>>(),
8932            vec!["--bump", "--tglobal"],
8933            "the mounted spec's root global belongs to the mounted program; the mounting CLI's \
8934             `--silent` does not, and the mount's non-global root flag is not inherited",
8935        );
8936    }
8937
8938    #[test]
8939    fn test_mounted_cmd_does_not_offer_mounting_cli_globals() {
8940        // Regression for jdx/mise#11282. A mounted command describes another program, which
8941        // does not accept the mounting CLI's globals (mise forwards everything after a task
8942        // name to the task). They must stay recognized — they may appear before the mounted
8943        // command — but must not be offered in completions there.
8944        let spec = mounted_task_flag_spec();
8945        let parsed = parse_partial(&spec, &input(&["test", "run", "mytask"])).unwrap();
8946
8947        // Still recognized for parsing...
8948        assert!(parsed.available_flags.contains_key("--silent"));
8949        assert!(parsed.available_flags.contains_key("-E"));
8950        // ...but belonging to a command above the mount, so not offered.
8951        assert_eq!(
8952            parsed.completion_flags().keys().collect::<Vec<_>>(),
8953            vec!["--bump", "--env"],
8954            "only the mounted command's own flags may be offered",
8955        );
8956
8957        // `run`'s own non-global flag is dropped on descent, as it always was.
8958        assert!(!parsed.available_flags.contains_key("--force"));
8959    }
8960
8961    #[test]
8962    fn test_mounted_cmd_flag_wins_over_inherited_global() {
8963        // Second half of jdx/mise#11282: the mounted `--env` (with choices) used to be shadowed
8964        // by the root's `--env` global, so completing its value fell back to file completion.
8965        let spec = mounted_task_flag_spec();
8966        let parsed = parse_partial(&spec, &input(&["test", "run", "mytask", "--env"])).unwrap();
8967
8968        let awaiting = parsed
8969            .flag_awaiting_value
8970            .first()
8971            .expect("--env should await a value");
8972        assert_eq!(
8973            awaiting
8974                .arg
8975                .as_ref()
8976                .and_then(|a| a.choices.as_ref())
8977                .map(|c| c.choices.clone()),
8978            Some(vec![
8979                "dev".to_string(),
8980                "stage".to_string(),
8981                "prod".to_string()
8982            ]),
8983            "the mounted command's own --env must win over the inherited global",
8984        );
8985
8986        // The global's short is not declared by the mounted command, so it keeps pointing at
8987        // the global and a value passed before the mounted command still parses.
8988        let parsed =
8989            parse_partial(&spec, &input(&["test", "-E", "anything", "run", "mytask"])).unwrap();
8990        assert!(
8991            parsed.args.is_empty(),
8992            "prefix global tokens must not be consumed as positionals, got {:?}",
8993            parsed.args
8994        );
8995        assert_eq!(
8996            parsed.as_env().get("usage_env").map(String::as_str),
8997            Some("anything"),
8998        );
8999    }
9000
9001    #[test]
9002    fn test_prefix_flag_keeps_the_flag_it_was_read_as() {
9003        // A word before the mounted command is re-parsed by Phase 2, when the mounted command
9004        // already owns the name. It has to stay bound to the flag Phase 1 read it as, or the
9005        // global's value would be validated against the mounted flag's choices and a legitimate
9006        // value would be rejected.
9007        let spec = mounted_task_flag_spec();
9008        let parsed = parse_partial(
9009            &spec,
9010            &input(&["test", "--env", "not-a-task-choice", "run", "mytask"]),
9011        )
9012        .unwrap();
9013        assert!(
9014            parsed.errors.is_empty(),
9015            "prefix global value must not be validated against the mounted flag: {:?}",
9016            parsed
9017                .errors
9018                .iter()
9019                .map(|e| e.to_string())
9020                .collect::<Vec<_>>(),
9021        );
9022        assert_eq!(
9023            parsed.as_env().get("usage_env").map(String::as_str),
9024            Some("not-a-task-choice"),
9025        );
9026
9027        // The embedded-value form binds the same way.
9028        let parsed = parse_partial(
9029            &spec,
9030            &input(&["test", "--env=not-a-task-choice", "run", "mytask"]),
9031        )
9032        .unwrap();
9033        assert!(parsed.errors.is_empty());
9034        assert_eq!(
9035            parsed.as_env().get("usage_env").map(String::as_str),
9036            Some("not-a-task-choice"),
9037        );
9038
9039        // Meanwhile a word *after* the mounted command belongs to the mounted flag, even when
9040        // the same name was already used before it.
9041        let parsed = parse_partial(
9042            &spec,
9043            &input(&["test", "--env", "prod", "run", "mytask", "--env"]),
9044        )
9045        .unwrap();
9046        let awaiting = parsed
9047            .flag_awaiting_value
9048            .first()
9049            .expect("--env should await a value");
9050        assert_eq!(
9051            awaiting
9052                .arg
9053                .as_ref()
9054                .and_then(|a| a.choices.as_ref())
9055                .map(|c| c.choices.clone()),
9056            Some(vec![
9057                "dev".to_string(),
9058                "stage".to_string(),
9059                "prod".to_string()
9060            ]),
9061            "the mounted command's --env must own the name after the mounted command",
9062        );
9063    }
9064
9065    #[test]
9066    fn test_non_global_flag_does_not_hide_subcommand() {
9067        // A non-global flag may precede a subcommand (`mycli run --force task`). Phase 1 used to
9068        // stop scanning at one, so the subcommand — and any mount on it — was never reached and
9069        // its name was left to Phase 2 to mis-read as a positional: `unexpected word: mytask`.
9070        let spec = mounted_task_flag_spec();
9071
9072        for words in [
9073            // `run` declares `-f/--force` as non-global.
9074            &["test", "run", "--force", "mytask"][..],
9075            &["test", "run", "-f", "mytask"][..],
9076            // Mixed with a global before the subcommand.
9077            &["test", "-E", "prod", "run", "--force", "mytask"][..],
9078        ] {
9079            let parsed = parse_partial(&spec, &input(words)).unwrap();
9080            assert_eq!(
9081                parsed
9082                    .cmds
9083                    .iter()
9084                    .map(|c| c.name.as_str())
9085                    .collect::<Vec<_>>(),
9086                vec!["test", "run", "mytask"],
9087                "{words:?} should descend into the mounted command",
9088            );
9089            assert!(
9090                parsed.args.is_empty(),
9091                "{words:?} should not consume a positional, got {:?}",
9092                parsed.args,
9093            );
9094            assert_eq!(
9095                parsed.as_env().get("usage_force").map(String::as_str),
9096                Some("true"),
9097                "the non-global flag must still be recorded for {words:?}",
9098            );
9099        }
9100
9101        // A non-global flag that takes a value consumes it, rather than reading the value as the
9102        // subcommand.
9103        let mut run_cmd = SpecCommand::builder()
9104            .name("run")
9105            .flag(
9106                SpecFlag::builder()
9107                    .name("output")
9108                    .short('o')
9109                    .long("output")
9110                    .arg(SpecArg::builder().name("mode").build())
9111                    .global(false)
9112                    .build(),
9113            )
9114            .build();
9115        run_cmd.subcommands.insert(
9116            "task".to_string(),
9117            SpecCommand::builder().name("task").build(),
9118        );
9119        let mut cmd = SpecCommand::builder().name("test").build();
9120        cmd.subcommands.insert("run".to_string(), run_cmd);
9121        let spec = Spec {
9122            name: "test".to_string(),
9123            bin: "test".to_string(),
9124            cmd,
9125            ..Default::default()
9126        };
9127
9128        let parsed =
9129            parse_partial(&spec, &input(&["test", "run", "--output", "quiet", "task"])).unwrap();
9130        assert_eq!(
9131            parsed
9132                .cmds
9133                .iter()
9134                .map(|c| c.name.as_str())
9135                .collect::<Vec<_>>(),
9136            vec!["test", "run", "task"],
9137        );
9138        assert_eq!(
9139            parsed.as_env().get("usage_output").map(String::as_str),
9140            Some("quiet"),
9141        );
9142
9143        // An unknown flag still stops the scan: it may take a value, so the next word cannot be
9144        // assumed to be a subcommand. `run` takes no positional, so this stays an error.
9145        assert_parse_err(
9146            parse_partial(&spec, &input(&["test", "run", "--nope", "task"])),
9147            "unexpected word: --nope",
9148        );
9149    }
9150
9151    #[test]
9152    fn test_non_mounted_subcommand_offers_inherited_globals() {
9153        // Nothing changes for ordinary (non-mounted) subcommands: a global declared above is
9154        // still both recognized and offered.
9155        let mut run_cmd = SpecCommand::builder().name("run").build();
9156        run_cmd.subcommands.insert(
9157            "nested".to_string(),
9158            SpecCommand::builder().name("nested").build(),
9159        );
9160        let mut cmd = SpecCommand::builder()
9161            .name("test")
9162            .flag(
9163                SpecFlag::builder()
9164                    .name("silent")
9165                    .long("silent")
9166                    .global(true)
9167                    .build(),
9168            )
9169            .build();
9170        cmd.subcommands.insert("run".to_string(), run_cmd);
9171        let spec = Spec {
9172            name: "test".to_string(),
9173            bin: "test".to_string(),
9174            cmd,
9175            ..Default::default()
9176        };
9177
9178        let parsed = parse_partial(&spec, &input(&["test", "run", "nested"])).unwrap();
9179        assert_eq!(
9180            parsed.completion_flags().keys().collect::<Vec<_>>(),
9181            parsed.available_flags.keys().collect::<Vec<_>>(),
9182        );
9183        assert!(parsed.completion_flags().contains_key("--silent"));
9184    }
9185
9186    #[test]
9187    fn test_subcommand_alias_collision_keeps_last_owner() {
9188        // The orphan-alias merge must not disturb how two flags in the SAME subcommand that
9189        // share an alias are resolved. Historically the flattened flag map gave the shared
9190        // alias to the LAST-declared flag (last-writer-wins); that must be preserved.
9191        let run_cmd = SpecCommand::builder()
9192            .name("run")
9193            .flag(
9194                SpecFlag::builder()
9195                    .name("alpha")
9196                    .short('x')
9197                    .long("alpha")
9198                    .global(false)
9199                    .build(),
9200            )
9201            .flag(
9202                SpecFlag::builder()
9203                    .name("beta")
9204                    .short('x')
9205                    .long("beta")
9206                    .global(false)
9207                    .build(),
9208            )
9209            .build();
9210        let mut cmd = SpecCommand::builder().name("test").build();
9211        cmd.subcommands.insert("run".to_string(), run_cmd);
9212        let spec = Spec {
9213            name: "test".to_string(),
9214            bin: "test".to_string(),
9215            cmd,
9216            ..Default::default()
9217        };
9218
9219        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
9220        // `-x` is declared by both flags; the last one (`beta`) keeps it, as before the fix.
9221        assert_eq!(
9222            parsed.available_flags.get("-x").map(|f| f.name.as_str()),
9223            Some("beta"),
9224            "the last-declared flag must keep a shared short alias",
9225        );
9226        // Both distinct long aliases remain recognized and point to their own flag.
9227        assert_eq!(
9228            parsed
9229                .available_flags
9230                .get("--alpha")
9231                .map(|f| f.name.as_str()),
9232            Some("alpha"),
9233        );
9234        assert_eq!(
9235            parsed
9236                .available_flags
9237                .get("--beta")
9238                .map(|f| f.name.as_str()),
9239            Some("beta"),
9240        );
9241    }
9242
9243    #[test]
9244    fn test_default_subcommand_same_name_child() {
9245        // Test that default_subcommand doesn't cause issues when the default subcommand
9246        // has a child with the same name (e.g., "run" has a task named "run").
9247        // This verifies we don't switch multiple times or get stuck in a loop.
9248        let run_task = SpecCommand::builder()
9249            .name("run")
9250            .arg(SpecArg::builder().name("args").build())
9251            .build();
9252        let mut run_cmd = SpecCommand::builder().name("run").build();
9253        run_cmd.subcommands.insert("run".to_string(), run_task);
9254
9255        let mut cmd = SpecCommand::builder().name("test").build();
9256        cmd.subcommands.insert("run".to_string(), run_cmd);
9257
9258        let spec = Spec {
9259            name: "test".to_string(),
9260            bin: "test".to_string(),
9261            cmd,
9262            default_subcommand: Some("run".to_string()),
9263            ..Default::default()
9264        };
9265
9266        // "test run" explicitly matches the "run" subcommand (not via default_subcommand)
9267        let input = vec!["test".to_string(), "run".to_string()];
9268        let parsed = parse(&spec, &input).unwrap();
9269
9270        // Should have two commands: root and "run"
9271        assert_eq!(parsed.cmds.len(), 2);
9272        assert_eq!(parsed.cmds[0].name, "test");
9273        assert_eq!(parsed.cmds[1].name, "run");
9274
9275        // "test run run" should descend into the "run" task (child of "run" subcommand)
9276        let input = vec![
9277            "test".to_string(),
9278            "run".to_string(),
9279            "run".to_string(),
9280            "hello".to_string(),
9281        ];
9282        let parsed = parse(&spec, &input).unwrap();
9283
9284        assert_eq!(parsed.cmds.len(), 3);
9285        assert_eq!(parsed.cmds[0].name, "test");
9286        assert_eq!(parsed.cmds[1].name, "run");
9287        assert_eq!(parsed.cmds[2].name, "run");
9288        assert_eq!(parsed.args.len(), 1);
9289        let value = parsed.args.values().next().unwrap();
9290        assert_eq!(value.to_string(), "hello");
9291
9292        // Key test case: "test other" should switch to default subcommand "run"
9293        // and treat "other" as a positional arg (not try to switch again because
9294        // "run" also has a "run" child).
9295        let mut run_cmd = SpecCommand::builder()
9296            .name("run")
9297            .arg(SpecArg::builder().name("task").build())
9298            .build();
9299        let run_task = SpecCommand::builder().name("run").build();
9300        run_cmd.subcommands.insert("run".to_string(), run_task);
9301
9302        let mut cmd = SpecCommand::builder().name("test").build();
9303        cmd.subcommands.insert("run".to_string(), run_cmd);
9304
9305        let spec = Spec {
9306            name: "test".to_string(),
9307            bin: "test".to_string(),
9308            cmd,
9309            default_subcommand: Some("run".to_string()),
9310            ..Default::default()
9311        };
9312
9313        let input = vec!["test".to_string(), "other".to_string()];
9314        let parsed = parse(&spec, &input).unwrap();
9315
9316        // Should have two commands: root and "run" (the default)
9317        // We should NOT have switched again to the "run" task child
9318        assert_eq!(parsed.cmds.len(), 2);
9319        assert_eq!(parsed.cmds[0].name, "test");
9320        assert_eq!(parsed.cmds[1].name, "run");
9321
9322        // "other" should be parsed as a positional arg
9323        assert_eq!(parsed.args.len(), 1);
9324        let value = parsed.args.values().next().unwrap();
9325        assert_eq!(value.to_string(), "other");
9326    }
9327
9328    #[test]
9329    fn test_restart_token() {
9330        // Test that restart_token resets argument parsing
9331        let run_cmd = SpecCommand::builder()
9332            .name("run")
9333            .arg(SpecArg::builder().name("task").build())
9334            .restart_token(":::".to_string())
9335            .build();
9336        let mut cmd = SpecCommand::builder().name("test").build();
9337        cmd.subcommands.insert("run".to_string(), run_cmd);
9338
9339        let spec = Spec {
9340            name: "test".to_string(),
9341            bin: "test".to_string(),
9342            cmd,
9343            ..Default::default()
9344        };
9345
9346        // "test run task1 ::: task2" - should end up with task2 as the arg
9347        let input = vec![
9348            "test".to_string(),
9349            "run".to_string(),
9350            "task1".to_string(),
9351            ":::".to_string(),
9352            "task2".to_string(),
9353        ];
9354        let parsed = parse(&spec, &input).unwrap();
9355
9356        // After restart, args were cleared and task2 was parsed
9357        assert_eq!(parsed.args.len(), 1);
9358        let value = parsed.args.values().next().unwrap();
9359        assert_eq!(value.to_string(), "task2");
9360    }
9361
9362    #[test]
9363    fn test_restart_token_multiple() {
9364        // Test multiple restart tokens
9365        let run_cmd = SpecCommand::builder()
9366            .name("run")
9367            .arg(SpecArg::builder().name("task").build())
9368            .restart_token(":::".to_string())
9369            .build();
9370        let mut cmd = SpecCommand::builder().name("test").build();
9371        cmd.subcommands.insert("run".to_string(), run_cmd);
9372
9373        let spec = Spec {
9374            name: "test".to_string(),
9375            bin: "test".to_string(),
9376            cmd,
9377            ..Default::default()
9378        };
9379
9380        // "test run task1 ::: task2 ::: task3" - should end up with task3 as the arg
9381        let input = vec![
9382            "test".to_string(),
9383            "run".to_string(),
9384            "task1".to_string(),
9385            ":::".to_string(),
9386            "task2".to_string(),
9387            ":::".to_string(),
9388            "task3".to_string(),
9389        ];
9390        let parsed = parse(&spec, &input).unwrap();
9391
9392        // After multiple restarts, args were cleared and task3 was parsed
9393        assert_eq!(parsed.args.len(), 1);
9394        let value = parsed.args.values().next().unwrap();
9395        assert_eq!(value.to_string(), "task3");
9396    }
9397
9398    #[test]
9399    fn test_restart_token_clears_flag_awaiting_value() {
9400        // Test that restart_token clears pending flag values
9401        let run_cmd = SpecCommand::builder()
9402            .name("run")
9403            .arg(SpecArg::builder().name("task").build())
9404            .flag(
9405                SpecFlag::builder()
9406                    .name("jobs")
9407                    .long("jobs")
9408                    .arg(SpecArg::builder().name("count").build())
9409                    .build(),
9410            )
9411            .restart_token(":::".to_string())
9412            .build();
9413        let mut cmd = SpecCommand::builder().name("test").build();
9414        cmd.subcommands.insert("run".to_string(), run_cmd);
9415
9416        let spec = Spec {
9417            name: "test".to_string(),
9418            bin: "test".to_string(),
9419            cmd,
9420            ..Default::default()
9421        };
9422
9423        // "test run task1 --jobs ::: task2" - task2 should be an arg, not a flag value
9424        let input = vec![
9425            "test".to_string(),
9426            "run".to_string(),
9427            "task1".to_string(),
9428            "--jobs".to_string(),
9429            ":::".to_string(),
9430            "task2".to_string(),
9431        ];
9432        let parsed = parse(&spec, &input).unwrap();
9433
9434        // task2 should be parsed as the task arg, not as --jobs value
9435        assert_eq!(parsed.args.len(), 1);
9436        let value = parsed.args.values().next().unwrap();
9437        assert_eq!(value.to_string(), "task2");
9438        // --jobs should not have a value
9439        assert!(parsed.flag_awaiting_value.is_empty());
9440    }
9441
9442    #[test]
9443    fn test_restart_token_resets_double_dash() {
9444        // Test that restart_token resets the -- separator effect
9445        let run_cmd = SpecCommand::builder()
9446            .name("run")
9447            .arg(SpecArg::builder().name("task").build())
9448            .arg(SpecArg::builder().name("extra_args").var(true).build())
9449            .flag(SpecFlag::builder().name("verbose").long("verbose").build())
9450            .restart_token(":::".to_string())
9451            .build();
9452        let mut cmd = SpecCommand::builder().name("test").build();
9453        cmd.subcommands.insert("run".to_string(), run_cmd);
9454
9455        let spec = Spec {
9456            name: "test".to_string(),
9457            bin: "test".to_string(),
9458            cmd,
9459            ..Default::default()
9460        };
9461
9462        // "test run task1 -- extra ::: --verbose task2" - --verbose should be a flag after :::
9463        let input = vec![
9464            "test".to_string(),
9465            "run".to_string(),
9466            "task1".to_string(),
9467            "--".to_string(),
9468            "extra".to_string(),
9469            ":::".to_string(),
9470            "--verbose".to_string(),
9471            "task2".to_string(),
9472        ];
9473        let parsed = parse(&spec, &input).unwrap();
9474
9475        // --verbose should be parsed as a flag (not an arg) after the restart
9476        assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
9477        // task2 should be the arg after restart
9478        let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
9479        let value = parsed.args.get(task_arg).unwrap();
9480        assert_eq!(value.to_string(), "task2");
9481    }
9482
9483    #[test]
9484    fn test_double_dashes_without_preserve() {
9485        // Only the first `--` is a separator; a later one is a value, because flag
9486        // parsing has already stopped and there is nothing left for it to do.
9487        // `preserve` is about the *first* one — see the test below, where none is
9488        // consumed at all.
9489        let run_cmd = SpecCommand::builder()
9490            .name("run")
9491            .arg(SpecArg::builder().name("args").var(true).build())
9492            .build();
9493        let mut cmd = SpecCommand::builder().name("test").build();
9494        cmd.subcommands.insert("run".to_string(), run_cmd);
9495
9496        let spec = Spec {
9497            name: "test".to_string(),
9498            bin: "test".to_string(),
9499            cmd,
9500            ..Default::default()
9501        };
9502
9503        // "test run arg1 -- arg2 -- arg3": the first separates, the second is a value
9504        let input = vec![
9505            "test".to_string(),
9506            "run".to_string(),
9507            "arg1".to_string(),
9508            "--".to_string(),
9509            "arg2".to_string(),
9510            "--".to_string(),
9511            "arg3".to_string(),
9512        ];
9513        let parsed = parse(&spec, &input).unwrap();
9514
9515        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
9516        let value = parsed.args.get(args_arg).unwrap();
9517        assert_eq!(value.to_string(), "arg1 arg2 -- arg3");
9518    }
9519
9520    #[test]
9521    fn test_double_dashes_with_preserve() {
9522        // Test that variadic args WITH `preserve` keep all double dashes
9523        let run_cmd = SpecCommand::builder()
9524            .name("run")
9525            .arg(
9526                SpecArg::builder()
9527                    .name("args")
9528                    .var(true)
9529                    .double_dash(SpecDoubleDashChoices::Preserve)
9530                    .build(),
9531            )
9532            .build();
9533        let mut cmd = SpecCommand::builder().name("test").build();
9534        cmd.subcommands.insert("run".to_string(), run_cmd);
9535
9536        let spec = Spec {
9537            name: "test".to_string(),
9538            bin: "test".to_string(),
9539            cmd,
9540            ..Default::default()
9541        };
9542
9543        // "test run arg1 -- arg2 -- arg3" - all double dashes should be preserved
9544        let input = vec![
9545            "test".to_string(),
9546            "run".to_string(),
9547            "arg1".to_string(),
9548            "--".to_string(),
9549            "arg2".to_string(),
9550            "--".to_string(),
9551            "arg3".to_string(),
9552        ];
9553        let parsed = parse(&spec, &input).unwrap();
9554
9555        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
9556        let value = parsed.args.get(args_arg).unwrap();
9557        assert_eq!(value.to_string(), "arg1 -- arg2 -- arg3");
9558    }
9559
9560    #[test]
9561    fn test_double_dashes_with_preserve_only_dashes() {
9562        // Test that variadic args WITH `preserve` keep all double dashes even
9563        // if the values are just double dashes
9564        let run_cmd = SpecCommand::builder()
9565            .name("run")
9566            .arg(
9567                SpecArg::builder()
9568                    .name("args")
9569                    .var(true)
9570                    .double_dash(SpecDoubleDashChoices::Preserve)
9571                    .build(),
9572            )
9573            .build();
9574        let mut cmd = SpecCommand::builder().name("test").build();
9575        cmd.subcommands.insert("run".to_string(), run_cmd);
9576
9577        let spec = Spec {
9578            name: "test".to_string(),
9579            bin: "test".to_string(),
9580            cmd,
9581            ..Default::default()
9582        };
9583
9584        // "test run -- --" - all double dashes should be preserved
9585        let input = vec![
9586            "test".to_string(),
9587            "run".to_string(),
9588            "--".to_string(),
9589            "--".to_string(),
9590        ];
9591        let parsed = parse(&spec, &input).unwrap();
9592
9593        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
9594        let value = parsed.args.get(args_arg).unwrap();
9595        assert_eq!(value.to_string(), "-- --");
9596    }
9597
9598    #[test]
9599    fn test_double_dashes_with_preserve_multiple_args() {
9600        // Test with multiple args where only the second has has `preserve`
9601        let run_cmd = SpecCommand::builder()
9602            .name("run")
9603            .arg(SpecArg::builder().name("task").build())
9604            .arg(
9605                SpecArg::builder()
9606                    .name("extra_args")
9607                    .var(true)
9608                    .double_dash(SpecDoubleDashChoices::Preserve)
9609                    .build(),
9610            )
9611            .build();
9612        let mut cmd = SpecCommand::builder().name("test").build();
9613        cmd.subcommands.insert("run".to_string(), run_cmd);
9614
9615        let spec = Spec {
9616            name: "test".to_string(),
9617            bin: "test".to_string(),
9618            cmd,
9619            ..Default::default()
9620        };
9621
9622        // The first arg "task1" is captured normally
9623        // Then extra_args with `preserve` captures everything, including the "--" tokens
9624        let input = vec![
9625            "test".to_string(),
9626            "run".to_string(),
9627            "task1".to_string(),
9628            "--".to_string(),
9629            "arg1".to_string(),
9630            "--".to_string(),
9631            "--foo".to_string(),
9632        ];
9633        let parsed = parse(&spec, &input).unwrap();
9634
9635        let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
9636        let task_value = parsed.args.get(task_arg).unwrap();
9637        assert_eq!(task_value.to_string(), "task1");
9638
9639        let extra_arg = parsed.args.keys().find(|a| a.name == "extra_args").unwrap();
9640        let extra_value = parsed.args.get(extra_arg).unwrap();
9641        assert_eq!(extra_value.to_string(), "-- arg1 -- --foo");
9642    }
9643
9644    fn spec_with_args(args: impl IntoIterator<Item = SpecArg>) -> Spec {
9645        let cmd = SpecCommand::builder().name("test").args(args).build();
9646        Spec {
9647            name: "test".to_string(),
9648            bin: "test".to_string(),
9649            cmd,
9650            ..Default::default()
9651        }
9652    }
9653
9654    fn arg_value(parsed: &ParseOutput, name: &str) -> String {
9655        let arg = parsed
9656            .args
9657            .keys()
9658            .find(|a| a.name == name)
9659            .unwrap_or_else(|| panic!("expected arg {name} to be parsed"));
9660        parsed.args.get(arg).unwrap().to_string()
9661    }
9662
9663    fn required_arg(name: &str) -> SpecArg {
9664        SpecArg::builder()
9665            .name(name)
9666            .var(true)
9667            .required(false)
9668            .double_dash(SpecDoubleDashChoices::Required)
9669            .build()
9670    }
9671
9672    #[test]
9673    fn test_double_dash_required_reports_error_once_for_variadic() {
9674        // A variadic arg is offered every remaining word, but the mistake is one mistake.
9675        let spec = spec_with_args([required_arg("files")]);
9676
9677        let parsed = parse_partial(&spec, &input(&["test", "a", "b", "c"])).unwrap();
9678
9679        assert!(parsed.args.is_empty());
9680        assert_eq!(parsed.errors.len(), 1);
9681        assert!(
9682            matches!(&parsed.errors[0], UsageErr::ArgRequiresDoubleDash(name) if name == "files")
9683        );
9684    }
9685
9686    #[test]
9687    fn test_double_dash_required_suppresses_missing_arg() {
9688        // The arg is never filled, so the end-of-parse check would also call it missing.
9689        let spec = spec_with_args([SpecArg::builder()
9690            .name("file")
9691            .required(true)
9692            .double_dash(SpecDoubleDashChoices::Required)
9693            .build()]);
9694
9695        let parsed = parse_partial(&spec, &input(&["test", "x"])).unwrap();
9696
9697        assert_eq!(parsed.errors.len(), 1);
9698        assert!(matches!(
9699            &parsed.errors[0],
9700            UsageErr::ArgRequiresDoubleDash(_)
9701        ));
9702        // The cursor stays put, so a completion keeps offering the same arg.
9703        assert_eq!(
9704            parsed.next_arg.as_ref().map(|a| a.name.as_str()),
9705            Some("file")
9706        );
9707        assert!(!parsed.double_dash_seen);
9708    }
9709
9710    #[test]
9711    fn test_double_dash_routes_to_required_arg() {
9712        // Everything after `--` belongs to the arg that requires it, even though the greedy
9713        // variadic before it would otherwise swallow the rest (clap's `Arg::last(true)`).
9714        let spec = spec_with_args([
9715            SpecArg::builder()
9716                .name("tool")
9717                .var(true)
9718                .required(false)
9719                .build(),
9720            required_arg("command"),
9721        ]);
9722
9723        let parsed = parse(&spec, &input(&["test", "node@20", "--", "node", "app.js"])).unwrap();
9724
9725        assert_eq!(arg_value(&parsed, "tool"), "node@20");
9726        assert_eq!(arg_value(&parsed, "command"), "node app.js");
9727        assert!(parsed.double_dash_seen);
9728    }
9729
9730    #[test]
9731    fn test_double_dash_routes_with_gap_reports_missing_arg() {
9732        // Jumping the cursor leaves `tool` empty even though `command` is filled, so the
9733        // "is it filled?" check cannot be a count of how many args were filled.
9734        let spec = spec_with_args([
9735            SpecArg::builder()
9736                .name("tool")
9737                .var(true)
9738                .required(true)
9739                .build(),
9740            required_arg("command"),
9741        ]);
9742
9743        let parsed = parse_partial(&spec, &input(&["test", "--", "ls"])).unwrap();
9744
9745        assert_eq!(arg_value(&parsed, "command"), "ls");
9746        assert!(parsed.args.keys().all(|a| a.name != "tool"));
9747        assert!(
9748            parsed
9749                .errors
9750                .iter()
9751                .any(|e| matches!(e, UsageErr::MissingArg(name) if name == "tool"))
9752        );
9753    }
9754
9755    #[test]
9756    fn test_double_dash_gap_applies_defaults() {
9757        // Same gap, seen from `Parser::parse`: the skipped arg still gets its default.
9758        let spec = spec_with_args([
9759            SpecArg::builder()
9760                .name("tool")
9761                .var(true)
9762                .required(false)
9763                .default_value("node@20")
9764                .build(),
9765            required_arg("command"),
9766        ]);
9767
9768        let parsed = parse(&spec, &input(&["test", "--", "ls"])).unwrap();
9769
9770        assert_eq!(arg_value(&parsed, "command"), "ls");
9771        assert_eq!(arg_value(&parsed, "tool"), "node@20");
9772    }
9773
9774    fn spec_with_restart_token_and_required_arg() -> Spec {
9775        let run_cmd = SpecCommand::builder()
9776            .name("run")
9777            .arg(SpecArg::builder().name("task").build())
9778            .arg(required_arg("run_args"))
9779            .restart_token(":::".to_string())
9780            .build();
9781        let mut cmd = SpecCommand::builder().name("test").build();
9782        cmd.subcommands.insert("run".to_string(), run_cmd);
9783        Spec {
9784            name: "test".to_string(),
9785            bin: "test".to_string(),
9786            cmd,
9787            ..Default::default()
9788        }
9789    }
9790
9791    #[test]
9792    fn test_double_dash_required_restart_token_resets_separator() {
9793        // The `--` before `:::` belongs to the previous invocation only.
9794        let spec = spec_with_restart_token_and_required_arg();
9795
9796        let parsed = parse_partial(
9797            &spec,
9798            &input(&["test", "run", "task1", "--", "a", ":::", "task2", "b"]),
9799        )
9800        .unwrap();
9801
9802        assert_eq!(arg_value(&parsed, "task"), "task2");
9803        assert!(parsed.args.keys().all(|a| a.name != "run_args"));
9804        // Reported once even though the arg was violated after already succeeding once.
9805        assert_eq!(
9806            parsed
9807                .errors
9808                .iter()
9809                .filter(|e| matches!(e, UsageErr::ArgRequiresDoubleDash(_)))
9810                .count(),
9811            1
9812        );
9813    }
9814
9815    #[test]
9816    fn test_double_dash_required_restart_token_accepts_new_separator() {
9817        let spec = spec_with_restart_token_and_required_arg();
9818
9819        let parsed = parse(
9820            &spec,
9821            &input(&["test", "run", "task1", "--", "a", ":::", "task2", "--", "c"]),
9822        )
9823        .unwrap();
9824
9825        assert_eq!(arg_value(&parsed, "task"), "task2");
9826        assert_eq!(arg_value(&parsed, "run_args"), "c");
9827    }
9828
9829    #[test]
9830    fn test_double_dash_preserve_is_not_a_separator() {
9831        // A `--` that `preserve` keeps is a *value* of that arg, so it must not unlock the
9832        // arg that requires a separator. Deliberate: one token cannot be both.
9833        let spec = spec_with_args([
9834            SpecArg::builder()
9835                .name("kept")
9836                .var(true)
9837                .var_max(1)
9838                .required(false)
9839                .double_dash(SpecDoubleDashChoices::Preserve)
9840                .build(),
9841            required_arg("rest"),
9842        ]);
9843
9844        let parsed = parse_partial(&spec, &input(&["test", "--", "x"])).unwrap();
9845
9846        assert_eq!(arg_value(&parsed, "kept"), "--");
9847        assert!(parsed.args.keys().all(|a| a.name != "rest"));
9848        assert!(!parsed.double_dash_seen);
9849        assert_eq!(parsed.errors.len(), 1);
9850    }
9851
9852    #[test]
9853    fn test_double_dash_required_does_not_bail_in_parse_partial() {
9854        // Completions parse half-typed command lines; they must still get a result.
9855        let spec = spec_with_args([required_arg("file")]);
9856
9857        assert!(parse_partial(&spec, &input(&["test", "x"])).is_ok());
9858        assert!(parse(&spec, &input(&["test", "x"])).is_err());
9859    }
9860
9861    #[test]
9862    fn test_double_dash_without_required_arg_does_not_move_cursor() {
9863        // Specs with no `double_dash="required"` arg are untouched by the jump.
9864        let spec = spec_with_args([
9865            SpecArg::builder().name("first").required(false).build(),
9866            SpecArg::builder().name("second").required(false).build(),
9867        ]);
9868
9869        let parsed = parse(&spec, &input(&["test", "--", "a", "b"])).unwrap();
9870
9871        assert_eq!(arg_value(&parsed, "first"), "a");
9872        assert_eq!(arg_value(&parsed, "second"), "b");
9873        assert!(parsed.next_arg.is_none());
9874    }
9875
9876    #[test]
9877    fn test_parser_with_custom_env_for_required_arg() {
9878        let spec = spec_with_arg(
9879            SpecArg::builder()
9880                .name("name")
9881                .env("NAME")
9882                .required(true)
9883                .build(),
9884        );
9885        // FIXME: Audit that the environment access only happens in single-threaded code.
9886        unsafe { std::env::remove_var("NAME") };
9887
9888        let parsed = parse_with_env(&spec, &["test"], &[("NAME", "john")])
9889            .expect("parse should succeed with custom env");
9890        assert_eq!(parsed.args.len(), 1);
9891        assert_eq!(first_string_value(&parsed), "john");
9892    }
9893
9894    #[test]
9895    fn test_parser_with_custom_env_for_required_flag() {
9896        let spec = spec_with_flag(
9897            SpecFlag::builder()
9898                .long("name")
9899                .env("NAME")
9900                .required(true)
9901                .arg(SpecArg::builder().name("name").build())
9902                .build(),
9903        );
9904        // FIXME: Audit that the environment access only happens in single-threaded code.
9905        unsafe { std::env::remove_var("NAME") };
9906
9907        let parsed = parse_with_env(&spec, &["test"], &[("NAME", "jane")])
9908            .expect("parse should succeed with custom env");
9909        assert_eq!(parsed.flags.len(), 1);
9910        assert_eq!(first_string_value(&parsed), "jane");
9911    }
9912
9913    #[test]
9914    fn test_flag_environment_fallbacks_preserve_declaration_order() {
9915        let spec = spec_with_flag(
9916            SpecFlag::builder()
9917                .long("name")
9918                .env("NAME")
9919                .env_fallback("OLD_NAME")
9920                .env_fallback("OLDER_NAME")
9921                .deprecated_env("DEPRECATED_NAME")
9922                .arg(SpecArg::builder().name("name").build())
9923                .build(),
9924        );
9925
9926        let parsed = parse_with_env(
9927            &spec,
9928            &["test"],
9929            &[
9930                ("NAME", "canonical"),
9931                ("OLD_NAME", "fallback"),
9932                ("DEPRECATED_NAME", "deprecated"),
9933            ],
9934        )
9935        .unwrap();
9936        assert_eq!(first_string_value(&parsed), "canonical");
9937
9938        let parsed = parse_with_env(
9939            &spec,
9940            &["test"],
9941            &[("OLDER_NAME", "older"), ("OLD_NAME", "old")],
9942        )
9943        .unwrap();
9944        assert_eq!(first_string_value(&parsed), "old");
9945
9946        let parsed =
9947            parse_with_env(&spec, &["test"], &[("DEPRECATED_NAME", "deprecated")]).unwrap();
9948        assert_eq!(first_string_value(&parsed), "deprecated");
9949    }
9950
9951    #[test]
9952    fn a_value_from_a_deprecated_alias_says_which_name_to_use() {
9953        let spec = spec_with_flag(
9954            SpecFlag::builder()
9955                .long("name")
9956                .env("NAME")
9957                .deprecated_env("DEPRECATED_NAME")
9958                .arg(SpecArg::builder().name("name").build())
9959                .build(),
9960        );
9961
9962        // The current name is not a deprecated one, and says nothing.
9963        let parsed = parse_with_env(&spec, &["test"], &[("NAME", "canonical")]).unwrap();
9964        assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
9965
9966        let parsed =
9967            parse_with_env(&spec, &["test"], &[("DEPRECATED_NAME", "deprecated")]).unwrap();
9968        assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
9969        assert_eq!(
9970            parsed.warnings[0].kind,
9971            crate::warn::WarningKind::DeprecatedEnv
9972        );
9973        assert_eq!(parsed.warnings[0].name, "DEPRECATED_NAME");
9974        assert_eq!(parsed.warnings[0].replacement.as_deref(), Some("NAME"));
9975        // Reported, not printed, and the value still arrives.
9976        assert_eq!(first_string_value(&parsed), "deprecated");
9977    }
9978
9979    #[test]
9980    fn a_deprecated_flag_reports_only_when_it_was_used() {
9981        let spec = spec_with_flag(
9982            SpecFlag::builder()
9983                .long("output")
9984                .deprecated("use --out")
9985                .deprecated_remove_at("3.0.0")
9986                .arg(SpecArg::builder().name("output").build())
9987                .build(),
9988        );
9989
9990        let parsed = parse_with_env(&spec, &["test"], &[]).unwrap();
9991        assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
9992
9993        let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
9994        assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
9995        assert_eq!(
9996            parsed.warnings[0].kind,
9997            crate::warn::WarningKind::DeprecatedFlag
9998        );
9999        // Named the way it was typed, dashes and all.
10000        assert_eq!(parsed.warnings[0].name, "--output");
10001        assert_eq!(parsed.warnings[0].remove_at.as_deref(), Some("3.0.0"));
10002        assert_eq!(
10003            parsed.warnings[0].render(),
10004            "warning: --output is deprecated, removed at 3.0.0: use --out\n",
10005        );
10006    }
10007
10008    #[test]
10009    fn a_milestone_the_spec_has_not_reached_stays_quiet() {
10010        let flag = SpecFlag::builder()
10011            .long("output")
10012            .deprecated("use --out")
10013            .deprecated_warn_at("9.0.0")
10014            .arg(SpecArg::builder().name("output").build())
10015            .build();
10016        let mut spec = spec_with_flag(flag);
10017        spec.version = Some("2.0.0".to_string());
10018
10019        let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
10020        assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
10021
10022        // And once the CLI is the release that was named, it speaks up.
10023        spec.version = Some("9.0.0".to_string());
10024        let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
10025        assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
10026    }
10027
10028    #[test]
10029    fn test_parser_with_custom_env_still_fails_when_missing() {
10030        let spec = spec_with_arg(
10031            SpecArg::builder()
10032                .name("name")
10033                .env("NAME")
10034                .required(true)
10035                .build(),
10036        );
10037        // FIXME: Audit that the environment access only happens in single-threaded code.
10038        unsafe { std::env::remove_var("NAME") };
10039        assert!(parse_with_env(&spec, &["test"], &[]).is_err());
10040    }
10041
10042    #[test]
10043    fn test_parser_does_not_treat_env_choice_value_as_help() {
10044        let spec = spec_with_arg(
10045            SpecArg::builder()
10046                .name("env")
10047                .env("CURRENT_ENV")
10048                .choices(["dev", "staging"])
10049                .required(false)
10050                .build(),
10051        );
10052
10053        assert_parse_err(
10054            parse_with_env(&spec, &["test"], &[("CURRENT_ENV", "--help")]),
10055            "Invalid choice for arg env: --help, expected one of dev, staging",
10056        );
10057    }
10058
10059    #[test]
10060    fn test_parser_does_not_treat_default_choice_value_as_help() {
10061        let spec = spec_with_flag(
10062            SpecFlag::builder()
10063                .long("env")
10064                .arg(
10065                    SpecArg::builder()
10066                        .name("env")
10067                        .choices(["dev", "staging"])
10068                        .build(),
10069                )
10070                .default_value("--help")
10071                .build(),
10072        );
10073
10074        assert_parse_err(
10075            parse_with_env(&spec, &["test"], &[]),
10076            "Invalid choice for option env: --help, expected one of dev, staging",
10077        );
10078    }
10079
10080    /// argv as `parse` wants it, program name included.
10081    fn words(of: &[&str]) -> Vec<String> {
10082        of.iter().map(|s| s.to_string()).collect()
10083    }
10084
10085    #[test]
10086    fn a_command_that_needs_a_subcommand_says_so() {
10087        // The spec has carried `subcommand_required` since the derive needed it, and this parser
10088        // never read it — so `mise generate`, which declares it, parsed as a complete
10089        // invocation while usage-argv and clap both refused. Found by the differential fuzzer.
10090        let spec: Spec = r#"
10091name "ex"
10092bin "ex"
10093cmd "gen" subcommand_required=#true {
10094    cmd "two" {}
10095    cmd "one" {}
10096    cmd "secret" hide=#true {}
10097    alias "g"
10098}
10099cmd "open" {
10100    cmd "sub" {}
10101}
10102"#
10103        .parse()
10104        .unwrap();
10105
10106        let err = parse(&spec, &words(&["ex", "gen"])).unwrap_err();
10107        // Sorted, so the message does not depend on map order; hidden commands left out,
10108        // because a message telling someone to type a hidden name is worse than a vague one;
10109        // and the alias not listed beside the name it points at.
10110        assert_eq!(err.to_string(), "`gen` needs a subcommand: one of one, two");
10111
10112        // Reached through its alias, and still about the command rather than the spelling.
10113        let err = parse(&spec, &words(&["ex", "g"])).unwrap_err();
10114        assert!(err.to_string().starts_with("`gen` needs a subcommand"));
10115
10116        // Given one: fine.
10117        parse(&spec, &words(&["ex", "gen", "one"])).unwrap();
10118
10119        // And a command that has subcommands without declaring them required is untouched —
10120        // this is the half that keeps the check from being "any command with children".
10121        parse(&spec, &words(&["ex", "open"])).unwrap();
10122        parse(&spec, &words(&["ex", "open", "sub"])).unwrap();
10123    }
10124
10125    #[test]
10126    fn arg_required_else_help_observes_the_selected_commands_argv() {
10127        let spec: Spec = r#"
10128name "ex"
10129bin "ex"
10130flag "--verbose" global=#true
10131cmd "run" arg_required_else_help=#true {
10132    flag "--all"
10133}
10134"#
10135        .parse()
10136        .unwrap();
10137        let words = |items: &[&str]| items.iter().map(|s| (*s).to_string()).collect::<Vec<_>>();
10138
10139        let err = parse(&spec, &words(&["ex", "run"])).unwrap_err();
10140        assert!(err.to_string().contains("Usage: ex run"), "{err}");
10141
10142        // A global before the command belongs to the ancestor. It selected `run`, but did not
10143        // give `run` an argument of its own.
10144        let err = parse(&spec, &words(&["ex", "--verbose", "run"])).unwrap_err();
10145        assert!(err.to_string().contains("Usage: ex run"), "{err}");
10146
10147        parse(&spec, &words(&["ex", "run", "--all"])).expect("run received an argv token");
10148    }
10149
10150    #[test]
10151    fn empty_default_with_parent_flags_observes_child_argv() {
10152        let spec: Spec = r#"
10153name "ex"
10154bin "ex"
10155default_subcommand "run"
10156default_subcommand_on_empty #true
10157flag "--verbose" global=#true
10158cmd "run" arg_required_else_help=#true {}
10159"#
10160        .parse()
10161        .unwrap();
10162        let words = |items: &[&str]| items.iter().map(|s| (*s).to_string()).collect::<Vec<_>>();
10163        let err = parse(&spec, &words(&["ex", "--verbose"])).unwrap_err();
10164        assert!(err.to_string().contains("Usage: ex run"), "{err}");
10165    }
10166
10167    #[test]
10168    fn an_unmatched_word_is_forwarded_when_external_subcommand_is_set() {
10169        let spec: Spec = r#"
10170name "ex"
10171bin "ex"
10172unknown_flags "error"
10173external_subcommand #true
10174cmd "install"
10175flag "-v --verbose" global=#true
10176"#
10177        .parse()
10178        .unwrap();
10179
10180        let parsed = parse(&spec, &input(&["ex", "foo", "--help", "bar"])).unwrap();
10181        assert_eq!(
10182            parsed.external,
10183            Some(vec!["foo".into(), "--help".into(), "bar".into()])
10184        );
10185        assert!(parsed.flags.is_empty());
10186
10187        // Known subcommands still win.
10188        let parsed = parse(&spec, &input(&["ex", "install"])).unwrap();
10189        assert_eq!(parsed.cmd.name, "install");
10190        assert!(parsed.external.is_none());
10191
10192        // A global flag before the unmatched word still binds on the parent.
10193        let parsed = parse(&spec, &input(&["ex", "-v", "foo", "--verbose"])).unwrap();
10194        assert_eq!(
10195            parsed.external,
10196            Some(vec!["foo".into(), "--verbose".into()])
10197        );
10198        assert!(parsed.flags.keys().any(|flag| flag.name == "verbose"));
10199
10200        // An unknown flag on the parent is still an error, which is what clap does.
10201        assert!(parse(&spec, &input(&["ex", "--wat"])).is_err());
10202
10203        // A negative number is a value, not a flag, so it can be the unmatched word.
10204        // usage-argv already forwarded `-1`; Phase 1 used to treat every `starts_with('-')`
10205        // token as a flag and never reach the catch-all.
10206        let parsed = parse(&spec, &input(&["ex", "-1", "rest"])).unwrap();
10207        assert_eq!(parsed.external, Some(vec!["-1".into(), "rest".into()]));
10208    }
10209
10210    #[test]
10211    fn an_external_subcommand_satisfies_subcommand_required() {
10212        let mut spec: Spec = r#"
10213name "ex"
10214bin "ex"
10215external_subcommand #true
10216cmd "install"
10217"#
10218        .parse()
10219        .unwrap();
10220        spec.cmd.subcommand_required = true;
10221
10222        parse(&spec, &input(&["ex", "foo", "--help"])).unwrap();
10223        assert!(parse(&spec, &input(&["ex"])).is_err());
10224    }
10225
10226    #[test]
10227    fn a_default_subcommand_outranks_an_external_one() {
10228        let spec: Spec = r#"
10229name "ex"
10230bin "ex"
10231default_subcommand "run"
10232external_subcommand #true
10233cmd "run" {
10234    arg "[task]"
10235}
10236"#
10237        .parse()
10238        .unwrap();
10239
10240        let parsed = parse(&spec, &input(&["ex", "build"])).unwrap();
10241        assert_eq!(parsed.cmd.name, "run");
10242        assert!(parsed.external.is_none());
10243        assert_eq!(first_string_value(&parsed), "build");
10244    }
10245
10246    #[test]
10247    fn multicall_basename_strips_a_path_and_exe() {
10248        assert_eq!(multicall_basename("/usr/bin/ls"), "ls");
10249        assert_eq!(multicall_basename(r"C:\busybox\ls.exe"), "ls");
10250        assert_eq!(multicall_basename("LS.EXE"), "LS");
10251        assert_eq!(multicall_basename("busybox"), "busybox");
10252    }
10253
10254    #[test]
10255    fn a_multicall_applet_is_the_first_word() {
10256        let spec: Spec = r#"
10257name "busybox"
10258bin "busybox"
10259multicall #true
10260cmd "ls" {
10261    arg "[ARGS]" var=#true
10262}
10263cmd "cat"
10264"#
10265        .parse()
10266        .unwrap();
10267
10268        // A symlink: argv[0] is the applet.
10269        let parsed = parse(&spec, &input(&["/usr/bin/ls", "-l"])).unwrap();
10270        assert_eq!(parsed.cmd.name, "ls");
10271        match parsed.args.values().next() {
10272            Some(ParseValue::MultiString(values)) => assert_eq!(values, &["-l".to_string()]),
10273            other => panic!("expected ARGS to collect -l, got {other:?}"),
10274        }
10275
10276        // A dispatcher invocation still skips argv[0].
10277        let parsed = parse(&spec, &input(&["/usr/bin/busybox", "ls", "-l"])).unwrap();
10278        assert_eq!(parsed.cmd.name, "ls");
10279
10280        // Configured dispatcher values receive the same path and extension normalization.
10281        let mut configured = spec.clone();
10282        configured.name = "BusyBox".to_string();
10283        configured.bin = "/opt/bin/busybox.exe".to_string();
10284        let parsed = parse(&configured, &input(&["/usr/bin/busybox.exe", "ls", "-l"])).unwrap();
10285        assert_eq!(parsed.cmd.name, "ls");
10286
10287        // `.exe` is stripped so Windows and Unix agree.
10288        let parsed = parse(&spec, &input(&["ls.exe"])).unwrap();
10289        assert_eq!(parsed.cmd.name, "ls");
10290
10291        // Without the property, argv[0] is discarded as usual.
10292        let mut plain = spec.clone();
10293        plain.multicall = false;
10294        let parsed = parse(&plain, &input(&["/usr/bin/ls", "ls"])).unwrap();
10295        assert_eq!(parsed.cmd.name, "ls");
10296    }
10297
10298    #[test]
10299    fn a_multicall_unknown_applet_can_be_external() {
10300        let spec: Spec = r#"
10301name "busybox"
10302bin "busybox"
10303multicall #true
10304unknown_flags "error"
10305external_subcommand #true
10306cmd "ls"
10307"#
10308        .parse()
10309        .unwrap();
10310
10311        let parsed = parse(&spec, &input(&["/usr/bin/git", "--help"])).unwrap();
10312        assert_eq!(parsed.external, Some(vec!["git".into(), "--help".into()]));
10313
10314        let mut closed = spec.clone();
10315        closed.cmd.external_subcommand = false;
10316        assert!(parse(&closed, &input(&["wat"])).is_err());
10317    }
10318
10319    #[cfg(feature = "unstable_choices_env")]
10320    #[test]
10321    fn test_parser_arg_choices_from_custom_env() {
10322        let spec = spec_arg_choices_env("DEPLOY_ENVS");
10323
10324        let parsed =
10325            parse_with_env(&spec, &["test", "bar"], &[("DEPLOY_ENVS", "foo,bar baz")]).unwrap();
10326        assert_eq!(first_string_value(&parsed), "bar");
10327
10328        assert_parse_err(
10329            parse_with_env(&spec, &["test", "prod"], &[("DEPLOY_ENVS", "foo,bar baz")]),
10330            "Invalid choice for arg env: prod, expected one of foo, bar, baz",
10331        );
10332        assert_parse_err(
10333            parse_with_env(&spec, &["test", "prod"], &[]),
10334            "Invalid choice for arg env: prod, no choices resolved from env DEPLOY_ENVS",
10335        );
10336    }
10337
10338    #[cfg(feature = "unstable_choices_env")]
10339    #[test]
10340    fn test_parser_validates_flag_choices_from_custom_env() {
10341        let spec = spec_flag_choices_env("DEPLOY_ENVS");
10342        let parsed = parse_with_env(
10343            &spec,
10344            &["test", "--env", "baz"],
10345            &[("DEPLOY_ENVS", "foo,bar baz")],
10346        )
10347        .unwrap();
10348        assert_eq!(first_string_value(&parsed), "baz");
10349    }
10350
10351    #[cfg(feature = "unstable_choices_env")]
10352    #[test]
10353    fn test_parser_revalidates_env_and_default_values_against_choices_env() {
10354        let arg_env_spec = spec_with_arg(
10355            SpecArg::builder()
10356                .name("env")
10357                .env("CURRENT_ENV")
10358                .choices_env("DEPLOY_ENVS")
10359                .build(),
10360        );
10361        assert_parse_err(
10362            parse_with_env(
10363                &arg_env_spec,
10364                &["test"],
10365                &[("CURRENT_ENV", "prod"), ("DEPLOY_ENVS", "dev,staging")],
10366            ),
10367            "Invalid choice for arg env: prod, expected one of dev, staging",
10368        );
10369
10370        let flag_default_spec = spec_with_flag(
10371            SpecFlag::builder()
10372                .long("env")
10373                .arg(
10374                    SpecArg::builder()
10375                        .name("env")
10376                        .choices_env("DEPLOY_ENVS")
10377                        .build(),
10378                )
10379                .default_value("prod")
10380                .build(),
10381        );
10382        assert_parse_err(
10383            parse_with_env(
10384                &flag_default_spec,
10385                &["test"],
10386                &[("DEPLOY_ENVS", "dev,staging")],
10387            ),
10388            "Invalid choice for option env: prod, expected one of dev, staging",
10389        );
10390    }
10391
10392    #[test]
10393    fn test_variadic_arg_captures_unknown_flags_from_spec_string() {
10394        let spec: Spec = r#"
10395            flag "-v --verbose" var=#true
10396            arg "[database]" default="myapp_dev"
10397            arg "[args...]"
10398        "#
10399        .parse()
10400        .unwrap();
10401        let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
10402            .into_iter()
10403            .map(String::from)
10404            .collect();
10405        let parsed = parse(&spec, &input).unwrap();
10406        let env = parsed.as_env();
10407        assert_eq!(env.get("usage_database").unwrap(), "mydb");
10408        assert_eq!(env.get("usage_args").unwrap(), "--host localhost");
10409    }
10410
10411    #[test]
10412    fn test_variadic_arg_captures_unknown_flags() {
10413        let cmd = SpecCommand::builder()
10414            .name("test")
10415            .flag(SpecFlag::builder().short('v').long("verbose").build())
10416            .arg(SpecArg::builder().name("database").required(false).build())
10417            .arg(
10418                SpecArg::builder()
10419                    .name("args")
10420                    .required(false)
10421                    .var(true)
10422                    .build(),
10423            )
10424            .build();
10425        let spec = Spec {
10426            name: "test".to_string(),
10427            bin: "test".to_string(),
10428            cmd,
10429            ..Default::default()
10430        };
10431
10432        // Unknown --host flag and its value should be captured by [args...]
10433        let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
10434            .into_iter()
10435            .map(String::from)
10436            .collect();
10437        let parsed = parse(&spec, &input).unwrap();
10438        assert_eq!(parsed.args.len(), 2);
10439        let args_val = parsed
10440            .args
10441            .iter()
10442            .find(|(a, _)| a.name == "args")
10443            .unwrap()
10444            .1;
10445        match args_val {
10446            ParseValue::MultiString(v) => {
10447                assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
10448            }
10449            _ => panic!("Expected MultiString, got {:?}", args_val),
10450        }
10451    }
10452
10453    #[test]
10454    fn test_variadic_arg_captures_unknown_flags_with_double_dash() {
10455        let cmd = SpecCommand::builder()
10456            .name("test")
10457            .flag(SpecFlag::builder().short('v').long("verbose").build())
10458            .arg(SpecArg::builder().name("database").required(false).build())
10459            .arg(
10460                SpecArg::builder()
10461                    .name("args")
10462                    .required(false)
10463                    .var(true)
10464                    .build(),
10465            )
10466            .build();
10467        let spec = Spec {
10468            name: "test".to_string(),
10469            bin: "test".to_string(),
10470            cmd,
10471            ..Default::default()
10472        };
10473
10474        // With explicit -- separator
10475        let input: Vec<String> = vec!["test", "--", "mydb", "--host", "localhost"]
10476            .into_iter()
10477            .map(String::from)
10478            .collect();
10479        let parsed = parse(&spec, &input).unwrap();
10480        assert_eq!(parsed.args.len(), 2);
10481        let args_val = parsed
10482            .args
10483            .iter()
10484            .find(|(a, _)| a.name == "args")
10485            .unwrap()
10486            .1;
10487        match args_val {
10488            ParseValue::MultiString(v) => {
10489                assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
10490            }
10491            _ => panic!("Expected MultiString, got {:?}", args_val),
10492        }
10493    }
10494
10495    #[test]
10496    fn test_variadic_arg_unknown_flag_equals_value_not_split() {
10497        // Regression: --flag=value should be treated as a single positional token when
10498        // --flag is not a known spec flag, not split into "--flag=value" AND "value".
10499        let spec: Spec = r#"arg "[other_args]" var=#true"#.parse().unwrap();
10500
10501        // Single unknown --flag=value: must not produce a stray "3" positional.
10502        // as_env() shell-joins values, so "=" gets quoted.
10503        let input: Vec<String> = vec!["test", "--option=3"]
10504            .into_iter()
10505            .map(String::from)
10506            .collect();
10507        let parsed = parse(&spec, &input).unwrap();
10508        let env = parsed.as_env();
10509        assert_eq!(
10510            env.get("usage_other_args").map(String::as_str),
10511            Some("'--option=3'"),
10512            "expected a single --option=3 token, got {:?}",
10513            env.get("usage_other_args"),
10514        );
10515
10516        // Multiple unknown --flag=value args should each be kept intact
10517        let input2: Vec<String> = vec!["test", "--foo=bar", "--baz=qux"]
10518            .into_iter()
10519            .map(String::from)
10520            .collect();
10521        let parsed2 = parse(&spec, &input2).unwrap();
10522        let env2 = parsed2.as_env();
10523        assert_eq!(
10524            env2.get("usage_other_args").map(String::as_str),
10525            Some("'--foo=bar' '--baz=qux'"),
10526            "expected two intact tokens, got {:?}",
10527            env2.get("usage_other_args"),
10528        );
10529
10530        // Mix of plain positional args and unknown --flag=value tokens
10531        let input3: Vec<String> = vec!["test", "positional1", "--option=3", "positional2"]
10532            .into_iter()
10533            .map(String::from)
10534            .collect();
10535        let parsed3 = parse(&spec, &input3).unwrap();
10536        let env3 = parsed3.as_env();
10537        assert_eq!(
10538            env3.get("usage_other_args").map(String::as_str),
10539            Some("positional1 '--option=3' positional2"),
10540            "expected positional args and intact flag token, got {:?}",
10541            env3.get("usage_other_args"),
10542        );
10543    }
10544
10545    #[test]
10546    fn test_allow_hyphen_values_consumes_short_flag_collision() {
10547        let spec = r#"
10548flag "-d --working-dir <DIR>"
10549flag "-a --args <ARGS>" allow_hyphen_values=#true
10550"#
10551        .parse::<Spec>()
10552        .unwrap();
10553
10554        let parsed = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap();
10555
10556        assert_eq!(parsed.flags.len(), 1);
10557        assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
10558    }
10559
10560    #[test]
10561    fn test_allow_hyphen_values_consumes_embedded_long_value() {
10562        let spec = r#"
10563flag "-d --working-dir <DIR>"
10564flag "-a --args <ARGS>" allow_hyphen_values=#true
10565"#
10566        .parse::<Spec>()
10567        .unwrap();
10568
10569        let parsed = parse(&spec, &input(&["test", "--args=-destroy"])).unwrap();
10570
10571        assert_eq!(parsed.flags.len(), 1);
10572        assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
10573    }
10574
10575    #[test]
10576    fn test_allow_hyphen_values_takes_the_separator_as_its_value() {
10577        // The flag is declared to accept a token that looks like a flag, and `--` looks
10578        // like one, so it binds — which is what clap does with the same declaration.
10579        // Letting the separator arm run first consumed it and left the flag hungry, and
10580        // the flag then ate the word past it: `-a -- -x` bound `-x` with the `--` gone.
10581        let spec = r#"
10582flag "-a --args <ARGS>" allow_hyphen_values=#true
10583arg "[rest]..."
10584"#
10585        .parse::<Spec>()
10586        .unwrap();
10587
10588        let parsed = parse(&spec, &input(&["test", "-a", "--", "-x"])).unwrap();
10589
10590        assert_eq!(flag_string_value(&parsed, "args"), "--");
10591        let rest = parsed
10592            .args
10593            .values()
10594            .next()
10595            .expect("expected the word after the separator to reach the argument");
10596        assert_eq!(rest.to_string(), "-x");
10597    }
10598
10599    #[test]
10600    fn test_variadic_allow_hyphen_values_collects_after_a_hyphenated_first_value() {
10601        // Which token supplied the first value says nothing about how many the argument
10602        // takes, so collection carries on from a hyphenated one exactly as from a plain
10603        // one. It still stops at the next flag-like token, which is what keeps a second
10604        // occurrence of the flag from being eaten as a value.
10605        let spec = r#"
10606flag "-a --args <ARGS>..." allow_hyphen_values=#true
10607"#
10608        .parse::<Spec>()
10609        .unwrap();
10610
10611        let parsed = parse(&spec, &input(&["test", "-a", "-x", "b", "c"])).unwrap();
10612
10613        let flag = parsed
10614            .flags
10615            .keys()
10616            .find(|flag| flag.name == "args")
10617            .expect("expected args flag");
10618        match parsed.flags.get(flag).expect("expected args value") {
10619            ParseValue::MultiString(values) => assert_eq!(values, &["-x", "b", "c"]),
10620            other => panic!("expected a list of values, got {other:?}"),
10621        }
10622    }
10623
10624    #[test]
10625    fn test_variadic_allow_hyphen_values_consumes_repeated_flag_values() {
10626        let spec = r#"
10627flag "-a --args <ARGS>" var=#true allow_hyphen_values=#true
10628"#
10629        .parse::<Spec>()
10630        .unwrap();
10631
10632        let parsed = parse(&spec, &input(&["test", "-a", "-val1", "-a", "-val2"])).unwrap();
10633
10634        let flag = parsed
10635            .flags
10636            .keys()
10637            .find(|flag| flag.name == "args")
10638            .expect("expected args flag");
10639        let value = parsed.flags.get(flag).expect("expected args value");
10640        match value {
10641            ParseValue::MultiString(values) => {
10642                assert_eq!(values, &vec!["-val1".to_string(), "-val2".to_string()]);
10643            }
10644            _ => panic!("expected MultiString, got {value:?}"),
10645        }
10646    }
10647
10648    #[test]
10649    fn test_require_equals_accepts_attached_and_refuses_detached() {
10650        let spec = r#"
10651flag "--inspect <PORT>" require_equals=#true
10652"#
10653        .parse::<Spec>()
10654        .unwrap();
10655
10656        let parsed = parse(&spec, &input(&["test", "--inspect=9229"])).unwrap();
10657        assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
10658
10659        let err = parse(&spec, &input(&["test", "--inspect", "9229"])).unwrap_err();
10660        let msg = format!("{err}");
10661        assert!(
10662            msg.contains("requires an argument") || msg.contains("inspect"),
10663            "detached value must be refused: {msg}"
10664        );
10665    }
10666
10667    #[test]
10668    fn boolean_flags_can_accept_attached_values_when_enabled() {
10669        let spec: Spec = r#"
10670name "ex"
10671bin "ex"
10672flag "--color" negate="--no-color" bool_value=#true
10673arg "[rest]"
10674"#
10675        .parse()
10676        .unwrap();
10677
10678        for (token, expected) in [
10679            ("--color", true),
10680            ("--color=true", true),
10681            ("--color=false", false),
10682            ("--no-color", false),
10683            ("--no-color=false", true),
10684        ] {
10685            let parsed = parse(&spec, &input(&["ex", token])).unwrap();
10686            assert!(
10687                matches!(
10688                    parsed.flags.get(&spec.cmd.flags[0]),
10689                    Some(ParseValue::Bool(value)) if *value == expected
10690                ),
10691                "{token}"
10692            );
10693        }
10694
10695        let parsed = parse(&spec, &input(&["ex", "--color=false", "word"])).unwrap();
10696        assert!(matches!(
10697            parsed.args.get(&spec.cmd.args[0]),
10698            Some(ParseValue::String(value)) if value == "word"
10699        ));
10700        let err = parse(&spec, &input(&["ex", "--color=maybe"])).unwrap_err();
10701        assert!(err.to_string().contains("expected `true` or `false`"));
10702
10703        let strict: Spec = r#"
10704name "ex"
10705bin "ex"
10706args_override_self #false
10707flag "--color" negate="--no-color" bool_value=#true
10708"#
10709        .parse()
10710        .unwrap();
10711        assert!(parse(&strict, &input(&["ex", "--color=false", "--color=true"])).is_err());
10712        let parsed = parse(
10713            &strict,
10714            &input(&["ex", "--color=false", "--no-color=false"]),
10715        )
10716        .unwrap();
10717        assert!(matches!(
10718            parsed.flags.get(&strict.cmd.flags[0]),
10719            Some(ParseValue::Bool(true))
10720        ));
10721    }
10722
10723    #[test]
10724    fn test_require_equals_refuses_a_detached_value_after_a_short_bundle() {
10725        let spec = r#"
10726flag "-a --all"
10727flag "-i --inspect <PORT>" require_equals=#true
10728"#
10729        .parse::<Spec>()
10730        .unwrap();
10731
10732        let err = parse(&spec, &input(&["test", "-ai", "9229"])).unwrap_err();
10733        let msg = format!("{err}");
10734        assert!(
10735            msg.contains("requires an argument") || msg.contains("inspect"),
10736            "bundled short must refuse the following word: {msg}"
10737        );
10738    }
10739
10740    #[test]
10741    fn test_default_missing_binds_when_the_value_is_left_off() {
10742        let spec = r#"
10743flag "-c --color <WHEN>" default_missing="always"
10744flag "-v --verbose"
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 parsed = parse(&spec, &input(&["test", "--color", "never"])).unwrap();
10756        assert_eq!(flag_string_value(&parsed, "color"), "never");
10757
10758        let parsed = parse(&spec, &input(&["test", "--color", "--verbose"])).unwrap();
10759        assert_eq!(flag_string_value(&parsed, "color"), "always");
10760        assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
10761
10762        let parsed = parse(&spec, &input(&["test", "--color="])).unwrap();
10763        assert_eq!(flag_string_value(&parsed, "color"), "");
10764
10765        let parsed = parse(&spec, &input(&["test", "-cnever"])).unwrap();
10766        assert_eq!(flag_string_value(&parsed, "color"), "never");
10767
10768        let parsed = parse(&spec, &input(&["test", "-c", "-v"])).unwrap();
10769        assert_eq!(flag_string_value(&parsed, "color"), "always");
10770        assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
10771    }
10772
10773    #[test]
10774    fn test_default_missing_requires_opt_in_for_detached_negative_flag_values() {
10775        let spec = r#"
10776flag "--apps <N>"
10777flag "--jobs <N>" default_missing="default missing"
10778flag "--kids <N>" default_missing="default missing" allow_negative_numbers=#true
10779"#
10780        .parse::<Spec>()
10781        .unwrap();
10782
10783        let parsed = parse(&spec, &input(&["test", "--apps", "-1"])).unwrap();
10784        assert_eq!(flag_string_value(&parsed, "apps"), "-1");
10785
10786        let err = parse(&spec, &input(&["test", "--jobs", "-1"])).unwrap_err();
10787        assert!(
10788            err.to_string().contains("unexpected word: -1"),
10789            "default_missing must keep an unopted negative value separate: {err}"
10790        );
10791
10792        let parsed = parse(&spec, &input(&["test", "--kids", "-1"])).unwrap();
10793        assert_eq!(flag_string_value(&parsed, "kids"), "-1");
10794
10795        let external_spec = r#"
10796external_subcommand #true
10797flag "--apps <N>"
10798"#
10799        .parse::<Spec>()
10800        .unwrap();
10801        let parsed = parse(&external_spec, &input(&["test", "--apps", "-1"])).unwrap();
10802        assert_eq!(flag_string_value(&parsed, "apps"), "-1");
10803        assert!(parsed.external.is_none());
10804
10805        for words in [
10806            &["test", "--apps", "--jobs", "1"][..],
10807            &["test", "--apps", "--kids", "-1"][..],
10808        ] {
10809            let err = parse(&spec, &input(words)).unwrap_err();
10810            let message = err.to_string();
10811            assert!(
10812                message.contains("--apps") && message.contains("requires an argument"),
10813                "the earlier flag must report its missing value for {words:?}: {message}"
10814            );
10815        }
10816    }
10817
10818    #[test]
10819    fn test_optional_flag_value_preserves_bare_and_explicit_empty_forms() {
10820        let spec = r#"
10821flag "--bump [LEVEL]" value_optional=#true
10822flag "--verbose"
10823arg "[FILE]"
10824"#
10825        .parse::<Spec>()
10826        .unwrap();
10827
10828        let absent = parse(&spec, &input(&["test"])).unwrap();
10829        assert!(!absent.flags.keys().any(|flag| flag.name == "bump"));
10830
10831        let bare = parse(&spec, &input(&["test", "--bump", "--verbose", "file.txt"])).unwrap();
10832        let bump = bare
10833            .flags
10834            .iter()
10835            .find(|(flag, _)| flag.name == "bump")
10836            .map(|(_, value)| value)
10837            .unwrap();
10838        assert!(matches!(bump, ParseValue::MultiString(values) if values.is_empty()));
10839        assert!(bare.flags.keys().any(|flag| flag.name == "verbose"));
10840        assert_eq!(arg_value(&bare, "FILE"), "file.txt");
10841
10842        let explicit = parse(&spec, &input(&["test", "--bump=", "file.txt"])).unwrap();
10843        assert_eq!(flag_string_value(&explicit, "bump"), "");
10844
10845        let corrected = parse(
10846            &spec,
10847            &input(&["test", "--bump=2", "--bump", "--verbose", "file.txt"]),
10848        )
10849        .unwrap();
10850        let bump = corrected
10851            .flags
10852            .iter()
10853            .find(|(flag, _)| flag.name == "bump")
10854            .map(|(_, value)| value)
10855            .unwrap();
10856        assert!(matches!(bump, ParseValue::MultiString(values) if values.is_empty()));
10857
10858        let collecting = r#"
10859flag "--tag [TAG]..." value_optional=#true
10860flag "--verbose"
10861"#
10862        .parse::<Spec>()
10863        .unwrap();
10864        let valued = parse(
10865            &collecting,
10866            &input(&["test", "--tag", "one", "two", "--verbose"]),
10867        )
10868        .unwrap();
10869        let tag = valued
10870            .flags
10871            .iter()
10872            .find(|(flag, _)| flag.name == "tag")
10873            .map(|(_, value)| value)
10874            .unwrap();
10875        assert!(matches!(tag, ParseValue::MultiString(values) if values == &["one", "two"]));
10876    }
10877
10878    #[test]
10879    fn test_repeatable_bare_optional_values_count_each_occurrence() {
10880        let spec = r#"
10881flag "--tag [TAG]" var=#true var_min=2 var_max=2 value_optional=#true
10882"#
10883        .parse::<Spec>()
10884        .unwrap();
10885
10886        let parsed = parse(&spec, &input(&["test", "--tag", "--tag"])).unwrap();
10887        let tag = parsed
10888            .flags
10889            .iter()
10890            .find(|(flag, _)| flag.name == "tag")
10891            .map(|(_, value)| value)
10892            .unwrap();
10893        assert!(matches!(tag, ParseValue::MultiString(values) if values == &["", ""]));
10894
10895        assert!(parse(&spec, &input(&["test", "--tag"])).is_err());
10896        assert!(parse(&spec, &input(&["test", "--tag", "--tag", "--tag"])).is_err());
10897    }
10898
10899    #[test]
10900    fn test_repeatable_variadic_optional_values_do_not_gain_bare_occurrences() {
10901        let spec = r#"
10902flag "--tag [TAG]..." var=#true value_optional=#true
10903flag "--verbose"
10904"#
10905        .parse::<Spec>()
10906        .unwrap();
10907
10908        for argv in [
10909            &["test", "--tag", "one", "two"][..],
10910            &["test", "--tag", "one", "two", "--verbose"][..],
10911            &["test", "--tag", "one", "--tag", "two"][..],
10912        ] {
10913            let parsed = parse(&spec, &input(argv)).unwrap();
10914            let tag = parsed
10915                .flags
10916                .iter()
10917                .find(|(flag, _)| flag.name == "tag")
10918                .map(|(_, value)| value)
10919                .unwrap();
10920            assert!(
10921                matches!(tag, ParseValue::MultiString(values) if values == &["one", "two"]),
10922                "argv={argv:?}: {tag:?}"
10923            );
10924        }
10925
10926        let bare = parse(&spec, &input(&["test", "--tag", "--verbose"])).unwrap();
10927        let tag = bare
10928            .flags
10929            .iter()
10930            .find(|(flag, _)| flag.name == "tag")
10931            .map(|(_, value)| value)
10932            .unwrap();
10933        assert!(matches!(tag, ParseValue::MultiString(values) if values == &[""]));
10934    }
10935
10936    #[test]
10937    fn test_default_missing_with_require_equals_refuses_the_following_word() {
10938        let spec = r#"
10939flag "--inspect <PORT>" require_equals=#true default_missing="9229"
10940arg "[rest]"
10941"#
10942        .parse::<Spec>()
10943        .unwrap();
10944
10945        let parsed = parse(&spec, &input(&["test", "--inspect"])).unwrap();
10946        assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
10947
10948        let parsed = parse(&spec, &input(&["test", "--inspect=1234"])).unwrap();
10949        assert_eq!(flag_string_value(&parsed, "inspect"), "1234");
10950
10951        // The following word is not the value; the missing value is, and 80 is a positional.
10952        let parsed = parse(&spec, &input(&["test", "--inspect", "80"])).unwrap();
10953        assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
10954        assert_eq!(
10955            parsed
10956                .args
10957                .values()
10958                .next()
10959                .map(|v| v.to_string())
10960                .as_deref(),
10961            Some("80")
10962        );
10963
10964        let parsed = parse(&spec, &input(&["test", "--inspect="])).unwrap();
10965        assert_eq!(flag_string_value(&parsed, "inspect"), "");
10966    }
10967
10968    #[test]
10969    fn test_default_missing_must_be_a_choice() {
10970        let spec = r#"
10971flag "--color <WHEN>" default_missing="always" {
10972    choices "auto" "always" "never"
10973}
10974"#
10975        .parse::<Spec>()
10976        .unwrap();
10977
10978        let parsed = parse(&spec, &input(&["test", "--color"])).unwrap();
10979        assert_eq!(flag_string_value(&parsed, "color"), "always");
10980
10981        let parsed = parse(&spec, &input(&["test", "--color=never"])).unwrap();
10982        assert_eq!(flag_string_value(&parsed, "color"), "never");
10983
10984        let spec = r#"
10985flag "--color <WHEN>" default_missing="wat" {
10986    choices "auto" "always" "never"
10987}
10988"#
10989        .parse::<Spec>()
10990        .unwrap();
10991
10992        let err = parse(&spec, &input(&["test", "--color"])).unwrap_err();
10993        let msg = format!("{err}");
10994        assert!(
10995            msg.contains("Invalid choice for option color: wat"),
10996            "missing default has to pass choices the same way a typed value does: {msg}"
10997        );
10998
10999        let err = parse(&spec, &input(&["test", "--color=wat"])).unwrap_err();
11000        let msg = format!("{err}");
11001        assert!(
11002            msg.contains("Invalid choice for option color: wat"),
11003            "an attached value that is not a choice is still refused: {msg}"
11004        );
11005
11006        let spec = r#"
11007flag "--inspect <PORT>" require_equals=#true default_missing="wat" {
11008    choices "9229" "80"
11009}
11010arg "[rest]"
11011"#
11012        .parse::<Spec>()
11013        .unwrap();
11014
11015        let err = parse(&spec, &input(&["test", "--inspect", "80"])).unwrap_err();
11016        let msg = format!("{err}");
11017        assert!(
11018            msg.contains("Invalid choice for option inspect: wat"),
11019            "require_equals still binds the missing string, so the error is the choice: {msg}"
11020        );
11021    }
11022
11023    #[test]
11024    fn test_hyphen_values_still_start_short_flag_parsing() {
11025        let spec = r#"
11026flag "-d --working-dir <DIR>"
11027flag "-a --args <ARGS>"
11028"#
11029        .parse::<Spec>()
11030        .unwrap();
11031
11032        let err = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap_err();
11033        let message = err.to_string();
11034        assert!(
11035            message.contains("--args") && message.contains("requires an argument"),
11036            "the recognized -d must leave the earlier -a missing: {message}"
11037        );
11038    }
11039
11040    /// `available_flags` has to agree with what an actual parse accepts, since
11041    /// its whole reason to exist is answering that question without one.
11042    mod available_flags {
11043        use super::*;
11044
11045        fn spec() -> Spec {
11046            r#"
11047bin "test"
11048flag "-v --verbose" global=#true
11049flag "--raw" global=#true effect="write"
11050flag "--local-only"
11051cmd "run" {
11052    flag "-r --raw"
11053    flag "-w --watch"
11054    cmd "once"
11055}
11056"#
11057            .parse::<Spec>()
11058            .unwrap()
11059        }
11060
11061        fn chain<'a>(spec: &'a Spec, path: &[&str]) -> Vec<&'a SpecCommand> {
11062            let mut chain = vec![&spec.cmd];
11063            for segment in path {
11064                chain.push(chain.last().unwrap().find_subcommand(segment).unwrap());
11065            }
11066            chain
11067        }
11068
11069        fn names(spec: &Spec, path: &[&str]) -> Vec<String> {
11070            let mut names: Vec<_> = available_flags(&chain(spec, path))
11071                .iter()
11072                .map(|f| f.name.clone())
11073                .collect();
11074            names.sort();
11075            names
11076        }
11077
11078        #[test]
11079        fn an_empty_chain_yields_nothing() {
11080            assert!(available_flags(&[]).is_empty());
11081        }
11082
11083        #[test]
11084        fn the_root_gets_its_own_flags() {
11085            let spec = spec();
11086            assert_eq!(names(&spec, &[]), ["local-only", "raw", "verbose"]);
11087        }
11088
11089        #[test]
11090        fn a_subcommand_keeps_globals_and_drops_local_only_ancestors() {
11091            let spec = spec();
11092            assert_eq!(names(&spec, &["run"]), ["raw", "verbose", "watch"]);
11093        }
11094
11095        #[test]
11096        fn a_re_declared_global_is_listed_once() {
11097            // The merge can leave the long key on the merged flag and the short
11098            // key on the pre-merge one. Same flag; it must not be listed twice.
11099            let spec = r#"
11100bin "test"
11101flag "-y --yes" global=#true effect="write"
11102cmd "rm" {
11103    flag "-y --yes"
11104}
11105"#
11106            .parse::<Spec>()
11107            .unwrap();
11108            let flags = available_flags(&chain(&spec, &["rm"]));
11109            assert_eq!(flags.len(), 1, "{flags:?}");
11110            assert_eq!(flags[0].effect.map(|e| e.as_str()), Some("write"));
11111        }
11112
11113        #[test]
11114        fn a_re_declared_global_keeps_the_globals_declaration() {
11115            // `run` re-declares the long-only global `--raw` as `-r --raw`
11116            // without `global`. That is the same flag: the global's `effect`
11117            // survives, the orphan short is unioned in, and it stays global.
11118            let spec = spec();
11119            let flags = available_flags(&chain(&spec, &["run"]));
11120            let raw = flags.iter().find(|f| f.name == "raw").unwrap();
11121            assert!(raw.global);
11122            assert_eq!(raw.effect.map(|e| e.as_str()), Some("write"));
11123            assert_eq!(raw.short, ['r']);
11124        }
11125
11126        #[test]
11127        fn it_matches_what_a_parse_accepts() {
11128            // The invariant. If these ever disagree, one of them is lying to a
11129            // caller about which flags a command takes.
11130            let spec = spec();
11131            for path in [vec![], vec!["run"], vec!["run", "once"]] {
11132                let argv = std::iter::once("test".to_string())
11133                    .chain(path.iter().map(|s| s.to_string()))
11134                    .collect::<Vec<_>>();
11135                let parsed = parse_partial(&spec, &argv).unwrap();
11136
11137                let mut from_parse: Vec<_> = unique_flags(parsed.available_flags.values())
11138                    .map(|f| f.name.clone())
11139                    .collect();
11140                from_parse.sort();
11141                assert_eq!(names(&spec, &path), from_parse, "path {path:?}");
11142            }
11143        }
11144    }
11145
11146    // Provenance: which token bound what, and where a value came from when no token did.
11147
11148    /// Every role a token was given, rendered the way `Debug` renders it, so a test can
11149    /// assert on the whole picture rather than on one field at a time.
11150    fn roles(parsed: &ParseOutput, index: usize) -> Vec<String> {
11151        parsed
11152            .tokens
11153            .iter()
11154            .find(|token| token.index == index)
11155            .unwrap_or_else(|| panic!("no token at {index}"))
11156            .roles
11157            .iter()
11158            .map(render_role)
11159            .collect()
11160    }
11161
11162    fn origins(parsed: &ParseOutput, flag: &str) -> Vec<ValueOrigin> {
11163        parsed
11164            .flag_origins
11165            .iter()
11166            .find(|(f, _)| f.name == flag)
11167            .map(|(_, origins)| origins.clone())
11168            .unwrap_or_default()
11169    }
11170
11171    fn explain_with_env(spec: &Spec, words: &[&str], env: &[(&str, &str)]) -> ParseOutput {
11172        let env = env
11173            .iter()
11174            .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
11175            .collect();
11176        Parser::new(spec)
11177            .with_env(env)
11178            .explain(&input(words))
11179            .unwrap()
11180    }
11181
11182    fn explain(spec: &Spec, words: &[&str]) -> ParseOutput {
11183        explain_with_env(spec, words, &[])
11184    }
11185
11186    #[test]
11187    fn an_attached_long_value_is_recorded_on_the_flag_token() {
11188        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\n"
11189            .parse()
11190            .unwrap();
11191
11192        let parsed = explain(&spec, &["ex", "--env=prod"]);
11193
11194        assert_eq!(roles(&parsed, 0), ["program"]);
11195        assert_eq!(
11196            roles(&parsed, 1),
11197            ["flag env as --env", "value of env = [\"prod\"], attached"]
11198        );
11199        // This is jdx/mise discussion #8883: a hand-written scanner dropped the attached
11200        // form while the detached one worked, and nothing could show the difference.
11201        assert!(origins(&parsed, "env").is_empty(), "typed, so no fallback");
11202    }
11203
11204    #[test]
11205    fn a_detached_long_value_is_recorded_on_its_own_token() {
11206        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\n"
11207            .parse()
11208            .unwrap();
11209
11210        let parsed = explain(&spec, &["ex", "--env", "prod"]);
11211
11212        assert_eq!(roles(&parsed, 1), ["flag env as --env"]);
11213        assert_eq!(roles(&parsed, 2), ["value of env = [\"prod\"]"]);
11214    }
11215
11216    #[test]
11217    fn a_short_bundle_is_attributed_to_the_bundle_token() {
11218        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-a\"\nflag \"-b\"\nflag \"-j <n>\"\n"
11219            .parse()
11220            .unwrap();
11221
11222        let parsed = explain(&spec, &["ex", "-abj8"]);
11223
11224        // One word the caller wrote, four things it did — and the re-queued tails are
11225        // folded back onto it rather than appearing as tokens nobody typed.
11226        assert_eq!(
11227            roles(&parsed, 1),
11228            [
11229                "flag a as -a",
11230                "flag b as -b",
11231                "flag j as -j",
11232                "value of j = [\"8\"], attached",
11233            ]
11234        );
11235        assert_eq!(parsed.tokens.len(), 2);
11236    }
11237
11238    #[test]
11239    fn a_delimiter_splits_one_token_into_several_values() {
11240        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--tags <tags>...\" delimiter=\",\"\n"
11241            .parse()
11242            .unwrap();
11243
11244        let parsed = explain(&spec, &["ex", "--tags", "a,b,c"]);
11245
11246        assert_eq!(
11247            roles(&parsed, 2),
11248            ["value of tags = [\"a\", \"b\", \"c\"]"],
11249            "the values meant, not the word typed"
11250        );
11251    }
11252
11253    #[test]
11254    fn a_separator_and_the_words_after_it_are_distinguished() {
11255        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"<src>\"\narg \"[raw]...\"\n"
11256            .parse()
11257            .unwrap();
11258
11259        let parsed = explain(&spec, &["ex", "a", "--", "-x"]);
11260
11261        assert_eq!(roles(&parsed, 1), ["arg src = [\"a\"]"]);
11262        assert_eq!(roles(&parsed, 2), ["separator"]);
11263        // Past the separator `-x` is data, not an unknown flag.
11264        assert_eq!(roles(&parsed, 3), ["arg raw = [\"-x\"]"]);
11265    }
11266
11267    #[test]
11268    fn a_second_separator_is_data() {
11269        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[raw]...\"\n"
11270            .parse()
11271            .unwrap();
11272
11273        let parsed = explain(&spec, &["ex", "--", "a", "--", "b"]);
11274
11275        assert_eq!(roles(&parsed, 1), ["separator"]);
11276        assert_eq!(roles(&parsed, 3), ["arg raw = [\"--\"]"]);
11277    }
11278
11279    #[test]
11280    fn an_unknown_flag_says_what_took_it() {
11281        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[rest]...\"\n"
11282            .parse()
11283            .unwrap();
11284
11285        let parsed = explain(&spec, &["ex", "--wat"]);
11286
11287        // The default is lax, so the word became data. Which is the useful thing to be
11288        // told: the alternative reading is "you have a typo".
11289        assert_eq!(roles(&parsed, 1), ["unknown flag, bound as rest"]);
11290    }
11291
11292    #[test]
11293    fn a_subcommand_word_is_not_a_positional() {
11294        let spec: Spec = "name \"ex\"\nbin \"ex\"\ncmd \"build\" {\n    arg \"<target>\"\n}\n"
11295            .parse()
11296            .unwrap();
11297
11298        let parsed = explain(&spec, &["ex", "build", "a"]);
11299
11300        assert_eq!(roles(&parsed, 1), ["subcommand build"]);
11301        assert_eq!(roles(&parsed, 2), ["arg target = [\"a\"]"]);
11302    }
11303
11304    #[test]
11305    fn a_multicall_applet_is_read_at_argv0() {
11306        let spec: Spec =
11307            "name \"box\"\nbin \"box\"\nmulticall #true\ncmd \"ls\" {\n    flag \"-l\"\n}\n"
11308                .parse()
11309                .unwrap();
11310
11311        let parsed = explain(&spec, &["/usr/bin/ls", "-l"]);
11312
11313        // argv[0] is both the program and the word that selected the applet, and the word
11314        // read there is not the word the caller wrote.
11315        assert_eq!(roles(&parsed, 0), ["program", "subcommand ls"]);
11316        assert!(parsed.tokens[0].synthesized);
11317        assert_eq!(parsed.tokens[0].word, "/usr/bin/ls");
11318    }
11319
11320    #[test]
11321    fn words_the_parse_never_reached_say_so() {
11322        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[rest]...\"\n"
11323            .parse()
11324            .unwrap();
11325
11326        let parsed = Parser::new(&spec)
11327            .explain(&input(&["ex", "--help", "a"]))
11328            .unwrap();
11329
11330        assert_eq!(roles(&parsed, 2), ["unread"]);
11331    }
11332
11333    #[test]
11334    fn an_env_origin_names_the_variable_that_fired() {
11335        let spec: Spec =
11336            "name \"ex\"\nbin \"ex\"\nflag \"--token <t>\" env=\"EX_TOKEN\" env_fallback=\"EX_TOKEN_OLD\"\n"
11337                .parse()
11338                .unwrap();
11339
11340        let primary = explain_with_env(&spec, &["ex"], &[("EX_TOKEN", "a")]);
11341        assert_eq!(
11342            origins(&primary, "token"),
11343            [ValueOrigin::Env("EX_TOKEN".to_string())]
11344        );
11345
11346        // The fallback firing is a different fact from the primary firing, and which one it
11347        // was is what says which declaration to delete.
11348        let fallback = explain_with_env(&spec, &["ex"], &[("EX_TOKEN_OLD", "b")]);
11349        assert_eq!(
11350            origins(&fallback, "token"),
11351            [ValueOrigin::Env("EX_TOKEN_OLD".to_string())]
11352        );
11353    }
11354
11355    #[test]
11356    fn a_default_origin_is_recorded_for_flags_and_args() {
11357        let spec: Spec =
11358            "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" default=\"auto\"\narg \"[src]\" default=\".\"\n"
11359                .parse()
11360                .unwrap();
11361
11362        let parsed = explain(&spec, &["ex"]);
11363
11364        assert_eq!(origins(&parsed, "color"), [ValueOrigin::Default]);
11365        let (arg, origins) = parsed.arg_origins.iter().next().unwrap();
11366        assert_eq!(arg.name, "src");
11367        assert_eq!(origins, &[ValueOrigin::Default]);
11368    }
11369
11370    #[test]
11371    fn a_default_if_origin_carries_the_condition_that_fired() {
11372        let spec: Spec = r#"
11373name "ex"
11374bin "ex"
11375flag "--profile <p>"
11376flag "--strict" {
11377    default_if "--profile" "prod" "true"
11378}
11379        "#
11380        .parse()
11381        .unwrap();
11382
11383        let parsed = explain(&spec, &["ex", "--profile", "prod"]);
11384
11385        // The selector alone is ambiguous: several conditions may name it with different
11386        // `when` values, so the report has to say which one matched.
11387        assert_eq!(
11388            origins(&parsed, "strict"),
11389            [ValueOrigin::DefaultIf {
11390                selector: "--profile".to_string(),
11391                when: Some("prod".to_string()),
11392            }]
11393        );
11394    }
11395
11396    #[test]
11397    fn a_bare_optional_value_flag_records_default_missing() {
11398        let spec: Spec =
11399            "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" default_missing=\"always\"\nflag \"-v\"\n"
11400                .parse()
11401                .unwrap();
11402
11403        let parsed = explain(&spec, &["ex", "--color", "-v"]);
11404
11405        // The flag was typed and the value was not, which is the distinction a spec author
11406        // is asking about when they ask why `--color` came out `always`.
11407        assert_eq!(roles(&parsed, 1), ["flag color as --color"]);
11408        assert_eq!(origins(&parsed, "color"), [ValueOrigin::DefaultMissing]);
11409        assert_eq!(roles(&parsed, 2), ["flag v as -v"]);
11410    }
11411
11412    #[test]
11413    fn a_var_flag_can_take_one_value_from_argv_and_one_from_default_missing() {
11414        let spec: Spec =
11415            "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" var=#true default_missing=\"always\"\n"
11416                .parse()
11417                .unwrap();
11418
11419        let parsed = explain(&spec, &["ex", "--color=red", "--color"]);
11420
11421        // Why origins are a list: one declaration, two occurrences, two different answers.
11422        assert_eq!(
11423            roles(&parsed, 1),
11424            [
11425                "flag color as --color",
11426                "value of color = [\"red\"], attached"
11427            ]
11428        );
11429        assert_eq!(origins(&parsed, "color"), [ValueOrigin::DefaultMissing]);
11430    }
11431
11432    #[test]
11433    fn an_override_names_the_flag_that_did_it() {
11434        let spec: Spec =
11435            "name \"ex\"\nbin \"ex\"\nflag \"--quiet\" default=\"true\"\nflag \"--loud\" overrides=\"--quiet\"\n"
11436                .parse()
11437                .unwrap();
11438
11439        let parsed = explain(&spec, &["ex", "--loud"]);
11440
11441        // Without the overriding name, "`--quiet` is unset despite its default" has no
11442        // answer: the fallback phase silently declines to fill an overridden flag.
11443        assert_eq!(parsed.overridden_flags.get("quiet").unwrap(), "loud");
11444        assert!(origins(&parsed, "quiet").is_empty());
11445    }
11446
11447    #[test]
11448    fn a_restart_token_leaves_the_tokens_and_clears_the_arg_origins() {
11449        let spec: Spec = r#"
11450name "ex"
11451bin "ex"
11452cmd "run" restart_token=":::" {
11453    arg "<task>" default="build"
11454}
11455        "#
11456        .parse()
11457        .unwrap();
11458
11459        let parsed = explain(&spec, &["ex", "run", "lint", ":::", "test"]);
11460
11461        // The values belong to the last invocation, so provenance must too — but the words
11462        // of the first were still read, and a report that dropped them would show a command
11463        // line with a hole in it.
11464        assert_eq!(roles(&parsed, 2), ["arg task = [\"lint\"]"]);
11465        // And the token that did the resetting says so: without a role of its own it reads
11466        // as a word that did nothing, next to a `lint` that filled an arg now empty.
11467        assert_eq!(roles(&parsed, 3), ["restart"]);
11468        assert_eq!(roles(&parsed, 4), ["arg task = [\"test\"]"]);
11469        assert!(parsed.arg_origins.is_empty());
11470    }
11471
11472    #[test]
11473    fn a_value_terminator_says_which_run_it_ended() {
11474        let spec: Spec = r#"
11475name "ex"
11476bin "ex"
11477flag "--exec <cmd>..." value_terminator=";"
11478arg "<src>"
11479        "#
11480        .parse()
11481        .unwrap();
11482
11483        let parsed = explain(&spec, &["ex", "--exec", "rm", "tmp", ";", "a"]);
11484
11485        assert_eq!(roles(&parsed, 3), ["value of exec = [\"tmp\"]"]);
11486        // The terminator is consumed and is not one of the values, which is the whole reason
11487        // it was declared — so it needs a row saying that rather than an empty one.
11488        assert_eq!(roles(&parsed, 4), ["value terminator, ends exec"]);
11489        assert_eq!(roles(&parsed, 5), ["arg src = [\"a\"]"]);
11490    }
11491
11492    #[test]
11493    fn an_args_value_terminator_says_which_run_it_ended() {
11494        let spec: Spec = r#"
11495name "ex"
11496bin "ex"
11497arg "<files>..." value_terminator=";"
11498arg "[dest]"
11499        "#
11500        .parse()
11501        .unwrap();
11502
11503        let parsed = explain(&spec, &["ex", "a", "b", ";", "out"]);
11504
11505        assert_eq!(roles(&parsed, 2), ["arg files = [\"b\"]"]);
11506        assert_eq!(roles(&parsed, 3), ["value terminator, ends files"]);
11507        assert_eq!(roles(&parsed, 4), ["arg dest = [\"out\"]"]);
11508    }
11509
11510    #[test]
11511    fn explain_keeps_the_bindings_of_a_command_line_that_fails() {
11512        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\narg \"<src>\"\n"
11513            .parse()
11514            .unwrap();
11515
11516        let parsed = Parser::new(&spec)
11517            .explain(&input(&["ex", "--env=prod"]))
11518            .unwrap();
11519
11520        // `parse` reports "missing required <src>" and nothing else, which is the report the
11521        // caller already had. This is the case the whole thing exists for.
11522        assert!(
11523            Parser::new(&spec)
11524                .parse(&input(&["ex", "--env=prod"]))
11525                .is_err()
11526        );
11527        assert_eq!(
11528            roles(&parsed, 1),
11529            ["flag env as --env", "value of env = [\"prod\"], attached"]
11530        );
11531        assert!(
11532            parsed.errors.iter().any(|e| e.to_string().contains("src")),
11533            "{:?}",
11534            parsed.errors
11535        );
11536    }
11537
11538    #[test]
11539    fn an_external_subcommand_forwards_whole_tokens() {
11540        let spec: Spec = "name \"ex\"\nbin \"ex\"\nexternal_subcommand #true\ncmd \"build\"\n"
11541            .parse()
11542            .unwrap();
11543
11544        let parsed = explain(&spec, &["ex", "deploy", "--now"]);
11545
11546        assert_eq!(roles(&parsed, 1), ["external"]);
11547        assert_eq!(roles(&parsed, 2), ["external"]);
11548    }
11549
11550    #[test]
11551    fn a_view_keeps_the_callers_argv_positions() {
11552        let spec: Spec = r#"
11553bin "ex"
11554view "runner" root="run"
11555cmd "run" {
11556    flag "--token <token>"
11557}
11558        "#
11559        .parse()
11560        .unwrap();
11561
11562        let parsed = explain(&spec, &["runner", "--token", "secret"]);
11563
11564        // A view re-enters the parse with the same argv, so the positions still mean what
11565        // the caller wrote.
11566        assert_eq!(roles(&parsed, 0), ["program"]);
11567        assert_eq!(roles(&parsed, 2), ["value of token = [\"secret\"]"]);
11568    }
11569
11570    #[test]
11571    fn empty_default_does_not_hide_a_missing_parent_flag_value() {
11572        let spec: Spec = r#"
11573name "ex"
11574bin "ex"
11575default_subcommand "run"
11576default_subcommand_on_empty #true
11577flag "--value <value>"
11578cmd "run" arg_required_else_help=#true
11579"#
11580        .parse()
11581        .unwrap();
11582        let err = Parser::new(&spec)
11583            .parse(&input(&["ex", "--value"]))
11584            .unwrap_err();
11585        assert!(err.to_string().contains("requires an argument"), "{err}");
11586        assert!(!err.to_string().contains("Usage: ex run"), "{err}");
11587    }
11588
11589    #[test]
11590    fn empty_default_honors_parent_args_conflict_policy() {
11591        let spec: Spec = r#"
11592name "ex"
11593bin "ex"
11594default_subcommand "run"
11595default_subcommand_on_empty #true
11596args_conflicts_with_subcommands #true
11597flag "--verbose"
11598cmd "run"
11599"#
11600        .parse()
11601        .unwrap();
11602        let err = Parser::new(&spec)
11603            .parse(&input(&["ex", "--verbose"]))
11604            .unwrap_err();
11605        assert!(
11606            err.to_string().contains("cannot be used with arguments"),
11607            "{err}"
11608        );
11609    }
11610
11611    #[test]
11612    fn empty_default_mount_matches_explicit_selection() {
11613        let spec: Spec = r#"
11614name "ex"
11615bin "ex"
11616default_subcommand "run"
11617default_subcommand_on_empty #true
11618cmd "run" { mount run="discover" }
11619"#
11620        .parse()
11621        .unwrap();
11622        let mut mounts = HashMap::new();
11623        mounts.insert(
11624            "discover".to_string(),
11625            "name \"run\"\nbin \"run\"\nflag \"--mounted <mounted>\" default=\"yes\"\n".to_string(),
11626        );
11627        for argv in [
11628            vec!["ex".to_string()],
11629            vec!["ex".to_string(), "run".to_string()],
11630        ] {
11631            let out = Parser::new(&spec)
11632                .with_mount_outputs(mounts.clone())
11633                .parse(&argv)
11634                .unwrap();
11635            assert_eq!(out.cmds.last().unwrap().name, "run");
11636            assert_eq!(out.flags.values().next().unwrap().to_string(), "yes");
11637        }
11638    }
11639
11640    #[test]
11641    fn pending_parent_value_stays_pending_in_partial_parse() {
11642        let spec: Spec = r#"
11643name "ex"
11644bin "ex"
11645default_subcommand "run"
11646default_subcommand_on_empty #true
11647flag "--value <value>"
11648cmd "run" {}
11649"#
11650        .parse()
11651        .unwrap();
11652        let out = parse_partial(&spec, &input(&["ex", "--value"])).unwrap();
11653        assert_eq!(out.cmd.name, "ex");
11654        assert_eq!(out.flag_awaiting_value[0].name, "value");
11655    }
11656
11657    #[test]
11658    fn optional_default_missing_still_selects_empty_child() {
11659        let spec: Spec = r#"
11660name "ex"
11661bin "ex"
11662default_subcommand "run"
11663default_subcommand_on_empty #true
11664flag "--value <value>" default_missing="fallback"
11665cmd "run" {}
11666"#
11667        .parse()
11668        .unwrap();
11669        let out = Parser::new(&spec)
11670            .parse(&input(&["ex", "--value"]))
11671            .unwrap();
11672        assert_eq!(out.cmd.name, "run");
11673        assert_eq!(out.flags.values().next().unwrap().to_string(), "fallback");
11674    }
11675
11676    #[test]
11677    fn empty_default_resets_cursor_after_root_sigil() {
11678        let spec: Spec = r#"
11679name "ex"
11680bin "ex"
11681default_subcommand "run"
11682default_subcommand_on_empty #true
11683arg "[tool]..." sigil="@"
11684cmd "run" { arg "[path]" }
11685"#
11686        .parse()
11687        .unwrap();
11688        let out = parse_partial(&spec, &input(&["ex"])).unwrap();
11689        assert_eq!(out.cmd.name, "run");
11690        assert_eq!(out.next_arg.as_ref().unwrap().name, "path");
11691    }
11692
11693    #[test]
11694    fn root_clause_positional_prevents_empty_default() {
11695        let spec: Spec = r#"
11696name "ex"
11697bin "ex"
11698default_subcommand "run"
11699default_subcommand_on_empty #true
11700clause "tasks" {
11701  arg "<number>" allow_negative_numbers=#true
11702}
11703cmd "run" { arg "[task]" }
11704"#
11705        .parse()
11706        .unwrap();
11707        let out = Parser::new(&spec).parse(&input(&["ex", "-7"])).unwrap();
11708        assert!(
11709            out.cmds.iter().all(|cmd| cmd.name != "run"),
11710            "cmds={:?} args={:?}",
11711            out.cmds.iter().map(|c| &c.name).collect::<Vec<_>>(),
11712            out.args
11713        );
11714        let number = spec.cmd.clause.as_ref().unwrap().args.first().unwrap();
11715        assert_eq!(
11716            out.clauses["tasks"][0].get(number).unwrap().to_string(),
11717            "-7"
11718        );
11719    }
11720}