Skip to main content

usage/
parse.rs

1use crate::miette::{self, bail};
2use indexmap::IndexMap;
3use itertools::Itertools;
4use log::trace;
5use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
6use std::fmt::{Debug, Display, Formatter};
7use std::sync::Arc;
8
9#[cfg(feature = "cli-help")]
10use crate::docs;
11use crate::error::UsageErr;
12use crate::spec::arg::SpecDoubleDashChoices;
13use crate::spec::unknown_flags::UnknownFlags;
14use crate::warn::Warning;
15use crate::{Spec, SpecArg, SpecChoices, SpecCommand, SpecFlag};
16
17/// Merge a subcommand's flags into the currently available flags when descending
18/// into that subcommand.
19///
20/// On descent we drop the parent's non-global flags (they are scoped to the parent)
21/// but keep its global flags so they remain recognized further down. A subcommand may
22/// re-declare a flag that the parent exposed as global (e.g. `-C/--cd`) but mark its own
23/// copy as non-global. In that case we must NOT let the non-global re-declaration shadow
24/// the inherited global flag, otherwise the next descent's `retain(global)` would drop it
25/// entirely and later parsing would treat the already-consumed global token as an
26/// unexpected positional/flag value.
27///
28/// Descending into a *mounted* subcommand (`crossing_mount`) is different: the mounted
29/// command describes another program, which does not accept the mounting CLI's globals.
30/// Those globals stay recognized (they may appear before the mounted command, and Phase 2
31/// re-parses them), but the mounted command's own flags take precedence over them, so its
32/// choices/completions are not replaced by a global's. Which flags a completion may offer
33/// there is a separate question, answered by [`ParseOutput::completion_flags`].
34fn merge_subcommand_flags(
35    available: &mut BTreeMap<String, Arc<SpecFlag>>,
36    new_flags: BTreeMap<String, Arc<SpecFlag>>,
37    crossing_mount: bool,
38) {
39    // Keep only inherited global flags from the parent.
40    available.retain(|_, f| f.global);
41
42    if crossing_mount {
43        // A mounted command owns its flags outright, including names an inherited global also
44        // uses: a word after the mounted command belongs to the mounted program. Words before
45        // it keep resolving to the global they were read as, via `Token::binding`. Aliases the
46        // mounted command does not declare (e.g. a global's short) stay inherited.
47        for (key, flag) in new_flags {
48            available.insert(key, flag);
49        }
50        return;
51    }
52
53    // Cache the merged (global ∪ orphan-alias) flag per re-declared child so every alias key of
54    // that flag ends up sharing one `Arc`. Keyed by the child `Arc`'s identity.
55    let mut merged_cache: HashMap<usize, Arc<SpecFlag>> = HashMap::new();
56    // Maps each merged flag produced below back to the inherited global it was merged from, so
57    // the collision check can compare *origins*: a flag this loop already merged is not a
58    // different global, even though it is a different `Arc`.
59    let mut merged_origin: HashMap<usize, usize> = HashMap::new();
60    // The inherited global a flag stands for: itself, or — for a merged flag — its source global.
61    fn origin_of(merged_origin: &HashMap<usize, usize>, flag: &Arc<SpecFlag>) -> usize {
62        let ptr = Arc::as_ptr(flag) as usize;
63        *merged_origin.get(&ptr).unwrap_or(&ptr)
64    }
65
66    // Iterate the *flattened* child map directly (one entry per alias key). This preserves the
67    // map's existing intra-subcommand collision resolution: when two flags in the same command
68    // share an alias (e.g. `-x --alpha` then `-x --beta`), the BTreeMap already collapsed `-x`
69    // to its last-declared owner, and we must not change which flag owns it.
70    for (key, flag) in new_flags {
71        if flag.global {
72            // A child that re-declares (or adds) a global flag stays recognized everywhere.
73            available.insert(key, flag);
74            continue;
75        }
76
77        // A non-global re-declaration that shares a LONG name with an inherited global flag is
78        // the SAME logical flag (e.g. mise's `-r --raw` re-declaring the long-only `--raw`
79        // global). Keep the global flag (global precedence, so it survives the next descent's
80        // `retain`), but union in any short/long aliases that exist only on the re-declaration,
81        // otherwise those orphan aliases would be silently dropped. Matching on a shared long is
82        // deliberate: a re-declaration sharing only a short letter with an unrelated global
83        // (`-q --quiet` vs `-q --quoting`) is a genuine collision, not an alias addition, and is
84        // handled by the `contains_key` skip below instead.
85        let inherited_global = flag.long.iter().find_map(|l| {
86            available
87                .get(&format!("--{l}"))
88                .filter(|f| f.global)
89                .cloned()
90        });
91        if let Some(global_flag) = inherited_global {
92            // Never clobber a *different* inherited global's alias. If this re-declaration's
93            // orphan alias (e.g. `-r`) is already owned by some other global (e.g. an unrelated
94            // `-r --restrict`), that is a genuine collision: keep the existing global, as global
95            // precedence dictates, instead of stealing the alias for the merged flag.
96            //
97            // Compare origins, not `Arc`s: when the global has several aliases of its own, an
98            // earlier key of this same child already replaced some of them with the merged flag,
99            // which the lookups above may now resolve to. That is the same logical flag, so it
100            // must not read as a collision and leave this key on the pre-merge global.
101            let global_origin = origin_of(&merged_origin, &global_flag);
102            if available.get(&key).is_some_and(|existing| {
103                existing.global && origin_of(&merged_origin, existing) != global_origin
104            }) {
105                continue;
106            }
107            let merged = match merged_cache.get(&(Arc::as_ptr(&flag) as usize)) {
108                Some(merged) => merged.clone(),
109                None => {
110                    let mut merged = (*global_flag).clone();
111                    // `exclusive` is deliberately *not* reconciled here, in either direction.
112                    // One object now answers to two alias sets that may disagree: the child
113                    // owns the spellings it declared, the ancestor keeps the ones only it
114                    // declared. A single bool cannot hold both, so the merged flag carries the
115                    // ancestor's and validation resolves the occurrence by the spelling that
116                    // was typed — the ledger it already consults to decide whether selecting
117                    // the child is company.
118                    for s in &flag.short {
119                        if !merged.short.contains(s) {
120                            merged.short.push(*s);
121                        }
122                    }
123                    for l in &flag.long {
124                        if !merged.long.contains(l) {
125                            merged.long.push(l.clone());
126                        }
127                    }
128                    // A child may deliberately promote one of the ancestor's hidden aliases.
129                    // Hidden lists are subsets of the accepted spellings, so a spelling present
130                    // on the child but absent from its hidden subset is visible at this level.
131                    merged.hidden_short_aliases.retain(|alias| {
132                        !flag.short.contains(alias) || flag.hidden_short_aliases.contains(alias)
133                    });
134                    merged.hidden_aliases.retain(|alias| {
135                        !flag.long.contains(alias) || flag.hidden_aliases.contains(alias)
136                    });
137                    for s in &flag.hidden_short_aliases {
138                        if !merged.hidden_short_aliases.contains(s) {
139                            merged.hidden_short_aliases.push(*s);
140                        }
141                    }
142                    for l in &flag.hidden_aliases {
143                        if !merged.hidden_aliases.contains(l) {
144                            merged.hidden_aliases.push(l.clone());
145                        }
146                    }
147                    let merged = Arc::new(merged);
148                    merged_cache.insert(Arc::as_ptr(&flag) as usize, Arc::clone(&merged));
149                    merged_origin.insert(Arc::as_ptr(&merged) as usize, global_origin);
150                    // Rebind the global's *other* aliases onto the merged flag. The loop only
151                    // visits keys the child declared, so an alias the child left out (the `-y` of
152                    // a `-y --yes` global re-declared as just `--yes`) would otherwise keep
153                    // pointing at the pre-merge flag and miss the aliases just unioned in. One
154                    // logical flag must be one object under every key it answers to.
155                    for existing in available.values_mut() {
156                        if origin_of(&merged_origin, existing) == global_origin {
157                            *existing = Arc::clone(&merged);
158                        }
159                    }
160                    merged
161                }
162            };
163            available.insert(key, merged);
164            continue;
165        }
166
167        // Purely-local flag (shares nothing with an inherited global), or one that collides only
168        // on a short with an unrelated global. Insert this alias but never shadow an inherited
169        // global flag. Such non-global flags are dropped by the next descent's `retain`.
170        if available.contains_key(&key) {
171            continue;
172        }
173        available.insert(key, flag);
174    }
175}
176
177/// Build the lookup keys a flag is registered under in `available_flags`:
178/// `--<long>` for each long name, `-<short>` for each short char, plus the `negate` token.
179fn flag_keys(flag: &SpecFlag) -> Vec<String> {
180    let mut keys: Vec<String> = flag
181        .long
182        .iter()
183        .map(|l| format!("--{l}"))
184        .chain(flag.short.iter().map(|s| format!("-{s}")))
185        .collect();
186    if let Some(negate) = &flag.negate {
187        keys.push(negate.clone());
188    }
189    keys
190}
191
192/// The flags a command declares, keyed by each of their aliases.
193fn gather_flags(cmd: &SpecCommand) -> BTreeMap<String, Arc<SpecFlag>> {
194    cmd.flags
195        .iter()
196        .flat_map(|f| {
197            let f = Arc::new(f.clone()); // One clone per flag, then cheap Arc refs
198            flag_keys(&f)
199                .into_iter()
200                .map(|key| (key, Arc::clone(&f)))
201                .collect::<Vec<_>>()
202        })
203        .collect()
204}
205
206fn unique_flags<'a>(
207    flags: impl IntoIterator<Item = &'a Arc<SpecFlag>>,
208) -> impl Iterator<Item = &'a Arc<SpecFlag>> {
209    let mut seen = HashSet::new();
210    flags
211        .into_iter()
212        .filter(move |flag| seen.insert(Arc::as_ptr(flag) as usize))
213}
214
215/// Every flag a command accepts, resolved the way parsing an invocation of it
216/// resolves them.
217///
218/// `chain` runs from the root command (`spec.cmd`) down to the command in
219/// question; an empty chain yields no flags.
220///
221/// This is not "the command's flags plus its ancestors' globals". A subcommand
222/// that re-declares a global's long name is describing the *same* flag rather
223/// than a new one, so the global's help, argument and effect survive and only
224/// the re-declaration's extra aliases are added — see
225/// [`merge_subcommand_flags`]. Anything that reports a command's flags without
226/// going through this will disagree with what the parser actually accepts.
227pub fn available_flags(chain: &[&SpecCommand]) -> Vec<Arc<SpecFlag>> {
228    let Some((root, rest)) = chain.split_first() else {
229        return vec![];
230    };
231    let mut available = gather_flags(root);
232    for cmd in rest {
233        merge_subcommand_flags(&mut available, gather_flags(cmd), false);
234    }
235
236    // Deduplicating by `Arc` identity is not enough. When a child re-declares a
237    // global that has both a short and a long, the merged flag is written under
238    // the long key while the short key keeps pointing at the pre-merge `Arc` —
239    // two objects for one logical flag. That is harmless for parsing, which
240    // looks flags up by key, but a caller listing flags would see it twice.
241    //
242    // Names break the tie because a long key always sorts before a short one
243    // (`--x` < `-y` at the second byte), so the merged declaration is the one
244    // reached first. Two genuinely distinct flags sharing a name is a spec bug
245    // that `usage lint` reports as a duplicate flag.
246    let mut seen_names = HashSet::new();
247    unique_flags(available.values())
248        .filter(|f| seen_names.insert(f.name.clone()))
249        .cloned()
250        .collect()
251}
252
253/// Extract the flag key from a flag word for lookup in available_flags map
254/// Handles both long flags (--flag, --flag=value) and short flags (-f)
255fn get_flag_key(word: &str) -> &str {
256    if word.starts_with("--") {
257        // Long flag: strip =value if present
258        word.split_once('=').map(|(k, _)| k).unwrap_or(word)
259    } else if let Some((end, _)) = word.char_indices().nth(2) {
260        // Short flag: the dash and one letter, which is one character and not
261        // necessarily one byte.
262        &word[0..end]
263    } else {
264        word
265    }
266}
267
268/// Where a value came from, when it did not come from the command line.
269///
270/// About the *value*, not the flag. `--color` typed bare with `default_missing` has a
271/// token for the flag and none for the value, and that distinction is the whole question
272/// a spec author is asking when they ask why `--color` came out `always`. Values that were
273/// typed are attributed to the token that carried them instead — see [`TokenRole::Value`].
274#[derive(Debug, Clone, PartialEq, Eq)]
275#[non_exhaustive]
276pub enum ValueOrigin {
277    /// A flag that takes a value was given without one, so the declaration supplied it:
278    /// `default_missing`, or the empty tri-state a bare `value_optional` flag records.
279    /// One variant for both, because from argv's side the same thing happened — the flag
280    /// was typed and the value was not.
281    DefaultMissing,
282    /// An environment variable, named.
283    ///
284    /// Named because a flag may list several — `env`, `env_fallback` and `deprecated_env`,
285    /// folded together by [`SpecFlag::env_names`] — and "it came from the environment" does
286    /// not say which declaration fired or which one to delete.
287    Env(String),
288    /// A declared `default`, on the flag or on the flag's argument.
289    ///
290    /// Not two variants: the precedence between them is a spec-authoring oddity rather than
291    /// a fact about the value, and `usage lint` is the place to complain about declaring
292    /// both.
293    Default,
294    /// A `default_if` whose condition matched, with the condition that decided it. The
295    /// selector alone is ambiguous — several conditions may name it with different `when`
296    /// values.
297    DefaultIf {
298        selector: String,
299        when: Option<String>,
300    },
301}
302
303/// What one word of the command line became.
304///
305/// Several because a single token can do more than one thing: `-abc` sets three flags,
306/// `-j8` is a flag and its value.
307#[derive(Debug, Clone)]
308#[non_exhaustive]
309pub enum TokenRole {
310    /// argv[0]. Also a `Command` when a multicall symlink makes the basename a word.
311    Program,
312    /// Selected a subcommand.
313    Command { name: String },
314    /// Named a flag, in this spelling. `negated` for the `negate` form.
315    Flag {
316        flag: Arc<SpecFlag>,
317        spelling: String,
318        negated: bool,
319    },
320    /// Supplied a flag's value. Several values when a `delimiter` split the word.
321    Value {
322        flag: Arc<SpecFlag>,
323        values: Vec<String>,
324        /// Whether the value rode along on the flag's own token (`--env=prod`, `-j8`)
325        /// rather than following it as its own word.
326        attached: bool,
327    },
328    /// Filled a positional argument. Several values when a `delimiter` split the word.
329    Arg {
330        arg: Arc<SpecArg>,
331        values: Vec<String>,
332    },
333    /// An explicit `--`, consumed as a separator.
334    Separator,
335    /// A word the parser answers itself rather than binding: `--help`, `-h`, `--version`,
336    /// `-V`. The parse stops here and the answer travels as an error carrying the text, so
337    /// without a role the word reads as having done nothing while a whole help page arrives
338    /// in the error list.
339    Builtin { spelling: String },
340    /// A declared `value_terminator`, consumed to end a run of values. `ends` names the
341    /// declaration whose run it closed — the word is not one of that run's values, which is
342    /// the whole reason it was declared.
343    ValueTerminator { ends: String },
344    /// A declared `restart_token`: the positional cursor and the values it had filled start
345    /// over here. Recorded because the words before it are still in the report, and without
346    /// this row they look like they filled arguments that then came back empty.
347    Restart,
348    /// A flag-like word no declaration matched. `bound_as` is the positional that took it
349    /// under `unknown_flags="value"`, and `None` when the word was refused.
350    UnknownFlag { bound_as: Option<Arc<SpecArg>> },
351    /// The word reached a declaration that would not take it, and was dropped. Without this
352    /// the token reads as having done nothing, which is the one thing it did not do.
353    Refused { reason: String },
354    /// Forwarded to an external subcommand.
355    External,
356    /// The parser stopped before this word — a help request, a refused value.
357    Unread,
358}
359
360/// One word of the command line, and what it became.
361#[derive(Debug, Clone)]
362#[non_exhaustive]
363pub struct TokenBinding {
364    /// Position in the argv slice the parse was given, argv[0] included.
365    pub index: usize,
366    pub word: String,
367    /// Roles a word the parser made up contributed, folded onto the token it was derived
368    /// from: the tail of a short bundle onto the bundle, a multicall applet name onto
369    /// argv[0]. `word` is what the caller wrote, not what the parser read.
370    pub synthesized: bool,
371    pub roles: Vec<TokenRole>,
372}
373
374#[non_exhaustive]
375pub struct ParseOutput {
376    pub cmd: SpecCommand,
377    pub cmds: Vec<SpecCommand>,
378    pub args: IndexMap<Arc<SpecArg>, ParseValue>,
379    pub flags: IndexMap<Arc<SpecFlag>, ParseValue>,
380    /// What each word of the command line became, in argv order, one entry per word.
381    ///
382    /// The token half of provenance; [`ParseOutput::flag_origins`] and
383    /// [`ParseOutput::arg_origins`] are the other half. A table keyed by token cannot show
384    /// a value that came from nowhere in argv, and a table keyed by declaration cannot show
385    /// a token that bound to nothing, so both exist.
386    pub tokens: Vec<TokenBinding>,
387    /// Where a flag's value came from when it did not come from argv, in the order the
388    /// fallbacks fired. Keyed as [`ParseOutput::flags`] is.
389    ///
390    /// A list rather than one origin: repeated bare occurrences of a `var` flag each take a
391    /// `default_missing` value, so one flag can have several.
392    pub flag_origins: IndexMap<Arc<SpecFlag>, Vec<ValueOrigin>>,
393    /// Where an argument's value came from when it did not come from argv. Keyed as
394    /// [`ParseOutput::args`] is.
395    pub arg_origins: IndexMap<Arc<SpecArg>, Vec<ValueOrigin>>,
396    /// Flags a later occurrence removed, and the flag that removed them.
397    ///
398    /// The overriding name is the half a caller needs: the fallback phase silently declines
399    /// to fill an overridden flag, so "why is `--quiet` unset when its default says
400    /// otherwise" has no answer without it.
401    pub overridden_flags: BTreeMap<String, String>,
402    /// Every flag the parser recognizes at this point, keyed by each of its aliases
403    /// (`--long`, `-s`, negations).
404    ///
405    /// This includes flags that only remain recognized because they may appear *before* a
406    /// mounted command — see [`ParseOutput::completion_flags`] for the set a completion
407    /// should offer.
408    pub available_flags: BTreeMap<String, Arc<SpecFlag>>,
409    pub flag_awaiting_value: Vec<Arc<SpecFlag>>,
410    pub errors: Vec<UsageErr>,
411    /// Deprecated declarations this command line used, for the caller to render when its
412    /// logging is up. Empty from [`parse_partial`]: a half-typed line being completed has
413    /// not used anything yet.
414    pub warnings: Vec<Warning>,
415    /// The positional argument the next word would have filled, i.e. where the parser's
416    /// cursor stopped. `None` once every argument is satisfied.
417    ///
418    /// Completions need exactly this: the parser already accounts for `var_max`, for
419    /// `restart_token` rewinds, and for the jump an explicit `--` performs onto a
420    /// `double_dash="required"` argument, so re-deriving it from `args` would disagree.
421    pub next_arg: Option<Arc<SpecArg>>,
422    /// Whether an explicit `--` was consumed *as a separator*.
423    ///
424    /// A `--` that `double_dash="preserve"` keeps as a value does not count: it is a value
425    /// of the variadic argument collecting it, not a separator, so it does not unlock a
426    /// `double_dash="required"` argument.
427    pub double_dash_seen: bool,
428    /// Remaining argv captured when an unmatched word was forwarded as an external
429    /// subcommand: the command name first, then every token after it.
430    ///
431    /// Absent when no external command was selected. See [`SpecCommand::external_subcommand`].
432    pub external: Option<Vec<String>>,
433}
434
435impl ParseOutput {
436    /// The flags a completion should offer for the parsed command.
437    ///
438    /// Usually every recognized flag, i.e. [`ParseOutput::available_flags`]. Once a mounted
439    /// command has been reached, though, the commands above it belong to the mounting CLI and
440    /// their flags are not accepted there — mise, for example, forwards everything after a task
441    /// name to the task itself — so only the flags declared from the mount boundary down are
442    /// offered. Those globals stay in `available_flags` because they may legitimately appear
443    /// *before* the mounted command.
444    pub fn completion_flags(&self) -> BTreeMap<String, Arc<SpecFlag>> {
445        let Some(boundary) = self.cmds.iter().position(|cmd| cmd.mounted) else {
446            return self.available_flags.clone();
447        };
448        // A mount can also merge flags from its spec's root into the command it is mounted on
449        // (`SpecCommand::flags_from_mount`). Those describe the mounted program too, so the
450        // replay starts one level up to inherit its globals.
451        let start = match boundary.checked_sub(1) {
452            Some(prev) if self.cmds[prev].flags_from_mount => prev,
453            _ => boundary,
454        };
455        // Re-run the descent from there, which starts with no inherited flags. Below the
456        // boundary the mounted program's commands are ordinary commands, so the descents use
457        // the same merge as the real parse.
458        let mut offered = gather_flags(&self.cmds[start]);
459        for cmd in &self.cmds[start + 1..] {
460            merge_subcommand_flags(&mut offered, gather_flags(cmd), false);
461        }
462        offered
463    }
464}
465
466#[derive(Debug, Clone)]
467pub enum ParseValue {
468    Bool(bool),
469    String(String),
470    MultiBool(Vec<bool>),
471    MultiString(Vec<String>),
472}
473
474impl ParseValue {
475    pub fn try_as_bool(self) -> Option<bool> {
476        match self {
477            Self::Bool(value) => Some(value),
478            _ => None,
479        }
480    }
481
482    pub const fn try_as_bool_ref(&self) -> Option<&bool> {
483        match self {
484            Self::Bool(value) => Some(value),
485            _ => None,
486        }
487    }
488
489    pub fn try_as_bool_mut(&mut self) -> Option<&mut bool> {
490        match self {
491            Self::Bool(value) => Some(value),
492            _ => None,
493        }
494    }
495
496    pub fn try_as_string(self) -> Option<String> {
497        match self {
498            Self::String(value) => Some(value),
499            _ => None,
500        }
501    }
502
503    pub const fn try_as_string_ref(&self) -> Option<&String> {
504        match self {
505            Self::String(value) => Some(value),
506            _ => None,
507        }
508    }
509
510    pub fn try_as_string_mut(&mut self) -> Option<&mut String> {
511        match self {
512            Self::String(value) => Some(value),
513            _ => None,
514        }
515    }
516
517    pub fn try_as_multi_bool(self) -> Option<Vec<bool>> {
518        match self {
519            Self::MultiBool(value) => Some(value),
520            _ => None,
521        }
522    }
523
524    pub const fn try_as_multi_bool_ref(&self) -> Option<&Vec<bool>> {
525        match self {
526            Self::MultiBool(value) => Some(value),
527            _ => None,
528        }
529    }
530
531    pub fn try_as_multi_bool_mut(&mut self) -> Option<&mut Vec<bool>> {
532        match self {
533            Self::MultiBool(value) => Some(value),
534            _ => None,
535        }
536    }
537
538    pub fn try_as_multi_string(self) -> Option<Vec<String>> {
539        match self {
540            Self::MultiString(value) => Some(value),
541            _ => None,
542        }
543    }
544
545    pub const fn try_as_multi_string_ref(&self) -> Option<&Vec<String>> {
546        match self {
547            Self::MultiString(value) => Some(value),
548            _ => None,
549        }
550    }
551
552    pub fn try_as_multi_string_mut(&mut self) -> Option<&mut Vec<String>> {
553        match self {
554            Self::MultiString(value) => Some(value),
555            _ => None,
556        }
557    }
558}
559
560/// The deprecated declarations argv itself named: the commands it descended through, and the
561/// flags it bound.
562///
563/// Called before the environment and defaults have filled anything, because afterwards nothing
564/// distinguishes a flag the user typed from one a variable supplied — and the two are reported
565/// differently, at the point where each is applied.
566///
567/// The root is skipped. A `deprecated` root would otherwise warn on every invocation of the CLI,
568/// including `--help`, and the compiled parser reports selected commands rather than the one the
569/// process already is.
570fn collect_deprecations(out: &mut ParseOutput) {
571    for cmd in out.cmds.iter().skip(1) {
572        if cmd.deprecated.is_none()
573            && cmd.deprecated_warn_at.is_none()
574            && cmd.deprecated_remove_at.is_none()
575        {
576            continue;
577        }
578        out.warnings.push(Warning::command(
579            cmd.name.clone(),
580            cmd.deprecated.clone(),
581            cmd.deprecated_warn_at.clone(),
582            cmd.deprecated_remove_at.clone(),
583        ));
584    }
585    for flag in out.flags.keys() {
586        if let Some(warning) = flag_deprecation(flag) {
587            out.warnings.push(warning);
588        }
589    }
590}
591
592/// A warning for a flag that was used, if its declaration is deprecated at all.
593fn flag_deprecation(flag: &SpecFlag) -> Option<Warning> {
594    if flag.deprecated.is_none()
595        && flag.deprecated_warn_at.is_none()
596        && flag.deprecated_remove_at.is_none()
597    {
598        return None;
599    }
600    Some(Warning::flag(
601        flag_spelling(flag),
602        flag.deprecated.clone(),
603        flag.deprecated_warn_at.clone(),
604        flag.deprecated_remove_at.clone(),
605    ))
606}
607
608/// A flag named the way the user names it. The spec's name for it has no dashes, and a warning
609/// about `old-flag` would be about a word nobody typed.
610fn flag_spelling(flag: &SpecFlag) -> String {
611    flag.long
612        .first()
613        .map(|long| format!("--{long}"))
614        .or_else(|| flag.short.first().map(|short| format!("-{short}")))
615        .unwrap_or_else(|| flag.name.clone())
616}
617
618/// The name this flag reads first, which is what to use instead of a deprecated alias.
619fn flag_current_env(flag: &SpecFlag) -> Option<String> {
620    flag.env
621        .clone()
622        .or_else(|| flag.env_fallback.first().cloned())
623}
624
625fn flag_env_is_deprecated(flag: &SpecFlag, name: &str) -> bool {
626    flag.deprecated_env.iter().any(|declared| declared == name)
627}
628
629/// The same two questions for a positional, which has aliases but no `deprecated` of its own.
630fn arg_current_env(arg: &SpecArg) -> Option<String> {
631    arg.env
632        .clone()
633        .or_else(|| arg.env_fallback.first().cloned())
634}
635
636fn arg_env_is_deprecated(arg: &SpecArg, name: &str) -> bool {
637    arg.deprecated_env.iter().any(|declared| declared == name)
638}
639
640/// The first of `names` that is set, and which one it was.
641///
642/// `env_names()` yields the current name, then the declared fallbacks, then the deprecated
643/// aliases, so the winner's identity is what says whether a value arrived through an alias.
644/// Deciding that a second time, from the outside, would be a copy of this precedence rule free to
645/// disagree with it.
646fn first_set_env<'a>(
647    mut names: impl Iterator<Item = &'a str>,
648    get_env: &impl Fn(&str) -> Option<String>,
649) -> Option<(&'a str, String)> {
650    names.find_map(|name| get_env(name).map(|value| (name, value)))
651}
652
653/// Builder for parsing command-line arguments with custom options.
654///
655/// Use this when you need to customize parsing behavior, such as providing
656/// a custom environment variable map instead of using the process environment.
657///
658/// # Example
659/// ```
660/// use std::collections::HashMap;
661/// use usage::Spec;
662/// use usage::parse::Parser;
663///
664/// let spec: Spec = r#"flag "--name <name>" env="NAME""#.parse().unwrap();
665/// let env: HashMap<String, String> = [("NAME".into(), "john".into())].into();
666///
667/// let result = Parser::new(&spec)
668///     .with_env(env)
669///     .parse(&["cmd".into()])
670///     .unwrap();
671/// ```
672#[non_exhaustive]
673pub struct Parser<'a> {
674    spec: &'a Spec,
675    env: Option<HashMap<String, String>>,
676    mount_outputs: Option<HashMap<String, String>>,
677}
678
679impl<'a> Parser<'a> {
680    /// Create a new parser for the given spec.
681    pub fn new(spec: &'a Spec) -> Self {
682        Self {
683            spec,
684            env: None,
685            mount_outputs: None,
686        }
687    }
688
689    /// Use a custom environment variable map instead of the process environment.
690    ///
691    /// This is useful when parsing for tasks in a monorepo where the env vars
692    /// come from a child config file rather than the current process environment.
693    pub fn with_env(mut self, env: HashMap<String, String>) -> Self {
694        self.env = Some(env);
695        self
696    }
697
698    /// Inject deterministic outputs for mount commands instead of executing them.
699    ///
700    /// Keys are the exact `run` strings declared by mount nodes and values are the
701    /// usage specs those commands would print. When this is set, every encountered
702    /// mount must have an entry. Production parsing remains process-backed unless a
703    /// caller explicitly opts into injection.
704    pub fn with_mount_outputs(mut self, outputs: HashMap<String, String>) -> Self {
705        self.mount_outputs = Some(outputs);
706        self
707    }
708
709    /// Parse the input arguments.
710    ///
711    /// Returns the parsed arguments and flags, with defaults and env vars applied.
712    pub fn parse(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
713        let out = self.parse_collecting(input)?;
714        if let Some(err) = out
715            .errors
716            .iter()
717            .find(|e| matches!(e, UsageErr::Help(_) | UsageErr::Version(_)))
718        {
719            bail!("{err}");
720        }
721        if !out.errors.is_empty() {
722            bail!("{}", out.errors.iter().map(|e| e.to_string()).join("\n"));
723        }
724        Ok(out)
725    }
726
727    /// Everything the parse learned, whether or not it succeeded.
728    ///
729    /// [`Parser::parse`] wants the first error and nothing else, which is right for a
730    /// caller about to act on a command line. A caller that wants to *explain* one wants
731    /// the opposite: the bindings that worked and every complaint about the rest, since a
732    /// report saying only "missing required <src>" is the report you already had.
733    ///
734    /// Failures that stop the parse dead — a mount that will not run, a word no
735    /// declaration can take — still come back as `Err`. There is no output to describe in
736    /// those cases; see [`Parser::explain`] for what to do about it.
737    pub fn explain(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
738        self.parse_collecting(input)
739    }
740
741    /// The binding phase's own answer for a line [`Parser::explain`] refused.
742    ///
743    /// `Ok` when the binding phase finished and the failure came after it — a flag left
744    /// waiting for a value, say. Everything argv supplied is there and only the
745    /// environment-and-defaults phase is missing.
746    ///
747    /// `Err` when the binding phase is where it died, leaving the tokens it had attributed
748    /// by then. Those words are most of what a report is for: "no declaration takes `bogus`"
749    /// is more useful next to the three tokens that did bind than on its own. The word that
750    /// caused the failure carries a role saying so, and everything still queued behind it is
751    /// [`TokenRole::Unread`], for the two failures a command line reaches on its own — a
752    /// word nothing declares, and a flag a strict spec refuses. A failure in the spec rather
753    /// than in the line, such as a mount that will not run, stops the trace where it stopped
754    /// and the words past it carry no role.
755    pub fn explain_refused(self, input: &[String]) -> Result<ParseOutput, Vec<TokenBinding>> {
756        let mut trace = Trace::new(input);
757        match parse_partial_traced(
758            self.spec,
759            input,
760            self.env.as_ref(),
761            self.mount_outputs.as_ref(),
762            MountTiming::WhenAWordIsUnknown,
763            &mut trace,
764        ) {
765            // A parse that got as far as stopping normally already moved its tokens onto the
766            // output, which is where a caller should read them from.
767            Ok((out, _)) => Ok(out),
768            Err(_) => Err(trace.tokens),
769        }
770    }
771
772    fn parse_collecting(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
773        let custom_env = self.env.as_ref();
774        let (mut out, overridden_flags) = parse_partial_with_env(
775            self.spec,
776            input,
777            custom_env,
778            self.mount_outputs.as_ref(),
779            MountTiming::WhenAWordIsUnknown,
780        )?;
781        trace!("{out:?}");
782
783        // A flag still waiting for a value never got one, so the command line ended
784        // mid-flag. `parse_partial` leaves this for completions to look at — a
785        // half-typed `--jobs ` is exactly what a completion is asked about — but a
786        // full parse has nothing left to wait for, and dropping the flag silently
787        // made a forgotten value look like a working command.
788        while try_bind_default_missing(
789            &mut out.flags,
790            &mut out.flag_awaiting_value,
791            custom_env,
792            &mut out.flag_origins,
793        )? {}
794        if let Some(flag) = out.flag_awaiting_value.first() {
795            let token = flag
796                .long
797                .first()
798                .map(|l| format!("--{l}"))
799                .or_else(|| flag.short.first().map(|s| format!("-{s}")))
800                .unwrap_or_else(|| flag.name.clone());
801            let rendered = input.join(" ");
802            let span = rendered
803                .rfind(&token)
804                .map(|at| (at, token.len()))
805                .unwrap_or((0, 0));
806            return Err(UsageErr::InvalidFlag {
807                token,
808                reason: "requires an argument".to_string(),
809                span: span.into(),
810                input: rendered,
811            }
812            .into());
813        }
814
815        // Before the environment and defaults have their turn, because both mark a field as
816        // filled and only argv can be reported as something the user typed. Env is reported
817        // where it is applied, below; a default is nobody's request and reports nothing.
818        collect_deprecations(&mut out);
819
820        let get_env = |key: &str| -> Option<String> {
821            if let Some(env_map) = custom_env {
822                env_map.get(key).cloned()
823            } else {
824                std::env::var(key).ok()
825            }
826        };
827
828        // Apply env vars and defaults for args
829        //
830        // Not `skip(out.args.len())`: an explicit `--` can jump the parser's cursor past an arg
831        // that stayed empty, leaving a gap that makes the fill count a wrong starting offset.
832        for arg in out.cmd.args.iter() {
833            if out.args.contains_key(arg) {
834                continue;
835            }
836            if let Some((env_name, env_value)) = first_set_env(arg.env_names(), &get_env) {
837                if arg_env_is_deprecated(arg, env_name) {
838                    out.warnings
839                        .push(Warning::env(env_name, arg_current_env(arg)));
840                }
841                let values = split_fallback_values(std::slice::from_ref(&env_value), arg.delimiter);
842                validate_choice_values(
843                    ChoiceTarget::arg(arg),
844                    &values,
845                    arg.choices.as_ref(),
846                    custom_env,
847                )?;
848                let parsed = if arg.var {
849                    validate_arg_fallback_count(arg, values.len(), &mut out.errors);
850                    ParseValue::MultiString(values)
851                } else {
852                    ParseValue::String(values.into_iter().next().unwrap_or_default())
853                };
854                out.args.insert(Arc::new(arg.clone()), parsed);
855                out.arg_origins
856                    .entry(Arc::new(arg.clone()))
857                    .or_default()
858                    .push(ValueOrigin::Env(env_name.to_string()));
859                continue;
860            }
861            if !arg.default.is_empty() {
862                // Consider var when deciding the type of default return value
863                if arg.var {
864                    let values = split_fallback_values(&arg.default, arg.delimiter);
865                    validate_arg_fallback_count(arg, values.len(), &mut out.errors);
866                    validate_choice_values(
867                        ChoiceTarget::arg(arg),
868                        &values,
869                        arg.choices.as_ref(),
870                        custom_env,
871                    )?;
872                    // For var=true, always return a vec (MultiString)
873                    out.args
874                        .insert(Arc::new(arg.clone()), ParseValue::MultiString(values));
875                    out.arg_origins
876                        .entry(Arc::new(arg.clone()))
877                        .or_default()
878                        .push(ValueOrigin::Default);
879                } else {
880                    validate_choice_value(
881                        ChoiceTarget::arg(arg),
882                        &arg.default[0],
883                        arg.choices.as_ref(),
884                        custom_env,
885                    )?;
886                    // For var=false, return the first default value as String
887                    out.args.insert(
888                        Arc::new(arg.clone()),
889                        ParseValue::String(arg.default[0].clone()),
890                    );
891                    out.arg_origins
892                        .entry(Arc::new(arg.clone()))
893                        .or_default()
894                        .push(ValueOrigin::Default);
895                }
896            }
897        }
898
899        // Environment first, for every flag, so a `default_if` can see a sibling
900        // that was filled from env. Applying both in one pass would make the
901        // answer depend on declaration order: `--bin-names` before `--json`
902        // would miss `EX_JSON=1`.
903        let flags: Vec<Arc<SpecFlag>> = out.available_flags.values().cloned().collect();
904        for flag in &flags {
905            if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
906                continue;
907            }
908            if let Some((env_name, env_value)) = first_set_env(flag.env_names(), &get_env) {
909                // The flag's own deprecation before the alias's, which is the order the
910                // compiled parser reports them in: it walks a command's flags and then its
911                // aliases. Using a deprecated flag through a variable is still using it.
912                if let Some(warning) = flag_deprecation(flag) {
913                    out.warnings.push(warning);
914                }
915                if flag_env_is_deprecated(flag, env_name) {
916                    out.warnings
917                        .push(Warning::env(env_name, flag_current_env(flag)));
918                }
919                if let Some(arg) = flag.arg.as_ref() {
920                    let values =
921                        split_fallback_values(std::slice::from_ref(&env_value), arg.delimiter);
922                    validate_choice_values(
923                        ChoiceTarget::option(flag),
924                        &values,
925                        arg.choices.as_ref(),
926                        custom_env,
927                    )?;
928                    let parsed = if flag.var || arg.var {
929                        if flag.var {
930                            validate_flag_fallback_count(flag, values.len(), &mut out.errors);
931                        }
932                        if arg.var {
933                            validate_flag_arg_fallback_count(
934                                flag,
935                                arg,
936                                values.len(),
937                                &mut out.errors,
938                            );
939                        }
940                        ParseValue::MultiString(values)
941                    } else {
942                        ParseValue::String(values.into_iter().next().unwrap_or_default())
943                    };
944                    out.flags.insert(Arc::clone(flag), parsed);
945                } else {
946                    let is_true = matches!(env_value.as_str(), "1" | "true" | "True" | "TRUE");
947                    out.flags
948                        .insert(Arc::clone(flag), ParseValue::Bool(is_true));
949                }
950                out.flag_origins
951                    .entry(Arc::clone(flag))
952                    .or_default()
953                    .push(ValueOrigin::Env(env_name.to_string()));
954            }
955        }
956        // Decide every `default_if` against argv+env only. Binding as we go would put
957        // a default into `out.flags` and make the next flag's condition treat it as
958        // explicit — Go's `Given()` and the derive's `__given_*` both ignore defaults
959        // here, so an unconditional `default` on `--json` must not fire
960        // `default_if "--json"`.
961        let mut from_default_if: Vec<(Arc<SpecFlag>, crate::SpecDefaultIf)> = Vec::new();
962        for flag in &flags {
963            if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
964                continue;
965            }
966            if let Some(condition) = flag.default_if.iter().find(|condition| {
967                default_if_condition_matches(condition, &out, &overridden_flags, custom_env)
968            }) {
969                from_default_if.push((Arc::clone(flag), condition.clone()));
970            }
971        }
972        for (flag, condition) in &from_default_if {
973            // The whole condition, not just the value: several conditions may name the same
974            // selector with different `when` values, so the selector alone does not say
975            // which one fired.
976            bind_flag_fallback(
977                flag,
978                std::slice::from_ref(&condition.value),
979                &mut out,
980                custom_env,
981                ValueOrigin::DefaultIf {
982                    selector: condition.selector.clone(),
983                    when: condition.when.clone(),
984                },
985            )?;
986        }
987        for flag in &flags {
988            if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
989                continue;
990            }
991            if !flag.default.is_empty() {
992                bind_flag_fallback(
993                    flag,
994                    &flag.default,
995                    &mut out,
996                    custom_env,
997                    ValueOrigin::Default,
998                )?;
999                continue;
1000            }
1001            if let Some(arg) = flag.arg.as_ref() {
1002                if !arg.default.is_empty() {
1003                    bind_flag_fallback(
1004                        flag,
1005                        &arg.default,
1006                        &mut out,
1007                        custom_env,
1008                        ValueOrigin::Default,
1009                    )?;
1010                }
1011            }
1012        }
1013        // Declarative value validation is deliberately post-binding. Defaults and
1014        // environment fallbacks have landed by here, and delimiters were already split
1015        // while binding. Like clap's value parsers, a declaration judges each resulting
1016        // raw value independently.
1017        for (arg, parsed) in &out.args {
1018            validate_expression(
1019                &arg.name,
1020                arg.validate.as_deref(),
1021                arg.validate_error.as_deref(),
1022                parsed,
1023                &mut out.errors,
1024            );
1025        }
1026        for (flag, parsed) in &out.flags {
1027            if let Some(arg) = &flag.arg {
1028                validate_expression(
1029                    &flag.name,
1030                    arg.validate.as_deref(),
1031                    arg.validate_error.as_deref(),
1032                    parsed,
1033                    &mut out.errors,
1034                );
1035            }
1036        }
1037        // Applied once, here, because this is where the CLI's own version is known: a
1038        // `deprecated_warn_at` the spec has not reached yet is an author saying *not yet*.
1039        crate::warn::retain_reached(&mut out.warnings, self.spec.version.as_deref());
1040        Ok(out)
1041    }
1042}
1043
1044/// Parse command-line arguments according to a spec.
1045///
1046/// Returns the parsed arguments and flags, with defaults and env vars applied.
1047/// Uses `std::env::var` for environment variable lookups.
1048///
1049/// For custom environment variable handling, use [`Parser`] instead.
1050#[must_use = "parsing result should be used"]
1051pub fn parse(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
1052    Parser::new(spec).parse(input)
1053}
1054
1055/// Parse command-line arguments without applying defaults.
1056///
1057/// Use this for help text generation or when you need the raw parsed values.
1058#[must_use = "parsing result should be used"]
1059pub fn parse_partial(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
1060    parse_partial_with_env(spec, input, None, None, MountTiming::Eager).map(|(out, _)| out)
1061}
1062
1063/// Basename of argv[0] for a multicall CLI: last path component, with a trailing
1064/// `.exe` stripped so Windows and Unix agree.
1065pub fn multicall_basename(argv0: &str) -> &str {
1066    let name = argv0.rsplit(['/', '\\']).next().unwrap_or(argv0);
1067    match name.get(name.len().saturating_sub(4)..) {
1068        Some(ext) if ext.eq_ignore_ascii_case(".exe") => &name[..name.len() - 4],
1069        _ => name,
1070    }
1071}
1072
1073/// The applet name to parse as the first word, when argv[0] is not the dispatcher.
1074///
1075/// `None` means a dispatcher invocation (`busybox ls`): skip argv[0] and parse the
1076/// rest. `Some` is a symlink invocation (`ls -l`): inject the basename.
1077pub fn multicall_applet<'a>(argv0: &'a str, name: &str, bin: Option<&str>) -> Option<&'a str> {
1078    let base = multicall_basename(argv0);
1079    if !name.is_empty() && base == multicall_basename(name) {
1080        return None;
1081    }
1082    if let Some(bin) = bin {
1083        if !bin.is_empty() && base == multicall_basename(bin) {
1084            return None;
1085        }
1086    }
1087    Some(base)
1088}
1089
1090/// Internal version of parse_partial that accepts an optional custom env map.
1091/// When a command's own `mount` runs, for the root — which nothing descends into.
1092///
1093/// A completion has to know every command before it can offer one, even with
1094/// nothing typed yet, so it resolves up front. An execution knows the word it was
1095/// given, so it only pays for discovery when that word matches nothing declared —
1096/// and a CLI that declares its commands and mounts a few more does not spawn a
1097/// process on every invocation.
1098#[derive(Clone, Copy, PartialEq, Eq)]
1099enum MountTiming {
1100    Eager,
1101    WhenAWordIsUnknown,
1102}
1103
1104/// One word on its way through the parser, with what the parser has learned about it.
1105///
1106/// This holds what a side queue used to: the flag Phase 1 read a word as, previously a
1107/// `VecDeque` popped in step with the words. Two queues staying aligned is an invariant
1108/// nothing checks, and it was delicate enough to need explaining at three call sites; on
1109/// the word itself there is nothing to keep aligned. The argv position is here for the
1110/// same reason: the queue is popped, re-queued, split on `=`, and has subcommand words
1111/// removed from the middle, so position in the queue stops meaning position in argv on the
1112/// first descent.
1113struct Token {
1114    word: String,
1115    /// Where in the caller's argv this word came from.
1116    ///
1117    /// A word the parser made up points at the token it was derived from — the tail of a
1118    /// short bundle at the bundle, a multicall applet name at argv[0] — because that is the
1119    /// token a reader would point at, and there is nothing else to point at.
1120    argv: usize,
1121    /// The flag Phase 1 read this word as, and the command level it read it at.
1122    ///
1123    /// `Some((flag, command_level))` for a flag word, `None` for its value, for anything
1124    /// unresolved, and for every word Phase 1 never reached. The words stay in the queue
1125    /// for Phase 2 to re-parse — that is how they reach `out.flags` and `as_env()` — but by
1126    /// then the recognized flags have changed, because each descent drops the parent's
1127    /// non-global flags and a mounted command may declare the same name as a global seen
1128    /// here. Recording the owner keeps a word bound to the flag it was read as.
1129    ///
1130    /// The level matters to strict parsing: clap permits an inherited global once on each
1131    /// side of a subcommand boundary.
1132    binding: Option<(Arc<SpecFlag>, usize)>,
1133}
1134
1135impl Token {
1136    fn new(word: String, argv: usize) -> Self {
1137        Self {
1138            word,
1139            argv,
1140            binding: None,
1141        }
1142    }
1143}
1144
1145/// The token trace, while it is being built.
1146///
1147/// One row per word of the caller's argv, so a role can be recorded against a position
1148/// without the recorder having to know how many words came before it. Words the parser
1149/// made up have no row of their own and fold onto the row they were derived from.
1150struct Trace {
1151    tokens: Vec<TokenBinding>,
1152}
1153
1154impl Trace {
1155    fn new(input: &[String]) -> Self {
1156        Self {
1157            tokens: input
1158                .iter()
1159                .enumerate()
1160                .map(|(index, word)| TokenBinding {
1161                    index,
1162                    word: word.clone(),
1163                    synthesized: false,
1164                    roles: vec![],
1165                })
1166                .collect(),
1167        }
1168    }
1169
1170    fn record(&mut self, argv: usize, role: TokenRole) {
1171        if let Some(token) = self.tokens.get_mut(argv) {
1172            token.roles.push(role);
1173        }
1174    }
1175
1176    /// Note that what was read at this position is not what the caller wrote there.
1177    fn note_synthesized(&mut self, argv: usize) {
1178        if let Some(token) = self.tokens.get_mut(argv) {
1179            token.synthesized = true;
1180        }
1181    }
1182
1183    /// Every word the parse never reached, once it has stopped.
1184    fn close(&mut self, unread: &VecDeque<Token>) {
1185        for token in unread {
1186            self.record(token.argv, TokenRole::Unread);
1187        }
1188    }
1189}
1190
1191fn parse_partial_with_env(
1192    spec: &Spec,
1193    input: &[String],
1194    custom_env: Option<&HashMap<String, String>>,
1195    mount_outputs: Option<&HashMap<String, String>>,
1196    mount_timing: MountTiming,
1197) -> Result<(ParseOutput, HashSet<String>), miette::Error> {
1198    let mut trace = Trace::new(input);
1199    parse_partial_traced(
1200        spec,
1201        input,
1202        custom_env,
1203        mount_outputs,
1204        mount_timing,
1205        &mut trace,
1206    )
1207}
1208
1209/// The binding phase, with the trace left somewhere the caller can still read it.
1210///
1211/// A failure this phase cannot continue past — a word no declaration can take, a flag a
1212/// strict spec refuses — leaves through `?`, and a trace owned by the loop goes with it. The
1213/// words read before the failure are most of what a report wants, so the caller owns the
1214/// trace instead and keeps them. See [`Parser::explain_refused`].
1215fn parse_partial_traced(
1216    spec: &Spec,
1217    input: &[String],
1218    custom_env: Option<&HashMap<String, String>>,
1219    mount_outputs: Option<&HashMap<String, String>>,
1220    mount_timing: MountTiming,
1221    trace: &mut Trace,
1222) -> Result<(ParseOutput, HashSet<String>), miette::Error> {
1223    if let Some(view) = input.first().and_then(|argv0| spec.view_for_program(argv0)) {
1224        let viewed = spec.for_view(view)?;
1225        return parse_partial_traced(
1226            &viewed,
1227            input,
1228            custom_env,
1229            mount_outputs,
1230            mount_timing,
1231            trace,
1232        );
1233    }
1234    trace!("parse_partial: {input:?}");
1235    let mut input = input
1236        .iter()
1237        .enumerate()
1238        .map(|(argv, word)| Token::new(word.clone(), argv))
1239        .collect::<VecDeque<_>>();
1240    let argv0 = input.pop_front();
1241    if let Some(argv0) = argv0.as_ref() {
1242        trace.record(argv0.argv, TokenRole::Program);
1243    }
1244    if spec.multicall {
1245        if let Some(raw) = argv0 {
1246            if let Some(applet) = multicall_applet(&raw.word, &spec.name, Some(spec.bin.as_str())) {
1247                // A symlink invocation reads a word the caller never typed — the basename of
1248                // the program itself — so argv[0] is both the program and, below, whatever
1249                // that word selects.
1250                trace.note_synthesized(raw.argv);
1251                input.push_front(Token::new(applet.to_string(), raw.argv));
1252            }
1253        }
1254    }
1255    // The policy observes the selected command's own argv, not values eventually filled from
1256    // env/default. Start at the root, then reset on every explicit descent. A default
1257    // subcommand receives the unmatched word that selected it, so it is necessarily non-bare.
1258    let mut command_has_argv = !input.is_empty();
1259
1260    let mut out = ParseOutput {
1261        cmd: spec.cmd.clone(),
1262        cmds: vec![spec.cmd.clone()],
1263        args: IndexMap::new(),
1264        flags: IndexMap::new(),
1265        tokens: vec![],
1266        flag_origins: IndexMap::new(),
1267        arg_origins: IndexMap::new(),
1268        overridden_flags: BTreeMap::new(),
1269        available_flags: gather_flags(&spec.cmd),
1270        flag_awaiting_value: vec![],
1271        errors: vec![],
1272        warnings: vec![],
1273        next_arg: None,
1274        double_dash_seen: false,
1275        external: None,
1276    };
1277    // Keep this internal so adding relationship support remains semver-compatible. The full
1278    // parser uses it to prevent defaults and environment values from restoring overridden flags.
1279    let mut overridden_flags = HashSet::new();
1280    // Which spelling supplied each parsed flag. A child may re-declare one long form of an
1281    // inherited global while the merge keeps the ancestor's other aliases on the same `Arc`.
1282    // The declaration object alone then cannot answer whether `--clean` belonged to the child
1283    // or an inherited `-c` belonged to the ancestor.
1284    let mut parsed_flag_spellings: HashMap<usize, HashSet<String>> = HashMap::new();
1285
1286    // Phase 1: Scan for subcommands and collect global flags
1287    //
1288    // This phase identifies subcommands early because they may have mount points
1289    // that need to be executed with the global flags that appeared before them.
1290    //
1291    // Example: "usage --verbose run task"
1292    //   -> finds "run" subcommand, passes ["--verbose"] to its mount command
1293    //   -> then finds "task" as a subcommand of "run" (if it exists)
1294    //
1295    // We only collect global flags for mounts because:
1296    // - Non-global flags are specific to the current command, not subcommands
1297    // - Global flags affect all commands and should be passed to mount points
1298    let mut prefix_flags: Vec<(Arc<SpecFlag>, Vec<String>)> = vec![];
1299    // Which flag each word skipped here belongs to is recorded on the word — see
1300    // `Token::binding`.
1301    let mut command_arg_found = false;
1302    let mut variadic_flag_active = false;
1303    let mut idx = 0;
1304    // Track whether we've already applied the default_subcommand to prevent
1305    // multiple switches (e.g., if default is "run" and there's a task named "run")
1306    let mut used_default_subcommand = false;
1307    // Whether the command in scope has had its own mounts run. A mount on the root
1308    // is the case that needs this: a subcommand's mounts are run when the parser
1309    // descends into it, but nothing descends into the root.
1310    let mut mounts_resolved = false;
1311    // A completion needs the whole command list before it can offer anything, and
1312    // `mycli <tab>` has no word to trigger discovery with — so waiting for one would
1313    // mean a root mount never contributed to the very thing it exists for.
1314    //
1315    // The default-subcommand gate applies here too, and has to: offering a discovered
1316    // command that a real parse would hand to the default instead would be worse than
1317    // not offering it. A root mount under a `default_subcommand` that does not say
1318    // `overrides_default` therefore contributes nothing anywhere, which is what
1319    // "the default outranks discovery" means.
1320    let default_outranks_mounts =
1321        spec.default_subcommand.is_some() && !out.cmd.mounts.iter().any(|m| m.overrides_default);
1322    if mount_timing == MountTiming::Eager && !default_outranks_mounts && !out.cmd.mounts.is_empty()
1323    {
1324        mounts_resolved = true;
1325        let mut mounted = out.cmd.clone();
1326        mounted.mount(&[], mount_outputs)?;
1327        merge_subcommand_flags(&mut out.available_flags, gather_flags(&mounted), false);
1328        if let Some(last) = out.cmds.last_mut() {
1329            *last = mounted.clone();
1330        }
1331        out.cmd = mounted;
1332    }
1333
1334    while idx < input.len() {
1335        // Only for a word that could name a command, and only when it matches
1336        // nothing already declared. A CLI that declares its commands and mounts more
1337        // does not spawn a process for every invocation, and a flag — `--help`, or
1338        // anything unrecognized — never triggers discovery at all, which it would
1339        // otherwise do simply by not being a subcommand.
1340        // A declared `default_subcommand` already says what an unmatched word means,
1341        // and it costs nothing — so discovery waits behind it unless a mount asks to
1342        // outrank it. Without this, a task runner would spawn its discovery process
1343        // once per task invocation.
1344        let default_catches_it = spec.default_subcommand.as_deref().is_some_and(|name| {
1345            default_accepts_word(&out.cmd, name, &input[idx].word)
1346                && !out.cmd.mounts.iter().any(|m| m.overrides_default)
1347        });
1348        if !mounts_resolved
1349            && !out.cmd.mounts.is_empty()
1350            && !default_catches_it
1351            && is_command_word(&input[idx].word)
1352            && !is_negative_number(&input[idx].word)
1353            && out.cmd.find_subcommand(&input[idx].word).is_none()
1354        {
1355            mounts_resolved = true;
1356            let mut mounted = out.cmd.clone();
1357            mounted.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1358            merge_subcommand_flags(&mut out.available_flags, gather_flags(&mounted), false);
1359            if let Some(last) = out.cmds.last_mut() {
1360                *last = mounted.clone();
1361            }
1362            out.cmd = mounted;
1363        }
1364        if variadic_flag_active
1365            && out.cmd.find_subcommand(&input[idx].word).is_some()
1366            && !out.cmd.subcommand_precedence_over_arg
1367        {
1368            break;
1369        }
1370        if let Some(subcommand) = out.cmd.find_subcommand(&input[idx].word) {
1371            if out.cmd.args_conflicts_with_subcommands && command_arg_found {
1372                bail!(
1373                    "subcommand '{}' cannot be used with arguments on its parent command",
1374                    input[idx].word
1375                );
1376            }
1377            let mut subcommand = subcommand.clone();
1378            // Pass prefix words (global flags before this subcommand) to mount
1379            subcommand.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1380            // Only the *boundary* is a mount crossing: below it, the mounted program's own
1381            // commands are ordinary commands relative to each other.
1382            let crossing_mount = subcommand.mounted && !out.cmd.mounted;
1383            merge_subcommand_flags(
1384                &mut out.available_flags,
1385                gather_flags(&subcommand),
1386                crossing_mount,
1387            );
1388            // Remove subcommand from input
1389            let selected = input.remove(idx);
1390            if let Some(selected) = selected {
1391                trace.record(
1392                    selected.argv,
1393                    TokenRole::Command {
1394                        name: subcommand.name.clone(),
1395                    },
1396                );
1397            }
1398            command_has_argv = idx < input.len();
1399            out.cmds.push(subcommand.clone());
1400            out.cmd = subcommand.clone();
1401            // A descent already ran the new command's mounts, above.
1402            mounts_resolved = true;
1403            prefix_flags.clear();
1404            command_arg_found = false;
1405            variadic_flag_active = false;
1406            // Continue from current position (don't reset to 0)
1407            // After remove(), idx now points to the next element
1408        } else if !is_command_word(&input[idx].word)
1409            || declared_numeric_short(&out.available_flags, &input[idx].word)
1410        {
1411            // Check if this is a known flag
1412            let word = input[idx].word.clone();
1413            let flag_key = get_flag_key(&word);
1414
1415            // A short token keys on its first letter, so `-az` would be recorded as
1416            // `-a` and its tail left over. Check the whole token here, where it is
1417            // first read: a token containing an unrecognized letter is not a bundle,
1418            // and recording it as one is what let `-a` be applied from a token that
1419            // never named it.
1420            let is_bundle = word.starts_with("--")
1421                || short_bundle_is_known(spec, &out.cmds, &out.available_flags, &word);
1422            if let Some(f) = out
1423                .available_flags
1424                .get(flag_key)
1425                .cloned()
1426                .filter(|_| is_bundle)
1427            {
1428                command_arg_found = true;
1429                variadic_flag_active = f.arg.as_ref().is_some_and(|arg| arg.var);
1430                // Skip the flag and keep scanning. Both global and non-global flags may precede
1431                // a subcommand (`mycli --verbose run task`, `mycli run --force task`), and
1432                // stopping at one would hide the subcommand — and any mount on it — from the
1433                // parse, leaving the subcommand name to be mis-read as a positional argument.
1434                //
1435                // Only globals are forwarded to mounts: a non-global flag belongs to the
1436                // command that declared it, not to what is mounted below it.
1437                input[idx].binding = Some((Arc::clone(&f), out.cmds.len() - 1));
1438                let mut forwarded = f.global.then(|| vec![word.clone()]);
1439                idx += 1;
1440
1441                // Only consume next word if flag takes an argument AND value isn't embedded
1442                // Example: "--dir foo" consumes "foo", but "--dir=foo" or "--verbose" do not
1443                if f.arg.is_some()
1444                    && !word.contains('=')
1445                    && idx < input.len()
1446                    && accepts_detached_flag_value(&f, &input[idx].word)
1447                {
1448                    if let Some(words) = forwarded.as_mut() {
1449                        words.push(input[idx].word.clone());
1450                    }
1451                    idx += 1;
1452                }
1453                if let Some(words) = forwarded {
1454                    apply_prefix_flag_overrides(&mut prefix_flags, Arc::clone(&f));
1455                    prefix_flags.push((f, words));
1456                }
1457            } else {
1458                // Unknown flag - stop looking for subcommands
1459                // Let the main parsing phase handle the error
1460                break;
1461            }
1462        } else {
1463            if variadic_flag_active && out.cmd.subcommand_precedence_over_arg {
1464                idx += 1;
1465                continue;
1466            }
1467            // Found a word that's not a flag or subcommand
1468            // Check if we should use the default_subcommand (only once, and only at the
1469            // root, which is the only place a spec can declare one — `out.cmds` holds just
1470            // the root until something descends). Without that second condition the one
1471            // declared name is looked up wherever the parser happens to be standing, so an
1472            // unrelated command acquires a default because a name matched one level down:
1473            // with `default_subcommand "ls"` at the top, `ex config zzz` descended into
1474            // `config ls`.
1475            if !used_default_subcommand && out.cmds.len() == 1 {
1476                if let Some(default_name) = &spec.default_subcommand {
1477                    if let Some(subcommand) = out
1478                        .cmd
1479                        .find_subcommand(default_name)
1480                        .filter(|_| default_accepts_word(&out.cmd, default_name, &input[idx].word))
1481                    {
1482                        if out.cmd.args_conflicts_with_subcommands && command_arg_found {
1483                            bail!(
1484                                "subcommand '{}' cannot be used with arguments on its parent command",
1485                                subcommand.name
1486                            );
1487                        }
1488                        let mut subcommand = subcommand.clone();
1489                        // Pass prefix words (global flags before this) to mount
1490                        subcommand.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1491                        let crossing_mount = subcommand.mounted && !out.cmd.mounted;
1492                        merge_subcommand_flags(
1493                            &mut out.available_flags,
1494                            gather_flags(&subcommand),
1495                            crossing_mount,
1496                        );
1497                        out.cmds.push(subcommand.clone());
1498                        out.cmd = subcommand.clone();
1499                        command_has_argv = true;
1500                        prefix_flags.clear();
1501                        command_arg_found = false;
1502                        variadic_flag_active = false;
1503                        // This descent ran the new command's mounts, so lazy
1504                        // discovery must not run them a second time.
1505                        mounts_resolved = true;
1506                        used_default_subcommand = true;
1507                        // Continue the loop to check if this word is a subcommand of the
1508                        // default subcommand (e.g., a task name added via mount).
1509                        // If it's not a subcommand, the next iteration will break and
1510                        // Phase 2 will handle it as a positional arg.
1511                        continue;
1512                    }
1513                }
1514            }
1515            // An unmatched word that names no subcommand is forwarded as an external
1516            // command: this word, then every token after it, including flags. Known
1517            // subcommands already won above, and a default_subcommand already caught.
1518            // clap's `allow_external_subcommands` is this, not `unknown_flags=value`.
1519            if out.cmd.external_subcommand {
1520                let rest: Vec<Token> = input.drain(idx..).collect();
1521                for token in &rest {
1522                    trace.record(token.argv, TokenRole::External);
1523                }
1524                out.external = Some(rest.into_iter().map(|t| t.word).collect());
1525                break;
1526            }
1527            // This could be a positional argument, so stop subcommand search
1528            break;
1529        }
1530    }
1531
1532    // Phase 2: Main argument and flag parsing
1533    //
1534    // Now that we've identified all subcommands and executed their mounts,
1535    // we can parse the remaining arguments, flags, and their values.
1536
1537    // The cursor into `out.cmd.args`, kept as an index rather than a reference because an
1538    // explicit `--` may jump it *past* arguments that stay empty (see the `w == "--"` arm).
1539    // With such a gap `out.args.len()` no longer equals the cursor, so anything asking "is this
1540    // argument filled?" has to consult `out.args` by key instead of counting.
1541    let mut next_arg_idx: usize = 0;
1542    let mut enable_flags = true;
1543    let mut grouped_flag = false;
1544    // Whether an explicit `--` has been consumed *as a separator* (as opposed to being kept as a
1545    // value by `double_dash="preserve"`). Args declared `double_dash="required"` only accept
1546    // words that come after it — see `report_double_dash_violation`.
1547    let mut seen_double_dash = false;
1548    // Args already reported as having been offered a word before the `--` they require, so a
1549    // variadic one does not report the same violation for every word it is offered.
1550    let mut double_dash_violations: HashSet<String> = HashSet::new();
1551    // Scalar occurrences are scoped to the command level where they were written. Inherited
1552    // globals may therefore appear once before and once after a subcommand under clap's strict
1553    // `args_override_self(false)` policy. The bitset also keeps both forms of a negatable flag:
1554    // opposite forms may override one another, while repeating either spelling is an error.
1555    let mut scalar_occurrences: HashMap<(usize, usize), u8> = HashMap::new();
1556
1557    while !input.is_empty() {
1558        let token = input.pop_front().unwrap();
1559        // The flag this word was read as in Phase 1, if it skipped it (see `Token::binding`).
1560        let binding = token.binding;
1561        let argv = token.argv;
1562        let mut w = token.word;
1563        // A short's attached value is re-queued with `grouped_flag` set, and that
1564        // continuation is not a following word. `require_equals` refuses only the
1565        // following word; `-i9229` and `-i=9229` still bind. `default_missing` binds
1566        // only when the value is actually missing, so `-cnever` is still `never`.
1567        let attached_continuation = grouped_flag;
1568
1569        // Check for restart_token - resets argument parsing for multiple command invocations
1570        // e.g., `mise run lint ::: test ::: check` with restart_token=":::"
1571        if let Some(ref restart_token) = out.cmd.restart_token {
1572            if w == *restart_token {
1573                // Reset argument parsing state for a fresh command invocation, keeping the
1574                // flags. `double_dash_violations` is deliberately *not* cleared: `out.errors`
1575                // is not cleared here either, so clearing it would let one arg report the same
1576                // violation once per invocation.
1577                out.args.clear();
1578                // With the values gone, so is where they came from — otherwise the second
1579                // invocation of `run lint ::: test` reports the first one's provenance. The
1580                // token trace is *not* cleared: those words were read, and a report that
1581                // dropped them would show a command line with a hole in it.
1582                out.arg_origins.clear();
1583                trace.record(argv, TokenRole::Restart);
1584                next_arg_idx = 0;
1585                out.flag_awaiting_value.clear(); // Clear any pending flag values
1586                enable_flags = true; // Reset -- separator effect
1587                seen_double_dash = false; // The next invocation needs its own `--`
1588                continue;
1589            }
1590        }
1591
1592        // A flag declared `allow_hyphen_values` takes the next token whatever it looks
1593        // like, and that has to be asked before the separator arm below rather than
1594        // after it. Asked after, a `--` was consumed as a separator while the flag
1595        // stayed hungry, and the flag then ate the word past it: `ex -a -- -x` bound
1596        // `-x` and the separator was simply gone. Asked here, the flag takes the `--`
1597        // itself, which is what clap does with the same declaration — and no flag can
1598        // still be waiting once the separator has done its job, so the starvation rule
1599        // below has no path around it.
1600        if enable_flags
1601            && !attached_continuation
1602            && w.starts_with('-')
1603            && out
1604                .flag_awaiting_value
1605                .last()
1606                .is_some_and(|flag| accepts_detached_flag_value(flag, &w))
1607        {
1608            // A variadic argument collects here too: which token supplied its first
1609            // value says nothing about how many it takes.
1610            let should_return = bind_pending_flag_value(
1611                spec,
1612                &out.cmd,
1613                &mut out.errors,
1614                &mut out.flags,
1615                &mut out.flag_awaiting_value,
1616                &mut w,
1617                &mut input,
1618                custom_env,
1619                trace,
1620                argv,
1621                // The token a hyphen-valued flag takes is the following word, never attached.
1622                false,
1623            )?;
1624            if should_return {
1625                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1626                return Ok((out, overridden_flags));
1627            }
1628            continue;
1629        }
1630
1631        // A flag whose value may be omitted that cannot take this token as a detached
1632        // value finishes bare and leaves the token for whatever comes next:
1633        // `--color --verbose` colours with the missing value and still sets verbose,
1634        // and `--inspect 9229` with `require_equals` binds the missing value rather
1635        // than treating 9229 as the port.
1636        if enable_flags
1637            && !attached_continuation
1638            && !out.flag_awaiting_value.is_empty()
1639            && out.flag_awaiting_value.last().is_some_and(|flag| {
1640                (flag.default_missing.is_some() || flag.value_optional)
1641                    && !accepts_detached_flag_value(flag, &w)
1642            })
1643        {
1644            try_bind_default_missing(
1645                &mut out.flags,
1646                &mut out.flag_awaiting_value,
1647                custom_env,
1648                &mut out.flag_origins,
1649            )?;
1650        }
1651
1652        // The first explicit `--` is still a separator after an `automatic` argument has
1653        // stopped flag parsing. Once an explicit separator has done its job, a second one is
1654        // an ordinary value: every parser worth comparing against keeps it (POSIX getopt,
1655        // argparse, clap, commander, yargs), and jdx/usage#229 was a user reporting the old
1656        // behavior as the bug it is.
1657        if w == "--" && !seen_double_dash {
1658            enable_flags = false;
1659
1660            // Only preserve the double dash token if we're collecting values for a variadic arg
1661            // in double_dash == `preserve` mode
1662            let should_preserve = out
1663                .cmd
1664                .args
1665                .get(next_arg_idx)
1666                .map(|arg| arg.var && arg.double_dash == SpecDoubleDashChoices::Preserve)
1667                .unwrap_or(false);
1668
1669            if should_preserve {
1670                // Fall through to arg parsing. This `--` is a *value*, not a separator, so it
1671                // neither counts as one nor unlocks a `double_dash="required"` arg.
1672            } else {
1673                seen_double_dash = true;
1674                trace.record(argv, TokenRole::Separator);
1675
1676                // Everything after an explicit `--` belongs to the arg that requires one, so
1677                // jump the cursor there — past any earlier arg, including a greedy variadic
1678                // that would otherwise swallow the rest. This mirrors clap's `Arg::last(true)`,
1679                // which is what `double_dash="required"` is generated from. Specs without such
1680                // an arg find nothing and keep the cursor where it was.
1681                let target = out.cmd.args.iter().position(|arg| {
1682                    arg.double_dash == SpecDoubleDashChoices::Required
1683                        && !out.args.contains_key(arg)
1684                });
1685                if let Some(target) = target {
1686                    // Forward only. An unfilled required arg declared *before* the cursor is
1687                    // left where it is rather than rewound to — words already assigned to
1688                    // later args would have to be taken back for that to mean anything, and
1689                    // the arg keeps its `MissingArg`. `double_dash="required"` mirrors clap's
1690                    // `Arg::last(true)`, which is the final positional, so a spec that puts
1691                    // one ahead of others is already outside what this models.
1692                    if target > next_arg_idx {
1693                        next_arg_idx = target;
1694                    }
1695                }
1696                continue;
1697            }
1698        }
1699
1700        // long flags
1701        if enable_flags && w.starts_with("--") {
1702            grouped_flag = false;
1703            // `Some` only when an `=` was actually written, so `--jobs=` can supply
1704            // an empty value while `--jobs` supplies none. Collapsing the two lost
1705            // the flag entirely.
1706            let split = w.split_once('=');
1707            let word = split.map(|(word, _)| word).unwrap_or(&w);
1708            let bound_flag = binding.as_ref().map(|(flag, _)| flag);
1709            if let Some(f) = bound_flag.or_else(|| out.available_flags.get(word)) {
1710                let command_level = binding
1711                    .as_ref()
1712                    .map(|(_, level)| *level)
1713                    .unwrap_or(out.cmds.len() - 1);
1714                parsed_flag_spellings
1715                    .entry(Arc::as_ptr(f) as usize)
1716                    .or_default()
1717                    .insert(word.to_string());
1718                // Recorded before the action check below: a token that named a flag named it
1719                // whether or not the parse can carry on afterwards.
1720                trace.record(
1721                    argv,
1722                    TokenRole::Flag {
1723                        flag: Arc::clone(f),
1724                        spelling: word.to_string(),
1725                        negated: f.negate.as_deref() == Some(word),
1726                    },
1727                );
1728                if f.action != crate::SpecFlagAction::Set {
1729                    out.errors.push(render_action_err(spec, &out.cmd, f, word));
1730                    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1731                    return Ok((out, overridden_flags));
1732                }
1733                apply_flag_overrides(
1734                    f,
1735                    &out.available_flags,
1736                    &mut out.flags,
1737                    &mut out.flag_awaiting_value,
1738                    &mut overridden_flags,
1739                    &mut out.overridden_flags,
1740                );
1741                if let Some(pending) = out.flag_awaiting_value.first() {
1742                    out.errors.push(render_missing_flag_value(pending, &w));
1743                    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1744                    return Ok((out, overridden_flags));
1745                }
1746                // An attached value only means something to a flag that takes one:
1747                // `--jobs=` is an empty string, while `--force=yes` has nothing to
1748                // give a flag that holds no value. Handing that leftover to the
1749                // positionals would re-split one token into two, so `ex --force=yes`
1750                // would fill an argument the caller never typed a word for.
1751                if f.arg.is_some() {
1752                    record_scalar_flag_occurrence(
1753                        &out.cmds,
1754                        f,
1755                        command_level,
1756                        None,
1757                        &mut scalar_occurrences,
1758                        &mut out.errors,
1759                    );
1760                    let f = Arc::clone(f);
1761                    out.flag_awaiting_value.push(Arc::clone(&f));
1762                    // The `=` has already settled that this text is the value, so it
1763                    // binds here rather than going back on the queue to be read as a
1764                    // token again — where `--jobs=--force` looked like a flag of its
1765                    // own and bound `force`, leaving `jobs` unset.
1766                    if let Some((_, val)) = split {
1767                        // The `=` settles where the *first* value came from and nothing
1768                        // more, so a variadic argument goes on collecting from the words
1769                        // after it exactly as the detached form does.
1770                        let mut val = val.to_string();
1771                        let should_return = bind_pending_flag_value(
1772                            spec,
1773                            &out.cmd,
1774                            &mut out.errors,
1775                            &mut out.flags,
1776                            &mut out.flag_awaiting_value,
1777                            &mut val,
1778                            &mut input,
1779                            custom_env,
1780                            trace,
1781                            argv,
1782                            // The `=` settled that this text is the value, so it rode in on
1783                            // the flag's own token.
1784                            true,
1785                        )?;
1786                        if should_return {
1787                            record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1788                            return Ok((out, overridden_flags));
1789                        }
1790                    }
1791                } else if f.count {
1792                    let arr = out
1793                        .flags
1794                        .entry(Arc::clone(f))
1795                        .or_insert_with(|| ParseValue::MultiBool(vec![]))
1796                        .try_as_multi_bool_mut()
1797                        .unwrap();
1798                    arr.push(true);
1799                } else {
1800                    let negate = f.negate.clone().unwrap_or_default();
1801                    let negated_form = word == negate;
1802                    let value = if f.bool_value {
1803                        match split.map(|(_, value)| value) {
1804                            Some("true") => !negated_form,
1805                            Some("false") => negated_form,
1806                            Some(value) => {
1807                                out.errors.push(UsageErr::InvalidValue {
1808                                    name: f.name.clone(),
1809                                    value: value.to_string(),
1810                                    reason: "expected `true` or `false`".to_string(),
1811                                });
1812                                continue;
1813                            }
1814                            None => !negated_form,
1815                        }
1816                    } else {
1817                        !negated_form
1818                    };
1819                    // Which form was typed is a question about the name, so it is
1820                    // asked of `word` rather than the whole token: the attached value
1821                    // is dropped just above, and comparing `--no-color=yes` against
1822                    // `--no-color` would take the negation down with it.
1823                    record_scalar_flag_occurrence(
1824                        &out.cmds,
1825                        f,
1826                        command_level,
1827                        Some(!negated_form),
1828                        &mut scalar_occurrences,
1829                        &mut out.errors,
1830                    );
1831                    out.flags.insert(Arc::clone(f), ParseValue::Bool(value));
1832                }
1833                continue;
1834            }
1835            if is_help_arg(spec, &out.cmd, &w) {
1836                out.errors
1837                    .push(render_help_err(spec, &out.cmd, w.len() > 2));
1838                trace.record(
1839                    argv,
1840                    TokenRole::Builtin {
1841                        spelling: w.clone(),
1842                    },
1843                );
1844                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1845                return Ok((out, overridden_flags));
1846            }
1847            if is_version_arg(spec, &out.cmds, &w) {
1848                out.errors.push(render_version_err(spec, w.len() > 2));
1849                trace.record(
1850                    argv,
1851                    TokenRole::Builtin {
1852                        spelling: w.clone(),
1853                    },
1854                );
1855                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1856                return Ok((out, overridden_flags));
1857            }
1858            if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
1859                trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
1860                trace.close(&input);
1861                return Err(refused.into());
1862            }
1863        }
1864
1865        // short flags
1866        //
1867        // A fresh token is checked whole before any of it is applied: `-az` with only
1868        // `-a` declared is not a bundle at all, so it must not set `a` on the way to
1869        // discovering that `z` names nothing. A grouped continuation is exempt — its
1870        // token was already checked when it arrived.
1871        let declared_numeric_short = declared_numeric_short(&out.available_flags, &w);
1872        let positional_negative_number = !declared_numeric_short
1873            && is_negative_number(&w)
1874            && out
1875                .cmd
1876                .args
1877                .get(next_arg_idx)
1878                .is_some_and(|arg| arg.allow_negative_numbers);
1879        if enable_flags
1880            && !grouped_flag
1881            // A word phase 1 already resolved to a flag needs no re-checking, and
1882            // the flags in scope have changed since, so re-checking would be wrong.
1883            && binding.is_none()
1884            && w.starts_with('-')
1885            && w.len() > 1
1886            && is_flag_like(&w)
1887            && !positional_negative_number
1888            && !short_bundle_is_known(spec, &out.cmds, &out.available_flags, &w)
1889        {
1890            // Refused if this command asked for that; otherwise it carries on below
1891            // as one word, with none of its letters applied.
1892            if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
1893                trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
1894                trace.close(&input);
1895                return Err(refused.into());
1896            }
1897        } else if enable_flags && !positional_negative_number && w.starts_with('-') && w.len() > 1 {
1898            let short = w.chars().nth(1).unwrap();
1899            if let Some(f) = binding
1900                .as_ref()
1901                .map(|(flag, _)| flag)
1902                .or_else(|| out.available_flags.get(&format!("-{short}")))
1903            {
1904                let command_level = binding
1905                    .as_ref()
1906                    .map(|(_, level)| *level)
1907                    .unwrap_or(out.cmds.len() - 1);
1908                if f.action != crate::SpecFlagAction::Set {
1909                    out.errors
1910                        .push(render_action_err(spec, &out.cmd, f, &format!("-{short}")));
1911                    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1912                    return Ok((out, overridden_flags));
1913                }
1914                parsed_flag_spellings
1915                    .entry(Arc::as_ptr(f) as usize)
1916                    .or_default()
1917                    .insert(format!("-{short}"));
1918                trace.record(
1919                    argv,
1920                    TokenRole::Flag {
1921                        flag: Arc::clone(f),
1922                        spelling: format!("-{short}"),
1923                        // A short spelling is never the negated form: `negate` is a long.
1924                        negated: false,
1925                    },
1926                );
1927                apply_flag_overrides(
1928                    f,
1929                    &out.available_flags,
1930                    &mut out.flags,
1931                    &mut out.flag_awaiting_value,
1932                    &mut overridden_flags,
1933                    &mut out.overridden_flags,
1934                );
1935                if !attached_continuation {
1936                    if let Some(pending) = out.flag_awaiting_value.first() {
1937                        out.errors.push(render_missing_flag_value(pending, &w));
1938                        record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1939                        return Ok((out, overridden_flags));
1940                    }
1941                }
1942                let rest = &w[1 + short.len_utf8()..];
1943                if !rest.is_empty() {
1944                    // `-abc` is one token that names three flags, so the tail is read at the
1945                    // bundle's own position rather than at one of its own.
1946                    input.push_front(Token::new(format!("-{rest}"), argv));
1947                }
1948                // A fully consumed short is no longer a grouped continuation.
1949                // Leaving this set after `-ai` made `-i` skip `require_equals`
1950                // and bind the following word.
1951                grouped_flag = !rest.is_empty();
1952                if f.arg.is_some() {
1953                    record_scalar_flag_occurrence(
1954                        &out.cmds,
1955                        f,
1956                        command_level,
1957                        None,
1958                        &mut scalar_occurrences,
1959                        &mut out.errors,
1960                    );
1961                    out.flag_awaiting_value.push(Arc::clone(f));
1962                } else if f.count {
1963                    let arr = out
1964                        .flags
1965                        .entry(Arc::clone(f))
1966                        .or_insert_with(|| ParseValue::MultiBool(vec![]))
1967                        .try_as_multi_bool_mut()
1968                        .unwrap();
1969                    arr.push(true);
1970                } else {
1971                    let negate = f.negate.clone().unwrap_or_default();
1972                    let value = w != negate;
1973                    record_scalar_flag_occurrence(
1974                        &out.cmds,
1975                        f,
1976                        command_level,
1977                        Some(value),
1978                        &mut scalar_occurrences,
1979                        &mut out.errors,
1980                    );
1981                    out.flags.insert(Arc::clone(f), ParseValue::Bool(value));
1982                }
1983                continue;
1984            }
1985            // The letter nothing declared may still be one the parser supplies, and it may
1986            // sit anywhere in the token: `-hv` asks for help as surely as `-vh` does, and
1987            // neither reaches the whole-token spellings below.
1988            if let Some(err) = supplied_short(spec, &out.cmds, short) {
1989                out.errors.push(err);
1990                trace.record(
1991                    argv,
1992                    TokenRole::Builtin {
1993                        spelling: format!("-{short}"),
1994                    },
1995                );
1996                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1997                return Ok((out, overridden_flags));
1998            }
1999            if is_help_arg(spec, &out.cmd, &w) {
2000                out.errors
2001                    .push(render_help_err(spec, &out.cmd, w.len() > 2));
2002                trace.record(
2003                    argv,
2004                    TokenRole::Builtin {
2005                        spelling: w.clone(),
2006                    },
2007                );
2008                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2009                return Ok((out, overridden_flags));
2010            }
2011            if is_version_arg(spec, &out.cmds, &w) {
2012                out.errors.push(render_version_err(spec, w.len() > 2));
2013                trace.record(
2014                    argv,
2015                    TokenRole::Builtin {
2016                        spelling: w.clone(),
2017                    },
2018                );
2019                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2020                return Ok((out, overridden_flags));
2021            }
2022            if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
2023                trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
2024                trace.close(&input);
2025                return Err(refused.into());
2026            }
2027            if grouped_flag {
2028                grouped_flag = false;
2029                w.remove(0);
2030                // What is left is a short flag's attached value, and one `=` between
2031                // the letter and the value is a separator: `-j=8` means 8. Only one,
2032                // so `-j==8` still means `=8`.
2033                if !out.flag_awaiting_value.is_empty() && w.starts_with('=') {
2034                    w.remove(0);
2035                }
2036            }
2037        }
2038
2039        // Only while flags are still being read. A flag still waiting when the separator
2040        // was consumed is starved: its value would have to come from after the `--`,
2041        // where every token is data. Draining there gave `ex --jobs -- x` the word after
2042        // the separator, so the command line quietly meant `ex --jobs=x` and the `--`
2043        // was gone. Left waiting, it is reported as the missing value it is.
2044        // `require_equals` refuses a detached value: `--flag value` is a missing
2045        // value, not a flag of `"value"`. The attached form is still bound above.
2046        // Reported here rather than left waiting until the end of the line: falling
2047        // through would offer `value` to the positionals and call it an unexpected
2048        // word, which is the wrong error and a different one from usage-argv.
2049        if enable_flags
2050            && !attached_continuation
2051            && !out.flag_awaiting_value.is_empty()
2052            && out
2053                .flag_awaiting_value
2054                .last()
2055                .is_some_and(|flag| flag.require_equals)
2056        {
2057            let flag = out.flag_awaiting_value.last().unwrap();
2058            let token = flag
2059                .long
2060                .first()
2061                .map(|l| format!("--{l}"))
2062                .or_else(|| flag.short.first().map(|s| format!("-{s}")))
2063                .unwrap_or_else(|| flag.name.clone());
2064            out.errors.push(UsageErr::InvalidFlag {
2065                token: token.clone(),
2066                reason: "requires an argument".to_string(),
2067                span: (0, 0).into(),
2068                input: format!("{token} {w}"),
2069            });
2070            record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2071            return Ok((out, overridden_flags));
2072        }
2073        if enable_flags
2074            && !out.flag_awaiting_value.is_empty()
2075            && (attached_continuation
2076                || out
2077                    .flag_awaiting_value
2078                    .last()
2079                    .is_some_and(|flag| accepts_detached_flag_value(flag, &w)))
2080        {
2081            // Held before the drain pops it: a flag whose argument is variadic keeps
2082            // taking values after this first one.
2083            let should_return = bind_pending_flag_value(
2084                spec,
2085                &out.cmd,
2086                &mut out.errors,
2087                &mut out.flags,
2088                &mut out.flag_awaiting_value,
2089                &mut w,
2090                &mut input,
2091                custom_env,
2092                trace,
2093                argv,
2094                attached_continuation,
2095            )?;
2096            if should_return {
2097                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2098                return Ok((out, overridden_flags));
2099            }
2100            continue;
2101        }
2102
2103        if out.cmd.allow_missing_positional {
2104            while let Some(current) = out.cmd.args.get(next_arg_idx) {
2105                if current.required || out.args.contains_key(current) {
2106                    break;
2107                }
2108                let required_after = out.cmd.args[next_arg_idx + 1..]
2109                    .iter()
2110                    .filter(|arg| arg.required)
2111                    .count();
2112                if required_after == 0 {
2113                    break;
2114                }
2115                let remaining_values = 1 + input
2116                    .iter()
2117                    .filter(|token| !enable_flags || !is_flag_like(&token.word))
2118                    .count();
2119                if remaining_values > required_after {
2120                    break;
2121                }
2122                next_arg_idx += 1;
2123            }
2124        }
2125
2126        if let Some(arg) = out.cmd.args.get(next_arg_idx) {
2127            if arg.var
2128                && out.args.contains_key(arg)
2129                && arg.value_terminator.as_deref() == Some(w.as_str())
2130            {
2131                trace.record(
2132                    argv,
2133                    TokenRole::ValueTerminator {
2134                        ends: arg.name.clone(),
2135                    },
2136                );
2137                next_arg_idx += 1;
2138                continue;
2139            }
2140            // Before anything else: an arg that requires `--` accepts nothing until one has been
2141            // seen. Checking ahead of `validate_choices` keeps a discarded word from also being
2142            // reported as an invalid choice, and from reaching that function's help escape.
2143            if arg.double_dash == SpecDoubleDashChoices::Required && !seen_double_dash {
2144                report_double_dash_violation(arg, &mut out.errors, &mut double_dash_violations);
2145                trace.record(
2146                    argv,
2147                    TokenRole::Refused {
2148                        reason: format!("{} only accepts words after `--`", arg.name),
2149                    },
2150                );
2151                // Drop the word without filling the arg or advancing the cursor: every later
2152                // word hits the same arg and is rejected the same way, so the parse still ends
2153                // in an error rather than in `unexpected word`.
2154                continue;
2155            }
2156            // Split before judging, as the flag path does: after the split the word is
2157            // no longer one value, and `choices` has to be asked about each. Judging
2158            // first rejects `src:docs` against a list that both halves are on, and
2159            // names the whole word rather than the half that was wrong.
2160            let trailing_value =
2161                seen_double_dash || arg.double_dash == SpecDoubleDashChoices::Automatic;
2162            let suppress_trailing_delimiter =
2163                out.cmds.iter().any(|cmd| cmd.dont_delimit_trailing_values);
2164            let delimiter = if suppress_trailing_delimiter && trailing_value {
2165                None
2166            } else {
2167                arg.delimiter
2168            };
2169            let parts: Vec<String> = match delimiter {
2170                Some(delimiter) => w.split(delimiter).map(str::to_string).collect(),
2171                None => vec![w.clone()],
2172            };
2173            let mut refused = false;
2174            for part in &parts {
2175                if validate_choices(
2176                    spec,
2177                    &out.cmd,
2178                    &mut out.errors,
2179                    ChoiceTarget::arg(arg),
2180                    part,
2181                    arg.choices.as_ref(),
2182                    custom_env,
2183                )? {
2184                    refused = true;
2185                    break;
2186                }
2187            }
2188            if refused {
2189                record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2190                return Ok((out, overridden_flags));
2191            }
2192            // `double_dash="automatic"` means the first value this arg takes is the last
2193            // token read as anything but data: a wrapper declaring it can forward flags
2194            // without its caller typing a `--`. Set before the value is stored, so the
2195            // rest of the command line is already past flag parsing.
2196            if arg.double_dash == SpecDoubleDashChoices::Automatic {
2197                enable_flags = false;
2198            }
2199            // A flag-like word reaching a positional while flags are still being read was
2200            // offered to every declaration and matched none: under the default
2201            // `unknown_flags="value"` it becomes data, and saying so is the difference
2202            // between "you have a typo" and "this argument took your typo".
2203            let unknown_flag = enable_flags
2204                && !positional_negative_number
2205                && is_flag_like(&w)
2206                && binding.is_none();
2207            trace.record(
2208                argv,
2209                if unknown_flag {
2210                    TokenRole::UnknownFlag {
2211                        bound_as: Some(Arc::new(arg.clone())),
2212                    }
2213                } else {
2214                    TokenRole::Arg {
2215                        arg: Arc::new(arg.clone()),
2216                        values: parts.clone(),
2217                    }
2218                },
2219            );
2220            if arg.var {
2221                let arr = out
2222                    .args
2223                    .entry(Arc::new(arg.clone()))
2224                    .or_insert_with(|| ParseValue::MultiString(vec![]))
2225                    .try_as_multi_string_mut()
2226                    .unwrap();
2227                // The values this word carried, split above so that everything
2228                // downstream — `choices`, `var_max` stopping the collection, `var_min` —
2229                // counts the values the user meant rather than the words they typed.
2230                arr.extend(parts.iter().cloned());
2231                if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
2232                    next_arg_idx += 1;
2233                }
2234            } else {
2235                out.args
2236                    .insert(Arc::new(arg.clone()), ParseValue::String(w));
2237                next_arg_idx += 1;
2238            }
2239            continue;
2240        }
2241        if is_help_arg(spec, &out.cmd, &w) {
2242            out.errors
2243                .push(render_help_err(spec, &out.cmd, w.len() > 2));
2244            trace.record(
2245                argv,
2246                TokenRole::Builtin {
2247                    spelling: w.clone(),
2248                },
2249            );
2250            record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2251            return Ok((out, overridden_flags));
2252        }
2253        if is_version_arg(spec, &out.cmds, &w) {
2254            out.errors.push(render_version_err(spec, w.len() > 2));
2255            trace.record(
2256                argv,
2257                TokenRole::Builtin {
2258                    spelling: w.clone(),
2259                },
2260            );
2261            record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2262            return Ok((out, overridden_flags));
2263        }
2264        trace.record(
2265            argv,
2266            TokenRole::Refused {
2267                reason: "no declaration takes this word".to_string(),
2268            },
2269        );
2270        trace.close(&input);
2271        bail!("unexpected word: {w}");
2272    }
2273
2274    record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2275
2276    // `out.flags` is keyed by `SpecFlag`, whose equality is intentionally name-only. Two
2277    // declarations with the same canonical name therefore share one public value entry even
2278    // when both were typed. The spelling ledger is keyed by declaration identity and retains
2279    // both, which is what exclusivity needs.
2280    let flag_was_parsed =
2281        |flag: &Arc<SpecFlag>| parsed_flag_spellings.contains_key(&(Arc::as_ptr(flag) as usize));
2282
2283    // The spellings the selected command's own declaration speaks for, on this object.
2284    //
2285    // Empty unless that declaration really is this object's: a parent and child may each
2286    // declare `--clean` without merging, leaving two flags that share a name, and the
2287    // ancestor's must not be read as the child's. The test is whether every spelling the child
2288    // declared resolves back here — true of a merged flag, and of a plain local one, but not of
2289    // an ancestor whose long form the child took over.
2290    let child_spellings = |flag: &Arc<SpecFlag>| -> HashSet<String> {
2291        let declared: HashSet<String> = out
2292            .cmd
2293            .flags
2294            .iter()
2295            .filter(|declared| declared.name == flag.name)
2296            .flat_map(flag_keys)
2297            .collect();
2298        // *Any* of them, not all. All was too strong: a child may declare a spelling that
2299        // some other inherited global already owns — `-c --clean` beside an inherited
2300        // `-c --config` — and that collision is resolved in the other global's favor, so the
2301        // child's `-c` resolves elsewhere. Requiring every spelling to land here let one
2302        // unrelated collision disown the child from the `--clean` it plainly does own.
2303        //
2304        // Still enough to tell the two-object case apart, which is what this guards: when a
2305        // child re-declares a global as global, the child's own spellings resolve to the
2306        // child's separate flag, so none of them lands on the ancestor's.
2307        let speaks_for_this_flag = declared.iter().any(|spelling| {
2308            out.available_flags
2309                .get(spelling)
2310                .is_some_and(|available| Arc::ptr_eq(available, flag))
2311        });
2312        if speaks_for_this_flag {
2313            declared
2314        } else {
2315            HashSet::new()
2316        }
2317    };
2318
2319    // Whose `exclusive` an occurrence activates, as `(the child's, an ancestor's)`.
2320    //
2321    // A child that re-declares an inherited global merges into one object answering to two
2322    // alias sets whose declarations may disagree, so there is no single owner to name: the
2323    // child owns the spellings it declared and the ancestor keeps the ones only it declared.
2324    // Both sides can be in play at once — `run -c --clean` is the ancestor's alias and the
2325    // child's in one invocation — and each carries its own declaration's answer.
2326    let exclusivity_in_play = |flag: &Arc<SpecFlag>| -> (bool, bool) {
2327        let child = child_spellings(flag);
2328        let child_exclusive = !child.is_empty()
2329            && out
2330                .cmd
2331                .flags
2332                .iter()
2333                .any(|declared| declared.name == flag.name && declared.exclusive);
2334        match parsed_flag_spellings.get(&(Arc::as_ptr(flag) as usize)) {
2335            Some(spellings) => (
2336                child_exclusive && spellings.iter().any(|s| child.contains(s)),
2337                flag.exclusive && spellings.iter().any(|s| !child.contains(s)),
2338            ),
2339            // An environment value has no spelling to attribute it by. The declaration the
2340            // selected command has in scope is the one that answers — which is the child's
2341            // when it re-declared the flag, and the ancestor's when it did not.
2342            None => (child_exclusive, flag.exclusive && child.is_empty()),
2343        }
2344    };
2345
2346    let exclusive_occurrence = |flag: &Arc<SpecFlag>| {
2347        let (child, ancestor) = exclusivity_in_play(flag);
2348        child || ancestor
2349    };
2350
2351    // clap's `exclusive` is also an escape from requiredness: `--version` has to work on a
2352    // command that otherwise needs an input. Companions are still diagnosed below, but an
2353    // exclusive occurrence suppresses the missing-value checks that would make it unusable
2354    // whether it was alone or not.
2355    let exclusive_present =
2356        unique_flags(out.available_flags.values().chain(out.flags.keys())).any(|flag| {
2357            exclusive_occurrence(flag)
2358                && !overridden_flags.contains(&flag.name)
2359                && (flag_was_parsed(flag) || flag_has_env(flag, custom_env))
2360        });
2361    let requirements_apply = |command_index: usize| {
2362        command_index + 1 == out.cmds.len() || !out.cmds[command_index].subcommand_negates_reqs
2363    };
2364
2365    if out.cmd.arg_required_else_help && !command_has_argv {
2366        out.errors.push(render_help_err(spec, &out.cmd, false));
2367    }
2368
2369    // A command that says it needs a subcommand, given none. Checked on `out.cmd` and nowhere
2370    // else, because `out.cmd` *is* the command the words reached: had a subcommand been taken,
2371    // the child would be here instead. The spec has carried `subcommand_required` since it was
2372    // added for the derive, and this parser never read it — so `mise generate` parsed as a
2373    // complete invocation while usage-argv and clap both refused it.
2374    if out.cmd.subcommand_required && !out.cmd.subcommands.is_empty() && out.external.is_none() {
2375        let mut names: Vec<&str> = out
2376            .cmd
2377            .subcommands
2378            .iter()
2379            // Aliases share a map entry with the name they point at; listing both would offer
2380            // the same command twice under two spellings.
2381            .filter(|(name, sub)| sub.name == **name && !sub.hide)
2382            .map(|(name, _)| name.as_str())
2383            .collect();
2384        names.sort_unstable();
2385        out.errors.push(UsageErr::MissingSubcommand(
2386            out.cmd.name.clone(),
2387            names.join(", "),
2388        ));
2389    }
2390
2391    // Not `skip(out.args.len())`: a `--` may have jumped the cursor past an arg that stayed
2392    // empty, so position and fill count can disagree. Ask `out.args` which args it holds.
2393    if !exclusive_present {
2394        for arg in out
2395            .cmds
2396            .iter()
2397            .enumerate()
2398            .filter(|(index, _)| requirements_apply(*index))
2399            .flat_map(|(_, cmd)| &cmd.args)
2400        {
2401            if out.args.contains_key(arg) {
2402                continue;
2403            }
2404            // Already reported as needing a `--`; one mistake should not yield two messages.
2405            if double_dash_violations.contains(&arg.name) {
2406                continue;
2407            }
2408            let required_if = arg.required_if.iter().any(|selector| {
2409                selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2410            });
2411            let required_if_eq = arg.required_if_eq.iter().any(|condition| {
2412                selector_explicit_has_value(
2413                    &condition.selector,
2414                    &condition.value,
2415                    &out,
2416                    &overridden_flags,
2417                    custom_env,
2418                )
2419            });
2420            let required_if_eq_all = !arg.required_if_eq_all.is_empty()
2421                && arg.required_if_eq_all.iter().all(|condition| {
2422                    selector_explicit_has_value(
2423                        &condition.selector,
2424                        &condition.value,
2425                        &out,
2426                        &overridden_flags,
2427                        custom_env,
2428                    )
2429                });
2430            let unless_any = arg.required_unless.iter().any(|selector| {
2431                selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2432            });
2433            let unless_all = !arg.required_unless_all.is_empty()
2434                && arg.required_unless_all.iter().all(|selector| {
2435                    selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2436                });
2437            let required_unless = !(unless_any
2438                || unless_all
2439                || (arg.required_unless.is_empty() && arg.required_unless_all.is_empty()));
2440            if (arg.required
2441                || required_if
2442                || required_if_eq
2443                || required_if_eq_all
2444                || required_unless)
2445                && arg.default.is_empty()
2446            {
2447                // Check if there's an env var available (custom env map takes precedence)
2448                let has_env = arg
2449                    .env
2450                    .as_ref()
2451                    .is_some_and(|env_var| env_contains(custom_env, env_var));
2452                if !has_env {
2453                    out.errors.push(UsageErr::MissingArg(arg.name.clone()));
2454                }
2455            }
2456        }
2457    }
2458
2459    // Conflicts are a question about the invocation as a whole rather than about any one
2460    // token, so they are checked here beside the requirement checks rather than at the
2461    // point a flag is matched — the flag it conflicts with may still be ahead of it.
2462    // Its own loop: the requirement loop below skips the flags that *were* given, which
2463    // is exactly the set this needs.
2464    //
2465    // A value from the environment counts on both sides, matching what
2466    // `selector_is_explicit` says about the other flag: the question is whether a flag
2467    // has a value, not how it got one. That is what clap does, and an asymmetric rule
2468    // would make the same pair of flags a conflict or not depending on which one
2469    // happened to be typed.
2470    for flag in unique_flags(out.available_flags.values()) {
2471        let given = out.flags.contains_key(flag) || flag_has_env(flag, custom_env);
2472        if !given || overridden_flags.contains(&flag.name) {
2473            continue;
2474        }
2475        for other in &flag.conflicts {
2476            if selector_is_explicit(other, &out, &overridden_flags, custom_env) {
2477                out.errors.push(UsageErr::InvalidFlag {
2478                    token: format!("--{}", flag.name),
2479                    reason: format!("conflicts with {other}"),
2480                    span: (0, 0).into(),
2481                    input: format!("--{} {other}", flag.name),
2482                });
2483            }
2484        }
2485        // The positive form, checked in the same pass and under the same rule: a value
2486        // from the environment satisfies a requirement, because the question is whether
2487        // the other flag has a value rather than how it got one. A flag that was
2488        // overridden away has not been given, so it cannot satisfy anything either —
2489        // which is what `selector_is_explicit` already accounts for.
2490        //
2491        // Reported as the missing flag rather than as something wrong with the flag that
2492        // named it, which is what clap says too: an unmet `requires` is a required
2493        // argument that was not provided. Named by its own name, resolved through the
2494        // same matcher, so a `requires="-f"` reports `--force` rather than the selector.
2495        let owner = out
2496            .cmds
2497            .iter()
2498            .rposition(|cmd| cmd.flags.iter().any(|declared| declared.name == flag.name))
2499            .unwrap_or(out.cmds.len() - 1);
2500        if !exclusive_present && requirements_apply(owner) {
2501            for other in &flag.requires {
2502                if !selector_is_satisfied(other, &out, &overridden_flags, custom_env) {
2503                    let name = selector_flag_name(other, &out).unwrap_or_else(|| other.clone());
2504                    if other.starts_with('-') {
2505                        out.errors.push(UsageErr::MissingFlag(name));
2506                    } else {
2507                        out.errors.push(UsageErr::MissingArg(name));
2508                    }
2509                }
2510            }
2511            for condition in &flag.requires_if {
2512                if explicit_flag_has_value(flag, &condition.value, &out, custom_env)
2513                    && !selector_is_satisfied(
2514                        &condition.requires,
2515                        &out,
2516                        &overridden_flags,
2517                        custom_env,
2518                    )
2519                {
2520                    let name = selector_flag_name(&condition.requires, &out)
2521                        .unwrap_or_else(|| condition.requires.clone());
2522                    out.errors.push(UsageErr::MissingFlag(name));
2523                }
2524            }
2525        }
2526    }
2527
2528    // Positionals can declare the same pairwise conflict as flags. Their selector is
2529    // the bare argument name, while a flag keeps its dashed spelling.
2530    for (command_index, arg) in out
2531        .cmds
2532        .iter()
2533        .enumerate()
2534        .flat_map(|(index, cmd)| cmd.args.iter().map(move |arg| (index, arg)))
2535    {
2536        let given = arg_is_explicit(arg, &out, custom_env);
2537        if !given {
2538            continue;
2539        }
2540        for other in &arg.conflicts {
2541            if selector_is_explicit(other, &out, &overridden_flags, custom_env) {
2542                out.errors.push(UsageErr::InvalidFlag {
2543                    token: arg.name.clone(),
2544                    reason: format!("conflicts with {other}"),
2545                    span: (0, 0).into(),
2546                    input: format!("{} {other}", arg.name),
2547                });
2548            }
2549        }
2550        if !exclusive_present && requirements_apply(command_index) {
2551            for other in &arg.requires {
2552                if !selector_is_satisfied(other, &out, &overridden_flags, custom_env) {
2553                    let name = selector_flag_name(other, &out).unwrap_or_else(|| other.clone());
2554                    if other.starts_with('-') {
2555                        out.errors.push(UsageErr::MissingFlag(name));
2556                    } else {
2557                        out.errors.push(UsageErr::MissingArg(name));
2558                    }
2559                }
2560            }
2561        }
2562    }
2563
2564    // An exclusive flag is the whole-command form of a conflict: `--version` means the
2565    // rest of the line has nothing to act on. Everything the invocation supplied counts,
2566    // positionals included, which is what distinguishes it from being in a group with
2567    // every other flag.
2568    //
2569    // Only what was *given*, as `conflicts` reads it: a defaulted flag standing beside an
2570    // exclusive one is nobody saying anything, and counting it would make the exclusive
2571    // flag unusable on any command that has a default. Environment values do count, also as
2572    // `conflicts` reads them, so the spec parser and the derive agree.
2573    for flag in unique_flags(out.available_flags.values().chain(out.flags.keys())) {
2574        // `SpecFlag` equality is intentionally name-only for the public parsed-value map,
2575        // but re-declared aliases can leave distinct declarations with that same name in
2576        // scope. Exclusivity is about the declaration the typed spelling resolved to, so
2577        // compare the parser's `Arc`s by identity here.
2578        let given = flag_was_parsed(flag) || flag_has_env(flag, custom_env);
2579        if !exclusive_occurrence(flag) || !given || overridden_flags.contains(&flag.name) {
2580            continue;
2581        }
2582        let other_flag = unique_flags(out.available_flags.values().chain(out.flags.keys()))
2583            .find(|other| {
2584                !Arc::ptr_eq(other, flag)
2585                    && !overridden_flags.contains(&other.name)
2586                    && (flag_was_parsed(other) || flag_has_env(other, custom_env))
2587            })
2588            .map(|other| format!("--{}", other.name));
2589        let other_arg = out.cmd.args.iter().find(|arg| {
2590            out.args.keys().any(|given| given.name == arg.name)
2591                || arg
2592                    .env
2593                    .as_ref()
2594                    .is_some_and(|env| env_contains(custom_env, env))
2595        });
2596        // Selecting a child is company for an exclusive flag declared by an ancestor. An
2597        // exclusive flag belonging to the child itself does not conflict with the command word
2598        // needed to reach that child — so the question is not who owns the flag but whose
2599        // exclusivity is the one being enforced, which is what `exclusivity_in_play` already
2600        // separated.
2601        let (_, ancestor_exclusivity) = exclusivity_in_play(flag);
2602        let selected_subcommand =
2603            (out.cmds.len() > 1 && ancestor_exclusivity).then(|| out.cmd.name.clone());
2604        let other = other_flag
2605            .or_else(|| other_arg.map(|arg| format!("<{}>", arg.name)))
2606            .or(selected_subcommand);
2607        if let Some(other) = other {
2608            out.errors.push(UsageErr::InvalidFlag {
2609                token: format!("--{}", flag.name),
2610                reason: format!("must be given on its own, and {other} was given too"),
2611                span: (0, 0).into(),
2612                input: format!("--{} {other}", flag.name),
2613            });
2614        }
2615    }
2616
2617    // Groups, checked once per group rather than per flag: both questions a group asks —
2618    // how many members were given, and whether that is enough — are about the set, which
2619    // is the whole reason a group exists rather than a pile of pairwise conflicts.
2620    //
2621    // The same "given" rule as everything else here, so a member filled from the
2622    // environment or a default counts.
2623    // Every command in the chain, not only the selected one: a group may name global
2624    // flags, which belong to an ancestor and are declared there.
2625    let mut group_errors: Vec<UsageErr> = Vec::new();
2626    for (command_index, group) in out
2627        .cmds
2628        .iter()
2629        .enumerate()
2630        .flat_map(|(index, cmd)| cmd.groups.iter().map(move |group| (index, group)))
2631    {
2632        // Counted by the *flag* a selector resolves to, not by the selector. `-f` and
2633        // `--file` are two spellings of one flag, and a group naming both — or naming one
2634        // flag twice — would otherwise report that flag as conflicting with itself the
2635        // moment it was given. Deduplicated rather than refused where the group is
2636        // written, because listing both spellings is redundant, not wrong.
2637        let mut given: Vec<&str> = Vec::new();
2638        let mut seen: Vec<String> = Vec::new();
2639        for selector in &group.members {
2640            if !selector_is_explicit(selector, &out, &overridden_flags, custom_env) {
2641                continue;
2642            }
2643            let name = selector_flag_name(selector, &out).unwrap_or_else(|| selector.clone());
2644            if seen.contains(&name) {
2645                continue;
2646            }
2647            seen.push(name);
2648            given.push(selector.as_str());
2649        }
2650        if !group.multiple && given.len() > 1 {
2651            group_errors.push(UsageErr::InvalidFlag {
2652                token: given[1].to_string(),
2653                reason: format!("cannot be used with {} in group {}", given[0], group.name),
2654                span: (0, 0).into(),
2655                input: format!("{} {}", given[0], given[1]),
2656            });
2657        }
2658        // Requiredness is a *positive* rule, so it reads a default as filling a member —
2659        // the rule `requires` follows. That is also why it cannot reuse `given` above:
2660        // exclusivity must count only what was supplied, or a defaulted member would
2661        // collide with the sibling the user actually typed.
2662        let satisfied = group
2663            .members
2664            .iter()
2665            .any(|selector| selector_is_satisfied(selector, &out, &overridden_flags, custom_env));
2666        if group.required && requirements_apply(command_index) && !satisfied && !exclusive_present {
2667            // The members are what a user has to type, so they are in the message; the
2668            // group's name is there too, since a command with several groups would
2669            // otherwise report the same sentence twice with nothing to tell them apart.
2670            group_errors.push(UsageErr::MissingGroup {
2671                group: group.name.clone(),
2672                members: group.members.join(", "),
2673            });
2674        }
2675    }
2676    out.errors.extend(group_errors);
2677
2678    if !exclusive_present {
2679        for flag in unique_flags(out.available_flags.values()) {
2680            let owner = out
2681                .cmds
2682                .iter()
2683                .rposition(|cmd| cmd.flags.iter().any(|declared| declared.name == flag.name))
2684                .unwrap_or(out.cmds.len() - 1);
2685            if !requirements_apply(owner) {
2686                continue;
2687            }
2688            if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
2689                continue;
2690            }
2691            let has_default =
2692                !flag.default.is_empty() || flag.arg.iter().any(|a| !a.default.is_empty());
2693            let has_env = flag_has_env(flag, custom_env);
2694            let required_if = flag.required_if.iter().any(|selector| {
2695                selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2696            });
2697            let required_if_eq = flag.required_if_eq.iter().any(|condition| {
2698                selector_explicit_has_value(
2699                    &condition.selector,
2700                    &condition.value,
2701                    &out,
2702                    &overridden_flags,
2703                    custom_env,
2704                )
2705            });
2706            let required_if_eq_all = !flag.required_if_eq_all.is_empty()
2707                && flag.required_if_eq_all.iter().all(|condition| {
2708                    selector_explicit_has_value(
2709                        &condition.selector,
2710                        &condition.value,
2711                        &out,
2712                        &overridden_flags,
2713                        custom_env,
2714                    )
2715                });
2716            let unless_any = flag.required_unless.iter().any(|selector| {
2717                selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2718            });
2719            let unless_all = !flag.required_unless_all.is_empty()
2720                && flag.required_unless_all.iter().all(|selector| {
2721                    selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2722                });
2723            let required_unless = !(unless_any
2724                || unless_all
2725                || (flag.required_unless.is_empty() && flag.required_unless_all.is_empty()));
2726            if (flag.required
2727                || required_if
2728                || required_if_eq
2729                || required_if_eq_all
2730                || required_unless)
2731                && !has_default
2732                && !has_env
2733            {
2734                out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
2735            }
2736        }
2737    }
2738
2739    // Validate var_min/var_max constraints for variadic args
2740    for (arg, value) in &out.args {
2741        if arg.var {
2742            if let ParseValue::MultiString(values) = value {
2743                if let Some(min) = arg.var_min {
2744                    if values.len() < min {
2745                        out.errors.push(UsageErr::VarArgTooFew {
2746                            name: arg.name.clone(),
2747                            min,
2748                            got: values.len(),
2749                        });
2750                    }
2751                }
2752                if let Some(max) = arg.var_max {
2753                    if values.len() > max {
2754                        out.errors.push(UsageErr::VarArgTooMany {
2755                            name: arg.name.clone(),
2756                            max,
2757                            got: values.len(),
2758                        });
2759                    }
2760                }
2761            }
2762        }
2763    }
2764
2765    // Validate var_min/var_max constraints for variadic flags. These are bounds on
2766    // repeated occurrences of the flag itself. Bounds on its nested argument are enforced
2767    // by binding once per occurrence, where the per-occurrence count is still available.
2768    for flag in unique_flags(out.available_flags.values()) {
2769        if flag.var {
2770            let bound = match out.flags.get(flag) {
2771                Some(ParseValue::MultiString(values)) => values.len(),
2772                Some(ParseValue::MultiBool(values)) => values.len(),
2773                Some(_) => 1,
2774                None => 0,
2775            };
2776            // A partial parse deliberately leaves the final value-optional flag pending so
2777            // completion can still answer for it. It is nevertheless a real occurrence for
2778            // the repeated flag's bounds; the full parser closes it just after this phase.
2779            let pending = out
2780                .flag_awaiting_value
2781                .iter()
2782                .filter(|pending| {
2783                    Arc::ptr_eq(pending, flag)
2784                        && (pending.value_optional || pending.default_missing.is_some())
2785                })
2786                .count();
2787            let count = bound + pending;
2788            if count == 0 {
2789                continue;
2790            }
2791            if let Some(min) = flag.var_min {
2792                if count < min {
2793                    out.errors.push(UsageErr::VarFlagTooFew {
2794                        name: flag.name.clone(),
2795                        min,
2796                        got: count,
2797                    });
2798                }
2799            }
2800            if let Some(max) = flag.var_max {
2801                if count > max {
2802                    out.errors.push(UsageErr::VarFlagTooMany {
2803                        name: flag.name.clone(),
2804                        max,
2805                        got: count,
2806                    });
2807                }
2808            }
2809        }
2810    }
2811
2812    Ok((out, overridden_flags))
2813}
2814
2815fn validate_expression(
2816    name: &str,
2817    expression: Option<&str>,
2818    message: Option<&str>,
2819    parsed: &ParseValue,
2820    errors: &mut Vec<UsageErr>,
2821) {
2822    let Some(expression) = expression else {
2823        return;
2824    };
2825    #[cfg(not(feature = "validation"))]
2826    let _ = expression;
2827    let values: &[String] = match parsed {
2828        ParseValue::String(value) => std::slice::from_ref(value),
2829        ParseValue::MultiString(values) => values,
2830        ParseValue::Bool(_) | ParseValue::MultiBool(_) => return,
2831    };
2832    #[cfg(feature = "validation")]
2833    for value in values {
2834        let reason = match usage_validation::validate(expression, value) {
2835            Ok(true) => continue,
2836            Ok(false) => message
2837                .unwrap_or("does not satisfy the validation expression")
2838                .to_string(),
2839            Err(error) => format!("validation expression failed: {error}"),
2840        };
2841        errors.push(UsageErr::InvalidValue {
2842            name: name.to_string(),
2843            value: value.clone(),
2844            reason,
2845        });
2846        break;
2847    }
2848    #[cfg(not(feature = "validation"))]
2849    if let Some(value) = values.first() {
2850        let _ = message;
2851        errors.push(UsageErr::InvalidValue {
2852            name: name.to_string(),
2853            value: value.clone(),
2854            reason: "expression validation requires the `validation` feature".to_string(),
2855        });
2856    }
2857}
2858
2859#[cfg(all(test, not(feature = "validation")))]
2860mod optional_validation_tests {
2861    use crate::{parse, Spec};
2862
2863    #[test]
2864    fn validation_declarations_require_the_opt_in_runtime_feature() {
2865        let spec: Spec = r#"
2866name "ex"
2867bin "ex"
2868arg "<port>" validate="int(value) > 0"
2869        "#
2870        .parse()
2871        .unwrap();
2872        let error = parse(&spec, &["ex".to_string(), "1".to_string()]).unwrap_err();
2873        assert!(
2874            error
2875                .to_string()
2876                .contains("requires the `validation` feature"),
2877            "{error:?}"
2878        );
2879    }
2880}
2881
2882fn flag_matches_selector(flag: &SpecFlag, selector: &str) -> bool {
2883    flag.name == selector || flag_keys(flag).iter().any(|key| key == selector)
2884}
2885
2886fn flags_override(overrider: &SpecFlag, overridden: &SpecFlag) -> bool {
2887    overrider
2888        .overrides
2889        .iter()
2890        .any(|selector| flag_matches_selector(overridden, selector))
2891}
2892
2893fn apply_prefix_flag_overrides(
2894    prefix_flags: &mut Vec<(Arc<SpecFlag>, Vec<String>)>,
2895    flag: Arc<SpecFlag>,
2896) {
2897    prefix_flags
2898        .retain(|(other, _)| !(flags_override(&flag, other) || flags_override(other, &flag)));
2899}
2900
2901fn mount_prefix_words(prefix_flags: &[(Arc<SpecFlag>, Vec<String>)]) -> Vec<String> {
2902    prefix_flags
2903        .iter()
2904        .flat_map(|(_, words)| words.iter().cloned())
2905        .collect()
2906}
2907
2908fn env_contains(custom_env: Option<&HashMap<String, String>>, env_var: &str) -> bool {
2909    match custom_env {
2910        Some(env) => env.contains_key(env_var),
2911        None => std::env::var(env_var).is_ok(),
2912    }
2913}
2914
2915fn flag_has_env(flag: &SpecFlag, custom_env: Option<&HashMap<String, String>>) -> bool {
2916    flag.env_names()
2917        .any(|env_var| env_contains(custom_env, env_var))
2918}
2919
2920fn fallback_is_true(value: &str) -> bool {
2921    matches!(value, "1" | "true" | "True" | "TRUE")
2922}
2923
2924fn split_fallback_values(values: &[String], delimiter: Option<char>) -> Vec<String> {
2925    match delimiter {
2926        Some(delimiter) => values
2927            .iter()
2928            .flat_map(|value| value.split(delimiter).map(str::to_string))
2929            .collect(),
2930        None => values.to_vec(),
2931    }
2932}
2933
2934fn validate_arg_fallback_count(arg: &SpecArg, count: usize, errors: &mut Vec<UsageErr>) {
2935    if let Some(min) = arg.var_min {
2936        if count < min {
2937            errors.push(UsageErr::VarArgTooFew {
2938                name: arg.name.clone(),
2939                min,
2940                got: count,
2941            });
2942        }
2943    }
2944    if let Some(max) = arg.var_max {
2945        if count > max {
2946            errors.push(UsageErr::VarArgTooMany {
2947                name: arg.name.clone(),
2948                max,
2949                got: count,
2950            });
2951        }
2952    }
2953}
2954
2955fn validate_flag_fallback_count(flag: &SpecFlag, count: usize, errors: &mut Vec<UsageErr>) {
2956    if let Some(min) = flag.var_min {
2957        if count < min {
2958            errors.push(UsageErr::VarFlagTooFew {
2959                name: flag.name.clone(),
2960                min,
2961                got: count,
2962            });
2963        }
2964    }
2965    if let Some(max) = flag.var_max {
2966        if count > max {
2967            errors.push(UsageErr::VarFlagTooMany {
2968                name: flag.name.clone(),
2969                max,
2970                got: count,
2971            });
2972        }
2973    }
2974}
2975
2976fn validate_flag_arg_fallback_count(
2977    flag: &SpecFlag,
2978    arg: &SpecArg,
2979    count: usize,
2980    errors: &mut Vec<UsageErr>,
2981) {
2982    if let Some(min) = arg.var_min {
2983        if count < min {
2984            errors.push(UsageErr::VarFlagTooFew {
2985                name: flag.name.clone(),
2986                min,
2987                got: count,
2988            });
2989        }
2990    }
2991    if let Some(max) = arg.var_max {
2992        if count > max {
2993            errors.push(UsageErr::VarFlagTooMany {
2994                name: flag.name.clone(),
2995                max,
2996                got: count,
2997            });
2998        }
2999    }
3000}
3001
3002/// Bind a fallback the way an unconditional `default` does: one value, or several
3003/// for `var`, and choices checked the same way.
3004fn bind_flag_fallback(
3005    flag: &Arc<SpecFlag>,
3006    values: &[String],
3007    out: &mut ParseOutput,
3008    custom_env: Option<&HashMap<String, String>>,
3009    origin: ValueOrigin,
3010) -> Result<(), miette::Error> {
3011    if values.is_empty() {
3012        return Ok(());
3013    }
3014    if let Some(arg) = flag.arg.as_ref() {
3015        let values = split_fallback_values(values, arg.delimiter);
3016        if flag.var || arg.var {
3017            if flag.var {
3018                validate_flag_fallback_count(flag, values.len(), &mut out.errors);
3019            }
3020            if arg.var {
3021                validate_flag_arg_fallback_count(flag, arg, values.len(), &mut out.errors);
3022            }
3023            validate_choice_values(
3024                ChoiceTarget::option(flag),
3025                &values,
3026                arg.choices.as_ref(),
3027                custom_env,
3028            )?;
3029            out.flags
3030                .insert(Arc::clone(flag), ParseValue::MultiString(values));
3031        } else {
3032            let value = values.into_iter().next().unwrap_or_default();
3033            validate_choice_value(
3034                ChoiceTarget::option(flag),
3035                &value,
3036                arg.choices.as_ref(),
3037                custom_env,
3038            )?;
3039            out.flags
3040                .insert(Arc::clone(flag), ParseValue::String(value));
3041        }
3042    } else if flag.var {
3043        validate_flag_fallback_count(flag, values.len(), &mut out.errors);
3044        let bools: Vec<bool> = values.iter().map(|s| fallback_is_true(s)).collect();
3045        out.flags
3046            .insert(Arc::clone(flag), ParseValue::MultiBool(bools));
3047    } else {
3048        out.flags.insert(
3049            Arc::clone(flag),
3050            ParseValue::Bool(fallback_is_true(&values[0])),
3051        );
3052    }
3053    out.flag_origins
3054        .entry(Arc::clone(flag))
3055        .or_default()
3056        .push(origin);
3057    Ok(())
3058}
3059
3060fn default_if_condition_matches(
3061    condition: &crate::SpecDefaultIf,
3062    out: &ParseOutput,
3063    overridden_flags: &HashSet<String>,
3064    custom_env: Option<&HashMap<String, String>>,
3065) -> bool {
3066    match &condition.when {
3067        None => selector_is_explicit(&condition.selector, out, overridden_flags, custom_env),
3068        Some(when) => {
3069            let Some(flag) = out
3070                .available_flags
3071                .values()
3072                .chain(out.flags.keys())
3073                .find(|flag| flag_matches_selector(flag, &condition.selector))
3074            else {
3075                return false;
3076            };
3077            if overridden_flags.contains(&flag.name) {
3078                return false;
3079            }
3080            explicit_flag_has_value(flag, when, out, custom_env)
3081        }
3082    }
3083}
3084
3085/// Whether an explicitly supplied value of `flag` equals `expected`.
3086///
3087/// clap treats command-line and environment values as explicit for `requires_if`, but
3088/// not defaults. Keep that source distinction here instead of consulting the flag's
3089/// defaults through `selector_is_satisfied`.
3090fn explicit_flag_has_value(
3091    flag: &SpecFlag,
3092    expected: &str,
3093    out: &ParseOutput,
3094    custom_env: Option<&HashMap<String, String>>,
3095) -> bool {
3096    let parsed_matches = out.flags.get(flag).is_some_and(|value| match value {
3097        ParseValue::Bool(value) => value.to_string() == expected,
3098        ParseValue::String(value) => value == expected,
3099        ParseValue::MultiBool(values) => values.iter().any(|value| value.to_string() == expected),
3100        ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3101    });
3102    if out.flags.contains_key(flag) {
3103        return parsed_matches;
3104    }
3105
3106    let value = flag.env_names().find_map(|env| match custom_env {
3107        Some(values) => values.get(env).cloned(),
3108        None => std::env::var(env).ok(),
3109    });
3110    value.is_some_and(
3111        |value| match flag.arg.as_ref().and_then(|arg| arg.delimiter) {
3112            Some(delimiter) => value.split(delimiter).any(|value| value == expected),
3113            None if flag.arg.is_none() => {
3114                matches!(value.as_str(), "1" | "true" | "True" | "TRUE").to_string() == expected
3115            }
3116            None => value == expected,
3117        },
3118    )
3119}
3120
3121fn selector_explicit_has_value(
3122    selector: &str,
3123    expected: &str,
3124    out: &ParseOutput,
3125    overridden_flags: &HashSet<String>,
3126    custom_env: Option<&HashMap<String, String>>,
3127) -> bool {
3128    if let Some(flag) = out
3129        .available_flags
3130        .values()
3131        .chain(out.flags.keys())
3132        .find(|flag| flag_matches_selector(flag, selector))
3133    {
3134        return !overridden_flags.contains(&flag.name)
3135            && explicit_flag_has_value(flag, expected, out, custom_env);
3136    }
3137    let Some(arg) = selector_arg(selector, out) else {
3138        return false;
3139    };
3140    let parsed = out
3141        .args
3142        .iter()
3143        .find(|(given, _)| given.name == arg.name)
3144        .map(|(_, value)| value);
3145    if let Some(value) = parsed {
3146        return match value {
3147            ParseValue::String(value) => value == expected,
3148            ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3149            ParseValue::Bool(value) => value.to_string() == expected,
3150            ParseValue::MultiBool(values) => {
3151                values.iter().any(|value| value.to_string() == expected)
3152            }
3153        };
3154    }
3155    let value = arg.env_names().find_map(|env| match custom_env {
3156        Some(values) => values.get(env).cloned(),
3157        None => std::env::var(env).ok(),
3158    });
3159    value.is_some_and(|value| match arg.delimiter {
3160        Some(delimiter) => value.split(delimiter).any(|value| value == expected),
3161        None => value == expected,
3162    })
3163}
3164
3165fn selector_is_explicit(
3166    selector: &str,
3167    out: &ParseOutput,
3168    overridden_flags: &HashSet<String>,
3169    custom_env: Option<&HashMap<String, String>>,
3170) -> bool {
3171    let flag_is_explicit = out
3172        .available_flags
3173        .values()
3174        .chain(out.flags.keys())
3175        .any(|flag| {
3176            flag_matches_selector(flag, selector)
3177                && !overridden_flags.contains(&flag.name)
3178                && (out.flags.contains_key(flag) || flag_has_env(flag, custom_env))
3179        });
3180    flag_is_explicit
3181        || selector_arg(selector, out).is_some_and(|arg| arg_is_explicit(arg, out, custom_env))
3182}
3183
3184/// The name of the flag a selector points at, for an error that has to name it.
3185///
3186/// `selector_is_explicit` only answers yes or no, which is all a check needs; a message
3187/// about a flag that is *missing* has to say which one, and the selector may be a short
3188/// form or an alias rather than the name.
3189fn selector_flag_name(selector: &str, out: &ParseOutput) -> Option<String> {
3190    out.available_flags
3191        .values()
3192        .chain(out.flags.keys())
3193        .find(|flag| flag_matches_selector(flag, selector))
3194        .map(|flag| flag.name.clone())
3195        .or_else(|| selector_arg(selector, out).map(|arg| arg.name.clone()))
3196}
3197
3198/// Whether a selector's flag ended up with a value, however it got one.
3199///
3200/// The rule for a *positive* relationship, and the difference from
3201/// [`selector_is_explicit`] is deliberate. A negative rule — `conflicts`, or a group's
3202/// exclusivity — has to count only what was given, or a flag with a default would
3203/// conflict with everything and no command line would parse. A positive one asks whether
3204/// the flag it names has a value, and a default is a value: that is already how plain
3205/// `required`, `required_if` and `required_unless` read a default, and `requires` saying
3206/// otherwise would have made the same flag missing here and present ten lines below.
3207fn selector_is_satisfied(
3208    selector: &str,
3209    out: &ParseOutput,
3210    overridden_flags: &HashSet<String>,
3211    custom_env: Option<&HashMap<String, String>>,
3212) -> bool {
3213    if selector_is_explicit(selector, out, overridden_flags, custom_env) {
3214        return true;
3215    }
3216    let flag_is_satisfied = out
3217        .available_flags
3218        .values()
3219        .chain(out.flags.keys())
3220        .filter(|flag| flag_matches_selector(flag, selector))
3221        .any(|flag| {
3222            !overridden_flags.contains(&flag.name)
3223                && (!flag.default.is_empty()
3224                    || flag.arg.iter().any(|a| !a.default.is_empty())
3225                    || flag.default_if.iter().any(|condition| {
3226                        default_if_condition_matches(condition, out, overridden_flags, custom_env)
3227                    }))
3228        });
3229    flag_is_satisfied || selector_arg(selector, out).is_some_and(|arg| !arg.default.is_empty())
3230}
3231
3232fn selector_arg<'a>(selector: &str, out: &'a ParseOutput) -> Option<&'a SpecArg> {
3233    // Bare words are positional selectors. Keep accepting a flag's internal name above
3234    // for existing specs; when both exist, the dashed flag spelling removes ambiguity.
3235    if selector.starts_with('-') {
3236        return None;
3237    }
3238    out.cmds
3239        .iter()
3240        .flat_map(|cmd| &cmd.args)
3241        .find(|arg| arg.name == selector)
3242}
3243
3244fn arg_is_explicit(
3245    arg: &SpecArg,
3246    out: &ParseOutput,
3247    custom_env: Option<&HashMap<String, String>>,
3248) -> bool {
3249    out.args.keys().any(|given| given.name == arg.name)
3250        || arg
3251            .env
3252            .as_ref()
3253            .is_some_and(|env| env_contains(custom_env, env))
3254}
3255
3256fn apply_flag_overrides(
3257    flag: &Arc<SpecFlag>,
3258    available_flags: &BTreeMap<String, Arc<SpecFlag>>,
3259    parsed_flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
3260    pending_flags: &mut Vec<Arc<SpecFlag>>,
3261    overridden_flags: &mut HashSet<String>,
3262    // The reportable half of the same fact: which flag did the overriding. The set above
3263    // only stops a default or an environment value restoring what was overridden, and
3264    // "`--quiet` is unset despite its default" has no answer without the name.
3265    attributed: &mut BTreeMap<String, String>,
3266) {
3267    let overridden_names: HashSet<String> = available_flags
3268        .values()
3269        .chain(parsed_flags.keys())
3270        .filter(|other| flags_override(flag, other) || flags_override(other, flag))
3271        .map(|other| other.name.clone())
3272        .collect();
3273
3274    parsed_flags.retain(|parsed, _| !overridden_names.contains(&parsed.name));
3275    pending_flags.retain(|pending| !overridden_names.contains(&pending.name));
3276    for name in &overridden_names {
3277        attributed.insert(name.clone(), flag.name.clone());
3278    }
3279    overridden_flags.extend(overridden_names);
3280    // An explicit occurrence always restores this flag, including self-overrides.
3281    overridden_flags.remove(&flag.name);
3282    attributed.remove(&flag.name);
3283}
3284
3285#[cfg(feature = "cli-help")]
3286fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool) -> UsageErr {
3287    UsageErr::Help(docs::cli::render_help(spec, cmd, long))
3288}
3289
3290#[cfg(feature = "cli-help")]
3291fn render_help_all_err(spec: &Spec, cmd: &SpecCommand) -> UsageErr {
3292    fn append(out: &mut String, spec: &Spec, cmd: &SpecCommand) {
3293        if !out.is_empty() {
3294            out.push('\n');
3295        }
3296        out.push_str(&docs::cli::render_help(spec, cmd, true));
3297        let mut children: Vec<_> = cmd
3298            .subcommands
3299            .values()
3300            .filter(|child| !child.hide)
3301            .collect();
3302        children.sort_by_key(|child| (child.display_order.unwrap_or(999), child.name.as_str()));
3303        for child in children {
3304            append(out, spec, child);
3305        }
3306    }
3307
3308    let mut out = String::new();
3309    append(&mut out, spec, cmd);
3310    UsageErr::Help(out)
3311}
3312
3313#[cfg(not(feature = "cli-help"))]
3314fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool) -> UsageErr {
3315    UsageErr::Help("help".to_string())
3316}
3317
3318#[cfg(not(feature = "cli-help"))]
3319fn render_help_all_err(_spec: &Spec, _cmd: &SpecCommand) -> UsageErr {
3320    UsageErr::Help("help".to_string())
3321}
3322
3323/// The version to answer with. `--version` prefers the long text and `-V` the concise
3324/// one, each falling back to the other when only one is declared.
3325fn render_version_err(spec: &Spec, long: bool) -> UsageErr {
3326    let value = if long {
3327        spec.long_version.as_ref().or(spec.version.as_ref())
3328    } else {
3329        spec.version.as_ref().or(spec.long_version.as_ref())
3330    };
3331    UsageErr::Version(value.cloned().unwrap_or_default())
3332}
3333
3334fn render_action_err(spec: &Spec, cmd: &SpecCommand, flag: &SpecFlag, spelling: &str) -> UsageErr {
3335    use crate::SpecFlagAction;
3336    match flag.action {
3337        SpecFlagAction::Help => render_help_err(spec, cmd, spelling.starts_with("--")),
3338        SpecFlagAction::HelpShort => render_help_err(spec, cmd, false),
3339        SpecFlagAction::HelpLong => render_help_err(spec, cmd, true),
3340        SpecFlagAction::HelpAll => render_help_all_err(spec, cmd),
3341        SpecFlagAction::Version => render_version_err(spec, spelling.starts_with("--")),
3342        SpecFlagAction::Set => unreachable!("binding actions are handled before this helper"),
3343    }
3344}
3345
3346/// Report a required flag value that was displaced by a later option.
3347fn render_missing_flag_value(flag: &SpecFlag, following: &str) -> UsageErr {
3348    let token = flag
3349        .long
3350        .first()
3351        .map(|long| format!("--{long}"))
3352        .or_else(|| flag.short.first().map(|short| format!("-{short}")))
3353        .unwrap_or_else(|| flag.name.clone());
3354    UsageErr::InvalidFlag {
3355        token: token.clone(),
3356        reason: "requires an argument".to_string(),
3357        span: (0, 0).into(),
3358        input: format!("{token} {following}"),
3359    }
3360}
3361
3362#[derive(Copy, Clone)]
3363struct ChoiceTarget<'a> {
3364    kind: &'a str,
3365    name: &'a str,
3366}
3367
3368impl<'a> ChoiceTarget<'a> {
3369    fn arg(arg: &'a SpecArg) -> Self {
3370        Self {
3371            kind: "arg",
3372            name: &arg.name,
3373        }
3374    }
3375
3376    fn option(flag: &'a SpecFlag) -> Self {
3377        Self {
3378            kind: "option",
3379            name: &flag.name,
3380        }
3381    }
3382}
3383
3384/// Whether every letter of a short token names a flag in scope.
3385///
3386/// Scanning stops at the first letter whose flag takes a value, because everything
3387/// after it is that value rather than more letters.
3388fn short_bundle_is_known(
3389    spec: &Spec,
3390    cmds: &[SpecCommand],
3391    available: &BTreeMap<String, Arc<SpecFlag>>,
3392    token: &str,
3393) -> bool {
3394    for c in token.chars().skip(1) {
3395        match available.get(&format!("-{c}")) {
3396            // `-h` and `-V` are recognized letters even though no spec declares them, so a
3397            // bundle containing one is a bundle. Without this `-vh` was not read as one at
3398            // all and fell through to `unexpected word`, while usage-argv, usage-go and
3399            // clap all answer it with help.
3400            None if supplied_short(spec, cmds, c).is_some() => {}
3401            None => return false,
3402            Some(f) if f.arg.is_some() => return true,
3403            Some(_) => {}
3404        }
3405    }
3406    true
3407}
3408
3409/// The response `-h` or `-V` produces where nothing declares that letter.
3410///
3411/// The letter form of the flags the parser supplies rather than a spec declaring them,
3412/// under exactly the conditions [`is_help_arg`] and [`is_version_arg`] state — asked
3413/// about here one letter at a time, because a bundle is read one letter at a time.
3414///
3415/// Always the short response: `-h` is short help however many letters share its token,
3416/// and `-V` the concise version. The long forms belong to the long spellings. `-?` is not
3417/// here — it is a whole-token spelling of `-h` rather than a letter anyone bundles.
3418fn supplied_short(spec: &Spec, cmds: &[SpecCommand], letter: char) -> Option<UsageErr> {
3419    let cmd = cmds.last()?;
3420    match letter {
3421        'h' if is_help_arg(spec, cmd, "-h") => Some(render_help_err(spec, cmd, false)),
3422        'V' if is_version_arg(spec, cmds, "-V") => Some(render_version_err(spec, false)),
3423        _ => None,
3424    }
3425}
3426
3427/// Refuse a flag-like token that named nothing, if this command asked for that.
3428///
3429/// Called from the flag branches, where the lookup has just failed and nothing from
3430/// the token has been applied yet — so a bundle like `-az` is refused whole rather
3431/// than after setting `-a`.
3432fn reject_unknown_flag_if_asked(
3433    spec: &Spec,
3434    path: &[SpecCommand],
3435    token: &str,
3436) -> Result<(), UsageErr> {
3437    // A lone `-` is a value by convention. A negative number reaches this only
3438    // when no pending value opted into the narrower exception.
3439    if !is_flag_like(token) {
3440        return Ok(());
3441    }
3442    if effective_unknown_flags(spec, path) != UnknownFlags::Error {
3443        return Ok(());
3444    }
3445    Err(UsageErr::InvalidFlag {
3446        token: token.to_string(),
3447        reason: "no such flag".to_string(),
3448        span: (0, 0).into(),
3449        input: token.to_string(),
3450    })
3451}
3452
3453/// Whether a flag-like token that matches nothing is a value or an error, here.
3454///
3455/// The nearest enclosing command that stated a preference wins, then the spec,
3456/// then the default. Inherited, unlike `effect`: it describes how a command line
3457/// is read, and a CLI that forwards options tends to forward them at every level.
3458fn effective_unknown_flags(spec: &Spec, path: &[SpecCommand]) -> UnknownFlags {
3459    path.iter()
3460        .rev()
3461        .find_map(|cmd| cmd.unknown_flags)
3462        .or(spec.unknown_flags)
3463        .unwrap_or_default()
3464}
3465
3466/// Whether a token would be read as a flag, for the purpose of rejecting unknown
3467/// ones.
3468///
3469/// A lone `-` is a value by convention. Other dash-prefixed tokens are flag-like;
3470/// a field may make the narrower negative-number exception.
3471fn is_flag_like(token: &str) -> bool {
3472    match token.strip_prefix('-') {
3473        None | Some("") => false,
3474        Some(_) => true,
3475    }
3476}
3477
3478fn is_negative_number(token: &str) -> bool {
3479    token.strip_prefix('-').is_some_and(is_number)
3480}
3481
3482/// Whether a flag may claim its following token as a detached value.
3483///
3484/// A required value keeps the historical negative-number exception. A value
3485/// that may be omitted needs an explicit opt-in to distinguish a negative value
3486/// from the flag's bare form.
3487fn accepts_detached_flag_value(flag: &SpecFlag, token: &str) -> bool {
3488    !flag.require_equals
3489        && (!is_flag_like(token)
3490            || flag.allow_hyphen_values()
3491            || (is_negative_number(token)
3492                && (flag
3493                    .arg
3494                    .as_ref()
3495                    .is_some_and(|arg| arg.allow_negative_numbers)
3496                    || (flag.default_missing.is_none() && !flag.value_optional))))
3497}
3498
3499fn record_scalar_flag_occurrence(
3500    cmds: &[SpecCommand],
3501    flag: &Arc<SpecFlag>,
3502    command_level: usize,
3503    bool_value: Option<bool>,
3504    occurrences: &mut HashMap<(usize, usize), u8>,
3505    errors: &mut Vec<UsageErr>,
3506) {
3507    let strict = cmds
3508        .get(command_level)
3509        .is_some_and(|cmd| !cmd.args_override_self);
3510    let collects_values = flag.var || flag.arg.as_ref().is_some_and(|arg| arg.var);
3511    if !strict || flag.count || collects_values {
3512        return;
3513    }
3514
3515    let bit = match bool_value {
3516        Some(false) if flag.negate.is_some() => 0b10,
3517        _ => 0b01,
3518    };
3519    let key = (Arc::as_ptr(flag) as usize, command_level);
3520    let seen = occurrences.entry(key).or_default();
3521    if *seen & bit != 0 {
3522        errors.push(UsageErr::DuplicateFlag(flag.name.clone()));
3523    }
3524    *seen |= bit;
3525}
3526
3527/// A token that can select a subcommand, trigger a mount, or be forwarded as an
3528/// external command.
3529///
3530/// Flag-like tokens are not words. A lone `-` is a value — conventionally stdin —
3531/// so it was never a candidate to *select* anything either. usage-argv uses the
3532/// same rule; without it, `-1` skipped the external-subcommand path because Phase 1
3533/// treated every token that `starts_with('-')` as a flag.
3534fn is_command_word(token: &str) -> bool {
3535    (!is_flag_like(token) || is_negative_number(token)) && token != "-"
3536}
3537
3538/// Whether an otherwise numeric-looking token is an exact declared short flag.
3539///
3540/// This check belongs in both parse phases: phase 1 must skip the flag while it
3541/// searches for a later subcommand, and phase 2 must bind it instead of offering
3542/// it to an `allow_negative_numbers` positional.
3543fn declared_numeric_short(available_flags: &BTreeMap<String, Arc<SpecFlag>>, token: &str) -> bool {
3544    token.len() == 2 && token.as_bytes()[1].is_ascii_digit() && available_flags.contains_key(token)
3545}
3546
3547/// Whether an unmatched word belongs to the root's default command.
3548///
3549/// Ordinary words always do. A negative number only does when the default command's
3550/// first positional explicitly accepts one; otherwise it stays at the root, matching
3551/// usage-argv and generated Go.
3552fn default_accepts_word(cmd: &SpecCommand, default_name: &str, token: &str) -> bool {
3553    !is_negative_number(token)
3554        || cmd
3555            .find_subcommand(default_name)
3556            .and_then(|default| default.args.first())
3557            .is_some_and(|arg| arg.allow_negative_numbers)
3558}
3559
3560/// Digits, at most one `.`, and an optional exponent.
3561///
3562/// Spelled out rather than deferred to `f64::from_str`, which also accepts `inf` and
3563/// `NaN`: `-inf` is far likelier to be a misspelled flag than a number somebody meant
3564/// to pass. usage-argv implements the same rule, and the corpus pins the edges — the
3565/// two disagreed about `-1e5` when one used a float parse and the other did not.
3566fn is_number(rest: &str) -> bool {
3567    let (mantissa, exponent) = match rest.find(['e', 'E']) {
3568        Some(at) => (&rest[..at], Some(&rest[at + 1..])),
3569        None => (rest, None),
3570    };
3571
3572    let mut seen_digit = false;
3573    let mut seen_dot = false;
3574    for c in mantissa.chars() {
3575        match c {
3576            '0'..='9' => seen_digit = true,
3577            '.' if !seen_dot => seen_dot = true,
3578            _ => return false,
3579        }
3580    }
3581    if !seen_digit {
3582        return false;
3583    }
3584
3585    match exponent {
3586        None => true,
3587        Some(exp) => {
3588            let digits = exp
3589                .strip_prefix('+')
3590                .or_else(|| exp.strip_prefix('-'))
3591                .unwrap_or(exp);
3592            !digits.is_empty() && digits.chars().all(|c| c.is_ascii_digit())
3593        }
3594    }
3595}
3596
3597/// Bind one value to the flag waiting for it, and let a variadic argument go on
3598/// collecting from the words that follow.
3599///
3600/// Every route to a flag's value comes through here — the following word, the text
3601/// after an `=`, and the token a `allow_hyphen_values` flag takes whatever it looks
3602/// like — so that all three agree on how many values the flag ends up with.
3603#[allow(clippy::too_many_arguments)]
3604fn bind_pending_flag_value(
3605    spec: &Spec,
3606    cmd: &SpecCommand,
3607    errors: &mut Vec<UsageErr>,
3608    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
3609    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
3610    word: &mut String,
3611    input: &mut VecDeque<Token>,
3612    custom_env: Option<&HashMap<String, String>>,
3613    trace: &mut Trace,
3614    // Which token supplied `word`, and whether it rode along on the flag's own token
3615    // (`--jobs=8`, `-j8`) rather than following it. A variadic run's later words carry
3616    // their own positions and are recorded where they are read.
3617    argv: usize,
3618    attached: bool,
3619) -> miette::Result<bool> {
3620    // Held before the drain pops it, along with what the flag is already carrying: a
3621    // `var_max` bounds the values this occurrence takes, not the list they are appended
3622    // to, so a second `--include` starts counting again.
3623    let collecting = flag_awaiting_value
3624        .last()
3625        .filter(|flag| flag.arg.as_ref().is_some_and(|arg| arg.var))
3626        .cloned()
3627        .map(|flag| {
3628            let carried = flags.get(&flag).map(value_count).unwrap_or(0);
3629            (flag, carried)
3630        });
3631    let mut bound = vec![];
3632    let refused = drain_pending_flag_values(
3633        spec,
3634        cmd,
3635        errors,
3636        flags,
3637        flag_awaiting_value,
3638        word,
3639        custom_env,
3640        &mut bound,
3641    )?;
3642    for (flag, values) in bound {
3643        trace.record(
3644            argv,
3645            TokenRole::Value {
3646                flag,
3647                values,
3648                attached,
3649            },
3650        );
3651    }
3652    if refused {
3653        return Ok(true);
3654    }
3655    let Some((flag, carried)) = collecting else {
3656        return Ok(false);
3657    };
3658    collect_variadic_flag_values(
3659        spec,
3660        cmd,
3661        errors,
3662        flags,
3663        flag_awaiting_value,
3664        &flag,
3665        carried,
3666        input,
3667        custom_env,
3668        trace,
3669    )
3670}
3671
3672/// Keep feeding a flag whose argument is variadic from the words that follow it.
3673///
3674/// `--include <pattern>...` collects from a single occurrence, so it takes tokens until
3675/// one is flag-like, a `--` arrives, its `var_max` is reached, or the command line ends.
3676/// This is greedy by design — a command declaring both such a flag and positionals will
3677/// find the flag eating them, and `--` or a `var_max` is how the run is stopped.
3678///
3679/// `carried` is what the flag already held when this occurrence began, so the bound
3680/// counts this run rather than everything the flag has collected across the command
3681/// line. Each value goes through the same drain as the first, so choices are checked
3682/// and the value lands in the same list rather than by a second route that could
3683/// disagree.
3684#[allow(clippy::too_many_arguments)]
3685fn collect_variadic_flag_values(
3686    spec: &Spec,
3687    cmd: &SpecCommand,
3688    errors: &mut Vec<UsageErr>,
3689    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
3690    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
3691    flag: &Arc<SpecFlag>,
3692    carried: usize,
3693    input: &mut VecDeque<Token>,
3694    custom_env: Option<&HashMap<String, String>>,
3695    trace: &mut Trace,
3696) -> miette::Result<bool> {
3697    let max = flag
3698        .arg
3699        .as_ref()
3700        .and_then(|arg| arg.var_max)
3701        .unwrap_or(usize::MAX);
3702    while flags
3703        .get(flag)
3704        .map(value_count)
3705        .unwrap_or(0)
3706        .saturating_sub(carried)
3707        < max
3708    {
3709        let Some(next) = input.front().map(|token| token.word.as_str()) else {
3710            break;
3711        };
3712        if flag
3713            .arg
3714            .as_ref()
3715            .and_then(|arg| arg.value_terminator.as_deref())
3716            == Some(next)
3717        {
3718            let terminator = input.pop_front().unwrap();
3719            trace.record(
3720                terminator.argv,
3721                TokenRole::ValueTerminator {
3722                    ends: flag.name.clone(),
3723                },
3724            );
3725            break;
3726        }
3727        // The separator is left where it is: stopping here hands it to the arm that
3728        // knows what it means, rather than reading it as one more value.
3729        if next == "--"
3730            || (is_flag_like(next)
3731                && !(flag
3732                    .arg
3733                    .as_ref()
3734                    .is_some_and(|arg| arg.allow_negative_numbers)
3735                    && is_negative_number(next)))
3736        {
3737            break;
3738        }
3739        let taken = input.pop_front().unwrap();
3740        let argv = taken.argv;
3741        let mut word = taken.word;
3742        flag_awaiting_value.push(Arc::clone(flag));
3743        let mut bound = vec![];
3744        let refused = drain_pending_flag_values(
3745            spec,
3746            cmd,
3747            errors,
3748            flags,
3749            flag_awaiting_value,
3750            &mut word,
3751            custom_env,
3752            &mut bound,
3753        )?;
3754        for (flag, values) in bound {
3755            // A later word of the same occurrence is its own token, and never attached:
3756            // only the first value can ride along on the flag.
3757            trace.record(
3758                argv,
3759                TokenRole::Value {
3760                    flag,
3761                    values,
3762                    attached: false,
3763                },
3764            );
3765        }
3766        if refused {
3767            return Ok(true);
3768        }
3769    }
3770    // The loop stops once the occurrence has reached its bound, which without a delimiter is
3771    // exactly when it has taken `max` words. A delimiter breaks that: one word can carry
3772    // several values, so the run can end up *past* the bound rather than on it, and stopping
3773    // is no longer the same as staying within it. `--include a,b,c` under `var_max=2` is the
3774    // case — three values out of the one word the loop was entitled to take.
3775    //
3776    // Counted against `carried` like the loop itself, so this stays a statement about the
3777    // occurrence rather than about the list the occurrences build up.
3778    let taken = flags
3779        .get(flag)
3780        .map(value_count)
3781        .unwrap_or(0)
3782        .saturating_sub(carried);
3783    if let Some(min) = flag.arg.as_ref().and_then(|arg| arg.var_min) {
3784        if taken < min {
3785            errors.push(UsageErr::VarFlagTooFew {
3786                name: flag.name.clone(),
3787                min,
3788                got: taken,
3789            });
3790        }
3791    }
3792    if taken > max {
3793        errors.push(UsageErr::VarFlagTooMany {
3794            name: flag.name.clone(),
3795            max,
3796            got: taken,
3797        });
3798    }
3799    Ok(false)
3800}
3801
3802/// How many values a flag is holding, for a bound that counts them.
3803fn value_count(value: &ParseValue) -> usize {
3804    match value {
3805        ParseValue::MultiString(values) => values.len(),
3806        ParseValue::MultiBool(values) => values.len(),
3807        _ => 1,
3808    }
3809}
3810
3811/// Finish a value-optional flag that was given with no value.
3812///
3813/// Returns whether anything was bound. Completions keep the flag waiting — a
3814/// half-typed `--color ` is a question about the value — so this is asked only
3815/// once a full parse has decided the value is not coming, or once the next token
3816/// has made that decision.
3817///
3818/// The missing string is a real value: if the flag names `choices`, it has to
3819/// be one of them, the same way an env var or a `default` is checked. Binding
3820/// first and failing later would leave the flag set to a value the spec forbids.
3821fn try_bind_default_missing(
3822    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
3823    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
3824    custom_env: Option<&HashMap<String, String>>,
3825    origins: &mut IndexMap<Arc<SpecFlag>, Vec<ValueOrigin>>,
3826) -> miette::Result<bool> {
3827    let Some(flag) = flag_awaiting_value.last() else {
3828        return Ok(false);
3829    };
3830    let value = match flag.default_missing.clone() {
3831        Some(value) => value,
3832        None if flag.value_optional => {
3833            let flag = flag_awaiting_value.pop().unwrap();
3834            // Presence in the map distinguishes this from an absent flag; an
3835            // empty collection distinguishes it from an explicitly empty
3836            // `--flag=` string without inventing a sentinel value.
3837            let variadic_value = flag.arg.as_ref().is_some_and(|arg| arg.var);
3838            origins
3839                .entry(Arc::clone(&flag))
3840                .or_default()
3841                .push(ValueOrigin::DefaultMissing);
3842            if flag.var {
3843                // A repeated bare occurrence is still an occurrence. The string collection
3844                // uses an empty value for it, just as the concrete `default_missing` path
3845                // pushes one value per occurrence; otherwise bounds and consumers silently
3846                // lose every bare repeat after the first.
3847                flags
3848                    .entry(flag)
3849                    .or_insert_with(|| ParseValue::MultiString(Vec::new()))
3850                    .try_as_multi_string_mut()
3851                    .unwrap()
3852                    .push(String::new());
3853            } else if variadic_value {
3854                // A variadic occurrence stays pending after each value. Reaching the next
3855                // flag (or EOF) closes that same occurrence; it must not erase what it took.
3856                flags
3857                    .entry(flag)
3858                    .or_insert_with(|| ParseValue::MultiString(Vec::new()));
3859            } else {
3860                // A scalar pending here is a new bare occurrence. The normal permissive
3861                // repeat policy makes the later occurrence a correction, including a
3862                // correction from an explicit value back to the bare tri-state.
3863                flags.insert(flag, ParseValue::MultiString(Vec::new()));
3864            }
3865            return Ok(true);
3866        }
3867        None => return Ok(false),
3868    };
3869    if let Some(arg) = flag.arg.as_ref() {
3870        validate_choice_value(
3871            ChoiceTarget::option(flag),
3872            &value,
3873            arg.choices.as_ref(),
3874            custom_env,
3875        )?;
3876    }
3877    let flag = flag_awaiting_value.pop().unwrap();
3878    origins
3879        .entry(Arc::clone(&flag))
3880        .or_default()
3881        .push(ValueOrigin::DefaultMissing);
3882    let collecting = flag.var || flag.arg.as_ref().is_some_and(|arg| arg.var);
3883    if collecting {
3884        let arr = flags
3885            .entry(flag)
3886            .or_insert_with(|| ParseValue::MultiString(vec![]))
3887            .try_as_multi_string_mut()
3888            .unwrap();
3889        arr.push(value);
3890    } else {
3891        flags.insert(flag, ParseValue::String(value));
3892    }
3893    Ok(true)
3894}
3895
3896/// `bound` collects what each drained flag took, in the order it took it. The values are
3897/// the word after any `delimiter` split, which is the only place that split is known: by the
3898/// time they are in `flags` a scalar and a one-element list are indistinguishable, and a
3899/// second occurrence has appended to the same list.
3900#[allow(clippy::too_many_arguments)]
3901fn drain_pending_flag_values(
3902    spec: &Spec,
3903    cmd: &SpecCommand,
3904    errors: &mut Vec<UsageErr>,
3905    flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
3906    flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
3907    word: &mut String,
3908    custom_env: Option<&HashMap<String, String>>,
3909    bound: &mut Vec<(Arc<SpecFlag>, Vec<String>)>,
3910) -> miette::Result<bool> {
3911    while let Some(flag) = flag_awaiting_value.pop() {
3912        let arg = flag.arg.as_ref().unwrap();
3913        // Split before anything judges the word, because after the split it is no longer
3914        // one value: `--env dev,prod` is two, and `choices` has to be asked about each.
3915        // Judging first would reject the whole word against a list neither half is on.
3916        let parts: Vec<String> = match arg.delimiter {
3917            Some(delimiter) => word.split(delimiter).map(str::to_string).collect(),
3918            None => vec![std::mem::take(word)],
3919        };
3920        for part in &parts {
3921            if validate_choices(
3922                spec,
3923                cmd,
3924                errors,
3925                ChoiceTarget::option(&flag),
3926                part,
3927                arg.choices.as_ref(),
3928                custom_env,
3929            )? {
3930                return Ok(true);
3931            }
3932        }
3933        word.clear();
3934        bound.push((Arc::clone(&flag), parts.clone()));
3935        // Two ways to hold several values, and both record a list: a `var` flag
3936        // collects one per occurrence, a variadic argument collects several from one.
3937        if flag.var || arg.var {
3938            let arr = flags
3939                .entry(flag)
3940                .or_insert_with(|| ParseValue::MultiString(vec![]))
3941                .try_as_multi_string_mut()
3942                .unwrap();
3943            arr.extend(parts);
3944        } else {
3945            // Nowhere for a second value to go, so the word stands as it was typed. A
3946            // delimiter on a flag that takes one value is refused where it is written.
3947            flags.insert(
3948                flag,
3949                ParseValue::String(parts.into_iter().next().unwrap_or_default()),
3950            );
3951        }
3952    }
3953    Ok(false)
3954}
3955
3956fn choice_error(
3957    target: ChoiceTarget<'_>,
3958    value: &str,
3959    choices: Option<&SpecChoices>,
3960    custom_env: Option<&HashMap<String, String>>,
3961) -> Option<String> {
3962    let choices = choices?;
3963    if !choices.strict {
3964        return None;
3965    }
3966    let values = choices.values_with_env(custom_env);
3967    if choices.matches_with_env(value, custom_env) {
3968        return None;
3969    }
3970    if let Some(env) = choices.env() {
3971        if values.is_empty() {
3972            return Some(format!(
3973                "Invalid choice for {} {}: {value}, no choices resolved from env {env}",
3974                target.kind, target.name,
3975            ));
3976        }
3977    }
3978    Some(format!(
3979        "Invalid choice for {} {}: {value}, expected one of {}",
3980        target.kind,
3981        target.name,
3982        values.join(", ")
3983    ))
3984}
3985
3986fn validate_choices(
3987    spec: &Spec,
3988    cmd: &SpecCommand,
3989    errors: &mut Vec<UsageErr>,
3990    target: ChoiceTarget<'_>,
3991    value: &str,
3992    choices: Option<&SpecChoices>,
3993    custom_env: Option<&HashMap<String, String>>,
3994) -> miette::Result<bool> {
3995    if is_help_arg(spec, cmd, value)
3996        && choices
3997            .is_some_and(|choices| choices.strict && !choices.matches_with_env(value, custom_env))
3998    {
3999        errors.push(render_help_err(spec, cmd, value.len() > 2));
4000        return Ok(true);
4001    }
4002
4003    if let Some(err) = choice_error(target, value, choices, custom_env) {
4004        bail!("{err}");
4005    }
4006    Ok(false)
4007}
4008
4009fn validate_choice_value(
4010    target: ChoiceTarget<'_>,
4011    value: &str,
4012    choices: Option<&SpecChoices>,
4013    custom_env: Option<&HashMap<String, String>>,
4014) -> miette::Result<()> {
4015    if let Some(err) = choice_error(target, value, choices, custom_env) {
4016        bail!("{err}");
4017    }
4018    Ok(())
4019}
4020
4021fn validate_choice_values(
4022    target: ChoiceTarget<'_>,
4023    values: &[String],
4024    choices: Option<&SpecChoices>,
4025    custom_env: Option<&HashMap<String, String>>,
4026) -> miette::Result<()> {
4027    for value in values {
4028        validate_choice_value(target, value, choices, custom_env)?;
4029    }
4030    Ok(())
4031}
4032
4033/// Everything a parse records about where it stopped: the positional cursor, so callers that
4034/// do not re-run the parse — completions, above all — agree with it, and the token trace.
4035///
4036/// Every exit from the binding phase comes through here, which is what makes it the right
4037/// place to close the trace: whatever is still queued was never read, and saying so is more
4038/// useful than leaving those words out of the report entirely.
4039fn record_stop(
4040    out: &mut ParseOutput,
4041    next_arg_idx: usize,
4042    seen_double_dash: bool,
4043    trace: &mut Trace,
4044    unread: &VecDeque<Token>,
4045) {
4046    out.next_arg = out.cmd.args.get(next_arg_idx).cloned().map(Arc::new);
4047    out.double_dash_seen = seen_double_dash;
4048    trace.close(unread);
4049    out.tokens = std::mem::take(&mut trace.tokens);
4050}
4051
4052/// Record that `arg` was handed a word before the `--` it requires.
4053///
4054/// A variadic arg would otherwise report the same mistake once per word it was offered, so the
4055/// message is emitted only the first time each arg is seen. The set is also what suppresses the
4056/// `MissingArg` that a `required` + `double_dash="required"` arg would otherwise collect at the
4057/// end of the parse.
4058fn report_double_dash_violation(
4059    arg: &SpecArg,
4060    errors: &mut Vec<UsageErr>,
4061    violations: &mut HashSet<String>,
4062) {
4063    if violations.insert(arg.name.clone()) {
4064        errors.push(UsageErr::ArgRequiresDoubleDash(arg.name.clone()));
4065    }
4066}
4067
4068/// `--version` and `-V`, which the parser supplies where the spec declares a version.
4069///
4070/// The twin of [`is_help_arg`], and of the `version` bit in usage-argv's and usage-go's
4071/// command tables — both of which accepted these spellings while this parser called them
4072/// unknown words. The help page has always listed `-V, --version` under the same
4073/// condition, and said in as many words that it did so "only where a version is
4074/// declared, which is where a parser accepts one", so a spec with a `version` rendered a
4075/// page advertising a flag the parse refused.
4076///
4077/// The root only, because that is where the page lists it: `version` is a property of
4078/// the program, and a subcommand answering with the program's version is a claim no spec
4079/// made. A declared flag wins by arriving first — every scan consults this only after
4080/// nothing declared matched — so a CLI that spends `-V` on something else keeps it, and
4081/// keeps `--version` supplied beside it.
4082fn is_version_arg(spec: &Spec, cmds: &[SpecCommand], w: &str) -> bool {
4083    (spec.version.is_some() || spec.long_version.is_some())
4084        && cmds.len() == 1
4085        && !spec.cmd.disable_version_flag
4086        && (w == "--version" || w == "-V")
4087}
4088
4089fn is_help_arg(spec: &Spec, cmd: &SpecCommand, w: &str) -> bool {
4090    spec.disable_help != Some(true)
4091        && (((w == "--help" || w == "-h" || w == "-?") && !cmd.disable_help_flag)
4092            || (w == "help" && !cmd.disable_help_subcommand && cmd.subcommands.is_empty()))
4093}
4094
4095impl ParseOutput {
4096    pub fn as_env(&self) -> BTreeMap<String, String> {
4097        let mut env = BTreeMap::new();
4098        for (flag, val) in &self.flags {
4099            let key = format!("usage_{}", crate::case::snake(&flag.name));
4100            let val = match val {
4101                ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
4102                ParseValue::String(s) => s.clone(),
4103                ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
4104                ParseValue::MultiString(s) => crate::shell_words::join(s),
4105            };
4106            env.insert(key, val);
4107        }
4108        for (arg, val) in &self.args {
4109            let key = format!("usage_{}", crate::case::snake(&arg.name));
4110            env.insert(key, val.to_string());
4111        }
4112        env
4113    }
4114}
4115
4116impl Display for ParseValue {
4117    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
4118        match self {
4119            ParseValue::Bool(b) => write!(f, "{b}"),
4120            ParseValue::String(s) => write!(f, "{s}"),
4121            ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
4122            ParseValue::MultiString(s) => write!(f, "{}", crate::shell_words::join(s)),
4123        }
4124    }
4125}
4126
4127/// One `tokens` line for [`Debug`]: the position, the word, and what it became.
4128fn render_token(token: &TokenBinding) -> String {
4129    let roles = token.roles.iter().map(render_role).join(", ");
4130    let synthesized = if token.synthesized { " (read as)" } else { "" };
4131    format!("[{}] {}{synthesized}: {roles}", token.index, token.word)
4132}
4133
4134fn render_role(role: &TokenRole) -> String {
4135    match role {
4136        TokenRole::Program => "program".to_string(),
4137        TokenRole::Command { name } => format!("subcommand {name}"),
4138        TokenRole::Flag {
4139            flag,
4140            spelling,
4141            negated,
4142        } => {
4143            let negated = if *negated { ", negated" } else { "" };
4144            format!("flag {} as {spelling}{negated}", flag.name)
4145        }
4146        TokenRole::Value {
4147            flag,
4148            values,
4149            attached,
4150        } => {
4151            let attached = if *attached { ", attached" } else { "" };
4152            format!("value of {} = {values:?}{attached}", flag.name)
4153        }
4154        TokenRole::Arg { arg, values } => format!("arg {} = {values:?}", arg.name),
4155        TokenRole::Separator => "separator".to_string(),
4156        TokenRole::Builtin { spelling } => format!("built-in {spelling}"),
4157        TokenRole::ValueTerminator { ends } => format!("value terminator, ends {ends}"),
4158        TokenRole::Restart => "restart".to_string(),
4159        TokenRole::UnknownFlag { bound_as } => match bound_as {
4160            Some(arg) => format!("unknown flag, bound as {}", arg.name),
4161            None => "unknown flag".to_string(),
4162        },
4163        TokenRole::Refused { reason } => format!("refused: {reason}"),
4164        TokenRole::External => "external".to_string(),
4165        TokenRole::Unread => "unread".to_string(),
4166    }
4167}
4168
4169impl Debug for ParseOutput {
4170    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
4171        f.debug_struct("ParseOutput")
4172            .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
4173            .field(
4174                "args",
4175                &self
4176                    .args
4177                    .iter()
4178                    .map(|(a, w)| format!("{}: {w}", a.name))
4179                    .collect_vec(),
4180            )
4181            .field(
4182                "available_flags",
4183                &self
4184                    .available_flags
4185                    .iter()
4186                    .map(|(f, w)| format!("{f}: {w}"))
4187                    .collect_vec(),
4188            )
4189            .field(
4190                "flags",
4191                &self
4192                    .flags
4193                    .iter()
4194                    .map(|(f, w)| format!("{}: {w}", f.name))
4195                    .collect_vec(),
4196            )
4197            .field("flag_awaiting_value", &self.flag_awaiting_value)
4198            .field("errors", &self.errors)
4199            .field("external", &self.external)
4200            // Provenance, one line per token and one per fallback. This is the parser's
4201            // debug channel under `USAGE_LOG=trace`, so it is where a spec author looks
4202            // first — `usage explain` renders the same facts for a reader.
4203            .field(
4204                "tokens",
4205                &self.tokens.iter().map(render_token).collect_vec(),
4206            )
4207            .field(
4208                "origins",
4209                &self
4210                    .flag_origins
4211                    .iter()
4212                    .map(|(f, o)| format!("{}: {o:?}", f.name))
4213                    .chain(
4214                        self.arg_origins
4215                            .iter()
4216                            .map(|(a, o)| format!("{}: {o:?}", a.name)),
4217                    )
4218                    .collect_vec(),
4219            )
4220            .field("overridden_flags", &self.overridden_flags)
4221            .finish()
4222    }
4223}
4224
4225#[cfg(test)]
4226mod tests {
4227    use super::*;
4228    use crate::SpecFlagAction;
4229
4230    fn input(words: &[&str]) -> Vec<String> {
4231        words.iter().map(|word| (*word).to_string()).collect()
4232    }
4233
4234    #[test]
4235    fn a_declared_version_supplies_the_flag_the_help_page_lists() {
4236        // The page has always listed `-V, --version` wherever a `version` is declared,
4237        // and usage-argv and usage-go have always accepted both. This parser called them
4238        // unknown words, so the one implementation the corpus measures the others against
4239        // was the one that disagreed.
4240        let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ncmd \"run\"\n"
4241            .parse()
4242            .unwrap();
4243
4244        for spelling in ["--version", "-V"] {
4245            let err = parse(&spec, &input(&["ex", spelling]))
4246                .expect_err("answering with a version ends the parse");
4247            assert_eq!(err.to_string(), "1.2.3", "{spelling}");
4248        }
4249    }
4250
4251    #[test]
4252    fn the_supplied_version_flag_is_the_roots_alone() {
4253        // `version` describes the program, and the page lists the entry on the program's
4254        // own page only. A subcommand answering with it would be a claim no spec made.
4255        let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ncmd \"run\"\n"
4256            .parse()
4257            .unwrap();
4258
4259        let err = parse(&spec, &input(&["ex", "run", "--version"])).unwrap_err();
4260        assert_eq!(err.to_string(), "unexpected word: --version");
4261    }
4262
4263    #[test]
4264    fn no_declared_version_supplies_nothing() {
4265        // A `--version` answering with nothing is worse than one that is not there, which
4266        // is why the entry is conditional on the page and the spelling on the parse.
4267        let spec: Spec = "name \"ex\"\nbin \"ex\"\n".parse().unwrap();
4268
4269        for spelling in ["--version", "-V"] {
4270            let err = parse(&spec, &input(&["ex", spelling])).unwrap_err();
4271            assert_eq!(err.to_string(), format!("unexpected word: {spelling}"));
4272        }
4273    }
4274
4275    #[test]
4276    fn disable_version_flag_removes_the_supplied_spellings() {
4277        let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ndisable_version_flag #true\n"
4278            .parse()
4279            .unwrap();
4280
4281        for spelling in ["--version", "-V"] {
4282            let err = parse(&spec, &input(&["ex", spelling])).unwrap_err();
4283            assert_eq!(err.to_string(), format!("unexpected word: {spelling}"));
4284        }
4285    }
4286
4287    #[test]
4288    fn a_spelling_the_spec_spends_elsewhere_keeps_its_meaning() {
4289        // The page drops each supplied spelling the CLI claimed and keeps the other; the
4290        // parse agrees without being told, because a declared flag is matched first.
4291        let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-V --verbose\"\n"
4292            .parse()
4293            .unwrap();
4294
4295        let out = parse(&spec, &input(&["ex", "-V"])).expect("-V is the CLI's own flag");
4296        assert_eq!(out.flags.len(), 1);
4297
4298        let err = parse(&spec, &input(&["ex", "--version"])).unwrap_err();
4299        assert_eq!(err.to_string(), "1.2.3");
4300    }
4301
4302    #[test]
4303    fn the_supplied_spellings_split_the_two_version_texts() {
4304        // The same split `render_action_err` gives a declared version flag: the long
4305        // spelling prefers `long_version`, the short prefers the concise one.
4306        let spec: Spec =
4307            "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nlong_version \"1.2.3 (abcdef)\"\n"
4308                .parse()
4309                .unwrap();
4310
4311        assert_eq!(
4312            parse(&spec, &input(&["ex", "--version"]))
4313                .unwrap_err()
4314                .to_string(),
4315            "1.2.3 (abcdef)"
4316        );
4317        assert_eq!(
4318            parse(&spec, &input(&["ex", "-V"])).unwrap_err().to_string(),
4319            "1.2.3"
4320        );
4321    }
4322
4323    #[test]
4324    fn a_supplied_short_is_a_letter_a_bundle_may_contain() {
4325        // `-h` and `-V` are recognized letters that no spec declares, so a token holding
4326        // one beside a declared letter is a bundle. usage-lib alone read `-vh` as a word
4327        // naming nothing: usage-argv and usage-go both resolve the letter through the
4328        // same lookup that finds a declared short, and clap prints help for it too.
4329        let spec: Spec =
4330            "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\"\ncmd \"run\"\n"
4331                .parse()
4332                .unwrap();
4333
4334        for token in ["-vh", "-hv"] {
4335            let err = parse(&spec, &input(&["ex", token])).expect_err("help ends the parse");
4336            assert!(err.to_string().starts_with("ex 1.2.3"), "{token}: {err}");
4337        }
4338        for token in ["-vV", "-Vv"] {
4339            let err = parse(&spec, &input(&["ex", token])).expect_err("a version ends it too");
4340            assert_eq!(err.to_string(), "1.2.3", "{token}");
4341        }
4342    }
4343
4344    #[test]
4345    fn a_bundled_help_letter_asks_for_the_short_page() {
4346        // Whatever else shares the token: `-h` is the short spelling, and the letters
4347        // beside it say nothing about which page was asked for.
4348        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"
4349            .parse()
4350            .unwrap();
4351
4352        let short = parse(&spec, &input(&["ex", "-vh"]))
4353            .unwrap_err()
4354            .to_string();
4355        let long = parse(&spec, &input(&["ex", "--help"]))
4356            .unwrap_err()
4357            .to_string();
4358        assert!(short.contains("Be loud"), "{short}");
4359        assert!(!short.contains("at length"), "{short}");
4360        assert!(long.contains("at length"), "{long}");
4361    }
4362
4363    #[test]
4364    fn the_bundled_version_letter_is_the_roots_alone() {
4365        // The same rule the whole-token spelling follows, asked one letter at a time.
4366        let spec: Spec =
4367            "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\" global=#true\ncmd \"run\"\n"
4368                .parse()
4369                .unwrap();
4370
4371        let err = parse(&spec, &input(&["ex", "run", "-vV"])).unwrap_err();
4372        assert_eq!(err.to_string(), "unexpected word: -vV");
4373    }
4374
4375    #[test]
4376    fn a_declared_letter_keeps_its_meaning_inside_a_bundle() {
4377        // Nothing is supplied where the CLI spent the letter itself, so `-vh local` is
4378        // this spec's own `-h`, taking its value from the rest of the token.
4379        let spec: Spec =
4380            "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\"\nflag \"-h --host <host>\"\n"
4381                .parse()
4382                .unwrap();
4383
4384        let out = parse(&spec, &input(&["ex", "-vhlocal"])).expect("a bundle and its value");
4385        assert_eq!(out.flags.len(), 2);
4386        assert!(out
4387            .flags
4388            .iter()
4389            .any(|(flag, value)| flag.name == "host" && value.to_string() == "local"));
4390    }
4391
4392    #[test]
4393    fn disabling_help_takes_the_letter_back_out_of_the_bundle() {
4394        let spec: Spec =
4395            "name \"ex\"\nbin \"ex\"\ndisable_help_flag #true\nflag \"-v --verbose\"\n"
4396                .parse()
4397                .unwrap();
4398
4399        let err = parse(&spec, &input(&["ex", "-vh"])).unwrap_err();
4400        assert_eq!(err.to_string(), "unexpected word: -vh");
4401    }
4402
4403    #[test]
4404    fn a_letter_nothing_supplies_still_refuses_the_whole_bundle() {
4405        // The rule this must not weaken: `-az` is not a bundle at all, so `-a` is not set
4406        // on the way to discovering that `z` names nothing.
4407        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-a --all\"\narg \"[file]\"\n"
4408            .parse()
4409            .unwrap();
4410
4411        let out = parse(&spec, &input(&["ex", "-az"])).expect("it falls through to the argument");
4412        assert!(out.flags.is_empty(), "{:?}", out.flags);
4413        assert_eq!(out.args.len(), 1);
4414    }
4415
4416    fn spec_with_arg(arg: SpecArg) -> Spec {
4417        let cmd = SpecCommand::builder().name("test").arg(arg).build();
4418        Spec {
4419            name: "test".to_string(),
4420            bin: "test".to_string(),
4421            cmd,
4422            ..Default::default()
4423        }
4424    }
4425
4426    fn spec_with_flag(flag: SpecFlag) -> Spec {
4427        let cmd = SpecCommand::builder().name("test").flag(flag).build();
4428        Spec {
4429            name: "test".to_string(),
4430            bin: "test".to_string(),
4431            cmd,
4432            ..Default::default()
4433        }
4434    }
4435
4436    fn parse_with_env(
4437        spec: &Spec,
4438        words: &[&str],
4439        env: &[(&str, &str)],
4440    ) -> Result<ParseOutput, miette::Error> {
4441        let env = env
4442            .iter()
4443            .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
4444            .collect();
4445        Parser::new(spec).with_env(env).parse(&input(words))
4446    }
4447
4448    fn first_string_value(parsed: &ParseOutput) -> &str {
4449        if let Some(ParseValue::String(value)) = parsed.args.values().next() {
4450            return value;
4451        }
4452        if let Some(ParseValue::String(value)) = parsed.flags.values().next() {
4453            return value;
4454        }
4455        panic!("expected first parsed value to be ParseValue::String");
4456    }
4457
4458    #[test]
4459    fn custom_environment_parser_dispatches_executable_views() {
4460        let spec: Spec = r#"
4461bin "ex"
4462view "runner" root="run"
4463cmd "run" {
4464    flag "--token <token>" env="TOKEN"
4465}
4466        "#
4467        .parse()
4468        .unwrap();
4469        let parsed = Parser::new(&spec)
4470            .with_env([("TOKEN".to_string(), "secret".to_string())].into())
4471            .parse(&input(&["runner"]))
4472            .unwrap();
4473
4474        assert_eq!(parsed.cmd.name, "runner");
4475        assert!(parsed.flags.iter().any(|(flag, value)| flag.name == "token"
4476            && matches!(value, ParseValue::String(value) if value == "secret")));
4477    }
4478
4479    #[test]
4480    fn an_executable_view_keeps_the_hosts_version_action() {
4481        let spec: Spec = r#"
4482bin "ex"
4483version "1.2.3"
4484flag "-V --version" action="version"
4485flag "--verbose" global=#true
4486view "runner" root="run" globals=#true
4487cmd "run"
4488        "#
4489        .parse()
4490        .unwrap();
4491
4492        let error = Parser::new(&spec)
4493            .parse(&input(&["runner", "--version"]))
4494            .expect_err("the host version action should answer before view projection");
4495        assert_eq!(error.to_string(), "1.2.3");
4496
4497        let error = Parser::new(&spec)
4498            .parse(&input(&["runner", "--verbose", "--version"]))
4499            .expect_err("the host version action should remain after a carried global");
4500        assert_eq!(error.to_string(), "1.2.3");
4501    }
4502
4503    fn flag_string_value<'a>(parsed: &'a ParseOutput, name: &str) -> &'a str {
4504        let flag = parsed
4505            .flags
4506            .keys()
4507            .find(|flag| flag.name == name)
4508            .unwrap_or_else(|| panic!("expected flag {name}"));
4509        let value = parsed
4510            .flags
4511            .get(flag)
4512            .unwrap_or_else(|| panic!("expected value for flag {name}"));
4513        match value {
4514            ParseValue::String(value) => value,
4515            _ => panic!("expected flag {name} to be ParseValue::String"),
4516        }
4517    }
4518
4519    fn assert_parse_err(result: Result<ParseOutput, miette::Error>, expected: &str) {
4520        let err = result.expect_err("expected parser error");
4521        assert_eq!(format!("{err}"), expected);
4522    }
4523
4524    #[test]
4525    fn a_short_version_action_falls_back_to_the_long_version() {
4526        let flag = SpecFlag::builder()
4527            .short('R')
4528            .action(SpecFlagAction::Version)
4529            .build();
4530        let spec = Spec {
4531            name: "test".to_string(),
4532            bin: "test".to_string(),
4533            long_version: Some("1.2.3\ncommit abc123".to_string()),
4534            ..Default::default()
4535        };
4536        let UsageErr::Version(version) = render_action_err(&spec, &spec.cmd, &flag, "-R") else {
4537            panic!("expected version action")
4538        };
4539        assert_eq!(version, "1.2.3\ncommit abc123");
4540    }
4541
4542    #[cfg(feature = "unstable_choices_env")]
4543    fn spec_arg_choices_env(key: &str) -> Spec {
4544        spec_with_arg(
4545            SpecArg::builder()
4546                .name("env")
4547                .choices_env(key)
4548                .required(false)
4549                .build(),
4550        )
4551    }
4552
4553    #[cfg(feature = "unstable_choices_env")]
4554    fn spec_flag_choices_env(key: &str) -> Spec {
4555        spec_with_flag(
4556            SpecFlag::builder()
4557                .long("env")
4558                .arg(SpecArg::builder().name("env").choices_env(key).build())
4559                .build(),
4560        )
4561    }
4562
4563    #[test]
4564    fn test_parse() {
4565        let cmd = SpecCommand::builder()
4566            .name("test")
4567            .arg(SpecArg::builder().name("arg").build())
4568            .flag(SpecFlag::builder().long("flag").build())
4569            .build();
4570        let spec = Spec {
4571            name: "test".to_string(),
4572            bin: "test".to_string(),
4573            cmd,
4574            ..Default::default()
4575        };
4576        let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
4577        let parsed = parse(&spec, &input).unwrap();
4578        assert_eq!(parsed.cmds.len(), 1);
4579        assert_eq!(parsed.cmds[0].name, "test");
4580        assert_eq!(parsed.args.len(), 1);
4581        assert_eq!(parsed.flags.len(), 1);
4582        assert_eq!(parsed.available_flags.len(), 1);
4583    }
4584
4585    #[test]
4586    fn test_flag_overrides_last_occurrence_wins() {
4587        let spec: Spec = r#"
4588flag "--stdin" default=#true
4589flag "--file <file>" overrides="--stdin"
4590        "#
4591        .parse()
4592        .unwrap();
4593
4594        let file_wins = parse(&spec, &input(&["test", "--stdin", "--file", "input.txt"])).unwrap();
4595        assert_eq!(file_wins.flags.len(), 1);
4596        assert_eq!(flag_string_value(&file_wins, "file"), "input.txt");
4597        assert!(!file_wins.flags.keys().any(|flag| flag.name == "stdin"));
4598
4599        let stdin_wins = parse(&spec, &input(&["test", "--file", "input.txt", "--stdin"])).unwrap();
4600        assert_eq!(stdin_wins.flags.len(), 1);
4601        assert!(stdin_wins.flags.keys().any(|flag| flag.name == "stdin"));
4602        assert!(!stdin_wins.flags.keys().any(|flag| flag.name == "file"));
4603    }
4604
4605    #[test]
4606    fn test_flag_override_clears_pending_value() {
4607        let spec: Spec = r#"
4608flag "--file <file>" overrides="--stdin"
4609flag "--stdin"
4610arg "[input]"
4611        "#
4612        .parse()
4613        .unwrap();
4614
4615        let parsed = parse(&spec, &input(&["test", "--file", "--stdin", "input.txt"])).unwrap();
4616        assert_eq!(parsed.flags.len(), 1);
4617        assert!(parsed.flags.keys().any(|flag| flag.name == "stdin"));
4618        assert_eq!(first_string_value(&parsed), "input.txt");
4619    }
4620
4621    #[cfg(unix)]
4622    #[test]
4623    fn a_mount_on_the_root_discovers_subcommands() {
4624        // The root is a command like any other, so it can find its own subcommands
4625        // by running something. Uses `echo` rather than a fixture because resolving
4626        // a mount is what is being tested.
4627        let spec: Spec = r#"
4628name "ex"
4629bin "ex"
4630cmd "declared"
4631mount run="echo 'cmd \"discovered\"'"
4632"#
4633        .parse()
4634        .unwrap();
4635
4636        let out = parse(&spec, &["ex".to_string(), "discovered".to_string()]).unwrap();
4637        assert_eq!(out.cmd.name, "discovered");
4638    }
4639
4640    #[test]
4641    fn injected_mount_outputs_are_complete_and_never_fall_back_to_processes() {
4642        let spec: Spec = r#"
4643name "ex"
4644bin "ex"
4645mount run="this command must never run"
4646cmd "declared"
4647"#
4648        .parse()
4649        .unwrap();
4650
4651        Parser::new(&spec)
4652            .with_mount_outputs(HashMap::new())
4653            .parse(&input(&["ex", "declared"]))
4654            .expect("a declared command does not resolve the mount");
4655
4656        let error = Parser::new(&spec)
4657            .with_mount_outputs(HashMap::new())
4658            .parse(&input(&["ex", "discovered"]))
4659            .unwrap_err();
4660        assert!(
4661            error
4662                .to_string()
4663                .contains("No injected output was provided for mount command"),
4664            "{error}"
4665        );
4666    }
4667
4668    #[cfg(unix)]
4669    #[test]
4670    fn completion_sees_root_mounted_commands_with_nothing_typed() {
4671        // The case a root mount exists for. `mycli <tab>` has no word to trigger
4672        // discovery with, so a completion has to resolve up front or the mounted
4673        // commands are never offered.
4674        let spec: Spec = r#"
4675name "ex"
4676bin "ex"
4677cmd "declared"
4678mount run="echo 'cmd \"discovered\"'"
4679"#
4680        .parse()
4681        .unwrap();
4682
4683        let out = parse_partial(&spec, &["ex".to_string()]).unwrap();
4684        assert!(
4685            out.cmd.subcommands.contains_key("discovered"),
4686            "a completion should see mounted commands; got {:?}",
4687            out.cmd.subcommands.keys().collect::<Vec<_>>()
4688        );
4689    }
4690
4691    #[cfg(unix)]
4692    #[test]
4693    fn completion_and_execution_agree_about_discovery() {
4694        // Offering a command that a real parse would hand to the default instead is
4695        // worse than not offering it, so the gate applies to both paths. The mount
4696        // fails if it runs, which is how both halves are checked at once.
4697        let spec: Spec = r#"
4698name "ex"
4699bin "ex"
4700default_subcommand "run"
4701cmd "run" {
4702  arg "<task>"
4703}
4704mount run="exit 1"
4705"#
4706        .parse()
4707        .unwrap();
4708
4709        let out = parse_partial(&spec, &["ex".to_string()]).unwrap();
4710        assert!(
4711            !out.cmd.subcommands.contains_key("discovered"),
4712            "a completion must not offer what execution will not route"
4713        );
4714
4715        let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
4716        assert_eq!(out.cmd.name, "run");
4717    }
4718
4719    #[cfg(unix)]
4720    #[test]
4721    fn a_default_subcommand_outranks_discovery() {
4722        // The default already says what an unmatched word means, and says it for
4723        // free. The mount fails if it runs, so parsing proves discovery was skipped.
4724        let spec: Spec = r#"
4725name "ex"
4726bin "ex"
4727default_subcommand "run"
4728cmd "run" {
4729  arg "<task>"
4730}
4731mount run="exit 1"
4732"#
4733        .parse()
4734        .unwrap();
4735
4736        let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
4737        assert_eq!(out.cmd.name, "run");
4738    }
4739
4740    #[cfg(unix)]
4741    #[test]
4742    fn a_mount_may_ask_to_outrank_the_default() {
4743        // Opting in, and paying for it: discovery runs first, so a discovered
4744        // command wins over the fallback.
4745        let spec: Spec = r#"
4746name "ex"
4747bin "ex"
4748default_subcommand "run"
4749cmd "run" {
4750  arg "<task>"
4751}
4752mount run="echo 'cmd \"discovered\"'" overrides_default=#true
4753"#
4754        .parse()
4755        .unwrap();
4756
4757        let out = parse(&spec, &["ex".to_string(), "discovered".to_string()]).unwrap();
4758        assert_eq!(out.cmd.name, "discovered");
4759
4760        // A word it does not know still reaches the default.
4761        let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
4762        assert_eq!(out.cmd.name, "run");
4763    }
4764
4765    #[cfg(unix)]
4766    #[test]
4767    fn a_flag_does_not_run_the_mount() {
4768        // A flag matches no subcommand, which would have been enough to trigger
4769        // discovery — so `ex --help` spawned a process. The mount fails if it runs,
4770        // so parsing at all is the proof that it did not.
4771        let spec: Spec = r#"
4772name "ex"
4773bin "ex"
4774flag "--verbose"
4775cmd "declared"
4776mount run="exit 1"
4777"#
4778        .parse()
4779        .unwrap();
4780
4781        let out = parse(&spec, &["ex".to_string(), "--verbose".to_string()]).unwrap();
4782        assert_eq!(out.cmd.name, "ex");
4783    }
4784
4785    #[cfg(unix)]
4786    #[test]
4787    fn a_declared_subcommand_does_not_run_the_mount() {
4788        // The mount would fail if it ran, so this parsing at all is the proof that
4789        // discovery is skipped when the word is already known. Worth pinning: a root
4790        // mount that resolved eagerly would spawn a process on every invocation.
4791        let spec: Spec = r#"
4792name "ex"
4793bin "ex"
4794cmd "declared"
4795mount run="exit 1"
4796"#
4797        .parse()
4798        .unwrap();
4799
4800        let out = parse(&spec, &["ex".to_string(), "declared".to_string()]).unwrap();
4801        assert_eq!(out.cmd.name, "declared");
4802    }
4803
4804    #[test]
4805    fn a_root_mount_survives_being_written_out() {
4806        let spec: Spec = "name \"ex\"\nbin \"ex\"\nmount run=\"ex plugins --usage\"\n"
4807            .parse()
4808            .unwrap();
4809        assert_eq!(spec.cmd.mounts.len(), 1);
4810
4811        let reparsed: Spec = spec.to_string().parse().unwrap();
4812        assert_eq!(reparsed.cmd.mounts.len(), 1, "written:\n{spec}");
4813        assert_eq!(reparsed.cmd.mounts[0].run, "ex plugins --usage");
4814    }
4815
4816    #[test]
4817    fn test_mount_prefix_applies_flag_overrides() {
4818        let stdin = Arc::new(
4819            SpecFlag::builder()
4820                .name("stdin")
4821                .long("stdin")
4822                .global(true)
4823                .build(),
4824        );
4825        let file = Arc::new(
4826            SpecFlag::builder()
4827                .name("file")
4828                .long("file")
4829                .arg(SpecArg::builder().name("file").build())
4830                .global(true)
4831                .overrides_with(vec!["--stdin".to_string()])
4832                .build(),
4833        );
4834        let mut prefix_flags = vec![(stdin, vec!["--stdin".to_string()])];
4835
4836        apply_prefix_flag_overrides(&mut prefix_flags, Arc::clone(&file));
4837        prefix_flags.push((file, vec!["--file".to_string(), "input.txt".to_string()]));
4838
4839        assert_eq!(mount_prefix_words(&prefix_flags), ["--file", "input.txt"]);
4840    }
4841
4842    #[test]
4843    fn test_flag_override_suppresses_env_value() {
4844        let spec: Spec = r#"
4845flag "--stdin" env="USE_STDIN"
4846flag "--file <file>" overrides="--stdin"
4847        "#
4848        .parse()
4849        .unwrap();
4850
4851        let parsed = parse_with_env(
4852            &spec,
4853            &["test", "--file", "input.txt"],
4854            &[("USE_STDIN", "true")],
4855        )
4856        .unwrap();
4857        assert_eq!(parsed.flags.len(), 1);
4858        assert_eq!(flag_string_value(&parsed, "file"), "input.txt");
4859    }
4860
4861    #[test]
4862    fn test_flag_override_suppresses_required_check() {
4863        let spec: Spec = r#"
4864flag "--stdin" required=#true
4865flag "--file <file>" overrides="--stdin"
4866        "#
4867        .parse()
4868        .unwrap();
4869
4870        let parsed = parse(&spec, &input(&["test", "--file", "input.txt"])).unwrap();
4871        assert_eq!(parsed.flags.len(), 1);
4872        assert_eq!(flag_string_value(&parsed, "file"), "input.txt");
4873    }
4874
4875    #[test]
4876    fn test_flag_required_if() {
4877        let spec: Spec = r#"
4878flag "--dir <dir>"
4879flag "--file <file>" required_if="--dir"
4880        "#
4881        .parse()
4882        .unwrap();
4883
4884        parse(&spec, &input(&["test"])).unwrap();
4885        assert_parse_err(
4886            parse(&spec, &input(&["test", "--dir", "src"])),
4887            "Missing required flag: --file <file>",
4888        );
4889        parse(
4890            &spec,
4891            &input(&["test", "--dir", "src", "--file", "input.txt"]),
4892        )
4893        .unwrap();
4894    }
4895
4896    #[test]
4897    fn test_flag_required_unless() {
4898        let spec: Spec = r#"
4899flag "--stdin"
4900flag "--file <file>" required_unless="--stdin"
4901        "#
4902        .parse()
4903        .unwrap();
4904
4905        assert_parse_err(
4906            parse(&spec, &input(&["test"])),
4907            "Missing required flag: --file <file>",
4908        );
4909        parse(&spec, &input(&["test", "--stdin"])).unwrap();
4910        parse(&spec, &input(&["test", "--file", "input.txt"])).unwrap();
4911    }
4912
4913    #[test]
4914    fn complete_required_relationship_truth_tables() {
4915        let spec: Spec = r#"
4916name "ex"
4917bin "ex"
4918flag "--mode <mode>"
4919flag "--scope <scope>"
4920flag "--token <token>" {
4921    required_if_eq "--mode" "remote"
4922}
4923flag "--approval <approval>" {
4924    required_if_eq_all "--mode" "remote" "--scope" "global"
4925}
4926flag "--input <input>" {
4927    required_unless "--stdin" "--file"
4928}
4929flag "--checksum <checksum>" {
4930    required_unless_all "--stdin" "--file"
4931}
4932flag "--stdin"
4933flag "--file <file>"
4934arg "[request]" {
4935    requires "--mode" "--scope"
4936}
4937"#
4938        .parse()
4939        .unwrap();
4940        let parse_args = |args: &[&str]| {
4941            parse(
4942                &spec,
4943                &args
4944                    .iter()
4945                    .map(|arg| (*arg).to_string())
4946                    .collect::<Vec<_>>(),
4947            )
4948        };
4949
4950        assert!(parse_args(&["ex", "--mode", "remote", "--stdin"]).is_err());
4951        assert!(parse_args(&[
4952            "ex", "--mode", "remote", "--token", "secret", "--scope", "global", "--stdin",
4953        ])
4954        .is_err());
4955        parse_args(&[
4956            "ex",
4957            "--mode",
4958            "remote",
4959            "--token",
4960            "secret",
4961            "--scope",
4962            "global",
4963            "--approval",
4964            "yes",
4965            "--stdin",
4966            "--file",
4967            "in",
4968        ])
4969        .unwrap();
4970        parse_args(&[
4971            "ex",
4972            "--mode",
4973            "local",
4974            "--scope",
4975            "project",
4976            "--stdin",
4977            "--checksum",
4978            "sum",
4979            "request.json",
4980        ])
4981        .unwrap();
4982
4983        let reparsed: Spec = spec.to_string().parse().unwrap();
4984        assert_eq!(reparsed.cmd.flags[2].required_if_eq.len(), 1);
4985        assert_eq!(reparsed.cmd.flags[3].required_if_eq_all.len(), 2);
4986        assert_eq!(reparsed.cmd.flags[5].required_unless_all.len(), 2);
4987        assert_eq!(reparsed.cmd.args[0].requires.len(), 2);
4988    }
4989
4990    #[test]
4991    fn test_conditional_requirements_treat_env_as_explicit() {
4992        let spec: Spec = r#"
4993flag "--dir <dir>" env="INPUT_DIR"
4994flag "--stdin" env="USE_STDIN"
4995flag "--file <file>" required_if="--dir" required_unless="--stdin"
4996        "#
4997        .parse()
4998        .unwrap();
4999
5000        assert_parse_err(
5001            parse_with_env(&spec, &["test"], &[("INPUT_DIR", "src")]),
5002            "Missing required flag: --file <file>",
5003        );
5004        parse_with_env(&spec, &["test"], &[("USE_STDIN", "true")]).unwrap();
5005    }
5006
5007    #[test]
5008    fn test_custom_env_does_not_fall_back_to_process_env() {
5009        assert!(std::env::var("PATH").is_ok());
5010        let spec: Spec = r#"flag "--file <file>" env="PATH" required=#true"#.parse().unwrap();
5011
5012        assert_parse_err(
5013            parse_with_env(&spec, &["test"], &[]),
5014            "Missing required flag: --file <file>",
5015        );
5016    }
5017
5018    #[test]
5019    fn test_conditional_requirements_ignore_defaults_on_condition_flags() {
5020        let spec: Spec = r#"
5021flag "--dir <dir>" default="src"
5022flag "--file <file>" required_if="--dir"
5023        "#
5024        .parse()
5025        .unwrap();
5026
5027        parse(&spec, &input(&["test"])).unwrap();
5028    }
5029
5030    #[test]
5031    fn test_conditional_requirements_see_overridden_flags_as_absent() {
5032        let spec: Spec = r#"
5033flag "--stdin"
5034flag "--dir <dir>" overrides="--stdin"
5035flag "--file <file>" required_unless="--stdin"
5036        "#
5037        .parse()
5038        .unwrap();
5039
5040        assert_parse_err(
5041            parse(&spec, &input(&["test", "--stdin", "--dir", "src"])),
5042            "Missing required flag: --file <file>",
5043        );
5044    }
5045
5046    #[test]
5047    fn short_flag_is_one_character_not_one_byte() {
5048        // A short is declared and read by character. Counting bytes instead either
5049        // refuses the declaration or slices the token inside the character, and clap
5050        // — which many specs are generated from — accepts shorts like this one.
5051        let spec = spec_with_flag(
5052            SpecFlag::builder()
5053                .short('磨')
5054                .long("polish")
5055                .arg(SpecArg::builder().name("opt").build())
5056                .build(),
5057        );
5058        let attached = Parser::new(&spec)
5059            .parse(&input(&["test", "-磨VALUE"]))
5060            .unwrap();
5061        assert_eq!(flag_string_value(&attached, "polish"), "VALUE");
5062        let detached = Parser::new(&spec)
5063            .parse(&input(&["test", "-磨", "V"]))
5064            .unwrap();
5065        assert_eq!(flag_string_value(&detached, "polish"), "V");
5066    }
5067
5068    #[test]
5069    fn test_as_env() {
5070        let cmd = SpecCommand::builder()
5071            .name("test")
5072            .arg(SpecArg::builder().name("arg").build())
5073            .flag(SpecFlag::builder().long("flag").build())
5074            .flag(
5075                SpecFlag::builder()
5076                    .long("force")
5077                    .negate("--no-force")
5078                    .build(),
5079            )
5080            .build();
5081        let spec = Spec {
5082            name: "test".to_string(),
5083            bin: "test".to_string(),
5084            cmd,
5085            ..Default::default()
5086        };
5087        let input = vec![
5088            "test".to_string(),
5089            "--flag".to_string(),
5090            "--no-force".to_string(),
5091        ];
5092        let parsed = parse(&spec, &input).unwrap();
5093        let env = parsed.as_env();
5094        assert_eq!(env.len(), 2);
5095        assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
5096        assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
5097    }
5098
5099    #[test]
5100    fn test_arg_env_var() {
5101        let cmd = SpecCommand::builder()
5102            .name("test")
5103            .arg(
5104                SpecArg::builder()
5105                    .name("input")
5106                    .env("TEST_ARG_INPUT")
5107                    .required(true)
5108                    .build(),
5109            )
5110            .build();
5111        let spec = Spec {
5112            name: "test".to_string(),
5113            bin: "test".to_string(),
5114            cmd,
5115            ..Default::default()
5116        };
5117
5118        // Set env var
5119        std::env::set_var("TEST_ARG_INPUT", "test_file.txt");
5120
5121        let input = vec!["test".to_string()];
5122        let parsed = parse(&spec, &input).unwrap();
5123
5124        assert_eq!(parsed.args.len(), 1);
5125        let arg = parsed.args.keys().next().unwrap();
5126        assert_eq!(arg.name, "input");
5127        let value = parsed.args.values().next().unwrap();
5128        assert_eq!(value.to_string(), "test_file.txt");
5129
5130        // Clean up
5131        std::env::remove_var("TEST_ARG_INPUT");
5132    }
5133
5134    #[test]
5135    fn test_flag_env_var_with_arg() {
5136        let cmd = SpecCommand::builder()
5137            .name("test")
5138            .flag(
5139                SpecFlag::builder()
5140                    .long("output")
5141                    .env("TEST_FLAG_OUTPUT")
5142                    .arg(SpecArg::builder().name("file").build())
5143                    .build(),
5144            )
5145            .build();
5146        let spec = Spec {
5147            name: "test".to_string(),
5148            bin: "test".to_string(),
5149            cmd,
5150            ..Default::default()
5151        };
5152
5153        // Set env var
5154        std::env::set_var("TEST_FLAG_OUTPUT", "output.txt");
5155
5156        let input = vec!["test".to_string()];
5157        let parsed = parse(&spec, &input).unwrap();
5158
5159        assert_eq!(parsed.flags.len(), 1);
5160        let flag = parsed.flags.keys().next().unwrap();
5161        assert_eq!(flag.name, "output");
5162        let value = parsed.flags.values().next().unwrap();
5163        assert_eq!(value.to_string(), "output.txt");
5164
5165        // Clean up
5166        std::env::remove_var("TEST_FLAG_OUTPUT");
5167    }
5168
5169    #[test]
5170    fn test_flag_env_var_boolean() {
5171        let cmd = SpecCommand::builder()
5172            .name("test")
5173            .flag(
5174                SpecFlag::builder()
5175                    .long("verbose")
5176                    .env("TEST_FLAG_VERBOSE")
5177                    .build(),
5178            )
5179            .build();
5180        let spec = Spec {
5181            name: "test".to_string(),
5182            bin: "test".to_string(),
5183            cmd,
5184            ..Default::default()
5185        };
5186
5187        // Set env var to true
5188        std::env::set_var("TEST_FLAG_VERBOSE", "true");
5189
5190        let input = vec!["test".to_string()];
5191        let parsed = parse(&spec, &input).unwrap();
5192
5193        assert_eq!(parsed.flags.len(), 1);
5194        let flag = parsed.flags.keys().next().unwrap();
5195        assert_eq!(flag.name, "verbose");
5196        let value = parsed.flags.values().next().unwrap();
5197        assert_eq!(value.to_string(), "true");
5198
5199        // Clean up
5200        std::env::remove_var("TEST_FLAG_VERBOSE");
5201    }
5202
5203    #[test]
5204    fn test_env_var_precedence() {
5205        // CLI args should take precedence over env vars
5206        let cmd = SpecCommand::builder()
5207            .name("test")
5208            .arg(
5209                SpecArg::builder()
5210                    .name("input")
5211                    .env("TEST_PRECEDENCE_INPUT")
5212                    .required(true)
5213                    .build(),
5214            )
5215            .build();
5216        let spec = Spec {
5217            name: "test".to_string(),
5218            bin: "test".to_string(),
5219            cmd,
5220            ..Default::default()
5221        };
5222
5223        // Set env var
5224        std::env::set_var("TEST_PRECEDENCE_INPUT", "env_file.txt");
5225
5226        let input = vec!["test".to_string(), "cli_file.txt".to_string()];
5227        let parsed = parse(&spec, &input).unwrap();
5228
5229        assert_eq!(parsed.args.len(), 1);
5230        let value = parsed.args.values().next().unwrap();
5231        // CLI arg should take precedence
5232        assert_eq!(value.to_string(), "cli_file.txt");
5233
5234        // Clean up
5235        std::env::remove_var("TEST_PRECEDENCE_INPUT");
5236    }
5237
5238    #[test]
5239    fn test_flag_var_true_with_single_default() {
5240        // When var=true and default="bar", the default should be MultiString(["bar"])
5241        let cmd = SpecCommand::builder()
5242            .name("test")
5243            .flag(
5244                SpecFlag::builder()
5245                    .long("foo")
5246                    .var(true)
5247                    .arg(SpecArg::builder().name("foo").build())
5248                    .default_value("bar")
5249                    .build(),
5250            )
5251            .build();
5252        let spec = Spec {
5253            name: "test".to_string(),
5254            bin: "test".to_string(),
5255            cmd,
5256            ..Default::default()
5257        };
5258
5259        // User doesn't provide the flag
5260        let input = vec!["test".to_string()];
5261        let parsed = parse(&spec, &input).unwrap();
5262
5263        assert_eq!(parsed.flags.len(), 1);
5264        let flag = parsed.flags.keys().next().unwrap();
5265        assert_eq!(flag.name, "foo");
5266        let value = parsed.flags.values().next().unwrap();
5267        // Should be MultiString, not String
5268        match value {
5269            ParseValue::MultiString(v) => {
5270                assert_eq!(v.len(), 1);
5271                assert_eq!(v[0], "bar");
5272            }
5273            _ => panic!("Expected MultiString, got {:?}", value),
5274        }
5275    }
5276
5277    #[test]
5278    fn test_flag_var_true_with_multiple_defaults() {
5279        // When var=true and multiple defaults, should return MultiString(["xyz", "bar"])
5280        let cmd = SpecCommand::builder()
5281            .name("test")
5282            .flag(
5283                SpecFlag::builder()
5284                    .long("foo")
5285                    .var(true)
5286                    .arg(SpecArg::builder().name("foo").build())
5287                    .default_values(["xyz", "bar"])
5288                    .build(),
5289            )
5290            .build();
5291        let spec = Spec {
5292            name: "test".to_string(),
5293            bin: "test".to_string(),
5294            cmd,
5295            ..Default::default()
5296        };
5297
5298        // User doesn't provide the flag
5299        let input = vec!["test".to_string()];
5300        let parsed = parse(&spec, &input).unwrap();
5301
5302        assert_eq!(parsed.flags.len(), 1);
5303        let value = parsed.flags.values().next().unwrap();
5304        // Should be MultiString with both values
5305        match value {
5306            ParseValue::MultiString(v) => {
5307                assert_eq!(v.len(), 2);
5308                assert_eq!(v[0], "xyz");
5309                assert_eq!(v[1], "bar");
5310            }
5311            _ => panic!("Expected MultiString, got {:?}", value),
5312        }
5313    }
5314
5315    #[test]
5316    fn test_flag_var_false_with_default_remains_string() {
5317        // When var=false (default), the default should still be String("bar")
5318        let cmd = SpecCommand::builder()
5319            .name("test")
5320            .flag(
5321                SpecFlag::builder()
5322                    .long("foo")
5323                    .var(false) // Default behavior
5324                    .arg(SpecArg::builder().name("foo").build())
5325                    .default_value("bar")
5326                    .build(),
5327            )
5328            .build();
5329        let spec = Spec {
5330            name: "test".to_string(),
5331            bin: "test".to_string(),
5332            cmd,
5333            ..Default::default()
5334        };
5335
5336        // User doesn't provide the flag
5337        let input = vec!["test".to_string()];
5338        let parsed = parse(&spec, &input).unwrap();
5339
5340        assert_eq!(parsed.flags.len(), 1);
5341        let value = parsed.flags.values().next().unwrap();
5342        // Should be String, not MultiString
5343        match value {
5344            ParseValue::String(s) => {
5345                assert_eq!(s, "bar");
5346            }
5347            _ => panic!("Expected String, got {:?}", value),
5348        }
5349    }
5350
5351    #[test]
5352    fn test_arg_var_true_with_single_default() {
5353        // When arg has var=true and default="bar", the default should be MultiString(["bar"])
5354        let cmd = SpecCommand::builder()
5355            .name("test")
5356            .arg(
5357                SpecArg::builder()
5358                    .name("files")
5359                    .var(true)
5360                    .default_value("default.txt")
5361                    .required(false)
5362                    .build(),
5363            )
5364            .build();
5365        let spec = Spec {
5366            name: "test".to_string(),
5367            bin: "test".to_string(),
5368            cmd,
5369            ..Default::default()
5370        };
5371
5372        // User doesn't provide the arg
5373        let input = vec!["test".to_string()];
5374        let parsed = parse(&spec, &input).unwrap();
5375
5376        assert_eq!(parsed.args.len(), 1);
5377        let value = parsed.args.values().next().unwrap();
5378        // Should be MultiString, not String
5379        match value {
5380            ParseValue::MultiString(v) => {
5381                assert_eq!(v.len(), 1);
5382                assert_eq!(v[0], "default.txt");
5383            }
5384            _ => panic!("Expected MultiString, got {:?}", value),
5385        }
5386    }
5387
5388    #[test]
5389    fn test_arg_var_true_with_multiple_defaults() {
5390        // When arg has var=true and multiple defaults
5391        let cmd = SpecCommand::builder()
5392            .name("test")
5393            .arg(
5394                SpecArg::builder()
5395                    .name("files")
5396                    .var(true)
5397                    .default_values(["file1.txt", "file2.txt"])
5398                    .required(false)
5399                    .build(),
5400            )
5401            .build();
5402        let spec = Spec {
5403            name: "test".to_string(),
5404            bin: "test".to_string(),
5405            cmd,
5406            ..Default::default()
5407        };
5408
5409        // User doesn't provide the arg
5410        let input = vec!["test".to_string()];
5411        let parsed = parse(&spec, &input).unwrap();
5412
5413        assert_eq!(parsed.args.len(), 1);
5414        let value = parsed.args.values().next().unwrap();
5415        // Should be MultiString with both values
5416        match value {
5417            ParseValue::MultiString(v) => {
5418                assert_eq!(v.len(), 2);
5419                assert_eq!(v[0], "file1.txt");
5420                assert_eq!(v[1], "file2.txt");
5421            }
5422            _ => panic!("Expected MultiString, got {:?}", value),
5423        }
5424    }
5425
5426    #[test]
5427    fn test_arg_var_false_with_default_remains_string() {
5428        // When arg has var=false (default), the default should still be String
5429        let cmd = SpecCommand::builder()
5430            .name("test")
5431            .arg(
5432                SpecArg::builder()
5433                    .name("file")
5434                    .var(false)
5435                    .default_value("default.txt")
5436                    .required(false)
5437                    .build(),
5438            )
5439            .build();
5440        let spec = Spec {
5441            name: "test".to_string(),
5442            bin: "test".to_string(),
5443            cmd,
5444            ..Default::default()
5445        };
5446
5447        // User doesn't provide the arg
5448        let input = vec!["test".to_string()];
5449        let parsed = parse(&spec, &input).unwrap();
5450
5451        assert_eq!(parsed.args.len(), 1);
5452        let value = parsed.args.values().next().unwrap();
5453        // Should be String, not MultiString
5454        match value {
5455            ParseValue::String(s) => {
5456                assert_eq!(s, "default.txt");
5457            }
5458            _ => panic!("Expected String, got {:?}", value),
5459        }
5460    }
5461
5462    #[test]
5463    fn test_scalar_defaults_validate_only_first_default_choice() {
5464        let specs = [
5465            spec_with_arg(
5466                SpecArg::builder()
5467                    .name("env")
5468                    .var(false)
5469                    .default_values(["dev", "prod"])
5470                    .choices(["dev"])
5471                    .required(false)
5472                    .build(),
5473            ),
5474            spec_with_flag(
5475                SpecFlag::builder()
5476                    .long("env")
5477                    .arg(
5478                        SpecArg::builder()
5479                            .name("env")
5480                            .default_values(["dev", "prod"])
5481                            .choices(["dev"])
5482                            .build(),
5483                    )
5484                    .build(),
5485            ),
5486        ];
5487
5488        for spec in specs {
5489            let parsed = parse(&spec, &input(&["test"])).unwrap();
5490            assert_eq!(first_string_value(&parsed), "dev");
5491        }
5492    }
5493
5494    #[test]
5495    fn a_delimiter_turns_one_word_into_several_values() {
5496        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--tags <tag>\" var=#true delimiter=\",\"\narg \"[files]...\" var=#true delimiter=\":\"\n"
5497            .parse()
5498            .unwrap();
5499
5500        let parsed = parse(&spec, &input(&["ex", "--tags", "a,b,c", "x:y"])).unwrap();
5501        let multi = |value: &ParseValue| match value {
5502            ParseValue::MultiString(values) => values.clone(),
5503            other => panic!("expected several values, got {other:?}"),
5504        };
5505        let tags = parsed
5506            .flags
5507            .iter()
5508            .find(|(f, _)| f.name == "tags")
5509            .map(|(_, v)| v)
5510            .unwrap();
5511        assert_eq!(multi(tags), vec!["a", "b", "c"]);
5512        assert_eq!(multi(parsed.args.values().next().unwrap()), vec!["x", "y"]);
5513    }
5514
5515    #[test]
5516    fn a_positional_splits_before_its_choices_are_asked() {
5517        // The flag path did this and the positional path did not, so a word whose parts
5518        // were all choices was rejected as one value, and a bad half was reported as the
5519        // whole word.
5520        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[paths]...\" var=#true delimiter=\":\" {\n  choices \"src\" \"docs\"\n}\n"
5521            .parse()
5522            .unwrap();
5523
5524        parse(&spec, &input(&["ex", "src:docs"])).expect("both halves are choices");
5525
5526        let err = parse(&spec, &input(&["ex", "src:nowhere"])).unwrap_err();
5527        let message = err.to_string();
5528        assert!(message.contains("nowhere"), "{message}");
5529        assert!(
5530            !message.contains("src:nowhere"),
5531            "the bad half should be named, not the whole word: {message}"
5532        );
5533    }
5534
5535    #[test]
5536    fn a_split_value_is_counted_and_judged_as_values() {
5537        // Split during the parse rather than after it, so everything downstream sees the
5538        // values the user meant rather than the words they typed: `choices` judges each
5539        // one, and the bounds count them.
5540        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <e>\" var=#true delimiter=\",\" var_max=2 {\n  choices \"dev\" \"prod\"\n}\n"
5541            .parse()
5542            .unwrap();
5543
5544        parse(&spec, &input(&["ex", "--env", "dev,prod"])).expect("two values, both allowed");
5545        let err = parse(&spec, &input(&["ex", "--env", "dev,staging"])).unwrap_err();
5546        assert!(err.to_string().contains("staging"), "{err}");
5547        assert!(
5548            parse(&spec, &input(&["ex", "--env", "dev,prod,dev"])).is_err(),
5549            "three values should breach var_max=2"
5550        );
5551    }
5552
5553    #[test]
5554    fn a_split_bound_counts_one_occurrence_at_a_time() {
5555        // The bound on a variadic flag *argument* is what one occurrence may take. Without a
5556        // delimiter the collection simply stops at it, so it could never be exceeded; a word
5557        // carrying several values can carry an occurrence past it in one step, and that is
5558        // the only way this bound is ever breached.
5559        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--include <pattern>...\" delimiter=\",\" {\n  arg \"<pattern>...\" var=#true var_max=2\n}\n"
5560            .parse()
5561            .unwrap();
5562
5563        parse(&spec, &input(&["ex", "--include", "a,b"])).expect("exactly the bound is fine");
5564        assert!(
5565            parse(&spec, &input(&["ex", "--include", "a,b,c"])).is_err(),
5566            "three values out of one word is still three values"
5567        );
5568        // The rule the corpus documents for plain words, on split ones: a second occurrence
5569        // starts counting again rather than adding to the first.
5570        parse(
5571            &spec,
5572            &input(&["ex", "--include", "a,b", "--include", "c,d"]),
5573        )
5574        .expect("two per occurrence, twice, is within the bound");
5575    }
5576
5577    #[test]
5578    fn a_nested_minimum_is_checked_once_per_flag_occurrence() {
5579        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pair <value>...\" {\n  arg \"<value>...\" var=#true var_min=2 var_max=2\n}\n"
5580            .parse()
5581            .unwrap();
5582
5583        parse(
5584            &spec,
5585            &input(&["ex", "--pair", "a", "b", "--pair", "c", "d"]),
5586        )
5587        .expect("each occurrence satisfies the bound independently");
5588
5589        let error = parse(&spec, &input(&["ex", "--pair", "a", "--pair", "b", "c"])).unwrap_err();
5590        assert!(
5591            error
5592                .to_string()
5593                .contains("requires at least 2 value(s), got 1"),
5594            "{error:?}"
5595        );
5596    }
5597
5598    #[test]
5599    fn an_exclusive_flag_has_to_be_alone() {
5600        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--verbose\"\narg \"[target]\"\n"
5601            .parse()
5602            .unwrap();
5603
5604        parse(&spec, &input(&["ex", "--dump"])).expect("alone is the point");
5605
5606        // Any other flag.
5607        let err = parse(&spec, &input(&["ex", "--dump", "--verbose"])).unwrap_err();
5608        assert!(err.to_string().contains("on its own"), "{err}");
5609
5610        // And a positional, which is what makes this more than a conflict with every
5611        // other flag.
5612        let err = parse(&spec, &input(&["ex", "--dump", "t"])).unwrap_err();
5613        assert!(err.to_string().contains("on its own"), "{err}");
5614
5615        // Not given, so it imposes nothing.
5616        parse(&spec, &input(&["ex", "--verbose", "t"])).expect("without it, nothing changes");
5617    }
5618
5619    #[test]
5620    fn an_exclusive_flag_is_not_disturbed_by_a_default() {
5621        // Only what was supplied counts, as `conflicts` reads it. A default counting as
5622        // company would make an exclusive flag unusable on any command that has one.
5623        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--jobs <n>\" default=\"4\"\n"
5624            .parse()
5625            .unwrap();
5626
5627        parse(&spec, &input(&["ex", "--dump"])).expect("a default is nobody saying anything");
5628        assert!(parse(&spec, &input(&["ex", "--dump", "--jobs", "8"])).is_err());
5629    }
5630
5631    #[test]
5632    fn an_exclusive_flag_bypasses_required_siblings() {
5633        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--out <path>\" required=#true\narg \"<target>\"\n"
5634            .parse()
5635            .unwrap();
5636
5637        parse(&spec, &input(&["ex", "--dump"]))
5638            .expect("exclusive is the command's requiredness escape");
5639        assert!(parse(
5640            &spec,
5641            &input(&["ex", "--dump", "--out", "somewhere", "target"])
5642        )
5643        .is_err());
5644    }
5645
5646    #[test]
5647    fn an_environment_value_counts_for_an_exclusive_flag() {
5648        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--out <path>\" env=\"EX_OUT\"\n"
5649            .parse()
5650            .unwrap();
5651
5652        assert!(parse_with_env(&spec, &["ex", "--dump"], &[("EX_OUT", "somewhere")]).is_err());
5653        parse_with_env(&spec, &["ex", "--dump"], &[]).expect("without the value it is alone");
5654    }
5655
5656    #[test]
5657    fn a_selected_subcommand_counts_for_an_ancestor_exclusive_flag() {
5658        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--version\" global=#true exclusive=#true\ncmd \"run\"\n"
5659            .parse()
5660            .unwrap();
5661
5662        parse(&spec, &input(&["ex", "--version"])).expect("alone is allowed");
5663        assert!(parse(&spec, &input(&["ex", "--version", "run"])).is_err());
5664    }
5665
5666    #[test]
5667    fn a_child_exclusive_flag_is_not_mistaken_for_a_same_named_parent_flag() {
5668        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n}\n"
5669            .parse()
5670            .unwrap();
5671
5672        parse(&spec, &input(&["ex", "run", "--clean"]))
5673            .expect("the child flag is alone within the child command");
5674        assert!(
5675            parse(&spec, &input(&["ex", "--clean", "run"])).is_err(),
5676            "the parent flag still conflicts with selecting the child"
5677        );
5678    }
5679
5680    #[test]
5681    fn a_child_local_exclusive_redeclaration_belongs_to_the_child() {
5682        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n}\n"
5683            .parse()
5684            .unwrap();
5685
5686        parse(&spec, &input(&["ex", "run", "--clean"]))
5687            .expect("the child-local exclusive flag is alone inside the child command");
5688        assert!(
5689            parse(&spec, &input(&["ex", "--clean", "run"])).is_err(),
5690            "the ancestor spelling still conflicts with selecting the child"
5691        );
5692    }
5693
5694    #[test]
5695    fn a_same_named_parent_flag_is_company_for_a_child_exclusive_flag() {
5696        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" global=#true exclusive=#true\n}\n"
5697            .parse()
5698            .unwrap();
5699
5700        parse(&spec, &input(&["ex", "run", "--clean"])).expect("the child exclusive flag is alone");
5701        assert!(
5702            parse(&spec, &input(&["ex", "--clean", "run", "--clean"])).is_err(),
5703            "the distinct parent declaration is still company despite sharing a name"
5704        );
5705    }
5706
5707    #[test]
5708    fn a_local_child_redeclaration_keeps_its_exclusivity_when_merged() {
5709        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n  flag \"--verbose\"\n}\n"
5710            .parse()
5711            .unwrap();
5712
5713        parse(&spec, &input(&["ex", "run", "--clean"]))
5714            .expect("the child exclusive flag is valid alone");
5715        assert!(
5716            parse(&spec, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
5717            "merging with the inherited global must not discard child exclusivity"
5718        );
5719    }
5720
5721    #[test]
5722    fn an_orphan_parent_alias_does_not_disown_a_child_local_exclusive_flag() {
5723        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n}\n"
5724            .parse()
5725            .unwrap();
5726
5727        parse(&spec, &input(&["ex", "run", "--clean"]))
5728            .expect("the typed long form belongs to the child declaration");
5729        assert!(
5730            parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
5731            "the inherited short form still belongs to the ancestor"
5732        );
5733        assert!(
5734            parse(&spec, &input(&["ex", "run", "-c", "--clean"])).is_err(),
5735            "a child spelling cannot mask the ancestor-exclusive occurrence on the same merged flag"
5736        );
5737    }
5738
5739    #[test]
5740    fn an_inherited_alias_keeps_its_ancestor_exclusivity() {
5741        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" global=#true\n}\n"
5742            .parse()
5743            .unwrap();
5744
5745        parse(&spec, &input(&["ex", "run", "--clean"]))
5746            .expect("the child's spelling does not activate the orphan ancestor alias");
5747        assert!(
5748            parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
5749            "the inherited short alias still belongs to the ancestor exclusive flag"
5750        );
5751    }
5752
5753    #[test]
5754    fn an_inherited_negated_alias_keeps_its_ancestor_exclusivity() {
5755        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"
5756            .parse()
5757            .unwrap();
5758
5759        assert!(
5760            parse(&spec, &input(&["ex", "run", "--no-clean"])).is_err(),
5761            "the inherited negated alias still belongs to the ancestor exclusive flag"
5762        );
5763    }
5764
5765    #[test]
5766    fn a_colliding_alias_does_not_disown_the_child_from_the_rest() {
5767        // The child re-declares the inherited `--clean` as exclusive and gives it a `-c` that
5768        // an unrelated inherited global already owns. That collision is resolved in the other
5769        // global's favor, so the child's `-c` resolves elsewhere — but the child plainly owns
5770        // the `--clean` it declared, and its exclusivity holds.
5771        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"
5772            .parse()
5773            .unwrap();
5774
5775        parse(&spec, &input(&["ex", "run", "--clean"])).expect("alone is allowed");
5776        assert!(
5777            parse(&spec, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
5778            "one unrelated alias collision cannot disown the child from its own flag"
5779        );
5780    }
5781
5782    #[test]
5783    fn a_local_child_declaration_is_not_in_scope_before_the_subcommand() {
5784        // A child's *local* re-declaration describes the flag at the child. Typed ahead of the
5785        // subcommand word the flag can only be the ancestor's, because that is the only one in
5786        // scope there — so the ancestor's exclusivity is the one that answers, whichever way it
5787        // is set. The pair below differ in nothing else, which is what makes this one rule
5788        // rather than two behaviors.
5789        let quiet: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n  flag \"--verbose\"\n}\n"
5790            .parse()
5791            .unwrap();
5792        parse(&quiet, &input(&["ex", "--clean", "run", "--verbose"]))
5793            .expect("the ancestor owns this occurrence, and it is not exclusive");
5794        assert!(
5795            parse(&quiet, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
5796            "after the subcommand word the child's declaration is in scope, and it is exclusive"
5797        );
5798
5799        let loud: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n}\n"
5800            .parse()
5801            .unwrap();
5802        assert!(
5803            parse(&loud, &input(&["ex", "--clean", "run"])).is_err(),
5804            "the same rule, with an exclusive ancestor: selecting the child is company for it"
5805        );
5806    }
5807
5808    #[test]
5809    fn an_orphan_ancestor_alias_keeps_its_exclusivity_past_a_plain_child_redeclaration() {
5810        // The mirror of `a_local_child_redeclaration_keeps_its_exclusivity_when_merged`: the
5811        // child owns `--clean` and says nothing about exclusivity, but `-c` is a spelling only
5812        // the ancestor ever declared, so the ancestor's answer still governs it.
5813        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n  flag \"--clean\"\n  flag \"--verbose\"\n}\n"
5814            .parse()
5815            .unwrap();
5816
5817        assert!(
5818            parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
5819            "the orphan ancestor alias is still the ancestor's exclusive flag"
5820        );
5821        parse(&spec, &input(&["ex", "run", "--clean", "--verbose"]))
5822            .expect("the child's own spelling drops the exclusivity the child did not restate");
5823    }
5824
5825    #[test]
5826    fn a_child_spelling_carries_its_exclusivity_even_beside_an_ancestor_spelling() {
5827        // Both spellings of one merged flag, typed together. The child's `--clean` is exclusive
5828        // whatever else was typed alongside it, so `--verbose` is company; attributing the whole
5829        // occurrence to the ancestor because `-c` appeared in it lost that.
5830        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true\ncmd \"run\" {\n  flag \"--clean\" exclusive=#true\n  flag \"--verbose\"\n}\n"
5831            .parse()
5832            .unwrap();
5833
5834        assert!(
5835            parse(&spec, &input(&["ex", "run", "-c", "--clean", "--verbose"])).is_err(),
5836            "the child spelling is exclusive whatever it was typed beside"
5837        );
5838        parse(&spec, &input(&["ex", "run", "-c", "--verbose"]))
5839            .expect("the ancestor's own spelling was never exclusive");
5840    }
5841
5842    #[test]
5843    fn an_environment_value_takes_the_exclusivity_of_the_declaration_in_scope() {
5844        // An environment value has no spelling to attribute, so the declaration the selected
5845        // command has in scope answers — in both directions. Comparing whole alias sets asked
5846        // the ancestor instead, because the merged flag also carries its orphan `-c`.
5847        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"
5848            .parse()
5849            .unwrap();
5850
5851        assert!(
5852            parse_with_env(&added, &["ex", "run", "--verbose"], &[("EX_CLEAN", "1")]).is_err(),
5853            "the child added exclusivity the environment value has to honor"
5854        );
5855
5856        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"
5857            .parse()
5858            .unwrap();
5859
5860        parse_with_env(&dropped, &["ex", "run", "--verbose"], &[("EX_CLEAN", "1")])
5861            .expect("the child dropped the exclusivity, and the environment value follows it");
5862    }
5863
5864    #[test]
5865    fn a_merged_child_exclusive_flag_still_escapes_requiredness() {
5866        // Exclusivity suppresses missing-value checks, and that has to survive the merge for
5867        // the same reason the companion check does.
5868        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"
5869            .parse()
5870            .unwrap();
5871
5872        parse(&spec, &input(&["ex", "run", "--clean"]))
5873            .expect("a merged child exclusive flag is still the command's requiredness escape");
5874    }
5875
5876    #[test]
5877    fn a_group_allows_one_member_and_refuses_two() {
5878        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\"\nflag \"--url <u>\"\nflag \"--stdin\"\ngroup \"input\" \"--file\" \"--url\" \"--stdin\"\n"
5879            .parse()
5880            .unwrap();
5881
5882        // One is fine, and so is none: a plain group says "at most one".
5883        parse(&spec, &input(&["ex", "--file", "a.txt"])).expect("one member is fine");
5884        parse(&spec, &input(&["ex"])).expect("a group that is not required asks for nothing");
5885
5886        let err = parse(&spec, &input(&["ex", "--file", "a.txt", "--stdin"])).unwrap_err();
5887        assert!(err.to_string().contains("group input"), "{err}");
5888    }
5889
5890    #[test]
5891    fn positional_selectors_work_in_conflicts_and_groups() {
5892        let conflicts: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--from-file <path>\" conflicts=\"value\"\narg \"[value]\"\n"
5893            .parse()
5894            .unwrap();
5895        parse(&conflicts, &input(&["ex", "--from-file", "vars.env"]))
5896            .expect("the flag alone is valid");
5897        parse(&conflicts, &input(&["ex", "literal"])).expect("the positional alone is valid");
5898        assert!(parse(
5899            &conflicts,
5900            &input(&["ex", "--from-file", "vars.env", "literal"])
5901        )
5902        .is_err());
5903
5904        let positional_source: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--from-file <path>\"\narg \"[value]\" conflicts=\"--from-file\"\n"
5905            .parse()
5906            .unwrap();
5907        assert!(parse(
5908            &positional_source,
5909            &input(&["ex", "--from-file", "vars.env", "literal"])
5910        )
5911        .is_err());
5912
5913        let group: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <path>\"\narg \"[target]\"\ngroup \"input\" \"--file\" \"target\" required=#true\n"
5914            .parse()
5915            .unwrap();
5916        assert!(parse(&group, &input(&["ex"])).is_err());
5917        parse(&group, &input(&["ex", "target-name"]))
5918            .expect("a positional satisfies a required group");
5919        assert!(parse(
5920            &group,
5921            &input(&["ex", "--file", "input.txt", "target-name"])
5922        )
5923        .is_err());
5924    }
5925
5926    #[test]
5927    fn a_required_group_needs_one_of_its_members() {
5928        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
5929            .parse()
5930            .unwrap();
5931
5932        let err = parse(&spec, &input(&["ex"])).unwrap_err();
5933        // The members, because that is what a user has to type; the name, because a
5934        // command with several groups would otherwise report the same sentence twice.
5935        assert!(err.to_string().contains("--file, --url"), "{err}");
5936        assert!(err.to_string().contains("input"), "{err}");
5937
5938        parse(&spec, &input(&["ex", "--url", "u"])).expect("one member satisfies it");
5939    }
5940
5941    #[test]
5942    fn a_multiple_group_only_polices_requiredness() {
5943        // `multiple` with `required` is "at least one of these", so two is fine and
5944        // none is not.
5945        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--a\"\nflag \"--b\"\ngroup \"any\" \"--a\" \"--b\" required=#true multiple=#true\n"
5946            .parse()
5947            .unwrap();
5948
5949        parse(&spec, &input(&["ex", "--a", "--b"])).expect("multiple allows both");
5950        assert!(parse(&spec, &input(&["ex"])).is_err());
5951    }
5952
5953    #[test]
5954    fn a_group_reads_a_default_for_requiredness_and_not_for_exclusivity() {
5955        // The two halves of a group are two kinds of rule, and they read a default
5956        // differently on purpose. Requiredness asks whether a member has a value, and a
5957        // default is a value — the rule `requires` follows. Exclusivity asks what the
5958        // user supplied, because a defaulted member counted as supplied would collide
5959        // with the sibling they actually typed and refuse a correct command line.
5960        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" default=\"a.txt\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
5961            .parse()
5962            .unwrap();
5963
5964        parse(&spec, &input(&["ex"])).expect("the default fills the group");
5965        parse(&spec, &input(&["ex", "--url", "u"]))
5966            .expect("the default must not conflict with the flag the user typed");
5967    }
5968
5969    #[test]
5970    fn a_group_naming_two_spellings_of_one_flag_is_not_a_conflict() {
5971        // `-f` and `--file` are one flag. Counted by selector, giving it once would
5972        // report it as conflicting with itself.
5973        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-f --file <f>\"\nflag \"--url <u>\"\ngroup \"input\" \"-f\" \"--file\" \"--url\"\n"
5974            .parse()
5975            .unwrap();
5976
5977        parse(&spec, &input(&["ex", "--file", "a.txt"])).expect("one flag is one member");
5978        parse(&spec, &input(&["ex", "-f", "a.txt"])).expect("either spelling, still one member");
5979
5980        // A genuine collision is still one.
5981        let err = parse(&spec, &input(&["ex", "--file", "a.txt", "--url", "u"])).unwrap_err();
5982        assert!(err.to_string().contains("group input"), "{err}");
5983    }
5984
5985    #[test]
5986    fn a_group_reads_the_environment_as_given() {
5987        // The environment does count, which is the same asymmetry `conflicts` has: an
5988        // env var is somebody saying something, a default is nobody saying anything.
5989        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" env=\"EX_FILE\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
5990            .parse()
5991            .unwrap();
5992
5993        parse_with_env(&spec, &["ex"], &[("EX_FILE", "a.txt")]).expect("the environment fills it");
5994    }
5995
5996    #[test]
5997    fn a_requirement_names_the_flag_that_is_missing() {
5998        // Reported as the missing flag rather than as something wrong with `--out`,
5999        // which is what clap says for an unmet `requires` and what a user can act on.
6000        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\"\n"
6001            .parse()
6002            .unwrap();
6003
6004        let err = parse(&spec, &input(&["ex", "--out", "a.txt"])).unwrap_err();
6005        assert!(
6006            err.to_string().contains("format"),
6007            "the missing flag should be named: {err}"
6008        );
6009
6010        // Satisfied, in either order.
6011        for words in [
6012            &["ex", "--out", "a.txt", "--format", "json"][..],
6013            &["ex", "--format", "json", "--out", "a.txt"][..],
6014        ] {
6015            parse(&spec, &input(words)).unwrap_or_else(|e| panic!("{words:?}: {e}"));
6016        }
6017
6018        // Nothing happens when the flag that imposes the rule is absent: a requirement
6019        // is a consequence of using the flag, not a rule about the command line.
6020        parse(&spec, &input(&["ex"])).expect("a bare invocation requires nothing");
6021    }
6022
6023    #[test]
6024    fn a_value_activates_only_its_conditional_requirement() {
6025        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"
6026            .parse()
6027            .unwrap();
6028
6029        parse(&spec, &input(&["ex", "--config", "ordinary.toml"]))
6030            .expect("an unrelated value requires nothing");
6031
6032        let key = parse(&spec, &input(&["ex", "--config", "special.toml"])).unwrap_err();
6033        assert!(key.to_string().contains("key"), "{key}");
6034        parse(
6035            &spec,
6036            &input(&["ex", "--config", "special.toml", "--key", "secret"]),
6037        )
6038        .expect("the matching requirement is satisfied");
6039
6040        let token = parse(&spec, &input(&["ex", "--config", "remote.toml"])).unwrap_err();
6041        assert!(token.to_string().contains("token"), "{token}");
6042    }
6043
6044    #[test]
6045    fn conditional_requirements_read_explicit_env_but_not_defaults() {
6046        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"
6047            .parse()
6048            .unwrap();
6049        let err = parse_with_env(&from_env, &["ex"], &[("EX_CONFIG", "special.toml")]).unwrap_err();
6050        assert!(err.to_string().contains("key"), "{err}");
6051
6052        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"
6053            .parse()
6054            .unwrap();
6055        parse(&from_default, &input(&["ex"]))
6056            .expect("a default is not an explicit conditional value");
6057    }
6058
6059    #[test]
6060    fn command_line_values_override_env_for_conditional_requirements() {
6061        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--config <file>\" env=\"EX_CONFIG\" {\n  requires_if \"special.toml\" \"--key\"\n}\nflag \"--key <key>\"\n"
6062            .parse()
6063            .unwrap();
6064
6065        parse_with_env(
6066            &spec,
6067            &["ex", "--config", "ordinary.toml"],
6068            &[("EX_CONFIG", "special.toml")],
6069        )
6070        .expect("the command-line value takes precedence over the environment");
6071    }
6072
6073    #[test]
6074    fn conditional_requirements_normalize_boolean_env_values() {
6075        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--feature\" env=\"EX_FEATURE\" {\n  requires_if \"true\" \"--key\"\n}\nflag \"--key <key>\"\n"
6076            .parse()
6077            .unwrap();
6078
6079        for value in ["1", "true", "True", "TRUE"] {
6080            let err = parse_with_env(&spec, &["ex"], &[("EX_FEATURE", value)]).unwrap_err();
6081            assert!(err.to_string().contains("key"), "{value}: {err}");
6082        }
6083        parse_with_env(&spec, &["ex"], &[("EX_FEATURE", "false")])
6084            .expect("a false environment value does not activate a true condition");
6085    }
6086
6087    #[test]
6088    fn a_default_satisfies_a_requirement() {
6089        // The flag it names has a value, which is the question a requirement asks. Read
6090        // any other way, `--format` would be missing here and present ten lines further
6091        // down, where plain required-ness reads the same default as filling it.
6092        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" default=\"json\"\n"
6093            .parse()
6094            .unwrap();
6095
6096        parse(&spec, &input(&["ex", "--out", "a.txt"]))
6097            .expect("a defaulted flag is not a missing one");
6098    }
6099
6100    #[test]
6101    fn a_present_flag_binds_a_conditional_default() {
6102        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" {\n  default_if \"--json\" \"true\"\n}\nflag \"--json\"\n"
6103            .parse()
6104            .unwrap();
6105
6106        let with = parse(&spec, &input(&["ex", "--json"])).unwrap();
6107        assert_eq!(
6108            with.as_env().get("usage_bin_names").map(String::as_str),
6109            Some("true")
6110        );
6111
6112        let without = parse(&spec, &input(&["ex"])).unwrap();
6113        assert!(
6114            !without.as_env().contains_key("usage_bin_names"),
6115            "IsPresent does nothing when the selector is absent"
6116        );
6117    }
6118
6119    #[test]
6120    fn an_equals_condition_binds_a_conditional_default() {
6121        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--style <s>\" {\n  default_if \"--output\" \"json\" \"pretty\"\n}\nflag \"--output <fmt>\"\n"
6122            .parse()
6123            .unwrap();
6124
6125        let json = parse(&spec, &input(&["ex", "--output", "json"])).unwrap();
6126        assert_eq!(
6127            json.as_env().get("usage_style").map(String::as_str),
6128            Some("pretty")
6129        );
6130        let yaml = parse(&spec, &input(&["ex", "--output", "yaml"])).unwrap();
6131        assert!(!yaml.as_env().contains_key("usage_style"));
6132    }
6133
6134    #[test]
6135    fn an_equals_condition_reads_a_negated_flag() {
6136        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pretty\" {\n  default_if \"--json\" \"false\" \"true\"\n}\nflag \"--json\" negate=\"--no-json\"\n"
6137            .parse()
6138            .unwrap();
6139
6140        let off = parse(&spec, &input(&["ex", "--no-json"])).unwrap();
6141        assert_eq!(
6142            off.as_env().get("usage_pretty").map(String::as_str),
6143            Some("true")
6144        );
6145        let on = parse(&spec, &input(&["ex", "--json"])).unwrap();
6146        assert!(
6147            !on.as_env().contains_key("usage_pretty"),
6148            "--json is true, so when=false should miss"
6149        );
6150    }
6151
6152    #[test]
6153    fn the_first_matching_conditional_default_wins() {
6154        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"
6155            .parse()
6156            .unwrap();
6157
6158        let out = parse(&spec, &input(&["ex", "--json", "--pretty"])).unwrap();
6159        assert_eq!(
6160            out.as_env().get("usage_style").map(String::as_str),
6161            Some("compact")
6162        );
6163    }
6164
6165    #[test]
6166    fn argv_and_env_suppress_a_conditional_default() {
6167        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" env=\"EX_BIN\" {\n  default_if \"--json\" \"true\"\n}\nflag \"--json\"\n"
6168            .parse()
6169            .unwrap();
6170
6171        let from_env = parse_with_env(&spec, &["ex", "--json"], &[("EX_BIN", "false")]).unwrap();
6172        assert_eq!(
6173            from_env.as_env().get("usage_bin_names").map(String::as_str),
6174            Some("false"),
6175            "the target's environment wins over default_if"
6176        );
6177    }
6178
6179    #[test]
6180    fn a_sibling_env_activates_a_conditional_default() {
6181        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" {\n  default_if \"--json\" \"true\"\n}\nflag \"--json\" env=\"EX_JSON\"\n"
6182            .parse()
6183            .unwrap();
6184
6185        let out = parse_with_env(&spec, &["ex"], &[("EX_JSON", "1")]).unwrap();
6186        assert_eq!(
6187            out.as_env().get("usage_bin_names").map(String::as_str),
6188            Some("true")
6189        );
6190    }
6191
6192    #[test]
6193    fn a_default_does_not_activate_a_conditional_default() {
6194        // `--json` sorts before `--pretty` in the available-flag map, so a one-pass
6195        // bind would put json's default into `out.flags` and then treat it as
6196        // explicit for pretty's `default_if`. Go and the derive ignore defaults.
6197        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pretty\" {\n  default_if \"--json\" \"true\"\n}\nflag \"--json\" default=#true\n"
6198            .parse()
6199            .unwrap();
6200
6201        let out = parse(&spec, &input(&["ex"])).unwrap();
6202        assert_eq!(
6203            out.as_env().get("usage_json").map(String::as_str),
6204            Some("true")
6205        );
6206        assert!(
6207            !out.as_env().contains_key("usage_pretty"),
6208            "a default is not an explicit value for default_if"
6209        );
6210    }
6211
6212    #[test]
6213    fn a_conditional_default_does_not_activate_requires_if() {
6214        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"
6215            .parse()
6216            .unwrap();
6217
6218        parse(&spec, &input(&["ex", "--json"]))
6219            .expect("a default_if value is not explicit for requires_if");
6220        assert!(parse(&spec, &input(&["ex", "--format", "json"])).is_err());
6221    }
6222
6223    #[test]
6224    fn a_conditional_default_satisfies_a_requirement() {
6225        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" {\n  default_if \"--json\" \"json\"\n}\nflag \"--json\"\n"
6226            .parse()
6227            .unwrap();
6228
6229        parse(&spec, &input(&["ex", "--out", "a.txt", "--json"]))
6230            .expect("default_if fills the required flag");
6231        assert!(parse(&spec, &input(&["ex", "--out", "a.txt"])).is_err());
6232    }
6233
6234    #[test]
6235    fn an_environment_value_satisfies_a_requirement() {
6236        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" env=\"EX_FORMAT\"\n"
6237            .parse()
6238            .unwrap();
6239
6240        assert!(parse(&spec, &input(&["ex", "--out", "a.txt"])).is_err());
6241        parse_with_env(&spec, &["ex", "--out", "a.txt"], &[("EX_FORMAT", "json")])
6242            .expect("the environment supplies it");
6243    }
6244
6245    #[test]
6246    fn a_requirement_is_satisfied_by_a_short_form() {
6247        // The selector may spell the other flag any way it answers to, so the check
6248        // resolves it the way every other selector is resolved rather than matching
6249        // text. The error names the flag, not the selector.
6250        let spec: Spec =
6251            "name \"ex\"\nbin \"ex\"\nflag \"--sign\" requires=\"-k\"\nflag \"-k --key <k>\"\n"
6252                .parse()
6253                .unwrap();
6254
6255        parse(&spec, &input(&["ex", "--sign", "--key", "x"])).expect("--key satisfies -k");
6256
6257        let err = parse(&spec, &input(&["ex", "--sign"])).unwrap_err();
6258        assert!(err.to_string().contains("key"), "{err}");
6259    }
6260
6261    #[test]
6262    fn conflicting_flags_are_rejected_in_either_order() {
6263        // Declared once, on `--file`, which is all clap exposes — so the check has to
6264        // be order-independent by looking at every flag that was given rather than at
6265        // the one that declared the conflict.
6266        let spec: Spec =
6267            "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" conflicts=\"--stdin\"\nflag \"--stdin\"\n"
6268                .parse()
6269                .unwrap();
6270
6271        for words in [
6272            &["ex", "--file", "a.txt", "--stdin"][..],
6273            &["ex", "--stdin", "--file", "a.txt"][..],
6274        ] {
6275            let err = parse(&spec, &input(words)).unwrap_err();
6276            assert!(
6277                err.to_string().contains("conflicts with --stdin"),
6278                "{words:?} should be refused: {err}"
6279            );
6280        }
6281
6282        // Either one alone is fine.
6283        parse(&spec, &input(&["ex", "--stdin"])).unwrap();
6284        parse(&spec, &input(&["ex", "--file", "a.txt"])).unwrap();
6285    }
6286
6287    #[test]
6288    fn unknown_flags_are_values_by_default() {
6289        // The default, and the reason it is the default: a spec often parses a
6290        // command line whose flags belong to something else.
6291        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--force\"\narg \"[rest]...\"\n"
6292            .parse()
6293            .unwrap();
6294        let out = parse(
6295            &spec,
6296            &["ex".to_string(), "--wat".to_string(), "x".to_string()],
6297        )
6298        .unwrap();
6299        let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
6300        assert_eq!(out.args[rest].to_string(), "--wat x");
6301    }
6302
6303    #[test]
6304    fn repeated_scalar_flags_override_by_default_and_can_be_strict() {
6305        let permissive: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--jobs <n>\"\nflag \"--verbose\"\n"
6306            .parse()
6307            .unwrap();
6308        let out = parse(&permissive, &input(&["ex", "--jobs", "1", "--jobs", "2"]))
6309            .expect("a repeat is a correction by default");
6310        let jobs = out.flags.keys().find(|f| f.name == "jobs").unwrap();
6311        assert_eq!(out.flags[jobs].to_string(), "2");
6312        parse(&permissive, &input(&["ex", "--verbose", "--verbose"]))
6313            .expect("switches use the same default");
6314
6315        let strict: Spec = "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--jobs <n>\"\nflag \"--verbose\"\n"
6316            .parse()
6317            .unwrap();
6318        for words in [
6319            &["ex", "--jobs", "1", "--jobs", "2"][..],
6320            &["ex", "--verbose", "--verbose"][..],
6321        ] {
6322            let err = parse(&strict, &input(words)).unwrap_err();
6323            assert!(
6324                err.to_string().contains("cannot be used multiple times"),
6325                "{err}"
6326            );
6327        }
6328
6329        let reparsed: Spec = strict.to_string().parse().unwrap();
6330        assert!(!reparsed.cmd.args_override_self);
6331    }
6332
6333    #[test]
6334    fn strict_negated_flags_allow_opposite_forms_but_reject_the_same_form() {
6335        let spec: Spec = "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--color\" negate=\"--no-color\"\n"
6336            .parse()
6337            .unwrap();
6338
6339        let out = parse(&spec, &input(&["ex", "--color", "--no-color"]))
6340            .expect("opposite forms override each other");
6341        let color = out.flags.keys().find(|f| f.name == "color").unwrap();
6342        assert!(matches!(out.flags[color], ParseValue::Bool(false)));
6343
6344        for words in [
6345            &["ex", "--color", "--color"][..],
6346            &["ex", "--no-color", "--no-color"][..],
6347        ] {
6348            let err = parse(&spec, &input(words)).unwrap_err();
6349            assert!(err.to_string().contains("cannot be used multiple times"));
6350        }
6351    }
6352
6353    #[test]
6354    fn strict_global_flags_may_repeat_across_command_levels() {
6355        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"
6356            .parse()
6357            .unwrap();
6358
6359        let out = parse(
6360            &spec,
6361            &input(&[
6362                "ex", "--color", "--jobs", "1", "run", "--color", "--jobs", "2",
6363            ]),
6364        )
6365        .expect("an inherited global is allowed once at each command level");
6366        let jobs = out.flags.keys().find(|f| f.name == "jobs").unwrap();
6367        assert_eq!(out.flags[jobs].to_string(), "2");
6368
6369        for words in [
6370            &["ex", "--color", "--color", "run", "--no-color"][..],
6371            &["ex", "--jobs", "1", "run", "--jobs", "2", "--jobs", "3"][..],
6372        ] {
6373            let err = parse(&spec, &input(words)).unwrap_err();
6374            assert!(err.to_string().contains("cannot be used multiple times"));
6375        }
6376    }
6377
6378    #[test]
6379    fn a_subcommand_can_negate_only_its_parents_requirements() {
6380        let base = r#"name "ex"
6381bin "ex"
6382flag "--config" required=#true
6383flag "--mode" requires="--config"
6384flag "--other"
6385arg "<input>"
6386group "source" "--config" "--other" required=#true
6387cmd "run" { flag "--child" required=#true }
6388"#;
6389        let strict: Spec = base.parse().unwrap();
6390        let err = parse(&strict, &input(&["ex", "run"])).unwrap_err();
6391        let message = err.to_string();
6392        assert!(
6393            message.contains("input") || message.contains("config"),
6394            "{message}"
6395        );
6396
6397        let negated: Spec = base
6398            .replacen("bin \"ex\"", "bin \"ex\"\nsubcommand_negates_reqs #true", 1)
6399            .parse()
6400            .unwrap();
6401        let err = parse(&negated, &input(&["ex", "run"])).unwrap_err();
6402        assert!(
6403            err.to_string().contains("child"),
6404            "the selected command keeps its own requirements: {err}"
6405        );
6406
6407        let mut child_optional = negated.clone();
6408        child_optional.cmd.subcommands["run"].flags[0].required = false;
6409        parse(&child_optional, &input(&["ex", "run"]))
6410            .expect("the child selection satisfies all parent requirements");
6411        parse(&child_optional, &input(&["ex", "--mode", "run"]))
6412            .expect("parent requires relationships are negated too");
6413    }
6414
6415    #[test]
6416    fn a_parent_argument_can_conflict_with_a_later_subcommand() {
6417        let spec: Spec = r#"name "ex"
6418bin "ex"
6419args_conflicts_with_subcommands #true
6420flag "--verbose"
6421cmd "run"
6422"#
6423        .parse()
6424        .unwrap();
6425
6426        parse(&spec, &input(&["ex", "run"]))
6427            .expect("the subcommand is valid without a parent argument");
6428        let err = parse(&spec, &input(&["ex", "--verbose", "run"])).unwrap_err();
6429        assert!(
6430            err.to_string().contains("cannot be used with arguments"),
6431            "{err}"
6432        );
6433    }
6434
6435    #[test]
6436    fn a_subcommand_can_take_precedence_over_a_variadic_flag() {
6437        let base = r#"name "ex"
6438bin "ex"
6439flag "--values <value>..."
6440cmd "run"
6441"#;
6442        let plain: Spec = base.parse().unwrap();
6443        let out = parse(&plain, &input(&["ex", "--values", "a", "run"])).unwrap();
6444        assert_eq!(out.cmd.name, "ex");
6445
6446        let precedence: Spec = base
6447            .replacen(
6448                "bin \"ex\"",
6449                "bin \"ex\"\nsubcommand_precedence_over_arg #true",
6450                1,
6451            )
6452            .parse()
6453            .unwrap();
6454        let out = parse(&precedence, &input(&["ex", "--values", "a", "run"])).unwrap();
6455        assert_eq!(out.cmd.name, "run");
6456    }
6457
6458    #[test]
6459    fn a_required_positional_can_follow_an_unfilled_optional_one() {
6460        let base = r#"name "ex"
6461bin "ex"
6462arg "[optional]"
6463arg "<required>"
6464"#;
6465        let plain: Spec = base.parse().unwrap();
6466        let err = parse(&plain, &input(&["ex", "value"])).unwrap_err();
6467        assert!(err.to_string().contains("required"), "{err}");
6468
6469        let enabled: Spec = base
6470            .replacen(
6471                "bin \"ex\"",
6472                "bin \"ex\"\nallow_missing_positional #true",
6473                1,
6474            )
6475            .parse()
6476            .unwrap();
6477        let out = parse(&enabled, &input(&["ex", "value"])).unwrap();
6478        assert!(!out.args.keys().any(|arg| arg.name == "optional"));
6479        let value = &out
6480            .args
6481            .iter()
6482            .find(|(arg, _)| arg.name == "required")
6483            .unwrap()
6484            .1;
6485        assert!(matches!(value, ParseValue::String(value) if value == "value"));
6486    }
6487
6488    #[test]
6489    fn unknown_flags_can_be_rejected_for_the_whole_cli() {
6490        let spec: Spec =
6491            "name \"ex\"\nbin \"ex\"\nunknown_flags \"error\"\nflag \"--force\"\nflag \"-0 --print0\"\narg \"[rest]...\" allow_negative_numbers=#true\n"
6492                .parse()
6493                .unwrap();
6494        let err = parse(&spec, &["ex".to_string(), "--wat".to_string()]).unwrap_err();
6495        assert!(
6496            err.to_string().contains("--wat"),
6497            "the message should name the token: {err}"
6498        );
6499
6500        // The positional opts into the narrower negative-number carve-out without
6501        // accepting arbitrary unknown flags.
6502        let out = parse(&spec, &["ex".to_string(), "-1".to_string()]).unwrap();
6503        let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
6504        assert_eq!(out.args[rest].to_string(), "-1");
6505
6506        let out = parse(&spec, &["ex".to_string(), "-0".to_string()]).unwrap();
6507        let print0 = out.flags.keys().find(|flag| flag.name == "print0").unwrap();
6508        assert!(matches!(out.flags[print0], ParseValue::Bool(true)));
6509    }
6510
6511    #[test]
6512    fn a_declared_digit_short_does_not_stop_the_subcommand_scan() {
6513        let spec: Spec = r#"
6514name "ex"
6515bin "ex"
6516unknown_flags "error"
6517flag "-0 --print0" global=#true
6518cmd "run" {
6519  flag "--force"
6520}
6521"#
6522        .parse()
6523        .unwrap();
6524        let out = parse(&spec, &input(&["ex", "-0", "run", "--force"])).unwrap();
6525        assert_eq!(out.cmd.name, "run");
6526        let print0 = out.flags.keys().find(|flag| flag.name == "print0").unwrap();
6527        let force = out.flags.keys().find(|flag| flag.name == "force").unwrap();
6528        assert!(matches!(out.flags[print0], ParseValue::Bool(true)));
6529        assert!(matches!(out.flags[force], ParseValue::Bool(true)));
6530    }
6531
6532    #[test]
6533    fn a_command_may_override_the_cli_wide_setting() {
6534        // Strict overall, lenient for the one command that forwards options.
6535        let spec: Spec = r#"
6536name "ex"
6537bin "ex"
6538unknown_flags "error"
6539cmd "exec" unknown_flags="value" {
6540  arg "[rest]..."
6541}
6542cmd "build" {
6543  arg "[rest]..."
6544}
6545"#
6546        .parse()
6547        .unwrap();
6548
6549        let out = parse(
6550            &spec,
6551            &["ex".to_string(), "exec".to_string(), "--wat".to_string()],
6552        )
6553        .unwrap();
6554        let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
6555        assert_eq!(out.args[rest].to_string(), "--wat");
6556
6557        assert!(
6558            parse(
6559                &spec,
6560                &["ex".to_string(), "build".to_string(), "--wat".to_string()]
6561            )
6562            .is_err(),
6563            "a command that says nothing inherits the CLI's choice"
6564        );
6565    }
6566
6567    #[test]
6568    fn the_setting_survives_a_round_trip() {
6569        let spec: Spec =
6570            "name \"ex\"\nbin \"ex\"\nunknown_flags \"error\"\ncmd \"x\" unknown_flags=\"value\"\n"
6571                .parse()
6572                .unwrap();
6573        let reparsed: Spec = spec.to_string().parse().unwrap();
6574        assert_eq!(reparsed.unknown_flags, Some(UnknownFlags::Error));
6575        assert_eq!(
6576            reparsed.cmd.subcommands["x"].unknown_flags,
6577            Some(UnknownFlags::Value)
6578        );
6579    }
6580
6581    #[test]
6582    fn test_default_subcommand() {
6583        // Test that default_subcommand routes to the specified subcommand
6584        let run_cmd = SpecCommand::builder()
6585            .name("run")
6586            .arg(SpecArg::builder().name("task").build())
6587            .build();
6588        let mut cmd = SpecCommand::builder().name("test").build();
6589        cmd.subcommands.insert("run".to_string(), run_cmd);
6590
6591        let spec = Spec {
6592            name: "test".to_string(),
6593            bin: "test".to_string(),
6594            cmd,
6595            default_subcommand: Some("run".to_string()),
6596            ..Default::default()
6597        };
6598
6599        // "test mytask" should be parsed as if it were "test run mytask"
6600        let input = vec!["test".to_string(), "mytask".to_string()];
6601        let parsed = parse(&spec, &input).unwrap();
6602
6603        // Should have two commands: root and "run"
6604        assert_eq!(parsed.cmds.len(), 2);
6605        assert_eq!(parsed.cmds[1].name, "run");
6606
6607        // Should have parsed the task argument
6608        assert_eq!(parsed.args.len(), 1);
6609        let arg = parsed.args.keys().next().unwrap();
6610        assert_eq!(arg.name, "task");
6611        let value = parsed.args.values().next().unwrap();
6612        assert_eq!(value.to_string(), "mytask");
6613    }
6614
6615    #[test]
6616    fn test_default_subcommand_explicit_still_works() {
6617        // Test that explicit subcommand takes precedence
6618        let run_cmd = SpecCommand::builder()
6619            .name("run")
6620            .arg(SpecArg::builder().name("task").build())
6621            .build();
6622        let other_cmd = SpecCommand::builder()
6623            .name("other")
6624            .arg(SpecArg::builder().name("other_arg").build())
6625            .build();
6626        let mut cmd = SpecCommand::builder().name("test").build();
6627        cmd.subcommands.insert("run".to_string(), run_cmd);
6628        cmd.subcommands.insert("other".to_string(), other_cmd);
6629
6630        let spec = Spec {
6631            name: "test".to_string(),
6632            bin: "test".to_string(),
6633            cmd,
6634            default_subcommand: Some("run".to_string()),
6635            ..Default::default()
6636        };
6637
6638        // "test other foo" should use "other" subcommand, not default
6639        let input = vec!["test".to_string(), "other".to_string(), "foo".to_string()];
6640        let parsed = parse(&spec, &input).unwrap();
6641
6642        // Should have used "other" subcommand
6643        assert_eq!(parsed.cmds.len(), 2);
6644        assert_eq!(parsed.cmds[1].name, "other");
6645    }
6646
6647    #[test]
6648    fn test_default_subcommand_applies_only_at_the_root() {
6649        // `default_subcommand` is declared once, for the whole spec, and only at the top. It
6650        // was being looked up wherever the parser happened to be standing, so a command with
6651        // an unrelated subcommand of the same name acquired a default of its own: with
6652        // `default_subcommand "ls"`, `ex config zzz` descended into `config ls` and bound
6653        // `zzz` there. Nothing declared that, and nothing could have.
6654        let mut config_ls = SpecCommand::builder().name("ls").build();
6655        config_ls.args.push(SpecArg::builder().name("what").build());
6656        let mut config_cmd = SpecCommand::builder().name("config").build();
6657        config_cmd.subcommands.insert("ls".to_string(), config_ls);
6658
6659        // The root's own `ls`, which is what its default points at. It takes an argument so
6660        // that a routed word has somewhere to land.
6661        let mut root_ls = SpecCommand::builder().name("ls").build();
6662        root_ls.args.push(SpecArg::builder().name("what").build());
6663        let mut cmd = SpecCommand::builder().name("ex").build();
6664        cmd.subcommands.insert("ls".to_string(), root_ls);
6665        cmd.subcommands.insert("config".to_string(), config_cmd);
6666
6667        let spec = Spec {
6668            name: "ex".to_string(),
6669            bin: "ex".to_string(),
6670            cmd,
6671            default_subcommand: Some("ls".to_string()),
6672            ..Default::default()
6673        };
6674
6675        // `config` has an `ls`, but `config` did not declare a default, so `zzz` is `config`'s
6676        // own business — and `config` takes no argument, so this is an error rather than a
6677        // silent descent.
6678        let input = vec!["ex".to_string(), "config".to_string(), "zzz".to_string()];
6679        assert!(
6680            parse(&spec, &input).is_err(),
6681            "`config` has no default subcommand and no argument, so `zzz` cannot bind"
6682        );
6683
6684        // At the root, where it is declared, it still applies.
6685        let input = vec!["ex".to_string(), "zzz".to_string()];
6686        let parsed = parse(&spec, &input).expect("the root's default applies");
6687        assert_eq!(
6688            parsed
6689                .cmds
6690                .iter()
6691                .map(|c| c.name.as_str())
6692                .collect::<Vec<_>>(),
6693            ["ex", "ls"]
6694        );
6695        assert_eq!(
6696            parsed.args.values().next().map(|v| v.to_string()),
6697            Some("zzz".to_string()),
6698            "and the word binds inside the command it reached"
6699        );
6700    }
6701
6702    #[test]
6703    fn test_default_subcommand_with_nested_subcommands() {
6704        // Test that default_subcommand works when the default subcommand has nested subcommands.
6705        // This is the mise use case: "mise say" should be parsed as "mise run say"
6706        // where "say" is a subcommand of "run" (a task).
6707        let say_cmd = SpecCommand::builder()
6708            .name("say")
6709            .arg(SpecArg::builder().name("name").build())
6710            .build();
6711        let mut run_cmd = SpecCommand::builder().name("run").build();
6712        run_cmd.subcommands.insert("say".to_string(), say_cmd);
6713
6714        let mut cmd = SpecCommand::builder().name("test").build();
6715        cmd.subcommands.insert("run".to_string(), run_cmd);
6716
6717        let spec = Spec {
6718            name: "test".to_string(),
6719            bin: "test".to_string(),
6720            cmd,
6721            default_subcommand: Some("run".to_string()),
6722            ..Default::default()
6723        };
6724
6725        // "test say hello" should be parsed as "test run say hello"
6726        let input = vec!["test".to_string(), "say".to_string(), "hello".to_string()];
6727        let parsed = parse(&spec, &input).unwrap();
6728
6729        // Should have three commands: root, "run", and "say"
6730        assert_eq!(parsed.cmds.len(), 3);
6731        assert_eq!(parsed.cmds[0].name, "test");
6732        assert_eq!(parsed.cmds[1].name, "run");
6733        assert_eq!(parsed.cmds[2].name, "say");
6734
6735        // Should have parsed the "name" argument
6736        assert_eq!(parsed.args.len(), 1);
6737        let arg = parsed.args.keys().next().unwrap();
6738        assert_eq!(arg.name, "name");
6739        let value = parsed.args.values().next().unwrap();
6740        assert_eq!(value.to_string(), "hello");
6741    }
6742
6743    /// Build a spec equivalent to the post-mount structure produced by mise's
6744    /// `mise usage` output: a root with a value-taking global flag (`-C/--cd`), a `run`
6745    /// subcommand that re-declares the same flag as NON-global, and a mounted task
6746    /// (`sample:run`) carrying a positional arg with `choices`.
6747    ///
6748    /// We construct the merged structure directly instead of executing a real mount so the
6749    /// test stays hermetic and cross-platform while still exercising the parser defect.
6750    fn mounted_global_flag_spec() -> Spec {
6751        let task_cmd = SpecCommand::builder()
6752            .name("sample:run")
6753            .arg(
6754                SpecArg::builder()
6755                    .name("profile")
6756                    .choices(["alpha", "beta", "gamma"])
6757                    .build(),
6758            )
6759            .build();
6760        // `run` re-declares `-C/--cd` but as a NON-global flag, mirroring the mise spec.
6761        let mut run_cmd = SpecCommand::builder()
6762            .name("run")
6763            .flag(
6764                SpecFlag::builder()
6765                    .name("cd")
6766                    .short('C')
6767                    .long("cd")
6768                    .arg(SpecArg::builder().name("dir").build())
6769                    .global(false)
6770                    .build(),
6771            )
6772            .build();
6773        run_cmd
6774            .subcommands
6775            .insert("sample:run".to_string(), task_cmd);
6776
6777        let mut cmd = SpecCommand::builder()
6778            .name("test")
6779            .flag(
6780                SpecFlag::builder()
6781                    .name("cd")
6782                    .short('C')
6783                    .long("cd")
6784                    .arg(SpecArg::builder().name("dir").build())
6785                    .global(true)
6786                    .build(),
6787            )
6788            .build();
6789        cmd.subcommands.insert("run".to_string(), run_cmd);
6790
6791        Spec {
6792            name: "test".to_string(),
6793            bin: "test".to_string(),
6794            cmd,
6795            ..Default::default()
6796        }
6797    }
6798
6799    #[test]
6800    fn test_prefix_global_flag_does_not_pollute_choices() {
6801        // Regression for the parser-side root cause referenced by jdx/mise#10069.
6802        //
6803        // When `run` re-declares the global `-C/--cd` as non-global, descending into it (and
6804        // then into the mounted `sample:run`) used to drop the inherited global flag from
6805        // `available_flags`. Phase 2 then no longer recognized the prefix `-C`, so it was
6806        // mis-validated against the task's `choices` positional arg.
6807        let spec = mounted_global_flag_spec();
6808
6809        // The prefix global flag must stay recognized so it is consumed as a flag (not as the
6810        // positional). Before the fix this bailed with "Invalid choice for arg profile: -C".
6811        for words in [
6812            &["test", "-C", "/tmp", "run", "sample:run"][..],
6813            // Embedded-value form must behave identically.
6814            &["test", "--cd=/tmp", "run", "sample:run"][..],
6815        ] {
6816            let parsed = parse_partial(&spec, &input(words)).unwrap();
6817            assert_eq!(
6818                parsed
6819                    .cmds
6820                    .iter()
6821                    .map(|c| c.name.as_str())
6822                    .collect::<Vec<_>>(),
6823                vec!["test", "run", "sample:run"],
6824            );
6825            // No positional arg should have been consumed by the leftover global-flag tokens.
6826            assert!(
6827                parsed.args.is_empty(),
6828                "args should be empty, got {:?}",
6829                parsed.args
6830            );
6831
6832            // Fix (B): the inherited global flag survives the descent even though `run`
6833            // re-declares `-C/--cd` as non-global.
6834            let cd = parsed
6835                .available_flags
6836                .get("--cd")
6837                .expect("--cd should remain available after descending into the subcommand");
6838            assert!(cd.global, "--cd must stay global after descent");
6839            assert!(
6840                parsed.available_flags.get("-C").is_some_and(|f| f.global),
6841                "-C must stay global after descent",
6842            );
6843
6844            // The global flag must still be recorded in `out.flags` so it reaches `as_env()`
6845            // for normal execution and for the env passed to mount scripts. (Removing the
6846            // token in Phase 1 instead of re-parsing it would silently drop `usage_cd`.)
6847            assert_eq!(
6848                parsed.as_env().get("usage_cd").map(String::as_str),
6849                Some("/tmp"),
6850                "global flag value must survive in as_env(), got {:?}",
6851                parsed.as_env(),
6852            );
6853        }
6854
6855        // A real, valid choice still parses through the global flag prefix.
6856        let parsed = parse_partial(
6857            &spec,
6858            &input(&["test", "-C", "/tmp", "run", "sample:run", "alpha"]),
6859        )
6860        .unwrap();
6861        assert_eq!(parsed.args.len(), 1);
6862        assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
6863
6864        // And genuinely invalid choices are still rejected (we didn't disable validation).
6865        assert_parse_err(
6866            parse_partial(&spec, &input(&["test", "run", "sample:run", "wrong"])),
6867            "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
6868        );
6869    }
6870
6871    /// Build a spec mirroring mise's orphan-short re-declarations: a root with a LONG-ONLY
6872    /// global boolean flag (`--raw`, no short), a `run` subcommand that re-declares it as a
6873    /// NON-global flag while ADDING a short (`-r --raw`) plus a purely-local `-f/--force`
6874    /// flag, and a mounted task (`sample:run`) with a `choices` positional arg.
6875    fn mounted_orphan_short_spec() -> Spec {
6876        let task_cmd = SpecCommand::builder()
6877            .name("sample:run")
6878            .arg(
6879                SpecArg::builder()
6880                    .name("profile")
6881                    .choices(["alpha", "beta", "gamma"])
6882                    .build(),
6883            )
6884            .build();
6885        // `run` re-declares `--raw` as NON-global but adds a `-r` short that exists only here,
6886        // and also carries a purely-local `-f/--force` flag (shares nothing with a global).
6887        let mut run_cmd = SpecCommand::builder()
6888            .name("run")
6889            .flag(
6890                SpecFlag::builder()
6891                    .name("raw")
6892                    .short('r')
6893                    .long("raw")
6894                    .global(false)
6895                    .build(),
6896            )
6897            .flag(
6898                SpecFlag::builder()
6899                    .name("force")
6900                    .short('f')
6901                    .long("force")
6902                    .global(false)
6903                    .build(),
6904            )
6905            .build();
6906        run_cmd
6907            .subcommands
6908            .insert("sample:run".to_string(), task_cmd);
6909
6910        // Root global is LONG-ONLY: `--raw` with no short.
6911        let mut cmd = SpecCommand::builder()
6912            .name("test")
6913            .flag(
6914                SpecFlag::builder()
6915                    .name("raw")
6916                    .long("raw")
6917                    .global(true)
6918                    .build(),
6919            )
6920            .build();
6921        cmd.subcommands.insert("run".to_string(), run_cmd);
6922
6923        Spec {
6924            name: "test".to_string(),
6925            bin: "test".to_string(),
6926            cmd,
6927            ..Default::default()
6928        }
6929    }
6930
6931    #[test]
6932    fn test_orphan_short_alias_survives_merge() {
6933        // Follow-up to test_prefix_global_flag_does_not_pollute_choices (jdx/mise#10069):
6934        // when `run` re-declares the long-only global `--raw` as a non-global `-r --raw`, the
6935        // added short `-r` must be unioned onto the surviving inherited global flag instead of
6936        // being discarded with the wholesale re-declaration. Otherwise `mycli run -r <task>`
6937        // would not recognize `-r` and would mis-validate it against the task's `choices` arg.
6938        let spec = mounted_orphan_short_spec();
6939
6940        let parsed = parse_partial(&spec, &input(&["test", "run", "-r", "sample:run"])).unwrap();
6941        assert_eq!(
6942            parsed
6943                .cmds
6944                .iter()
6945                .map(|c| c.name.as_str())
6946                .collect::<Vec<_>>(),
6947            vec!["test", "run", "sample:run"],
6948        );
6949
6950        // (a) The orphan short `-r` survives the descent, merged onto the inherited global flag,
6951        // and the original long `--raw` is still global too.
6952        assert!(
6953            parsed.available_flags.get("-r").is_some_and(|f| f.global),
6954            "-r must be merged onto the inherited global flag and stay global after descent",
6955        );
6956        assert!(
6957            parsed
6958                .available_flags
6959                .get("--raw")
6960                .is_some_and(|f| f.global),
6961            "--raw must stay global after descent",
6962        );
6963
6964        // (b) The token is consumed as a flag, not mistaken for the `choices` positional.
6965        assert!(
6966            parsed.args.is_empty(),
6967            "args should be empty, got {:?}",
6968            parsed.args
6969        );
6970
6971        // (c) The value still reaches as_env() so `usage_raw` is produced for execution/mounts.
6972        assert_eq!(
6973            parsed.as_env().get("usage_raw").map(String::as_str),
6974            Some("true"),
6975            "merged short's value must survive in as_env(), got {:?}",
6976            parsed.as_env(),
6977        );
6978
6979        // (d) Negative case: a purely-local flag that shares nothing with a global is NOT
6980        // promoted/merged — it is correctly dropped when descending into the mount.
6981        assert!(
6982            !parsed.available_flags.contains_key("-f"),
6983            "purely-local -f must not be promoted onto a global",
6984        );
6985        assert!(
6986            !parsed.available_flags.contains_key("--force"),
6987            "purely-local --force must not be promoted onto a global",
6988        );
6989
6990        // A real, valid choice still parses through the merged short prefix.
6991        let parsed =
6992            parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "alpha"])).unwrap();
6993        assert_eq!(parsed.args.len(), 1);
6994        assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
6995
6996        // And genuinely invalid choices are still rejected.
6997        assert_parse_err(
6998            parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "wrong"])),
6999            "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
7000        );
7001    }
7002
7003    #[test]
7004    fn test_orphan_short_does_not_clobber_unrelated_global() {
7005        // When a re-declaration's orphan short collides with a DIFFERENT inherited global's
7006        // short, the merge must not steal it. Here the root has both a long-only `--raw` global
7007        // and a `-r --restrict` global; `run` re-declares `-r --raw` as non-global. `-r` is a
7008        // genuine collision with `--restrict`, so global precedence must keep `-r -> restrict`.
7009        let run_cmd = SpecCommand::builder()
7010            .name("run")
7011            .flag(
7012                SpecFlag::builder()
7013                    .name("raw")
7014                    .short('r')
7015                    .long("raw")
7016                    .global(false)
7017                    .build(),
7018            )
7019            .build();
7020        let mut cmd = SpecCommand::builder()
7021            .name("test")
7022            .flag(
7023                SpecFlag::builder()
7024                    .name("raw")
7025                    .long("raw")
7026                    .global(true)
7027                    .build(),
7028            )
7029            .flag(
7030                SpecFlag::builder()
7031                    .name("restrict")
7032                    .short('r')
7033                    .long("restrict")
7034                    .global(true)
7035                    .build(),
7036            )
7037            .build();
7038        cmd.subcommands.insert("run".to_string(), run_cmd);
7039        let spec = Spec {
7040            name: "test".to_string(),
7041            bin: "test".to_string(),
7042            cmd,
7043            ..Default::default()
7044        };
7045
7046        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7047        // `-r` stays owned by the unrelated `--restrict` global, not stolen by the merged raw.
7048        assert_eq!(
7049            parsed.available_flags.get("-r").map(|f| f.name.as_str()),
7050            Some("restrict"),
7051            "-r must remain owned by the unrelated global it already belonged to",
7052        );
7053        // Both globals are still recognized and global after the descent.
7054        assert!(parsed
7055            .available_flags
7056            .get("--raw")
7057            .is_some_and(|f| f.global));
7058        assert!(parsed
7059            .available_flags
7060            .get("--restrict")
7061            .is_some_and(|f| f.global));
7062    }
7063
7064    #[test]
7065    fn test_redeclared_global_aliases_share_one_flag() {
7066        // A global declared with BOTH a short and a long, re-declared non-globally by a
7067        // subcommand that adds a third alias. Every alias key must resolve to the SAME merged
7068        // flag: the child's keys iterate in BTreeMap order (`--assume-yes`, `--yes`, `-y`), so by
7069        // the time `-y` is reached the long already points at the merged flag. That merged flag is
7070        // not a *different* inherited global, so the collision guard must not skip `-y` and leave
7071        // it pointing at the pre-merge global (which lacks the added `assume-yes` alias).
7072        let spec = r#"
7073flag "-y --yes" global=#true effect="write"
7074cmd "run" {
7075    flag "-y --yes --assume-yes"
7076}
7077"#
7078        .parse::<Spec>()
7079        .unwrap();
7080
7081        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7082
7083        for key in ["-y", "--yes", "--assume-yes"] {
7084            let flag = parsed
7085                .available_flags
7086                .get(key)
7087                .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
7088            assert!(flag.global, "{key} must stay global after the descent");
7089            assert_eq!(
7090                flag.long,
7091                vec!["yes".to_string(), "assume-yes".to_string()],
7092                "{key} must resolve to the flag carrying every alias",
7093            );
7094            assert_eq!(flag.short, vec!['y'], "{key} must keep the global's short");
7095        }
7096
7097        // One logical flag means one object: all three keys share a single `Arc`.
7098        assert_eq!(
7099            unique_flags(parsed.available_flags.values()).count(),
7100            1,
7101            "all aliases must point at one flag object, got {:?}",
7102            parsed.available_flags,
7103        );
7104
7105        // The global's effect survives the merge, so `-y` still marks the command as writing.
7106        assert_eq!(
7107            parsed.available_flags["-y"].effect,
7108            Some(crate::SpecCommandEffect::Write),
7109        );
7110    }
7111
7112    #[test]
7113    fn test_redeclared_global_keeps_hidden_alias_metadata() {
7114        let spec = r#"
7115flag "--yes" global=#true {
7116    alias "-q" "--quietly" hide=#true
7117}
7118cmd "run" {
7119    flag "--yes --assume-yes" {
7120        alias "-s" "--secret" hide=#true
7121    }
7122}
7123"#
7124        .parse::<Spec>()
7125        .unwrap();
7126
7127        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7128        let merged = &parsed.available_flags["--yes"];
7129        assert_eq!(merged.hidden_short_aliases, ['q', 's']);
7130        assert_eq!(merged.hidden_aliases, ["quietly", "secret"]);
7131        for key in ["-q", "-s", "--quietly", "--secret"] {
7132            assert!(Arc::ptr_eq(&parsed.available_flags[key], merged), "{key}");
7133        }
7134    }
7135
7136    #[test]
7137    fn test_redeclared_global_can_promote_hidden_aliases() {
7138        let spec = r#"
7139flag "--yes" global=#true {
7140    alias "-q" "--quietly" hide=#true
7141}
7142cmd "run" {
7143    flag "-q --yes --quietly"
7144}
7145"#
7146        .parse::<Spec>()
7147        .unwrap();
7148
7149        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7150        let merged = &parsed.available_flags["--yes"];
7151        assert!(merged.hidden_short_aliases.is_empty());
7152        assert!(merged.hidden_aliases.is_empty());
7153        for key in ["-q", "--quietly"] {
7154            assert!(Arc::ptr_eq(&parsed.available_flags[key], merged), "{key}");
7155        }
7156    }
7157
7158    #[test]
7159    fn test_partially_redeclared_global_keeps_all_aliases_on_one_flag() {
7160        // Same one-flag-one-object requirement as above, but the child re-declares only ONE of
7161        // the global's three aliases (`--yes`, not `-y`/`--confirm`) while adding a new one. The
7162        // aliases the child omits are never visited by the merge loop, so they must be rebound to
7163        // the merged flag explicitly — otherwise `-y` and `--confirm` keep pointing at the
7164        // pre-merge global and miss the added `assume-yes`.
7165        let spec = r#"
7166flag "-y --yes --confirm" global=#true
7167cmd "run" {
7168    flag "--yes --assume-yes"
7169}
7170"#
7171        .parse::<Spec>()
7172        .unwrap();
7173
7174        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7175
7176        for key in ["-y", "--yes", "--confirm", "--assume-yes"] {
7177            let flag = parsed
7178                .available_flags
7179                .get(key)
7180                .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
7181            assert!(flag.global, "{key} must stay global after the descent");
7182            assert_eq!(
7183                flag.long,
7184                vec![
7185                    "yes".to_string(),
7186                    "confirm".to_string(),
7187                    "assume-yes".to_string()
7188                ],
7189                "{key} must resolve to the flag carrying every alias",
7190            );
7191        }
7192
7193        assert_eq!(
7194            unique_flags(parsed.available_flags.values()).count(),
7195            1,
7196            "all aliases must point at one flag object, got {:?}",
7197            parsed.available_flags,
7198        );
7199    }
7200
7201    /// Build a spec shaped like mise's post-mount structure for jdx/mise#11282: a root with
7202    /// globals (`-E/--env <ENV>`, `--silent`), a `run` subcommand with a non-global flag, and a
7203    /// MOUNTED task command that declares its own `--env` (with choices) plus `--bump`.
7204    ///
7205    /// The task command is marked `mounted` the same way `SpecCommand::mount()` marks the
7206    /// commands it merges in, so the test stays hermetic (no mount subprocess).
7207    fn mounted_task_flag_spec() -> Spec {
7208        let mut task_cmd = SpecCommand::builder()
7209            .name("mytask")
7210            .flag(
7211                SpecFlag::builder()
7212                    .name("env")
7213                    .long("env")
7214                    .arg(
7215                        SpecArg::builder()
7216                            .name("name")
7217                            .choices(["dev", "stage", "prod"])
7218                            .build(),
7219                    )
7220                    .global(false)
7221                    .build(),
7222            )
7223            .flag(
7224                SpecFlag::builder()
7225                    .name("bump")
7226                    .long("bump")
7227                    .arg(
7228                        SpecArg::builder()
7229                            .name("type")
7230                            .choices(["auto", "major"])
7231                            .build(),
7232                    )
7233                    .global(false)
7234                    .build(),
7235            )
7236            .build();
7237        task_cmd.mounted = true;
7238
7239        let mut run_cmd = SpecCommand::builder()
7240            .name("run")
7241            .flag(
7242                SpecFlag::builder()
7243                    .name("force")
7244                    .short('f')
7245                    .long("force")
7246                    .global(false)
7247                    .build(),
7248            )
7249            .build();
7250        run_cmd.subcommands.insert("mytask".to_string(), task_cmd);
7251
7252        let mut cmd = SpecCommand::builder()
7253            .name("test")
7254            .flag(
7255                SpecFlag::builder()
7256                    .name("env")
7257                    .short('E')
7258                    .long("env")
7259                    .arg(SpecArg::builder().name("ENV").build())
7260                    .global(true)
7261                    .build(),
7262            )
7263            .flag(
7264                SpecFlag::builder()
7265                    .name("silent")
7266                    .long("silent")
7267                    .global(true)
7268                    .build(),
7269            )
7270            .build();
7271        cmd.subcommands.insert("run".to_string(), run_cmd);
7272
7273        Spec {
7274            name: "test".to_string(),
7275            bin: "test".to_string(),
7276            cmd,
7277            ..Default::default()
7278        }
7279    }
7280
7281    #[test]
7282    fn test_mount_boundary_does_not_apply_inside_the_mounted_tree() {
7283        // The mounted program's own commands are ordinary commands relative to each other, so
7284        // descending *within* the mounted tree must follow the normal rules — including keeping
7285        // an inherited global that a nested command re-declares as non-global (jdx/usage#649).
7286        // Treating every level of the tree as a mount boundary let the re-declaration shadow the
7287        // global, which the next descent's `retain(global)` then dropped entirely.
7288        let deep = SpecCommand::builder().name("deep").build();
7289        let mut sub = SpecCommand::builder()
7290            .name("sub")
7291            // Re-declares the mounted program's own global as non-global.
7292            .flag(
7293                SpecFlag::builder()
7294                    .name("cd")
7295                    .short('C')
7296                    .long("cd")
7297                    .arg(SpecArg::builder().name("dir").build())
7298                    .global(false)
7299                    .build(),
7300            )
7301            .build();
7302        sub.subcommands.insert("deep".to_string(), deep);
7303        let mut task = SpecCommand::builder()
7304            .name("task")
7305            .flag(
7306                SpecFlag::builder()
7307                    .name("cd")
7308                    .short('C')
7309                    .long("cd")
7310                    .arg(SpecArg::builder().name("dir").build())
7311                    .global(true)
7312                    .build(),
7313            )
7314            .build();
7315        task.subcommands.insert("sub".to_string(), sub);
7316        task.mark_mounted();
7317
7318        let mut run_cmd = SpecCommand::builder().name("run").build();
7319        run_cmd.subcommands.insert("task".to_string(), task);
7320        let mut cmd = SpecCommand::builder().name("test").build();
7321        cmd.subcommands.insert("run".to_string(), run_cmd);
7322        let spec = Spec {
7323            name: "test".to_string(),
7324            bin: "test".to_string(),
7325            cmd,
7326            ..Default::default()
7327        };
7328
7329        let parsed = parse_partial(&spec, &input(&["test", "run", "task", "sub", "deep"])).unwrap();
7330        assert!(
7331            parsed.available_flags.get("--cd").is_some_and(|f| f.global),
7332            "the mounted program's own global must survive descents inside the mounted tree",
7333        );
7334        assert!(
7335            parsed.completion_flags().contains_key("--cd"),
7336            "and must still be offered there: it belongs to the mounted program",
7337        );
7338        assert!(
7339            parsed.completion_flags().contains_key("-C"),
7340            "including the short the nested command re-declared",
7341        );
7342    }
7343
7344    #[test]
7345    fn test_mount_flags_merged_into_the_mounting_cmd_are_offered() {
7346        // A mounted spec may declare flags on its own root, which `SpecCommand::merge` folds
7347        // into the command the mount sits on. They belong to the mounted program, so they must
7348        // be offered inside the mounted commands rather than filtered out with the mounting
7349        // CLI's own flags.
7350        let mut task = SpecCommand::builder()
7351            .name("task")
7352            .flag(
7353                SpecFlag::builder()
7354                    .name("bump")
7355                    .long("bump")
7356                    .global(false)
7357                    .build(),
7358            )
7359            .build();
7360        task.mark_mounted();
7361
7362        let mut run_cmd = SpecCommand::builder().name("run").build();
7363        run_cmd.subcommands.insert("task".to_string(), task);
7364        // What `mount()` leaves behind when the mounted spec's root declares flags.
7365        run_cmd.flags = vec![
7366            SpecFlag::builder()
7367                .name("tglobal")
7368                .long("tglobal")
7369                .global(true)
7370                .build(),
7371            SpecFlag::builder()
7372                .name("tlocal")
7373                .long("tlocal")
7374                .global(false)
7375                .build(),
7376        ];
7377        run_cmd.flags_from_mount = true;
7378
7379        let mut cmd = SpecCommand::builder()
7380            .name("test")
7381            .flag(
7382                SpecFlag::builder()
7383                    .name("silent")
7384                    .long("silent")
7385                    .global(true)
7386                    .build(),
7387            )
7388            .build();
7389        cmd.subcommands.insert("run".to_string(), run_cmd);
7390        let spec = Spec {
7391            name: "test".to_string(),
7392            bin: "test".to_string(),
7393            cmd,
7394            ..Default::default()
7395        };
7396
7397        let parsed = parse_partial(&spec, &input(&["test", "run", "task"])).unwrap();
7398        assert_eq!(
7399            parsed.completion_flags().keys().collect::<Vec<_>>(),
7400            vec!["--bump", "--tglobal"],
7401            "the mounted spec's root global belongs to the mounted program; the mounting CLI's \
7402             `--silent` does not, and the mount's non-global root flag is not inherited",
7403        );
7404    }
7405
7406    #[test]
7407    fn test_mounted_cmd_does_not_offer_mounting_cli_globals() {
7408        // Regression for jdx/mise#11282. A mounted command describes another program, which
7409        // does not accept the mounting CLI's globals (mise forwards everything after a task
7410        // name to the task). They must stay recognized — they may appear before the mounted
7411        // command — but must not be offered in completions there.
7412        let spec = mounted_task_flag_spec();
7413        let parsed = parse_partial(&spec, &input(&["test", "run", "mytask"])).unwrap();
7414
7415        // Still recognized for parsing...
7416        assert!(parsed.available_flags.contains_key("--silent"));
7417        assert!(parsed.available_flags.contains_key("-E"));
7418        // ...but belonging to a command above the mount, so not offered.
7419        assert_eq!(
7420            parsed.completion_flags().keys().collect::<Vec<_>>(),
7421            vec!["--bump", "--env"],
7422            "only the mounted command's own flags may be offered",
7423        );
7424
7425        // `run`'s own non-global flag is dropped on descent, as it always was.
7426        assert!(!parsed.available_flags.contains_key("--force"));
7427    }
7428
7429    #[test]
7430    fn test_mounted_cmd_flag_wins_over_inherited_global() {
7431        // Second half of jdx/mise#11282: the mounted `--env` (with choices) used to be shadowed
7432        // by the root's `--env` global, so completing its value fell back to file completion.
7433        let spec = mounted_task_flag_spec();
7434        let parsed = parse_partial(&spec, &input(&["test", "run", "mytask", "--env"])).unwrap();
7435
7436        let awaiting = parsed
7437            .flag_awaiting_value
7438            .first()
7439            .expect("--env should await a value");
7440        assert_eq!(
7441            awaiting
7442                .arg
7443                .as_ref()
7444                .and_then(|a| a.choices.as_ref())
7445                .map(|c| c.choices.clone()),
7446            Some(vec![
7447                "dev".to_string(),
7448                "stage".to_string(),
7449                "prod".to_string()
7450            ]),
7451            "the mounted command's own --env must win over the inherited global",
7452        );
7453
7454        // The global's short is not declared by the mounted command, so it keeps pointing at
7455        // the global and a value passed before the mounted command still parses.
7456        let parsed =
7457            parse_partial(&spec, &input(&["test", "-E", "anything", "run", "mytask"])).unwrap();
7458        assert!(
7459            parsed.args.is_empty(),
7460            "prefix global tokens must not be consumed as positionals, got {:?}",
7461            parsed.args
7462        );
7463        assert_eq!(
7464            parsed.as_env().get("usage_env").map(String::as_str),
7465            Some("anything"),
7466        );
7467    }
7468
7469    #[test]
7470    fn test_prefix_flag_keeps_the_flag_it_was_read_as() {
7471        // A word before the mounted command is re-parsed by Phase 2, when the mounted command
7472        // already owns the name. It has to stay bound to the flag Phase 1 read it as, or the
7473        // global's value would be validated against the mounted flag's choices and a legitimate
7474        // value would be rejected.
7475        let spec = mounted_task_flag_spec();
7476        let parsed = parse_partial(
7477            &spec,
7478            &input(&["test", "--env", "not-a-task-choice", "run", "mytask"]),
7479        )
7480        .unwrap();
7481        assert!(
7482            parsed.errors.is_empty(),
7483            "prefix global value must not be validated against the mounted flag: {:?}",
7484            parsed
7485                .errors
7486                .iter()
7487                .map(|e| e.to_string())
7488                .collect::<Vec<_>>(),
7489        );
7490        assert_eq!(
7491            parsed.as_env().get("usage_env").map(String::as_str),
7492            Some("not-a-task-choice"),
7493        );
7494
7495        // The embedded-value form binds the same way.
7496        let parsed = parse_partial(
7497            &spec,
7498            &input(&["test", "--env=not-a-task-choice", "run", "mytask"]),
7499        )
7500        .unwrap();
7501        assert!(parsed.errors.is_empty());
7502        assert_eq!(
7503            parsed.as_env().get("usage_env").map(String::as_str),
7504            Some("not-a-task-choice"),
7505        );
7506
7507        // Meanwhile a word *after* the mounted command belongs to the mounted flag, even when
7508        // the same name was already used before it.
7509        let parsed = parse_partial(
7510            &spec,
7511            &input(&["test", "--env", "prod", "run", "mytask", "--env"]),
7512        )
7513        .unwrap();
7514        let awaiting = parsed
7515            .flag_awaiting_value
7516            .first()
7517            .expect("--env should await a value");
7518        assert_eq!(
7519            awaiting
7520                .arg
7521                .as_ref()
7522                .and_then(|a| a.choices.as_ref())
7523                .map(|c| c.choices.clone()),
7524            Some(vec![
7525                "dev".to_string(),
7526                "stage".to_string(),
7527                "prod".to_string()
7528            ]),
7529            "the mounted command's --env must own the name after the mounted command",
7530        );
7531    }
7532
7533    #[test]
7534    fn test_non_global_flag_does_not_hide_subcommand() {
7535        // A non-global flag may precede a subcommand (`mycli run --force task`). Phase 1 used to
7536        // stop scanning at one, so the subcommand — and any mount on it — was never reached and
7537        // its name was left to Phase 2 to mis-read as a positional: `unexpected word: mytask`.
7538        let spec = mounted_task_flag_spec();
7539
7540        for words in [
7541            // `run` declares `-f/--force` as non-global.
7542            &["test", "run", "--force", "mytask"][..],
7543            &["test", "run", "-f", "mytask"][..],
7544            // Mixed with a global before the subcommand.
7545            &["test", "-E", "prod", "run", "--force", "mytask"][..],
7546        ] {
7547            let parsed = parse_partial(&spec, &input(words)).unwrap();
7548            assert_eq!(
7549                parsed
7550                    .cmds
7551                    .iter()
7552                    .map(|c| c.name.as_str())
7553                    .collect::<Vec<_>>(),
7554                vec!["test", "run", "mytask"],
7555                "{words:?} should descend into the mounted command",
7556            );
7557            assert!(
7558                parsed.args.is_empty(),
7559                "{words:?} should not consume a positional, got {:?}",
7560                parsed.args,
7561            );
7562            assert_eq!(
7563                parsed.as_env().get("usage_force").map(String::as_str),
7564                Some("true"),
7565                "the non-global flag must still be recorded for {words:?}",
7566            );
7567        }
7568
7569        // A non-global flag that takes a value consumes it, rather than reading the value as the
7570        // subcommand.
7571        let mut run_cmd = SpecCommand::builder()
7572            .name("run")
7573            .flag(
7574                SpecFlag::builder()
7575                    .name("output")
7576                    .short('o')
7577                    .long("output")
7578                    .arg(SpecArg::builder().name("mode").build())
7579                    .global(false)
7580                    .build(),
7581            )
7582            .build();
7583        run_cmd.subcommands.insert(
7584            "task".to_string(),
7585            SpecCommand::builder().name("task").build(),
7586        );
7587        let mut cmd = SpecCommand::builder().name("test").build();
7588        cmd.subcommands.insert("run".to_string(), run_cmd);
7589        let spec = Spec {
7590            name: "test".to_string(),
7591            bin: "test".to_string(),
7592            cmd,
7593            ..Default::default()
7594        };
7595
7596        let parsed =
7597            parse_partial(&spec, &input(&["test", "run", "--output", "quiet", "task"])).unwrap();
7598        assert_eq!(
7599            parsed
7600                .cmds
7601                .iter()
7602                .map(|c| c.name.as_str())
7603                .collect::<Vec<_>>(),
7604            vec!["test", "run", "task"],
7605        );
7606        assert_eq!(
7607            parsed.as_env().get("usage_output").map(String::as_str),
7608            Some("quiet"),
7609        );
7610
7611        // An unknown flag still stops the scan: it may take a value, so the next word cannot be
7612        // assumed to be a subcommand. `run` takes no positional, so this stays an error.
7613        assert_parse_err(
7614            parse_partial(&spec, &input(&["test", "run", "--nope", "task"])),
7615            "unexpected word: --nope",
7616        );
7617    }
7618
7619    #[test]
7620    fn test_non_mounted_subcommand_offers_inherited_globals() {
7621        // Nothing changes for ordinary (non-mounted) subcommands: a global declared above is
7622        // still both recognized and offered.
7623        let mut run_cmd = SpecCommand::builder().name("run").build();
7624        run_cmd.subcommands.insert(
7625            "nested".to_string(),
7626            SpecCommand::builder().name("nested").build(),
7627        );
7628        let mut cmd = SpecCommand::builder()
7629            .name("test")
7630            .flag(
7631                SpecFlag::builder()
7632                    .name("silent")
7633                    .long("silent")
7634                    .global(true)
7635                    .build(),
7636            )
7637            .build();
7638        cmd.subcommands.insert("run".to_string(), run_cmd);
7639        let spec = Spec {
7640            name: "test".to_string(),
7641            bin: "test".to_string(),
7642            cmd,
7643            ..Default::default()
7644        };
7645
7646        let parsed = parse_partial(&spec, &input(&["test", "run", "nested"])).unwrap();
7647        assert_eq!(
7648            parsed.completion_flags().keys().collect::<Vec<_>>(),
7649            parsed.available_flags.keys().collect::<Vec<_>>(),
7650        );
7651        assert!(parsed.completion_flags().contains_key("--silent"));
7652    }
7653
7654    #[test]
7655    fn test_subcommand_alias_collision_keeps_last_owner() {
7656        // The orphan-alias merge must not disturb how two flags in the SAME subcommand that
7657        // share an alias are resolved. Historically the flattened flag map gave the shared
7658        // alias to the LAST-declared flag (last-writer-wins); that must be preserved.
7659        let run_cmd = SpecCommand::builder()
7660            .name("run")
7661            .flag(
7662                SpecFlag::builder()
7663                    .name("alpha")
7664                    .short('x')
7665                    .long("alpha")
7666                    .global(false)
7667                    .build(),
7668            )
7669            .flag(
7670                SpecFlag::builder()
7671                    .name("beta")
7672                    .short('x')
7673                    .long("beta")
7674                    .global(false)
7675                    .build(),
7676            )
7677            .build();
7678        let mut cmd = SpecCommand::builder().name("test").build();
7679        cmd.subcommands.insert("run".to_string(), run_cmd);
7680        let spec = Spec {
7681            name: "test".to_string(),
7682            bin: "test".to_string(),
7683            cmd,
7684            ..Default::default()
7685        };
7686
7687        let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7688        // `-x` is declared by both flags; the last one (`beta`) keeps it, as before the fix.
7689        assert_eq!(
7690            parsed.available_flags.get("-x").map(|f| f.name.as_str()),
7691            Some("beta"),
7692            "the last-declared flag must keep a shared short alias",
7693        );
7694        // Both distinct long aliases remain recognized and point to their own flag.
7695        assert_eq!(
7696            parsed
7697                .available_flags
7698                .get("--alpha")
7699                .map(|f| f.name.as_str()),
7700            Some("alpha"),
7701        );
7702        assert_eq!(
7703            parsed
7704                .available_flags
7705                .get("--beta")
7706                .map(|f| f.name.as_str()),
7707            Some("beta"),
7708        );
7709    }
7710
7711    #[test]
7712    fn test_default_subcommand_same_name_child() {
7713        // Test that default_subcommand doesn't cause issues when the default subcommand
7714        // has a child with the same name (e.g., "run" has a task named "run").
7715        // This verifies we don't switch multiple times or get stuck in a loop.
7716        let run_task = SpecCommand::builder()
7717            .name("run")
7718            .arg(SpecArg::builder().name("args").build())
7719            .build();
7720        let mut run_cmd = SpecCommand::builder().name("run").build();
7721        run_cmd.subcommands.insert("run".to_string(), run_task);
7722
7723        let mut cmd = SpecCommand::builder().name("test").build();
7724        cmd.subcommands.insert("run".to_string(), run_cmd);
7725
7726        let spec = Spec {
7727            name: "test".to_string(),
7728            bin: "test".to_string(),
7729            cmd,
7730            default_subcommand: Some("run".to_string()),
7731            ..Default::default()
7732        };
7733
7734        // "test run" explicitly matches the "run" subcommand (not via default_subcommand)
7735        let input = vec!["test".to_string(), "run".to_string()];
7736        let parsed = parse(&spec, &input).unwrap();
7737
7738        // Should have two commands: root and "run"
7739        assert_eq!(parsed.cmds.len(), 2);
7740        assert_eq!(parsed.cmds[0].name, "test");
7741        assert_eq!(parsed.cmds[1].name, "run");
7742
7743        // "test run run" should descend into the "run" task (child of "run" subcommand)
7744        let input = vec![
7745            "test".to_string(),
7746            "run".to_string(),
7747            "run".to_string(),
7748            "hello".to_string(),
7749        ];
7750        let parsed = parse(&spec, &input).unwrap();
7751
7752        assert_eq!(parsed.cmds.len(), 3);
7753        assert_eq!(parsed.cmds[0].name, "test");
7754        assert_eq!(parsed.cmds[1].name, "run");
7755        assert_eq!(parsed.cmds[2].name, "run");
7756        assert_eq!(parsed.args.len(), 1);
7757        let value = parsed.args.values().next().unwrap();
7758        assert_eq!(value.to_string(), "hello");
7759
7760        // Key test case: "test other" should switch to default subcommand "run"
7761        // and treat "other" as a positional arg (not try to switch again because
7762        // "run" also has a "run" child).
7763        let mut run_cmd = SpecCommand::builder()
7764            .name("run")
7765            .arg(SpecArg::builder().name("task").build())
7766            .build();
7767        let run_task = SpecCommand::builder().name("run").build();
7768        run_cmd.subcommands.insert("run".to_string(), run_task);
7769
7770        let mut cmd = SpecCommand::builder().name("test").build();
7771        cmd.subcommands.insert("run".to_string(), run_cmd);
7772
7773        let spec = Spec {
7774            name: "test".to_string(),
7775            bin: "test".to_string(),
7776            cmd,
7777            default_subcommand: Some("run".to_string()),
7778            ..Default::default()
7779        };
7780
7781        let input = vec!["test".to_string(), "other".to_string()];
7782        let parsed = parse(&spec, &input).unwrap();
7783
7784        // Should have two commands: root and "run" (the default)
7785        // We should NOT have switched again to the "run" task child
7786        assert_eq!(parsed.cmds.len(), 2);
7787        assert_eq!(parsed.cmds[0].name, "test");
7788        assert_eq!(parsed.cmds[1].name, "run");
7789
7790        // "other" should be parsed as a positional arg
7791        assert_eq!(parsed.args.len(), 1);
7792        let value = parsed.args.values().next().unwrap();
7793        assert_eq!(value.to_string(), "other");
7794    }
7795
7796    #[test]
7797    fn test_restart_token() {
7798        // Test that restart_token resets argument parsing
7799        let run_cmd = SpecCommand::builder()
7800            .name("run")
7801            .arg(SpecArg::builder().name("task").build())
7802            .restart_token(":::".to_string())
7803            .build();
7804        let mut cmd = SpecCommand::builder().name("test").build();
7805        cmd.subcommands.insert("run".to_string(), run_cmd);
7806
7807        let spec = Spec {
7808            name: "test".to_string(),
7809            bin: "test".to_string(),
7810            cmd,
7811            ..Default::default()
7812        };
7813
7814        // "test run task1 ::: task2" - should end up with task2 as the arg
7815        let input = vec![
7816            "test".to_string(),
7817            "run".to_string(),
7818            "task1".to_string(),
7819            ":::".to_string(),
7820            "task2".to_string(),
7821        ];
7822        let parsed = parse(&spec, &input).unwrap();
7823
7824        // After restart, args were cleared and task2 was parsed
7825        assert_eq!(parsed.args.len(), 1);
7826        let value = parsed.args.values().next().unwrap();
7827        assert_eq!(value.to_string(), "task2");
7828    }
7829
7830    #[test]
7831    fn test_restart_token_multiple() {
7832        // Test multiple restart tokens
7833        let run_cmd = SpecCommand::builder()
7834            .name("run")
7835            .arg(SpecArg::builder().name("task").build())
7836            .restart_token(":::".to_string())
7837            .build();
7838        let mut cmd = SpecCommand::builder().name("test").build();
7839        cmd.subcommands.insert("run".to_string(), run_cmd);
7840
7841        let spec = Spec {
7842            name: "test".to_string(),
7843            bin: "test".to_string(),
7844            cmd,
7845            ..Default::default()
7846        };
7847
7848        // "test run task1 ::: task2 ::: task3" - should end up with task3 as the arg
7849        let input = vec![
7850            "test".to_string(),
7851            "run".to_string(),
7852            "task1".to_string(),
7853            ":::".to_string(),
7854            "task2".to_string(),
7855            ":::".to_string(),
7856            "task3".to_string(),
7857        ];
7858        let parsed = parse(&spec, &input).unwrap();
7859
7860        // After multiple restarts, args were cleared and task3 was parsed
7861        assert_eq!(parsed.args.len(), 1);
7862        let value = parsed.args.values().next().unwrap();
7863        assert_eq!(value.to_string(), "task3");
7864    }
7865
7866    #[test]
7867    fn test_restart_token_clears_flag_awaiting_value() {
7868        // Test that restart_token clears pending flag values
7869        let run_cmd = SpecCommand::builder()
7870            .name("run")
7871            .arg(SpecArg::builder().name("task").build())
7872            .flag(
7873                SpecFlag::builder()
7874                    .name("jobs")
7875                    .long("jobs")
7876                    .arg(SpecArg::builder().name("count").build())
7877                    .build(),
7878            )
7879            .restart_token(":::".to_string())
7880            .build();
7881        let mut cmd = SpecCommand::builder().name("test").build();
7882        cmd.subcommands.insert("run".to_string(), run_cmd);
7883
7884        let spec = Spec {
7885            name: "test".to_string(),
7886            bin: "test".to_string(),
7887            cmd,
7888            ..Default::default()
7889        };
7890
7891        // "test run task1 --jobs ::: task2" - task2 should be an arg, not a flag value
7892        let input = vec![
7893            "test".to_string(),
7894            "run".to_string(),
7895            "task1".to_string(),
7896            "--jobs".to_string(),
7897            ":::".to_string(),
7898            "task2".to_string(),
7899        ];
7900        let parsed = parse(&spec, &input).unwrap();
7901
7902        // task2 should be parsed as the task arg, not as --jobs value
7903        assert_eq!(parsed.args.len(), 1);
7904        let value = parsed.args.values().next().unwrap();
7905        assert_eq!(value.to_string(), "task2");
7906        // --jobs should not have a value
7907        assert!(parsed.flag_awaiting_value.is_empty());
7908    }
7909
7910    #[test]
7911    fn test_restart_token_resets_double_dash() {
7912        // Test that restart_token resets the -- separator effect
7913        let run_cmd = SpecCommand::builder()
7914            .name("run")
7915            .arg(SpecArg::builder().name("task").build())
7916            .arg(SpecArg::builder().name("extra_args").var(true).build())
7917            .flag(SpecFlag::builder().name("verbose").long("verbose").build())
7918            .restart_token(":::".to_string())
7919            .build();
7920        let mut cmd = SpecCommand::builder().name("test").build();
7921        cmd.subcommands.insert("run".to_string(), run_cmd);
7922
7923        let spec = Spec {
7924            name: "test".to_string(),
7925            bin: "test".to_string(),
7926            cmd,
7927            ..Default::default()
7928        };
7929
7930        // "test run task1 -- extra ::: --verbose task2" - --verbose should be a flag after :::
7931        let input = vec![
7932            "test".to_string(),
7933            "run".to_string(),
7934            "task1".to_string(),
7935            "--".to_string(),
7936            "extra".to_string(),
7937            ":::".to_string(),
7938            "--verbose".to_string(),
7939            "task2".to_string(),
7940        ];
7941        let parsed = parse(&spec, &input).unwrap();
7942
7943        // --verbose should be parsed as a flag (not an arg) after the restart
7944        assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
7945        // task2 should be the arg after restart
7946        let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
7947        let value = parsed.args.get(task_arg).unwrap();
7948        assert_eq!(value.to_string(), "task2");
7949    }
7950
7951    #[test]
7952    fn test_double_dashes_without_preserve() {
7953        // Only the first `--` is a separator; a later one is a value, because flag
7954        // parsing has already stopped and there is nothing left for it to do.
7955        // `preserve` is about the *first* one — see the test below, where none is
7956        // consumed at all.
7957        let run_cmd = SpecCommand::builder()
7958            .name("run")
7959            .arg(SpecArg::builder().name("args").var(true).build())
7960            .build();
7961        let mut cmd = SpecCommand::builder().name("test").build();
7962        cmd.subcommands.insert("run".to_string(), run_cmd);
7963
7964        let spec = Spec {
7965            name: "test".to_string(),
7966            bin: "test".to_string(),
7967            cmd,
7968            ..Default::default()
7969        };
7970
7971        // "test run arg1 -- arg2 -- arg3": the first separates, the second is a value
7972        let input = vec![
7973            "test".to_string(),
7974            "run".to_string(),
7975            "arg1".to_string(),
7976            "--".to_string(),
7977            "arg2".to_string(),
7978            "--".to_string(),
7979            "arg3".to_string(),
7980        ];
7981        let parsed = parse(&spec, &input).unwrap();
7982
7983        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
7984        let value = parsed.args.get(args_arg).unwrap();
7985        assert_eq!(value.to_string(), "arg1 arg2 -- arg3");
7986    }
7987
7988    #[test]
7989    fn test_double_dashes_with_preserve() {
7990        // Test that variadic args WITH `preserve` keep all double dashes
7991        let run_cmd = SpecCommand::builder()
7992            .name("run")
7993            .arg(
7994                SpecArg::builder()
7995                    .name("args")
7996                    .var(true)
7997                    .double_dash(SpecDoubleDashChoices::Preserve)
7998                    .build(),
7999            )
8000            .build();
8001        let mut cmd = SpecCommand::builder().name("test").build();
8002        cmd.subcommands.insert("run".to_string(), run_cmd);
8003
8004        let spec = Spec {
8005            name: "test".to_string(),
8006            bin: "test".to_string(),
8007            cmd,
8008            ..Default::default()
8009        };
8010
8011        // "test run arg1 -- arg2 -- arg3" - all double dashes should be preserved
8012        let input = vec![
8013            "test".to_string(),
8014            "run".to_string(),
8015            "arg1".to_string(),
8016            "--".to_string(),
8017            "arg2".to_string(),
8018            "--".to_string(),
8019            "arg3".to_string(),
8020        ];
8021        let parsed = parse(&spec, &input).unwrap();
8022
8023        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
8024        let value = parsed.args.get(args_arg).unwrap();
8025        assert_eq!(value.to_string(), "arg1 -- arg2 -- arg3");
8026    }
8027
8028    #[test]
8029    fn test_double_dashes_with_preserve_only_dashes() {
8030        // Test that variadic args WITH `preserve` keep all double dashes even
8031        // if the values are just double dashes
8032        let run_cmd = SpecCommand::builder()
8033            .name("run")
8034            .arg(
8035                SpecArg::builder()
8036                    .name("args")
8037                    .var(true)
8038                    .double_dash(SpecDoubleDashChoices::Preserve)
8039                    .build(),
8040            )
8041            .build();
8042        let mut cmd = SpecCommand::builder().name("test").build();
8043        cmd.subcommands.insert("run".to_string(), run_cmd);
8044
8045        let spec = Spec {
8046            name: "test".to_string(),
8047            bin: "test".to_string(),
8048            cmd,
8049            ..Default::default()
8050        };
8051
8052        // "test run -- --" - all double dashes should be preserved
8053        let input = vec![
8054            "test".to_string(),
8055            "run".to_string(),
8056            "--".to_string(),
8057            "--".to_string(),
8058        ];
8059        let parsed = parse(&spec, &input).unwrap();
8060
8061        let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
8062        let value = parsed.args.get(args_arg).unwrap();
8063        assert_eq!(value.to_string(), "-- --");
8064    }
8065
8066    #[test]
8067    fn test_double_dashes_with_preserve_multiple_args() {
8068        // Test with multiple args where only the second has has `preserve`
8069        let run_cmd = SpecCommand::builder()
8070            .name("run")
8071            .arg(SpecArg::builder().name("task").build())
8072            .arg(
8073                SpecArg::builder()
8074                    .name("extra_args")
8075                    .var(true)
8076                    .double_dash(SpecDoubleDashChoices::Preserve)
8077                    .build(),
8078            )
8079            .build();
8080        let mut cmd = SpecCommand::builder().name("test").build();
8081        cmd.subcommands.insert("run".to_string(), run_cmd);
8082
8083        let spec = Spec {
8084            name: "test".to_string(),
8085            bin: "test".to_string(),
8086            cmd,
8087            ..Default::default()
8088        };
8089
8090        // The first arg "task1" is captured normally
8091        // Then extra_args with `preserve` captures everything, including the "--" tokens
8092        let input = vec![
8093            "test".to_string(),
8094            "run".to_string(),
8095            "task1".to_string(),
8096            "--".to_string(),
8097            "arg1".to_string(),
8098            "--".to_string(),
8099            "--foo".to_string(),
8100        ];
8101        let parsed = parse(&spec, &input).unwrap();
8102
8103        let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
8104        let task_value = parsed.args.get(task_arg).unwrap();
8105        assert_eq!(task_value.to_string(), "task1");
8106
8107        let extra_arg = parsed.args.keys().find(|a| a.name == "extra_args").unwrap();
8108        let extra_value = parsed.args.get(extra_arg).unwrap();
8109        assert_eq!(extra_value.to_string(), "-- arg1 -- --foo");
8110    }
8111
8112    fn spec_with_args(args: impl IntoIterator<Item = SpecArg>) -> Spec {
8113        let cmd = SpecCommand::builder().name("test").args(args).build();
8114        Spec {
8115            name: "test".to_string(),
8116            bin: "test".to_string(),
8117            cmd,
8118            ..Default::default()
8119        }
8120    }
8121
8122    fn arg_value(parsed: &ParseOutput, name: &str) -> String {
8123        let arg = parsed
8124            .args
8125            .keys()
8126            .find(|a| a.name == name)
8127            .unwrap_or_else(|| panic!("expected arg {name} to be parsed"));
8128        parsed.args.get(arg).unwrap().to_string()
8129    }
8130
8131    fn required_arg(name: &str) -> SpecArg {
8132        SpecArg::builder()
8133            .name(name)
8134            .var(true)
8135            .required(false)
8136            .double_dash(SpecDoubleDashChoices::Required)
8137            .build()
8138    }
8139
8140    #[test]
8141    fn test_double_dash_required_reports_error_once_for_variadic() {
8142        // A variadic arg is offered every remaining word, but the mistake is one mistake.
8143        let spec = spec_with_args([required_arg("files")]);
8144
8145        let parsed = parse_partial(&spec, &input(&["test", "a", "b", "c"])).unwrap();
8146
8147        assert!(parsed.args.is_empty());
8148        assert_eq!(parsed.errors.len(), 1);
8149        assert!(
8150            matches!(&parsed.errors[0], UsageErr::ArgRequiresDoubleDash(name) if name == "files")
8151        );
8152    }
8153
8154    #[test]
8155    fn test_double_dash_required_suppresses_missing_arg() {
8156        // The arg is never filled, so the end-of-parse check would also call it missing.
8157        let spec = spec_with_args([SpecArg::builder()
8158            .name("file")
8159            .required(true)
8160            .double_dash(SpecDoubleDashChoices::Required)
8161            .build()]);
8162
8163        let parsed = parse_partial(&spec, &input(&["test", "x"])).unwrap();
8164
8165        assert_eq!(parsed.errors.len(), 1);
8166        assert!(matches!(
8167            &parsed.errors[0],
8168            UsageErr::ArgRequiresDoubleDash(_)
8169        ));
8170        // The cursor stays put, so a completion keeps offering the same arg.
8171        assert_eq!(
8172            parsed.next_arg.as_ref().map(|a| a.name.as_str()),
8173            Some("file")
8174        );
8175        assert!(!parsed.double_dash_seen);
8176    }
8177
8178    #[test]
8179    fn test_double_dash_routes_to_required_arg() {
8180        // Everything after `--` belongs to the arg that requires it, even though the greedy
8181        // variadic before it would otherwise swallow the rest (clap's `Arg::last(true)`).
8182        let spec = spec_with_args([
8183            SpecArg::builder()
8184                .name("tool")
8185                .var(true)
8186                .required(false)
8187                .build(),
8188            required_arg("command"),
8189        ]);
8190
8191        let parsed = parse(&spec, &input(&["test", "node@20", "--", "node", "app.js"])).unwrap();
8192
8193        assert_eq!(arg_value(&parsed, "tool"), "node@20");
8194        assert_eq!(arg_value(&parsed, "command"), "node app.js");
8195        assert!(parsed.double_dash_seen);
8196    }
8197
8198    #[test]
8199    fn test_double_dash_routes_with_gap_reports_missing_arg() {
8200        // Jumping the cursor leaves `tool` empty even though `command` is filled, so the
8201        // "is it filled?" check cannot be a count of how many args were filled.
8202        let spec = spec_with_args([
8203            SpecArg::builder()
8204                .name("tool")
8205                .var(true)
8206                .required(true)
8207                .build(),
8208            required_arg("command"),
8209        ]);
8210
8211        let parsed = parse_partial(&spec, &input(&["test", "--", "ls"])).unwrap();
8212
8213        assert_eq!(arg_value(&parsed, "command"), "ls");
8214        assert!(parsed.args.keys().all(|a| a.name != "tool"));
8215        assert!(parsed
8216            .errors
8217            .iter()
8218            .any(|e| matches!(e, UsageErr::MissingArg(name) if name == "tool")));
8219    }
8220
8221    #[test]
8222    fn test_double_dash_gap_applies_defaults() {
8223        // Same gap, seen from `Parser::parse`: the skipped arg still gets its default.
8224        let spec = spec_with_args([
8225            SpecArg::builder()
8226                .name("tool")
8227                .var(true)
8228                .required(false)
8229                .default_value("node@20")
8230                .build(),
8231            required_arg("command"),
8232        ]);
8233
8234        let parsed = parse(&spec, &input(&["test", "--", "ls"])).unwrap();
8235
8236        assert_eq!(arg_value(&parsed, "command"), "ls");
8237        assert_eq!(arg_value(&parsed, "tool"), "node@20");
8238    }
8239
8240    fn spec_with_restart_token_and_required_arg() -> Spec {
8241        let run_cmd = SpecCommand::builder()
8242            .name("run")
8243            .arg(SpecArg::builder().name("task").build())
8244            .arg(required_arg("run_args"))
8245            .restart_token(":::".to_string())
8246            .build();
8247        let mut cmd = SpecCommand::builder().name("test").build();
8248        cmd.subcommands.insert("run".to_string(), run_cmd);
8249        Spec {
8250            name: "test".to_string(),
8251            bin: "test".to_string(),
8252            cmd,
8253            ..Default::default()
8254        }
8255    }
8256
8257    #[test]
8258    fn test_double_dash_required_restart_token_resets_separator() {
8259        // The `--` before `:::` belongs to the previous invocation only.
8260        let spec = spec_with_restart_token_and_required_arg();
8261
8262        let parsed = parse_partial(
8263            &spec,
8264            &input(&["test", "run", "task1", "--", "a", ":::", "task2", "b"]),
8265        )
8266        .unwrap();
8267
8268        assert_eq!(arg_value(&parsed, "task"), "task2");
8269        assert!(parsed.args.keys().all(|a| a.name != "run_args"));
8270        // Reported once even though the arg was violated after already succeeding once.
8271        assert_eq!(
8272            parsed
8273                .errors
8274                .iter()
8275                .filter(|e| matches!(e, UsageErr::ArgRequiresDoubleDash(_)))
8276                .count(),
8277            1
8278        );
8279    }
8280
8281    #[test]
8282    fn test_double_dash_required_restart_token_accepts_new_separator() {
8283        let spec = spec_with_restart_token_and_required_arg();
8284
8285        let parsed = parse(
8286            &spec,
8287            &input(&["test", "run", "task1", "--", "a", ":::", "task2", "--", "c"]),
8288        )
8289        .unwrap();
8290
8291        assert_eq!(arg_value(&parsed, "task"), "task2");
8292        assert_eq!(arg_value(&parsed, "run_args"), "c");
8293    }
8294
8295    #[test]
8296    fn test_double_dash_preserve_is_not_a_separator() {
8297        // A `--` that `preserve` keeps is a *value* of that arg, so it must not unlock the
8298        // arg that requires a separator. Deliberate: one token cannot be both.
8299        let spec = spec_with_args([
8300            SpecArg::builder()
8301                .name("kept")
8302                .var(true)
8303                .var_max(1)
8304                .required(false)
8305                .double_dash(SpecDoubleDashChoices::Preserve)
8306                .build(),
8307            required_arg("rest"),
8308        ]);
8309
8310        let parsed = parse_partial(&spec, &input(&["test", "--", "x"])).unwrap();
8311
8312        assert_eq!(arg_value(&parsed, "kept"), "--");
8313        assert!(parsed.args.keys().all(|a| a.name != "rest"));
8314        assert!(!parsed.double_dash_seen);
8315        assert_eq!(parsed.errors.len(), 1);
8316    }
8317
8318    #[test]
8319    fn test_double_dash_required_does_not_bail_in_parse_partial() {
8320        // Completions parse half-typed command lines; they must still get a result.
8321        let spec = spec_with_args([required_arg("file")]);
8322
8323        assert!(parse_partial(&spec, &input(&["test", "x"])).is_ok());
8324        assert!(parse(&spec, &input(&["test", "x"])).is_err());
8325    }
8326
8327    #[test]
8328    fn test_double_dash_without_required_arg_does_not_move_cursor() {
8329        // Specs with no `double_dash="required"` arg are untouched by the jump.
8330        let spec = spec_with_args([
8331            SpecArg::builder().name("first").required(false).build(),
8332            SpecArg::builder().name("second").required(false).build(),
8333        ]);
8334
8335        let parsed = parse(&spec, &input(&["test", "--", "a", "b"])).unwrap();
8336
8337        assert_eq!(arg_value(&parsed, "first"), "a");
8338        assert_eq!(arg_value(&parsed, "second"), "b");
8339        assert!(parsed.next_arg.is_none());
8340    }
8341
8342    #[test]
8343    fn test_parser_with_custom_env_for_required_arg() {
8344        let spec = spec_with_arg(
8345            SpecArg::builder()
8346                .name("name")
8347                .env("NAME")
8348                .required(true)
8349                .build(),
8350        );
8351        std::env::remove_var("NAME");
8352
8353        let parsed = parse_with_env(&spec, &["test"], &[("NAME", "john")])
8354            .expect("parse should succeed with custom env");
8355        assert_eq!(parsed.args.len(), 1);
8356        assert_eq!(first_string_value(&parsed), "john");
8357    }
8358
8359    #[test]
8360    fn test_parser_with_custom_env_for_required_flag() {
8361        let spec = spec_with_flag(
8362            SpecFlag::builder()
8363                .long("name")
8364                .env("NAME")
8365                .required(true)
8366                .arg(SpecArg::builder().name("name").build())
8367                .build(),
8368        );
8369        std::env::remove_var("NAME");
8370
8371        let parsed = parse_with_env(&spec, &["test"], &[("NAME", "jane")])
8372            .expect("parse should succeed with custom env");
8373        assert_eq!(parsed.flags.len(), 1);
8374        assert_eq!(first_string_value(&parsed), "jane");
8375    }
8376
8377    #[test]
8378    fn test_flag_environment_fallbacks_preserve_declaration_order() {
8379        let spec = spec_with_flag(
8380            SpecFlag::builder()
8381                .long("name")
8382                .env("NAME")
8383                .env_fallback("OLD_NAME")
8384                .env_fallback("OLDER_NAME")
8385                .deprecated_env("DEPRECATED_NAME")
8386                .arg(SpecArg::builder().name("name").build())
8387                .build(),
8388        );
8389
8390        let parsed = parse_with_env(
8391            &spec,
8392            &["test"],
8393            &[
8394                ("NAME", "canonical"),
8395                ("OLD_NAME", "fallback"),
8396                ("DEPRECATED_NAME", "deprecated"),
8397            ],
8398        )
8399        .unwrap();
8400        assert_eq!(first_string_value(&parsed), "canonical");
8401
8402        let parsed = parse_with_env(
8403            &spec,
8404            &["test"],
8405            &[("OLDER_NAME", "older"), ("OLD_NAME", "old")],
8406        )
8407        .unwrap();
8408        assert_eq!(first_string_value(&parsed), "old");
8409
8410        let parsed =
8411            parse_with_env(&spec, &["test"], &[("DEPRECATED_NAME", "deprecated")]).unwrap();
8412        assert_eq!(first_string_value(&parsed), "deprecated");
8413    }
8414
8415    #[test]
8416    fn a_value_from_a_deprecated_alias_says_which_name_to_use() {
8417        let spec = spec_with_flag(
8418            SpecFlag::builder()
8419                .long("name")
8420                .env("NAME")
8421                .deprecated_env("DEPRECATED_NAME")
8422                .arg(SpecArg::builder().name("name").build())
8423                .build(),
8424        );
8425
8426        // The current name is not a deprecated one, and says nothing.
8427        let parsed = parse_with_env(&spec, &["test"], &[("NAME", "canonical")]).unwrap();
8428        assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
8429
8430        let parsed =
8431            parse_with_env(&spec, &["test"], &[("DEPRECATED_NAME", "deprecated")]).unwrap();
8432        assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
8433        assert_eq!(
8434            parsed.warnings[0].kind,
8435            crate::warn::WarningKind::DeprecatedEnv
8436        );
8437        assert_eq!(parsed.warnings[0].name, "DEPRECATED_NAME");
8438        assert_eq!(parsed.warnings[0].replacement.as_deref(), Some("NAME"));
8439        // Reported, not printed, and the value still arrives.
8440        assert_eq!(first_string_value(&parsed), "deprecated");
8441    }
8442
8443    #[test]
8444    fn a_deprecated_flag_reports_only_when_it_was_used() {
8445        let spec = spec_with_flag(
8446            SpecFlag::builder()
8447                .long("output")
8448                .deprecated("use --out")
8449                .deprecated_remove_at("3.0.0")
8450                .arg(SpecArg::builder().name("output").build())
8451                .build(),
8452        );
8453
8454        let parsed = parse_with_env(&spec, &["test"], &[]).unwrap();
8455        assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
8456
8457        let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
8458        assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
8459        assert_eq!(
8460            parsed.warnings[0].kind,
8461            crate::warn::WarningKind::DeprecatedFlag
8462        );
8463        // Named the way it was typed, dashes and all.
8464        assert_eq!(parsed.warnings[0].name, "--output");
8465        assert_eq!(parsed.warnings[0].remove_at.as_deref(), Some("3.0.0"));
8466        assert_eq!(
8467            parsed.warnings[0].render(),
8468            "warning: --output is deprecated, removed at 3.0.0: use --out\n",
8469        );
8470    }
8471
8472    #[test]
8473    fn a_milestone_the_spec_has_not_reached_stays_quiet() {
8474        let flag = SpecFlag::builder()
8475            .long("output")
8476            .deprecated("use --out")
8477            .deprecated_warn_at("9.0.0")
8478            .arg(SpecArg::builder().name("output").build())
8479            .build();
8480        let mut spec = spec_with_flag(flag);
8481        spec.version = Some("2.0.0".to_string());
8482
8483        let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
8484        assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
8485
8486        // And once the CLI is the release that was named, it speaks up.
8487        spec.version = Some("9.0.0".to_string());
8488        let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
8489        assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
8490    }
8491
8492    #[test]
8493    fn test_parser_with_custom_env_still_fails_when_missing() {
8494        let spec = spec_with_arg(
8495            SpecArg::builder()
8496                .name("name")
8497                .env("NAME")
8498                .required(true)
8499                .build(),
8500        );
8501        std::env::remove_var("NAME");
8502        assert!(parse_with_env(&spec, &["test"], &[]).is_err());
8503    }
8504
8505    #[test]
8506    fn test_parser_does_not_treat_env_choice_value_as_help() {
8507        let spec = spec_with_arg(
8508            SpecArg::builder()
8509                .name("env")
8510                .env("CURRENT_ENV")
8511                .choices(["dev", "staging"])
8512                .required(false)
8513                .build(),
8514        );
8515
8516        assert_parse_err(
8517            parse_with_env(&spec, &["test"], &[("CURRENT_ENV", "--help")]),
8518            "Invalid choice for arg env: --help, expected one of dev, staging",
8519        );
8520    }
8521
8522    #[test]
8523    fn test_parser_does_not_treat_default_choice_value_as_help() {
8524        let spec = spec_with_flag(
8525            SpecFlag::builder()
8526                .long("env")
8527                .arg(
8528                    SpecArg::builder()
8529                        .name("env")
8530                        .choices(["dev", "staging"])
8531                        .build(),
8532                )
8533                .default_value("--help")
8534                .build(),
8535        );
8536
8537        assert_parse_err(
8538            parse_with_env(&spec, &["test"], &[]),
8539            "Invalid choice for option env: --help, expected one of dev, staging",
8540        );
8541    }
8542
8543    /// argv as `parse` wants it, program name included.
8544    fn words(of: &[&str]) -> Vec<String> {
8545        of.iter().map(|s| s.to_string()).collect()
8546    }
8547
8548    #[test]
8549    fn a_command_that_needs_a_subcommand_says_so() {
8550        // The spec has carried `subcommand_required` since the derive needed it, and this parser
8551        // never read it — so `mise generate`, which declares it, parsed as a complete
8552        // invocation while usage-argv and clap both refused. Found by the differential fuzzer.
8553        let spec: Spec = r#"
8554name "ex"
8555bin "ex"
8556cmd "gen" subcommand_required=#true {
8557    cmd "two" {}
8558    cmd "one" {}
8559    cmd "secret" hide=#true {}
8560    alias "g"
8561}
8562cmd "open" {
8563    cmd "sub" {}
8564}
8565"#
8566        .parse()
8567        .unwrap();
8568
8569        let err = parse(&spec, &words(&["ex", "gen"])).unwrap_err();
8570        // Sorted, so the message does not depend on map order; hidden commands left out,
8571        // because a message telling someone to type a hidden name is worse than a vague one;
8572        // and the alias not listed beside the name it points at.
8573        assert_eq!(err.to_string(), "`gen` needs a subcommand: one of one, two");
8574
8575        // Reached through its alias, and still about the command rather than the spelling.
8576        let err = parse(&spec, &words(&["ex", "g"])).unwrap_err();
8577        assert!(err.to_string().starts_with("`gen` needs a subcommand"));
8578
8579        // Given one: fine.
8580        parse(&spec, &words(&["ex", "gen", "one"])).unwrap();
8581
8582        // And a command that has subcommands without declaring them required is untouched —
8583        // this is the half that keeps the check from being "any command with children".
8584        parse(&spec, &words(&["ex", "open"])).unwrap();
8585        parse(&spec, &words(&["ex", "open", "sub"])).unwrap();
8586    }
8587
8588    #[test]
8589    fn arg_required_else_help_observes_the_selected_commands_argv() {
8590        let spec: Spec = r#"
8591name "ex"
8592bin "ex"
8593flag "--verbose" global=#true
8594cmd "run" arg_required_else_help=#true {
8595    flag "--all"
8596}
8597"#
8598        .parse()
8599        .unwrap();
8600        let words = |items: &[&str]| items.iter().map(|s| (*s).to_string()).collect::<Vec<_>>();
8601
8602        let err = parse(&spec, &words(&["ex", "run"])).unwrap_err();
8603        assert!(err.to_string().contains("Usage: ex run"), "{err}");
8604
8605        // A global before the command belongs to the ancestor. It selected `run`, but did not
8606        // give `run` an argument of its own.
8607        let err = parse(&spec, &words(&["ex", "--verbose", "run"])).unwrap_err();
8608        assert!(err.to_string().contains("Usage: ex run"), "{err}");
8609
8610        parse(&spec, &words(&["ex", "run", "--all"])).expect("run received an argv token");
8611    }
8612
8613    #[test]
8614    fn an_unmatched_word_is_forwarded_when_external_subcommand_is_set() {
8615        let spec: Spec = r#"
8616name "ex"
8617bin "ex"
8618unknown_flags "error"
8619external_subcommand #true
8620cmd "install"
8621flag "-v --verbose" global=#true
8622"#
8623        .parse()
8624        .unwrap();
8625
8626        let parsed = parse(&spec, &input(&["ex", "foo", "--help", "bar"])).unwrap();
8627        assert_eq!(
8628            parsed.external,
8629            Some(vec!["foo".into(), "--help".into(), "bar".into()])
8630        );
8631        assert!(parsed.flags.is_empty());
8632
8633        // Known subcommands still win.
8634        let parsed = parse(&spec, &input(&["ex", "install"])).unwrap();
8635        assert_eq!(parsed.cmd.name, "install");
8636        assert!(parsed.external.is_none());
8637
8638        // A global flag before the unmatched word still binds on the parent.
8639        let parsed = parse(&spec, &input(&["ex", "-v", "foo", "--verbose"])).unwrap();
8640        assert_eq!(
8641            parsed.external,
8642            Some(vec!["foo".into(), "--verbose".into()])
8643        );
8644        assert!(parsed.flags.keys().any(|flag| flag.name == "verbose"));
8645
8646        // An unknown flag on the parent is still an error, which is what clap does.
8647        assert!(parse(&spec, &input(&["ex", "--wat"])).is_err());
8648
8649        // A negative number is a value, not a flag, so it can be the unmatched word.
8650        // usage-argv already forwarded `-1`; Phase 1 used to treat every `starts_with('-')`
8651        // token as a flag and never reach the catch-all.
8652        let parsed = parse(&spec, &input(&["ex", "-1", "rest"])).unwrap();
8653        assert_eq!(parsed.external, Some(vec!["-1".into(), "rest".into()]));
8654    }
8655
8656    #[test]
8657    fn an_external_subcommand_satisfies_subcommand_required() {
8658        let mut spec: Spec = r#"
8659name "ex"
8660bin "ex"
8661external_subcommand #true
8662cmd "install"
8663"#
8664        .parse()
8665        .unwrap();
8666        spec.cmd.subcommand_required = true;
8667
8668        parse(&spec, &input(&["ex", "foo", "--help"])).unwrap();
8669        assert!(parse(&spec, &input(&["ex"])).is_err());
8670    }
8671
8672    #[test]
8673    fn a_default_subcommand_outranks_an_external_one() {
8674        let spec: Spec = r#"
8675name "ex"
8676bin "ex"
8677default_subcommand "run"
8678external_subcommand #true
8679cmd "run" {
8680    arg "[task]"
8681}
8682"#
8683        .parse()
8684        .unwrap();
8685
8686        let parsed = parse(&spec, &input(&["ex", "build"])).unwrap();
8687        assert_eq!(parsed.cmd.name, "run");
8688        assert!(parsed.external.is_none());
8689        assert_eq!(first_string_value(&parsed), "build");
8690    }
8691
8692    #[test]
8693    fn multicall_basename_strips_a_path_and_exe() {
8694        assert_eq!(multicall_basename("/usr/bin/ls"), "ls");
8695        assert_eq!(multicall_basename(r"C:\busybox\ls.exe"), "ls");
8696        assert_eq!(multicall_basename("LS.EXE"), "LS");
8697        assert_eq!(multicall_basename("busybox"), "busybox");
8698    }
8699
8700    #[test]
8701    fn a_multicall_applet_is_the_first_word() {
8702        let spec: Spec = r#"
8703name "busybox"
8704bin "busybox"
8705multicall #true
8706cmd "ls" {
8707    arg "[ARGS]" var=#true
8708}
8709cmd "cat"
8710"#
8711        .parse()
8712        .unwrap();
8713
8714        // A symlink: argv[0] is the applet.
8715        let parsed = parse(&spec, &input(&["/usr/bin/ls", "-l"])).unwrap();
8716        assert_eq!(parsed.cmd.name, "ls");
8717        match parsed.args.values().next() {
8718            Some(ParseValue::MultiString(values)) => assert_eq!(values, &["-l".to_string()]),
8719            other => panic!("expected ARGS to collect -l, got {other:?}"),
8720        }
8721
8722        // A dispatcher invocation still skips argv[0].
8723        let parsed = parse(&spec, &input(&["/usr/bin/busybox", "ls", "-l"])).unwrap();
8724        assert_eq!(parsed.cmd.name, "ls");
8725
8726        // Configured dispatcher values receive the same path and extension normalization.
8727        let mut configured = spec.clone();
8728        configured.name = "BusyBox".to_string();
8729        configured.bin = "/opt/bin/busybox.exe".to_string();
8730        let parsed = parse(&configured, &input(&["/usr/bin/busybox.exe", "ls", "-l"])).unwrap();
8731        assert_eq!(parsed.cmd.name, "ls");
8732
8733        // `.exe` is stripped so Windows and Unix agree.
8734        let parsed = parse(&spec, &input(&["ls.exe"])).unwrap();
8735        assert_eq!(parsed.cmd.name, "ls");
8736
8737        // Without the property, argv[0] is discarded as usual.
8738        let mut plain = spec.clone();
8739        plain.multicall = false;
8740        let parsed = parse(&plain, &input(&["/usr/bin/ls", "ls"])).unwrap();
8741        assert_eq!(parsed.cmd.name, "ls");
8742    }
8743
8744    #[test]
8745    fn a_multicall_unknown_applet_can_be_external() {
8746        let spec: Spec = r#"
8747name "busybox"
8748bin "busybox"
8749multicall #true
8750unknown_flags "error"
8751external_subcommand #true
8752cmd "ls"
8753"#
8754        .parse()
8755        .unwrap();
8756
8757        let parsed = parse(&spec, &input(&["/usr/bin/git", "--help"])).unwrap();
8758        assert_eq!(parsed.external, Some(vec!["git".into(), "--help".into()]));
8759
8760        let mut closed = spec.clone();
8761        closed.cmd.external_subcommand = false;
8762        assert!(parse(&closed, &input(&["wat"])).is_err());
8763    }
8764
8765    #[cfg(feature = "unstable_choices_env")]
8766    #[test]
8767    fn test_parser_arg_choices_from_custom_env() {
8768        let spec = spec_arg_choices_env("DEPLOY_ENVS");
8769
8770        let parsed =
8771            parse_with_env(&spec, &["test", "bar"], &[("DEPLOY_ENVS", "foo,bar baz")]).unwrap();
8772        assert_eq!(first_string_value(&parsed), "bar");
8773
8774        assert_parse_err(
8775            parse_with_env(&spec, &["test", "prod"], &[("DEPLOY_ENVS", "foo,bar baz")]),
8776            "Invalid choice for arg env: prod, expected one of foo, bar, baz",
8777        );
8778        assert_parse_err(
8779            parse_with_env(&spec, &["test", "prod"], &[]),
8780            "Invalid choice for arg env: prod, no choices resolved from env DEPLOY_ENVS",
8781        );
8782    }
8783
8784    #[cfg(feature = "unstable_choices_env")]
8785    #[test]
8786    fn test_parser_validates_flag_choices_from_custom_env() {
8787        let spec = spec_flag_choices_env("DEPLOY_ENVS");
8788        let parsed = parse_with_env(
8789            &spec,
8790            &["test", "--env", "baz"],
8791            &[("DEPLOY_ENVS", "foo,bar baz")],
8792        )
8793        .unwrap();
8794        assert_eq!(first_string_value(&parsed), "baz");
8795    }
8796
8797    #[cfg(feature = "unstable_choices_env")]
8798    #[test]
8799    fn test_parser_revalidates_env_and_default_values_against_choices_env() {
8800        let arg_env_spec = spec_with_arg(
8801            SpecArg::builder()
8802                .name("env")
8803                .env("CURRENT_ENV")
8804                .choices_env("DEPLOY_ENVS")
8805                .build(),
8806        );
8807        assert_parse_err(
8808            parse_with_env(
8809                &arg_env_spec,
8810                &["test"],
8811                &[("CURRENT_ENV", "prod"), ("DEPLOY_ENVS", "dev,staging")],
8812            ),
8813            "Invalid choice for arg env: prod, expected one of dev, staging",
8814        );
8815
8816        let flag_default_spec = spec_with_flag(
8817            SpecFlag::builder()
8818                .long("env")
8819                .arg(
8820                    SpecArg::builder()
8821                        .name("env")
8822                        .choices_env("DEPLOY_ENVS")
8823                        .build(),
8824                )
8825                .default_value("prod")
8826                .build(),
8827        );
8828        assert_parse_err(
8829            parse_with_env(
8830                &flag_default_spec,
8831                &["test"],
8832                &[("DEPLOY_ENVS", "dev,staging")],
8833            ),
8834            "Invalid choice for option env: prod, expected one of dev, staging",
8835        );
8836    }
8837
8838    #[test]
8839    fn test_variadic_arg_captures_unknown_flags_from_spec_string() {
8840        let spec: Spec = r#"
8841            flag "-v --verbose" var=#true
8842            arg "[database]" default="myapp_dev"
8843            arg "[args...]"
8844        "#
8845        .parse()
8846        .unwrap();
8847        let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
8848            .into_iter()
8849            .map(String::from)
8850            .collect();
8851        let parsed = parse(&spec, &input).unwrap();
8852        let env = parsed.as_env();
8853        assert_eq!(env.get("usage_database").unwrap(), "mydb");
8854        assert_eq!(env.get("usage_args").unwrap(), "--host localhost");
8855    }
8856
8857    #[test]
8858    fn test_variadic_arg_captures_unknown_flags() {
8859        let cmd = SpecCommand::builder()
8860            .name("test")
8861            .flag(SpecFlag::builder().short('v').long("verbose").build())
8862            .arg(SpecArg::builder().name("database").required(false).build())
8863            .arg(
8864                SpecArg::builder()
8865                    .name("args")
8866                    .required(false)
8867                    .var(true)
8868                    .build(),
8869            )
8870            .build();
8871        let spec = Spec {
8872            name: "test".to_string(),
8873            bin: "test".to_string(),
8874            cmd,
8875            ..Default::default()
8876        };
8877
8878        // Unknown --host flag and its value should be captured by [args...]
8879        let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
8880            .into_iter()
8881            .map(String::from)
8882            .collect();
8883        let parsed = parse(&spec, &input).unwrap();
8884        assert_eq!(parsed.args.len(), 2);
8885        let args_val = parsed
8886            .args
8887            .iter()
8888            .find(|(a, _)| a.name == "args")
8889            .unwrap()
8890            .1;
8891        match args_val {
8892            ParseValue::MultiString(v) => {
8893                assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
8894            }
8895            _ => panic!("Expected MultiString, got {:?}", args_val),
8896        }
8897    }
8898
8899    #[test]
8900    fn test_variadic_arg_captures_unknown_flags_with_double_dash() {
8901        let cmd = SpecCommand::builder()
8902            .name("test")
8903            .flag(SpecFlag::builder().short('v').long("verbose").build())
8904            .arg(SpecArg::builder().name("database").required(false).build())
8905            .arg(
8906                SpecArg::builder()
8907                    .name("args")
8908                    .required(false)
8909                    .var(true)
8910                    .build(),
8911            )
8912            .build();
8913        let spec = Spec {
8914            name: "test".to_string(),
8915            bin: "test".to_string(),
8916            cmd,
8917            ..Default::default()
8918        };
8919
8920        // With explicit -- separator
8921        let input: Vec<String> = vec!["test", "--", "mydb", "--host", "localhost"]
8922            .into_iter()
8923            .map(String::from)
8924            .collect();
8925        let parsed = parse(&spec, &input).unwrap();
8926        assert_eq!(parsed.args.len(), 2);
8927        let args_val = parsed
8928            .args
8929            .iter()
8930            .find(|(a, _)| a.name == "args")
8931            .unwrap()
8932            .1;
8933        match args_val {
8934            ParseValue::MultiString(v) => {
8935                assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
8936            }
8937            _ => panic!("Expected MultiString, got {:?}", args_val),
8938        }
8939    }
8940
8941    #[test]
8942    fn test_variadic_arg_unknown_flag_equals_value_not_split() {
8943        // Regression: --flag=value should be treated as a single positional token when
8944        // --flag is not a known spec flag, not split into "--flag=value" AND "value".
8945        let spec: Spec = r#"arg "[other_args]" var=#true"#.parse().unwrap();
8946
8947        // Single unknown --flag=value: must not produce a stray "3" positional.
8948        // as_env() shell-joins values, so "=" gets quoted.
8949        let input: Vec<String> = vec!["test", "--option=3"]
8950            .into_iter()
8951            .map(String::from)
8952            .collect();
8953        let parsed = parse(&spec, &input).unwrap();
8954        let env = parsed.as_env();
8955        assert_eq!(
8956            env.get("usage_other_args").map(String::as_str),
8957            Some("'--option=3'"),
8958            "expected a single --option=3 token, got {:?}",
8959            env.get("usage_other_args"),
8960        );
8961
8962        // Multiple unknown --flag=value args should each be kept intact
8963        let input2: Vec<String> = vec!["test", "--foo=bar", "--baz=qux"]
8964            .into_iter()
8965            .map(String::from)
8966            .collect();
8967        let parsed2 = parse(&spec, &input2).unwrap();
8968        let env2 = parsed2.as_env();
8969        assert_eq!(
8970            env2.get("usage_other_args").map(String::as_str),
8971            Some("'--foo=bar' '--baz=qux'"),
8972            "expected two intact tokens, got {:?}",
8973            env2.get("usage_other_args"),
8974        );
8975
8976        // Mix of plain positional args and unknown --flag=value tokens
8977        let input3: Vec<String> = vec!["test", "positional1", "--option=3", "positional2"]
8978            .into_iter()
8979            .map(String::from)
8980            .collect();
8981        let parsed3 = parse(&spec, &input3).unwrap();
8982        let env3 = parsed3.as_env();
8983        assert_eq!(
8984            env3.get("usage_other_args").map(String::as_str),
8985            Some("positional1 '--option=3' positional2"),
8986            "expected positional args and intact flag token, got {:?}",
8987            env3.get("usage_other_args"),
8988        );
8989    }
8990
8991    #[test]
8992    fn test_allow_hyphen_values_consumes_short_flag_collision() {
8993        let spec = r#"
8994flag "-d --working-dir <DIR>"
8995flag "-a --args <ARGS>" allow_hyphen_values=#true
8996"#
8997        .parse::<Spec>()
8998        .unwrap();
8999
9000        let parsed = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap();
9001
9002        assert_eq!(parsed.flags.len(), 1);
9003        assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
9004    }
9005
9006    #[test]
9007    fn test_allow_hyphen_values_consumes_embedded_long_value() {
9008        let spec = r#"
9009flag "-d --working-dir <DIR>"
9010flag "-a --args <ARGS>" allow_hyphen_values=#true
9011"#
9012        .parse::<Spec>()
9013        .unwrap();
9014
9015        let parsed = parse(&spec, &input(&["test", "--args=-destroy"])).unwrap();
9016
9017        assert_eq!(parsed.flags.len(), 1);
9018        assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
9019    }
9020
9021    #[test]
9022    fn test_allow_hyphen_values_takes_the_separator_as_its_value() {
9023        // The flag is declared to accept a token that looks like a flag, and `--` looks
9024        // like one, so it binds — which is what clap does with the same declaration.
9025        // Letting the separator arm run first consumed it and left the flag hungry, and
9026        // the flag then ate the word past it: `-a -- -x` bound `-x` with the `--` gone.
9027        let spec = r#"
9028flag "-a --args <ARGS>" allow_hyphen_values=#true
9029arg "[rest]..."
9030"#
9031        .parse::<Spec>()
9032        .unwrap();
9033
9034        let parsed = parse(&spec, &input(&["test", "-a", "--", "-x"])).unwrap();
9035
9036        assert_eq!(flag_string_value(&parsed, "args"), "--");
9037        let rest = parsed
9038            .args
9039            .values()
9040            .next()
9041            .expect("expected the word after the separator to reach the argument");
9042        assert_eq!(rest.to_string(), "-x");
9043    }
9044
9045    #[test]
9046    fn test_variadic_allow_hyphen_values_collects_after_a_hyphenated_first_value() {
9047        // Which token supplied the first value says nothing about how many the argument
9048        // takes, so collection carries on from a hyphenated one exactly as from a plain
9049        // one. It still stops at the next flag-like token, which is what keeps a second
9050        // occurrence of the flag from being eaten as a value.
9051        let spec = r#"
9052flag "-a --args <ARGS>..." allow_hyphen_values=#true
9053"#
9054        .parse::<Spec>()
9055        .unwrap();
9056
9057        let parsed = parse(&spec, &input(&["test", "-a", "-x", "b", "c"])).unwrap();
9058
9059        let flag = parsed
9060            .flags
9061            .keys()
9062            .find(|flag| flag.name == "args")
9063            .expect("expected args flag");
9064        match parsed.flags.get(flag).expect("expected args value") {
9065            ParseValue::MultiString(values) => assert_eq!(values, &["-x", "b", "c"]),
9066            other => panic!("expected a list of values, got {other:?}"),
9067        }
9068    }
9069
9070    #[test]
9071    fn test_variadic_allow_hyphen_values_consumes_repeated_flag_values() {
9072        let spec = r#"
9073flag "-a --args <ARGS>" var=#true allow_hyphen_values=#true
9074"#
9075        .parse::<Spec>()
9076        .unwrap();
9077
9078        let parsed = parse(&spec, &input(&["test", "-a", "-val1", "-a", "-val2"])).unwrap();
9079
9080        let flag = parsed
9081            .flags
9082            .keys()
9083            .find(|flag| flag.name == "args")
9084            .expect("expected args flag");
9085        let value = parsed.flags.get(flag).expect("expected args value");
9086        match value {
9087            ParseValue::MultiString(values) => {
9088                assert_eq!(values, &vec!["-val1".to_string(), "-val2".to_string()]);
9089            }
9090            _ => panic!("expected MultiString, got {value:?}"),
9091        }
9092    }
9093
9094    #[test]
9095    fn test_require_equals_accepts_attached_and_refuses_detached() {
9096        let spec = r#"
9097flag "--inspect <PORT>" require_equals=#true
9098"#
9099        .parse::<Spec>()
9100        .unwrap();
9101
9102        let parsed = parse(&spec, &input(&["test", "--inspect=9229"])).unwrap();
9103        assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
9104
9105        let err = parse(&spec, &input(&["test", "--inspect", "9229"])).unwrap_err();
9106        let msg = format!("{err}");
9107        assert!(
9108            msg.contains("requires an argument") || msg.contains("inspect"),
9109            "detached value must be refused: {msg}"
9110        );
9111    }
9112
9113    #[test]
9114    fn boolean_flags_can_accept_attached_values_when_enabled() {
9115        let spec: Spec = r#"
9116name "ex"
9117bin "ex"
9118flag "--color" negate="--no-color" bool_value=#true
9119arg "[rest]"
9120"#
9121        .parse()
9122        .unwrap();
9123
9124        for (token, expected) in [
9125            ("--color", true),
9126            ("--color=true", true),
9127            ("--color=false", false),
9128            ("--no-color", false),
9129            ("--no-color=false", true),
9130        ] {
9131            let parsed = parse(&spec, &input(&["ex", token])).unwrap();
9132            assert!(
9133                matches!(
9134                    parsed.flags.get(&spec.cmd.flags[0]),
9135                    Some(ParseValue::Bool(value)) if *value == expected
9136                ),
9137                "{token}"
9138            );
9139        }
9140
9141        let parsed = parse(&spec, &input(&["ex", "--color=false", "word"])).unwrap();
9142        assert!(matches!(
9143            parsed.args.get(&spec.cmd.args[0]),
9144            Some(ParseValue::String(value)) if value == "word"
9145        ));
9146        let err = parse(&spec, &input(&["ex", "--color=maybe"])).unwrap_err();
9147        assert!(err.to_string().contains("expected `true` or `false`"));
9148
9149        let strict: Spec = r#"
9150name "ex"
9151bin "ex"
9152args_override_self #false
9153flag "--color" negate="--no-color" bool_value=#true
9154"#
9155        .parse()
9156        .unwrap();
9157        assert!(parse(&strict, &input(&["ex", "--color=false", "--color=true"])).is_err());
9158        let parsed = parse(
9159            &strict,
9160            &input(&["ex", "--color=false", "--no-color=false"]),
9161        )
9162        .unwrap();
9163        assert!(matches!(
9164            parsed.flags.get(&strict.cmd.flags[0]),
9165            Some(ParseValue::Bool(true))
9166        ));
9167    }
9168
9169    #[test]
9170    fn test_require_equals_refuses_a_detached_value_after_a_short_bundle() {
9171        let spec = r#"
9172flag "-a --all"
9173flag "-i --inspect <PORT>" require_equals=#true
9174"#
9175        .parse::<Spec>()
9176        .unwrap();
9177
9178        let err = parse(&spec, &input(&["test", "-ai", "9229"])).unwrap_err();
9179        let msg = format!("{err}");
9180        assert!(
9181            msg.contains("requires an argument") || msg.contains("inspect"),
9182            "bundled short must refuse the following word: {msg}"
9183        );
9184    }
9185
9186    #[test]
9187    fn test_default_missing_binds_when_the_value_is_left_off() {
9188        let spec = r#"
9189flag "-c --color <WHEN>" default_missing="always"
9190flag "-v --verbose"
9191"#
9192        .parse::<Spec>()
9193        .unwrap();
9194
9195        let parsed = parse(&spec, &input(&["test", "--color"])).unwrap();
9196        assert_eq!(flag_string_value(&parsed, "color"), "always");
9197
9198        let parsed = parse(&spec, &input(&["test", "--color=never"])).unwrap();
9199        assert_eq!(flag_string_value(&parsed, "color"), "never");
9200
9201        let parsed = parse(&spec, &input(&["test", "--color", "never"])).unwrap();
9202        assert_eq!(flag_string_value(&parsed, "color"), "never");
9203
9204        let parsed = parse(&spec, &input(&["test", "--color", "--verbose"])).unwrap();
9205        assert_eq!(flag_string_value(&parsed, "color"), "always");
9206        assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
9207
9208        let parsed = parse(&spec, &input(&["test", "--color="])).unwrap();
9209        assert_eq!(flag_string_value(&parsed, "color"), "");
9210
9211        let parsed = parse(&spec, &input(&["test", "-cnever"])).unwrap();
9212        assert_eq!(flag_string_value(&parsed, "color"), "never");
9213
9214        let parsed = parse(&spec, &input(&["test", "-c", "-v"])).unwrap();
9215        assert_eq!(flag_string_value(&parsed, "color"), "always");
9216        assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
9217    }
9218
9219    #[test]
9220    fn test_default_missing_requires_opt_in_for_detached_negative_flag_values() {
9221        let spec = r#"
9222flag "--apps <N>"
9223flag "--jobs <N>" default_missing="default missing"
9224flag "--kids <N>" default_missing="default missing" allow_negative_numbers=#true
9225"#
9226        .parse::<Spec>()
9227        .unwrap();
9228
9229        let parsed = parse(&spec, &input(&["test", "--apps", "-1"])).unwrap();
9230        assert_eq!(flag_string_value(&parsed, "apps"), "-1");
9231
9232        let err = parse(&spec, &input(&["test", "--jobs", "-1"])).unwrap_err();
9233        assert!(
9234            err.to_string().contains("unexpected word: -1"),
9235            "default_missing must keep an unopted negative value separate: {err}"
9236        );
9237
9238        let parsed = parse(&spec, &input(&["test", "--kids", "-1"])).unwrap();
9239        assert_eq!(flag_string_value(&parsed, "kids"), "-1");
9240
9241        let external_spec = r#"
9242external_subcommand #true
9243flag "--apps <N>"
9244"#
9245        .parse::<Spec>()
9246        .unwrap();
9247        let parsed = parse(&external_spec, &input(&["test", "--apps", "-1"])).unwrap();
9248        assert_eq!(flag_string_value(&parsed, "apps"), "-1");
9249        assert!(parsed.external.is_none());
9250
9251        for words in [
9252            &["test", "--apps", "--jobs", "1"][..],
9253            &["test", "--apps", "--kids", "-1"][..],
9254        ] {
9255            let err = parse(&spec, &input(words)).unwrap_err();
9256            let message = err.to_string();
9257            assert!(
9258                message.contains("--apps") && message.contains("requires an argument"),
9259                "the earlier flag must report its missing value for {words:?}: {message}"
9260            );
9261        }
9262    }
9263
9264    #[test]
9265    fn test_optional_flag_value_preserves_bare_and_explicit_empty_forms() {
9266        let spec = r#"
9267flag "--bump [LEVEL]" value_optional=#true
9268flag "--verbose"
9269arg "[FILE]"
9270"#
9271        .parse::<Spec>()
9272        .unwrap();
9273
9274        let absent = parse(&spec, &input(&["test"])).unwrap();
9275        assert!(!absent.flags.keys().any(|flag| flag.name == "bump"));
9276
9277        let bare = parse(&spec, &input(&["test", "--bump", "--verbose", "file.txt"])).unwrap();
9278        let bump = bare
9279            .flags
9280            .iter()
9281            .find(|(flag, _)| flag.name == "bump")
9282            .map(|(_, value)| value)
9283            .unwrap();
9284        assert!(matches!(bump, ParseValue::MultiString(values) if values.is_empty()));
9285        assert!(bare.flags.keys().any(|flag| flag.name == "verbose"));
9286        assert_eq!(arg_value(&bare, "FILE"), "file.txt");
9287
9288        let explicit = parse(&spec, &input(&["test", "--bump=", "file.txt"])).unwrap();
9289        assert_eq!(flag_string_value(&explicit, "bump"), "");
9290
9291        let corrected = parse(
9292            &spec,
9293            &input(&["test", "--bump=2", "--bump", "--verbose", "file.txt"]),
9294        )
9295        .unwrap();
9296        let bump = corrected
9297            .flags
9298            .iter()
9299            .find(|(flag, _)| flag.name == "bump")
9300            .map(|(_, value)| value)
9301            .unwrap();
9302        assert!(matches!(bump, ParseValue::MultiString(values) if values.is_empty()));
9303
9304        let collecting = r#"
9305flag "--tag [TAG]..." value_optional=#true
9306flag "--verbose"
9307"#
9308        .parse::<Spec>()
9309        .unwrap();
9310        let valued = parse(
9311            &collecting,
9312            &input(&["test", "--tag", "one", "two", "--verbose"]),
9313        )
9314        .unwrap();
9315        let tag = valued
9316            .flags
9317            .iter()
9318            .find(|(flag, _)| flag.name == "tag")
9319            .map(|(_, value)| value)
9320            .unwrap();
9321        assert!(matches!(tag, ParseValue::MultiString(values) if values == &["one", "two"]));
9322    }
9323
9324    #[test]
9325    fn test_repeatable_bare_optional_values_count_each_occurrence() {
9326        let spec = r#"
9327flag "--tag [TAG]" var=#true var_min=2 var_max=2 value_optional=#true
9328"#
9329        .parse::<Spec>()
9330        .unwrap();
9331
9332        let parsed = parse(&spec, &input(&["test", "--tag", "--tag"])).unwrap();
9333        let tag = parsed
9334            .flags
9335            .iter()
9336            .find(|(flag, _)| flag.name == "tag")
9337            .map(|(_, value)| value)
9338            .unwrap();
9339        assert!(matches!(tag, ParseValue::MultiString(values) if values == &["", ""]));
9340
9341        assert!(parse(&spec, &input(&["test", "--tag"])).is_err());
9342        assert!(parse(&spec, &input(&["test", "--tag", "--tag", "--tag"])).is_err());
9343    }
9344
9345    #[test]
9346    fn test_repeatable_variadic_optional_values_do_not_gain_bare_occurrences() {
9347        let spec = r#"
9348flag "--tag [TAG]..." var=#true value_optional=#true
9349flag "--verbose"
9350"#
9351        .parse::<Spec>()
9352        .unwrap();
9353
9354        for argv in [
9355            &["test", "--tag", "one", "two"][..],
9356            &["test", "--tag", "one", "two", "--verbose"][..],
9357            &["test", "--tag", "one", "--tag", "two"][..],
9358        ] {
9359            let parsed = parse(&spec, &input(argv)).unwrap();
9360            let tag = parsed
9361                .flags
9362                .iter()
9363                .find(|(flag, _)| flag.name == "tag")
9364                .map(|(_, value)| value)
9365                .unwrap();
9366            assert!(
9367                matches!(tag, ParseValue::MultiString(values) if values == &["one", "two"]),
9368                "argv={argv:?}: {tag:?}"
9369            );
9370        }
9371
9372        let bare = parse(&spec, &input(&["test", "--tag", "--verbose"])).unwrap();
9373        let tag = bare
9374            .flags
9375            .iter()
9376            .find(|(flag, _)| flag.name == "tag")
9377            .map(|(_, value)| value)
9378            .unwrap();
9379        assert!(matches!(tag, ParseValue::MultiString(values) if values == &[""]));
9380    }
9381
9382    #[test]
9383    fn test_default_missing_with_require_equals_refuses_the_following_word() {
9384        let spec = r#"
9385flag "--inspect <PORT>" require_equals=#true default_missing="9229"
9386arg "[rest]"
9387"#
9388        .parse::<Spec>()
9389        .unwrap();
9390
9391        let parsed = parse(&spec, &input(&["test", "--inspect"])).unwrap();
9392        assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
9393
9394        let parsed = parse(&spec, &input(&["test", "--inspect=1234"])).unwrap();
9395        assert_eq!(flag_string_value(&parsed, "inspect"), "1234");
9396
9397        // The following word is not the value; the missing value is, and 80 is a positional.
9398        let parsed = parse(&spec, &input(&["test", "--inspect", "80"])).unwrap();
9399        assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
9400        assert_eq!(
9401            parsed
9402                .args
9403                .values()
9404                .next()
9405                .map(|v| v.to_string())
9406                .as_deref(),
9407            Some("80")
9408        );
9409
9410        let parsed = parse(&spec, &input(&["test", "--inspect="])).unwrap();
9411        assert_eq!(flag_string_value(&parsed, "inspect"), "");
9412    }
9413
9414    #[test]
9415    fn test_default_missing_must_be_a_choice() {
9416        let spec = r#"
9417flag "--color <WHEN>" default_missing="always" {
9418    choices "auto" "always" "never"
9419}
9420"#
9421        .parse::<Spec>()
9422        .unwrap();
9423
9424        let parsed = parse(&spec, &input(&["test", "--color"])).unwrap();
9425        assert_eq!(flag_string_value(&parsed, "color"), "always");
9426
9427        let parsed = parse(&spec, &input(&["test", "--color=never"])).unwrap();
9428        assert_eq!(flag_string_value(&parsed, "color"), "never");
9429
9430        let spec = r#"
9431flag "--color <WHEN>" default_missing="wat" {
9432    choices "auto" "always" "never"
9433}
9434"#
9435        .parse::<Spec>()
9436        .unwrap();
9437
9438        let err = parse(&spec, &input(&["test", "--color"])).unwrap_err();
9439        let msg = format!("{err}");
9440        assert!(
9441            msg.contains("Invalid choice for option color: wat"),
9442            "missing default has to pass choices the same way a typed value does: {msg}"
9443        );
9444
9445        let err = parse(&spec, &input(&["test", "--color=wat"])).unwrap_err();
9446        let msg = format!("{err}");
9447        assert!(
9448            msg.contains("Invalid choice for option color: wat"),
9449            "an attached value that is not a choice is still refused: {msg}"
9450        );
9451
9452        let spec = r#"
9453flag "--inspect <PORT>" require_equals=#true default_missing="wat" {
9454    choices "9229" "80"
9455}
9456arg "[rest]"
9457"#
9458        .parse::<Spec>()
9459        .unwrap();
9460
9461        let err = parse(&spec, &input(&["test", "--inspect", "80"])).unwrap_err();
9462        let msg = format!("{err}");
9463        assert!(
9464            msg.contains("Invalid choice for option inspect: wat"),
9465            "require_equals still binds the missing string, so the error is the choice: {msg}"
9466        );
9467    }
9468
9469    #[test]
9470    fn test_hyphen_values_still_start_short_flag_parsing() {
9471        let spec = r#"
9472flag "-d --working-dir <DIR>"
9473flag "-a --args <ARGS>"
9474"#
9475        .parse::<Spec>()
9476        .unwrap();
9477
9478        let err = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap_err();
9479        let message = err.to_string();
9480        assert!(
9481            message.contains("--args") && message.contains("requires an argument"),
9482            "the recognized -d must leave the earlier -a missing: {message}"
9483        );
9484    }
9485
9486    /// `available_flags` has to agree with what an actual parse accepts, since
9487    /// its whole reason to exist is answering that question without one.
9488    mod available_flags {
9489        use super::*;
9490
9491        fn spec() -> Spec {
9492            r#"
9493bin "test"
9494flag "-v --verbose" global=#true
9495flag "--raw" global=#true effect="write"
9496flag "--local-only"
9497cmd "run" {
9498    flag "-r --raw"
9499    flag "-w --watch"
9500    cmd "once"
9501}
9502"#
9503            .parse::<Spec>()
9504            .unwrap()
9505        }
9506
9507        fn chain<'a>(spec: &'a Spec, path: &[&str]) -> Vec<&'a SpecCommand> {
9508            let mut chain = vec![&spec.cmd];
9509            for segment in path {
9510                chain.push(chain.last().unwrap().find_subcommand(segment).unwrap());
9511            }
9512            chain
9513        }
9514
9515        fn names(spec: &Spec, path: &[&str]) -> Vec<String> {
9516            let mut names: Vec<_> = available_flags(&chain(spec, path))
9517                .iter()
9518                .map(|f| f.name.clone())
9519                .collect();
9520            names.sort();
9521            names
9522        }
9523
9524        #[test]
9525        fn an_empty_chain_yields_nothing() {
9526            assert!(available_flags(&[]).is_empty());
9527        }
9528
9529        #[test]
9530        fn the_root_gets_its_own_flags() {
9531            let spec = spec();
9532            assert_eq!(names(&spec, &[]), ["local-only", "raw", "verbose"]);
9533        }
9534
9535        #[test]
9536        fn a_subcommand_keeps_globals_and_drops_local_only_ancestors() {
9537            let spec = spec();
9538            assert_eq!(names(&spec, &["run"]), ["raw", "verbose", "watch"]);
9539        }
9540
9541        #[test]
9542        fn a_re_declared_global_is_listed_once() {
9543            // The merge can leave the long key on the merged flag and the short
9544            // key on the pre-merge one. Same flag; it must not be listed twice.
9545            let spec = r#"
9546bin "test"
9547flag "-y --yes" global=#true effect="write"
9548cmd "rm" {
9549    flag "-y --yes"
9550}
9551"#
9552            .parse::<Spec>()
9553            .unwrap();
9554            let flags = available_flags(&chain(&spec, &["rm"]));
9555            assert_eq!(flags.len(), 1, "{flags:?}");
9556            assert_eq!(flags[0].effect.map(|e| e.as_str()), Some("write"));
9557        }
9558
9559        #[test]
9560        fn a_re_declared_global_keeps_the_globals_declaration() {
9561            // `run` re-declares the long-only global `--raw` as `-r --raw`
9562            // without `global`. That is the same flag: the global's `effect`
9563            // survives, the orphan short is unioned in, and it stays global.
9564            let spec = spec();
9565            let flags = available_flags(&chain(&spec, &["run"]));
9566            let raw = flags.iter().find(|f| f.name == "raw").unwrap();
9567            assert!(raw.global);
9568            assert_eq!(raw.effect.map(|e| e.as_str()), Some("write"));
9569            assert_eq!(raw.short, ['r']);
9570        }
9571
9572        #[test]
9573        fn it_matches_what_a_parse_accepts() {
9574            // The invariant. If these ever disagree, one of them is lying to a
9575            // caller about which flags a command takes.
9576            let spec = spec();
9577            for path in [vec![], vec!["run"], vec!["run", "once"]] {
9578                let argv = std::iter::once("test".to_string())
9579                    .chain(path.iter().map(|s| s.to_string()))
9580                    .collect::<Vec<_>>();
9581                let parsed = parse_partial(&spec, &argv).unwrap();
9582
9583                let mut from_parse: Vec<_> = unique_flags(parsed.available_flags.values())
9584                    .map(|f| f.name.clone())
9585                    .collect();
9586                from_parse.sort();
9587                assert_eq!(names(&spec, &path), from_parse, "path {path:?}");
9588            }
9589        }
9590    }
9591
9592    // Provenance: which token bound what, and where a value came from when no token did.
9593
9594    /// Every role a token was given, rendered the way `Debug` renders it, so a test can
9595    /// assert on the whole picture rather than on one field at a time.
9596    fn roles(parsed: &ParseOutput, index: usize) -> Vec<String> {
9597        parsed
9598            .tokens
9599            .iter()
9600            .find(|token| token.index == index)
9601            .unwrap_or_else(|| panic!("no token at {index}"))
9602            .roles
9603            .iter()
9604            .map(render_role)
9605            .collect()
9606    }
9607
9608    fn origins(parsed: &ParseOutput, flag: &str) -> Vec<ValueOrigin> {
9609        parsed
9610            .flag_origins
9611            .iter()
9612            .find(|(f, _)| f.name == flag)
9613            .map(|(_, origins)| origins.clone())
9614            .unwrap_or_default()
9615    }
9616
9617    fn explain_with_env(spec: &Spec, words: &[&str], env: &[(&str, &str)]) -> ParseOutput {
9618        let env = env
9619            .iter()
9620            .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
9621            .collect();
9622        Parser::new(spec)
9623            .with_env(env)
9624            .explain(&input(words))
9625            .unwrap()
9626    }
9627
9628    fn explain(spec: &Spec, words: &[&str]) -> ParseOutput {
9629        explain_with_env(spec, words, &[])
9630    }
9631
9632    #[test]
9633    fn an_attached_long_value_is_recorded_on_the_flag_token() {
9634        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\n"
9635            .parse()
9636            .unwrap();
9637
9638        let parsed = explain(&spec, &["ex", "--env=prod"]);
9639
9640        assert_eq!(roles(&parsed, 0), ["program"]);
9641        assert_eq!(
9642            roles(&parsed, 1),
9643            ["flag env as --env", "value of env = [\"prod\"], attached"]
9644        );
9645        // This is jdx/mise discussion #8883: a hand-written scanner dropped the attached
9646        // form while the detached one worked, and nothing could show the difference.
9647        assert!(origins(&parsed, "env").is_empty(), "typed, so no fallback");
9648    }
9649
9650    #[test]
9651    fn a_detached_long_value_is_recorded_on_its_own_token() {
9652        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\n"
9653            .parse()
9654            .unwrap();
9655
9656        let parsed = explain(&spec, &["ex", "--env", "prod"]);
9657
9658        assert_eq!(roles(&parsed, 1), ["flag env as --env"]);
9659        assert_eq!(roles(&parsed, 2), ["value of env = [\"prod\"]"]);
9660    }
9661
9662    #[test]
9663    fn a_short_bundle_is_attributed_to_the_bundle_token() {
9664        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-a\"\nflag \"-b\"\nflag \"-j <n>\"\n"
9665            .parse()
9666            .unwrap();
9667
9668        let parsed = explain(&spec, &["ex", "-abj8"]);
9669
9670        // One word the caller wrote, four things it did — and the re-queued tails are
9671        // folded back onto it rather than appearing as tokens nobody typed.
9672        assert_eq!(
9673            roles(&parsed, 1),
9674            [
9675                "flag a as -a",
9676                "flag b as -b",
9677                "flag j as -j",
9678                "value of j = [\"8\"], attached",
9679            ]
9680        );
9681        assert_eq!(parsed.tokens.len(), 2);
9682    }
9683
9684    #[test]
9685    fn a_delimiter_splits_one_token_into_several_values() {
9686        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--tags <tags>...\" delimiter=\",\"\n"
9687            .parse()
9688            .unwrap();
9689
9690        let parsed = explain(&spec, &["ex", "--tags", "a,b,c"]);
9691
9692        assert_eq!(
9693            roles(&parsed, 2),
9694            ["value of tags = [\"a\", \"b\", \"c\"]"],
9695            "the values meant, not the word typed"
9696        );
9697    }
9698
9699    #[test]
9700    fn a_separator_and_the_words_after_it_are_distinguished() {
9701        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"<src>\"\narg \"[raw]...\"\n"
9702            .parse()
9703            .unwrap();
9704
9705        let parsed = explain(&spec, &["ex", "a", "--", "-x"]);
9706
9707        assert_eq!(roles(&parsed, 1), ["arg src = [\"a\"]"]);
9708        assert_eq!(roles(&parsed, 2), ["separator"]);
9709        // Past the separator `-x` is data, not an unknown flag.
9710        assert_eq!(roles(&parsed, 3), ["arg raw = [\"-x\"]"]);
9711    }
9712
9713    #[test]
9714    fn a_second_separator_is_data() {
9715        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[raw]...\"\n"
9716            .parse()
9717            .unwrap();
9718
9719        let parsed = explain(&spec, &["ex", "--", "a", "--", "b"]);
9720
9721        assert_eq!(roles(&parsed, 1), ["separator"]);
9722        assert_eq!(roles(&parsed, 3), ["arg raw = [\"--\"]"]);
9723    }
9724
9725    #[test]
9726    fn an_unknown_flag_says_what_took_it() {
9727        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[rest]...\"\n"
9728            .parse()
9729            .unwrap();
9730
9731        let parsed = explain(&spec, &["ex", "--wat"]);
9732
9733        // The default is lax, so the word became data. Which is the useful thing to be
9734        // told: the alternative reading is "you have a typo".
9735        assert_eq!(roles(&parsed, 1), ["unknown flag, bound as rest"]);
9736    }
9737
9738    #[test]
9739    fn a_subcommand_word_is_not_a_positional() {
9740        let spec: Spec = "name \"ex\"\nbin \"ex\"\ncmd \"build\" {\n    arg \"<target>\"\n}\n"
9741            .parse()
9742            .unwrap();
9743
9744        let parsed = explain(&spec, &["ex", "build", "a"]);
9745
9746        assert_eq!(roles(&parsed, 1), ["subcommand build"]);
9747        assert_eq!(roles(&parsed, 2), ["arg target = [\"a\"]"]);
9748    }
9749
9750    #[test]
9751    fn a_multicall_applet_is_read_at_argv0() {
9752        let spec: Spec =
9753            "name \"box\"\nbin \"box\"\nmulticall #true\ncmd \"ls\" {\n    flag \"-l\"\n}\n"
9754                .parse()
9755                .unwrap();
9756
9757        let parsed = explain(&spec, &["/usr/bin/ls", "-l"]);
9758
9759        // argv[0] is both the program and the word that selected the applet, and the word
9760        // read there is not the word the caller wrote.
9761        assert_eq!(roles(&parsed, 0), ["program", "subcommand ls"]);
9762        assert!(parsed.tokens[0].synthesized);
9763        assert_eq!(parsed.tokens[0].word, "/usr/bin/ls");
9764    }
9765
9766    #[test]
9767    fn words_the_parse_never_reached_say_so() {
9768        let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[rest]...\"\n"
9769            .parse()
9770            .unwrap();
9771
9772        let parsed = Parser::new(&spec)
9773            .explain(&input(&["ex", "--help", "a"]))
9774            .unwrap();
9775
9776        assert_eq!(roles(&parsed, 2), ["unread"]);
9777    }
9778
9779    #[test]
9780    fn an_env_origin_names_the_variable_that_fired() {
9781        let spec: Spec =
9782            "name \"ex\"\nbin \"ex\"\nflag \"--token <t>\" env=\"EX_TOKEN\" env_fallback=\"EX_TOKEN_OLD\"\n"
9783                .parse()
9784                .unwrap();
9785
9786        let primary = explain_with_env(&spec, &["ex"], &[("EX_TOKEN", "a")]);
9787        assert_eq!(
9788            origins(&primary, "token"),
9789            [ValueOrigin::Env("EX_TOKEN".to_string())]
9790        );
9791
9792        // The fallback firing is a different fact from the primary firing, and which one it
9793        // was is what says which declaration to delete.
9794        let fallback = explain_with_env(&spec, &["ex"], &[("EX_TOKEN_OLD", "b")]);
9795        assert_eq!(
9796            origins(&fallback, "token"),
9797            [ValueOrigin::Env("EX_TOKEN_OLD".to_string())]
9798        );
9799    }
9800
9801    #[test]
9802    fn a_default_origin_is_recorded_for_flags_and_args() {
9803        let spec: Spec =
9804            "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" default=\"auto\"\narg \"[src]\" default=\".\"\n"
9805                .parse()
9806                .unwrap();
9807
9808        let parsed = explain(&spec, &["ex"]);
9809
9810        assert_eq!(origins(&parsed, "color"), [ValueOrigin::Default]);
9811        let (arg, origins) = parsed.arg_origins.iter().next().unwrap();
9812        assert_eq!(arg.name, "src");
9813        assert_eq!(origins, &[ValueOrigin::Default]);
9814    }
9815
9816    #[test]
9817    fn a_default_if_origin_carries_the_condition_that_fired() {
9818        let spec: Spec = r#"
9819name "ex"
9820bin "ex"
9821flag "--profile <p>"
9822flag "--strict" {
9823    default_if "--profile" "prod" "true"
9824}
9825        "#
9826        .parse()
9827        .unwrap();
9828
9829        let parsed = explain(&spec, &["ex", "--profile", "prod"]);
9830
9831        // The selector alone is ambiguous: several conditions may name it with different
9832        // `when` values, so the report has to say which one matched.
9833        assert_eq!(
9834            origins(&parsed, "strict"),
9835            [ValueOrigin::DefaultIf {
9836                selector: "--profile".to_string(),
9837                when: Some("prod".to_string()),
9838            }]
9839        );
9840    }
9841
9842    #[test]
9843    fn a_bare_optional_value_flag_records_default_missing() {
9844        let spec: Spec =
9845            "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" default_missing=\"always\"\nflag \"-v\"\n"
9846                .parse()
9847                .unwrap();
9848
9849        let parsed = explain(&spec, &["ex", "--color", "-v"]);
9850
9851        // The flag was typed and the value was not, which is the distinction a spec author
9852        // is asking about when they ask why `--color` came out `always`.
9853        assert_eq!(roles(&parsed, 1), ["flag color as --color"]);
9854        assert_eq!(origins(&parsed, "color"), [ValueOrigin::DefaultMissing]);
9855        assert_eq!(roles(&parsed, 2), ["flag v as -v"]);
9856    }
9857
9858    #[test]
9859    fn a_var_flag_can_take_one_value_from_argv_and_one_from_default_missing() {
9860        let spec: Spec =
9861            "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" var=#true default_missing=\"always\"\n"
9862                .parse()
9863                .unwrap();
9864
9865        let parsed = explain(&spec, &["ex", "--color=red", "--color"]);
9866
9867        // Why origins are a list: one declaration, two occurrences, two different answers.
9868        assert_eq!(
9869            roles(&parsed, 1),
9870            [
9871                "flag color as --color",
9872                "value of color = [\"red\"], attached"
9873            ]
9874        );
9875        assert_eq!(origins(&parsed, "color"), [ValueOrigin::DefaultMissing]);
9876    }
9877
9878    #[test]
9879    fn an_override_names_the_flag_that_did_it() {
9880        let spec: Spec =
9881            "name \"ex\"\nbin \"ex\"\nflag \"--quiet\" default=\"true\"\nflag \"--loud\" overrides=\"--quiet\"\n"
9882                .parse()
9883                .unwrap();
9884
9885        let parsed = explain(&spec, &["ex", "--loud"]);
9886
9887        // Without the overriding name, "`--quiet` is unset despite its default" has no
9888        // answer: the fallback phase silently declines to fill an overridden flag.
9889        assert_eq!(parsed.overridden_flags.get("quiet").unwrap(), "loud");
9890        assert!(origins(&parsed, "quiet").is_empty());
9891    }
9892
9893    #[test]
9894    fn a_restart_token_leaves_the_tokens_and_clears_the_arg_origins() {
9895        let spec: Spec = r#"
9896name "ex"
9897bin "ex"
9898cmd "run" restart_token=":::" {
9899    arg "<task>" default="build"
9900}
9901        "#
9902        .parse()
9903        .unwrap();
9904
9905        let parsed = explain(&spec, &["ex", "run", "lint", ":::", "test"]);
9906
9907        // The values belong to the last invocation, so provenance must too — but the words
9908        // of the first were still read, and a report that dropped them would show a command
9909        // line with a hole in it.
9910        assert_eq!(roles(&parsed, 2), ["arg task = [\"lint\"]"]);
9911        // And the token that did the resetting says so: without a role of its own it reads
9912        // as a word that did nothing, next to a `lint` that filled an arg now empty.
9913        assert_eq!(roles(&parsed, 3), ["restart"]);
9914        assert_eq!(roles(&parsed, 4), ["arg task = [\"test\"]"]);
9915        assert!(parsed.arg_origins.is_empty());
9916    }
9917
9918    #[test]
9919    fn a_value_terminator_says_which_run_it_ended() {
9920        let spec: Spec = r#"
9921name "ex"
9922bin "ex"
9923flag "--exec <cmd>..." value_terminator=";"
9924arg "<src>"
9925        "#
9926        .parse()
9927        .unwrap();
9928
9929        let parsed = explain(&spec, &["ex", "--exec", "rm", "tmp", ";", "a"]);
9930
9931        assert_eq!(roles(&parsed, 3), ["value of exec = [\"tmp\"]"]);
9932        // The terminator is consumed and is not one of the values, which is the whole reason
9933        // it was declared — so it needs a row saying that rather than an empty one.
9934        assert_eq!(roles(&parsed, 4), ["value terminator, ends exec"]);
9935        assert_eq!(roles(&parsed, 5), ["arg src = [\"a\"]"]);
9936    }
9937
9938    #[test]
9939    fn an_args_value_terminator_says_which_run_it_ended() {
9940        let spec: Spec = r#"
9941name "ex"
9942bin "ex"
9943arg "<files>..." value_terminator=";"
9944arg "[dest]"
9945        "#
9946        .parse()
9947        .unwrap();
9948
9949        let parsed = explain(&spec, &["ex", "a", "b", ";", "out"]);
9950
9951        assert_eq!(roles(&parsed, 2), ["arg files = [\"b\"]"]);
9952        assert_eq!(roles(&parsed, 3), ["value terminator, ends files"]);
9953        assert_eq!(roles(&parsed, 4), ["arg dest = [\"out\"]"]);
9954    }
9955
9956    #[test]
9957    fn explain_keeps_the_bindings_of_a_command_line_that_fails() {
9958        let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\narg \"<src>\"\n"
9959            .parse()
9960            .unwrap();
9961
9962        let parsed = Parser::new(&spec)
9963            .explain(&input(&["ex", "--env=prod"]))
9964            .unwrap();
9965
9966        // `parse` reports "missing required <src>" and nothing else, which is the report the
9967        // caller already had. This is the case the whole thing exists for.
9968        assert!(Parser::new(&spec)
9969            .parse(&input(&["ex", "--env=prod"]))
9970            .is_err());
9971        assert_eq!(
9972            roles(&parsed, 1),
9973            ["flag env as --env", "value of env = [\"prod\"], attached"]
9974        );
9975        assert!(
9976            parsed.errors.iter().any(|e| e.to_string().contains("src")),
9977            "{:?}",
9978            parsed.errors
9979        );
9980    }
9981
9982    #[test]
9983    fn an_external_subcommand_forwards_whole_tokens() {
9984        let spec: Spec = "name \"ex\"\nbin \"ex\"\nexternal_subcommand #true\ncmd \"build\"\n"
9985            .parse()
9986            .unwrap();
9987
9988        let parsed = explain(&spec, &["ex", "deploy", "--now"]);
9989
9990        assert_eq!(roles(&parsed, 1), ["external"]);
9991        assert_eq!(roles(&parsed, 2), ["external"]);
9992    }
9993
9994    #[test]
9995    fn a_view_keeps_the_callers_argv_positions() {
9996        let spec: Spec = r#"
9997bin "ex"
9998view "runner" root="run"
9999cmd "run" {
10000    flag "--token <token>"
10001}
10002        "#
10003        .parse()
10004        .unwrap();
10005
10006        let parsed = explain(&spec, &["runner", "--token", "secret"]);
10007
10008        // A view re-enters the parse with the same argv, so the positions still mean what
10009        // the caller wrote.
10010        assert_eq!(roles(&parsed, 0), ["program"]);
10011        assert_eq!(roles(&parsed, 2), ["value of token = [\"secret\"]"]);
10012    }
10013}